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
1 change: 1 addition & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ B站 页面上的颜色是 `tokens.css` 的第三份拷贝:`content.css` 里
- 进度胶囊只从它画:`runs.pill(顺序)` 取第一个在跑、在等或有行的任务,都没有再取跑完留下的绿色结果行;等限流时一律在进度后面接「· B站限流卡住,m:ss 后重试」。别处来的状态(关注的后台刷新和它的「B站限流变慢」、观看进度、刷新中、标签建议待确认、「所有收藏夹」的加载)用 `runs.put` 放一行进去。收藏夹的顺序是 `fav-base.js` 的 `PILL`,按钮是 `PILL_BTN`;关注的在 `follow.js` 的 `renderSync` 旁边。第 3 行的「暂停粗看 N/M」也读 `runs.get`。
- 限流等多久只有 `hold`:收藏夹是 `fav-base.js` 的 `THROTTLES`(`MoonTriage.hold`),关注的 B站限流是 90 秒的 `TriageUi.BILI_HOLD_MS`。后台的队列暂停(`follow_jobs.hold`、`follow_hold`)和观看进度的 30 分钟来自后台存储,不走它。`runs.selftest.js` 在 node 里用假时钟测它。
- 后台的 B站 GET 只走 `triage-bg.js` 的 `triageBiliGetJson`(带 B站 的 referrer、不缓存),30 秒没回应算断网(「网络断了:30 秒没有回应」,时长和文字是 `MdgErrors.NET_TIMEOUT_MS` / `netTimeout`,视频页自己发的请求 `fetchOk` 也用这一份)。细看(`triageAnalyze`)、生成笔记(`triageBuildNote`)和侧边栏接着问 AI 的上下文(`resolveAiSidepanelContext`)都经 `TRIAGE_BILI_IO` 走它(WBI 签名)。视频页的 `fetch-json` 代理也走它(不带 pace,只试一次,由视频页重试;视频页不等了就发 `fetch-json-cancel`,后台按发送方标签页和请求 id 中止那一个请求),失败时把 `code` 和 HTTP 状态一起回给视频页,所以视频页和分拣台、侧边栏读同一个视频失败时写的是同一句(「B站请求失败 HTTP 403」「B站返回的不是 JSON(…)」)。认 B站 限流只有一处:`sites.js` 的 B站 读取都经 `biliJson`,把 HTTP 412、HTTP 429、风控码和「播放器接口回了却没有字幕对象」算成 `THROTTLED`(文字是 `MdgErrors.TEXT.biliThrottled`),视频页、分拣台和侧边栏读同一个视频都经它,所以 B站 限流报成限流,不会变成「没有字幕」,也不再去请求备用接口。收藏夹每次只试一次,限流由页面退避;关注传入自己的队列(间隔、断网重试、风控整队暂停)。写操作 `triageBiliPost` 也有这 30 秒超时,但只发一次、从不自动重试;超时报「不确定 B站 是否已改」,调用方按失败处理,不改计数、不记撤销。风控码统一用 `BILI_RISK_CODES`,WBI 签名用 `sites.js`。
- 收藏夹视频的分区只有一处取:后台消息 `triage-zones { items: [{ bvid, aid }] }`,一次一个 `resource/infos` 请求,最多 `TriageUi.ZONE_BATCH`(50,超过 B站 回空列表)个,不用登录;后台只回 `{ bvid: tid }`(`tid` 是旧版子分区编号;B站 没返回的视频,如已删除,记 0,不再请求;整批回空时回空),不写存储。页面在 `loadResults` 里启动 `fav-folders.js` 的 `fillZones`:`runs` 的 `"zones"` 任务,每批之间隔 1 秒,限流按 `THROTTLES` 等,换了视图就停、丢掉在途的回答;回答由页面经 `storeEdit` 并进 `KEYS.triageZones`(`triage_zones`),在锁里滤掉本页清理过的视频(`zoneGone`),所以在途的回答不会把清理掉的写回去;失败或回空的那批记进 `zoneFailed`,本页不再重发,刷新页面后再试。不改收藏夹快照。编号到大分区名是 `TriageCore.zoneName` 里的静态表(旧编号已冻结,来自 `app.bilibili.com/x/v2/region/index`),运行时不再请求。页面只在卡片信息行末尾和搜索里用它(`fav-base.js` 的 `zoneOf`),不进筛选。这份数据是 B站 的,缺了再取:不进完整备份,清理视频时一起删掉,`storageUsage` 不单列。
- 页面怎么显示一个错误只写一份:`extension/errors.js`(`MdgErrors`)。后台负责分类(`triage-bg.js` 的 `triageAiHttpError`、HTTP 200 里的错误 `triageAiBodyError`、AI 平台没填的项 `aiProviderProblem`,侧边栏、测试连接和分拣台都用它们;`obsidian.js` 的 `obsidianError`),回 `{ ok: false, error, code }`;页面用 `MdgErrors.action(code)` 决定给「去设置」还是「去登录」(Obsidian 没启用、没配置、Key 不对或地址回 404 是 `OBSIDIAN`,给「去设置」;没打开是 `OBSIDIAN_DOWN`,不给),`MdgErrors.text` / `fromResp` 读出要显示的话(B站 的 62004 换成「视频审核中,过后再试」;没登录 B站 一律是 `TEXT.biliLogin`:后台的 `NOT_LOGGED_IN` 由 `triage-bg.js` 的 `triageNotLoggedIn` 建,关注那边 B站 回 -101 时 `follow-bg.js` 也换成它,所以各阶段都停下,不当成「暂时没查到」),缺 AI 平台地址的访问权限时后台回 code `AI_ACCESS`(分拣台按它停下,不认文字;请求没回应时是 `AI_ACCESS` 还是 `AI_NETWORK` 只在 `triage-bg.js` 的 `aiFetchError` 分),模型输出被截断回 `AI_TRUNCATED`(分拣台按它提示调大输出上限),`grantOrigin` 从「未授权访问 …」读出地址并给出授权按钮的请求,`reloaded` 认出扩展更新后内容脚本失效。视频页的内容脚本没回应也只认一处:`chrome.tabs.sendMessage` 的英文错误(「Receiving end does not exist」「Could not establish connection」、端口先关了、标签页或框架没了)由 `MdgErrors.noReceiver`(只认没有内容脚本,这时注入再问才有用)和 `MdgErrors.pageSilent`(这些错、发送方标了 `chrome: true` 的错、超时都换成 `TEXT.pageSilent`「视频页没有回应,请刷新网页后重试」,带 code `PAGE_SILENT`,`CODES` 里它不给「重试」)认,弹窗、侧边栏和后台都用它;发消息只有一份:`MdgErrors.tabMessage`(Chrome 的错标上 `chrome: true`;传 `inject` 时遇到 `noReceiver` 让后台 `ensure-reader-content` 补注入,再问一次,弹窗和侧边栏时间跳转这样用)。重试几次、隔多久各自留着(弹窗注入后问一次,后台专注模式问 12 次、新开的视频页等内容脚本,侧边栏时间跳转问 12 次)。弹窗状态行红不红只看视频页给的 `statusError`(读视频失败,或读完但字幕、评论有一项失败),不从文字猜。后台注入内容脚本失败(`ensureReaderContentReady`)也是这一句。表里的错误码按名字导出成 `MdgErrors.CODE`(写或比较 `THROTTLED`、`AI_SETUP` 的地方用它,不手打),超时的 HTTP 状态 408 是 `MdgErrors.TIMEOUT_STATUS`。HTML 转义也只有这里一份:`MdgErrors.escapeHtml` 转 `& < > " '` 五个字符,页面、视频页和分拣台(`TriageUi.esc` 就是它)拼 HTML 都用它,`errors.selftest.js` 查别处有没有再写一份;`badges.js` 用 `textContent` 建 DOM,不需要它。Obsidian 的几句话(包括取消时那句)和「扩展刚刚更新,请刷新网页」只写在 `MdgErrors.TEXT` 里,后台也读它。它也是全扩展的文字表:多个页面都显示的句子在 `TEXT`,按钮名和页面名(复制 Markdown、下载 .md、写入 Obsidian、去设置、重试、刷新、摘读记录、B站分拣台、已保留、已取消收藏、新标签名称)在 `LABEL`,html 的静态副本标 `data-text="键"`;规则见 DESIGN.md 术语表一节。内容脚本、后台和每个用到它的页面都在用它的脚本之前加载 `errors.js`(`load-order.selftest.js` 核对),`wording.selftest.js` 查别处有没有再写一份这些话(`TriageUi`、`MdgLimits` 里有名字的句子常量也一样),也查同一句话有没有在两个页面各写一份(现有的在文件里的 `DUPLICATES`,每条写明文件和「待修」或「例外」的理由,新出现的失败)和术语表里「不用」的词(只查能和普通文字分开的那些,见 `TERMS`)。按钮灰掉写原因(`disabled`、tooltip 和 `aria-description`)也只有这里一份:`MdgErrors.setReason`,分拣台(`TriageUi.setReason` 就是它)、弹窗、侧边栏和摘读记录页都用它,`errors.selftest.js` 查别处有没有再写一份。
- 设置的默认值和整理函数只写一份:`extension/settings.js`(`MdgSettings`)。`DEFAULTS`(sync)和 `LOCAL_DEFAULTS`(Obsidian 的 Key)是唯一一份默认值,默认的 AI 提示词也只写在这里;`normalizeSyncSettings` 用它整理全部 sync 设置,后台存和读、设置页从表单取值都经它。get-settings 失败时,设置页、弹窗和视频页报「读取设置失败」(`TEXT.settingsReadFailed`),不拿 `DEFAULTS` 接着做;其余页面这时才拿 `DEFAULTS` 兜底。设置页的开关和输入框以设置键为 id,`options.js` 按 `DEFAULTS` 里的类型(布尔、字符串和数字)成批读写(`SWITCH_KEYS`、`VALUE_KEYS`),列表、Obsidian 地址和 Key 单写。安装和更新不把默认值写进 sync,读的时候补(`getMergedSettings`、`sync.get(DEFAULTS)`),所以改了默认值,没改过这项的人都会拿到新的;以前写进去的不迁移。几个页面都要的整理函数(API Key、下载格式、AI 总结要求、快捷追问和它的 12 条上限、自定属性(属性名的规则 `FIXED_PROPERTY_KEY_RE` 和笔记自己写的名字 `RESERVED_PROPERTY_KEYS`:笔记字段、星标的 `starred`、网页的 `published`,`note.js` 不写、设置页拒收并说「与自带属性重复」)、正文附加段落、专注模式的主题和四个档位:合法值只在 `READER_OPTIONS`,默认值在 `DEFAULTS`,视频页的档位按钮和主题切换都读它)也在这里,字符串一律按 `String(v ?? "")` 处理,数字保留。开关(`DEFAULTS` 里是布尔值的设置)怎么取值也只在这里(`normalizeSwitches`):默认开的只有存的是 `false` 才关,默认关的只有存的是 `true` 才开,导入文件里的 `"true"` 这类非布尔值一律按默认值,后台 `normalizeSyncSettings` 存和读都先过它,笔记(`note.js` 的封面、AI 问答、热门评论、文件名日期)和专注模式的章节开关也经它读,专注模式的初始值读 `DEFAULTS`。设置文件的导出和导入都经后台的 `withoutApiKeys`,去掉 Obsidian 和 AI 平台的 Key(包括平台条目里带的 `apiKey`),Key 只存本机;导入后某个 AI 平台(按 id)或 Obsidian 的地址换了 origin,本机存的那个 Key 也删掉,用户重新填(`dropMovedKeys`,和设置页的 `savedKeyApplies` 同一条规则:Key 跟着它保存时的地址)。数字设置不对时怎么取值(`intIn`:空、不是数字、小于下限用默认值,大于上限用上限,四舍五入)也只在这里,`TriageUi.intIn` 就是它,`normalizeSeenThreshold` 也用它,所以凡是加载 `shared.js` 的地方都先加载 `settings.js`。设置页自己的表单检查留在 `options.js`。`note.js` 也读它,所以凡是加载 `note.js` 的地方都先加载 `settings.js`;加载顺序由 `load-order.selftest.js` 核对;`settings.selftest.js` 查别处有没有再写一份默认提示词或这些函数。
- AI 判断的三类(值得留 / 可清理 / 拿不准)和怎么判只写一份:`extension/tag-core.js` 的 `VERDICTS` 和 `verdictOf`(细看优先于粗看,已失效一律可清理)。分拣台(`fav-*.js`)、B站 页面(`badges.js`)、后台(`triage-bg.js` 认模型的回答)和笔记里的「AI 判断:…」(`note.js`)都读它,所以凡是加载 `note.js` 的地方也先加载 `tag-core.js`,加载顺序由 `load-order.selftest.js` 核对,`tag-core.selftest.js` 查别处有没有再写一份。B站 页面只在收藏夹页知道哪些视频已失效(读这个收藏夹的 `triage_snapshot_<fid>`);别的页面不读,所有收藏夹的快照太大。
Expand Down
3 changes: 3 additions & 0 deletions docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
| 导出入口 | 导出 | 批量导出(作为入口名时) | |
| 导出菜单里的一项 | 摘录或笔记…(导出菜单里那一项,按产出命名) | 批量导出… | |
| 收藏夹的判断依据 | 判断标准 | 标准、规则 | |
| 视频在 B站 的大类(舞蹈、生活、知识、时尚…) | 分区 | 子分区、B站标签(指它时) | 2026-11-11 用户定:只用大分区,不用子分区;视频自己的 B站 标签太杂,不用 |
| 临时的待看清单 | 待播(临时的待看清单,跨收藏夹,不参与导出;卡片上写「+ 待播」「✓ 待播」) | 播放列表、优先看 | |
| 分拣台页面 | B站分拣台 | 收藏夹分拣台 | |
| 关注的第二个页签 | 视频投稿(关注的那个页签:只读 B站 的视频投稿,不含图文和转发) | 动态 | |
Expand Down Expand Up @@ -158,6 +159,7 @@ AI 打标签的建议在应用或放弃前一直存着,刷新或重开分拣
行的样子、间距和窄屏排法见「三、视觉设计 · 分拣台」。

- 收藏夹和关注的第 4 行开头是自己的「全部」,接着「未打标签 / 已打标签」(视频投稿按视频的 UP 算,写「UP 未打标签 / UP 已打标签」),再是标签。收藏夹按卡片上显示的标签算:「所有收藏夹」里只算视频所在收藏夹的标签。2026-11-10 用户定:以前这组在第 3 行。为什么:有没有标签是你自己打的,和标签放一行。
- 分区(B站 给的大分区:舞蹈、生活、知识、时尚…)只显示在收藏夹卡片的信息行里,能被搜索(搜「舞蹈」筛出舞蹈区的视频),不放进筛选区。2026-11-11 用户定。为什么:分区太粗,又是 UP 主自己选的,拿来筛选意义不大。只用大分区,不用子分区和视频自己的 B站 标签(太杂)。番剧和已失效的视频没有分区。分区由后台按 50 个一批向 B站 补取,每批之间有间隔,遇到限流在进度胶囊里倒计时再试;补取失败只是卡片上没有分区。
- 第 3、4 行用细竖线分组。组内单选(标签片多选,「并且」),再点一次已选中的就取消;组和组之间、两行之间都是「并且」。第 3 行的「全部」只清空第 3 行,第 4 行的「全部」只清空第 4 行(有没有标签和标签片),都不碰左栏。两个「全部」样子一样,所在行什么都没开时是按下的。2026-11-10 用户定。
- 收藏夹第 3 行的「已保留」「待决定」和三个 AI 判断是同一个单选。保留过的视频只算「已保留」,不再算 AI 判断,和卡片一致(保留过的卡片不显示 AI 判断)。「待决定」是还没保留的视频,三个 AI 判断是它的细分,套在它后面的细框里,个数加起来等于它(「全部」和「未分析」里还有 AI 没看过的,所以会少)。「未分析」没有 AI 判断,只有「已保留」「待决定」。2026-11-10 用户定。为什么:人工判断过就不需要 AI 判断;上一版两组能叠加,「已保留 + 可清理」这种组合没有意义。框而不用括号或箭头:箭头在页签行表示 AI 往前走,括号像文字。
- 第 3 行按 AI 判断的批量按钮(「取消收藏(AI:可清理)N 个」这类)只算待决定的视频。
Expand All @@ -180,6 +182,7 @@ AI 打标签的建议在应用或放弃前一直存着,刷新或重开分拣

