yangdongzhen590/dsh-knj-obsidian

yangdongzhen590/dsh-knj-obsidian

DSH 内置版 Obsidian 知识库:agent 把对话、文档与网页蒸馏为结构化 .wiki 知识图,支持检索、图谱导出、笔记编辑、增量构建与多库管理。

记忆

dsh-knj-obsidian

DSH(DeepSeek Harness)内简化版 Obsidian:为 AI agent 提供项目级知识库(wiki)的构建能力。agent 通过工具把对话、文档、网页等源材料蒸馏为结构化知识页,落盘到项目根目录的 .wiki/,形成可复用、可维护的知识资产。

当前版本为 v1 构建核心 + v2 检索 + v3 图谱导出 + v4 UI + v5 笔记编辑:写入侧(ingest / capture / lint)已完整;检索侧提供 wiki_query 工具与 wiki-query skill 双通道(见下文「v2:检索」);图谱侧提供 wiki_export 工具导出交互图谱与结构化图数据(见下文「v3:图谱导出」);UI 侧提供右侧边栏"知识库"标签 + 笔记/图谱工作台(见下文「v4:UI」);v5 起笔记工作台支持富渲染、双链原地跳转、源码视图与全文编辑(见下文「v5:笔记编辑」)。

安装方式

需要 Node.js ≥ 20,且已安装 DSH 宿主(提供 dsh CLI 与 web profile)。
# 1. 在插件目录内打包
npm pack

# 2. 在插件目录内执行:安装到 DSH 的 web profile
dsh plugin --profile web add ./dsh-knj-obsidian-2026.8.257.tgz

# 3. 重启 DSH,确认宿主日志无 dsh-knj-obsidian 相关报错

安装后插件在 DSH 启动时自动向 agent 暴露工具(wiki_ingest / wiki_capture / wiki_normalize_tags / wiki_lint / wiki_query / wiki_drill / wiki_read / wiki_export / wiki_mine / wiki_init / wiki_checkpoint / wiki_checkpoints / wiki_search_semantic),并随包分发 5 个 wiki-* skill,无需额外配置。

v1 能力

工具 · 作用 wiki_ingest · 把 agent 提取好的知识页批量写入 wiki。入参 pages(id / title / category / tags / confidence / body)+ source(源材料标识)。同 id 页面自动合并正文(保留 created、更新 updated);传入 contentHash(源内容 SHA-256)且与 manifest 记录一致时整体跳过,实现增量 ingest。 wiki_capture · 把当前讨论快速沉淀为一条知识页(quick 模式,默认写入 references/,confidence=inferred),适合把对话结论即时落盘。 wiki_lint · 健康度检查:孤儿页(入链或出链缺失的页面)、断链([[wikilink]] 指向不存在页面)、缺 frontmatter。返回报告供 agent 自查或人工查看。

.wiki 结构

知识库位于项目根目录的 .wiki/(由 process.cwd() 决定):

.wiki/
├── index.md          # 维护的索引页
├── .manifest.json    # 源材料增量追踪(contentHash → 跳过重复 ingest)
├── _system/          # 会话归档、处理器与进度等内部运行数据(不参与知识检索)
├── concepts/         # 概念页
├── entities/         # 实体页
├── references/       # 参考资料
├── synthesis/        # 综合/跨主题结论
└── projects/         # 项目知识

每页为 Markdown + YAML frontmatter:

---
id: kebab-case-stable-id
title: 页面标题
category: concepts
tags: [tag-a, tag-b]
source: 源材料标识
confidence: extracted|inferred|ambiguous
created: 2026-08-25T00:00:00.000Z
updated: 2026-08-25T00:00:00.000Z
---

markdown 正文(不含 frontmatter)

v2:检索

v2 检索基于同一 .wiki/ 知识库,提供双通道能力,均只读(不创建/修改任何页面):

通道 · 触发方式 · 说明 wiki_query 工具 · agent 自动调用 · 内置于插件,DSH 启动即暴露。agent 被问到「我之前关于 X 踩过什么坑」「我了解 Y 吗」这类既有知识问题时自动调用它检索 .wiki/,再基于候选合成带引用的回答。 wiki-query skill · 独立 skill(fallback) · 随包分发的 skill(wiki-query/SKILL.md + references/retrieval-guide.md)。在工具不可用/受限的环境里,agent 按该 skill 用 grep / glob / read 完成同等分层检索(通道对齐关系见下)。

分层检索策略

