// ==UserScript==
// @name Notion AI 快速保存助手
// @namespace http://tampermonkey.net/
// @version 2.1
// @description 一个由 AI 驱动的用户脚本(UserScript),支持 OpenAI 和 Gemini,可以快速将网页保存并智能分类到您的 Notion 数据库中。它拥有一个设计优雅、可拖动的悬浮 UI、一个功能全面的设置面板,并为兼容现代网站而精心设计。
// @author tsdw
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @license MIT
// @connect *
// ==/UserScript==
(function() {
'use strict';
// ===================================================================
// ===================== 初始化锁 (双重保险) =========================
// ===================================================================
if (window.self !== window.top) { return; }
if (window.NQS_SCRIPT_RUNNING) { return; }
window.NQS_SCRIPT_RUNNING = true;
// ===================================================================
// ===================== 全局容器与样式隔离 ========================
// ===================================================================
const globalContainer = document.createElement('div');
globalContainer.id = 'NQS_GLOBAL_CONTAINER';
document.body.appendChild(globalContainer);
// ===================================================================
// ===================== 默认配置与分类管理 ========================
// ===================================================================
const DEFAULT_CATEGORIES = [ "前端开发", "后端开发", "人工智能", "运维安全", "生活服务", "美食烹饪", "旅行", "健康医疗", "影视", "音乐", "游戏", "动漫","综艺", "学术研究", "教育学习", "金融理财", "二手交易", "求职招聘", "办公工具", "职业规划", "热点与政策", "社区互动", "其他" ];
const SETTINGS_DEFAULTS = {
theme: 'auto',
notion_api_key: '',
database_id: '',
ai_provider: 'openai',
ai_api_key: '',
ai_api_url: 'https://api.openai.com/v1/chat/completions',
ai_model: 'gpt-3.5-turbo',
ai_enabled: true,
ai_include_body: false,
ai_timeout: 20000,
ai_model_fetch_timeout: 10000, // 新增: 独立的模型列表获取超时
ai_retry_count: 2, // 新增: AI调用重试次数
ai_confidence_threshold: 0.7, // 新增: AI分类置信度阈值
ai_learning_enabled: true, // 新增: 启用AI分类学习
content_extraction_enabled: true, // 新增: 启用内容提取增强
auto_summary_enabled: false, // 新增: 自动生成摘要
auto_keywords_enabled: false, // 新增: 自动提取关键词
content_save_mode: 'link', // 新增: 内容保存模式 (link/summary/full)
notification_enabled: true, // 新增: 启用浏览器通知
notification_success_enabled: true, // 新增: 成功通知
notification_error_enabled: true, // 新增: 错误通知
progress_indicator_enabled: true, // 新增: 显示进度指示器
proxy_enabled: false,
proxy_url: '',
user_categories: JSON.stringify(DEFAULT_CATEGORIES),
prop_name_title: '名称',
prop_name_url: '链接',
prop_name_category: '类型',
read_later_enabled: true,
read_later_category: '稍后读',
ai_prompt: `你是一个专精于网页内容分类的AI模型。你的唯一任务是遵循下述协议,分析网页信息,并从一个预设的分类列表中,选择唯一且最匹配的一个分类。
# 分析层级与协议 (Analysis Hierarchy & Protocol)
你必须严格按照以下优先级顺序获取和分析信息。高优先级方法成功后,低优先级信息仅用作辅助验证。
1. **最高优先级:实时网页分析 (Live Webpage Analysis)**
* **行动指令:** 如果你的能力允许,首先尝试直接访问并完整分析 Page URL 指向的实时网页内容。
* **基本原理:** 实时网页是最权威、最准确的信息源。它的内容、结构和互动元素能最完整地反映页面的真实用途。
2. **次高优先级:静态元数据分析 (Static Metadata Analysis)**
* **触发条件:** 当且仅当你无法访问实时网页(例如,技术限制、访问错误、防火墙)时,启用此层级。
* **行动指令:** 详细分析提供的网页标题 (Page Title) 和 元描述 (Meta Description)。这两个字段是网站所有者设定的核心摘要。
3. **最低优先级:辅助正文参考 (Supplementary Body Text)**
* **触发条件:** 仅在前序层级(实时网页或静态元数据)分析后,分类结果依然高度模糊、难以抉择时,才可使用此信息。
* **行动指令:** 将提供的{page_body_text}作为最后的补充线索。
* **注意:** 此文本可能是不完整或过时的快照,其权重远低于实时网页内容和核心元数据。
# 分类决策原则
在获得信息后,运用以下原则进行最终决策:
- **选择最具体的:** 如果页面符合一个宽泛分类(如"社区互动")和一个具体分类(如"前端开发"),优先选择更具体的那个。
- **识别核心用途:** 对于多主题页面(如门户网站首页),判断其最核心、最主要的功能或主题进行分类。
- **识别平台/服务核心价值:** 对于像 GitHub, YouTube, Amazon 这样的平台型网站,应根据其核心服务来分类(例如,GitHub -> "后端开发" 或 "办公工具",YouTube -> "影视",Amazon -> "生活服务"),而不是其首页上可能出现的具体内容。
- **选择最佳匹配:** 必须从下方列表中选择。即使没有完美的选项,也要选择最接近的一个。
# 预设分类列表
{categories}
# 输出格式规定
- **【绝对必须】** 只输出最终的分类名称,且该名称必须完整存在于"预设分类列表"中。
- **【绝对禁止】** 禁止添加任何形式的解释、说明、标点符号(如 "", 「」)、Markdown标记(如 *, \`\`)或其他任何多余字符。
# 任务开始
根据下面提供的信息,并严格遵守上述所有协议、原则和规则,给出你的分类结果。
## 网页信息:
{page_metadata}{optional_body_section}`
,
ai_prompt_templates: JSON.stringify([{ id: 'default', title: '默认', content: '', builtIn: true }]),
ai_active_template_id: 'default'
};
const LOG_LIMIT = 100;
// ===================================================================
// ================= CSP TrustedHTML 处理器 ==========================
// ===================================================================
let nqsPolicy;
if (window.trustedTypes && window.trustedTypes.createPolicy) {
try {
nqsPolicy = window.trustedTypes.createPolicy('nqs-policy', { createHTML: string => string });
} catch (e) {
nqsPolicy = window.trustedTypes.defaultPolicy || window.trustedTypes.policies.get("nqs-policy");
}
}
function createSafeHTML(htmlString) { return nqsPolicy ? nqsPolicy.createHTML(htmlString) : htmlString; }
function setSafeInnerHTML(element, htmlString) { element.innerHTML = createSafeHTML(htmlString); }
// ===================================================================
// ====================== 核心功能与辅助函数 =======================
// ===================================================================
function getHighSignalPageData(doc) { const url = doc.location.href; const title = doc.title; const description = (doc.querySelector('meta[name="description"]') || {}).content || '无'; const keywords = (doc.querySelector('meta[name="keywords"]') || {}).content || '无'; return `Page URL: ${url}\nPage Title: ${title}\nMeta Description: ${description.trim()}\nMeta Keywords: ${keywords.trim()}`; }
// ===================================================================
// ====================== 增强内容提取功能 ============================
// ===================================================================
function extractMainContent(doc) {
try {
const main = doc.querySelector('main');
if (main) return main.innerText;
const articles = doc.querySelectorAll('article');
if (articles.length > 0) return Array.from(articles).map(el => el.innerText).join('\n\n');
const clonedBody = doc.body.cloneNode(true);
clonedBody.querySelectorAll('nav, footer, header, aside, .sidebar, #sidebar, [role="navigation"], [role="banner"], [role="contentinfo"], .ad, #ad, .advertisement').forEach(el => el.remove());
const cleanedText = clonedBody.innerText;
return (cleanedText && cleanedText.trim().length > 100) ? cleanedText : doc.body.innerText;
} catch (error) {
console.error("NQS - 智能内容提取失败:", error);
return doc.body.innerText;
}
}
function extractAdvancedContent(doc) {
const content = {
title: doc.title,
url: doc.location.href,
description: (doc.querySelector('meta[name="description"]') || {}).content || '',
keywords: (doc.querySelector('meta[name="keywords"]') || {}).content || '',
author: (doc.querySelector('meta[name="author"]') || {}).content || '',
publishTime: '',
readingTime: 0,
mainContent: '',
images: [],
links: []
};
// 提取发布时间
const timeSelectors = [
'time[datetime]',
'[datetime]',
'.publish-time',
'.date',
'.post-date'
];
for (const selector of timeSelectors) {
const timeEl = doc.querySelector(selector);
if (timeEl) {
content.publishTime = timeEl.getAttribute('datetime') || timeEl.textContent.trim();
break;
}
}
// 提取主要内容
content.mainContent = extractMainContent(doc);
// 计算阅读时间(基于词数,中文按字符数)
const textLength = content.mainContent.replace(/\s+/g, '').length;
content.readingTime = Math.ceil(textLength / 500); // 假设每分钟500字
// 提取图片
const images = doc.querySelectorAll('img[src]');
content.images = Array.from(images).slice(0, 5).map(img => ({
src: img.src,
alt: img.alt || '',
title: img.title || ''
}));
// 提取重要链接
const links = doc.querySelectorAll('a[href]');
content.links = Array.from(links).filter(link =>
link.href.startsWith('http') &&
!link.href.includes(doc.location.hostname)
).slice(0, 10).map(link => ({
href: link.href,
text: link.textContent.trim()
}));
return content;
}
async function generateContentSummary(content, settings) {
if (!settings.ai_enabled || !settings.auto_summary_enabled) {
return null;
}
try {
const summaryPrompt = `请为以下网页内容生成一个简洁的摘要(100-200字):
标题:${content.title}
内容:${content.mainContent.substring(0, 2000)}
要求:
1. 提取核心要点
2. 语言简洁明了
3. 保持客观中性
4. 只输出摘要内容,无需其他说明`;
const aiResult = await makeSummaryRequest(settings, summaryPrompt);
return aiResult.summary;
} catch (error) {
console.warn('NQS - 自动摘要生成失败:', error.message);
return null;
}
}
async function extractKeywords(content, settings) {
if (!settings.ai_enabled || !settings.auto_keywords_enabled) {
return [];
}
try {
const keywordPrompt = `请从以下网页内容中提取5-8个关键词:
标题:${content.title}
描述:${content.description}
内容:${content.mainContent.substring(0, 1500)}
要求:
1. 关键词应该是名词或名词短语
2. 体现内容的核心主题
3. 用逗号分隔
4. 只输出关键词,无需其他文字`;
const aiResult = await makeSummaryRequest(settings, keywordPrompt);
return aiResult.summary.split(',').map(k => k.trim()).filter(k => k.length > 0);
} catch (error) {
console.warn('NQS - 关键词提取失败:', error.message);
return [];
}
}
async function makeSummaryRequest(settings, prompt) {
const { ai_provider, ai_api_key, ai_api_url, ai_model, ai_timeout, proxy_enabled, proxy_url } = settings;
return new Promise((resolve, reject) => {
let requestDetails = {
method: 'POST',
url: '',
headers: { 'Content-Type': 'application/json' },
data: '',
timeout: parseInt(ai_timeout, 10) || 20000,
ontimeout: () => reject(new Error("摘要生成超时")),
onload: null,
onerror: () => reject(new Error('摘要生成网络请求失败'))
};
if (ai_provider === 'gemini') {
const geminiUrl = `https://generativelanguage.googleapis.com/v1/models/${ai_model}:generateContent?key=${ai_api_key}`;
requestDetails.url = (proxy_enabled && proxy_url) ? proxy_url : geminiUrl;
if (proxy_enabled && proxy_url) { requestDetails.headers['Authorization'] = `Bearer ${ai_api_key}`; }
requestDetails.data = JSON.stringify({
contents: [{ parts: [{ text: prompt }] }],
generationConfig: { temperature: 0.3, maxOutputTokens: 300 }
});
requestDetails.onload = (response) => {
if (response.status >= 200 && response.status < 300) {
try {
const result = JSON.parse(response.responseText);
const summary = result.candidates[0]?.content?.parts[0]?.text || "";
resolve({ summary: summary.trim() });
} catch (e) {
reject(new Error(`解析摘要响应失败: ${e.message}`));
}
} else {
reject(new Error(`摘要生成API错误: ${response.status}`));
}
};
} else { // OpenAI
requestDetails.url = (proxy_enabled && proxy_url) ? proxy_url : ai_api_url;
requestDetails.headers['Authorization'] = `Bearer ${ai_api_key}`;
requestDetails.data = JSON.stringify({
model: ai_model,
messages: [{ role: 'user', content: prompt }],
temperature: 0.3,
max_tokens: 300
});
requestDetails.onload = (response) => {
if (response.status >= 200 && response.status < 300) {
try {
const result = JSON.parse(response.responseText);
const summary = result.choices[0].message.content || "";
resolve({ summary: summary.trim() });
} catch (e) {
reject(new Error(`解析摘要响应失败: ${e.message}`));
}
} else {
reject(new Error(`摘要生成API错误: ${response.status}`));
}
};
}
GM_xmlhttpRequest(requestDetails);
});
}
async function addLog(level, message, details = {}) { let logs = []; try { logs = JSON.parse(await GM_getValue('nqs_logs', '[]') || '[]'); } catch (e) { console.error("NQS - 解析本地日志失败:", e); logs = []; } logs.unshift({ timestamp: Date.now(), level, message, details }); if (logs.length > LOG_LIMIT) logs.splice(LOG_LIMIT); await GM_setValue('nqs_logs', JSON.stringify(logs)); }
async function loadAllSettings() { const settings = {}; for (const key of Object.keys(SETTINGS_DEFAULTS)) { settings[key] = await GM_getValue(key, SETTINGS_DEFAULTS[key]); } return settings; }
function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }
async function applyTheme() { const theme = await GM_getValue('theme', SETTINGS_DEFAULTS.theme); const container = document.getElementById('NQS_GLOBAL_CONTAINER'); if(container) container.dataset.theme = theme; }
// ===================================================================
// ===================== UI创建与管理 (CSP-Ready) =====================
// ===================================================================
function closeAllNQSPopups() { const container = document.getElementById('NQS_GLOBAL_CONTAINER'); if (container) container.querySelectorAll('.nqs-overlay').forEach(el => el.remove()); }
function createBasePanel(title, subtitle = '', options = {}) {
injectStyles();
const container = document.getElementById('NQS_GLOBAL_CONTAINER');
const overlay = document.createElement('div');
overlay.className = 'nqs-overlay';
if (options.panelId) overlay.id = options.panelId;
if (options.isNested) {
const existingOverlays = container.querySelectorAll('.nqs-overlay');
let topZ = 100000;
if (existingOverlays.length > 0) {
const zIndexes = Array.from(existingOverlays).map(el => parseInt(window.getComputedStyle(el).zIndex, 10));
const maxZ = Math.max(...zIndexes.filter(z => !isNaN(z)));
if (isFinite(maxZ)) topZ = maxZ;
}
overlay.style.zIndex = topZ + 1;
}
const panel = document.createElement('div');
panel.className = `nqs-panel ${options.panelClass || ''}`;
if (options.maxWidth) panel.style.maxWidth = options.maxWidth;
const header = document.createElement('div');
header.className = 'nqs-header';
const h1 = document.createElement('h1');
h1.textContent = title;
header.appendChild(h1);
if (subtitle) {
const p = document.createElement('p');
p.textContent = subtitle;
header.appendChild(p);
}
const body = document.createElement('div');
body.className = 'nqs-body';
const footer = document.createElement('div');
footer.className = 'nqs-footer';
panel.appendChild(header);
panel.appendChild(body);
panel.appendChild(footer);
overlay.appendChild(panel);
container.appendChild(overlay);
const close = () => { overlay.classList.remove('visible'); setTimeout(() => overlay.remove(), 300); };
panel.addEventListener('click', e => e.stopPropagation());
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
setTimeout(() => overlay.classList.add('visible'), 10);
return { overlay, body, footer, close };
}
function highlightJson(jsonString) { if (typeof jsonString !== 'string') jsonString = JSON.stringify(jsonString, undefined, 2); jsonString = jsonString.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); return jsonString.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { let cls = 'json-number'; if (/^"/.test(match)) { if (/:$/.test(match)) { cls = 'json-key'; } else { cls = 'json-string'; } } else if (/true|false/.test(match)) { cls = 'json-boolean'; } else if (/null/.test(match)) { cls = 'json-null'; } return '<span class="' + cls + '">' + match + '</span>'; });}
function showAlertModal(title, message) { return new Promise((resolve) => { const { body, footer, close } = createBasePanel(title, '', { maxWidth: '480px', isNested: true }); setSafeInnerHTML(body, `<p class="nqs-p">${message}</p>`); setSafeInnerHTML(footer, `<div style="flex-grow: 1;"></div><button id="nqs-alert-ok" class="nqs-button nqs-button-primary">确定</button>`); footer.querySelector('#nqs-alert-ok').onclick = () => { close(); resolve(); }; });}
function showConfirmationModal(title, message, options = {}) { return new Promise((resolve) => { const { body, footer, close } = createBasePanel(title, '', { maxWidth: '480px', isNested: true }); const { danger = false, confirmText = '确认', cancelText = '取消' } = options; setSafeInnerHTML(body, `<p class="nqs-p">${message}</p>`); const confirmClass = danger ? 'nqs-button-danger' : 'nqs-button-primary'; setSafeInnerHTML(footer, `<button id="nqs-confirm-cancel" class="nqs-button nqs-button-secondary">${cancelText}</button><div style="flex-grow: 1;"></div><button id="nqs-confirm-ok" class="nqs-button ${confirmClass}">${confirmText}</button>`); footer.querySelector('#nqs-confirm-ok').onclick = () => { close(); resolve(true); }; footer.querySelector('#nqs-confirm-cancel').onclick = () => { close(); resolve(false); }; });}
function showDetailsModal(title, detailsObject) { const { body, footer, close } = createBasePanel(title, '上下文详细信息', { maxWidth: '800px', isNested: true }); body.classList.add('nqs-json-viewer'); const pre = document.createElement('pre'); pre.style.whiteSpace = 'pre-wrap'; pre.style.wordWrap = 'break-word'; pre.style.margin = '0'; setSafeInnerHTML(pre, highlightJson(detailsObject)); body.appendChild(pre); setSafeInnerHTML(footer, `<div style="flex-grow: 1;"></div><button class="nqs-button nqs-button-primary">关闭</button>`); footer.querySelector('button').addEventListener('click', close);}
async function openSettingsPanel() {
closeAllNQSPopups();
const { body, footer, close } = createBasePanel('⚙️ 脚本设置', '配置您的 Notion AI 快速保存助手,实现高效网页收藏', { maxWidth: '900px', panelClass: 'nqs-panel--settings' });
const elements = {};
// 创建设置分组的辅助函数
const createSettingsGroup = (title, description = '', icon = '⚙️') => {
const group = document.createElement('div');
group.className = 'nqs-settings-group';
const header = document.createElement('div');
header.className = 'nqs-settings-group-header';
header.innerHTML = `
<div class="nqs-settings-group-icon">${icon}</div>
<div class="nqs-settings-group-content">
<h3 class="nqs-settings-group-title">${title}</h3>
${description ? `<p class="nqs-settings-group-description">${description}</p>` : ''}
</div>
`;
const content = document.createElement('div');
content.className = 'nqs-settings-group-content';
group.appendChild(header);
group.appendChild(content);
return { group, content };
};
// 创建设置字段的辅助函数
const createSettingField = (parent, id, labelText, descText, type = 'text', options = {}) => {
const field = document.createElement('div');
field.className = 'nqs-setting-field';
// 用于后续显隐控制(例如代理URL)
field.dataset.fieldId = id;
// 支持全宽字段(无标签或明确要求时)
if (!labelText || options.fullWidth) {
field.classList.add('nqs-setting-field--full');
}
const labelGroup = document.createElement('div');
labelGroup.className = 'nqs-setting-label-group';
const label = document.createElement('label');
label.htmlFor = 'nqs-' + id;
label.textContent = labelText;
labelGroup.appendChild(label);
if (descText) {
const desc = document.createElement('p');
desc.className = 'nqs-setting-description';
desc.innerHTML = createSafeHTML(descText);
labelGroup.appendChild(desc);
}
field.appendChild(labelGroup);
const inputGroup = document.createElement('div');
inputGroup.className = 'nqs-setting-input-group';
const creators = {
toggle: () => createToggleSwitch(id),
select: () => createSelectInput(id, options.choices || []),
textarea: () => createTextareaInput(id, options.placeholder || ''),
number: () => createNumberInput(id, options.min, options.max, options.step),
multiselect: () => createMultiSelectInput(id, options.choices || [])
};
const input = creators[type] ? creators[type]() : createTextInput(id, type, options.placeholder || '');
inputGroup.appendChild(input);
field.appendChild(inputGroup);
parent.appendChild(field);
elements[id] = input;
return field;
};
// 创建开关组件
const createToggleSwitch = (id) => {
const wrapper = document.createElement('div');
wrapper.className = 'nqs-toggle-switch';
const label = document.createElement('label');
label.className = 'nqs-switch';
const input = document.createElement('input');
input.type = 'checkbox';
input.id = 'nqs-' + id;
const span = document.createElement('span');
span.className = 'nqs-slider';
label.appendChild(input);
label.appendChild(span);
wrapper.appendChild(label);
return wrapper;
};
// 创建选择器组件
const createSelectInput = (id, choices) => {
const select = document.createElement('select');
select.id = 'nqs-' + id;
select.className = 'nqs-select';
choices.forEach(choice => {
const option = document.createElement('option');
option.value = choice.value;
option.textContent = choice.label;
select.appendChild(option);
});
return select;
};
// 创建多选选择器组件(统一风格)
const createMultiSelectInput = (id, choices) => {
const select = document.createElement('select');
select.id = 'nqs-' + id;
select.className = 'nqs-select';
select.multiple = true;
choices.forEach(choice => {
const option = document.createElement('option');
option.value = choice.value;
option.textContent = choice.label;
select.appendChild(option);
});
return select;
};
// 创建文本输入组件
const createTextInput = (id, type, placeholder) => {
const input = document.createElement('input');
input.id = 'nqs-' + id;
input.className = 'nqs-input';
input.type = type;
if (placeholder) input.placeholder = placeholder;
return input;
};
// 创建数字输入组件
const createNumberInput = (id, min, max, step) => {
const input = document.createElement('input');
input.id = 'nqs-' + id;
input.className = 'nqs-input';
input.type = 'number';
if (min !== undefined) input.min = min;
if (max !== undefined) input.max = max;
if (step !== undefined) input.step = step;
return input;
};
// 创建文本域组件
const createTextareaInput = (id, placeholder) => {
const textarea = document.createElement('textarea');
textarea.id = 'nqs-' + id;
textarea.className = 'nqs-textarea';
if (placeholder) textarea.placeholder = placeholder;
return textarea;
};
// 创建分类管理组件
const createCategoryManager = (parent) => {
const categorySection = document.createElement('div');
categorySection.className = 'nqs-category-manager-section';
const header = document.createElement('div');
header.className = 'nqs-category-manager-header';
header.innerHTML = `
<h4>分类列表</h4>
<p>管理您的自定义分类,这些分类将用于手动选择和AI自动分类</p>
`;
categorySection.appendChild(header);
const inputGroup = document.createElement('div');
inputGroup.className = 'nqs-category-input-group';
const input = document.createElement('input');
input.type = 'text';
input.className = 'nqs-category-input';
input.placeholder = '输入新分类名称...';
input.id = 'nqs-new-category-input';
const addBtn = document.createElement('button');
addBtn.className = 'nqs-button nqs-button-primary nqs-category-add-btn';
addBtn.textContent = '添加分类';
addBtn.id = 'nqs-add-category-btn';
inputGroup.appendChild(input);
inputGroup.appendChild(addBtn);
categorySection.appendChild(inputGroup);
const categoryList = document.createElement('div');
categoryList.className = 'nqs-category-list-container';
categorySection.appendChild(categoryList);
parent.appendChild(categorySection);
return { input, addBtn, categoryList };
};
// 创建AI模型选择器组件
const createModelSelector = (parent) => {
const modelSection = document.createElement('div');
modelSection.className = 'nqs-model-selector-section';
const inputGroup = document.createElement('div');
inputGroup.className = 'nqs-model-input-group';
const input = document.createElement('input');
input.type = 'text';
input.className = 'nqs-input';
input.id = 'nqs-ai_model';
input.placeholder = '选择AI模型...';
const fetchBtn = document.createElement('button');
fetchBtn.className = 'nqs-icon-button nqs-fetch-models-btn';
fetchBtn.title = '获取可用模型列表';
fetchBtn.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
<path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm-1.293-2.707a1 1 0 0 1-1.414-1.414l.001-.001 2.122-2.121a1 1 0 0 1 1.414 0l2.121 2.121a1 1 0 0 1-1.414 1.414L13 16.414V20a1 1 0 1 1-2 0v-3.586l-.293.293zM13 4a1 1 0 1 1 2 0v3.586l.293-.293a1 1 0 1 1 1.414 1.414l-2.121 2.121a1 1 0 0 1-1.414 0L10.05 8.707a1 1 0 0 1 1.414-1.414L11.76 7.586 11 7.586V4h2z"></path>
</svg>
`;
inputGroup.appendChild(input);
inputGroup.appendChild(fetchBtn);
modelSection.appendChild(inputGroup);
const dropdown = document.createElement('div');
dropdown.className = 'nqs-model-dropdown';
modelSection.appendChild(dropdown);
parent.appendChild(modelSection);
return { input, fetchBtn, dropdown };
};
// 创建设置内容容器(供右侧内容滚动)
const settingsContainer = document.createElement('div');
settingsContainer.className = 'nqs-settings-container';
// 1. 外观设置组
const appearanceGroup = createSettingsGroup('🎨 外观设置', '自定义界面主题和显示效果', '🎨');
createSettingField(appearanceGroup.content, 'theme', '主题模式', '选择UI的显示主题,"自动"将跟随系统设置', 'select', {
choices: [
{ value: 'auto', label: '自动 (跟随系统)' },
{ value: 'light', label: '明亮主题' },
{ value: 'dark', label: '暗黑主题' }
]
});
// 2. 核心功能组
const coreGroup = createSettingsGroup('🚀 核心功能', '配置脚本的基本功能和AI自动分类', '🚀');
createSettingField(coreGroup.content, 'ai_enabled', 'AI 自动分类', '开启后将自动判断分类,关闭则需要手动选择', 'toggle');
createSettingField(coreGroup.content, 'read_later_enabled', '"稍后读"功能', '开启后将显示一个独立的"稍后读"快捷按钮', 'toggle');
createSettingField(coreGroup.content, 'read_later_category', '"稍后读"分类名', '指定用于"稍后读"功能的分类名称', 'text', { placeholder: '例如:稍后读' });
// 3. 通知设置组
const notificationGroup = createSettingsGroup('🔔 通知设置', '配置各种通知和进度提示', '🔔');
createSettingField(notificationGroup.content, 'notification_enabled', '浏览器通知', '启用桌面通知功能', 'toggle');
createSettingField(notificationGroup.content, 'notification_success_enabled', '成功通知', '保存成功时显示通知', 'toggle');
createSettingField(notificationGroup.content, 'notification_error_enabled', '错误通知', '保存失败时显示通知', 'toggle');
createSettingField(notificationGroup.content, 'progress_indicator_enabled', '顶部进度条', '显示操作进度的顶部状态栏', 'toggle');
// 4. 分类管理组
const categoryGroup = createSettingsGroup('📂 分类管理', '管理您的自定义分类列表', '📂');
const categoryManager = createCategoryManager(categoryGroup.content);
// 5. Notion 配置组
const notionGroup = createSettingsGroup('📝 Notion 配置', '配置Notion API和数据库连接(重要)', '📝');
createSettingField(notionGroup.content, 'notion_api_key', 'Notion API Key', '请填入您的Notion Internal Integration Token', 'password', { placeholder: 'sk_...' });
createSettingField(notionGroup.content, 'database_id', '数据库ID', '请填入您要保存到的Notion数据库ID', 'text', { placeholder: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' });
const fieldMappingHeader = document.createElement('div');
fieldMappingHeader.className = 'nqs-subsection-header';
fieldMappingHeader.innerHTML = '<h4>数据库字段名称</h4><p>请确保以下名称与您Notion数据库中的属性列名完全一致</p>';
notionGroup.content.appendChild(fieldMappingHeader);
createSettingField(notionGroup.content, 'prop_name_title', '标题属性名', '', 'text', { placeholder: '例如:名称' });
createSettingField(notionGroup.content, 'prop_name_url', '链接属性名', '', 'text', { placeholder: '例如:链接' });
createSettingField(notionGroup.content, 'prop_name_category', '分类属性名', '', 'text', { placeholder: '例如:类型' });
// 6. AI 配置组
const aiGroup = createSettingsGroup('🤖 AI 配置', '配置AI提供商和模型参数', '🤖');
createSettingField(aiGroup.content, 'ai_provider', 'AI 提供商', '选择用于内容分类的 AI 服务', 'select', {
choices: [
{ value: 'openai', label: 'OpenAI (GPT)' },
{ value: 'gemini', label: 'Google Gemini' }
]
});
createSettingField(aiGroup.content, 'ai_api_key', 'AI API Key', '填入所选提供商的 API Key', 'password', { placeholder: 'sk-... 或 AIza...' });
createSettingField(aiGroup.content, 'ai_api_url', 'AI API Endpoint', '兼容 OpenAI 格式的 API 地址,Gemini 可留空', 'text', { placeholder: 'https://api.openai.com/v1/chat/completions' });
const modelSelector = createModelSelector(aiGroup.content);
elements['ai_model'] = modelSelector.input;
createSettingField(aiGroup.content, 'ai_include_body', '附加网页正文', '开启后会提取并发送部分正文,可能增加成本但有助于分析复杂页面', 'toggle');
createSettingField(aiGroup.content, 'ai_timeout', 'AI分析超时(ms)', 'AI进行内容分析请求的等待上限', 'number', { min: 5000, max: 60000, step: 1000 });
createSettingField(aiGroup.content, 'ai_model_fetch_timeout', '获取模型超时(ms)', '获取可用模型列表的等待上限', 'number', { min: 5000, max: 30000, step: 1000 });
createSettingField(aiGroup.content, 'ai_retry_count', 'AI重试次数', 'AI请求失败时的重试次数', 'number', { min: 0, max: 5, step: 1 });
createSettingField(aiGroup.content, 'ai_confidence_threshold', '置信度阈值', '低于此阈值时将提示用户手动确认分类', 'number', { min: 0.1, max: 1.0, step: 0.1 });
createSettingField(aiGroup.content, 'ai_learning_enabled', 'AI分类学习', '记录用户的分类习惯,提高AI分类准确性', 'toggle');
// 7. 内容提取增强组
const contentGroup = createSettingsGroup('📊 内容提取增强', '配置页面内容提取和AI分析功能', '📊');
createSettingField(contentGroup.content, 'content_extraction_enabled', '启用内容提取增强', '提取页面的详细信息,如发布时间、阅读时间等', 'toggle');
createSettingField(contentGroup.content, 'auto_summary_enabled', '自动生成摘要', '使用AI自动为保存的页面生成内容摘要', 'toggle');
createSettingField(contentGroup.content, 'auto_keywords_enabled', '自动提取关键词', '使用AI自动提取页面关键词', 'toggle');
createSettingField(contentGroup.content, 'content_save_mode', '内容保存模式', '选择保存到Notion的内容详细程度', 'select', {
choices: [
{ value: 'link', label: '仅链接' },
{ value: 'summary', label: '链接+摘要' },
{ value: 'full', label: '完整内容' }
]
});
// 8. AI 提示词组
const promptGroup = createSettingsGroup('💬 AI 提示词', '自定义AI分类指令模板(高级用户)', '💬');
const promptHeader = document.createElement('div');
promptHeader.className = 'nqs-prompt-header';
promptHeader.innerHTML = `
<div class="nqs-prompt-info">
<h4>系统提示词</h4>
<p>自定义AI分类的指令模板,影响分类的准确性和风格</p>
</div>
<button id="nqs-reset-prompt" class="nqs-button nqs-button-text">
<span>🔄</span> 恢复默认
</button>
`;
promptGroup.content.appendChild(promptHeader);
// 提示词模式切换(模板 / 自定义)
const promptMode = document.createElement('div');
promptMode.className = 'nqs-segmented';
promptMode.innerHTML = `
<button class="nqs-seg-btn is-active" data-mode="template">模板</button>
<button class="nqs-seg-btn" data-mode="custom">自定义</button>
`;
promptGroup.content.appendChild(promptMode);
// 默认模板模式:隐藏文本域,仅展示卡片
promptGroup.content.setAttribute('data-prompt-mode', 'template');
createSettingField(
promptGroup.content,
'ai_prompt',
'',
'',
'textarea',
{ placeholder: '输入自定义的 AI 系统提示词模板(支持 {categories}、{page_metadata}、{optional_body_section} 占位符)', fullWidth: true }
);
// 提示词文本域增强:自适应高度 + 计数信息
const enhanceSystemPromptField = () => {
const promptField = elements && elements['ai_prompt'];
if (!promptField) return;
promptField.classList.add('nqs-prompt-textarea');
const autosize = () => {
promptField.style.height = 'auto';
const maxPx = Math.max(200, Math.floor(window.innerHeight * 0.5));
promptField.style.height = Math.min(promptField.scrollHeight + 2, maxPx) + 'px';
};
promptField.addEventListener('input', autosize);
setTimeout(autosize, 0);
const meta = document.createElement('div');
meta.className = 'nqs-prompt-meta';
const hint = document.createElement('span');
hint.textContent = '可使用占位符 {categories}、{page_metadata}、{optional_body_section}';
const counter = document.createElement('span');
counter.className = 'nqs-prompt-counter';
const updateCounter = () => { counter.textContent = (promptField.value || '').length + ' 字符'; };
promptField.addEventListener('input', updateCounter);
setTimeout(updateCounter, 0);
meta.appendChild(hint);
meta.appendChild(counter);
const inputGroup = promptField.parentElement; // .nqs-setting-input-group
if (inputGroup) inputGroup.appendChild(meta);
};
enhanceSystemPromptField();
// 覆盖旧的提示词输入方式:仅使用模板管理
(async () => {
try {
// 清空提示词分组内容,改为模板模式
if (promptGroup && promptGroup.content) {
promptGroup.content.innerHTML = '';
}
const checkPlaceholders = (text) => {
const t = String(text || '');
const hasPageMeta = /\{page_metadata\}/.test(t);
const hasCategories = /\{categories\}/.test(t);
const hasOptional = /\{optional_body_section\}/.test(t);
const missing = [];
if (!hasPageMeta) missing.push('{page_metadata}');
if (!hasCategories) missing.push('{categories}');
if (!hasOptional) missing.push('{optional_body_section}');
return { hasPageMeta, hasCategories, hasOptional, missing };
};
let promptTemplates = [];
let activeTemplateId = await GM_getValue('ai_active_template_id', SETTINGS_DEFAULTS.ai_active_template_id);
try {
promptTemplates = JSON.parse(await GM_getValue('ai_prompt_templates', SETTINGS_DEFAULTS.ai_prompt_templates) || '[]');
} catch { promptTemplates = []; }
if (!promptTemplates.find(t => t.id === 'default')) {
promptTemplates.unshift({ id: 'default', title: '默认', content: SETTINGS_DEFAULTS.ai_prompt, builtIn: true });
} else {
const d = promptTemplates.find(t => t.id === 'default');
if (!d.content) d.content = SETTINGS_DEFAULTS.ai_prompt;
}
if (!activeTemplateId) activeTemplateId = 'default';
const saveTemplatesState = async () => {
await GM_setValue('ai_prompt_templates', JSON.stringify(promptTemplates));
await GM_setValue('ai_active_template_id', activeTemplateId);
};
const openTemplateEditor = async (tpl) => {
const creating = !tpl;
const data = tpl ? { ...tpl } : { id: `t_${Date.now()}`, title: '', content: '', builtIn: false };
const { body: mb, footer: mf, close } = createBasePanel(creating ? '新增模板' : '编辑模板', '必须包含 {page_metadata}、{categories}、{optional_body_section}', { maxWidth: '720px', isNested: true });
mb.innerHTML = `
<div class="nqs-field nqs-field-full">
<div class="nqs-setting-label-group"><label>模板名称</label></div>
<div class="nqs-setting-input-group"><input type="text" id="nqs-tpl-title" class="nqs-input" placeholder="例如:精确分类"></div>
</div>
<div class="nqs-field nqs-field-full">
<div class="nqs-setting-label-group"><label>模板内容</label><p class="nqs-setting-description">必须包含 {page_metadata}、{categories}、{optional_body_section}</p></div>
<div class="nqs-setting-input-group"><textarea id="nqs-tpl-content" class="nqs-textarea" rows="10" placeholder="输入模板内容..."></textarea></div>
</div>
`;
mf.innerHTML = `<button class="nqs-button nqs-button-secondary" id="nqs-tpl-cancel">取消</button><div style="flex:1"></div><button class="nqs-button nqs-button-primary" id="nqs-tpl-ok">保存</button>`;
const ti = mb.querySelector('#nqs-tpl-title');
const tc = mb.querySelector('#nqs-tpl-content');
ti.value = data.title || '';
tc.value = data.content || '';
const doSave = async () => {
const title = ti.value.trim();
const content = tc.value;
if (!title) { await showAlertModal('校验失败', '请填写模板名称'); return; }
const chk = checkPlaceholders(content);
if (chk.missing.length) { await showAlertModal('占位符缺失', `模板缺少:${chk.missing.join(', ')}`); return; }
data.title = title; data.content = content;
if (creating) promptTemplates.unshift(data); else {
const i = promptTemplates.findIndex(t => t.id === data.id); if (i >= 0) promptTemplates[i] = data;
}
await saveTemplatesState();
renderPromptTemplates();
close();
};
mf.querySelector('#nqs-tpl-ok').addEventListener('click', doSave);
mf.querySelector('#nqs-tpl-cancel').addEventListener('click', close);
};
const renderPromptTemplates = () => {
promptGroup.content.innerHTML = '';
const header = document.createElement('div');
header.className = 'nqs-prompt-header';
header.innerHTML = `
<div class="nqs-prompt-info">
<h4>模板列表</h4>
<p>仅展示名称,点击可预览内容</p>
</div>
<div class="nqs-prompt-actions">
<button id="nqs-add-template" class="nqs-button nqs-button-primary"><span>+</span> 新增模板</button>
</div>`;
promptGroup.content.appendChild(header);
const section = document.createElement('div');
section.className = 'nqs-prompt-templates';
const grid = document.createElement('div');
grid.className = 'nqs-template-grid';
section.appendChild(grid);
promptGroup.content.appendChild(section);
promptTemplates.forEach(tpl => {
const card = document.createElement('div');
const isActive = tpl.id === activeTemplateId;
card.className = 'nqs-template-card nqs-template-card--compact' + (isActive ? ' is-active' : '');
card.innerHTML = `
<div class="nqs-template-card-top">
<label class="nqs-template-select"><input type="radio" name="nqs-active-template" value="${tpl.id}" ${isActive ? 'checked' : ''}></label>
<h5 class="nqs-template-title">${tpl.title}${tpl.builtIn ? '(默认)' : ''}</h5>
</div>
<div class="nqs-template-card-actions">
<button class="nqs-button nqs-button-secondary" data-act="edit" data-id="${tpl.id}">编辑</button>
${tpl.builtIn ? '' : `<button class="nqs-button nqs-button-danger" data-act="del" data-id="${tpl.id}">删除</button>`}
</div>`;
grid.appendChild(card);
});
// 选择模板(单选)
grid.addEventListener('change', async (e) => {
const input = e.target; if (input && input.name === 'nqs-active-template') {
activeTemplateId = input.value;
// 更新选中样式
grid.querySelectorAll('.nqs-template-card').forEach(card => card.classList.remove('is-active'));
const activeCard = input.closest('.nqs-template-card');
if (activeCard) activeCard.classList.add('is-active');
const at = promptTemplates.find(t => t.id === activeTemplateId);
const chk = checkPlaceholders(at?.content || '');
if (chk.missing.length) await showAlertModal('占位符缺失', `当前模板缺少:${chk.missing.join(', ')}`);
await saveTemplatesState();
}
});
// 卡片点击快捷选择 + 操作按钮
grid.addEventListener('click', async (e) => {
const actBtn = e.target.closest('button[data-act]');
if (actBtn) {
const id = actBtn.getAttribute('data-id');
const act = actBtn.getAttribute('data-act');
const idx = promptTemplates.findIndex(t => t.id === id); if (idx < 0) return;
if (act === 'del') {
if (promptTemplates[idx].builtIn) return;
const ok = await showConfirmationModal('删除模板', '确定要删除该模板吗?', { danger: true });
if (!ok) return;
const removingActive = (activeTemplateId === id);
promptTemplates.splice(idx, 1);
if (removingActive) activeTemplateId = 'default';
await saveTemplatesState();
renderPromptTemplates();
} else if (act === 'edit') {
await openTemplateEditor(promptTemplates[idx]);
}
return;
}
// 非操作区域点击则选中该卡片
const card = e.target.closest('.nqs-template-card');
if (card) {
const radio = card.querySelector('input[type="radio"]');
if (radio) {
radio.checked = true;
radio.dispatchEvent(new Event('change', { bubbles: true }));
}
}
});
header.querySelector('#nqs-add-template').addEventListener('click', async () => { await openTemplateEditor(); });
};
renderPromptTemplates();
} catch (e) { console.warn('Prompt templates init failed', e); }
})();
// 提示词模板库(卡片列表 + 多选插入)
const initPromptTemplates = () => {
const promptTextarea = elements && elements['ai_prompt'];
if (!promptTextarea) return;
const templates = [
{
id: 'precise-classify',
title: '精确分类(严格匹配)',
brief: '强调只从给定列表中选择,并说明优先级与权重',
content: '【精确分类】\n- 仅从预设列表中选择,禁止输出列表外类别\n- 优先级:实时内容 > 元数据 > 正文快照\n- 若无法确定,选择最具体且覆盖度最高的一项\n- 输出:仅类别名,无其他内容'
},
{
id: 'strict-output',
title: '严格输出(零其他字符)',
brief: '输出只包含类别名称,严禁标点或解释',
content: '【严格输出】\n- 仅输出最终分类名称\n- 禁止任何标点、引号、解释或多余字符\n- 如果分类不在列表中,选择“其他”'
},
{
id: 'learning-context',
title: '含学习上下文偏好',
brief: '结合域名历史修正进行偏好调整',
content: '【学习上下文】\n- 结合历史修正示例调整判断偏好\n- 若历史记录与当前判断冲突,参考历史选择但以当前内容为准\n- 如无冲突,优先采纳更具体类别'
}
];
const section = document.createElement('div');
section.className = 'nqs-prompt-templates';
section.innerHTML = `
<div class="nqs-prompt-templates-header">
<h4>模板库</h4>
<div class="nqs-prompt-templates-actions">
<button class="nqs-button nqs-button-secondary" id="nqs-insert-selected" disabled>插入所选</button>
<button class="nqs-button nqs-button-primary" id="nqs-apply-selected" disabled>应用所选</button>
</div>
</div>
<div class="nqs-template-grid"></div>
`;
const grid = section.querySelector('.nqs-template-grid');
const selected = new Set();
const updateBulkBtn = () => {
const btn1 = section.querySelector('#nqs-insert-selected');
const btn2 = section.querySelector('#nqs-apply-selected');
const disabled = selected.size === 0;
btn1.disabled = disabled;
btn2.disabled = disabled;
};
const previewTemplate = (tpl) => {
const { body: mBody, footer: mFooter, close } = createBasePanel(`模板预览 - ${tpl.title}`, '', { maxWidth: '800px', isNested: true });
const pre = document.createElement('pre');
pre.style.whiteSpace = 'pre-wrap';
pre.style.wordWrap = 'break-word';
pre.style.margin = '0';
pre.textContent = tpl.content;
mBody.appendChild(pre);
mFooter.innerHTML = '<div style="flex:1"></div><button class="nqs-button nqs-button-primary">关闭</button>';
mFooter.querySelector('button').addEventListener('click', close);
};
templates.forEach(tpl => {
const card = document.createElement('div');
card.className = 'nqs-template-card';
card.innerHTML = `
<div class="nqs-template-card-top">
<label class="nqs-template-select">
<input type="checkbox" data-id="${tpl.id}" />
</label>
<h5 class="nqs-template-title">${tpl.title}</h5>
<p class="nqs-template-brief">${tpl.brief}</p>
</div>
<div class="nqs-template-card-actions">
<button class="nqs-button nqs-button-text" data-action="preview">预览</button>
<button class="nqs-button nqs-button-secondary" data-action="insert">插入</button>
</div>
`;
card.querySelector('[data-action="preview"]').addEventListener('click', () => previewTemplate(tpl));
card.querySelector('[data-action="insert"]').addEventListener('click', () => {
const joiner = (promptTextarea.value && !promptTextarea.value.endsWith('\n')) ? '\n\n' : '';
promptTextarea.value = promptTextarea.value + joiner + tpl.content;
promptTextarea.dispatchEvent(new Event('input'));
});
const checkbox = card.querySelector('input[type="checkbox"]');
checkbox.addEventListener('change', () => {
if (checkbox.checked) selected.add(tpl.id); else selected.delete(tpl.id);
updateBulkBtn();
});
grid.appendChild(card);
});
section.querySelector('#nqs-insert-selected').addEventListener('click', () => {
const picked = templates.filter(t => selected.has(t.id)).map(t => t.content);
if (picked.length === 0) return;
const addition = picked.join('\n\n');
const joiner = (promptTextarea.value && !promptTextarea.value.endsWith('\n')) ? '\n\n' : '';
promptTextarea.value = promptTextarea.value + joiner + addition;
promptTextarea.dispatchEvent(new Event('input'));
});
section.querySelector('#nqs-apply-selected').addEventListener('click', () => {
const picked = templates.filter(t => selected.has(t.id)).map(t => t.content);
if (picked.length === 0) return;
promptTextarea.value = picked.join('\n\n');
promptTextarea.dispatchEvent(new Event('input'));
// 切换到“自定义”模式以便查看与微调
setPromptMode('custom');
});
// 将模板区块追加到提示词输入区域之后
const inputGroup = promptTextarea.parentElement;
if (inputGroup) inputGroup.appendChild(section);
};
initPromptTemplates();
// 模式切换逻辑
const setPromptMode = (mode) => {
if (!mode || (mode !== 'template' && mode !== 'custom')) return;
promptGroup.content.setAttribute('data-prompt-mode', mode);
const btns = promptMode.querySelectorAll('.nqs-seg-btn');
btns.forEach(b => b.classList.toggle('is-active', b.dataset.mode === mode));
};
promptMode.addEventListener('click', (e) => {
const btn = e.target.closest('.nqs-seg-btn');
if (!btn) return;
setPromptMode(btn.dataset.mode);
});
// 9. 网络配置组
const networkGroup = createSettingsGroup('🌐 网络配置', '配置代理和自定义网络端点(高级)', '🌐');
createSettingField(networkGroup.content, 'proxy_enabled', '启用自定义端点', '当需要使用第三方中继服务器或反代地址来访问AI服务时,请开启此项', 'toggle');
createSettingField(networkGroup.content, 'proxy_url', '端点/代理服务器地址', '一个兼容目标AI提供商API格式的请求中继地址', 'text', { placeholder: 'https://your-proxy.com/api' });
// 添加所有设置组到容器
settingsContainer.appendChild(appearanceGroup.group);
settingsContainer.appendChild(coreGroup.group);
settingsContainer.appendChild(notificationGroup.group);
settingsContainer.appendChild(categoryGroup.group);
settingsContainer.appendChild(notionGroup.group);
settingsContainer.appendChild(aiGroup.group);
settingsContainer.appendChild(contentGroup.group);
settingsContainer.appendChild(promptGroup.group);
settingsContainer.appendChild(networkGroup.group);
// --- 左侧侧边导航栏(快速跳转)---
// 组装分组元数据(用于构建导航和滚动联动)
const groupsMeta = [
{ key: 'appearance', title: '外观设置', icon: '🎨', el: appearanceGroup.group },
{ key: 'core', title: '核心功能', icon: '🚀', el: coreGroup.group },
{ key: 'notify', title: '通知设置', icon: '🔔', el: notificationGroup.group },
{ key: 'category', title: '分类管理', icon: '📂', el: categoryGroup.group },
{ key: 'notion', title: 'Notion 配置', icon: '📝', el: notionGroup.group },
{ key: 'ai', title: 'AI 配置', icon: '🤖', el: aiGroup.group },
{ key: 'content', title: '内容提取增强', icon: '📊', el: contentGroup.group },
{ key: 'prompt', title: 'AI 提示词', icon: '💬', el: promptGroup.group },
{ key: 'network', title: '网络配置', icon: '🌐', el: networkGroup.group }
];
groupsMeta.forEach(g => { if (g.el) g.el.id = `nqs-group-${g.key}`; });
// 构建布局:左侧导航 + 右侧内容
const layout = document.createElement('div');
layout.className = 'nqs-settings-layout';
const sidebar = document.createElement('aside');
sidebar.className = 'nqs-settings-sidebar';
const nav = document.createElement('nav');
nav.className = 'nqs-settings-nav';
sidebar.appendChild(nav);
// 构建导航项
const navItems = {};
// 控制点击时的高亮与观察抑制,避免快速切换造成错乱
let suppressIO = false;
let navClickTimer = null;
const buildNavItem = (g) => {
if (!g || !g.el) return null;
const a = document.createElement('a');
a.href = `#${g.el.id}`;
a.className = 'nqs-nav-item';
a.setAttribute('data-target', g.el.id);
a.setAttribute('role', 'button');
a.setAttribute('tabindex', '0');
a.innerHTML = `<span class="nqs-nav-icon">${g.icon}</span><span class="nqs-nav-text">${g.title}</span>`;
a.addEventListener('click', (e) => {
e.preventDefault();
const target = document.getElementById(g.el.id);
if (!target) return;
// 精确滚动到分组顶部(相对于滚动容器)
const offsetTop = target.getBoundingClientRect().top - contentWrap.getBoundingClientRect().top + contentWrap.scrollTop;
// 若在快速连续点击期间,采用瞬间跳转,避免队列中的平滑动画叠加造成错乱
const isInstant = suppressIO === true;
contentWrap.scrollTo({ top: offsetTop, behavior: isInstant ? 'auto' : 'smooth' });
// 立即同步侧边栏高亮,避免平滑滚动期间高亮不同步
setActiveNav(target.id);
// 在抑制窗口内忽略 IntersectionObserver 更新,滚动后恢复
suppressIO = true;
if (navClickTimer) clearTimeout(navClickTimer);
navClickTimer = setTimeout(() => { suppressIO = false; }, 400);
// 滚动完成后再确认一次(处理不同浏览器的动画时序)
setTimeout(() => setActiveNav(target.id), 300);
// 鼠标点击后移除焦点,避免与滚动同步产生“双高亮”
if (e.detail && e.detail > 0) {
setTimeout(() => a.blur(), 0);
}
});
a.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
a.click();
}
});
navItems[g.key] = a;
return a;
};
groupsMeta.forEach(g => { const item = buildNavItem(g); if (item) nav.appendChild(item); });
const contentWrap = document.createElement('div');
contentWrap.className = 'nqs-settings-content';
contentWrap.appendChild(settingsContainer);
layout.appendChild(sidebar);
layout.appendChild(contentWrap);
body.appendChild(layout);
// 激活状态联动(滚动高亮)
const setActiveNav = (id) => {
const items = nav.querySelectorAll('.nqs-nav-item');
items.forEach(it => it.classList.toggle('active', it.getAttribute('data-target') === id));
// 仅当活动项不在可视区域时才滚动,避免频繁滚动造成“滑动错乱”的观感
const active = nav.querySelector('.nqs-nav-item.active');
if (active) {
const navRect = nav.getBoundingClientRect();
const itemRect = active.getBoundingClientRect();
const outOfView = itemRect.top < navRect.top || itemRect.bottom > navRect.bottom;
if (outOfView && typeof active.scrollIntoView === 'function') {
active.scrollIntoView({ block: 'nearest' });
}
}
};
let io = null;
const attachObservers = () => {
if (!('IntersectionObserver' in window)) return;
if (io) io.disconnect();
io = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (!suppressIO && entry.isIntersecting) setActiveNav(entry.target.id);
});
}, { root: contentWrap, threshold: [0.1, 0.3, 0.6], rootMargin: '-15% 0px -65% 0px' });
groupsMeta.forEach(g => {
if (g.el && g.el.style.display !== 'none') io.observe(g.el);
});
};
attachObservers();
// 补充:基于滚动位置的高亮同步(作为 IO 的稳定兜底)
const syncActiveByScroll = () => {
if (suppressIO) return;
const wrapRect = contentWrap.getBoundingClientRect();
let bestId = null;
let bestDelta = Infinity;
groupsMeta.forEach(g => {
if (!g.el || g.el.style.display === 'none') return;
const rect = g.el.getBoundingClientRect();
const delta = Math.abs(rect.top - wrapRect.top - 12); // 与容器顶部的距离
if (delta < bestDelta) { bestDelta = delta; bestId = g.el.id; }
});
if (bestId) setActiveNav(bestId);
};
const debouncedScrollSync = debounce(syncActiveByScroll, 50);
contentWrap.addEventListener('scroll', debouncedScrollSync, { passive: true });
// 初始高亮第一个可见分组
const firstVisible = groupsMeta.find(g => g.el && g.el.style.display !== 'none');
if (firstVisible && firstVisible.el) setActiveNav(firstVisible.el.id);
// 底部按钮
const cancelButton = document.createElement('button');
cancelButton.id = 'nqs-close';
cancelButton.className = 'nqs-button nqs-button-secondary';
cancelButton.innerHTML = '<span>❌</span> 取消';
const spacer = document.createElement('div');
spacer.style.flexGrow = '1';
const saveButton = document.createElement('button');
saveButton.id = 'nqs-save';
saveButton.className = 'nqs-button nqs-button-primary';
saveButton.innerHTML = '<span>💾</span> 保存设置';
footer.append(cancelButton, spacer, saveButton);
// --- 逻辑与事件绑定 ---
let currentCategories = JSON.parse(await GM_getValue('user_categories', SETTINGS_DEFAULTS.user_categories));
// 切换AI相关设置组的可见性(同步侧边栏)
const toggleAISectionVisibility = () => {
if (!elements['ai_enabled'] || !elements['ai_enabled'].querySelector('input')) return;
const aiEnabled = elements['ai_enabled'].querySelector('input').checked;
if (aiGroup && aiGroup.group) aiGroup.group.style.display = aiEnabled ? 'block' : 'none';
if (contentGroup && contentGroup.group) contentGroup.group.style.display = aiEnabled ? 'block' : 'none';
if (promptGroup && promptGroup.group) promptGroup.group.style.display = aiEnabled ? 'block' : 'none';
if (networkGroup && networkGroup.group) networkGroup.group.style.display = aiEnabled ? 'block' : 'none';
// 同步侧边栏导航项显示状态
const safeToggle = (key, visible) => { const item = navItems[key]; if (item) item.style.display = visible ? '' : 'none'; };
safeToggle('ai', aiEnabled);
safeToggle('content', aiEnabled);
safeToggle('prompt', aiEnabled);
safeToggle('network', aiEnabled);
// 如果当前高亮项隐藏了,则选中第一个可见项
const activeItem = nav.querySelector('.nqs-nav-item.active');
if (activeItem && activeItem.style.display === 'none') {
const first = Array.from(nav.querySelectorAll('.nqs-nav-item')).find(i => i.style.display !== 'none');
if (first) setActiveNav(first.getAttribute('data-target'));
}
// 重新挂载观察器,仅观察可见分组
attachObservers();
};
// 切换代理URL字段的可见性
const toggleProxyUrlVisibility = () => {
if (!elements['proxy_enabled'] || !elements['proxy_enabled'].querySelector('input')) return;
const proxyEnabled = elements['proxy_enabled'].querySelector('input').checked;
if (networkGroup && networkGroup.content) {
const proxyFieldWrapper = networkGroup.content.querySelector('[data-field-id="proxy_url"]');
if (proxyFieldWrapper) {
proxyFieldWrapper.style.display = proxyEnabled ? 'grid' : 'none';
}
}
};
// 更新提供商特定的UI
const updateProviderSpecificUI = () => {
if (!elements['ai_provider'] || !elements['ai_api_url'] || !modelSelector || !modelSelector.input) return;
const provider = elements['ai_provider'].value;
const apiUrlInput = elements['ai_api_url'];
const modelInput = modelSelector.input;
if (provider === 'gemini') {
apiUrlInput.placeholder = '通常留空,会自动使用谷歌官方地址';
modelInput.placeholder = '例如: gemini-1.5-flash-latest';
if (apiUrlInput.value.includes('openai.com')) {
apiUrlInput.value = '';
}
} else { // openai
apiUrlInput.placeholder = '例如: https://api.openai.com/v1/chat/completions';
modelInput.placeholder = '例如: gpt-3.5-turbo';
if (!apiUrlInput.value) {
apiUrlInput.value = SETTINGS_DEFAULTS.ai_api_url;
}
}
};
// 渲染分类列表
const renderCategories = () => {
if (!categoryManager || !categoryManager.categoryList) return;
categoryManager.categoryList.innerHTML = '';
currentCategories.forEach(cat => {
const categoryItem = document.createElement('div');
categoryItem.className = 'nqs-category-item';
categoryItem.innerHTML = `
<span class="nqs-category-name">${cat}</span>
<button class="nqs-category-delete-btn" data-category="${cat}">
<span>×</span>
</button>
`;
categoryManager.categoryList.appendChild(categoryItem);
});
};
// 添加分类
const addCategoryAction = () => {
if (!categoryManager || !categoryManager.input) return;
const newCat = categoryManager.input.value.trim();
if (newCat && !currentCategories.includes(newCat)) {
currentCategories.unshift(newCat);
renderCategories();
categoryManager.input.value = '';
}
};
// 删除分类
const deleteCategory = (category) => {
currentCategories = currentCategories.filter(c => c !== category);
renderCategories();
};
// 重置提示词
const resetPrompt = async () => {
if (!elements['ai_prompt']) return;
if (await showConfirmationModal('恢复默认提示词', '您当前输入的内容将被覆盖。确定要恢复吗?')) {
elements['ai_prompt'].value = SETTINGS_DEFAULTS.ai_prompt;
}
};
// 获取可用模型列表
let availableModels = [], activeOptionIndex = -1;
const renderModelDropdown = (filter = '') => {
if (!modelSelector || !modelSelector.dropdown || !modelSelector.input) return;
const filteredModels = availableModels.filter(model =>
model.toLowerCase().includes(filter.toLowerCase())
);
modelSelector.dropdown.innerHTML = '';
if (filteredModels.length === 0) {
modelSelector.dropdown.classList.remove('is-visible');
return;
}
filteredModels.forEach((model, index) => {
const item = document.createElement('div');
item.className = 'nqs-dropdown-item';
item.textContent = model;
item.dataset.index = index;
item.addEventListener('click', () => {
modelSelector.input.value = model;
modelSelector.dropdown.classList.remove('is-visible');
});
modelSelector.dropdown.appendChild(item);
});
modelSelector.dropdown.classList.add('is-visible');
activeOptionIndex = -1;
};
const updateActiveModelOption = (newIndex) => {
if (!modelSelector || !modelSelector.dropdown) return;
const items = modelSelector.dropdown.querySelectorAll('.nqs-dropdown-item');
if (activeOptionIndex >= 0 && items[activeOptionIndex]) {
items[activeOptionIndex].classList.remove('is-active');
}
if (newIndex >= 0 && items[newIndex]) {
items[newIndex].classList.add('is-active');
items[newIndex].scrollIntoView({ block: 'nearest' });
activeOptionIndex = newIndex;
}
};
// 获取模型列表
const fetchModels = async () => {
if (!elements['ai_provider'] || !elements['ai_api_url'] || !elements['ai_api_key'] ||
!elements['ai_model_fetch_timeout'] || !elements['proxy_enabled'] || !elements['proxy_url'] ||
!modelSelector || !modelSelector.fetchBtn || !modelSelector.input) {
showAlertModal('操作失败', '必要的配置元素未找到。');
return;
}
const provider = elements['ai_provider'].value;
const baseUrl = elements['ai_api_url'].value;
const apiKey = elements['ai_api_key'].value;
const fetchTimeout = elements['ai_model_fetch_timeout'].value;
const proxySettings = {
enabled: elements['proxy_enabled'].querySelector('input').checked,
url: elements['proxy_url'].value
};
if (!apiKey) {
showAlertModal('操作失败', '请先填写 AI API Key。');
return;
}
modelSelector.fetchBtn.classList.add('is-loading');
modelSelector.fetchBtn.disabled = true;
try {
availableModels = await fetchAvailableModels(provider, baseUrl, apiKey, fetchTimeout, proxySettings);
renderModelDropdown(modelSelector.input.value);
await showAlertModal('操作成功', `成功获取 ${availableModels.length} 个可用模型!`);
} catch (error) {
await showAlertModal('操作失败', `无法获取模型列表,请检查您的网络、配置和 API Key 是否正确。\n\n错误详情: ${error.message}`);
} finally {
modelSelector.fetchBtn.classList.remove('is-loading');
modelSelector.fetchBtn.disabled = false;
}
};
// 加载当前设置值
for (const key of Object.keys(SETTINGS_DEFAULTS)) {
const element = elements[key];
if (element) {
const value = await GM_getValue(key, SETTINGS_DEFAULTS[key]);
if (element.type === 'checkbox' || element.querySelector('input[type="checkbox"]')) {
const checkbox = element.querySelector('input[type="checkbox"]') || element;
checkbox.checked = value;
} else if (element.tagName === 'SELECT') {
element.value = value;
} else {
element.value = value;
}
}
}
// 初始化UI状态
renderCategories();
toggleAISectionVisibility();
toggleProxyUrlVisibility();
updateProviderSpecificUI();
// 绑定事件 - 确保所有元素都已创建
setTimeout(() => {
// AI相关事件
if (elements['ai_enabled'] && elements['ai_enabled'].querySelector('input')) {
elements['ai_enabled'].querySelector('input').addEventListener('change', toggleAISectionVisibility);
}
if (elements['proxy_enabled'] && elements['proxy_enabled'].querySelector('input')) {
elements['proxy_enabled'].querySelector('input').addEventListener('change', toggleProxyUrlVisibility);
}
if (elements['ai_provider']) {
elements['ai_provider'].addEventListener('change', updateProviderSpecificUI);
}
// 分类管理事件
if (categoryManager && categoryManager.addBtn) {
categoryManager.addBtn.addEventListener('click', addCategoryAction);
}
if (categoryManager && categoryManager.input) {
categoryManager.input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
addCategoryAction();
}
});
}
// 分类删除事件委托
if (categoryManager && categoryManager.categoryList) {
categoryManager.categoryList.addEventListener('click', (e) => {
if (e.target.closest('.nqs-category-delete-btn')) {
const btn = e.target.closest('.nqs-category-delete-btn');
const category = btn.dataset.category;
deleteCategory(category);
}
});
}
// 重置提示词事件
const resetPromptBtn = footer.querySelector('#nqs-reset-prompt');
if (resetPromptBtn) {
resetPromptBtn.addEventListener('click', resetPrompt);
}
// 模型选择器事件
if (modelSelector && modelSelector.fetchBtn) {
modelSelector.fetchBtn.addEventListener('click', fetchModels);
}
if (modelSelector && modelSelector.input) {
modelSelector.input.addEventListener('focus', () => {
if (availableModels.length > 0) renderModelDropdown(modelSelector.input.value);
});
modelSelector.input.addEventListener('input', () => {
renderModelDropdown(modelSelector.input.value);
});
modelSelector.input.addEventListener('keydown', (e) => {
const items = modelSelector.dropdown.querySelectorAll('.nqs-dropdown-item');
if (!modelSelector.dropdown.classList.contains('is-visible') || items.length === 0) return;
switch(e.key) {
case 'ArrowDown':
e.preventDefault();
updateActiveModelOption(activeOptionIndex < items.length - 1 ? activeOptionIndex + 1 : 0);
break;
case 'ArrowUp':
e.preventDefault();
updateActiveModelOption(activeOptionIndex > 0 ? activeOptionIndex - 1 : items.length - 1);
break;
case 'Enter':
e.preventDefault();
if (activeOptionIndex >= 0 && items[activeOptionIndex]) {
items[activeOptionIndex].click();
}
break;
case 'Escape':
modelSelector.dropdown.classList.remove('is-visible');
break;
}
});
}
// 点击外部关闭下拉
if (modelSelector && modelSelector.input) {
document.addEventListener('click', (e) => {
if (!modelSelector.input.parentNode.contains(e.target)) {
modelSelector.dropdown.classList.remove('is-visible');
}
});
}
}, 100);
// 保存设置
saveButton.addEventListener('click', async () => {
// 使用当前选择的模板作为 ai_prompt
try {
const tpls = JSON.parse(await GM_getValue('ai_prompt_templates', SETTINGS_DEFAULTS.ai_prompt_templates) || '[]');
const activeId = await GM_getValue('ai_active_template_id', SETTINGS_DEFAULTS.ai_active_template_id);
const activeTpl = (tpls.find(t => t.id === activeId) || tpls.find(t => t.id === 'default'));
const content = (activeTpl && activeTpl.content) ? activeTpl.content : SETTINGS_DEFAULTS.ai_prompt;
const missing = [];
if (!/\{page_metadata\}/.test(content)) missing.push('{page_metadata}');
if (!/\{categories\}/.test(content)) missing.push('{categories}');
if (!/\{optional_body_section\}/.test(content)) missing.push('{optional_body_section}');
if (missing.length) {
await showAlertModal('占位符缺失', `当前使用的模板缺少必需占位符:${missing.join(', ')}`);
return;
}
if (!/\{page_metadata\}/.test(content)) {
await showAlertModal('占位符缺失', '当前使用的模板缺少必需占位符 {page_metadata}');
return;
}
await GM_setValue('ai_prompt', content);
} catch (e) {
console.warn('Failed to apply active template, fallback to default.', e);
await GM_setValue('ai_prompt', SETTINGS_DEFAULTS.ai_prompt);
}
for (const key of Object.keys(SETTINGS_DEFAULTS)) {
const element = elements[key];
if (element) {
let value;
if (element.type === 'checkbox' || element.querySelector('input[type="checkbox"]')) {
const checkbox = element.querySelector('input[type="checkbox"]') || element;
value = checkbox.checked;
} else {
value = element.value;
}
// ai_prompt 已由模板系统统一保存
if (key !== 'user_categories' && key !== 'ai_prompt') {
await GM_setValue(key, value);
}
}
}
await GM_setValue('user_categories', JSON.stringify(currentCategories));
await applyTheme();
notificationManager.showSuccessNotification('设置保存成功', '您的设置已成功保存并应用');
close();
initFloatingButtons();
});
cancelButton.addEventListener('click', close);
}
async function openLogViewerPanel() {
closeAllNQSPopups();
const allLogs = JSON.parse(await GM_getValue('nqs_logs', '[]'));
const { body, footer, close } = createBasePanel('📋 操作日志', '', { maxWidth: '1000px', panelClass: 'nqs-panel--log-viewer' });
// 创建日志统计卡片区域
const statsContainer = document.createElement('div');
statsContainer.className = 'nqs-log-stats';
// 统计不同类型的日志数量
const stats = allLogs.reduce((acc, log) => {
const level = log.level || 'info';
acc[level] = (acc[level] || 0) + 1;
acc.total++;
return acc;
}, { total: 0, info: 0, error: 0, debug: 0 });
statsContainer.innerHTML = `
<div class="nqs-stat-card nqs-stat-total">
<div class="nqs-stat-icon">📊</div>
<div class="nqs-stat-content">
<div class="nqs-stat-number">${stats.total}</div>
<div class="nqs-stat-label">总记录</div>
</div>
</div>
<div class="nqs-stat-card nqs-stat-success">
<div class="nqs-stat-icon">✅</div>
<div class="nqs-stat-content">
<div class="nqs-stat-number">${stats.info || 0}</div>
<div class="nqs-stat-label">成功操作</div>
</div>
</div>
<div class="nqs-stat-card nqs-stat-error">
<div class="nqs-stat-icon">❌</div>
<div class="nqs-stat-content">
<div class="nqs-stat-number">${stats.error || 0}</div>
<div class="nqs-stat-label">失败记录</div>
</div>
</div>
<div class="nqs-stat-card nqs-stat-debug">
<div class="nqs-stat-icon">🔧</div>
<div class="nqs-stat-content">
<div class="nqs-stat-number">${stats.debug || 0}</div>
<div class="nqs-stat-label">调试信息</div>
</div>
</div>
`;
body.appendChild(statsContainer);
// 创建过滤和搜索栏
const filterContainer = document.createElement('div');
filterContainer.className = 'nqs-log-filter-bar';
filterContainer.innerHTML = `
<div class="nqs-filter-left">
<div class="nqs-filter-group">
<button class="nqs-filter-btn active" data-filter="all">
<span class="nqs-filter-icon">📋</span>
<span>全部</span>
</button>
<button class="nqs-filter-btn" data-filter="info">
<span class="nqs-filter-icon">✅</span>
<span>成功</span>
</button>
<button class="nqs-filter-btn" data-filter="error">
<span class="nqs-filter-icon">❌</span>
<span>错误</span>
</button>
</div>
</div>
<div class="nqs-filter-right">
<div class="nqs-search-box">
<input type="text" id="nqs-log-search" placeholder="搜索日志消息..." class="nqs-search-input">
<span class="nqs-search-icon">🔍</span>
</div>
<div class="nqs-toggle-group">
<label class="nqs-toggle-label">
<input type="checkbox" id="nqs-show-debug" class="nqs-toggle-input">
<span class="nqs-toggle-slider"></span>
<span class="nqs-toggle-text">调试模式</span>
</label>
</div>
</div>
`;
body.appendChild(filterContainer);
// 创建表格容器
const tableContainer = document.createElement('div');
tableContainer.className = 'nqs-table-container';
body.appendChild(tableContainer);
let currentFilter = 'all', showDebug = false, searchTerm = '';
const rerenderTable = () => {
const filteredLogs = allLogs.filter(log => {
const level = (log.level || 'info').toLowerCase();
const matchesFilter = (currentFilter === 'all' || level === currentFilter || (!log.level && currentFilter === 'info'));
const matchesDebug = (showDebug || level !== 'debug');
const matchesSearch = (!searchTerm || log.message.toLowerCase().includes(searchTerm.toLowerCase()));
return matchesFilter && matchesDebug && matchesSearch;
}).slice(0, 100);
if (filteredLogs.length === 0) {
tableContainer.innerHTML = `
<div class="nqs-empty-state">
<div class="nqs-empty-icon">📝</div>
<div class="nqs-empty-title">暂无日志记录</div>
<div class="nqs-empty-message">当前筛选条件下没有找到相关的日志记录</div>
</div>
`;
return;
}
const cardsHTML = `
<div class="nqs-log-cards">
${filteredLogs.map((log, index) => {
const originalIndex = allLogs.indexOf(log);
const level = log.level || 'info';
const displayMessage = log.level ? log.message : `[Legacy] Saved '${log.title}' as '${log.result || 'N/A'}'`;
const isLegacy = level === 'info' && !log.level;
const levelIcons = {
'info': '✅',
'error': '❌',
'debug': '🔧',
'warn': '⚠️'
};
const levelColors = {
'info': '#34C759',
'error': '#FF3B30',
'debug': '#8E8E93',
'warn': '#FF9500'
};
return `
<div class="nqs-log-card nqs-log-card--${level}" data-log-index="${originalIndex}">
<div class="nqs-log-card-header">
<div class="nqs-log-level-badge" style="background: ${levelColors[level]}20; color: ${levelColors[level]};">
<span class="nqs-log-level-icon">${levelIcons[level] || '📋'}</span>
<span class="nqs-log-level-text">${getLogLevelText(level, isLegacy)}</span>
</div>
<div class="nqs-log-time">${formatLogTime(log.timestamp)}</div>
</div>
<div class="nqs-log-card-body">
<div class="nqs-log-message">${displayMessage}</div>
</div>
<div class="nqs-log-card-footer">
<button class="nqs-log-detail-btn" data-log-index="${originalIndex}">
<span>🔍</span> 查看详情
</button>
</div>
</div>
`;
}).join('')}
</div>
`;
setSafeInnerHTML(tableContainer, cardsHTML);
};
// 格式化时间显示
window.formatLogTime = (timestamp) => {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) return '刚刚';
if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前`;
if (diff < 2592000000) return `${Math.floor(diff / 86400000)}天前`;
return date.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
};
// 获取日志级别文本
window.getLogLevelText = (level, isLegacy = false) => {
if (isLegacy) return 'LEGACY';
const levelMap = {
'info': 'INFO',
'error': 'ERROR',
'debug': 'DEBUG',
'warn': 'WARN'
};
return levelMap[level] || level.toUpperCase();
};
// 创建日志详情弹窗函数
const showLogDetailModal = (log) => {
const { body: detailBody, footer: detailFooter, close: closeDetail } = createBasePanel(
'📋 日志详情',
'',
{ maxWidth: '800px', panelClass: 'nqs-panel--log-detail', isNested: true }
);
// 格式化日志数据
const logData = {
时间: new Date(log.timestamp).toLocaleString('zh-CN'),
级别: log.level ? log.level.toUpperCase() : 'LEGACY',
消息: log.message || (log.title ? `[Legacy] Saved '${log.title}' as '${log.result || 'N/A'}'` : '无消息'),
...(log.context && typeof log.context === 'object' ? { 上下文: log.context } : {}),
...(log.title && !log.level ? { 标题: log.title } : {}),
...(log.result && !log.level ? { 结果: log.result } : {}),
...(log.url && !log.level ? { 链接: log.url } : {})
};
detailBody.innerHTML = `
<div class="nqs-log-detail-content">
<div class="nqs-json-viewer">
<pre class="nqs-json-code">${JSON.stringify(logData, null, 2)}</pre>
</div>
</div>
`;
setSafeInnerHTML(detailFooter, `
<div class="nqs-log-detail-footer">
<button class="nqs-button nqs-button-secondary" id="copy-log-data">
<span>📋</span> 复制数据
</button>
<button class="nqs-button nqs-button-primary" id="close-log-detail">
<span>✅</span> 关闭
</button>
</div>
`);
// 绑定事件
detailFooter.querySelector('#copy-log-data').addEventListener('click', () => {
navigator.clipboard.writeText(JSON.stringify(logData, null, 2)).then(() => {
notificationManager.showSuccessNotification('复制成功', '日志数据已复制到剪贴板');
}).catch(() => {
notificationManager.showErrorNotification('复制失败', '无法访问剪贴板');
});
});
detailFooter.querySelector('#close-log-detail').addEventListener('click', closeDetail);
// 显示弹窗
setTimeout(() => {
document.querySelector('.nqs-overlay:last-child').classList.add('visible');
}, 10);
};
// 绑定卡片点击事件
tableContainer.addEventListener('click', (e) => {
if (e.target.classList.contains('nqs-log-detail-btn') || e.target.closest('.nqs-log-detail-btn')) {
const btn = e.target.classList.contains('nqs-log-detail-btn') ? e.target : e.target.closest('.nqs-log-detail-btn');
const logIndex = btn.dataset.logIndex;
const log = allLogs[logIndex];
if (log) {
showLogDetailModal(log);
}
}
});
// 绑定过滤事件
filterContainer.querySelector('.nqs-filter-group').addEventListener('click', (e) => {
if (!e.target.closest('.nqs-filter-btn')) return;
const btn = e.target.closest('.nqs-filter-btn');
filterContainer.querySelectorAll('.nqs-filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentFilter = btn.dataset.filter;
rerenderTable();
});
filterContainer.querySelector('#nqs-show-debug').addEventListener('change', (e) => {
showDebug = e.target.checked;
rerenderTable();
});
filterContainer.querySelector('#nqs-log-search').addEventListener('input', (e) => {
searchTerm = e.target.value.trim();
rerenderTable();
});
// 底部操作按钮
setSafeInnerHTML(footer, `
<div class="nqs-log-footer-actions">
<div class="nqs-log-footer-left">
<button class="nqs-button nqs-button-secondary" id="export-logs">
<span>📤</span> 导出日志
</button>
<button class="nqs-button nqs-button-danger" id="clear-logs">
<span>🗑️</span> 清空日志
</button>
</div>
<div class="nqs-log-footer-right">
<button class="nqs-button nqs-button-primary" id="close-logs">
<span>✅</span> 关闭
</button>
</div>
</div>
`);
footer.querySelector('#clear-logs').addEventListener('click', async () => {
const confirmed = await showConfirmationModal('确认清空日志', '此操作不可撤销,您确定要删除所有日志记录吗?', { danger: true, confirmText: '确认清空' });
if (confirmed) {
await GM_setValue('nqs_logs', '[]');
notificationManager.showSuccessNotification('日志已清空', '所有日志记录已成功清除');
close();
}
});
footer.querySelector('#export-logs').addEventListener('click', () => {
const dataStr = JSON.stringify(allLogs, null, 2);
const dataBlob = new Blob([dataStr], {type: 'application/json'});
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `notion-ai-logs-${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
notificationManager.showSuccessNotification('日志已导出', '日志文件已成功下载到本地');
});
footer.querySelector('#close-logs').addEventListener('click', close);
rerenderTable();
}
async function openCategorySelectorPanel(pageTitle, pageUrl, uiContext) {
closeAllNQSPopups();
const settings = await loadAllSettings();
let categories = JSON.parse(settings.user_categories);
if (settings.read_later_enabled && settings.read_later_category) { categories = categories.filter(c => c !== settings.read_later_category); }
const { body, close } = createBasePanel('选择或创建分类', '', { maxWidth: '560px' });
setSafeInnerHTML(body, `<div id="nqs-selector-list">${categories.map(cat => `<button class="nqs-button nqs-button-secondary" data-cat="${cat}">${cat}</button>`).join('')}</div><div class="nqs-category-manager"><input type="text" class="nqs-input" id="nqs-new-cat-input" placeholder="或输入新分类..."><button id="nqs-save-new-cat" class="nqs-button nqs-button-primary">添加并保存</button></div>`);
const doSave = async (category) => {
if (!category) return; close();
const saveButton = document.getElementById('nqs-save-button'); const originalText = '➤ Notion';
if (uiContext.source === 'fab' && saveButton) showNotification(saveButton, `🚀 保存为: ${category}`, true, originalText); else console.log(`NQS: [Menu] 🚀 即将保存为: ${category}`);
try {
const notionResponse = await saveToNotion(settings.notion_api_key, settings.database_id, pageTitle, pageUrl, category, settings);
// 记录手动分类学习数据(如果之前有AI建议的话)
const domain = new URL(pageUrl).hostname;
const pageMetadata = getHighSignalPageData(document);
await recordCategoryLearning(domain, pageMetadata, category, null);
await addLog('info', `页面已手动保存 (${uiContext.source})`, { title: pageTitle, url: pageUrl, result: category });
if (uiContext.source === 'fab' && saveButton) showNotification(saveButton, notionResponse, false, originalText); else console.log(`NQS: [Menu] ${notionResponse}`);
} catch (error) {
if (uiContext.source === 'fab' && saveButton) showNotification(saveButton, error.message, false, originalText); else console.error(`NQS: [Menu] 保存失败!错误信息: ${error.message}`);
await addLog('error', '手动保存至Notion失败', { title: pageTitle, url: pageUrl, category: category, error: error.message, stack: error.stack });
}
};
body.querySelector('#nqs-selector-list').addEventListener('click', e => { if (e.target.tagName === 'BUTTON' && e.target.dataset.cat) doSave(e.target.dataset.cat); });
const newCatInputAction = async () => { const newCat = body.querySelector('#nqs-new-cat-input').value.trim(); if (newCat && !categories.includes(newCat)) { const allCategories = JSON.parse(await GM_getValue('user_categories', SETTINGS_DEFAULTS.user_categories)); allCategories.unshift(newCat); await GM_setValue('user_categories', JSON.stringify(allCategories)); } doSave(newCat); };
body.querySelector('#nqs-save-new-cat').addEventListener('click', newCatInputAction);
body.querySelector('#nqs-new-cat-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); newCatInputAction(); } });
}
// ===================================================================
// ====================== API 调用与主逻辑 =========================
// ===================================================================
// ===================================================================
// ====================== AI分类学习功能 =============================
// ===================================================================
async function recordCategoryLearning(domain, pageMetadata, userChoice, aiSuggestion) {
if (!await GM_getValue('ai_learning_enabled', SETTINGS_DEFAULTS.ai_learning_enabled)) return;
let learningData = [];
try {
learningData = JSON.parse(await GM_getValue('nqs_learning_data', '[]') || '[]');
} catch (e) {
console.error("NQS - 解析学习数据失败:", e);
learningData = [];
}
learningData.unshift({
timestamp: Date.now(),
domain: domain,
metadata: pageMetadata,
userChoice: userChoice,
aiSuggestion: aiSuggestion
});
// 保持最近1000条记录
if (learningData.length > 1000) learningData.splice(1000);
await GM_setValue('nqs_learning_data', JSON.stringify(learningData));
}
async function getDomainLearningContext(domain) {
try {
const learningData = JSON.parse(await GM_getValue('nqs_learning_data', '[]') || '[]');
const domainData = learningData.filter(item => item.domain === domain).slice(0, 10);
if (domainData.length === 0) return '';
const corrections = domainData.filter(item => item.userChoice !== item.aiSuggestion);
if (corrections.length === 0) return '';
return `\n\n# 历史分类学习 (${domain})\n用户在此域名下的历史修正:\n` +
corrections.map(c => `- "${c.metadata.split('\n')[1]}" → 用户选择: ${c.userChoice} (AI建议: ${c.aiSuggestion})`).join('\n');
} catch (e) {
console.error("NQS - 获取学习上下文失败:", e);
return '';
}
}
// ===================================================================
// ====================== 增强的AI分类功能 ============================
// ===================================================================
function determineCategoryAI(settings, pageMetadata, pageBodyText) {
return new Promise(async (resolve, reject) => {
const { ai_provider, ai_api_key, ai_api_url, ai_model, ai_prompt, user_categories, ai_include_body, ai_timeout, ai_retry_count, read_later_enabled, read_later_category, proxy_enabled, proxy_url } = settings;
if (!ai_model) return reject(new Error("AI Model 未在设置中指定"));
if (!ai_api_key) return reject(new Error("AI API Key 未在设置中指定"));
let categories = JSON.parse(user_categories);
if (read_later_enabled && read_later_category) { categories = categories.filter(c => c !== read_later_category); }
// 添加学习上下文
const domain = new URL(pageMetadata.split('\n')[0].replace('Page URL: ', '')).hostname;
const learningContext = await getDomainLearningContext(domain);
const bodySectionTemplate = `\n\n## (可选) 辅助正文快照:\n"""\n{page_body_text}\n"""`;
const optionalBodySection = ai_include_body ? bodySectionTemplate.replace('{page_body_text}', pageBodyText) : "N/A";
const finalPrompt = ai_prompt.replace('{categories}', JSON.stringify(categories)).replace('{page_metadata}', pageMetadata).replace('{optional_body_section}', optionalBodySection) + learningContext;
// 重试逻辑
const makeAIRequest = (attempt = 1) => {
return new Promise((resolveRequest, rejectRequest) => {
addLog('debug', `发送请求至AI (第${attempt}次尝试)`, { provider: ai_provider, model: ai_model, attempt });
let requestDetails = {
method: 'POST',
url: '',
headers: { 'Content-Type': 'application/json' },
data: '',
timeout: parseInt(ai_timeout, 10) || 20000,
ontimeout: () => rejectRequest(new Error("AI分析超时")),
onload: null,
onerror: () => rejectRequest(new Error('AI网络请求失败'))
};
if (ai_provider === 'gemini') {
const geminiUrl = `https://generativelanguage.googleapis.com/v1/models/${ai_model}:generateContent?key=${ai_api_key}`;
requestDetails.url = (proxy_enabled && proxy_url) ? proxy_url : geminiUrl;
if (proxy_enabled && proxy_url) { requestDetails.headers['Authorization'] = `Bearer ${ai_api_key}`; }
requestDetails.data = JSON.stringify({
contents: [{ parts: [{ text: finalPrompt }] }],
generationConfig: { temperature: 0.0, maxOutputTokens: 100 },
safetySettings: [
{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" },
{ category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_NONE" },
{ category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_NONE" },
{ category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_NONE" }
]
});
requestDetails.onload = (response) => {
if (response.status >= 200 && response.status < 300) {
try {
const result = JSON.parse(response.responseText);
if (!result.candidates || result.candidates.length === 0) {
if (result.promptFeedback && result.promptFeedback.blockReason) {
throw new Error(`AI请求被拒绝: ${result.promptFeedback.blockReason}`);
}
throw new Error("AI响应中缺少 'candidates' 字段");
}
const rawResponse = (result.candidates[0]?.content?.parts[0]?.text || "").trim().replace(/["'「」`*]/g, '');
if (!rawResponse) throw new Error("AI返回了空响应。");
// 计算置信度(简单实现)
const confidence = categories.includes(rawResponse) ? 0.9 : 0.3;
const finalCategory = categories.includes(rawResponse) ? rawResponse : "其他";
if (finalCategory === "其他") console.warn("NQS - AI返回意外分类,回退至'其他'. Raw:", rawResponse);
resolveRequest({ category: finalCategory, rawResponse, confidence, fullApiResponse: result, domain });
} catch (e) {
rejectRequest(new Error(`解析AI响应失败: ${e.message}`));
}
} else {
rejectRequest(new Error(`AI接口错误: ${response.status} ${response.statusText}. 响应: ${response.responseText}`));
}
};
} else { // OpenAI
requestDetails.url = (proxy_enabled && proxy_url) ? proxy_url : ai_api_url;
requestDetails.headers['Authorization'] = `Bearer ${ai_api_key}`;
requestDetails.data = JSON.stringify({
model: ai_model,
messages: [{ role: 'user', content: finalPrompt }],
temperature: 0.0,
max_tokens: 50
});
requestDetails.onload = (response) => {
if (response.status >= 200 && response.status < 300) {
try {
const result = JSON.parse(response.responseText);
if (!result.choices || result.choices.length === 0) throw new Error("AI响应中缺少 'choices' 字段");
const rawResponse = (result.choices[0].message.content || "").trim().replace(/["'「」]/g, '');
if (!rawResponse) throw new Error("AI返回了空响应。");
// 计算置信度(基于简单匹配,因为logprobs可能不被所有API支持)
const confidence = categories.includes(rawResponse) ? 0.9 : 0.3;
const finalCategory = categories.includes(rawResponse) ? rawResponse : "其他";
if (finalCategory === "其他") console.warn("NQS - AI返回意外分类,回退至'其他'. Raw:", rawResponse);
resolveRequest({ category: finalCategory, rawResponse, confidence, fullApiResponse: result, domain });
} catch (e) {
rejectRequest(new Error(`解析AI响应失败: ${e.message}`));
}
} else {
rejectRequest(new Error(`AI接口错误: ${response.status} ${response.statusText}. 响应: ${response.responseText}`));
}
};
}
GM_xmlhttpRequest(requestDetails);
});
};
// 执行带重试的AI请求
const maxRetries = parseInt(ai_retry_count, 10) || 2;
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
try {
const result = await makeAIRequest(attempt);
resolve(result);
return;
} catch (error) {
if (attempt === maxRetries + 1) {
reject(error);
return;
}
addLog('debug', `AI请求失败,准备重试`, { attempt, error: error.message });
// 等待一段时间后重试
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
});
}
function saveToNotion(notionKey, dbId, title, url, category, settings, content = null) {
return new Promise((resolve, reject) => {
if (!notionKey || !dbId) return reject(new Error("Notion Key或DB ID未在设置中指定"));
const properties = {
[settings.prop_name_title]: { title: [{ text: { content: title } }] },
[settings.prop_name_url]: { url: url },
[settings.prop_name_category]: { select: { name: category } }
};
const requestBody = {
parent: { database_id: dbId },
properties
};
// 如果有内容,添加到页面body中
if (content) {
requestBody.children = [{
object: "block",
type: "paragraph",
paragraph: {
rich_text: [{
type: "text",
text: {
content: content.substring(0, 2000) // Notion限制单个文本块长度
}
}]
}
}];
}
GM_xmlhttpRequest({
method: 'POST',
url: 'https://api.notion.com/v1/pages',
headers: {
'Authorization': `Bearer ${notionKey}`,
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28'
},
data: JSON.stringify(requestBody),
onload: (response) => {
if (response.status >= 200 && response.status < 300) {
resolve('✅ 成功保存到 Notion!');
} else {
try {
const error = JSON.parse(response.responseText);
console.error('NQS - Notion API Error:', error);
reject(new Error(`Notion保存失败: ${error.message}`));
} catch (e) {
reject(new Error(`Notion保存失败: ${response.status} ${response.statusText}. 响应: ${response.responseText}`));
}
}
},
onerror: () => reject(new Error('❌ Notion网络请求失败!'))
});
});
}
function fetchAvailableModels(provider, baseUrl, apiKey, timeout, proxySettings) {
return new Promise((resolve, reject) => {
let finalUrl, headers = { 'Content-Type': 'application/json' };
if (proxySettings.enabled && proxySettings.url) {
if (provider === 'gemini') {
finalUrl = `${proxySettings.url}`;
if (!finalUrl.endsWith('/models')) finalUrl += (finalUrl.endsWith('/') ? 'v1beta/models' : '/v1beta/models');
} else { // openai
finalUrl = `${proxySettings.url}`;
if (!finalUrl.endsWith('/models')) finalUrl += (finalUrl.endsWith('/') ? 'models' : '/models');
}
headers['Authorization'] = `Bearer ${apiKey}`;
} else {
if (provider === 'gemini') {
finalUrl = `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`;
} else { // openai
if (!baseUrl) return reject(new Error("未提供 OpenAI API Endpoint"));
try { const tempUrl = new URL(baseUrl); const pathParts = tempUrl.pathname.split('/').filter(Boolean); const v1Index = pathParts.indexOf('v1'); if (v1Index === -1) { finalUrl = `${tempUrl.origin}/v1/models`; } else { const basePath = pathParts.slice(0, v1Index + 1).join('/'); finalUrl = `${tempUrl.origin}/${basePath}/models`; }
} catch (e) { return reject(new Error(`无效的 Endpoint URL: ${e.message}`)); }
headers['Authorization'] = `Bearer ${apiKey}`;
}
}
GM_xmlhttpRequest({
method: 'GET', url: finalUrl, headers: headers, timeout: parseInt(timeout, 10) || 10000,
ontimeout: () => reject(new Error('获取模型列表超时')),
onload: (response) => {
if (response.status >= 200 && response.status < 300) {
try {
const result = JSON.parse(response.responseText); let modelIds;
if (provider === 'gemini') { if (!result.models || !Array.isArray(result.models)) throw new Error("API响应格式不正确,缺少 'models' 数组。"); modelIds = result.models.filter(m => m.supportedGenerationMethods.includes('generateContent')).map(m => m.name.replace('models/', '')).sort();
} else { if (!result.data || !Array.isArray(result.data)) throw new Error("API响应格式不正确,缺少 'data' 数组。"); modelIds = result.data.map(model => model.id).sort(); }
resolve(modelIds);
} catch (e) { reject(new Error(`解析模型列表响应失败: ${e.message}`)); }
} else { reject(new Error(`API错误: ${response.status} ${response.statusText}. 响应: ${response.responseText}`)); }
},
onerror: (e) => reject(new Error('网络请求失败,请检查网络或浏览器控制台。'))
});
});
}
// ===================================================================
// ====================== 增强通知系统 ===============================
// ===================================================================
class NotificationManager {
constructor() {
this.permission = null;
this.toastContainer = null;
this.fabNotifications = new Map(); // 存储FAB按钮通知状态
this.checkPermission();
this.createToastContainer();
}
async checkPermission() {
if ('Notification' in window) {
this.permission = Notification.permission;
if (this.permission === 'default') {
this.permission = await Notification.requestPermission();
}
}
}
createToastContainer() {
if (this.toastContainer) return;
this.toastContainer = document.createElement('div');
this.toastContainer.className = 'nqs-toast-container';
const globalContainer = document.getElementById('NQS_GLOBAL_CONTAINER');
globalContainer.appendChild(this.toastContainer);
}
async showToast(title, message, type = 'info', options = {}) {
const settings = await loadAllSettings();
// 检查通知类型设置
if (type === 'success' && !settings.notification_success_enabled) return;
if (type === 'error' && !settings.notification_error_enabled) return;
const toast = document.createElement('div');
toast.className = `nqs-toast ${type}`;
const iconMap = {
success: '✓',
error: '✕',
warning: '⚠',
info: 'ℹ'
};
toast.innerHTML = `
<div class="nqs-toast-content">
<div class="nqs-toast-icon">${iconMap[type] || iconMap.info}</div>
<div class="nqs-toast-text">
<div class="nqs-toast-title">${title}</div>
${message ? `<div class="nqs-toast-message">${message}</div>` : ''}
</div>
</div>
<button class="nqs-toast-close" aria-label="关闭">×</button>
`;
this.toastContainer.appendChild(toast);
// 添加关闭事件
const closeBtn = toast.querySelector('.nqs-toast-close');
closeBtn.addEventListener('click', () => this.hideToast(toast));
// 显示动画
setTimeout(() => {
toast.classList.add('visible');
}, 10);
// 自动隐藏
const duration = options.duration || (type === 'error' ? 6000 : 4000);
setTimeout(() => {
this.hideToast(toast);
}, duration);
// 同时显示浏览器通知(如果启用)
if (settings.notification_enabled && this.permission === 'granted') {
this.showBrowserNotification(title, message, type);
}
}
hideToast(toast) {
toast.classList.remove('visible');
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}
async showBrowserNotification(title, message, type) {
const iconMap = {
success: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzEwYjk4MSI+PHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bTQgOGwtNiA2LTMtMy0xLjQxIDEuNDFMMTAgMTQgMTcuNTkgNi40MSAxNiA1IDEwIDExWiIvPjwvc3ZnPg==',
error: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iI2VmNDQ0NCI+PHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bTEgMTVoLTJ2LTZoMnY2em0wLThoLTJWN2gydjJaIi8+PC9zdmc+',
warning: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iI2Y1OWUwYiI+PHBhdGggZD0iTTEgMjFoMjJMMTIgMiAxIDIxWm0xMi0zaC0ydi0yaDJWMThabS0yLTRoMlY5aC0yVjE0WiIvPjwvc3ZnPg==',
info: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzNiODJmNiI+PHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bTEgMTVoLTJ2LTZoMnY2em0wLThoLTJWN2gydjJaIi8+PC9zdmc+'
};
new Notification(title, {
body: message,
icon: iconMap[type] || iconMap.info,
tag: 'nqs-notification',
requireInteraction: false,
silent: false
});
}
showSuccessNotification(title, message = '') {
return this.showToast(title, message, 'success');
}
showErrorNotification(title, message = '') {
return this.showToast(title, message, 'error');
}
showWarningNotification(title, message = '') {
return this.showToast(title, message, 'warning');
}
showInfoNotification(title, message = '') {
return this.showToast(title, message, 'info');
}
// 统一的FAB按钮通知方法
showFabNotification(button, message, isLoading = false, originalText = '') {
if (!button) return;
const buttonId = button.id || 'unknown';
// 清除之前的定时器
if (this.fabNotifications.has(buttonId)) {
clearTimeout(this.fabNotifications.get(buttonId));
}
// 更新按钮文本和状态
button.textContent = message;
if (isLoading) {
button.classList.add('loading');
button.disabled = true;
} else {
button.classList.remove('loading');
button.disabled = false;
// 如果不是加载状态,3秒后恢复原始文本
if (originalText) {
const timeoutId = setTimeout(() => {
button.textContent = originalText;
this.fabNotifications.delete(buttonId);
}, 3000);
this.fabNotifications.set(buttonId, timeoutId);
}
}
}
// 统一的通知方法 - 自动选择Toast或FAB
showUnifiedNotification(title, message = '', type = 'info', fabButton = null, originalText = '') {
// 显示Toast通知
this.showToast(title, message, type);
// 如果有FAB按钮,也更新按钮状态
if (fabButton) {
const isLoading = type === 'loading';
this.showFabNotification(fabButton, title, isLoading, originalText);
}
}
}
class ProgressIndicator {
constructor() {
this.container = null;
this.progressBar = null;
this.isShowing = false;
this.createProgressBar();
}
createProgressBar() {
// 创建顶部进度条
this.container = document.createElement('div');
this.container.className = 'nqs-top-progress';
this.container.innerHTML = `
<div class="nqs-progress-bar">
<div class="nqs-progress-fill"></div>
</div>
<div class="nqs-progress-status">
<span class="nqs-progress-text">准备就绪</span>
</div>
`;
const globalContainer = document.getElementById('NQS_GLOBAL_CONTAINER');
globalContainer.appendChild(this.container);
this.progressBar = this.container.querySelector('.nqs-progress-fill');
}
async show(message = '处理中...') {
const settings = await loadAllSettings();
if (!settings.progress_indicator_enabled) return;
if (this.isShowing) return;
this.isShowing = true;
this.updateMessage(message);
this.container.classList.add('visible');
// 开始进度条动画
this.progressBar.style.width = '0%';
this.animateProgress();
}
animateProgress() {
let progress = 0;
const interval = setInterval(() => {
progress += Math.random() * 15;
if (progress > 90) progress = 90;
this.progressBar.style.width = progress + '%';
if (!this.isShowing) {
clearInterval(interval);
}
}, 200);
}
updateMessage(message) {
if (this.container) {
const textEl = this.container.querySelector('.nqs-progress-text');
if (textEl) textEl.textContent = message;
}
}
hide() {
if (!this.isShowing) return;
// 完成进度条
this.progressBar.style.width = '100%';
setTimeout(() => {
this.container.classList.remove('visible');
this.isShowing = false;
// 重置进度条
setTimeout(() => {
this.progressBar.style.width = '0%';
this.updateMessage('准备就绪');
}, 300);
}, 200);
}
}
const notificationManager = new NotificationManager();
const progressIndicator = new ProgressIndicator();
// ===================================================================
// ====================== 批量保存功能 ===============================
// ===================================================================
// ===================================================================
// ====================== 智能收藏夹功能 =============================
// ===================================================================
async function openSmartBookmarkManager() {
closeAllNQSPopups();
const { body, footer, close } = createBasePanel('🚀 智能收藏夹管理', '快速保存和整理网页资源', { maxWidth: '900px', panelClass: 'nqs-panel--bookmark-manager' });
const content = `
<div class="nqs-bookmark-manager">
<!-- 主要功能区域 -->
<div class="nqs-feature-grid">
<div class="nqs-feature-card nqs-feature-primary" data-action="save-session">
<div class="nqs-feature-icon">🖥️</div>
<div class="nqs-feature-content">
<h4>网站会话保存</h4>
<p>智能识别并保存当前网站的相关页面,自动归类整理</p>
<div class="nqs-feature-badge">推荐</div>
</div>
<div class="nqs-feature-arrow">→</div>
</div>
<div class="nqs-feature-card" data-action="save-links">
<div class="nqs-feature-icon">🔗</div>
<div class="nqs-feature-content">
<h4>页面链接提取</h4>
<p>提取页面中的有价值链接,批量保存到Notion</p>
</div>
<div class="nqs-feature-arrow">→</div>
</div>
<div class="nqs-feature-card" data-action="create-reading-list">
<div class="nqs-feature-icon">📚</div>
<div class="nqs-feature-content">
<h4>主题阅读清单</h4>
<p>基于当前内容创建个性化的学习资源清单</p>
</div>
<div class="nqs-feature-arrow">→</div>
</div>
</div>
<!-- 快速分类保存 -->
<div class="nqs-quick-save-section">
<div class="nqs-section-header">
<h3>⚡ 快速分类保存</h3>
<p>一键保存到预设分类</p>
</div>
<div class="nqs-quick-categories">
<button class="nqs-category-btn" data-category="重要参考">
<span class="nqs-category-icon">📌</span>
<span class="nqs-category-name">重要参考</span>
</button>
<button class="nqs-category-btn" data-category="学习教程">
<span class="nqs-category-icon">🎯</span>
<span class="nqs-category-name">学习教程</span>
</button>
<button class="nqs-category-btn" data-category="灵感素材">
<span class="nqs-category-icon">💡</span>
<span class="nqs-category-name">灵感素材</span>
</button>
<button class="nqs-category-btn" data-category="工具资源">
<span class="nqs-category-icon">🛠️</span>
<span class="nqs-category-name">工具资源</span>
</button>
<button class="nqs-category-btn" data-category="技术文档">
<span class="nqs-category-icon">📖</span>
<span class="nqs-category-name">技术文档</span>
</button>
<button class="nqs-category-btn" data-category="设计案例">
<span class="nqs-category-icon">🎨</span>
<span class="nqs-category-name">设计案例</span>
</button>
</div>
</div>
<!-- 页面信息预览 -->
<div class="nqs-page-preview">
<div class="nqs-section-header">
<h3>📄 当前页面信息</h3>
</div>
<div class="nqs-page-info">
<div class="nqs-page-title">${document.title}</div>
<div class="nqs-page-url">${window.location.href}</div>
<div class="nqs-page-meta">
<span class="nqs-meta-item">
<span class="nqs-meta-label">域名:</span>
<span class="nqs-meta-value">${window.location.hostname}</span>
</span>
<span class="nqs-meta-item">
<span class="nqs-meta-label">类型:</span>
<span class="nqs-meta-value">${getPageType()}</span>
</span>
</div>
</div>
</div>
</div>
`;
setSafeInnerHTML(body, content);
setSafeInnerHTML(footer, `
<div class="nqs-bookmark-footer">
<div class="nqs-footer-left">
<button class="nqs-button nqs-button-secondary" id="cancel-bookmark">
<span>❌</span> 取消
</button>
</div>
<div class="nqs-footer-center">
<button class="nqs-button nqs-button-text" id="open-settings">
<span>⚙️</span> 设置收藏规则
</button>
</div>
<div class="nqs-footer-right">
<button class="nqs-button nqs-button-primary" id="save-current-page">
<span>💾</span> 保存当前页面
</button>
</div>
</div>
`);
// 获取页面类型的辅助函数
function getPageType() {
const url = window.location.href.toLowerCase();
const title = document.title.toLowerCase();
if (url.includes('github.com')) return 'GitHub项目';
if (url.includes('stackoverflow.com')) return '技术问答';
if (url.includes('medium.com') || url.includes('dev.to')) return '技术博客';
if (url.includes('youtube.com')) return '视频教程';
if (url.includes('docs.') || title.includes('documentation')) return '技术文档';
if (url.includes('tutorial') || title.includes('tutorial')) return '教程指南';
if (url.includes('news') || url.includes('blog')) return '新闻博客';
return '普通网页';
}
// 绑定主要功能事件
body.querySelector('[data-action="save-session"]').addEventListener('click', () => {
close();
saveCurrentSession();
});
body.querySelector('[data-action="save-links"]').addEventListener('click', () => {
close();
saveDomainLinks();
});
body.querySelector('[data-action="create-reading-list"]').addEventListener('click', () => {
close();
createReadingList();
});
// 绑定快速分类按钮事件
body.querySelectorAll('.nqs-category-btn').forEach(btn => {
btn.addEventListener('click', () => {
const category = btn.dataset.category;
close();
quickSaveWithCategory(category);
});
});
// 绑定底部按钮事件
footer.querySelector('#cancel-bookmark').addEventListener('click', close);
footer.querySelector('#open-settings').addEventListener('click', () => {
close();
openSettingsPanel();
});
footer.querySelector('#save-current-page').addEventListener('click', () => {
close();
startSaveProcess({ source: 'bookmark-manager' });
});
// 添加卡片悬浮效果
body.querySelectorAll('.nqs-feature-card').forEach(card => {
card.addEventListener('mouseenter', () => {
card.style.transform = 'translateY(-4px)';
});
card.addEventListener('mouseleave', () => {
card.style.transform = 'translateY(0)';
});
});
}
async function saveCurrentSession() {
try {
const settings = await loadAllSettings();
if (!settings.notion_api_key || !settings.database_id) {
throw new Error('❌ 配置不完整!');
}
await progressIndicator.show('📚 分析当前会话...');
const currentUrl = window.location.href;
const currentTitle = document.title;
const domain = new URL(currentUrl).hostname;
// 创建会话标题
const sessionTitle = `【会话】${domain} - ${new Date().toLocaleDateString()}`;
// 分析页面内容,提取相关信息
const content = extractAdvancedContent(document);
progressIndicator.updateMessage('🤖 生成会话摘要...');
let sessionSummary = '';
if (settings.ai_enabled && settings.auto_summary_enabled) {
try {
const summaryPrompt = `基于以下网页信息,生成一个会话摘要,说明这个网站/页面的主要价值和学习要点:
网站:${domain}
页面标题:${currentTitle}
页面描述:${content.description}
主要内容:${content.mainContent.substring(0, 1000)}
请生成:
1. 网站/页面的主要价值(1-2句话)
2. 关键学习要点(3-5个要点)
3. 推荐的后续行动(如继续阅读的建议)
格式:简洁的markdown格式`;
const result = await makeSummaryRequest(settings, summaryPrompt);
sessionSummary = result.summary;
} catch (error) {
console.warn('AI摘要生成失败');
}
}
const sessionContent = `# 📚 网站会话记录
**访问时间:** ${new Date().toLocaleString('zh-CN')}
**网站域名:** ${domain}
**当前页面:** [${currentTitle}](${currentUrl})
## 📋 会话摘要
${sessionSummary || '本次会话的主要页面和资源记录'}
## 🎯 页面信息
- **阅读时长:** 约 ${content.readingTime} 分钟
- **页面类型:** ${content.author ? '文章页面' : '信息页面'}
${content.publishTime ? `- **发布时间:** ${content.publishTime}` : ''}
## 🔗 相关链接
${content.links.slice(0, 5).map(link => `- [${link.text}](${link.href})`).join('\n')}
---
*记录时间:${new Date().toLocaleString('zh-CN')}*`;
progressIndicator.updateMessage('💾 保存会话记录...');
// 保存会话记录
await saveToNotion(settings.notion_api_key, settings.database_id, sessionTitle, currentUrl, '学习记录', settings, sessionContent);
progressIndicator.hide();
notificationManager.showSuccessNotification('会话已保存', '当前网站会话记录已保存到Notion');
await addLog('info', '网站会话已保存', {
domain: domain,
title: currentTitle,
linksCount: content.links.length
});
} catch (error) {
progressIndicator.hide();
notificationManager.showErrorNotification('会话保存失败', error.message);
console.error('保存会话失败:', error);
}
}
async function saveDomainLinks() {
try {
const settings = await loadAllSettings();
if (!settings.notion_api_key || !settings.database_id) {
throw new Error('❌ 配置不完整!');
}
await progressIndicator.show('🔍 提取页面链接...');
const currentUrl = window.location.href;
const currentTitle = document.title;
const domain = new URL(currentUrl).hostname;
// 提取所有外部链接
const links = Array.from(document.querySelectorAll('a[href]'))
.filter(link => {
const href = link.href;
return href.startsWith('http') &&
!href.includes(domain) &&
link.textContent.trim().length > 0;
})
.map(link => ({
text: link.textContent.trim(),
href: link.href,
context: link.closest('h1, h2, h3, h4, h5, h6, p, li')?.textContent.trim() || ''
}))
.filter((link, index, array) =>
// 去重
array.findIndex(l => l.href === link.href) === index
)
.slice(0, 20); // 限制数量
if (links.length === 0) {
throw new Error('❌ 未找到有效的外部链接');
}
progressIndicator.updateMessage('📝 整理链接资源...');
const linksTitle = `【链接收集】${currentTitle}`;
const linksContent = `# 🔗 链接资源收集
**来源页面:** [${currentTitle}](${currentUrl})
**收集时间:** ${new Date().toLocaleString('zh-CN')}
**链接数量:** ${links.length}
## 📋 链接列表
${links.map((link, index) => `### ${index + 1}. [${link.text}](${link.href})
${link.context ? `> 上下文:${link.context.substring(0, 100)}${link.context.length > 100 ? '...' : ''}` : ''}
`).join('\n')}
---
*由 Notion AI 助手自动收集整理*`;
progressIndicator.updateMessage('💾 保存链接收集...');
await saveToNotion(settings.notion_api_key, settings.database_id, linksTitle, currentUrl, '资源收集', settings, linksContent);
progressIndicator.hide();
notificationManager.showSuccessNotification('链接已收集', `已收集 ${links.length} 个有价值的链接`);
await addLog('info', '页面链接已收集', {
sourceTitle: currentTitle,
linksCount: links.length
});
} catch (error) {
progressIndicator.hide();
notificationManager.showErrorNotification('链接收集失败', error.message);
console.error('链接收集失败:', error);
}
}
async function createReadingList() {
try {
const settings = await loadAllSettings();
if (!settings.notion_api_key || !settings.database_id) {
throw new Error('❌ 配置不完整!');
}
await progressIndicator.show('📖 创建阅读清单...');
const currentUrl = window.location.href;
const currentTitle = document.title;
const content = extractAdvancedContent(document);
progressIndicator.updateMessage('🤖 AI生成推荐...');
let recommendations = '';
if (settings.ai_enabled) {
try {
const recommendPrompt = `基于以下页面信息,推荐5-8个相关的学习主题和关键词,用于进一步深入学习:
页面标题:${currentTitle}
页面描述:${content.description}
关键词:${content.keywords}
主要内容概要:${content.mainContent.substring(0, 800)}
请提供:
1. 核心学习主题(3-4个)
2. 相关技术栈/概念(4-6个)
3. 推荐的学习路径(简要说明)
格式:markdown列表格式,简洁明了`;
const result = await makeSummaryRequest(settings, recommendPrompt);
recommendations = result.summary;
} catch (error) {
console.warn('AI推荐生成失败');
}
}
const listTitle = `【学习清单】${currentTitle}`;
const listContent = `# 📚 主题学习清单
**起始页面:** [${currentTitle}](${currentUrl})
**创建时间:** ${new Date().toLocaleString('zh-CN')}
## 🎯 当前页面要点
- **阅读时长:** ${content.readingTime} 分钟
- **主要类型:** ${content.description || '知识学习'}
- **关键信息:** ${content.keywords || '待补充'}
## 🚀 推荐学习方向
${recommendations || `基于"${currentTitle}"的内容,建议深入学习以下方向:
### 核心概念深化
- [ ] 相关基础理论
- [ ] 实践应用案例
- [ ] 最佳实践总结
### 扩展学习
- [ ] 相关技术栈
- [ ] 进阶应用
- [ ] 行业应用案例`}
## ✅ 学习计划
- [ ] 完成当前页面学习
- [ ] 查找相关补充资料
- [ ] 实践练习/项目应用
- [ ] 总结学习心得
## 📖 待读资源
${content.links.slice(0, 3).map(link => `- [ ] [${link.text}](${link.href})`).join('\n')}
---
*学习清单由 AI 助手生成,可根据个人需要调整*`;
progressIndicator.updateMessage('💾 保存学习清单...');
await saveToNotion(settings.notion_api_key, settings.database_id, listTitle, currentUrl, '学习计划', settings, listContent);
progressIndicator.hide();
notificationManager.showSuccessNotification('学习清单已创建', '个人化学习计划已保存');
await addLog('info', '学习清单已创建', {
sourceTitle: currentTitle,
hasAIRecommendations: !!recommendations
});
} catch (error) {
progressIndicator.hide();
notificationManager.showErrorNotification('清单创建失败', error.message);
console.error('学习清单创建失败:', error);
}
}
async function quickSaveWithCategory(customCategory) {
try {
const settings = await loadAllSettings();
if (!settings.notion_api_key || !settings.database_id) {
throw new Error('❌ 配置不完整!');
}
await progressIndicator.show(`💾 保存为${customCategory}...`);
const currentUrl = window.location.href;
const currentTitle = document.title;
const quickTitle = `【${customCategory}】${currentTitle}`;
await saveToNotion(settings.notion_api_key, settings.database_id, quickTitle, currentUrl, customCategory, settings);
progressIndicator.hide();
notificationManager.showSuccessNotification('快速保存成功', `已保存为"${customCategory}"`);
await addLog('info', '快速分类保存', {
title: currentTitle,
category: customCategory
});
} catch (error) {
progressIndicator.hide();
notificationManager.showErrorNotification('快速保存失败', error.message);
console.error('快速保存失败:', error);
}
}
// ===================================================================
// ====================== 智能摘录功能 ===============================
// ===================================================================
async function saveSelectedTextAsNote() {
try {
const selectedText = window.getSelection().toString().trim();
if (!selectedText) {
throw new Error('❌ 请先选择要保存的文本内容。');
}
if (selectedText.length < 20) {
throw new Error('❌ 选中的文本太短,建议选择至少20个字符的内容。');
}
const settings = await loadAllSettings();
if (!settings.notion_api_key || !settings.database_id) {
throw new Error('❌ 配置不完整!请在"设置"中填写 Notion API Key 和数据库ID。');
}
await progressIndicator.show('📝 处理选中文本...');
// 获取选中文本的上下文
const selection = window.getSelection();
const range = selection.getRangeAt(0);
const container = range.commonAncestorContainer.nodeType === Node.TEXT_NODE
? range.commonAncestorContainer.parentNode
: range.commonAncestorContainer;
// 尝试获取更多上下文信息
let contextInfo = '';
const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
let nearestHeading = '';
// 找到距离选中文本最近的标题
for (const heading of headings) {
if (container.compareDocumentPosition &&
container.compareDocumentPosition(heading) & Node.DOCUMENT_POSITION_PRECEDING) {
nearestHeading = heading.textContent.trim();
}
}
if (nearestHeading) {
contextInfo = `\n\n**所在章节:** ${nearestHeading}`;
}
// 创建富文本摘录标题
const originalTitle = document.title;
const sourceInfo = `【摘录】${nearestHeading || originalTitle}`;
// 构建摘录内容
let noteContent = '';
if (settings.content_save_mode === 'full' || settings.auto_summary_enabled) {
progressIndicator.updateMessage('🤖 AI分析中...');
// 使用AI生成摘录的背景和要点
try {
const analysisPrompt = `请分析以下文本摘录,并提供:
1. 这段文字的核心观点(1-2句话)
2. 为什么这段话值得摘录(价值分析)
3. 相关的关键词标签(3-5个)
原文标题:${originalTitle}
${nearestHeading ? `章节:${nearestHeading}` : ''}
摘录内容:
${selectedText}
请用以下格式回答:
核心观点:[观点内容]
价值分析:[为什么重要]
关键词:[词1, 词2, 词3]`;
const analysisResult = await makeSummaryRequest(settings, analysisPrompt);
const analysis = analysisResult.summary;
noteContent = `## 📖 文本摘录
**原文链接:** [${originalTitle}](${window.location.href})${contextInfo}
**摘录内容:**
> ${selectedText}
## 🎯 AI 分析
${analysis}
---
*摘录时间:${new Date().toLocaleString('zh-CN')}*`;
} catch (aiError) {
console.warn('AI分析失败,使用基础格式:', aiError);
noteContent = `## 📖 文本摘录
**原文链接:** [${originalTitle}](${window.location.href})${contextInfo}
**摘录内容:**
> ${selectedText}
---
*摘录时间:${new Date().toLocaleString('zh-CN')}*`;
}
} else {
noteContent = selectedText;
}
progressIndicator.updateMessage('🚀 保存到 Notion...');
// 智能分类
let category = "学习笔记";
if (settings.ai_enabled) {
try {
const pageMetadata = `Page URL: ${window.location.href}\nPage Title: ${originalTitle}\nContext: ${nearestHeading}\nSelected Text: ${selectedText.substring(0, 500)}`;
const aiResult = await determineCategoryAI(settings, pageMetadata, selectedText);
category = aiResult.category;
} catch (error) {
console.warn('AI分类失败,使用默认分类');
}
}
// 保存摘录(这里需要扩展saveToNotion函数支持富文本内容)
await saveToNotion(settings.notion_api_key, settings.database_id, sourceInfo, window.location.href, category, settings, noteContent);
progressIndicator.hide();
await addLog('info', '文本摘录已保存', {
originalTitle: originalTitle,
textLength: selectedText.length,
category: category,
hasAnalysis: noteContent.includes('AI 分析')
});
notificationManager.showSuccessNotification('摘录保存成功', `已保存到分类:"${category}"`);
console.log(`NQS: [Menu] ✅ 文本摘录已保存为: ${category}`);
} catch (error) {
progressIndicator.hide();
notificationManager.showErrorNotification('摘录保存失败', error.message);
console.error('NQS: [Menu] 保存文本摘录失败:', error.message);
await addLog('error', '保存文本摘录失败', { error: error.message });
}
}
// ===================================================================
// ====================== 统一的保存逻辑 (Refactored) ===============
// ===================================================================
async function runAiSave(settings, pageTitle, pageUrl, uiContext) {
const saveButton = uiContext.buttonElement;
const originalText = '➤ Notion';
try {
// 显示进度条和状态
await progressIndicator.show('🧠 AI 分类中...');
// 只更新FAB按钮状态,不显示Toast通知
if (uiContext.source === 'fab' && saveButton) {
notificationManager.showFabNotification(saveButton, '🧠 AI 分析中...', true, originalText);
}
const pageMetadata = getHighSignalPageData(document);
const pageBodyText = settings.ai_include_body ? extractMainContent(document).substring(0, 4000) : "N/A";
const aiResult = await determineCategoryAI(settings, pageMetadata, pageBodyText);
const { category, confidence, domain } = aiResult;
// 更新进度
progressIndicator.updateMessage('🚀 保存到 Notion...');
// 显示置信度信息
const confidenceText = confidence ? ` (${Math.round(confidence * 100)}%)` : '';
// 更新FAB按钮状态
if (uiContext.source === 'fab' && saveButton) {
notificationManager.showFabNotification(saveButton, `🚀 保存中...`, true, originalText);
}
const notionResponse = await saveToNotion(settings.notion_api_key, settings.database_id, pageTitle, pageUrl, category, settings);
// 隐藏进度条
progressIndicator.hide();
await addLog('info', `页面已通过AI保存 (${uiContext.source})`, {
title: pageTitle,
url: pageUrl,
result: category,
confidence: confidence,
provider: settings.ai_provider,
domain: domain
});
// 显示成功通知和恢复FAB按钮状态
const confidenceInfo = confidence ? ` (AI置信度: ${Math.round(confidence * 100)}%)` : '';
notificationManager.showSuccessNotification('保存成功', `已保存为"${category}"${confidenceInfo}`);
if (uiContext.source === 'fab' && saveButton) {
notificationManager.showFabNotification(saveButton, '✅ 已保存', false, originalText);
setTimeout(() => {
notificationManager.showFabNotification(saveButton, originalText, false, originalText);
}, 2000);
}
} catch(error) {
progressIndicator.hide();
// 只更新FAB按钮状态,显示错误Toast通知
if (uiContext.source === 'fab' && saveButton) {
notificationManager.showFabNotification(saveButton, '❌ 失败', false, originalText);
setTimeout(() => {
notificationManager.showFabNotification(saveButton, originalText, false, originalText);
}, 3000);
}
// 显示错误Toast通知
notificationManager.showErrorNotification('保存失败', error.message);
throw error;
}
}
async function startSaveProcess(uiContext) { const pageTitle = document.title; const pageUrl = window.location.href; const saveButton = uiContext.buttonElement; try { if (uiContext.source === 'fab') showNotification(saveButton, '⚙️ 读取中...', true, '➤ Notion'); else console.log('NQS: [Menu] 开始处理 "保存"...'); const settings = await loadAllSettings(); if (!settings.notion_api_key || !settings.database_id) { throw new Error('❌ 配置不完整!请在"设置"中填写 Notion API Key 和数据库ID。'); } if (settings.ai_enabled) { await runAiSave(settings, pageTitle, pageUrl, uiContext); } else { if (uiContext.source === 'fab') showNotification(saveButton, '📂 手动选择', false, '➤ Notion'); else console.log('NQS: [Menu] AI已关闭,正在打开分类选择器...'); await openCategorySelectorPanel(pageTitle, pageUrl, uiContext); } } catch (error) { if (uiContext.source === 'fab') showNotification(saveButton, error.message, false, '➤ Notion'); else console.error(`NQS: [Menu] 保存失败!错误信息: ${error.message}`); await addLog('error', `保存操作失败 (${uiContext.source}): ${error.message}`, { title: pageTitle, url: pageUrl, error: error.message, stack: error.stack }); if (error.message.includes('配置')) setTimeout(openSettingsPanel, 1000); } }
async function startReadLaterSave(uiContext) {
const pageTitle = document.title;
const pageUrl = window.location.href;
const readLaterButton = uiContext.buttonElement;
const originalText = '◷ 稍后读';
try {
// 显示进度条
await progressIndicator.show('⚙️ 读取配置中...');
// 只更新FAB按钮状态
if (uiContext.source === 'fab' && readLaterButton) {
notificationManager.showFabNotification(readLaterButton, '⚙️ 准备中...', true, originalText);
}
const settings = await loadAllSettings();
if (!settings.notion_api_key || !settings.database_id) {
throw new Error('❌ 配置不完整!请在"设置"中填写 Notion API Key 和数据库ID。');
}
if (!settings.read_later_enabled) {
throw new Error('💡 "稍后读"功能未开启。');
}
if (!settings.read_later_category) {
throw new Error('❌ 未设置"稍后读"分类名称。');
}
const category = settings.read_later_category;
// 更新保存状态
progressIndicator.updateMessage('🚀 保存到 Notion...');
// 只更新FAB按钮状态
if (uiContext.source === 'fab' && readLaterButton) {
notificationManager.showFabNotification(readLaterButton, '🚀 保存中...', true, originalText);
}
const notionResponse = await saveToNotion(settings.notion_api_key, settings.database_id, pageTitle, pageUrl, category, settings);
// 隐藏进度条
progressIndicator.hide();
await addLog('info', `页面已存为稍后读 (${uiContext.source})`, {
title: pageTitle,
url: pageUrl,
result: category
});
// 显示成功通知和恢复FAB按钮状态
notificationManager.showSuccessNotification('保存成功', `已添加到"${category}"`);
if (uiContext.source === 'fab' && readLaterButton) {
notificationManager.showFabNotification(readLaterButton, '✅ 已保存', false, originalText);
setTimeout(() => {
notificationManager.showFabNotification(readLaterButton, originalText, false, originalText);
}, 2000);
}
} catch (error) {
progressIndicator.hide();
// 只更新FAB按钮状态,显示错误Toast通知
if (uiContext.source === 'fab' && readLaterButton) {
notificationManager.showFabNotification(readLaterButton, '❌ 失败', false, originalText);
setTimeout(() => {
notificationManager.showFabNotification(readLaterButton, originalText, false, originalText);
}, 3000);
}
// 显示错误Toast通知
notificationManager.showErrorNotification('保存失败', error.message);
await addLog('error', `"稍后读"失败 (${uiContext.source}): ${error.message}`, {
title: pageTitle,
url: pageUrl,
error: error.message,
stack: error.stack
});
if (error.message.includes('配置')) {
setTimeout(openSettingsPanel, 1000);
}
}
}
// ===================================================================
// ====================== 脚本初始化与执行 =======================
// ===================================================================
class FabPositionManager { constructor() { this.position = null; this.storageKey = 'nqs_fab_pos'; } async loadPosition() { try { const rawPos = await GM_getValue(this.storageKey, null); if (typeof rawPos === 'string') { this.position = JSON.parse(rawPos); } else if (rawPos && typeof rawPos === 'object') { this.position = rawPos; } return this.position; } catch (error) { console.warn('Failed to load fab position:', error); return null; } } async savePosition(position) { this.position = position; try { await GM_setValue(this.storageKey, JSON.stringify(position)); } catch (error) { console.error('Failed to save fab position:', error); } } updatePosition(fabElement) { if (!fabElement) return; const rect = fabElement.getBoundingClientRect(); this.position = { topPercent: rect.top / window.innerHeight, leftPercent: rect.left / window.innerWidth, snapped: false }; } }
class FabDragHandler { constructor(fabElement, positionManager) { this.fabElement = fabElement; this.positionManager = positionManager; this.isDragging = false; this.wasDragged = false; this.offset = { x: 0, y: 0 }; this.bindEvents(); } bindEvents() { const trigger = this.fabElement.querySelector('#nqs-fab-trigger'); if (!trigger) return; trigger.addEventListener('mousedown', this.handleDragStart.bind(this)); document.addEventListener('mousemove', this.handleDragMove.bind(this)); document.addEventListener('mouseup', this.handleDragEnd.bind(this)); trigger.addEventListener('click', (e) => { if (this.wasDragged) { e.preventDefault(); e.stopPropagation(); } }); } handleDragStart(e) { this.fabElement.classList.remove('is-expanded'); this.isDragging = true; this.wasDragged = false; this.fabElement.classList.add('is-dragging'); const rect = this.fabElement.getBoundingClientRect(); this.offset.x = e.clientX - rect.left; this.offset.y = e.clientY - rect.top; document.body.style.userSelect = 'none'; } handleDragMove(e) { if (!this.isDragging) return; e.preventDefault(); this.wasDragged = true; const newPosition = this.calculateNewPosition(e); this.applyPosition(newPosition); this.removeSnapState(); fabInstance.updateAlignmentClass(); } calculateNewPosition(e) { let newX = e.clientX - this.offset.x; let newY = e.clientY - this.offset.y; const fabRect = this.fabElement.getBoundingClientRect(); const maxX = window.innerWidth - fabRect.width; const maxY = window.innerHeight - fabRect.height; return { x: Math.max(0, Math.min(newX, maxX)), y: Math.max(0, Math.min(newY, maxY)) }; } applyPosition({ x, y }) { Object.assign(this.fabElement.style, { left: `${x}px`, top: `${y}px`, right: 'auto', bottom: 'auto' }); } removeSnapState() { this.fabElement.classList.remove('snapped-right', 'snapped-left'); } async handleDragEnd() { if (!this.isDragging) return; this.isDragging = false; this.fabElement.classList.remove('is-dragging'); document.body.style.userSelect = 'auto'; setTimeout(() => { this.wasDragged = false; }, 0); const position = this.calculateFinalPosition(); fabInstance.applyPosition(position); await this.positionManager.savePosition(position); } calculateFinalPosition() { const rect = this.fabElement.getBoundingClientRect(); const snapThresholdRight = window.innerWidth * 0.98; const snapThresholdLeft = window.innerWidth * 0.02; if (rect.right > snapThresholdRight) { return { topPercent: rect.top / window.innerHeight, snapped: 'right' }; } else if (rect.left < snapThresholdLeft) { return { topPercent: rect.top / window.innerHeight, snapped: 'left' }; } return { topPercent: rect.top / window.innerHeight, leftPercent: rect.left / window.innerWidth, snapped: false }; } }
class FloatingActionButton { constructor() { this.positionManager = new FabPositionManager(); this.dragHandler = null; this.container = null; this.resizeHandler = debounce(this.handleResize.bind(this), 150); this.leaveTimeoutId = null; } async init() { await this.cleanup(); injectStyles(); await this.createContainer(); await this.setupPosition(); this.setupInteractions(); this.bindResizeHandler(); } async cleanup() { const existingContainer = document.querySelector('#nqs-fab-container'); if (existingContainer) existingContainer.remove(); } async createContainer() { const globalContainer = document.getElementById('NQS_GLOBAL_CONTAINER'); this.container = document.createElement('div'); this.container.id = 'nqs-fab-container'; await this.createButtons(); globalContainer.appendChild(this.container); } async createButtons() { const settings = await loadAllSettings(); const optionsWrapper = document.createElement('div'); optionsWrapper.className = 'nqs-fab-options'; const saveButton = this.createActionButton('nqs-save-button', '➤ Notion'); saveButton.addEventListener('click', (e) => { e.stopPropagation(); this.handleButtonClick(e, 'save', saveButton); }); optionsWrapper.appendChild(saveButton); if (settings.read_later_enabled) { const readLaterButton = this.createActionButton('nqs-read-later-button', '◷ 稍后读'); readLaterButton.addEventListener('click', (e) => { e.stopPropagation(); this.handleButtonClick(e, 'readLater', readLaterButton); }); optionsWrapper.appendChild(readLaterButton); } const triggerButton = document.createElement('div'); triggerButton.id = 'nqs-fab-trigger'; setSafeInnerHTML(triggerButton, '➤'); this.container.appendChild(triggerButton); this.container.appendChild(optionsWrapper); } createActionButton(id, text) { const button = document.createElement('div'); button.id = id; button.className = 'nqs-fab-action-btn'; button.textContent = text; return button; } handleButtonClick(e, action, buttonElement) { (action === 'save' ? startSaveProcess : startReadLaterSave)({ source: 'fab', buttonElement }); } async setupPosition() { const savedPos = await this.positionManager.loadPosition(); if (savedPos) this.applyPosition(savedPos); else this.setDefaultPosition(); } applyPosition(position) { this.container.classList.remove('snapped-right', 'snapped-left'); if (position.snapped === 'right') { Object.assign(this.container.style, { top: `${position.topPercent * window.innerHeight}px`, right: '0px', left: 'auto', bottom: 'auto' }); this.container.classList.add('snapped-right'); } else if (position.snapped === 'left') { Object.assign(this.container.style, { top: `${position.topPercent * window.innerHeight}px`, left: '0px', right: 'auto', bottom: 'auto' }); this.container.classList.add('snapped-left'); } else { Object.assign(this.container.style, { top: `${position.topPercent * window.innerHeight}px`, left: `${position.leftPercent * window.innerWidth}px`, right: 'auto', bottom: 'auto' }); } this.updateAlignmentClass(); } setDefaultPosition() { Object.assign(this.container.style, { right: '30px', bottom: '30px' }); setTimeout(() => { this.positionManager.updatePosition(this.container); this.updateAlignmentClass(); }, 0); } updateAlignmentClass() { if (!this.container) return; const rect = this.container.getBoundingClientRect(); const viewportCenterX = window.innerWidth / 2; const fabCenterX = rect.left + rect.width / 2; if (fabCenterX < viewportCenterX) this.container.classList.add('align-left'); else this.container.classList.remove('align-left'); }
setupInteractions() { this.dragHandler = new FabDragHandler(this.container, this.positionManager); this.container.addEventListener('mouseenter', (event) => { if (this.leaveTimeoutId) { clearTimeout(this.leaveTimeoutId); this.leaveTimeoutId = null; } if (this.dragHandler.isDragging || event.buttons === 1) return; this.container.classList.add('is-expanded'); }); this.container.addEventListener('mouseleave', () => { this.leaveTimeoutId = setTimeout(() => { this.container.classList.remove('is-expanded'); }, 300); }); }
handleResize() { const position = this.positionManager.position; if (!this.container || !position || this.dragHandler?.isDragging) return; if (!position.snapped) { const newX = position.leftPercent * window.innerWidth; this.container.style.left = `${newX}px`; } const newY = position.topPercent * window.innerHeight; this.container.style.top = `${newY}px`; this.updateAlignmentClass(); } bindResizeHandler() { window.addEventListener('resize', this.resizeHandler); } destroy() { window.removeEventListener('resize', this.resizeHandler); this.container?.remove(); }
}
let fabInstance = null;
function injectStyles() { const styleId = 'nqs-custom-ui-styles'; const container = document.getElementById('NQS_GLOBAL_CONTAINER'); if (container.querySelector(`#${styleId}`)) return; const css = `
/* === 统一的主题变量系统 === */
#NQS_GLOBAL_CONTAINER {
all: initial;
/* 明亮主题变量 */
--nqs-bg: #ffffff;
--nqs-bg-subtle: #f8fafc;
--nqs-bg-hover: #f1f5f9;
--nqs-bg-active: #e2e8f0;
--nqs-border: #e2e8f0;
--nqs-border-hover: #cbd5e1;
--nqs-text-primary: #0f172a;
--nqs-text-secondary: #64748b;
--nqs-text-tertiary: #94a3b8;
--nqs-accent: #3b82f6;
--nqs-accent-hover: #2563eb;
--nqs-accent-light: #dbeafe;
--nqs-success: #10b981;
--nqs-success-light: #d1fae5;
--nqs-warning: #f59e0b;
--nqs-warning-light: #fef3c7;
--nqs-danger: #ef4444;
--nqs-danger-light: #fecaca;
--nqs-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--nqs-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--nqs-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
--nqs-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--nqs-radius: 8px;
--nqs-radius-lg: 12px;
--nqs-radius-xl: 16px;
}
/* 暗黑主题变量 */
#NQS_GLOBAL_CONTAINER[data-theme='dark'] {
--nqs-bg: #0f172a;
--nqs-bg-subtle: #1e293b;
--nqs-bg-hover: #334155;
--nqs-bg-active: #475569;
--nqs-border: #334155;
--nqs-border-hover: #475569;
--nqs-text-primary: #f8fafc;
--nqs-text-secondary: #cbd5e1;
--nqs-text-tertiary: #94a3b8;
--nqs-accent: #3b82f6;
--nqs-accent-hover: #60a5fa;
--nqs-accent-light: #1e3a8a;
--nqs-success: #10b981;
--nqs-success-light: #064e3b;
--nqs-warning: #f59e0b;
--nqs-warning-light: #78350f;
--nqs-danger: #ef4444;
--nqs-danger-light: #7f1d1d;
--nqs-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
--nqs-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
--nqs-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.4), 0 10px 10px -5px rgba(0, 0, 0, 0.3);
}
/* 自动主题跟随系统 */
@media (prefers-color-scheme: dark) {
#NQS_GLOBAL_CONTAINER[data-theme='auto'] {
--nqs-bg: #0f172a;
--nqs-bg-subtle: #1e293b;
--nqs-bg-hover: #334155;
--nqs-bg-active: #475569;
--nqs-border: #334155;
--nqs-border-hover: #475569;
--nqs-text-primary: #f8fafc;
--nqs-text-secondary: #cbd5e1;
--nqs-text-tertiary: #94a3b8;
--nqs-accent: #3b82f6;
--nqs-accent-hover: #60a5fa;
--nqs-accent-light: #1e3a8a;
--nqs-success: #10b981;
--nqs-success-light: #064e3b;
--nqs-warning: #f59e0b;
--nqs-warning-light: #78350f;
--nqs-danger: #ef4444;
--nqs-danger-light: #7f1d1d;
--nqs-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
--nqs-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
--nqs-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.4), 0 10px 10px -5px rgba(0, 0, 0, 0.3);
}
}
/* === 基础样式和动画 === */
#NQS_GLOBAL_CONTAINER * {
box-sizing: border-box;
font-family: var(--nqs-font-family);
}
@keyframes nqs-spin {
from { transform: translateY(-50%) rotate(0deg); }
to { transform: translateY(-50%) rotate(360deg); }
}
@keyframes nqs-fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes nqs-slideIn {
from { transform: translateX(-100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
/* === 模态弹窗样式 === */
#NQS_GLOBAL_CONTAINER .nqs-overlay{
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(15, 23, 42, 0.8);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 100000;
}
#NQS_GLOBAL_CONTAINER .nqs-overlay.visible{
opacity: 1;
}
#NQS_GLOBAL_CONTAINER .nqs-panel{
width: 100%;
max-width: 720px;
background: var(--nqs-bg);
border: 1px solid var(--nqs-border);
border-radius: var(--nqs-radius-xl);
box-shadow: var(--nqs-shadow-xl);
display: flex;
flex-direction: column;
max-height: 90vh;
margin: 1rem;
transform: scale(0.95) translateY(20px);
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
overflow: hidden;
}
#NQS_GLOBAL_CONTAINER .nqs-overlay.visible .nqs-panel{
transform: scale(1) translateY(0);
}
/* === 面板内容样式 === */
#NQS_GLOBAL_CONTAINER .nqs-header{
padding: 1.5rem 2rem;
border-bottom: 1px solid var(--nqs-border);
flex-shrink: 0;
background: var(--nqs-bg-subtle);
border-top-left-radius: var(--nqs-radius-xl);
border-top-right-radius: var(--nqs-radius-xl);
}
#NQS_GLOBAL_CONTAINER .nqs-header h1{
font-size: 1.5rem;
margin: 0;
color: var(--nqs-text-primary);
font-weight: 700;
letter-spacing: -0.025em;
}
#NQS_GLOBAL_CONTAINER .nqs-header p{
font-size: 0.9rem;
margin: 0.5rem 0 0 0;
color: var(--nqs-text-secondary);
line-height: 1.5;
}
#NQS_GLOBAL_CONTAINER .nqs-body{
padding: 2rem;
overflow-y: auto;
flex-grow: 1;
min-height: 0;
background: var(--nqs-bg);
}
/* === 日志查看器布局样式 === */
#NQS_GLOBAL_CONTAINER .nqs-panel--log-viewer .nqs-body {
padding: 0;
display: flex;
flex-direction: column;
height: 75vh;
min-height: 600px;
max-height: 800px;
background: var(--nqs-bg-subtle);
width: 100%;
overflow: hidden;
}
/* === 日志统计卡片样式 === */
#NQS_GLOBAL_CONTAINER .nqs-log-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
padding: 20px;
background: var(--nqs-bg);
border-bottom: 1px solid var(--nqs-border);
}
#NQS_GLOBAL_CONTAINER .nqs-stat-card {
background: var(--nqs-bg);
border: 1px solid var(--nqs-border);
border-radius: var(--nqs-radius-lg);
padding: 20px;
display: flex;
align-items: center;
gap: 16px;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
#NQS_GLOBAL_CONTAINER .nqs-stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: var(--nqs-accent);
}
#NQS_GLOBAL_CONTAINER .nqs-stat-card.nqs-stat-success::before {
background: linear-gradient(90deg, #34C759, #30D158);
}
#NQS_GLOBAL_CONTAINER .nqs-stat-card.nqs-stat-error::before {
background: linear-gradient(90deg, #FF3B30, #FF453A);
}
#NQS_GLOBAL_CONTAINER .nqs-stat-card.nqs-stat-debug::before {
background: linear-gradient(90deg, #8E8E93, #AEAEB2);
}
#NQS_GLOBAL_CONTAINER .nqs-stat-card:hover {
transform: translateY(-2px);
box-shadow: var(--nqs-shadow-lg);
}
#NQS_GLOBAL_CONTAINER .nqs-stat-icon {
font-size: 28px;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: var(--nqs-bg-subtle);
border-radius: 12px;
flex-shrink: 0;
}
#NQS_GLOBAL_CONTAINER .nqs-stat-content {
flex: 1;
}
#NQS_GLOBAL_CONTAINER .nqs-stat-number {
font-size: 24px;
font-weight: 700;
color: var(--nqs-text-primary);
line-height: 1.2;
}
#NQS_GLOBAL_CONTAINER .nqs-stat-label {
font-size: 14px;
color: var(--nqs-text-secondary);
margin-top: 4px;
}
/* === 过滤栏样式 === */
#NQS_GLOBAL_CONTAINER .nqs-log-filter-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
flex-shrink: 0;
background: var(--nqs-bg);
border-bottom: 1px solid var(--nqs-border);
gap: 20px;
}
#NQS_GLOBAL_CONTAINER .nqs-filter-left {
display: flex;
align-items: center;
gap: 16px;
}
#NQS_GLOBAL_CONTAINER .nqs-filter-right {
display: flex;
align-items: center;
gap: 16px;
}
#NQS_GLOBAL_CONTAINER .nqs-search-box {
position: relative;
display: flex;
align-items: center;
}
#NQS_GLOBAL_CONTAINER .nqs-search-input {
padding: 8px 12px 8px 36px;
border: 1px solid var(--nqs-border);
border-radius: var(--nqs-radius-lg);
background: var(--nqs-bg-subtle);
color: var(--nqs-text-primary);
font-size: 14px;
width: 200px;
transition: all 0.2s ease;
}
#NQS_GLOBAL_CONTAINER .nqs-search-input:focus {
outline: none;
border-color: var(--nqs-accent);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
#NQS_GLOBAL_CONTAINER .nqs-search-icon {
position: absolute;
left: 12px;
font-size: 14px;
color: var(--nqs-text-secondary);
}
#NQS_GLOBAL_CONTAINER .nqs-toggle-group {
display: flex;
align-items: center;
}
#NQS_GLOBAL_CONTAINER .nqs-toggle-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 14px;
color: var(--nqs-text-primary);
}
#NQS_GLOBAL_CONTAINER .nqs-toggle-input {
display: none;
}
#NQS_GLOBAL_CONTAINER .nqs-toggle-slider {
width: 40px;
height: 20px;
background: var(--nqs-border);
border-radius: 10px;
position: relative;
transition: all 0.3s ease;
}
#NQS_GLOBAL_CONTAINER .nqs-toggle-slider::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
background: white;
border-radius: 50%;
transition: all 0.3s ease;
}
#NQS_GLOBAL_CONTAINER .nqs-toggle-input:checked + .nqs-toggle-slider {
background: var(--nqs-accent);
}
#NQS_GLOBAL_CONTAINER .nqs-toggle-input:checked + .nqs-toggle-slider::after {
transform: translateX(20px);
}
#NQS_GLOBAL_CONTAINER .nqs-toggle-text {
font-weight: 500;
}
/* === 表格容器样式 === */
#NQS_GLOBAL_CONTAINER .nqs-table-container {
overflow-y: auto;
flex-grow: 1;
padding: 0;
width: 100%;
background: var(--nqs-bg);
min-height: 0;
}
/* === 日志卡片样式 === */
#NQS_GLOBAL_CONTAINER .nqs-log-cards {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px;
}
#NQS_GLOBAL_CONTAINER .nqs-log-card {
background: var(--nqs-bg);
border: 1px solid var(--nqs-border);
border-radius: var(--nqs-radius-lg);
padding: 16px;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
#NQS_GLOBAL_CONTAINER .nqs-log-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: var(--nqs-accent);
}
#NQS_GLOBAL_CONTAINER .nqs-log-card--info::before {
background: linear-gradient(90deg, #34C759, #30D158);
}
#NQS_GLOBAL_CONTAINER .nqs-log-card--error::before {
background: linear-gradient(90deg, #FF3B30, #FF453A);
}
#NQS_GLOBAL_CONTAINER .nqs-log-card--debug::before {
background: linear-gradient(90deg, #8E8E93, #AEAEB2);
}
#NQS_GLOBAL_CONTAINER .nqs-log-card--warn::before {
background: linear-gradient(90deg, #FF9500, #FF9F0A);
}
#NQS_GLOBAL_CONTAINER .nqs-log-card:hover {
transform: translateY(-2px);
box-shadow: var(--nqs-shadow-lg);
border-color: var(--nqs-accent-light);
}
#NQS_GLOBAL_CONTAINER .nqs-log-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
#NQS_GLOBAL_CONTAINER .nqs-log-level-badge {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: var(--nqs-radius);
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
#NQS_GLOBAL_CONTAINER .nqs-log-level-icon {
font-size: 14px;
}
#NQS_GLOBAL_CONTAINER .nqs-log-time {
font-size: 12px;
color: var(--nqs-text-secondary);
font-weight: 500;
}
#NQS_GLOBAL_CONTAINER .nqs-log-card-body {
margin-bottom: 12px;
}
#NQS_GLOBAL_CONTAINER .nqs-log-message {
color: var(--nqs-text-primary);
line-height: 1.5;
font-size: 14px;
word-break: break-word;
}
#NQS_GLOBAL_CONTAINER .nqs-log-card-footer {
display: flex;
justify-content: flex-end;
align-items: center;
padding-top: 8px;
border-top: 1px solid var(--nqs-border-light);
}
#NQS_GLOBAL_CONTAINER .nqs-log-detail-btn {
background: var(--nqs-accent-light);
color: var(--nqs-accent);
border: 1px solid var(--nqs-accent);
padding: 6px 12px;
border-radius: var(--nqs-radius);
cursor: pointer;
font-size: 12px;
font-weight: 500;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 4px;
}
#NQS_GLOBAL_CONTAINER .nqs-log-detail-btn:hover {
background: var(--nqs-accent);
color: white;
transform: scale(1.05);
}
#NQS_GLOBAL_CONTAINER .nqs-no-details {
color: var(--nqs-text-tertiary);
font-size: 12px;
font-style: italic;
}
/* === 空状态样式 === */
#NQS_GLOBAL_CONTAINER .nqs-empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
}
#NQS_GLOBAL_CONTAINER .nqs-empty-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
#NQS_GLOBAL_CONTAINER .nqs-empty-title {
font-size: 18px;
font-weight: 600;
color: var(--nqs-text-primary);
margin-bottom: 8px;
}
#NQS_GLOBAL_CONTAINER .nqs-empty-message {
font-size: 14px;
color: var(--nqs-text-secondary);
line-height: 1.5;
}
/* === 底部操作栏样式 === */
#NQS_GLOBAL_CONTAINER .nqs-log-footer-actions {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
#NQS_GLOBAL_CONTAINER .nqs-log-footer-left {
display: flex;
gap: 12px;
align-items: center;
}
#NQS_GLOBAL_CONTAINER .nqs-log-footer-right {
display: flex;
align-items: center;
}
#NQS_GLOBAL_CONTAINER .nqs-log-footer-actions .nqs-button {
display: flex;
align-items: center;
gap: 8px;
}
#NQS_GLOBAL_CONTAINER .nqs-log-footer-actions .nqs-button span:first-child {
font-size: 16px;
}
/* === 日志详情弹窗样式 === */
#NQS_GLOBAL_CONTAINER .nqs-panel--log-detail .nqs-body {
padding: 0;
max-height: 70vh;
overflow: hidden;
}
#NQS_GLOBAL_CONTAINER .nqs-log-detail-content {
height: 100%;
display: flex;
flex-direction: column;
}
#NQS_GLOBAL_CONTAINER .nqs-json-viewer {
flex: 1;
overflow: auto;
background: var(--nqs-bg-subtle);
border-radius: var(--nqs-radius);
border: 1px solid var(--nqs-border);
margin: 16px;
}
#NQS_GLOBAL_CONTAINER .nqs-json-code {
margin: 0;
padding: 20px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
font-size: 13px;
line-height: 1.6;
color: var(--nqs-text-primary);
background: transparent;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
}
#NQS_GLOBAL_CONTAINER .nqs-log-detail-footer {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
#NQS_GLOBAL_CONTAINER .nqs-log-detail-footer .nqs-button {
display: flex;
align-items: center;
gap: 8px;
}
#NQS_GLOBAL_CONTAINER .nqs-log-detail-footer .nqs-button span:first-child {
font-size: 16px;
}
#NQS_GLOBAL_CONTAINER .nqs-filter-group {
display: flex;
gap: 8px;
align-items: center;
}
/* === 现代化过滤按钮样式 === */
#NQS_GLOBAL_CONTAINER .nqs-filter-btn {
background: var(--nqs-bg-subtle);
border: 1px solid var(--nqs-border);
color: var(--nqs-text-primary);
padding: 10px 16px;
border-radius: var(--nqs-radius-lg);
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
gap: 8px;
}
#NQS_GLOBAL_CONTAINER .nqs-filter-icon {
font-size: 16px;
}
#NQS_GLOBAL_CONTAINER .nqs-filter-btn:hover {
background: var(--nqs-bg-hover);
border-color: var(--nqs-accent);
transform: translateY(-1px);
box-shadow: var(--nqs-shadow);
}
#NQS_GLOBAL_CONTAINER .nqs-filter-btn.active {
background: var(--nqs-accent);
color: white;
border-color: var(--nqs-accent);
box-shadow: var(--nqs-shadow-lg);
}
#NQS_GLOBAL_CONTAINER .nqs-filter-btn.active:hover {
background: var(--nqs-accent-hover);
transform: translateY(-1px);
}
/* === 详情按钮样式 === */
#NQS_GLOBAL_CONTAINER .nqs-detail-btn {
background: var(--nqs-accent-light);
color: var(--nqs-accent);
border: 1px solid var(--nqs-accent);
padding: 6px 12px;
border-radius: var(--nqs-radius);
cursor: pointer;
font-size: 12px;
font-weight: 500;
transition: all 0.2s ease;
}
#NQS_GLOBAL_CONTAINER .nqs-detail-btn:hover {
background: var(--nqs-accent);
color: white;
transform: scale(1.05);
}
#NQS_GLOBAL_CONTAINER .nqs-no-detail {
color: var(--nqs-text-tertiary);
font-size: 12px;
}
/* === 底部操作栏样式 === */
#NQS_GLOBAL_CONTAINER .nqs-footer{
padding: 1.5rem 2rem;
border-top: 1px solid var(--nqs-border);
display: flex;
align-items: center;
gap: 1rem;
background: var(--nqs-bg-subtle);
border-bottom-left-radius: var(--nqs-radius-xl);
border-bottom-right-radius: var(--nqs-radius-xl);
flex-shrink: 0;
}
/* === 统一按钮样式 === */
#NQS_GLOBAL_CONTAINER .nqs-button{
display: inline-flex;
align-items: center;
justify-content: center;
text-align: center;
border-radius: var(--nqs-radius);
padding: 0.75rem 1.5rem;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
border: 1px solid transparent;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
text-decoration: none;
white-space: nowrap;
position: relative;
overflow: hidden;
}
#NQS_GLOBAL_CONTAINER .nqs-button:hover{
transform: translateY(-2px);
box-shadow: var(--nqs-shadow-lg);
}
#NQS_GLOBAL_CONTAINER .nqs-button:active{
transform: translateY(0);
box-shadow: var(--nqs-shadow);
}
#NQS_GLOBAL_CONTAINER .nqs-button-primary{
background: var(--nqs-accent);
color: white;
border-color: var(--nqs-accent);
}
#NQS_GLOBAL_CONTAINER .nqs-button-primary:hover{
background: var(--nqs-accent-hover);
border-color: var(--nqs-accent-hover);
}
#NQS_GLOBAL_CONTAINER .nqs-button-secondary{
background: var(--nqs-bg-subtle);
border-color: var(--nqs-border);
color: var(--nqs-text-primary);
}
#NQS_GLOBAL_CONTAINER .nqs-button-secondary:hover{
background: var(--nqs-bg-hover);
border-color: var(--nqs-border-hover);
}
#NQS_GLOBAL_CONTAINER .nqs-button-danger{
background: var(--nqs-danger);
color: white;
border-color: var(--nqs-danger);
}
#NQS_GLOBAL_CONTAINER .nqs-button-danger:hover{
background: #dc2626;
border-color: #dc2626;
}
#NQS_GLOBAL_CONTAINER .nqs-button-text{
background: none;
border: none;
padding: 0.5rem 1rem;
color: var(--nqs-accent);
font-weight: 500;
cursor: pointer;
text-align: left;
line-height: 1.5;
font-size: 0.9rem;
border-radius: var(--nqs-radius);
transition: all 0.2s ease;
}
#NQS_GLOBAL_CONTAINER .nqs-button-text:hover{
background: var(--nqs-accent-light);
color: var(--nqs-accent-hover);
transform: none;
box-shadow: none;
}
/* === iOS风格日志表格样式 === */
#NQS_GLOBAL_CONTAINER .nqs-log-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
font-size: 15px;
background: rgba(255, 255, 255, 0.95);
margin: 0;
letter-spacing: -0.24px;
}
/* 暗色主题适配 */
#NQS_GLOBAL_CONTAINER[data-theme='dark'] .nqs-log-table {
background: rgba(28, 28, 30, 0.95);
}
#NQS_GLOBAL_CONTAINER .nqs-log-table th {
text-align: left;
padding: 16px 20px;
font-weight: 600;
color: var(--nqs-text-secondary);
background: rgba(242, 242, 247, 0.95);
position: sticky;
top: 0;
z-index: 5;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.6px;
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
}
/* 暗色主题适配 */
#NQS_GLOBAL_CONTAINER[data-theme='dark'] .nqs-log-table th {
background: rgba(44, 44, 46, 0.95);
border-bottom-color: rgba(255, 255, 255, 0.12);
}
#NQS_GLOBAL_CONTAINER .nqs-log-table td {
padding: 16px 20px;
vertical-align: middle;
color: var(--nqs-text-primary);
background: rgba(255, 255, 255, 0.95);
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
position: relative;
font-size: 15px;
letter-spacing: -0.24px;
}
/* 暗色主题适配 */
#NQS_GLOBAL_CONTAINER[data-theme='dark'] .nqs-log-table td {
background: rgba(28, 28, 30, 0.95);
border-bottom-color: rgba(255, 255, 255, 0.08);
}
/* 基础行样式 */
#NQS_GLOBAL_CONTAINER .nqs-log-table tbody tr {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
#NQS_GLOBAL_CONTAINER .nqs-log-table tbody tr::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 0;
background: linear-gradient(90deg, var(--nqs-accent), transparent);
transition: width 0.3s ease;
z-index: 1;
}
#NQS_GLOBAL_CONTAINER .nqs-log-table tbody tr:nth-child(even) td {
background: rgba(242, 242, 247, 0.5);
}
#NQS_GLOBAL_CONTAINER[data-theme='dark'] .nqs-log-table tbody tr:nth-child(even) td {
background: rgba(44, 44, 46, 0.5);
}
#NQS_GLOBAL_CONTAINER .nqs-log-table tbody tr:hover {
box-shadow:
0 4px 20px rgba(0, 122, 255, 0.08),
0 1px 3px rgba(0, 0, 0, 0.1);
transform: scale(1.005);
}
#NQS_GLOBAL_CONTAINER .nqs-log-table tbody tr:hover::before {
width: 3px;
}
#NQS_GLOBAL_CONTAINER .nqs-log-table tbody tr:hover td {
background: rgba(0, 122, 255, 0.04) !important;
position: relative;
z-index: 2;
}
#NQS_GLOBAL_CONTAINER[data-theme='dark'] .nqs-log-table tbody tr:hover td {
background: rgba(0, 122, 255, 0.08) !important;
}
/* iOS风格日志行状态样式 */
#NQS_GLOBAL_CONTAINER .nqs-log-row--info td {
border-left: 3px solid #007AFF;
background: rgba(0, 122, 255, 0.04);
}
#NQS_GLOBAL_CONTAINER .nqs-log-row--info:nth-child(even) td {
background: rgba(0, 122, 255, 0.06);
}
#NQS_GLOBAL_CONTAINER .nqs-log-row--info:hover td {
background: rgba(0, 122, 255, 0.1) !important;
}
#NQS_GLOBAL_CONTAINER .nqs-log-row--error td {
border-left: 3px solid #FF3B30;
background: rgba(255, 59, 48, 0.04);
}
#NQS_GLOBAL_CONTAINER .nqs-log-row--error:nth-child(even) td {
background: rgba(255, 59, 48, 0.06);
}
#NQS_GLOBAL_CONTAINER .nqs-log-row--error:hover td {
background: rgba(255, 59, 48, 0.1) !important;
}
#NQS_GLOBAL_CONTAINER .nqs-log-row--debug td {
border-left: 3px solid #8E8E93;
background: rgba(142, 142, 147, 0.04);
}
#NQS_GLOBAL_CONTAINER .nqs-log-row--debug:nth-child(even) td {
background: rgba(142, 142, 147, 0.06);
}
#NQS_GLOBAL_CONTAINER .nqs-log-row--debug:hover td {
background: rgba(142, 142, 147, 0.1) !important;
}
/* iOS风格单元格内容样式 */
#NQS_GLOBAL_CONTAINER .nqs-log-cell-time {
white-space: nowrap;
color: var(--nqs-text-secondary);
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 13px;
font-weight: 500;
min-width: 140px;
letter-spacing: -0.08px;
opacity: 0.8;
}
#NQS_GLOBAL_CONTAINER .nqs-log-cell-level {
min-width: 80px;
}
#NQS_GLOBAL_CONTAINER .nqs-log-cell-message {
word-break: break-word;
white-space: normal;
line-height: 1.5;
max-width: 400px;
letter-spacing: -0.24px;
}
#NQS_GLOBAL_CONTAINER .nqs-log-cell-actions {
min-width: 80px;
text-align: center;
}
#NQS_GLOBAL_CONTAINER .nqs-log-cell-actions a {
color: #007AFF;
text-decoration: none;
font-weight: 500;
padding: 6px 12px;
border-radius: 16px;
transition: all 0.2s ease;
font-size: 13px;
letter-spacing: -0.08px;
background: rgba(0, 122, 255, 0.1);
}
#NQS_GLOBAL_CONTAINER[data-theme='dark'] .nqs-log-cell-actions a {
color: #0A84FF;
background: rgba(10, 132, 255, 0.15);
}
#NQS_GLOBAL_CONTAINER .nqs-log-cell-actions a:hover {
background: rgba(0, 122, 255, 0.2);
color: #005BB5;
transform: scale(1.05);
}
#NQS_GLOBAL_CONTAINER[data-theme='dark'] .nqs-log-cell-actions a:hover {
background: rgba(10, 132, 255, 0.25);
color: #409CFF;
}
/* === 日志标签样式 === */
#NQS_GLOBAL_CONTAINER .nqs-log-tag {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.4rem 0.8rem;
border-radius: var(--nqs-radius);
font-weight: 600;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
line-height: 1;
border: none;
min-width: 60px;
text-align: center;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
}
#NQS_GLOBAL_CONTAINER .nqs-log-tag.info {
background: linear-gradient(135deg, #3b82f6, #2563eb);
color: white;
}
#NQS_GLOBAL_CONTAINER .nqs-log-tag.error {
background: linear-gradient(135deg, #ef4444, #dc2626);
color: white;
}
#NQS_GLOBAL_CONTAINER .nqs-log-tag.debug {
background: linear-gradient(135deg, #6b7280, #4b5563);
color: white;
}
#NQS_GLOBAL_CONTAINER .nqs-log-tag.legacy {
background: linear-gradient(135deg, #f59e0b, #d97706);
color: white;
}
#NQS_GLOBAL_CONTAINER .nqs-json-viewer { background-color: var(--nqs-bg-subtle); color: var(--nqs-text-primary); font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 14px; padding: 1.5rem; }
#NQS_GLOBAL_CONTAINER .json-key { color: #9cdcfe; } #NQS_GLOBAL_CONTAINER .json-string { color: #ce9178; } #NQS_GLOBAL_CONTAINER .json-number { color: #b5cea8; } #NQS_GLOBAL_CONTAINER .json-boolean { color: #569cd6; } #NQS_GLOBAL_CONTAINER .json-null { color: #c586c0; }
/* 保留旧的标签组样式以兼容其他功能 */
#NQS_GLOBAL_CONTAINER .nqs-label-group{padding-top:.5rem;display:flex;flex-direction:column;}
#NQS_GLOBAL_CONTAINER .nqs-label-group label{display:block;font-weight:500;color:var(--nqs-text-primary); margin: 0; padding: 0;}
#NQS_GLOBAL_CONTAINER p.nqs-p, #NQS_GLOBAL_CONTAINER .nqs-label-group p {font-size:.85rem;color:var(--nqs-text-secondary);margin:.25rem 0 0 0; line-height: 1.6;}
/* 新的设置标签组样式 */
#NQS_GLOBAL_CONTAINER .nqs-toggle-switch{display:flex;align-items:center;justify-content:flex-end}
#NQS_GLOBAL_CONTAINER .nqs-toggle-switch .nqs-switch{position:relative;display:inline-block;width:50px;height:28px}
#NQS_GLOBAL_CONTAINER .nqs-toggle-switch .nqs-switch input{opacity:0;width:0;height:0}
#NQS_GLOBAL_CONTAINER .nqs-toggle-switch .nqs-slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:#ccc;transition:.4s;border-radius:28px}
#NQS_GLOBAL_CONTAINER[data-theme='dark'] .nqs-toggle-switch .nqs-slider { background-color: var(--nqs-border); }
#NQS_GLOBAL_CONTAINER .nqs-toggle-switch .nqs-slider:before{position:absolute;content:"";height:20px;width:20px;left:4px;bottom:4px;background-color:#fff;transition:.4s;border-radius:50%}
#NQS_GLOBAL_CONTAINER .nqs-toggle-switch input:checked+.nqs-slider{background-color:var(--nqs-accent)}
#NQS_GLOBAL_CONTAINER .nqs-toggle-switch input:checked+.nqs-slider:before{transform:translateX(22px)}
/* === 表单元素样式 === */
#NQS_GLOBAL_CONTAINER .nqs-input,
#NQS_GLOBAL_CONTAINER .nqs-textarea{
width: 100%;
padding: 0.875rem 1rem;
font-size: 0.95rem;
font-family: inherit;
color: var(--nqs-text-primary);
background-color: var(--nqs-bg);
border: 1px solid var(--nqs-border);
border-radius: var(--nqs-radius);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
line-height: 1.5;
}
#NQS_GLOBAL_CONTAINER .nqs-input:hover,
#NQS_GLOBAL_CONTAINER .nqs-textarea:hover{
border-color: var(--nqs-border-hover);
background-color: var(--nqs-bg-hover);
}
#NQS_GLOBAL_CONTAINER .nqs-input:focus,
#NQS_GLOBAL_CONTAINER .nqs-textarea:focus{
outline: 0;
border-color: var(--nqs-accent);
background-color: var(--nqs-bg);
box-shadow: 0 0 0 3px var(--nqs-accent-light);
transform: translateY(-1px);
}
#NQS_GLOBAL_CONTAINER .nqs-input::placeholder,
#NQS_GLOBAL_CONTAINER .nqs-textarea::placeholder{
color: var(--nqs-text-tertiary);
}
#NQS_GLOBAL_CONTAINER .nqs-section{margin-bottom:2.5rem}.nqs-section:last-child{margin-bottom:0}
#NQS_GLOBAL_CONTAINER .nqs-section-title{font-size:1.25rem;font-weight:600;color:var(--nqs-text-primary);border-bottom:1px solid var(--nqs-border);padding-bottom:.75rem;margin-bottom:1.5rem}
#NQS_GLOBAL_CONTAINER .nqs-subsection-title{font-size:1rem;font-weight:600;color:var(--nqs-text-primary);margin-top:1.5rem;margin-bottom:0.5rem;}
#NQS_GLOBAL_CONTAINER .nqs-subsection-p{font-size:.85rem;color:var(--nqs-text-secondary);margin-top:0;margin-bottom:1rem;}
/* 保留旧的字段样式以兼容其他功能 */
#NQS_GLOBAL_CONTAINER .nqs-field{display:grid;grid-template-columns:1fr 1.5fr;gap:1rem 2rem;align-items:flex-start;margin-bottom:1.5rem}
#NQS_GLOBAL_CONTAINER .nqs-field-full{grid-template-columns:1fr}
/* 新的设置字段样式 */
/* 保留旧的分类管理样式以兼容其他功能 */
#NQS_GLOBAL_CONTAINER .nqs-category-manager{display: flex; margin-top: 0.5rem;}
#NQS_GLOBAL_CONTAINER .nqs-category-manager input{border-top-right-radius:0;border-bottom-right-radius:0}
#NQS_GLOBAL_CONTAINER .nqs-category-manager button{border-top-left-radius:0;border-bottom-left-radius:0}
/* 新的分类管理样式 */
/* 保留旧的模型选择器样式以兼容其他功能 */
#NQS_GLOBAL_CONTAINER .nqs-model-selector-wrapper { position: relative; }
#NQS_GLOBAL_CONTAINER .nqs-model-selector-wrapper .nqs-input { padding-right: 44px !important; }
/* 新的模型选择器样式 */
/* 保留旧的图标按钮样式以兼容其他功能 */
#NQS_GLOBAL_CONTAINER .nqs-icon-button { position: absolute; top: 50%; right: 5px; transform: translateY(-50%); width: 34px; height: 34px; border: none; background: transparent; color: var(--nqs-text-secondary); border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: color 0.2s, background-color 0.2s; }
#NQS_GLOBAL_CONTAINER .nqs-icon-button:hover { background-color: var(--nqs-bg-subtle); color: var(--nqs-text-primary); }
#NQS_GLOBAL_CONTAINER .nqs-icon-button:disabled { cursor: not-allowed; color: var(--nqs-border); }
#NQS_GLOBAL_CONTAINER .nqs-icon-button.is-loading svg { animation: nqs-spin 1s linear infinite; }
/* 新的获取模型按钮样式 */
/* 保留旧的自定义下拉框样式以兼容其他功能 */
#NQS_GLOBAL_CONTAINER .nqs-custom-dropdown { position: absolute; top: 100%; left: 0; right: 0; z-index: 10; background: var(--nqs-bg); border: 1px solid var(--nqs-border); border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); margin-top: 0.5rem; max-height: 200px; overflow-y: auto; opacity: 0; transform: translateY(-10px); transition: opacity .2s ease, transform .2s ease; pointer-events: none; }
#NQS_GLOBAL_CONTAINER .nqs-custom-dropdown.is-visible { opacity: 1; transform: translateY(0); pointer-events: auto; }
/* 新的模型下拉框样式 */
/* 保留旧的下拉框项目样式以兼容其他功能 */
#NQS_GLOBAL_CONTAINER .nqs-dropdown-item { padding: 0.75rem 1rem; cursor: pointer; transition: background-color .2s; color: var(--nqs-text-primary); }
#NQS_GLOBAL_CONTAINER .nqs-dropdown-item:hover, #NQS_GLOBAL_CONTAINER .nqs-dropdown-item.is-active { background-color: var(--nqs-bg-subtle); }
/* 新的下拉框项目样式 */
#NQS_GLOBAL_CONTAINER .nqs-textarea{min-height:150px;resize:vertical}
/* 保留旧的分类列表样式以兼容其他功能 */
#NQS_GLOBAL_CONTAINER #nqs-category-list{list-style:none;padding:0;margin-top:1rem;display:flex;flex-wrap:wrap;gap:.75rem}
#NQS_GLOBAL_CONTAINER #nqs-category-list li{display:flex;align-items:center;background:var(--nqs-bg-subtle);color:var(--nqs-text-primary);padding:.5rem 1rem;border-radius:8px;font-size:.9rem;font-weight:500; line-height:1; border: 1px solid var(--nqs-border);}
#NQS_GLOBAL_CONTAINER #nqs-category-list .delete-cat{margin-left:.75rem;cursor:pointer;color:var(--nqs-text-secondary);font-weight:700;font-size:1.3em;line-height:1;transition:color .2s ease}
#NQS_GLOBAL_CONTAINER #nqs-category-list .delete-cat:hover{color:var(--nqs-danger)}
/* 新的分类列表样式 */
#NQS_GLOBAL_CONTAINER .nqs-button-text{background:none;border:none;padding:4px 8px;color:var(--nqs-accent);font-weight:500;cursor:pointer;text-align:left;line-height:1.5; font-size: 0.8rem;}
#NQS_GLOBAL_CONTAINER .nqs-button-text:hover{text-decoration:underline}
#NQS_GLOBAL_CONTAINER #nqs-selector-list {display: flex; flex-wrap: wrap; gap: 0.75rem; margin-bottom: 1.5rem;}
#NQS_GLOBAL_CONTAINER #nqs-selector-list .nqs-button-secondary { background: var(--nqs-bg-subtle); color: var(--nqs-text-primary); border-color: var(--nqs-border); }
/* === FAB (悬浮球) 样式 === */
#NQS_GLOBAL_CONTAINER #nqs-fab-container { position: fixed; z-index: 99999; width: 48px; height: 48px; transition: transform 0.3s ease-out; }
#NQS_GLOBAL_CONTAINER #nqs-fab-trigger { width: 100%; height: 100%; background: var(--nqs-accent); color: white; border-radius: 50%; display: flex; justify-content: center; align-items: center; font-size: 22px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); cursor: grab; transition: transform 0.2s ease, box-shadow 0.2s ease; position: relative; z-index: 2; }
#NQS_GLOBAL_CONTAINER #nqs-fab-trigger:active { cursor: grabbing; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3); }
#NQS_GLOBAL_CONTAINER #nqs-fab-container.is-dragging #nqs-fab-trigger { transform: scale(1.1); }
#NQS_GLOBAL_CONTAINER #nqs-fab-container.is-expanded #nqs-fab-trigger { transform: rotate(90deg); }
#NQS_GLOBAL_CONTAINER .nqs-fab-options { position: absolute; bottom: calc(100% + 15px); right: 0; display: flex; flex-direction: column; align-items: flex-end; gap: 12px; transition: opacity 0.2s ease, transform 0.2s ease; opacity: 0; transform: translateY(10px); pointer-events: none; z-index: 1; }
#NQS_GLOBAL_CONTAINER #nqs-fab-container.align-left .nqs-fab-options { right: auto; left: 0; align-items: flex-start; }
#NQS_GLOBAL_CONTAINER #nqs-fab-container.is-expanded .nqs-fab-options { opacity: 1; transform: translateY(0); pointer-events: auto; }
#NQS_GLOBAL_CONTAINER .nqs-fab-action-btn { padding: 8px 16px; color: white; border-radius: 20px; font-size: 14px; font-weight: 600; box-shadow: 0 6px 15px rgba(0,0,0,0.15); transition: all .2s ease-out; white-space: nowrap; cursor: pointer; }
#NQS_GLOBAL_CONTAINER #nqs-save-button { background-color: rgba(0, 122, 255, 0.95); }
#NQS_GLOBAL_CONTAINER #nqs-read-later-button { background-color: rgba(90, 90, 90, 0.95); }
#NQS_GLOBAL_CONTAINER #nqs-fab-container.is-expanded .nqs-fab-options #nqs-read-later-button { transition-delay: 0.05s; }
#NQS_GLOBAL_CONTAINER #nqs-fab-container.is-expanded .nqs-fab-options #nqs-save-button { transition-delay: 0.1s; }
#NQS_GLOBAL_CONTAINER #nqs-fab-container.snapped-right { transform: translateX(30%); }
#NQS_GLOBAL_CONTAINER #nqs-fab-container.snapped-right:hover, #NQS_GLOBAL_CONTAINER #nqs-fab-container.snapped-right.is-expanded { transform: translateX(0); }
#NQS_GLOBAL_CONTAINER #nqs-fab-container.snapped-left { transform: translateX(-30%); }
#NQS_GLOBAL_CONTAINER #nqs-fab-container.snapped-left:hover, #NQS_GLOBAL_CONTAINER #nqs-fab-container.snapped-left.is-expanded { transform: translateX(0); }
/* === 顶部进度条样式 === */
#NQS_GLOBAL_CONTAINER .nqs-top-progress {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100001;
background: var(--nqs-bg);
border-bottom: 1px solid var(--nqs-border);
transform: translateY(-100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
#NQS_GLOBAL_CONTAINER .nqs-top-progress.visible {
transform: translateY(0);
}
#NQS_GLOBAL_CONTAINER .nqs-progress-bar {
height: 3px;
background: var(--nqs-border);
overflow: hidden;
}
#NQS_GLOBAL_CONTAINER .nqs-progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--nqs-accent), var(--nqs-accent-hover));
width: 0%;
transition: width 0.3s ease;
position: relative;
}
#NQS_GLOBAL_CONTAINER .nqs-progress-fill::after {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 20px;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3));
animation: nqs-shimmer 1.5s infinite;
}
@keyframes nqs-shimmer {
0% { transform: translateX(-20px); }
100% { transform: translateX(20px); }
}
#NQS_GLOBAL_CONTAINER .nqs-progress-status {
padding: 0.75rem 1.5rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
#NQS_GLOBAL_CONTAINER .nqs-progress-text {
color: var(--nqs-text-secondary);
font-size: 0.85rem;
font-weight: 500;
letter-spacing: -0.025em;
}
#NQS_GLOBAL_CONTAINER .nqs-progress-status::before {
content: '';
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--nqs-accent);
animation: nqs-pulse 2s infinite;
}
@keyframes nqs-pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(1.2); }
}
/* === 浮动通知样式 === */
#NQS_GLOBAL_CONTAINER .nqs-toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 100002;
pointer-events: none;
display: flex;
flex-direction: column;
gap: 12px;
}
#NQS_GLOBAL_CONTAINER .nqs-toast {
background: var(--nqs-bg);
border: 1px solid var(--nqs-border);
border-radius: var(--nqs-radius-lg);
box-shadow: var(--nqs-shadow-lg);
padding: 1rem 1.5rem;
min-width: 320px;
max-width: 400px;
pointer-events: auto;
transform: translateX(100%);
opacity: 0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border-left: 4px solid var(--nqs-accent);
}
#NQS_GLOBAL_CONTAINER .nqs-toast.visible {
transform: translateX(0);
opacity: 1;
}
#NQS_GLOBAL_CONTAINER .nqs-toast.success {
border-left-color: var(--nqs-success);
}
#NQS_GLOBAL_CONTAINER .nqs-toast.error {
border-left-color: var(--nqs-danger);
}
#NQS_GLOBAL_CONTAINER .nqs-toast.warning {
border-left-color: var(--nqs-warning);
}
#NQS_GLOBAL_CONTAINER .nqs-toast-content {
display: flex;
align-items: flex-start;
gap: 12px;
}
#NQS_GLOBAL_CONTAINER .nqs-toast-icon {
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: bold;
color: white;
flex-shrink: 0;
margin-top: 2px;
}
#NQS_GLOBAL_CONTAINER .nqs-toast.success .nqs-toast-icon {
background: var(--nqs-success);
}
#NQS_GLOBAL_CONTAINER .nqs-toast.error .nqs-toast-icon {
background: var(--nqs-danger);
}
#NQS_GLOBAL_CONTAINER .nqs-toast.warning .nqs-toast-icon {
background: var(--nqs-warning);
}
#NQS_GLOBAL_CONTAINER .nqs-toast.info .nqs-toast-icon {
background: var(--nqs-accent);
}
#NQS_GLOBAL_CONTAINER .nqs-toast-text {
flex: 1;
min-width: 0;
}
#NQS_GLOBAL_CONTAINER .nqs-toast-title {
font-weight: 600;
color: var(--nqs-text-primary);
margin: 0 0 4px 0;
font-size: 0.95rem;
line-height: 1.4;
}
#NQS_GLOBAL_CONTAINER .nqs-toast-message {
color: var(--nqs-text-secondary);
margin: 0;
font-size: 0.85rem;
line-height: 1.5;
}
#NQS_GLOBAL_CONTAINER .nqs-toast-close {
position: absolute;
top: 8px;
right: 8px;
width: 24px;
height: 24px;
border: none;
background: none;
color: var(--nqs-text-tertiary);
cursor: pointer;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
font-size: 14px;
}
#NQS_GLOBAL_CONTAINER .nqs-toast-close:hover {
background: var(--nqs-bg-hover);
color: var(--nqs-text-primary);
}
/* iOS风格的加载状态按钮 */
#NQS_GLOBAL_CONTAINER .nqs-button.loading {
position: relative;
color: transparent !important;
}
#NQS_GLOBAL_CONTAINER .nqs-button.loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 16px;
height: 16px;
margin: -8px 0 0 -8px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: nqs-spin 1s linear infinite;
}
/* === 现代化设置界面样式 === */
#NQS_GLOBAL_CONTAINER .nqs-panel--settings .nqs-body {
padding: 0;
background: var(--nqs-bg-subtle);
}
#NQS_GLOBAL_CONTAINER .nqs-settings-container {
padding: 24px;
max-height: 70vh;
overflow-y: auto;
}
/* === 设置侧边导航布局与样式 === */
#NQS_GLOBAL_CONTAINER .nqs-settings-layout {
display: grid;
grid-template-columns: 220px 1fr;
gap: 16px;
padding: 24px;
height: 70vh;
box-sizing: border-box;
}
#NQS_GLOBAL_CONTAINER .nqs-settings-sidebar {
align-self: start;
position: sticky;
top: 0;
background: var(--nqs-bg);
border: 1px solid var(--nqs-border);
border-radius: var(--nqs-radius-lg);
overflow: hidden;
}
#NQS_GLOBAL_CONTAINER .nqs-settings-nav {
display: flex;
flex-direction: column;
}
#NQS_GLOBAL_CONTAINER .nqs-nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
text-decoration: none;
color: var(--nqs-text-secondary);
border-left: 3px solid transparent;
transition: background .2s ease, color .2s ease, border-color .2s ease;
min-height: 40px; /* 保持各项高度一致,避免“AI 配置”显得突兀 */
line-height: 1.2;
}
#NQS_GLOBAL_CONTAINER .nqs-nav-item:hover {
background: var(--nqs-bg-subtle);
color: var(--nqs-text-primary);
}
#NQS_GLOBAL_CONTAINER .nqs-nav-item.active {
background: var(--nqs-bg-subtle);
color: var(--nqs-accent);
border-left-color: var(--nqs-accent);
}
#NQS_GLOBAL_CONTAINER .nqs-nav-icon {
width: 20px;
height: 20px; /* 统一高度,避免表情/图标导致的垂直错位 */
display: inline-flex;
justify-content: center;
}
/* 导航文字单行省略,避免“AI 配置”等出现换行导致的错位感 */
#NQS_GLOBAL_CONTAINER .nqs-nav-text {
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 140px; /* 为侧栏保留足够留白,防止过长文本折行 */
}
#NQS_GLOBAL_CONTAINER .nqs-settings-content {
overflow: auto;
}
/* 避免内层再产生滚动条,由外层 content 控制滚动 */
#NQS_GLOBAL_CONTAINER .nqs-settings-content .nqs-settings-container {
padding: 0;
max-height: none;
overflow: visible;
}
@media (max-width: 880px) {
#NQS_GLOBAL_CONTAINER .nqs-settings-layout {
grid-template-columns: 1fr;
height: auto;
}
#NQS_GLOBAL_CONTAINER .nqs-settings-sidebar {
position: static;
overflow: auto;
}
#NQS_GLOBAL_CONTAINER .nqs-settings-nav {
flex-direction: row;
gap: 8px;
padding-bottom: 8px;
}
#NQS_GLOBAL_CONTAINER .nqs-nav-item {
border-left: none;
border-bottom: 2px solid transparent;
padding: 10px 12px;
white-space: nowrap;
}
#NQS_GLOBAL_CONTAINER .nqs-nav-item.active {
border-bottom-color: var(--nqs-accent);
}
}
#NQS_GLOBAL_CONTAINER .nqs-settings-group {
background: var(--nqs-bg);
border: 1px solid var(--nqs-border);
border-radius: var(--nqs-radius-lg);
margin-bottom: 20px;
overflow: hidden;
transition: all 0.3s ease;
}
#NQS_GLOBAL_CONTAINER .nqs-settings-group:hover {
border-color: var(--nqs-accent-light);
box-shadow: var(--nqs-shadow);
}
#NQS_GLOBAL_CONTAINER .nqs-settings-group-header {
display: flex;
align-items: center;
gap: 16px;
padding: 20px 24px;
background: var(--nqs-bg-subtle);
border-bottom: 1px solid var(--nqs-border);
}
#NQS_GLOBAL_CONTAINER .nqs-settings-group-icon {
font-size: 24px;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: var(--nqs-accent-light);
border-radius: var(--nqs-radius-lg);
color: var(--nqs-accent);
flex-shrink: 0;
}
#NQS_GLOBAL_CONTAINER .nqs-settings-group-content {
flex: 1;
padding: 24px;
}
#NQS_GLOBAL_CONTAINER .nqs-settings-group-title {
margin: 0 0 8px 0;
font-size: 18px;
font-weight: 600;
color: var(--nqs-text-primary);
line-height: 1.3;
}
#NQS_GLOBAL_CONTAINER .nqs-settings-group-description {
margin: 0;
font-size: 14px;
color: var(--nqs-text-secondary);
line-height: 1.5;
}
/* === 设置字段样式 === */
#NQS_GLOBAL_CONTAINER .nqs-setting-field {
display: grid;
grid-template-columns: 1fr 1.5fr;
gap: 24px;
align-items: flex-start;
margin-bottom: 24px;
padding: 16px 0;
border-bottom: 1px solid var(--nqs-border);
}
#NQS_GLOBAL_CONTAINER .nqs-setting-field:last-child {
border-bottom: none;
margin-bottom: 0;
}
#NQS_GLOBAL_CONTAINER .nqs-setting-label-group {
display: flex;
flex-direction: column;
gap: 8px;
}
#NQS_GLOBAL_CONTAINER .nqs-setting-label-group label {
font-weight: 600;
color: var(--nqs-text-primary);
font-size: 15px;
line-height: 1.4;
}
#NQS_GLOBAL_CONTAINER .nqs-setting-description {
font-size: 13px;
color: var(--nqs-text-secondary);
line-height: 1.5;
margin: 0;
}
#NQS_GLOBAL_CONTAINER .nqs-setting-input-group {
display: flex;
align-items: center;
gap: 12px;
}
/* === 分类管理样式 === */
#NQS_GLOBAL_CONTAINER .nqs-category-manager-section {
margin-top: 16px;
}
#NQS_GLOBAL_CONTAINER .nqs-category-manager-header {
margin-bottom: 16px;
}
#NQS_GLOBAL_CONTAINER .nqs-category-manager-header h4 {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 600;
color: var(--nqs-text-primary);
}
#NQS_GLOBAL_CONTAINER .nqs-category-manager-header p {
margin: 0;
font-size: 13px;
color: var(--nqs-text-secondary);
line-height: 1.5;
}
#NQS_GLOBAL_CONTAINER .nqs-category-input-group {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
#NQS_GLOBAL_CONTAINER .nqs-category-input {
flex: 1;
padding: 12px 16px;
border: 1px solid var(--nqs-border);
border-radius: var(--nqs-radius);
font-size: 14px;
background: var(--nqs-bg);
color: var(--nqs-text-primary);
transition: all 0.2s ease;
}
#NQS_GLOBAL_CONTAINER .nqs-category-input:focus {
outline: none;
border-color: var(--nqs-accent);
box-shadow: 0 0 0 3px var(--nqs-accent-light);
}
#NQS_GLOBAL_CONTAINER .nqs-category-add-btn {
padding: 12px 20px;
white-space: nowrap;
}
#NQS_GLOBAL_CONTAINER .nqs-category-list-container {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
#NQS_GLOBAL_CONTAINER .nqs-category-item {
display: flex;
align-items: center;
gap: 12px;
background: var(--nqs-bg-subtle);
border: 1px solid var(--nqs-border);
border-radius: var(--nqs-radius);
padding: 8px 16px;
transition: all 0.2s ease;
}
#NQS_GLOBAL_CONTAINER .nqs-category-item:hover {
border-color: var(--nqs-accent);
background: var(--nqs-accent-light);
}
#NQS_GLOBAL_CONTAINER .nqs-category-name {
font-size: 14px;
font-weight: 500;
color: var(--nqs-text-primary);
}
#NQS_GLOBAL_CONTAINER .nqs-category-delete-btn {
background: none;
border: none;
color: var(--nqs-text-tertiary);
cursor: pointer;
padding: 4px;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
font-size: 16px;
font-weight: bold;
}
#NQS_GLOBAL_CONTAINER .nqs-category-delete-btn:hover {
background: var(--nqs-danger-light);
color: var(--nqs-danger);
}
/* === AI模型选择器样式 === */
#NQS_GLOBAL_CONTAINER .nqs-model-selector-section {
position: relative;
}
#NQS_GLOBAL_CONTAINER .nqs-model-input-group {
position: relative;
display: flex;
align-items: center;
gap: 12px;
}
#NQS_GLOBAL_CONTAINER .nqs-model-input-group .nqs-input {
padding-right: 50px;
}
#NQS_GLOBAL_CONTAINER .nqs-fetch-models-btn {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
width: 36px;
height: 36px;
border: none;
background: var(--nqs-accent-light);
color: var(--nqs-accent);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
}
#NQS_GLOBAL_CONTAINER .nqs-fetch-models-btn:hover {
background: var(--nqs-accent);
color: white;
transform: translateY(-50%) scale(1.1);
}
#NQS_GLOBAL_CONTAINER .nqs-fetch-models-btn.is-loading svg {
animation: nqs-spin 1s linear infinite;
}
#NQS_GLOBAL_CONTAINER .nqs-model-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 10;
background: var(--nqs-bg);
border: 1px solid var(--nqs-border);
border-radius: var(--nqs-radius);
box-shadow: var(--nqs-shadow-lg);
margin-top: 4px;
max-height: 200px;
overflow-y: auto;
opacity: 0;
transform: translateY(-10px);
transition: all 0.2s ease;
pointer-events: none;
}
#NQS_GLOBAL_CONTAINER .nqs-model-dropdown.is-visible {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
#NQS_GLOBAL_CONTAINER .nqs-dropdown-item {
padding: 12px 16px;
cursor: pointer;
transition: background-color 0.2s;
color: var(--nqs-text-primary);
border-bottom: 1px solid var(--nqs-border);
}
#NQS_GLOBAL_CONTAINER .nqs-dropdown-item:last-child {
border-bottom: none;
}
#NQS_GLOBAL_CONTAINER .nqs-dropdown-item:hover,
#NQS_GLOBAL_CONTAINER .nqs-dropdown-item.is-active {
background: var(--nqs-accent-light);
color: var(--nqs-accent);
}
/* === 提示词头部样式 === */
#NQS_GLOBAL_CONTAINER .nqs-prompt-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
padding: 16px 0;
border-bottom: 1px solid var(--nqs-border);
}
#NQS_GLOBAL_CONTAINER .nqs-prompt-info h4 {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 600;
color: var(--nqs-text-primary);
}
#NQS_GLOBAL_CONTAINER .nqs-prompt-info p {
margin: 0;
font-size: 13px;
color: var(--nqs-text-secondary);
line-height: 1.5;
}
/* === 子分组标题样式 === */
#NQS_GLOBAL_CONTAINER .nqs-subsection-header {
margin: 24px 0 16px 0;
padding: 16px 0;
border-bottom: 1px solid var(--nqs-border);
}
#NQS_GLOBAL_CONTAINER .nqs-subsection-header h4 {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 600;
color: var(--nqs-text-primary);
}
#NQS_GLOBAL_CONTAINER .nqs-subsection-header p {
margin: 0;
font-size: 13px;
color: var(--nqs-text-secondary);
line-height: 1.5;
}
/* === 选择器样式 === */
#NQS_GLOBAL_CONTAINER .nqs-select {
width: 100%;
padding: 12px 16px;
border: 1px solid var(--nqs-border);
border-radius: var(--nqs-radius-lg);
background: linear-gradient(0deg, rgba(0,0,0,0.02), rgba(255,255,255,0.02)), var(--nqs-bg);
color: var(--nqs-text-primary);
font-size: 14px;
cursor: pointer;
transition: border-color .15s ease, box-shadow .15s ease, background-color .15s ease, transform .15s ease;
min-height: 40px;
line-height: 1.2;
box-shadow: inset 0 1px 0 rgba(0,0,0,0.03);
}
#NQS_GLOBAL_CONTAINER .nqs-select:hover {
border-color: var(--nqs-border-hover);
background-color: var(--nqs-bg-hover);
}
#NQS_GLOBAL_CONTAINER .nqs-select:focus {
outline: none;
border-color: var(--nqs-accent);
box-shadow: 0 0 0 3px var(--nqs-accent-light);
}
#NQS_GLOBAL_CONTAINER .nqs-select:focus-visible {
outline: none;
border-color: var(--nqs-accent);
box-shadow: 0 0 0 3px var(--nqs-accent-light);
}
/* === 现代化智能收藏夹管理样式 === */
#NQS_GLOBAL_CONTAINER .nqs-bookmark-manager {
padding: 24px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
background: var(--nqs-bg-subtle);
border-radius: var(--nqs-radius-lg);
}
/* === 主要功能卡片网格 === */
#NQS_GLOBAL_CONTAINER .nqs-feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 32px;
}
#NQS_GLOBAL_CONTAINER .nqs-feature-card {
background: var(--nqs-bg);
border: 1px solid var(--nqs-border);
border-radius: var(--nqs-radius-lg);
padding: 24px;
display: flex;
align-items: center;
gap: 20px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
#NQS_GLOBAL_CONTAINER .nqs-feature-card::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.05), transparent);
transition: left 0.6s ease;
}
#NQS_GLOBAL_CONTAINER .nqs-feature-card:hover::before {
left: 100%;
}
#NQS_GLOBAL_CONTAINER .nqs-feature-card:hover {
border-color: var(--nqs-accent);
box-shadow: var(--nqs-shadow-lg);
}
#NQS_GLOBAL_CONTAINER .nqs-feature-card.nqs-feature-primary {
border-color: var(--nqs-accent);
background: linear-gradient(135deg, var(--nqs-bg) 0%, rgba(59, 130, 246, 0.02) 100%);
}
#NQS_GLOBAL_CONTAINER .nqs-feature-icon {
font-size: 32px;
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
background: var(--nqs-bg-subtle);
border-radius: var(--nqs-radius-lg);
flex-shrink: 0;
}
#NQS_GLOBAL_CONTAINER .nqs-feature-primary .nqs-feature-icon {
background: linear-gradient(135deg, var(--nqs-accent-light), var(--nqs-accent));
color: white;
}
#NQS_GLOBAL_CONTAINER .nqs-feature-content {
flex: 1;
}
#NQS_GLOBAL_CONTAINER .nqs-feature-content h4 {
margin: 0 0 8px 0;
font-size: 18px;
font-weight: 600;
color: var(--nqs-text-primary);
line-height: 1.3;
}
#NQS_GLOBAL_CONTAINER .nqs-feature-content p {
margin: 0;
font-size: 14px;
color: var(--nqs-text-secondary);
line-height: 1.5;
}
#NQS_GLOBAL_CONTAINER .nqs-feature-badge {
position: absolute;
top: 12px;
right: 12px;
background: linear-gradient(135deg, #FF6B6B, #FF8E53);
color: white;
padding: 4px 12px;
border-radius: var(--nqs-radius);
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
#NQS_GLOBAL_CONTAINER .nqs-feature-arrow {
font-size: 20px;
color: var(--nqs-text-tertiary);
transition: all 0.3s ease;
}
#NQS_GLOBAL_CONTAINER .nqs-feature-card:hover .nqs-feature-arrow {
color: var(--nqs-accent);
transform: translateX(4px);
}
/* === 快速分类保存区域 === */
#NQS_GLOBAL_CONTAINER .nqs-quick-save-section {
margin-bottom: 32px;
}
#NQS_GLOBAL_CONTAINER .nqs-section-header {
margin-bottom: 20px;
}
#NQS_GLOBAL_CONTAINER .nqs-section-header h3 {
margin: 0 0 8px 0;
font-size: 20px;
font-weight: 600;
color: var(--nqs-text-primary);
display: flex;
align-items: center;
gap: 12px;
}
#NQS_GLOBAL_CONTAINER .nqs-section-header p {
margin: 0;
font-size: 14px;
color: var(--nqs-text-secondary);
line-height: 1.5;
}
#NQS_GLOBAL_CONTAINER .nqs-quick-categories {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 16px;
}
#NQS_GLOBAL_CONTAINER .nqs-category-btn {
background: var(--nqs-bg);
border: 1px solid var(--nqs-border);
border-radius: var(--nqs-radius-lg);
padding: 16px 20px;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
}
#NQS_GLOBAL_CONTAINER .nqs-category-btn:hover {
border-color: var(--nqs-accent);
background: var(--nqs-accent-light);
transform: translateY(-2px);
box-shadow: var(--nqs-shadow);
}
#NQS_GLOBAL_CONTAINER .nqs-category-icon {
font-size: 24px;
display: block;
}
#NQS_GLOBAL_CONTAINER .nqs-category-name {
font-size: 13px;
font-weight: 500;
color: var(--nqs-text-primary);
line-height: 1.2;
}
/* === 页面信息预览 === */
#NQS_GLOBAL_CONTAINER .nqs-page-preview {
margin-bottom: 24px;
}
#NQS_GLOBAL_CONTAINER .nqs-page-info {
background: var(--nqs-bg);
border: 1px solid var(--nqs-border);
border-radius: var(--nqs-radius-lg);
padding: 20px;
}
#NQS_GLOBAL_CONTAINER .nqs-page-title {
font-size: 16px;
font-weight: 600;
color: var(--nqs-text-primary);
margin-bottom: 8px;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
#NQS_GLOBAL_CONTAINER .nqs-page-url {
font-size: 13px;
color: var(--nqs-accent);
margin-bottom: 12px;
word-break: break-all;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
#NQS_GLOBAL_CONTAINER .nqs-page-meta {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
#NQS_GLOBAL_CONTAINER .nqs-meta-item {
display: flex;
align-items: center;
gap: 6px;
}
#NQS_GLOBAL_CONTAINER .nqs-meta-label {
font-size: 12px;
color: var(--nqs-text-tertiary);
font-weight: 500;
}
#NQS_GLOBAL_CONTAINER .nqs-meta-value {
font-size: 12px;
color: var(--nqs-text-secondary);
background: var(--nqs-bg-subtle);
padding: 2px 8px;
border-radius: var(--nqs-radius);
}
/* === 底部操作栏 === */
#NQS_GLOBAL_CONTAINER .nqs-bookmark-footer {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
gap: 20px;
}
#NQS_GLOBAL_CONTAINER .nqs-footer-left,
#NQS_GLOBAL_CONTAINER .nqs-footer-center,
#NQS_GLOBAL_CONTAINER .nqs-footer-right {
display: flex;
align-items: center;
}
#NQS_GLOBAL_CONTAINER .nqs-footer-center {
flex: 1;
justify-content: center;
}
#NQS_GLOBAL_CONTAINER .nqs-bookmark-footer .nqs-button {
display: flex;
align-items: center;
gap: 8px;
}
#NQS_GLOBAL_CONTAINER .nqs-bookmark-footer .nqs-button span:first-child {
font-size: 16px;
}
/* ===== 追加的设置界面优化样式 ===== */
/* 小屏幕:设置字段改为单列,间距更紧凑 */
@media (max-width: 720px) {
#NQS_GLOBAL_CONTAINER .nqs-setting-field {
grid-template-columns: 1fr !important;
gap: 12px !important;
padding: 12px 0 !important;
}
}
/* 设置内容滚动体验优化 */
#NQS_GLOBAL_CONTAINER .nqs-settings-content {
overscroll-behavior: contain;
}
/* 统一滚动条样式(WebKit) */
#NQS_GLOBAL_CONTAINER .nqs-settings-content::-webkit-scrollbar,
#NQS_GLOBAL_CONTAINER .nqs-settings-container::-webkit-scrollbar,
#NQS_GLOBAL_CONTAINER .nqs-json-viewer::-webkit-scrollbar,
#NQS_GLOBAL_CONTAINER .nqs-model-dropdown::-webkit-scrollbar {
width: 10px;
height: 10px;
}
#NQS_GLOBAL_CONTAINER .nqs-settings-content::-webkit-scrollbar-thumb,
#NQS_GLOBAL_CONTAINER .nqs-settings-container::-webkit-scrollbar-thumb,
#NQS_GLOBAL_CONTAINER .nqs-json-viewer::-webkit-scrollbar-thumb,
#NQS_GLOBAL_CONTAINER .nqs-model-dropdown::-webkit-scrollbar-thumb {
background: var(--nqs-border);
border-radius: 8px;
border: 2px solid transparent;
background-clip: content-box;
}
#NQS_GLOBAL_CONTAINER .nqs-settings-content::-webkit-scrollbar-thumb:hover,
#NQS_GLOBAL_CONTAINER .nqs-settings-container::-webkit-scrollbar-thumb:hover,
#NQS_GLOBAL_CONTAINER .nqs-json-viewer::-webkit-scrollbar-thumb:hover,
#NQS_GLOBAL_CONTAINER .nqs-model-dropdown::-webkit-scrollbar-thumb:hover {
background: var(--nqs-border-hover);
}
/* 导航项键盘焦点样式(可达性) */
/* 仅键盘导航时显示焦点态,避免鼠标点击后残留视觉高亮 */
#NQS_GLOBAL_CONTAINER .nqs-nav-item:focus-visible {
outline: none;
color: var(--nqs-accent);
box-shadow: 0 0 0 3px var(--nqs-accent-light) inset;
}
/* 输入/选择器交互反馈更细腻 */
#NQS_GLOBAL_CONTAINER .nqs-input,
#NQS_GLOBAL_CONTAINER .nqs-textarea,
#NQS_GLOBAL_CONTAINER .nqs-select {
transition: border-color .15s ease, box-shadow .15s ease, background-color .15s ease, transform .15s ease;
}
/* 模型下拉样式优化 */
#NQS_GLOBAL_CONTAINER .nqs-model-dropdown {
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
border-color: var(--nqs-border-hover);
}
/* 暗色模式下边框对比度提升 */
#NQS_GLOBAL_CONTAINER[data-theme='dark'] .nqs-panel,
#NQS_GLOBAL_CONTAINER[data-theme='dark'] .nqs-settings-group,
#NQS_GLOBAL_CONTAINER[data-theme='dark'] .nqs-header,
#NQS_GLOBAL_CONTAINER[data-theme='dark'] .nqs-footer {
border-color: rgba(148, 163, 184, 0.22);
}
/* 移动端 Toast 布局优化 */
@media (max-width: 520px) {
#NQS_GLOBAL_CONTAINER .nqs-toast-container {
top: auto;
bottom: 12px;
right: 12px;
left: 12px;
}
#NQS_GLOBAL_CONTAINER .nqs-toast {
width: 100%;
min-width: auto;
max-width: none;
}
}
/* 尊重“减少动态效果”设置 */
@media (prefers-reduced-motion: reduce) {
#NQS_GLOBAL_CONTAINER * {
animation: none !important;
transition: none !important;
}
}
/* ===== 追加样式(本次优化) ===== */
/* 设置面板:仅保留内部滚动(去掉外层面板滚动条) */
#NQS_GLOBAL_CONTAINER .nqs-panel--settings .nqs-body { overflow: hidden; }
/* 提示词文本域专属优化 */
#NQS_GLOBAL_CONTAINER #nqs-ai_prompt,
#NQS_GLOBAL_CONTAINER .nqs-prompt-textarea {
min-height: 240px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
font-size: 13.5px;
line-height: 1.6;
white-space: pre-wrap;
tab-size: 2;
}
#NQS_GLOBAL_CONTAINER .nqs-prompt-meta {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 6px;
color: var(--nqs-text-tertiary);
font-size: 12px;
}
/* 左侧导航视觉优化:更柔和的卡片与高亮 */
#NQS_GLOBAL_CONTAINER .nqs-settings-sidebar { padding: 8px; }
#NQS_GLOBAL_CONTAINER .nqs-settings-nav { gap: 6px; padding: 4px; }
#NQS_GLOBAL_CONTAINER .nqs-nav-item {
border-radius: 10px;
margin: 2px 4px;
}
#NQS_GLOBAL_CONTAINER .nqs-nav-item.active {
background: var(--nqs-accent-light);
color: var(--nqs-accent);
}
#NQS_GLOBAL_CONTAINER .nqs-nav-text { font-weight: 600; letter-spacing: 0.2px; }
/* 右侧内容区域滚动优化 */
#NQS_GLOBAL_CONTAINER .nqs-settings-content { overflow: auto; overscroll-behavior: contain; }
/* 全宽字段布局:用于无标签或大块输入(如系统提示词) */
#NQS_GLOBAL_CONTAINER .nqs-setting-field--full {
grid-template-columns: 1fr !important;
}
#NQS_GLOBAL_CONTAINER .nqs-setting-field--full .nqs-setting-label-group {
display: none;
}
/* 统一 Select 下拉样式(与输入框一致) */
#NQS_GLOBAL_CONTAINER .nqs-select {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 20 20' fill='none'%3E%3Cpath d='M5 7l5 5 5-5' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 12px 12px;
padding-right: 40px; /* 预留箭头空间 */
font-family: inherit;
}
#NQS_GLOBAL_CONTAINER[data-theme='dark'] .nqs-select {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 20 20' fill='none'%3E%3Cpath d='M5 7l5 5 5-5' stroke='%23cbd5e1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
#NQS_GLOBAL_CONTAINER .nqs-select:disabled {
background-color: var(--nqs-bg-subtle);
color: var(--nqs-text-tertiary);
cursor: not-allowed;
}
/* 多选下拉的尺寸与滚动控制 */
#NQS_GLOBAL_CONTAINER .nqs-select[multiple] {
min-height: 140px;
background-image: none;
padding-right: 16px;
overflow: auto;
}
#NQS_GLOBAL_CONTAINER .nqs-select[multiple] option {
padding: 6px 8px;
}
/* 模板占位符提示徽章与预览 */
#NQS_GLOBAL_CONTAINER .nqs-template-badges { display:flex; gap:6px; flex-wrap: wrap; margin-bottom: 6px; }
#NQS_GLOBAL_CONTAINER .nqs-badge { display:inline-flex; align-items:center; padding: 2px 6px; border-radius: 999px; font-size: 12px; border: 1px solid var(--nqs-border); }
#NQS_GLOBAL_CONTAINER .nqs-badge.ok { background: var(--nqs-success-light); color: var(--nqs-success); border-color: var(--nqs-success); }
#NQS_GLOBAL_CONTAINER .nqs-badge.warn { background: var(--nqs-warning-light); color: var(--nqs-warning); border-color: var(--nqs-warning); }
#NQS_GLOBAL_CONTAINER .nqs-template-preview { max-height: 120px; overflow: hidden; white-space: pre-wrap; color: var(--nqs-text-secondary); font-size: 12px; border-top: 1px dashed var(--nqs-border); padding-top: 6px; }
/* Toggle 开关统一 hover/focus/disabled 细节 */
#NQS_GLOBAL_CONTAINER .nqs-toggle-switch .nqs-switch:hover .nqs-slider {
filter: brightness(0.98);
}
#NQS_GLOBAL_CONTAINER .nqs-toggle-switch .nqs-switch:focus-within .nqs-slider {
box-shadow: 0 0 0 3px var(--nqs-accent-light);
}
#NQS_GLOBAL_CONTAINER .nqs-toggle-switch .nqs-switch input:disabled + .nqs-slider {
background: var(--nqs-border);
cursor: not-allowed;
opacity: 0.7;
}
/* 提示词模板库样式 */
#NQS_GLOBAL_CONTAINER .nqs-prompt-templates { margin-top: 12px; }
#NQS_GLOBAL_CONTAINER .nqs-prompt-templates-header { display:flex; justify-content: space-between; align-items:center; margin: 8px 0 10px; }
#NQS_GLOBAL_CONTAINER .nqs-template-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
#NQS_GLOBAL_CONTAINER .nqs-template-card { background: var(--nqs-bg); border:1px solid var(--nqs-border); border-radius: var(--nqs-radius-lg); padding: 12px; display:flex; flex-direction: column; gap:10px; transition: box-shadow .2s, border-color .2s; }
#NQS_GLOBAL_CONTAINER .nqs-template-card:hover { border-color: var(--nqs-accent); box-shadow: var(--nqs-shadow); }
#NQS_GLOBAL_CONTAINER .nqs-template-card.is-active { border-color: var(--nqs-accent); box-shadow: var(--nqs-shadow-lg); background: linear-gradient(0deg, var(--nqs-accent-light), transparent); }
#NQS_GLOBAL_CONTAINER .nqs-template-card--compact .nqs-template-preview { display: none; }
#NQS_GLOBAL_CONTAINER .nqs-template-select input {
appearance: none;
-webkit-appearance: none;
width: 16px; height: 16px;
border-radius: 50%;
border: 2px solid var(--nqs-border-hover);
background: var(--nqs-bg);
display: inline-block;
transition: all .15s ease;
box-shadow: inset 0 0 0 0 var(--nqs-success);
}
#NQS_GLOBAL_CONTAINER .nqs-template-select input:checked {
border-color: var(--nqs-success);
box-shadow: inset 0 0 0 4px var(--nqs-success);
}
#NQS_GLOBAL_CONTAINER .nqs-template-card.is-active .nqs-template-select input {
border-color: var(--nqs-success);
box-shadow: inset 0 0 0 4px var(--nqs-success);
}
#NQS_GLOBAL_CONTAINER .nqs-template-card-top { position: relative; padding-right: 8px; }
#NQS_GLOBAL_CONTAINER .nqs-template-select { position: absolute; top: 0; right: 0; }
#NQS_GLOBAL_CONTAINER .nqs-template-title { margin: 0 24px 0 0; font-size: 14px; color: var(--nqs-text-primary); font-weight: 600; }
#NQS_GLOBAL_CONTAINER .nqs-template-brief { display:none; }
#NQS_GLOBAL_CONTAINER .nqs-template-card-actions { display:flex; gap:8px; justify-content:flex-end; }
/* 提示词模式分段控件 */
#NQS_GLOBAL_CONTAINER .nqs-segmented { display:inline-flex; border:1px solid var(--nqs-border); border-radius: 8px; overflow:hidden; background: var(--nqs-bg); margin: 8px 0 12px; }
#NQS_GLOBAL_CONTAINER .nqs-seg-btn { padding: 6px 12px; font-size: 13px; color: var(--nqs-text-secondary); background: transparent; border: none; cursor: pointer; }
#NQS_GLOBAL_CONTAINER .nqs-seg-btn.is-active { color: var(--nqs-accent); background: var(--nqs-accent-light); }
/* 模板/自定义模式切换显示控制 */
#NQS_GLOBAL_CONTAINER .nqs-settings-group-content[data-prompt-mode="template"] [data-field-id="ai_prompt"] { display:none; }
#NQS_GLOBAL_CONTAINER .nqs-settings-group-content[data-prompt-mode="custom"] .nqs-prompt-templates { display:none; }
/* ===== 追加样式结束 ===== */
/* ===== 追加样式 v2(导航与提示词卡片美化) ===== */
/* 左侧导航:更清晰的激活态与指示条 */
#NQS_GLOBAL_CONTAINER .nqs-settings-sidebar {
border-radius: var(--nqs-radius-lg);
background: linear-gradient(0deg, rgba(0,0,0,0.02), rgba(255,255,255,0.02)), var(--nqs-bg);
}
#NQS_GLOBAL_CONTAINER .nqs-nav-item {
position: relative;
border-left: 3px solid transparent;
}
#NQS_GLOBAL_CONTAINER .nqs-nav-item::after {
content: '›';
position: absolute;
right: 10px;
font-size: 14px;
color: var(--nqs-text-tertiary);
transition: color .2s ease, transform .2s ease;
}
#NQS_GLOBAL_CONTAINER .nqs-nav-item:hover::after {
color: var(--nqs-accent);
transform: translateX(2px);
}
#NQS_GLOBAL_CONTAINER .nqs-nav-item.active {
border-left-color: var(--nqs-accent);
background: linear-gradient(0deg, var(--nqs-accent-light), transparent);
}
#NQS_GLOBAL_CONTAINER .nqs-nav-item.active::after {
background: var(--nqs-accent);
}
/* 分组:滚动校准,避免粘到顶部时视觉拥挤 */
#NQS_GLOBAL_CONTAINER .nqs-settings-group {
scroll-margin-top: 12px;
}
/* 提示词模板卡片:层次更分明、交互更细腻 */
#NQS_GLOBAL_CONTAINER .nqs-template-card {
border: 1px solid var(--nqs-border);
background: linear-gradient(180deg, rgba(0,0,0,0.00), rgba(0,0,0,0.02)) , var(--nqs-bg);
transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
}
#NQS_GLOBAL_CONTAINER .nqs-template-card:hover {
transform: translateY(-2px);
border-color: var(--nqs-accent);
box-shadow: var(--nqs-shadow-lg);
}
#NQS_GLOBAL_CONTAINER .nqs-template-card.is-active {
border-color: var(--nqs-accent);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15), var(--nqs-shadow);
background: linear-gradient(0deg, var(--nqs-accent-light), transparent);
}
#NQS_GLOBAL_CONTAINER .nqs-template-card-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 8px;
}
#NQS_GLOBAL_CONTAINER .nqs-template-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#NQS_GLOBAL_CONTAINER .nqs-template-card-actions .nqs-button {
padding: 6px 10px;
}
/* Prompt 模式切换控件:更显著的激活态 */
#NQS_GLOBAL_CONTAINER .nqs-seg-btn {
border-right: 1px solid var(--nqs-border);
}
#NQS_GLOBAL_CONTAINER .nqs-seg-btn:last-child {
border-right: none;
}
#NQS_GLOBAL_CONTAINER .nqs-seg-btn.is-active {
color: var(--nqs-accent);
background: linear-gradient(0deg, var(--nqs-accent-light), transparent);
}
/* 模型下拉:活动项更明显 */
#NQS_GLOBAL_CONTAINER .nqs-model-dropdown .nqs-dropdown-item.is-active {
background: var(--nqs-accent-light);
color: var(--nqs-accent);
}
/* ===== 追加样式 v2 结束 ===== */
`; const style = document.createElement('style'); style.id = styleId; style.textContent = css; container.appendChild(style); }
const showNotification = (button, message, stay = false, originalText) => { if (!button) return; button.textContent = message; if (!stay) { setTimeout(() => { button.textContent = originalText; }, 3000); } };
async function initFloatingButtons() { if (fabInstance) fabInstance.destroy(); fabInstance = new FloatingActionButton(); await fabInstance.init(); }
function savePageViaMenu() { startSaveProcess({ source: 'menu' }); }
function savePageForLaterViaMenu() { startReadLaterSave({ source: 'menu' }); }
async function runScript() {
await applyTheme();
GM_registerMenuCommand('⚙️ 设置 (Settings)', openSettingsPanel);
GM_registerMenuCommand('📋 查看日志 (View Logs)', openLogViewerPanel);
GM_registerMenuCommand('─'.repeat(20), () => {});
GM_registerMenuCommand('➤ 保存到 Notion', savePageViaMenu);
GM_registerMenuCommand('◷ 保存为稍后读', savePageForLaterViaMenu);
GM_registerMenuCommand('📝 智能文本摘录', () => saveSelectedTextAsNote());
GM_registerMenuCommand('📚 智能收藏夹管理', () => openSmartBookmarkManager());
GM_registerMenuCommand('─'.repeat(20), () => {});
try {
initFloatingButtons();
} catch (e) {
console.error("NQS: 无法初始化悬浮按钮UI。这可能是由于网站的安全策略(CSP)限制。请放心使用油猴菜单中的备用按钮,功能完全相同。", e);
addLog('error', '悬浮按钮UI初始化失败', { error: e.message, stack: e.stack });
}
}
runScript();
})();