- 收藏夹和关注的 UP 主卡片:点卡片任何地方(包括封面、头像)都是选中或取消这一张;点视频标题在分拣台里播放(收藏夹卡片的标题、UP 主卡片里的投稿标题);关注里点 UP 名(UP 主卡片和「视频投稿」卡片上的)在右侧播放框打开这个 UP 的空间,这时 T 和播放框上方那行都对着这个 UP。空间页在播放框里去掉 B站 顶栏和右侧「个人资料」,缩到播放框的宽度,不横向滚动;空间页里不再贴 MoonDigest 的标记(和视频播放框一样),UP 标签看播放框上方那行和左边的卡片。为什么:清理久不更新的 UP 时要看一眼主页才想得起他是做什么的,不用离开分拣台。Shift 点选中从上一次点的那张到这张的一段;卡片里的按钮、输入框、标签片、「+ 标签」各管各的;拖选文字不选中。封面、标题、UP 名仍是去 B站 的真链接:右键、⌘/Ctrl 点、中键照常在 B站 打开。选中的卡片是蓝框加蓝底,卡片上没有勾选框;正在播放的卡片是粉框。X 键选中当前卡片,「已出分拣范围」里也一样(那里的「清理选中的」「收藏选中的…」「重新关注选中的」都对着选中)。判断在 `TriageUi.cardPickClick` 和 `pickCard`,两种模式共用。
- 「视频投稿」的卡片和待播列表没有多选:点卡片任何地方都在分拣台里播放(「视频投稿」走 `TriageUi.cardPlayClick`),卡片里的按钮、输入框、标签片、UP 主链接各管各的,拖选文字不播放。键盘用标题链接回车播放,卡片本身不另加 Tab 停靠点。
- 收藏夹卡片的信息行(UP 主 · 时间 · 播放量 …)最后写分区名,不另起一行。2026-11-11 用户定。

