@dsh-workbench/core 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +19 -0
  2. package/bin/dsh-workbench +21 -1
  3. package/client.js +163 -89
  4. package/cordis.patch.yml +40 -0
  5. package/dist/client/client.body.js +163 -89
  6. package/dist/client/index.js +163 -89
  7. package/dist/host/index.js +1474 -93
  8. package/dist/types/app/__tests__/workbench-styles.test.d.ts +1 -0
  9. package/dist/types/app/client.d.ts +39 -15
  10. package/dist/types/app/layouts/rightbar/board-columns.d.ts +8 -0
  11. package/dist/types/app/layouts/rightbar/board-columns.test.d.ts +1 -0
  12. package/dist/types/app/layouts/rightbar/market-rows.d.ts +2 -0
  13. package/dist/types/app/layouts/workbench-styles.d.ts +2 -0
  14. package/dist/types/modules/panels/__tests__/diy-harness.d.ts +51 -0
  15. package/dist/types/modules/panels/__tests__/diy-tools-status.test.d.ts +1 -0
  16. package/dist/types/modules/panels/__tests__/diy-tools.test.d.ts +1 -0
  17. package/dist/types/modules/panels/model/diy-tools.d.ts +4 -0
  18. package/dist/types/modules/panels/unit.d.ts +1 -0
  19. package/dist/types/shared/api/bundle-install.d.ts +1 -0
  20. package/dist/types/shared/api/host/diy-facts.d.ts +9 -0
  21. package/dist/types/shared/api/host/diy-facts.test.d.ts +1 -0
  22. package/dist/types/shared/api/host/diy-recon-fs.d.ts +3 -0
  23. package/dist/types/shared/api/host/diy-recon-fs.test.d.ts +1 -0
  24. package/dist/types/shared/api/host/diy-recon.d.ts +39 -0
  25. package/dist/types/shared/api/host/diy-recon.test.d.ts +1 -0
  26. package/dist/types/shared/api/host/diy-workshop.d.ts +12 -0
  27. package/dist/types/shared/api/host/diy-workshop.test.d.ts +1 -0
  28. package/dist/types/shared/api/host/installed-module.d.ts +2 -0
  29. package/dist/types/shared/api/host/opencli-command.d.ts +10 -0
  30. package/dist/types/shared/api/host/opencli-command.test.d.ts +1 -0
  31. package/dist/types/shared/api/host/runtime-seed.d.ts +40 -0
  32. package/dist/types/shared/api/host/runtime-seed.test.d.ts +1 -0
  33. package/docs/HANDOFF.md +82 -44
  34. package/docs/OPERATIONS.md +22 -1
  35. package/package.json +5 -5
  36. package/scripts/seed-profile.sh +4 -113
  37. package/scripts/seed-workshop.sh +216 -0
  38. package/skills/workbench-module-diy/BUILD.md +36 -66
  39. package/skills/workbench-module-diy/DATASOURCE.md +80 -12
  40. package/skills/workbench-module-diy/DESIGN.md +27 -2
  41. package/skills/workbench-module-diy/LINKAGE.md +49 -19
  42. package/skills/workbench-module-diy/PANEL.md +78 -4
  43. package/skills/workbench-module-diy/REVIEW.md +6 -5
  44. package/skills/workbench-module-diy/SKILL.md +32 -12
  45. package/skills/workbench-module-diy/STEPS.md +4 -0
  46. package/skills/workbench-module-diy/TOOLING.md +47 -0
  47. package/skills/workbench-module-diy/TOOLS.md +147 -5
  48. package/skills/workbench-module-diy/WIRING.md +88 -0
@@ -1,6 +1,7 @@
1
1
  # 面板:他看什么、他配什么、画成什么样
2
2
 
3
- 步骤 1 的参考。全部用用户视角写 —— 这一层不出现实现。
3
+ 步骤 1 的参考:问什么、他看到什么。正文用**他的视角**写,唯一的例外是 §5.1 ——
4
+ "这份配置存不存"有代码形状,那一节给写法。
4
5
 
5
6
  ## 1 面板上该有的东西只有三类
6
7
 
@@ -63,6 +64,53 @@
63
64
  | 怎么生效 | 他按下保存才生效;保存成功直接回到看板并立刻按新的显示;保存失败留在原地告诉他为什么 |
64
65
  | 怎么清 | 他自己显式清;收起或卸载这块面板**不删**他的配置 |
65
66
 
