# dsh 扩展点调研笔记 > 记录于 2026-09-30,基于本机安装的 **DeepSeek Harness 桌面端 0.2.0-rc.2**(`G:\deepseek_agent`)。 > dsh 处于 developer preview,官方明说 "THERE WILL BE COMPATIBILITY-BREAKING CHANGES" —— 下面每条结论都标了取证位置,版本一变请重新核对。 ## 1. 桌面端是什么 | 项 | 值 | 取证 | |---|---|---| | 包名 / 版本 | `@deepseek-ai/dsh-desktop` 0.2.0-rc.2 | `resources/app.asar` → `/package.json` | | 定位 | "Electron desktop shell for a bundled dsh runtime and external plugins" | 同上(description) | | 应用 ID | `com.deepseek.dsh` | 同上 | | 更新源 | `https://download.deepseek.com/dsh-desk/feeds/win-x64/`(nightly) | `resources/app-update.yml` | | 发布者 | CN=Hangzhou DeepSeek Artificial Intelligence Co., Ltd. | 同上 | | 内置运行时 | `@deepseek-ai/dsh-desktop-runtime` 0.2.0-rc.2,含 **289 个 `@deepseek-ai` 包** | asar → `/dsh/package.json`、`/dsh/desktop-runtime.json` | | UI 形态 | 主 UI 是 HTTP 加载的 web 前端;asar 里的 `/renderer` 只有 welcome / 更新 / 登录页 | asar 目录树 | asar 结构:`/lib`(主进程,`main.js` 490 KB + 一堆 `preload-*.cjs`)、`/renderer`(外壳页)、`/dsh`(内置运行时)、`/node_modules`。 ## 2. 三个直接相关的 UI 扩展点 ### 2.1 文件树 → 打开文件到 Sidebar `@deepseek-ai/dsh-client-ui-sidebar-files`(客户端包 40 KB) - 命令 `workspace.files` 打开文件页(桌面默认键 `Mod+P`) - 注册:`ctx.sidebarRightTabs.register(...)`,kind `files`,档位 `builtin` - **点文件**:`useTabInfo().tab.actions.openResource('dsh-resource://file/session//')`,地址由 `@deepseek-ai/dsh-util-workspace-path` 的 `fileAddressFor` 生成 - 目录列表:`remote.workspaceFiles.list(sessionId, absolutePath)`(`@deepseek-ai/dsh-api-workspace-files` 的 Remote namespace) - 头部动作槽:`sidebar.right.tab.files.actions`(owner prop `absolutePath`) ### 2.2 文档预览 → 注册渲染器 / 控制滚动 `@deepseek-ai/dsh-client-ui-sidebar-documentpreview`(7.1 MB,含 PDF/Excel 渲染器) - 描述:**"Extensible Sidebar previews for Office documents, spreadsheets, Markdown, code, images, PDF, HTML, and plain text"** - 类型注册:`ctx.sidebarRightTabs.register(...)`,kind `text`,pattern `dsh-resource://file/**`,档位 `fallback` - **渲染器注册**:`ctx.documentPreviews.register({ id, extensions, binaryExtensions?, priority, title, loading, wrap? })`,正文以同一 id 注册到 keyed slot `sidebar.right.tab.document` - **正文收到的 props**:`resourceAddress`、`content`、`wrap`、**`scrollportRef`**、`addResource`、`setResources`,以及 `useTabInfo` / `useResource` 钩子 - 头部动作槽:`sidebar.right.tab.document.actions`;不可预览空态槽:`sidebar.right.tab.document.unpreviewable` - 档位优先级:`extension` > `builtin` > `fallback`,同档按后缀长度、再按注册顺序 → **跳行**:`scrollportRef` 是"内部滚动元素"的 ref,正文自己负责滚动;**标色**:注册一个更高 `priority` 的自定义渲染器即可完全接管绘制。 ### 2.3 槽位系统 `@deepseek-ai/dsh-client-ui-slots`(23 KB,不依赖 React) - "typed ordinary Slots and reusable Component Factories, derived props, Store seats, and renderer installation" - 渲染侧配合 `@deepseek-ai/dsh-client-ui-renderer` ## 3. 插件包的约定(照抄官方客户端包) `package.json` 关键字段(以 `dsh-client-ui-sidebar-files` 为范本): ```json { "type": "module", "main": "lib/index.js", "exports": { ".": { "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, "./client": { "types": "./lib/types/client/index.d.ts", "default": "./lib/client.js" }, "./src/*": "./src/*", "./package.json": "./package.json" }, "dsh": { "client": { "inject": ["<客户端服务包名>", "..."], "platform": "web" } }, "peerDependencies": { "@deepseek-ai/cordis": "~4.0.4" } } ``` - **`dsh.client.inject`** 声明客户端一侧依赖的服务(host 侧则用源码里的 `export const inject = [...]`) - 源码布局(同一包的开发备注):`src/client/` 下 `definition.tsx` / `store.ts` / `face.ts` / `*-node.ts` / `Body.tsx` / `Title.tsx` / `locales.ts` / `index.ts` - 构建产物进 `lib/`(`main` + `./client` 两个入口) - 官方仓库路径约定:`packages/client/ui-`(本插件是第三方,不占这个 scope) ## 4. host 侧的候选取数点 - 编辑工具:`@deepseek-ai/dsh-tool-str-replace-editor`("Model-facing view, create, literal replace, and line insert tool over the Harness filesystem service") - 工具注册/钩子:`ctx.tools.register(...)`;`ctx` 生命周期事件(官方文档见下方链接) - 官方文档:(插件 = 导出 `apply(ctx)` 的 TS 模块;`inject` 声明依赖;通过 `cordis.yml` 的 `insert` 或 `dsh plugin add` 加载) ## 5. 生态与开源价值 | 指标 | 值 | |---|---| | `deepseek-ai/deepseek-harness` | **241k stars**、28.9k forks、41 contributors、20,470 commits、MIT | | GitHub `dsh-plugin` topic | **16,885 个公开仓库**、15.8k followers | | 已有市场 | dshplugin.dev、dsh.deepseek404.com、dsh-web(8.2k★)、dsh-desktop(10.8k★) | | 官方要求 | 插件仓库请加 `dsh-plugin` topic 以便被发现 | **风险**:官方 `documentpreview` 已经是 "extensible previews for … code",自己补一个 diff 高亮渲染器是很自然的下一步 —— 有可能被官方抢先;且 developer preview 阶段 API 会漂移。 ## 6. 怎么重新取这些文档(asar 里的包 README) asar 是 `header JSON + 数据区`。快速提取某个包 README 的脚本: ```python import json ASAR = r"G:\deepseek_agent\resources\app.asar" head = open(ASAR, "rb").read(24_000_000) # header 约 3.4 MB,从 offset 16 开始 start = head.find(b'{"files"}') header, end = json.JSONDecoder().raw_decode(head[start:].decode("utf-8", "replace")) base = start + end; base += (4 - base % 4) % 4 # 数据区起点(4 字节对齐) def find(node, prefix="", out=None): out = out if out is not None else [] for name, meta in node.items(): p = f"{prefix}/{name}" find(meta["files"], p, out) if isinstance(meta, dict) and "files" in meta else out.append((p, int(meta.get("offset", 0)), int(meta.get("size", 0)))) return out for path, offset, size in find(header["files"]): if path.endswith("README.zh.md") and "dsh-client-ui-" in path: with open(ASAR, "rb") as fh: fh.seek(base + offset); print(path, fh.read(size).decode("utf-8", "replace")[:2000]) ``` 同理可提取 `lib/client.js`(编译产物)看真实实现。 --- ## 7. 已核实结论(对照 0.2.0-rc.2 内置包逐一验证,2026-09-30) > 下面每条都读自 `app.asar` 里的真实 `.js`(tsdown 产物,带 JSDoc),不是猜的。 ### 7.1 编辑数据源 —— 用 `sessionProjections` 折叠已提交事件,不是自定义 Remote - 编辑工具会提交 `tool/call` 事件:`data = { turn, step, callId, name, arguments: string }`(`arguments` 是 JSON 字符串)。取证:`dsh-agent-loop` 的 `appendToolCall`。 - `tool/result` 事件:`data = { turn, step, message: { source:{callId}, toolCallId, content, isError }, error?, meta? }`。取证:`dsh-agent-loop` 的 `appendToolResult` + `dsh-llm` 的 `createToolResultMessage`。 - **host → client 通道**:不是自定义 Remote(`dsh-api-remotes` 的 `API_REMOTE_FORWARDED_EVENTS` 是编译期写死的 29 项白名单,不含编辑事件)。正路是 **`ctx.sessionProjections.register({...})` + `wire.view`**: ```js ctx.sessionProjections.register({ key: 'followEdits', stateVersion: 1, stateSchema, // zod init: () => ({...}), apply: (state, event) => {...}, // 纯同步折叠,Object.is 闸门 wire: { viewSchema, view: state => state.last }, // zod }) ``` 取证:`dsh-session-projection/lib/index.js`、`dsh-session-stats/lib/types/projection.js`(完整范本)、`dsh-api-session-controller/lib/types/model-selection-projection.js`。 - 约束:`apply`/`view` 必须**同步**;`stateSchema`/`viewSchema` 用 **zod**(`zod@^4.4.3`)。 ### 7.2 client 读投影值 —— `faceOf(key)`,第三方无需白名单 ```js const binding = ctx.sessions.binding(sessionId) const face = binding.session.projections.faceOf('followEdits') // .subscribe(fn) + .getSnapshot() ``` - `ctx.sessions` 服务由 `@deepseek-ai/dsh-api-session-controller` 提供;`sessions.list` 是 `createSnapshotStore` 的 store(`.subscribe` + `.getSnapshot()`),`byId[id].cwd` 是工作目录。 - 取证:`dsh-client-ui-model-selection/lib/client.js`(`binding.session.projections.faceOf("modelSelection")`)、`dsh-api-session-controller/lib/client.js`(`ProjectionValueStore.faceOf`)。 ### 7.3 打开 + 跳行 —— 平台内建 - `ctx.sidebarRight.openResource(address, { params: { line } })`,文本预览声明 `{ line?: number }`,代码预览直接滚动定位。取证:`dsh-client-ui-sidebar-right` README §`ctx.sidebarRight`。 - `sidebarRight` 服务由 `@deepseek-ai/dsh-client-ui-sidebar-right` 提供。 ### 7.4 官方第三方插件形态(内置 `cordis-plugin-development` skill + 模板) - 包形态:`package.json` 带 `dsh.bundle.patch` + `dsh.client{platform,immediately,inject}`;`index.js`(host) 导 `apply`;`client.js`(browser) 走 `window.__ModuleLoader__.load({id, factory})`;patch 里 `- insert: - id + name`。 - **client 插件禁止 import 任何 `@deepseek-ai/*` 作为模块**(`dsh.client.inject` 只排激活顺序);React 从 `require('react')` 取;控件用 `--dsw-alias-*` 主题 token 自写。 - 安装走 `plugin_manager install_bundle`;验证用 `cordis_inspect_query`。 - 取证:`dsh-agent-preset/skills/cordis-plugin-development/`(SKILL.md、references/*.md、templates/decoration/*)。 ### 7.5 与内建功能的关系(开源定位的关键) - `dsh-workspace-changes`(host)+ `dsh-client-ui-deliverables`(client)**已经内置**"每轮改动文件 + 逐文件 diff 红绿对比"。所以本插件的唯一增量是**实时跟随**,不是 diff 可视化。 ### 7.6 v0.3:删除的精确行号 —— 必须在编辑【前】捕获 - 删除的内容编辑后就不在文件里了,事后(`tool/result` 之后)无论 host 还是 client 都搜不到。 - 正解:host 挂 `tools/pre-execute`(waterfall,编辑**【前】**触发,此时文件还是旧内容),`ctx.fs.readText` 读旧文件,`indexOf(oldStr)` → 行号,存进 `callId → line` 内存映射;投影在 `tool/result` 折叠时把该行号合并进 `last`。 - 事件顺序(取证 `dsh-agent-loop`):`tool/call` 提交 → `tools/pre-execute` 触发 → 工具执行 → `tool/result` 提交。所以折叠 `tool/result` 时映射已就绪。 - `tools/pre-execute` 是 waterfall,不掌决策的监听器必须 `return next()`。 - `exec` 形状:`{ name, callId, arguments(已解析的对象), agent, signal }`。 - 代价:投影 `apply` 读外部映射属于 `practices.md` 点名的「读取撕裂的非会话状态的 apply」;冷读/重放时映射为空,行号回退为 null,由 client 按 `newStr` 搜索兜底。 - 客户端另有两条易踩的坑(都已在实现里修正): 1. `documentPreviews` 的 `extensions` 是**后缀匹配**(`name.endsWith('.'+ext)`):`['*']` 和 `[]` 都匹配不到任何文件;纯文本 fallback 是靠硬编码 id 兜底的,占不到。要接管就得列具体扩展名 + `priority: 'extension'`(rank 1)压过内置 code 的 `builtin`(rank 0)。 2. 渲染器 body 的 `content.pages` 只有 `{ offset, ...page }`,**没有 `absolutePath`**;文件匹配要用资源地址里的路径(`dsh-resource://file/session//`)做后缀匹配,或 `useResource(address).value.absolutePath`。 ### 7.7 v0.3b:终端 / shell 改动 —— 只能靠文件系统监听 - shell 命令跑在子进程里直接写盘,**绕过 `ctx.fs`**:既不产生 `tool/call` 编辑事件,也不产生 `fs/observed`。所以没有任何现成事件可挂。 - **工作区根 = `session.header.cwd`**(注意是**扁平**字段,不是 `meta.cwd`)。取证:`dsh-session/lib/index.js` 的 `header = { version, id, createdAt, cwd?, parentSession?, isSeeded, ... }`;`dsh-sandbox-policy/lib/index.js` 用 `resolveWorkspaceRoot(session?.header.cwd ?? this.workspaceRoot)`。`ctx.sessions.create(id, { meta: { cwd } })` 的 `meta.cwd` 会被填进 header 的 `cwd`。 - **`ctx.fs.watch` 不够用**:`dsh-fs` README 明说「报告单个文件或**目录直接子项**的失效通知」,`dsh-fs-local` 实现是 chokidar `depth: 0`;而且目录监听触发时 `changed()` **不带改动文件名**。`remote.workspaceFiles.changes(dir)` 同理,只回目录自己的 stat。 → 拿不到「哪个文件变了」。 - 正解:host 侧用 `node:fs.watch(cwd, { recursive: true })`(Windows/macOS 支持递归;Linux 抛错 → 静默跳过),回调直接给相对文件名。 - **投递时序**:文件系统事件是异步的,而投影只在会话事件(`session/event`)驱动时重算 wire 值 —— 所以 shell 改动记录进内存映射后,要等**下一次会话事件**才发布。实践中通常是 shell 自己那条 `tool/result`(若 watcher 先到)或顺延一跳。 - 与路径 A 共用一条投影:`command: 'shell'`、`line: null`、`oldStr/newStr` 为空 → client 只打开、不跳行、不标亮(`computeHighlight` 对 `command === 'shell'` 直接返回 null)。 - 渲染器 body 收到的 props:`{ resourceAddress, content, wrap, scrollportRef, addResource, setResources }`;`scrollToLine` 靠找 `[data-textpreview-line="N"]` 元素滚动,所以自定义渲染器每行必须带该属性。