// ==UserScript==
// @name 网页文章总结助手
// @namespace http://tampermonkey.net/
// @version 0.1.3
// @description 自动总结网页文章内容,支持多种格式输出,适用于各类文章网站
// @author h7ml <[email protected]>
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_getResourceText
// @connect api.gptgod.online
// @connect api.deepseek.com
// @resource marked https://cdn.bootcdn.net/ajax/libs/marked/4.3.0/marked.min.js
// @resource highlight https://cdn.bootcdn.net/ajax/libs/highlight.js/11.7.0/highlight.min.js
// @resource highlightStyle https://cdn.bootcdn.net/ajax/libs/highlight.js/11.7.0/styles/github.min.css
// ==/UserScript==
(function () {
'use strict';
// 加载 marked.js 库
function loadMarkedJS() {
return new Promise((resolve) => {
try {
const markedScript = GM_getResourceText('marked');
if (markedScript) {
// 创建一个函数来执行 marked.js 的内容
const executeMarked = new Function(markedScript);
executeMarked();
resolve();
} else {
console.error('无法加载 marked.js');
resolve(); // 即使加载失败也继续
}
} catch (error) {
console.error('加载 marked.js 失败:', error);
resolve(); // 即使加载失败也继续
}
});
}
// 加载 highlight.js 库
function loadHighlightJS() {
return new Promise((resolve) => {
try {
const highlightScript = GM_getResourceText('highlight');
const highlightStyle = GM_getResourceText('highlightStyle');
if (highlightScript) {
// 创建一个函数来执行 highlight.js 的内容
const executeHighlight = new Function(highlightScript);
executeHighlight();
}
if (highlightStyle) {
const style = document.createElement('style');
style.textContent = highlightStyle;
document.head.appendChild(style);
}
resolve();
} catch (error) {
console.error('加载 highlight.js 失败:', error);
resolve(); // 即使加载失败也继续
}
});
}
// 创建样式 - 使用普通 CSS
const style = document.createElement('style');
style.textContent = `
/* 基础样式 */
#article-summary-app {
position: fixed;
top: 1rem;
right: 1rem;
width: 24rem;
z-index: 2147483647;
min-width: 300px;
min-height: 200px;
resize: both;
overflow: auto;
cursor: move;
font-family: system-ui, -apple-system, sans-serif;
font-size: 0.875rem;
line-height: 1.5;
color: #374151;
background-color: white;
border-radius: 0.5rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
transition: all 0.2s;
}
/* 头部样式 */
#summary-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background-color: #2563eb;
color: white;
}
#summary-header h3 {
margin: 0;
font-size: 1rem;
font-weight: 600;
}
#summary-header-actions {
display: flex;
gap: 0.5rem;
}
.header-btn {
background: transparent;
border: none;
color: white;
cursor: pointer;
padding: 0.25rem;
border-radius: 0.25rem;
display: flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
transition: background-color 0.2s;
}
.header-btn:hover {
background-color: rgba(255, 255, 255, 0.2);
}
/* 主体内容 */
#summary-body {
padding: 1rem;
}
/* 配置面板 */
#config-section {
margin-bottom: 1rem;
}
#configToggle {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.625rem 0.75rem;
background-color: #f3f4f6;
border-radius: 0.25rem;
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s;
}
#configToggle:hover {
background-color: #e5e7eb;
}
#configToggle.collapsed .toggle-icon {
transform: rotate(-90deg);
}
.toggle-icon {
transition: transform 0.2s;
}
#configPanel {
max-height: 500px;
overflow: hidden;
transition: all 0.3s ease-out;
opacity: 1;
margin-top: 0.75rem;
}
#configPanel.collapsed {
max-height: 0;
opacity: 0;
margin-top: 0;
}
/* 表单元素 */
.form-group {
margin-bottom: 0.75rem;
}
.form-label {
display: block;
margin-bottom: 0.375rem;
font-weight: 500;
color: #374151;
}
.form-select, .form-input {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.25rem;
background-color: #f9fafb;
transition: all 0.2s;
}
.form-select:focus, .form-input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 1px #3b82f6;
}
/* 格式选择按钮 */
#formatOptions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.format-btn {
padding: 0.375rem 0.75rem;
background-color: #f3f4f6;
border: 1px solid #d1d5db;
border-radius: 0.25rem;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s;
}
.format-btn:hover {
background-color: #e5e7eb;
}
.format-btn.active {
background-color: #2563eb;
color: white;
border-color: #2563eb;
}
/* 生成按钮 */
#generateBtn {
width: 100%;
padding: 0.625rem;
background-color: #2563eb;
color: white;
border: none;
border-radius: 0.25rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
}
#generateBtn:hover {
background-color: #1d4ed8;
}
#generateBtn:disabled {
background-color: #93c5fd;
cursor: not-allowed;
}
/* 结果区域 */
#summaryResult {
margin-top: 1rem;
display: none;
}
#summaryHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
#summaryHeader h4 {
margin: 0;
font-size: 1rem;
font-weight: 600;
color: #1f2937;
}
#summaryActions {
display: flex;
gap: 0.5rem;
}
.action-btn {
padding: 0.25rem 0.5rem;
background-color: #f3f4f6;
border: 1px solid #d1d5db;
border-radius: 0.25rem;
font-size: 0.75rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.25rem;
transition: background-color 0.2s;
}
.action-btn:hover {
background-color: #e5e7eb;
}
#summaryContent {
border: 1px solid #e5e7eb;
border-radius: 0.25rem;
padding: 1rem;
background-color: #f9fafb;
max-height: 400px;
overflow-y: auto;
line-height: 1.625;
}
/* Markdown 样式 */
#summaryContent.markdown-body {
font-family: system-ui, -apple-system, sans-serif;
}
#summaryContent h1 {
font-size: 1.25rem;
margin: 0.5rem 0;
font-weight: 600;
color: #111827;
}
#summaryContent h2 {
font-size: 1.125rem;
margin: 0.5rem 0;
font-weight: 600;
color: #111827;
}
#summaryContent h3 {
font-size: 1rem;
margin: 0.5rem 0;
font-weight: 600;
color: #111827;
}
#summaryContent ul, #summaryContent ol {
padding-left: 1.5rem;
margin: 0.5rem 0;
}
#summaryContent p {
margin: 0.5rem 0;
}
#summaryContent pre {
background-color: #f3f4f6;
padding: 0.75rem;
border-radius: 0.25rem;
overflow-x: auto;
margin: 0.5rem 0;
}
#summaryContent code {
font-family: ui-monospace, monospace;
font-size: 0.875rem;
background-color: #f3f4f6;
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
}
/* 加载指示器 */
#loadingIndicator {
display: none;
text-align: center;
padding: 1.25rem 0;
}
.spinner {
width: 2.5rem;
height: 2.5rem;
margin: 0 auto;
border: 3px solid #e5e7eb;
border-radius: 9999px;
border-top-color: #2563eb;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 响应式调整 */
@media (max-width: 640px) {
#article-summary-app {
width: 90%;
right: 5%;
left: 5%;
}
}
/* 工具提示 */
.tooltip {
position: relative;
}
.tooltip:hover::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
padding: 0.25rem 0.5rem;
background-color: #1f2937;
color: white;
border-radius: 0.25rem;
font-size: 0.75rem;
white-space: nowrap;
z-index: 10;
}
/* 图标 */
.icon {
width: 1rem;
height: 1rem;
display: inline-block;
}
/* 添加拖拽相关样式 */
.draggable {
user-select: none;
cursor: move;
}
.resizable {
resize: both;
overflow: auto;
}
/* 总结内容样式优化 */
.summary-container {
font-family: system-ui, -apple-system, sans-serif;
line-height: 1.6;
color: #333;
padding: 1rem;
}
.summary-container h1 {
font-size: 1.5rem;
color: #1a365d;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid #e2e8f0;
}
.summary-container h2 {
font-size: 1.25rem;
color: #2d3748;
margin: 1.5rem 0 1rem;
}
.summary-container ul {
list-style: none;
padding-left: 1.5rem;
margin: 1rem 0;
}
.summary-container li {
position: relative;
padding-left: 1.5rem;
margin-bottom: 0.5rem;
}
.summary-container li:before {
content: "•";
color: #4299e1;
font-weight: bold;
position: absolute;
left: 0;
}
.summary-container strong {
color: #2b6cb0;
font-weight: 600;
}
.summary-container code {
background: #f7fafc;
padding: 0.2rem 0.4rem;
border-radius: 0.25rem;
font-family: monospace;
font-size: 0.9em;
color: #4a5568;
border: 1px solid #edf2f7;
}
`;
document.head.appendChild(style);
// 创建应用容器
const app = document.createElement('div');
app.id = 'article-summary-app';
// 创建HTML内容 - 使用更现代的UI设计
app.innerHTML = `
<div id="summary-header">
<h3>文章总结助手</h3>
<div id="summary-header-actions">
<button id="toggleMaxBtn" class="header-btn" data-tooltip="最大化">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M8 3v3a2 2 0 01-2 2H3m18 0h-3a2 2 0 01-2-2V3m0 18v-3a2 2 0 012-2h3M3 16h3a2 2 0 012 2v3"></path>
</svg>
</button>
<button id="toggleMinBtn" class="header-btn" data-tooltip="最小化">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 12H4"></path>
</svg>
</button>
</div>
</div>
<div id="summary-body">
<div id="config-section">
<div id="configToggle">
<span>配置选项</span>
<svg class="icon toggle-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 9l-7 7-7-7"></path>
</svg>
</div>
<div id="configPanel">
<div class="form-group">
<label class="form-label" for="apiService">API服务</label>
<select id="apiService" class="form-select">
<option value="gptgod">GPT God</option>
<option value="deepseek">DeepSeek</option>
<option value="custom">自定义</option>
</select>
</div>
<div id="customApiUrlContainer" class="form-group" style="display: none;">
<label class="form-label" for="customApiUrl">API地址</label>
<input type="text" id="customApiUrl" class="form-input" placeholder="https://api.example.com/v1/chat/completions">
</div>
<div class="form-group">
<label class="form-label" for="apiKey">API Key</label>
<input type="password" id="apiKey" class="form-input" placeholder="sk-...">
</div>
<div class="form-group">
<label class="form-label" for="modelName">模型</label>
<input type="text" id="modelName" class="form-input" placeholder="gpt-4o-all">
</div>
<div class="form-group">
<label class="form-label">输出格式</label>
<div id="formatOptions">
<span class="format-btn active" data-format="markdown">Markdown</span>
<span class="format-btn" data-format="bullet">要点列表</span>
<span class="format-btn" data-format="paragraph">段落</span>
</div>
</div>
</div>
</div>
<button type="button" id="generateBtn">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>
</svg>
生成总结
</button>
<div id="summaryResult">
<div id="summaryHeader">
<h4>文章总结</h4>
<div id="summaryActions">
<button id="copyBtn" class="action-btn" data-tooltip="复制到剪贴板">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"></path>
</svg>
复制
</button>
</div>
</div>
<div id="summaryContent" class="markdown-body">
<textarea class="content-textarea resizable"></textarea>
</div>
</div>
<div id="loadingIndicator">
<div class="spinner"></div>
<p>正在生成总结,请稍候...</p>
</div>
</div>
`;
document.body.appendChild(app);
// 加载 Markdown 处理库
Promise.all([loadMarkedJS(), loadHighlightJS()]).then(() => {
console.log('Markdown 渲染库加载完成');
// 如果 marked 库加载成功,配置它
if (window.marked) {
marked.setOptions({
renderer: new marked.Renderer(),
highlight: function (code, lang) {
if (window.hljs) {
const language = lang && hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, { language }).value;
}
return code;
},
langPrefix: 'hljs language-',
pedantic: false,
gfm: true,
breaks: true,
sanitize: false,
smartypants: false,
xhtml: false
});
}
}).catch(error => {
console.error('加载 Markdown 渲染库失败:', error);
});
// 获取DOM元素
const apiServiceSelect = document.getElementById('apiService');
const customApiUrlContainer = document.getElementById('customApiUrlContainer');
const customApiUrlInput = document.getElementById('customApiUrl');
const apiKeyInput = document.getElementById('apiKey');
const modelNameInput = document.getElementById('modelName');
const generateBtn = document.getElementById('generateBtn');
const summaryResult = document.getElementById('summaryResult');
const summaryContent = document.getElementById('summaryContent');
const loadingIndicator = document.getElementById('loadingIndicator');
const configToggle = document.getElementById('configToggle');
const configPanel = document.getElementById('configPanel');
const toggleMaxBtn = document.getElementById('toggleMaxBtn');
const toggleMinBtn = document.getElementById('toggleMinBtn');
const formatBtns = document.querySelectorAll('.format-btn');
const copyBtn = document.getElementById('copyBtn');
// 默认设置
const DEFAULT_API_SERVICE = 'gptgod';
const DEFAULT_CONFIGS = {
gptgod: {
url: 'https://api.gptgod.online/v1/chat/completions',
model: 'gpt-4o-all',
key: 'sk-L1rbJXBp3aDrZLgyrUq8FugKU54FxElTbzt7RfnBaWgHOtFj'
},
deepseek: {
url: 'https://api.deepseek.com/v1/chat/completions',
model: 'deepseek-chat',
key: ''
},
custom: {
url: '',
model: '',
key: ''
}
};
const DEFAULT_FORMAT = 'markdown';
// 从存储中恢复设置
const savedConfigs = GM_getValue('apiConfigs', DEFAULT_CONFIGS);
const savedApiService = GM_getValue('apiService', DEFAULT_API_SERVICE);
const savedFormat = GM_getValue('outputFormat', DEFAULT_FORMAT);
const savedConfigCollapsed = GM_getValue('configCollapsed', false);
// 设置输入框的值
const currentConfig = savedConfigs[savedApiService];
if (currentConfig) {
apiKeyInput.value = currentConfig.key;
modelNameInput.value = currentConfig.model;
if (savedApiService === 'custom') {
customApiUrlInput.value = currentConfig.url;
customApiUrlContainer.style.display = 'block';
}
}
apiServiceSelect.value = savedApiService;
// 设置输出格式按钮状态
formatBtns.forEach(btn => {
if (btn.dataset.format === savedFormat) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
// 设置配置面板折叠状态
if (savedConfigCollapsed) {
configPanel.classList.add('collapsed');
configToggle.querySelector('.toggle-icon').style.transform = 'rotate(-90deg)';
}
// 事件监听
apiServiceSelect.addEventListener('change', function () {
const service = this.value;
const configs = GM_getValue('apiConfigs', DEFAULT_CONFIGS);
const currentConfig = configs[service];
// 更新输入框的值
apiKeyInput.value = currentConfig.key;
modelNameInput.value = currentConfig.model;
if (service === 'custom') {
customApiUrlInput.value = currentConfig.url;
customApiUrlContainer.style.display = 'block';
} else {
customApiUrlContainer.style.display = 'none';
}
GM_setValue('apiService', service);
});
// 修改输入框的事件监听
apiKeyInput.addEventListener('change', function () {
const service = apiServiceSelect.value;
const configs = GM_getValue('apiConfigs', DEFAULT_CONFIGS);
configs[service].key = this.value;
GM_setValue('apiConfigs', configs);
});
customApiUrlInput.addEventListener('change', function () {
const service = apiServiceSelect.value;
const configs = GM_getValue('apiConfigs', DEFAULT_CONFIGS);
configs[service].url = this.value;
GM_setValue('apiConfigs', configs);
});
modelNameInput.addEventListener('change', function () {
const service = apiServiceSelect.value;
const configs = GM_getValue('apiConfigs', DEFAULT_CONFIGS);
configs[service].model = this.value;
GM_setValue('apiConfigs', configs);
});
// 配置面板折叠/展开
configToggle.addEventListener('click', function () {
configPanel.classList.toggle('collapsed');
const isCollapsed = configPanel.classList.contains('collapsed');
const toggleIcon = this.querySelector('.toggle-icon');
toggleIcon.style.transform = isCollapsed ? 'rotate(-90deg)' : '';
GM_setValue('configCollapsed', isCollapsed);
});
// 拖拽相关变量声明
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
const header = document.getElementById('summary-header');
header.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
function dragStart(e) {
initialX = e.clientX - app.offsetLeft;
initialY = e.clientY - app.offsetTop;
isDragging = true;
}
function drag(e) {
if (isDragging) {
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
// 确保不超出屏幕边界
currentX = Math.max(0, Math.min(currentX, window.innerWidth - app.offsetWidth));
currentY = Math.max(0, Math.min(currentY, window.innerHeight - app.offsetHeight));
app.style.left = currentX + "px";
app.style.top = currentY + "px";
}
}
function dragEnd() {
isDragging = false;
// 保存位置
GM_setValue('appPosition', { x: currentX, y: currentY });
}
// 添加最大化/最小化功能
let isMaximized = false;
let previousSize = {};
toggleMaxBtn.addEventListener('click', () => {
if (!isMaximized) {
// 保存当前大小和位置
previousSize = {
width: app.style.width,
height: app.style.height,
left: app.style.left,
top: app.style.top
};
// 最大化
app.style.width = '100%';
app.style.height = '100vh';
app.style.left = '0';
app.style.top = '0';
toggleMaxBtn.innerHTML = `
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M8 3v3a2 2 0 01-2 2H3m18 0h-3a2 2 0 01-2-2V3m0 18v-3a2 2 0 012-2h3M3 16h3a2 2 0 012 2v3"></path>
</svg>
`;
} else {
// 恢复之前的大小和位置
Object.assign(app.style, previousSize);
toggleMaxBtn.innerHTML = `
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"></path>
</svg>
`;
}
isMaximized = !isMaximized;
});
// 输出格式选择
formatBtns.forEach(btn => {
btn.addEventListener('click', function () {
formatBtns.forEach(b => b.classList.remove('active'));
this.classList.add('active');
GM_setValue('outputFormat', this.dataset.format);
});
});
// 复制按钮功能
copyBtn.addEventListener('click', function () {
const outputFormat = document.querySelector('.format-btn.active').dataset.format;
let textToCopy;
if (outputFormat === 'markdown') {
// 获取原始的 Markdown 文本
textToCopy = summaryContent.getAttribute('data-markdown') || summaryContent.textContent;
} else {
textToCopy = summaryContent.textContent;
}
navigator.clipboard.writeText(textToCopy).then(() => {
const originalHTML = this.innerHTML;
this.innerHTML = `
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M5 13l4 4L19 7"></path>
</svg>
已复制
`;
setTimeout(() => {
this.innerHTML = originalHTML;
}, 2000);
}).catch(err => {
console.error('复制失败:', err);
alert('复制失败,请手动选择文本复制');
});
});
// 修改 simpleMarkdownRender 函数
function simpleMarkdownRender(text) {
// 首先将文本包装在容器中
let html = '<div class="summary-container">';
// 处理文本内容
const content = text
// 处理标题
.replace(/^# (.*$)/gm, '<h1>$1</h1>')
.replace(/^## (.*$)/gm, '<h2>$1</h2>')
.replace(/^### (.*$)/gm, '<h3>$1</h3>')
// 处理列表
.replace(/^\d+\.\s+\*\*(.*?)\*\*:([\s\S]*?)(?=(?:\d+\.|$))/gm, (match, title, items) => {
const listItems = items
.split(/\n\s*-\s+/)
.filter(item => item.trim())
.map(item => `<li>${item.trim()}</li>`)
.join('');
return `<h2>${title}</h2><ul>${listItems}</ul>`;
})
// 处理加粗
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
// 处理代码
.replace(/`([^`]+)`/g, '<code>$1</code>')
// 处理普通段落
.replace(/([^\n]+)(?:\n|$)/g, (match, p1) => {
if (!p1.startsWith('<') && p1.trim()) {
return `<p>${p1}</p>`;
}
return p1;
});
// 关闭容器
html += content + '</div>';
return html;
}
// 修改生成总结按钮的事件处理
generateBtn.addEventListener('click', async function () {
const apiService = apiServiceSelect.value;
const configs = GM_getValue('apiConfigs', DEFAULT_CONFIGS);
const currentConfig = configs[apiService];
const apiKey = apiKeyInput.value.trim();
const modelName = modelNameInput.value.trim();
const customApiUrl = customApiUrlInput.value.trim();
if (!apiKey) {
alert('请输入有效的 API Key');
return;
}
// 保存当前配置
currentConfig.key = apiKey;
currentConfig.model = modelName;
if (apiService === 'custom') {
currentConfig.url = customApiUrl;
}
GM_setValue('apiConfigs', configs);
if (apiService === 'custom' && !customApiUrl) {
alert('请输入自定义API地址');
return;
}
// 显示加载指示器
loadingIndicator.style.display = 'block';
generateBtn.disabled = true;
generateBtn.innerHTML = `
<svg class="icon spinner" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10" stroke-opacity="0.25" stroke-dasharray="30" stroke-dashoffset="0"></circle>
<circle cx="12" cy="12" r="10" stroke-dasharray="30" stroke-dashoffset="15"></circle>
</svg>
生成中...
`;
try {
const content = await getArticleContent();
if (!content || content.length < 10) {
throw new Error('无法获取文章内容或内容太短');
}
const summary = await generateSummary(content, apiService, apiKey, customApiUrl, modelName, currentConfig.model);
// 显示结果
if (currentConfig.model === 'markdown') {
// 保存原始 Markdown 文本用于复制
summaryContent.setAttribute('data-markdown', summary);
// 尝试使用 marked.js 渲染,如果不可用则使用简单渲染
if (window.marked) {
summaryContent.innerHTML = marked.parse(summary);
// 如果有 highlight.js,应用代码高亮
if (window.hljs) {
document.querySelectorAll('#summaryContent pre code').forEach((block) => {
hljs.highlightElement(block);
});
}
} else {
// 使用优化后的简单渲染
summaryContent.innerHTML = simpleMarkdownRender(summary);
}
} else {
summaryContent.innerHTML = simpleMarkdownRender(summary);
}
summaryResult.style.display = 'block';
} catch (error) {
let errorMsg = error.message;
// 提供更友好的错误信息
if (errorMsg.includes('Authentication Fails') || errorMsg.includes('no such user')) {
errorMsg = 'API Key 无效或已过期,请更新您的 API Key';
} else if (errorMsg.includes('rate limit')) {
errorMsg = 'API 调用次数已达上限,请稍后再试';
}
alert('生成总结失败:' + errorMsg);
console.error('API 错误详情:', error);
} finally {
// 隐藏加载指示器
loadingIndicator.style.display = 'none';
generateBtn.disabled = false;
generateBtn.innerHTML = `
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>
</svg>
生成总结
`;
}
});
// 获取文章内容
async function getArticleContent() {
// 常见的文章内容选择器
const selectors = [
// 微信公众号
'#js_content',
// 知乎
'.RichText',
// 简书
'.article-content',
// 掘金
'.article-content',
// CSDN
'#article_content',
// 博客园
'#cnblogs_post_body',
// 通用文章容器
'article',
'.article',
'.post-content',
'.content',
'.entry-content',
'.article-content',
// 如果找不到特定容器,尝试获取主要内容区域
'main',
'#main',
'.main'
];
// 尝试使用不同的选择器获取内容
for (const selector of selectors) {
const element = document.querySelector(selector);
if (element) {
// 移除不需要的元素
const clone = element.cloneNode(true);
const removeSelectors = [
'script',
'style',
'iframe',
'nav',
'header',
'footer',
'.advertisement',
'.ad',
'.ads',
'.social-share',
'.related-posts',
'.comments',
'.comment',
'.author-info',
'.article-meta',
'.article-info',
'.article-header',
'.article-footer'
];
removeSelectors.forEach(selector => {
const elements = clone.querySelectorAll(selector);
elements.forEach(el => el.remove());
});
// 获取文本内容
let content = clone.innerText.trim();
// 清理文本
content = content
.replace(/\s+/g, ' ') // 将多个空白字符替换为单个空格
.replace(/\n\s*\n/g, '\n') // 将多个空行替换为单个换行
.trim();
if (content.length > 100) { // 确保内容足够长
return content;
}
}
}
// 如果上述方法都失败,尝试获取整个页面的主要内容
const body = document.body.cloneNode(true);
const removeSelectors = [
'script',
'style',
'iframe',
'nav',
'header',
'footer',
'.advertisement',
'.ad',
'.ads',
'.social-share',
'.related-posts',
'.comments',
'.comment',
'.author-info',
'.article-meta',
'.article-info',
'.article-header',
'.article-footer',
'#article-summary-app' // 移除我们的应用界面
];
removeSelectors.forEach(selector => {
const elements = body.querySelectorAll(selector);
elements.forEach(el => el.remove());
});
let content = body.innerText.trim();
// 清理文本
content = content
.replace(/\s+/g, ' ') // 将多个空白字符替换为单个空格
.replace(/\n\s*\n/g, '\n') // 将多个空行替换为单个换行
.trim();
if (content.length < 100) {
throw new Error('无法获取足够的文章内容');
}
return content;
}
// 生成总结
async function generateSummary(content, apiService, apiKey, customApiUrl, modelName, outputFormat) {
let apiEndpoint;
if (apiService === 'deepseek') {
apiEndpoint = 'https://api.deepseek.com/v1/chat/completions';
} else if (apiService === 'gptgod') {
apiEndpoint = 'https://api.gptgod.online/v1/chat/completions';
} else {
apiEndpoint = customApiUrl;
}
// 根据输出格式调整系统提示
let systemPrompt;
switch (outputFormat) {
case 'markdown':
systemPrompt = "请用中文总结以下文章的主要内容,以标准Markdown格式输出,包括标题、小标题和要点。确保格式规范,便于阅读。";
break;
case 'bullet':
systemPrompt = "请用中文总结以下文章的主要内容,以简洁的要点列表形式输出,每个要点前使用'- '标记。";
break;
case 'paragraph':
systemPrompt = "请用中文总结以下文章的主要内容,以连贯的段落形式输出,突出文章的核心观点和结论。";
break;
default:
systemPrompt = "请用中文总结以下文章的主要内容,以简洁的方式列出重点。";
}
const messages = [
{
role: "system",
content: systemPrompt
},
{
role: "user",
content: content
}
];
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: apiEndpoint,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
data: JSON.stringify({
model: modelName,
messages: messages,
stream: false
}),
onload: function (response) {
try {
console.log('API响应状态码:', response.status);
console.log('API响应内容:', response.responseText.substring(0, 200) + '...');
// 检查是否为HTML响应(通常表示错误页面)
if (response.responseText.trim().startsWith('<')) {
reject(new Error(`API返回了HTML而不是JSON (状态码: ${response.status})`));
return;
}
if (response.status >= 400) {
try {
const data = JSON.parse(response.responseText);
reject(new Error(data.error?.message || `请求失败 (${response.status})`));
} catch (e) {
reject(new Error(`请求失败 (${response.status}): ${response.responseText.substring(0, 100)}`));
}
return;
}
const data = JSON.parse(response.responseText);
if (data.error) {
reject(new Error(data.error.message));
} else if (data.choices && data.choices[0] && data.choices[0].message) {
resolve(data.choices[0].message.content);
} else {
console.error('异常API响应结构:', data);
reject(new Error('API 返回格式异常'));
}
} catch (error) {
console.error('解析响应失败:', error, response.responseText.substring(0, 200));
reject(new Error(`解析API响应失败: ${error.message}`));
}
},
onerror: function (error) {
console.error('请求错误:', error);
reject(new Error('网络请求失败'));
}
});
});
}
})();