67
+ ### 5.1 两种配置写法,别混
68
+
69
+ 配置要分清**存不存** —— 它决定代码怎么写,也决定他刷新之后看到什么。`design.json` 的
70
+ `config[].persistence` 这一格写的就是这个(`prefs` = 存起来;`view` = 只选一下)。
71
+
72
+ | 写法 | 什么时候用 | 刷新后 | 怎么实现 |
73
+ |---|---|---|---|
74
+ | `view` | 卡片底部一次性的选择(看哪种口径、看几天) | 回默认 | 当 **route 参数**传给模块自己的只读口;非法的退路写进 `wrongChoice` |
75
+ | `prefs` | 他配一次、以后一直算数(钉哪个环境、忽略哪些、超时多久) | **还在** | `mount` 返回 `prefs: { resource, handle }`,`handle` 里用 `deps.store` 落盘 |
76
+
77
+ **宿主半侧**(最小可抄;工坊里 `module-diy-doctor/src/prefs.ts` 是完整例子):
78
+
79
+ ```ts
80
+ // mount(deps) 里。按项目分开记:这个项目一份,换项目不串
81
+ const store = deps.store.for(UNIT_ID, workspace);
82
+ return {
83
+ // …
84
+ prefs: {
85
+ resource: 'your-module',
86
+ // 校验 + 落盘 + 把新的整份偏好回给调用方(别只回 ok)
87
+ handle: (payload) => applyPref(store, payload),
88
+ },
89
+ };
90
+ ```
91
+
92
+ **浏览器半侧**:写走 `prefs` 通道,读还是模块自己的只读口(别为配置另开一条)。
93
+ `Field`(标签 + 提示 + 输入框的排布)与 `wb-textField` 的内边距都是外壳给的默认,**模块不需要为"能看"写 CSS**:
94
+
95
+ ```tsx
96
+ const write = useHostWrite({ channel: 'prefs', resource: 'your-module' });
97
+ // Field + TextField / ChoiceGroup,外面套 SettingsSection,保存用 SaveBar;失败留在原地
98
+ // (输入框的内边距和"标签 + 提示"的排布由外壳给默认,模块不用自己写 CSS)
99
+ write.send({ ignored, timeoutSeconds, workspace });
100
+ ```
101
+
102
+ `handle` 里那几件事(成熟模块都这么做,`argocd` 的 `prefs.test.ts` 把每一条都钉住了):
103
+
104
+ - **未设置 ≠ 空**:没写过就出"第一次选"那一屏;显式选空是"空态",两者不能混;
105
+ - **清空 = 回到默认,并把文件收掉**(不留一个空对象);
106
+ - **按项目 / 工坊分开记**,换项目不串;
107
+ - **不认识的值报错,并把还认的候选列出来**(不悄悄换一个别的);
108
+ - 去重、按他给的顺序存。
109
+
110
+ **他会看到什么**:面板上多一段"他能自己配的"(填 / 选 + 保存 + 回到默认);存成功卡上立刻按新的显示
111
+ (写偏好会 bump 这个单元声明过的事实,见 [LINKAGE.md](LINKAGE.md) §8);**存失败留在原地并说清为什么**。
112
+ agent 那一侧也要能通过模块自己的能力读到同一份偏好。
113
+
66
114
  ## 6 界面的几个习惯
67
115
 
68
116
  - **有不打扰的默认就别弹设置**:能用默认值读出他关心的那份,就直接显示,别先问他。真的分不清是哪一份,才把选择摆到他面前(不是先让他读一句话再点一次)。
@@ -72,6 +120,19 @@
72
120
  - **默认收起**:历史、明细折叠,露出最近一条;只有一条时不给折叠三角(没有可展开的东西)。
73
121
  - **屏幕上的字越少越好**:他能看懂的(红点绿点、数字)就不用再写一段规则解释;删掉的东西不要留一行说明。
74
122
  - **默认状态点**只有四种意思:在做 / 做完了 / 出错 / 没在跑。
123
+ - **外链一律用 kit 的 `Link`**(`h(Link, { href: … }, 文案)`),而且**它自带的样子就是唯一一份**:
124
+ 新窗口 + `rel="noopener noreferrer"` + 12px 正文色 + hover 下划线 + 自带那个外链小箭头。
125
+ **别去掉箭头、别换箭头、别自己调**(没有 `icon` 这个口子)—— 全仓的链接必须长得一样。
126
+ 模块源码里手写 `<a>` / `h('a', …)` 会被「样式归属」判据**直接判红**。
127
+ **文案里也不许自带箭头**(`'去网关看账单 →'` 这种)—— 箭头由组件给,自带的话页面上就是两颗。
128
+ 它内部已经把 dsh 的图标原语包好了,所以模块**不用**为它声明 `client.external`。
129
+ - **"去看某处"用哪个**:跳**整张卡对应的系统** → 卡头的图标按钮(`CardHead.platformHref`:只有图标 + Tooltip。
130
+ **给这一个就够了**,那颗 ↗ 立刻就画;`platform` / `platformTitle` 是可选的,只决定 Tooltip 与
131
+ `aria-label` 上的字 —— 以前的实现要两个都给才画,少给一个就静默少一颗按钮);
132
+ 跳**一个具体页面 / 一条具体记录** → `Link`(正文里的文字链接);**触发一个动作**(刷新、重建、去登录)→
133
+ dsh 的 `Button`,或阻断屏里的 `action`。区别在"跳去哪"和"它是不是一个动作",不在好看不好看。
134
+ - **标签 + 输入框的排布用 `Field`**,输入框用 `TextField` —— 内边距与排版是外壳给的默认,
135
+ 别为"能看"写模块自己的 CSS(见 §5.1)。
75
136
 
76
137
  ## 7 状态矩阵怎么写
77
138
 
@@ -118,7 +179,20 @@
118
179
  - **画不成的才写词**:动作、agent 的下一步、时间性的"不变 / 自己变",写成词并说清意思。
119
180
  - **真机上造不出来的状态**:沟通时照样画(画的是设计意图);实现时用评审夹具把它造出来给人切。
120
181
 
121
- ## 8 范例(看真实产出的形状)
182
+ ## 8 范例(真实产出的形状)
183
+
184
+ 范例都在**工作台仓库里**:`.agents/skills/module-design/EXAMPLES.md` §1 —— 工坊会话里够不到那儿,
185
+ 就照 [SKILL.md](SKILL.md) 的「找不到一件事的时候」读模板与装机模块的源码。
186
+
187
+ ## 9 界面边界:kit 不够用时怎么办
188
+
189
+ **先从 kit 拿**(`@dsh-workbench/core/client`,`check --only kit` 会列全这份外壳有什么)。
190
+ kit 里确实没有、而你要的又是标准控件(确认条、选择器、设置区块那种)时,可以引 dsh 的客户端原语:
191
+
192
+ * 只在**浏览器侧**引(`src/client.tsx` 那一侧);
193
+ * 在 `package.json` 的 `dsh.client.external` 里**声明** `@deepseek-ai/dsh-client-ui-primitives`;
194
+ * 宿主侧(`src/host.ts`)**一个字都不许碰** —— `check` 的源码边界判据会红。
122
195
 
123
- - 卡片四态 + 最小确认条 + 设置面:`.scratch/cmdb-module/spec.md` §3.0 / §3.1(图 A–O)。
124
- - 卡片即全部 + 设置面(平铺单选、显式保存、保存成功回看板)+ 缺态矩阵:`.scratch/coding-analysis/spec.md` §3.1(图 A–L)/ §4(设置面)/ §3.3(缺态矩阵)。
196
+ 理由不是"能省事":装机成熟模块里有一半的界面是这么搭的(argocd / cmdb / coding-issues / coding-analysis
197
+ 四个包下 23 个文件引了这个包)。但**能拿 kit 的时候别自己画** —— kit 那套跟着外壳一起换主题、跟着语言走,
198
+ 自己画的不会。
@@ -14,7 +14,7 @@
14
14
  | `standalone` | "只有工作台源码在场才编得过" ⇒ 发布出去就散架 | `build 在外壳源码不在场时失败` / `typecheck 在外壳源码不在场时失败` | 别引用工作台源码;`tsconfig` 里 `paths` 的值必须是**相对路径**(`"./src/*"`,不是 `"src/*"`) |
15
15
  | `markers` | 内网域名 / 企业内部名泄进要公开发布的包 | `命中内网 npm 源` / `命中内网域名` | 那些值不该写进包里;要配就落到模块自己的配置面,或走用户自己声明的来源 |
16
16
  | `alias` | `@/` 伸进工作台源码 ⇒ 装的人手上没有那份源码 | `…指向外壳源码` | `@/` 只许指自己的 `src/`;跨包的东西走包名 |
17
- | `style` | 模块定义 `.wb-*` 去覆盖外壳的类(全局类名,两份规则按注入顺序打架) | `定义了外壳前缀 wb-* 的 CSS 规则` / `借用了契约里没有的类` | 自己的类名前缀 `wb-<unit>-`;外壳给的类只能用契约里列出的那些 |
17
+ | `style` | 模块定义 `.wb-*` 去覆盖外壳的类(全局类名,两份规则按注入顺序打架) | `定义了外壳前缀 wb-* 的 CSS 规则` / `借用了契约里没有的类` | 自己的样式走 `*.module.css`(类名**不带** `wb-` 前缀,构建时按文件路径哈希);外壳给的类只能用契约里列出的那些,要借就写 `:global(.wb-…)` |
18
18
 
19
19
  ## 什么时候该怀疑门禁本身
20
20
 
@@ -25,7 +25,8 @@
25
25
 
26
26
  ## 三条"不是判据、但更常踩"的
27
27
 
28
- 1. **装上去没反应** —— 面板默认是空的,装了还要在面板配置里打开它(那一跳会顺手做掉)。
29
- 2. **改完没热换** —— 你是不是把构建改成了"写临时文件再改名"?宿主的监听只认 `change`,不认 `rename`。
30
- 另外:改 `cordis.patch.yml` 或 `package.json` 里的 `dsh.client` / `exports["./client"]` 本来就要重启。
31
- 3. **"再装一遍"** —— 同名重装必然要求重启,而且不会重载。日常迭代只重建,不重装。
28
+ 一条结论 + 去哪读全(细节只有一份,住在 BUILD.md):
29
+
30
+ 1. **装上去没反应** → 先问 `workbench_diy`(`action=status, unit=…`),别自己拼。[WIRING.md](WIRING.md) §3
31
+ 2. **改完没热换** → 宿主侧是秒级热换的;客户端侧刷新一下页面。[WIRING.md](WIRING.md) §4
32
+ 3. **"再装一遍"** → 日常迭代只重建、不重装。[WIRING.md](WIRING.md) §3
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: workbench-module-diy
3
- description: 在工作台里凭需求造一个模块(面板上的一块内容 + 它背后的能力)。用在「我想在面板上看到 X」「加一块面板 / 一张卡 / 一个模块」「让 agent 替我读或写某个东西」「agent 动完面板哪一格跟着变」这类话上 —— 用户只说需求,实现由 agent 自己做、自己过门禁,最后给他一个能点开验收的东西。
3
+ description: 在工作台里凭需求造一个模块(面板上的一块内容 + 它背后的能力)—— 用在「我想在面板上看到 X / 加一块卡」「让 agent 替我读或写某个东西」「agent 动完面板哪一格跟着变」这类话上。用户只说需求,实现由 agent 自己做、自己过门禁,最后给他一个能点开验收的东西。
4
4
  ---
5
5
 
6
6
  # 造一个工作台模块:他说需求,你做完给他看
