您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
为 V2EX 帖子生成总结
// ==UserScript== // @name V2EX 文章总结助手 // @name:zh-CN V2EX 文章总结助手 // @namespace https://github.com/jandaes/v2ex_ai // @version 2.0.1 // @description 为 V2EX 帖子生成总结 // @description:zh-CN 为 V2EX 帖子生成总结 // @author Jandaes // @homepage https://gf.qytechs.cn/zh-CN/scripts/521732-v2ex-%E6%96%87%E7%AB%A0%E6%80%BB%E7%BB%93%E5%8A%A9%E6%89%8B // @supportURL https://github.com/Jandaes/v2ex_ai // @match *.v2ex.com/* // @connect * // @grant GM_xmlhttpRequest // @icon https://www.v2ex.com/favicon.ico // @license MIT // @copyright 2024, Jandaes (https://github.com/Jandaes) // ==/UserScript== (function(){ 'use strict'; const d=document,ls=localStorage,w=window; const $=(s,p=d)=>p.querySelector(s); const t={dark:{bg:'#2d2d2d',t:'#e0e0e0',i:'#3d3d3d',b:'#4d4d4d'},light:{bg:'#fff',t:'#333',i:'#f5f5f5',b:'#ddd'}}; const STORAGE_KEY = 'v2ex_summary_settings'; const DEFAULT_SETTINGS = { apiUrl: '', apiKey: '', modelName: '', prompt: '只精简总结文章内容和评论的核心要点、不需要加入你的任何观点。分别输出文章内容和用户评论', theme: 'system' // 默认跟随系统 }; const store = { get: () => { try { return { ...DEFAULT_SETTINGS, ...JSON.parse(ls.getItem(STORAGE_KEY) || '{}') }; } catch (e) { return { ...DEFAULT_SETTINGS }; } }, set: (settings) => { ls.setItem(STORAGE_KEY, JSON.stringify({ ...store.get(), ...settings })); } }; function modal(){ // 获取当前主题 const settings = store.get(); const isDark = settings.theme === 'dark' || (settings.theme === 'system' && w.matchMedia('(prefers-color-scheme:dark)').matches); const th = t[isDark ? 'dark' : 'light']; const m = createElement('div', { style: `position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.6);display:flex;justify-content:center;align-items:center;z-index:1000` }); const c = createElement('div', { style: ` position:relative; background:${th.bg}; padding:25px; border-radius:12px; width:450px; max-width:90%; color:${th.t}; padding-bottom:20px; border:1px solid ${th.b} ` }); m.appendChild(c); c.innerHTML = ` <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;border-bottom:1px solid ${th.b};padding-bottom:10px"> <h3 style="margin:0;font-size:18px;color:${th.t}">V2EX 文章总结助手设置</h3> <div style="display:flex;align-items:center;gap:8px"> <span style="font-size:14px">主题</span> <select id="theme" style="padding:4px 8px;background:${th.i};color:${th.t};border:1px solid ${th.b};border-radius:4px"> <option value="system">跟随系统</option> <option value="light">浅色</option> <option value="dark">深色</option> </select> </div> </div> <div class="form"> <div class="group"><label>API URL:</label><input id="url" placeholder="输入API地址"></div> <div class="group"><label>API Key:</label><div class="pwd"><input type="password" id="key" placeholder="输入API Key"><span class="eye">🔒</span></div></div> <div class="group"><label>模型名称:</label><input id="model" placeholder="输入模型名称"></div> <div class="group"><label>系统提示词:</label><textarea id="prompt" placeholder="请输入"></textarea></div> </div> <div style="display:flex;justify-content:space-between;align-items:center;margin-top:25px"> <a href="https://github.com/Jandaes/v2ex_ai" target="_blank" class="github"> <svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg> GitHub </a> <div style="display:flex;gap:10px"> <button id="cancel">取消</button> <button id="save" class="primary">保存</button> </div> </div> `; addStyle(c, ` .form{display:flex;flex-direction:column;gap:15px} .group{display:flex;align-items:center} .group label{width:85px;text-align:right;margin-right:15px;color:${th.t}} .group input,.group textarea{ flex:1; padding:8px 12px; border:1px solid ${th.b}; border-radius:6px; background:${th.i}; color:${th.t} } .group textarea{height:100px;resize:vertical} .pwd{position:relative;flex:1;display:flex} .eye{position:absolute;right:12px;top:50%;transform:translateY(-50%);cursor:pointer;user-select:none;opacity:.7} button{ padding:8px 16px; border:none; border-radius:6px; background:${th.i}; color:${th.t}; cursor:pointer } .primary{background:#0066cc;color:#fff} .github{color:${th.t};text-decoration:none;opacity:.8;display:flex;align-items:center;gap:6px;font-size:14px} `); // 加载设置 $('#url', c).value = settings.apiUrl; $('#key', c).value = settings.apiKey; $('#model', c).value = settings.modelName; $('#prompt', c).value = settings.prompt; $('#theme', c).value = settings.theme; // 添加主题切换事件 $('#theme', c).onchange = function() { const newTheme = this.value; const isDark = newTheme === 'dark' || (newTheme === 'system' && w.matchMedia('(prefers-color-scheme:dark)').matches); const th = t[isDark ? 'dark' : 'light']; // 更新所有颜色 c.style.background = th.bg; c.style.color = th.t; c.style.borderColor = th.b; // 更新所有输入框和按钮 c.querySelectorAll('input, textarea, select').forEach(el => { el.style.background = th.i; el.style.color = th.t; el.style.borderColor = th.b; }); // 更新标签颜色 c.querySelectorAll('label, h3, .github').forEach(el => { el.style.color = th.t; }); // 更新普通按钮 c.querySelectorAll('button:not(.primary)').forEach(el => { el.style.background = th.i; el.style.color = th.t; }); }; // 将 modal 添加到 body d.body.appendChild(m); // 绑定事件 $('.eye', c).onclick = e => { const i = $('#key', c); i.type = i.type === 'password' ? 'text' : 'password'; e.target.textContent = i.type === 'password' ? '🔒' : '🔓'; }; $('#save', c).onclick = () => { store.set({ apiUrl: $('#url', c).value, apiKey: $('#key', c).value, modelName: $('#model', c).value, prompt: $('#prompt', c).value }); m.remove(); }; $('#cancel', c).onclick = () => m.remove(); m.onclick = e => { if(e.target === m) m.remove(); }; } function summary(){ // 检查是否是文章页面(URL 包含 /t/数字) if (!w.location.pathname.match(/^\/t\/\d+/)) return; // 获取 gray 元素 const gray = $('#Main .box .header .gray'); if (!gray) { // 如果没找到元素,等待后重试 setTimeout(summary, 500); // 增加延迟时间 return; } // 避免重复添加 if (gray.querySelector('.summary-tools')) return; // 创建一个容器来包裹总结和设置按钮 const toolsContainer = createElement('span', { className: 'summary-tools', style: 'display: inline-block; margin-left: 5px' // 修改样式确保显示 }); // 创建总结按钮 const sum = createElement('a', { href: 'javascript:void(0)', className: 'tb summary-button', innerHTML: '总结 <span style="font-size:14px">✨</span>', style: 'margin-left: 5px' // 添加间距 }); // 创建设置按钮 const set = createElement('a', { href: 'javascript:void(0)', className: 'tb settings-button', innerHTML: '设置 <span style="font-size:14px">⚙️</span>', style: 'margin-left: 5px' // 添加间距 }); // 绑定点击事件 sum.onclick = async () => { // 获取文章内容,如果没有内容则使用空字符串 const content = getContent() || ''; const container = getContainer(); if(!container) return; const cont = $('.summary-content',container); // 如果已经有内容且不是错误消息,直接显示 if(container.style.display==='none' && cont.innerHTML && !cont.innerHTML.includes('失败')) { container.style.display='block'; return; } // 显示加载状态 cont.textContent='正在获取评论...'; container.style.display='block'; // 获取所有评论 const comments = await getAllComments(); // 组合文章内容和评论 const fullContent = ` 文章内容: ${content} 评论内容: ${comments.map(c => c.trim()).join(' ')}`; // 更新状态 cont.textContent='正在生成总结...'; // 发送到 LLM const sum = await request(fullContent); if(sum){ cont.innerHTML = sum; }else{ cont.textContent='生成总结失败,请检查设置和网络连接'; } }; set.onclick = modal; // 将按钮添加到容器中 toolsContainer.appendChild(document.createTextNode(' • ')); toolsContainer.appendChild(sum); toolsContainer.appendChild(document.createTextNode(' • ')); toolsContainer.appendChild(set); // 将容器添加到 gray 元素中 gray.appendChild(toolsContainer); } async function getAllComments() { let allComments = []; // 获取分页信息 const pagination = $('.cell.ps_container'); let pageInfo = { currentPage: 1, totalPages: 1 }; if(pagination) { const current = pagination.querySelector('div.page_current'); if(current) { pageInfo.currentPage = parseInt(current.textContent); } const pages = [...pagination.querySelectorAll('a.page_normal')]; if(pages.length > 0) { const lastPage = parseInt(pages[pages.length - 1].textContent); pageInfo.totalPages = Math.max(lastPage, pageInfo.currentPage); } } // 获取所有页面的评论 const topicId = w.location.pathname.match(/\/t\/(\d+)/)?.[1]; if(topicId) { for(let page = 1; page <= pageInfo.totalPages; page++) { try { if(page === pageInfo.currentPage) { // 如果是当前页,直接获取DOM中的评论 allComments = allComments.concat(getPageComments(d)); } else { // 获取其他页面的评论 const response = await fetch(`https://www.v2ex.com/t/${topicId}?p=${page}`); const text = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(text, 'text/html'); const pageComments = getPageComments(doc); allComments = allComments.concat(pageComments); } if(page < pageInfo.totalPages) { await new Promise(resolve => setTimeout(resolve, 500)); } } catch(e) { console.error(`获取第 ${page} 页评论失败:`, e); } } } return allComments; } function getPageComments(doc) { return [...doc.querySelectorAll('div[id^="r_"].cell')] .map(comment => comment.querySelector('.reply_content')?.textContent .replace(/\s+/g, ' ') // 将多个空白字符替换为单个空格 .trim()) .filter(Boolean); // 过滤掉空评论 } function getContainer(){ // 检查是否存在容器 const existingContainer = $('.summary-container'); if (existingContainer) return existingContainer; // 取 #Main .box 元素 const mainBox = $('#Main .box'); if (!mainBox) return null; // 创建总结容器,添加圆角边框样式 const c = createElement('div', { className: 'summary-container cell', style: `padding:15px;font-size:14px;line-height:1.6;display:none;border-radius:6px;border:1px solid var(--box-border-color,#eee)` }); const tb = createElement('div', { style: 'display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--box-border-color,#eee)' }); const tl = createElement('div', {style: 'display:flex;align-items:center;gap:10px'}); const title = createElement('div', {innerHTML: '📝 文章总结', style: 'font-weight:500'}); const regen = createElement('a', { href: 'javascript:void(0)', className: 'tb', innerHTML: '🔄 重新生成', style: 'font-size:12px' }); regen.onclick = async () => { const content = getContent(); if (!content) return; const cont = $('.summary-content'); cont.textContent = '正在重新生成总结...'; const sum = await request(content); if (sum) { cont.innerHTML = sum; } else { cont.textContent = '生成总结失败,请检查设置和网络连接'; } }; tl.appendChild(title); tl.appendChild(regen); const close = createElement('span', { innerHTML: '✕', style: 'cursor:pointer;opacity:.6;font-size:16px;padding:4px 8px' }); close.onclick = () => c.style.display = 'none'; tb.appendChild(tl); tb.appendChild(close); c.appendChild(tb); const cont = createElement('div', { className: 'summary-content', style: 'white-space:pre-wrap;word-break:break-word;text-align:left;padding:10px 0;line-height:1.8' }); c.appendChild(cont); // 将容器插入到第一个 cell 之前 const firstCell = mainBox.querySelector('.cell'); if (firstCell) { mainBox.insertBefore(c, firstCell); } else { mainBox.appendChild(c); } return c; } async function request(content, retries = 3, timeout = 10000) { const settings = store.get(); if (!settings.apiUrl || !settings.apiKey || !settings.modelName) { alert('请先完成设置(API URL、API Key 和模型名称为必填项)'); return null; } const fetchWithTimeout = (url, options, timeout) => { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: options.method, url: url, headers: options.headers, data: options.body, timeout: timeout, onload: function(response) { resolve({ ok: response.status >= 200 && response.status < 300, status: response.status, json: () => JSON.parse(response.responseText) }); }, onerror: function(error) { reject(new Error('Network error')); }, ontimeout: function() { reject(new Error('Request timeout')); } }); }); }; for (let i = 0; i < retries; i++) { try { const r = await fetchWithTimeout(settings.apiUrl, { method: 'POST', headers: { 'Authorization': `Bearer ${settings.apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: [ {role: "system", content: settings.prompt}, {role: "user", content} ], model: settings.modelName, stream: false }) }, timeout); if (!r.ok) throw new Error(`HTTP error! status: ${r.status}`); const d = await r.json(); return d.choices?.[0]?.message?.content || '总结生成失败,请检查API返回格式'; } catch (e) { if (i === retries - 1) { alert(`请求失败: ${e.message}`); return null; } await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); console.log(`第 ${i + 1} 次重试失败,准备重试...`); } } } function createElement(tag,props={}){ const el=d.createElement(tag); Object.assign(el,props); return el; } function addStyle(el,css){ const s=createElement('style'); s.textContent=css; el.appendChild(s); } function getContent() { const contentElement = document.querySelector('#Main .topic_content'); return contentElement ? contentElement.innerText : ''; } function summarizeContent() { const content = getContent(); // 如果内容为空,可以提前返回或显示提示 if (!content) { alert('未找到文章内容'); return; } // 其余代码... } function addButton() { const mainElement = document.querySelector('#Main'); if (!mainElement) return; const button = document.createElement('button'); button.textContent = '总结内容'; button.style.marginBottom = '10px'; button.onclick = summarizeContent; // 直接使用函数引用 const resummaryButton = document.createElement('button'); resummaryButton.textContent = '重新总结'; resummaryButton.style.marginLeft = '10px'; resummaryButton.style.marginBottom = '10px'; resummaryButton.onclick = summarizeContent; // 同样直接使用函数引用 mainElement.insertBefore(button, mainElement.firstChild); mainElement.insertBefore(resummaryButton, mainElement.firstChild.nextSibling); } // 为了处理可能的动态加载情况,添加 MutationObserver const observer = new MutationObserver((mutations, obs) => { if (!w.location.pathname.match(/^\/t\/\d+/)) return; const gray = $('#Main .box .header .gray'); if (gray && !gray.querySelector('.summary-tools')) { summary(); } }); observer.observe(d.body, { childList: true, subtree: true }); // 确保在 DOM 加载完成后执行 if(d.readyState === 'loading') { d.addEventListener('DOMContentLoaded', () => setTimeout(summary, 0)); } else { setTimeout(summary, 0); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址