🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

边做边学知识库 Dayi-Z/dsh-learn-wiki

DeepSeek Harnessv0.3.1
Dayi-Z48 天前有提交已提供中文文档扫描:含敏感能力

卡住时自动联网补料,蒸馏沉淀后下次直接命中

30.8/100
综合分
1
GitHub 星标
204
npm 周下载
—/5
用户评分

数据截至 2026/9/18 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add dsh-learn-wiki
npm 源 · dsh-learn-wiki 的发布者账号已校验与本仓库一致

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:仅索引
是什么[目录]—
dsh 原生插件 · search
暂无时间戳·判据 →
装得上吗[实装验证]✓
静态安装检查通过
未做真实安装
L4 · 真实安装等级说明 →
数据截至 2026/10/25·判据 →
安全吗[自动扫描]—
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
数据截至 2026/11/04·判据 →
还在维护吗[仓库]—
更新放缓:最近一次提交在 48 天前
暂无时间戳·判据 →
图例:✓ 通过! 需注意— 未判定 / 无数据· 「未判定」= 本站尚无数据,不代表插件有问题
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →

可信度证据

安装兼容性检查✓ 自动检查通过
展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✓npm 包dsh-learn-wiki @ 0.3.1
✓Node 引擎要求 >=20 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/12 08:16:10

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 67 处,展示前 60 条)
  • 读取本地文件
    index.js:389
    try { fileCfg = JSON.parse(await readFile(cfgPath, 'utf8')) } catch { }
  • 写入本地文件
    index.js:397
    await writeFile(cfgPath, JSON.stringify(fileCfg, null, 2) + '\n', 'utf8')
  • 删除本地文件
    index.js:416
    try { await unlink(page.path) } catch { }
  • 读取本地文件
    index.js:554
    try { fileCfg = JSON.parse(await readFile(cfgPath, 'utf8')) } catch { }
  • 写入本地文件
    index.js:565
    await writeFile(cfgPath, JSON.stringify(fileCfg, null, 2) + '\n', 'utf8')
  • 读取本地文件
    index.js:606
    const st = await stat(s.file)
  • 发起网络请求
    client/client.js:451
    return fetch(url, { headers: { accept: 'application/json' } }).then(readJson).catch(fail)
  • 发起网络请求
    client/client.js:454
    return fetch(path, {
  • 读取本地文件
    lib/config.js:273
    const raw = await readFile(join(root, 'wiki.config.json'), 'utf8')
  • 读取本地文件
    lib/wiki.js:123
    try { entries = await readdir(dir, { withFileTypes: true }) } catch { return out }
  • 读取本地文件
    lib/wiki.js:150
    try { text = await readFile(f, 'utf8') } catch (e) { errors.push({ file: f, error: String(e.message) }); continue }
  • 写入本地文件
    lib/wiki.js:180
    await mkdir(dir, { recursive: true })
  • 写入本地文件
    lib/wiki.js:188
    await writeFile(file, fm + '\n\n' + String(page.body ?? '').trim() + '\n', 'utf8')
  • 读取本地文件
    lib/wiki.js:203
    try { names = await readdir(dir) } catch { return [] }
  • 读取本地文件
    lib/wiki.js:208
    try { text = await readFile(join(dir, f), 'utf8') } catch { continue }
  • 读取本地文件
    lib/wiki.js:234
    try { ents = await readdir(join(repoRoot, sub), { withFileTypes: true }) } catch { return 0 }
  • 读取本地文件
    lib/wiki.js:238
    try { n += (await readdir(join(repoRoot, sub, e.name))).filter(isPage).length } catch { }
  • 写入本地文件
    lib/wiki.js:403
    await mkdir(join(repoRoot, 'staged'), { recursive: true })
  • 写入本地文件
    lib/wiki.js:404
    await rename(r.abs, dest)
  • 删除本地文件
    lib/wiki.js:421
    await unlink(r.abs)
  • 删除本地文件
    lib/wiki.js:434
    try { await rmdir(dir) } catch { }
  • 写入本地文件
    lib/wiki.js:439
    await mkdir(join(repoRoot, d), { recursive: true })
  • 读取本地文件
    lib/acquire.js:27
    const raw = await readFile(join(repoRoot, GAPS), 'utf8')
  • 写入本地文件
    lib/acquire.js:33
    await mkdir(join(repoRoot, 'gaps'), { recursive: true })
  • 写入本地文件
    lib/acquire.js:34
    await writeFile(join(repoRoot, GAPS), gaps.map(g => JSON.stringify(g)).join('\n') + (gaps.length ? '\n' : ''), 'utf8')
  • 发起网络请求
    lib/acquire.js:166
    const r = await ctx.web.fetch({ url })
  • 发起网络请求
    lib/acquire.js:190
    const res = await fetch(url, {
  • 读取本地文件
    lib/acquire.js:251
    const j = JSON.parse(await readFile(join(repoRoot, CACHE_FILE), 'utf8'))
  • 写入本地文件
    lib/acquire.js:271
    await mkdir(join(repoRoot, '.index'), { recursive: true })
  • 写入本地文件
    lib/acquire.js:272
    await writeFile(join(repoRoot, CACHE_FILE), JSON.stringify(cache), 'utf8')
  • 读取本地文件
    lib/session-store.js:137
    const text = decodeSession(readFileSync(file))
  • 读取本地文件
    lib/session-store.js:195
    const buf = readFileSync(file)
  • 读取本地文件
    lib/session-store.js:235
    const text = decodeSession(readFileSync(file))
  • 读取本地文件
    lib/session-store.js:250
    try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
  • 读取本地文件
    lib/session-store.js:257
    if (!existsSync(root)) return []
  • 删除本地文件递归
    lib/session-remote.js:52
    try { if (dir) rmSync(dir, { recursive: true, force: true }) } catch { }
  • 写入本地文件
    lib/session-remote.js:60
    writeFileSync(reqPath, JSON.stringify(req), 'utf8')
  • 执行外部命令来自配置/变量
    lib/session-remote.js:63
    child = fork(CHILD, [reqPath, outPath], {
  • 读取本地文件
    lib/session-remote.js:82
    if (!existsSync(outPath)) {
  • 读取本地文件
    lib/session-remote.js:89
    const parsed = JSON.parse(readFileSync(outPath, 'utf8'))
  • 写入本地文件
    lib/log.js:37
    try { appendFileSync(file, line + '\n', 'utf8') } catch { }
  • 写入本地文件
    lib/log.js:41
    appendFile(file, line + '\n', 'utf8').catch(() => {})
  • 写入本地文件
    lib/struggle.js:355
    await appendFile(struggleLogPath(wikiRoot), JSON.stringify(entry) + '\n', 'utf8')
  • 读取本地文件
    lib/struggle.js:361
    const raw = await readFile(struggleLogPath(wikiRoot), 'utf8')
  • 读取本地文件
    lib/capabilities.js:28
    const raw = await readFile(join(wikiRoot, SNAP_FILE), 'utf8')
  • 写入本地文件
    lib/capabilities.js:36
    await mkdir(join(wikiRoot, '.index'), { recursive: true })
  • 写入本地文件
    lib/capabilities.js:37
    await writeFile(join(wikiRoot, SNAP_FILE), JSON.stringify({ capturedAt: new Date().toISOString(), items }, null, 1), 'utf8')
  • 读取本地文件
    lib/usage.js:52
    const raw = await readFile(join(repoRoot, FILE), 'utf8')
  • 写入本地文件
    lib/usage.js:61
    await mkdir(repoRoot, { recursive: true })
  • 写入本地文件
    lib/usage.js:62
    await writeFile(join(repoRoot, FILE), JSON.stringify(usage, null, 2) + '\n', 'utf8')
  • 写入本地文件
    lib/merge.js:166
    await mkdir(stagedDir, { recursive: true })
  • 写入本地文件
    lib/merge.js:174
    await writeFile(stagedPath, fm + '\n\n' + String(merged.body).trim() + '\n', 'utf8')
  • 写入本地文件
    lib/merge.js:178
    await mkdir(rejectedDir, { recursive: true })
  • 读取本地文件
    lib/merge.js:182
    try { text = await readFile(proposal.absorbPath, 'utf8') } catch { text = '' }
  • 写入本地文件
    lib/merge.js:192
    await writeFile(dest, withReason, 'utf8')
  • 删除本地文件
    lib/merge.js:193
    try { await unlink(proposal.absorbPath) } catch { }
  • 读取本地文件
    lib/session-index.js:24
    const j = JSON.parse(await readFile(join(wikiRoot, SNAP), 'utf8'))
  • 写入本地文件
    lib/session-index.js:36
    await mkdir(join(wikiRoot, '.index'), { recursive: true })
  • 写入本地文件
    lib/session-index.js:37
    await writeFile(join(wikiRoot, SNAP), JSON.stringify(snap, null, 1), 'utf8')
  • 读取本地文件
    lib/session-index.js:67
    try { st = await stat(s.file) } catch { continue }
扫描于 2026/11/4(1 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

社区实装还没有人打卡 · 装过的第一个就是你
依赖的 DSH / Cordis 模块
@deepseek-ai/dsh-llm@deepseek-ai/dsh-tools
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 卡住时联网补料
  • 蒸馏落staged
  • 两段式commit进召回

边做边学知识库:CRAG 式纠错检索(L1 Markdown wiki + L2 Hindsight)。agent 反复撞同一堵墙时后台限流联网补料,蒸馏落 staged,两段式 commit 才进召回。DSH 插件。

README

由 DeepSeek 最新模型翻译生成
dsh-learn-wiki DSH 的「边做边学」知识库插件:工作前自动检索 → 卡住时后台限流联网补料 → 蒸馏落暂存 → 两段式 commit 升入知识库 → 下次自动命中。 它把 CRAG(Corrective RAG)的纠错回路从"单次问答"搬到 agent 循环上:知识库检索不到就自己去学,学到的东西沉淀下来,下次就不用再学。 它解决什么问题 绝大多数"记忆插件"是静态管道——检索不到就检索不到,不会去学。结果就是同一个知识缺口在项目里反复踩,每次都要重新上网查。 本插件的增量只有一个,但是关键的那个:"反复撞同一堵墙"是一个可观测的信号,会触发补料并沉淀。 ★ 触发器是挣扎,不是"检索未命中"。原设计确实用未命中,但实测那个信号太廉价——任何新话题都会未命中,于是系统为聊到的每件新鲜事都去联网。未命中仍可用(gapTrigger: 'miss' | 'both'),只是默认关。
展开完整文档(README 剩余 1 节)
dsh-learn-wiki

DSH 的「边做边学」知识库插件:工作前自动检索 → 卡住时后台限流联网补料 → 蒸馏落暂存 → 两段式 commit 升入知识库 → 下次自动命中。

它把 CRAG(Corrective RAG)的纠错回路从"单次问答"搬到 agent 循环上:知识库检索不到就自己去学,学到的东西沉淀下来,下次就不用再学。

它解决什么问题

绝大多数"记忆插件"是静态管道——检索不到就检索不到,不会去学。结果就是同一个知识缺口在项目里反复踩,每次都要重新上网查。

本插件的增量只有一个,但是关键的那个:"反复撞同一堵墙"是一个可观测的信号,会触发补料并沉淀。

★ 触发器是挣扎,不是"检索未命中"。原设计确实用未命中,但实测那个信号太廉价——任何新话题都会未命中,于是系统为聊到的每件新鲜事都去联网。未命中仍可用(gapTrigger: 'miss' | 'both'),只是默认关。

三层知识架构

| 层 | 载体 | 角色 | 谁写 |
| L1 | 独立 Markdown 仓库 | 精选知识页,强命中直接注入 | 本插件蒸馏 + 人工编辑 |
| L2 | Hindsight | 原始情景记忆,语义召回 | Hindsight(由既有 hindsight 插件负责) |
| L3 | 网络 | 兜底补料,限流 | 本插件经 ctx.web |

不重复实现 L2。 既有 hindsight 插件已经负责每轮情景记忆召回;本插件只管 L1、未命中判定和 L3 补料,避免两套注入互相打架。

三条设计铁律

1. 自动的不阻塞,阻塞的必须显式。
自动注入走 agent/pre-step(每轮第一步);自动补料走 turn/end 之后的后台 worker;真正"现在就要这个事实"时由模型显式调 wiki_recall —— 那一次阻塞天经地义。
中途阻塞 5–30 秒联网会拖垮轮次、打断工具链,而同一个缺口通常后面几轮还会出现——所以"后台学、下轮用"才是"边做边学"的正确形态。

2. staged/ 永不参与召回。
自动产出必须经 commit 才升入 L1。投毒面被限制在暂存区。

3. 无 sources 不 commit。
每条知识必须可溯源到 URL / 文件。commitReadiness() 强制这条。

安装

从 GitHub 安装
dsh plugin --profile web add github:Dayi-Z/dsh-learn-wiki

本地开发(直接链接到源码目录)
dsh plugin --profile web add link:D:/Harness/dsh-learn-wiki

重启 DSH 生效

零构建。 插件是纯 JS:host 半是 ESM,client 半是手写 CJS(由 window.__ModuleLoader__ 装载)。
没有打包步骤,所以从源码安装不需要 allowBuilds 授权,也不需要预先构建产物。

依赖只声明为 peerDependencies(@deepseek-ai/dsh-tools / dsh-llm)——它们由宿主提供,不要装第二份:同一进程里存在两份实现会让 import 解析到插件自带的那份。

配置

配置来源(后者覆盖前者):代码内默认值 → /wiki.config.json → apply(ctx, config) 第二参数。

刻意不使用 Cordis 的 Config schema:rc 阶段要能容忍未知键,严格 schema 会把用户多写的一个键变成加载失败。

关键项(完整列表见 lib/config.js):

| 键 | 默认 | 说明 |
| wikiRoot | D:\\Harness\\dsh-wiki | L1 仓库根目录 |
| autoContext | true | 旋钮 A:每轮第一步自动注入 |
| autoAcquire | true | 旋钮 B2:后台补料(非阻塞)。触发条件由 gapTrigger 决定 |
| gapTrigger | 'struggle' | 什么触发补料:struggle(默认)\| miss \| both \| off |
| gapTriggerSignals | ['repeat-failure','recurring-error'] | 哪些挣扎信号适合联网(只认带可搜错误文本的那些) |
| hitThreshold | 0.20 | score ≥ 此值 → hit(注入正文) |
| weakThreshold | 0.15 | score ≥ 此值 → weak(只注入标题索引);低于 → miss。见下方"阈值是量出来的" |
| injectOncePerSession | true | 内容不变则只注入一次(KV cache 友好) |
| maxAcquisitionsPerRun | 2 | 单次后台补料最多处理的缺口数 |
| webMaxResults | 5 | 每次联网取多少条结果 |

阈值是量出来的,不是拍的

打分依赖语料规模,所以语料显著增长后必须重跑标定。这条规则有可执行的形状:

node scripts/calibrate-real.mjs # 整体:正例/负例分布与 gap
node scripts/calibrate-thresholds.mjs # 逐条 + 扫阈值,回答"该不该调"

最近一次重标定(2026-09-16,66 页语料 / 13 正例 / 5 负例)抓到的不是阈值问题:

| 负例 | 修前 | 修后 | 根因 |
| 「Rust 的 borrow checker 报错怎么绕过」 | 0.8364 | 0.0524 | 检索到了自己 —— 命中的那一页正文里逐字引用了这条标定查询 |
| 「如何配置 kubernetes sidecar 注入策略」 | 0.223 | 0.1493 | kubernetes 等词整个语料里都不存在,而 sidecar+注入+策略 恰好同页 |

两条修法都不是调阈值:前者给"关于本工具自己"的页打 meta 标签、不参与自动注入(显式 wiki_recall 仍查得到);后者给"查询里有语料根本没有的标识符"加折扣。
修完 Gap = +0.013(正例最低 0.1621 / 负例最高 0.1493)—— 薄,所以 verify-core 里留了哨兵断言:超线就红,逼着重标定而不是把上限改高。

给这个知识库写页面时:如果一页讲的是“本插件自己的打分/检索/标定”,请打上 meta 标签。
否则它会跟标定查询共享同一批词,把标定本身污染掉。

工具

| 工具 | 作用 |
| wiki_recall | 显式检索 L1,返回三分桶判定与页面正文 |
| wiki_learn | 显式沉淀一条知识(默认落 staged) |
| wiki_harvest | 从一段对话里提炼(当前会话,或带 session 提历史会话;默认落 staged,无 commit 参数) |
| wiki_sessions | 读历史会话:list / brief 接手简报 / walls 跨会话反复撞的墙 / show 单个会话详情 |
| wiki_review | 查看 staged 暂存队列与 gaps 缺口队列 |
| wiki_commit | staged → pages(唯一升入 L1 的闸门) |

历史会话:这个仓库最被低估的资产

本地躺着几十个会话、完整的对话与工具调用记录。在此之前没有任何路径读它——预注入、挣扎检测、wiki_harvest 全都只看得到“现在”。于是“上次我是怎么解决的” 这个问题在整个系统里没有位置,只能靠人去记。

wiki_sessions 补上这一块。它的 brief 是接手简报:

node -e "..." # 或直接让 agent 调 wiki_sessions { action: "brief" }

实测输出(本仓库真实数据):

=== 跨会话反复撞的墙(前 8)=== [8 会话 / 46 次] Error: edit requires reading "" first — read the file, then retry [5 会话 / 18 次] Error: old_string was not found in "" [4 会话 / 20 次] Error: platform github unavailable (tried: github): GitHub returned no results [2 会话 / 18 次] Error: invalid arguments: missing required property "file_path"

“这个项目反复卡在哪”以前没有任何地方看得见,现在一句话就出来了。

它怎么工作(lib/session-store.js + lib/session-digest.js + lib/session-index.js):

- 会话文件是多帧 zstd 拼接(每次追加写一帧)。★ zlib.createZstdDecompress() 不能用——它和 gzip 不一样,遇到第二帧就停(实测只解出第一帧的 170 字符)。
必须自己按魔数 28 B5 2F FD 切帧,逐个解。
- ★ 文件名里带格式版本:session.jsonl.zstd(v0)或 session.v3.jsonl.zstd(v3,宿主 0.1.5-rc.2 起)。判定用整名锚定的正则,不靠 includes——目录里躺着 session.jsonl.zstd.bak-20260915、...frame-broken-bak 这类备份,用 includes 会把已经判定损坏的文件重新塞回历史。版本本身从事件流头部的 version 读,文件名只是副本;副本与真源不一致时信真源,读不到就给 null(0 是 v0,“不知道”不是 v0)。
- 列会话只读第一帧(头部里有 id / cwd / 创建时间 / 委托深度)。全量解码一个会话实测要解 11M 字符,列表动作用不起。
- 索引带缓存与预算:摘要按文件 (size, mtime) 缓存进 /.index/sessions.json,每次调用最多新摘 8 个,没摘完的如实报在 notYetIndexed 里——假装全都索引了,等于让人以为历史就这么多。
- 按会话头里的 cwd 字段过滤,不靠目录名反推(目录名是被 mangle 过的)。
而且过滤成空时会退回全部并说明——“查不到”和“没有”必须分得开(见知识页 scoped-registry-empty-result:这个项目已经栽过一次)。

摘要有三条判据,每条都有实测依据

| 判据 | 为什么 |
| 内层派发有两个事件名(tool/code-dispatch / tool/ptc-dispatch)都要认 | 宿主 0.1.5-rc.2 起 Code Mode 的派发改叫 ptc-dispatch,载荷结构一样。漏掉之后 toolsUsed 只剩 run_code、filesTouched 全空、failures 为 0——摘要会反过来说话(“这次没改过文件也没踩过坑”),而 v3 会话不再写流式 chunk,这些派发事件是唯一线索 |
| 外层 run_code 的失败是内层派发的复述,不重复计数 | 实测 124 次内层失败对应 122 次外层失败,几乎 1:1。两个都记 = 同一堵墙数两次,阈值全部失真。关联是精确的:dispatch.rootCallId === tool/call.callId |
| 退出码标记只对 shell 工具算数 | 759 条带非零退出标记的结果里 51 条来自非 shell 工具——最多的是 job_output(35 条,它返回的就是另一个进程的 stdout)。那些是回显,不是自己失败 |
| 没有可描述症状的失败不成“墙”,但计入 weakFailures | 一个命令以非零退出、输出却全是 PASS 行时,“指纹”就是那堆无关日志(真实出现过的例子:指纹是 ui: /learn-wiki 已注册 … PASS)。既搜不出来也无从推理。但不能静默丢,所以计数照记 |

与 Hermes 生态的 headroom learn 同形:它挖历史会话的失败模式并与“最终成功的那次修正” 做关联,写回 agent 原生的记忆文件。而它内置的 adapter 只有 ClaudeCode / Codex / Gemini —— 没有 DSH。 这一块就是补那个缺口。

学习触发器

| 触发器 | 回答的问题 | 默认 |
| 挣扎(连续失败 / 撞同一堵墙 / 改了还是不通) | 我卡住了 | 开(gapTrigger: 'struggle') |
| 检索未命中 | 我不知道 | 关(需显式设 'miss' 或 'both') |
| 被纠正(用户说"不对") | 你说的不对 | 开,但不联网 —— 答案来自用户,只记证据与日志 |
联网这一段怎么设计的

补料要真的"补到东西",靠的是三件事各自成立,而它们失败的样子完全不同:

| 层 | 判据 | 失败时长什么样 |
| 查询 | 先剥掉指令性尾缀("常见故障原因 解决办法"),空了再用原查询兜底一次 | 搜回一堆泛泛的页面,看起来"有结果但没用" |
| 抓取 | 宿主 ctx.web.fetch 优先(DNS 钉死 + SSRF 防护),直连兜底;结果带路径(host / direct) | 分不清是宿主拦了、站点拦了、还是要浏览器 |
| 抽取 | 配对标签树选正文容器,而不是"整页去标签" | 抓到了,但截断截在导航上,答案没进提示词 |

三条实测依据(都在 verify:web 里钉住):

- 抽取:nodejs.org 的正文与左侧 API 目录同属一个包装层 —— 朴素去标签会把一百多个 API 名字当成正文;改成配对容器后抽出的是文章本体(含 createZstdDecompress 这类答案词)。
fetchMaxChars 也因此从 4000 提到 12000:原来那个预算先被导航吃掉。
- 壳页:GitHub issue 抓回 373KB HTML,剥完只剩 874 字符的表单提示语(正文是 JS 渲染的)。
现在这种页会被标成 shell 并计入统计 —— 它和"抓取失败"是两种故障,修法相反。
- 缓存:同一批 URL 会被反复抓(一次 gap 两次尝试、相似缺口搜出同一批页面),单页实测约 1.4 秒。现在按 URL 缓存抽取后的文本(成功 24h / 失败 10min),命中时不再发请求。

每次补料的取材统计会写进 gap 记录与页面正文,例如: "取材: 2 usable, 3 tried, path host×2/direct×1, 9000 chars"。理由很直接:队列是 "哪些缺口学不动"的唯一台账,而没有这一列时它只能回答"被拒了",答不出为什么 ——是搜不到、抓不到、还是蒸馏器太保守,这三种修法完全相反。
| wiki_harvest | 我们刚刚想清楚了一件事 | 显式 |

★ 默认是挣扎而不是未命中:未命中太廉价(任何新话题都会触发),挣扎稀有、昂贵、且必须当场兑现。这个取舍有实测依据,见下面的两节。

挣扎检测修过两次,两次都是"信号在撒谎"

第一次:失败根本看不见。 判"这次失败了"原来只看 result.isError,而那是 harness 层的语义(工具没找到、参数非法)。实测 15,868 条真实工具结果:

| | 条数 | 说明 |
| isError: true | 1,228 | 全部是 unknown tool "X" 这类接口误用 |
| 正文带 [exit code: N](N≠0) | 588 | 没有一条置了 isError |

也就是说"改了 → 跑检查 → 失败 → 再改"这条最典型的死胡同对检测器完全不可见;两个号称零误报的低噪信号只在模型用错工具接口时响。现在 looksLikeFailure() 把两类都算上,判据刻意保守(只认证据不认语气:正文里出现 "Error:" 但退出码为 0 不算失败)。

第二次:edit-churn 把"努力"当成了"卡住"。 43 条挣扎记录里 35 条是它,全部来自正常迭代(同一个 client.js 改了十几次、每次都跑通);它登记的两条 gap,一条被蒸馏器拒绝、一条沉淀出关于另一个撞名项目的内容。阈值从 4 提到 8 没用——病不在阈值上:"反复修改"是努力,"反复修改并且撞墙"才是死胡同。
现在 struggleEditChurnNeedsFailure(默认 true)要求 churn 与失败共现,信号里带上 failCount 便于事后核账;设成 false 可退回旧行为。

连带修掉一处查询撒谎:symptomQuery() 原来对 edit-churn 硬编码
"反复修改 X 仍不成功 常见原因"——一个只是"改了很多次"的信号,跑到搜索引擎那里声称自己"不成功"。现在只有真的有失败证据时才这么说。

前两个都是关于"我们自己失败"的信号,发现不了第三种,而它往往最值钱。

实测(2026-09-11):一次会话里用户亲口说出的设计规则("位置表达状态是个陷阱")一条都没有被沉淀 —— 因为它不经过任何一个自动触发器。而同一次会话自动闭环产出的,是一页关于另一个撞名项目的内容。信号选错,产出就是反的。

wiki_harvest 补的就是这个入口。它读本会话已经发生的内容(只要人说的话与助手的 text 段,不要注入块、不要 reasoning 草稿),提炼后落 staged,仍需人工固化。

与 Hermes Agent 的 /learn 同形:它也能拿"刚走完的这段对话"当输入(/learn how I just deployed the staging server)。一个有意的差异是我们学的是知识页(事实 + 教训),不是技能(可复用过程)—— 后者要引入一个新物种,召回、证据账、界面全都要分叉,不在本期。

阈值标定(重要)

打分依赖语料规模,阈值必须随语料重新标定,不能跨语料复用:

node scripts/calibrate.mjs

它会用一组标注查询实测分数分布并给出建议阈值。当前默认值来自 7 正例 / 5 负例的实测:正例 0.152–0.541,负例 0.000–0.105。

★ 对真实语料标定(合成语料不够用)

node scripts/calibrate-real.mjs

calibrate.mjs 用的是 7 页写死的合成语料。它标出来的东西在真实语料上会失效——这不是猜测,是实测:那条「已知缺陷」负例在合成语料里得 0.0000,在真实语料里跨过 hitThreshold 被判成 hit,把整页正文注进了提示词。

calibrate-real.mjs 跑在真实的 D:\Harness\dsh-wiki 上,用 13 条正例(每条都指定期望命中的页)与 5 条负例,输出分数分布与两者之间的 Gap。

语料长大之后暴露的真问题:打分不可分,而不是阈值没调好

语料从 15 页涨到 19 页后,那条已知缺陷从 0.2141 涨到 0.2728。对真实语料重跑标定,得到的第一个结论是:

(修之前)正例最低 0.2811 负例最高 0.3737 Gap -0.093

最好的负例比最差的正例还高。 一条「Rust 的 borrow checker 报错怎么绕过」拿到 0.3737,比 13 条真实正例里的 4 条还高。单靠调阈值救不了。

根因是分词器没有停用词概念:中文走字符二元组,而「的」出现在 15/19 页(df/n≈0.79)且 tf 很高,却和内容词被同等对待。那条 Rust 查询真正命中的是
「的」(15)、「报错」(4)、「怎么」(4)、「绕过」(2)——撑起分数的主要是「的」。

修法是标准 IR 做法:出现比例过高的词不携带区分度,不计入覆盖度(maxDfRatio)。
扫了一遍取值:

| maxDfRatio | 正例最低 | 负例最高 | Gap | 正例 top1 |
| 基线(不过滤) | 0.2811 | 0.3737 | −0.093 | 12/13 |
| 0.5 | 0.2811 | 0.3136 | −0.033 | 12/13 |
| 0.2 | 0.2446 | 0.1969 | +0.048 | 12/13 |

0.2 是第一个让 Gap 转正的取值,且正例 top1 正确率与基线完全相同——没有为了分离开而牺牲命中。修完之后 5 条负例没有一条判成 hit,那条已知缺陷从 0.2728 降到 0.1394。

★ 样本只有 19 页 / 18 条查询,间隔 +0.048 是薄的。语料显著增长后必须重扫。
下限取 2 而不是 1:纯比例在小语料上会退化(n=2 时 floor(2×0.2)=0 → 所有词被滤掉 → 永远返回空),而插件冷启动时正是那个状态。

一个踩过的坑

中文没有词边界,本插件用字符二元组分词,于是"协议的分帧"会产生 议的 / 的分 / 帧和 这类跨词边界的噪声二元组。而 idf() 对 df=0 的词返回的是上界(ln(1+(N+0.5)/0.5),N=1 时约 1.386),语料内常见词只有约 0.288 —— 差 5 倍。

结果:这些永远不可能命中的噪声词反而主导了分母,把每个自然语言查询的分数压到接近 0,几乎每轮都判 miss、反复触发联网。修复是给 df=0 的词中性权重(语料内词的平均 IDF),而不是最大 IDF。实测把"Widget 协议的分帧和魔数是什么"从 0.094(误判 miss)拉回 0.32(hit)。

界面

侧边栏底部的 learn-wiki 入口打开一个面板,三个页签:

| 页签 | 回答的问题 |
| 能力 | 71 个工具里哪些在每轮都收费、我该裁哪个;11 个技能的常驻目录成本各是多少 |
| 知识 | 哪些知识被确认过、哪些有反证、哪些被隔离;每条可以就地展开读全文 |
| 补料 | 挣扎信号分布、缺口队列、最近的判定 |

一条贯穿界面的规矩:位置不表达状态

上一版把"裁掉的工具"顶到工具表最前面,于是你点一下方框,这一行就从指针底下消失——连点第二下都做不到。知识页签当时也有同一个病:按"需关注度"排(隔离 > 有嫌疑 > 未确认 > 其余),而这几档全部由证据决定,证据每 8 秒轮询刷新一次,所以你展开一行读正文,下一轮询它就可能换位置。

现在两处都按不随证据变化的键排(工具按 族→名字,知识按 id), "我该先看哪些"改由筛选器回答(已确认 / 未确认 / 有反证 / 已隔离,都带计数)。

乐观更新

勾选工具、固化暂存页都是先动界面再发请求:失败则回滚,并明确写出"这一格已回滚"。
固化那条不会假装完成——它以「固化中…」的标记躺在表里,真值一到自动换成真实分类(靠取差集,不靠"记得在某处删掉它")。

键盘

页签是 role="tab" 且可方向键移动;展开控件是真的 (带 aria-expanded),不是"给 挂个 onClick"——键盘用户 Tab 不到 。
面板是模态的,焦点被圈在里面,Esc 关闭并把焦点还给入口。

界面自检与预览

agent 看不到这个界面(没有浏览器运行时,而且当前模型可能不接受图像输入)。
所以有两个脚本把界面变成可读的东西:

npm run verify:render # 结构级渲染测试(真 React + 自写的原语替身)
npm run ui:probe # ★ 从真实浏览器排版量出文本:列宽/行高/滚动长度/截断
npm run ui:snapshot # 截成 PNG + 写出 HTML(给看得见图的模型或人)
node scripts/ui-probe.mjs --only knowledge --json .snapshots/probe.json

- ui:probe 是给 agent 用的那条路。 它把同一份页面交给无头 Chrome 真排版,再用 CDP 把几何取出来翻成文本:面板/表体矩形、"内容高 vs 可视高 → 要滚几屏"、每列真实像素宽度、哪些单元格的文字被 CSS 截断了。装不了 playwright 也能跑——直接用系统里的 Chrome/Edge,通过 Node 自带的 WebSocket 说 CDP。
- verify:render 抓的是崩溃类 bug。 用真 React 把组件树渲成静态 HTML,断言"不崩 + 分组正确 + 行数与顺序对"。它第一次跑就抓到一个真 bug:工具表族标题的计数用的是 out[out.length - 1].n++,而循环体已经 push 过行了,于是计数加到了行对象上,每个族的标题恒显示 1——界面上一直这么显示着。

两个脚本共用 scripts/lib/ui-harness.mjs,所以"测到的"和"看到的"是同一个东西。
夹具也从那里来:想量真实数据,把 /learn-wiki/api/state 的响应存成 JSON,用 --state 传进来。

⚠️ 原语是替身。 Pill / Button / StateDot / MarkdownText / 图标都是照 dsh-client-ui-primitives 当前实现写的最小等价物(每个都带 data-stub 标记),不是真原语。所以:结构、密度、列宽、行数、截断是可信的;胶囊的圆角、按钮的填充、状态点的形状不代表真实应用。

这件事有代价也有教训。替身图标一开始漏了 width/height,无尺寸的会拿到浏览器默认的 300×150,把 26px 宽的展开列撑到 105px 高——量出来的 "行太胖"完全是替身自己的问题。替身漏掉一个属性,量出来的"问题"就是替身的问题。

自检

node scripts/verify-core.mjs # 解析 / 索引 / 打分 / 三分桶
node scripts/verify-loop.mjs # 端到端闭环(含防投毒与 commit 闸门)
node scripts/verify-plugin.mjs # 插件接线(mock ctx,无需重启 DSH)
node scripts/verify-ui-api.mjs # UI 数据接口(含桌面 app:// 载体的 POST 形态)
node scripts/verify-render.mjs # 组件树结构级渲染
node scripts/verify-subagent-guard.mjs # 子代理不污染长期记忆(两个方向都测)
node scripts/verify-harvest.mjs # 会话提炼:不把自己的注入物当成人说的话

verify-plugin.mjs 用 mock ctx 跑 apply(),能在不重启 DSH 的情况下抓出事件名拼错、工具注册缺 output 声明这类错误——本插件开发中它实际抓到了一个阈值标定 bug。

状态

Phase 1(host 半)与 Phase 2(client 半)均已完成并验证:

- host:自动注入 / 三档判定(hit·weak·miss)/ 挣扎检测 / 后台限流补料 / 使用证据与强化因子
- client:能力(工具+技能,按族折叠)/ 知识(可展开、可筛选)/ 分拣(回收站与已拒绝的恢复/删除)/ 补料 四个页签
- 另外注册了 tool.call.toolview(keyed on wiki_recall):把三档判定与命中页做成对话内卡片,而不是让通用卡片渲染一大坨 JSON。解析不了就回落通用卡片 —— 不画半懂的卡。
- 界面可离线自检与预览(见上),不需要开浏览器、不需要重启 DSH

自检:24 个套件,全部离线可跑(npm run verify:*)。最大的那个是结构级渲染测试——真 React + 自写的原语替身,把组件树渲染成静态 HTML 再断言结构与行序。

已完成(原来的 Phase 3 清单):

- wiki_lint:死链 / 来源失效 / 来源不像指针 / 疑似重复 / 主题重叠 / 过期。只读,且结果里会列出"这次没查什么"(URL 离线验证不了,如实说没查)
- wiki_merge:两页并一页。默认只出提案,apply:true 才写;合并稿落 staged/、被并入的页移进 .rejected/ 并附 > REJECTED: 原因
- 命中率与补料收益度量(wiki_review detail=metrics):注入命中率 + 缺口→暂存→固化→确认 的漏斗 + 知识库利用率
- 技能按 agent 粒度裁剪(默认关)
- 第三、四个触发器:被纠正 / 已是显式的 wiki_harvest
- 工具表按族折叠:ui:probe 实测内容高 4093px ≈ 6.1 屏 → 672px(不需要滚动)
- 宿主兼容性自检(lib/compat.js):启动时把"插件自带 vs 宿主实际"的版本与 8 项宿主 API 摆进日志

还没做:

- 技能表「来源」列偏窄:11 条来源路径里最长的一条被截掉 108px
- 彻底消除"两份实现":本地 link: 安装下插件仍会解析到自己那份 @deepseek-ai/dsh-tools(用户从 npm/GitHub 安装则不会——已改为只声明 peerDependencies)。这是 dev 环境的常态,不是缺陷,但值得知道
预览分享卡片长什么样 ↓Dayi-Z/dsh-learn-wiki
分享 · DeepSeek Harness Hub
Dayi-Z/dsh-learn-wiki

卡住时自动联网补料,蒸馏沉淀后下次直接命中

这个插件是你维护的吗?
作者本人可认领并核对本条目;认领通过后,详情页会显示「作者已认领」标记。
认领条目 →
回到顶部