@@ -11,17 +11,27 @@ description: 在工作台里凭需求造一个模块(面板上的一块内容
11
11
 
12
12
  1. **判域** —— 先读 [DESIGN.md](DESIGN.md) §0。不是所有需求都该在这里做;判完给他**一句人话 + 下一步**。
13
13
  2. **问三张表** —— 见 [STEPS.md](STEPS.md)。只问他看得见、能决定的事;**实现一律不问**。
14
- 这一步的产出是 ASCII 画面 + 三张表,不是文字。
14
+ 这一步的产出是 ASCII 画面 + 三张表,不是文字。三张表当场落进模块里的 `design.json`
15
+ (命令与形状:[DESIGN.md](DESIGN.md) §5)—— 只留在对话里等于没定。
15
16
  3. **先定数据从哪来** —— [DATASOURCE.md](DATASOURCE.md):干净机器上没有现成 skill 可抄,
16
- 用 `opencli` 复用他的登录去探,探明后**固化成一条读**。顺手把三态写掉。
17
+ 用 `workbench_diy`(`action=probe, url=…, script=…`)复用他的登录去探 —— 两拍、重试、失败码归一
18
+ 都在那把工具里;探明后**固化成一条读**(`@dsh-workbench/shared/host` 的 `browserRead`)。顺手把三态写掉。
17
19
  4. **从模板改** —— [BUILD.md](BUILD.md) §1–§2:在工坊里 `dsh-wb-module init` 一个骨架,
18
- 只改数据源与渲染。**不从零写。**
19
- 5. **自己过门禁** —— `dsh-wb-module build` + `check`;红了自己修([REVIEW.md](REVIEW.md) 有对照),
20
- 不抛给他、也不贴报错给他看。
21
- 6. **让他看一眼** —— 先对着 ASCII 图确认,再**装上看看**(装是宿主侧那一跳,它会顺手在面板里打开它)。
22
- **这一跳一辈子只走一次**:之后只重建、不重装。
20
+ 把 `design.json` 里的**三张表与数据源记录先改成你问出来的那份**(`design check` 会点名叫出还空着哪一格),
21
+ 再改数据源与渲染。
22
+ **不从零写**。
23
+ 命令写成 `pnpm exec dsh-wb-module …`(工坊里 PATH 上也有同名入口,但 `pnpm exec` 到处都对)。
24
+ 5. **自己过门禁** —— `dsh-wb-module build` + `check` + `design check`;
25
+ 红了自己修([REVIEW.md](REVIEW.md) 有对照),不抛给他、也不贴报错给他看。
26
+ 6. **让他看一眼** —— 先对着 ASCII 图确认,再用 `workbench_diy`(`action=install, unit=<单元 id>`)装上:
27
+ 它只认工坊里造好的那个模块,装完顺手在**这个项目**的面板上打开(装的是这台机器上的东西,他会点头一次)。
28
+ **这一跳永远只走一次**:之后只重建、不重装(宿主侧秒级热换;改了 client.js 刷新一下页面)。
29
+ 拿不准它现在走到哪一步(装了没 / 面板认得没 / 这个项目上开着没 / 产物齐不齐 / 源码比产物新没),
30
+ 就问工具:`workbench_diy`(`action=status, unit=<单元 id>`)会把这几件事一次说清并给出下一步 ——
31
+ **别自己拼,也别再装一遍**;已经装过只是想上板,用 `action=open`。
23
32
  7. **验收** —— 逐条对着三张表走:「打开哪 → 看什么 → 点什么 → 应该看到什么」。
24
33
  他说哪不对就回对话改,改完让他看**同一处**;不给他看 diff、日志、代码。
34
+ 逐条过他都说对了,再 `dsh-wb-module design accept` 把验收打勾(**没验完不许打**)。
25
35
 
26
36
  ## 每个问题都带推荐答案
27
37
 
@@ -39,15 +49,25 @@ description: 在工作台里凭需求造一个模块(面板上的一块内容
39
49
  | 面板上该有什么、状态矩阵怎么写、界面习惯 | [PANEL.md](PANEL.md) |
40
50
  | 能力怎么划、传什么拿回什么、哪几件要点头 | [TOOLS.md](TOOLS.md) |
41
51
  | agent 动完面板哪一格变、他正在干的时候看哪儿 | [LINKAGE.md](LINKAGE.md) |
42
- | 落点、文件形状、命令、装与热换的纪律 | [BUILD.md](BUILD.md) |
52
+ | 落点、文件形状、命令、两个面 | [BUILD.md](BUILD.md) |
53
+ | 依赖、会被拦的写法、装、热换、发布 | [WIRING.md](WIRING.md) |
43
54
  | 门禁红了、装上去没反应、改完没热换 | [REVIEW.md](REVIEW.md) |
55
+ | 哪条纪律已经固化成工具、哪些还留给 agent | [TOOLING.md](TOOLING.md) |
44
56
 
45
57
  ## 找不到一件事的时候,按这个顺序找
46
58
 
47
59
  1. **先跑门禁**:`dsh-wb-module check --only <那一条>` 的报错原文最准。
48
- 2. **再看模板**:已经有一个同形状、能跑的最小实现,比任何描述都准。
49
- 3. **最后看工作台仓库**:`packages/module-hello` 是最小样本;契约口径在 `docs/ARCHITECTURE.md` 与 `docs/adr/`;
50
- 自己包里的 `node_modules/@dsh-workbench/core` 就是外壳本体(可以读它的 `dist/types/**`)。
60
+ 「外壳的 kit 里到底有什么组件 / hooks」也问它:`check --only kit` 会把这份外壳的 kit 成员列出来。
61
+ 2. **再看模板**:已经有一个同形状、能跑的最小实现,比任何描述都准 ——
62
+ 在你工坊的 `node_modules/@dsh-workbench/module-tools/template-diy/`(带真数据源的样板)
63
+ 与 `template/`(空壳)里,原文都是可读的。
64
+ 3. **最后看你装的那份外壳**:`node_modules/@dsh-workbench/core/dist/types/**` 是它的类型面,
65
+ `node_modules/@dsh-workbench/core/skills/workbench-module-diy/**` 就是这份指引。
66
+ 它的 README 与 `docs/HANDOFF.md` / `docs/OPERATIONS.md` 也在包里。
67
+
68
+ ⚠️ **别去工坊外面找**:这几份指引里出现的 `docs/adr/**`、`.scratch/**`、`packages/**`
69
+ 是**工作台仓库里**的路径(只有你恰好在那份仓库里时才有)。工坊里的会话够不到它们 ——
70
+ `node_modules/@dsh-workbench/…` 才是你在工坊里够得到的那一份。
51
71
 
52
72
  ## 三条不许松的
53
73
 
@@ -2,6 +2,10 @@
2
2
 
3
3
  [DESIGN.md](DESIGN.md) 是三张表的来历,本文是每一步的**问法**与**完成判据**。
4
4
 
5
+ 每一步定下来的东西都**当场落进**模块里的 `design.json`:面板 = `panel` / `states`、配置 = `config`、
6
+ 能力 = `capabilities`、联动 = `linkage`、数据源 = `datasource` / `reads[]`、验收 = `acceptance`。
7
+ `dsh-wb-module design check` 会把空着的格列出来 —— **只留在对话里等于没定**。
8
+
5
9
  ## 步骤 1 — 面板:他看什么,他配什么
6
10
 
7
11
  先问出来(一次问一组,每组带推荐答案):
@@ -0,0 +1,47 @@
1
+ # 哪些纪律已经固化成工具,哪些留给 agent
2
+
3
+ 这份是 DIY 指引(`SKILL.md` / `STEPS.md` / `PANEL.md` / `TOOLS.md` / `LINKAGE.md` / `BUILD.md` /
4
+ `WIRING.md` / `REVIEW.md` / `DATASOURCE.md` / `DESIGN.md`)与**工具面**之间的对照表。判据只有一条:
5
+
6
+ > **机器能判定的,就不该让 agent 记;只有需要判断的,才留给人。**
7
+
8
+ 它回答两件事:**这条纪律还用不用我盯着**(不用 → 跑哪一条命令),
9
+ 以及**手上这件事能不能也做成工具**(判断类的别做)。
10
+
11
+ ## 机器判的(跑一次就有答案)
12
+
13
+ | 纪律(出处) | 固化形态 |
14
+ |---|---|
15
+ | 十条判据([REVIEW.md](REVIEW.md)) | `dsh-wb-module check`(`--only <判据>` 单跑;`--only kit` 列这份外壳的 kit 成员) |
16
+ | 落点、文件形状、四个脚本([BUILD.md](BUILD.md) §1–§2) | `dsh-wb-module init --template diy`(骨架带一条真读盘的数据源)+ `scripts/smoke.mjs` |
17
+ | 装、上板、不重装([WIRING.md](WIRING.md) §3) | `workbench_diy`(`install` / `open` / `status`)+ `workbench_panels` |
18
+ | 空着的格就是还没定的事([DESIGN.md](DESIGN.md) §5) | `design.json` + `design check`(点名叫出空格)/ `render` / `accept` |
19
+ | 发货前的清单([WIRING.md](WIRING.md) §5) | `dsh-wb-module publish`(门禁 + 打包清单 `files` 漏项,只 dry run) |
20
+ | 干净机器上的第一脚([SKILL.md](SKILL.md) 第 4 步) | 启动器自愈骨架 + PATH 上的 `dsh-wb-module` |
21
+ | 会写的工具要点几次头([TOOLS.md](TOOLS.md) §6) | `writes: true` + `needsApproval` + `approvalDetail`,`check --only contracts` 查 |
22
+ | 跑一会儿的事怎么让用户看见([TOOLS.md](TOOLS.md) §9) | `longTasks` + kit 的 `useLongTask` / `LongTaskRow`,`check --only contracts` 查 |
23
+ | 这个能力要靠什么才跑得起来([TOOLS.md](TOOLS.md) §8) | `manifest.env` + `libInstallCommand`,`check --only contracts` 查 |
24
+ | 配置存不存([PANEL.md](PANEL.md) §5.1) | `config[].persistence`(`prefs` / `view`)+ `check --only contracts` |
25
+ | agent 动完面板哪一格变([LINKAGE.md](LINKAGE.md) §8) | `factsOf` + `factOfRoute` / `context` / `warmReads` / `values`,`check --only contracts` 只查形状 |
26
+ | kit 不够用时能不能引 dsh 的界面原语([PANEL.md](PANEL.md) §9) | 只在客户端侧引 + `dsh.client.external` 里声明;宿主侧一个字都不许碰(`check --only source`) |
27
+ | DOM 读怎么读([DATASOURCE.md](DATASOURCE.md) 第 2 步) | `probe`(探)+ `browserFind`(固化)—— 切格 / 按列取值是两个纯函数 |
28
+ | 链接只有一副样子([PANEL.md](PANEL.md) §6) | kit 的 `Link` 是唯一一份(**没有 `icon` 口子**),`check --only style` 拦裸 `<a>` |
29
+ | 输入框不用自己配样式([PANEL.md](PANEL.md) §5.1) | `TextField` 的默认内边距 + kit 的 `Field`(标签 + 提示的排布) |
30
+ | 同一把工具在有些项目里不该出现([TOOLS.md](TOOLS.md) §12) | `gate.deniedTools` + `design.json` 能力行的 `scope`;`check --only contracts` 只查正向 —— 反向会误伤"按自己的开关收窄"那一种用法 |
31
+
32
+ ## 留给 agent 的(判断,不该固化)
33
+
34
+ - **判域**:这个需求该不该在这里做 —— [DESIGN.md](DESIGN.md) §0
35
+ - **问三张表**:用他的词问、把决定翻译成"你会看到什么"、取舍与口径
36
+ - **探数时的创造性**:找哪个列表页、看哪个 XHR("怎么读一次"已固化,见上表)
37
+ - **信息设计与排版**:一屏放什么、结论放哪、字少到什么程度 —— [PANEL.md](PANEL.md)
38
+ - **安全取舍**:"说不清这次会发生什么就不许问" —— [TOOLS.md](TOOLS.md) §6
39
+ - **联动颗粒度**:动的是整卡、某一行、还是某一列 —— [LINKAGE.md](LINKAGE.md) §1
40
+ (工具只能保证"形状对","该多窄"是判断)
41
+
42
+ 工具只负责把**事实**摆出来(状态、报错原文、预览结果),判断仍然是人 / agent 的事。
43
+
44
+ ---
45
+
46
+ 为什么这样挑、哪一轮落地的(连同实测与反例):工作台仓库的
47
+ `.agents/skills/module-design/EXAMPLES.md` §4 —— 工坊会话里够不到那儿。
@@ -76,6 +76,29 @@
76
76
 
77
77
  **硬要求:说不清这次会发生什么,就不许问。**
78
78
 
79
+ ### 6.1 写法(清单 + 两个函数;缺一个门禁就红)
80
+
81
+ ```ts
82
+ tools: [{ name: 'workbench_demo_rebuild', writes: true }],
83
+
84
+ needsApproval: (call) => ({
85
+ kind: 'ask',
86
+ reason: '会覆盖产物',
87
+ question: { zh: '要重建这 3 个模块吗?', en: 'Rebuild these 3 modules?' },
88
+ fallback: { zh: '会覆盖它们的 dist 与 client.js', en: 'Overwrites their dist and client.js' },
89
+ }),
90
+
91
+ approvalDetail: async (call) => ({ zh: '会覆盖这 3 个模块的产物,不碰源码', en: '…' }),
92
+ ```
93
+
94
+ * `needsApproval` 回 `{ kind: 'read' }` = 这件不用问;回 `{ kind: 'ask', … }` = 要问;
95
+ **清单写着 `writes: true` 却不认领,外壳当场 deny**("在套件的 needsApproval 里补一条审批")——
96
+ 但那条错误只有真跑一次才看得见,所以 `check --only contracts` 会替用户先看一遍:缺 `needsApproval`、
97
+ `question` / `fallback` 的 zh/en 写不全、或 `approvalDetail` 没给 → **红**。
98
+ * `question` 是问句,`fallback` 是"细节拼不出来时"的兜底文案,两者都要 zh/en;
99
+ * `approvalDetail` 就是 §7 里"这次会发生什么"那段明细(一行一个事实);拿不到料时回
100
+ `{ blocked: '…' }` —— 那等于**不弹**(§7 那一行),不是弹一个空问句。
101
+
79
102
  - 缺东西就**先自己去看一遍**(走一遍预览,不真的动),把确定的事实摆出来;
80
103
  - 补不齐就**停下**,把原因交给 agent 去查 —— 不许让弹窗指挥用户去指挥 agent;
81
104
  - 用户侧不该出现"看不到版本 / 分不清是哪个项目"这类话(他根本不该在这个状态下被问)。
@@ -97,7 +120,97 @@
97
120
 
98
121
  问答的按钮、快捷键、弹窗本身都是现成的(不是这个模块自己画);模块能决定的是**里面那块明细画什么**。那块明细要自己排版:**一行一个事实**,别一行塞三句。
99
122
 
100
- ## 8 用户自己按的那个入口
123
+ ## 8 外部供给:这个能力要靠什么才跑得起来
124
+
125
+ 模块要的东西机器上不一定有(opencli 插件、内网源上的 npm 库)。**清单里的 `env` 段就是说这件事的** ——
126
+ 外壳的隔离运行时按它去装、去探,装机器的状态在 `workbench_modules list` 里逐条看得到
127
+ (实测:`cmdb` 声明了插件 `cmdb`/`coding` 与库 `opencli-workflow`,那份列表里就按名字列着它们与版本)。
128
+
129
+ ```ts
130
+ env: {
131
+ command: 'opencli',
132
+ libs: [
133
+ {
134
+ name: 'opencli-workflow',
135
+ registryEnv: 'DSH_WORKBENCH_X_REGISTRY',
136
+ registryDefault: 'https://…/pagoda-npm/',
137
+ purpose: '发布编排(合并 → 构建 → 发布)',
138
+ },
139
+ ],
140
+ plugins: [
141
+ {
142
+ name: 'cmdb',
143
+ commands: ['envs', 'builds', 'publish'],
144
+ installSource: { kind: 'npm', package: 'opencli-plugin-cmdb', registryEnv: '…', registryDefault: 'https://…/' },
145
+ },
146
+ ],
147
+ }
148
+ ```
149
+
150
+ * `libs` 四个字段都要:`registryEnv` 让**用户的源**能覆盖 `registryDefault`(换源时不用改代码),
151
+ `purpose` 就是给用户看的那半句"装它干嘛";
152
+ * `plugins[].installSource` 只有三种:`{kind:'npm', package, registryEnv}` / `{kind:'git', url}` / `{kind:'local', env}`;
153
+ * **没装好时用户看到的那句话,由你写**(外壳只负责装,它不知道你这个能力叫什么):
154
+
155
+ ```ts
156
+ import { libInstallCommand } from '@dsh-workbench/shared/host';
157
+ const need = manifest.env?.libs?.[0];
158
+ try {
159
+ return await loadWorkflow();
160
+ } catch (error) {
161
+ if (need === undefined) throw error;
162
+ throw new Error(`${need.purpose}还没装 —— 装它:${libInstallCommand({ need })}`, { cause: error });
163
+ }
164
+ ```
165
+
166
+ `libInstallCommand({ need })` 会连源和缓存目录一起拼好(`npm i --prefix … --registry '<源>' --cache …`),
167
+ **原样给用户,别自己拼 npm 命令**;卡片上把这句话放进失败态([PANEL.md](PANEL.md) 的三态里那条"照做")。
168
+
169
+ 判据(`check --only contracts`):`env` 不能是空的;`libs` 缺 `purpose`/`registryEnv`、`registryDefault`
170
+ 不是 `http(s)://` 或 `file://`、插件写错 `installSource` 都红;**声明了 `libs` 就必须用
171
+ `libInstallCommand`**(否则用户只看到"没装",没有能照做的那条命令)。
172
+
173
+ ## 9 长任务:跑一会儿的事
174
+
175
+ 一件事要跑几十秒(构建、发布、批量删),做成**长任务**:用户看得到进行中,也看得到结果。
176
+
177
+ 宿主半侧 —— `mount` 的 deps 里有 `longTasks`,用它建注册表,并把结果挂回 mount 返回值:
178
+
179
+ ```ts
180
+ mount({ longTasks, ... }) {
181
+ const runs = longTasks?.create({
182
+ key: 'rebuild',
183
+ run: async ({ payload, onStep }) => {
184
+ onStep({ kind: 'start', text: '开始重建 3 个模块' });
185
+ const done = await rebuild(payload, onStep);
186
+ return { count: done };
187
+ },
188
+ step: (step) => ({ step: step.text }),
189
+ outcome: (result) => ({ why: `重建了 ${(result as { count: number }).count} 个` }),
190
+ view: (snapshot) => ({ steps: String(snapshot.steps.length) }),
191
+ });
192
+ return {
193
+ routes: {},
194
+ registerTools: () => [],
195
+ ...(runs === undefined ? {} : { longTasks: runs }),
196
+ };
197
+ }
198
+ ```
199
+
200
+ 浏览器半侧 —— kit 里现成的(别自己轮询、别自己映射状态):
201
+
202
+ ```tsx
203
+ const feed = kit.hooks.useLongTask({ unit: 'demo', workspace });
204
+ // feed.runs:每条有 status / step / steps / why;画的时候用 <LongTaskRow .../> 或 longTaskDot
205
+ ```
206
+
207
+ * 状态只有四种:`running` 进行中 / `needs-login` 要登录(带 `loginUrl`)/ `ok` 完成 / `failed` 出错;
208
+ * **契约里没有"停止"**:用户要能中途停下,得你自己在 `run` 里看信号 —— 别在界面上摆一个不存在的按钮;
209
+ * 判据(`check --only contracts`):注册表五个方法要全、`create` 的 `key` / `run` / `view` 不能缺、
210
+ 客户端产物里必须出现 `useLongTask` / `LongTaskRow` / `longTaskDot` 之一 ——
211
+ 否则用户点完只看到"没反应",看不到进行中 / 要登录 / 完成 / 出错。
212
+
213
+ ## 10 用户自己按的那个入口
101
214
 
102
215
  他自己按按钮发起的事**不问他**(是他自己按的),但:
103
216
 
@@ -105,8 +218,37 @@
105
218
  - 做失败留在条里说原因,不假装成功;
106
219
  - 和 agent 走的是同一件事、同一个进度、同一个结果。
107
220
 
108
- ## 9 范例
221
+ ## 11 范例
222
+
223
+ 范例都在**工作台仓库里**:`.agents/skills/module-design/EXAMPLES.md` §2 —— 工坊会话里够不到那儿,
224
+ 就照 [SKILL.md](SKILL.md) 的「找不到一件事的时候」读模板与装机模块的源码。
225
+
226
+ ## 12 同一把工具,在有些项目里不该出现
227
+
228
+ 工具是**这台机器上的**,但用户的项目不止一个:在 A 项目里有意义的工具,到了 B 项目就是噪音 —— agent 看得见
229
+ 就会去调,调了才发现不行(两次调用白花,而且它还会再试)。**别让工具自己判断再回"这个项目用不了"**,
230
+ 而是当场把它从清单里拿掉:
231
+
232
+ ```ts
233
+ return {
234
+ routes,
235
+ registerTools,
236
+ gate: {
237
+ deniedTools: (workspace) => (workspace === null ? [TOOL] : []),
238
+ subscribe: (listener) => store.subscribe(listener),
239
+ },
240
+ };
241
+ ```
242
+
243
+ - `deniedTools(workspace)`:回"**在这个项目里,我的哪几个工具不存在**"(工具名数组,名字跟 `registerTools()`
244
+ 注册的完全一致)。`workspace` 是项目 id,也可能是 `null`(没有项目上下文)。
245
+ - `subscribe(listener)`:让你在自己状态变了(比如用户把这个能力关了)时通知外壳重算 —— 不接的话,
246
+ 用户关掉开关要等下次重启才生效。
247
+ - 外壳两道都做:清单里不出现(agent 看不到)+ 真调到时当场拒(防手滑)。你只要把 `deniedTools` 写对。
248
+
249
+ 外壳自己就是这么用的(工坊里看 `node_modules/@dsh-workbench/core/…`,仓库里是
250
+ `packages/workbench/src/modules/panels/index.ts`):某张卡在这个项目上没开 → 那张卡的工具全不出现;
251
+ 工坊那把窄门工具 `workbench_diy` **只在工坊会话里**出现,别处一律不出现。
109
252
 
110
- - 先摸清能流转到哪、再动手;预览 → 确认;每一页给下一层的标识;参数错了让 agent 自己能改:`.scratch/coding-analysis/spec.md` §5.2(每一页给下一层的标识)/ §5.3(参数错了让 agent 自己能改)。
111
- - 封成一个动作的发布:一跳、随时可续、失败给原始输出、点头时把"版本 / 环境 / 配置 / 分支 / 目标主机"摆出来:`.scratch/cmdb-module/spec.md` §5.1(工具与参数)/ §5.2(逐字返回)/ §5.4(点头时他看到什么)/ §6(联动)。
112
- - 一句话:"只放 agent 推不出来的事实":`.scratch/coding-analysis/spec.md` §5.1 的 token 一条。
253
+ 表里怎么写:`design.json` 的能力行有「在哪些项目里出现」那一格(`scope`:`always` / `workspace`)。
254
+ 写了 `workspace` 就必须真的实现 `gate.deniedTools` —— 表和代码对不上,`check --only contracts` 会红。
@@ -0,0 +1,88 @@
1
+ # 接上外壳并交付:依赖、禁区、装、热换、发布
2
+
3
+ [BUILD.md](BUILD.md) 的下一半。模块造出来了 —— 这儿说它从哪拿依赖、哪些写法会被门禁拦、
4
+ 怎么装上、改一行怎么看到它变、怎么发出去。
5
+
6
+ 设计口径看 [DESIGN.md](DESIGN.md) 与 [STEPS.md](STEPS.md);门禁红了看 [REVIEW.md](REVIEW.md)。
7
+
8
+ ## 1 依赖从哪拿
9
+
10
+ | 要什么 | 从哪拿 |
11
+ |---|---|
12
+ | 宿主侧要用的纯函数与常量(`param`、平台解析、工作区存储…) | `@dsh-workbench/shared/host`(写进 `dependencies`) |
13
+ | 浏览器侧要的一切(hooks、组件、图标、文案工具) | `@dsh-workbench/core/client` 的 `kit` |
14
+ | 类型 | `import type` 随便用(编译时擦除,不会进产物) |
15
+ | 外壳本体 | `@dsh-workbench/core`(peer 依赖,装的时候要比版本) |
16
+
17
+ **`kit` 里到底有什么,不要猜也不要背**:`dsh-wb-module check --only kit` 会把**这份外壳**的
18
+ kit 成员(`components` / `hooks` / `api` / `lib` 各有哪些)列出来 —— 它取的是外壳真实产物
19
+ `client.js` 里那一份,不是另抄的清单,所以永远和用户手上那版一致。
20
+
21
+ ⚠️ **浏览器侧不许 `import` `@dsh-workbench/shared` 的运行值**(两个入口都不行):
22
+ 它**不在**客户端构建的 external 默认表里(工坊里那份是
23
+ `node_modules/@dsh-workbench/module-tools/src/build.mjs`),
24
+ import 运行值会被**打进你的包**(第二份实例),而且浏览器运行时也解析不到它。
25
+ 宿主侧正相反:`@dsh-workbench/shared` 与 `/host` 都在 external 里,直接 import 才对。
26
+
27
+ **单例只能有一份**:kit 里的 hooks 与容器组件**不许被复制进模块包**
28
+ (各打一份的症状不是报错,而是"agent 改完面板不自己刷新")。它们只能从 `kit` 拿。
29
+
30
+ ## 2 四条会被门禁拦住的写法
31
+
32
+ | 别这么写 | 为什么 |
33
+ |---|---|
34
+ | 从 DOM + CSS 手搓一个 dsh 已经有的东西(按钮、弹窗、输入框、图标) | 重画就是第二份材质与交互(悬停、禁用、焦点环、色板),迟早跟 dsh 漂。**先从 `kit` 拿**;kit 里没有再自己写,并在 spec 里记理由 |
35
+ | `import … from '@deepseek-ai/dsh-client-ui-primitives'` | 模块**不直接** import dsh 的包;dsh 原语由外壳经 `kit` 交给你。直接引会被判红(那是第二份实例) |
36
+ | `import … from '@/shared/…'`、`../../workbench/src/…` | 装你的人手上没有工作台的源码。`@/` 只许指**你自己**的 `src/` |
37
+ | 在公开包里写内网地址 / 内网域名 / 企业内部名 | 门禁会扫出来并指到行 |
38
+
39
+ 另外两条不是"被拦",是纪律:**样式走 `*.module.css`,类名别带 `wb-` 前缀**(`wb-*` 是外壳的**全局**
40
+ 类名:模块定义它、或借契约里没有的那一个,`check:style` 都判红;模块自己的类名由构建**按文件路径
41
+ 哈希**,天然不会跟谁撞。要借外壳契约里的类,写 `:global(.wb-…)`,且那个类必须在契约里);
42
+ **`src/**` 里零注释** —— 要说的写进 `README.md` 或 spec。
43
+
44
+ ## 3 装上,看一眼
45
+
46
+ 模块造好、门禁绿了,**装这一步你自己做** —— 用 `workbench_diy`(`action=install, unit=<单元 id>`):
47
+
48
+ - 它只认**工坊里造好的**那个模块(按 unit 在工坊里查,不接任何路径)。
49
+ - 装完顺手在**当前会话这个工作区**的面板上把它打开。
50
+ - 装是这台机器上的持久变更 ⇒ 用户会看到一次点头,话里说清是哪个模块、会改什么。
51
+ - 会话的审批被关成 `never` 时,这一步会被**直接拒**(不是用户点了拒绝,是没人能被问)——
52
+ 让用户把审批开成 `ask`,或者在界面上点一次;判据与构建不受影响,只有这一步过不去。
53
+
54
+ 两个必须记住的后果:
55
+
56
+ - **那一跳一辈子只该走一次。** 包名一旦在 profile 里,**再调它不会再装**(会告诉你"已经装过了"),
57
+ 改源码 + `dsh-wb-module build` 就行;同名重装本来就只会得到"要重启",而那一支连重载都不做。
58
+ - **装 ≠ 上板**:面板默认是空的,得在面板配置里把它打开 —— 那一跳顺手做掉了。
59
+ 只有在"要重启才生效"或它还没登记进外壳时才会没打开,那时返回里会说清:
60
+ 等它生效后调 `workbench_panels`(`action=show, id=<单元 id>`)。
61
+ - **拿不准它现在走到哪一步就问 `action=status`**:装了没 / 面板认得没 / 这个项目上开着没 / 产物齐不齐 /
62
+ 源码是否比产物新,外加一句"下一步做什么"。这五件事别自己去 panels、profile、工坊目录三处拼 ——
63
+ 实测会拼错(把"要重启才注册"读成"没装上",或者源码改了没 build 却去重装一遍)。
64
+ - **已经装过、只是没上板**:`action=open`(只上板,不装)。
65
+
66
+ `workbench_diy` **只在工坊那个会话里**(工具清单按工作区收窄,别处连它都不在),
67
+ 落在别的项目里调用会被当场拒 —— 这就是"DIY 模式"在工具面上的样子。
68
+ 普通项目里装别的东西是另一把工具:`workbench_modules`。
69
+
70
+ ## 4 改一行,看到它变
71
+
72
+ 装完之后**不要再装一遍**。改源码 → `dsh-wb-module build` → 用户面板上那张卡自己变。
73
+
74
+ - **宿主侧**(`src/host.ts` / `balance.ts` … → `dist/host.js`):**秒级热换**,不重启、不刷新
75
+ (实测 2026-10-11:加一行探针 → build → 3 秒内接口返回新内容)。这条曾经是坏的,
76
+ 坏因是 `dsh-hmr` 跟着软链把文件描述符用光(`cordis.patch.yml` 的 `followSymlinks: false` 为它而加)。
77
+ - **客户端侧**(`src/client.tsx` / CSS → `client.js`):浏览器**不会自己**换,**刷新一下页面**即可
78
+ (实测:重建后 60 秒页面没有任何反应)。`*.module.css` 的改动是就地改写那一份 `<style>`,刷新即生效。
79
+ - **改 `cordis.patch.yml`、改 `package.json` 里的 `dsh.client` / `exports["./client"]` 需要重启。**
80
+ 这些是"模块怎么被装上"的声明,不是模块的内容。
81
+ - 重启操作只有在极少数情况下才需要用户做。真需要时,说清**为什么**、**要重启哪个**。
82
+
83
+ ## 5 发布
84
+
85
+ **工坊里的模块一律 `private: true`,不发到任何源** —— 模型写的代码在他自己的机器上跑,摘掉它是另一个决定。
86
+
87
+ 真要给别人用:先 `pnpm exec dsh-wb-module publish`(门禁 + 三张表 + 打包清单的 `files` 漏项,**只 dry run**),
88
+ 过了再摘 `private` 并**显式**写发布目标(`publishConfig.access` 或内网源 `publishConfig.registry`)。