# dsh-errkb · 报错回收再利用 ![状态](https://img.shields.io/badge/status-scaffolding%20landed-yellow) ![许可证](https://img.shields.io/github/license/jingchangzhao-gif/dsh-errkb) ![DeepSeek Harness](https://img.shields.io/badge/DeepSeek%20Harness-plugin-blue) [English](README.md) > 回收报错:同一个失败只诊断一次,记录下来,下次出现时直接作为已知解法交回给模型。 `dsh-errkb` 是一个 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)插件。它把模型遇到的报错变成一份带编号、人可编辑的知识库 —— 然后在模型**开始诊断之前**,把已记录的解法塞回上下文。 > **状态:P1 骨架已落地。** 设计文档已写完、正在审查中 —— 其中 §17 还有 8 项 > 未拍板 —— 工作被拆成 **18 个 task**,归入 7 个里程碑。**T01–T04 已完成:** > 包装得上、构建得出、过得了类型检查与 lint、格式与测试也都跑得通,CI 在每次 > PR 上都会执行这五步。 > > **插件本身仍然什么都不做。** `apply` 只打一行启动日志,也还没装进任何 profile > (那是 T17),所以现在没有任何可用行为。下面标着「尚未实现」的部分,依然只是 > 目标路径的描述。 > > - 设计文档:[`docs/设计说明书.md`](docs/设计说明书.md) —— 19 节 > - 它将来会做什么:[它怎么工作](#它怎么工作) > - 你的数据将落在哪里:[知识库落在哪里](#知识库落在哪里) > - 卡在哪一步:[待拍板的事项](#待拍板的事项) —— §17 仍未补齐 ## 目录 - [目前完成到哪一步](#目前完成到哪一步) - [为什么需要它](#为什么需要它) - [能力](#能力) - [它怎么工作](#它怎么工作) - [知识库落在哪里](#知识库落在哪里) - [ERRORS.md 的格式](#errorsmd-的格式) - [注入:什么时候开口、说多少](#注入什么时候开口说多少) - [token 账本](#token-账本) - [环境与兼容性](#环境与兼容性) - [安装](#安装) - [用法](#用法) - [配置](#配置) - [隐私与脱敏](#隐私与脱敏) - [失败模式与安全](#失败模式与安全) - [已知局限](#已知局限) - [怎么确认它真的在工作](#怎么确认它真的在工作) - [开发路线图](#开发路线图) - [开发](#开发) - [常见问题排查](#常见问题排查) - [卸载](#卸载) - [待拍板的事项](#待拍板的事项) - [后续想法](#后续想法) - [参与与反馈](#参与与反馈) - [许可证](#许可证) ## 目前完成到哪一步 | 内容 | 状态 | | ------------------------------------------------------------------------ | -------------- | | `docs/设计说明书.md` —— 设计文档(19 节) | ✅ 已写完待审查 | | `README.md`、`README.zh-CN.md` | ✅ 就位 | | `LICENSE` | ✅ 就位 | | `.github/` —— CI、隐私守卫与 PR 模板 | ✅ 就位 | | `package.json`、`tsconfig.json`、`tsdown.config.ts`、`vitest.config.ts` | ✅ 就位(T01–T03) | | `.prettierignore`、`.gitattributes` —— 格式与行尾策略 | ✅ 就位 | | `cordis.patch.yml` —— bundle patch | ✅ 就位(T04) | | `src/index.ts` —— 插件入口(`name`、`inject`、`Config`、`apply`) | ✅ 就位(T04),仅入口、无行为 | | `src/paths.ts` —— 库目录解析 | ⛔ 未开始 | | `src/signature.ts` —— 规范化与指纹 | ⛔ 未开始 | | `src/redact.ts` —— 强制脱敏 | ⛔ 未开始 | | `src/store.ts` —— 解析、渲染、追加、归档 | ⛔ 未开始 | | `src/match.ts` —— 精确、模糊与兜底匹配 | ⛔ 未开始 | | `src/state.ts` —— `state.json`、`.machine.json`、环境指纹 | ⛔ 未开始 | | `src/capture.ts` —— 钩子载荷与噪声规则 | ⛔ 未开始 | | `src/inject.ts` —— 通知生成与硬上限 | ⛔ 未开始 | | `src/resolve-detect.ts` —— 解决检测 | ⛔ 未开始 | | `src/tools.ts` —— 五个工具 | ⛔ 未开始 | | `tests/` | ✅ 已配置(T03),尚无用例 | | 装进 `web` profile | ⛔ 未开始 | | 发布到 npm | ⛔ 未开始 —— 还没有 task 覆盖它,见[开发路线图](#开发路线图) | ## 为什么需要它 模型没有「上次遇到过」的记忆,所以每次都从零重新推导:思考、失败一次、再失败一次。代价是 800 到 3000 token。 只出现一次的失败,这没什么。但每周都撞一次的失败 —— 被占用的 `node_modules` 抛 `EPERM`、`CONTEXT_OVERFLOW`、provider 名字拼错 —— 就是纯浪费。人一眼能认出「这不就是上次那个吗」,模型认不出,因为路径变了、行号变了、PID 也不一样。 `dsh-errkb` 把知识留在磁盘上,需要时把已知的东西注入进去,补上这个缺口。 ## 能力 > **设计已定,尚未实现。** 以下每一条都在设计文档里有规格、有测试计划。 > **现在没有一条能跑** —— 见[目前完成到哪一步](#目前完成到哪一步)。 | 行为 | 细节 | | -------------------- | -------------------------------------------------------- | | **编号稳定** | 每个不同的报错拿到一个编号,从 `E-0001` 起,永不变更 | | **同种报错一个编号** | 路径、行号、PID、时间戳变化都仍算同一种 | | **诊断前注入** | 命中时把已记录的解法放进上下文 —— 约 80 token | | **人可编辑** | `ERRORS.md` 就是普通 Markdown;你手写的解法立刻生效 | | **强制脱敏** | 密钥、邮箱、绝对路径在落盘之前就被替换掉 | | **靠 git 跨设备** | 追加式块干净合并;计数器留本机,永不冲突 | | **可审计的账本** | `err_stats` 报告命中数、注入量与估算节省的 token | | **对压缩友好** | 事实在磁盘上、按需注入,不躺在对话历史里 | | **完全本地** | 匹配、存储、查询不调用任何模型,也不碰网络 | | **上限即设计** | 每一次注入都有硬上限,上限本身就是规格的一部分 | ## 它怎么工作 ``` 1. 采集 agent/request-error · agent/error · tools/result · 非零退出 只观察 —— 永不抛错,永不接管重试 │ ▼ 2. 指纹 normalize:去 ANSI;ts / pid / port / line / uuid / tmp → 占位符 绝对路径 → ,保留文件名 signature = sha256(category + "\0" + message)[0:12] │ ▼ 3. 匹配 精确指纹 → Jaccard 模糊(≥ 0.72)→ 短消息 同 code 兜底 → 都不中则分配新编号 │ ┌─────────────┴─────────────┐ 命中│ │未命中 ▼ ▼ 4. 注入 每条 ≤ 120 token 5. 记录 向 ERRORS.md 追加块 每步 ≤ 1、每回合 ≤ 3、同编号 ≤ 2 分配下一编号(只增不改) ▲ ▲ └─────────────┬─────────────┘ │ 6. 解决 模型或用户通过 `err_record` 写解法(唯一入口) 解决检测 → 一次性提示 → 状态转 `fixed` ``` 难的是第 3 步,不是第 2 步。规范化一条消息是机械的;判定两条看起来不一样的消息**是同一回事**,才是知识库要么帮上忙、要么悄悄用错误解法污染上下文的分水岭。匹配分三层跑 —— 精确指纹、模糊 token 相似度、消息太短时的同 code 兜底 —— 近似命中会在注入文本里被标注为「近似」,而不是当成确定结论蒙混过去。 **有一条规则撑住了整个设计**:`ERRORS.md` 是唯一真源。`errors.index.json` 是由它派生的缓存,可删可重建;`state.json` 存本机计数器,丢了也无所谓。三者永不互写对方的事实。 ### 什么才配拿一个编号 不是每个失败都值得占用一个编号。瞬时失败只计数,反复出现才升级 —— 限流不是知识。 | 来源 | 钩子 | 记成什么 | 立即编号? | | -------------------- | --------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------- | | LLM API 失败 | `agent/request-error`(waterfall)| `code` + 规范化后的 message | 永久性 code:`AUTH`、`QUOTA`、`INVALID_REQUEST`、`CONTEXT_OVERFLOW`、`NO_ADAPTER`、`UNKNOWN` → ✅ 立即 | | 回合级异常 | `agent/error`(emit) | `error.message` / `code` / `name` | ✅ 立即(`unknown` 时安全字符串化) | | 工具失败 | `tools/result`(emit) | `exec.name` + `result.error.message` + `result.error.info.code` | ✅ 立即(`result.isError`) | | 命令非零退出 | `tools/result` 内容嗅探 | 匹配 `\[exit code: (\d+)\]` 且 N ≠ 0 | ✅ 立即,除非关掉 `captureExitCodes` | | 瞬时 LLM 失败 | `agent/request-error` | `RATE_LIMIT`、`SERVER`、`TIMEOUT`、`TRANSPORT`、`EMPTY_RESPONSE` | ❌ 只计数 —— 同一会话内累计到 `transientThreshold`(默认 5)才升级为条目 | `agent/request-error` 上的监听器必须 `await next()` 并原样返回下游结果。它只观察,永不接管恢复。 ## 知识库落在哪里 `kbDir` 按三级顺序解析: | # | 条件 | 解析结果 | | - | --------------------------------------------------------------------------- | -------------------------------------------------------------- | | 1 | 配置了 `kbDir` | 相对路径 → 基于**插件包根**;绝对路径 → 原样。设备本地差异只写在这里 | | 2 | `kbDir` 为空,插件包根可写且**不在 `node_modules` 内** | `<插件根>/errors/` | | 3 | 否则 —— npm 全局安装、包目录只读、磁盘不可写 | `$DSH_HOME/errkb/` | 启动时插件会用一行日志打印**解析后的真实路径**,所以位置永远不是猜的;`err_stats` 也会打印它。 开发期是 `link:` 安装,所以知识库就是本仓库里的 `errors/`,随 GitHub 一起走。clone 到任何机器、任何盘符,路径自动跟着走;**文档里永远不出现绝对路径**。 这个目录里有什么: | 文件 | 角色 | 怎么对待它 | | ------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------- | | `ERRORS.md` | **唯一真源** —— 编号条目,只追加 | 可手改,提交进 git | | `ERRORS.archive.md` | 超出 `maxEntries` 后归档的老条目,只追加 | 提交进 git | | `errors.index.json` | 派生缓存:指纹 → 编号 | 可删,会从 `ERRORS.md` 重建;建议 gitignore | | `state.json` | 本机状态:命中数、`lastSeen`、`nextId`、环境指纹 | 可删;**本机文件,绝不提交** | | `.machine.json` | 设备标识 `deviceSlug`(如 `DESKTOP-A`),用于条目里的设备字段 | 可删可重建;设备本地文件,同样别进 git | | `.lock` | 瞬时写锁,写完即删 | 忽略它 | 给知识库目录一份够用的 `.gitignore`: ```gitignore state.json errors.index.json .machine.json .lock ``` 设计文档明确点名了 `state.json` 与 `errors.index.json`;`.machine.json` 与 `.lock` 出于同样的理由(设备本地 / 瞬时)也不该进版本库。 这个分工就是跨设备的全部故事:知识靠往 `ERRORS.md` 追加来合并,计数器留在产生它的那台机器上,两者永不打架。至于哪些还没建,见[开发路线图](#开发路线图)。 ## ERRORS.md 的格式 编号从 `E-0001` 起,由 `idPrefix`(默认 `E-`)与 `idWidth`(默认 `4`)拼出,并且**只增不改 —— 编号一旦写下就永不变更**,连合并冲突也不例外。 单条目样例。机器字段装在 HTML 注释里,渲染后干净,手工编辑也安全: ````md ## E-0007 · [tool:pwsh] EPERM: operation not permitted, rename - 指纹: `3f2a1c9d0b71` - 分类: `tool / pwsh` - 首次: 2026-09-14 09:12 · 最近: 2026-09-14 15:40 · 命中: 5 - 触发: `pnpm install` 在中文路径下写 `node_modules` 时被占用 - 原始信息: ```text EPERM: operation not permitted, rename '\node_modules\.pnpm\' ``` - 解法: 关闭占用该目录的编辑器/杀软实时扫描后重跑 `pnpm install`;仍失败则改用 `pnpm install --config.node-linker=hoisted`。 - 状态: `fixed` - 备注: ```` **解析规则**:以 `^## (E-\d+) ·` 切块;块内 `` 提供机器字段;`- 解法:` 取到下一个 `- ` 字段为止。**你对文档的手工修改优先于索引** —— 索引只是缓存,所以你在任意 Markdown 阅读器里补写的解法,下一次命中就能用上。 **条目状态**:`open`、`fixed`、`wontfix`。把条目设为 `wontfix`(或标记为误判)会让它永久退出自动注入,但仍然计数 —— 这样一条坏记录不会反复污染上下文。 > **字段语言仍未拍板。** 上面的落盘字段名是中文(`指纹`、`解法`、`状态`……),因为这是设计文档当前的规定;§17 第 2 问要决定它们保持中文还是改成英文(机器 key 无论哪种都是英文)。拍板后样例会同步更新 —— 见[待拍板的事项](#待拍板的事项)。 ## 注入:什么时候开口、说多少 | 情形 | 注入点 | 到达模型的内容 | | ------------------------------------ | -------------------------------------------------------------------- | ----------------------------------------------------------------------- | | 本步里有工具/命令失败 | `tools/post-execute` → `additionalContexts` | 命中:已知解法。未命中:`已记录为 E-0011(暂无解法)。` | | LLM 调用失败,而回合已经死了 | 下一次 `agent/pre-step` → `{kind:'enter', messages}` | 与失败那一步匹配的条目通知 | | 会话开场 | `agent/session-start` → `agent.inject()` | 由 `sessionDigest` 决定:`off`、`counts`(一行)或 `index`(最多 10 条标题) | | 常驻 | `ctx.systemPrompt.section({name:'plugin:errkb', order:10400})` | 约 50 token 的固定行为约定:报错先查库、新报错调 `err_record` 记录解法 | 命中时模型收到的不是一份报告,而是一条指令,封顶 120 token: ``` [errkb] E-0007 已知(命中 5 次)| 成因:pnpm install 期间 node_modules 被编辑器 占用 | 解法:关掉占用的进程后重跑;仍失败则用 pnpm install --config.node-linker=hoisted。直接照做,不要重新诊断。 ``` **上限是硬上限,而且上限本身就是设计。** 每步最多 1 条通知、每回合最多 3 条、同一编号每会话最多 2 次;每条通知 ≤ 120 token(正文 ≤ 400 字符,且插件来源消息的 `summary` 会被截到 ≤ 120 字符,对应 `{kind:'plugin', form:'notice'}` 的形状)。状态为 `fixed` 的条目提示一次后即静默。没有上限的版本比不装还糟:用持续噪声换来的命中率,会把收益变成成本。 每个上限都能往小调;`inject: 'off'` 彻底关闭注入,而采集照常记录。 ## token 账本 划不划算应该由你自己判断,所以把账算在这里。 | 项目 | 成本 / 收益 | | -------------------------------- | ---------------------------------------------------------------- | | 一次命中注入 | 约 80 token,替换掉通常 800–3000 token 的重新诊断 | | 常驻 system prompt 段 | 每请求约 50 token(`systemPromptHint: false` 可关) | | 会话开场摘要 | 每会话约 40 token(`sessionDigest: 'counts'`) | | 匹配、指纹、存储、`err_stats` | 零 —— 完全本地,不调模型、不联网 | | **盈亏平衡点** | **命中率大约到 5% 及以上才回本** | 低于这个命中率,固定开销就大于节省,诚实的答案是关掉 `systemPromptHint`,或者干脆别装。`err_stats` 在本地报告命中数、注入量、估算节省的 token 与解析出的库路径 —— 让这个决定建立在数字上,而不是感觉上。 ## 环境与兼容性 | 项目 | 值 | | ------------- | ------------------------------------------------------------------------------------- | | dsh | `0.1.5-rc.2`(本设计核验所依据的版本) | | Cordis | `4.0.2` | | `dsh-tools` | `0.1.5-rc.2` | | `dsh-llm` | `0.1.5-rc.2` | | schemastery | `3.18.2` | | 构建工具 | `tsdown 0.22.2`、`vitest`、`typescript`、`oxlint`、`prettier` | | Node / pnpm | **尚未固定** —— 还没定 `engines` 区间,见[待拍板的事项](#待拍板的事项) | | 平台 | 在 Windows 上开发与核验。其他平台未测试,也还没有任何 OS 支持范围的声明 | 下面这些扩展点是**对着本机安装实际核验过的**,不是照着公开文档推测的 —— 文件与行号引用见设计文档 §2: `agent/request-error` · `agent/error` · `tools/result` · `tools/post-execute` · `agent/pre-step` · `agent/session-start` · `agent.inject()` · `ctx.systemPrompt.section()` · `defineTool` + `ctx.tools.register()` · `{kind:'plugin', plugin, form:'notice', summary}` 消息形状。 `dsh` 目前是 rc 版本。在区间固定下来之前,请把每一次 `dsh` 升级都当成可能的破坏性变更。 ## 安装 > **什么都没发布,插件也还没装进任何地方。** 前两条命令从 T01–T04 起可用 —— > `pnpm install` 与 `pnpm build` 都退出 0 并产出 `lib/index.js` —— 但把包加进 > profile 要到 T17 才验证,而且装进去也没有行为可观察。 ```sh cd pnpm install pnpm build dsh plugin --profile web add . # 相对路径,基于当前目录锚定 ``` 然后**重启** `dsh web`。`dsh.profile.bundles` 的变化不在 `patchReload: live` 的监视范围内,所以是必须重启,不是可选。 声明了 `dsh.bundle.patch` 的包会被自动并入 `dsh.profile.bundles` —— 不需要手工改 profile 配置。又因为开发期是 `link:` 安装,知识库会落在本仓库的 `errors/` 里 —— 这正是「靠 git 跨设备」能成立的原因。详见[知识库落在哪里](#知识库落在哪里)。 ## 用法 > **尚未实现。** 现在没有工具可调。下面的形状是规格里的接口,摆出来是为了 > 让设计可以被评判,不是因为它能跑。 问 agent 一个报错,或者直接调工具: ```json { "query": "EPERM: operation not permitted, rename", "full": false } ``` [注入](#注入什么时候开口说多少)里那一段就是整个产品。它替换掉的是一次本来要花 800–3000 token 的诊断。 ### 参数 | 工具 | 参数 | 默认 / 说明 | 返回 | | ------------ | ----------------------------------------------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------- | | `err_lookup` | `query`(原文、指纹或编号)、`full?` | `full: false` 时不返回原始信息,省 token | 命中的条目;未命中则 `null` + 最接近的 3 条 | | `err_record` | `id?`、`message?`、`fix?`、`status?`、`note?`、`category?` | `id` 与 `message` 二选一;`status` 取 `fixed`/`wontfix`/`open` | 编号,以及是否新建 | | `err_list` | `cat?`、`status?`、`limit?` | `limit` 默认 20 | 编号、标题、命中数 —— 不含正文 | | `err_forget` | `id`、`reason?` | 移入 `ERRORS.archive.md` 留痕,不真删 | 条目是否已归档 | | `err_stats` | `scope?` | `scope` 取 `session` 或 `all` | 条数、命中率、估算节省的 token、解析出的库路径 | 五个工具的输出统一走 `output.schema` + `render`,保证模型侧收到的是受控纯文本。 ## 配置 ### 这些配置写在哪 配置写在 profile patch 里,不是另开一个配置文件: ```yaml # cordis.patch.yml - insert: - id: err-kb name: dsh-errkb config: fuzzyThreshold: 0.72 inject: hit-only ``` ### 配置项 | 配置项 | 默认 | 用途 | | -------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------- | | `kbDir` | `''` | 空则自动解析:配置 → 插件根 `errors/` → `$DSH_HOME/errkb/`([详见](#知识库落在哪里))。相对路径基于插件包根 | | `idPrefix` | `'E-'` | 编号前缀(设计文档 §8 定义;§10 的配置表里没有它) | | `idWidth` | `4` | 编号的补零位宽 | | `capture` | `['tool','command','llm','agent']` | 采集开关,逐个可关 | | `captureExitCodes` | `true` | 是否记录非零退出的命令 | | `transientThreshold` | `5` | 瞬时 LLM 错误升级为编号条目所需的次数 | | `fuzzyThreshold` | `0.72` | 模糊匹配所需的相似度(取值区间 0.5–1.0) | | `captureFix` | `'prompt-once'` | `prompt-once` 或 `off`:新条目是否一次性提示模型补写解法 | | `inject` | `'hit-only'` | `hit-only`、`always` 或 `off` | | `sessionDigest` | `'counts'` | 会话开场摘要:`off`、`counts` 或 `index`(最多 10 条) | | `systemPromptHint` | `true` | 是否注入那 50 token 的行为约定段 | | `providers` | `['*']` | 限定只记录哪些 provider(可填 `deepseek-official`) | | `share` | `'public'` | 脱敏强度 —— `public` 或 `private`([详见](#隐私与脱敏)) | | `maxEntries` | `200` | 超出后归档到 `ERRORS.archive.md` | | `maxSampleChars` | `500` | 原始样本的存储上限 | | `exportDir` | `''` | 可选的设备本地导出目录;空则关闭(例如某台机器上的 Obsidian 路径) | ## 隐私与脱敏 这个插件做的每件事都是本地的:不联网、无遥测,也不会让任何模型去做分类、匹配或摘要。离开这台机器的,只有你自己提交上去的东西。 脱敏是**强制的,而且发生在落盘之前,不是分享之前** —— 未脱敏的原文根本不会到磁盘,只存在于内存里,活到算出指纹为止。落盘前会被替换掉的内容: - 凭据与请求头:`sk-*`、`Bearer *`、`api[_-]?key=*`、`token=*`、`authorization:`; - 长 base64 串、32 位以上十六进制串、邮箱地址; - 原始 `requestId`; - 当 `share: 'public'`(默认)时额外:绝对路径压成 ``,原始样本封顶 500 字符。 `share: 'private'` 会保留项目内相对路径,自查更方便,但前提是这份文件只留在你自己的机器上。 **设计假设知识库会被 git 跟踪**,以便跨设备同步,脱敏是兜底。你要是不愿意,把 `share` 改成 `'private'`,并把 `ERRORS.md`、`ERRORS.archive.md` 加进 `.gitignore` 即可。 目前还没有 `SECURITY.md`,也没有安全策略 —— 在有之前,任何未脱敏原文落进文档的情况都请当 bug 报出来。见[待拍板的事项](#待拍板的事项)。 ## 失败模式与安全 插件自己的失误,绝不能变成 agent 的问题。下面每一条风险,在设计文档 §13 里都有一条对应的实现约束。 | 风险 | 约束 | | ------------------------------------------ | -------------------------------------------------------------------------------------------------------- | | 监听器抛错 → 回合直接被关闭 | 所有监听器包 `try/catch`;失败降级为「本次不记录」,在 `state.json` 累计并节流上报 | | 抢走 `dsh-llm-retry` 的恢复权 | `agent/request-error` 必须 `await next()` 并原样返回 —— 只观察,不接管 | | 写文件阻塞回合 | 本地写 + 3 次重试 + 500 ms 超时;超时则跳过该次记录,永不抛错 | | 多进程同写(web + headless 并发) | `.lock` 文件(`wx` 打开,10 s 过期可抢占)+ 临时文件 + `rename` 原子替换 | | 磁盘只读、权限失败 | 自动回退 `$DSH_HOME/errkb/`;再失败则本次静默跳过 | | 文档被写坏 | 严格解析;解析失败时原文件另存为 `ERRORS.corrupt-<时间戳>.md`,新条目只做追加 —— **损坏的文档永不回写** | | 密钥/隐私泄进公开仓库 | 落盘前强制脱敏;`share: 'public'` 时连 `requestId` 都不保留 | | 条目无限膨胀拖慢上下文 | `maxEntries` 归档 + 每条 400 字符封顶 + 注入去重与每步/每回合/每会话上限 | **它永远不会做的事**:接管重试、把异常抛进回合、回写损坏的文档、让一次写入阻塞回合、把任何东西发出这台机器。 ## 已知局限 **它不接管重试,这是刻意的。** `dsh-llm-retry` 拥有重试所有权,且每次重试都会再次计费。`agent/request-error` 的监听器必须 `await next()` 并原样返回结果。一个在这里「热心地」做恢复的插件,等于在和重试拥有者的正确性契约打架。 **模糊匹配可能把两个只是长得像的报错并成一个。** 0.72 的相似度下,近似命中终究是没命中。缓解手段是社会性的而非算法性的:近似命中会在注入文本里标注为「近似」,且任何条目都能被标为误判,永久退出注入。 **知识库的质量等于写进去的东西的质量。** 一条没有解法的条目,注入时也没有解法 —— 那就是花 token 说了一句废话。这正是新条目会一次性提示补写解法、以及 `err_stats` 会报告有多少 open 条目在空耗的原因。 **它可能变成净亏损。** 命中率低于大约 5% 时,固定开销就超过节省;上限的存在只是让亏损小一点,不是让它消失。见 [token 账本](#token-账本)。 **知识只靠 git 传播,这是唯一通道。** 没有云服务,没有远程共享。两台设备靠合并追加式 Markdown 收敛,可靠,但要手动。 **所有项目共用一个库。** 条目里带 project 字段用于区分来源,但还没有按项目隔离。 **没有 GUI 面板。** 目前否掉了。设计文档把它记为发布后的候选项。 ## 怎么确认它真的在工作 ### 验收标准 设计文档 §1 用 7 条标准定义「做成了」,每条都配了验证方式: | # | 标准 | 怎么验证 | | -- | ----------------------------------------------------------------- | --------------------------------------------------------------------- | | S1 | 报错自动入库并获得唯一编号(`E-0001` 起) | 制造一次失败工具调用 → `ERRORS.md` 出现新编号块 | | S2 | 同种报错**不重复编号** —— 路径、行号、PID、时间戳变化也算同种 | 换临时目录、换行号重跑 → 命中计数 +1,编号不变 | | S3 | 命中时向模型注入 ≤120 token 的已知解法 | 会话里出现 `plugin` 来源的 notice 行,模型不再重新诊断 | | S4 | 新报错在解决后被写入「解法」字段 | 模型调用 `err_record`,或收到一次性提示后补写 | | S5 | 插件自身的任何失败都**不会**中断回合或影响重试 | 单测注入异常 → 监听器吞掉,`agent/request-error` 仍返回下游结果 | | S6 | 文档可被人和模型双向编辑,机器索引可重建 | 手工改 `ERRORS.md` 的解法 → 命中时读到改后文本 | | S7 | 跨设备安全:无绝对路径、无密钥 | 全文检索 `D:\`、`sk-`、`Bearer` 均为 0 命中 | ### 手工验收 1. `dsh --profile web --dump-config` 能看到 `err-kb` 条目(证明 bundle 层生效)。 2. 触发一次必然失败的命令(例如访问不存在的盘符)→ 出现 `## E-0001`。 3. 换临时目录或行号重跑同类失败 → 仍是 `E-0001`,命中数 +1。 4. 会话里出现 plugin notice 行,模型**不**再重新诊断。 5. `err_stats` 打印解析出的库路径、命中率与估算节省。 ## 开发路线图 设计已定稿,工作被拆成 **18 个 task**,归入 7 个里程碑。每个 task 的产出与验收标准见下方[完整任务分解](#完整任务分解)。 | 状态 | Tasks | | ---- | -------------------------------------------------------------------- | | ✅ | T01–T04 —— 工程骨架:package、tsconfig、tsdown、vitest、bundle patch | | 🔜 | T05–T09 —— 纯本地层:paths、signature、redact、store、match | | 🔜 | T10–T11 —— 采集层:分类,以及前两个钩子 | | 🔜 | T12–T14 —— 注入层:通知文本、四个注入点、解决检测 | | 🔜 | T15 —— 五个工具 | | 🔜 | T16–T17 —— LLM 失败接入,以及安装进 web profile | | 🔜 | T18 —— 可选:Obsidian 导出 | T01–T04 已勾选并合并。其余仍开放;T18 随时可以砍掉,不影响主线。 ### 阶段对照 设计文档按 P0–P7 阶段规划(§15),任务清单把同一批工作编成 T01–T18。两者这样对齐: | 阶段 | Tasks | 完成标志 | | ------------ | ------- | --------------------------------------------------------------------- | | P0 | —— | 设计文档已写完,且**你已补齐 §17** —— §17 仍未补齐 | | P1 | T01–T04 | ✅ `pnpm typecheck` 通过 —— 本地与 CI 均已验证 | | P2 | T05–T09 | 单测全绿,覆盖率达标 | | P3 | T10–T11 | 一条必然失败的命令产出 `E-0001` | | P4 | T12–T14 | 重复的失败被自动注入,模型不再重新诊断 | | P5 | T15 | 模型可调 `err_lookup` 与 `err_record` | | P6 | T16–T17 | `--dump-config` 可见该条目;云端与本地报错各记一条 | | P7(可选) | T18 | 导出文件可读 | ### 没有任何 task 覆盖的部分 有两个缺口,与其藏在勾选框后面,不如明说: - **发布没有任何 task。** 状态表里写着「发布到 npm」,§17 第 1 问也承认 npm 名是否被占用尚未核实,但 T01–T18 里没有一条覆盖名字核查或发布;CI 以仓库基础设施的形式单独落地了(见[目前完成到哪一步](#目前完成到哪一步))。 - **`err_export` 从 README 的任务清单里掉了。** 设计文档 §15 的 P7 在 Obsidian 导出之外还提到 `err_export` 单文件 JSON 备份,而 T18 只写了 Obsidian 导出。要么把工具找回来,要么把阶段描述改窄。 ### 完整任务分解 | # | Task | 产出 | 验收 | | ----- | -------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------ | | ☑ T01 | 初始化 `package.json` | 包名、入口、bundle patch、peer 依赖 | `pnpm install` 无错 | | ☑ T02 | 配置 tsconfig 与 tsdown | `tsconfig.json`、`tsdown.config.ts` | `pnpm build` 产出 `lib/index.js` | | ☑ T03 | 配置 vitest 与覆盖率门槛 | `vitest.config.ts`、`tests/` | `pnpm test` 跑通 | | ☑ T04 | bundle patch 与空 `apply` | 插件入口:`name`、`inject`、`Config`、`apply` | `pnpm typecheck` 通过 | | ☐ T05 | 解析库路径 | `src/paths.ts` —— 三级解析、启动日志 | 三种情形各返回预期路径 | | ☐ T06 | 规范化与指纹 | `src/signature.ts` | 路径、行号、PID、时间戳、UUID 变化 → 指纹不变;不同报错 → 指纹不同 | | ☐ T07 | 脱敏 | `src/redact.ts` | 断言输出中 0 命中 | | ☐ T08 | 文档存储 | `src/store.ts` —— 解析、渲染、追加、归档、锁、原子写 | 写读往返一致;手改的解法能读回;并发 50 次记录产出 50 个唯一编号 | | ☐ T09 | 匹配 | `src/match.ts` —— 精确、模糊、code 兜底、误判兜底 | 边界值 0.71 / 0.72 / 0.73 符合规格 | | ☐ T10 | 分类与噪声抑制 | `src/capture.ts` | 瞬时错误达阈值前不编号;关闭采集则零写入 | | ☐ T11 | 接前两个钩子 | `agent/error`、`tools/result` 监听器 | 一条必然失败的命令产出 `E-0001` | | ☐ T12 | 生成通知 | `src/inject.ts` —— 模板、上限、去重 | 上限成立;source 形状与 summary 长度精确 | | ☐ T13 | 接四个注入点 | `tools/post-execute`、`agent/pre-step`、`agent/session-start`、system prompt | 重复的失败被自动注入,模型不再重新诊断 | | ☐ T14 | 解决检测 | `src/resolve-detect.ts` | `fixed` 条目提示一次后静默 | | ☐ T15 | 五个工具 | `src/tools.ts` | 模型可调 `err_lookup` 与 `err_record` | | ☐ T16 | 监听 `agent/request-error` | 监听器,以及 LLM 失败分类 | 返回值与下游结果对象同一 | | ☐ T17 | 装进 web profile | `dsh plugin --profile web add .` | `--dump-config` 可见该条目;云端与本地报错各记一条 | | ☐ T18 | 可选:Obsidian 导出 | `exportDir` 导出 | 导出文件可读 | ## 开发 骨架已就位(T01–T04),下面这些命令都能跑。`build`、`typecheck`、`lint`、`test`、`format:check` 正是 CI 在每次 PR 上执行的五步: ```sh pnpm install pnpm build # tsdown → lib/ pnpm test # vitest pnpm typecheck # tsc --noEmit pnpm lint # oxlint pnpm format # prettier --write . pnpm format:check # prettier --check .(CI 跑的就是这条) ``` ### 测试计划 测试计划在设计文档 §14。覆盖率门槛:statements 与 lines ≥ 99。计划分七组: 1. `signature` —— 路径、行号、PID、时间戳、UUID 变化后指纹不变;不同报错指纹不同;空串、纯 ANSI、超长串不崩。 2. `match` —— 精确命中、Jaccard 边界(0.71 / 0.72 / 0.73)、短消息 code 兜底、误判条目永不注入。 3. `redact` —— `sk-`、`Bearer`、`api_key=`、长 hex、邮箱、绝对路径全部被替换;断言输出中 0 命中。 4. `store` —— 写入/解析往返一致;手改的解法能读回;损坏文件走「另存 + 只追加」;归档阈值触发;编号单调递增;**并发 50 次记录产出 50 个唯一编号,且文档仍可完整解析**。 5. `capture` —— 四类载荷分类正确;瞬时限流达阈值前不编号;采集开关全关时零写入。 6. `inject` —— 文本长度上限、每步 ≤1 条、每回合 ≤3 条、source 形状 `{kind:'plugin',plugin:'err-kb',form:'notice',summary}` 且 `summary` ≤120 字符。 7. `plugin` —— 用假 ctx 校验监听器已注册、异常被吞、`agent/request-error` 返回值与下游结果完全一致(对象同一性断言)。 第 4 组里那条并发测试是最要紧的一条,因为它对应的是唯一权威文件被写坏的失败模式。 ## 常见问题排查 下面全部是「设计规定的行为」,不是「实测到的现象」—— 现在还没有东西能跑。 | 症状 | 该查什么 | | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | 「它到底装上了没有?」 | 启动日志那一行会打印解析出的库路径;`dsh --profile web --dump-config` 能看到 `err-kb` 条目。装完必须重启 —— `dsh.profile.bundles` 不热重载 | | 「我的报错去哪了?」 | `err_stats` 会打印解析出的路径。先看[知识库落在哪里](#知识库落在哪里) | | 「什么都没被记录。」 | 查 `capture` 开关、`captureExitCodes`,以及这次失败是不是瞬时类(`RATE_LIMIT`、`SERVER`、`TIMEOUT`、`TRANSPORT`、`EMPTY_RESPONSE`)—— 那些只计数,累计到 `transientThreshold` 才升级;每个计数器都能在 `state.json` 里看到 | | 「写入被静默跳过了。」 | 这正是库目录不可写、拿不到锁、或写入 3 次重试 + 500 ms 后超时时的既定行为。检查库目录权限,以及是否已回退到 `$DSH_HOME/errkb/` | | 「注入太吵了。」 | `inject: 'off'` 彻底关掉注入而采集继续;`systemPromptHint: false` 去掉常驻的 50 token 段;`sessionDigest: 'off'` 去掉开场摘要 | | 「某条条目一直带着错误解法被注入。」 | 用 `err_record` 把它设成 `wontfix`,或标记为误判 —— 它不再被自动注入,但仍在计数 | | 「`ERRORS.md` 看起来坏了。」 | 插件永不回写自己解析不了的文档:原文件会另存为 `ERRORS.corrupt-<时间戳>.md`,新条目只追加在其后。修好另存的那份再放回去 | | 「我手改了某条解法,却没生效。」 | 手工修改优先于索引,本不该发生 —— 检查改动是否落在 `- 解法:` 字段内、且在下一条 `- ` 字段之前,然后删掉 `errors.index.json` 强制重建 | | 「我想从头来过。」 | 删 `state.json`(计数器)、删 `errors.index.json`(缓存),两者都会重建。删 `ERRORS.md` 就是删知识 —— 那才是唯一要紧的文件 | | 「两台机器的计数对不上。」 | 预期如此:计数器是本机的,刻意不进 git。共享的只有知识 | ## 卸载 **尚未规定。** 设计文档没有覆盖卸载,也没有对应的 task —— 见[待拍板的事项](#待拍板的事项)。 目前确定的事实是:安装插件会往 `dsh.profile.bundles` 里加一条(通过 bundle patch),所以干净的卸载就是移除那条并重启 `dsh web`。知识库不过是一个目录里的若干文件,插件对它只做追加;插件不会删除它,所以要备份或删除请自己动手。 ## 待拍板的事项 **这些不补齐,什么都不会开始。** 设计文档现在停在 P0,§17 里放着 8 个问题与建议默认值,回答它们就是 P1 的闸门(见[阶段对照](#阶段对照))。答案写在 §17 的「你的回答」列里,或者随便写在 [`docs/设计说明书.md`](docs/设计说明书.md) 的 §19 批注区。 | # | 问题 | 建议默认值 | 状态 | | - | ------------------------------------------------- | ------------------------------------------------------------------- | ---- | | 1 | 包名 / 插件 id / 工具前缀 / 编号前缀 | `dsh-errkb` / `err-kb` / `err_` / `E-` —— **npm 名是否被占用未核实** | 未定 | | 2 | 文档字段名用什么语言 | 字段名中文 + 机器 key 英文 | 未定 | | 3 | 接受「计数器不进 git、只同步知识」 | 接受 | 未定 | | 4 | `errors/` 是否直接提交进 GitHub 仓库 | 是,以强制脱敏作为兜底 | 未定 | | 5 | 复用范围 | 所有项目共用一个库,用条目里的 project 字段区分来源 | 未定 | | 6 | `captureExitCodes` 默认开还是关 | 开 —— 命令失败是最常见的复用场景 | 未定 | | 7 | P7 要哪些(GUI 面板 / 自动执行修复命令 / Obsidian 导出) | 先只要 Obsidian 导出 | 未定 | | 8 | 包根落点 | `` | 未定 | 还有两个缺口**不在** §17 里,但在 README 能不再写「尚未规定」之前,它们同样需要一个答案: - **环境要求**:没有 Node、pnpm、OS 支持范围声明,也没有 `engines` 区间。 - **分发与维护**:没有任何 task 覆盖 npm 发布、npm 名占用核查、CI,以及卸载。 ## 后续想法 记录在设计文档 §18。一条都没排期;T18 随时可以砍掉,不影响主线。 1. **把「回收」做成「预检」(防错 > 记错)** —— 用 `tools/pre-execute` 加一道 guard,认出「已知必败」的命令并连同已记录的解法一起拦下,把一个 3 轮失败循环压成 1 条消息。它与 `dsh-repeat-tool-reminder` 互补:那个管「重复」,这个管「已知必败」。 2. **`err_fix` —— 一键复现修复方案** —— 条目里若记下的是确定性命令,提供 `err_fix ` 经审批后重跑,真正闭环「回收 → 再利用」。需要接 `dsh-user-approval`,所以默认不开。 3. **把错误库同时做成一个 skill** —— 借 `dsh-skill` 的文件系统发现能力把库暴露为 `SKILL.md`,模型按需读取相关段落,而不用一次吃满上下文。 4. **环境指纹 + 解法可信度** —— 每条记录 os / node / pnpm / shell / provider / 模型版本;在其他设备命中且环境指纹不同时,通知里加一句「可能不适用」,避免拿 A 机的解法坑 B 机。 5. **天然对压缩友好** —— `dsh-compaction-tool-result-pruner` 会丢旧工具结果,但这些事实**在磁盘上**、需要时再注入;这是本设计相对「把解法留在对话历史里」的结构性优势。 6. **token 账本可视化** —— 接 `@deepseek-ai/dsh-token-meter` / `dsh-session-stats`,让 `err_stats` 输出「注入 0.9k / 机制上省掉的下限 8k」。没有数字,阈值就无从调起。 7. **内置种子条目** —— 把 `CONTEXT_OVERFLOW`、`NO_ADAPTER`、Windows 中文路径 EPERM 这类「必踩一次」的坑预置成条目,让全新安装第一天就有价值。 8. **团队共享** —— 因为文档进 git,把 `errors/` 放进团队仓库就等于一个共享踩坑库;再加一层 `err_promote`(本地条目提升为公共条目)就能做知识沉淀流程。 9. **Web GUI 面板** —— 被否掉那个选项的完整版:第三方包可以声明 `dsh.client`(`platform: 'web'`)+ 客户端模块,做列表视图、命中曲线和设置页。代价是要重建 Web 产物,建议 P6 跑稳后再评估。 10. **反向利用** —— `err_list --status open` 就是一份现成的「这个项目当前还没解决的坑」,可以直接当 issue 草稿或新人上手文档。 ## 参与与反馈 现在没有代码可贡献 —— 这反而是对设计提异议最便宜的时刻。 - **设计文档是唯一真源。** 如果本 README 和 [`docs/设计说明书.md`](docs/设计说明书.md) 冲突,以设计文档为准(并且本 README 有 bug 值得报)。 - **意见写进文档。** §17 是一张留了空「你的回答」列的表,§19 是一块批注区,都是给人直接写进去的。 - **两份 README 必须同步。** `README.md` 是英文,`README.zh-CN.md` 是中文,而它们已经漂移过一次。改其中一份,就要在同一次改动里改另一份。 - **仓库的语言约定**:`README.md` 以及代码、注释、提交信息、文档用英文;`README.zh-CN.md` 与设计文档用中文(设计文档用中文是刻意的选择)。 ## 许可证 [MIT](LICENSE) --- DeepSeek Harness 的第三方插件,与 DeepSeek 官方无关。