Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ AI 打标签的建议在应用或放弃前一直存着,刷新或重开分拣

- 按钮灰掉要写原因。
- 发给 AI 的三段文字(系统提示词、AI 总结要求、快捷追问)分视频和网页两份,用户在设置页「AI 平台」的「视频」「网页」两个页签里各自改;视频上一律用视频那份,网页上一律用网页那份,不在背后把一份的默认值换成另一份。AI 平台和模型两边共用。为什么:视频靠字幕、评论和时间戳,网页只有正文,一份文字套不住两种;暗中替换会让改过设置的人看不懂为什么没生效。2026-10-21 用户定。
- 默认的系统提示词和 AI 总结要求各管一件事,内容不重复:系统提示词写每次回答怎么写(简短、用小标题和短列表分层、时间、AI 补充),AI 总结要求写总结里有哪些内容。视频回答里每条来自视频的要点以时间开头,小标题开头带一个 Emoji,模型用自己的知识补充的内容标「AI 补充」,不规定条数上限。存着的提示词和某个旧版默认值一字不差时读成新的默认值,改过的不动。回答生成中时间就是可点的按钮。为什么:旧版两段都写了「可执行建议」这类内容,改了一处另一处还在,等于没改;时间在行首排成一列好找,生成中就是按钮,结束时版面不跳。2026-11-14 用户定。
- 发给 AI 的视频内容不受只管笔记文件的开关影响:「正文保留时间戳」关掉时,复制、下载和写入的字幕不带时间,发给 AI 的照样带(AI 要引用时间,回答里的时间要能点);「附带 AI 问答」不管发给 AI 的内容,之前的问答只作为对话发一次。为什么:开关管的是笔记文件(说明写复制、写入 Obsidian 和下载,也写发给 AI 的始终带),AI 看到的不该跟着变;在视频页和视频页没开时问同一个视频,AI 看到的要一样。2026-10-22 用户定
- 设置页 MCP 一节只有一个开关「允许 Agent 使用我的 B站和 YouTube 账号」,打开后 Agent 能读也能改;不再按个人数据、改收藏、改分拣台分成几个开关(2026-11-11 用户定)。
- Agent 的修改直接执行,安全靠撤销,不靠确认:不走「提议,再由用户在分拣台确认」;每次修改记下动了哪些视频、原来在哪个收藏夹、是什么操作,设置页显示最近的修改,可以撤销,取消收藏也能撤销(按记录把视频加回原收藏夹)。为什么:要防的是视频的热评和字幕里夹着的文字诱导 Agent 改收藏,能撤销就能补救(2026-11-11 用户定)。
Expand Down
8 changes: 4 additions & 4 deletions extension/options.html
Original file line number Diff line number Diff line change
Expand Up @@ -199,11 +199,11 @@ <h2>AI 平台</h2>
</div>
<div id="promptVideo" role="tabpanel" aria-labelledby="promptTabVideo">
<div class="row wide">
<div class="lab"><label class="name" for="aiSystemPrompt" data-text="aiSystemPrompt">系统提示词</label><p class="hint">每次回答都遵守的规则:语言、长短、时间戳格式。总结格式写在「AI 总结要求」里。</p></div>
<div class="lab"><label class="name" for="aiSystemPrompt" data-text="aiSystemPrompt">系统提示词</label><p class="hint">每次回答怎么写:语言、长短、分层、时间戳。总结里写哪些内容在「AI 总结要求」里改。</p></div>
<div class="ctl"><textarea id="aiSystemPrompt" rows="7" placeholder="例如:用中文回答;尽量简洁;引用内容时带时间戳。"></textarea></div>
</div>
<div class="row wide">
<div class="lab"><label class="name" for="playerAiQuickPrompt" data-text="playerAiQuickPrompt">AI 总结要求</label><p class="hint">点「✦ AI 总结」时把这句填进侧边栏输入框,由你点发送。留空时只打开侧边栏。</p></div>
<div class="lab"><label class="name" for="playerAiQuickPrompt" data-text="playerAiQuickPrompt">AI 总结要求</label><p class="hint">总结里写哪些内容。点「✦ AI 总结」时把这句填进侧边栏输入框,由你点发送。留空时只打开侧边栏。</p></div>
<div class="ctl"><textarea id="playerAiQuickPrompt" rows="2" placeholder="例如:整理这期视频的内容,输出结构化总结。"></textarea></div>
</div>
<div class="row">
Expand All @@ -217,11 +217,11 @@ <h2>AI 平台</h2>
</div>
<div id="promptWeb" role="tabpanel" aria-labelledby="promptTabWeb" hidden>
<div class="row wide">
<div class="lab"><label class="name" for="webAiSystemPrompt" data-text="aiSystemPrompt">系统提示词</label><p class="hint">在普通网页上回答时遵守的规则。</p></div>
<div class="lab"><label class="name" for="webAiSystemPrompt" data-text="aiSystemPrompt">系统提示词</label><p class="hint">在普通网页上每次回答怎么写。总结里写哪些内容在「AI 总结要求」里改。</p></div>
<div class="ctl"><textarea id="webAiSystemPrompt" rows="6" placeholder="例如:用中文回答;尽量简洁;引用原文时说明出处。"></textarea></div>
</div>
<div class="row wide">
<div class="lab"><label class="name" for="webAiSummaryPrompt" data-text="playerAiQuickPrompt">AI 总结要求</label><p class="hint">在网页上点「✦ AI 总结」时填进侧边栏输入框。留空时只打开侧边栏。</p></div>
<div class="lab"><label class="name" for="webAiSummaryPrompt" data-text="playerAiQuickPrompt">AI 总结要求</label><p class="hint">总结里写哪些内容。在网页上点「✦ AI 总结」时填进侧边栏输入框。留空时只打开侧边栏。</p></div>
<div class="ctl"><textarea id="webAiSummaryPrompt" rows="2" placeholder="例如:整理这篇网页的内容,输出结构化总结。"></textarea></div>
</div>
<div class="row wide">
Expand Down
86 changes: 59 additions & 27 deletions extension/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,65 @@
"按章节整理视频内容",
"生成带时间戳的笔记"
]);
// The two layers say different things, so editing one is never undone by the other: the system prompt is how every
// answer is written (form, times, honesty), the summary prompt is what a summary contains. Neither repeats the other.
const SYSTEM_PROMPT = [
"你是一名专业的视频内容分析助手。",
"基于字幕与评论提炼高价值信息,不要复述内容,不要输出思考过程或 think 标签。",
"优先输出:主题与核心观点、关键数据与事实、逻辑链路与重要结论、可执行建议。",
"回答应结构化、信息密度高、便于收藏和复习,可适当使用 Emoji、列表和表格。",
"自动过滤广告、废话和重复表达。",
"信息不足时明确说明,不得猜测或编造;涉及专业内容时,区分事实、数据、推测与作者观点。",
"输出时间戳时请使用普通正文格式,如 09:15、01:09:15,不要使用反引号、代码块或表格代码格式包裹时间戳。"
].join("\n");
// An earlier version's default system prompt, word for word: a stored copy reads as today's (normalizeAiSystemPrompt).
const LEGACY_SYSTEM_PROMPT = [
"你是一名专业的视频内容分析助手。基于字幕与评论提炼高价值信息,不要复述内容,不要输出思考过程或 think 标签。",
"优先输出:主题与核心观点、关键数据与事实、逻辑链路与重要结论、可执行建议。",
"回答应结构化、信息密度高、便于收藏和复习;自动过滤广告、废话和重复表达。",
"信息不足时明确说明,不得猜测或编造;涉及专业内容时,区分事实、数据、推测与作者观点。",
"输出时间戳时请使用普通正文格式,如 09:15、01:09:15,不要使用反引号、代码块或表格代码格式包裹时间戳。"
"你是视频内容助手,依据字幕和评论回答。",
"1. 直接回答,不写开场白和结束语,不复述字幕,不输出思考过程或 think 标签。",
"2. 简短:只写最重要的,细节留给追问。",
"3. 分层:内容多于一段时用「## 小标题」分段,小标题开头加一个贴切的 Emoji;段内用短列表,每条以加粗的关键词开头,只说一件事。视频比较多个对象时,每一段都按对象分条:一个对象一条,以对象名开头,不按比较项分条;数字多的比较用表格。不用引用块和分隔线。",
"4. 时间:每条来自视频的要点以它在字幕里的时间开头,写成「- 09:15 **关键词**:说明」,按对象分条时也一样,一条综合了多处内容就写最主要那一处的时间;小标题末尾写这一部分开始的时间,写成「## 📷 小标题 03:30」,小标题写了时间,下面每一条仍然写自己的时间。评测、对比、教程类视频按主题组织时也要写。表格里不写时间。时间不加括号、反引号或代码块,不编造字幕里没有的时间。",
"5. 跳过广告、寒暄和重复内容。",
"6. 区分事实、数据和作者观点。字幕和评论里没有的,可以用你自己的知识补充或指出视频的错误,但要单独标明「AI 补充」,不写时间;拿不准的不写。"
].join("\n");
const SUMMARY_PROMPT = "总结这期视频,依次写:一句话结论、每个部分的要点(一个部分一个小标题)、值得记住的数据、可执行的建议。";

