把你真实的 Obsidian 知识库放进 DSH 右侧栏 —— 浏览目录树、全库搜索、按 Obsidian…
31.5/100
综合分
2
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/10/28 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
该插件未发布到 npm,按作者文档从 GitHub 源安装即可(静态检查只覆盖 npm 路径,不代表插件不可用)。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add DedsecLemon/dsh-obsidian该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)
状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:需留意
是什么
dsh 原生插件 · browser
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 7 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查需源码安装仓库 package.json 标记 private,未发布到 npm,需从源码安装
展开查看检查明细 ↓
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
✗npm 包dsh-obsidian-panel(未发布到 npm,仅可源码安装)
✓Node 引擎要求 >=22.12.0 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明
源码安装时若 pnpm 拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑。
验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/14 16:18:29
安全扫描自动 · 静态分析源码v2.7.0🔵 扫描:含敏感能力尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。
读写/删除本地文件写入剪贴板(不计风险)发起外部网络请求
查看扫描证据(共 59 处,展示前 17 条)
- 读取本地文件index.mjs:181
if (path === undefined || !existsSync(path)) return [] - 读取本地文件index.mjs:183
const config = JSON.parse(readFileSync(path, 'utf8')) - 读取本地文件index.mjs:204
if (path === undefined || !existsSync(path)) return undefined - 读取本地文件index.mjs:206
const parsed = JSON.parse(readFileSync(path, 'utf8')) - 读取本地文件index.mjs:210
return chosen !== '' && existsSync(chosen) ? chosen : undefined - 写入本地文件index.mjs:226
mkdirSync(dirname(path), { recursive: true }) - 写入本地文件index.mjs:227
writeFileSync(path, JSON.stringify({ path: folder, at: Date.now() }, null, 2), 'utf8') - 读取本地文件index.mjs:236
const app = candidateApps().find(candidate => existsSync(candidate)) - 写入本地文件index.mjs:428
writeFileSync(absolute, text, 'utf8') - 写入本地文件index.mjs:501
renameSync(absolute, target) - 写入本地文件index.mjs:521
writeFileSync(target, body, 'utf8') - 写入本地文件index.mjs:539
mkdirSync(trashDir, { recursive: true }) - 发起网络请求client.js:642
return fetch(url).then((response) => { - 发起网络请求client.js:652
return fetch(url, { - 写入剪贴板client.js:3055
navigator.clipboard.writeText(text === '' ? ' ' : text).then( - 发起网络请求client.js:4116
void fetch(API.diag, { - 发起网络请求client.js:4230
void fetch(API.diag, {
扫描于 2026/11/4(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
功能亮点
- 浏览知识库目录树
- 全库全文搜索
- 笔记大纲展示
把 Obsidian 知识库放进 DSH 右侧栏:目录树、全文搜索、Obsidian 排版风格笔记页、笔记大纲;对话交给应用自己的面板在中间打开,不占侧栏。
README
由 DeepSeek 最新模型翻译生成English · 中文
dsh-obsidian
把你真实的 Obsidian 知识库放进 DSH 右侧栏 —— 浏览目录树、全库搜索、按 Obsidian 阅读视图的排版读笔记、点开 [[双向链接]]、就地编辑,想用真身时一键交给 Obsidian 打开。
装上之后
- 接的是你真实在用的那个库。 首次启动直接给出 Obsidian 正打开的知识库,也可以自己选文件夹;选定后一直生效,并优先于自动探测。
- 右侧栏里的目录树。 只读你展开的那一层。
- 全库搜索。 给出命中的行和它所在的那篇笔记,不只是文件名。
- 笔记页与 Obsidian 同样排版。 用的是 Obsidian 阅读视图自己的排版数值,同一篇笔记在两边读起来一致。
- [[双向链接]] 点得开。 链接在整库内解析,点开就是那篇笔记自己的页面。
- 大纲跳转、就地编辑。 点标题直接跳过去;在同一个框里改完,保存这一篇。
为什么是这一个,而不是 obsidian-workbench
Bob-Bo1/obsidian-workbench 是一个 Obsidian 风格的 Markdown 工作台:把 Markdown 文件交给它,它在工作区里浏览、编辑、预览、新建、移动、删除。
展开完整文档(README 剩余 1 节)
English · 中文
dsh-obsidian
把你真实的 Obsidian 知识库放进 DSH 右侧栏 —— 浏览目录树、全库搜索、按 Obsidian 阅读视图的排版读笔记、点开 [[双向链接]]、就地编辑,想用真身时一键交给 Obsidian 打开。
装上之后
- 接的是你真实在用的那个库。 首次启动直接给出 Obsidian 正打开的知识库,也可以自己选文件夹;选定后一直生效,并优先于自动探测。
- 右侧栏里的目录树。 只读你展开的那一层。
- 全库搜索。 给出命中的行和它所在的那篇笔记,不只是文件名。
- 笔记页与 Obsidian 同样排版。 用的是 Obsidian 阅读视图自己的排版数值,同一篇笔记在两边读起来一致。
- [[双向链接]] 点得开。 链接在整库内解析,点开就是那篇笔记自己的页面。
- 大纲跳转、就地编辑。 点标题直接跳过去;在同一个框里改完,保存这一篇。
为什么是这一个,而不是 obsidian-workbench
Bob-Bo1/obsidian-workbench 是一个 Obsidian 风格的 Markdown 工作台:把 Markdown 文件交给它,它在工作区里浏览、编辑、预览、新建、移动、删除。本插件从另一端出发 —— 接的是 Obsidian 自己用的那个知识库文件夹(从 Obsidian 注册表自动探测,或首次手动选一次),并且以「一个库」为单位工作:[[双向链接]] 在整个库里解析,笔记页用 Obsidian 阅读视图的排版数值,还能把一篇笔记交给 Obsidian 打开。如果你的笔记本来就在 Obsidian 库里、你想在 DSH 里读它改它,那这个插件知道「库」是什么;如果你要的是工作区里的通用 Markdown 文件管理器,工作台更合适。
安装(3 行)
git clone https://github.com/DedsecLemon/dsh-obsidian.git D:/skill/dsh-obsidian
在 ~/.dsh/profiles/<你的 profile>/package.json 里加: "dsh-obsidian-panel": "link:D:/skill/dsh-obsidian" 并写进嵌套的 dsh.profile.bundles(不是顶层 "bundles"),然后在那个目录里 pnpm install
完整可抄的 profile 文件、重启规则与排错见 INSTALL.zh-CN.md。
环境要求: DSH ^0.2.0-rc.2(web profile)、Node >=22.12.0。宿主如果没有 sessions/workspaces,目录树、搜索、笔记页和大纲照常可用 —— 只是没有 对话 入口。
把 Obsidian 知识库放进 DSH 右侧栏。安装步骤见 INSTALL.zh-CN.md。 不需要构建 —— index.mjs 和 client.js 就是直接运行的源码。
git clone https://github.com/DedsecLemon/dsh-obsidian.git D:/skill/dsh-obsidian
然后写在 ~/.dsh/profiles/<你的 profile>/package.json 里:
{
"dependencies": { "dsh-obsidian-panel": "link:D:/skill/dsh-obsidian" },
"dsh": { "profile": { "bundles": ["dsh-obsidian-panel"] } }
}
bundle 列表在 dsh.profile.bundles —— 嵌套字段,不是顶层 bundles。写成顶层的话依赖会装上、bundle 永远不会挂载,插件就这样静默消失。完整可抄的整份文件见 INSTALL.zh-CN.md。
命名
项目叫 dsh-obsidian,包名是 dsh-obsidian-panel,并且没有发布到 npm —— 从本仓库安装即可。
这个名字没得选:npm 上的 dsh-obsidian 属于 另一个作者的 DSH + Obsidian 插件, 用那个名字既发不上去,装下来也会变成它。插件运行期的身份不受影响:路由是 /dsh-obsidian/, slot 键是 dsh-obsidian/,界面上的名字是 知识库。
兼容范围
| DSH | ^0.2.0-rc.2 —— 已在 0.2.0-rc.2 上验证 |
| Node.js | >=22.12.0 —— 只用稳定的 node: API,实测 22.12.0 |
| 系统 | Windows / macOS / Linux(os)。Obsidian 按平台各自查找:macOS 找 /Applications 与 ~/Applications,Linux 找 AppImage、/usr/bin、/usr/local/bin、snap、flatpak,Windows 找常见的用户级与全局安装;DSH_OBSIDIAN_APP 可以覆盖以上全部。知识库探测则读取该平台 Obsidian 自己的注册表。三份路径清单由 test/platform-check.mjs 断言。目前只在 Windows 上实跑过:macOS/Linux 的清单是被断言过的,还没对着真实的 Obsidian 跑过 |
| Profile | web(dsh.client.platform),即任何带客户端半的 profile |
| 逐版本声明 | package.json 里的 dsh.compatibility.dshReleases。只有 0.2.0-rc.2 是 compatible,其余是 unknown —— 直到有人真的跑过,这个字段就是这个意思 |
| 安装/启动/卸载 | 已用打包产物在一次性 profile 里验证:docs/LIFECYCLE.md |
发布新版本前,重跑一遍兼容矩阵与生命周期证据:
node test/profile-lifecycle.mjs # 安装 → 启动 → 卸载,全部在临时 profile 里
node test/mount-check.cjs # 客户端半,真实挂载
node test/render-check.cjs # 注册 + 各类护栏
node test/host-check.mjs # 全部 HTTP 路由 + 安全边界
它碰了什么、碰不到时会发生什么
知识库面板不是玩具:它读你的笔记、写你正在编辑的那一篇,还需要一个地方放三个小状态文件。所以这个插件把它的能力面声明出来,而不是等人去发现:
| 信号 | 实际情况 |
| files | 读你选定的知识库文件夹(以及只为首次猜测而读 Obsidian 自己的注册表 %APPDATA%\obsidian\obsidian.json)。写:三个状态文件 DSH_HOME/dsh-obsidian/(vault.json、chat.json、diag.json,共约 0.3 KB),以及你按下 保存 的那一篇笔记。所有读取都限制在知识库根目录内 —— 通过符号链接/junction 的逃逸会被拒绝(host harness 有断言) |
| network | 没有任何对外请求。 面板访问的是宿主自己的回环路由(/dsh-obsidian/),这也是客户端半里有 fetch 的原因。没有遥测,没有第三方端点 |
| commands | 打开 Obsidian 走的是宿主自己的 subprocess 服务 —— 插件从不 import node:child_process,也不拼 shell 字符串。只在你点「在 Obsidian 中打开」时发生 |
| credentials | 没有。 商城的扫描器把任何 process.env 读取都算作 credentials 信号;本插件读的是 APPDATA、LOCALAPPDATA、ProgramFiles、ProgramFiles(x86)、DSH_HOME、DSH_OBSIDIAN_APP —— 这些都是环境路径。没有 token、没有 API key、没有 cookie、没有 OAuth |
| 依赖 | 无运行时依赖(dependencies 为空)。jsdom/react/react-dom 只给 harness 用 |
| 外部服务 | Obsidian 桌面应用(可选,只影响「在 Obsidian 中打开」)+ 它所运行的 DSH 宿主。打开笔记时把 Obsidian 的 obsidian:// URI 交给该平台自己的处理器(Windows start、macOS open、Linux xdg-open) |
失败边界 —— 缺东西时是成块地退化,而不是注册到一半:
| 缺什么 | 读者看到什么 |
| slots、sidebarRightTabs、sidebarRight | 插件完全不激活 —— 左侧栏没有入口,也不会留下半截注册 |
| sessions、workspaces | 目录树、笔记页、大纲照常;点 对话 会说明为什么打不开 |
| uiWorkspace | 没有文件夹选择器、也无法导航到对话;知识库路径仍可在面板里手动设置 |
| 宿主的 webServer | 路由注册不上,面板没有数据 |
| 本机没装 Obsidian | 只有「在 Obsidian 中打开」不可用(仍会尝试 obsidian:// 协议处理器) |
| Obsidian 装在非常规位置 | 把 DSH_OBSIDIAN_APP 设成它的绝对路径 —— 在任何平台上都优先于自动查找 |
| 知识库被移动或删除 | 目录树显示错误并重新询问文件夹 —— 读失败绝不会被缓存成「空目录」 |
因为确实用到了 files 和 network,只给「无能力」插件自动放行的商城会把本插件保持在 user-reviewed。这是诚实的结果,不是需要绕开的缺陷 —— 另一种做法是做一个读不了笔记的笔记浏览器。
哪个文件夹才是知识库
Obsidian 的注册表只是一个猜测,不是答案。第一次运行时,面板显示的不是目录树,而是选择器:它给出探测到的知识库,外加一个文件夹选择器;在读者确认某个文件夹之前,什么都不读。这个选择存在 DSH_HOME 下(dsh-obsidian/vault.json),此后每一次读取都由它说了算,优先于注册表。标题栏保留了一个文件夹按钮,所以之后想换只需一次点击。
GET/POST /dsh-obsidian/vault 表达的就是这个状态。POST 会校验:文件夹存在、是一个目录,而且看起来像一个知识库 —— 它要么带着 Obsidian 自己的 .obsidian 文件夹,要么在两层之内至少有一个 .md/.markdown/.canvas/.txt 文件。机器上每一个文件夹都满足「存在且是目录」,所以把面板指向任意一个,都会被当成装着笔记的库来提供服务;拒绝时会指明是哪个路径、缺了什么。host-check 的探针会先把这个文件备份、跑完再还原 ——和聊天的 Session id 一样,它是活状态而不是夹具,一个覆盖它的 harness 会悄悄改变用户的插件读的是哪个文件夹。
一个 DeepSeek Harness bundle,把 Obsidian 知识库放进应用的右侧栏:一个 知识库 标签页,带知识库目录树和全库搜索;一个笔记页,按 Obsidian 阅读视图的方式渲染 Markdown,并且可以就地编辑文件;还有一段与 Agent 的对话 —— 它的 Session 就在知识库自己的 Workspace 里,用应用自己的对话面板打开,所以侧栏的空间一点不占。
它会写入知识库,而且恰好只有一条很窄的路径。 POST /dsh-obsidian/note 覆盖一篇已存在的笔记,并且仅当:路径解析后仍落在知识库内、目标是这个插件已经作为笔记展示的文件、且它当前的大小没有超过读取上限 —— 所以被截断过的视图永远不可能被回写、覆盖整个文件。这个包里没有别的东西会写进知识库;对话的 Session id 是 DSH 自己的记账数据,存在 DSH_HOME 下。
它贡献了什么
| 表面 | 类型 | 位置 |
| 面板主体 | 客户端 slot | sidebar.right.pane.tab,key 为 dsh-obsidian/notes |
| 对话 | shell 面板 | 用 uiWorkspace.openSession(知识库 Session) 打开 —— 目录树面板和笔记页上的 对话;本插件不声明任何座位,也不渲染它 |
| 笔记页主体 | 客户端 slot | sidebar.right.pane.tab,key 为 dsh-obsidian/note |
| 笔记大纲 | 客户端 UI | 笔记页的 大纲 面板,锚点是 renderMarkdown 写在标题上的 dsh-obsidian-outline- id |
| 启动行 | 客户端 slot | sidebar.footer.action(id dsh-obsidian) |
| 标签页类型 | 客户端服务 | sidebarRightTabs.register({ id, kind, patterns, canOpen, title }) |
| GET /dsh-obsidian/status | 宿主路由 | 插件解析出的结果(app、知识库、配置) |
| POST /dsh-obsidian/open | 宿主路由 | 打开/聚焦 Obsidian,可选 { "file": "笔记/x.md" } |
| GET /dsh-obsidian/tree | 宿主路由 | 一个目录层级,?path=<dir>('' = 知识库根) |
| GET /dsh-obsidian/note | 宿主路由 | ?path=<note> → 有上限的文本 + stat(外加 root) |
| GET /dsh-obsidian/search | 宿主路由 | ?q=<term>&limit=<n> → 命中的行 |
| GET /dsh-obsidian/resolve-link | 宿主路由 | ?name=<wikilink> → 匹配的笔记路径 |
| GET/POST /dsh-obsidian/chat | 宿主路由 | 记住的那个对话 Session id |
| GET/POST /dsh-obsidian/vault | 宿主路由 | 读者确认过的那个文件夹 |
| GET/POST /dsh-obsidian/diag | 宿主路由 | 客户端半对自己注册情况的报告 |
| obsidian_open | Agent 工具 | 打开/聚焦 Obsidian,可选打开某一篇笔记 |
每个路由都同时应答 GET 和 POST;其它方法一律 405,而不是悄无声息地退化成 GET。这里刻意没有 sidebar.panellist 或 main 注册:这个插件曾经在左侧栏多加一个标着 知识库 的入口,如今唯一的入口就是那行 footer。
有三个标识必须彼此一致,而且各自出错的方式都不同:标签页类型的 title 是那枚小标签上的文字,它的 kind 是 openTab 使用的名字,它的 id 则是主体在 sidebar.right.pane.tab 里注册所用的 key。
它刻意不做什么
它不嵌入 Obsidian 窗口。Obsidian 是原生 Electron 应用,没有可寻址的 Web 界面,所以 iframe、<webview> 或浏览器标签页都装不下它 —— shell 自带的浏览器标签页加载的是 URL,而 Obsidian 不是 URL。
这个不对称值得写下来,因为反方向确实成立:dsh-harness 这个 Obsidian 插件把本应用嵌进了 Obsidian。它能成立,只因为 DSH 本身就是一个网页。镜像方向没有对应物。
所以这个插件把知识库的内容带进右侧栏,而真正的 Obsidian 只隔着一次点击 —— 通过 obsidian_open 路由/工具。
知识库里的对话
知识库 面板旁边开着一段真正的 Agent 对话 —— 它开在中间区域,是一个普通 Session,只不过 Workspace 是知识库;它不在官方侧栏聊天的标签页里,也不在「新建 Session」界面上。它就是一个 Session:第一次使用时创建、之后复用,所以一份计划能扛过一次刷新,也能跨天继续打磨。
它必须属于知识库的 Workspace,而不只是属于它的目录。 这一点最容易搞错,而且错了看得出来:只用 cwd 创建的 Session 不属于任何 Workspace,于是对话会打开在空白的「新建 Session」界面上、还带着一个 Workspace 选择器,整体读起来就是「又开了一个新对话」。所以 resolveVaultChatSession 先调用 ctx.workspaces.create({ path }) —— 文档说它是幂等地解析一个已存在的路径,因此知识库的 Workspace 不会被重复注册 —— 再把得到的 workspaceId(加上 cwd)交给 ctx.sessions.create。
对话 UI 没有任何一部分被重新实现,也没有占用任何座位。 应用本来就有唯一合适的地方放对话、唯一的面板渲染它:中间区域的 main 键 conversation。所以 对话 不挂载本插件的任何东西 —— 它通过 uiWorkspace.openSession(sessionId) 选中知识库那个 Session,由 shell 自己画:
navigator.openSession(sessionId) // ctx.get('uiWorkspace')
这也是本插件现在不声明任何子 slot 的原因:早先的版本在自己两个界面上声明过对话座位(再早还试图占住 sidebar.chat.conversation,而那个座位属于 ui-subagent —— 第二次声明会在 apply 里抛错,shell 随即回滚本插件做过的全部注册)。什么都不声明,既不花钱也不会撞车。
交接的是文件,不是 AI 的计划。 把一篇笔记「丢进」对话,做法是在光标处插入它的 @path 提及 —— inputActions.captureInsertion() + inputActions.insertText(' @… ', span),和拖拽会产生的是同一套机制。这个提及是绝对路径(@D:/知识库/笔记/foo.md),因为接收对话的 Workspace 不是知识库:相对提及会在错误的根上解析。两个控件分别位于目录树中选中笔记的旁边,以及笔记页的标题栏里。
目录树的右键菜单
在目录树某一行上右键 —— 或者用悬停时出现的 ⋯ 按钮(右键是隐性交互,只靠它会被找不到)—— 开关同一个菜单:在同一行再点一次就关掉。里面是那些平时"必须先选中笔记才能用"的动作:
- 打开 / 编辑 —— 编辑 打开笔记并直接进编辑模式,靠 pendingEdit 按库内路径做 hand-off(和标题锚点用的 pendingAnchors 同一个招数,原因也一样:真正要动的是另一个组件实例)。
- 引入对话 —— 引用到对话 插入 @路径 提及。只留一个是刻意的:另加一条"把正文贴进去"听起来有用,实际用起来像重复,于是删掉而不是先留着当备选。
- 复制 —— 只留 [[名称]]:四个复制里唯一不算忙活的一个(在别的笔记里造链接就靠它)。库内路径、@绝对路径、文件系统完整路径都是给脚本和终端用的,不是给侧栏里的读者用的。
- 关联 —— 查看关联 打开笔记并滚到它的「关联」面板。
- 在别处打开 —— 在 Obsidian 里打开,或在系统文件管理器中定位。Windows 上走的是已在运行的外壳(COM),不是 explorer /select:spawn 出来的 explorer 会继承调用方的隐藏显示状态,于是窗口建了、文件选中了、窗口却是隐形的 —— 读者什么都看不到。
- 重命名 / 移到 .trash / 新建笔记 —— 见下。
写操作刻意保守。 移到 .trash 从不硬删除:文件被移进 <库>/.trash/<时间戳>-<原名>,完全在面板的列表之外,误操作就是移回来。重命名 只在同一目录内改名,且不重写别的笔记里的 [[链接]] —— 改名后断链样式会精确告诉你哪些链接失效了,这比静默扫一遍全场去改更诚实。新建笔记 只建一个文件、写最小 frontmatter。三者同一套路:先按库根校验路径、目标已存在一律拒绝、Windows 会改写(甲.)或根本建不了(NUL)的名字也拒绝,失败时说清原因而不是吞掉。
笔记页
笔记在自己的标签页里打开,而不是内联:整高渲染的 Markdown,里面的 [[双向链接]] 可点击。每个链接都通过 /dsh-obsidian/resolve-link 在知识库里解析,并把目标打开成它自己的一页 —— 顺着链接走永远不会丢掉你正在读的那一页。目录树仍然待在 知识库 面板里;笔记不会取代它。
指向本文内部的链接 —— [[#标题]]、[[#标题|别名]]、x —— 不解析,而是就地滚动。合订本《高性价比人生指南》的整个目录就是这么写的:45 个格子都是 [[#第 N 节 …\|N. 短标题]],别名管道为了下面那层表格解析器转义成了 \|;以前这些全送进笔记解析器,于是每个章节链接都是死按钮。跳转读的是已渲染标题的文本和 id,不把笔记再解析一遍;对用 slug 锚点转换过的笔记,会退回到 GitHub 式 slug 再匹配一次。标题确实不存在时会明确说出来。
指向别的笔记的链接会带上锚点。 [[笔记#标题]] 打开那篇笔记并滚到标题:发起方把锚点记在 pendingAnchors(按库内路径为键),接收页在把笔记画完之后取走它 —— 目标是另一个标签页实例,没渲染完就无从滚动。匹配时 Obsidian 的标题原文优先;若锚点恰好只命中一个标题的包含关系,也接受(本库的沙盘笔记就是这么写的:[[笔记#整合性]] 指 5.1 整合性 (Integration));命中多个则拒绝,不去猜。[[笔记#^块id]] 落到块上:段落或列表项末尾的 ^id 会变成 data-block-id,不会作为文字显示。
属性区:显示而不是丢弃。 frontmatter 以前被当成「元数据不是正文」整段丢掉 —— 对一份文档成立,对一个侧栏阅读器不成立:这篇笔记是什么(type、status、parent)、怎么归档(tags),恰恰是最该先看到的。面板在笔记页顶部渲染一个可折叠的属性卡,按文件里的顺序列出:列表变 chips、布尔打勾、日期与数字照原样、值是 [[链接]] 的仍然可点。渲染器与属性卡共用同一个 parseProperties,两边对「正文从哪开始」不可能有分歧。
关系图谱:只做局部。 笔记页底部是这篇笔记的边:引用本文(哪些笔记链到这里,并显示那一行原文)与 本文引用(这篇链接到谁,坏目标标出来)。点任意一行,目标笔记打开成它自己的一页。全库关系图谱是有意不做的:窄侧栏里一张发丝球图提供不了信息,读者真正需要的是脚下这条边。
两者都读同一份索引:GET /dsh-obsidian/links 走一遍库,返回每篇笔记的标题、摘录、标签,以及它的 [[链接]] 落到哪里。本库实测 714 篇 / ~1080 条链接 / ~0.5 秒 / ~290 KB,缓存 15 秒(?refresh=1 重建,面板上的 刷新 就是它)。它和搜索一样有界 —— 文件数/字节/深度上限 + truncated 标记,因为建索引等于把整个库读一遍。
链接状态。 指向不存在笔记的 [[链接]] 会按 Obsidian 的断链样式画:变淡 + 虚线 + 悬停说明 —— 否则一张密集的目录通篇一个颜色,断了一半也看不出来。悬停正常链接会浮出一张卡,显示目标标题和目标的第一行;目标不存在时明确说「这篇笔记不存在」,而不是给一张空卡。
高亮。 ==这段== 渲染成 Obsidian 的 (同为 40% 琥珀底色 --text-highlight-bg),不再原样显示两个等号。孤立的 ==(正文里的比较)保持原样;高亮内容里不允许出现反引号,所以代码段前面的比较符不会跟代码里的 == 配对、把整段代码吞进高亮。
排版是 Obsidian 的,照抄过来的。 READ 里的数字就是默认主题阅读视图自己的值 —— 16px、行高 1.5;标题阶梯(1.618em/1.462em/1.318em/1.188em/1.076em/1em)及其字重(h1 为 700,其余为 600)与行高;块与块之间 1rem;标题紧跟在另一个块之后时上方 2.5rem;列表缩进 2.25em;引用块有 2px 的强调竖线和 24px 内边距;代码块不加装饰。render-check 把其中好几项钉住了,因为正是这里的漂移让页面不再像 Obsidian。
GFM 表格会渲染成表格。在这之前,它渲染成一段由竖线组成的段落,而一篇带表格的笔记大部分时间看起来就是这样。
Obsidian 的 callout 按 Obsidian 的方式折叠。 > [!cite]- 来源(点开展开) 是 callout 而不是引用块:- 表示默认折叠,+ 表示展开但可折,光秃秃的 > [!note] 展开且不可折叠。合订本《高性价比人生指南》767 条每条末尾都有这样一个折叠的来源块,当成普通引用渲染会把几百个 DOI 铺进正文中间。callout 保留 Obsidian 按类型区分的竖线色和 10% 底色;折叠的正文仍然挂在 DOM 里、只是用 CSS 隐藏,所以展开不需要重新取数,折叠也不会改变大纲看到的东西。原生 HTML 的 / 渲染成同一种控件 —— 同一篇笔记的 41 条术语表就是这么折的,里面的表格也照常渲染。render-check 钉住了三种折叠状态、标题、正文里的 markdown,以及「不是 callout 的引用块仍保持上面的引用排版」。
大纲。 笔记标题栏里的 大纲 会在窗格右缘浮出一个标题清单:点其中一条,阅读区就滚到那个标题;你当前正在读的标题那一行会被标出来。层级用缩进表示,所以 ## 下面的 ### 一眼就能看出是嵌套的。
它刻意只是对已渲染页面的一层视图,而不是把笔记再解析一遍。renderMarkdown 在渲染时就把每个标题的锚点(id)和层级(data-outline)写成 DOM 属性,面板再用 querySelectorAll 读回来 —— 只解析一次,清单就不可能描述出与屏幕上不同的另一页。这也包括被截断的情况:超过 MAX_RENDER_LINES 的笔记只渲染开头,大纲列出的正是真实存在的那些标题,不会出现指向根本没画出来的标题的条目。锚点按页面实例编号,同时开两个笔记页也不会撞。
面板可以用它自己的控件、×,或进入编辑模式来收起 —— 渲染出来的页面已经没了,也就没什么可大纲的了。mount-check 用一篇标题阶梯为 一/二/三/二 的笔记跑完 打开 → 选择 → 跳转 → 收起,并断言锚点、缩进和滚动目标。
编辑。 编辑 把渲染视图换成同一文件的纯文本编辑器;保存 把整段文本 POST 到 /dsh-obsidian/note。读取时被截断的笔记会直接被拒绝编辑 —— 局部视图不是覆盖该文件的安全基础。
把笔记引用进对话。 只有一个控件,因为提及只有一个落点:引用到对话 把这篇笔记的绝对 @path 插到当前中间区域那个对话的输入区里 —— 和把文件拖进去是同一件事。路径必须是绝对的,因为接收方的 Workspace 未必是知识库:相对提及会解析到错误的位置。知识库根未知时是拒绝(no-path),而不是插一个 @undefined。
已经不存在「第二个输入区」可以够,所以这是一个控件而不是两个:知识库对话就是读者从 对话 打开的那个 Session。mount-check 断言这次点击把该 Session 交给了 shell,并且引用落进了页面拿到的那个输入区。
对话:开在中间,由 shell 承载
对话本身就是一个普通的 DSH Session —— 只不过它的 Workspace 是知识库 —— 对话 把它交给应用:uiWorkspace.openSession(sessionId) 选中它,中间区域用 shell 自己的对话面板显示它。本插件不渲染它的任何一部分,它也完全不碰右侧栏的布局:目录树和笔记还留在旁边,而插件自己的界面只有一个按钮。
这就是全部设计,也是对「这个插件连错三次的问题」的答案。右侧栏的一个窗格只有几百像素的工具空间,把对话塞进去只有两种形状,而两种都是错的:要么吃掉目录树的高度(最早的页脚、后来的浮动弹出框),要么把它顶掉(独立标签页)。两种形态都得替一个「一阵一阵用」的东西编一个尺寸,而这一列的本职是展示知识库。中间区域才是应用放对话的地方:整宽、已经自带输入区/记录/停止按钮等等,而且 Session 在宿主侧 —— 所以打开和关掉这个视图对对话毫无损失。
两个值得说明的后果:
- 本插件现在不声明任何子 slot。 它曾经在两个界面上各自声明一个对话座位,等于自己养一份对话状态并一直喂着。现在没有属于我们的东西会和 sidebar.chat.conversation(在这个 profile 里归 ui-subagent)撞车,也没有东西需要维持。
- 笔记页上的 引用到对话 写进的是屏幕上那个对话。 标签页主体拿到的 inputActions 属于拥有这个侧栏的那个 Session —— 也就是中间区域正在显示的那个 —— 所以引用一篇笔记,就是把它的绝对 @path 放进当前打开的对话;只要读者是从 对话 进去的,那就是知识库对话。模块级句柄已经没有了,因为不存在第二个输入区可以够。
继续上次,还是新开一个
记住的 Session 就是握着历史的那个,所以「打开知识库对话」本来就应该指它 —— 按钮原先就是这么做的,而且是默默做的。这让另一种意图(从空白开始)变成必须去工作区里翻找才能实现,而反过来(永远新建)又会把历史丢掉。所以 对话 先解析,发现有可回去的历史时先问:
继续上次对话 回到知识库里的那段历史新开一个对话 同一工作区,从空白开始
没有记住的 Session 就不弹菜单:第一次使用直接建一个并打开。「新开」是在知识库 Workspace 里真建一个新 Session,而且只在它建好之后才替换记住的 id —— 失败时历史依然找得到。
选好文件夹,工作区就出现
知识库的 Workspace 在读者确认文件夹的那一刻就注册,而不是等到第一次点 对话。Workspace 是这个文件夹「能作为 Agent 的目的地」的凭证,也是侧栏列出来的东西,所以惰性解析意味着在读你去点对话之前什么都不会出现。workspaces.create 按契约是幂等的,所以 resolveVaultChatTarget 里那条惰性路径照常可用,且在工作区已存在时不花任何代价。
侧栏会被放回去
右侧栏的标签页是 session 作用域的:选中另一个 Session 会给窗格一套全新的、空的标签页 —— 目录树,或者读者正在看的笔记页,会跟着它所属的那个 Session 一起消失。打开一个对话不等于关掉目录树,所以插件记住自己当时在显示哪个界面(rememberSurface),在 openSession 之后把它放回去(restoreSurface);因为窗格会为新 Session 重新挂载,这里有几次短暂重试。目录树自己的浏览状态 —— 展开、选中、搜索词、已经取回的层级 —— 也会穿过这次重挂载保留下来,因为知识库还是同一个;但若发现换成了另一个知识库,就整个丢弃。
编辑态和阅读态是同一个盒子
编辑 把渲染好的一页换成同一文件的 textarea,两者必须是同一个盒子:16px、行高 1.5、同样的 18px 22px 96px 内边距、同样的字体 —— 以及正文上方同一行元信息(大小 / 时间 / 已截断)。那一行原先只存在于阅读态,所以按一下按钮正文就上移了它的高度(约 24px)。编辑器本身原先用的是 13px 等宽字体加自己的内边距,模式切换成了这个功能里最显眼的东西。mount-check 会把两个盒子互相对比:同样的内边距、同样的字体度量、两边都有同一行元信息。
一个 slot,一个声明者 —— 以及它为什么能让整个插件倒下
sidebar.chat.conversation 是 single slot,而 slots 核心拒绝第二次声明它(dsh-client-ui-slots):
if (options.children) for (const childKey of Object.keys(options.children)) {
const childRec = this.records.get(childKey);
if (childRec?.spec) throw new Error(slot "${childKey}" is already declared (by ${childRec.declaredBy}));
}
这个抛错发生在 apply 里面,而 shell 对 apply 失败的处理是回滚这个插件做过的每一次注册。所以从笔记面板和笔记页两处都声明这个座位,代价不是少一个标签页 —— 而是整个插件,侧栏里什么都看不到。
早先的版本是这样解决的:笔记页声明自己的座位 dsh-obsidian/note.conversation,并占住它。不同的名字不会撞车 —— 而且声明一个 session 作用域的子节点,恰恰也是让那个主体挣到对话所需那两样东西的原因:渲染器只把 renderSlot 和 SessionProvider 发给声明了子节点的条目:
if (entry.children !== void 0) {
kit["renderSlot"] = boundRenderSlot(host, entry);
if (Object.values(entry.children).some((spec) => spec.scope !== "root")) {
kit["SessionProvider"] = scopeAreaProvider(adapter);
}
}
相比之下,renderFactorySlot 是发给每个条目的 —— 但它自己不够:没有 SessionProvider,工厂就没有可渲染的 session 作用域,这就是为什么只有工厂的笔记页显示的是「没有座位也没有工厂」,而不是一段对话。
这是早先版本用来承载自己那个对话的机制。现在的插件不声明任何子 slot,所以它根本拿不到 renderSlot/SessionProvider —— 也不需要:知识库对话由 shell 的 Conversation 面板绘制,本插件只是按 Session id 把它选中。
两个客户端 harness 现在都照搬 slots 核心的规则,遇到第二次声明就抛错;render-check 通过声明两次同一个座位来证明这道护栏是活的,并断言本插件一个子 slot 都没声明。一道不出声的护栏比没有更糟:这正是它们第一次没抓到的那次故障。
为什么记住的 Session 永远优先
resolveVaultChatSession 无条件复用记住的 Session id,根本不去问 Workspace。记住的 Session 就是握着对话历史的那个;如果靠重新推导 Workspace 归属来决定要不要复用它,那么只要推导结果不一致,历史立刻就没了 —— 「重启后我的历史不见了」就是这个样子。Workspace 只在创建 Session 时被问到,这样新建的会话会归到 知识库 下,而不是变成孤儿。
读取失败不等于「什么都没记住」。 对失败的 GET /chat 回答 '',会让插件新建一个 Session,并把它的 id POST 覆盖掉记住的那个:一次瞬时读取错误,历史就没了。现在读取失败会进入一个可见的失败状态,并且绝不写入。写失败同样是可见的 —— 宿主返回 400 并带上出错的路径,而不是一个让「这个 id 从来没被记住」看起来像成功的 200。
Sessions 和 Workspaces 是惰性查找的(ctx.get),从不写在 exports.inject 里。要求一个 profile 并不发放的服务,会让整个条目卡住不激活 —— apply 根本不会跑,侧栏也就丢掉了这个插件,连目录树一起。惰性查找才让「对话不可用,但目录树照常工作」成为一个真能到达的状态,而不是死代码。uiWorkspace 同理:少了它,对话 只是报一句「没有会话导航」,目录树毫发无损。
显示会话的 retain/release 现在归 shell —— 它显示哪个 Session,就由它持有。本插件不再 retain 任何东西,因为不再有任何界面承载对话。
为什么面板是纵向堆叠的
右侧栏的一个窗格只有几百像素宽,所以面板保持单列:主体显示目录树(或实时搜索结果)。笔记永远不霸占这一列 —— 它打开成自己的一页;对话也完全不在这列里:由 shell 显示在中间。没有任何东西来抢这个窗格的高度。harness 断言这个纵向堆叠,所以将来某次改动若重新引入并排两列,失败的是测试而不是布局。
为什么目录树是一层一层取的
一个知识库里可能既有笔记,也躺着一份完整的源码检出。让宿主在一次响应里返回整棵树,会让某一个又深又宽的目录花光整个节点预算,知识库剩下的部分就悄无声息地消失了 —— 这个插件早期的一个版本正因为如此,只返回了 17 个根条目中的 4 个。
所以 /tree 只回答恰好一个目录层级,面板在这个文件夹第一次展开时才去取这一层。每一层都是完整的,再大的知识库也保持响应,而且没有任何东西会在读者不知情的情况下被丢掉:当宿主的每层上限真的咬到时,面板会显示 仅显示前 N 项,而不是悄悄把列表结束掉。
取某一层失败时,不会把它缓存成空目录。[] 会宣称这个文件夹是空的,并让它再也不会被重试;现在改成丢掉这个失败的 key、把提示显示出来,再由一个 重试 控件重新去取。搜索结果带同样的截断提示,而且对更早一次查询(或更早一篇笔记)的回答,绝不会覆盖更新的那个 —— 每个请求都带一个序号,过期的回答会被丢弃。
它怎么认出你这台机器
没有任何东西硬编码到某一次安装。调用的时候,宿主半会:
1. 读取 %APPDATA%\obsidian\obsidian.json —— Obsidian 自己的知识库注册表 ——
并挑出最近打开过的那个知识库(Obsidian 的 URI 处理器期望的知识库名,就是文件夹的基本名);
2. 依次从 DSH_OBSIDIAN_APP、常见安装位置定位 Obsidian 可执行文件(Windows 的 Obsidian.exe、macOS 的 app 二进制、Linux 的 AppImage 或发行版包);
3. 把 obsidian://open?vault=<vault>[&file=<file>] 交给它。
已经在运行的 Obsidian 会被聚焦,而不是再开一个,因为 Obsidian 自己就强制单实例。URI 走哪条通道交付取决于平台:Windows 和 Linux 用已知的可执行文件,macOS 用 open —— 在 macOS 上直接执行 app 二进制、把 URI 当 argv 传进去并不能交付它,而那个二进制总是存在,所以「直接跑二进制」原先总是抢先、open 那条路根本不可达。现在直接执行二进制只是 macOS 上解析不到 open 时的后备。Windows 后备到注册的 obsidian:// 处理器(cmd /c start),Linux 后备到 xdg-open。三种平台的选择由 test/platform-check.mjs 断言。
已知限制,如实写明:macOS 这条路没有在 macOS 上跑过。 手边没有 Mac,所以被断言的是选择逻辑(只要 open 能解析到就选它),这部分可以离线验证;真正在 Mac 上交给 Obsidian 的那一步没有实跑过。macOS/Linux 的路径清单和注册表路径同样处于「被断言、未被实跑」的状态。
GET /dsh-obsidian/status 会如实报告解析出了什么。
安全
- 来自浏览器的每一条路径都会对着知识库根解析,一旦逃逸就被拒绝;.. 和 Windows 风格的 ..\ 都会失败。
- 字符串前缀不等于包含。 锚点是根目录的 realpathSync,已存在的目标会再解析一次并且必须落回它内部,所以知识库里面的符号链接或 NTFS junction,再也不能经由 /note、/tree 或写入路径去读、去覆盖外面的文件。realpathSync 失败(EPERM、悬空或损坏的链接)一律是拒绝,绝不是放行。
- 点目录和构建/VCS 目录(.obsidian、.git、node_modules,…)永远不会被列出 —— 而且任何带 . 前缀段的路径,目录树、笔记读取和笔记写入都一样拒绝,所以 .git/config、.mcp.json 和 .obsidian/ 都到不了。笔记读取还必须指名一个真正的笔记文件(NOTE_FILE),和写入路径一直以来的那道护栏相同。
- 笔记读取上限 512 KB。一次搜索有四重边界 —— 200 条结果、读 3000 个文件、进 5000 个目录、深度 24 —— 并跳过大于 1 MB 的文件。
- truncated 表示这个回答不完整,原因可能是上面任意一种。 撞到上限的搜索、或跳过了超大笔记的搜索,都会置 truncated: true,而被跳过的超大笔记数量会单独以 skippedLarge 报告。这两者刻意不是同一件事:truncated 是面板据以行动的信号,skippedLarge 则告诉它这是哪一种不完整。跳过一篇大笔记以前是无声的,于是一个装满大笔记的知识库会回答「搜过了,什么也没找到」,却从来没读过其中任何一篇 —— 现在面板会说这篇笔记没有被搜索,而且即使没有别的结果也会说。读完了一切的搜索仍然回答 truncated: false。
- 撞到深度上限的分支是被剪掉,而不是致命错误:遍历不再往下走,仍然搜索知识库的其余部分,并报告截断。在那里直接中止,会让一个很深的文件夹把它旁边所有笔记都藏起来。
- 请求体在读取过程中就被限制在 8 MB,不是读完之后:超限时读取停止、请求被拆掉,路由回答 413。以前 4 MB 的笔记上限是唯一的边界,而且它是在整个 body 已经进了内存之后才跑,所以一次上传在任何检查生效之前就已经被完整缓冲了。
- 写入。 只有一条:POST /dsh-obsidian/note 覆盖一篇笔记。它会拒绝逃出知识库的路径、隐藏路径、不存在的目标、不是文件的目标、不是这个插件已经展示为笔记的目标、当前大小超过读取上限的笔记(被截断的视图不是覆盖的安全基础),以及超过 4 MB 的内容。字节按原样写入,UTF-8。
- /diag 只存客户端半报告的内容。 它接受 inject、report 和 renderError,其它一切丢掉;而它的 at 时间戳是在这些 key 复制之后由服务端赋的 —— { at: Date.now(), ...body } 会让调用方既覆盖时间,又把任意 key 塞进这个插件写到磁盘的文件里。
- 对话的 Session id 写到 <DSH_HOME>/dsh-obsidian/chat.json。那是 DSH 自己的记账数据,不是知识库内容。写不到磁盘会返回 400 并指明路径,所以「没被记住」从来不是无声的。
- host-check 用它自己创建、随后删除的探针文件来演练写入路径,所以不会碰到任何已有笔记,并断言上面每一条拒绝。它会读取并还原 chat.json、vault.json 和 diag.json:这三个都是活状态,不是夹具。协议探针 —— junction 逃逸、各种上限、文件夹校验 —— 跑在 os.tmpdir() 下创建的知识库上,并通过真实的 /vault 路由切换进去,所以用户自己的知识库永远不会被进入。
目录结构
package.json # dsh.bundle.patch + dsh.client
cordis.patch.yml # bundle layer: mounts this package by name
index.mjs # host half: resolution, routes, obsidian_open tool
client.js # client half: the right-Sidebar tab type + its launcher
test/ # headless harnesses, not shipped
测试
两半都能在没有浏览器、也没有活的 DSH 的情况下运行 —— 宿主 harness 把子进程服务造假,所以它从不启动 Obsidian。
npm install # once: the client harnesses need react/jsdom
node test/mount-check.cjs # mounts the panel into jsdom with a stubbed fetch
and drives every state the shell can put it in
node test/render-check.cjs # executes client.js as the module table does,
then server-renders every seat with real React
node test/host-check.mjs # drives every route against the real vault
两个客户端 harness 从这个包自己的 devDependencies 解析 react、react-dom 和 jsdom,React 钉在 shell 打包的那个 18.2.0 上。它们以前是从 DSH 源码检出里借这些依赖的,这让它们被一个和本插件毫无关系的目录挟持 —— 而那个目录后来还被删了,直接从它们脚下抽走。
mount-check.cjs 之所以存在,是因为服务端渲染不够。它带着面板走一遍 挂载 → 目录树 → 展开 → 笔记 → 大纲 → 对话 → 返回 → 搜索 → 启动行,而且 effect 是真的在跑。这个插件的第一个版本在自己声明之前用了 const:抛错发生在 passive mount effect 里,而 SSR 从不执行它,slot 框架的条目边界则以把这个标签页主体 retire 掉作为回应 —— 于是右侧栏打开是空的,产品里没有任何东西指向原因。这一类 bug 只有真实挂载才看得见。
它也是用回归测试抓住那个流到用户手上的缺陷的地方:这个框在每一次渲染时都 retain 它的 Session(resolver 每次都是新标识,于是 acquire effect 在循环里重跑)。
render-check.cjs 覆盖那些肉眼看不见的接线:标签页类型的 id 等于它主体座位的 key;本插件一个子 slot 都不声明(并且同一个座位声明两次会被拒绝);footer 行恰好只有一行、且没有对话标签页;sessions、workspaces 和 uiWorkspace 不在 inject 里(并且以属性方式读它们任意一个都会抛错);没有 Sessions 的 profile 里目录树面板照常渲染,并且用客户端半真正写出的那个标记指向对话;未知的知识库根是 no-path 拒绝,而不是一个 @undefined 提及。它还钉住了 Obsidian 阅读视图的排版和 GFM 表格;并且为了防止「断言一个源码里根本不存在的标记」再次腐烂,只要两个客户端 harness 查询了 client.js 里没有的 data-dsh-obsidian- 标记,它就会失败。
host-check.mjs 对着真实的知识库跑全部九个路由,并断言对话的 Session id 落在 DSH_HOME 下、而不是知识库内部 —— 而且是对着真实的存储路径,因为旧断言指的那个文件这个插件从不写。它还断言 .git/config、.mcp.json 和 .obsidian/ 既读不到也列不出,非笔记文件读不到,PUT/DELETE 是 405,/diag 能往返,写不到磁盘的 chat 写入是带路径的 400,以及 8 MB 的 body 是 413、且它的读取提前停止了。
那些需要自己一块知识库的探针 —— 指向外面的 junction、超过单文件上限的笔记、26 层深的链、5001 个同级目录、被当作知识库提供的空文件夹 —— 用的是 os.tmpdir() 下的临时知识库,通过真实的 /vault 路由切进去,跑完再还原。用户的知识库从不被写入;junction 探针在临时树里创建、也在那里删除。进程建不出来的 junction 会以带原因的 skip 报告,绝不是通过。host-check 会在最后一行打印检查数量和任何 skip。
安装
dsh plugin --profile <name> add /path/to/dsh-obsidian
该 profile 的 dsh.profile.bundles 会多出 dsh-obsidian,这一行从 cordis.patch.yml 加载。卸载:
dsh plugin --profile <name> remove dsh-obsidian
新装上的宿主半在启动时被 import,而 Node 按解析后的 URL 缓存 ESM —— 所以让改动过的宿主半生效靠的是重启。改动过的 client.js 还额外需要刷新页面,除非 shell 正跑着它的客户端 watcher。
开发笔记
client.js 是手写的 loader 产物,不是 tsdown 的输出,所以这个包不需要构建步骤:
window.__ModuleLoader__.load({
id: '<package name>',
factory: (require) => {
var module = { exports: {} }; var exports = module.exports;
// require answers only from the shell's frozen table:
// react, react/jsx-runtime, react-dom, @deepseek-ai/cordis,
// @deepseek-ai/dsh-client-store, -ui-slots, -ui-primitives, -ui-dockkit.
exports.apply = (ctx) => { / ctx.slots.inject(...) / };
exports.inject = ['slots', 'sidebarRightTabs', 'sidebarRight'];
return module.exports;
},
});
超出这条基线的任何东西都必须走 dsh.client.external 和真正的构建;禁止跨插件做值导入 —— 请改用 Cordis 服务协作。面板的配色来自 shell 发布的主题 token(--dsw-alias-),所以不用样式表就能跟随明暗。
inject 是承诺,不是许愿单。 一个条目如果要求了 profile 不发放的服务,就永远不会激活 —— 没有报错,没有窗格,整个插件就是不在。所以 inject 只写这个插件离开它就活不了的东西,而 profile 可能合理地缺的一切(Sessions、Workspaces、文件夹选择器)都在需要的那一刻用 ctx.get('name') 去取。把这样的服务当属性读会在 shell 内部抛错(cannot get property "X" without inject),这也是为什么 harness 用 Proxy 把这条规则明确排除掉。
openTab 写入当前挂载的 Session 的右侧栏,并且在没有任何 session 界面挂载时抛错,所以启动行会接住它并把情况说出来,而不是无声失败。预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
DedsecLemon/dsh-obsidian
把你真实的 Obsidian 知识库放进 DSH 右侧栏 —— 浏览目录树、全库搜索、按 Obsidian…
相关插件 · 浏览器
智能体插件框架deepseek-ai/deepseek-harness
DeepSeek Harnessdsh是由 DeepSeek AI 开发的开源 agent harness智能体框架。
★ 270,422已汉化DSH
71.9
多设备 Agent 看板YaoApp/yao
✨ 所有 Agent 和工作区集中管理,覆盖你的每一台设备。任务看板追踪进度,桌面端、移动端、浏览器、API…
★ 8,147已汉化DSH
71.2
可扩展侧边栏工作台omdsh-dev/DSH-better-sidebar
DSH 的双工作台:原生右侧栏 + 自绘底部面板,并把 ctx.betterSidebar 服务开放给所有插件
★ 4,761已汉化DSH
71.1
浏览器自动化桥Tencent/BrowserSkill
你的 Agent。你的浏览器。连接,就能协作。
★ 9,967已汉化DSH
71
AI 小说创作工作台EthanYoQ/AI-Novel-Writer
AI 小说作家 / AI Novel Writer
★ 1,591已汉化DSH
70.7
智能体浏览器引擎platonai/Browser4
DeepSeek Harness 集成
★ 1,175已汉化DSH
70.4
这是你的仓库?认领条目