dsh-plugin-development

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

DSH 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

  1. If the user or the current session has named a local
    deepseek-harness
    checkout, first check that its
    master
    branch is aligned with upstream; if not, fetch and pull the latest
    master
    before relying on that checkout. Then read it.
  2. Otherwise, analyze the published package — the
    @deepseek-ai/dsh-*
    packages under
    node_modules
    (what
    npx @deepseek-ai/dsh …
    runs) — rather than assuming a local source tree exists.
  1. 如果用户或当前会话指定了本地的
    deepseek-harness
    代码库,请先确认其
    master
    分支与上游保持同步;若未同步,先拉取最新的
    master
    分支再使用该代码库,随后阅读其中内容。
  2. 若没有本地代码库,请分析已发布的包——即
    node_modules
    下的
    @deepseek-ai/dsh-*
    包(
    npx @deepseek-ai/dsh …
    运行的就是这些包),不要假设存在本地源码树。

Profile

配置文件(Profile)

dsh's user config lives under
$DSH_HOME
(default
~/.dsh
). A profile is a named composition under
profiles/<name>/
, holding:
  • package.json
    — out-of-tree plugin
    dependencies
    plus the
    dsh.profile
    manifest with its ordered
    bundles
    list.
    dsh plugin
    maintains it; never write it by hand.
  • cordis.patch.yml
    — the user's own patch layer (id-targeted whole-
    config
    overrides, disables, and
    insert
    lists). It is an override layer, not the mechanism a plugin loads through.
  • node_modules/
    — plugins pnpm installed for this profile.
Layers apply over an empty entry list in order: each bundle patch in
dsh.profile.bundles
order, then the profile's
cordis.patch.yml
, then the home-level
$DSH_HOME/cordis.patch.yml
, then each
--patch
overlay. Pick a profile with
--profile <name>
(the GUI
web
profile is the common one). A profile is unrelated to a workspace/cwd.
dsh的用户配置位于
$DSH_HOME
(默认路径为
~/.dsh
)。配置文件
profiles/<name>/
下的命名组合,包含:
  • package.json
    ——树外插件的
    dependencies
    ,以及带有有序
    bundles
    列表的
    dsh.profile
    清单。该文件由
    dsh plugin
    维护;请勿手动编辑。
  • cordis.patch.yml
    ——用户自定义的补丁层(基于ID的完整
    config
    覆盖、禁用和
    insert
    列表)。这是一个覆盖层,而非插件加载的机制。
  • node_modules/
    ——为该配置文件通过pnpm安装的插件。
配置层按顺序应用于空条目列表:首先是
dsh.profile.bundles
顺序中的每个包补丁,然后是配置文件的
cordis.patch.yml
,接着是主目录级别的
$DSH_HOME/cordis.patch.yml
,最后是每个
--patch
覆盖层。使用
--profile <name>
选择配置文件(GUI的
web
配置文件是常用选项)。配置文件与工作区/当前目录无关。

Preset

预设(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
    ,
    online
    ), mounted once per process under a standing scope; every session naming it joins by scope parentage.
  • 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
    isolate
    realm (entry-local = one private instance per mounted session).
  • Use it by creating a session against a preset —
    session.create({ agentPreset })
    ; 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 (
    standard
    ).
  • Manage presets through the
    agentPreset.*
    RPCs (list / select / read / copy / remove / openDocument).
