Files
zhu1090093659 30ab8692a9 chore(sdk): advance the plugin cohort to 0.1.7-rc.2
Move every family manifest, the plugin scaffold and the shared workspace
package onto the 0.1.7-rc.2 cohort, together with the surfaces that state
the same fact:

- the dsh.engines.dsh floors and the matching @deepseek-ai/dsh host peers
- the release-age exclusion ledger and the two packageExtensions keys
- the root README badges, the CI/release mount-smoke pins and the
  docs/publish-prep.md and docs/plugins.md prose

Two rc.2 facts were verified against the published artifacts rather than
inferred from the version number:

- the shell's frozen static module table is unchanged (the same nine
  specifiers in the dsh-web-frontend rc.2 dist bundle), so
  shared/web-platform.ts keeps its list and only its provenance comment
  moves; the new dsh-client-shortcuts is an ordinary client plugin, not a
  static module
- @deepseek-ai/dsh-client-ui-primitives@0.1.7-rc.2 imports a further
  undeclared face, @deepseek-ai/dsh-util-code-language, so the
  primitives packageExtensions patch gains that pin

The satellite peer floors in pnpm-lock.yaml still read >=0.1.7-rc.1: they
belong to the satellite packages and move when those repositories release
the aligned version.
2026-09-24 22:26:05 +08:00

