今日词汇 · dsh-vocab-study
DSH(DeepSeek Harness)的一个纯客户端 UI 插件:在输入框右边放一颗胶囊,点开就是一个背单词面板。
数据只存在你自己的浏览器里,不上报、不联网、不占 Host 服务。
不装插件先看效果:demo/index.html 双击就开,纯本地。
目录
| 想干的事 | 去哪看 |
|---|---|
| 看它长什么样、有什么功能 | 它长什么样 |
| 不装插件先试试 | demo/index.html 双击打开 |
| 装进 DSH / 卸载 / 出问题自救 | 装进 DSH |
| 搞懂它内部怎么组织的 | 它是怎么工作的 |
| 跑测试 | 测试 |
| 它承诺不做什么(12 条硬约束) | 12 条硬约束 |
| 它现在还做不到什么(诚实清单) | 已知缺口 |
| 开发中踩过的坑(11 个事故) | docs/INCIDENTS.md |
状态
| 项 | 说明 |
|---|---|
| 版本 | 0.1.0(Releases) |
| 成熟度 | 功能可用、在真界面里逐条点过;8 套件 / 344 条断言全绿(详见测试) |
| 依赖 | 零。没有 node_modules,不联网、不上报,数据只在本机浏览器里 |
| 装法 | dsh plugin --profile web add dsh-vocab-study(npm,推荐)或 dsh plugin --profile web add github:wt0812/dsh-vocab-study |
| 国内镜像 | gitee.com/wutong2005/dsh-vocab-study(与 GitHub 逐提交一致,见下) |
| 市场收录 | 已提 PR awesome-dsh-plugin#7043,等上游合并 |
| 已知缺口 | 见已知缺口(8 条,含"未在真实网络环境验证词库拉取"这种) |
国内镜像(Gitee)
GitHub 在国内时常连不上,所以有一份 Gitee 镜像:https://gitee.com/wutong2005/dsh-vocab-study
它和 GitHub 逐提交一致(HEAD sha 相同,24 个提交 + v0.1.0 标签都在)。
我在两边的克隆里各跑过 8 个套件,都是 0 失败。
从 Gitee 装的做法是「先克隆,再装本地目录」(dsh plugin 没有 gitee: 这种形态):
git clone https://gitee.com/wutong2005/dsh-vocab-study.git
cd dsh-vocab-study
python tools/install.py
install.py 默认装到 desktop(桌面客户端)。要装进浏览器 Web 界面那套 profile,
把脚本里的 DEFAULT_PROFILE 指到 web 下的同名目录。
镜像不是自动同步的,是我推的。一切以 GitHub 为准。
「成熟度」这一行我写得很克制:能用是真的(你亲手点过), 但它是 0.1.0、只有一个人在用、只有一台机器验过 —— 不是"生产级"。
它长什么样
① 输入框旁那颗胶囊(conversation.input.right,排在「权限」与「模型」之间):

② 右侧栏里的面板(sidebar.right.pane.tab,四个页签:今日 / 进度 / 生词本 / 设置):

