# 完整 Toolbox 插件开发指南(独立开发路径) > 本文面向完整 Toolbox 的工具插件作者,不是 `dsh-flowglass` 的安装或使用文档。流镜是默认产品;其安装、构建与 better-sidebar 集成请见 [`README.md`](README.md)。 > 配套阅读:`REBUILD.md`(目录结构/重建/迭代流程)。本文只讲**怎么写一个新工具插件**和**必踩的坑**。 ## 架构一句话 `tbx` 框架插件(Host 注册表 + Client 面板壳)+ N 个 Host-only 工具插件。工具经 `ctx.get('toolboxRegistry').register(...)` 挂进框架,Tab 自动出现;插件停止时 Tab 级联消失。 ## shared/host.js 可用助手(自动拼接到每个 Host-only 工具包开头) - 基础:`esc` / `fmtSize` / `tryRegisterTool`(500ms 快重试幂等注册,注册成功后降 2s 慢心跳,框架重启自动重挂)/ `b64encode` / `b64decode` - 会话日志:`makeSessionLogReader(ctx, sessionQuery)`(缓存,count 不变即命中) - 持久化:`readJsonStore` / `writeJsonStore`(`<工作区>/.dsh-dynamic-toolbox/`,持久化失败要出警告) - 内容产物:`pluginDataDir(key)` → `<工作区>/.dsh-dynamic-toolbox/data//`(人会直接打开的产物——附件/导出件——放这里,与 .dsh-dynamic-toolbox 内部状态分家;根目录不散落插件文件夹,.gitignore 一行 `.dsh-dynamic-toolbox/data/` 收工) - 工作区:`resolveWorkspace(ctx, root, session)` → `{ root, session }` - **AI 工具**:`makeLlmHelper(ctx)` → `{ available, resolveRoute(st), chat(st, system, user, timeoutMs?, track?), rollup(root, tool), routeRow(st, route, note?) }`。 `chat` 返回 `{ a, ms, out, route }` 或 `{ err, ms, route }`(120s 超时守卫;system 并入首条 user 消息,最稳); `track = { root, session, tool }` 让每次调用结果异步追加进用量台账 `.dsh-dynamic-toolbox/toolbox-ai-usage.json`(cap 100,不阻塞响应),`rollup(root, tool)` 取该工具累计 `{ calls, out }` 展示在路由行; 模型清单缓存监听 `llm/adapters-updated` 自动失效(provider 拓扑变化不陈旧); `routeRow` 渲染 provider/model 双下拉(provider 切换走 `data-action-onchange="route"`,工具 handler 里把 `st.model` 清空即可)。 参考实现:`plugins/aiassist/tool.js`(AI 助手 7 合一:PRESETS 表驱动 问答/翻译/优化/评审/提交信息/摘要/对比, preset = { id, label, mode:'single'|'multi', input:'text'|'fileOrText'|'gitsource'|'sessionlog', params, sys(st), store, cap }—— 新增 AI 用途只需在 PRESETS 加一行;大本体进闭包;台账 tool 键 = preset id)。 多合一计算类参考:`plugins/calc/tool.js`(子模式命名空间 `st.` + 子模式芯片切换)。 ## 新工具三步 1. 建文件夹 `plugins//`,写 `tool.js`(plain JS,禁 import/require/TS,文件末尾 `return` 插件对象); 2. `build/plugin-catalog.mjs` 的 PLUGINS 表加一行(key/idPrefix/order/name/purpose/inject/hostFiles;idPrefix 限 3-6 小写字母)。要能被编译合集 CLI 选择,补 `bundle` 元数据:`{ selectable, defaultLabel, aliases, dependencies, conflicts, scope }`——scope 取 `workspace`(Host-only 默认,按工作区实例)或 `process`(含 Client 半强制 process,避免按 root 重复批准); 3. `node make-payloads.mjs`(生成 plugin.json + payload.json + 总清单)→ `cordis_define` ← `plugins//payload.json` → `cordis_run(mode: run)`。Host-only 免批准。 **建议同步写一个 `smoke/sim-.cjs`**(mock ctx/服务求值 impl,断言面板协议与 state 契约;参考 sim-calc 的零服务纯逻辑形态、sim-jira 的 mock credentials+subprocess 形态)。`node smoke.mjs` 一键全量回归。注意沙箱禁命名管道:子进程 stdout/stderr 用临时文件重定向,别用 pipe。 ## impl 骨架 ```js return { name: 'xxx-tool', inject: ['fs', 'subprocess', 'timer'], // 硬依赖才进 inject apply(ctx) { const render = (st, busy) => '
…(tb- 共享类)…
' const handler = async ({ action, fields, state, root }) => { const st = state || { /* 初始状态,纯 JSON */ } // if (action === 'xxx') { … } return { ok: true, html: render(st, false), state: st } } tryRegisterTool(ctx, { id: 'xxx', label: 'XXX', order: 10 }, handler) // shared/host.js 提供,500ms 重试幂等注册 }, } ``` ## 面板契约(Host ↔ Client 壳) | 方向 | 约定 | | --- | --- | | Host → Client | `{ ok, html, state }`;html 经 `dangerouslySetInnerHTML` 渲染;可附带 `copy` 字符串 → Client 壳写剪贴板并提示(复制提交信息/译文等) | | Client → Host | 点击 `[data-action]` 元素触发;元素自身 `data-*`(如 `data-key`)经 `fields.__el` 回传 | | 表单 | input/textarea/select 加 `data-field="name"`,任何动作提交时全部收集进 `fields`;回车自动触发 `data-action="query"` | | 下拉联动 | select 加 `data-action-onchange="xxx"`:change 即自动触发该动作(无需切换按钮);provider→模型列表联动就用它。联动请求在途时面板内 select 自动锁定(失败自动恢复);Client 壳按工具发请求序号,过期响应直接丢弃——handler 无需关心并发乱序。**注意:面板是 innerHTML 裸 DOM,React 合成 onChange 对它永不派发,壳用的是抽屉根节点上的原生 change 监听——新事件类型别想当然走 React onXxx** | | 分类归属 | 导航是「搜索 / 分类 / 工具」三行:工具经 `DEFAULT_CAT`(toolbox/client.js)归入 AI/开发/会话/系统,**新工具默认落「开发」**,要归别的类就改 DEFAULT_CAT;终端用户可在管理树(齿轮)里把节点拖到别的分类(覆盖存 localStorage `dsh.toolbox.cats`,优先级高于 DEFAULT_CAT) | | 环境 | 每次动作透传 `root`(当前工作区路径)与 `session`(当前会话 ID,轨迹类工具用) | | live 叠加层(0.1.5) | Client → Host 额外透传 `live`(仅流镜使用):`{ sessionId, revision, attempts, settled }`——Session Controller 事件窗的瞬时 `assistant/live-chunk` 在 Client 预折叠成 attempt 快照,Host 折叠器与持久事件走同一逻辑;`settled` 携带最近结算 attempt 的 `firstSeq` 供 UI 连续性。经 `shared/registry.js` 的 `panel()` 透传,工具 handler 自行校验形状(见 `plugins/flow/tool.js`) | | state | 纯 JSON,宿主侧按工具持有,每次动作原样回传 | ## 右侧栏承载(流镜专属,0.1.5) `plugins/toolbox/client.js`(仅 `bundleId === 'flow'`)按层级注册承载面,同一时刻只有一条路径生效: 1. `ctx.inject(['sidebarRightTabs'])` → `tabs.register({ id: 'dsh-flowglass/native', kind: 'dsh-flowglass:flow', title, guide })` 注册原生 page type;同一 definition id 在 `sidebar.right.pane.tab` Slot 注册 body(`FlowglassNativeTabBody`,props:`sessionId`/`useSessions`/`useTabInfo()`→`tab.visible`)。自有入口点击 → `ctx.sidebarRight.openTab(kind)` 自动展开。 2. 原生不可用时 `ctx.inject(['betterSidebar'])` → `service.registerTab(descriptor)`(descriptor 带 `description`;`single: true` 只保证目标 pane 内去重)。原生接管时该桥自动撤销、原生消失时自动恢复。 3. 都不可用 / 注册抛错 → 固定右侧兜底面板(覆盖显示、不挤压主会话、不可拖成悬浮窗)。承载方式固定按上述优先级自动适配,不再暴露显示方式偏好。 ## 必踩的坑(实战血泪) 1. **样式用框架共享设计系统(`tb-` 类),不要自己内嵌 `