简体中文 | English
dsh-openwiki
DSH 插件:把 openwiki 的代码库知识库能力搬进 DeepSeek Harness —— 一键生成 / 阅读 / 更新仓库 Wiki 与 Grounded Claims(溯源知识卡片),直接复用 DSH 已配置的模型,无需二次填 Key。
dsh-openwiki 是 DeepSeek Harness(Cordis 全插件架构)的 Web profile 包插件:Host 半部托管 openwiki CLI 运行时并驱动生成任务,Client 半部在 DSH 主界面提供知识库入口、浮动窗口、目录树、文档阅读等交互。核心思路是把 openwiki 当作增量生成引擎(命令行 + 磁盘产物契约),DSH 侧负责 UI、任务编排、模型桥接与触发器。
📑 目录
🚀 安装
前置依赖:Node ≥ 20、openwiki CLI(插件可自动安装)、DSH 已配置模型与凭证。
方式一:npm 包(推荐)
dsh plugin --profile web add dsh-openwiki
方式二:本地源码
# 1. 获取源码到本地任意目录(下文以 <源码目录> 指代)
git clone <仓库地址> <源码目录>
# 2. 构建产物(lib/ 与 client/)
cd <源码目录>
npm run build
# 3. 以本地目录方式安装到 DSH web profile
dsh plugin --profile web add <源码目录>
安装后配置(两种方式相同)
# 编辑 <你的 DSH 用户目录>\profiles\web\cordis.patch.yml,追加:
# - id: dsh-openwiki
# name: 'dsh-openwiki'
# 重启 dsh web
说明:
本地目录方式安装时,
dsh plugin add会在 profile 的node_modules下建立指向<源码目录>的链接(pnpm 目录安装默认行为),改源码 +npm run build后重启 dsh web 即生效
dsh.profile.bundles需含dsh-openwiki(dsh.client manifest:platform web,peerDependency schemastery)
🛠️ 开发与维护
目录结构
<源码目录>/
├── src/host/index.js # Host 权威源码(动态/包共用)
├── src/client/index.js # Client 权威源码
├── scripts/build-host.mjs # → lib/index.js(ESM 入口)
├── scripts/build-client.mjs # → client/client.js(ModuleLoader bundle)
├── lib/ client/ # 构建产物
├── tests/ # Playwright / 离线验证脚本 + screenshots/
└── package.json # dsh.client manifest(platform web)
面向 AI 的注意事项
构建脚本改写:build-host 把
harness.handle换成rpc.handle并注入webServer/settings;build-client 把host.call换成rpc.call。新增 Host handler 用harness.handle(...),新增 Client 交互用host.call(...),构建自动适配readBytes 限制:
fs.readBytes超限会抛错(不是截断)——读文件头用readText+ slicemanifest 缺失是常态:所有 wiki 读取都要走 disk-scan fallback(
readWikiTree/readWikiOverview/readWikiClaims)shim EPERM:Windows 下不要 spawn
.cmd,解析 shim 内脚本路径直接 spawn nodenot found 静默:可选状态文件缺失(
.page-manifest.json/.run.json/.last-update.json)不要console.error,否则轮询刷屏settings 命名空间:
settings.register('openwiki', schema)依赖@deepseek-ai/schemastery,从 profile 动态 import 可能失败(settings namespace skipped)——不依赖它,用内存态 + localStorageclient 服务注册表每次刷新重建:better-sidebar 注册必须自动重注册(autoRegisterSidebar),不能依赖用户点击
openwiki 无 git 触发器:自动更新靠插件轮询
git rev-parse HEAD非 git 仓库无法生成:
startJob前置校验已拦截并报明确错误Markdown 渲染:fence 检测必须
startsWith('```')(见「🧠 关键逻辑」死循环教训)性能:空闲轮询只刷 jobs + overview(轻量),有运行任务才全量
refreshWorkspace;避免每 3s 全量磁盘扫描
✨ 功能一览
📚 知识库入口:DSH 主界面左下角「设置」上方「openwiki知识库」入口(可关闭,
localStorage持久化),点击打开可拖拽/可缩放的浮动窗口;另有「conversation.view」会话视图 Tab 与 better-sidebar 侧边页面两种嵌入形态。工作区列表每次打开知识库/点击「刷新」都实时重新拉取(新增工作区无需刷新页面)
🗂️ 三模式视图:
Open Wiki(仓库 Wiki 文档树)、知识卡片(Grounded Claims 溯源抽点)与知识库(personal 模式个人知识库)三个常驻 Tab,选中高亮;另有「忽略文件」「刷新」按钮;左栏宽度可拖动调整
🧠 知识库(personal 模式):第三个 Tab「知识库」——上传本地文件(文本格式,含文件类型提示;新增/删除文件的行为说明见「生成知识库」按钮下方)生成个人知识库,输出到
<项目>/openwiki-kb/wiki(OPENWIKI_CONFIG_DIR注入),树形阅读复用文档视图;模型凭据单点化:从~/.openwiki/.env注入子进程,项目内不重复配置,openwiki-kb/一条 gitignore 排除全部 personal 产物
📄 文档阅读:预览 / 代码双视图 + 目录分栏(点击「目录」在工具栏下方分出左侧目录面板,独立竖滚动条、sticky 不随正文滚动;点条目平滑滚动到对应标题)