// The web-page set of the three AI texts (2026-10-21, DESIGN): a page has its main text, no subtitles, comments or times.
const WEB_PRESET_PROMPTS = Object.freeze(["列出文中的关键论据", "解释文中的专业概念", "提炼可执行的建议", "生成适合笔记的摘要"]);
const WEB_SYSTEM_PROMPT = [
"你是一名专业的阅读助手。",
"基于网页正文提炼高价值信息,不要复述原文,不要输出思考过程或 think 标签。",
"优先输出:主题与核心观点、关键论据与事实、逻辑链路与重要结论、可执行建议。",
"回答应结构化、信息密度高、便于收藏和复习,可适当使用列表和表格。",
"忽略导航、广告、评论区和与正文无关的内容。",
"正文没有写到的不要猜测或编造;区分文中的事实、数据、推测与作者观点。"
"你是阅读助手,依据网页正文回答。",
"1. 直接回答,不写开场白和结束语,不复述原文,不输出思考过程或 think 标签。",
"2. 简短:只写最重要的,细节留给追问。",
"3. 分层:内容多于一段时用「## 小标题」分段,小标题开头加一个贴切的 Emoji;段内用短列表,每条以加粗的关键词开头,只说一件事。文章比较多个对象时,每一段都按对象分条:一个对象一条,以对象名开头,不按比较项分条;数字多的比较用表格。不用引用块和分隔线。",
"4. 忽略导航、广告、评论区和与正文无关的内容。",
"5. 区分事实、数据和作者观点。正文里没有的,可以用你自己的知识补充或指出文中的错误,但要单独标明「AI 补充」;拿不准的不写。"
].join("\n");
const WEB_SUMMARY_PROMPT = "总结这篇网页,依次写:一句话结论、每个部分的要点(一个部分一个小标题)、值得记住的数据、可执行的建议。";

