Files
dingtalk-workspace-cli/docs/skill-architecture-optimization.md
T
玉澜andCursor 402429ac2a feat(skill): default installs and upgrades to multi-skill layout
Flip the agent-skill default from mono (single dws/ dir) to multi
(per-product dingtalk-* + dws-shared) across all distribution faces,
and fix the upgrade path so it no longer re-installs mono alongside
multi (mono+multi co-existence bug).

- upgrade: LocateSkillsRoot prefers the zip multi/ tree; multi refresh
  removes mono leftovers and stale skills, refreshes the multi cache
- install.sh/ps1/install-skills.sh/npm install.js: multi real-install
  (was print-only), default flipped, mono stays opt-in via DWS_SKILL_MODE
- skill setup: non-interactive default multi; full installs now clean
  stale dingtalk-*/dws-shared with confirm-preview disclosure, filtered
  (-s/-x) installs stay additive
- mutual exclusion is symmetric and includes dws-shared (previously
  leaked through the dingtalk- prefix) on all faces
- install.js: guard empty/corrupt multi trees (fall back to mono),
  validate SKILL.md on the mono branch, guard cache refreshes
- docs: roadmap (8/30 back-schedule), migration plan, distribution
  mechanism, rollout capability, capability completion, architecture
  optimization, wukong comparison (archived; line retired)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 14:13:45 +08:00

