// ==UserScript==
// @name 网页文章总结助手
// @namespace http://tampermonkey.net/
// @version 0.2.3
// @description 自动总结网页文章内容,支持多种格式输出,适用于各类文章网站
// @author h7ml <[email protected]>
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant GM_getResourceText
// @connect api.gptgod.online
// @connect api.deepseek.com
// @connect localhost
// @connect *
// @resource jquery https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js
// @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==
(() => {
// 检查jQuery是否已经存在
if (typeof jQuery === 'undefined') {
console.log('正在加载jQuery...');
try {
// 使用油猴的GM_getResourceText加载jQuery
const jqueryCode = GM_getResourceText('jquery');
// 使用eval执行jQuery代码
eval(jqueryCode);
console.log('jQuery加载完成,初始化应用...');
initApp();
} catch (error) {
console.error('使用GM_getResourceText加载jQuery失败:', error);
// 回退方案:尝试创建本地脚本元素
const script = document.createElement('script');
script.textContent = GM_getResourceText('jquery');
script.onload = function () {
console.log('jQuery通过本地脚本元素加载完成,初始化应用...');
initApp();
};
document.head.appendChild(script);
}
} else {
console.log('jQuery已存在,直接初始化应用...');
initApp();
}
function initApp() {
'use strict';
// 配置管理类
class ConfigManager {
constructor() {
this.DEFAULT_API_SERVICE = 'ollama';
this.DEFAULT_CONFIGS = {
ollama: {
url: 'http:/160.202.244.103:11434/api/chat',
model: 'deepseek-r1:7b',
key: '', // Ollama 不需要 API key
params: {
temperature: 0.8,
top_p: 0.9,
top_k: 40,
num_ctx: 4096,
repeat_penalty: 1.1,
seed: 0
}
},
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: ''
}
};
this.DEFAULT_FORMAT = 'markdown';
this.DEFAULT_APP_SIZE = {
width: 400,
height: 500
};
this.ollamaModels = [];
this.OLLAMA_RECOMMEND_MODELS = [
'llama2',
'llama2:13b',
'llama2:70b',
'mistral',
'mixtral',
'gemma:2b',
'gemma:7b',
'qwen:14b',
'qwen:72b',
'phi3:mini',
'phi3:small',
'phi3:medium',
'yi:34b',
'vicuna:13b',
'vicuna:33b',
'codellama',
'wizardcoder',
'nous-hermes2',
'neural-chat',
'openchat',
'dolphin-mixtral',
'starling-lm'
];
this.OLLAMA_PARAMS = {
temperature: { default: 0.8, min: 0, max: 2, step: 0.1 },
top_p: { default: 0.9, min: 0, max: 1, step: 0.05 },
top_k: { default: 40, min: 1, max: 100, step: 1 },
num_ctx: { default: 4096, min: 512, max: 8192, step: 512 },
repeat_penalty: { default: 1.1, min: 0.5, max: 2, step: 0.1 },
seed: { default: 0, min: 0, max: 1000000, step: 1 }
};
// 添加API转发配置,默认关闭
this.DEFAULT_API_PROXY = false;
this.DEFAULT_API_PROXY_DOMAIN = 'https://nakoruru.h7ml.cn';
// 添加交互式配置选项
this.FEATURES = {
'1': { id: 'autoExtract', name: '自动提取文章内容', default: true },
'2': { id: 'apiProxy', name: 'API转发服务', default: false },
'3': { id: 'ollamaIntegration', name: 'Ollama本地模型集成', default: true },
'4': { id: 'customStyleMode', name: '自定义样式模式', default: false },
'5': { id: 'advancedModelParams', name: '高级模型参数设置', default: false },
'6': { id: 'showThinking', name: '显示思考过程', default: true }
};
// 特性配置状态
this.featureStates = GM_getValue('featureStates', {
autoExtract: true,
apiProxy: false,
ollamaIntegration: true,
customStyleMode: false,
advancedModelParams: false,
showThinking: true
});
}
getConfigs() {
return GM_getValue('apiConfigs', this.DEFAULT_CONFIGS);
}
getApiService() {
return GM_getValue('apiService', this.DEFAULT_API_SERVICE);
}
getOutputFormat() {
return GM_getValue('outputFormat', this.DEFAULT_FORMAT);
}
getConfigCollapsed() {
return GM_getValue('configCollapsed', false);
}
getAppMinimized() {
return GM_getValue('appMinimized', false);
}
getAppPosition() {
return GM_getValue('appPosition', null);
}
getIconPosition() {
return GM_getValue('iconPosition', null);
}
getAppSize() {
return GM_getValue('appSize', this.DEFAULT_APP_SIZE);
}
setConfigs(configs) {
GM_setValue('apiConfigs', configs);
}
setApiService(service) {
GM_setValue('apiService', service);
}
setOutputFormat(format) {
GM_setValue('outputFormat', format);
}
setConfigCollapsed(collapsed) {
GM_setValue('configCollapsed', collapsed);
}
setAppMinimized(minimized) {
GM_setValue('appMinimized', minimized);
}
setAppPosition(position) {
GM_setValue('appPosition', position);
}
setIconPosition(position) {
GM_setValue('iconPosition', position);
}
setAppSize(size) {
GM_setValue('appSize', size);
}
// 添加获取和设置API转发配置的方法
getApiProxyEnabled() {
return GM_getValue('apiProxyEnabled', this.DEFAULT_API_PROXY);
}
getApiProxyDomain() {
return GM_getValue('apiProxyDomain', this.DEFAULT_API_PROXY_DOMAIN);
}
setApiProxyEnabled(enabled) {
GM_setValue('apiProxyEnabled', enabled);
}
setApiProxyDomain(domain) {
GM_setValue('apiProxyDomain', domain);
}
getFeatureStates() {
return this.featureStates;
}
isFeatureEnabled(featureId) {
return this.featureStates[featureId] === true;
}
setFeatureStates(states) {
this.featureStates = { ...this.featureStates, ...states };
GM_setValue('featureStates', this.featureStates);
}
showFeaturePrompt() {
// 使用对话框而不是prompt
return false;
}
// 添加获取和设置Ollama模型列表的方法
getOllamaModels() {
return this.ollamaModels;
}
setOllamaModels(models) {
this.ollamaModels = models;
}
// 添加获取和设置Ollama参数的方法
getOllamaParams(modelName) {
const configs = this.getConfigs();
if (configs.ollama && configs.ollama.params) {
return configs.ollama.params;
}
return {
temperature: 0.8,
top_p: 0.9,
top_k: 40,
num_ctx: 4096,
repeat_penalty: 1.1,
seed: 0
};
}
setOllamaParams(params) {
const configs = this.getConfigs();
if (!configs.ollama) {
configs.ollama = this.DEFAULT_CONFIGS.ollama;
}
configs.ollama.params = { ...configs.ollama.params, ...params };
this.setConfigs(configs);
}
}
// UI管理类
class UIManager {
constructor(configManager) {
this.configManager = configManager;
this.app = null;
this.iconElement = null;
this.elements = {};
this.isDragging = false;
this.isIconDragging = false;
this.isMaximized = false;
this.previousSize = {};
this.apiService = null; // 将在 init 中初始化
}
async init() {
try {
console.log('开始初始化UI管理器');
this.apiService = new APIService(this.configManager);
await this.loadLibraries();
console.log('库加载完成');
this.createApp();
console.log('应用创建完成');
this.createIcon();
console.log('图标创建完成');
this.bindEvents();
console.log('事件绑定完成');
this.restoreState();
console.log('状态恢复完成');
this.applyFeatureStates();
console.log('特性状态应用完成');
// 如果当前服务是 Ollama,并且启用了Ollama集成特性,尝试获取模型列表
if (this.configManager.getApiService() === 'ollama' &&
this.configManager.isFeatureEnabled('ollamaIntegration')) {
this.fetchOllamaModels();
}
console.log('UI管理器初始化完成');
} catch (error) {
console.error('UI管理器初始化失败:', error);
alert('界面初始化失败,请刷新页面重试。错误信息: ' + error.message);
}
}
// 应用特性状态到UI界面
applyFeatureStates() {
const featureStates = this.configManager.getFeatureStates();
// 处理自定义样式模式
if (featureStates.customStyleMode) {
document.body.classList.add('summary-custom-style');
this.app.classList.add('custom-style-enabled');
} else {
document.body.classList.remove('summary-custom-style');
this.app.classList.remove('custom-style-enabled');
}
// 处理API转发设置显示
if (featureStates.apiProxy) {
if (this.elements.proxySettingsContainer) {
this.elements.proxySettingsContainer.style.display = 'block';
}
} else {
if (this.elements.proxySettingsContainer) {
this.elements.proxySettingsContainer.style.display = 'none';
}
}
// 处理显示思考过程开关
if (this.elements.featureCheckboxes && this.elements.featureCheckboxes.showThinking) {
this.elements.featureCheckboxes.showThinking.checked = featureStates.showThinking === true;
}
// 处理Ollama集成
if (featureStates.ollamaIntegration) {
const ollamaOption = this.elements.apiService.querySelector('option[value="ollama"]');
if (ollamaOption) {
ollamaOption.style.display = 'block';
}
} else {
const ollamaOption = this.elements.apiService.querySelector('option[value="ollama"]');
if (ollamaOption) {
ollamaOption.style.display = 'none';
// 如果当前选中的是Ollama,切换到其他服务
if (this.elements.apiService.value === 'ollama') {
this.elements.apiService.value = 'gptgod';
this.handleApiServiceChange();
}
}
}
// 处理高级模型参数设置
if (featureStates.advancedModelParams && this.elements.ollamaParamsContainer) {
this.elements.ollamaParamsContainer.style.display = 'block';
this.updateModelParamsUI();
} else if (this.elements.ollamaParamsContainer) {
this.elements.ollamaParamsContainer.style.display = 'none';
}
}
async loadLibraries() {
// 添加基础样式
GM_addStyle(`
/* 基础样式 */
#article-summary-app {
position: fixed;
top: 20px;
right: 20px;
width: 400px;
max-height: 80vh;
min-width: 320px;
min-height: 300px;
background: rgba(255, 255, 255, 0.98);
border-radius: 12px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.08);
z-index: 999999;
display: flex;
flex-direction: column;
resize: both;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', sans-serif;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(0, 0, 0, 0.06);
transition: all 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
}
#article-summary-icon {
position: fixed;
bottom: 20px;
right: 20px;
width: 48px;
height: 48px;
background: #007AFF;
border-radius: 50%;
display: none; /* 默认隐藏图标 */
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 122, 255, 0.3);
z-index: 999999;
color: white;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
#article-summary-icon:hover {
transform: scale(1.05);
box-shadow: 0 6px 16px rgba(0, 122, 255, 0.4);
}
#article-summary-icon:active {
transform: scale(0.98);
}
#summary-header {
padding: 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
display: flex;
justify-content: space-between;
align-items: center;
cursor: move;
-webkit-app-region: drag;
user-select: none;
}
#summary-header h3 {
margin: 0;
font-size: 16px;
font-weight: 500;
color: #1D1D1F;
}
#summary-header-actions {
display: flex;
gap: 12px;
-webkit-app-region: no-drag;
}
.header-btn {
background: none;
border: none;
padding: 6px;
cursor: pointer;
color: #8E8E93;
border-radius: 6px;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.header-btn:hover {
background: rgba(0, 0, 0, 0.05);
color: #1D1D1F;
}
.header-btn:active {
transform: scale(0.95);
}
#summary-body {
padding: 20px;
overflow-y: auto;
flex: 1;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
margin-bottom: 6px;
color: #6E6E73;
font-size: 13px;
font-weight: 500;
}
.form-input, .form-select {
width: 100%;
padding: 10px 12px;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 8px;
font-size: 14px;
background-color: rgba(0, 0, 0, 0.02);
color: #1D1D1F;
transition: all 0.2s ease;
-webkit-appearance: none;
appearance: none;
}
.form-input:focus, .form-select:focus {
outline: none;
border-color: #007AFF;
box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.15);
background-color: #fff;
}
.form-select {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%238E8E93' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 16px;
padding-right: 36px;
}
#configPanel {
margin-top: 12px;
padding: 16px;
background: rgba(0, 0, 0, 0.02);
border-radius: 10px;
border: 1px solid rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
#configPanel.collapsed {
display: none;
}
#formatOptions {
display: flex;
gap: 8px;
}
.format-btn {
padding: 8px 12px;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 8px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s ease;
background: rgba(255, 255, 255, 0.8);
color: #1D1D1F;
}
.format-btn:hover {
background: rgba(0, 0, 0, 0.05);
}
.format-btn.active {
background: #007AFF;
color: white;
border-color: #007AFF;
font-weight: 500;
}
#generateBtn {
width: 100%;
padding: 14px;
background: #007AFF;
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 15px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 122, 255, 0.3);
}
#generateBtn:hover {
background: #0071E3;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 122, 255, 0.4);
}
#generateBtn:active {
transform: translateY(1px);
box-shadow: 0 1px 4px rgba(0, 122, 255, 0.3);
}
#generateBtn:disabled {
background: #A2A2A7;
cursor: not-allowed;
box-shadow: none;
transform: none;
}
#summaryResult {
margin-top: 20px;
display: none;
flex-direction: column;
height: 100%;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
#summaryHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
#summaryHeader h4 {
margin: 0;
color: #1D1D1F;
font-size: 15px;
font-weight: 500;
}
.action-btn {
background: none;
border: none;
padding: 6px 10px;
cursor: pointer;
color: #007AFF;
display: flex;
align-items: center;
gap: 6px;
border-radius: 6px;
transition: all 0.2s ease;
font-size: 13px;
font-weight: 500;
}
.action-btn:hover {
background: rgba(0, 122, 255, 0.1);
}
.action-btn:active {
transform: scale(0.95);
}
#loadingIndicator {
display: none;
text-align: center;
padding: 30px;
animation: fadeIn 0.3s ease;
}
.spinner {
width: 36px;
height: 36px;
margin: 0 auto 16px;
border: 3px solid rgba(0, 0, 0, 0.05);
border-top: 3px solid #007AFF;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.app-minimized {
display: none;
}
.icon {
width: 18px;
height: 18px;
}
.toggle-icon {
transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.markdown-body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', sans-serif;
line-height: 1.6;
color: #1D1D1F;
flex: 1;
display: flex;
flex-direction: column;
}
.markdown-body h1 { font-size: 1.5rem; margin: 1.2rem 0; font-weight: 600; }
.markdown-body h2 { font-size: 1.25rem; margin: 1.1rem 0; font-weight: 600; }
.markdown-body h3 { font-size: 1.1rem; margin: 1rem 0; font-weight: 600; }
.markdown-body p { margin: 0.8rem 0; }
.markdown-body code {
background: rgba(0, 0, 0, 0.04);
padding: 0.2em 0.4em;
border-radius: 4px;
font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.9em;
}
.markdown-body pre {
background: rgba(0, 0, 0, 0.04);
padding: 1rem;
border-radius: 8px;
overflow-x: auto;
}
#modelSelect {
width: 100%;
padding: 10px 12px;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 8px;
font-size: 14px;
background-color: rgba(0, 0, 0, 0.02);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%238E8E93' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 16px;
padding-right: 36px;
-webkit-appearance: none;
appearance: none;
transition: all 0.2s ease;
}
#modelSelect:focus {
outline: none;
border-color: #007AFF;
box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.15);
background-color: #fff;
}
#modelName {
display: none;
}
.ollama-service #modelSelect {
display: block;
}
.ollama-service #modelName {
display: none;
}
.non-ollama-service #modelSelect {
display: none;
}
.non-ollama-service #modelName {
display: block;
}
.content-textarea {
width: 100%;
height: 100%;
min-height: 200px;
padding: 14px;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 10px;
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', sans-serif;
font-size: 14px;
line-height: 1.6;
resize: vertical;
flex: 1;
box-sizing: border-box;
background-color: rgba(255, 255, 255, 0.8);
color: #1D1D1F;
transition: all 0.2s ease;
}
.content-textarea:focus {
outline: none;
border-color: #007AFF;
box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.15);
background-color: #fff;
}
.resize-handle {
position: absolute;
bottom: 0;
right: 0;
width: 16px;
height: 16px;
cursor: nwse-resize;
background: linear-gradient(135deg, transparent 50%, rgba(0, 0, 0, 0.1) 50%, rgba(0, 0, 0, 0.1) 100%);
border-radius: 0 0 10px 0;
transition: opacity 0.2s ease;
opacity: 0.5;
}
.resize-handle:hover {
opacity: 1;
}
.proxy-settings {
margin-top: 12px;
}
.checkbox-container {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.form-checkbox {
margin-right: 10px;
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 4px;
background-color: white;
cursor: pointer;
position: relative;
transition: all 0.2s ease;
}
.form-checkbox:checked {
background-color: #007AFF;
border-color: #007AFF;
}
.form-checkbox:checked::after {
content: '';
position: absolute;
left: 6px;
top: 2px;
width: 4px;
height: 9px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.form-checkbox:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.15);
}
#configToggle {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
background: rgba(0, 0, 0, 0.03);
border-radius: 8px;
cursor: pointer;
margin-bottom: 12px;
transition: all 0.2s ease;
user-select: none;
}
#configToggle:hover {
background: rgba(0, 0, 0, 0.05);
}
#configToggle span {
font-weight: 500;
font-size: 14px;
color: #1D1D1F;
}
/* 对话框样式 */
.dialog {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: none;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000000;
}
.dialog-content {
background-color: white;
border-radius: 12px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
width: 90%;
max-width: 400px;
max-height: 90vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.dialog-header {
padding: 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.dialog-header h3 {
margin: 0;
font-size: 18px;
font-weight: 500;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
color: #8E8E93;
cursor: pointer;
padding: 0;
}
.dialog-body {
padding: 20px;
overflow-y: auto;
max-height: 60vh;
}
.dialog-footer {
padding: 16px;
border-top: 1px solid rgba(0, 0, 0, 0.1);
display: flex;
justify-content: flex-end;
}
.btn {
padding: 10px 16px;
border-radius: 8px;
border: none;
background-color: #007AFF;
color: white;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn:hover {
background-color: #0062CC;
}
.feature-options {
display: flex;
flex-direction: column;
gap: 12px;
}
`);
console.log('Markdown 渲染库加载完成');
// 加载第三方库
try {
// 加载 marked 库
if (typeof marked === 'undefined') {
const markedCode = GM_getResourceText('marked');
eval(markedCode);
console.log('marked 库加载成功');
}
// 加载 highlight.js
if (typeof hljs === 'undefined') {
const highlightCode = GM_getResourceText('highlight');
eval(highlightCode);
console.log('highlight.js 库加载成功');
}
// 添加高亮样式
const highlightCSS = GM_getResourceText('highlightStyle');
GM_addStyle(highlightCSS);
console.log('highlight.js CSS 样式加载成功');
} catch (error) {
console.error('加载第三方库失败:', error);
}
}
createApp() {
this.app = document.createElement('div');
this.app.id = 'article-summary-app';
this.app.innerHTML = this.getAppHTML();
document.body.appendChild(this.app);
this.initializeElements();
}
createIcon() {
this.iconElement = document.createElement('div');
this.iconElement.id = 'article-summary-icon';
this.iconElement.innerHTML = this.getIconHTML();
document.body.appendChild(this.iconElement);
// 不需要在这里设置display,因为CSS已经默认设置为none
}
initializeElements() {
// 使用jQuery获取元素
this.elements = {
apiService: $('#apiService')[0],
apiUrl: $('#apiUrl')[0],
apiUrlContainer: $('#apiUrlContainer')[0],
apiKey: $('#apiKey')[0],
apiKeyContainer: $('#apiKeyContainer')[0],
modelName: $('#modelName')[0],
modelSelect: $('#modelSelect')[0],
ollamaParamsContainer: $('#ollamaParamsContainer')[0],
generateBtn: $('#generateBtn')[0],
summaryResult: $('#summaryResult')[0],
summaryContent: $('#summaryContent')[0],
loadingIndicator: $('#loadingIndicator')[0],
configToggle: $('#configToggle')[0],
configPanel: $('#configPanel')[0],
toggleMaxBtn: $('#toggleMaxBtn')[0],
toggleMinBtn: $('#toggleMinBtn')[0],
formatBtns: $('.format-btn'),
copyBtn: $('#copyBtn')[0],
apiProxyEnabled: $('#apiProxyEnabled')[0],
apiProxyDomain: $('#apiProxyDomain')[0],
proxyDomainContainer: $('#proxyDomainContainer')[0],
proxySettingsContainer: $('#proxySettingsContainer')[0],
openConfigBtn: $('#openConfigBtn')[0],
featureDialog: $('#featureDialog')[0],
closeFeatureDialog: $('#closeFeatureDialog')[0],
saveFeatures: $('#saveFeatures')[0],
featureSettings: $('#featureSettings')[0],
featureToggles: $('.feature-toggle'),
dialogFeatureCheckboxes: {
autoExtract: $('#dialog_autoExtract')[0],
apiProxy: $('#dialog_apiProxy')[0],
ollamaIntegration: $('#dialog_ollamaIntegration')[0],
customStyleMode: $('#dialog_customStyleMode')[0],
advancedModelParams: $('#dialog_advancedModelParams')[0],
showThinking: $('#dialog_showThinking')[0]
},
featureCheckboxes: {
autoExtract: $('#feature_autoExtract')[0],
apiProxy: $('#feature_apiProxy')[0],
ollamaIntegration: $('#feature_ollamaIntegration')[0],
customStyleMode: $('#feature_customStyleMode')[0],
advancedModelParams: $('#feature_advancedModelParams')[0],
showThinking: $('#feature_showThinking')[0]
},
viewBtns: $('.view-btn'),
summaryTextarea: $('#summaryTextarea')[0],
summaryPreview: $('#summaryPreview')[0],
};
// 检查关键元素是否存在
const missingElements = [];
if (!this.elements.apiService) missingElements.push('apiService');
if (!this.elements.toggleMinBtn) missingElements.push('toggleMinBtn');
if (!this.elements.toggleMaxBtn) missingElements.push('toggleMaxBtn');
if (!$('#summary-header').length) missingElements.push('summary-header');
if (missingElements.length > 0) {
console.error('初始化元素检查: 以下元素未找到:', missingElements.join(', '));
} else {
console.log('初始化元素完成: 所有关键元素都已找到');
}
}
bindEvents() {
this.bindAppEvents();
this.bindIconEvents();
this.bindConfigEvents();
this.bindResizeEvents();
}
bindAppEvents() {
// 使用jQuery绑定事件,简化代码,增加安全性
const self = this;
// 绑定标题栏拖拽事件
$('#summary-header').on('mousedown', function (e) { self.dragStart(e); });
$(document).on('mousemove', function (e) { self.drag(e); });
$(document).on('mouseup', function (e) { self.dragEnd(e); });
// 按钮事件
$('#toggleMaxBtn').on('click', function () { self.toggleMaximize(); });
$('#toggleMinBtn').on('click', function () { self.toggleMinimize(); });
$('#copyBtn').on('click', function () { self.copyContent(); });
// 设置按钮事件
$('#openConfigBtn').on('click', function () {
// 打开特性设置对话框并传递true,表示来自设置按钮的点击
self.openFeatureDialog(true);
});
$('#closeFeatureDialog').on('click', function () { self.closeFeatureDialog(); });
$('#saveFeatures').on('click', function () { self.saveFeatureSettings(); });
// 记录绑定状态
console.log('应用事件绑定完成,绑定元素存在状态:', {
'summary-header': $('#summary-header').length > 0,
'toggleMaxBtn': $('#toggleMaxBtn').length > 0,
'toggleMinBtn': $('#toggleMinBtn').length > 0
});
}
bindIconEvents() {
const self = this;
// 图标拖拽和点击事件
$('#article-summary-icon').on('mousedown', function (e) { self.iconDragStart(e); });
$('#article-summary-icon').on('click', function () { self.toggleApp(); });
$(document).on('mousemove', function (e) { self.iconDrag(e); });
$(document).on('mouseup', function (e) { self.iconDragEnd(e); });
console.log('图标事件绑定完成,绑定状态:', {
'article-summary-icon': $('#article-summary-icon').length > 0
});
}
bindConfigEvents() {
const self = this;
// API服务选择
$('#apiService').on('change', function () { self.handleApiServiceChange(); });
// 配置输入
$('#apiUrl').on('change', function () { self.handleConfigChange(); });
$('#apiKey').on('change', function () { self.handleConfigChange(); });
$('#modelName').on('change', function () { self.handleConfigChange(); });
$('#modelSelect').on('change', function () { self.handleModelSelectChange(); });
// 配置面板
$('#configToggle').on('click', function () { self.toggleConfig(); });
// 格式按钮
$('.format-btn').on('click', function (e) { self.handleFormatChange(e); });
// 代理配置
$('#apiProxyEnabled').on('change', function () { self.handleProxyConfigChange(); });
$('#apiProxyDomain').on('change', function () { self.handleProxyConfigChange(); });
// Ollama参数滑块
$('.param-slider').on('input change', function (e) { self.handleParamChange(e); });
// 功能设置复选框
$('.feature-toggle').on('change', function (e) { self.handleFeatureToggle(e); });
// 视图切换按钮
$('.view-btn').on('click', function (e) { self.handleViewChange(e); });
console.log('配置事件绑定完成');
}
bindResizeEvents() {
const self = this;
$('.resize-handle').on('mousedown', function (e) {
e.preventDefault();
e.stopPropagation();
self.startResize(e);
});
console.log('调整大小事件绑定完成,绑定状态:', {
'resize-handle': $('.resize-handle').length > 0
});
}
startResize(e) {
e.preventDefault();
e.stopPropagation();
// 初始位置
this.isResizing = true;
this.initialWidth = this.app.offsetWidth;
this.initialHeight = this.app.offsetHeight;
this.initialX = e.clientX;
this.initialY = e.clientY;
// 创建绑定的处理函数
this.resizeHandler = this.resize.bind(this);
this.stopResizeHandler = this.stopResize.bind(this);
// 添加临时事件监听器
document.addEventListener('mousemove', this.resizeHandler);
document.addEventListener('mouseup', this.stopResizeHandler);
}
resize(e) {
if (!this.isResizing) return;
// 计算新尺寸,设置最小值限制
const minWidth = 320;
const minHeight = 300;
const newWidth = Math.max(minWidth, this.initialWidth + (e.clientX - this.initialX));
const newHeight = Math.max(minHeight, this.initialHeight + (e.clientY - this.initialY));
// 应用新尺寸
this.app.style.width = newWidth + 'px';
this.app.style.height = newHeight + 'px';
// 保存尺寸到配置
this.saveAppSize(newWidth, newHeight);
}
saveAppSize(width, height) {
// 保存应用尺寸到配置
const appSize = { width, height };
this.configManager.setAppSize(appSize);
}
stopResize() {
this.isResizing = false;
// 移除临时事件监听器
document.removeEventListener('mousemove', this.resizeHandler);
document.removeEventListener('mouseup', this.stopResizeHandler);
}
restoreState() {
try {
const configs = this.configManager.getConfigs();
const apiService = this.configManager.getApiService();
// 确保服务配置存在
if (!configs[apiService]) {
configs[apiService] = {
url: apiService === 'ollama' ? 'http:/160.202.244.103:11434/api/chat' : '',
model: apiService === 'ollama' ? 'deepseek-r1:7b' : '',
key: ''
};
// 保存新创建的配置
this.configManager.setConfigs(configs);
}
const currentConfig = configs[apiService];
// 设置表单值
$('#apiKey').val(currentConfig.key || '');
$('#modelName').val(currentConfig.model || '');
$('#apiUrl').val(currentConfig.url || '');
// 显示/隐藏 API Key 输入框
$('#apiKeyContainer').css('display', apiService === 'ollama' ? 'none' : 'block');
// 显示/隐藏 Ollama 模型参数配置
if ($('#ollamaParamsContainer').length) {
$('#ollamaParamsContainer').css('display', (apiService === 'ollama' &&
this.configManager.isFeatureEnabled('advancedModelParams')) ? 'block' : 'none');
}
// 根据服务类型添加类名
if (apiService === 'ollama') {
$(this.app).addClass('ollama-service').removeClass('non-ollama-service');
// 尝试获取 Ollama 模型列表,但需要检查特性是否启用
if (this.configManager.isFeatureEnabled('ollamaIntegration')) {
this.fetchOllamaModels();
} else {
// 如果特性未启用,仍然需要设置模型选择
const models = this.configManager.getOllamaModels();
if (models && models.length > 0) {
this.updateModelSelectOptions(models);
} else {
this.loadRecommendedOllamaModels();
}
}
} else {
$(this.app).removeClass('ollama-service').addClass('non-ollama-service');
}
$('#apiService').val(apiService);
const format = this.configManager.getOutputFormat();
$('.format-btn').each(function () {
if ($(this).data('format') === format) {
$(this).addClass('active');
} else {
$(this).removeClass('active');
}
});
const configCollapsed = this.configManager.getConfigCollapsed();
if (configCollapsed) {
$('#configPanel').addClass('collapsed');
$('#configToggle .toggle-icon').css('transform', 'rotate(-90deg)');
}
// 恢复最小化状态 - 使用jQuery操作DOM
const appMinimized = this.configManager.getAppMinimized();
console.log('恢复状态: 最小化状态 =', appMinimized);
if (appMinimized) {
// 使用jQuery设置显示状态
$('#article-summary-app').hide();
$('#article-summary-icon').css('display', 'flex');
console.log('已恢复最小化状态');
} else {
// 使用jQuery设置显示状态
$('#article-summary-app').css('display', 'flex');
$('#article-summary-icon').hide();
console.log('已恢复正常状态');
}
// 恢复位置
const appPosition = this.configManager.getAppPosition();
if (appPosition && this.app) {
$(this.app).css({
left: appPosition.x + 'px',
top: appPosition.y + 'px',
right: 'auto',
bottom: 'auto'
});
}
// 恢复尺寸
const appSize = this.configManager.getAppSize();
if (appSize && this.app) {
$(this.app).css({
width: appSize.width + 'px',
height: appSize.height + 'px'
});
}
const iconPosition = this.configManager.getIconPosition();
if (iconPosition && this.iconElement) {
$(this.iconElement).css({
left: iconPosition.x + 'px',
top: iconPosition.y + 'px',
right: 'auto',
bottom: 'auto'
});
}
// 恢复转发配置
const proxyEnabled = this.configManager.getApiProxyEnabled();
const proxyDomain = this.configManager.getApiProxyDomain();
$('#apiProxyEnabled').prop('checked', proxyEnabled);
$('#apiProxyDomain').val(proxyDomain);
$('#proxyDomainContainer').css('display', proxyEnabled ? 'block' : 'none');
// 应用特性状态配置
this.applyFeatureStates();
// 更新功能设置复选框状态
this.updateFeatureCheckboxes();
console.log('状态恢复完成');
} catch (error) {
console.error('恢复状态过程中出错:', error);
}
}
// 拖拽相关方法
dragStart(e) {
this.isDragging = true;
this.initialX = e.clientX - this.app.offsetLeft;
this.initialY = e.clientY - this.app.offsetTop;
}
drag(e) {
if (this.isDragging) {
e.preventDefault();
const currentX = Math.max(0, Math.min(
e.clientX - this.initialX,
window.innerWidth - this.app.offsetWidth
));
const currentY = Math.max(0, Math.min(
e.clientY - this.initialY,
window.innerHeight - this.app.offsetHeight
));
this.app.style.left = currentX + 'px';
this.app.style.top = currentY + 'px';
}
}
dragEnd() {
if (this.isDragging) {
this.isDragging = false;
const position = {
x: parseInt(this.app.style.left),
y: parseInt(this.app.style.top)
};
this.configManager.setAppPosition(position);
}
}
// 图标拖拽相关方法
iconDragStart(e) {
this.isIconDragging = true;
this.iconInitialX = e.clientX - this.iconElement.offsetLeft;
this.iconInitialY = e.clientY - this.iconElement.offsetTop;
this.iconElement.style.cursor = 'grabbing';
}
iconDrag(e) {
if (this.isIconDragging) {
e.preventDefault();
const currentX = Math.max(0, Math.min(
e.clientX - this.iconInitialX,
window.innerWidth - this.iconElement.offsetWidth
));
const currentY = Math.max(0, Math.min(
e.clientY - this.iconInitialY,
window.innerHeight - this.iconElement.offsetHeight
));
this.iconElement.style.left = currentX + 'px';
this.iconElement.style.top = currentY + 'px';
this.iconElement.style.right = 'auto';
}
}
iconDragEnd() {
if (this.isIconDragging) {
this.isIconDragging = false;
this.iconElement.style.cursor = 'pointer';
const position = {
x: parseInt(this.iconElement.style.left),
y: parseInt(this.iconElement.style.top)
};
this.configManager.setIconPosition(position);
}
}
// 配置相关方法
handleApiServiceChange() {
const service = this.elements.apiService.value;
const configs = this.configManager.getConfigs();
// 确保服务配置存在,如果不存在则创建默认配置
if (!configs[service]) {
configs[service] = {
url: service === 'ollama' ? 'http:/160.202.244.103:11434/api/chat' : '',
model: service === 'ollama' ? 'deepseek-r1:7b' : '',
key: service === 'ollama' ? '' : '必填' // Ollama不需要API Key
};
// 保存新创建的配置
this.configManager.setConfigs(configs);
}
const currentConfig = configs[service];
// 设置表单值
this.elements.apiKey.value = currentConfig.key || '';
this.elements.modelName.value = currentConfig.model || '';
this.elements.apiUrl.value = currentConfig.url || '';
// 显示/隐藏 API Key 输入框 - Ollama不需要API Key
this.elements.apiKeyContainer.style.display = service === 'ollama' ? 'none' : 'block';
// 显示/隐藏 Ollama 模型参数配置
if (this.elements.ollamaParamsContainer) {
this.elements.ollamaParamsContainer.style.display = service === 'ollama' ?
(this.configManager.isFeatureEnabled('advancedModelParams') ? 'block' : 'none') : 'none';
}
// 根据服务类型添加类名
if (service === 'ollama') {
this.app.classList.add('ollama-service');
this.app.classList.remove('non-ollama-service');
// 尝试获取 Ollama 模型列表,但需要检查特性是否启用
if (this.configManager.isFeatureEnabled('ollamaIntegration')) {
this.fetchOllamaModels();
} else {
// 如果特性未启用,仍然需要设置模型选择
const models = this.configManager.getOllamaModels();
if (models && models.length > 0) {
this.updateModelSelectOptions(models);
} else {
this.loadRecommendedOllamaModels();
}
}
} else {
this.app.classList.remove('ollama-service');
this.app.classList.add('non-ollama-service');
}
this.configManager.setApiService(service);
}
handleConfigChange() {
const service = this.elements.apiService.value;
const configs = this.configManager.getConfigs();
// 确保服务配置存在
if (!configs[service]) {
configs[service] = {
url: service === 'ollama' ? 'http:/160.202.244.103:11434/api/chat' : '',
model: service === 'ollama' ? 'deepseek-r1:7b' : '',
key: ''
};
}
// 获取当前表单值
const apiKey = this.elements.apiKey.value || '';
const modelName = service === 'ollama' ?
(this.elements.modelSelect.value || 'llama2') :
(this.elements.modelName.value || '');
const apiUrl = this.elements.apiUrl.value ||
(service === 'ollama' ? 'http:/160.202.244.103:11434/api/chat' : '');
// 更新配置
configs[service] = {
...configs[service],
key: apiKey,
model: modelName,
url: apiUrl
};
// 保存配置
this.configManager.setConfigs(configs);
}
toggleConfig() {
this.elements.configPanel.classList.toggle('collapsed');
const isCollapsed = this.elements.configPanel.classList.contains('collapsed');
const toggleIcon = this.elements.configToggle.querySelector('.toggle-icon');
toggleIcon.style.transform = isCollapsed ? 'rotate(-90deg)' : '';
this.configManager.setConfigCollapsed(isCollapsed);
}
handleFormatChange(e) {
// 修复 forEach 不是函数的错误,改用 jQuery 的 each 方法迭代
$(this.elements.formatBtns).each(function () {
$(this).removeClass('active');
});
$(e.target).addClass('active');
this.configManager.setOutputFormat(e.target.dataset.format);
}
handleModelSelectChange() {
// 获取当前选中的值
const selectValue = this.elements.modelSelect.value;
// 如果选择了空值,则允许手动输入
if (!selectValue) {
const customModel = prompt('请输入Ollama模型名称:\n\n提示:您可以访问 https://freeollama.oneplus1.top/ 获取免费的Ollama服务', '');
if (customModel && customModel.trim()) {
// 检查是否已存在该选项
const existingOption = Array.from(this.elements.modelSelect.options).find(opt => opt.value === customModel);
if (!existingOption) {
// 添加新选项
const newOption = document.createElement('option');
newOption.value = customModel;
newOption.textContent = customModel + ' (自定义)';
this.elements.modelSelect.appendChild(newOption);
}
// 设置为选中状态
this.elements.modelSelect.value = customModel;
this.elements.modelName.value = customModel;
} else {
// 如果取消或输入为空,恢复之前的选择
const configs = this.configManager.getConfigs();
const currentModel = configs.ollama.model || 'llama2';
this.elements.modelSelect.value = currentModel;
this.elements.modelName.value = currentModel;
return;
}
} else {
// 正常选择了一个值
this.elements.modelName.value = selectValue;
}
// 触发配置更新
this.handleConfigChange();
// 如果启用了高级参数设置,显示相应参数配置区域
if (this.configManager.isFeatureEnabled('advancedModelParams')) {
this.updateModelParamsUI();
}
}
// 更新模型参数UI
updateModelParamsUI() {
if (!this.elements.ollamaParamsContainer) return;
// 获取当前模型参数
const params = this.configManager.getOllamaParams();
// 更新UI上的参数值
Object.keys(this.configManager.OLLAMA_PARAMS).forEach(paramKey => {
const element = document.getElementById(`param_${paramKey}`);
if (element) {
element.value = params[paramKey] || this.configManager.OLLAMA_PARAMS[paramKey].default;
// 更新显示值
const displayElement = document.getElementById(`param_${paramKey}_value`);
if (displayElement) {
displayElement.textContent = element.value;
}
}
});
}
// 处理参数变更
handleParamChange(event) {
const paramName = event.target.id.replace('param_', '');
const paramValue = parseFloat(event.target.value);
// 更新显示值
const displayElement = document.getElementById(`param_${paramName}_value`);
if (displayElement) {
displayElement.textContent = paramValue;
}
// 更新配置
const params = this.configManager.getOllamaParams();
params[paramName] = paramValue;
this.configManager.setOllamaParams(params);
}
// UI状态相关方法
toggleMaximize() {
if (!this.isMaximized) {
this.previousSize = {
width: this.app.style.width,
height: this.app.style.height,
left: this.app.style.left,
top: this.app.style.top
};
this.app.style.width = '100%';
this.app.style.height = '100vh';
this.app.style.left = '0';
this.app.style.top = '0';
this.elements.toggleMaxBtn.innerHTML = this.getMaximizeIcon();
} else {
Object.assign(this.app.style, this.previousSize);
this.elements.toggleMaxBtn.innerHTML = this.getRestoreIcon();
}
this.isMaximized = !this.isMaximized;
}
toggleMinimize() {
try {
// 使用jQuery切换显示状态
$('#article-summary-app').hide();
$('#article-summary-icon').css('display', 'flex');
// 保存状态
this.configManager.setAppMinimized(true);
console.log('应用已最小化');
} catch (error) {
console.error('最小化过程中出错:', error);
}
}
toggleApp() {
try {
// 使用jQuery切换显示状态
$('#article-summary-app').css('display', 'flex');
$('#article-summary-icon').hide();
// 保存状态
this.configManager.setAppMinimized(false);
console.log('应用已恢复');
} catch (error) {
console.error('恢复应用过程中出错:', error);
}
}
// 工具方法
copyContent() {
const activeViewBtn = document.querySelector('.view-btn.active');
const viewMode = activeViewBtn ? activeViewBtn.dataset.view : 'markdown';
let textToCopy;
if (viewMode === 'markdown') {
textToCopy = this.elements.summaryTextarea.value;
} else {
// 从预览区域提取纯文本内容
textToCopy = this.elements.summaryTextarea.value;
}
navigator.clipboard.writeText(textToCopy).then(() => {
const originalHTML = this.elements.copyBtn.innerHTML;
this.elements.copyBtn.innerHTML = this.getCopiedIcon();
setTimeout(() => {
this.elements.copyBtn.innerHTML = originalHTML;
}, 2000);
}).catch(err => {
console.error('复制失败:', err);
alert('复制失败,请手动选择文本复制');
});
}
// HTML模板方法
getAppHTML() {
return `
<div id="summary-header">
<h3>文章总结助手</h3>
<div id="summary-header-actions">
<button id="openConfigBtn" class="header-btn" data-tooltip="功能设置">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></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="M8 18h8"></path>
</svg>
</button>
<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>
</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="ollama">Ollama</option>
<option value="gptgod">GPT God</option>
<option value="deepseek">DeepSeek</option>
<option value="custom">自定义</option>
</select>
</div>
<div id="apiUrlContainer" class="form-group">
<label class="form-label" for="apiUrl">API地址</label>
<input type="text" id="apiUrl" class="form-input" placeholder="http:/160.202.244.103:11434/api/chat">
</div>
<div class="form-group" id="apiKeyContainer">
<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>
<select id="modelSelect" class="form-select">
<option value="">-- 输入或选择模型 --</option>
<option value="llama2">llama2</option>
<option value="llama2:13b">llama2:13b</option>
<option value="llama2:70b">llama2:70b</option>
<option value="mistral">mistral</option>
<option value="mixtral">mixtral</option>
<option value="gemma:2b">gemma:2b</option>
<option value="gemma:7b">gemma:7b</option>
<option value="qwen:14b">qwen:14b</option>
<option value="qwen:72b">qwen:72b</option>
<option value="phi3:mini">phi3:mini</option>
<option value="phi3:small">phi3:small</option>
<option value="phi3:medium">phi3:medium</option>
<option value="yi:34b">yi:34b</option>
</select>
<input type="text" id="modelName" class="form-input" placeholder="模型名称">
<small class="form-tip">没有Ollama服务?<a href="https://freeollama.oneplus1.top/" target="_blank">点击这里</a>获取免费Ollama服务</small>
</div>
<!-- Ollama模型参数设置部分 -->
<div id="ollamaParamsContainer" class="form-group" style="display: none;">
<label class="form-label">Ollama 模型参数设置</label>
<div class="ollama-params">
<!-- 温度参数 -->
<div class="param-item">
<div class="param-header">
<label for="param_temperature">创造性 (<span id="param_temperature_value">0.8</span>)</label>
<div class="param-desc">值越高结果越有创造性,越低越保守</div>
</div>
<input type="range" id="param_temperature" class="param-slider" min="0" max="2" step="0.1" value="0.8">
</div>
<!-- Top P参数 -->
<div class="param-item">
<div class="param-header">
<label for="param_top_p">多样性 (<span id="param_top_p_value">0.9</span>)</label>
<div class="param-desc">控制输出的多样性</div>
</div>
<input type="range" id="param_top_p" class="param-slider" min="0" max="1" step="0.05" value="0.9">
</div>
<!-- Top K参数 -->
<div class="param-item">
<div class="param-header">
<label for="param_top_k">词汇范围 (<span id="param_top_k_value">40</span>)</label>
<div class="param-desc">控制生成内容的词汇范围</div>
</div>
<input type="range" id="param_top_k" class="param-slider" min="1" max="100" step="1" value="40">
</div>
<!-- Context长度参数 -->
<div class="param-item">
<div class="param-header">
<label for="param_num_ctx">上下文长度 (<span id="param_num_ctx_value">4096</span>)</label>
<div class="param-desc">控制模型可用的上下文窗口大小</div>
</div>
<input type="range" id="param_num_ctx" class="param-slider" min="512" max="8192" step="512" value="4096">
</div>
<!-- 重复惩罚参数 -->
<div class="param-item">
<div class="param-header">
<label for="param_repeat_penalty">重复惩罚 (<span id="param_repeat_penalty_value">1.1</span>)</label>
<div class="param-desc">控制模型避免重复内容的程度</div>
</div>
<input type="range" id="param_repeat_penalty" class="param-slider" min="0.5" max="2" step="0.1" value="1.1">
</div>
</div>
</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 class="form-group">
<label class="form-label">显示选项</label>
<div class="checkbox-container">
<input type="checkbox" id="feature_showThinking" class="feature-toggle form-checkbox">
<label for="feature_showThinking">显示思考过程</label>
</div>
</div>
<div id="proxySettingsContainer" class="form-group">
<label class="form-label">API转发设置</label>
<div class="proxy-settings">
<div class="checkbox-container">
<input type="checkbox" id="apiProxyEnabled" class="form-checkbox">
<label for="apiProxyEnabled">启用API转发</label>
</div>
<div id="proxyDomainContainer" class="form-group" style="display: none;">
<label class="form-label" for="apiProxyDomain">转发服务域名</label>
<input type="text" id="apiProxyDomain" class="form-input" placeholder="https://nakoruru.h7ml.cn">
</div>
</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">
<div id="viewOptions">
<span class="view-btn active" data-view="markdown">Markdown</span>
<span class="view-btn" data-view="preview">预览</span>
</div>
<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 id="summaryTextarea" class="content-textarea" placeholder="生成的总结将显示在这里..."></textarea>
<div id="summaryPreview" class="preview-area"></div>
</div>
</div>
<div id="loadingIndicator">
<div class="spinner"></div>
<p>正在生成总结,请稍候...</p>
</div>
<div class="resize-handle"></div>
</div>
<!-- 功能设置对话框 -->
<div id="featureDialog" class="dialog">
<div class="dialog-content">
<div class="dialog-header">
<h3>功能设置</h3>
<button id="closeFeatureDialog" class="close-btn">×</button>
</div>
<div class="dialog-body">
<div class="feature-options">
<div class="checkbox-container">
<input type="checkbox" id="dialog_autoExtract" class="feature-checkbox">
<label for="dialog_autoExtract">自动提取文章内容</label>
<div class="desc-text">启用后自动从页面提取文章内容,禁用则需要手动输入</div>
</div>
<div class="checkbox-container">
<input type="checkbox" id="dialog_apiProxy" class="feature-checkbox">
<label for="dialog_apiProxy">API转发服务</label>
<div class="desc-text">启用后可以使用转发服务连接被墙的API</div>
</div>
<div class="checkbox-container">
<input type="checkbox" id="dialog_ollamaIntegration" class="feature-checkbox">
<label for="dialog_ollamaIntegration">Ollama本地模型集成</label>
<div class="desc-text">启用后可以使用本地运行的Ollama模型</div>
</div>
<div class="checkbox-container">
<input type="checkbox" id="dialog_customStyleMode" class="feature-checkbox">
<label for="dialog_customStyleMode">自定义样式模式</label>
<div class="desc-text">启用后使用自定义样式主题</div>
</div>
<div class="checkbox-container">
<input type="checkbox" id="dialog_advancedModelParams" class="feature-checkbox">
<label for="dialog_advancedModelParams">高级模型参数设置</label>
<div class="desc-text">启用后可以调整模型参数(温度、top_p等)</div>
</div>
<div class="checkbox-container">
<input type="checkbox" id="dialog_showThinking" class="feature-checkbox">
<label for="dialog_showThinking">显示思考过程</label>
<div class="desc-text">启用后显示模型的思考过程(<think>标签内容)</div>
</div>
</div>
</div>
<div class="dialog-footer">
<button id="saveFeatures" class="btn">保存设置</button>
</div>
</div>
</div>
`;
}
getIconHTML() {
return `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 12h6m-6 4h6m2-10H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V8a2 2 0 00-2-2z"></path>
</svg>
`;
}
getMaximizeIcon() {
return `
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<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>
`;
}
getRestoreIcon() {
return `
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"></path>
</svg>
`;
}
getCopiedIcon() {
return `
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 6L9 17l-5-5"></path>
</svg>
已复制
`;
}
async fetchOllamaModels() {
return new Promise((resolve, reject) => {
const ollamaConfig = this.configManager.getConfigs().ollama;
if (!ollamaConfig || !ollamaConfig.url) {
console.error('Ollama API地址未配置');
resolve([]); // 返回空数组
return;
}
try {
// 从 API URL 中提取基础 URL
let baseUrl = ollamaConfig.url;
// 移除路径部分,只保留协议和主机部分
if (baseUrl.includes('/api/')) {
baseUrl = baseUrl.split('/api/')[0];
} else {
// 如果没有/api/,尝试提取协议和主机
const urlObj = new URL(baseUrl);
baseUrl = `${urlObj.protocol}//${urlObj.hostname}:${urlObj.port || 11434}`;
}
const modelsEndpoint = `${baseUrl}/api/tags`;
console.log('正在获取Ollama模型列表,请求地址:', modelsEndpoint);
const transformedEndpoint = this.transformUrl(modelsEndpoint);
GM_xmlhttpRequest({
method: 'GET',
url: transformedEndpoint,
headers: {
'Content-Type': 'application/json'
},
onload: (response) => {
try {
if (response.status >= 400) {
console.warn('获取 Ollama 模型列表失败:', response.statusText);
// 加载推荐模型作为后备方案
this.loadRecommendedOllamaModels();
resolve([]);
return;
}
const data = JSON.parse(response.responseText);
if (data.models && Array.isArray(data.models)) {
// 提取模型名称
const models = data.models.map(model => model.name);
console.log('成功获取Ollama模型列表:', models);
// 更新模型到配置
this.configManager.setOllamaModels(models);
// 更新下拉选项
this.updateModelSelectOptions(models);
resolve(models);
} else {
console.warn('Ollama API 返回的模型列表格式异常:', data);
// 加载推荐模型作为后备方案
this.loadRecommendedOllamaModels();
resolve([]);
}
} catch (error) {
console.error('解析 Ollama 模型列表失败:', error);
// 加载推荐模型作为后备方案
this.loadRecommendedOllamaModels();
resolve([]);
}
},
onerror: (error) => {
console.error('获取 Ollama 模型列表请求失败:', error);
// 加载推荐模型作为后备方案
this.loadRecommendedOllamaModels();
resolve([]);
}
});
} catch (error) {
console.error('构建Ollama API请求URL失败:', error);
// 加载推荐模型作为后备方案
this.loadRecommendedOllamaModels();
resolve([]);
}
});
}
// 使用获取到的模型列表更新下拉选项
updateModelSelectOptions(models) {
// 清空现有选项
this.elements.modelSelect.innerHTML = '';
// 添加默认选项,允许用户手动输入
const defaultOption = document.createElement('option');
defaultOption.value = "";
defaultOption.textContent = "-- 输入或选择模型 --";
this.elements.modelSelect.appendChild(defaultOption);
// 添加从服务器获取的模型选项
if (models && models.length > 0) {
models.forEach(model => {
const option = document.createElement('option');
option.value = model;
option.textContent = model;
this.elements.modelSelect.appendChild(option);
});
} else {
// 如果没有获取到模型,使用推荐模型列表
this.configManager.OLLAMA_RECOMMEND_MODELS.forEach(model => {
const option = document.createElement('option');
option.value = model;
option.textContent = model + ' (推荐)';
this.elements.modelSelect.appendChild(option);
});
}
// 设置当前选中的模型
const configs = this.configManager.getConfigs();
const currentModel = configs.ollama.model;
if (currentModel) {
// 检查当前模型是否在列表中
const existingOption = Array.from(this.elements.modelSelect.options).find(opt => opt.value === currentModel);
if (existingOption) {
this.elements.modelSelect.value = currentModel;
} else {
// 如果不在列表中,添加一个新选项
const newOption = document.createElement('option');
newOption.value = currentModel;
newOption.textContent = currentModel + ' (自定义)';
this.elements.modelSelect.appendChild(newOption);
this.elements.modelSelect.value = currentModel;
}
}
}
// 加载推荐的Ollama模型列表
loadRecommendedOllamaModels() {
// 清空现有选项
this.elements.modelSelect.innerHTML = '';
// 添加默认选项,允许用户手动输入
const defaultOption = document.createElement('option');
defaultOption.value = "";
defaultOption.textContent = "-- 输入或选择模型 --";
this.elements.modelSelect.appendChild(defaultOption);
// 添加推荐模型选项
this.configManager.OLLAMA_RECOMMEND_MODELS.forEach(model => {
const option = document.createElement('option');
option.value = model;
option.textContent = model + ' (推荐)';
this.elements.modelSelect.appendChild(option);
});
// 设置当前选中的模型
const configs = this.configManager.getConfigs();
const currentModel = configs.ollama.model;
if (currentModel) {
// 检查当前模型是否在列表中
const existingOption = Array.from(this.elements.modelSelect.options).find(opt => opt.value === currentModel);
if (existingOption) {
this.elements.modelSelect.value = currentModel;
} else {
// 如果不在列表中,添加一个新选项
const newOption = document.createElement('option');
newOption.value = currentModel;
newOption.textContent = currentModel + ' (自定义)';
this.elements.modelSelect.appendChild(newOption);
this.elements.modelSelect.value = currentModel;
}
}
}
// 添加处理转发配置变更的方法
handleProxyConfigChange() {
const proxyEnabled = this.elements.apiProxyEnabled.checked;
const proxyDomain = this.elements.apiProxyDomain.value.trim();
// 更新配置
this.configManager.setApiProxyEnabled(proxyEnabled);
if (proxyDomain) {
this.configManager.setApiProxyDomain(proxyDomain);
}
// 显示/隐藏域名输入框
this.elements.proxyDomainContainer.style.display = proxyEnabled ? 'block' : 'none';
}
// 更新功能设置复选框状态
updateFeatureCheckboxes() {
// 获取当前特性状态
const featureStates = this.configManager.getFeatureStates();
// 更新对话框中的复选框状态
if (this.elements.dialogFeatureCheckboxes) {
Object.keys(this.elements.dialogFeatureCheckboxes).forEach(key => {
const checkbox = this.elements.dialogFeatureCheckboxes[key];
if (checkbox) {
checkbox.checked = featureStates[key] === true;
}
});
}
// 更新主界面中的复选框状态
if (this.elements.featureCheckboxes) {
Object.keys(this.elements.featureCheckboxes).forEach(key => {
const checkbox = this.elements.featureCheckboxes[key];
if (checkbox) {
checkbox.checked = featureStates[key] === true;
}
});
}
}
// 打开特性设置对话框
openFeatureDialog(fromConfigButton = false) {
if (this.elements.featureDialog) {
// 如果是从设置按钮点击来的,则切换配置面板状态
if (fromConfigButton) {
this.toggleConfig(); // 切换配置面板的展开/收起状态
} else if (this.elements.configPanel.classList.contains('collapsed')) {
// 如果是从其他地方调用的且配置面板是收起的,则展开配置面板
this.toggleConfig();
}
// 更新对话框中的复选框状态
this.updateFeatureCheckboxes();
// 显示对话框
this.elements.featureDialog.style.display = 'flex';
} else {
console.error('功能设置对话框元素不存在');
}
}
// 关闭特性设置对话框
closeFeatureDialog() {
if (this.elements.featureDialog) {
this.elements.featureDialog.style.display = 'none';
}
}
// 保存特性设置
saveFeatureSettings() {
const newStates = {};
// 从对话框中获取状态
if (this.elements.dialogFeatureCheckboxes) {
Object.keys(this.elements.dialogFeatureCheckboxes).forEach(key => {
const checkbox = this.elements.dialogFeatureCheckboxes[key];
if (checkbox) {
newStates[key] = checkbox.checked;
}
});
}
// 保存到配置
this.configManager.setFeatureStates(newStates);
// 应用新状态到UI
this.applyFeatureStates();
// 关闭对话框
this.closeFeatureDialog();
}
// 处理特性开关变更
handleFeatureToggle(event) {
const featureId = event.target.id.replace('feature_', '');
const isEnabled = event.target.checked;
const newStates = {};
newStates[featureId] = isEnabled;
// 保存到配置
this.configManager.setFeatureStates(newStates);
// 应用新状态到UI
this.applyFeatureStates();
}
// URL转换函数 - 从APIService类引入到UIManager类
transformUrl(url) {
// 检查是否启用了转发
if (!this.configManager.getApiProxyEnabled() || !this.configManager.isFeatureEnabled('apiProxy')) {
return url; // 如果未启用转发,直接返回原始URL
}
try {
// 获取转发服务域名
const proxyDomain = this.configManager.getApiProxyDomain();
if (!proxyDomain) {
return url; // 如果未设置转发域名,直接返回原始URL
}
// 解析原始URL
const urlObj = new URL(url);
const protocol = urlObj.protocol;
const hostname = urlObj.hostname;
const pathname = urlObj.pathname;
const search = urlObj.search;
// 根据规则转换URL
let proxyUrl;
if (protocol === 'https:') {
// HTTPS转发
proxyUrl = `${proxyDomain}/proxy/${hostname}${pathname}${search}`;
} else if (protocol === 'http:') {
// HTTP转发
proxyUrl = `${proxyDomain}/httpproxy/${hostname}${pathname}${search}`;
} else if (url.includes('api.')) {
// API转发 - 针对包含api.的域名
proxyUrl = `${proxyDomain}/api/${hostname}${pathname}${search}`;
} else {
return url; // 不符合转发规则,返回原始URL
}
console.log(`API转发: ${url} -> ${proxyUrl}`);
return proxyUrl;
} catch (error) {
console.error('URL转换失败:', error);
return url; // 出错时返回原始URL
}
}
// 添加处理视图切换的方法
handleViewChange(e) {
$('.view-btn').removeClass('active');
$(e.target).addClass('active');
const viewMode = e.target.dataset.view;
this.toggleViewMode(viewMode);
}
toggleViewMode(viewMode) {
if (viewMode === 'markdown') {
$(this.elements.summaryTextarea).show();
$(this.elements.summaryPreview).hide();
} else if (viewMode === 'preview') {
$(this.elements.summaryTextarea).hide();
$(this.elements.summaryPreview).show();
}
}
}
// 文章提取类
class ArticleExtractor {
constructor(configManager) {
this.configManager = configManager;
this.selectors = [
'#js_content',
'.RichText',
'.article-content',
'#article_content',
'#cnblogs_post_body',
'article',
'.article',
'.post-content',
'.content',
'.entry-content',
'.article-content',
'main',
'#main',
'.main'
];
this.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'
];
}
async extract() {
// 如果禁用了自动提取特性,则显示文本输入框让用户手动输入
if (!this.configManager.isFeatureEnabled('autoExtract')) {
const userInput = prompt('请输入要总结的文本内容:');
if (userInput && userInput.length > 20) {
return userInput;
} else if (userInput) {
alert('输入内容太短,将尝试自动提取文章内容');
}
}
// 尝试使用不同的选择器获取内容
for (const selector of this.selectors) {
const element = document.querySelector(selector);
if (element) {
const content = this.processElement(element);
if (content.length > 100) {
return content;
}
}
}
// 如果上述方法都失败,尝试获取整个页面的主要内容
const content = this.processElement(document.body);
if (content.length < 100) {
throw new Error('无法获取足够的文章内容');
}
return content;
}
processElement(element) {
const clone = element.cloneNode(true);
this.removeUnwantedElements(clone);
return this.cleanText(clone.innerText);
}
removeUnwantedElements(element) {
this.removeSelectors.forEach(selector => {
const elements = element.querySelectorAll(selector);
elements.forEach(el => el.remove());
});
}
cleanText(text) {
return text
.replace(/\s+/g, ' ')
.replace(/\n\s*\n/g, '\n')
.trim();
}
}
// API服务类
class APIService {
constructor(configManager) {
this.configManager = configManager;
}
async generateSummary(content) {
const configs = this.configManager.getConfigs();
const apiService = this.configManager.getApiService();
const currentConfig = configs[apiService];
const outputFormat = this.configManager.getOutputFormat();
const apiEndpoint = this.getApiEndpoint(apiService, currentConfig);
const transformedEndpoint = this.transformUrl(apiEndpoint);
const systemPrompt = this.getSystemPrompt(outputFormat);
const messages = this.createMessages(systemPrompt, content);
return this.makeRequest(transformedEndpoint, currentConfig, messages);
}
async fetchOllamaModels() {
return new Promise((resolve, reject) => {
const ollamaConfig = this.configManager.getConfigs().ollama;
// 从 API URL 中提取基础 URL
const baseUrl = ollamaConfig.url.split('/api/')[0] || 'http://localhost:11434';
const modelsEndpoint = `${baseUrl}/api/tags`;
const transformedEndpoint = this.transformUrl(modelsEndpoint);
GM_xmlhttpRequest({
method: 'GET',
url: transformedEndpoint,
headers: {
'Content-Type': 'application/json'
},
onload: (response) => {
try {
if (response.status >= 400) {
console.warn('获取 Ollama 模型列表失败:', response.statusText);
resolve([]); // 失败时返回空数组,使用默认模型列表
return;
}
const data = JSON.parse(response.responseText);
if (data.models && Array.isArray(data.models)) {
// 提取模型名称
const models = data.models.map(model => model.name);
resolve(models);
} else {
console.warn('Ollama API 返回的模型列表格式异常:', data);
resolve([]);
}
} catch (error) {
console.error('解析 Ollama 模型列表失败:', error);
resolve([]); // 失败时返回空数组,使用默认模型列表
}
},
onerror: (error) => {
console.error('获取 Ollama 模型列表请求失败:', error);
resolve([]); // 失败时返回空数组,使用默认模型列表
}
});
});
}
getApiEndpoint(apiService, config) {
// 特殊处理freeollama.oneplus1.top域名的URL
if (config.url && config.url.includes('freeollama.oneplus1.top')) {
// 确保URL格式正确
if (!config.url.endsWith('/api/chat')) {
return config.url.replace(/\/?$/, '/api/chat');
}
}
return config.url;
}
getSystemPrompt(format) {
const prompts = {
markdown: "请用中文总结以下文章的主要内容,以标准Markdown格式输出,包括标题、小标题和要点。确保格式规范,便于阅读。",
bullet: "请用中文总结以下文章的主要内容,以简洁的要点列表形式输出,每个要点前使用'- '标记。",
paragraph: "请用中文总结以下文章的主要内容,以连贯的段落形式输出,突出文章的核心观点和结论。"
};
return prompts[format] || "请用中文总结以下文章的主要内容,以简洁的方式列出重点。";
}
createMessages(systemPrompt, content) {
const apiService = this.configManager.getApiService();
if (apiService === 'ollama') {
return [
{ role: "system", content: systemPrompt },
{ role: "user", content: content }
];
} else {
return [
{ role: "system", content: systemPrompt },
{ role: "user", content: content }
];
}
}
makeRequest(endpoint, config, messages) {
return new Promise((resolve, reject) => {
const apiService = this.configManager.getApiService();
// 确保配置有效
if (!endpoint) {
reject(new Error('API 地址无效'));
return;
}
if (!config.model) {
reject(new Error('模型名称无效'));
return;
}
// 构建请求数据
const requestData = {
model: config.model,
messages: messages,
stream: false
};
// 为 Ollama 服务添加参数
if (apiService === 'ollama' && this.configManager.isFeatureEnabled('advancedModelParams')) {
const ollamaParams = this.configManager.getOllamaParams();
if (ollamaParams) {
requestData.options = { ...ollamaParams };
}
}
// 构建请求头
const headers = {
'Content-Type': 'application/json'
};
// 非 Ollama 服务需要 API Key
if (apiService !== 'ollama' && config.key) {
headers['Authorization'] = `Bearer ${config.key}`;
}
// 发送请求
GM_xmlhttpRequest({
method: 'POST',
url: endpoint,
headers: headers,
data: JSON.stringify(requestData),
onload: this.handleResponse.bind(this, resolve, reject, apiService),
onerror: (error) => reject(new Error('网络请求失败: ' + (error.message || '未知错误')))
});
});
}
handleResponse(resolve, reject, apiService, response) {
try {
// 检查响应是否为 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 || '未知错误'));
return;
}
// 根据不同的 API 服务提取内容
if (apiService === 'ollama' && data.message) {
// Ollama API 响应格式
resolve(data.message.content);
} else if (data.choices && data.choices.length > 0 && data.choices[0].message) {
// OpenAI 兼容的 API 响应格式
resolve(data.choices[0].message.content);
} else {
// 未知的响应格式
console.warn('未知的 API 响应格式:', data);
// 尝试从响应中提取可能的内容
if (data.content) {
resolve(data.content);
} else if (data.text) {
resolve(data.text);
} else if (data.result) {
resolve(data.result);
} else if (data.response) {
resolve(data.response);
} else if (data.output) {
resolve(data.output);
} else if (data.generated_text) {
resolve(data.generated_text);
} else {
reject(new Error('API 返回格式异常,无法提取内容'));
}
}
} catch (error) {
reject(new Error(`解析API响应失败: ${error.message || '未知错误'}`));
}
}
// 添加URL转换函数
transformUrl(url) {
// 检查是否启用了转发
if (!this.configManager.getApiProxyEnabled() || !this.configManager.isFeatureEnabled('apiProxy')) {
return url; // 如果未启用转发,直接返回原始URL
}
try {
// 获取转发服务域名
const proxyDomain = this.configManager.getApiProxyDomain();
if (!proxyDomain) {
return url; // 如果未设置转发域名,直接返回原始URL
}
// 解析原始URL
const urlObj = new URL(url);
const protocol = urlObj.protocol;
const hostname = urlObj.hostname;
const pathname = urlObj.pathname;
const search = urlObj.search;
// 根据规则转换URL
let proxyUrl;
if (protocol === 'https:') {
// HTTPS转发
proxyUrl = `${proxyDomain}/proxy/${hostname}${pathname}${search}`;
} else if (protocol === 'http:') {
// HTTP转发
proxyUrl = `${proxyDomain}/httpproxy/${hostname}${pathname}${search}`;
} else if (url.includes('api.')) {
// API转发 - 针对包含api.的域名
proxyUrl = `${proxyDomain}/api/${hostname}${pathname}${search}`;
} else {
return url; // 不符合转发规则,返回原始URL
}
console.log(`API转发: ${url} -> ${proxyUrl}`);
return proxyUrl;
} catch (error) {
console.error('URL转换失败:', error);
return url; // 出错时返回原始URL
}
}
}
// 主应用类
class ArticleSummaryApp {
constructor() {
this.configManager = new ConfigManager();
this.uiManager = new UIManager(this.configManager);
this.articleExtractor = new ArticleExtractor(this.configManager);
this.apiService = new APIService(this.configManager);
this.elements = {}; // 初始化elements对象
this.version = '0.2.3'; // 更新版本号 - 增加Markdown和预览视图切换功能
}
async init() {
this.logScriptInfo();
try {
// 先尝试获取已有的元素,避免重复创建
const existingApp = document.getElementById('article-summary-app');
const existingIcon = document.getElementById('article-summary-icon');
// 如果已经存在,则先移除
if (existingApp) {
existingApp.remove();
console.log('已移除现有的应用元素');
}
if (existingIcon) {
existingIcon.remove();
console.log('已移除现有的图标元素');
}
// 初始化应用
await this.uiManager.init();
this.bindGenerateButton();
console.log('应用初始化完成');
} catch (error) {
console.error('应用初始化失败:', error);
// 尝试恢复或提示用户
alert('文章总结助手初始化失败,请刷新页面重试。错误信息:' + error.message);
}
}
logScriptInfo() {
const styles = {
title: 'font-size: 16px; font-weight: bold; color: #4CAF50;',
subtitle: 'font-size: 14px; font-weight: bold; color: #2196F3;',
normal: 'font-size: 12px; color: #333;',
key: 'font-size: 12px; color: #E91E63;',
value: 'font-size: 12px; color: #3F51B5;'
};
console.log('%c网页文章总结助手', styles.title);
console.log('%c基本信息', styles.subtitle);
console.log(`%c版本:%c ${this.version}`, styles.key, styles.value);
console.log(`%c作者:%c h7ml <[email protected]>`, styles.key, styles.value);
console.log(`%c描述:%c 自动总结网页文章内容,支持多种格式输出,适用于各类文章网站`, styles.key, styles.value);
console.log('%c支持的API服务', styles.subtitle);
console.log(`%c- Ollama:%c 本地大语言模型服务,无需API Key`, styles.key, styles.normal);
console.log(`%c- GPT God:%c 支持多种OpenAI模型`, styles.key, styles.normal);
console.log(`%c- DeepSeek:%c 支持DeepSeek系列模型`, styles.key, styles.normal);
console.log(`%c- 自定义:%c 支持任何兼容OpenAI API格式的服务`, styles.key, styles.normal);
console.log('%c支持的功能', styles.subtitle);
console.log(`%c- 自动提取:%c 智能提取网页文章内容`, styles.key, styles.normal);
console.log(`%c- 多种格式:%c 支持Markdown、要点列表、段落等输出格式`, styles.key, styles.normal);
console.log(`%c- 动态获取:%c 自动获取Ollama本地已安装模型列表`, styles.key, styles.normal);
console.log(`%c- 界面定制:%c 支持拖拽、最小化、最大化等操作`, styles.key, styles.normal);
console.log('%c当前配置', styles.subtitle);
const configs = this.configManager.getConfigs();
const apiService = this.configManager.getApiService();
const currentConfig = configs[apiService] || {};
console.log(`%c当前API服务:%c ${apiService}`, styles.key, styles.value);
console.log(`%c当前模型:%c ${currentConfig.model || '未设置'}`, styles.key, styles.value);
console.log(`%c当前API地址:%c ${currentConfig.url || '未设置'}`, styles.key, styles.value);
console.log(`%c输出格式:%c ${this.configManager.getOutputFormat()}`, styles.key, styles.value);
console.log('%c使用提示', styles.subtitle);
console.log(`%c- 点击右上角按钮可最小化或最大化界面`, styles.normal);
console.log(`%c- 最小化后可通过右下角图标恢复界面`, styles.normal);
console.log(`%c- 可拖动顶部标题栏移动位置`, styles.normal);
console.log(`%c- 使用Ollama服务时会自动获取本地已安装模型`, styles.normal);
}
bindGenerateButton() {
if (this.uiManager.elements.generateBtn) {
this.uiManager.elements.generateBtn.addEventListener('click', this.handleGenerate.bind(this));
console.log('生成按钮事件已绑定');
} else {
console.error('生成按钮元素不存在');
}
}
async handleGenerate() {
const apiService = this.uiManager.elements.apiService.value;
const apiKey = this.uiManager.elements.apiKey.value.trim();
const apiUrl = this.uiManager.elements.apiUrl.value.trim();
// 获取当前配置
const configs = this.configManager.getConfigs();
const currentConfig = configs[apiService] || {
url: apiService === 'ollama' ? 'http:/160.202.244.103:11434/api/chat' : '',
model: apiService === 'ollama' ? 'deepseek-r1:7b' : '',
key: ''
};
// 检查 API URL 是否有效
if (!apiUrl) {
alert('请输入有效的 API 地址');
return;
}
// 检查 API Key(Ollama 不需要)
if (apiService !== 'ollama' && !apiKey) {
alert('请输入有效的 API Key');
return;
}
// 检查模型是否有效
const modelName = apiService === 'ollama' ?
(this.uiManager.elements.modelSelect.value || 'llama2') :
(this.uiManager.elements.modelName.value || '');
if (!modelName) {
alert('请选择或输入有效的模型名称');
return;
}
this.showLoading();
try {
const content = await this.articleExtractor.extract();
const summary = await this.apiService.generateSummary(content);
this.displaySummary(summary);
} catch (error) {
this.handleError(error);
} finally {
this.hideLoading();
}
}
showLoading() {
this.uiManager.elements.loadingIndicator.style.display = 'block';
this.uiManager.elements.generateBtn.disabled = true;
this.uiManager.elements.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>
生成中...
`;
}
hideLoading() {
this.uiManager.elements.loadingIndicator.style.display = 'none';
this.uiManager.elements.generateBtn.disabled = false;
this.uiManager.elements.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>
生成总结
`;
}
displaySummary(summary) {
const outputFormat = this.configManager.getOutputFormat();
const summaryContent = this.uiManager.elements.summaryContent;
const summaryTextarea = this.uiManager.elements.summaryTextarea;
const summaryPreview = this.uiManager.elements.summaryPreview;
// 处理思考过程的逻辑
let processedSummary = summary;
const showThinking = this.configManager.isFeatureEnabled('showThinking');
// 使用正则表达式找出所有<think>...</think>标签对
if (!showThinking) {
// 如果禁用显示思考过程,则移除所有<think>...</think>内容
processedSummary = summary.replace(/<think>[\s\S]*?<\/think>/g, '');
}
// 设置markdown文本内容
summaryTextarea.value = processedSummary;
// 设置预览内容
summaryPreview.innerHTML = this.simpleMarkdownRender(processedSummary);
// 根据当前视图模式显示相应区域
const activeViewBtn = document.querySelector('.view-btn.active');
const viewMode = activeViewBtn ? activeViewBtn.dataset.view : 'markdown';
this.uiManager.toggleViewMode(viewMode);
this.uiManager.elements.summaryResult.style.display = 'flex';
}
handleError(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);
}
simpleMarkdownRender(text) {
let html = '<div class="summary-container">';
// 预处理 - 替换连续的换行为单个换行
let processedText = text.replace(/\n\s*\n/g, '\n\n');
// 处理代码块 (必须在其他处理前进行)
processedText = processedText.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
// 按行处理文本
const lines = processedText.split('\n');
let inList = false;
let listType = '';
let listHtml = '';
let inParagraph = false;
let paragraphContent = '';
const content = lines.map(line => {
// 空行处理
if (!line.trim()) {
let result = '';
// 如果在段落中,结束段落
if (inParagraph) {
result = `<p>${paragraphContent}</p>`;
inParagraph = false;
paragraphContent = '';
}
// 如果在列表中,结束列表
if (inList) {
result += listType === 'ol' ? `<ol>${listHtml}</ol>` : `<ul>${listHtml}</ul>`;
inList = false;
listHtml = '';
}
return result;
}
// 标题处理
if (line.startsWith('# ')) return `<h1>${line.substring(2)}</h1>`;
if (line.startsWith('## ')) return `<h2>${line.substring(3)}</h2>`;
if (line.startsWith('### ')) return `<h3>${line.substring(4)}</h3>`;
if (line.startsWith('#### ')) return `<h4>${line.substring(5)}</h4>`;
// 有序列表
const olMatch = line.match(/^\s*(\d+)\.\s+(.*)/);
if (olMatch) {
// 处理已存在的段落
let result = '';
if (inParagraph) {
result = `<p>${paragraphContent}</p>`;
inParagraph = false;
paragraphContent = '';
}
// 如果不在列表中或列表类型不同,开始新列表
if (!inList || listType !== 'ol') {
// 结束之前的列表(如果有)
if (inList) {
result += listType === 'ol' ? `<ol>${listHtml}</ol>` : `<ul>${listHtml}</ul>`;
listHtml = '';
}
inList = true;
listType = 'ol';
}
// 添加列表项
const itemContent = this.formatInlineMarkdown(olMatch[2]);
listHtml += `<li>${itemContent}</li>`;
return result; // 返回结果(可能为空)
}
// 无序列表
const ulMatch = line.match(/^\s*[\-\*]\s+(.*)/);
if (ulMatch) {
// 处理已存在的段落
let result = '';
if (inParagraph) {
result = `<p>${paragraphContent}</p>`;
inParagraph = false;
paragraphContent = '';
}
// 如果不在列表中或列表类型不同,开始新列表
if (!inList || listType !== 'ul') {
// 结束之前的列表(如果有)
if (inList) {
result += listType === 'ol' ? `<ol>${listHtml}</ol>` : `<ul>${listHtml}</ul>`;
listHtml = '';
}
inList = true;
listType = 'ul';
}
// 添加列表项
const itemContent = this.formatInlineMarkdown(ulMatch[1]);
listHtml += `<li>${itemContent}</li>`;
return result; // 返回结果(可能为空)
}
// 引用块
if (line.startsWith('> ')) {
// 处理已存在的段落或列表
let result = '';
if (inParagraph) {
result = `<p>${paragraphContent}</p>`;
inParagraph = false;
paragraphContent = '';
}
if (inList) {
result += listType === 'ol' ? `<ol>${listHtml}</ol>` : `<ul>${listHtml}</ul>`;
inList = false;
listHtml = '';
}
const quoteContent = this.formatInlineMarkdown(line.substring(2));
return `${result}<blockquote>${quoteContent}</blockquote>`;
}
// 水平线
if (line.match(/^\s*[-*_]{3,}\s*$/)) {
// 处理已存在的段落或列表
let result = '';
if (inParagraph) {
result = `<p>${paragraphContent}</p>`;
inParagraph = false;
paragraphContent = '';
}
if (inList) {
result += listType === 'ol' ? `<ol>${listHtml}</ol>` : `<ul>${listHtml}</ul>`;
inList = false;
listHtml = '';
}
return `${result}<hr>`;
}
// 普通段落
if (!inList) {
if (!inParagraph) {
inParagraph = true;
paragraphContent = this.formatInlineMarkdown(line);
} else {
paragraphContent += ' ' + this.formatInlineMarkdown(line);
}
return ''; // 段落内容稍后添加
} else {
return ''; // 在列表中,内容已添加到listHtml
}
}).filter(html => html).join('\n');
// 处理最后的段落或列表
let finalContent = content;
if (inParagraph) {
finalContent += `<p>${paragraphContent}</p>`;
}
if (inList) {
finalContent += listType === 'ol' ? `<ol>${listHtml}</ol>` : `<ul>${listHtml}</ul>`;
}
html += finalContent + '</div>';
return html;
}
// 处理行内Markdown
formatInlineMarkdown(text) {
return text
// 加粗
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
// 斜体
.replace(/\*(.*?)\*/g, '<em>$1</em>')
// 行内代码
.replace(/`([^`]+)`/g, '<code>$1</code>')
// 链接
.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank">$1</a>')
// 图片
.replace(/!\[(.*?)\]\((.*?)\)/g, '<img src="$2" alt="$1">');
}
// 打开特性设置对话框
openFeatureDialog(fromConfigButton = false) {
if (this.elements.featureDialog) {
// 如果是从设置按钮点击来的,则切换配置面板状态
if (fromConfigButton) {
this.toggleConfig(); // 切换配置面板的展开/收起状态
} else if (this.elements.configPanel.classList.contains('collapsed')) {
// 如果是从其他地方调用的且配置面板是收起的,则展开配置面板
this.toggleConfig();
}
// 更新对话框中的复选框状态
this.updateFeatureCheckboxes();
// 显示对话框
this.elements.featureDialog.style.display = 'flex';
} else {
console.error('功能设置对话框元素不存在');
}
}
// 关闭特性设置对话框
closeFeatureDialog() {
if (this.elements.featureDialog) {
this.elements.featureDialog.style.display = 'none';
}
}
// 保存特性设置
saveFeatureSettings() {
const newStates = {};
// 从对话框中获取状态
if (this.elements.dialogFeatureCheckboxes) {
Object.keys(this.elements.dialogFeatureCheckboxes).forEach(key => {
const checkbox = this.elements.dialogFeatureCheckboxes[key];
if (checkbox) {
newStates[key] = checkbox.checked;
}
});
}
// 保存到配置
this.configManager.setFeatureStates(newStates);
// 应用新状态到UI
this.applyFeatureStates();
// 关闭对话框
this.closeFeatureDialog();
}
// 处理特性开关变更
handleFeatureToggle(event) {
const featureId = event.target.id.replace('feature_', '');
const isEnabled = event.target.checked;
const newStates = {};
newStates[featureId] = isEnabled;
// 保存到配置
this.configManager.setFeatureStates(newStates);
// 应用新状态到UI
this.applyFeatureStates();
}
}
// 初始化应用
const app = new ArticleSummaryApp();
app.init();
console.log('总结助手已加载完成,当前版本:' + app.version);
}
// 添加视图切换按钮的样式
GM_addStyle(`
/* 视图切换按钮样式 */
#viewOptions {
display: flex;
margin-right: 8px;
}
.view-btn {
padding: 2px 8px;
font-size: 12px;
border-radius: 4px;
cursor: pointer;
background-color: #f5f5f5;
margin-right: 4px;
user-select: none;
}
.view-btn.active {
background-color: #007bff;
color: #fff;
}
#summaryPreview {
padding: 10px;
overflow-y: auto;
line-height: 1.5;
display: none;
height: 100%;
}
#summaryActions {
display: flex;
align-items: center;
}
.form-tip {
display: block;
font-size: 12px;
color: #666;
margin-top: 4px;
}
.form-tip a {
color: #007bff;
text-decoration: none;
}
.form-tip a:hover {
text-decoration: underline;
}
/* Markdown预览样式 */
.summary-container {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
color: #333;
line-height: 1.6;
}
.summary-container h1,
.summary-container h2,
.summary-container h3,
.summary-container h4 {
margin-top: 1.5em;
margin-bottom: 0.5em;
font-weight: 600;
line-height: 1.25;
}
.summary-container h1 {
font-size: 1.8em;
border-bottom: 1px solid #eaecef;
padding-bottom: 0.3em;
}
.summary-container h2 {
font-size: 1.5em;
border-bottom: 1px solid #eaecef;
padding-bottom: 0.3em;
}
.summary-container h3 {
font-size: 1.25em;
}
.summary-container h4 {
font-size: 1em;
}
.summary-container p {
margin-top: 0;
margin-bottom: 1em;
}
.summary-container ul,
.summary-container ol {
padding-left: 2em;
margin-top: 0;
margin-bottom: 1em;
}
.summary-container li {
margin-bottom: 0.25em;
}
.summary-container ul li {
list-style: disc;
}
.summary-container ol li {
list-style: decimal;
}
.summary-container blockquote {
padding: 0 1em;
margin-left: 0;
color: #6a737d;
border-left: 0.25em solid #dfe2e5;
}
.summary-container code {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
background-color: rgba(27,31,35,0.05);
border-radius: 3px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
}
.summary-container pre {
padding: 16px;
overflow: auto;
line-height: 1.45;
background-color: #f6f8fa;
border-radius: 3px;
}
.summary-container pre > code {
padding: 0;
margin: 0;
font-size: 85%;
background-color: transparent;
border-radius: 0;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
white-space: pre;
display: block;
}
.summary-container img {
max-width: 100%;
box-sizing: content-box;
}
.summary-container hr {
height: 0.25em;
padding: 0;
margin: 24px 0;
background-color: #e1e4e8;
border: 0;
}
.summary-container a {
color: #0366d6;
text-decoration: none;
}
.summary-container a:hover {
text-decoration: underline;
}
`);
})();