@dsh-workbench/core 0.3.2 → 0.3.4

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 (68) hide show
  1. package/client.js +689 -149
  2. package/cordis.patch.yml +32 -0
  3. package/dist/client/client.body.js +689 -149
  4. package/dist/client/index.js +689 -149
  5. package/dist/host/index.js +3638 -2104
  6. package/dist/types/app/client.d.ts +120 -3
  7. package/dist/types/app/layouts/approval-detail.d.ts +13 -0
  8. package/dist/types/app/layouts/panel-hint.d.ts +3 -0
  9. package/dist/types/app/layouts/rightbar/market-hint.d.ts +16 -0
  10. package/dist/types/app/layouts/rightbar/market-rows.d.ts +2 -3
  11. package/dist/types/app/layouts/sidebar/RecordFooterAction.d.ts +16 -0
  12. package/dist/types/app/layouts/sidebar/record-footer.d.ts +4 -0
  13. package/dist/types/modules/panels/client.d.ts +1 -0
  14. package/dist/types/modules/panels/model/diy-browser-act.d.ts +2 -39
  15. package/dist/types/modules/panels/model/diy-browser-tools.d.ts +39 -0
  16. package/dist/types/modules/panels/model/diy-browser.d.ts +1 -14
  17. package/dist/types/modules/panels/model/diy-components.d.ts +3 -0
  18. package/dist/types/modules/panels/model/diy-components.test.d.ts +1 -0
  19. package/dist/types/modules/panels/model/diy-observers.d.ts +11 -0
  20. package/dist/types/modules/panels/model/diy-observers.test.d.ts +1 -0
  21. package/dist/types/modules/panels/model/diy-page.d.ts +1 -0
  22. package/dist/types/modules/panels/model/diy-record-bar.test.d.ts +1 -0
  23. package/dist/types/modules/panels/model/diy-record-capture.d.ts +27 -0
  24. package/dist/types/modules/panels/model/diy-record-capture.test.d.ts +1 -0
  25. package/dist/types/modules/panels/model/diy-record-failure.test.d.ts +1 -0
  26. package/dist/types/modules/panels/model/diy-record-routes.d.ts +2 -0
  27. package/dist/types/modules/panels/model/diy-record-routes.test.d.ts +1 -0
  28. package/dist/types/modules/panels/model/diy-record-scripts.d.ts +6 -0
  29. package/dist/types/modules/panels/model/diy-record-text.d.ts +89 -0
  30. package/dist/types/modules/panels/model/diy-record-text.test.d.ts +1 -0
  31. package/dist/types/modules/panels/model/diy-record-tools.d.ts +2 -0
  32. package/dist/types/modules/panels/model/diy-record.d.ts +76 -0
  33. package/dist/types/modules/panels/model/diy-record.test.d.ts +1 -0
  34. package/dist/types/modules/panels/model/diy-scripts.d.ts +0 -63
  35. package/dist/types/modules/panels/model/diy-spec.d.ts +5 -0
  36. package/dist/types/modules/panels/model/diy-spec.test.d.ts +1 -0
  37. package/dist/types/modules/panels/model/handoff.d.ts +1 -0
  38. package/dist/types/modules/panels/unit.d.ts +20 -12
  39. package/dist/types/shared/api/host/diy-acts.d.ts +4 -0
  40. package/dist/types/shared/api/host/diy-components.d.ts +54 -0
  41. package/dist/types/shared/api/host/diy-debug-act.d.ts +24 -7
  42. package/dist/types/shared/api/host/diy-recon.d.ts +10 -0
  43. package/dist/types/shared/api/host/diy-record-act.d.ts +19 -0
  44. package/dist/types/shared/api/host/diy-workshop.d.ts +1 -0
  45. package/dist/types/shared/api/host/mount-units.d.ts +4 -0
  46. package/dist/types/shared/hooks/use-diy-record.d.ts +8 -0
  47. package/package.json +3 -3
  48. package/scripts/seed-workshop.sh +42 -2
  49. package/skills/workbench-module-diy/BUILD.md +39 -17
  50. package/skills/workbench-module-diy/DATASOURCE.md +4 -1
  51. package/skills/workbench-module-diy/DESIGN.md +40 -8
  52. package/skills/workbench-module-diy/FAMILY.md +55 -0
  53. package/skills/workbench-module-diy/HOOKS.md +95 -0
  54. package/skills/workbench-module-diy/KIT.md +18 -6
  55. package/skills/workbench-module-diy/PANEL.md +9 -5
  56. package/skills/workbench-module-diy/REVIEW.md +117 -49
  57. package/skills/workbench-module-diy/SKILL.md +42 -9
  58. package/skills/workbench-module-diy/STEPS.md +1 -1
  59. package/skills/workbench-module-diy/TOOLING.md +10 -4
  60. package/skills/workbench-module-diy/TOOLS.md +56 -204
  61. package/skills/workbench-module-diy/WIRING.md +17 -8
  62. package/skills/workbench-module-diy/WRITES.md +131 -0
  63. package/dist/types/modules/panels/model/diy-act.d.ts +0 -4
  64. package/dist/types/modules/panels/model/diy-look.d.ts +0 -15
  65. package/dist/types/modules/panels/model/diy-net.d.ts +0 -3
  66. /package/dist/types/modules/panels/model/{diy-act.test.d.ts → diy-browser-act.test.d.ts} +0 -0
  67. /package/dist/types/modules/panels/model/{diy-look.test.d.ts → diy-browser-tools-shot.test.d.ts} +0 -0
  68. /package/dist/types/modules/panels/model/{diy-net.test.d.ts → diy-browser-tools.test.d.ts} +0 -0
@@ -1,259 +1,111 @@
1
1
  # agent 能力:他能替用户做什么,哪几件要用户点头
2
2
 
3
- 步骤 2 的参考。写的是"agent 传什么拿回什么、用户看到什么",不是怎么实现。
3
+ 步骤 2 的参考:agent 传什么拿回什么、用户看到什么(不是怎么实现)。
4
4
 
5
5
  ## 1 能力怎么划
6
6
 
7
7
  | 情况 | 结论 |
8
8
  |---|---|
9
- | 同一件事、同一份数据,只是"要不要展开明细" | **一件事**,加一个"展开"的选项(agent 少学一件事,也少花上下文) |
10
- | 不同的动作、不同的后果(一个只是看,一个会改东西) | **分开** |
11
- | 用户要反复做、而且每次都得他本人点头 | 别硬拆成一堆小事;考虑封成一个动作(一个按钮 + 一件事) |
9
+ | 同一件事、同一份数据,只是"要不要展开明细" | **一件事**,加一个"展开"的选项 |
10
+ | 不同的动作、不同的后果(看 vs 改) | **分开** |
11
+ | 用户要反复做、每次都得他点头 | 别硬拆成一堆小事;封成一个动作(一个按钮 + 一件事) |
12
12
 
13
13
  每件事都要能回答:"用户会怎么跟 agent 说这件事?"
