dsh-plugin-development
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseDSH plugin development
DSH 插件开发
General reference for writing dsh plugins. It records durable conventions and gotchas — the what / why, not the where — so re-locate the current files by grepping the harness rather than relying on memorized paths.
这是编写dsh插件的通用参考文档,记录了长期适用的约定和常见陷阱——重点在于是什么/为什么,而非在哪里——因此请通过搜索harness来定位当前文件,不要依赖记忆中的路径。
Locating the harness
定位Harness
- If the user or the current session has named a local checkout, first check that its
deepseek-harnessbranch is aligned with upstream; if not, fetch and pull the latestmasterbefore relying on that checkout. Then read it.master - Otherwise, analyze the published package — the packages under
@deepseek-ai/dsh-*(whatnode_modulesruns) — rather than assuming a local source tree exists.npx @deepseek-ai/dsh …
- 如果用户或当前会话指定了本地的代码库,请先确认其
deepseek-harness分支与上游保持同步;若未同步,先拉取最新的master分支再使用该代码库,随后阅读其中内容。master - 若没有本地代码库,请分析已发布的包——即下的
node_modules包(@deepseek-ai/dsh-*运行的就是这些包),不要假设存在本地源码树。npx @deepseek-ai/dsh …
Profile
配置文件(Profile)
dsh's user config lives under (default ). A profile is a named composition under , holding:
$DSH_HOME~/.dshprofiles/<name>/- — out-of-tree plugin
package.jsonplus thedependenciesmanifest with its ordereddsh.profilelist.bundlesmaintains it; never write it by hand.dsh plugin - — the user's own patch layer (id-targeted whole-
cordis.patch.ymloverrides, disables, andconfiglists). It is an override layer, not the mechanism a plugin loads through.insert - — plugins pnpm installed for this profile.
node_modules/
Layers apply over an empty entry list in order: each bundle patch in order, then the profile's , then the home-level , then each overlay. Pick a profile with (the GUI profile is the common one). A profile is unrelated to a workspace/cwd.
dsh.profile.bundlescordis.patch.yml$DSH_HOME/cordis.patch.yml--patch--profile <name>webdsh的用户配置位于(默认路径为)。配置文件是下的命名组合,包含:
$DSH_HOME~/.dshprofiles/<name>/- ——树外插件的
package.json,以及带有有序dependencies列表的bundles清单。该文件由dsh.profile维护;请勿手动编辑。dsh plugin - ——用户自定义的补丁层(基于ID的完整
cordis.patch.yml覆盖、禁用和config列表)。这是一个覆盖层,而非插件加载的机制。insert - ——为该配置文件通过pnpm安装的插件。
node_modules/
配置层按顺序应用于空条目列表:首先是顺序中的每个包补丁,然后是配置文件的,接着是主目录级别的,最后是每个覆盖层。使用选择配置文件(GUI的配置文件是常用选项)。配置文件与工作区/当前目录无关。
dsh.profile.bundlescordis.patch.yml$DSH_HOME/cordis.patch.yml--patch--profile <name>webPreset
预设(Preset)
A preset is an agent-plane composition — the tools, prompt sections, and services that build one session's agent. It is orthogonal to a profile: a profile is process/host-plane (registries, sandbox, approval, persistence, model route), while a preset decides what each agent can do.
- Each preset is one agent-plane roster (named presets include ,
standard,code,minimal,cordis), mounted once per process under a standing scope; every session naming it joins by scope parentage.online - A preset mounts its own per-agent copies (shell/filesystem tools, skills discovery + catalog loader, goals, plan mode, compaction, delegation/workflows); the host composition keeps the shared registries.
- Per-agent service rows sit inside a group carrying an realm (entry-local = one private instance per mounted session).
isolate - Use it by creating a session against a preset — ; the id is stored on the session header and resume rebuilds the same agent. The default is the user's stored choice, else the deployment default (
session.create({ agentPreset })).standard - Manage presets through the RPCs (list / select / read / copy / remove / openDocument).
agentPreset.*
预设是代理层面的组合——构建单个会话代理所需的工具、提示片段和服务。它与配置文件相互独立:配置文件属于进程/主机层面(注册表、沙箱、审批、持久化、模型路由),而预设决定每个代理可以执行的操作。
- 每个预设都是一个代理层面的配置列表(已命名的预设包括、
standard、code、minimal、cordis),在进程中挂载到固定作用域下;每个指定该预设的会话都会通过作用域父级关系加入。online - 预设会挂载自己的每个代理副本(Shell/文件系统工具、技能发现 + 目录加载器、目标、计划模式、压缩、委托/工作流);主机组合保留共享注册表。
- 每个代理的服务行位于带有领域的组内(条目本地 = 每个挂载会话一个私有实例)。
isolate - 通过针对预设创建会话来使用它——;会话头中会存储该ID,恢复会话时会重建相同的代理。默认值为用户存储的选择,否则为部署默认值(
session.create({ agentPreset }))。standard - 通过RPC管理预设(列出/选择/读取/复制/删除/打开文档)。
agentPreset.*
Package shape
包结构
- One plugin = one npm package, named under a user scope: when no scope is explicitly specified, default to the current user's git config —
user.name. Never use the@<git-user.name>/dsh-plugin-<name>scope for a third-party plugin — it is reserved for the harness's own packages. Depending on@deepseek-ai/*via@deepseek-ai/*is expected; the reserved-scope rule is about the plugin's own package name.peerDependencies - carries
package.json/main(exports,.), a./clientblock —dshpointing at the package's owndsh.bundle.patch, plus optionalcordis.patch.yml(dsh.client,platform,inject) — aimmediatelylist that ships every built entry andfiles, andcordis.patch.ymlagainstpeerDependencies.@deepseek-ai/* - The package's inserts its own host row by package name (never a relative source path), so Node resolution finds the installed code.
cordis.patch.yml - Layout: (host entry),
src/index.ts(client entry),src/client.ts(pure logic with no dsh imports → unit-testable withsrc/core/),node --test(i18n dictionaries),src/locales/.tests/ - Host entry: default-export a Cordis Service — ,
static inject = [...], init insuper(ctx, 'name')— or a plainasync *[Service.init]().apply(ctx) - Client entry: →
exports["./client"], exportingsrc/client.ts(service names) +inject; render withapply(ctx)(no JSX).React.createElement
- 一个插件对应一个npm包,命名需包含用户作用域:若未显式指定作用域,默认使用当前用户git配置中的——
user.name。第三方插件绝不能使用@<git-user.name>/dsh-plugin-<name>作用域——该作用域为Harness自身的包保留。插件通过@deepseek-ai/*依赖peerDependencies是合理的;作用域规则仅针对插件自身的包名。@deepseek-ai/* - 包含
package.json/main(exports、.)、一个./client块——dsh指向包自身的dsh.bundle.patch,以及可选的cordis.patch.yml(dsh.client、platform、inject)、一个包含所有构建条目和immediately的cordis.patch.yml列表,以及针对files的@deepseek-ai/*。peerDependencies - 包的通过包名插入自己的主机行(绝不能使用相对源路径),以便Node解析能找到已安装的代码。
cordis.patch.yml - 目录结构:(主机入口)、
src/index.ts(客户端入口)、src/client.ts(纯逻辑,无dsh导入→可通过src/core/进行单元测试)、node --test(国际化字典)、src/locales/。tests/ - 主机入口:默认导出一个Cordis Service——、
static inject = [...]、在super(ctx, 'name')中初始化——或一个普通的async *[Service.init]()。apply(ctx) - 客户端入口:指向
exports["./client"],导出src/client.ts(服务名称) +inject;使用apply(ctx)渲染(禁止使用JSX)。React.createElement
Constraints
约束条件
- Naming. Third-party plugin packages are scoped: when no scope is explicitly specified, default to the current user's git config —
user.name.@<git-user.name>/dsh-plugin-<name>is reserved for the harness and must never be a plugin's own scope.@deepseek-ai/* - Self-contained loading. A plugin must declare pointing at its own
dsh.bundle.patch, whosecordis.patch.ymllists the package's host row by package name.insertautomatically appends any installed dependency that declaresdsh plugin add <spec>to the profile'sdsh.bundle— never hand-edit the profile'sdsh.profile.bundles,cordis.patch.yml, or acordis.ymlmanifest just to load a plugin. Profile/home patches remain user override layers.cordis:include - Bundle-less packages are libraries. A package without still installs but activates no layer (dsh warns); reserve that shape for libraries plugins import, not for plugins.
dsh.bundle - Ship runnable artifacts. must include the built host/client entries and
files. Git installs fetch sources, not build output: ship a self-containedcordis.patch.ymlscript that builds the published entries without dev-only assumptions, or distribute built artifacts (npm / tarball). A user allowlisting a gitprepareis permitting install-time code, so they should pin a commit.prepare - Whole-row patches. A later patch replaces a row's entire by id — when overriding, restate every retained key. Users can override your rows in their profile patch without touching your package, so prefer configuration defaults they will keep.
config - Client manifest matches the built bundle. must be
dsh.client.platform;'web'must point at the built client bundle;exports["./client"]edges are informational (preflight display / HMR diffing), not activation order — activation waits on service injection only.dsh.client.inject - Pure core. imports no dsh package, so
src/core/exercises the logic directly.node --test - No harness forks for one plugin. When a need crosses the plugin surface, follow the general-purpose proposal path below instead of patching or forking dsh ad hoc.
- 命名规则:第三方插件包必须带作用域;若未显式指定作用域,默认使用当前用户git配置中的——
user.name。@<git-user.name>/dsh-plugin-<name>为Harness保留,绝不能作为插件自身的作用域。@deepseek-ai/* - 独立加载:插件必须声明指向自身的
dsh.bundle.patch,其cordis.patch.yml列表通过包名列出主机行。insert会自动将任何声明了dsh plugin add <spec>的已安装依赖添加到配置文件的dsh.bundle中——绝不能为了加载插件而手动编辑配置文件的dsh.profile.bundles、cordis.patch.yml或cordis.yml清单。配置文件/主目录补丁始终是用户覆盖层。cordis:include - 无包结构的包是库:没有的包仍可安装,但不会激活任何层(dsh会发出警告);这种结构仅用于插件导入的库,而非插件本身。
dsh.bundle - 交付可运行产物:必须包含构建后的主机/客户端条目和
files。Git安装会获取源码而非构建输出:需交付一个独立的cordis.patch.yml脚本,在不依赖开发环境的情况下构建发布条目,或分发构建后的产物(npm / tarball)。用户允许Git的prepare脚本意味着允许安装时代码执行,因此他们应该固定提交版本。prepare - 整行补丁:后续补丁会通过ID替换行的完整——覆盖时需重述所有保留的键。用户无需修改你的包,即可在其配置文件补丁中覆盖你的行,因此优先选择用户会保留的配置默认值。
config - 客户端清单与构建包匹配:必须为
dsh.client.platform;'web'必须指向构建后的客户端包;exports["./client"]关联仅用于信息展示(预检查显示 / HMR差异对比),而非激活顺序——激活仅等待服务注入完成。dsh.client.inject - 纯核心逻辑:不导入任何dsh包,因此
src/core/可直接测试逻辑。node --test - 不为单个插件分叉Harness:当需求超出插件边界时,请遵循下文的通用提议流程,而非临时补丁或分叉dsh。
Develop → load → reload loop
开发→加载→重载循环
- Edit host/client sources under .
src/ - Build + typecheck + test: (emits the host ESM and the CJS client bundle),
npm run build,npx tsc --noEmit.npm test - Install and load: (symlinks the package into the profile's
dsh plugin --profile <name> add link:<path>). Because the package declaresnode_modules, dsh appends it to the profile'sdsh.bundleand its owndsh.profile.bundlesinserts the host row — the plugin is self-contained, with no manual profile patch or manifest edit. Remove it withcordis.patch.yml(dependency and layer together).dsh plugin --profile <name> remove <package> - Reload: restart dsh. (While runs from the same checkout, client bundles hot-reload without a page refresh.)
dev:web
link:file:The client bundle is CJS wrapped in ; and imports are external (resolved from the loader's module table), relative imports are bundled.
window.__ModuleLoader__.load({ id, factory })react@deepseek-ai/*- 编辑下的主机/客户端源码。
src/ - 构建 + 类型检查 + 测试:(生成主机ESM和CJS客户端包)、
npm run build、npx tsc --noEmit。npm test - 安装并加载:(将包软链接到配置文件的
dsh plugin --profile <name> add link:<path>中)。由于包声明了node_modules,dsh会将其添加到配置文件的dsh.bundle中,且其自身的dsh.profile.bundles会插入主机行——插件是独立的,无需手动修改配置文件补丁或清单。使用cordis.patch.yml移除插件(同时移除依赖和层)。dsh plugin --profile <name> remove <package> - 重载:重启dsh。(当从同一代码库运行时,客户端包无需刷新页面即可热重载。)
dev:web
link:file:客户端包是被包裹的CJS;和导入是外部依赖(从加载器的模块表解析),相对导入会被打包。
window.__ModuleLoader__.load({ id, factory })react@deepseek-ai/*Client surfaces
客户端接口
- Slots — . Kinds:
ctx.slots.register({ name, id, order, label, inject }, Component)/single/list/keyed; scope:chain/root/session-maybe. Registering into an undeclared slot throws.session - Locale — then
ctx.locale.register(ns, { zh, en }); merge the key union intoctx.locale.bind(ns).LocaleNamespaceMap - Settings — ; persist via the
ctx.settingsScope.bind({ namespace, decode })RPC. A third-party namespace must be insettings.mutateor its writes are silently rejected.exposedNamespaces() - RPC — reach host domains through (the apiproxy client face), not a custom transport.
ctx.get('connection').api.<domain>.<method>
- 插槽(Slots)——。类型:
ctx.slots.register({ name, id, order, label, inject }, Component)/single/list/keyed;作用域:chain/root/session-maybe。注册到未声明的插槽会抛出错误。session - 本地化(Locale)——,然后调用
ctx.locale.register(ns, { zh, en });将键合并到ctx.locale.bind(ns)中。LocaleNamespaceMap - 设置(Settings)——;通过
ctx.settingsScope.bind({ namespace, decode })RPC持久化。第三方命名空间必须在settings.mutate中,否则其写入操作会被静默拒绝。exposedNamespaces() - RPC——通过(apiproxy客户端接口)访问主机域,不要使用自定义传输方式。
ctx.get('connection').api.<domain>.<method>
Host surfaces
主机接口
- Provide a service with +
super(ctx, 'name'); read optional deps withstatic inject.ctx.get('...') - Register settings with a schema via .
ctx.settings.register(ns, schema, { applies })
- 使用+
super(ctx, 'name')提供服务;使用static inject读取可选依赖。ctx.get('...') - 通过注册带 schema 的设置。
ctx.settings.register(ns, schema, { applies })
Type & lint gotchas
类型与 lint 陷阱
- : optional props need an explicit
exactOptionalPropertyTypes: true.| undefined - CSS modules type as ; with
Record<string, string>,noUncheckedIndexedAccessiscss.x— pass it withstring | undefined, never?? ''(!is a lint error inno-non-null-assertion).src/** - Render components with , never
React.createElement(Component, props)— calling the function misattributes hooks (React error #310).Component(props) - The pre-commit hook lints with , so a type-aware rule's
typeAware: falseshows as an "unused directive" warning there; it's harmless and still required for the type-aware CI gate.// oxlint-disable-next-line
- :可选属性需要显式声明
exactOptionalPropertyTypes: true。| undefined - CSS模块类型为;启用
Record<string, string>时,noUncheckedIndexedAccess的类型为css.x——需通过string | undefined传递,绝不能使用?? ''(!目录中src/**是lint错误)。no-non-null-assertion - 使用渲染组件,绝不能使用
React.createElement(Component, props)——直接调用函数会导致钩子属性错误(React错误#310)。Component(props) - 提交前钩子使用进行lint检查,因此类型感知规则的
typeAware: false会显示为“未使用指令”警告;这是无害的,且类型感知CI检查仍需要该指令。// oxlint-disable-next-line
Proposing a change to dsh itself
提议修改dsh本身
A plugin should avoid forking the harness, but some needs (a slot, a wire field, a registry semantic) require a change to dsh. The change must be general-purpose — reusable beyond the one plugin that needs it, never a bespoke seam for it — and must fit the dsh / cordis design philosophy (plugin-first composition, explicit typed boundaries, minimal API surface). First search the existing discussions — dsh's GitHub Discussions and its Agent Notes tree — to check whether a similar proposal already exists; if so, build on it instead of duplicating.
For a new proposal, write a short discussion draft with this structure (sections only — mirror the existing drafts, don't hardcode them):
- Title — .
# [Feature request] <short summary> - English summary — a 1–2 sentence blockquote of the change and why.
- Background (the actual need I hit) — what plugin you're building, its scopes, and the concrete problem.
- Current state — quote the current code/comment/docs that explain the present behavior and why it's deliberate.
- Proposal — the minimal change, and why it's safe.
- Appendix: patch — the diff that implements it.
- Questions to confirm — the open questions for maintainers.
- Related — links to the docs and to related discussions/notes.
插件应避免分叉Harness,但某些需求(如插槽、字段、注册表语义)需要修改dsh。修改必须具有通用性——可用于多个插件,而非仅为单个插件定制——且必须符合dsh / cordis的设计理念(插件优先组合、显式类型边界、最小API接口)。首先搜索现有讨论——dsh的GitHub Discussions及其Agent Notes树——检查是否已有类似提议;若存在,请基于现有提议构建,不要重复。
对于新提议,请撰写简短的讨论草稿,结构如下(仅保留章节——参考现有草稿格式,不要硬编码):
- 标题——。
# [Feature request] <简短摘要> - 英文摘要——1-2句话的引用块,说明修改内容及原因。
- 背景(遇到的实际需求)——你正在构建的插件、其范围,以及具体问题。
- 当前状态——引用当前代码/注释/文档,说明现有行为及其设计意图。
- 提议——最小修改方案,以及修改的安全性。
- 附录:补丁——实现修改的diff。
- 待确认问题——需要维护者确认的开放问题。
- 相关链接——文档链接及相关讨论/笔记链接。