| 位置 | 内容 |
|---|---|
conversation.input.right |
一颗胶囊 今天第一词,排在「权限」与「模型」之间 |
sidebar.right.pane.tab |
一个叫「今日背单词」的标签页,四个页签:今日 / 进度 / 生词本 / 设置 |
截图:上面两张图就在
assets/里(screenshot-1-capsule.png、screenshot-2-panel.png), 同时由screenshots.json声明,市场详情页会用同一份声明 (上游probe-screenshots.mjs从仓库 HEAD 实时读取,不需要另外提 PR)。
不用它的日子它什么都不做。面板永远不会自己弹出来(这是刻意的,见下面「12 条硬约束」)。
背完一轮之后(不是死角)
一轮走完时不再只剩一个「换词库」,而是给三条各自不同的出路:
| 控件 | 做什么 | 什么时候出现 |
|---|---|---|
| 再背一轮 | 把刚才这一批原样重来一遍(不动记忆数据,不算新词) | 这一轮有内容可重背时 |
| 再来 5 个新词 | 往后加量:容器扩大,接在队列后面,不重复 | 新词闸门开着时 |
| 我还是想继续 | 打开新词闸门(安全阀仍然自动触发,只是给了用户自己开门的权力) | 闸门自动关闭时 |
| 换词库 | 换一本词库 | 始终有,但降到最弱的样式 |
两个容易搞混的数字,故意分开:
- 进度环 = 这一轮(
0/20→5/20→ 走完5/20),点「再背一轮」后归零; - 今天已背 = 今天累计(KPI 里第一个,抽空背 3 个也看得见),跨轮累加、不归零。
为什么拆开:环原来用的是跨轮累加的值,点了「再背一轮」之后环显示上一轮的数字, 和「今天已背」互相矛盾(实测:环说 1、今日说 6)。用户会以为数字坏了。
新词安全阀(规格 §7.6):今天新词错得太多时自动停新词。这是安全阀不是拦路虎 —— 它仍然自动触发,但界面上给一个「我还是想继续」,用户自己决定要不要开。
想先看效果,不装插件
# 双击即可,纯本地,不需要装任何东西
start demo\index.html
demo/ 是一个自包含的演示页:里面有一个假的 DSH 外壳(对话区 + 输入框 + 工具栏),
胶囊和面板都是同一份源码编译出来的,不是另写的仿制品。
demo/ 里的几个文件:
| 文件 | 用途 |
|---|---|
index.html |
打开就是这个 demo,可以真的点着背 |
smoke.html |
自检页:外壳 11 项检查 + core 自检,全绿才算过 |
preview.html |
直接截到「词卡」态(未揭晓) |
preview-reveal.html |
截到「已揭晓」态 |
preview-progress.html |
截到「进度」页 |
preview-settings.html |
截到「设置」页 |
preview-vocab.html |
截到「生词本」页 |
preview-hook.html |
截到「记忆钩子」相关态 |
preview*.html 只是为了截图/看态方便,它们会在打开后自动点开对应页签。
装进 DSH
给别人用
# 推荐:从 npm 装(快,国内有镜像)
dsh plugin --profile web add dsh-vocab-study
# 或者从 GitHub 源码装(npm 上不去时用)
dsh plugin --profile web add github:wt0812/dsh-vocab-study
装完 完全退出 DeepSeek Harness 再打开(cordis.patch.yml 是启动时读的,刷新页面没用)。
关于 --profile,这是最容易搞错的一处 —— 上面这两种写法就是对的。
dsh plugin 是命令行工具,它把后面的参数原样转发给 profile 目录里的 pnpm
(读的是 DSH 本体的命令定义:「manage a profile's plugins by forwarding the
remaining arguments to pnpm in the profile directory」),其中 --profile 是必填项。
⚠️ 不要写 --profile desktop —— 那会直接报错:
error: profile "desktop" is managed exclusively by the Electron application
读自 DSH 本体的启动器(rejectElectronProfile):desktop 这个 profile 名
保留给 Electron 应用独占管理,CLI 拒绝针对它的 plugin-management。
所以桌面客户端用户也用 --profile web —— 你桌面 App 一直用的就是
profiles/desktop 那个目录,是 App 自己写进去的,不需要你手动装进去。
dsh plugin --profile web add ... 写的是 profiles/web,你平时打开的那个 Web
界面(以及桌面版内嵌的那个)都读它。
市场目录里 4481 条条目的 install 命令全部是
--profile web—— 跟这里一致。
不需要 npm 命令行工具、不需要构建、不需要 Python。 从 npm 装时 pnpm 会去
registry 拉包;从 GitHub 装时 lib/ 里的产物已经提交进仓库,装完就能用。
卸载:
dsh plugin --profile web remove dsh-vocab-study
它不联网、不上报。 背单词的数据只落在你浏览器自己的
localStorage里,插件不注册任何 Host 服务, 也不发一个网络请求(音标朗读用的是系统 TTS,不是在线服务)。唯一声明的能力是往两个槽位里画东西。
自己改代码用:本机脚本
想改源码、看完再装,用仓库自带的脚本(幂等,坏了重跑一次就能修好):
# 1. 构建产物(lib/client.js 是插件本体,lib/index.js 是 host 占位入口)
python tools/build.py
python tools/build-plugin.py
# 2. 安装(改 profile 的 package.json 与 cordis.patch.yml)
python tools/install.py
想看它会做什么而不动任何文件:python tools/install.py --dry-run。
⚠️ 这个脚本里的路径是本机绝对路径,只适合作者自己用。 给别人装请用上面的
dsh plugin命令。
卸载
python tools/uninstall.py # 只想看会做什么就加 --dry-run
卸载前必须完全退出 DeepSeek Harness,否则 node_modules 里那份被正在运行的进程
占着删不掉(脚本会明确报 ✗ 删不掉 并以退出码 1 结束,不会假报成功)。
卸载只动属于本插件的东西:node_modules\dsh-vocab-study、package.json 里的
两条登记、cordis.patch.yml 里的一段。其它插件的条目一个都不碰(脚本每次都会
打印其余 bundles 清单让你核对),本仓库的源码/测试/demo 也一个都不删。
为什么必须「完全退出」而不是刷新页面
cordis.patch.yml 是启动时读的,客户端模块清单也在启动时扫描并缓存
(客户端半身的机会只有启动那一次)。只按 F5 不够 —— 现象就是「装了、重启了、
界面上什么都没有」。而 lib/client.js 的字节变化在刷新后是能生效的(走 ?rev=),
所以改了插件代码只要刷新;改了 cordis.patch.yml 必须重启。
万一重启后客户端起不来了(自救)
如果重启后整个客户端停在启动页、弹窗写着「DeepSeek Harness 无法使用」, 按弹窗那个「禁用第三方插件」按钮就能恢复(它会备份 profile patch 再禁用)。 然后完全退出 DSH,在本仓库里跑:
python tools/uninstall.py # 卸干净,回到装之前的状态
或者临时只改 cordis.patch.yml:把末尾 - id: vocab-study 那一条删掉(必须完整重启)。
这个问题曾经真实发生过一次,根因与修法见 排查笔记 · 事故二。
装插件前跑一遍 python tools/install.py --dry-run 可以确认不会动到别的插件。
换用 dsh plugin 命令也可以
# 装本机这个开发目录(注意不是 --profile desktop,那个 CLI 会拒绝)
dsh plugin --profile web add "F:\DeepSeek Harness\projects\vocab-study"
但它有两个已知副作用,所以本仓库默认用 tools/install.py:
- 它可能把安装位做成真实副本而不是联接(于是以后重建产物到不了插件目录);
- 它在管理界面里点「停用」会往
cordis.patch.yml写disabled: true,而且 点「启用」不会删掉那一行 —— 那种状态下 Host 侧显示enabled: true / fiberPhase: active,但客户端半身永远不加载。
tools/install.py 每次都会把这两件事纠正过来(重建联接、清掉 disabled)。
手动安装(不想用 dsh plugin 命令)
关键:一定要用联接(junction),不要复制文件。 原因见下面「最大的坑」。
$profile = "C:\Users\<你>\.dsh\profiles\desktop"
$src = "F:\DeepSeek Harness\projects\vocab-study"
# 用联接,不要 Copy-Item
cmd /c mklink /J "$profile\node_modules\dsh-vocab-study" "$src"
# 然后往 $profile\package.json 里补:
# dependencies: { "dsh-vocab-study": "file:F:/DeepSeek Harness/projects/vocab-study" }
# dsh.profile.bundles 数组末尾加 "dsh-vocab-study"
最大的坑:profile 里那份可能是「副本」,不是「联接」
pnpm install 装 file: 依赖时,可能把包复制成真实目录,而不是建联接。
一旦是副本,你在源码目录里 python tools/build-plugin.py 重建,
profile 里那份永远不会变 —— 现象就是「改了没反应、刷新也没用」,
而所有测试又是绿的,非常难查。
判断方法(PowerShell 的 LinkType 属性不可靠,用 fsutil 或 Node):
fsutil reparsepoint query "$profile\node_modules\dsh-vocab-study"
# "不是重解析点" / "not a reparse point" → 是副本,下面的坑就在你身上
// 或者用 Node(最准)
const fs = require('fs');
fs.lstatSync(path).isSymbolicLink(); // junction 在 Windows 上也算 true
fs.realpathSync(path); // 指向源码目录 = 联接;指向自己 = 副本
修法:删掉副本目录,改建联接(本节顶部那行 mklink /J)。
每次重建后自检(两边大小必须一致):
(Get-Item "$src\lib\client.js").Length -eq (Get-Item "$profile\node_modules\dsh-vocab-study\lib\client.js").Length
# True = 一致(联接正常);False = 你在看一份旧副本
第二个坑:插件管理器写的 disabled: true 清不掉
在插件管理界面里点「停用」,它会在 profile 的 cordis.patch.yml 里写下:
- id: vocab-study
disabled: true
然后在界面里点「启用」并不会删掉这行。 插件会一直处于停用状态:
Host 侧看起来是 enabled: true / active(因为运行时的树还记得启用过),
但客户端半身永远不加载,现象就是「装了、重启了、界面上什么都没有」。
修法:手动删掉那两行。文件末尾留一条注释提醒自己。
另外注意:cordis.patch.yml 是启动时读取的,改完必须完整重启 DSH
(刷新页面没用——客户端清单在启动时就扫描完了)。
它是怎么工作的
三层结构,界面与逻辑严格分开
core/ 纯逻辑。零宿主依赖,可跨环境运行(Node 里直接跑单测)
memory.mjs 半衰期记忆模型(HLR):多久会忘、什么时候该复习
mistake.mjs 错因诊断、形近词混淆、永久拉黑
queue.mjs 今日容器:三层配额(复习/收藏/新词)、断签缩量、连续打卡
game.mjs 连击倍率、等级、收服计数
beckon.mjs 「该不该招呼你」的状态机(第一词/好久不见/到期/学词中/交接…)
session.mjs 一次学习会话:轮次、同轮重训、撤销、拼写测验
library.mjs 词库:内置种子 / 剪贴板解析 / 降级到内置词表
speech.mjs 系统语音朗读(不支持就静默跳过,不影响背词)
app.mjs 把它们缝起来,对外只暴露一个 createApp(ports)
spec-cases.mjs 共享验收用例(demo 与 Node 单测跑的是同一套断言)
store/
client-store.mjs 浏览器持久化:多窗口只读锁 + 配额降级(不阻塞)
ui/ 界面。只依赖 core 的命名空间,不依赖任何 UI 框架
dom.mjs 极小的 DOM 工具(el / mount / setText …)
pages.mjs 进度页、生词本页、设置页的渲染
panel.mjs 整个面板:四个页签、键盘快捷键、卡片交互
capsule.mjs 输入框右边那颗胶囊
styles.css 所有样式,类名与 CSS 变量统一带 vs- 前缀,不会跟宿主打架
shell.mjs 演示用的假 DSH 外壳(**不属于插件**,只有 demo 用)
adapter/dsh/ 插件适配层
puppet.mjs 把 core + ui 组装成「胶囊」和「面板」两个傀儡
client.js 真正的 DSH 客户端入口:占槽位、注册标签页
命名空间:界面层怎么拿到 core
单文件产物把所有文件摊平进同一作用域,ESM 绑定全部消失;插件入口又只能 require('react'),
不能 import 自己人。所以界面层拿 core 的唯一通道是 ui/core-ref.mjs 的注册表:
// ESM 形态(Node 单测):ui/pages.mjs 自己登记
setCore({ memory: M_, mistake: MK_, game: G_, queue: Q_, library: L_, beckon: B_ });
// 摊平形态(构建产物):构建脚本按 CORE_EXPORTS 统一登记
setCore({ memory: { newMemory, encode, ... }, mistake: { ... }, ... });
读的时候键名只有全名,单字母只能当别名:
const { queue: Q } = getCore(); // ✅
const { Q } = getCore(); // ❌ Q 是 undefined(构建期就会报错拦下)
这个坑真的踩过:设置页报 Q.buildDailyContainer is not a function。
现在两个构建脚本都会扫描这种写法,构建期直接失败,不会留到运行时。
构建
python tools/build.py # → demo/(自包含 HTML,双击可开)
python tools/build-plugin.py # → lib/client.js + lib/index.js(真正装进 DSH 的产物)
两个脚本读的是同一份源码,没有手抄的核心逻辑。
构建是确定性的:同一份源码构建两次,产物逐字节相同(lib/ 和 demo/ 都是)。
产物头里那行构建时间取的是 core/ 与 ui/ 里最新的源码修改时间,不是 datetime.now();
也可以用标准的 SOURCE_DATE_EPOCH 环境变量覆盖它。
这一条不是洁癖,它让「提交的产物是不是最新的」可以被机器验证 —— CI 里重新构建一次,
只要有 diff 就红。以前时间戳是 now(),这条检查永远是假红,等于没做。
build-plugin.py 会:
- 剥掉所有 import/export(除
react外任何可注册的默认导入都会让构建失败) - 把 core 里的
Q.dayKey(...)改写成裸符号名dayKey(...)(摊平后没有命名空间对象) - 把 CSS 包成字符串 +
injectVocabStudyCss() - 用真的
node --check验证语法 - 检查符号清单、重名、
getCore()键名、ESM 别名残留
测试
python tools/build.py && python tools/build-plugin.py
node test/core.test.mjs
node test/plugin.test.mjs
node test/demo-boot.test.mjs
node test/client-activation.test.mjs
node test/bundle-audit.test.mjs
node test/mount.test.mjs
node test/round-control.test.mjs
node test/multi-conversation.test.mjs
| 测试 | 覆盖什么 | 当前 |
|---|---|---|
test/core.test.mjs |
共享验收用例 20 组 + 种子数据、字段名一致性等体检 + 记忆钩子 12 项 + 新词顺序 13 项 | 85 通过 / 0 失败 |
test/plugin.test.mjs |
假宿主:槽位契约、胶囊顺序、key 一致性、组件可实例化、样式只注入一次、真的点一次胶囊并断言走到了 openTabIn(见事故四) |
33 通过 / 0 失败 |
test/demo-boot.test.mjs |
最小 DOM 里真的跑一遍 demo 产物:外壳建起来、四个页签逐个点开、真的点一次「✨ 让我记住它」并检查输出非空、真的拨一次「出场顺序」开关并检查设置变了 | 34 通过 / 0 失败 |
test/client-activation.test.mjs |
真的把 lib/client.js 跑起来:注册 → 物化工厂 → 检查导出 → 调 apply(ctx) → 断言注册了哪些槽位。守的是"插件能不能被宿主激活" |
18 通过 / 0 失败 |
test/bundle-audit.test.mjs |
产物静态审计:只有 1 处 load()、只 require('react')、inject 声明齐全、没有"未声明服务上的裸方法调用"、openVocabTab 的契约(见事故三、四)、宿主容器能撑开面板 + 限宽居中不被 margin 简写清零(见事故五) |
28 项全过 |
test/mount.test.mjs |
真的把界面画出来并断言节点进了 DOM 树:从产物里抠出傀儡工厂 → 拿真容器建一遍 → 断言容器里真的有 .vs-panel / .vs-capsule。守的是"注册成功了但屏幕上是空的"(见事故三)。第 ⑤ 节守播放键不是死键(见事故六) |
33 通过 / 0 失败 |
test/round-control.test.mjs |
「背完一轮之后」的三条出路:今日已背计数、再背一轮、往后加量、安全阀(自动触发 + 用户放行)、跨天重置,三个只有真点一遍才发现的 bug 的回归锁,第 ⑧ 节换词库必须真的把词换掉(见事故七),第 ⑨ 节设置要留痕,第 ⑩ 节用真实存储件测写入/读取对称(见事故九 —— 这节的绿必须是真绿,因为前两节用的替身把它屏蔽过),第 ③.5 节**「再来 5 个新词」不许重考刚背过的**(见事故十 —— 断言的是"下一张卡是谁",不是容器自己的属性) | 100 通过 / 0 失败 |
test/multi-conversation.test.mjs |
两个对话必须共用同一个内核实例(见事故十一)。物化真产物 → 拿真注册的胶囊组件 → 用假 React 挂两次(conv-a / conv-b)→ 断言两次拿到的是同一个 app,再从一个对话改设置、验证另一个立刻看到。第 ③ 节只做不会误报的结构断言 |
13 通过 / 0 失败 |
CI
每次推送和 PR 都会在 GitHub Actions 上跑一遍(Node 20 / 22 两档 × Ubuntu):
.github/workflows/test.yml。它验四件事,都是能挡住真问题的那种:
- 仓库确实零依赖 —— 出现
node_modules就红(装插件本来也不需要它); - 提交的产物是最新的 —— 重新构建后
lib/有 diff 就红(靠上面那条确定性构建才成立); - 八个套件全过 + 命名空间清单守卫;
- 市场收录要的字段没坏 ——
dsh.bundle.patch指向的文件真的存在、dsh.client.platform仍是web。
第 2 条以前是做不了的:构建时间戳用
datetime.now(),重新构建必然差一行, 这条检查永远是假红。是加 CI 的过程中发现并修掉的 —— 见上面「构建」一节。
另外有两个浏览器里真点一遍的验证工具(不是单测,需要本机 Chrome):
python tools/verify-clicks.py # 真的点胶囊、背完一轮、点每个新按钮,断言状态变化
python tools/render-states.py # 把「开局 / 走完一轮 / 放行之后」三个状态截成一张对比图
为什么要"真点一遍":这一轮修「背完一轮之后」时,单元测试全绿、截图看着也对, 但我把按钮真点了一遍,抓到三个渲染和源码都看不出来的问题:
- 闸门关着时「再来 5 个新词」点了什么也没发生 —— 闸门把新词挡住,容器扩了但没有新词进来。 一个"点了没反应"的按钮比没有按钮更坏,所以闸门关着时不显示它。
- 点「再背一轮」后进度环显示上一轮的数字(环说 1、今日已背说 6),两个数字互相矛盾。
- 点「我还是想继续」后,端上来的居然是刚背完的那个词 —— 根因是
createSession无条件把容器里所有条目变成answered:false的新队列, 把整个容器丢给它,就等于让用户把刚背的 20 个词原样再背一遍。教训:渲染出来 ≠ 能用。截图能证明"画对了",证明不了"点下去有反应"。 这三个都锁进了
test/round-control.test.mjs(第 ⑦ 节)。
client-activation、bundle-audit、mount三个都是真实事故的产物,而且它们守的是 三个不同的失效模式,缺一个就会漏:
client-activation守「激活不了」——插件把整个客户端拦在启动页(事故二);bundle-audit守「契约写错」——调了不存在的方法、服务没声明、静默吞异常(事故三前半);mount守「注册成功了但看不见」——槽位active: true、存储有痕迹,屏幕上却什么都没有(事故三后半)。还有第四个失效模式,上面三套一个都守不住:界面画出来了、也在 DOM 里、也不报错, 但布局是塌的(事故五:面板只占 32% 高度、内容贴在左边)。这种只能靠 「把
offsetWidth/getComputedStyle打出来」或截图看出来。bundle-audit第 9、10 节是把当时量出来的那两条契约固化了 —— 它们的特征是静默失效:不会抛错,测试也不会红,只有肉眼看得出来。
demo-boot里那两条「真的点一次」是故意这么写的:以前那个 AI 按钮是假的 (有加载态、有失败文案,看着像"功能正常只是 AI 挂了"),只读代码发现不了; 而且它还顺带抓出了一个真 bug —— 在「设置」页待过之后切回「今日」,页面会空白 (lastCardKey没变就跳过重建卡片骨架,mount(null)抛错)。
调试开关:
$env:VS_DUMP=1; node test/demo-boot.test.mjs # 打印外壳 DOM 树与关键计数
test/plugin.test.mjs 是装进 DSH 之前的最后一道闸:它用假宿主真的把四个槽位的组件
各实例化一次。这一轮它抓出了 4 个「插件装上了但界面不出现」级别的 bug
(命名空间对象在摊平后不存在、TDZ、模板字面量剥离漏掉、ESM 别名残留)。
12 条硬约束
下面这些是设计规格里的顶层原则(那份规格不在本仓库里,它写在开发时的工作目录中),实现时当成不可协商的约束:
| # | 约束 | 怎么落地的 |
|---|---|---|
| P1 | 面板永不自作主张 | 胶囊只在那里待着,绝不自动展开;demo 外壳也一样 |
| P2 | 不打断当前工作 | 胶囊是输入框旁的普通按钮,不抢焦点、不弹窗 |
| P3 | 文案永不指责 | 断签文案是「好久不见」,不是「你落下了」;没有欠债措辞 |
| P4 | 加量必须点头 | 「每天几个词」要用户自己调,系统不悄悄加 |
| P5 | 任何时刻可离开 | 关面板不丢状态;Esc 把焦点还给输入框 |
| P6 | 不阻塞等待 | 词库拉不到就用内置词表;没有语音就静默跳过 |
| P7 | 数据诚实 | 「收服」要记忆强度 ≥ 0.9 且拼写答对,学了两下不算 |
| P8 | 互动不劫持 | 快捷键绑在面板元素上,焦点在输入框时按 1/2/3 完全没反应 |
| P9 | 不制造欠债感 | 断签后容器自动缩量(≤14 天 60%,再久 40%),不催补课 |
| P10 | 不主动召回 | 没有通知、没有红点、没有倒计时 |
| P11 | 进步必须可见 | 进度页有 30 天留存曲线、半月热力图、顽固词榜 |
| P12 | 回归门槛低于坚持门槛 | 回来随时能用,缩量后仍然算打卡(≥60% 容器即打卡) |
已知缺口(诚实清单)
客户端半身需要重启 DSH 才会出现。 已实测:在 DSH 运行中把插件装进 profile 后,Host 侧会被 Loader 正常认领 (
include:vocab-study / enabled: true / fiberPhase: active),但浏览器里不会长出来—— 客户端模块清单(@deepseek-ai/dsh-client-modules)在启动时就完成了扫描, 它按 Loader 条目名逐包做增量对账,且注释明确写着"包元数据(包括『这不是客户端包』 的否定结论)缓存到重启为止"。在运行中停用→启用该插件触发重扫,实测无效。 结论:装完必须完整退出 DSH 再打开。isStreaming恒为 false。adapter/dsh/client.js里createCapsulePuppet({ core, onOpen })的isStreaming: () => false。§3.1 的「交接态」招呼(任务在输出时把胶囊切成 「我先退到一边」)因此暂时不会触发。原因:我无法从 DSH 文档确认客户端能读到的 「本轮是否正在输出」信号(尝试过 Client inspect 查询,它挂在等待页面响应上)。 这是可降级缺口:不影响背词,只是少一种招呼文案。发音依赖系统语音。
speech.mjs用speechSynthesis。没有语音的环境静默跳过,不报错、不挡流程。AiPort没有实现 —— 记忆钩子是本地算出来的,不是 AI 编的。 规格 §9 设计了一个AiPort接口,让 DSH 适配层实现它、由一个模型生成记忆钩子。 本插件的AiPort仍然是空的,原因是纯客户端插件拿不到宿主的模型通道 (lib/index.js那个 Host 入口刻意什么都不做)。所以
✨ 让我记住它现在走core/hook.mjs:只用词库里真实存在的字段 (词根 / 例句 / 搭配 / 易混词)重组成一句钩子,离线可算、零等待、不编造。产出带 来源标注(「· 来自词根」),满足规格 §9「让人看得出它凭什么这么说」; 结果永久缓存(§9「每词一生只付一次」),第二次点零等待。为什么这样做:以前这个按钮是假的 —— 假装加载 900ms,然后写死一句 「✨ 暂时没编出来 · 重试」。它违反 P7(数据诚实),而且用户永远用不上这个功能。 与其留一个点了必然失败的按钮,不如给一个真的能用的。
以后接上 AI 的正确形态是叠加而不是替换:有 AI 时用 AI,AI 挂了回落到本地 结果(§9「AI 挂了不阻塞」)。那时「暂时没编出来」才是一句诚实的失败文案。
词库网络拉取未在真实环境验证。
library.mjs的拉取路径只有降级测试覆盖(拉不到 → 用内置词表)。演示与测试都走内置种子词表。node_modules那份安装是联接,跑pnpm install可能把它换成副本。 换了以后插件照旧能用(副本内容是完整的),只是以后重建产物不会自动生效。 自查与修复办法见下面「最大的坑」,或直接重跑python tools/install.py。多窗口只读锁是「尽力而为」。
client-store.mjs用sessionStorage的 tab id + 8 秒租约做单写者。浏览器异常退出 时锁会在 8 秒内自然过期,不会永久卡死;但极端时序下仍可能出现短暂的双写。胶囊点击不能把右侧栏标签页"拉出来"。 已修(见排查笔记 · 事故三)。 当时的判断是错的:
openTab确实在ctx.sidebarRight上,只是无参形态内部走require()守卫、侧栏没打开时必抛。正解是带sessionId走openTabIn(sessionId, kind), 它走actionsFor(sessionId),不要求侧栏已经打开。这条留在清单里而不是删掉:"已知缺口"也可能是自己判断错了。 当时把它写成"宿主不提供这个能力",实际上是"我调错了方法"。
排查笔记(事故记录)→ docs/INCIDENTS.md
开发过程中真实踩过的 11 个坑都记在那份文档里(症状 → 根因 → 修法 → 怎么验的), 从 README 搬过去单独放,内容一行没删 —— 这样 README 是产品文档,那份是工作日志。
这里只留一条对其他 DSH 插件作者也有用的结论,因为它不属于这个插件、属于 DSH:
- 纯客户端插件装完之后,必须完全退出 DSH 再打开,刷新页面没用。
客户端模块清单(
@deepseek-ai/dsh-client-modules)在启动时就扫完了, 之后按 Loader 条目名逐包做增量对账,而包元数据(包括「这不是客户端包」这个 否定结论)会被缓存到重启为止。所以运行中装插件,Host 侧会被正常认领, 浏览器里却不会长出来。详见 已知缺口 第 1 条。
被搬走的坑里,有几条是排查方法而不是产品 bug,同样值得一读:
「别用 /plugins/<id>/client.js 探测客户端是否加载(那条路由本来就 404)」、
「Host 侧成功不代表客户端会加载,两条链路要分别验」。
目录速查
VOCAB-STUDY-SPEC.md 设计规格(不在本仓库里,写在做它时的上一层目录)
package.json dsh.bundle.patch / dsh.client.platform 在这里
cordis.patch.yml host 一行条目(不注册服务)
core/ store/ ui/ adapter/dsh/ 源码
lib/ 构建产物(client.js / index.js)
demo/ 自包含演示与截图页
test/ 八个测试(core / plugin / demo-boot / client-activation / bundle-audit / mount / round-control / multi-conversation)
tools/build.py demo 构建
tools/build-plugin.py 插件构建
tools/install.py 正式安装(幂等,可 --dry-run)
tools/uninstall.py 卸载(诚实报告失败)
python tools/verify-clicks.py需要本机 Chrome,且跑之前先清掉残留 chrome 进程 —— 残留实例会让--dump-dom卡死(我撞过一次,白等十分钟)。
No comments yet. Be the first to write one.