# Plugin Development Spec

> DSH 插件开发规范（中英双语）——依据 DSH 插件生态倡议编写可组合插件：三层结构、宿主/客户端半边契约、loader id 一致性、Desktop 服务探测、组合守则与发布检查清单。Plugin development specification for DSH/DSH Desktop: composable plugin structure, host/client halves, loader-id consistency, Desktop service probing, composition rules and release checklist.

- Skill: `myyangyunfan/plugin-development-spec` (Agent Skill)
- Install (CLI): `npx skillmds@latest add myyangyunfan/plugin-development-spec`
- Raw SKILL.md: https://api.skillmd.com/api/skills/myyangyunfan/plugin-development-spec/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: myyangyunfan (https://skillmd.com/u/myyangyunfan)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/myyangyunfan/plugin-development-spec

---


# DSH 插件开发规范 / DSH Plugin Development Specification

本规范依据 DSH 插件生态倡议书（plugin-ecosystem.md）与官方插件开发文档（plugin-development.md）整理，并结合 DSH Desktop 内置插件的实际工程实践。
This specification condenses the DSH plugin ecosystem manifesto and the official plugin-development guide, combined with hands-on practice from DSH Desktop built-in plugins.

## 0. 三条原则 / The Three Principles

一切条目都从这三条推导。All rules derive from these three:

1. **组合优先 / Composition first**：通过官方 slot、service 和 patch 组合能力；绝不假设或覆盖其他插件的内部实现。Extend through official slots, services and patches; never assume or override another plugin's internals.
2. **声明清晰 / Declare clearly**：显式声明依赖的 service 与 slot，不依赖运行时巧合。Declare the services and slots you depend on; never rely on runtime coincidence.
3. **兼容优先 / Compatibility first**：升级保持向后兼容，不破坏已有组合。Keep upgrades backward compatible; never break existing compositions.

## 1. 两层插件模型 / The Two-Layer Model

一个 DSH 插件有两半，职责严格分离。A DSH plugin has two halves with strictly separated concerns:

| 半边 / Half | 运行环境 / Runtime | 形态 / Form | 职责 / Responsibility |
| --- | --- | --- | --- |
| 宿主半边 Host half | dsh web 进程（Node ESM） | `lib/index.js`（或 `.mjs`） | 注册 service / 路由 / 设置命名空间 / 事件 Register services, routes, settings namespaces, events |
| 客户端半边 Client half | 浏览器（渲染层） | `lib/client.js`（classic script） | UI 注入 / DOM 增强 / 状态展示 UI injection, DOM enhancement, display |

普通 DSH 插件应只依赖官方 DSH contract，从而在 CLI、普通 web profile、DSH Desktop 三处复用。An ordinary plugin should depend only on official DSH contracts so it works in CLI, plain web profiles and DSH Desktop alike.

## 2. 标准结构 / Standard Structure

目录式插件的标准布局（`assets/plugins/<dir>/`）：
Standard layout for a directory plugin:

```
my-plugin/
├── package.json        # 独立 npm 包声明 / npm package manifest
├── cordis.patch.yml    # 对宿主的组合声明 / composition patch for the host
├── dsh.plugin.json     # 可选：插件元数据（id/version/main）/ optional plugin metadata
├── lib/
│   ├── index.js        # 宿主半边（ESM）/ host half
│   └── client.js       # 客户端半边（classic script）/ client half
├── README.md           # 中英说明 / docs
└── LICENSE
```

关键 `package.json` 字段 / Key `package.json` fields:

```json
{
  "name": "@dsh-external/my-plugin",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "main": "lib/index.js",
  "exports": {
    ".": { "default": "./lib/index.js" },
    "./client": { "default": "./lib/client.js" },
    "./package.json": "./package.json"
  },
  "peerDependencies": { "@deepseek-ai/cordis": "*" },
  "dsh": {
    "client": {
      "inject": ["@deepseek-ai/dsh-client-ui-conversation"],
      "platform": "web"
    }
  }
}
```

- `dsh.client.inject` 声明客户端半边需要宿主预注入的模块；`platform: "web"` 限定浏览器端。
  `dsh.client.inject` lists host modules the client half needs pre-injected; `platform: "web"` scopes it to the browser.

`cordis.patch.yml` 最小形态（把宿主半边注册进插件栈）：
Minimal `cordis.patch.yml` (registers the host half into the plugin tree):

```yaml
# my-plugin profile composition patch.
# Kept as a flat list of entries (never mix a top-level `[]` with list items).
- insert:
    - id: my-plugin
      name: '@dsh-external/my-plugin'
      config: {}
```

## 3. 铁律：loader id 三处一致 / Iron Rule: Loader-id Consistency