156 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 如何把新插件加入全家桶
本指南说明如何把一个新插件加入 dsh-web 全家桶,使其可以被聚合插件包(`dsh-web-all`)一键装齐,也可独立安装。
## 流程
### 1. 脚手架生成
```sh
node scripts/dsh-plugin-new <name>
```
在 `packages/<name>/` 生成标准 bundle 骨架(`<name>` 限小写字母、数字、单连字符,如 `dsh-task-board`),并替换模板中的 `__NAME__` 占位。生成的结构:
```text
packages/<name>/
├── cordis.patch.yml # 插件行(- insert: - id: ui-<name> / name: ...)
├── package.json # dsh.bundle.patch 清单 + dsh.client 声明
├── src/
│ ├── index.ts # host 半区(node 进程侧)
│ └── client.ts # browser 半区(Web GUI 侧)
├── tsconfig.json
├── tsdown.config.ts
├── README.md # 英文版(含 H1 后语言切换行)
├── README.zh.md # 中文版(结构与英文镜像)
├── README.i18n.yaml # 配对一致性记录(docs/i18n.md)
└── AGENTS.md # 包级 AI 指令(可选,复杂包建议写)
```
### 2. 实现插件逻辑
- host 半区 `src/index.ts`:导出 cordis 插件,运行在 dsh host 进程(例如系统提示词公告、真实任务执行等)。
- browser 半区 `src/client.ts`:Web GUI 侧的 UI 逻辑,经 package.json 的 `dsh.client` 声明注入运行时。
- 形态参照 `packages/dsh-task-board/`:`dsh.bundle.patch` 指向包内 `cordis.patch.yml`;`dsh.client` 声明 `inject: ["@deepseek-ai/dsh-client-runtime"]` 与 `platform: "web"`。
### 3. 注册进聚合包
把 `- ../<name>` 追加到 `packages/dsh-web-all/aggregate.yml` 的 `patchFrom` 和 `deps` 两段:
- `patchFrom`:该包的 `cordis.patch.yml` insert 行会被汇总进聚合包 patch;
- `deps`:解析为包名写入聚合包 `package.json` 的 `dependencies`(`workspace:*`)。
皮肤(新增或改动)不需要进任何 aggregate.yml:皮肤是纯资产目录,位于独立仓 [dsh-skins](https://github.com/zhu1090093659/dsh-skins) 的 `skins/<id>/`(市场构建与预览的共同来源,本仓以 submodule `satellites/dsh-skins` 的 gitlink 固定要读的提交);npm 包 `files` 白名单只随发默认皮肤 `blue-fantasy`,其余皮肤由市场按需安装到 `$DSH_HOME/skins/<id>/` 后由皮肤中心管理。皮肤仓的 CI 跑 `skin-center:check`,本仓在 `pnpm market:fetch` 之后运行 `pnpm market:build` 刷新 market/dist。皮肤启用互斥由 `dsh-skin use` 管理(客户端原子切换,不改 cordis.patch.yml)。
### 4. 重新生成聚合包
聚合行 id 自动加 `web-ui-` 前缀,可与独立包共存;规则见 [packages/AGENTS.md](../packages/AGENTS.md)。
```sh
node scripts/aggregate.mjs # 重新生成聚合包 cordis.patch.yml + 依赖
node scripts/aggregate.mjs --check # 校验模式:漂移即失败(CI 用)
```
### 5. 构建验证
```sh
pnpm install # workspace 链接
pnpm -r build # 全仓构建
```
> **前置要求**:类型来源是官方 NPM SDK——`@deepseek-ai/*` 官方 NPM SDK 包(scope registry 为
> registry.npmjs.org),**不依赖任何 DSH 源码 checkout**。首次构建前:
> 1. 若仍使用私有 scope 认证,设置环境变量 `export NPM_TOKEN='<token>'`(真实令牌只放环境变量,勿提交);
> 当前 SDK 已结束内测,公开包通常无需令牌即可安装;
> 2. token 放**用户级 `~/.npmrc`**(`//registry.npmjs.org/:_authToken=${NPM_TOKEN}`,由 pnpm 展开
> 环境变量);项目 `.npmrc` 只留 scope 映射(`@deepseek-ai:registry=https://registry.npmjs.org/`,
> 已在 `.gitignore` 中)。注意:项目级 `.npmrc` 里的 `${NPM_TOKEN}` 占位符在 pnpm 11 下不会被
> 展开、被忽略,不承担认证职责;
> 3. 所有 pnpm/npm 命令必须在设置了 `NPM_TOKEN` 的环境中执行(fresh shell 需自行 export)。
> 缺失时 `pnpm install` 无法拉取私有 SDK 包,`pnpm -r build` / `pnpm typecheck` 会失败。
### 6. 本地验证
两种方式任选:
```sh
# 方式 A:用 link-profile 脚本把全家桶全部包链接进 profile(推荐;脚本自动处理 @linxin666 命名空间)
node scripts/link-profile.mjs # 链接/刷新全家桶;--dry-run 预览
# 方式 B:只把聚合包本身注册进 profile(聚合包的 workspace:* 依赖会回退解析到 npm 已发布版本,
# 因此请先确认 npm 上的 @linxin666/dsh-* 为最新且可用,或先用方式 A 链接全部子包)
dsh plugin --profile web add link:<dsh-web>/packages/dsh-web-all
```
重启 `dsh web`,确认聚合包插件行挂载生效。调试阶段也可先单独安装单包(`link:<dsh-web>/packages/<name>`)验证。
> 注意:profile 目录不是 pnpm workspace,聚合包 package.json 里的 `workspace:*` 依赖无法就地解析,
> 会回退拉取 npm 已发布的版本——若 npm 版本滞后或损坏(如历史上的 dsh-pet 0.1.1 缺 chunk),
> 会出现「宿主已挂载但 UI 不显示」的现象。此时用 `node scripts/link-profile.mjs` 把仓库构建产物
> 链接进 `~/.dsh/profiles/node_modules/@linxin666/`,即可让全部子包走本地代码。
## 第三方插件准入原则
家族仓库欢迎社区插件,但收编必须透明:
1. **活跃且有上游的第三方 → 不搬代码**。优先 fork 到 dsh-external 组织维护(保留上游关联,可随时 merge 上游更新),或作为依赖引用;全家桶只注册其安装入口。
2. **收编条件**(无活跃上游、上游已停更、或作者明确授权组织托管):
- 用 `git subtree add` 迁入,保留完整 git 历史;
- **必须**保留上游 LICENSE 文件与作者署名(包内 LICENSE、README 作者声明);
- 在包 README 记录来源仓库与迁移日期;
- 版权归原作者,本仓库仅托管,不主张版权。
3. **合规红线**:无 LICENSE、作者未授权、或版权归属不明的代码,一律不收编。
### 社区插件索引登记
第三方插件作者可把自己的插件登记进创意工坊商店的插件目录(设置 → 创意工坊 → 插件)与 dsh-market.com 创意工坊站:
1. 在 [dsh-community-plugins](https://github.com/zhu1090093659/dsh-community-plugins) 仓根目录的 `community.json` 追加条目:`id` / `name` / `nameEn` / `author` / `repo`(https:// 仓库 URL)必填,`description` / `descriptionEn` / `npm` 可选;`category`(一级分类)与 `subcategory`(二级分类)可选,合法枚举见该仓 `scripts/community-index.cjs` 的 `CATEGORIES` 与 `SUBCATEGORIES`,且 `subcategory` 只在 `category` 已填时被接受——分类与二级分类一同驱动创意工坊的两级筛选;
2. 在该仓运行 `pnpm community:check` 校验数据(CI 门禁同款校验);
3. 运行 `node scripts/market-build` 重新生成 `market/dist` 清单(`manifest/plugins.json` 由 community.json 派生)并提交生成物(`market:check` 校验一致)。
索引只收录链接、不搬代码,条目版权归原作者,由维护者审核合并。
## 插件规范要点
- **package.json 的 `dsh.bundle.patch` 声明**:指向包内 `cordis.patch.yml`,这是官方 bundle 清单,`dsh plugin` 依赖它识别与挂载插件。
- **`dsh.engines.dsh` 最低运行时声明**(issue #754):每个发布包必须在 `dsh` 对象内声明 `"engines": { "dsh": ">=X.Y.Z[-rc.N]" }`(如 `"dsh": ">=0.1.1-rc.1"`),唯一支持形式为 `>= <semver>`(顶层 `engines.dsh` 是插件管理器兼容读取的备用位,新声明统一用 `dsh.engines.dsh`)。该字段随 npm 清单发布,插件管理器在更新检查与更新前读取并据此提示/拦截;`scripts/family-dsh-engines.test.mjs` 强制每个家族包(含聚合包 `dsh-web-all`)与插件模板都声明,且声明的下限不得低于插件模板的 cohort 下限。SDK cohort 升级时必须同步提升所有包的该字段:宿主版本门槛跟随当前适配的 cohort,根 README 徽章与 CI 挂载冒烟道使用同一版本(决策见 [dsh-host-floor-tracks-cohort](../.agents/notes/implemented/architecture/2026-09-01-dsh-host-floor-tracks-cohort.zh.md))。
- **`@deepseek-ai/dsh` 宿主 peer 声明**:每个发布包还必须在 `peerDependencies` 中声明 `"@deepseek-ai/dsh": ">=X.Y.Z[-rc.N]"`,与 `dsh.engines.dsh` 下限同值(同以插件模板的 cohort 下限为源),唯一支持形式同样是 `>= <semver>`。它让 npm 解析器在安装插件时直接看到宿主版本要求,与插件管理器读取的 `dsh.engines.dsh` 互为补充;`scripts/family-dsh-engines.test.mjs` 强制每个家族包(含聚合包 `dsh-web-all`)与插件模板都声明,且不低于 cohort 下限。宿主是全局安装(profile 树里没有 `@deepseek-ai/dsh`,且 `autoInstallPeers: false`),该 peer 因此只用于声明与告警,不会被自动安装。
- **cordis.patch.yml insert 行格式**(包名用家族 scope `@linxin666`,与 npm 发布名一致):
```yaml
- insert:
- id: ui-<name>
name: '@linxin666/dsh-client-ui-<name>'
```
- **类型来源(只能基于官方 NPM SDK)**:各包把用到的 `@deepseek-ai/*` 包声明为 `devDependencies`
(`^0.1.7-rc.2`;cordis 用 `^4.0.4`),TS 从 node_modules 自动解析类型
(SDK 包的 `exports["."].types` 统一指向 `lib/types/index.d.ts`,client 半区子路径
`./client` 同理)。**禁止** tsconfig `extends` / `paths` / `references` 指向任何 DSH 源码
checkout(历史形态:`../../../test-zhu1090093659` 相对路径、`~/.dsh/source/current` 绝对
paths —— 均已废除)。tsconfig 为自包含单项目:`moduleResolution: "bundler"` +
`allowImportingTsExtensions`(emit 项目另加 `rewriteRelativeImportExtensions: true`,
参照 `packages/dsh-task-board/tsconfig.json`)。构建/类型/测试全部以 node_modules 的 SDK 包为
唯一类型来源,克隆后无需任何源码 checkout 即可构建。
- **浏览器 client 半区**:`@deepseek-ai/*/client` 子路径由 SDK 包 exports 提供(闭包工厂产物,
运行时经 `window.__ModuleLoader__` 加载)。官方 SDK 尚未发布的槽位(如
`conversation.input.selector.*`)用**模块形式**的本地 augmentation 补齐类型
(`import type {}` + `declare module '@deepseek-ai/dsh-client-ui-slots'`,参照
`packages/dsh-git-graph/src/client/slots-augment.ts`),SDK 发布对应槽位后移除。
- **构建预设**:统一走仓库内单一共享副本 `shared/tsdown.client.ts`(平台模块表
`shared/web-platform.ts`),各包 `tsdown.config.ts` 引用它并传参(`libExternal` /
`companions` 等)。**禁止**再复制预设到包内。
- **测试基建**:vitest 配置需 `server.deps.inline: [/@deepseek-ai\//]`(SDK 包走 vite 转译,
处理 CSS);client 半区闭包工厂在测试中不可直接 import——用 `vitest.setup.ts` 的最小
`__ModuleLoader__` stub(`packages/dsh-remote-web-ui/vitest.setup.ts`)或 `vi.mock` 替换
(`packages/dsh-remote-web-ui/tests/remote-entry.spec.tsx` 的 `createSnapshotStore` mock)。
- **设置页插件配置(20260811+ 可选能力)**:DSH web 的插件管理页为每个 bundle 的页面提供一片配置区(`plugins.bundle.config` 槽,按 bundle 包名分派)。Web 插件组、皮肤中心、社区插件、桌面宠物各注册一级设置分区(`settings.section`,`label` 用 thunk 跟随语言,内容直接展开);Web 插件组声明 `web-ui.plugin.item` 子槽归组 task-board 等卡片。插件接入只需两步:
1. **host 半区**:插件的 `Config`(schemastery)就是它的设置面——`@deepseek-ai/dsh-settings` 按 profile entry 自身的 schema 生成设置表单,因此注册命名空间、`installSection`、`setSource`、`onChange` 都不再存在。需要可编辑的字段必须标记 `.volatile()`(只有 `@deepseek-ai/schemastery` 提供;未标记 volatile 的 entry 根本不生成表单),并在插件运行时通过宿主交付的引用读取当前值,用 `loader/volatile-update` 事件让已派生的行为跟随已提交的修改,无需重启。
2. **browser 半区**:注入 `configForms`(`@deepseek-ai/dsh-client-ui-settings` 提供 `ctx.configForms`),`ctx.configForms.get(entryId)` 读写该 profile entry 自身的配置——设置命名空间即所属 profile entry id,不再由插件自选名称;一次写入返回布尔值,`false` 表示宿主拒绝或跳过,必须当作保存失败上报。然后注册卡片:家族归组用 `web-ui.plugin.item`,官方 bundle 配置页用 `plugins.bundle.config`,一级菜单用 `settings.section`(自行 `declare module '@deepseek-ai/dsh-client-ui-slots'` 声明该槽,shape 与官方一致;`order` 用 100+;一级分区卡片加 `alwaysOpen` 直接展开)。样板见 `packages/dsh-remote-web-ui`(自包含 staged 表单,不依赖兄弟 UI 包)。家族插件用共享的 `installPluginCard`(`shared/client/settings/plugin-card-seat.ts`)选席位:`dsh-web-settings` 已加载(`ctx.get('webUiSettings')` 有值)时进 `web-ui.plugin.item`,否则进官方 keyed 槽 `plugins.bundle.config`(key 用自身 bundle 包名);**不要**用「官方席位是否已声明」判定——官方插件面属于 harness bundle,其席位在每个 web 构建上都先于外部插件声明,据此判定会让家族分区永远为空。家族插件经 `dsh-web-settings` 的 `webUiSettings` 绑定时按包身份别名表把家族 namespace 解析成所属 entry id,再走原生 `configForms`;解析不到时回落到该包的 loopback HTTP 桥。
- **皮肤类插件**:皮肤已迁至独立仓 [dsh-skins](https://github.com/zhu1090093659/dsh-skins),用该仓的 `node scripts/dsh-skin-new.cjs <id>` 脚手架生成纯资产目录(皮肤规范见该仓 README),不经过本流程第 3-4 步的 `dsh-web-all` 注册。皮肤中心(skin-center)虽是皮肤聚合,其 GUI 是一级设置分区(设置 → 皮肤中心),自带启用开关。
## 移植 harness 插件的挂载约束
聚合包 insert 行不带 `config`,loader 调 `apply` 前会用插件 schema 默认值填充配置;`apply` 若无条件加载时校验会把填充后的空配置当配置而抛错,profile 加载失败。应改为:组合条目配置了关键字段才在加载时校验,否则调用时提示「未配置」(settings section 提交仍严格校验)。