// ==UserScript== // @name Miniflux 文章剪藏到思源笔记 // @namespace http://tampermonkey.net/ // @version 2.0.1 // @description 在Miniflux页面上添加按钮,将文章一键剪藏到思源笔记。请在设置中配置你的思源信息。 // @author You // @match https://rss.by00s.top/* // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_addStyle // @require https://cdn.jsdelivr.net/npm/turndown@7.1.2/dist/turndown.js // ==/UserScript== (function() { 'use strict'; // ===== 1. 配置管理模块 ===== const Config = { keys: { apiUrl: 'siyuan_api_url', token: 'siyuan_token', notebookId: 'siyuan_notebook_id' }, getAll: function() { return { apiUrl: GM_getValue(this.keys.apiUrl, ''), token: GM_getValue(this.keys.token, ''), notebookId: GM_getValue(this.keys.notebookId, '') }; }, setAll: function(newConfig) { GM_setValue(this.keys.apiUrl, newConfig.apiUrl); GM_setValue(this.keys.token, newConfig.token); GM_setValue(this.keys.notebookId, newConfig.notebookId); }, isComplete: function() { const config = this.getAll(); return config.apiUrl && config.token && config.notebookId; }, // ===== 修复后的 showSettings 函数 ===== showSettings: function() { const config = this.getAll(); if (document.getElementById('siyuan-settings-modal')) return; GM_addStyle(` #siyuan-settings-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); z-index: 100000; display: flex; justify-content: center; align-items: center; font-family: sans-serif; } #siyuan-settings-content { background: #fff; padding: 20px 30px; border-radius: 8px; width: 90%; max-width: 450px; box-shadow: 0 4px 15px rgba(0,0,0,0.2); } #siyuan-settings-content h2 { margin-top: 0; color: #333; } #siyuan-settings-content label { display: block; margin-top: 15px; margin-bottom: 5px; font-weight: bold; color: #555; } #siyuan-settings-content input { width: 100%; padding: 8px; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px; } #siyuan-settings-buttons { margin-top: 20px; text-align: right; } #siyuan-settings-buttons button { padding: 8px 15px; border: none; border-radius: 4px; cursor: pointer; margin-left: 10px; } #siyuan-save-btn { background-color: #007AFF; color: white; } #siyuan-cancel-btn { background-color: #f0f0f0; color: #333; } `); // 使用原生 DOM API 创建元素,避免 innerHTML 的 Trusted Types 问题 const modal = document.createElement('div'); modal.id = 'siyuan-settings-modal'; const content = document.createElement('div'); content.id = 'siyuan-settings-content'; const title = document.createElement('h2'); title.textContent = '思源笔记配置'; content.appendChild(title); // API 地址输入框 const apiUrlLabel = document.createElement('label'); apiUrlLabel.textContent = '思源 API 地址 (如: https://siyuan.example.com)'; apiUrlLabel.setAttribute('for', 'apiUrl'); content.appendChild(apiUrlLabel); const apiUrlInput = document.createElement('input'); apiUrlInput.type = 'url'; apiUrlInput.id = 'apiUrl'; apiUrlInput.placeholder = '请输入你的思源服务器地址'; apiUrlInput.value = config.apiUrl; content.appendChild(apiUrlInput); // Token 输入框 const tokenLabel = document.createElement('label'); tokenLabel.textContent = 'API Token'; tokenLabel.setAttribute('for', 'token'); content.appendChild(tokenLabel); const tokenInput = document.createElement('input'); tokenInput.type = 'text'; tokenInput.id = 'token'; tokenInput.placeholder = '在思源设置 -> 关于 -> API Token 中生成'; tokenInput.value = config.token; content.appendChild(tokenInput); // 笔记本 ID 输入框 const notebookIdLabel = document.createElement('label'); notebookIdLabel.textContent = '笔记本 ID (如: 20211231123456-abcdefg)'; notebookIdLabel.setAttribute('for', 'notebookId'); content.appendChild(notebookIdLabel); const notebookIdInput = document.createElement('input'); notebookIdInput.type = 'text'; notebookIdInput.id = 'notebookId'; notebookIdInput.placeholder = '在思源笔记设置 -> 关于 -> 笔记本列表中查找'; notebookIdInput.value = config.notebookId; content.appendChild(notebookIdInput); const buttons = document.createElement('div'); buttons.id = 'siyuan-settings-buttons'; const cancelButton = document.createElement('button'); cancelButton.id = 'siyuan-cancel-btn'; cancelButton.textContent = '取消'; cancelButton.addEventListener('click', () => modal.remove()); buttons.appendChild(cancelButton); const saveButton = document.createElement('button'); saveButton.id = 'siyuan-save-btn'; saveButton.textContent = '保存'; saveButton.addEventListener('click', () => { const newConfig = { apiUrl: apiUrlInput.value.trim(), token: tokenInput.value.trim(), notebookId: notebookIdInput.value.trim() }; this.setAll(newConfig); modal.remove(); alert('配置已保存!'); window.location.reload(); }); buttons.appendChild(saveButton); content.appendChild(buttons); modal.appendChild(content); document.body.appendChild(modal); modal.addEventListener('click', (e) => { if (e.target === modal) { modal.remove(); } }); } }; // ===== 2. 核心上传模块 (保持不变) ===== const SiYuanUploader = { config: {}, init: function() { this.config = Config.getAll(); if (!Config.isComplete()) { console.warn('思源配置不完整,请先进行配置。'); Config.showSettings(); return; } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.createUploadButton()); } else { setTimeout(() => this.createUploadButton(), 500); } }, fetchWithGM: function(details) { return new Promise((resolve, reject) => { details.onload = (res) => { if (res.status >= 200 && res.status < 300) { resolve(res); } else { reject(new Error(`请求失败: ${res.status} ${res.statusText}`)); } }; details.onerror = (err) => reject(err); GM_xmlhttpRequest(details); }); }, generateSafePath: function(title) { const safeTitle = title.toLowerCase().replace(/[^\w\u4e00-\u9fa5\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-').trim(); const timestamp = new Date().toISOString().slice(0, 10); return `/web-clips/${timestamp}/${safeTitle}`; }, getArticleTitle: function() { const selectors = ['h1#page-header-title', 'h1.page-header-title', '.entry-title h1', 'article h1', 'main h1', 'h1', 'title']; for (const selector of selectors) { const element = document.querySelector(selector); if (element && element.textContent.trim()) { return element.textContent.trim(); } } return '无标题文章'; }, getArticleContent: function() { const selectors = ['main article.entry-content', '.entry-content', 'article.content', '.content', 'main']; for (const selector of selectors) { const element = document.querySelector(selector); if (element) { return element.cloneNode(true); } } return null; }, convertToMarkdown: function(element) { if (!element) return ''; const turndownService = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced' }); return turndownService.turndown(element); }, createSiYuanDocument: async function(title, markdown) { const path = this.generateSafePath(title); const docData = { notebook: this.config.notebookId, path: path, markdown: markdown }; const response = await this.fetchWithGM({ method: 'POST', url: `${this.config.apiUrl}/api/filetree/createDocWithMd`, headers: { 'Content-Type': 'application/json', 'Authorization': `Token ${this.config.token}` }, data: JSON.stringify(docData) }); const result = JSON.parse(response.responseText); if (result.code !== 0) { throw new Error(`思源API错误: ${result.msg}`); } return result.data; }, uploadToSiYuan: async function() { const button = document.getElementById('siyuan-upload-btn'); if (!button) return; const originalText = button.textContent; button.disabled = true; button.textContent = '上传中...'; try { const title = this.getArticleTitle(); const content = this.getArticleContent(); if (!content) { throw new Error('无法获取文章内容'); } const markdown = this.convertToMarkdown(content); if (!markdown.trim()) { throw new Error('转换后的内容为空'); } await this.createSiYuanDocument(title, markdown); button.textContent = '成功'; setTimeout(() => { button.textContent = originalText; button.disabled = false; }, 2000); } catch (error) { console.error('上传失败:', error); button.textContent = '失败'; alert(`上传失败: ${error.message}`); setTimeout(() => { button.textContent = originalText; button.disabled = false; }, 3000); } }, createUploadButton: function() { if (document.getElementById('siyuan-upload-btn')) return; const actionsList = document.querySelector('.entry-actions ul'); if (!actionsList) { setTimeout(() => this.createUploadButton(), 1000); return; } const li = document.createElement('li'); const button = document.createElement('button'); button.id = 'siyuan-upload-btn'; button.className = 'page-button'; button.textContent = '上传思源'; button.title = '上传到思源笔记'; button.style.cssText = `width: auto; height: 32px; padding: 4px 8px; display: flex; justify-content: center; align-items: center; border-radius: 8px; background-color: #007AFF; color: white; border: none; cursor: pointer; transition: background-color 150ms ease; font-size: 12px; margin: 0 2px;`; button.addEventListener('mouseenter', () => { button.style.backgroundColor = '#0056CC'; }); button.addEventListener('mouseleave', () => { button.style.backgroundColor = '#007AFF'; }); button.addEventListener('click', () => this.uploadToSiYuan()); li.appendChild(button); actionsList.appendChild(li); } }; // ===== 3. 初始化和菜单注册 (保持不变) ===== GM_registerMenuCommand('⚙️ 思源笔记设置', () => { Config.showSettings(); }); console.log('Miniflux 文章剪藏到思源笔记脚本已加载'); SiYuanUploader.init(); })();