太长不看?AI时代当然是让AI来解决阅读困难
都什么年代了还自己看帖,现在都是让AI帮忙总结了。
其实是解决两个痛点:
- 主帖内容太长看的头疼,又怕抓不住重点,尤其是公告帖光看就看半天
- 有的热门讨论虽然有意思,但是几十上百楼的讨论太多太杂,懒得一条条翻
我TM发个帖子前面都给你总结好了,还是一直有人回复太长不看。这么玩是吧,AI启动!
使用方法
- 安装Tampermonkey插件,将本帖末尾的脚本代码添加进插件
- 添加自定义API,直接修改代码开头的
CONFIG,必填项包括:
API_ENDPOINT: 完整的 API 地址,如 https://api.openai.com/v1/chat/completionsAPI_KEY: API 密钥,一般是 sk 开头MODEL: 调用的模型,如deepseek-ai/DeepSeek-V3.1-Terminus
- 保存脚本并刷新帖子页面,帖子标题下方会增加总结按钮,enjoy!
功能说明
- ✅ 单独总结主帖或主帖+回帖
- ✅ 支持重新生成总结,不满意就让AI重来
- ✅ 兼容 NodeSeek 等同矩阵网站
- ✅ 兼容OpenAI格式的自定义API
- ✅ 根据个性化需求自定义提示词
- ✅ 流式输出,拒绝长时间等待
- ✅ 增强上下文关联,如楼层间的回复关系和同一个人的多条回复
- ✅ 配合NodeSeek X等脚本,可以对页面上所有加载出的楼层做总结,同时不会对论坛服务器做额外请求
已知问题
1. 提示词优化
显然提示词还有优化空间,这个我不太擅长,缝缝补补就这样了,欢迎大佬给出更好的提示词
2. 不支持HTTP的API
暂不支持HTTP协议的 API(因浏览器混合内容限制),请使用HTTPS。
因为目前请求用的是fetch方法,支持流式输出但是没法避免混合内容问题,而GM_xmlhttpRequest可以绕过,也就是支持HTTP请求,但是反复想办法变通也无法实现流式输出,我认为这会极大影响体验。
不过因为大部分人用的应该都是HTTPS协议,所以暂时先发布了。如果有大佬能解决这个问题,欢迎帮忙修复。
功能截图
1. 按钮功能展示
按钮位于帖子标题下方,三个按钮分别对应总结主帖、总结主帖+回复和关闭弹出的总结框