`cordis.patch.yml` 的 `id`、`dsh.plugin.json` 的 `id`、宿主半边导出的 `name`（包名可不同）必须指向同一个 loader id。一旦错位，bundle 迁移自愈、插件管理卸载标记等一切按 id 匹配的逻辑都会静默失效——典型后果是 `duplicate loader entry id` 双登记启动崩溃循环（DSH Desktop issue #104 教训）。
The `id` in `cordis.patch.yml`, the `id` in `dsh.plugin.json` and the host half's exported identity must agree on one loader id. Any drift silently breaks every id-matching path (bundle-migration healing, uninstall markers), typically ending in a `duplicate loader entry id` boot crash loop (DSH Desktop issue #104).

## 4. 宿主半边 / Host Half

导出契约：`name` / `inject` / `apply(ctx, config)`。
Export contract: `name` / `inject` / `apply(ctx, config)`.

```js
// lib/index.js — ESM host half
import z from "@deepseek-ai/schemastery";
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";

const name = "@dsh-external/my-plugin";
const inject = ["settings"];                    // 显式声明依赖的 service / declare dependencies

const NS = settingsNamespace("my-plugin");
const Config = z.object({ enabled: z.boolean().default(true) });

function apply(ctx, config) {
  // 设置注册必须降级：register 抛异常只告警，绝不阻断 dsh 启动。
  // Settings registration must degrade gracefully: warn, never crash host boot.
  try {
    const scope = ctx.settings.register(NS, Config, { base: config || {} });
    return () => { void scope; };
  } catch (error) {
    console.warn("[my-plugin] settings section unavailable: " + (error?.message || error));
  }
}

export { Config, apply, inject, name };
```

守则 / Rules:
- 需要什么 service 就写进 `inject`，不要运行时碰运气读取。Declare every service you need in `inject`; do not probe at runtime "just in case".
- 可选依赖用 `ctx.get("service", false)`（不抛错形态）动态探测并优雅降级。For optional dependencies use the non-throwing `ctx.get("service", false)` and degrade gracefully.
- 注册类调用（设置、路由、slot）全部 try/catch 降级——一个插件的失败不能拖垮整个插件树。Wrap registrations in try/catch—one plugin's failure must never take down the whole tree.

## 5. 客户端半边 / Client Half

classic script，经 `window.__ModuleLoader__.load` 注册模块：
A classic script registering a module through `window.__ModuleLoader__.load`:

```js
// lib/client.js — browser half (classic script)
window.__ModuleLoader__.load({
  id: "@dsh-external/my-plugin",
  factory: (require) => {
    const react = require("react");
    const { jsx } = require("react/jsx-runtime");
    // ... 组件与注入逻辑 / components and injection logic
    exports.apply = apply;
    exports.inject = ["slots"];                  // 客户端 service 声明 / client-side service deps
    return module.exports;
  }
});
```

守则 / Rules:
- 只插入自有节点，不移动/删除 React 管理的节点；DOM 增强须幂等（指纹去重）。Insert only nodes you own; never move/remove React-managed nodes; DOM enhancement must be idempotent (fingerprint dedupe).
- 长驻监听（MutationObserver/定时器）必须在关闭/卸载路径上彻底拆除——关闭态零开销。Tear down every observer/timer on the off path—zero cost when disabled.
- CSS 一律以作用域属性（如 `body[data-my-plugin]`）圈定，防跨插件污染。Scope all CSS behind an attribute selector to avoid cross-plugin leakage.

## 6. Desktop 专属服务 / Desktop-only Services

DSH Desktop 额外提供两个公开 Host service：`desktopProfiles`（读取/切换 profile）与 `desktopPnpm`（在当前 profile 执行 pnpm / 官方插件管理语义）。它们属于 Electron main 进程，Renderer 不可直达。
DSH Desktop exposes two public host services: `desktopProfiles` (read/switch profiles) and `desktopPnpm` (pnpm / official plugin-management semantics in the active profile). They live in the Electron main process and are not reachable from the renderer.

- `runPlugin(['add', target], invokingDir, signal)` 保留上游 `dsh plugin` 语义，安装/卸载/更新/依赖修复一律用它；`run()` 是低层 pnpm，不保证 profile 初始化。
  Use `runPlugin(['add'|'remove'|'update'|'install', ...], invokingDir, signal)` for package operations—it preserves upstream `dsh plugin` semantics; `run()` is low-level pnpm without profile guarantees.
- 参数按 argv 传递，绝不拼接 shell 字符串；一个 generation 同时只允许一个 package operation，插件卸载时必须取消并等待其结束。
  Args always go as argv—never concatenate shell strings. One package operation per generation; cancel and await it on dispose.

兼容普通 DSH 的写法——不要把 Desktop service 放进顶层 `inject`，运行时动态探测：
To stay compatible with plain DSH, keep Desktop services out of top-level `inject` and probe dynamically:

```js
export const inject = ["webServer", "loader"];

export function apply(ctx, config) {
  const profiles = ctx.get("desktopProfiles");
  if (profiles === undefined) {
    mountOrdinaryDshManager(ctx, config.profile ?? "web");   // 普通 DSH fallback
    return;
  }
  ctx.inject(["desktopPnpm"], (desktopPnpm) => {
    mountManager(ctx, { runPlugin: (args, cwd, signal) => desktopPnpm.runPlugin(args, cwd, signal) });
  });
}
```

