dsh-wiki-notes
Read-only retrieval for a folder of Markdown notes, exposed as one tool call.
Built for DeepSeek Harness. Instead of a 5-step loop — read the overview, read the index, shell out to a search script, open the page, chase its sources — wiki_recall returns the matching section's full text plus a page-level citation in one call.
中文文档从下面开始。实测:73 页的库,旧流程单次 ≈ 18.7k 字符,新流程 ≈ 2.5k。
设计从哪来
库结构来自 Karpathy 的 llm-wiki.md:与其每次提问都从原始文档重新检索(知识不累积),不如让 LLM 增量维护一个持久的 wiki。本插件只加速其中的 Query 一段。
| gist 里的概念 | 本插件默认位置 |
|---|
| Raw sources(不可变) | 原始资料-raw/ |
| The wiki(LLM 维护) | 知识层-wiki/ |
| The schema | 库根 AGENTS.md |
index.md + log.md | 同名同格式 |
库该长什么样
一个目录 + 一堆 .md 就能用。 更细的约定见 docs/库结构.md:frontmatter 七字段(英文写法 title/type/tags/aliases/status/created/updated/sources 也认)、index.md 摘要目录、log.md 时间线、总览-overview.md,满足得越多检索越准。可跑的样例在 examples/sample-wiki/。
路径指到上一层(比如 LLM-Wiki/,里面还有 原始资料-raw/ 和 知识层-wiki/)也没关系:插件会认 index.md,自动下钻到知识层,并在返回里注明它实际读的是哪一层。
装
在 DSH 里说一句即可:「帮我装一个 DSH 插件,目录是 <绝对路径>」。或自己发:
plugin_manager { action: "install_bundle", target: "<绝对路径>" }
返回 application: applied 就生效,不用重启。但改插件自己的代码要重启 DSH:lib/*.js(宿主半)和 lib/client.js(设置页)都一样,已载入的模块不会因为关开插件而重新求值。
配
设置 →「知识库」,填路径 → 测试 → 保存。四层优先级:
| 优先级 | 来源 |
|---|
| 1 | 插件 config 的 root |
| 2 | 环境变量 DSH_WIKI_ROOT |
| 3 | wiki-notes.config.json(设置页写这里) |
| 4 | lib/config.js 的 DEFAULT_WIKI_ROOT(默认空) |
没配时工具会直接告诉你「还没配置知识库路径」,不会静默扫空目录。
用
| mode | 干什么 | 参数 |
|---|
search(默认) | 关键词 AND 检索 | query、limit(5,≤12)、sectionChars(900,≤3000) |
page | 按页名/别名取整页 | name、pageChars(6000) |
list | 按类型/标签/状态列目录 | type、tags、status、limit(40,≤200) |
overview | 总览 + 规模行 + 最近日志 + 健康计数 | pageChars(4000)、logLimit(5,≤20) |
lint | 库健康检查:孤儿页、断链名字、缺字段、别名冲突、久未更新、无出链、草稿、index 与磁盘不一致;并和上次体检对比,报出新增与已修 | limit(8,≤40)、staleDays(120) |
draft | 把好答案回写成新页的落盘骨架(目录、frontmatter、index 行、log 条),不写文件 | title、type、tags、summary、sources |
help | 用法与状态说明:七个 mode 各返回什么、status 各取值怎么读、参数默认值 | 无 |
{ "mode": "search", "query": "上下文 预算", "limit": 5 }
每条命中带 kind(note / raw-summary / source)、sources(来源数,证据强度的唯一可靠代理)、status、gist、section(命中小节正文,不是 90 字片段)、citation(← [[页名]] §小节 → 路径)、brokenLinks(指向不存在页的双链 = 「库里没依据」的硬信号),以及 updated 后面的距今天数(超过 180 天标「可能已过时」——三年前的结论和上周的长得一样,模型需要这个信号)。overview 额外给三项健康信号:index 有磁盘无、磁盘有 index 无、跨目录重名。
模型怎么知道该查库:插件会从你库里现有的 标签 算出主题词,注入一行(约 50 token,例如「本地知识库主题:上下文工程、ai、agent…」)。不带页数 —— 页数一变就会打掉提示词的前缀缓存,而它对「该不该查」没有帮助。换库自动变,所以问题沾这些主题时它自己会去查,不用你提醒。设置页里可以关掉(不占上下文)、换成自己的整段、或在末尾追加一句,改完下一回合生效。
主题词怎么挑:跳过 总览/index 这类结构页,跳过 数据/工具/meta 这类空泛标签,默认只留出现 ≥2 次的标签(设置页可改回 ≥1)。全被滤掉时退回不过滤的前五 —— 小库不至于一条提示都没有。
limit 默认 5,一次约 2.5–3k token。嫌多可以传 3 省四成,但默认留 5 —— 少一次往返比省四成更值钱。
设置页
设置 →「知识库」。状态栏圆点表示实时同步是否生效,三个数字是页面 / 资料 / 红链页,下面是路径卡片和 测试 · 保存并生效 · 重新扫描。一次没配过时,卡片会变成三步引导。
浏览器半侧走同源 HTTP /api/wiki-notes/*(state / search / check / save / rescan),不碰 RPC。
检索面板
设置页里有一张「试一试检索」的卡,右侧栏另有一个「知识库」页签 —— 同一份组件,compact 控制密度。用途只有一个:让人自己看见检索依据,不必等模型说它查到了什么。
输入关键词回车,上面一行是「命中 7 页(显示前 5)· 参与打分 95 页 · 36 ms · 库 95 页」;紧跟一行是查询诊断:实际用的词 后面逐个列出这次真正参与打分的词,一页都没沾到的词会标成「没沾到」,末尾写清这次是「全词都要(AND)」还是「命中任一(OR)」。中文不分词,写整句时这一行通常只剩一个词 —— 这是最直观的提示:换词,而不是加词。
每条命中占一行,左边是名次、页名、类型,右边是一条按「相对第一名」缩放的分数条和总分。多类型命中会按类型分组(概念(3)、资料(2)…)。展开后有:
- 页名、类型、总分,以及「只命中部分词」的标记(该条没命中全部查询词);
- 命中理由:每个查询词一条徽章,主词加粗、明细灰字,例如
上下文 · 页名 120 · 别名 50 · 正文 ×7 21,未得分的词显示为「未命中」。悬停徽章有各通道的权重说明;
- 该条命中的小节标题与正文(正文里出现过的查询词带淡黄底纹),末尾附 citation,右侧一个复制按钮 —— 把「哪句话支撑了这条」直接抄进对话里;
- 查询降级成「命中任一」时,计数行旁边会黄字提示。
键盘:输入框防抖 350ms(够 2 个字符才查),Enter 立刻查,Esc 清空,↑/↓ 在结果里移动高亮并展开对应条目;任意位置按 / 或 Ctrl/⌘ + K 直接跳到输入框。查询进行中列表上方显示「检索中…」。输入框右边有「复制词」—— 查出来的说法常常要拿去别处再查一遍,省得重敲;citation 那行的复制按钮同理(键盘走过去会看到焦点框)。
覆盖统计在设置页的状态卡下面:发现 102 个文件 → 读到 95 页 / 677 个小节,后面是总体积、跳过 / 读不了 / 超大未读 / 读取失败的计数(非 0 时标黄)、最新修改时间、本次扫描耗时,以及这次实际扫的目录(自动下钻过时它和配置里的路径不是同一个)。
没查之前不是一片空白:显示库的规模、同步开关,以及库里出现最多的几个标签 —— 点一下直接拿它当查询词,省得先想「该查什么」。查不到时也顺带报覆盖:参与检索 95 页 / 677 个小节 · 跳过 7 ——「没命中」和「那几页根本没读」是两回事,面板得能分清。discovered = pages + unreadable + skipped 在 tests/api.mjs 里当恒等式断言,这几个数不是凑出来的。
面板是只读的:不写库、不改配置、不进模型上下文。
explain 是对既有检索的解释,不是第二套检索。 引擎里分数只有一个实现 —— termScore(page, token) 就是 termBreakdown(page, token).score,理由和排序分数出自同一份明细,所以面板不可能显示「一套理由、另一套排序」;tests/explain.mjs 对同一批查询比对开关前后的 name|score|allTerms 逐条相同,还核对了「每个词的明细加起来 === 该条总分」。explain 也不在工具 schema 里:模型触发不到它,wiki_recall 的输出体积一个 token 都不变。
工具输出里新增两个短字段,是给模型看的而不是给面板看的:matchMode(and/or)和 missedTerms(一个词都没沾到的那些)—— 模型看到「这个词没用上」才可能换词重查。它们会被渲染成一行:没沾到:量子纠缠(这些词在库里一页都没命中,换同义词或干脆去掉);没有漏词时这行不出现,正常路径不多花一个 token。而「每个词命中多少页」(termPages) 只在 explain 时出现:那是面板的事,模型用不上。
这条踩过一次:字段先加进了 run() 的返回值,但模型拿到的是 renderValue() 渲染出的文本 —— 不写进渲染,模型就等于没拿到。tests/status.mjs 现在有两条断言看着它(有漏词要出现、没漏词不许出现)。
侧栏是可选能力:ctx.inject(['sidebarRightTabs'], …) 运行时按需取,服务不在就静默跳过,设置页照常可用;dsh.client.inject 刻意不钉侧栏模块(钉了会把「侧栏在不在」变成整个客户端包的加载前提)。tests/contract.mjs 守着这两条。
接口:GET /api/wiki-notes/search?q=上下文 预算&limit=5,q 截到 200 字,limit 夹在 1–12,缺 q 返回 400。
实时同步
fs.watch(root, { recursive: true }),.md 新增 / 改 / 删 → 400ms 防抖 → 作废缓存。监听失败自动退回 30 秒 TTL 缓存。recursive 在 Linux 需 Node 20.13+,这也是 engines 的下限。
检索质量
基线是 v3:14 条 query / 68 个槽位,比 top5 集合,Recall@5 = 100%,当回归硬门禁(口径、判据与历史快照见 docs/metrics.md)。单条查询 0–2ms,扫 95 页约 76ms。
评分刻意朴素:标题全等 120 / 标题含 60 / 别名含 50 / 标签含 25 / 正文词频 ×3(封顶 36)/ index 摘要只在其它通道全 0 时补 8。三处经验细节:摘要分只补漏不抢位、同分用码点序(拼音序不可复现)、选节排除清单类与元讨论类小节。
上下游(可选)
已知局限
- 中文不分词,靠空格切分;连写词靠
别名 补。
- 页名本身是查询词时,正文很少重复该词,选节可能偏。
- 每次调用全量扫描;70–100 页无所谓,再大一个数量级要上增量索引。
- 重启后第一次扫描是冷盘读:95 页实测 5,217ms(此后 29ms)。插件启动就预热一次,所以这段延迟通常落在你还没提问的时候;但第一条查询正好赶上也确实要等。
- 面板没打开时
/、Ctrl/⌘ + K 不会有反应:快捷键绑在面板上,而宿主的 sidebarRightTabs 只提供 register(),没有「打开 / 激活页签」的接口,插件没法从外面把面板叫出来。
- 只读。不写库、不改页、不修红链。
目录 / 开发
lib/corpus.js 扫描 + frontmatter / 小节 / 双链解析
lib/engine.js 评分、选节、七种模式(纯函数)
lib/config.js 库路径解析与配置读写
lib/index.js 插件入口:工具、渲染、设置页 API、目录监听
lib/client.js 设置页 + 检索面板 + 右侧栏页签(手写 bundle,无构建)
bin/cli.mjs 命令行,不装插件也能跑
tools/probe.mjs 检索质量探针:hit@1 / hit@3 / Recall@5,追加趋势 CSV
tools/preview.mjs 把面板渲染成静态页(真组件 + 真 CSS),改样式不必重启 DSH
docs/库结构.md 库结构约定(这份才是一手文档)
docs/superpowers/specs/ 检索面板的设计稿
examples/ 可跑的最小样例库
tests/ smoke / status / lint / explain / api / help / contract / watch / client bundle
node --run test # 跑样例库,不需要本地知识库
node bin/cli.mjs search 上下文 --root=examples/sample-wiki
WIKI_ROOT="/path/your/wiki" node tests/smoke.mjs # 拿自己的库跑通就行(不校验答案)
node --run probe -- --root "/path/your/wiki" # 攒一行检索质量趋势
node tools/preview.mjs # 出 preview/panel.html,浏览器直接看面板
node tools/preview.mjs --diag # 同上,并量一遍宽度 / 截断 / 溢出 / 对比度
发版:改 version → node --run test 全绿 → npm pack --dry-run 看一眼打包内容(files 只含 lib/bin/docs/examples/README.md/LICENSE/cordis.patch.yml,preview/ 与 tools/ 不进包)→ npm publish。本机 npm 常是未登录状态(npm whoami 报 need auth),发布前先在浏览器里确认 token。
改样式的两条经验:
.wn-hit-row 那道 span 不能省。<summary> 自带 display 规则,某些浏览器会把 display:flex 顶掉,行就塌成内容宽度、分数贴在标题右边 —— 所以行布局放在 summary 里那层 span 上,tests/client.mjs 守着它。
- 小字别用
--wn-faint(这组 token 在浅色底上约 3.4:1,低于 WCAG AA 的 4.5),小字一律 --wn-muted(约 5:1),层级靠字号和字重拉开,不靠把字调淡。
--diag 遍历样式表时,别用「有 cssRules 就往下钻」的写法:现代 Chrome 的 CSSStyleRule 自己也带一个空的 cssRules(CSS 嵌套),会把你正要找的那条规则跳过,于是明明 display=flex 却报「NOT FOUND」。先判 rule.cssRules.length。
License
MIT