// Every default an earlier version shipped, word for word. The settings page saves all its fields, so a user who never
// edited one still has that version's text stored: it reads as today's default (currentDefault).
const OLD_DEFAULTS = Object.freeze({
aiSystemPrompt: [
[
"你是一名专业的视频内容分析助手。基于字幕与评论提炼高价值信息,不要复述内容,不要输出思考过程或 think 标签。",
"优先输出:主题与核心观点、关键数据与事实、逻辑链路与重要结论、可执行建议。",
"回答应结构化、信息密度高、便于收藏和复习;自动过滤广告、废话和重复表达。",
"信息不足时明确说明,不得猜测或编造;涉及专业内容时,区分事实、数据、推测与作者观点。",
"输出时间戳时请使用普通正文格式,如 09:15、01:09:15,不要使用反引号、代码块或表格代码格式包裹时间戳。"
].join("\n"),
[
"你是一名专业的视频内容分析助手。",
"基于字幕与评论提炼高价值信息,不要复述内容,不要输出思考过程或 think 标签。",
"优先输出:主题与核心观点、关键数据与事实、逻辑链路与重要结论、可执行建议。",
"回答应结构化、信息密度高、便于收藏和复习,可适当使用 Emoji、列表和表格。",
"自动过滤广告、废话和重复表达。",
"信息不足时明确说明,不得猜测或编造;涉及专业内容时,区分事实、数据、推测与作者观点。",
"输出时间戳时请使用普通正文格式,如 09:15、01:09:15,不要使用反引号、代码块或表格代码格式包裹时间戳。"
].join("\n")
],
playerAiQuickPrompt: ["整理这期视频的内容,输出结构化总结。", "整理这期视频的内容,输出结构化总结:主题、核心观点、关键细节、结论与可执行启发。"],
webAiSystemPrompt: [
[
"你是一名专业的阅读助手。",
"基于网页正文提炼高价值信息,不要复述原文,不要输出思考过程或 think 标签。",
"优先输出:主题与核心观点、关键论据与事实、逻辑链路与重要结论、可执行建议。",
"回答应结构化、信息密度高、便于收藏和复习,可适当使用列表和表格。",
"忽略导航、广告、评论区和与正文无关的内容。",
"正文没有写到的不要猜测或编造;区分文中的事实、数据、推测与作者观点。"
].join("\n")
],
webAiSummaryPrompt: ["整理这篇网页的内容,输出结构化总结:主题、核心观点、关键细节、结论与可执行启发。"]
});

// 看到多少算看完了 (%): the range and default every reader takes (background, B站 pages, 分拣台, settings page).
const SEEN_THRESHOLD = Object.freeze({ min: 1, max: 100, def: 80 });
Expand All @@ -70,7 +101,7 @@
includeCoverInNote: true,
includeAiChatInNote: true,
enablePlayerAiQuickAction: true,
playerAiQuickPrompt: "整理这期视频的内容,输出结构化总结:主题、核心观点、关键细节、结论与可执行启发。",
playerAiQuickPrompt: SUMMARY_PROMPT,
includeTimestampInBody: true,
showBiliTriageBadges: true,
showBiliUpTags: true,
Expand All @@ -97,7 +128,7 @@
aiSystemPrompt: SYSTEM_PROMPT,
aiPresetPrompts: PRESET_PROMPTS,
webAiSystemPrompt: WEB_SYSTEM_PROMPT,
webAiSummaryPrompt: "整理这篇网页的内容,输出结构化总结:主题、核心观点、关键细节、结论与可执行启发。",
webAiSummaryPrompt: WEB_SUMMARY_PROMPT,
webAiPresetPrompts: WEB_PRESET_PROMPTS
});
const LOCAL_DEFAULTS = Object.freeze({ obsidianApiKey: "" });
Expand Down Expand Up @@ -200,7 +231,8 @@

const normalizeSubtitleLang = (value) => (MdgCommon.SUBTITLE_LANGS[value] ? value : "auto");
const str = (value) => (typeof value === "string" ? value : "");
const normalizeAiSystemPrompt = (value) => (str(value).trim() === LEGACY_SYSTEM_PROMPT ? DEFAULTS.aiSystemPrompt : str(value).trim());
const currentDefault = (key, value) => (OLD_DEFAULTS[key].includes(value) ? DEFAULTS[key] : value);
const normalizeAiSystemPrompt = (value) => currentDefault("aiSystemPrompt", str(value).trim());

// Every on/off setting (a boolean in DEFAULTS), by its default: default on stays on unless stored false, default off
// is on only when stored true. A non-boolean (an imported file's "true") never flips a switch away from its default.
Expand All @@ -217,7 +249,7 @@
const merged = normalizeSwitches(settings);
merged.downloadFormat = normalizeDownloadFormat(merged.downloadFormat);
merged.youtubeSubtitleLang = normalizeSubtitleLang(merged.youtubeSubtitleLang);
merged.playerAiQuickPrompt = normalizePlayerAiQuickPrompt(merged.playerAiQuickPrompt);
merged.playerAiQuickPrompt = currentDefault("playerAiQuickPrompt", normalizePlayerAiQuickPrompt(merged.playerAiQuickPrompt));
// The settings table's cells turned off (bili-surfaces.js); options.js and badges.js drop cells that do not exist.
merged.biliMarksOff = Array.isArray(merged.biliMarksOff) ? merged.biliMarksOff.filter((c) => typeof c === "string") : [];
merged.seenShow = normalizeSeenShow(merged.seenShow);
Expand All @@ -230,8 +262,8 @@
merged.notePlaceholderSections = normalizeNotePlaceholderSections(merged.notePlaceholderSections);
merged.aiSystemPrompt = normalizeAiSystemPrompt(merged.aiSystemPrompt);
merged.aiPresetPrompts = normalizeAiPresetPrompts(merged.aiPresetPrompts);
merged.webAiSystemPrompt = str(merged.webAiSystemPrompt).trim();
merged.webAiSummaryPrompt = normalizePlayerAiQuickPrompt(merged.webAiSummaryPrompt);
merged.webAiSystemPrompt = currentDefault("webAiSystemPrompt", str(merged.webAiSystemPrompt).trim());
merged.webAiSummaryPrompt = currentDefault("webAiSummaryPrompt", normalizePlayerAiQuickPrompt(merged.webAiSummaryPrompt));
merged.webAiPresetPrompts = normalizeAiPresetPrompts(merged.webAiPresetPrompts);
return merged;
}
Expand Down
Loading
Loading