14
14
 
15
15
  ## 2 传什么,怎么写清
16
16
 
17
- **写法:贴真实的那一段。** 一件能力=真实调用(参数 JSON)+ 真实返回正文(**照抄,不转述**);"大概返回一个列表"这种句子不算写清。参数 schema 也要照抄一份(模型每轮都收到它)。
17
+ **贴真实的那一段**:能力 = 真实调用(参数 JSON)+ 真实返回正文(**照抄,不转述**);schema 也照抄一份(模型每轮都收到它)。能打到平台就**真跑一遍**(连时间戳);打不到就用真实代码的输出、标出样例;实现后换真机记录。
18
18
 
19
- **哪来的"真实"**:能打到平台就**真跑一遍**、把记录粘进来(最好连时间戳一起);打不到就用真实代码产出的输出,并在旁边标明哪些值是样例。**实现之后**换成真机记录。
20
-
21
- - **字段名用人和 agent 都懂的话**(别用平台内部字段名)。
22
- - **默认值**:不给他什么,等于什么。
23
- - **范围**:只认哪几个值(例:严重度只认"严重 / 错误");值不对就拒绝并说清怎么改。
24
- - **上限**:要多少给多少,**不悄悄截**;超了就说清上限是多少。
25
- - **该先问谁**:这件事需要先拿到别的东西(先看能流转到哪、先看成员名单),就在描述里指给 agent 看("先调 X 拿到目标值")。
19
+ - **字段名**用人和 agent 都懂的话(别用平台内部字段名)。**默认值**:不给他什么,等于什么。**范围**:只认哪几个值,不对就拒绝并说清怎么改。
20
+ - **上限**:要多少给多少,**不悄悄截**;超了说清上限;截断要说清截了什么、完整那份在哪。
21
+ - **该先问谁**:要先拿到别的东西,就在描述里指给 agent 看("先调 X")。
26
22
  - **会改变行为的话要留着**:默认只预览、要点头、别连刷、同一个做了一半的接着问。
27
23
 
28
24
  ## 3 拿回什么
29
25
 
30
26
  - 只放 **agent 推不出来的事实**:口径 + 数字 / 每一行 + 平台链接 + "要更多明细该走哪一步"。
31
- - 口径复述、"这是最后一批"、"你打不开链接"这类全删 —— 那是废话。
32
- - **截断要说清**:截了什么、截了多少、完整的那份在哪。
33
- - **总数读不到就说读不到**(例:手上这份能力给不出精确总数),不拿这一页冒充实测总数。
34
- - **每一页都要能往下走**:给能接着问的标识 + 还有多少 + 下一批怎么要。
35
- - 长编号原样保留(别让它被当成数字,转一次就可能查到别的东西)。
27
+ - 口径复述、"这是最后一批"这类全删;**总数读不到就说读不到**,不拿这一页冒充实测总数;每页都能往下走(能接着问的标识 + 还有多少 + 下一批怎么要)。
28
+ - 长编号原样保留(别让它被当成数字)。
36
29
 
37
30
  ## 4 每件事的全场景(一格不许空)
38
31
 
39
- 每一格写**真实的返回或真实的报错原文**(照抄),不写"会报错说清原因"这类转述 —— 转述会在实现时被重新编一遍。
32
+ 每一格写**真实的返回或真实的报错原文**(照抄),不写转述。
40
33
 
41
34
  | 情况 | 必须给什么 |
42
35
  |---|---|
43
36
  | 正常 | 口径 + 数字 / 行 + 链接 |
44
37
  | 没有 | 说"没有"并给依据(不是报错)+ 下一步 |
45
- | 分不清是哪一份 | 把候选举出来让他选;**不许猜、不许悄悄用默认那份** |
46
- | 参数不对 | 说清哪个字段、该给什么,让 agent 自己能改 |
47
- | 超出上限 | 拒绝并说清上限 |
38
+ | 分不清是哪一份 | 举出候选让他选;**不许猜、不许悄悄用默认那份** |
39
+ | 参数不对 / 超出上限 | 说清哪个字段该给什么 / 拒绝并说清上限 |
48
40
  | 能力还没装好 | 缺的是哪一件 + 该去哪更新它 |
49
- | 平台要登录 / 超时 | 分开说,并给一条重试或去登录的路 |
50
- | 还有下一批 | 这一页给了什么 + 下一批怎么要 |
51
- | 太长截断 | 截了什么、多少、完整的那份在哪 |
41
+ | 平台要登录 / 超时 | 分开说 + 一条重试或去登录的路 |
42
+ | 还有下一批 / 太长截断 | 这一页给了什么 + 下一批怎么要 / 截了什么、多少、完整那份在哪 |
52
43
  | 没有正在做的项目 | 说清这件事按哪个项目取数 |
53
44
 
54
45
  ## 5 动手的事:两拍
55
46
 
56
- 1. **第一拍(预览)**:说清**将要做什么**——哪一个对象、什么值、到哪去;然后请他确认。
57
- 预览的话要让他能判断"要不要做",不是"要不要允许"。
58
- 2. **第二拍(真的做)**:只有他确认之后才真的动平台,并回话说做完了。
47
+ 1. **预览**:说清**将要做什么**(哪个对象、什么值、到哪去)再请他确认 —— 让他能判断"要不要做",不是"要不要允许"。
48
+ 2. **真的做**:他确认后才动平台,并回话说做完了。
59
49
 
60
- - 一件事只做一条链路:他按按钮走的和 agent 走的是**同一件事**,结果一样、状态一样。
61
- - **重复发起要安全**:先看平台上是不是已经有一条(已经建了 / 已经发了),不重复触发;同一对象正在做就接着看那一次,不新开一次。
50
+ 一件事只做一条链路:他按按钮走的和 agent 走的是**同一件事**;**重复发起要安全**:先看平台上是不是已经有一条,同一对象正在做就接着看那一次。
62
51
 
63
52
  ## 6 哪几件要用户点头
64
53
 
65
- 三条判据,命中任一就要点头:
66
-
67
- 1. 会改**平台上**的东西;
68
- 2. 会改**这台机器**(装 / 卸 / 更新);
69
- 3. 会改**他自己的面板**(加 / 收 / 排序 / 清空)。
70
-
71
- 点头时,用户要看到的是**这次真的会发生什么**:
72
-
73
- - 哪一个对象、什么值 / 什么版本、到什么环境、影响几台;
74
- - 一句"这一步会真的做什么"(例:"这一步会真的合并分支");
75
- - **不是**"这个操作会写数据"这种空话。
54
+ 命中任一就点头:① 改**平台上**的东西;② 改**这台机器**(装 / 卸 / 更新);③ 改**他自己的面板**(加 / 收 / 排序 / 清空)。
76
55
 
77
- **硬要求:说不清这次会发生什么,就不许问。**
56
+ 点头时用户要看到**这次真的会发生什么**(哪个对象、什么值 / 版本、到什么环境、影响几台;一句"这一步会真的做什么")—— **不是**"这个操作会写数据"这种空话。**说不清这次会发生什么,就不许问。**
78
57
 
