DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

wt0812 /

wt0812/dsh-vocab-study

Verified

Offline vocabulary panel for DeepSeek Harness: a capsule beside the composer and a sidebar tab.

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@0cb2c4ff

今日词汇 · 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:

  1. 它可能把安装位做成真实副本而不是联接(于是以后重建产物到不了插件目录);
  2. 它在管理界面里点「停用」会往 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 会:

  1. 剥掉所有 import/export(除 react 外任何可注册的默认导入都会让构建失败)
  2. 把 core 里的 Q.dayKey(...) 改写成裸符号名 dayKey(...)(摊平后没有命名空间对象)
  3. 把 CSS 包成字符串 + injectVocabStudyCss()
  4. 用真的 node --check 验证语法
  5. 检查符号清单、重名、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。它验四件事,都是能挡住真问题的那种:

  1. 仓库确实零依赖 —— 出现 node_modules 就红(装插件本来也不需要它);
  2. 提交的产物是最新的 —— 重新构建后 lib/ 有 diff 就红(靠上面那条确定性构建才成立);
  3. 八个套件全过 + 命名空间清单守卫;
  4. 市场收录要的字段没坏 —— dsh.bundle.patch 指向的文件真的存在、dsh.client.platform 仍是 web。

第 2 条以前是做不了的:构建时间戳用 datetime.now(),重新构建必然差一行, 这条检查永远是假红。是加 CI 的过程中发现并修掉的 —— 见上面「构建」一节。

另外有两个浏览器里真点一遍的验证工具(不是单测,需要本机 Chrome):

python tools/verify-clicks.py    # 真的点胶囊、背完一轮、点每个新按钮,断言状态变化
python tools/render-states.py    # 把「开局 / 走完一轮 / 放行之后」三个状态截成一张对比图

为什么要"真点一遍":这一轮修「背完一轮之后」时,单元测试全绿、截图看着也对, 但我把按钮真点了一遍,抓到三个渲染和源码都看不出来的问题:

  1. 闸门关着时「再来 5 个新词」点了什么也没发生 —— 闸门把新词挡住,容器扩了但没有新词进来。 一个"点了没反应"的按钮比没有按钮更坏,所以闸门关着时不显示它。
  2. 点「再背一轮」后进度环显示上一轮的数字(环说 1、今日已背说 6),两个数字互相矛盾。
  3. 点「我还是想继续」后,端上来的居然是刚背完的那个词 —— 根因是 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% 容器即打卡)

已知缺口(诚实清单)

  1. 客户端半身需要重启 DSH 才会出现。 已实测:在 DSH 运行中把插件装进 profile 后,Host 侧会被 Loader 正常认领 (include:vocab-study / enabled: true / fiberPhase: active),但浏览器里不会长出来—— 客户端模块清单(@deepseek-ai/dsh-client-modules)在启动时就完成了扫描, 它按 Loader 条目名逐包做增量对账,且注释明确写着"包元数据(包括『这不是客户端包』 的否定结论)缓存到重启为止"。在运行中停用→启用该插件触发重扫,实测无效。 结论:装完必须完整退出 DSH 再打开。

  2. isStreaming 恒为 false。 adapter/dsh/client.js 里 createCapsulePuppet({ core, onOpen }) 的 isStreaming: () => false。§3.1 的「交接态」招呼(任务在输出时把胶囊切成 「我先退到一边」)因此暂时不会触发。原因:我无法从 DSH 文档确认客户端能读到的 「本轮是否正在输出」信号(尝试过 Client inspect 查询,它挂在等待页面响应上)。 这是可降级缺口:不影响背词,只是少一种招呼文案。

  3. 发音依赖系统语音。 speech.mjs 用 speechSynthesis。没有语音的环境静默跳过,不报错、不挡流程。

  4. AiPort 没有实现 —— 记忆钩子是本地算出来的,不是 AI 编的。 规格 §9 设计了一个 AiPort 接口,让 DSH 适配层实现它、由一个模型生成记忆钩子。 本插件的 AiPort 仍然是空的,原因是纯客户端插件拿不到宿主的模型通道 (lib/index.js 那个 Host 入口刻意什么都不做)。

    所以 ✨ 让我记住它 现在走 core/hook.mjs:只用词库里真实存在的字段 (词根 / 例句 / 搭配 / 易混词)重组成一句钩子,离线可算、零等待、不编造。产出带 来源标注(「· 来自词根」),满足规格 §9「让人看得出它凭什么这么说」; 结果永久缓存(§9「每词一生只付一次」),第二次点零等待。

    为什么这样做:以前这个按钮是假的 —— 假装加载 900ms,然后写死一句 「✨ 暂时没编出来 · 重试」。它违反 P7(数据诚实),而且用户永远用不上这个功能。 与其留一个点了必然失败的按钮,不如给一个真的能用的。

    以后接上 AI 的正确形态是叠加而不是替换:有 AI 时用 AI,AI 挂了回落到本地 结果(§9「AI 挂了不阻塞」)。那时「暂时没编出来」才是一句诚实的失败文案。

  5. 词库网络拉取未在真实环境验证。 library.mjs 的拉取路径只有降级测试覆盖(拉不到 → 用内置词表)。演示与测试都走内置种子词表。

  6. node_modules 那份安装是联接,跑 pnpm install 可能把它换成副本。 换了以后插件照旧能用(副本内容是完整的),只是以后重建产物不会自动生效。 自查与修复办法见下面「最大的坑」,或直接重跑 python tools/install.py。

  7. 多窗口只读锁是「尽力而为」。 client-store.mjs 用 sessionStorage 的 tab id + 8 秒租约做单写者。浏览器异常退出 时锁会在 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 卡死(我撞过一次,白等十分钟)。

—/ 5

No ratings yet

Verified DSH bundle

Commit 0cb2c4ff143d

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout