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

21 KiB
Raw Blame History

Skill 分发/安装架构优化方案

目标:把 DWS Agent skill 的"装在哪、怎么装、装完记什么"从 7+ 个安装面的 N 份漂移拷贝,收敛为 Go 侧单一事实源 + 单一安装引擎 + 脚本侧 bootstrap。 本文基于 feat/skill-mode-migration 工作区(multi 化落地后)的代码级盘点 (2026-08-05),所有锚点均可跳转验证。 前置文档:skill-multi-migration-plan.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 分层总览

┌──────────────────────────────────────────────────────────────────┐
│ 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(无版本固定、依赖 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 文案 仍按计划执行,与本文正交;备份式安装落地时直接写进引擎,天然覆盖所有面