79
58
  ### 6.1 写法(清单 + 两个函数;缺一个门禁就红)
80
59
 
81
- ```ts
82
- tools: [{ name: 'workbench_demo_rebuild', writes: true, key: 'rebuild' }],
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
- * `key` 是代码里那个短名(lowerCamelCase)。给了它就 `dsh-wb-module sync` 一次,生成
95
- `src/declared-tools.ts`,代码写 `DECLARED_TOOLS.rebuild.name` —— 工具名的字面量只剩清单这一处
96
- (键写错是编译错;生成物过期、半写 key、源码里又写字面量,门禁都红)。一个 key 都不写也行(不适用那一档)。
97
- * `needsApproval` 回 `{ kind: 'read' }` = 这件不用问;回 `{ kind: 'ask', … }` = 要问;
98
- **清单写着 `writes: true` 却不认领,外壳当场 deny**("在套件的 needsApproval 里补一条审批")——
99
- 但那条错误只有真跑一次才看得见,所以 `check --only contracts` 会替用户先看一遍:缺 `needsApproval`、
100
- `question` / `fallback` 的 zh/en 写不全、或 `approvalDetail` 没给 → **红**。
101
- * `question` 是问句,`fallback` 是"细节拼不出来时"的兜底文案,两者都要 zh/en;
102
- * `approvalDetail` 就是 §7 里"这次会发生什么"那段明细(一行一个事实);拿不到料时回
103
- `{ blocked: '…' }` —— 那等于**不弹**(§7 那一行),不是弹一个空问句。
104
-
105
- - 缺东西就**先自己去看一遍**(走一遍预览,不真的动),把确定的事实摆出来;
106
- - 补不齐就**停下**,把原因交给 agent 去查 —— 不许让弹窗指挥用户去指挥 agent;
107
- - 用户侧不该出现"看不到版本 / 分不清是哪个项目"这类话(他根本不该在这个状态下被问)。
60
+ 写法:清单 `tools[]` 一条(`writes: true` + `key` 短名),加 `needsApproval(call)` 与 `approvalDetail(call)` 两个函数(形状照 `template-diy/` 的实例;`check --only contracts` 逐条点名缺什么)。
61
+
62
+ - `key`(lowerCamelCase):给了就跑一次 `dsh-wb-module sync` 生成 `src/declared-tools.ts`,代码写 `DECLARED_TOOLS.rebuild.name` —— 工具名字面量只剩清单这一处(键写错编译错;生成物过期 / 半写 key / 又写字面量都红)。
63
+ - `needsApproval` 回 `{ kind: 'read' }` = 不问,`{ kind: 'ask', question, fallback }` = 要问;**`writes: true` 却不认领 = 当场 deny**。`check --only contracts` 先替你看:缺 `needsApproval`、zh/en 不齐、`approvalDetail` 没给 → 红。
64
+ - `question` 是问句、`fallback` 是拼不出细节时的兜底,都要 zh/en;`approvalDetail` 是 §7 那段明细(一行一个事实),拿不到料回 `{ blocked: '…' }` = **不弹**。缺东西先自己走一遍预览摆事实,补不齐就停下把原因交给 agent 查。
65
+ - **那屏的样子是定死的**([DESIGN.md](DESIGN.md) §3 末节):标题**只有一句**(`approvalDetail` 那块明细 ≤20 字一行,且**只给没有详情表的能力**);**事实与长内容走客户端面的 `approval.render`**,用外壳的 `ApprovalPlan` 画成两列表 —— 详情座那边等宽、能换行,路径 / diff / 多字段都放得下。宿主算的料到不了浏览器侧,事实要从**工具参数**重建。
108
66
 
109
67
  ## 7 点头的全场景
110
68
 
111
69
  | 情况 | 用户看到什么 | agent 得到什么 |
112
70
  |---|---|---|
113
- | 预览(还没到真的做) | 什么都不弹 | 预览的话:将要做什么 |
114
- | 真的做,且料齐 | 弹出的问话:**这次会发生什么**(几行确定的事实)+ "允许吗" | 等他答 |
115
- | 料补不齐(没绑定 / 平台读不到 / 关键值缺) | **不弹**(他不该被问) | 停下的原因 → 去查 |
116
- | 他拒绝 | 弹窗关掉 | 被拒的原因 |
117
- | 他这一轮没回答就中断了 | 弹窗消失 | 这一轮结束 |
118
- | 他设了"后台不许问" | 不弹 | 自动不许做(后台不许问,就不许做) |
119
- | 这件事在他当前项目里没开 | 不弹 | 能力根本不在手上 |
71
+ | 预览 | 什么都不弹 | 预览的话:将要做什么 |
72
+ | 真的做,且料齐 | 问话:**这次会发生什么** + "允许吗" | 等他答 |
73
+ | 料补不齐 / 项目对不上 / 这件事没开 | **不弹**(他不该被问) | 停下的原因 / 说清是哪个项目 |
74
+ | 他拒绝 / 他点头之后中途变了 | 弹窗关掉 / 料是当时那一份 | 被拒的原因 / 失败原因要能照做 |
75
+ | 他这一轮没回答就中断 / 后台不许问 | 弹窗消失 / 不弹 | 这一轮结束 / 自动不许做 |
120
76
  | 一口气连着要好几件 | 一次一条,逐条问 | 逐条等 |
121
- | 项目对不上 | 不弹 | 说清是哪个项目的事 |
122
- | 他点头之后中途情况变了 | 弹窗里的料是当时那一份 | 失败原因要能照做 |
123
77
 
124
- 问答的按钮、快捷键、弹窗本身都是现成的(不是这个模块自己画);模块能决定的是**里面那块明细画什么**。那块明细要自己排版:**一行一个事实**,别一行塞三句。
78
+ 按钮、快捷键、弹窗都是现成的;模块只决定**里面那块明细画什么**:**一行一个事实**。
125
79
 
126
80
  ## 8 外部供给:这个能力要靠什么才跑得起来
127
81
 
128
- 模块要的东西机器上不一定有(opencli 插件、内网源上的 npm 库)。**清单里的 `env` 段就是说这件事的** ——
129
- 外壳的隔离运行时按它去装、去探,状态在 `workbench_modules_list` 里逐条看得到
130
- (实测:`cmdb` 声明了插件 `cmdb`/`coding` 与库 `opencli-workflow`,那份列表里就按名字列着它们与版本)。
131
-
132
- ```ts
133
- env: {
134
- command: 'opencli',
135
- libs: [
136
- {
137
- name: 'opencli-workflow',
138
- registryEnv: 'DSH_WORKBENCH_X_REGISTRY',
139
- registryDefault: 'https://…/pagoda-npm/',
140
- purpose: '发布编排(合并 → 构建 → 发布)',
141
- },
142
- ],
143
- plugins: [
144
- {
145
- name: 'cmdb',
146
- commands: ['envs', 'builds', 'publish'],
147
- installSource: { kind: 'npm', package: 'opencli-plugin-cmdb', registryEnv: '…', registryDefault: 'https://…/' },
148
- },
149
- ],
150
- }
151
- ```
152
-
153
- * `libs` 四个字段都要:`registryEnv` 让**用户的源**能覆盖 `registryDefault`(换源时不用改代码),
154
- `purpose` 就是给用户看的那半句"装它干嘛";
155
- * `plugins[].installSource` 只有三种:`{kind:'npm', package, registryEnv}` / `{kind:'git', url}` / `{kind:'local', env}`;
156
- * **没装好时用户看到的那句话,由你写**(外壳只负责装,它不知道你这个能力叫什么):
157
-
158
- ```ts
159
- import { libInstallCommand } from '@dsh-workbench/shared/host';
160
- const need = manifest.env?.libs?.[0];
161
- try {
162
- return await loadWorkflow();
163
- } catch (error) {
164
- if (need === undefined) throw error;
165
- throw new Error(`${need.purpose}还没装 —— 装它:${libInstallCommand({ need })}`, { cause: error });
166
- }
167
- ```
168
-
169
- `libInstallCommand({ need })` 会连源和缓存目录一起拼好(`npm i --prefix … --registry '<源>' --cache …`),
170
- **原样给用户,别自己拼 npm 命令**;卡片上把这句话放进失败态([PANEL.md](PANEL.md) 的三态里那条"照做")。
171
-
172
- 判据(`check --only contracts`):`env` 不能是空的;`libs` 缺 `purpose`/`registryEnv`、`registryDefault`
173
- 不是 `http(s)://` 或 `file://`、插件写错 `installSource` 都红;**声明了 `libs` 就必须用
174
- `libInstallCommand`**(否则用户只看到"没装",没有能照做的那条命令)。
82
+ 模块要的东西机器上不一定有(opencli 插件、内网源 npm 库)。**清单的 `env` 段就是说这件事的**:外壳的隔离运行时按它装、探,状态在 `workbench_modules_list` 里逐条看得到。形状照抄:`env: { command, libs: [{ name, registryEnv, registryDefault, purpose }], plugins: [{ name, commands, installSource }] }`。
83
+
84
+ - `libs[]` 四个字段都要:`registryEnv` 让**用户的源**覆盖 `registryDefault`(换源不用改代码),`purpose` 是给用户看的那半句"装它干嘛"。`installSource` 三种:`{kind:'npm', package, registryEnv}` / `{kind:'git', url}` / `{kind:'local', env}`。
85
+ - **没装好时那句话由你写**(外壳不知道你这个能力叫什么):`libInstallCommand({ need })`(`@dsh-workbench/shared/host`)连源与缓存目录一起拼好,**原样给用户、别自己拼 npm**;卡片上放进失败态([PANEL.md](PANEL.md) 三态里那条"照做")。
86
+ - 判据(`check --only contracts`):`env` 不能空;`libs` 缺 `purpose`/`registryEnv`、`registryDefault` 不是 `http(s)://` 或 `file://`、`installSource` 写错都红;**声明了 `libs` 就必须用 `libInstallCommand`**。
175
87
 