2. 总结主帖+回复
以这个帖子为例(通过NodeSeek X脚本加载所有回帖,图中可以看到包括 #103都在总结范围,另外同一个人的不同楼层以及回复前后关系也有体现):
今天的元老状态更新了哈
https://www.deepflood.com/post-25303-1

3. 总结主帖
以这个帖子为例:
DeepFlood社区的定位和创立原因
https://www.deepflood.com/post-1410-1

脚本代码
// ==UserScript==
// @name DeepFlood帖子内容AI总结
// @namespace https://www.deepflood.com
// @version 0.2.1
// @description 帖子内容AI总结
// @author yacc@DeepFlood
// @match https://www.deepflood.com/post-*
// @match https://www.nodeseek.com/post-*
// @icon https://icons.duckduckgo.com/ip2/deepflood.com.ico
// @require https://cdn.jsdelivr.net/npm/marked/marked.min.js
// @require https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js
// @grant none
// @license GPL-3.0 License
// @supportURL https://www.deepflood.com/post-27610-1
// @homepageURL https://www.deepflood.com/post-27610-1
// ==/UserScript==
(function() {
'use strict';
// ========== 配置区域 - 请在此处修改你的API配置 ==========
const CONFIG = {
// 完整的 API 地址,如 https://api.openai.com/v1/chat/completions
API_ENDPOINT: 'YOUR_API_ENDPOINT',
// API 密钥,一般是 sk 开头
API_KEY: 'YOUR_API_KEY',
// 调用的模型,如 deepseek-ai/DeepSeek-V3.1-Terminus
MODEL: 'deepseek-ai/DeepSeek-V3.1-Terminus',
// 用于总结主帖的提示词
PROMPT_MAIN: `
作为论坛内容分析助手,请对帖子进行简要总结。
【总结要求】
1. 只做**客观总结**,不做主观评价
2. 根据帖子类型提炼**读者最需要了解**的内容:
- 技术帖: 技术要点、方案、问题
- 新闻帖: 简短摘要、关键信息
- 提问帖: 问题描述、背景、期望
- 讨论帖: 话题、主要观点
3. 总结长度: 50-200 字
4. 关键字重点标注
5. 禁止"好的,我将为您总结"等客套话
【输出格式】
一句话概括主题
## 核心内容
- [要点 1]
- [要点 2]
- [要点 3](如有)
## 关键信息
[链接/资源/数据等,如无可省略此部分]
帖子内容如下:\n\n`,
// 用于总结主帖和回帖全部内容的提示词
PROMPT_ALL: `
作为论坛内容分析助手,请对**主帖**及所有**回复**进行总结。
【输入格式】
- 楼层 0 代表主帖,其他为回复
- 每条内容包含: 楼层编号、作者、正文
- 回复关系用 @用户名 (楼层#X) 标注
【总结要求】
1. 只做**客观总结**,区分**事实**和**观点**
2. 首先总结**主帖核心内容**
3. 根据帖子类型针对性总结回复:
- 提问帖: 关注问题讨论和解决方案
- 辩论帖: 梳理正反方论点
- 技术帖: 关注有实质内容的技术回复
- 新闻帖: 关注用户反应和补充信息
4. **关联处理**:
- 针对性回复应联系上下文
- 同一用户的多次回复应整合
- 明显的讨论串应作为连续内容分析
5. **重点关注**:
- 楼主的回复
- 高质量/代表性回复
- 关键信息(链接、资源、方案)
- 用户间的矛盾点或争议
6. **过滤原则**:
- 纯水帖/无实质内容的回复可跳过
- 高度重复的观点应合并总结
- 主帖内容在关键观点章节不要重复归纳
- 回复数>50 时优先总结关键内容
7. 整体内容限制在500字以内
8. 关键字重点标注
9. 禁止"好的,我将为您总结"等客套话
【输出格式】(若没有实质性回复可适当跳过部分环节)
# 主帖概要
[核心内容,2-3 句话]
## 讨论焦点
[主要讨论方向,2-3 点]
## 关键观点(或代表性回复,适当合并条目压缩篇幅)
- **用户A** (楼层#X): [简述]
- **用户B** (楼层#Y): [简述]
## 实用信息
[解决方案/链接/资源等,如无可省略]
## 讨论状态
- 讨论氛围:是否友善,讨论是否积极,或者没有什么实质性回复等
- 问题状态: [已解决/讨论中/无共识](根据帖子类型选择)
帖子内容如下:\n\n`
};
// ========== 样式集中管理 ==========
const styles = {
buttonContainer: `
display: flex;
gap: 10px;
margin-top: 12px;
margin-bottom: 12px;
flex-wrap: wrap;
`,
button: `
padding: 6px 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
`,
buttonHover: `
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
`,
resultBox: `
margin: 20px 0;
padding: 20px;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
animation: slideDown 0.3s ease;
`,
resultTitle: `
font-weight: bold;
font-size: 16px;
color: #333;
margin-bottom: 12px;
display: flex;
align-items: center;
justify-content: space-between;
`,
resultContent: `
background: white;
padding: 15px;
border-radius: 8px;
line-height: 1.8;
color: #333;
min-height: 100px;
max-height: 1000px;
overflow-y: auto;
`,
closeButton: `
background: none;
border: none;
cursor: pointer;
font-size: 20px;
color: #666;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s ease;
`,
closeButtonHover: `
color: #d32f2f;
`,
loadingText: `
color: #667eea;
font-style: italic;
`,
globalStyles: `
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Markdown 样式 */
#ai-summary-content {
font-family: 'PingFang SC', 'Microsoft YaHei', 'Noto Sans CJK SC', sans-serif;
}
#ai-summary-content h1 {
font-size: 1.8em;
font-weight: 600;
margin: 0.8em 0 0.5em 0;
padding-bottom: 0.3em;
border-bottom: 2px solid #eaecef;
color: #2c3e50;
}
#ai-summary-content h2 {
font-size: 1.5em;
font-weight: 600;
margin: 0.7em 0 0.4em 0;
padding-bottom: 0.2em;
border-bottom: 1px solid #eaecef;
color: #34495e;
}
#ai-summary-content h3 {
font-size: 1.25em;
font-weight: 600;
margin: 0.6em 0 0.3em 0;
color: #34495e;
}
#ai-summary-content h4, #ai-summary-content h5, #ai-summary-content h6 {
font-size: 1.1em;
font-weight: 600;
margin: 0.5em 0 0.3em 0;
color: #34495e;
}
#ai-summary-content p {
margin: 0.8em 0;
line-height: 1.8;
}
#ai-summary-content ul, #ai-summary-content ol {
margin: 0.8em 0;
padding-left: 2em;
}
#ai-summary-content li {
margin: 0.3em 0;
line-height: 1.6;
}
#ai-summary-content code {
background-color: #f6f8fa;
padding: 0.2em 0.4em;
border-radius: 3px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 0.9em;
color: #e83e8c;
}
#ai-summary-content pre {
background-color: #f6f8fa;
padding: 1em;
border-radius: 6px;
overflow-x: auto;
margin: 1em 0;
border: 1px solid #e1e4e8;
}
#ai-summary-content pre code {
background-color: transparent;
padding: 0;
color: #333;
font-size: 0.95em;
}
#ai-summary-content blockquote {
border-left: 4px solid #667eea;
padding-left: 1em;
margin: 1em 0;
color: #666;
background-color: #f8f9fa;
padding: 0.8em 1em;
border-radius: 0 4px 4px 0;
}
#ai-summary-content a {
color: #667eea;
text-decoration: none;
transition: color 0.2s ease;
}
#ai-summary-content a:hover {
color: #764ba2;
text-decoration: underline;
}
#ai-summary-content table {
border-collapse: collapse;
width: 100%;
margin: 1em 0;
}
#ai-summary-content table th,
#ai-summary-content table td {
border: 1px solid #dfe2e5;
padding: 0.6em 1em;
text-align: left;
}
#ai-summary-content table th {
background-color: #f6f8fa;
font-weight: 600;
}
#ai-summary-content table tr:nth-child(even) {
background-color: #f8f9fa;
}
#ai-summary-content hr {
border: none;
border-top: 2px solid #eaecef;
margin: 1.5em 0;
}
#ai-summary-content strong {
font-weight: 600;
color: #2c3e50;
}
#ai-summary-content em {
font-style: italic;
}
#ai-summary-content img {
max-width: 100%;
height: auto;
border-radius: 4px;
margin: 0.5em 0;
}
`
};
// ========== Markdown 渲染配置 ==========
// 配置 marked
if (typeof marked !== 'undefined') {
marked.setOptions({
breaks: true, // 支持 GitHub 风格的换行
gfm: true, // 启用 GitHub 风格的 Markdown
headerIds: false, // 不为标题生成 ID
mangle: false // 不混淆邮箱地址
});
}
/**
* 将 Markdown 文本渲染为 HTML,并进行 XSS 防护
* @param {string} markdown - Markdown 文本
* @returns {string} - 安全的 HTML
*/
function renderMarkdown(markdown) {
if (typeof marked === 'undefined' || typeof DOMPurify === 'undefined') {
console.warn('Markdown 渲染库未加载,使用纯文本显示');
return markdown;
}
try {
// 使用 marked 解析 Markdown
const rawHtml = marked.parse(markdown);
// 使用 DOMPurify 清理 HTML,防止 XSS 攻击
const cleanHtml = DOMPurify.sanitize(rawHtml, {
ALLOWED_TAGS: [
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'p', 'br', 'hr',
'strong', 'em', 'b', 'i', 'u', 's', 'del',
'a', 'img',
'ul', 'ol', 'li',
'blockquote', 'pre', 'code',
'table', 'thead', 'tbody', 'tr', 'th', 'td'
],
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class']
});
return cleanHtml;
} catch (error) {
console.error('Markdown 渲染失败:', error);
return markdown;
}
}
// ========== 内容提取和处理函数 ==========
/**
* 预处理帖子内容,清洗和格式化HTML,使其更适合AI理解和总结。
* @param {HTMLElement | string} input - 帖子内容的HTML元素或HTML字符串。
* @returns {string} - 处理后的纯文本或Markdown格式的字符串。
*/
function preprocessPostContent(input) {
let tempDiv;
// 将输入转成临时DOM来操作,DOM直接克隆,字符串新建,否则返回空字符串
if (input instanceof HTMLElement) {
tempDiv = input.cloneNode(true);
} else if (typeof input === 'string') {
tempDiv = document.createElement('div');
tempDiv.innerHTML = input;
} else {
return '';
}
// 处理回复内容,查找 /member 和 /post 链接对
// <p><a href="/member?t=user">@user</a> <a href="/post-114-5#14">#14</a> 回复内容</p>
tempDiv.querySelectorAll('a[href*="/member?t="]').forEach(memberLink => {
const nextElement = memberLink.nextElementSibling;
// 检查紧跟其后的是否是指向楼层的链接
// <a>@user</a> <a>#14</a> 模式
if (nextElement && nextElement.tagName === 'A' && nextElement.getAttribute('href').includes('/post-')) {
const user = memberLink.textContent.trim(); // 如 @user
const floor = nextElement.textContent.trim(); // 如 #14
// 创建一个新的文本节点来替换这两个链接
const replyText = document.createTextNode(`回复 ${user} (楼层${floor}): `);
memberLink.parentNode.insertBefore(replyText, memberLink);
memberLink.remove();
nextElement.remove();
}
});
// 处理表情贴纸 (Stickers)
// <p>帖子内容 <img class="sticker" src="/static/image/sticker/xhj/001.gif" loading="lazy" alt="xhj001"></p>
tempDiv.querySelectorAll('img.sticker').forEach(sticker => {
sticker.replaceWith(document.createTextNode('[sticker]'));
});
// 处理普通图片,转换为Markdown格式
tempDiv.querySelectorAll('img:not(.sticker)').forEach(img => {
const src = img.getAttribute('src') || '';
const alt = img.getAttribute('alt') || 'image';
const markdownImage = document.createTextNode(`!${alt} <sup>1</sup> [<sup>1</sup>](${src})`);
img.replaceWith(markdownImage);
});
// 提取处理后的文本内容
return tempDiv.innerText.trim();
}
/**
* 获取帖子内容
* @param {string} type - 'main' 只获取主帖,'all' 获取主帖和所有回帖
* @returns {string} - 格式化后的帖子内容
*/
function getPostContent(type) {
// 获取帖子标题
const titleElement = document.querySelector('.post-title-link');
const title = titleElement?.textContent.trim() || '未知标题';
// 创建一个数组来存储所有楼层(包括主帖)的数据
const allPostsData = [];
// 主帖内容
const mainPostContainer = document.getElementById('0');
const mainAuthor = mainPostContainer?.querySelector('.author-name')?.textContent.trim();
const mainPostContentElement = mainPostContainer?.querySelector('.post-content');
if (mainPostContentElement) {
allPostsData.push({
floor: 0,
author: mainAuthor || '未知作者',
content: preprocessPostContent(mainPostContentElement)
});
}
// 如果是总结全部,获取所有回帖
if (type === 'all') {
const commentElements = document.querySelectorAll('.comments .content-item');
commentElements.forEach(commentElement => {
// 从元素的 id 中获取楼层号
const floor = parseInt(commentElement.id, 10);
// 元素内部查找作者和内容并添加数组
const author = commentElement.querySelector('.author-name')?.textContent.trim();
const postContentElement = commentElement.querySelector('.post-content');
if (postContentElement) {
allPostsData.push({
floor: floor,
author: author || '未知作者',
content: preprocessPostContent(postContentElement)
});
}
});
}
// 格式化输出
const formattedPosts = allPostsData.map(post => {
return `楼层 ${post.floor} (作者: ${post.author}):\n${post.content}`;
}).join('\n\n---\n\n');
return `标题: ${title}\n\n${formattedPosts}`;
}
// ========== UI 相关函数 ==========
// 注入全局样式
function injectGlobalStyles() {
if (document.getElementById('ai-summary-styles')) {
return;
}
const styleTag = document.createElement('style');
styleTag.id = 'ai-summary-styles';
styleTag.textContent = styles.globalStyles;
document.head.appendChild(styleTag);
}
// 创建按钮容器
function createButtonContainer() {
const container = document.createElement('div');
container.className = 'ai-summary-buttons';
container.style.cssText = styles.buttonContainer;
const btn1 = createButton('AI总结主帖', () => handleSummary('main'));
const btn2 = createButton('AI总结全部', () => handleSummary('all'));
const btn3 = createButton('关闭总结', () => closeSummary());
container.appendChild(btn1);
container.appendChild(btn2);
container.appendChild(btn3);
return container;
}
// 创建按钮
function createButton(text, onClick) {
const button = document.createElement('button');
button.textContent = text;
button.style.cssText = styles.button;
button.addEventListener('mouseenter', () => {
button.style.cssText = styles.button + '; ' + styles.buttonHover;
});
button.addEventListener('mouseleave', () => {
button.style.cssText = styles.button;
});
button.addEventListener('click', onClick);
return button;
}
// 创建结果展示框
function createResultBox() {
const box = document.createElement('div');
box.id = 'ai-summary-result';
box.style.cssText = styles.resultBox;
// 标题和关闭按钮
const titleBar = document.createElement('div');
titleBar.style.cssText = styles.resultTitle;
const titleText = document.createElement('span');
titleText.innerHTML = '🤖 AI总结结果';
const closeBtn = document.createElement('button');
closeBtn.innerHTML = '✕';
closeBtn.style.cssText = styles.closeButton;
closeBtn.addEventListener('mouseenter', () => {
closeBtn.style.cssText = styles.closeButton + '; ' + styles.closeButtonHover;
});
closeBtn.addEventListener('mouseleave', () => {
closeBtn.style.cssText = styles.closeButton;
});
closeBtn.addEventListener('click', closeSummary);
titleBar.appendChild(titleText);
titleBar.appendChild(closeBtn);
// 内容区域
const content = document.createElement('div');
content.id = 'ai-summary-content';
content.style.cssText = styles.resultContent;
// 显示加载提示
content.innerHTML = `<div style="${styles.loadingText}">AI总结中...</div>`;
box.appendChild(titleBar);
box.appendChild(content);
return box;
}
// 处理总结请求
async function handleSummary(type) {
// 移除之前的结果框(如果存在)
removeSummaryBox();
// 创建结果展示框
const resultBox = createResultBox();
// 插入到标题下方
const postTitle = document.querySelector('.post-title');
if (!postTitle) {
console.error('未找到标题元素');
return;
}
postTitle.parentNode.insertBefore(resultBox, postTitle.nextSibling);
// 获取帖子内容
const content = getPostContent(type);
console.log(content);
// 选择对应的 prompt
const prompt = type === 'main' ? CONFIG.PROMPT_MAIN : CONFIG.PROMPT_ALL;
// 调用 AI API
await callAIAPI(resultBox, prompt + content);
}
// 调用 AI API
async function callAIAPI(resultBox, fullPrompt) {
const contentDiv = document.getElementById('ai-summary-content');
let accumulatedText = '';
try {
const response = await fetch(CONFIG.API_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CONFIG.API_KEY}`
},
body: JSON.stringify({
model: CONFIG.MODEL,
messages: [{
role: 'user',
content: fullPrompt
}],
stream: true
})
});
if (!response.ok) {
throw new Error(`API 请求失败: ${response.status} ${response.statusText}`);
}
// 清空加载提示
contentDiv.innerHTML = '';
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const {done, value} = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
// 处理 SSE 格式的数据
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
const text = data.choices?.[0]?.delta?.content || '';
if (text) {
accumulatedText += text;
// 实时渲染 Markdown
contentDiv.innerHTML = renderMarkdown(accumulatedText);
contentDiv.scrollTop = contentDiv.scrollHeight;
}
} catch (e) {
// 忽略解析错误
}
}
}
}
// 最终渲染一次,确保完整
if (accumulatedText.trim()) {
contentDiv.innerHTML = renderMarkdown(accumulatedText);
} else {
contentDiv.textContent = '未获取到AI返回内容';
}
} catch (error) {
contentDiv.innerHTML = `<div style="color: #d32f2f;">❌ 获取AI总结失败:<br>${error.message}</div>`;
console.error('AI API 调用错误:', error);
}
}
// 关闭总结框
function closeSummary() {
removeSummaryBox();
}
// 移除总结框
function removeSummaryBox() {
const existingResult = document.getElementById('ai-summary-result');
if (existingResult) {
existingResult.remove();
}
}
// ========== 初始化 ==========
function init() {
const postTitle = document.querySelector('.post-title');
if (!postTitle) {
console.log('未找到标题元素');
return;
}
// 检查是否已经添加过按钮
if (postTitle.querySelector('.ai-summary-buttons')) {
return;
}
// 注入全局样式
injectGlobalStyles();
// 创建按钮容器
const buttonContainer = createButtonContainer();
// 将按钮容器插入到标题下一行
const h1 = postTitle.querySelector('h1');
if (h1 && h1.parentNode) {
h1.parentNode.insertBefore(buttonContainer, h1.nextSibling);
}
}
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
@酒神 求加分,我也要升三级口牙!
哈哈,之前我本来想自己写一个的,实在抽不出时间来
@yacc #1 已经帮友友加好了
非常好的技术贴
@花火 #3 感谢管理
不错的脚本,免费鸡腿附上
不错
嚯嚯嚯
支持大佬
支持
好东西