# 开发日志 —— 从上游 0.1.4 到 0.1.9 这份记的是**为什么这么改**,不是「改了什么」(那个看 [CHANGELOG](../CHANGELOG.md))。 以后回来看,最有用的是那些**踩过的坑**:它们都写了根因,不是"试了一下就好了"。 基线:`jojoman2024/dsh-bili-scroll` 0.1.4(它自己又是 `c1overzz/dsh-bili-scroll` 的分支)。 产物是 DSH 插件:host 半端(`index.js`,跑在 DSH 主进程里)+ client 半端(`client.js`,右侧栏面板)。 --- ## 0.1.4 → 0.1.5:先让它在这台机器上真能跑 ### ① 桌面版没有全局 `dsh`,headless 那轮根本起不来 根因:上游的 `resolveDsh()` 在找不到 dsh 时**回退成裸字符串 `dsh`**,`spawn('dsh', …)` 在 Windows 上直接 `'dsh' 不是内部或外部命令`,日志里只留一句 `刷B站结束(manual,code=1)`。 修法:桌面版自带一份 CLI —— `%LOCALAPPDATA%\Programs\DeepSeek Harness\resources\app.asar\dsh\node_modules\@deepseek-ai\dsh-desktop-host\lib\cli.js`。 用 `process.execPath`(Electron 本体)+ `ELECTRON_RUN_AS_NODE=1` 去跑它: ```js { command: process.execPath, args: [cli], shell: false, env: { ELECTRON_RUN_AS_NODE: '1' } } ``` ### ② run-as-node 模式里拿不到凭据服务 `ELECTRON_RUN_AS_NODE=1` 时 `safeStorage` 不可用,模型调用直接 `MISSING_CREDENTIAL: llm-deepseek: no API key for provider route "deepseek-official"`。 修法:`apiKeyEnv()` 从 `$DSH_HOME/.credentials.yaml` 用正则取出 `refs.DEEPSEEK_API_KEY`, 塞进子进程 env。**不写死、不落盘**。 ### ③ 产物不能只有「一份汇总」 上游的产物是 `out/<日期>.md` 一份大杂烩。使用者要的是「每轮一份、每条视频一份」, 所以改成:`reports/<标题>-.md`(每条视频一份,模型写)+ `out/<日期>/round-.md`(每轮一封信,宿主拼)。 **真相只有一份**:信里的内容全部从 `reports/` 解析出来,不另存一份状态。 --- ## 0.1.5 → 0.1.6:把「发信」从模型手里拿走 之前是**模型自己调 `send-report.py` 发信**。两个问题: 1. 模型在读**不可信内容**(B站字幕),同时手里有**发信能力**和**本地文件读写** —— 正好凑齐 Simon Willison 说的「致命三要素」; 2. 它发信的时刻不可控:有时跑完忘了发,有时一轮发两封。 改成 **host 半端发信**(`lib/mail.mjs` + `lib/send_mail.py`): - 模型只写笔记,`child.on('exit')` 里 host 判断 `code === 0` 才发; - 授权码只存在 `~/.dsh/mail.json`,模型那侧读不到(子进程 env 里不传); - `send_mail.py` 纯标准库,`--dry-run` 可以先试排版。 > 这条是**架构级的**:能由宿主做的事,绝不交给读外部内容的那个进程。 --- ## 0.1.6 → 0.1.7:「每轮刷几个」这件事统一成一个数 中间反复过两轮: - 先是三个旋钮(每次搜回几条 / 每次最多记几条 / 每轮读几条候选)—— 使用者说"画蛇添足"; - 合成一个 `perRound` 后,`perRound=5` 却"4 推荐 + 10 跳过" —— 因为**搜到的**和**读了的**是两个口径。 最终:`perRound` 同时写 `searchNum` / `candidatesPerRound` / `perDay`, 提示词明确"**这轮看了几条,就写几条结论**"——推荐的写完整笔记,不推荐的也写短的(`## 为什么不推荐`)。 使用者要的是"这 5 条都怎么样",不是"哪几条好"。 --- ## 0.1.7 → 0.1.8:今日列表 宿主在 `reconcileToday()` 里扫 `reports/*.md` 的 **mtime ≥ 今天 00:00**, 用笔记末尾的 marker 建条目: ```html ``` - 幂等靠 `meta.ingested` 记账(文件名列表),不是靠"跑过一次就置位"; - 兜底:没有 marker 的老笔记,用 `# 标题` / `UP` / `BV` 反解; - `verdict: reject` 的也进列表 —— 使用者要看到"这轮都看了什么",不只是好的。 **埋的坑(0.1.8 的事故)**:面板保存配置时把没改的字段写成了默认值, `obsidianEnabled` 被冲成 `false`、`obsidianDir` 被冲成空串 ⇒ Obsidian 静默不写。 修法:保存时只写**改过的**字段(`patch` 语义),不要整份 `config` 覆盖。 --- ## 0.1.8 → 0.1.9:版式、文件夹、跳过的标题 使用者拿他最喜欢的那封旧日报当模板,逐字对比,指出三个问题: 标题不显眼、字段挤成一段、漏了「我的一句话」、「为什么值得」写到两三百字太长。 - `lib/digest.mjs` 重写:`## B站日报 · <日期>(周X)` + 每条一组项目符号 + 末尾「总结」; - 「为什么值得」提示词从 100~250 字压到 **80~120 字**,渲染时再 `clampWhy()` 兜一道; - 「我的一句话」缺失时补默认句(整行消失比写一句废话更糟); - 今日页按文件夹分组:`meta.folders` + 条目上的 `folder` 字段,默认每轮一个文件夹, 拖拽用原生 HTML5 事件,不引第三方库; - 「本次跳过」的「(无标题)」:根因是模型写的 `_skip` 行里只有 BV。提示词强制带标题, 旧记录由宿主用 `bili_video_info` 回填(每次最多 8 条,避免一轮打太多请求)。 ### 自动续刷"只刷 1 条"与"一分钟刷两轮" 两个原因分开看: 1. **判重表饱和**:一天刷了十几轮之后,同一批关键词搜出来的大部分都"看过"了, 模型只找到 1 条新的就收工 —— 这不是 bug,是当天真的没新东西。提示词补了一句: 搜到的多数已在判重表里时,允许换 1~2 组词补足**没看过的**。 2. **重启后的时间戳丢失**:`autoNextAt` 只在内存里,重启后是 0 ⇒ 任何一次 tick 都判成"该续刷了", 于是 12:53 刷完、12:54 又来一轮。修法:每轮跑完都往 `today/.json` 的 `meta.autoNextAt` 写时间戳,判断间隔时**内存优先、落盘兜底**。 --- ## 工程约定(改代码前先看这几条) | 事 | 做法 | |---|---| | 语法检查 | **系统 node**(`C:\Program Files\nodejs\node.exe --check`)。Electron 的 `--check` 对坏文件也不报错,不能信 | | 宿主自检 | `DSH_HOST_MODULES=~/.dsh/profiles/desktop/node_modules/@deepseek-ai node --import ./tools/register.mjs selfcheck.mjs` | | 行为冒烟 | `node tools/smoke.mjs`(假 ctx + 假 req,验证今日列表 / promote / 幂等) | | 面板 CSS | `node tools/check-client-css.mjs client.js`(沙箱里 `spawn` 会 EPERM,单独跑) | | 装机 | `bili-dev/install-dev.ps1`(先备份到 `dsh-bili-scroll.bak-*`,`-Rollback` 回滚) | | 生效 | **改 host 或 client 都要重启桌面版**(插件在 bundle 层组装,重载页面不算) | | 家目录 | `~/.dsh/bili-duty/`:`config.json` / `mail.json` / `seen.md` / `reports/` / `out/` / `today/` | ⚠️ 这个仓库 `.gitignore` 里排掉了 `*.cmd`、`out/`、`data/`、`credential*.json` —— **别把使用者的邮件授权码或本机绝对路径提交上去**。