176
88
  ## 9 长任务:跑一会儿的事
177
89
 
178
- 一件事要跑几十秒(构建、发布、批量删),做成**长任务**:用户看得到进行中,也看得到结果。
179
-
180
- 宿主半侧 —— `mount` 的 deps 里有 `longTasks`,用它建注册表,并把结果挂回 mount 返回值:
181
-
182
- ```ts
183
- mount({ longTasks, ... }) {
184
- const runs = longTasks?.create({
185
- key: 'rebuild',
186
- run: async ({ payload, onStep }) => {
187
- onStep({ kind: 'start', text: '开始重建 3 个模块' });
188
- const done = await rebuild(payload, onStep);
189
- return { count: done };
190
- },
191
- step: (step) => ({ step: step.text }),
192
- outcome: (result) => ({ why: `重建了 ${(result as { count: number }).count} 个` }),
193
- view: (snapshot) => ({ steps: String(snapshot.steps.length) }),
194
- });
195
- return {
196
- routes: {},
197
- registerTools: () => [],
198
- ...(runs === undefined ? {} : { longTasks: runs }),
199
- };
200
- }
201
- ```
202
-
203
- 浏览器半侧 —— kit 里现成的(别自己轮询、别自己映射状态):
204
-
205
- ```tsx
206
- const feed = kit.hooks.useLongTask({ unit: 'demo', workspace });
207
- // feed.runs:每条有 status / step / steps / why;画的时候用 <LongTaskRow .../> 或 longTaskDot
208
- ```
209
-
210
- * 状态只有四种:`running` 进行中 / `needs-login` 要登录(带 `loginUrl`)/ `ok` 完成 / `failed` 出错;
211
- * **契约里没有"停止"**:用户要能中途停下,得你自己在 `run` 里看信号 —— 别在界面上摆一个不存在的按钮;
212
- * 判据(`check --only contracts`):注册表五个方法要全、`create` 的 `key` / `run` / `view` 不能缺、
213
- 客户端产物里必须出现 `useLongTask` / `LongTaskRow` / `longTaskDot` 之一 ——
214
- 否则用户点完只看到"没反应",看不到进行中 / 要登录 / 完成 / 出错。
215
-
216
- ## 10 用户自己按的那个入口
217
-
218
- 他自己按按钮发起的事**不问他**(是他自己按的),但:
219
-
220
- - 给他一个最小的确认条(将要做什么 + 取消 / 执行);按下去之后按钮自己变成"正在做…";
221
- - 做失败留在条里说原因,不假装成功;
222
- - 和 agent 走的是同一件事、同一个进度、同一个结果。
223
-
224
- ## 11 范例
225
-
226
- 要一个"同形状、能跑"的样子:[SKILL.md](SKILL.md) 的「找不到一件事的时候」—— 模板
227
- (`template-diy/` / `template/`)与装机模块的源码都可以直接读。
90
+ 跑几十秒的事(构建、发布、批量删)做成**长任务**。宿主半侧用 `mount` deps 里的 `longTasks` 建注册表并挂回返回值:`longTasks.create({ key, run, step, outcome, view })` —— `run({ payload, onStep })` 里用 `onStep({ kind: 'start', text })` 报步骤、`return` 结果;`step`/`outcome`/`view` 各映射一段给界面;返回里带上 `longTasks: runs`。
91
+
92
+ 浏览器半侧用 kit 现成的:`kit.hooks.useLongTask({ unit, workspace })` → `feed.runs`(每条 `status`/`step`/`steps`/`why`),画的时候用 `<LongTaskRow …/>` 或 `longTaskDot`。状态四种:`running` / `needs-login`(带 `loginUrl`)/ `ok` / `failed`;**契约里没有"停止"**,用户要中途停得自己在 `run` 里看信号。判据(`check --only contracts`):注册表五个方法要全、`create` 的 `key`/`run`/`view` 不能缺、产物里必须有 `useLongTask`/`LongTaskRow`/`longTaskDot` 之一。
93
+
94
+ ## 10 用户自己按的那个入口 / 范例
95
+
96
+ 他自己按按钮发起的事**不问他**:最小确认条(将要做什么 + 取消 / 执行),按下去按钮变"正在做…",失败留在条里说原因;和 agent 走同一件事、同一进度、同一结果。范例(同形状、能跑):[SKILL.md](SKILL.md) 的「找不到一件事的时候」—— 模板与装机模块源码都能直接读。
228
97
 