#### 筛选和计数

Expand Down
3 changes: 3 additions & 0 deletions extension/limits.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@
triageAgentLog: "triage_agent_log",
triageAgentProposals: "triage_agent_proposals",
triageBasket: "triage_basket",
// { [bvid]: tid }: B站's old sub-zone number of each listed video (0: B站 gave none), asked by triage-bg.js's
// triage-zones and written by the 分拣台 (fav-folders.js fillZones), which shows its 分区. B站's, read again when missing: not in 完整备份.
triageZones: "triage_zones",
// 摘读记录's 星标 and tags: { tags: [{ id, name, color, rule }], items: { [record key]: { star, tags, ref, updatedAt } } }.
// The record key is MdgNote.readingKey; ref keeps the title and address, so a record stays after its conversations go.
readingMarks: "mdg_reading_marks_v1",
Expand Down
2 changes: 1 addition & 1 deletion extension/limits.selftest.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ const names = Object.values(L.KEYS).filter((v) => typeof v === "string");
assert.deepStrictEqual([...names].sort(), [
"aiProviderKeys", "aiProviders", "follow_", "mdg_ai_conversations_v1", "mdg_ai_selected_provider", "mdg_obsidian_note_paths_v1", "mdg_player_ai_quick_action_v1", "mdg_reading_ai_history_v1", "mdg_reading_ai_proposal_v1", "mdg_reading_ai_recent_v1", "mdg_reading_ai_settings_v1", "mdg_reading_marks_v1", "mdg_subtitle_cache_",
"seen_", "triage_agent_log", "triage_agent_proposals", "triage_analysis_", "triage_basket", "triage_decisions_", "triage_folder_criteria", "triage_folder_list", "triage_included_folders", "triage_kept", "triage_notes", "triage_removed",
"triage_seen_meta", "triage_snapshot_", "triage_tags", "triage_title_", "triage_video_tags"
"triage_seen_meta", "triage_snapshot_", "triage_tags", "triage_title_", "triage_video_tags", "triage_zones"
]);
assert.strictEqual(L.KEYS.triageDecisions(7), "triage_decisions_7");
assert.strictEqual(L.KEYS.triageAnalysis("BV1"), "triage_analysis_BV1");
Expand Down
1 change: 1 addition & 0 deletions extension/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"triage-folder-page": { from: [TRIAGE_BG], to: "triage/fav-page.js", request: ["mediaId", "page"], response: [] },
"triage-folder-ids": { from: ["triage/fav-folders.js", "triage/fav-writes.js"], to: TRIAGE_BG, request: ["mediaId"], response: data },
"triage-fav-where": { from: ["triage/fav-folders.js"], to: TRIAGE_BG, request: ["aid", "mid"], response: data },
"triage-zones": { from: ["triage/fav-folders.js"], to: TRIAGE_BG, request: ["items"], response: data },
"triage-folder-create": { from: ["triage/fav-writes.js"], to: TRIAGE_BG, request: ["title", "privacy"], response: data },
"triage-unfav": { from: ["triage/fav-writes.js"], to: TRIAGE_BG, request: ["mediaId", "aids"], response: data },
"triage-refav": { from: ["triage/fav-writes.js"], to: TRIAGE_BG, request: ["mediaId", "aid"], response: data },
Expand Down
Loading
Loading