wiki_query 与 wiki-query skill 共用同一套从便宜到贵的分层层级(L1–L4),两者对 L1 的语义对齐如下:

  1. L1 — index 快速层:读 .wiki/index.md,匹配查询词所在行。工具 mode=index-only 只做这一步;skill 在 auto 模式下把 L1 当作预热扫描,命中后仍继续 L2 核对更强者(工具 auto 模式不查 index,直接从 L2 开始)。
  2. L2 — 标题 + 标签层:grep 标题与 frontmatter 标签,读命中文件的 title / tags / confidence(命中即停,不升 L3)。
  3. L3 — 正文层:L2 无果时打开正文定位查询词,截取上下文 ≤200 字符作为 snippet。
  4. L4 — 图谱邻居:解析命中页的 [[wikilink]] 出链,取一跳邻居作为关联候选。

通道对应关系:工具 auto 模式 = L2→L3→L4;工具 `mode=index-only` = skill 的 L1 快速层;skill 完整流程 = 工具 auto 模式 + L1 预热扫描。两通道在相同 vault 上对同一查询给出的候选集一致(skill 按上述语义执行时,L1 在 auto 下不产生候选,与工具一致)。

答案由 agent 基于候选页合成,必须带引用(页面路径 + confidence 标记:extracted / inferred / ambiguous)。无匹配时返回「wiki 无匹配」,并建议把相关内容吸收进 wiki。

wiki_query 工具

  • 入参:query(检索词,必填)、mode(auto 分层 / index-only 只查 index)、maxCandidates(默认 10)。
  • 返回:candidates[](page / id / category / title / confidence / snippet / matchedBy)+ strategy + totalPages。

wiki_drill(语义种子 + 一跳双链下钻)与 wiki_read(读整页)

词面分层(L1–L4)对改写式提问天然无力:句子与库内页没有共享字面词,整串/分词匹配都会 0 命中。 wiki_drill 专治这一类,并为「检索到了却没人读原文」补上最后一环:

工具 · 入参 · 返回 wiki_drill · query(模式 A)或 page(模式 B)、limit(种子数,默认 5 / 上限 10)、neighbors(每种子邻居数,默认 8 / 上限 20)、direction(模式 B:out/in/both) · seeds[]:模式 A 把语义(QMD 本地双通道)与词面两路按名次 RRF 融合,每条带 matchedBy 与 sources(哪些通道贡献);每个种子附一跳邻居 neighbors[](direction / broken / degree / via / matchedBy: 'graph-neighbor')。另有 strategy、truncated、message、semantic.status wiki_read · id(必填)、category(同 id 跨分类歧义时用) · body 整页正文 + frontmatter 摘要/tier/confidence + outLinks / inLinks / brokenLinks 清单

推荐流程:wiki_drill({query}) 拿种子 → wiki_read 读最像的那页 → 不满意就 wiki_drill({page: ''}) 继续一跳。

  • 只给一跳:实测本库两跳即覆盖全库 40%–60%,一次性多跳等于把半个库倒进上下文;更深由调用方逐跳迭代(模式 B 毫秒级、不跑嵌入)。
  • 邻居不是命中:邻居的 matchedBy 恒为 graph-neighbor(与 L4 的 graph 区分),属「顺链到达」,证据强度低于直接命中。
  • 降级如实回报:索引陈旧/模型缺失时结果照给,但 message 与 semantic.status 会说明「基于旧索引」「本次仅词面通道」;KNJ_OBSIDIAN_DRILL=off 可整体关闭这两个工具(wiki_query 不受影响),KNJ_OBSIDIAN_SEMANTIC_FALLBACK=off 会一并关掉下钻的语义通道(用户关它的意图就是别加载 300M 模型)。

wiki-query skill(无工具 fallback)

无 dsh 工具的环境里,agent 按 wiki-query/SKILL.md 的流程用 grep / glob / read 完成同等检索,降级路径(无 grep、无 index.md、结果过多)见 references/retrieval-guide.md。skill 只读,发现新知识时路由到 wiki_ingest / wiki_capture。

本地语义检索(wiki_search_semantic,进程内 QMD,严格离线)