229
98
  ## 12 同一把工具,在有些项目里不该出现
230
99
 
231
- 工具是**这台机器上的**,但用户的项目不止一个:在 A 项目里有意义的工具,到了 B 项目就是噪音 —— agent 看得见
232
- 就会去调,调了才发现不行(两次调用白花,而且它还会再试)。**别让工具自己判断再回"这个项目用不了"**,
233
- 而是当场把它从清单里拿掉:
234
-
235
- ```ts
236
- return {
237
- routes,
238
- registerTools,
239
- gate: {
240
- deniedTools: (workspace) => (workspace === null ? [TOOL] : []),
241
- subscribe: (listener) => store.subscribe(listener),
242
- },
243
- };
244
- ```
245
-
246
- - `deniedTools(workspace)`:回"**在这个项目里,我的哪几个工具不存在**"(工具名数组,名字跟 `registerTools()`
247
- 注册的完全一致)。`workspace` 是项目 id,也可能是 `null`(没有项目上下文)。
248
- - `subscribe(listener)`:让你在自己状态变了(比如用户把这个能力关了)时通知外壳重算 —— 不接的话,
249
- 用户关掉开关要等下次重启才生效。
250
- - 外壳两道都做:清单里不出现(agent 看不到)+ 真调到时当场拒(防手滑)。你只要把 `deniedTools` 写对。
251
-
252
- 外壳自己就是这么用的:某张卡在这个项目上没开 → 那张卡的工具全不出现;
253
- 工坊那七把(`workbench_diy_*`)**只在工坊会话里**出现,别处一律不出现。
254
-
255
- **"这条能力在哪些工作区出现"不用你声明 —— 那是用户的事。** 用户口径:「模块开发者不能定义 scope,
256
- scope 永远是用户来决定的」。他在 `workbench_panels_show` / `workbench_panels_hide`(agent 工具)里把它们放上/收起;一个单元在不在,
257
- 它的卡片与**全部**工具同判(外壳的 `resolveScope` 只看两份板:本工作区那份,否则机器级那份)。
258
-
259
- `gate.deniedTools` 可以接,它是**可选**的运行期收窄("这个能力还没配好,先按我自己的开关藏起来")。
100
+ 工具是**这台机器上的**,但项目不止一个:在 A 有意义的到 B 就是噪音。**别让工具自己判断再回"这个项目用不了"**,当场从清单里拿掉 —— `gate: { deniedTools(workspace), subscribe(listener) }`(挂进 `mount` 返回值):
101
+
102
+ - `deniedTools(workspace)` 回"这个项目里我的哪几个工具不存在"(名字跟 `registerTools()` 注册的一致;`workspace` 也可能是 `null`)。`subscribe(listener)`:你状态变了(用户把能力关了)时通知外壳重算 —— 不接要等下次重启。
103
+ - 外壳两道都做:清单里不出现 + 真调到时当场拒。它自己就这么用:工坊那一族 **33 把**(14 枚举 + 12 看 / 装 / 探 + 4 调试 + 3 录制交接,见 [FAMILY.md](FAMILY.md))只在工坊会话里出现。
104
+
105
+ **"哪些工作区出现"不用你声明**(「模块开发者不能定义 scope」):他用 `workbench_panels_show` / `_hide` 放上 / 收起,卡片与**全部**工具同判。`deniedTools` 可选。
106
+
107
+ ## 13 工坊会话里那一族:33 把
108
+
109
+ 那一族(枚举 opencli 的 14 把 + 看 / 装 / 探 12 把 + 调试约定 4 把 + 录制交接 3 把)与它的前置、
110
+ 参数、如实边界,全表在 [FAMILY.md](FAMILY.md) —— 这一份讲**你写的模块**的能力怎么写;
111
+ 那一份讲**工坊会话里 agent 手上真实有什么**。为什么按 opencli 子命令枚举见仓内 ADR-0062(这里不链仓内路径)。
@@ -11,6 +11,7 @@
11
11
  |---|---|
12
12
  | 宿主侧要用的纯函数与常量(`param`、平台解析、工作区存储…) | `@dsh-workbench/shared/host`(写进 `dependencies`) |
13
13
  | 浏览器侧要的一切(hooks、组件、图标、文案工具) | `@dsh-workbench/core/client` 的 `kit` |
14
+ | kit 里确实没有的 dsh 控件 / 图标(如 `Button`、整套 `Icon*Regular`) | `@deepseek-ai/dsh-client-ui-primitives`(声明 external + devDependency 精确版;清单见 `KIT.md` §8) |
14
15
  | 类型 | `import type` 随便用(编译时擦除,不会进产物) |
15
16
  | 外壳本体 | `@dsh-workbench/core`(peer 依赖,装的时候要比版本) |
16
17
 
@@ -18,6 +19,10 @@
18
19
  kit 成员(`components` / `hooks` / `api` / `lib` 各有哪些)列出来 —— 它取的是外壳真实产物
19
20
  `client.js` 里那一份,不是另抄的清单,所以永远和用户手上那版一致。
20
21
 
22
+ **kit 里没有的 dsh 原语有哪些,同样问命令**:`dsh-wb-module check --only primitives`
23
+ (`--all` 全表 / `--json` 机器档)列装机那份的导出、与 kit 求交、并给出该写进 `package.json`
24
+ 的两行。两边都是"取真产物",没有手抄清单(口径见 ADR-0063 · `.scratch/dsh-primitives-face/spec.md`)。
25
+
21
26
  ⚠️ **浏览器侧不许 `import` `@dsh-workbench/shared` 的运行值**(两个入口都不行):
22
27
  它**不在**客户端构建的 external 默认表里(工坊里那份是
23
28
  `node_modules/@dsh-workbench/module-tools/src/build.mjs`),
@@ -32,7 +37,7 @@ import 运行值会被**打进你的包**(第二份实例),而且浏览器
32
37
  | 别这么写 | 为什么 |
33
38
  |---|---|
34
39
  | 从 DOM + CSS 手搓一个 dsh 已经有的东西(按钮、弹窗、输入框、图标) | 重画就是第二份材质与交互(悬停、禁用、焦点环、色板),迟早跟 dsh 漂。**先从 `kit` 拿**;kit 里没有再自己写,并在 spec 里记理由 |