预设是代理层面的组合——构建单个会话代理所需的工具、提示片段和服务。它与配置文件相互独立:配置文件属于进程/主机层面(注册表、沙箱、审批、持久化、模型路由),而预设决定每个代理可以执行的操作。
  • 每个预设都是一个代理层面的配置列表(已命名的预设包括
    standard
    code
    minimal
    cordis
    online
    ),在进程中挂载到固定作用域下;每个指定该预设的会话都会通过作用域父级关系加入。
  • 预设会挂载自己的每个代理副本(Shell/文件系统工具、技能发现 + 目录加载器、目标、计划模式、压缩、委托/工作流);主机组合保留共享注册表。
  • 每个代理的服务行位于带有
    isolate
    领域的组内(条目本地 = 每个挂载会话一个私有实例)。
  • 通过针对预设创建会话来使用它——
    session.create({ agentPreset })
    ;会话头中会存储该ID,恢复会话时会重建相同的代理。默认值为用户存储的选择,否则为部署默认值(
    standard
    )。
  • 通过
    agentPreset.*
    RPC管理预设(列出/选择/读取/复制/删除/打开文档)。

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
    @<git-user.name>/dsh-plugin-<name>
    . Never use the
    @deepseek-ai/*
    scope for a third-party plugin
    — it is reserved for the harness's own packages. Depending on
    @deepseek-ai/*
    via
    peerDependencies
    is expected; the reserved-scope rule is about the plugin's own package name.
  • package.json
    carries
    main
    /
    exports
    (
    .
    ,
    ./client
    ), a
    dsh
    block —
    dsh.bundle.patch
    pointing at the package's own
    cordis.patch.yml
    , plus optional
    dsh.client
    (
    platform
    ,
    inject
    ,
    immediately
    ) — a
    files
    list that ships every built entry and
    cordis.patch.yml
    , and
    peerDependencies
    against
    @deepseek-ai/*
    .
  • The package's
    cordis.patch.yml
    inserts its own host row by package name (never a relative source path), so Node resolution finds the installed code.
  • Layout:
    src/index.ts
    (host entry),
    src/client.ts
    (client entry),
    src/core/
    (pure logic with no dsh imports → unit-testable with
    node --test
    ),
    src/locales/
    (i18n dictionaries),
    tests/
    .
  • Host entry: default-export a Cordis Service —
    static inject = [...]
    ,
    super(ctx, 'name')
    , init in
    async *[Service.init]()
    — or a plain
    apply(ctx)
    .
  • Client entry:
    exports["./client"]
    src/client.ts
    , exporting
    inject
    (service names) +
    apply(ctx)
    ; render with
    React.createElement
    (no JSX).
  • 一个插件对应一个npm包,命名需包含用户作用域:若未显式指定作用域,默认使用当前用户git配置中的
    user.name
    ——
    @<git-user.name>/dsh-plugin-<name>
    第三方插件绝不能使用
    @deepseek-ai/*
    作用域
    ——该作用域为Harness自身的包保留。插件通过
    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
  • 包的
    cordis.patch.yml
    通过包名插入自己的主机行(绝不能使用相对源路径),以便Node解析能找到已安装的代码。
  • 目录结构:
    src/index.ts
    (主机入口)、
    src/client.ts
    (客户端入口)、
    src/core/
    (纯逻辑,无dsh导入→可通过
    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)
    ;使用
    React.createElement
    渲染(禁止使用JSX)。

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>
    .
    @deepseek-ai/*
    is reserved for the harness and must never be a plugin's own scope.
  • Self-contained loading. A plugin must declare
    dsh.bundle.patch
    pointing at its own
    cordis.patch.yml
    , whose
    insert
    lists the package's host row by package name.
    dsh plugin add <spec>
    automatically appends any installed dependency that declares
    dsh.bundle
    to the profile's
    dsh.profile.bundles
    — never hand-edit the profile's
    cordis.patch.yml
    ,
    cordis.yml
    , or a
    cordis:include
    manifest just to load a plugin. Profile/home patches remain user override layers.
  • Bundle-less packages are libraries. A package without
    dsh.bundle
    still installs but activates no layer (dsh warns); reserve that shape for libraries plugins import, not for plugins.
  • Ship runnable artifacts.
    files
    must include the built host/client entries and
    cordis.patch.yml
    . Git installs fetch sources, not build output: ship a self-contained
    prepare
    script that builds the published entries without dev-only assumptions, or distribute built artifacts (npm / tarball). A user allowlisting a git
    prepare
    is permitting install-time code, so they should pin a commit.
  • Whole-row patches. A later patch replaces a row's entire
    config
    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.
  • Client manifest matches the built bundle.
    dsh.client.platform
    must be
    'web'
    ;
    exports["./client"]
    must point at the built client bundle;
    dsh.client.inject
    edges are informational (preflight display / HMR diffing), not activation order — activation waits on service injection only.
  • Pure core.
    src/core/
    imports no dsh package, so
    node --test
    exercises the logic directly.
  • 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>
    @deepseek-ai/*
    为Harness保留,绝不能作为插件自身的作用域。
  • 独立加载:插件必须声明
    dsh.bundle.patch
    指向自身的
    cordis.patch.yml
    ,其
    insert
    列表通过包名列出主机行。
    dsh plugin add <spec>
    会自动将任何声明了
    dsh.bundle
    的已安装依赖添加到配置文件的
    dsh.profile.bundles
    中——绝不能为了加载插件而手动编辑配置文件的
    cordis.patch.yml
    cordis.yml
    cordis:include
    清单。配置文件/主目录补丁始终是用户覆盖层。
  • 无包结构的包是库:没有
    dsh.bundle
    的包仍可安装,但不会激活任何层(dsh会发出警告);这种结构仅用于插件导入的库,而非插件本身。
  • 交付可运行产物
    files
    必须包含构建后的主机/客户端条目和
    cordis.patch.yml
    。Git安装会获取源码而非构建输出:需交付一个独立的
    prepare
    脚本,在不依赖开发环境的情况下构建发布条目,或分发构建后的产物(npm / tarball)。用户允许Git的
    prepare
    脚本意味着允许安装时代码执行,因此他们应该固定提交版本。
  • 整行补丁:后续补丁会通过ID替换行的完整
    config
    ——覆盖时需重述所有保留的键。用户无需修改你的包,即可在其配置文件补丁中覆盖你的行,因此优先选择用户会保留的配置默认值。
  • 客户端清单与构建包匹配
    dsh.client.platform
    必须为
    'web'
    exports["./client"]
    必须指向构建后的客户端包;
    dsh.client.inject
    关联仅用于信息展示(预检查显示 / HMR差异对比),而非激活顺序——激活仅等待服务注入完成。
  • 纯核心逻辑
    src/core/
    不导入任何dsh包,因此
    node --test
    可直接测试逻辑。
  • 不为单个插件分叉Harness:当需求超出插件边界时,请遵循下文的通用提议流程,而非临时补丁或分叉dsh。

Develop → load → reload loop

开发→加载→重载循环

  1. Edit host/client sources under
    src/
    .
  2. Build + typecheck + test:
    npm run build
    (emits the host ESM and the CJS client bundle),
    npx tsc --noEmit
    ,
    npm test
    .
  3. Install and load:
    dsh plugin --profile <name> add link:<path>
    (symlinks the package into the profile's
    node_modules
    ). Because the package declares
    dsh.bundle
    , dsh appends it to the profile's
    dsh.profile.bundles
    and its own
    cordis.patch.yml
    inserts the host row — the plugin is self-contained, with no manual profile patch or manifest edit. Remove it with
    dsh plugin --profile <name> remove <package>
    (dependency and layer together).
  4. Reload: restart dsh. (While
    dev:web
    runs from the same checkout, client bundles hot-reload without a page refresh.)
link:
is the dev-loop install; deployment uses
file:
(copies into the profile), so the source checkout is not a runtime dependency.
The client bundle is CJS wrapped in
window.__ModuleLoader__.load({ id, factory })
;
react
and
@deepseek-ai/*
imports are external (resolved from the loader's module table), relative imports are bundled.
  1. 编辑
    src/
    下的主机/客户端源码。
  2. 构建 + 类型检查 + 测试:
    npm run build
    (生成主机ESM和CJS客户端包)、
    npx tsc --noEmit
    npm test
  3. 安装并加载:
    dsh plugin --profile <name> add link:<path>
    (将包软链接到配置文件的
    node_modules
    中)。由于包声明了
    dsh.bundle
    ,dsh会将其添加到配置文件的
    dsh.profile.bundles
    中,且其自身的
    cordis.patch.yml
    会插入主机行——插件是独立的,无需手动修改配置文件补丁或清单。使用
    dsh plugin --profile <name> remove <package>
    移除插件(同时移除依赖和层)。
  4. 重载:重启dsh。(当
    dev:web
    从同一代码库运行时,客户端包无需刷新页面即可热重载。)
link:
是开发循环中的安装方式;部署时使用
file:
(复制到配置文件中),这样源码库就不会成为运行时依赖。
客户端包是被
window.__ModuleLoader__.load({ id, factory })
包裹的CJS;
react
@deepseek-ai/*
导入是外部依赖(从加载器的模块表解析),相对导入会被打包。

Client surfaces

客户端接口

  • Slots
    ctx.slots.register({ name, id, order, label, inject }, Component)
    . Kinds:
    single
    /
    list
    /
    keyed
    /
    chain
    ; scope:
    root
    /
    session-maybe
    /
    session
    . Registering into an undeclared slot throws.
  • Locale
    ctx.locale.register(ns, { zh, en })
    then
    ctx.locale.bind(ns)
    ; merge the key union into
    LocaleNamespaceMap
    .
  • Settings
    ctx.settingsScope.bind({ namespace, decode })
    ; persist via the
    settings.mutate
    RPC. A third-party namespace must be in
    exposedNamespaces()
    or its writes are silently rejected.
  • RPC — reach host domains through
    ctx.get('connection').api.<domain>.<method>
    (the apiproxy client face), not a custom transport.
  • 插槽(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 })
    ;通过
    settings.mutate
    RPC持久化。第三方命名空间必须在
    exposedNamespaces()
    中,否则其写入操作会被静默拒绝。
  • RPC——通过
    ctx.get('connection').api.<domain>.<method>
    (apiproxy客户端接口)访问主机域,不要使用自定义传输方式。

Host surfaces

主机接口

  • Provide a service with
    super(ctx, 'name')
    +
    static inject
    ; read optional deps with
    ctx.get('...')
    .
  • Register settings with a schema via
    ctx.settings.register(ns, schema, { applies })
    .
  • 使用
    super(ctx, 'name')
    +
    static inject
    提供服务;使用
    ctx.get('...')
    读取可选依赖。
  • 通过
    ctx.settings.register(ns, schema, { applies })
    注册带 schema 的设置。

Type & lint gotchas

类型与 lint 陷阱

  • exactOptionalPropertyTypes: true
    : optional props need an explicit
    | undefined
    .
  • CSS modules type as
    Record<string, string>
    ; with
    noUncheckedIndexedAccess
    ,
    css.x
    is
    string | undefined
    — pass it with
    ?? ''
    , never
    !
    (
    no-non-null-assertion
    is a lint error in
    src/**
    ).
  • Render components with
    React.createElement(Component, props)
    , never
    Component(props)
    — calling the function misattributes hooks (React error #310).
  • The pre-commit hook lints with
    typeAware: false
    , so a type-aware rule's
    // oxlint-disable-next-line
    shows as an "unused directive" warning there; it's harmless and still required for the type-aware CI gate.
  • exactOptionalPropertyTypes: true
    :可选属性需要显式声明
    | undefined
  • CSS模块类型为
    Record<string, string>
    ;启用
    noUncheckedIndexedAccess
    时,
    css.x
    的类型为
    string | undefined
    ——需通过
    ?? ''
    传递,绝不能使用
    !
    src/**
    目录中
    no-non-null-assertion
    是lint错误)。
  • 使用
    React.createElement(Component, props)
    渲染组件,绝不能使用
    Component(props)
    ——直接调用函数会导致钩子属性错误(React错误#310)。
  • 提交前钩子使用
    typeAware: false
    进行lint检查,因此类型感知规则的
    // oxlint-disable-next-line
    会显示为“未使用指令”警告;这是无害的,且类型感知CI检查仍需要该指令。

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):
  1. Title
    # [Feature request] <short summary>
    .
  2. English summary — a 1–2 sentence blockquote of the change and why.
  3. Background (the actual need I hit) — what plugin you're building, its scopes, and the concrete problem.
  4. Current state — quote the current code/comment/docs that explain the present behavior and why it's deliberate.
  5. Proposal — the minimal change, and why it's safe.
  6. Appendix: patch — the diff that implements it.
  7. Questions to confirm — the open questions for maintainers.
  8. Related — links to the docs and to related discussions/notes.
插件应避免分叉Harness,但某些需求(如插槽、字段、注册表语义)需要修改dsh。修改必须具有通用性——可用于多个插件,而非仅为单个插件定制——且必须符合dsh / cordis的设计理念(插件优先组合、显式类型边界、最小API接口)。首先搜索现有讨论——dsh的GitHub Discussions及其Agent Notes树——检查是否已有类似提议;若存在,请基于现有提议构建,不要重复。
对于新提议,请撰写简短的讨论草稿,结构如下(仅保留章节——参考现有草稿格式,不要硬编码):
  1. 标题——
    # [Feature request] <简短摘要>
  2. 英文摘要——1-2句话的引用块,说明修改内容及原因。
  3. 背景(遇到的实际需求)——你正在构建的插件、其范围,以及具体问题。
  4. 当前状态——引用当前代码/注释/文档,说明现有行为及其设计意图。
  5. 提议——最小修改方案,以及修改的安全性。
  6. 附录:补丁——实现修改的diff。
  7. 待确认问题——需要维护者确认的开放问题。
  8. 相关链接——文档链接及相关讨论/笔记链接。