🗂️ 知识卡片:Grounded Claims 溯源抽点(statement + evidence),点「知识卡片」Tab 查看

🔗 应用内链接导航:md 内链(含
/openwiki/...绝对路径)点击后在应用内跳转,绝不把浏览器带走到file://;目录链接(尾/)进入文件夹浏览⚡ 生成任务:生成 / 重新生成(增量更新)/ 取消,实时进度(
已完成 X/Y (Z%),处理中: N,失败: M),断点续跑依赖 openwiki 的.run.json;非 git 仓库明确报错📊 生成状态卡:右侧展示文件数 / 成功 / 失败 / 更新时间 / 文档位置(
openwiki/)与「重新生成」按钮🔌 模型复用:读取 DSH 默认模型(
agentDefaultModel)与凭证,映射为 openwiki 的 provider 配置写入~/.openwiki/.env(openai-compatible / anthropic / gemini / openrouter)🖥️ 运行时托管:自动检测 openwiki CLI(解析 npm shim)、版本检查(registry)、安装 / 升级 / 自检(
--help)🔔 自动更新:轮询各工作区 git HEAD,检测到新提交自动跑增量更新(openwiki 无原生 git 触发器,由插件补上);安装后默认对所有工作区开启——开关在知识库面板左栏「刷新」旁(仅 Open Wiki Tab 显示,知识卡片/知识库不涉及),按工作区独立,手动关闭某个工作区后不会自动重开
🧩 better-sidebar 集成:检测到
dsh-better-sidebar服务即自动注册一个侧边页面(刷新后自动重注册,等效持久化);未安装时设置页给出安装指引🛡️ 忽略文件:
.openwikiignore(gitignore 语法)图形化编辑保存⚙️ 设置页:运行时 / 模型 / better-sidebar 注册 / 入口显隐(自动更新开关已移至知识库面板,按工作区独立)