35
- | `import … from '@deepseek-ai/dsh-client-ui-primitives'` | 模块**不直接** import dsh 的包;dsh 原语由外壳经 `kit` 交给你。直接引会被判红(那是第二份实例) |
40
+ | `import … from '@deepseek-ai/dsh-client-ui-primitives'` | **可以**,但要三处齐:只在浏览器侧(宿主侧碰它 `source` 判红)、在 `dsh.client.external` 里声明、**用了就必须在产物里是 `require`**(漏声明 = 被内联成第二份实例,`primitives` 判红)。kit 里有的先拿 kit(`KIT.md` §8);模板默认已经把这三处备好了 |
36
41
  | `import … from '@/shared/…'`、`../../workbench/src/…` | 装你的人手上没有工作台的源码。`@/` 只许指**你自己**的 `src/` |
37
42
  | 在公开包里写内网地址 / 内网域名 / 企业内部名 | 门禁会扫出来并指到行 |
38
43
 
@@ -41,15 +46,16 @@ import 运行值会被**打进你的包**(第二份实例),而且浏览器
41
46
  哈希**,天然不会跟谁撞。要借外壳契约里的类,写 `:global(.wb-…)`,且那个类必须在契约里);
42
47
  **`src/**` 里零注释** —— 要说的写进 `README.md` 或 spec。
43
48
 
44
- ## 3 装上,看一眼
49
+ ## 3 装上、上板
45
50
 
46
51
  模块造好、门禁绿了,**装这一步你自己做** —— 用 `workbench_diy_install`(`unit=<单元 id>`):
47
52
 
48
53
  - 它只认**工坊里造好的**那个模块(按 unit 在工坊里查,不接任何路径)。
49
54
  - 装完顺手在**当前会话这个工作区**的面板上把它打开。
50
- - 装是这台机器上的持久变更 ⇒ 用户会看到一次点头,话里说清是哪个模块、会改什么。
51
- - 会话的审批被关成 `never` 时,这一步会被**直接拒**(不是用户点了拒绝,是没人能被问)——
52
- 让用户把审批开成 `ask`,或者在界面上点一次;判据与构建不受影响,只有这一步过不去。
55
+ - 装是这台机器上的持久变更,**但它不问审批**(工坊整族都是这个口径)—— 不会弹审批屏、也不用等他点头:
56
+ 别对他说"请你点一下确认"。
57
+ - 会话的审批被关成 `never` 时**拒的是"真回了 `ask`"的那些调用**(比如开始调试那次 `workbench_diy_debug_start`)——
58
+ 装 / 上板是 `allow`,`never` 下照样过,不用让用户去把审批开回来。判据与构建不受影响。
53
59
 
54
60
  两个必须记住的后果:
55
61
 
@@ -63,7 +69,8 @@ import 运行值会被**打进你的包**(第二份实例),而且浏览器
63
69
  实测会拼错(把"要重启才注册"读成"没装上",或者源码改了没 build 却去重装一遍)。
64
70
  - **已经装过、只是没上板**:`workbench_diy_show`(只上板,不装)。
65
71
 
66
- 工坊那七把 `workbench_diy_*` **只在工坊那个会话里**(工具清单按工作区收窄,别处连它都不在),
72
+ 工坊那一族 `workbench_diy_*`(33 把:14 把枚举的 opencli + 12 把看/装/探 + 4 把调试约定 + 3 把录制交接,
73
+ 全表见 [FAMILY.md](FAMILY.md))**只在工坊那个会话里**(工具清单按工作区收窄,别处连它都不在),
67
74
  落在别的项目里调用会被当场拒 —— 这就是"DIY 模式"在工具面上的样子。
68
75
  普通项目里装别的东西是另一套工具:`workbench_modules_install` / `workbench_modules_uninstall` 等(见 TOOLS.md)。
69
76
 
@@ -72,8 +79,10 @@ import 运行值会被**打进你的包**(第二份实例),而且浏览器
72
79
  装完之后**不要再装一遍**。改源码 → `dsh-wb-module build` → 用户面板上那张卡自己变。
73
80
 
74
81
  - **宿主侧**(`src/host.ts` / `balance.ts` … → `dist/host.js`):**秒级热换**,不重启、不刷新
75
- (加一行探针 → build → 3 秒内接口就返回新内容)。`cordis.patch.yml` 里的 `followSymlinks: false`
76
- 不能去掉:`dsh-hmr` 跟着软链会把文件描述符用光。
82
+ (加一行探针 → build → 3 秒内接口就返回新内容)。`cordis.patch.yml` 里 hmr 那两件都不能去掉:
83
+ `followSymlinks: false`(跟着软链会把文件描述符用光),以及 `ignored` 里那两条 `*(../)`
84
+ extglob(工坊那个根在外壳包目录之外,相对路径以 `..` 打头,而 picomatch 的 `**` 不吃 `..` ——
85
+ 少了它工坊 `node_modules` 的实目录会整棵进监听表)。为什么、实测数字与边界见仓内 ADR-0034 §3。
77
86
  - **客户端侧**(`src/client.tsx` / CSS → `client.js`):浏览器**不会自己**换,**刷新一下页面**即可
78
87
  (实测:重建后 60 秒页面没有任何反应)。`*.module.css` 的改动是就地改写那一份 `<style>`,刷新即生效。
79
88
  - **改 `cordis.patch.yml`、改 `package.json` 里的 `dsh.client` / `exports["./client"]` 需要重启。**
