// ==UserScript==
// @name V2EX 文章总结助手
// @name:zh-CN V2EX 文章总结助手
// @namespace http://tampermonkey.net/
// @version 0.1
// @description 为 V2EX 帖子生成总结
// @description:zh-CN 为 V2EX 帖子生成总结
// @author Jandaes
// @homepage https://github.com/Jandaes/v2ex_ai/
// @supportURL https://github.com/Jandaes/v2ex_ai/issues
// @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 文章总结助手设置
主题
`;
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=getContent();
if(!content)return;
const container=getContainer();
if(!container)return;
const cont=$('.summary-content',container);
const id=w.location.pathname.match(/\/t\/(\d+)/)?.[1];
const saved=store.get('summaries')[id];
if(saved){
cont.innerHTML=saved;
container.style.display='block';
return;
}
cont.textContent='正在生成总结...';
container.style.display='block';
const sum=await request(content);
if(sum){
const s=store.get('summaries');
s[id]=sum;
store.set('summaries',s);
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);
}
function getContent(){
const c=$('.topic_content');
if(!c)return null;
return c.textContent.trim();
}
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){
const id=w.location.pathname.match(/\/t\/(\d+)/)?.[1];
const s=store.get('summaries');
s[id]=sum;
store.set('summaries',s);
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){
const s=store.get('settings');
if(!s.apiUrl||!s.apiKey||!s.modelName){
alert('请先完成设置(API URL、API Key 和模型名称为必填项)');
return null;
}
try{
const r=await fetch(s.apiUrl,{
method:'POST',
headers:{
'Authorization':`Bearer ${s.apiKey}`,
'Content-Type':'application/json',
'Accept':'*/*',
'Connection':'keep-alive'
},
body:JSON.stringify({
messages:[
{role:"system",content:s.prompt||defaultPrompt},
{role:"user",content}
],
model:s.modelName,
stream:false
})
});
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){
alert(`请求失败: ${e.message}`);
return null;
}
}
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();
})();