// ==UserScript== // @name 网页剪藏工具 Pro (整合版) // @namespace http://tampermonkey.net/ // @version 5.0.1 // @description 完整整合版网页剪藏工具,内置内容提取和AI标签生成,支持自定义标签、选择笔记本和现代化UI // @author You // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_addStyle // @require https://cdn.jsdelivr.net/npm/@mozilla/readability@0.4.2/Readability.js // @require https://cdn.jsdelivr.net/npm/turndown@7.1.2/dist/turndown.js // @connect * // ==/UserScript== (function() { 'use strict'; // ===== 1. 配置管理模块 ===== const Config = { keys: { siyuanApiUrl: 'web_clipper_siyuan_api_url', siyuanApiToken: 'web_clipper_siyuan_api_token', aiApiUrl: 'web_clipper_ai_api_url', aiApiKey: 'web_clipper_ai_api_key', aiModel: 'web_clipper_ai_model', defaultNotebookId: 'web_clipper_default_notebook_id', buttonPosition: 'web_clipper_button_position', lastSelectedNotebookId: 'web_clipper_last_notebook_id' }, getAll: function() { return { siyuanApiUrl: GM_getValue(this.keys.siyuanApiUrl, ''), siyuanApiToken: GM_getValue(this.keys.siyuanApiToken, ''), aiApiUrl: GM_getValue(this.keys.aiApiUrl, ''), aiApiKey: GM_getValue(this.keys.aiApiKey, ''), aiModel: GM_getValue(this.keys.aiModel, 'glm-4-flash'), defaultNotebookId: GM_getValue(this.keys.defaultNotebookId, ''), buttonPosition: GM_getValue(this.keys.buttonPosition, { top: '100px', right: '20px' }), lastSelectedNotebookId: GM_getValue(this.keys.lastSelectedNotebookId, '') }; }, setAll: function(newConfig) { GM_setValue(this.keys.siyuanApiUrl, newConfig.siyuanApiUrl); GM_setValue(this.keys.siyuanApiToken, newConfig.siyuanApiToken); GM_setValue(this.keys.aiApiUrl, newConfig.aiApiUrl); GM_setValue(this.keys.aiApiKey, newConfig.aiApiKey); GM_setValue(this.keys.aiModel, newConfig.aiModel); GM_setValue(this.keys.defaultNotebookId, newConfig.defaultNotebookId); GM_setValue(this.keys.buttonPosition, newConfig.buttonPosition); if (newConfig.lastSelectedNotebookId !== undefined) { GM_setValue(this.keys.lastSelectedNotebookId, newConfig.lastSelectedNotebookId); } }, isComplete: function() { const config = this.getAll(); return config.siyuanApiUrl && config.siyuanApiToken && config.aiApiUrl && config.aiApiKey && config.aiModel; }, showSettings: function() { const config = this.getAll(); // 移除已存在的模态框 const existingModal = document.getElementById('web-clipper-settings-modal'); if (existingModal) { existingModal.remove(); } // 添加样式 GM_addStyle(` #web-clipper-settings-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.3); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); z-index: 2147483647; display: flex; justify-content: center; align-items: center; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } #web-clipper-settings-content { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); padding: 32px; border-radius: 20px; width: 90%; max-width: 600px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); position: relative; } #web-clipper-settings-content h2 { margin-top: 0; color: #1a1a1a; font-size: 20px; display: flex; align-items: center; gap: 8px; } #web-clipper-settings-content label { display: block; margin-top: 16px; margin-bottom: 6px; font-weight: 500; color: #4a4a4a; font-size: 14px; } #web-clipper-settings-content input, #web-clipper-settings-content textarea { width: 100%; padding: 12px; box-sizing: border-box; border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 12px; font-size: 14px; transition: all 0.3s; background: rgba(255, 255, 255, 0.8); } #web-clipper-settings-content input:focus, #web-clipper-settings-content textarea:focus { outline: none; border-color: rgba(76, 175, 80, 0.5); box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1); background: rgba(255, 255, 255, 0.95); } #web-clipper-settings-buttons { margin-top: 24px; text-align: right; display: flex; gap: 12px; justify-content: flex-end; } #web-clipper-settings-buttons button { padding: 12px 24px; border: none; border-radius: 12px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.3s; min-width: 80px; } #web-clipper-save-btn { background: rgba(76, 175, 80, 0.9); color: white; } #web-clipper-save-btn:hover { background: rgba(76, 175, 80, 1); transform: translateY(-1px); } #web-clipper-cancel-btn { background: rgba(0, 0, 0, 0.1); color: #4a4a4a; } #web-clipper-cancel-btn:hover { background: rgba(0, 0, 0, 0.15); } .config-section { margin-bottom: 24px; border-bottom: 1px solid rgba(0,0,0,0.05); padding-bottom: 20px; } .config-section:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; } .section-title { font-weight: 600; color: #1a1a1a; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; } `); // 创建模态框 const modal = document.createElement('div'); modal.id = 'web-clipper-settings-modal'; const content = document.createElement('div'); content.id = 'web-clipper-settings-content'; // 创建标题 const title = document.createElement('h2'); title.textContent = '⚙️ 剪藏工具配置'; content.appendChild(title); // 思源笔记配置部分 const siyuanSection = document.createElement('div'); siyuanSection.className = 'config-section'; const siyuanTitle = document.createElement('div'); siyuanTitle.className = 'section-title'; siyuanTitle.textContent = '📚 思源笔记配置'; siyuanSection.appendChild(siyuanTitle); // 思源 API 地址 const siyuanApiUrlLabel = document.createElement('label'); siyuanApiUrlLabel.textContent = '思源笔记 API 地址'; siyuanApiUrlLabel.setAttribute('for', 'siyuanApiUrl'); siyuanSection.appendChild(siyuanApiUrlLabel); const siyuanApiUrlInput = document.createElement('input'); siyuanApiUrlInput.type = 'url'; siyuanApiUrlInput.id = 'siyuanApiUrl'; siyuanApiUrlInput.placeholder = 'http://localhost:6806'; siyuanApiUrlInput.value = config.siyuanApiUrl; siyuanSection.appendChild(siyuanApiUrlInput); // 思源 API Token const siyuanApiTokenLabel = document.createElement('label'); siyuanApiTokenLabel.textContent = '思源笔记 API Token'; siyuanApiTokenLabel.setAttribute('for', 'siyuanApiToken'); siyuanSection.appendChild(siyuanApiTokenLabel); const siyuanApiTokenInput = document.createElement('input'); siyuanApiTokenInput.type = 'password'; siyuanApiTokenInput.id = 'siyuanApiToken'; siyuanApiTokenInput.placeholder = '请输入API Token'; siyuanApiTokenInput.value = config.siyuanApiToken; siyuanSection.appendChild(siyuanApiTokenInput); // 默认笔记本ID const defaultNotebookIdLabel = document.createElement('label'); defaultNotebookIdLabel.textContent = '默认笔记本ID (可选,可在剪藏时选择)'; defaultNotebookIdLabel.setAttribute('for', 'defaultNotebookId'); siyuanSection.appendChild(defaultNotebookIdLabel); const defaultNotebookIdInput = document.createElement('input'); defaultNotebookIdInput.type = 'text'; defaultNotebookIdInput.id = 'defaultNotebookId'; defaultNotebookIdInput.placeholder = '请输入笔记本ID'; defaultNotebookIdInput.value = config.defaultNotebookId; siyuanSection.appendChild(defaultNotebookIdInput); content.appendChild(siyuanSection); // AI 配置部分 const aiSection = document.createElement('div'); aiSection.className = 'config-section'; const aiTitle = document.createElement('div'); aiTitle.className = 'section-title'; aiTitle.textContent = '🤖 AI 配置'; aiSection.appendChild(aiTitle); // AI API 地址 const aiApiUrlLabel = document.createElement('label'); aiApiUrlLabel.textContent = 'AI API 地址'; aiApiUrlLabel.setAttribute('for', 'aiApiUrl'); aiSection.appendChild(aiApiUrlLabel); const aiApiUrlInput = document.createElement('input'); aiApiUrlInput.type = 'url'; aiApiUrlInput.id = 'aiApiUrl'; aiApiUrlInput.placeholder = 'https://api.openai.com/v1/chat/completions'; aiApiUrlInput.value = config.aiApiUrl; aiSection.appendChild(aiApiUrlInput); // AI API Key const aiApiKeyLabel = document.createElement('label'); aiApiKeyLabel.textContent = 'AI API Key'; aiApiKeyLabel.setAttribute('for', 'aiApiKey'); aiSection.appendChild(aiApiKeyLabel); const aiApiKeyInput = document.createElement('input'); aiApiKeyInput.type = 'password'; aiApiKeyInput.id = 'aiApiKey'; aiApiKeyInput.placeholder = '请输入API Key'; aiApiKeyInput.value = config.aiApiKey; aiSection.appendChild(aiApiKeyInput); // AI 模型 const aiModelLabel = document.createElement('label'); aiModelLabel.textContent = 'AI 模型'; aiModelLabel.setAttribute('for', 'aiModel'); aiSection.appendChild(aiModelLabel); const aiModelInput = document.createElement('input'); aiModelInput.type = 'text'; aiModelInput.id = 'aiModel'; aiModelInput.placeholder = 'glm-4-flash'; aiModelInput.value = config.aiModel; aiSection.appendChild(aiModelInput); content.appendChild(aiSection); // 创建按钮 const buttons = document.createElement('div'); buttons.id = 'web-clipper-settings-buttons'; const cancelButton = document.createElement('button'); cancelButton.id = 'web-clipper-cancel-btn'; cancelButton.textContent = '取消'; cancelButton.addEventListener('click', () => { modal.remove(); }); buttons.appendChild(cancelButton); const saveButton = document.createElement('button'); saveButton.id = 'web-clipper-save-btn'; saveButton.textContent = '保存'; saveButton.addEventListener('click', () => { const newConfig = { siyuanApiUrl: siyuanApiUrlInput.value.trim(), siyuanApiToken: siyuanApiTokenInput.value.trim(), aiApiUrl: aiApiUrlInput.value.trim(), aiApiKey: aiApiKeyInput.value.trim(), aiModel: aiModelInput.value.trim(), defaultNotebookId: defaultNotebookIdInput.value.trim(), buttonPosition: config.buttonPosition, lastSelectedNotebookId: config.lastSelectedNotebookId }; this.setAll(newConfig); modal.remove(); Toast.show('配置已保存!', 'success'); setTimeout(() => { if (!this.isComplete()) { this.showSettings(); } }, 1000); }); buttons.appendChild(saveButton); content.appendChild(buttons); modal.appendChild(content); // 添加到body document.body.appendChild(modal); // 绑定背景点击事件 modal.addEventListener('click', (e) => { if (e.target === modal) { modal.remove(); } }); } }; // ===== 2. Toast 通知系统 ===== const Toast = { show: function(message, type = 'info', duration = 3000) { // 添加样式 if (!document.querySelector('#toast-styles')) { GM_addStyle(` .toast { position: fixed; top: 20px; right: 20px; z-index: 2147483647; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border-radius: 12px; padding: 16px 20px; box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2); display: flex; align-items: center; gap: 12px; min-width: 250px; animation: slideInRight 0.3s ease; max-width: 90vw; } .toast-success { border-left: 4px solid rgba(76, 175, 80, 0.8); } .toast-error { border-left: 4px solid rgba(244, 67, 54, 0.8); } .toast-info { border-left: 4px solid rgba(33, 150, 243, 0.8); } .toast-icon { font-size: 20px; } .toast-message { flex: 1; font-size: 14px; color: #1a1a1a; } @keyframes slideInRight { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } `); } // 创建toast元素 const toast = document.createElement('div'); toast.className = `toast toast-${type}`; // 创建图标 const icon = document.createElement('div'); icon.className = 'toast-icon'; icon.textContent = this.getIcon(type); // 创建消息 const messageEl = document.createElement('div'); messageEl.className = 'toast-message'; messageEl.textContent = message; // 组装 toast.appendChild(icon); toast.appendChild(messageEl); // 添加到body document.body.appendChild(toast); // 自动移除 setTimeout(() => { toast.style.animation = 'slideInRight 0.3s ease reverse'; setTimeout(() => toast.remove(), 300); }, duration); }, getIcon: function(type) { const icons = { success: '✅', error: '❌', info: 'ℹ️' }; return icons[type] || icons.info; } }; // ===== 3. 剪藏面板模块 ===== const ClipperPanel = { isVisible: false, panel: null, show: function() { if (this.isVisible) return; this.isVisible = true; // 移除已存在的面板 const existingPanel = document.getElementById('clipper-panel'); if (existingPanel) { existingPanel.remove(); } // 添加样式 if (!document.querySelector('#clipper-styles')) { GM_addStyle(` #clipper-panel { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.3); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); z-index: 2147483647; display: flex; justify-content: center; align-items: center; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } .clipper-content { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border-radius: 20px; width: 90%; max-width: 500px; max-height: 80vh; overflow-y: auto; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); position: relative; } .clipper-header { padding: 24px; border-bottom: 1px solid rgba(0, 0, 0, 0.1); display: flex; justify-content: space-between; align-items: center; } .clipper-title { font-size: 18px; font-weight: 600; color: #1a1a1a; } .clipper-close { width: 32px; height: 32px; border-radius: 50%; border: none; background: rgba(0, 0, 0, 0.1); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.3s; font-size: 18px; color: #4a4a4a; } .clipper-close:hover { background: rgba(0, 0, 0, 0.15); transform: rotate(90deg); } .clipper-body { padding: 24px; } .info-item { margin-bottom: 20px; } .info-label { font-size: 12px; color: #6a6a6a; margin-bottom: 8px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; } .info-input, .info-select { width: 100%; padding: 12px; border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 12px; font-size: 14px; transition: all 0.3s; background: rgba(255, 255, 255, 0.8); box-sizing: border-box; } .info-select { -webkit-appearance: none; -moz-appearance: none; appearance: none; background-image: url("image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='rgba(0,0,0,0.5)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6,9 12,15 18,9'%3e%3c/polyline%3e%3c/svg%3e"); background-repeat: no-repeat; background-position: right 12px center; background-size: 1em; padding-right: 36px; } .info-input:focus, .info-select:focus { outline: none; border-color: rgba(76, 175, 80, 0.5); box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1); background: rgba(255, 255, 255, 0.95); } .info-input:hover, .info-select:hover { border-color: rgba(0, 0, 0, 0.2); } .tags-hint { font-size: 12px; color: #6a6a6a; margin-top: 8px; display: flex; align-items: center; gap: 4px; } .clipper-footer { padding: 24px; border-top: 1px solid rgba(0, 0, 0, 0.1); display: flex; gap: 12px; justify-content: flex-end; } .clipper-btn { padding: 12px 24px; border: none; border-radius: 12px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.3s; min-width: 100px; } .btn-clip { background: rgba(76, 175, 80, 0.9); color: white; } .btn-clip:hover:not(:disabled) { background: rgba(76, 175, 80, 1); transform: translateY(-1px); } .btn-clip:disabled { background: rgba(0, 0, 0, 0.2); cursor: not-allowed; } .btn-cancel { background: rgba(0, 0, 0, 0.1); color: #4a4a4a; } .btn-cancel:hover { background: rgba(0, 0, 0, 0.15); } .loading-spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid #ffffff; border-radius: 50%; border-top-color: transparent; animation: spin 0.8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } .notebook-container { display: flex; align-items: center; gap: 10px; } .notebook-select { flex: 1; } .notebook-refresh { width: 32px; height: 32px; border-radius: 8px; border: 1px solid rgba(0,0,0,0.1); background: white; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s; } .notebook-refresh:hover { background: rgba(0,0,0,0.05); } `); } // 创建面板 const panel = document.createElement('div'); panel.id = 'clipper-panel'; const content = document.createElement('div'); content.className = 'clipper-content'; // 创建头部 const header = document.createElement('div'); header.className = 'clipper-header'; const title = document.createElement('div'); title.className = 'clipper-title'; title.textContent = '⚡️ 网页剪藏'; const closeButton = document.createElement('button'); closeButton.className = 'clipper-close'; closeButton.textContent = '×'; closeButton.addEventListener('click', () => { this.hide(); }); header.appendChild(title); header.appendChild(closeButton); // 创建主体 const body = document.createElement('div'); body.className = 'clipper-body'; // 笔记本选择 const notebookContainer = document.createElement('div'); notebookContainer.className = 'info-item'; const notebookLabel = document.createElement('div'); notebookLabel.className = 'info-label'; notebookLabel.textContent = '笔记本'; const notebookSelectWrapper = document.createElement('div'); notebookSelectWrapper.className = 'notebook-container'; const notebookSelect = document.createElement('select'); notebookSelect.className = 'info-select notebook-select'; notebookSelect.id = 'notebook-select'; notebookSelect.innerHTML = ''; notebookSelect.disabled = true; const refreshButton = document.createElement('button'); refreshButton.className = 'notebook-refresh'; refreshButton.innerHTML = '🔄'; refreshButton.title = '刷新笔记本列表'; refreshButton.addEventListener('click', () => { WebClipper.fetchNotebooks(); }); notebookSelectWrapper.appendChild(notebookSelect); notebookSelectWrapper.appendChild(refreshButton); notebookContainer.appendChild(notebookLabel); notebookContainer.appendChild(notebookSelectWrapper); // 标题输入 const titleContainer = document.createElement('div'); titleContainer.className = 'info-item'; const titleLabel = document.createElement('div'); titleLabel.className = 'info-label'; titleLabel.textContent = '标题'; const titleInput = document.createElement('input'); titleInput.type = 'text'; titleInput.className = 'info-input'; titleInput.id = 'clip-title-input'; titleInput.value = document.title; titleInput.placeholder = '输入文章标题'; titleContainer.appendChild(titleLabel); titleContainer.appendChild(titleInput); // URL输入 const urlContainer = document.createElement('div'); urlContainer.className = 'info-item'; const urlLabel = document.createElement('div'); urlLabel.className = 'info-label'; urlLabel.textContent = '链接'; const urlInput = document.createElement('input'); urlInput.type = 'url'; urlInput.className = 'info-input'; urlInput.id = 'clip-url-input'; urlInput.value = window.location.href; urlInput.placeholder = '输入文章链接'; urlContainer.appendChild(urlLabel); urlContainer.appendChild(urlInput); // 标签输入 const tagsContainer = document.createElement('div'); tagsContainer.className = 'info-item'; const tagsLabel = document.createElement('div'); tagsLabel.className = 'info-label'; tagsLabel.textContent = '自定义标签(可选)'; const tagsInput = document.createElement('input'); tagsInput.type = 'text'; tagsInput.className = 'info-input'; tagsInput.id = 'custom-tags-input'; tagsInput.placeholder = '输入标签,用逗号或空格分隔'; const tagsHint = document.createElement('div'); tagsHint.className = 'tags-hint'; tagsHint.textContent = '💡 AI将根据您的标签生成补充标签,避免重复'; tagsContainer.appendChild(tagsLabel); tagsContainer.appendChild(tagsInput); tagsContainer.appendChild(tagsHint); // 添加所有组件到body body.appendChild(notebookContainer); body.appendChild(titleContainer); body.appendChild(urlContainer); body.appendChild(tagsContainer); // 创建底部 const footer = document.createElement('div'); footer.className = 'clipper-footer'; const cancelButton = document.createElement('button'); cancelButton.className = 'clipper-btn btn-cancel'; cancelButton.textContent = '取消'; cancelButton.addEventListener('click', () => { this.hide(); }); const clipButton = document.createElement('button'); clipButton.className = 'clipper-btn btn-clip'; const buttonText = document.createElement('span'); buttonText.id = 'clip-button-text'; buttonText.textContent = '开始剪藏'; clipButton.appendChild(buttonText); clipButton.addEventListener('click', () => { WebClipper.doClip(); }); footer.appendChild(cancelButton); footer.appendChild(clipButton); // 组装面板 content.appendChild(header); content.appendChild(body); content.appendChild(footer); panel.appendChild(content); // 添加到body document.body.appendChild(panel); this.panel = panel; // 绑定背景点击事件 panel.addEventListener('click', (e) => { if (e.target === panel) { this.hide(); } }); // 自动聚焦标题输入框 setTimeout(() => { titleInput.focus(); titleInput.select(); }, 300); // 显示后加载笔记本列表 setTimeout(() => { WebClipper.fetchNotebooks(); }, 300); }, hide: function() { if (this.panel) { this.panel.remove(); this.panel = null; } this.isVisible = false; }, setLoading: function(loading) { const button = document.querySelector('.btn-clip'); const buttonText = document.getElementById('clip-button-text'); if (button && buttonText) { if (loading) { button.disabled = true; buttonText.textContent = ''; const spinner = document.createElement('span'); spinner.className = 'loading-spinner'; buttonText.parentNode.insertBefore(spinner, buttonText); buttonText.textContent = ' 剪藏中...'; } else { button.disabled = false; const spinner = button.querySelector('.loading-spinner'); if (spinner) spinner.remove(); buttonText.textContent = '开始剪藏'; } } } }; // ===== 4. 核心剪藏模块 ===== const WebClipper = { config: {}, button: null, dragThrottle: null, notebooks: [], init: function() { this.config = Config.getAll(); if (!Config.isComplete()) { console.warn('脚本配置不完整,请在菜单中打开设置进行配置。'); // 自动打开设置 setTimeout(() => Config.showSettings(), 1000); return; } console.log(`Web Clipper: 启动`); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.createFloatingButton()); } else { setTimeout(() => this.createFloatingButton(), 500); } }, fetchWithGM: function(details) { return new Promise((resolve, reject) => { details.onload = (res) => { try { const response = { status: res.status, statusText: res.statusText, responseText: res.responseText }; const contentType = res.responseHeaders.match(/content-type:\s*([^;]+)/i); if (contentType && contentType[1].includes('application/json')) { response.data = JSON.parse(res.responseText); } if (res.status >= 200 && res.status < 300) { resolve(response); } else { reject(new Error(`请求失败: ${res.status} ${res.statusText}`)); } } catch (error) { reject(error); } }; details.onerror = (err) => reject(err); GM_xmlhttpRequest(details); }); }, fetchNotebooks: async function() { const selectElement = document.getElementById('notebook-select'); if (!selectElement) return; selectElement.disabled = true; selectElement.innerHTML = ''; try { const response = await this.fetchWithGM({ method: 'POST', url: `${this.config.siyuanApiUrl}/api/notebook/lsNotebooks`, headers: { 'Authorization': `Token ${this.config.siyuanApiToken}`, 'Content-Type': 'application/json' }, JSON.stringify({}) }); if (response.data.code === 0) { this.notebooks = response.data.data.notebooks; selectElement.innerHTML = ''; this.notebooks.forEach(nb => { const option = document.createElement('option'); option.value = nb.id; option.textContent = nb.name; selectElement.appendChild(option); }); // 设置上次选择的笔记本 if (this.config.lastSelectedNotebookId) { selectElement.value = this.config.lastSelectedNotebookId; } else if (this.config.defaultNotebookId) { selectElement.value = this.config.defaultNotebookId; } } else { throw new Error(response.data.msg || '获取笔记本列表失败'); } } catch (error) { console.error('加载笔记本列表失败:', error); selectElement.innerHTML = ''; Toast.show('加载笔记本列表失败', 'error'); } finally { selectElement.disabled = false; } }, // AI生成标签的函数(支持自定义标签参考) generateTagsWithLLM: async function(title, content, customTags = []) { const customTagsText = customTags.length > 0 ? `用户已提供的标签(请避免生成相似或重复的标签):${customTags.join('、')}` : ''; const prompt = `请根据以下文章标题和内容,生成3到5个最能概括文章核心主题的标签。${customTagsText} 要求: 1. 标签需要简洁、精准,使用中文,并且是两字名词 2. 如果用户已提供标签,请生成补充性的标签,避免重复 3. 请直接返回标签,用逗号分隔,不要有任何其他解释或格式 标题: ${title} 内容: ${content.substring(0, 2000)}...`; try { const response = await this.fetchWithGM({ method: 'POST', url: this.config.aiApiUrl, headers: { 'Authorization': `Bearer ${this.config.aiApiKey}`, 'Content-Type': 'application/json' }, data: JSON.stringify({ model: this.config.aiModel, messages: [ { role: "user", content: prompt } ], temperature: 0.5, max_tokens: 100, stream: false, thinking: { type: "disabled" } }) }); if (response.data.choices && response.data.choices[0] && response.data.choices[0].message) { const tags = response.data.choices[0].message.content.trim(); console.log(`AI生成的标签: ${tags}`); return tags; } else { console.error('AI响应格式不正确:', response.data); return ''; } } catch (error) { console.error('调用AI API失败:', error); return ''; } }, // 合并并去重标签 mergeTags: function(customTags, aiTagsString) { const aiTags = aiTagsString ? aiTagsString.split(/[,,]/).map(tag => tag.trim()).filter(tag => tag) : []; const allTags = [...new Set([...customTags, ...aiTags])]; return allTags.join(','); }, // 保存到思源笔记的函数 (支持按月分类和指定笔记本) saveToSiyuan: async function(title, markdown, tags, url, notebookId) { // 按月创建文件夹 const currentMonth = new Date().toISOString().slice(0, 7); // "YYYY-MM" const sanitizedTitle = title.replace(/[\/\\:*?"<>|]/g, '_'); const docPath = `/web-clips/${currentMonth}/${sanitizedTitle}`; const payload = { notebook: notebookId, path: docPath, markdown: markdown, tags: tags }; try { const response = await this.fetchWithGM({ method: 'POST', url: `${this.config.siyuanApiUrl}/api/filetree/createDocWithMd`, headers: { 'Authorization': `Token ${this.config.siyuanApiToken}`, 'Content-Type': 'application/json' }, JSON.stringify(payload) }); console.log('成功保存到思源笔记:', response.data); return response.data; } catch (error) { console.error('保存到思源笔记失败:', error); throw error; } }, doClip: async function() { const titleInput = document.getElementById('clip-title-input'); const urlInput = document.getElementById('clip-url-input'); const tagsInput = document.getElementById('custom-tags-input'); const notebookSelect = document.getElementById('notebook-select'); const title = titleInput ? titleInput.value.trim() : document.title; const url = urlInput ? urlInput.value.trim() : window.location.href; const customTags = tagsInput ? tagsInput.value.split(/[,,\s]+/).map(tag => tag.trim()).filter(tag => tag) : []; const notebookId = notebookSelect ? notebookSelect.value : null; // 验证必填字段 if (!title) { Toast.show('请输入标题', 'error'); return; } if (!url) { Toast.show('请输入链接', 'error'); return; } if (!notebookId) { Toast.show('请选择一个笔记本', 'error'); return; } ClipperPanel.setLoading(true); try { // 使用Readability提取主要内容 const articleContent = document.documentElement.innerHTML; const doc = document.implementation.createHTMLDocument('temp'); doc.documentElement.innerHTML = articleContent; const reader = new Readability(doc); const article = reader.parse(); if (!article || !article.content) { throw new Error('无法提取文章内容'); } // 使用Turndown将HTML转换为Markdown const turndownService = new TurndownService({ headingStyle: 'atx', bulletListMarker: '-', codeBlockStyle: 'fenced' }); const markdown = turndownService.turndown(article.content); const markdownWithHeader = `# ${article.title} > [原文链接](${url}) --- ${markdown}`; // 调用AI生成标签(参考自定义标签) const aiTags = await this.generateTagsWithLLM(article.title, article.textContent, customTags); // 合并并去重标签 const allTags = this.mergeTags(customTags, aiTags); // 保存到思源笔记 await this.saveToSiyuan(article.title, markdownWithHeader, allTags, url, notebookId); // 保存用户选择的笔记本ID const currentConfig = Config.getAll(); currentConfig.lastSelectedNotebookId = notebookId; Config.setAll(currentConfig); ClipperPanel.hide(); Toast.show(`剪藏成功!生成标签:${allTags}`, 'success'); if (this.button) { const originalText = this.button.textContent; this.button.textContent = '✅'; setTimeout(() => { this.button.textContent = originalText; }, 2000); } } catch (error) { console.error('剪藏失败:', error); Toast.show(`剪藏失败: ${error.message}`, 'error'); } finally { ClipperPanel.setLoading(false); } }, makeDraggable: function(element) { let isDragging = false; let startX, startY, initialTop, initialRight; function handleStart(e) { isDragging = true; startX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX; startY = e.type.includes('mouse') ? e.clientY : e.touches[0].clientY; const rect = element.getBoundingClientRect(); initialTop = rect.top; initialRight = window.innerWidth - rect.right; element.style.cursor = 'grabbing'; element.style.transition = 'none'; } function handleMove(e) { if (!isDragging) return; // 使用节流优化性能 if (WebClipper.dragThrottle) return; WebClipper.dragThrottle = requestAnimationFrame(() => { WebClipper.dragThrottle = null; e.preventDefault(); const currentX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX; const currentY = e.type.includes('mouse') ? e.clientY : e.touches[0].clientY; const deltaY = currentY - startY; const deltaX = currentX - startX; const newTop = initialTop + deltaY; const newRight = initialRight - deltaX; element.style.top = `${newTop}px`; element.style.right = `${newRight}px`; element.style.left = 'auto'; element.style.bottom = 'auto'; }); } function handleEnd() { if (!isDragging) return; isDragging = false; element.style.cursor = 'move'; element.style.transition = ''; if (WebClipper.dragThrottle) { cancelAnimationFrame(WebClipper.dragThrottle); WebClipper.dragThrottle = null; } const style = window.getComputedStyle(element); const newPosition = { top: style.top, right: style.right }; WebClipper.config.buttonPosition = newPosition; Config.setAll(WebClipper.config); } element.addEventListener('mousedown', handleStart); document.addEventListener('mousemove', handleMove); document.addEventListener('mouseup', handleEnd); element.addEventListener('touchstart', handleStart, { passive: false }); document.addEventListener('touchmove', handleMove, { passive: false }); element.addEventListener('touchend', handleEnd); }, createFloatingButton: function() { if (document.getElementById('web-clipper-btn')) return; // 添加样式 GM_addStyle(` #web-clipper-btn { position: fixed; z-index: 2147483646; cursor: move; user-select: none; font-size: 24px; top: ${this.config.buttonPosition.top}; right: ${this.config.buttonPosition.right}; width: 48px; height: 48px; border-radius: 50%; background: linear-gradient(135deg, rgba(76, 175, 80, 0.9), rgba(69, 160, 73, 0.9)); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); color: white; display: flex; align-items: center; justify-content: center; box-shadow: 0 8px 32px -8px rgba(76, 175, 80, 0.4); transition: all 0.2s ease; border: 1px solid rgba(255, 255, 255, 0.2); } #web-clipper-btn:hover { transform: scale(1.05); box-shadow: 0 12px 40px -8px rgba(76, 175, 80, 0.5); background: linear-gradient(135deg, rgba(76, 175, 80, 1), rgba(69, 160, 73, 1)); } #web-clipper-btn:active { transform: scale(0.95); } @media (max-width: 768px) { #web-clipper-btn { width: 44px; height: 44px; font-size: 20px; } } `); this.button = document.createElement('div'); this.button.id = 'web-clipper-btn'; this.button.title = '点击剪藏,双击打开设置'; this.button.textContent = '⚡️'; document.body.appendChild(this.button); this.makeDraggable(this.button); this.button.addEventListener('click', (e) => { if (e.detail === 1) { setTimeout(() => ClipperPanel.show(), 200); } }); this.button.addEventListener('dblclick', (e) => { e.stopPropagation(); Config.showSettings(); }); console.log('剪藏按钮已创建'); } }; // ===== 5. 初始化和菜单注册 ===== GM_registerMenuCommand('⚙️ 剪藏工具设置', () => { Config.showSettings(); }); console.log('Web Clipper Pro (整合版) 脚本已加载'); WebClipper.init(); })();