除关键词分层检索外,插件还提供本地语义检索工具 wiki_search_semantic:在 DSH 进程内直接使用 @tobilu/qmd 库(不 spawn 命令、不走 MCP、无需单独安装或启动任何 QMD 插件),只用两条不会联网的通道——本地嵌入模型向量检索 + BM25 关键词检索——并在本地做 RRF 融合。

  • 入参:query(必填)、limit(默认 8,硬上限 20)。
  • 返回:status(ready / index-empty / index-stale / model-missing / library-missing)+ results[](id / category / title / snippet / score,score 为融合分数非概率)+ 必要的 message。
  • 页面引用从命中路径推导(displayPath / qmd:// URI),不使用 QMD 的 `docid`(它是内容哈希);只接受七个正式分类下的 .md,_system/、_raw/、wiki-export/ 永远不会成为结果。

严格离线:@tobilu/qmd 的混合检索默认会拉起 1.7B 查询扩展模型与 0.6B 精排模型(默认值为 hf: 云端 URI,缺失时会自动下载),因此插件禁用这两条路径——把它们钉到 ~/.dsh/qmd/models/ 下不存在的本地路径,任何误用只会在本地失败。索引与查询全程零网络。

这一约束必须双写:既写进传给 createStore 的配置,也写进进程环境变量(QMD_EMBED_MODEL / QMD_GENERATE_MODEL / QMD_RERANK_MODEL)。原因是 QMD 的分块路径(chunkDocumentByTokens → getDefaultLlamaCpp() → tokenize())走的是模块级单例,它不读 store 配置、只认环境变量与默认值——只设 config 时,单例会去解析默认的 hf: 云端模型并联网,表现为嵌入阶段无限等待(进程内存不涨、CPU 不动)。这一点已由 semantic-index.test.mjs 的「单例环境」用例锁死。

模型放置(一次性人工步骤,约 320 MB):

# 目录不存在就建;插件不会自动下载,也不会联网
mkdir -Force $env:USERPROFILE\.dsh\qmd\models
# 把模型放到(文件名必须一致):
#   %USERPROFILE%\.dsh\qmd\models\embeddinggemma-300M-Q8_0.gguf
# 可选手动下载:https://hf-mirror.com/ggml-org/embeddinggemma-300M-GGUF/resolve/main/embeddinggemma-300M-Q8_0.gguf

模型缺失时工具如实回报未就绪(给出精确路径与文件名),并且不打开索引库(不 import 库、不建 sqlite、不留句柄);关键词检索 wiki_query 始终可用。语义状态固定在 ~/.dsh/qmd/(models/、index.sqlite、config.yml),索引文件不进 vault。

内网 / 离线部署:@tobilu/qmd 声明为插件的 optional peerDependency(peerDependenciesMeta 标 optional)。DSH profile 默认 autoInstallPeers: false,因此 pnpm 不会自动安装它——这既保证插件本体永远装得上,也避免了它的依赖(node-llama-cpp 及其 14 个 @node-llama-cpp/* 平台包,其中 CUDA/异平台变体常有上百 MB 且在内网拉不到)在每次安装任何插件时被反复尝试下载。

要启用语义检索需显式安装一次:

dsh plugin --profile web add @tobilu/qmd@2.8.3

(该命令会连带安装 node-llama-cpp 等原生依赖;内网需先把这批包镜像进私有仓库——清单与工具在源码仓库的 tools/qmd-offline/,含 README.md、packages.json 与 mirror-qmd.mjs 的 list/pack/verify 命令,不随 npm 包分发。未安装时 wiki_search_semantic 会如实返回 library-missing 并保持关键词检索 wiki_query 完全可用。)

索引新鲜度与「更新索引」

语义索引不会凭空存在,它需要先建一次:

  • 自动:wiki_ingest 落盘后会安排一次 debounce(默认 15 秒)后台刷新,连续的批量入库合并成一次;同一库同时最多一次刷新(单飞)。待嵌入积压超过阈值(默认 500 篇)时不自动跑,而是如实回报「请点更新索引」,避免一次大批入库在后台悄悄占满 CPU 很久。
  • 手动:边栏「语义索引」块里的 「更新索引」 按钮 → POST /api/obsidian-wiki/semantic-update(立刻返回 202,后台执行),状态由 GET /api/obsidian-wiki/semantic-status 轮询(重建期间每 2 秒),块内显示状态、已索引篇数、待嵌入篇数、进行中已用时间与上次结果。
  • 关掉自动刷新(保留手动按钮):环境变量 KNJ_OBSIDIAN_AUTO_REFRESH=off。
  • 模型未就位时状态块直接显示「模型未就位」与精确路径,按钮禁用——不会发一次注定无效的请求。

耗时预期(本机 i7-12700H + RTX 3060,实测):

场景 · 实测 · 说明 真实库 7 篇 · update 44ms + embed 86.7s · 几乎全是一次性冷启动(模型加载 + 后端探测) 合成 300 篇 冷启动 · update 1.7s + embed 96.2s · 冷启动成本与库大小基本无关 合成 300 篇 热态 · embed 11.9s = 0.04s/篇(25 篇/秒) · 稳态吞吐 外推(稳态) · 500 篇 ≈ 0.3 分钟,2000 篇 ≈ 1.3 分钟 · 首次另加约 1.5 分钟冷启动

后端差异很大(tools/bench-embed.mjs,1053 token 文本):CPU 饱和在 51–56 token/秒,Vulkan 3125–3400 token/秒(约 60 倍);本机 auto 会选 Vulkan,纯 CPU 机器的索引耗时应按两个数量级上调,因此务必用可见的手动入口而不是静默后台。注意 QMD_LLAMA_GPU=cuda 在离线环境不可用(会尝试 git clone llama.cpp),保持默认 auto 即可。

v3:图谱导出

v3 图谱基于同一 .wiki/ 知识库构建 wikilink 知识图谱(节点=页面、边=[[wikilink]] 链接),提供 wiki_export 工具导出,只读(不修改任何页面)。

wiki_export 工具

agent 被问到「导出 wiki 图谱」「看看知识库的结构/关联」「生成知识图谱」时自动调用它。入参 format:

format · 产物 · 用途 html(默认) · graph.html · 单文件交互可视化:内联 SVG + 原生 JS 力导向布局,零外部依赖,浏览器可直接打开 json · graph.json · 结构化图数据(节点 / 边 / 孤儿 / 统计),供外部工具(Gephi / Neo4j / 自研分析)使用

产物写入 /wiki-export/(vault = 项目根目录 .wiki/),返回 file / nodeCount / edgeCount。

浏览器打开

导出后直接打开 .wiki/wiki-export/graph.html:

  • 拖拽节点调整布局,滚轮缩放(viewBox),悬停节点显示标题与分类
  • 节点按 category 着色(concepts 蓝 / entities 绿 / references 橙 / synthesis 紫 / projects 灰)
  • 顶部显示 N 节点 · M 边 统计,底部为图例

graph.json 格式

{
  "nodes": [ { "id": "kebab-case-id", "title": "页面标题", "category": "concepts", "confidence": "extracted" } ],
  "edges": [ { "source": "a", "target": "b", "broken": false } ],
  "orphanIds": ["c"],
  "pageCount": 42
}
  • broken: true:出链指向不存在的页面(断链)
  • orphanIds:既无出链也无入链的页面(孤儿)

孤儿 / 断链在图谱中的表现

  • 断链:红色虚线边,末端带红点(指向不存在的页面;仅渲染不参与力学布局,防止幽灵节点漂移)
  • 孤儿:灰色节点(无任何连接)

两种异常直接在图上一眼可辨,配合 wiki_lint 可定位并修复(补链或删页)。

v4:UI

DSH Web 界面右侧边栏新增"知识库"标签(better-sidebar),无需离开对话即可浏览与管理知识库:

  • 浏览:按分类(概念/实体/参考/综合/项目)分组的 vault 树;点击笔记 → 主区域打开"笔记"工作台标签(markdown 渲染 + wikilink 跳转 + frontmatter 信息)
  • 搜索:顶部搜索框,回车检索(复用 v2 分层检索内核),结果带 snippet 与 confidence;可 ← 返回
  • lint 徽标:显示页数与健康度(孤儿/断链/缺 frontmatter 计数),绿/琥珀/红三态
  • 图谱:浏览/图谱切换,内嵌力导向交互图谱(复用 v3 图谱数据;点击节点打开笔记)
  • 空态引导:vault 为空时提示"对 agent 说『把 XX 吸收进 wiki』开始"

后端提供 /api/obsidian-wiki/* 只读端点(pages / page / search / graph / lint),全部复用 v1-v3 内核(VaultStore / retrieve / buildGraph / lintVault),与 agent 工具同源。

v5:笔记编辑

v5 解决笔记工作台的四个体验缺口(富渲染 / 双链导航 / 源码视图 / 编辑保存):

  • 富渲染:markdown 管线升级为 marked 解析(含 wikilink 内联扩展,解析期直接产出锚点 token)+ DOMPurify 白名单净化(表格、引用块、任务列表、外链、分隔线全支持;代码块内的 [[x]] 按字面呈现)
  • 双链原地导航:点击 [[wikilink]] 在当前工作台原地切页,带访问历史栈与「← 返回」按钮;目标不存在时提示断链且不离开当前页
  • 源码视图:笔记页「预览 / 源码」切换;源码态展示磁盘原文(含 frontmatter,逐字节)+ 一键复制
  • 全文编辑:源码态可直接编辑整份文件(含 frontmatter,可改 title/tags)并保存;保存走 POST /api/obsidian-wiki/page
  • 保存安全:服务端校验路径合法性(防穿越)与 frontmatter 一致性(id/category 与目标不符即 422 拒绝,磁盘不动);原子写(临时文件 + rename);写端点仅接受同源请求(Origin/Referer 校验,跨站 403)+ 仅 JSON(415)
  • 保存后联动:边栏树与 lint 徽标经 wiki:pages-changed 事件自动刷新

v6:增量构建

知识库的增量构建从「只能靠对话」升级为 UI 内一键完成:

  • 一键重建索引:边栏「重建索引」→ POST /rebuild-index,从全部页面重生成 index.md(- [[id]] 标题 — 摘要 格式,retriever L1 零迁移兼容)。index.md 是派生工件——重建会覆盖手工注释,想保留的内容请写进页面本身
  • 导入现有 md:边栏路径框(文件或目录)+ 分类选择 → POST /import。递归收集 .md(排除 node_modules/.git/target/dist,≤500 文件、单文件 ≤1MB、深度 ≤12);已有合法 frontmatter 按声明原样入库,缺失的自动补全(id=文件名净化、title=首个标题、source=import:原路径);id 冲突自动加 -2 后缀不覆盖;重导幂等(未变跳过、已变更新);源文件只读
  • lint 详情速修:lint 徽标点击展开面板——断链/孤儿页/缺 frontmatter 逐条可点;断链打开来源页(修链在来源页),其余打开对应页,直接进源码态修
  • 会话蒸馏:随包分发 wiki-distill skill(内嵌 zstd 多帧会话提取器,首次运行落位 /_system/tools/,vault 已有则用 vault 版);入口在边栏「知识蒸馏」分段(选项「近期会话蒸馏」,可选近 3/7/30 天),点击把触发指令预填进当前对话。原始会话归档写入 /_system/dsh-sessions/,不参与知识检索;流程:确认范围(默认近 3 天当前项目)→ 提取 → 按主题蒸馏 → wiki_ingest 入库(contentHash 增量,重复源自动跳过)
  • 新端点安全:两个新写端点沿用 v5 模式(同源 403 / 非 JSON 415);import 对源路径只读,写入落点经 SAFE_ID 净化 + vault 包含性双校验

v7:多 vault 管理

v7 解决「本地有几个库看不出来、没有切换/维护入口」:

  • 当前库身份可见:边栏顶部「📚 库名 + 路径」常驻显示,一眼知道在看哪个库
  • 库列表 + 切换:顶部下拉列出全部库(宿主工作区自动发现 + 显式挂接),附页数;切换即换当前库,边栏树/搜索/图谱与 agent 工具(wiki_ingest / wiki_query / …)全部跟随
  • 新建/挂接/移除:⚙ 面板填目录(或「选目录」走宿主原生目录选择器)+ 显示名 → 新建/挂接;「×」移除仅限显式挂接的库(带确认,绝不删除磁盘文件;工作区/默认种子库只读)
  • 跟工作区走:边栏挂载时按当前工作区自动激活对应库(客户端读取宿主工作区运行时;无该服务时降级为手动切换);服务端启动时读宿主工作区注册表,自动发现各工作区的 .wiki
  • 注册表:/knj-obsidian/vaults.json(可用插件配置 vaultRegistryFile 覆盖),损坏自动重建;vault 根不再写死宿主进程 cwd

新端点(写端点沿用同源 + JSON 校验):

端点 · 说明 GET /api/obsidian-wiki/vaults · 当前库 + 全部库(含只读 pageCount) POST /api/obsidian-wiki/vault/activate · 按目录激活(已注册仅切换,未注册自动挂接) POST /api/obsidian-wiki/vault/switch · 按 id 切换当前库 POST /api/obsidian-wiki/vault/attach · 新建/挂接(目录不存在自动创建 + 脚手架 .wiki) POST /api/obsidian-wiki/vault/remove · 从列表移除显式挂接的库(不删文件)

v8:界面重设计(设计系统 v2)