273 lines
21 KiB
Markdown
Raw 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.
# Skill 分发/安装架构优化方案
> 目标:把 DWS Agent skill 的"装在哪、怎么装、装完记什么"从 7+ 个安装面的
> N 份漂移拷贝,收敛为 **Go 侧单一事实源 + 单一安装引擎 + 脚本侧 bootstrap**。
> 本文基于 feat/skill-mode-migration 工作区(multi 化落地后)的代码级盘点
> (2026-08-05),所有锚点均可跳转验证。
> 前置文档:[skill-multi-migration-plan.md](skill-multi-migration-plan.md)(下称《迁移计划》)、
> [skill-distribution-mechanism.md](skill-distribution-mechanism.md)。
## 1. 问题量化
### 1.1 复制矩阵(能力 × 面)
multi 化之后,同一块逻辑在各安装面的拷贝数(✓ = 独立拷贝一份,数字 = 份数):
| 能力 \ 面 | install.sh | install.ps1 | npm install.js | install-skills.sh | install-event.sh | install-devapp.sh(+ps1) | Homebrew caveats | `dws skill setup` (Go) | `dws upgrade` (Go) |
|---|---|---|---|---|---|---|---|---|---|
| agent home 清单 | ✓×3 | ✓ | ✓ | ✓×2 | ✓ | ✓×2 | — | ✓ | ✓ |
| 互斥清理(mono↔multi) | ✓×2(其中 1 份死代码) | ✓ | ✓ | ✓ | — | — | — | ✓ | ✓ |
| mode 解析(env/flag/交互) | ✓ | ✓ | ✓ | — | — | — | — | ✓ | 布局嗅探 |
| stale `dingtalk-*` 清理 | ✓ | ✓ | ✓ | ✓ | — | — | — | ✗(additive,不清) | ✓ |
| `~/.dws/skills` 缓存写 | ✓ | ✓ | ✓ | — | ✓ | ✓ | — | —(只读回退) | ✓(仅 multi) |
| 安装状态(state.json) | 无 | 无 | 无 | 无 | 无 | 无 | — | 无 | 无 |
清单锚点(agent home 清单,全仓共 **15 份**):
| # | 位置 | 备注 |
|---|---|---|
| 1 | `internal/app/skill_setup.go:20-37` | `skillSetupAgentHomes`,16 项,**无 opencode** |
| 2 | `internal/app/skill_command.go:119-141` | `agentSkillPaths`,17 项,含 opencode(`:129`) |
| 3 | `internal/upgrade/paths.go:35-52` | `knownSkillDirs`,16 项,**无 opencode** |
| 4 | `scripts/install.sh:370-386` | mono 安装循环内联清单 |
| 5 | `scripts/install.sh:439-455` | multi 清单(**死代码**,见 1.2) |
| 6 | `scripts/install.sh:516-532` | multi 清单(生效) |
| 7 | `scripts/install.ps1:44-61` | `$AgentDirs` |
| 8 | `build/npm/install.js:11-28` | `AGENT_DIRS` |
| 9 | `scripts/install-skills.sh:166-182` | multi 清单 |
| 10 | `scripts/install-skills.sh:247-261` | mono 清单 |
| 11 | `scripts/install-event.sh:103-107` | 17 项,**含 `.config/opencode/skills`**(`:107`) |
| 12 | `scripts/install-devapp.sh:87-91` | 含 opencode(`:91`) |
| 13 | `scripts/install-devapp.ps1:106` | 含 opencode |
| 14 | `test/scripts/package_script_test.go` | `expectedPackagedSkillTargets` |
| 15 | `scripts/release/verify-package-managers.sh:75-76` | `HOME_AGENT_PARENTS` / `HOME_SKILL_TARGETS` |
互斥清理逻辑共 **≥8 份**:`skill_setup.go:570-608`、`paths.go:306-327` +
`paths.go:163-179`(mono 分支内联)、`install.sh:394-399` + `install.sh:557-570`
+ 死代码 `install.sh:477-486`、`install.ps1:488-498` + `install.ps1:562-570`、
`install.js:130-137` + `install.js:165-178`、`install-skills.sh:207-220`。
mode 解析共 **4 份**:`skill_setup.go:343-376`、`install.sh:220-257`、
`install.ps1:293-332`、`install.js:197-214`(各自实现 env 优先级、非法值报错、
TTY 交互、非 TTY 默认值,措辞与行为细节已不完全一致)。
stale `dingtalk-*` 清理共 **5 份**:`paths.go:317-325`、`install.sh:561-567`、
`install.ps1:562-570`、`install.js:168-172`、`install-skills.sh:211-220`。
缓存写共 **6 份**:`install.sh:326-344`(multi)+ `install.sh:349-360`(mono)、
`install.ps1:446-471`、`install.js:221-237`、`install-event.sh:165-169`、
`install-devapp.sh:82-84`、`paths.go:290-296`(upgrade 只刷 multi)。
### 1.2 已发生的漂移实例(不是假设,是现状)
1. **opencode 清单漂移**:`dws skill install` 支持 opencode 目标
(`skill_command.go:129`),install-event / install-devapp 也往
`~/.config/opencode/skills` 装(`install-event.sh:107`、
`install-devapp.sh:91`),但 `dws skill setup --target all` 与
`dws upgrade` 的清单都不含它(`skill_setup.go:20-37`、
`paths.go:35-52`)。结果:opencode 用户能收到 event/dev skill,
却永远收不到内置 skill 的安装与升级。
2. **install.sh 死代码**:`install_multi_skills_to_homes` 定义了两次
(`install.sh:434` 与 `install.sh:511`),`_install_multi_to_base`
同样两次(`install.sh:473` 与 `install.sh:549`)。bash 后定义覆盖先定义,
第一对(434-505)整体是死代码——本次 multi 化自己引入的重复。
3. **"镜像"注释与实现已不符**:`install.sh:507-510` 注释声称 multi 安装
"mirroring `dws skill setup --mode multi`",但脚本会删除 bundle 里不存在
的 stale `dingtalk-*`(`install.sh:561-567`),而 setup 是 additive 语义、
只清 `dws/`(`skill_setup.go:587-593`)。注释说镜像,行为已分叉。
4. **互斥语义自相矛盾**:install-event.sh 把 multi 的 `dingtalk-event` 和
mono 的 `dws` **同时**装进同一批 agent home(`install-event.sh:171-172`),
与其他所有面"mono/multi 互斥"的语义直接冲突。
5. **缓存只写不读、版本不可见**:`dws skill setup` 默认走二进制 embed
(`skill_setup_embed.go:50-59`),`~/.dws/skills` 只是 legacy 回退候选
(`skill_setup.go:442-447`);6 个写入方没有任何版本戳,upgrade 只刷
multi 不刷 mono(`paths.go:292-296`),缓存与 embed 漂移不可见。
6. **保留前缀规则已双份**:`dingtalk-` / `dws-shared` 的保留约定在
`skill_setup.go:199`(`multiSkillPrefix`)、`skill_setup.go:205`
(`multiSharedSkill`)与 `paths.go:332-334`(`isMultiSkillDirName`)
各写一份,互斥清理依赖"市场 skill 不用该前缀"这一隐性约定。
### 1.3 维护成本论证(本次 multi 化实证)
本次"把 7 个面全部 multi 化"这一个语义变更,工作区改动为 **13 个文件、
+759/−166 行**(`git diff --stat`),横跨 Go / POSIX sh / PowerShell /
JavaScript 四种语言:
| 文件 | 改动量 | 改了什么 |
|---|---|---|
| `scripts/install.sh` | +256/−… | mode 解析、multi 安装×2(含死代码)、互斥清理、缓存写 |
| `scripts/install.ps1` | +169 | 同上,PowerShell 再写一遍 |
| `build/npm/install.js` | +87 | 同上,JavaScript 再写一遍 |
| `scripts/install-skills.sh` | +101 | multi 安装 + stale 清理 |
| `internal/upgrade/paths.go` | +206 | upgrade multi 刷新 + 互斥清理 + 缓存 |
| `internal/app/skill_setup.go` 等 Go 侧 | ~+50 | setup multi 语义 |
| 测试与文档 | 其余 | 4 个测试文件 + README×2 + SKILL.md |
即:**一个语义变更 = 4 种语言 × 7+ 处同步编辑**,且仍然漏了 opencode、
制造了死代码、留下了语义分叉(1.2)。新增一个 agent home 今天要改 15 份
清单中的至少 11 份(另 4 份是测试/校验)。这不是"以后会漂移",而是
**每一次改动都在当场制造漂移**。
## 2. 目标架构
### 2.1 分层总览
```text
┌──────────────────────────────────────────────────────────────────┐
│ L3 分发面(7 个,全部退化为 bootstrap) │
│ install.sh / install.ps1 / npm install.js / install-skills.sh /│
│ install-event.sh / install-devapp.sh / Homebrew caveats │
│ 职责:装二进制 → exec `dws skill setup --mode X --yes` │
│ └─ 失败 → 冻结的内嵌 fallback 拷贝(一档,不再演进) │
├──────────────────────────────────────────────────────────────────┤
│ L2 单一安装引擎(Go,唯二入口) │
│ dws skill setup ─┐ │
│ dws upgrade ─┴─► 共用同一 install 实现(含互斥清理/记账) │
├──────────────────────────────────────────────────────────────────┤
│ L1 单一事实源:internal/skillhome(新共享包) │
│ AgentHomes() 清单 + 布局规则(mono→<home>/dws,multi→平铺) │
│ + 互斥/ stale 清理规则 + 保留前缀常量 + state.json 读写 │
├──────────────────────────────────────────────────────────────────┤
│ L0 事实数据 │
│ //go:embed skills/{mono,multi}(skills_embed.go:28,默认源) │
│ ~/.dws/skills/state.json(权威安装状态) │
│ ~/.dws/skills/{mono,multi}(显式回退源,带版本戳,唯一写方=Go) │
└──────────────────────────────────────────────────────────────────┘
▲ 门禁:scripts/policy/check-agent-homes-sync.sh(进 make policy)
比对各脚本可解析清单块 ↔ skillhome 导出清单
```
### 2.2 单一事实源:`internal/skillhome` 共享包
新建 `internal/skillhome`,把今天散在 3 处 Go 代码里的规则合并导出,
setup / upgrade / 测试共用(对应《迁移计划》P0c-1):
| 导出物 | 收敛的现有拷贝 |
|---|---|
| `AgentHomes()`(含 opencode,带"首项必装/其余父目录门控"元数据) | `skill_setup.go:20-37`、`paths.go:35-52`,并与 `skill_command.go:119-141` 互相断言 |
| `HomeForMode(base, mode)` 布局规则 | `skill_setup.go:502-507`(`agentHomeForMode`) |
| `MutualExclusionVictims(home, mode)` / stale 判定 | `skill_setup.go:570-596`、`paths.go:306-327` |
| `ReservedSkillPrefixes` / `ReservedSkillNames` 常量 | `skill_setup.go:199/205`、`paths.go:332-334` |
| `State` 读写(state.json,见 2.4) | 新增(《迁移计划》P0b-1) |
脚本侧不生成代码(sh/ps1/js 三语言片段生成成本高、措辞差异大),改为
**可解析标记块 + policy 比对**:每个脚本的清单包在
`# DWS-AGENT-HOMES-BEGIN` / `# DWS-AGENT-HOMES-END` 注释块内,新增
`scripts/policy/check-agent-homes-sync.sh` 提取 7 个脚本块与
`skillhome` 导出清单(`go run ./internal/skillhome/cmd/dump` 或
`dws skill setup --print-agent-homes` 之类调试出口)逐一比对,挂进
`make policy`(Makefile:86-97 现有政策链)。对应《迁移计划》P0c-2。
### 2.3 安装逻辑单引擎化:脚本退化为 bootstrap
**终态形态**:每个安装脚本只做两件事——装二进制、执行
`dws skill setup --mode <resolved> --yes`(mode 解析仍允许脚本做,因为它
要处理自己的 env/flag;也可以更进一步把 `DWS_SKILL_MODE` 透传给 setup)。
执行失败(二进制跑不起来、setup 非 0 退出)时,降级到**冻结的内嵌
fallback 拷贝逻辑一档**——即今天的拷贝实现原样保留但标记"不再演进",
语义变更只改 Go 引擎。
各面降级可行性分析:
| 面 | 二进制可得性 | 可行性 | 风险与对策 |
|---|---|---|---|
| install.sh | `main` 先 `install_binary` 后 `install_skills`(`install.sh:830-831`),二进制就在 `$INSTALL_DIR` | ✅ 直接调 `"$INSTALL_DIR/dws" skill setup --mode "$SKILL_MODE" --yes` | `DWS_SKILLS_ONLY=1` 时不装二进制 → 先 `command -v dws`,无则走 fallback。curl 下载不带 quarantine 属性,macOS 可直接执行。风险低 |
| install.ps1 | 同序(`Install-Binary`:337 → `Install-Skills`:622,main 入口 `:700`) | ✅ 调 `& $installDir\dws.exe skill setup ...` | ExecutionPolicy 约束的是 .ps1 脚本本身,**子进程 dws.exe 不受其限制**;AppLocker/WDAC 环境可能拦未签名二进制——但那种环境 dws 本身也跑不了,fallback 拷贝仍必要。风险低-中 |
| npm install.js | postinstall 已把平台二进制解到 `vendor/`(`install.js:260`)再装 skill(`:271-280`) | ✅ `execFileSync(path.join(vendorDir,'dws'), ['skill','setup','--mode',m,'--yes'])` | `npm i --ignore-scripts` 时 postinstall 整体不跑(现状如此,非新增风险);部分 CI 以 root 跑生命周期脚本权限怪异;Windows 用 `dws.exe`。风险中,fallback 必须保留 |
| install-skills.sh | skills-only 面,不装二进制 | ✅ `command -v dws` 有则调引擎,无则 fallback | 本质是"刷新 skill"路径,有 dws 才谈得上刷新。风险低 |
| install-event.sh | 装二进制 + 单 skill(dingtalk-event + mono dws) | ⚠️ 有条件 | `EVENT_VERSION` 可与已装二进制版本不同(尤其 `DWS_SKILLS_ONLY=1`),embed 与目标 zip 可能错配;且当前同时装 mono+multi 的语义(`:171-172`)需先按 1.2-4 收敛。**最后迁移**,迁移前先把 dingtalk-event 纳入 embed 并改成 `-s event` 调引擎 |
| install-devapp.sh(+ps1) | 同上(dingtalk-dev) | ⚠️ 有条件 | 同 install-event。风险中-高,最后迁移 |
| Homebrew caveats | 只打印提示 | ✅ 已是终态 | `homebrew.rb.tmpl:33-36` 现状就是"Run `dws skill setup`",无需改,是其他面的样板 |
配套收益:《迁移计划》P2-2(sh/ps1 的 multi 分支真装,1.5d)与 P2-3
(install.js / install-skills.sh 加 mode 支持,1d)在单引擎方案下**大幅
缩水**——脚本只需把 mode 透传给引擎,不再在 sh/ps1/js 里写安装逻辑。
### 2.4 状态与缓存
- **state.json 权威化**(《迁移计划》P0b 原样采纳):
`~/.dws/skills/state.json` 记录 `schema_version / mode / cli_version /
installed / agent_homes / previous`,写方只有 `dws skill setup` 与
`dws upgrade`(脚本侧不再各自写状态——它们调引擎,引擎记账);缺失/损坏
时按磁盘形态反推(有 `dws/` → mono;有 `dingtalk-*` → multi;都有 →
报 drift 要求显式收敛)。
- **`~/.dws/skills` 缓存收敛**:二选一,本文建议后者——
1. ~~被 state 取代,删除缓存~~:激进;embed 之外的"无源码机回退源"场景
(`skill_setup.go:442-447` 注释描述的场景)会断。
2. **明确为 setup 回退源 + 版本戳**(采纳):缓存目录写入
`cli_version` 戳文件(或并入 state.json 的 `cache_version` 字段);
**写缓存收敛到 Go 侧唯一实现**(setup/upgrade 共用),install.sh /
ps1 / js / event / devapp 的 6 份缓存写逻辑(1.1 矩阵)随 P1/P3 删除;
upgrade 补齐 mono 缓存刷新(修 `paths.go:292-296` 只刷 multi 的不对称)。
mono 下线版本再评估整体废弃缓存。
### 2.5 市场 skill 边界正式化
把"`dingtalk-*` 前缀与 `dws-shared` / `dws` 名称为 DWS 内置保留"从隐性约定
变成共享常量 + 测试:
- `skillhome.ReservedSkillPrefixes = ["dingtalk-"]`、
`skillhome.ReservedSkillNames = ["dws-shared", "dws"]`,替换
`skill_setup.go:199/205` 与 `paths.go:332-334` 两份私有拷贝;所有互斥/
stale 清理只认这两个常量。
- 测试一:扫描 `skills/multi/` 实际目录名,断言全部命中保留规则(防新增
产品 skill 破坏前缀约定)。
- 测试二:市场 skill 安装路径(`skill_command.go:488-498` 的目标解析)若
产物名命中保留名则拒绝安装——把"市场无同名前缀"从祈祷变成门禁。
## 3. 迁移步骤(渐进、不破坏存量用户)
| 阶段 | 内容 | 风险 |
|---|---|---|
| **P0 清单 + policy** | 建 `internal/skillhome`:AgentHomes(**补 opencode**,修 1.2-1)、布局/互斥/保留名常量;setup/upgrade 切到共享包;脚本清单包标记块;新增 `check-agent-homes-sync.sh` 进 `make policy`;删 install.sh 死代码(1.2-2) | 低。纯收敛不改行为;policy 脚本初期可能误报 → 先 warn-only 跑一个版本再转 hard-fail |
| **P1 setup 单引擎 + 脚本降级** | setup/upgrade 安装实现合一(互斥清理、stale 语义统一为"引擎内一种",修 1.2-3);install.sh / install.ps1 / install.js / install-skills.sh 改为 bootstrap + 冻结 fallback;install-event/devapp 暂不动 | 中。bootstrap 首版要在三语言 CI 矩阵上验证"引擎失败→fallback"链路;fallback 标记冻结,防止继续演进出第 9 份拷贝 |
| **P2 state 权威 + 缓存收敛** | state.json 读写进 skillhome,setup/upgrade 记账;缓存加版本戳、写方收敛到 Go、upgrade 补刷 mono 缓存;install-event/devapp 语义收敛(mono+multi 双装改为引擎语义)后同样 bootstrap 化 | 中。存量机器无 state → 磁盘反推逻辑必须有故障注入测试;event/devapp 版本错配场景需保留 zip 直装逃生门 |
| **P3 删除遗留拷贝** | 删除:脚本侧 6 份缓存写、install.sh/ps1/js 内被引擎接管的安装函数(保留冻结 fallback 一档)、Go 侧 `mutualExclusionVictims` 等被 skillhome 吸收的私有拷贝;观察一个版本后评估 fallback 是否可再降档 | 低-中。每删一处先确认 policy 与测试不再引用;fallback 删除是独立决策,不与本阶段捆绑 |
与《迁移计划》的先后关系:本文 P0 即计划 P0c,应**最先做**;P1 与计划
P0a/P0b 并行不冲突(引擎合一会让 P0a 的 mode-aware upgrade 少写一份代码);
P2 吸收计划 P0b;P3 在计划 P2 切默认之后执行。
## 4. 明确不做
- **不重写成 symlink canonical**(canonical 一份 + 各 agent home 软链):
已随生态分发通道一并评估否决,决策与实测原因见
《迁移计划》[§7.3](skill-multi-migration-plan.md)(无版本固定、依赖
Node/GitHub 可达、mono 会被一起发现、悟空 bundled/离线预装覆盖不了;
其中悟空分发线已于 2026-08-05 下线,该条约束随之消失)。
本地 symlink 模式(`setup --link`)同样维持计划 §8.2"可选/后置"定位,
不在本方案内。
- **不动 `dws-skills.zip` 产物布局**:zip 根 mono 副本 + `mono/` + `multi/`
双树保持不变(`scripts/release/post-goreleaser.sh:220-248`)。
- **不动市场 skill**:`dws skill install` 的安装语义、目标解析
(`skill_command.go:488-498`)不变;2.5 只新增保留名拒绝门禁与测试,
不改变既有安装行为。
- **不删 fallback**:脚本侧内嵌拷贝逻辑降级为冻结档保留,不追求"脚本零
拷贝"的纯净化。
## 5. 收益/成本与任务映射
### 5.1 收益/成本表
| 项 | 现状 | 目标 | 量化收益 |
|---|---|---|---|
| agent home 清单 | 15 份拷贝,已漂移(opencode) | 1 份 Go 源 + policy 比对 | 新增 agent 从"改 11 处"变"改 1 处" |
| 安装/互斥/stale 逻辑 | ≥8 份,4 种语言,语义已分叉 | 1 个 Go 引擎 | 语义变更从 4 语言 × 7 面(本次实证 13 文件 +759 行)变 1 包 |
| mode 解析 | 4 份,行为细节不一 | 脚本只做透传,引擎一处实现 | 非法值/默认值行为天然一致 |
| 缓存写 | 6 份,无版本戳,只写不读 | Go 唯一写方 + 版本戳 | 缓存漂移可观测、可判废 |
| 安装状态 | 无 | state.json 权威 | upgrade 粘性、回滚、drift 检测的前提 |
| 市场边界 | 隐性约定,2 份前缀拷贝 | 共享常量 + 2 个测试 | 互斥清理误伤市场 skill 的风险归零 |
| 成本 | — | P0≈1.5d,P1≈3-4d,P2≈2d,P3≈1d | 合计 7.5-8.5 人日;其中 P0/P2 与《迁移计划》P0b/P0c 重叠,净新增约 4-5 人日 |
### 5.2 与《迁移计划》§8 任务 ID 映射
| 本文阶段 | 对应计划任务 | 关系 |
|---|---|---|
| P0 清单 + policy | P0c-1(清单下沉共享包+补 opencode)、P0c-2(check-agent-homes-sync.sh) | 原样采纳,包名定为 `internal/skillhome` |
| P1 引擎合一 | P0a-1(UpgradeSkillLocations mode-aware)的前置简化 | 引擎合一后 P0a-1 不复写互斥/清理逻辑 |
| P1 脚本 bootstrap | **取代** P2-2(sh/ps1 multi 真装,1.5d)、P2-3(install.js/install-skills.sh mode 支持,1d) | 脚本不再写安装逻辑,两个任务缩水为"透传 mode + 调引擎 + 冻结 fallback",合计从 2.5d 降至 ~1d |
| P2 state 权威 | P0b-1(state.json schema+setup 写入)、P0b-2(磁盘反推) | 原样采纳;缓存版本戳与写方收敛为本文新增 |
| P2 event/devapp 收敛 | 新增(修 1.2-4 语义矛盾) | 依赖 P1 引擎稳定 |
| P3 删除遗留 | 新增 | 在计划 P2 切默认之后执行 |
| (不在本文) | P0-3 备份式安装、P0-4 请求头、P1-1 `dws skill mode`、P1-2/P2-1 默认翻转、P2-4 文案 | 仍按计划执行,与本文正交;备份式安装落地时直接写进引擎,天然覆盖所有面 |