不要从 `process.argv`、`ctx.baseUrl`、settings 或 `$DSH_HOME` 推断 Desktop profile；在 Desktop 中以 `desktopProfiles.current` 为准。
Never infer the Desktop profile from `process.argv`, `ctx.baseUrl`, settings or `$DSH_HOME`; in Desktop, `desktopProfiles.current` is authoritative.

## 7. 禁止依赖的内部接口 / Internal APIs You Must Not Depend On

以下即便出现在声明或运行时上下文中，也不属于第三方兼容 contract：
The following are Desktop internals even when they surface in declarations or runtime contexts:

`desktopRuntime`、`desktopPnpmBootstrap`、Electron `BrowserWindow`、托盘注册表、private Node helper、`ELECTRON_RUN_AS_NODE`、生成的 shim。
`desktopRuntime`, `desktopPnpmBootstrap`, Electron `BrowserWindow`, tray registries, the private Node helper, `ELECTRON_RUN_AS_NODE`, generated shims.

同时不要假设或覆盖其他插件（含官方插件）的内部实现、私有模块路径或 DOM 结构类名中的 hash 片段——官方类名带 hash，一律走结构定位或稳定 data-* 属性。
Likewise, never assume or override other plugins' internals, private module paths, or hash-suffixed class names—target stable structure or `data-*` attributes instead.

## 8. 测试与发布检查清单 / Test & Release Checklist

发布前逐项自查（不满足则打回）：
Verify every item before publishing:

- [ ] 普通 DSH（无 Desktop service）中能加载，或按产品定义保持 pending。Loads in plain DSH without Desktop services, or stays pending by design.
- [ ] Desktop 中读取的 profile name/dir 与用户实际选择一致。Profile name/dir read in Desktop match the user's actual selection.
- [ ] package operation 的取消、非零退出、spawn 失败、generation teardown 全部处理。Package operations handle cancel, non-zero exit, spawn failure and generation teardown.
- [ ] 插件变更后重启，bundle 能进入下一次 Loader 组合。After changes + restart the bundle enters the next loader composition.
- [ ] 配套自动化测试就位（纯函数 unit-* / 桌面逻辑 desktop-*；bug 修复必须带回归用例并注明 issue 号）。Automated tests in place; bug fixes carry regression tests referencing the issue.
- [ ] 版本号 bump——同步器按「安装包版本 > profile 版本」决定覆盖，不 bump 则存量用户拿不到更新。Bump the version—syncers only overwrite when the packaged version is newer.
- [ ] loader id 三处一致（§3）。Loader id consistent across all three sites (§3).
- [ ] 卸载/关闭路径拆除全部监听与定时器。All listeners and timers torn down on the off path.

## 9. 在 DSH Desktop 仓库登记配套插件 / Registering a Companion Plugin in DSH Desktop

内置插件还需两步（单一数据源约定）：
Built-in plugins additionally require two steps (single-source-of-truth convention):

1. 在 `dsh-desktop/scripts/lib/companion-plugins.js` 的 `COMPANION_PLUGINS` 追加 `{ id, name }` 条目（`id` = loader id；`name` = profile node_modules 包名）。桌面壳与 CLI 同步入口自动生效。
   Append an `{ id, name }` entry to `COMPANION_PLUGINS` in `dsh-desktop/scripts/lib/companion-plugins.js` (`id` = loader id; `name` = package name). Both sync entry points pick it up automatically.
2. 同步更新 `scripts/test/unit-patch-engine.test.js` 的前缀顺序断言（`slice` 长度与期望数组）——该测试是清单漂移的防线。
   Update the prefix-order assertion in `scripts/test/unit-patch-engine.test.js` (`slice` length and expected array)—it is the drift guard for the manifest.

## 10. 参考实现 / Reference Implementations

仓库内范例（由简到繁）：
In-repo references, simple to complex:

- `assets/plugins/dsh-session-manager/` —— 设置面板 + 行菜单桥 + RPC 扩展的最小完整样例。Minimal complete sample: settings panel + row-menu bridge + RPC extension.
- `assets/plugins/dsh-quest-ui/` —— 纯客户端 DOM 增强（默认关闭、零关闭态开销、单一去抖观察器）。Pure client-side DOM enhancement (off by default, zero idle cost, one debounced observer).
- `assets/plugins/dsh-side-session/` —— 宿主服务 + 悬浮窗客户端 + 设置命名空间的完整双半边实践。Full two-half practice: host service + floating-window client + settings namespace.

完整愿景见上游倡议书与 Community Fabric RFC（manifest/capability 仍为 Draft，不能作为依赖或发布目标）。
See the upstream manifesto and Community Fabric RFC for the full vision (manifest/capability are still drafts—do not depend on or target them).

