dsh-plugin-dock · 插件坞 (Plugin Dock)
一个坞,装下所有插件。 插件坞把 DSH 已安装插件的入口收进右侧栏窗口,让你在对话旁边直接调用插件、不必来回切页面;左侧栏的「插件坞」是它的总入口,输入框左侧的按钮是一键直达。
英文名:Plugin Dock · 包名
dsh-plugin-dock· 版本0.1.0· 双半插件(Host + Web Client)。
一句话介绍
插件坞 = 右侧随手调用面板 + 左侧全局入口页 + 输入框一键按钮。它不重写任何插件的界面,只通过 DSH 的公开插槽发现入口、通过公开 API 打开它们,并把「上下文归纳」作为内置快捷动作示范跨插件调用。
入口与界面
四个入口,同一个产品:
| 位置 | 形态 | 作用 |
|---|---|---|
| 左侧栏面板列表(与「插件」「定时任务」同级) | 插件坞 行(sidebar.panellist,id plugin-dock,order 60) |
全局门户:默认点击即跳转右栏 |
| 右侧栏 | 插件坞 标签页(tab type id dsh-plugin-dock,kind plugin-dock) |
主要工作面板:搜索、快捷动作、右侧面板、插件页、已安装插件 |
| 右侧栏「+」指南 | 右侧栏指南条目(order 45) |
从右栏指南里再次打开插件坞 |
| 对话框输入框工具行左侧 | 插件坞按钮(conversation.input.left,id plugin-dock-launcher,order 35) |
一键打开右栏插件坞,最快的「一边对话一边调用」路径 |
左侧栏入口(门户行为)
- 点左侧栏「插件坞」:默认不会停在整页,而是把主栏切回对话,并在右栏打开插件坞(
leftEntryOpensDock默认true)——因为插件的价值在于「不离开对话」。 - 需要完整控制中心时,在插件坞页或面板底部点「打开完整设置页」:此时才真正进入整页(状态栏 / 目录 / 设置 / 自检 / API 文档都在整页里)。
- 如果当前没有会话、右栏无法打开,会留在整页并给出温和提示,页面内容照常可用;若在这约 0.5 秒里你已经切到别的面板,则不会被强行拉回(导航守卫)。
右栏插件坞(工作面板)
右栏面板从上到下:
- 标题行:图标 + 插件坞 + 会话标识 + 搜索框 + 刷新 + 设置页(右栏的收起/关闭由 DSH 外壳控件提供,插件坞不重复实现,CONTRACT §1.2 已同步);
- 快捷动作:内置「上下文归纳」卡片(用量条 + 一键归纳)等适配器驱动动作;
- 右侧面板:枚举其他插件注册过的右栏 tab 类型,点一下就在右栏打开该插件;
- 插件页:枚举其他插件注册的左侧栏页面,提供「整页打开」;若该插件同时有右栏 tab,还提供「右侧打开」;
- 已安装插件:来自 Host 目录 API 的完整清单(图标、名称、版本、安装方式徽章、client/host 能力徽章、收藏星标、描述、来源包),点行展开详情与可用入口;
- 底部状态:偏好同步状态(已保存 / 本地缓存 / 无宿主)、设置页入口、刷新。
搜索框同时过滤上述所有区块(名称 / 标题 / 描述)。面板在 320–900 px 宽度与全屏下均可用,窄屏自动全屏,密集模式(density=compact)会收紧行高。
输入框一键按钮
输入框左侧(+ 按钮附近)的插件坞按钮:一次点击把插件坞开在右栏,不打断当前对话。没有会话、右栏打不开时,按钮会退到插件坞整页(sessionStorage 一次性允许标记),真正的调用入口在页内显示「先打开一个会话」的空状态,而不是静默失败。
右栏指南入口
右栏标签条上的「+」会打开右栏指南;指南里有「插件坞」条目(order 45),点击即打开插件坞标签页。插件坞标签页 keepMounted:切走再切回不会丢失搜索词和展开状态。
安装与卸载
前置条件
- DSH 0.2.1 系 Web/Desktop 组合,desktop profile 已由 DeepSeek Harness 初始化一次;
- Host 半要求 Node ≥ 20(随 DSH 运行,无需你自己起进程);
- 本仓库无构建步骤、无第三方依赖,直接以本地目录安装。
推荐安装(桌面版:一行脚本,不需要 pnpm / CLI)
在你自己的终端里:
cd /Users/sunchujun/Desktop/deepseek/dsh-plugin-dock
sh scripts/install-manual.sh
这个脚本不依赖 pnpm 或 dsh plugin(Desktop profile 由 Electron 应用托管,CLI 可能拒绝写入),而是手工完成 link: 安装的等价动作:备份 profile 的 package.json → 把本仓库软链进 <profile>/node_modules/dsh-plugin-dock → 写入 link: 依赖与 dsh.profile.bundles 行 → 验证 JSON 与 Node 解析。本机就是用它安装的(2026-10-28 已装入真实 desktop profile)。运行中的 DSH 可能通过 profile HMR 直接热加载 Host 半;客户端界面在**刷新 GUI(⌘/Ctrl+R)**后出现,若仍未出现则彻底退出并重启 DSH。快速自检:不带 token 请求 http://127.0.0.1:57598/dsh-plugin-dock/api/health 返回 401 JSON 说明 Host 半已加载;带页面 token 请求则返回 ok:true。
也可以直接用打包好的安装包(同样不碰源码目录):
dsh plugin --profile desktop add file:/Users/sunchujun/Desktop/deepseek/dsh-plugin-dock/dist/dsh-plugin-dock-0.1.0.tgz
备用安装(CLI / pnpm)
在你自己的终端里(不要在沙箱内、不要写 ~/.dsh 以外的路径):
cd /Users/sunchujun/Desktop/deepseek/dsh-plugin-dock
sh scripts/install-local.sh
脚本会:
- 校验仓库(
index.js/client.js/package.json); - 备份 profile 的
package.json为package.json.bak.<时间戳>; - 调用 DSH 插件管理器做本地 link 安装(
link:不下载 registry 包); - 幂等地把
dsh-plugin-dock补进dsh.profile.bundles(保留 2 空格缩进); - 打印重启与卸载说明。
桌面版注意:npm 全局安装的 dsh 会拒绝 desktop profile(报 “profile "desktop" is managed exclusively by the Electron application”)。install-local.sh 会自动优先使用 DeepSeek Harness 自带的 CLI(自带 pnpm):
"/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh" \
plugin --profile desktop add "link:/Users/sunchujun/Desktop/deepseek/dsh-plugin-dock"
手动安装命令
如果不用脚本,等价的核心命令是:
dsh plugin --profile desktop add link:/Users/sunchujun/Desktop/deepseek/dsh-plugin-dock
然后确认 profile 的 package.json(~/.dsh/profiles/desktop/package.json)里有 bundle 行:
{
"dsh": {
"profile": {
"bundles": [
"@deepseek-ai/dsh-base",
"dsh-plugin-dock"
]
}
}
}
dsh plugin add link:… 在插件管理器可用时会自动补这一行;若没有,手动把 "dsh-plugin-dock" 追加到 dsh.profile.bundles 末尾。
重启 DSH 后生效:完全退出 DeepSeek Harness 再打开(web profile 则重启 dsh web)。启动后左侧栏出现「插件坞」,输入框左侧出现插件坞按钮,右栏「+」指南里出现插件坞条目。
手动兜底
CLI 装不上(无 pnpm / 离线 / 权限)时,按顺序试:
DeepSeek Harness:插件 → 添加插件 → 本地目录,选择
/Users/sunchujun/Desktop/deepseek/dsh-plugin-dock;用应用自带 CLI(见上);
在 profile 目录手动 link:
cd ~/.dsh/profiles/desktop pnpm add "link:/Users/sunchujun/Desktop/deepseek/dsh-plugin-dock"再手动把
dsh-plugin-dock加进dsh.profile.bundles,重启 DSH。
卸载
桌面版(与 install-manual.sh 配套,同样不需要 pnpm):
cd /Users/sunchujun/Desktop/deepseek/dsh-plugin-dock
sh scripts/uninstall-manual.sh
其它 profile(CLI / pnpm):
sh scripts/uninstall-local.sh
脚本会从 dsh.profile.bundles 删掉 dsh-plugin-dock,再尽力调用 dsh plugin --profile desktop remove dsh-plugin-dock(pnpm 报错不会中断卸载),最后提示重启。
等价手动卸载:
dsh plugin --profile desktop remove dsh-plugin-dock
再把 "dsh-plugin-dock" 从 profile package.json 的 dsh.profile.bundles 中删掉,重启 DSH。插件自己的数据目录 <profile>/.dsh-plugin-dock/ 默认保留(重装后收藏/最近/设置还在);要彻底清空可手动 rm -rf ~/.dsh/profiles/desktop/.dsh-plugin-dock。
回滚:脚本留下
~/.dsh/profiles/desktop/package.json.bak.<时间戳>,直接拷回即可恢复安装前的 package.json。
使用流程
聊天中调用
- 打开一个会话,点输入框左侧的插件坞按钮(或右栏「+」→ 插件坞);
- 「快捷动作」里点「一键归纳」:对当前会话执行上下文归纳,显示用量与释放量(需已安装上下文归纳插件,见下);
- 「右侧面板」里点任意插件 chip:该插件直接在右栏打开,对话保持可见;
- 「插件页」里点「整页打开」:跳转到该插件的整页(这是诚实的兜底,不假装能塞进右栏);点「右侧打开」:优先用右栏 tab。
收藏与最近
- 已安装插件行右侧星标可收藏;快捷区与列表会尊重收藏与顺序;
- 打开过的插件会进入最近(最多 50 条),面板重新打开时优先展示;
- 所有偏好通过 Host 的
/api/prefs持久化(见下),右栏、整页、多个窗口共享;Host 不在时退回 localStorage 缓存并在底栏显示「本地缓存」。
整页打开
左侧栏「插件坞」默认是门户(开右栏);在面板底部点「设置页」或页面上点「打开完整设置页」即可进入整页控制中心:
- 顶栏:标题、副标题、「在右侧打开插件坞」主按钮、「刷新目录」次按钮;
- 状态条:Host 健康(目录数量 / prefs 版本 / profile)、发现数量(左侧面板 N · 右侧面板 M · 已安装插件 K);
- 统一目录:快捷动作 / 右侧面板 / 插件页 / 已安装插件四段,带搜索、收藏;
- 设置卡:左栏入口行为、自动打开、是否显示插件页、密度;
- 自检卡:调用
/api/self-check显示 pass/warn/fail 与 API 索引; - 帮助卡:安装、使用、接口速查(完整文档即本 README)。
设置与数据
| 设置项 | 默认 | 说明 |
|---|---|---|
leftEntryOpensDock |
true |
点左侧栏入口时直接开右栏并回到对话 |
autoOpenDock |
true |
安装后首次打开页面时自动打开一次右栏(有 localStorage 标记,不会反复弹) |
showPageEntries |
true |
面板中是否展示「插件页」区块 |
density |
comfortable |
comfortable / compact |
lastSection |
quick |
上次停留区块;仅作 UI 记忆 |
上下文归纳快捷动作
- 需要已安装
dsh-context-summarizer(上下文归纳)插件——本机工作区版本在/Users/sunchujun/Desktop/deepseek/dsh-context-summarizer; - 卡片显示当前上下文占用(tokens / 窗口 / 比例),「一键归纳」按钮在请求中禁用,成功后显示释放的 tokens 与摘要摘录;
- 错误码映射:
busy→ 「会话正在归纳或运行中」、noop→ 「历史太短,暂不需要」、unauthorized→ 「刷新页面后再试」、其它 → 可读错误; - 未安装 / token 缺失 / 无会话:卡片显示安静的空状态与「打开插件页」兜底,绝不阻塞整个面板。
设计原则
为什么是右侧窗口
插件坞最核心的判断:调用插件的时机,几乎总在对话进行中。把插件页面塞进主栏意味着离开对话、丢失上下文、回来还要重新定位。DSH 的右侧栏本来就是「与对话并排的工作面」,因此插件坞把自己做成右栏的一个 tab 类型(sidebarRightTabs.register)——打开任何插件都不替换对话,主栏永远是会话。窄屏时右栏自动全屏,保证 320 px 也点得到;桌面宽屏时保持并排。
左栏门户行为
左栏「插件坞」对 DSH 用户来说是发现入口,不是又一个整页应用。所以默认行为是「门户」:短暂挂载整页 → 把主栏切回对话(ctx.layout.selectPanel(null),null 即 Conversation)→ 在 0 / 120 / 400 ms 重试打开右栏 tab;成功即完成跳转,失败才留在整页并提示(例如当前没有会话)。失败回退带导航守卫:如果在这约 0.5 秒里你已经切去了别的页面,插件坞不会把你强行拉回来,只是安静结束。「打开完整设置页」按钮用 sessionStorage['dsh-plugin-dock.allow-page']='1' 明确表达「这次我要整页」,mount 后即清除标记,之后不会粘住。
为什么不重写别的插件 UI
- 只枚举入口,不复制界面:左侧入口来自
ctx.slots.entriesOfSlot('sidebar.panellist'),右栏 tab 来自ctx.sidebarRightTabs.entries();插件坞只渲染入口按钮/卡片,真正的 UI 仍由原插件渲染。 - 只调公开 API:打开右栏用
ctx.sidebarRight.openTab(kind),打开整页用ctx.layout.selectPanel(id);跨插件动作只走对方公开的 HTTP API(如上下文归纳的/dsh-ctx-summary/api/*),不读对方 DOM、不 import 对方内部模块、不改全局样式。 - 失败即降级:某能力缺失(无右栏 tab、无页面、无 token、无会话)时只损失对应按钮,不抛异常、不破坏应用其余部分;错误进入页面内的轻量提示条,不使用
alert()。 - 主题一致:颜色只用
var(--dsh-…)主题变量(带中性 fallback),深浅色与窄屏跟随应用;样式类统一dpd-前缀,只挂在自己的插槽根节点内。
数据与存储
Host 持久化(权威)
- 路径:
<profileDir>/.dsh-plugin-dock/state.json(例如~/.dsh/profiles/desktop/.dsh-plugin-dock/state.json);Host 运行时优先用 DSH boot 提供的profileContext服务({name, dir, home})定位 profile,DSH_*环境变量只在没有该服务时兜底; - 目录
0700、文件0600;写入为同目录临时文件 + rename 的原子替换;损坏文件回退默认值并在自检里给出 warn; - 文件形如
{"revision":N,"updatedAt":<ms>,"prefs":{…}},revision单调递增,多个标签页/窗口通过 mtime 校验共享; - 内容:
{
"version": 1,
"favorites": ["dsh-code-runner", "dsh-context-summarizer"],
"order": ["dsh-code-runner"],
"recent": [{ "id": "code-runner", "at": 1730000000000 }],
"settings": {
"leftEntryOpensDock": true,
"autoOpenDock": true,
"showPageEntries": true,
"density": "comfortable",
"lastSection": "quick"
}
}
- 校验:
favorites/order≤ 200 条去重字符串(≤ 160 字符);recent≤ 50 条{id, at};settings布尔 +density∈{comfortable,compact}+lastSection∈{quick,tabs,pages,plugins};未知键丢弃;非法 patch 返回400 bad-request。 - 合并语义:POST 的
patch局部覆盖当前值;recent按 id 合并并保留最新 50 条。 - 不存任何密钥;不写 profileDir 之外的路径。
Client 缓存(即时绘制)
| localStorage 键 | 用途 |
|---|---|
dsh-plugin-dock.cache.v1 |
prefs 本地镜像({prefs, savedAt, revision}),Host 不可用时即时绘制 |
dsh-plugin-dock.autopened.v1 |
自动打开只做一次的标记 |
dsh-plugin-dock.allow-page(sessionStorage) |
允许打开整页的门闩,mount 后清除 |
清空方法:浏览器 DevTools 删除上述键,或删除 Host 的 state.json(两者互相独立,Client 会在下次请求时以 Host 为准)。
对外开放接口
Host 半注册一个前缀路由 /dsh-plugin-dock,所有响应都是 JSON(图标除外),统一信封:
{ "ok": true, ... } // 成功
{ "ok": false, "error": { "code": "unauthorized", "message": "…" } } // 失败
鉴权
- 每个 Host 进程启动时生成随机 token(
crypto.randomBytes(24)),通过ctx.webServer.tapIndex注入页面:window.__DSH_PLUGIN_DOCK__ = { token: "…" }; - 所有
/api/*请求必须带请求头x-dsh-plugin-dock-token: <token>(常量时间比较); - 唯一例外:
GET /api/icon/<id>也接受?token=查询参数——<img>标签无法附加请求头,所以图标 URL 用查询参数携带同一个 token(Origin /sec-fetch-site校验不变)。页面内部使用这种 URL;不要把它截图/贴到聊天里,token 等同页面 API 的调用凭证; - 若请求带
Origin,必须与ctx.webServer.getOrigin()同源(或 loopback 的 host 一致);sec-fetch-site: cross-site一律拒绝; - 错误码:
401 unauthorized、403 forbidden、404 not-found、405 method-not-allowed、413 too-large、500 internal;Host 侧请求体上限 64 KiB;页面侧对插件坞 Host API 的单次请求超时 15 s(上下文归纳适配器另有 12 s / 10 分钟预算(与归纳插件自身的 10 分钟预算一致,避免慢模型下假超时);超时与传输错误在客户端映射为timeout/network,不会抛出未处理异常);所有 JSON 响应cache-control: no-store。
路由表
| 方法 | 路径 | 请求 | 响应 |
|---|---|---|---|
GET |
/dsh-plugin-dock/api |
– | {ok:true,name,version,endpoints:[{method,path,description}]} |
GET |
/dsh-plugin-dock/api/health |
– | {ok:true,name,version,profile,profileDir,dshHome,platform,node,pid,time,catalogCount,prefsRevision} |
GET |
/dsh-plugin-dock/api/catalog |
– | {ok:true,profile,profileDir,generatedAt,scanMs,counts:{plugins,bundles,deps,client,host},plugins:[Plugin]} |
GET |
/dsh-plugin-dock/api/prefs |
– | {ok:true,revision,updatedAt,prefs} |
POST |
/dsh-plugin-dock/api/prefs |
{patch:{…}} |
{ok:true,revision,updatedAt,prefs} |
GET |
/dsh-plugin-dock/api/self-check |
– | {ok:boolean,time,checks:[{id,label,status,detail}],summary:{pass,warn,fail}} |
GET |
/dsh-plugin-dock/api/icon/<node> |
头或 ?token= |
SVG/PNG 字节(image/svg+xml / image/png,cache-control: private, max-age=300;<img> 用查询参数认证,因 URL 携带凭证而用 private 避免共享缓存) |
GET |
/dsh-plugin-dock/api/docs |
– | {ok:true,markdown:"…"}(页面内的短接口文档) |
/api/icon/<node> 的 <node> 是插件 id 的 URL 编码(如 %40linxin666%2Fdsh-client-ui-task-board),Host 从最新扫描结果映射到图标文件,从不接受任意路径。该路由是唯一接受 ?token= 的端点:<img src> 无法设置请求头,页面把同一个 token 放进查询参数;响应本身不含 token,但 URL 会出现在浏览器网络面板里,按凭证对待即可。
Plugin 条目:
{
"id": "@linxin666/dsh-client-ui-task-board",
"name": "dsh-client-ui-task-board",
"displayName": "任务板",
"version": "0.4.11",
"description": "…",
"icon": "/dsh-plugin-dock/api/icon/…", // 仅当图标文件可读且 ≤ 256 KiB
"install": "link", // link | file | registry | unknown
"bundle": true,
"dependency": true,
"client": true,
"host": true,
"enabled": true,
"self": false, // 本插件自己为 true
"dir": "/abs/path" // 真实路径,仅供参考
}
目录规则:profile dependencies 与 dsh.profile.bundles 的并集;排除 @deepseek-ai/ 开头的宿主自带组合(计数保留、列表不显示);本插件标 self:true;按 bundles 优先、再按显示名排序。profile 不可读时降级为空目录 + 警告,不报 500。
curl 示例
从 DSH 页面 DevTools 控制台取得 token 与页面地址:
window.__DSH_PLUGIN_DOCK__.token // 只读,勿外传
location.origin // 例如 http://127.0.0.1:57598
ORIGIN='http://127.0.0.1:57598'
TOKEN='<从上一步取得的 token>'
# 健康检查
curl -sS \
-H "x-dsh-plugin-dock-token: $TOKEN" \
-H "Origin: $ORIGIN" \
"$ORIGIN/dsh-plugin-dock/api/health"
# 插件目录
curl -sS \
-H "x-dsh-plugin-dock-token: $TOKEN" \
-H "Origin: $ORIGIN" \
"$ORIGIN/dsh-plugin-dock/api/catalog"
# 读偏好
curl -sS \
-H "x-dsh-plugin-dock-token: $TOKEN" \
-H "Origin: $ORIGIN" \
"$ORIGIN/dsh-plugin-dock/api/prefs"
# 改偏好(局部 patch)
curl -sS -X POST \
-H "x-dsh-plugin-dock-token: $TOKEN" \
-H "Origin: $ORIGIN" \
-H "content-type: application/json" \
--data '{"patch":{"settings":{"density":"compact"}}}' \
"$ORIGIN/dsh-plugin-dock/api/prefs"
# 自检
curl -sS \
-H "x-dsh-plugin-dock-token: $TOKEN" \
-H "Origin: $ORIGIN" \
"$ORIGIN/dsh-plugin-dock/api/self-check"
token 每次 Host 进程启动都会变;重启 DSH 后请重新取值。不要把它写进日志、脚本或截图。
扩展适配器
插件坞的默认扩展面是 DSH 的公开插槽——大多数第三方插件什么都不用做:
- 左侧页面:注册
sidebar.panellist({id, order, label})并挂main页面,插件坞自动把它列入「插件页」,提供「整页打开」; - 右侧面板:通过
ctx.sidebarRightTabs.register({id, kind, title, guide?, keepMounted?})+sidebar.right.pane.tabbody 注册 tab 类型,插件坞自动把它列入「右侧面板」,点 chip 即openTab(kind); - 已安装插件清单:不依赖 UI——只有 Host 半的插件也会出现在「已安装插件」里;同时具备 client/host 能力时会显示两枚徽章。
内置快捷动作仍是「适配器」形态(client.js 内的 ADAPTERS 表):每个适配器描述 { id, titleKey, subtitleKey, usesSession } 并有自己的调用/状态映射函数。v1 只注册 context-summarizer 一个适配器,其余入口全部是通用发现。
第三方扩展边界(重要):
- 宿主注入的
window.__DSH_PLUGIN_DOCK__当前只含{ token },是插件坞与 Host 之间的凭证载体;不要覆盖它或把它当公共注册表(写入即破坏自己的面板鉴权); - v1 未开放运行时 push 适配器的公共 API;想给插件坞加「快捷动作卡」的插件,请把适配器按上面的 shape 提交到本仓库(或作为未来版本的扩展点约定),而不是依赖未文档化的内部字段;
- 跨插件调用只接受对方公开 HTTP API,并显式处理 token 缺失/未安装/错误码。上下文归纳卡片就是范例:能力缺失只降级为安静空状态,绝不影响插件坞本身。
测试与自检
三条命令,全部离线、零依赖、确定性:
cd /Users/sunchujun/Desktop/deepseek/dsh-plugin-dock
npm test # Host + Client 的 node:test 套件(94 项)
node tests/self-check.mjs # 静态自检(68 项;--live 可选)
sh scripts/e2e-profile.sh # 隔离的 profile 组装证明(不碰 live profile)
# 或一次跑完:
npm run check # = npm test && npm run self-check
npm run verify # = npm run check && npm run e2e
npm test
tests/host.test.mjs:假 Cordis 上下文上跑真实apply(),覆盖 token 注入、鉴权(无 token / origin 不匹配 / cross-site)、路由分发(404/405)、health/catalog 形状与排除规则、prefs 默认值/合并/持久化/非法 patch/超大 body、icon 字节与路径穿越、self-check 信封、profile 缺失时的降级、临时目录清理;tests/client.test.mjs:在vm+ 最小 React/Hook/槽位替身上加载真实client.js,断言 loader id、模块形状、四个入口注册(左栏行 / main 页 / 输入框按钮 / 右栏 tab + 指南 + body)、最小 ctx 与完整 ctx 下apply()均不抛、disposer 可卸载、纯函数行为;测试结束即退出(无残留计时器)。
npm test 需要在仓库内、无环境变量、无网络的条件下全绿(当前 94 项:Host 45 + Client 49)。npm run check 串上静态自检,npm run verify 再串隔离 e2e。
node tests/self-check.mjs
静态、离线、无依赖;逐项打印 PASS / WARN / FAIL 表格与计数,有 FAIL 时退出码 1,只有 WARN 时退出码 0:
- 包身份与打包元数据(name / version / type / exports / files /
dsh.bundle.patch/dsh.client.platform/ inject 列表); - 必需文件存在(两半、patch、icon、README、CONTRACT、tests、scripts,含
install-manual.sh/uninstall-manual.sh);shell 脚本可执行且sh -n通过; cordis.patch.yml的insert行(id + name 恰好一条);- 语法:
node --check index.js;client.js用vm.Script按 classic script 编译并做一次 loader 冒烟; - Client 静态守卫:只
require('react')、无import/export、无alert()/confirm()/document.write、固定契约字面量、公开 API 调用、只 fetch 同源、契约存储键; - Host 静态守卫:只 import
node:*/相对路径、路由前缀、token 头/全局、webServer.register/tapIndex、状态目录、无child_process/eval、无第三方require、name/inject/apply/__test__导出; - 图标(
<svg+viewBox+xmlns,< 64 KiB); - CONTRACT 固定标识逐个 grep(路由前缀、token 头、token 全局、tab kind/id、panel id、composer id、order 60/45/35);
- README 章节词、安装命令(含 manual 脚本)、状态路径、curl 示例、全部 8 个路由行、英文摘要;
- 测试文件存在且非平凡、测试只 import
node:*/相对路径、不碰网络与 live profile、package.json的test/self-check/check脚本。
可选 live 模式(对一个正在运行的 DSH 断言真实信封):
node tests/self-check.mjs --live http://127.0.0.1:57598 --token '<页面 token>'
不带 --live/--token 时该组显示 WARN live/skipped,不影响静态结果。
sh scripts/e2e-profile.sh
在 mktemp -d 出来的隔离 DSH_HOME 里做最佳努力的组装证明,绝不触碰真实 profile:
DSH_HOME=$TMP dsh plugin --profile e2e add "link:<repo>"(可用应用自带 CLI,带 pnpm);- 把
dsh-plugin-dock写进dsh.profile.bundles; DSH_HOME=$TMP dsh e2e --dump-config,断言输出含# == dsh-plugin-dock、- id: dsh-plugin-dock、name: dsh-plugin-dock;- 无论成败都删除临时目录。
离线 / pnpm 缺失导致 CLI 装不上时,脚本会退化为手写 link(等价于 pnpm link: 的 node_modules 软链)后继续断言——这是「证明 cordis.patch.yml 能被正确组装」,不是安装测试。环境完全不支持 --dump-config 时打印 SKIP 并退出 0;只有 dump 成功但插件行缺失(真正的组装失败)才退出 1。
验收
node tests/self-check.mjs # 无 FAIL(当前 67 pass / 1 warn / 0 fail)
npm test # 全绿(94/94)
sh scripts/e2e-profile.sh # PASS 或 SKIP(带原因)
# 或:npm run verify
已知限制
- 需要 DSH 0.2.1 系客户端契约(
slots/sidebarRightTabs/sidebarRight/layout/locale);旧版本组合上相关入口会安静降级。 - 只接受公开插槽发现:插件若没有注册
sidebar.panellist页面也没有右栏 tab,就只在「已安装插件」清单中出现,不能被直接打开(这是诚实的能力边界,不伪造入口)。 - 右栏 tab 需要当前有会话:没有会话时快捷/右侧打开会提示「先打开一个会话」,而不是假装成功。
- 上下文归纳适配器依赖
dsh-context-summarizer已安装、页面 token 全局存在;不同版本响应的嵌套/扁平形状已做兼容,但插件被禁用/版本不兼容时卡片显示未安装空状态。 - 自动打开只在安装后第一次打开页面触发一次(localStorage 标记),且仅在对话为主栏时;清掉浏览器站点数据后会再触发一次。
- 面板只在自身插槽内渲染,不接管应用外壳;右栏的收起/关闭由外壳控件负责,插件坞不重复提供(CONTRACT §1.2);右栏宽度低于 320 px 时以应用自身的窄屏行为为准。
- 图标服务只覆盖已安装插件中带可读
icon.svg/icon.png(≤ 256 KiB)的包;没有图标的插件使用内置中性图标。 - 偏好数据按 profile 隔离;token 每进程随机、不持久化;
/api/*仅本机页面可用,不做远程访问。 - 桌面版安装优先用
scripts/install-manual.sh(无需 pnpm/CLI),也可用应用内插件管理;npm 全局dsh会拒绝 desktop profile,见「安装与卸载」。
文件结构
| 文件 | 作用 |
|---|---|
package.json |
bundle 清单:dsh.bundle.patch(Host 半)+ dsh.client(Web 半),exports 导出两半与图标 |
cordis.patch.yml |
把 { id: dsh-plugin-dock, name: dsh-plugin-dock } 插入 profile 的 bundle patch |
index.js |
Host 半(ESM):token 注入、/dsh-plugin-dock 路由、插件目录扫描、prefs 持久化、icon 服务、self-check |
client.js |
Web 半(classic script):左栏入口 + 整页、右栏 tab + 指南、输入框按钮、上下文归纳适配器、发现/收藏/搜索/设置 UI |
icon.svg |
原创图标(左侧栏 + 右侧面板二层结构) |
tests/host.test.mjs |
Host 半 node:test 套件(假 Cordis 上下文 + 临时 profile fixture) |
tests/client.test.mjs |
Client 半 node:test 套件(vm + React/槽位替身) |
tests/self-check.mjs |
静态自检 + 可选 live 模式(本文档「测试与自检」) |
scripts/install-local.sh |
本地 link 安装(自动选 CLI、备份、幂等补 bundle 行) |
scripts/uninstall-local.sh |
卸载(删 bundle 行、尽力移除包、保留数据) |
scripts/install-manual.sh |
桌面版安装(无 pnpm/CLI:软链 + 写依赖与 bundle 行 + 校验) |
scripts/uninstall-manual.sh |
桌面版卸载(与 install-manual.sh 配套) |
scripts/e2e-profile.sh |
隔离 profile 的组装证明(不碰 live profile) |
dist/dsh-plugin-dock-0.1.0.tgz |
打包产物:可 dsh plugin add file:… 或归档分发的可移植安装包 |
CONTRACT.md |
实现契约(标识、API、行为、测试要求的唯一事实来源) |
README.md |
本文档 |
English summary
Plugin Dock (dsh-plugin-dock) is a DSH bundle plugin with two halves. The Host half (index.js, ESM, zero dependencies) injects a per-process random token into the Web page and exposes a token-gated, same-origin /dsh-plugin-dock HTTP surface: installed-plugin catalog with icons, its own persisted dock preferences, a self-check endpoint, and a short docs endpoint. State lives in <profileDir>/.dsh-plugin-dock/state.json (atomic write, no secrets).
The browser half (client.js, one classic script, React only) contributes three surfaces without touching the app shell: a left-sidebar 插件坞 row (its default behaviour is a door — open the dock in the right column and return the main column to the conversation), a right-sidebar dock tab with a guide entry (search, quick adapters, other plugins' right-panel chips and left-sidebar pages, installed-plugin catalog with favourites/recent, prefs-sync footer), and a composer launcher button that opens the dock in one click. Other plugins are discovered through public slots (sidebar.panellist, sidebarRightTabs) and opened through public APIs (sidebarRight.openTab, layout.selectPanel); nothing re-implements or DOM-hacks another plugin's UI. The built-in flagship adapter calls the installed context-summarizer's own HTTP API (/dsh-ctx-summary/api/*, its own token) and degrades to a quiet empty state when that plugin is absent.
Install locally with sh scripts/install-manual.sh (no pnpm or CLI needed: symlink + link: dependency + bundle row, then refresh/restart DSH); scripts/install-local.sh covers the CLI/pnpm path used by non-reserved profiles, and dist/dsh-plugin-dock-0.1.0.tgz is a portable package. The Desktop application's bundled CLI is preferred over an npm-installed dsh, which refuses the desktop profile. Verify with npm test, node tests/self-check.mjs (add --live <origin> --token <token> to probe a running instance) and sh scripts/e2e-profile.sh (isolated --dump-config composition proof; prints SKIP when the environment cannot compose at all). The icon route additionally accepts the same token as ?token= because an <img> cannot set headers; every other route is header-only.
License: MIT.
No comments yet. Be the first to write one.