:', e);
usedGm = false;
}
if (!usedGm) {
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
// 延迟释放URL
setTimeout(() => URL.revokeObjectURL(url), 10000);
},
// 清理文件名中的非法字符
sanitizeFilename(filename) {
return filename.replace(/[<>:"/\\|?*]/g, '_').substring(0, 200);
},
// ========== 智能内容分段功能 ==========
// 检测文章结构
detectArticleStructure() {
const headings = [];
const selectors = 'h1, h2, h3, h4, h5, h6';
document.querySelectorAll(selectors).forEach((h, index) => {
const level = parseInt(h.tagName[1]);
const text = h.textContent.trim();
// 过滤太短或无意义的标题
if (text.length < 2 || text.length > 200) return;
// 获取元素位置
const rect = h.getBoundingClientRect();
const position = rect.top + window.scrollY;
headings.push({
level,
text,
position,
element: h,
index,
tag: h.tagName.toLowerCase()
});
});
console.log(`[AI速读] 检测到 ${headings.length} 个标题`);
return headings;
},
// 按章节提取内容
extractBySection(heading, nextHeading = null) {
const start = heading.element;
let content = '';
let current = start.nextElementSibling;
// 确定结束位置
const endElement = nextHeading ? nextHeading.element : null;
while (current) {
// 如果遇到下一个标题,停止
if (endElement && current === endElement) break;
// 如果遇到同级或更高级的标题,停止
if (current.matches('h1, h2, h3, h4, h5, h6')) {
const currentLevel = parseInt(current.tagName[1]);
if (currentLevel <= heading.level) break;
}
// 提取内容
const html = current.outerHTML || '';
const text = this.cookedToAiText(html, {
includeImages: false,
includeQuotes: true,
includeCode: true
});
if (text.trim()) {
content += text + '\n\n';
}
current = current.nextElementSibling;
}
return content.trim();
},
// 生成结构化大纲
generateOutline(headings) {
let outline = '## 📑 文章大纲\n\n';
headings.forEach((h, index) => {
const indent = ' '.repeat(h.level - 1);
outline += `${indent}${index + 1}. ${h.text}\n`;
});
return outline + '\n';
},
// 生成结构化总结
async generateStructuredSummary(headings, onProgress) {
const summaries = [];
for (let i = 0; i < headings.length; i++) {
const section = headings[i];
const nextSection = headings[i + 1];
if (onProgress) {
onProgress(i + 1, headings.length, section.text);
}
const content = this.extractBySection(section, nextSection);
// 如果内容太短,跳过
if (content.length < 50) {
summaries.push({
title: section.text,
level: section.level,
summary: '(内容过短,已跳过)',
content: content
});
continue;
}
summaries.push({
title: section.text,
level: section.level,
summary: null, // 稍后填充
content: content
});
}
return summaries;
},
// HTML 转 AI 优化文本
cookedToAiText(html, options = {}) {
const {
includeImages = true,
includeQuotes = true,
includeCode = true
} = options;
const parser = new DOMParser();
const doc = parser.parseFromString(html || '', 'text/html');
function serialize(node, inPre = false) {
if (!node) return '';
// 文本节点
if (node.nodeType === Node.TEXT_NODE) {
return node.nodeValue || '';
}
// 非元素节点
if (node.nodeType !== Node.ELEMENT_NODE) {
return '';
}
const el = node;
const tag = el.tagName.toLowerCase();
// 特殊标签处理
switch(tag) {
case 'br':
return '\n';
case 'img':
if (!includeImages) return '';
const src = el.getAttribute('src') || el.getAttribute('data-src') || '';
const alt = el.getAttribute('alt') || '图片';
if (!src) return '';
return `\n[图片: ${alt}](${src})\n`;
case 'a':
const hasImg = el.querySelector('img');
if (hasImg) {
return Array.from(el.childNodes).map(c => serialize(c, inPre)).join('');
}
const href = el.getAttribute('href') || '';
const text = Array.from(el.childNodes).map(c => serialize(c, inPre)).join('').trim();
if (!href || !text) return text;
if (text === href) return text;
return `${text}(${href})`;
case 'pre':
if (!includeCode) return '';
const codeEl = el.querySelector('code');
const langClass = codeEl?.getAttribute('class') || '';
const lang = (langClass.match(/lang(?:uage)?-([a-z0-9_+-]+)/i) || [])[1] || '';
const code = (codeEl ? codeEl.textContent : el.textContent) || '';
return `\n\`\`\`${lang}\n${code.replace(/\n+$/g, '')}\n\`\`\`\n\n`;
case 'code':
if (inPre) return el.textContent || '';
const t = (el.textContent || '').replace(/\n/g, ' ');
return t ? `\`${t}\`` : '';
case 'blockquote':
if (!includeQuotes) {
const inner = (el.textContent || '').trim();
return inner ? '\n(引用已省略)\n' : '';
}
const inner = Array.from(el.childNodes).map(c => serialize(c, inPre)).join('');
return `\n【引用】\n${inner.trim()}\n【/引用】\n\n`;
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
const level = parseInt(tag[1]);
const heading = (el.textContent || '').trim();
return heading ? `\n${'#'.repeat(level)} ${heading}\n\n` : '';
case 'li':
const liContent = Array.from(el.childNodes).map(c => serialize(c, inPre)).join('').trim();
return liContent ? `- ${liContent}\n` : '';
case 'ul':
case 'ol':
const listContent = Array.from(el.childNodes).map(c => serialize(c, inPre)).join('');
return `\n${listContent}\n`;
case 'p':
const pContent = Array.from(el.childNodes).map(c => serialize(c, inPre)).join('').trim();
return pContent ? `${pContent}\n\n` : '\n';
case 'table':
// 简单的表格处理
return '\n[表格内容]\n';
default:
const nextInPre = inPre || tag === 'pre';
return Array.from(el.childNodes).map(c => serialize(c, nextInPre)).join('');
}
}
let text = Array.from(doc.body.childNodes).map(n => serialize(n, false)).join('');
// 清理文本
text = text.replace(/\r\n/g, '\n');
text = text.replace(/[ \t]+\n/g, '\n');
text = text.replace(/\n{3,}/g, '\n\n');
return text.trim();
}
};
class AppUI {
constructor() {
this.host = document.createElement('div');
this.host.id = 'ld-summary-pro';
document.body.appendChild(this.host);
this.shadow = this.host.attachShadow({ mode: 'open' });
this.isOpen = false;
this.btnPos = GM_getValue('btnPos', { side: 'right', top: '50%' });
this.side = this.btnPos.side;
this.sidebarWidth = GM_getValue('sidebarWidth', 420);
this.currentTheme = GM_getValue('currentTheme', 'light');
this.chatHistory = [];
this.postContent = '';
this.lastSummary = '';
this.isGenerating = false;
this.currentTab = 'summary';
this.userMessageCount = 0;
this.userScrolledUp = false;
this.isProgrammaticScroll = false;
this.init();
}
init() {
const style = document.createElement('style');
style.textContent = STYLES;
this.shadow.appendChild(style);
this.render();
this.restoreState();
// 使用 setTimeout 确保 DOM 完全渲染后再绑定事件
setTimeout(() => {
this.bindEvents();
this.bindKeyboardShortcuts();
console.log('[AI速读] 初始化完成');
}, 0);
}
Q(s) { return this.shadow.querySelector(s); }
render() {
const arrowLeft = ``;
const arrowRight = ``;
const sendIcon = ``;
const arrowUpIcon = ``;
const arrowDownIcon = ``;
// 创建容器元素而不是使用 innerHTML +=
const container = document.createElement('div');
container.innerHTML = `
${arrowLeft}
`;
// 将容器添加到 shadow DOM
this.shadow.appendChild(container);
}
restoreState() {
this.host.style.setProperty('--sidebar-width', `${this.sidebarWidth}px`);
const btn = this.Q('#toggle-btn');
btn.style.top = this.btnPos.top;
this.applySideState();
// 恢复主题
this.currentTheme = GM_getValue('currentTheme', 'light');
if (this.currentTheme === 'dark') {
this.host.classList.add('dark-theme');
this.Q('#btn-theme').textContent = '☀️';
this.Q('#btn-theme').title = '切换到浅色主题';
} else if (this.currentTheme === 'orange') {
this.host.classList.add('orange-theme');
this.Q('#btn-theme').textContent = '🟤';
this.Q('#btn-theme').title = '切换到棕色主题';
} else if (this.currentTheme === 'brown') {
this.host.classList.add('brown-theme');
this.Q('#btn-theme').textContent = '🟡';
this.Q('#btn-theme').title = '切换到金色主题';
} else if (this.currentTheme === 'gold') {
this.host.classList.add('gold-theme');
this.Q('#btn-theme').textContent = '⚫';
this.Q('#btn-theme').title = '切换到黑白主题';
} else if (this.currentTheme === 'mono') {
this.host.classList.add('mono-theme');
this.Q('#btn-theme').textContent = '🌙';
this.Q('#btn-theme').title = '切换到深色主题';
} else {
this.Q('#btn-theme').textContent = '🔶';
this.Q('#btn-theme').title = '切换到橙红主题';
}
this.Q('#cfg-url').value = GM_getValue('apiUrl', 'https://api.deepseek.com/v1/chat/completions');
this.Q('#cfg-key').value = GM_getValue('apiKey', '');
this.Q('#cfg-model').value = GM_getValue('model', 'deepseek-chat');
// 恢复模板选择
const savedTemplate = GM_getValue('summaryTemplate', 'general');
this.Q('#cfg-template').value = savedTemplate;
this.updateTemplateUI(savedTemplate);
// 恢复总结模式
const savedMode = GM_getValue('summaryMode', 'full');
this.Q('#summary-mode').value = savedMode;
this.updateSummaryModeUI(savedMode);
this.Q('#cfg-prompt-sum').value = GM_getValue('prompt_sum', SUMMARY_TEMPLATES.general.prompt);
this.Q('#cfg-prompt-chat').value = GM_getValue('prompt_chat', '你是一个网页内容阅读助手。基于上文中的网页内容,回答用户的问题。回答要准确、简洁,必要时引用原文。');
this.Q('#cfg-stream').checked = GM_getValue('useStream', true);
this.Q('#cfg-autoscroll').checked = GM_getValue('autoScroll', true);
// 显示当前页面信息
this.updatePageInfo();
}
applySideState() {
const btn = this.Q('#toggle-btn');
const sidebar = this.Q('#sidebar');
const resizer = this.Q('#resizer');
const arrowLeft = ``;
const arrowRight = ``;
btn.style.left = '';
btn.style.right = '';
if (this.side === 'left') {
sidebar.className = 'sidebar-panel panel-left' + (this.isOpen ? ' open' : '');
resizer.className = 'resize-handle handle-left';
btn.className = 'btn-snap-left' + (this.isOpen ? ' arrow-flip' : '');
btn.innerHTML = arrowRight;
} else {
sidebar.className = 'sidebar-panel panel-right' + (this.isOpen ? ' open' : '');
resizer.className = 'resize-handle handle-right';
btn.className = 'btn-snap-right' + (this.isOpen ? ' arrow-flip' : '');
btn.innerHTML = arrowLeft;
}
this.updateButtonPosition();
}
updateButtonPosition(useTransition = true) {
const btn = this.Q('#toggle-btn');
if (!useTransition) {
btn.style.transition = 'none';
} else {
btn.style.transition = '';
}
if (this.side === 'left') {
btn.style.right = 'auto';
btn.style.left = this.isOpen ? `${this.sidebarWidth}px` : '0';
} else {
btn.style.left = 'auto';
btn.style.right = this.isOpen ? `${this.sidebarWidth}px` : '0';
}
if (!useTransition) {
btn.offsetHeight;
requestAnimationFrame(() => {
btn.style.transition = '';
});
}
}
bindEvents() {
const btn = this.Q('#toggle-btn');
// 检查关键元素是否存在
const saveBtn = this.Q('#btn-save');
console.log('[AI速读] 检查元素:', {
toggleBtn: !!btn,
saveBtn: !!saveBtn,
cfgUrl: !!this.Q('#cfg-url'),
cfgKey: !!this.Q('#cfg-key'),
cfgModel: !!this.Q('#cfg-model')
});
this.shadow.addEventListener('click', (e) => {
const toggle = e.target.closest('[data-thinking-toggle]');
if (toggle) {
const block = toggle.closest('[data-thinking-block]');
if (block) {
block.classList.toggle('expanded');
}
}
});
let isDrag = false, hasMoved = false, startX, startY, startRect;
btn.addEventListener('mousedown', (e) => {
isDrag = true;
hasMoved = false;
startX = e.clientX;
startY = e.clientY;
startRect = btn.getBoundingClientRect();
if (!this.isOpen) {
btn.style.transition = 'none';
}
btn.style.cursor = 'grabbing';
e.preventDefault();
});
window.addEventListener('mousemove', (e) => {
if (!isDrag) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) hasMoved = true;
if (!this.isOpen && hasMoved) {
btn.style.left = `${startRect.left + dx}px`;
btn.style.top = `${startRect.top + dy}px`;
btn.style.right = 'auto';
btn.className = 'btn-floating';
}
});
window.addEventListener('mouseup', (e) => {
if (!isDrag) return;
isDrag = false;
btn.style.cursor = 'grab';
btn.style.transition = '';
if (hasMoved && !this.isOpen) {
const winW = window.innerWidth;
const btnRect = btn.getBoundingClientRect();
const centerX = btnRect.left + btnRect.width / 2;
this.side = centerX < winW / 2 ? 'left' : 'right';
let newTop = btnRect.top;
if (newTop < 10) newTop = 10;
if (newTop > window.innerHeight - 60) newTop = window.innerHeight - 60;
this.btnPos = { side: this.side, top: `${newTop}px` };
GM_setValue('btnPos', this.btnPos);
btn.style.top = `${newTop}px`;
this.applySideState();
} else if (!hasMoved) {
this.toggleSidebar();
}
});
this.Q('#btn-close').onclick = () => this.toggleSidebar();
this.Q('#btn-theme').onclick = () => this.toggleTheme();
this.shadow.querySelectorAll('.tab-item').forEach(tab => {
tab.onclick = () => {
const tabName = tab.dataset.tab;
this.switchTab(tabName);
};
});
let isResizing = false;
this.Q('#resizer').addEventListener('mousedown', (e) => {
isResizing = true;
document.body.style.cursor = 'col-resize';
this.Q('#sidebar').style.transition = 'none';
document.body.style.transition = 'none';
e.preventDefault();
});
window.addEventListener('mousemove', (e) => {
if (!isResizing) return;
let newW = this.side === 'right' ? (window.innerWidth - e.clientX) : e.clientX;
if (newW > 320 && newW < 700) {
this.sidebarWidth = newW;
this.host.style.setProperty('--sidebar-width', `${newW}px`);
if (this.isOpen) {
this.squeezeBody(true);
this.updateButtonPosition(false);
}
}
});
window.addEventListener('mouseup', () => {
if (isResizing) {
isResizing = false;
document.body.style.cursor = '';
this.Q('#sidebar').style.transition = '';
document.body.style.transition = 'margin 0.35s cubic-bezier(0.4, 0, 0.2, 1)';
GM_setValue('sidebarWidth', this.sidebarWidth);
}
});
// 移除了 range-all 和 range-recent 按钮的绑定(通用版本不需要)
this.Q('#btn-summary').onclick = () => this.doSummary();
this.Q('#btn-send').onclick = () => this.doChat();
this.Q('#chat-input').onkeydown = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.doChat();
}
};
this.Q('#chat-input').addEventListener('input', (e) => {
const el = e.target;
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 140) + 'px';
});
this.Q('#btn-clear-chat').onclick = () => this.clearChat();
this.Q('#btn-scroll-top').onclick = () => this.scrollToTop();
this.Q('#btn-scroll-bottom').onclick = () => this.forceScrollToBottom();
const chatMessages = this.Q('#chat-messages');
let lastScrollTop = 0;
chatMessages.addEventListener('scroll', () => {
const currentScrollTop = chatMessages.scrollTop;
const scrollHeight = chatMessages.scrollHeight;
const clientHeight = chatMessages.clientHeight;
const isNearBottom = scrollHeight - currentScrollTop - clientHeight < 80;
if (this.isGenerating && !this.isProgrammaticScroll) {
if (currentScrollTop < lastScrollTop - 10) {
this.userScrolledUp = true;
} else if (isNearBottom) {
this.userScrolledUp = false;
}
}
lastScrollTop = currentScrollTop;
this.updateScrollButtons();
});
// 保存按钮事件绑定
this.Q('#btn-save').onclick = () => {
console.log('[AI速读] 保存设置...');
try {
GM_setValue('apiUrl', this.Q('#cfg-url').value.trim());
GM_setValue('apiKey', this.Q('#cfg-key').value.trim());
GM_setValue('model', this.Q('#cfg-model').value.trim());
// 保存模板选择
const template = this.Q('#cfg-template').value;
GM_setValue('summaryTemplate', template);
// 只有自定义模板才保存提示词
if (template === 'custom') {
GM_setValue('prompt_sum', this.Q('#cfg-prompt-sum').value);
}
GM_setValue('prompt_chat', this.Q('#cfg-prompt-chat').value);
GM_setValue('useStream', this.Q('#cfg-stream').checked);
GM_setValue('autoScroll', this.Q('#cfg-autoscroll').checked);
console.log('[AI速读] 设置保存成功');
this.showToast('设置已保存', 'success');
this.switchTab('summary');
} catch (error) {
console.error('[AI速读] 保存设置失败:', error);
this.showToast('保存失败: ' + error.message, 'error');
}
};
// 导出功能事件绑定
this.Q('#export-type').onchange = (e) => {
const exportType = e.target.value;
this.Q('#summary-export-options').style.display = exportType === 'summary' ? 'block' : 'none';
this.Q('#html-export-options').style.display = exportType === 'html' ? 'block' : 'none';
};
this.Q('#btn-export').onclick = () => this.doExport();
// 模板切换事件
this.Q('#cfg-template').onchange = (e) => {
const template = e.target.value;
this.updateTemplateUI(template);
GM_setValue('summaryTemplate', template);
};
// 总结模式切换事件
this.Q('#summary-mode').onchange = (e) => {
const mode = e.target.value;
this.updateSummaryModeUI(mode);
GM_setValue('summaryMode', mode);
};
console.log('[AI速读] 事件绑定完成');
}
bindKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 's') {
e.preventDefault();
this.toggleSidebar();
}
if (e.key === 'Escape' && this.isOpen) {
this.toggleSidebar();
}
});
}
updateTemplateUI(template) {
const descEl = this.Q('#template-description');
const promptEl = this.Q('#cfg-prompt-sum');
if (template === 'custom') {
descEl.textContent = '使用自定义提示词,可以根据需要自由编辑';
promptEl.disabled = false;
promptEl.style.opacity = '1';
} else {
const tmpl = SUMMARY_TEMPLATES[template];
if (tmpl) {
descEl.textContent = `${tmpl.icon} ${tmpl.description}`;
promptEl.value = tmpl.prompt;
promptEl.disabled = true;
promptEl.style.opacity = '0.7';
}
}
}
updateSummaryModeUI(mode) {
const descEl = this.Q('#mode-description');
const descriptions = {
full: '对整篇文章进行全面总结,适合短文章或需要完整理解的内容',
structured: '识别文章结构,按章节分段总结,适合长文章和技术文档。会自动检测标题层级,生成结构化摘要',
outline: '快速生成文章大纲,列出所有章节标题,适合快速了解文章结构'
};
descEl.textContent = descriptions[mode] || descriptions.full;
}
toggleTheme() {
// 主题循环: light -> orange -> brown -> gold -> mono -> dark -> light
const currentTheme = this.currentTheme || 'light';
// 移除所有主题类
this.host.classList.remove('dark-theme', 'orange-theme', 'brown-theme', 'gold-theme', 'mono-theme');
if (currentTheme === 'light') {
// 切换到橙红主题
this.currentTheme = 'orange';
this.host.classList.add('orange-theme');
this.Q('#btn-theme').textContent = '🟤';
this.Q('#btn-theme').title = '切换到棕色主题';
} else if (currentTheme === 'orange') {
// 切换到棕色主题
this.currentTheme = 'brown';
this.host.classList.add('brown-theme');
this.Q('#btn-theme').textContent = '🟡';
this.Q('#btn-theme').title = '切换到金色主题';
} else if (currentTheme === 'brown') {
// 切换到金色主题
this.currentTheme = 'gold';
this.host.classList.add('gold-theme');
this.Q('#btn-theme').textContent = '⚫';
this.Q('#btn-theme').title = '切换到黑白主题';
} else if (currentTheme === 'gold') {
// 切换到黑白主题
this.currentTheme = 'mono';
this.host.classList.add('mono-theme');
this.Q('#btn-theme').textContent = '🌙';
this.Q('#btn-theme').title = '切换到深色主题';
} else if (currentTheme === 'mono') {
// 切换到深色主题
this.currentTheme = 'dark';
this.host.classList.add('dark-theme');
this.Q('#btn-theme').textContent = '☀️';
this.Q('#btn-theme').title = '切换到浅色主题';
} else {
// 切换到浅色主题
this.currentTheme = 'light';
this.Q('#btn-theme').textContent = '🔶';
this.Q('#btn-theme').title = '切换到橙红主题';
}
GM_setValue('currentTheme', this.currentTheme);
console.log('[AI速读] 切换主题:', this.currentTheme);
}
showToast(message, type = '') {
const toast = this.Q('#toast');
toast.textContent = message;
toast.className = 'toast' + (type ? ` ${type}` : '');
requestAnimationFrame(() => {
toast.classList.add('show');
});
setTimeout(() => {
toast.classList.remove('show');
}, 2500);
}
copyToClipboard(text) {
try {
GM_setClipboard(text, 'text');
return true;
} catch (e) {
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
return true;
}
}
updateScrollButtons() {
const chatMessages = this.Q('#chat-messages');
const scrollTop = chatMessages.scrollTop;
const scrollHeight = chatMessages.scrollHeight;
const clientHeight = chatMessages.clientHeight;
const distanceToBottom = scrollHeight - scrollTop - clientHeight;
const btnTop = this.Q('#btn-scroll-top');
const btnBottom = this.Q('#btn-scroll-bottom');
if (scrollTop > 50) {
btnTop.classList.add('visible');
} else {
btnTop.classList.remove('visible');
}
if (this.isGenerating && this.userScrolledUp) {
btnBottom.classList.add('visible', 'generating');
} else if (distanceToBottom > 50) {
btnBottom.classList.add('visible');
btnBottom.classList.remove('generating');
} else {
btnBottom.classList.remove('visible', 'generating');
}
}
scrollToTop() {
const chatMessages = this.Q('#chat-messages');
chatMessages.scrollTo({ top: 0, behavior: 'smooth' });
}
scrollToBottom(force = false) {
if (!force && !GM_getValue('autoScroll', true)) {
this.updateScrollButtons();
return;
}
if (!force && this.userScrolledUp) {
this.updateScrollButtons();
return;
}
const chatMessages = this.Q('#chat-messages');
this.isProgrammaticScroll = true;
setTimeout(() => {
chatMessages.scrollTop = chatMessages.scrollHeight;
setTimeout(() => {
this.isProgrammaticScroll = false;
this.updateScrollButtons();
}, 50);
}, 0);
}
forceScrollToBottom() {
this.userScrolledUp = false;
const chatMessages = this.Q('#chat-messages');
this.isProgrammaticScroll = true;
setTimeout(() => {
chatMessages.scrollTop = chatMessages.scrollHeight;
setTimeout(() => {
this.isProgrammaticScroll = false;
this.updateScrollButtons();
}, 50);
}, 0);
}
clearChat() {
if (this.chatHistory.length === 0) return;
if (confirm('确定要清空所有对话记录吗?\n(总结上下文将保留,可以继续提问)')) {
if (this.chatHistory.length > 3) {
this.chatHistory = this.chatHistory.slice(0, 3);
}
this.Q('#chat-list').innerHTML = '';
this.userMessageCount = 0;
this.updateMessageCount();
if (this.chatHistory.length <= 3) {
const emptyDiv = this.Q('#chat-empty');
emptyDiv.classList.remove('hidden');
emptyDiv.innerHTML = '💬对话已清空
可以继续基于网页内容提问';
}
this.showToast('对话已清空');
}
}
updateMessageCount() {
this.Q('#msg-count').textContent = this.userMessageCount;
}
toggleSidebar() {
this.isOpen = !this.isOpen;
const sidebar = this.Q('#sidebar');
const btn = this.Q('#toggle-btn');
if (this.isOpen) {
sidebar.classList.add('open');
btn.classList.add('arrow-flip');
this.squeezeBody(true);
this.updatePageInfo();
} else {
sidebar.classList.remove('open');
btn.classList.remove('arrow-flip');
this.squeezeBody(false);
}
this.updateButtonPosition();
}
squeezeBody(active) {
const body = document.body;
body.style.transition = 'margin 0.35s cubic-bezier(0.4, 0, 0.2, 1)';
if (!active) {
body.style.marginLeft = '';
body.style.marginRight = '';
} else {
if (this.side === 'left') {
body.style.marginLeft = `${this.sidebarWidth}px`;
body.style.marginRight = '';
} else {
body.style.marginRight = `${this.sidebarWidth}px`;
body.style.marginLeft = '';
}
}
}
switchTab(tabName) {
this.shadow.querySelectorAll('.tab-item').forEach(t => {
t.classList.toggle('active', t.dataset.tab === tabName);
});
this.shadow.querySelectorAll('.view-page').forEach(p => {
p.classList.toggle('active', p.id === `page-${tabName}`);
});
this.currentTab = tabName;
if (tabName === 'chat') {
setTimeout(() => this.updateScrollButtons(), 100);
}
if (tabName === 'summary') {
this.updatePageInfo();
}
}
updatePageInfo() {
try {
const pageData = Core.extractPageContent();
const titleEl = this.Q('#page-title-display');
const urlEl = this.Q('#page-url-display');
if (titleEl) titleEl.textContent = pageData.title || '无标题';
if (urlEl) {
urlEl.textContent = pageData.url;
urlEl.title = pageData.url;
}
// 在控制台输出调试信息
if (pageData.success) {
console.log('[AI速读] 页面信息更新成功:', {
title: pageData.title,
contentLength: pageData.contentLength,
method: pageData.extractMethod
});
} else {
console.error('[AI速读] 页面信息更新失败:', pageData.error);
}
} catch (error) {
console.error('[AI速读] updatePageInfo 错误:', error);
}
}
initRangeInputs() {
// 通用版本不需要楼层范围
}
setRange(type) {
// 通用版本不需要楼层范围
}
setLoading(btnId, isLoading) {
const btn = this.Q(btnId);
this.isGenerating = isLoading;
btn.disabled = isLoading;
btn.classList.toggle('loading', isLoading);
if (btnId === '#btn-send') {
const input = this.Q('#chat-input');
if (input) {
input.disabled = isLoading;
input.placeholder = isLoading ? '正在生成回复...' : '输入你的问题... (Enter 发送)';
}
}
}
async doSummary() {
this.setLoading('#btn-summary', true);
const resultBox = this.Q('#summary-result');
resultBox.classList.remove('empty');
// 获取总结模式
const mode = GM_getValue('summaryMode', 'full');
resultBox.innerHTML = `
`;
try {
// 根据模式选择不同的处理方式
if (mode === 'outline') {
await this.doOutlineSummary(resultBox);
} else if (mode === 'structured') {
await this.doStructuredSummary(resultBox);
} else {
await this.doFullSummary(resultBox);
}
} catch (e) {
const errorHtml = `
❌ 总结失败
${Core.escapeHtml(e.message)}
`;
resultBox.innerHTML = errorHtml;
this.setLoading('#btn-summary', false);
console.error('[AI速读] doSummary 错误:', e);
}
}
// 完整总结模式
async doFullSummary(resultBox) {
// 提取当前页面内容
const pageData = Core.extractPageContent();
console.log('[AI速读] 页面数据:', pageData);
if (!pageData.success || !pageData.content || pageData.content.length < 50) {
const errorMsg = pageData.error || '未能提取到有效的网页内容';
throw new Error(`${errorMsg}\n\n提示:\n1. 请确保页面已完全加载\n2. 某些动态加载的页面可能需要等待内容加载完成\n3. 可以尝试刷新页面后重试\n\n提取到的内容长度: ${pageData.contentLength || 0} 字`);
}
this.postContent = `标题: ${pageData.title}\nURL: ${pageData.url}\n时间: ${pageData.timestamp}\n提取方式: ${pageData.extractMethod}\n内容长度: ${pageData.contentLength} 字\n\n内容:\n${pageData.content}`;
resultBox.innerHTML = `
AI 正在分析中... (已提取 ${pageData.contentLength} 字)
`;
// 获取提示词(根据模板)
const template = GM_getValue('summaryTemplate', 'general');
let sysPrompt;
if (template === 'custom') {
sysPrompt = GM_getValue('prompt_sum', SUMMARY_TEMPLATES.general.prompt);
} else {
sysPrompt = SUMMARY_TEMPLATES[template]?.prompt || SUMMARY_TEMPLATES.general.prompt;
}
const messages = [
{ role: 'system', content: sysPrompt },
{ role: 'user', content: `网页内容:\n${this.postContent}` }
];
let aiText = '';
await Core.streamChat(messages,
(chunk) => {
aiText += chunk;
const currentBlock = resultBox.querySelector('[data-thinking-block]');
const isExpanded = currentBlock?.classList.contains('expanded') || false;
resultBox.innerHTML = `
` + Core.renderWithThinking(aiText, true, isExpanded);
if (GM_getValue('autoScroll', true)) {
setTimeout(() => {
resultBox.scrollTop = resultBox.scrollHeight;
const thinkingInner = resultBox.querySelector('.thinking-content-inner');
if (thinkingInner && isExpanded) {
thinkingInner.scrollTop = thinkingInner.scrollHeight;
}
}, 0);
}
const copyBtn = this.Q('#btn-copy-summary');
if (copyBtn) {
copyBtn.onclick = () => {
this.copyToClipboard(aiText);
copyBtn.classList.add('copied');
copyBtn.textContent = '✓ 已复制';
setTimeout(() => {
copyBtn.classList.remove('copied');
copyBtn.textContent = '📋 复制';
}, 2000);
};
}
},
() => {
this.setLoading('#btn-summary', false);
const arrowUpIcon = ``;
resultBox.innerHTML = `
` + Core.renderWithThinking(aiText, false);
// 默认展开状态,不添加 collapsed 类
resultBox.classList.remove('collapsed');
// 复制按钮
const copyBtn = this.Q('#btn-copy-summary');
if (copyBtn) {
copyBtn.onclick = () => {
this.copyToClipboard(aiText);
copyBtn.classList.add('copied');
copyBtn.textContent = '✓ 已复制';
setTimeout(() => {
copyBtn.classList.remove('copied');
copyBtn.textContent = '📋 复制';
}, 2000);
};
}
// 展开/收起按钮(顶部)
const toggleBtn = this.Q('#btn-toggle-expand');
if (toggleBtn) {
toggleBtn.onclick = () => {
const isCollapsed = resultBox.classList.contains('collapsed');
if (isCollapsed) {
resultBox.classList.remove('collapsed');
toggleBtn.textContent = '📖 收起';
} else {
resultBox.classList.add('collapsed');
toggleBtn.textContent = '📖 展开';
}
};
}
// 展开按钮(底部)
const expandBtn = this.Q('#btn-expand-bottom');
if (expandBtn) {
expandBtn.onclick = () => {
resultBox.classList.remove('collapsed');
if (toggleBtn) toggleBtn.textContent = '📖 收起';
};
}
this.lastSummary = aiText;
const chatPrompt = GM_getValue('prompt_chat', '');
this.chatHistory = [
{ role: 'system', content: chatPrompt },
{ role: 'user', content: `以下是网页内容供你参考:\n${this.postContent}` },
{ role: 'assistant', content: aiText }
];
this.Q('#chat-list').innerHTML = '';
this.userMessageCount = 0;
this.updateMessageCount();
this.Q('#chat-empty').classList.remove('hidden');
this.Q('#chat-empty').innerHTML = '✅总结已完成!
现在可以基于网页内容进行对话';
console.log('[AI速读] 总结完成');
},
(err) => {
resultBox.innerHTML = `❌ 错误: ${err}
`;
this.setLoading('#btn-summary', false);
this.showToast('总结失败: ' + err, 'error');
console.error('[AI速读] 总结失败:', err);
}
);
}
// 大纲模式
async doOutlineSummary(resultBox) {
resultBox.innerHTML = `
`;
// 检测文章结构
const headings = Core.detectArticleStructure();
if (headings.length === 0) {
throw new Error('未检测到文章标题结构\n\n提示:\n1. 该页面可能没有使用标准的标题标签(h1-h6)\n2. 建议使用"完整总结"模式');
}
// 生成大纲
const outline = Core.generateOutline(headings);
const pageData = Core.extractPageContent();
let result = `# 📋 文章大纲\n\n`;
result += `**标题**: ${pageData.title}\n`;
result += `**检测到**: ${headings.length} 个章节\n\n`;
result += `---\n\n`;
result += outline;
// 添加章节详情
result += `\n## 📊 章节详情\n\n`;
headings.forEach((h, index) => {
result += `### ${index + 1}. ${h.text}\n`;
result += `- **层级**: H${h.level}\n`;
result += `- **标签**: \`<${h.tag}>\`\n\n`;
});
resultBox.innerHTML = `
` + DOMPurify.sanitize(marked.parse(result));
// 复制按钮
const copyBtn = this.Q('#btn-copy-summary');
if (copyBtn) {
copyBtn.onclick = () => {
this.copyToClipboard(result);
copyBtn.classList.add('copied');
copyBtn.textContent = '✓ 已复制';
setTimeout(() => {
copyBtn.classList.remove('copied');
copyBtn.textContent = '📋 复制';
}, 2000);
};
}
this.lastSummary = result;
this.setLoading('#btn-summary', false);
console.log('[AI速读] 大纲生成完成');
}
// 结构化总结模式
async doStructuredSummary(resultBox) {
resultBox.innerHTML = `
`;
// 检测文章结构
const headings = Core.detectArticleStructure();
if (headings.length === 0) {
throw new Error('未检测到文章标题结构\n\n提示:\n1. 该页面可能没有使用标准的标题标签(h1-h6)\n2. 建议使用"完整总结"模式');
}
if (headings.length > 20) {
throw new Error(`检测到 ${headings.length} 个章节,数量过多\n\n提示:\n1. 建议使用"大纲模式"快速查看结构\n2. 或使用"完整总结"模式对全文总结`);
}
const pageData = Core.extractPageContent();
// 生成结构化总结
let result = `# 📑 结构化总结\n\n`;
result += `**标题**: ${pageData.title}\n`;
result += `**章节数**: ${headings.length}\n\n`;
result += `---\n\n`;
// 按章节总结
for (let i = 0; i < headings.length; i++) {
const section = headings[i];
const nextSection = headings[i + 1];
resultBox.innerHTML = `
正在总结第 ${i + 1}/${headings.length} 章节: ${section.text}
`;
const content = Core.extractBySection(section, nextSection);
// 添加章节标题
result += `${'#'.repeat(section.level + 1)} ${section.text}\n\n`;
if (content.length < 50) {
result += `> 内容过短,已跳过总结\n\n`;
} else if (content.length < 500) {
// 内容较短,直接显示
result += `${content}\n\n`;
} else {
// 内容较长,生成摘要
const summary = await this.summarizeSection(section.text, content);
result += `${summary}\n\n`;
}
result += `---\n\n`;
}
resultBox.innerHTML = `
` + DOMPurify.sanitize(marked.parse(result));
// 复制按钮
const copyBtn = this.Q('#btn-copy-summary');
if (copyBtn) {
copyBtn.onclick = () => {
this.copyToClipboard(result);
copyBtn.classList.add('copied');
copyBtn.textContent = '✓ 已复制';
setTimeout(() => {
copyBtn.classList.remove('copied');
copyBtn.textContent = '📋 复制';
}, 2000);
};
}
this.lastSummary = result;
this.postContent = `标题: ${pageData.title}\n\n结构化总结:\n${result}`;
// 设置对话上下文
const chatPrompt = GM_getValue('prompt_chat', '');
this.chatHistory = [
{ role: 'system', content: chatPrompt },
{ role: 'user', content: `以下是网页的结构化总结:\n${this.postContent}` },
{ role: 'assistant', content: result }
];
this.setLoading('#btn-summary', false);
console.log('[AI速读] 结构化总结完成');
}
// 总结单个章节
async summarizeSection(title, content) {
const prompt = `请简要总结以下章节的核心内容(2-3句话):
章节标题:${title}
内容:
${content.substring(0, 2000)}`;
let summary = '';
try {
const messages = [
{ role: 'user', content: prompt }
];
await Core.streamChat(messages,
(chunk) => {
summary += chunk;
},
() => {},
(err) => {
console.error('[AI速读] 章节总结失败:', err);
summary = '(总结失败)';
}
);
} catch (e) {
summary = '(总结失败)';
}
return summary || '(无内容)';
}
async doChat() {
if (this.isGenerating) return;
if (this.chatHistory.length === 0) {
return alert('请先在「总结」页面生成网页内容摘要');
}
const input = this.Q('#chat-input');
const txt = input.value.trim();
if (!txt) return;
input.value = '';
input.style.height = 'auto';
this.Q('#chat-empty').classList.add('hidden');
this.userScrolledUp = false;
this.addBubble('user', txt);
this.chatHistory.push({ role: 'user', content: txt });
this.userMessageCount++;
this.updateMessageCount();
const msgDiv = this.addBubble('ai', '');
msgDiv.innerHTML = `
`;
let aiText = '';
this.setLoading('#btn-send', true);
await Core.streamChat(this.chatHistory,
(chunk) => {
aiText += chunk;
const currentBlock = msgDiv.querySelector('[data-thinking-block]');
const isExpanded = currentBlock?.classList.contains('expanded') || false;
msgDiv.innerHTML = Core.renderWithThinking(aiText, true, isExpanded);
if (GM_getValue('autoScroll', true) && isExpanded) {
setTimeout(() => {
const thinkingInner = msgDiv.querySelector('.thinking-content-inner');
if (thinkingInner) {
thinkingInner.scrollTop = thinkingInner.scrollHeight;
}
}, 0);
}
this.scrollToBottom();
},
() => {
msgDiv.innerHTML = Core.renderWithThinking(aiText, false);
this.chatHistory.push({ role: 'assistant', content: aiText });
this.setLoading('#btn-send', false);
this.userScrolledUp = false;
this.updateScrollButtons();
},
(err) => {
msgDiv.innerHTML += `
❌ ${err}`;
this.setLoading('#btn-send', false);
}
);
}
addBubble(role, text) {
const div = document.createElement('div');
div.className = `bubble bubble-${role}`;
div.innerHTML = role === 'user' ? text : Core.renderWithThinking(text);
this.Q('#chat-list').appendChild(div);
this.scrollToBottom();
return div;
}
// ========== 导出功能相关方法 ==========
async doExport() {
const exportType = this.Q('#export-type').value;
const statusBox = this.Q('#export-status');
console.log('[AI速读] 开始导出,类型:', exportType);
this.setLoading('#btn-export', true);
statusBox.classList.remove('empty');
statusBox.innerHTML = ``;
try {
if (exportType === 'summary') {
await this.exportSummary(statusBox);
} else if (exportType === 'chat') {
await this.exportChat(statusBox);
} else if (exportType === 'html') {
await this.exportHtml(statusBox);
}
this.setLoading('#btn-export', false);
} catch (e) {
console.error('[AI速读] 导出失败:', e);
statusBox.innerHTML = `❌ 导出失败: ${e.message}
`;
this.setLoading('#btn-export', false);
this.showToast('导出失败: ' + e.message, 'error');
}
}
async exportSummary(statusBox) {
const includeThinking = this.Q('#export-include-thinking').checked;
if (!this.lastSummary) {
throw new Error('暂无总结内容,请先生成总结');
}
statusBox.innerHTML = ``;
const pageData = Core.extractPageContent();
let content = '';
// 添加页面信息
content += `# ${pageData.title}\n\n`;
content += `> 来源: ${pageData.url}\n`;
content += `> 导出时间: ${new Date().toLocaleString('zh-CN')}\n\n`;
content += `---\n\n`;
// 添加总结内容
if (includeThinking) {
const { thinking, content: mainContent } = Core.parseThinkingContent(this.lastSummary);
if (thinking) {
content += `## 💡 思考过程\n\n`;
content += `${thinking}\n\n`;
content += `---\n\n`;
}
if (mainContent) {
content += `## 📝 总结内容\n\n`;
content += `${mainContent}\n\n`;
}
} else {
const { content: mainContent } = Core.parseThinkingContent(this.lastSummary);
content += `## 📝 总结内容\n\n`;
content += `${mainContent}\n\n`;
}
// 添加页脚
content += `---\n\n`;
content += `*由 AI速读 生成*\n`;
const filename = Core.sanitizeFilename(`${pageData.title}_总结.md`);
Core.downloadFile(content, filename, 'text/markdown');
statusBox.innerHTML = `✅ Markdown 文件已导出!
文件名: ${filename}
`;
this.showToast('总结导出成功', 'success');
}
async exportChat(statusBox) {
if (this.chatHistory.length === 0) {
throw new Error('暂无对话历史');
}
statusBox.innerHTML = ``;
const pageData = Core.extractPageContent();
let content = '';
// 添加页面信息
content += `对话历史 - ${pageData.title}\n`;
content += `来源: ${pageData.url}\n`;
content += `导出时间: ${new Date().toLocaleString('zh-CN')}\n`;
content += `对话轮数: ${this.chatHistory.filter(m => m.role === 'user').length}\n`;
content += `\n${'='.repeat(60)}\n\n`;
// 添加对话内容
for (const msg of this.chatHistory) {
if (msg.role === 'system') continue;
const label = msg.role === 'user' ? '👤 用户' : '🤖 AI';
content += `${label}:\n`;
if (msg.role === 'assistant') {
const { thinking, content: mainContent } = Core.parseThinkingContent(msg.content);
if (thinking) {
content += `\n[思考过程]\n${thinking}\n\n`;
}
content += `${mainContent}\n`;
} else {
content += `${msg.content}\n`;
}
content += `\n${'-'.repeat(60)}\n\n`;
}
// 添加页脚
content += `\n由 AI速读 生成\n`;
const filename = Core.sanitizeFilename(`${pageData.title}_对话.txt`);
Core.downloadFile(content, filename, 'text/plain');
statusBox.innerHTML = `✅ 对话文本已导出!
文件名: ${filename}
`;
this.showToast('对话导出成功', 'success');
}
async exportHtml(statusBox) {
const includeOriginal = this.Q('#export-include-original').checked;
const theme = this.Q('#export-html-theme').value;
statusBox.innerHTML = ``;
const pageData = Core.extractPageContent();
const themeColors = theme === 'dark' ? {
bg: '#1a1f1c',
card: '#232a26',
text: '#e8f5ef',
textSec: '#a8c9b8',
border: 'rgba(107, 194, 153, 0.15)',
primary: '#6bc299'
} : {
bg: '#f5f9f7',
card: '#ffffff',
text: '#1f3a2e',
textSec: '#4a6456',
border: 'rgba(82, 166, 125, 0.12)',
primary: '#52a67d'
};
let summaryHtml = '';
if (this.lastSummary) {
const { thinking, content } = Core.parseThinkingContent(this.lastSummary);
if (thinking) {
summaryHtml += `
💡 思考过程
${marked.parse(thinking)}
`;
}
if (content) {
summaryHtml += `
${marked.parse(content)}
`;
}
}
let originalHtml = '';
if (includeOriginal && pageData.content) {
const contentPreview = pageData.content.length > 5000
? pageData.content.substring(0, 5000) + '\n\n[内容过长,已截断...]'
: pageData.content;
originalHtml = `
📄 原始内容
${Core.escapeHtml(contentPreview)}
`;
}
const html = `
${Core.escapeHtml(pageData.title)} - AI速读
📝 AI 总结
${summaryHtml || '
暂无总结内容
'}
${originalHtml}
`;
const filename = Core.sanitizeFilename(`${pageData.title}_快照.html`);
Core.downloadFile(html, filename, 'text/html');
statusBox.innerHTML = `✅ HTML 文件已导出!
文件名: ${filename}
`;
this.showToast('HTML 导出成功', 'success');
}
}
window.addEventListener('load', () => new AppUI());
})();