🔌 核心理念:openwiki 是 CLI 引擎,DSH 是宿主。一切状态(
openwiki/目录下的.page-manifest.json/.run.json/.last-update.json/.claims/)都是磁盘契约,插件只做读取 + 展示 + 触发,不干预生成算法本身。
🧱 架构设计
双半结构(Host / Client)
Cordis 插件分两半,各自有权威源码,构建脚本生成包产物:
| 半部 | 权威源码 | 构建产物 | 职责 |
|---|---|---|---|
| Host | src/host/index.js |
lib/index.js(ESM 入口) |
openwiki 运行时托管、模型桥接、生成任务、wiki/claims 磁盘读取、忽略文件、自动更新、RPC 服务端 |
| Client | src/client/index.js |
client/client.js(Web ModuleLoader bundle) |
全部 UI:入口、浮动窗口、目录树、文档渲染、设置页、better-sidebar 注册 |
构建脚本(scripts/build-host.mjs / build-client.mjs)把源码中的自由标识符改写为包形态:
Host:
harness.handle(→rpc.handle(;lib/index.js注入webServer/settings硬依赖并注册/dsh-openwiki/rpc同源 JSON-RPC 路由Client:
host.call(→rpc.call((fetch('/dsh-openwiki/rpc', { headers: {'x-dsh-openwiki':'1'} }));styles.insert→ 本地<style>注入
开发期还可用「插件开发模式」的 cordis_define/run 把
src/当动态插件直接跑,两种形态共用同一份源码。
通信桥(Client ↔ Host RPC)
Client bundle ──fetch──▶ /dsh-openwiki/rpc (webServer, kind: exact)
├─ 请求头 x-dsh-openwiki: 1(防 CSRF,跨站无法自设)
├─ body: { method, args }
└─ 响应: JSON(handler 返回值或 { ok:false, error })
Host 侧 handlers Map 注册 20+ 方法(见「📡 RPC API」),全部 fiber 绑定(ctx.effect 自动回收)。
客户端状态管理(closure store)
Client 用轻量订阅 store(createKbStore,非 Redux)承载全部 UI 状态,各 Slot 组件通过 useKb() 订阅:
state = {
open, runtime, model, busy, action, lastOutput,
workspaces[], selected, overview, tree, page, claims, jobs[],
tab ('wiki'|'cards'), search, tabs[] (文档多 Tab), activeTab,
browseDir, docView, tocOpen, showIgnore, ignoreContent, error,
win {x,y,w,h,max} ← 浮动窗口几何
expandedDirs {} ← 目录树折叠状态
sidebar, sidebarRegistered, showEntry,
autoUpdate {enabled}
}
Slot 注册清单(Client)
| Slot | id | 说明 |
|---|---|---|
sidebar.footer.action |
openwiki(order 10) |
左下角「设置」上方入口(showEntry=false 时隐藏) |
shell.overlay |
openwiki-kb |
浮动窗口宿主(.owk-win,可拖拽/缩放/最大化) |
conversation.view |
openwiki(order 20) |
会话区域的知识库视图 Tab |
settings.section |
openwiki(order 30) |
设置页:运行时 / 模型 / better-sidebar / 入口显示(自动更新开关在知识库面板) |
🖼️ 特性巡礼
浮动窗口(shell.overlay)
知识库不是全屏层而是可拖拽/可缩放窗口,便于边看文档边对话:
几何存于
kb.win {x,y,w,h,max};headeronMouseDown起拖动(documentmousemove/mouseup跟随),右下角.owk-win-resize手柄缩放(东南角锚定),□/▣最大化视口 clamp:拖动/缩放都经
clampWin(x,y,w,h)——窗口顶/左边永远>= 0,尺寸 clamp360..vw×240..vh,超视口时收缩。避免标题栏被拖出浏览器顶部导致窗口不可抓回窗口 z-index 99999,CSS 见
.owk-win*关闭按钮放标题旁(避开 better-sidebar 右上角 rail 的遮挡)
双模式 Tab 与按钮布局
左栏第一行:
Open Wiki/知识卡片常驻 Tab(.owk-tab.sel高亮,无 emoji)左栏第二行:
忽略文件/刷新左栏宽度可拖:
.owk-kb-resizer分隔条(col-resize光标),拖动更新kb.leftWidth(clamp 220–520px,状态存内存)搜索框 placeholder「🔍 搜索 Open Wiki」,对树做前缀父目录冒泡过滤(
filterPages)
可折叠目录树
buildTree(pages)把相对路径构造成{path, dirs{}, pages[]}树,目录节点带稳定pathkeyexpandedDirs {}记录折叠态,toggleDir切换;expandAllDirs在树加载时默认全展开排序:页面
index/quickstart(根级)权重最优先渲染在目录之前,其余按 title 字母序点击文档 →
openPage(path)(保留.md后缀,host 侧对无后缀输入有兜底);点击目录 →browseDirectory在右侧列出该目录文件
文档阅读与目录分栏
视图切换顺序 目录 / 预览 / 代码;「目录」toggle
tocOpen,在工具栏下方分栏:左侧.owk-toc-pane目录面板(overflow-y: auto独立竖滚动条 +position: sticky不随正文滚动,max-height: calc(100vh - 240px)),右侧.owk-doc-main为正文点目录条目 →
goToHeading(i):切回预览、setTimeout后scrollIntoView('owk-h-i')(锚点由renderMarkdown按 h2-h4 顺序编号),面板保持打开可连续跳转代码视图原样展示 Markdown 源文
应用内链接导航
md 渲染(renderInline)把内部链接渲染为 .owk-wiki-link(onClick + preventDefault 拦截):
href 判定:
http(s)/mailto/… → 新窗口
尾 "/" 或 /openwiki/xxx/ → browseDirectory(文件夹浏览)
其它 → openWikiLink(href)
├─ 剥离 /openwiki/ 前缀(与 host 树读取同一约定)
├─ 绝对路径不拼当前页目录;相对路径拼当前页目录
└─ 归一化 ./ ../ → openPage(目标)
生成任务(Host JobDriver)
startJob({workspaceId, mode, language}):
- 前置校验:workspace 存在、无并发任务、
readGitHead非空(非 git 仓库直接报错)、模型桥接就绪(写.env)、CLI 可解析 subprocess.spawn(openwiki --init|--update -p -l <lang>),cwd=工作区路径- 每 2s 轮询
openwiki/.run.json(readRunState)更新phase/total/done/pending/failed;结束读.last-update.json - 完成后 120s 延迟从 jobs Map 清理;client 每 3s
refreshJobs拉状态 - Client 检测到 error 状态的 job → 写
kb.error(醒目红字提示,避免"生成中很快消失无提示")
Wiki 读取(manifest 缺失 fallback)
openwiki 正常产出 .page-manifest.json,但边缘场景(init 中途 finalize)该文件缺失——此时所有读取走递归磁盘扫描:
| 数据 | manifest 缺失时的来源 |
|---|---|
readWikiTree |
递归扫 openwiki/ 下 .md,逐文件读 frontmatter title |
readWikiOverview |
递归计数 .md 得 pageCount/successCount;failedCount 取最近 run |
readWikiClaims |
扫 openwiki/.claims/*.json(与 manifest 页面清单对应) |
模型桥接(M2 核心)
buildOpenWikiEnv():
agentDefaultModel.currentSelection() → {provider, model}
├─ deepseek-official/deepseek → openai-compatible + DEEPSEEK_API_KEY
└─ pi-ai providers[provider] → 按 api 字段映射 anthropic/gemini/openrouter/openai-compatible
credentials.resolve(apiKeyEnv) → 真实 Key(source: env/credentials.yaml/user-env)
applyEnvUpdates() → 写 ~/.openwiki/.env(只更新 MANAGED_ENV_KEYS,保留其它键)
自动更新(git HEAD 轮询)
openwiki 没有 git 提交触发(无 hook/无文件监听,仅自带每日 CI cron 工作流);DSH 也没有 git 钩子服务。插件自实现:
Host
startAutoUpdate(workspaceId):每 15sreadGitHead(git rev-parse --verify HEAD),与上次比较;变化即startJob({mode:'update'})(增量:openwiki 按lastUpdate.gitHead与git diff只重生成变更页)开关在知识库面板左栏「刷新」旁(仅 Open Wiki Tab 显示),
openwiki/autoupdate/set|statusRPC(status支持按 workspaceId 查询单个工作区状态)0.2.1 起按工作区独立:每个工作区有独立开关/轮询/上次 HEAD(
autoUpdatesMap);关闭某工作区(userDisabled)不影响其他工作区,也不会被默认行为自动重开;无参status返回全部工作区状态;状态为进程内存态,重启 DSH 后重新进入即恢复默认开启(手动关闭过的除外)
better-sidebar 自动注册
ctx.get('betterSidebar') 是 better-sidebar 插件提供的客户端 Cordis 服务(registerTab/getTabs/…):
自动注册:
apply尾部timer.interval(400ms, ≤12 次)轮询服务出现,出现即registerTab({id:'openwiki', title, icon, order:50, single:true, component: SidebarKbView})客户端服务注册表每次页面加载重建,所以刷新后自动重注册 = "持久化";设置页手动按钮保留(幂等,注册后显示「已注册侧边页」)
未检测到时设置页给出安装指引与仓库地址
🧠 关键逻辑
Host 端(src/host/index.js)
| 区域 | 行号(约) | 说明 |
|---|---|---|
readText |
24 | 统一读文件;not found 静默返回 null(可选状态文件缺失是常态,避免刷屏) |
resolveCli |
47 | 解析 openwiki 可执行:Windows npm shim 里解析 node "%~dp0\...\cli.js" 直接 spawn node(spawn .cmd 会 EPERM) |
resolveNpm / readVersion / readLatest |
105 / 137 / 152 | npm 解析、版本读取、registry 最新版(失败只记一次) |
runtimeStatus / install / update / ensure / probe |
217–272 | 运行时状态、npm 安装/升级、惰性确保、--help 自检 |
buildOpenWikiEnv / applyEnvUpdates / readOpenWikiEnvStatus |
331 / 426 / 453 | 模型桥接三件套 |
readGitHead / startAutoUpdate / stopAutoUpdate |
484 / 500 / 492 | 自动更新守护 |
startJob / killJob / jobStatus |
569 / 684 / 697 | 生成任务驱动 |
readWikiTree / readWikiPage / readWikiOverview / readWikiClaims |
751 / 899 / 928 / 1008 | 磁盘读取(含 fallback) |
readIgnore / saveIgnore |
1063 / 1072 | .openwikiignore |
workspaceOf / readRunState / readLastUpdate |
525 / 535 / 558 | workspace 解析、.run.json 进度、.last-update.json |
安全约定:readWikiPage 对路径做 replace(/^\/+/,'').replace(/\.\./g,'') 防穿越;saveIgnore 限工作区路径。
Client 端(src/client/index.js)
| 区域 | 行号(约) | 说明 |
|---|---|---|
createKbStore / useKb |
14 / 72 | 订阅式状态 store |
openPage / browseDirectory / closeTab |
231 / 254 / 262 | 文档 Tab、文件夹浏览、关闭 |
startJob / killJob |
281 / 294 | 任务触发(含 git 校验错误透传) |
openWikiLink / renderInline / renderMarkdown |
341 / 379 / 424 | 轻量 Markdown 渲染 + 内链导航 |
buildTree / expandAllDirs / renderTree |
558 / 578 / 592 | 目录树(折叠、index 置顶) |
renderDoc / renderKb |
676 / 778 | 文档区(目录分栏)/ 知识库主面板 |
registerSidebarTab / autoRegisterSidebar |
1009 / 1037 | better-sidebar 注册 |
toggleShowEntry / SidebarKbView |
975 / 983 | 入口显隐 / 侧边页视图 |
Markdown 渲染注意(维护必读):renderMarkdown 的 fenced code block 检测必须是 line.startsWith('```')(曾用 /^```(\S*)\s*$/,对 ```ts type-equiv 这类**带空格语言串**匹配失败,行落入 paragraph 分支又被 !/^```/ 排除 → i 永不前进 → 无限循环卡死,已在 M6.1 修复)。
📡 RPC API
全部 POST /dsh-openwiki/rpc,需 x-dsh-openwiki: 1 头。Client 的 call(method, args) 封装。
| 方法 | 参数 | 返回要点 | |||
|---|---|---|---|---|---|
openwiki/runtime/status |
— | {installed, version, latestVersion, hasUpdate, exePath, scriptPath, error} |
|||
openwiki/runtime/install / update / probe |
— | `{ok, version | from | to | output};probe 返回 --help` 前 600 字符 |
openwiki/runtime/ensure / checkUpdate |
— | 惰性确保 / 版本对比 | |||
openwiki/model/status |
— | {selection, owProvider, apiKeyEnv, keyConfigured, keySource, envExists, envProvider, envModel, warnings} |
|||
openwiki/model/sync |
— | 执行 .env 写入 |
|||
openwiki/model/env |
— | 已管理 env 键值(密钥掩码) | |||
openwiki/workspaces |
— | {workspaces:[{id,path,title}]}(workspaceRegistry) |
|||
openwiki/job/start |
{workspaceId, mode('init'|'update'), language} |
{ok, jobId};非 git 仓库报明确错误 |
|||
openwiki/job/kill |
{workspaceId} |
{ok} |
|||
openwiki/job/status |
— | {jobs:[{jobId, workspaceId, status, phase, total, done, failed, message}]} |
|||
openwiki/wiki/tree |
{workspaceId} |
{pages:[{path,title,status}], inProgress[], scanError} |
|||
openwiki/wiki/page |
{workspaceId, path} |
{content, frontmatter}(无 .md 输入有兜底) |
|||
openwiki/wiki/overview |
{workspaceId} |
{pageCount, successCount, failedCount, wikiDirRelative, runActive, runPhase, runProgress, lastUpdate} |
|||
openwiki/wiki/claims |
{workspaceId} |
{claims:[{id,statement,evidenceCount,firstEvidence,page}]} |
|||
openwiki/ignore/get / save |
{workspaceId, content?} |
忽略文件读写 | |||
openwiki/autoupdate/set / status |
{workspaceId, enabled?} |
按工作区自动更新开关 / 状态(status 带 workspaceId 查单工作区,无参返回全部) |
|||
openwiki/logs/tail |
— | 占位(当前返回空) | |||
openwiki/kb/config / config/save |
{workspaceId, prompt?} |
personal 目录约定(configDirName/sourceRel/wikiDir/sourceDir)+ 可配置生成提示词 | |||
openwiki/kb/import / files / delete |
{workspaceId, files?|path?} |
上传文件到 openwiki-kb/source/files(路径穿越防护)/ 递归列表 / 删除 |
|||
openwiki/kb/tree / page / overview |
{workspaceId, path?} |
personal 模式知识库树 / 页面 / 状态(openwiki-kb/wiki 为根,.last-update.json 在其下) |
⚠️ 已知限制
| 项 | 说明 |
|---|---|
| settings 命名空间持久化 | openwiki 命名空间注册依赖 schemastery,从 profile node_modules 动态 import 可能失败;当前实际配置走内存态 + localStorage,核心功能不受影响 |
latestVersion |
无可用 web provider 时 readLatest 返回 null(只记一次日志),"最新版本"提示为空 |
| Markdown 渲染 | 轻量实现(标题/代码块/表格/列表/引用/行内样式),Mermaid 围栏按代码块展示,不做完整 Markdown 方言 |
| personal 模式 | ✅ 已支持(openwiki/kb/* + job/start {kind:'personal'}):输出 <项目>/openwiki-kb/wiki(OPENWIKI_CONFIG_DIR 注入);personal 模式无 .run.json 断点/无逐页进度;知识卡(Grounded Claims)仍为 code(仓库)模式专属 |
| 自动更新粒度 | HEAD 轮询间隔 15s,非即时;检测到新提交才触发增量更新 |
openwiki/logs/tail |
占位实现(返回空),后续可接 openwiki 运行日志 |
🧪 验证脚本
| 脚本 | 覆盖 |
|---|---|
tests/verify-host-entry.mjs |
Host 入口离线测试(name/inject/RPC 路由/鉴权/分发) |
tests/redesign-verify.mjs |
M4:浮动窗口/双 Tab/折叠树/内链跳转/设置页(23 项) |
tests/link-nav-verify.mjs |
内链跳转专项 |
tests/sidebar-reg-verify.mjs |
better-sidebar 手动注册专项 |
tests/autoreg-verify.mjs |
自动注册 + 刷新持久化专项 |
tests/round2-verify.mjs |
M5:按钮改名/排序/右侧状态卡/文件夹浏览/自动更新卡(16 项) |
tests/round3-verify.mjs |
M6:目录分栏/按钮两行/trae 生成错误(11 项) |
tests/fixverify.mjs |
状态误判/左侧路径/渲染回归 |
tests/verify-scan.mjs / verify-claims-scan.mjs / verify-frontmatter.mjs / verify-reader.mjs / verify-final.mjs |
离线解析验证 |
tests/verify-kb-host.mjs |
知识库(personal)host 半部:目录约定/导入(穿越防护、二进制)/文件管理/personal job env 注入与 argv/来源 RPC 已移除断言 |
Playwright 用法:$env:DSH_GUI_BASE='http://127.0.0.1:3081'; node tests/<script>.mjs(headless Chrome + playwright-core,createRequire('C:/nvm4w/nodejs/package.json'))。
本文档基于插件开发过程整理,供后续维护与 AI 开发使用;接口契约以源码与 Cordis Inspect Provider 实时查询为准。
No comments yet. Be the first to write one.