@@ -0,0 +1,131 @@
1
+ # 写通道:要真改东西的那一下,怎么找到、怎么确认
2
+
3
+ [DATASOURCE.md](DATASOURCE.md) 只管**读**(数据从哪来)。这一份管**写**:提交 / 发布 / 建单 / 改配置。
4
+ 判域用的是同一把尺(**要不要新凭据**,见 [DESIGN.md](DESIGN.md) §0):复用他已有登录、一次调用就能改完的 ⇒ DIY;
5
+ 要**新凭据** / 多步长流程(要等、中途要人、失败要接着来)/ 新服务、新进程、新依赖 ⇒ **工程域**,停下来照 [DESIGN.md](DESIGN.md) §0 那张表说清下一步。
6
+
7
+ ## 四步
8
+
9
+ 1. **先找现成写命令** —— `workbench_diy_recon`(`keyword=…`)现在每条命令都**标了读 / 写**:
10
+ `读` · `写(可预览)` · `写(无预览档,一次就是真的)`;写命令那一行还带**入参签名**(`--id --yes=false`)。
11
+ **命中现成的就别自己造**(自造等于绕过它已经处理好的登录、XSRF 与错误码)。真渲染长这样:
12
+
13
+ ```
14
+ opencli 自带这 1 组能力(不用装、不用点头):
15
+ · antigravity(127.0.0.1) · ui · 要用你的 Chrome 登录
16
+ 读 opencli antigravity cookies —— List cookies on the Antigravity renderer…
17
+ …
18
+ … 还有 6 条(其中写 2 条):opencli antigravity --help
19
+ 直接复用:opencli antigravity cookies(这台机器上就能跑,不用装、不用点头)
20
+ ```
21
+
22
+ 写命令那行长这样:`写(可预览) opencli antigravity delete —— … | 入参:--id --yes=false`。
23
+ 只匹配到写命令时,它最后一句会换成:先 `--help` 看入参;带 `--dryRun` / `--yes` 的**先用预览档**;
24
+ **没有预览档的不许试写**(那一次就是真的)。
25
+ 一条边界:**recon 只查 opencli 自带那份目录** —— `cmdb` / `coding` 这类是另装的**插件**,
26
+ 它只回命令名(`opencli cmdb --help` 看全部);插件的入参走第 2 步。
27
+
28
+ 2. **入参 / 出参按这个顺序查**(越靠前越可信):
29
+ - **`workbench_diy_spec`**(`command='<site> <name>'`,`cmdb publish` / `cmdb/publish` 都认)——
30
+ **插件与内置都能问**:它把 `opencli <site> <name> --help -f json` **原样**回,stdout 是 JSON 时另给 `spec` ——
31
+ `access`(读 / 写)、`positionals`、`command_options[{name, required, help, default}]`、出参列都在里面;
32
+ **不看屏、不要调试约定**。最快的一格,先问它。
33
+ - recon 回的 `args`(**必填 / 默认值**)与 `preview` 标记 —— 只覆盖 opencli 自带那份目录;插件以 `_spec` 为准。
34
+ - **adapter 源码是接口的一手来源**:隔离 HOME 下 `.opencli/plugins/<site>` 是**符号链接**,
35
+ 指到插件包 —— 接口 URL / 方法 / cookie / XSRF 都在那儿。例:coding 的评论接口在
36
+ `opencli-plugin-coding/src/issue-utils.js`(`issueCommentsApiUrl`,POST 表单),
37
+ cookie 与 `X-XSRF-TOKEN` 在同一个包的 `src/request.js` 里。
38
+ - `opencli <site> <name> --help`(`-f yaml` 更好读)—— 补**取值枚举**:manifest 里没有 `choices`,
39
+ 枚举只在 help 文本里;`_spec` 那条路回的是 JSON,`-f yaml` 适合人眼扫。
40
+
41
+ 3. **没有现成命令时,让他配合录一次**(agent 主导:你备好、他只动那一下、交回来):
42
+ - `workbench_diy_record_prepare({ unit, url, doing })` —— 开一扇窗**顶到最前**(**要用户点头一次**:审批屏上写清
43
+ 站点、录哪一下、落在哪、录完不替他切回来),窗里有一根浮条。请他 ① 按「开始录制」 ② 做**一次**那动作 ③ 按「结束录制」;
44
+ **别让他点第二次**。`url` 必须是**这个模块读源同域**的页面 —— 模块的 `design.json` 说过读源时,跨域会**当场拒**
45
+ (没说过就不拦;那一条比的是"别把别处的请求原文带进这个模块")。
46
+ - `workbench_diy_record_read` 取那一段(他按完结束再取):回的是**清单** `rows`(每条 `index` / `key` / `method` /
47
+ `url` / `status` / `ct` / `size` / `source` / `hasRequest` / `hasResponse` / `likelyWrite` / `marked`),
48
+ **一条都不删、也不回体**;`expect` 只用来标 ⭐ 并把 ⭐ 排到前面。要哪一条的原文再调
49
+ **`workbench_diy_record_detail`**(给清单里那条的 `key`,或序号 `index`;**同一个 key 跨轮会重名**,
50
+ 再带上那一行的 `at` 才钉得住是哪一次)——它回**入参(`request.body`)与
51
+ 出参(`response body`)原文**,外加请求头(可能含凭据,别落进 `design.json`)。他还没按结束时 read 会说清"还在录"
52
+ (要等就带 `waitMs`,帽 15 分钟;等不到会挂一条 notice 叫你);浮条被导航干掉过会顺手重新装上。
53
+ **抓不满就重新 prepare 一场**:一场就是一个周期(开始 → 结束),结束之后那一页上的「开始」按不动了。
54
+ - `workbench_diy_record_stop` 收工:停捕获、收掉那个标签、清状态;**原文另存成 `shots/rec-<unit>.jsonl.last`** ——
55
+ 收工之后给 `_record_detail` 一个 `unit` 还能看那一条。**先 read(或先 detail 完)再 stop**。
56
+ 左栏底部那一格是给他的状态:等出手 `[取消]` / 录制中 `[停]`,按下去只会**给你一条消息**(他不替你按工具);
57
+ **录完那一档没有键** —— 你自己来取、自己收工(宿主每 3 秒看一眼那一页,看到结束就发 notice 叫你)。
58
+ - 抓不到的照旧如实说:`<form>` 原生提交 / WebSocket / service worker / **别处的标签**;
59
+ 页面钩子只认 `fetch` 与 XHR,缓冲上限 50 条、单条请求体截 16 KB(`sendBeacon` 2026-10-20 起抓了)。
60
+ 会话捕获在浏览器那一层,盖得比钩子全、且带体 —— 两处合并去重,同名以钩子为主、缺的 key / 入参 / 出参由捕获补齐。
61
+ - **兜底的老路**(那一页在别的浏览器里、或录制族用不上时):`opencli browser <你的会话名> bind`(只认**当前聚焦窗的活动标签**)
62
+ 或 `open … --window foreground` 自己开一扇,**先把捕获臂上再让他动手**:`opencli browser <你的会话名> network --follow`
63
+ (一次性 `network` 不会替你开捕获;每读一次就把 live 缓冲抽干一次)。取那一段用 `network --raw`
64
+ (**同一次调用**才带 body;跨调用的 `--detail` 在我们这份隔离运行时落不了盘,恒 `cache_missing`),
65
+ 还是没有请求体就请他打开 DevTools,把那次请求**复制成 curl** 给你。
66
+ - agent 自己点只限**测试环境 / 可撤销的目标**(同下面第一条纪律)。
67
+ 取证结果要固化成模块自己的写能力:`writes: true` + `needsApproval` + `approvalDetail`([TOOLS.md](TOOLS.md) §6)。
68
+
69
+ 4. **文档 / 问用户** —— 接口文档、别人给的 curl 样例都行;这属于**需求确认**那一拍
70
+ (照 [STEPS.md](STEPS.md) 那张表问他"你会看到什么",别问技术细节)。
71
+
72
+ ## 两条硬纪律
73
+
74
+ - **写命令只有少数带预览档**:隔离运行时那份 manifest 实测 **357 条写命令里只有 12 条**有
75
+ `yes` / `dryRun` / `confirm`(那 12 条:antigravity `delete`/`revert` · codex `archive` ·
76
+ gemini `deep-research` · grok `delete` · kimi `history-rename`/`sign-out` · pinterest 三条 delete ·
77
+ slock `task-delete` · twitter `list-delete`)。**`opencli cmdb publish` / `cmdb build` 都没有预览开关**
78
+ —— 调一次就是**真发布 / 真构建**。有预览档的先用预览档;没有的**不许 agent 试写**。
79
+ - **要新凭据 / 多步长流程 / 新依赖 ⇒ 工程域**(同 [DESIGN.md](DESIGN.md) §0)。
80
+
81
+ ## 三条写纪律(与 opencli 自己那份 `deep-recon` 对齐)
82
+
83
+ 1. **授权观察一次 ≠ 授权重放。** 他点一次是"给你看这一次";**不许因为你已经看过就再放一次**。
84
+ 要再放一次,就得再跟他确认一次(同一场里"再录一段"算同一件事,见下)。
85
+ 2. **写后"不确定"要当一档说清,并且**不许自动重试**。命令报错不等于没写成功(可能已经写了);
86
+ 这种时候停下、**先回读**把状态读回来再决定,别重放。`design.json` 的 `failure` 那一格要写到这一档。
87
+ 3. **写要有配对的撤销 / 反操作**(`follow` ↔ `unfollow`、`publish` ↔ `delete`…)。opencli 自己有一条
88
+ 机器判据 `write-without-delete-pair` 盯这个;你交出去的那张卡至少要在**写通道探明记录**里写清
89
+ "这一步错了怎么退"(哪条命令 / 平台上哪里点)—— 那就是 `writes[].undo` 那一格,**必填**:
90
+ 真没有配对就写"没有配对,一次就是真的"(**空着 = 没想过**,`design check` 会点名 `write.undo`)。
91
+
92
+ ## 出参的最终确认靠**回读**
93
+
94
+ 写完拿**同族的读**(或卡片那条投影)把那一格读回来,**别看"命令没报错"就当成功** ——
95
+ 平台可能收下了却没生效,也可能失败在"**可能已经写了**"那一档。写进模块时,
96
+ `design.json` 的 `writes[].readback` 就是这一格(哪条读 / 出参哪个字段)。
97
+
98
+ ## 边界(如实)
99
+
100
+ - **`browser network --detail` 在工坊会话里拿不到 body**:它依赖一份盘上缓存,默认路径落在会话可写根之外
101
+ (`EPERM …/opencli/home/.opencli/cache/browser-network`,工具只发一个 `cache_warning`)⇒ 跨调用的 `--detail` 取不到。
102
+ **录制族不依赖它**:它自己那份 `shots/rec-<unit>.jsonl` 就是原文(`_record_detail` 按 key 现取)。
103
+ 手工取 body 的另一条路是 `workbench_diy_eval` 里的 `fetch` —— 用它自己的登录在页面里读,返回 JSON;
104
+ 或把 `OPENCLI_CACHE_DIR` 指到可写处,再让 `--detail <key>` 生效。
105
+ - **两套 opencli 真相**:PATH 上那份、你机器上 `~/.opencli` 那份、工作台**隔离运行时**那份
106
+ 可能不是同一代。工坊一律以**隔离运行时**(`$DSH_HOME/workbench/opencli/`)为准;
107
+ 核验用工具,别去看错文件。
108
+ - **取值枚举只在 help 文本里**(manifest 没有 `choices`),别指望 recon 的 `args` 带它。
109
+
110
+ ## 落进 `design.json`
111
+
112
+ 写通道填 `writes[]`(一个动作可以打几条写:先建后发布;只有一条写也可以写成 `write: {}`)。
113
+ 字段**七个必填**、一个可空;下表右边是 `design render` 渲染出来的**行名(逐字)**:
114
+
115
+ | 字段 | 渲染出来的行 |
116
+ |---|---|
117
+ | `command` | 哪条写(命令 / URL + 方法) |
118
+ | `params` | 参数(必填 / 取值枚举 / 上限) |
119
+ | `credentials` | 凭据从哪来 |
120
+ | `preview` | 有没有预览档(没有就写"没有,一次就是真的") |
121
+ | `readback` | 写完之后怎么回读确认(哪条读 / 出参哪个字段) |
122
+ | `undo` | 配对的那个撤销 / 删除(没有就写"没有配对,一次就是真的")← 第三条写纪律那一格 |
123
+ | `failure` | 失败长什么样(含"可能已经写了") |
124
+ | `probeCommand`(可空) | 当初探明 / 预览用的那次调用(原文) |
125
+
126
+ `design render` 把它写进 README 的 **`### 写通道探明记录`** 那一节,位置在「数据源探明记录」与「验收」之间
127
+ (一条一张表;多条渲染成 `#### 写 N`)。`dsh-wb-module design check` 会把空着的格点名叫出来。
128
+ **没有写通道的模块不用写** —— 它不判"一条都没有",`check` 不红。
129
+
130
+ `undo` 是 2026-10-19 加的(照 opencli 的 `write-without-delete-pair`):**旧模块**跑 `design check`
131
+ 会多出一格 `write.undo` 的空格 —— 补一句"没有配对,一次就是真的"(或真配对那条命令)就过,别的不用动。
@@ -1,4 +0,0 @@
1
- import type { DshHostContext, DshToolRegistry } from '@dsh-workbench/shared';
2
- import { type ActStep, type ActTarget } from './diy-browser-act';
3
- export declare function renderAct(verb: string, target: ActTarget, selector: string, step: ActStep): string;
4
- export declare function registerActTools(ctx: DshHostContext, tools: DshToolRegistry): readonly (() => void)[];
@@ -1,15 +0,0 @@
1
- import type { DshHostContext, DshToolRegistry } from '@dsh-workbench/shared';
2
- import { type BoardState } from './diy-page';
3
- import { type Shot } from './diy-browser';
4
- import { type CardView, type PickRequest, type SlotFacts } from './diy-scripts';
5
- export declare function registerLookTools(ctx: DshHostContext, tools: DshToolRegistry): readonly (() => void)[];
6
- export declare function rgbOf(value: string): readonly [number, number, number] | undefined;
7
- export declare function contrastOf(foreground: string, background: string): number | undefined;
8
- export declare function styleLineOf(view: CardView, pick: PickRequest): string;
9
- export declare function partsLineOf(view: CardView, pick: PickRequest): string;
10
- export declare function slotText(slot: SlotFacts, width: number): string;
11
- export declare function slotLineOf(view: CardView): string;
12
- export declare function outlineText(view: CardView): string;
13
- export declare function contrastLineOf(view: CardView): string;
14
- export declare function renderLook(unit: string, view: CardView, shot: Shot, pick: PickRequest): string;
15
- export declare function renderMissingCard(unit: string, view: CardView, board: BoardState): string;
@@ -1,3 +0,0 @@
1
- import type { DshHostContext, DshToolRegistry } from '@dsh-workbench/shared';
2
- export declare function nearestKeyOf(payload: Record<string, unknown> | undefined, key: string): string;
3
- export declare function registerNetTools(ctx: DshHostContext, tools: DshToolRegistry): readonly (() => void)[];