// ==UserScript== // @name V2EX 文章总结助手 // @name:zh-CN V2EX 文章总结助手 // @namespace http://tampermonkey.net/ // @version 2.0 // @description 为 V2EX 帖子生成总结 // @description:zh-CN 为 V2EX 帖子生成总结 // @author Jandaes // @homepage https://greasyfork.org/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 https://www.v2ex.com/* // @icon https://www.v2ex.com/favicon.ico // @grant none // @license MIT // @copyright 2024, Jandaes (https://github.com/Jandaes) // @downloadURL none // ==/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 store={get:k=>JSON.parse(ls.getItem(k)||'{}'),set:(k,v)=>ls.setItem(k,JSON.stringify(v))}; const defaultPrompt='只精简总结文章内容和评论的核心要点、不需要加入你的任何观点。分别输出文章内容和用户评论'; function modal(){ const isDark=store.get('theme')==='dark'||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` }); // 先将 modalContent 添加到 modal m.appendChild(c); c.innerHTML=`

V2EX 文章总结助手设置

主题
🔒
GitHub
`; 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} .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} `); // 先将 modal 添加到 body,这样后续的选择器才能找到元素 d.body.appendChild(m); // 加载设置 const settings=store.get('settings'); $('#url',c).value=settings.apiUrl||''; $('#key',c).value=settings.apiKey||''; $('#model',c).value=settings.modelName||''; $('#prompt',c).value=settings.prompt||defaultPrompt; // 绑定事件 $('.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('settings',{ 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(){ const h=$('.header'); if(!h)return; const g=$('.gray',h); if(!g||$('.summary-button',g))return; g.insertAdjacentText('beforeend',' ∙ '); const sum=createElement('a',{ href:'javascript:void(0)', className:'tb summary-button', innerHTML:'总结 ' }); sum.onclick=async()=>{ // 获取文章内容 const content = $('.topic_content')?.textContent.trim(); if(!content) return; 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='生成总结失败,请检查设置和网络连接'; } }; g.appendChild(sum); g.insertAdjacentText('beforeend',' ∙ '); const set=createElement('a',{ href:'javascript:void(0)', className:'tb settings-button', innerHTML:'设置 ⚙️' }); set.onclick=modal; g.appendChild(set); } 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 h=$('.header'); if(!h||$('.summary-container'))return $('.summary-container'); const c=createElement('div',{ className:'summary-container', style:`margin:10px 0;padding:15px;background:var(--box-background-color,#fff);border-radius:6px;font-size:14px;line-height:1.6;display:none;border:1px solid var(--box-border-color,#eee);box-shadow:0 2px 4px rgba(0,0,0,.05)` }); 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); h.parentNode.insertBefore(c,h.nextSibling); return c; } async function request(content, retries = 3, timeout = 10000) { const s = store.get('settings'); if (!s.apiUrl || !s.apiKey || !s.modelName) { alert('请先完成设置(API URL、API Key 和模型名称为必填项)'); return null; } const fetchWithTimeout = async (url, options, timeout) => { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { ...options, signal: controller.signal }); clearTimeout(timeoutId); return response; } catch (e) { clearTimeout(timeoutId); throw e; } }; for (let i = 0; i < retries; i++) { try { const r = await fetchWithTimeout(s.apiUrl, { method: 'POST', headers: { 'Authorization': `Bearer ${s.apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: [ {role: "system", content: s.prompt || defaultPrompt}, {role: "user", content} ], model: s.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); } if(d.readyState==='loading')d.addEventListener('DOMContentLoaded',summary); else summary(); })();