@dsh-workbench/core 0.3.1 → 0.3.3

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 (39) hide show
  1. package/client.js +177 -107
  2. package/dist/client/client.body.js +177 -107
  3. package/dist/client/index.js +177 -107
  4. package/dist/host/index.js +832 -1461
  5. package/dist/types/app/client.d.ts +15 -3
  6. package/dist/types/app/layouts/panel-hint.d.ts +3 -0
  7. package/dist/types/app/layouts/rightbar/market-hint.d.ts +16 -0
  8. package/dist/types/app/layouts/rightbar/market-rows.d.ts +2 -3
  9. package/dist/types/modules/panels/model/diy-browser-act.d.ts +2 -39
  10. package/dist/types/modules/panels/model/diy-browser-tools.d.ts +39 -0
  11. package/dist/types/modules/panels/model/diy-browser.d.ts +1 -14
  12. package/dist/types/modules/panels/model/diy-observers.d.ts +11 -0
  13. package/dist/types/modules/panels/model/diy-observers.test.d.ts +1 -0
  14. package/dist/types/modules/panels/model/diy-page.d.ts +1 -0
  15. package/dist/types/modules/panels/model/diy-scripts.d.ts +0 -63
  16. package/dist/types/modules/panels/model/diy-spec.d.ts +5 -0
  17. package/dist/types/modules/panels/model/diy-spec.test.d.ts +1 -0
  18. package/dist/types/modules/panels/unit.d.ts +13 -11
  19. package/dist/types/shared/api/host/diy-recon.d.ts +10 -0
  20. package/package.json +5 -5
  21. package/scripts/seed-workshop.sh +3 -1
  22. package/skills/workbench-module-diy/BUILD.md +37 -16
  23. package/skills/workbench-module-diy/DATASOURCE.md +11 -2
  24. package/skills/workbench-module-diy/DESIGN.md +15 -8
  25. package/skills/workbench-module-diy/KIT.md +2 -0
  26. package/skills/workbench-module-diy/PANEL.md +3 -2
  27. package/skills/workbench-module-diy/REVIEW.md +115 -48
  28. package/skills/workbench-module-diy/SKILL.md +33 -9
  29. package/skills/workbench-module-diy/STEPS.md +1 -1
  30. package/skills/workbench-module-diy/TOOLING.md +6 -3
  31. package/skills/workbench-module-diy/TOOLS.md +90 -204
  32. package/skills/workbench-module-diy/WIRING.md +7 -5
  33. package/skills/workbench-module-diy/WRITES.md +90 -0
  34. package/dist/types/modules/panels/model/diy-act.d.ts +0 -4
  35. package/dist/types/modules/panels/model/diy-look.d.ts +0 -15
  36. package/dist/types/modules/panels/model/diy-net.d.ts +0 -3
  37. /package/dist/types/modules/panels/model/{diy-act.test.d.ts → diy-browser-act.test.d.ts} +0 -0
  38. /package/dist/types/modules/panels/model/{diy-look.test.d.ts → diy-browser-tools-shot.test.d.ts} +0 -0
  39. /package/dist/types/modules/panels/model/{diy-net.test.d.ts → diy-browser-tools.test.d.ts} +0 -0
@@ -1,9 +1,9 @@
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
+ # 造一个工作台模块:他说需求,你做完自己验过再交给他
7
7
 
8
8
  交付物是**面板上能点开的一块内容**,不是一份要他读的技术方案。
9
9
 
@@ -16,6 +16,8 @@ description: 在工作台里凭需求造一个模块(面板上的一块内容
16
16
  3. **先定数据从哪来** —— [DATASOURCE.md](DATASOURCE.md):干净机器上没有现成 skill 可抄,
17
17
  用 `workbench_diy_probe`(`url=…, script=…`)复用他的登录去探 —— 两拍、重试、失败码归一
18
18
  都在那把工具里;探明后**固化成一条读**(`@dsh-workbench/shared/host` 的 `browserRead`)。顺手把三态写掉。
19
+ 要 agent **真去改东西**(提交 / 发布 / 建单 / 改配置)的,读完 [WRITES.md](WRITES.md) 再动手:
20
+ 四步(现成写命令 → 入参 / 出参来源 → 没有预览档的不许试写 → 写完后回读确认)。
19
21
  4. **从模板改** —— [BUILD.md](BUILD.md) §1–§2:在工坊里 `dsh-wb-module init` 一个骨架,
20
22
  把 `design.json` 里的**三张表与数据源记录先改成你问出来的那份**(`design check` 会点名叫出还空着哪一格),
21
23
  再改数据源与渲染。
@@ -23,16 +25,37 @@ description: 在工作台里凭需求造一个模块(面板上的一块内容
23
25
  命令写成 `pnpm exec dsh-wb-module …`(工坊里 PATH 上也有同名入口,但 `pnpm exec` 到处都对)。
24
26
  5. **自己过门禁** —— `dsh-wb-module build` + `check` + `design check`;
25
27
  红了自己修([REVIEW.md](REVIEW.md) 有对照),不抛给他、也不贴报错给他看。
26
- 6. **让他看一眼** —— 先对着 ASCII 图确认,再用 `workbench_diy_install`(`unit=<单元 id>`)装上:
27
- 它只认工坊里造好的那个模块,装完顺手在**这个项目**的面板上打开(装的是这台机器上的东西,他会点头一次)。
28
+ 6. **装上、上板** —— `workbench_diy_install`(`unit=<单元 id>`)装上:
29
+ 它只认工坊里造好的那个模块,装完顺手在**这个项目**的面板上打开(装的是这台机器上的东西;**这一步不问审批**,见 [WIRING.md](WIRING.md) §3 —— 别对他说"请你点一下确认",那一拍不会出现)。
28
30
  **这一跳永远只走一次**:之后只重建、不重装(宿主侧秒级热换;改了 client.js 刷新一下页面)。
29
31
  拿不准它现在走到哪一步(装了没 / 面板认得没 / 这个项目上开着没 / 产物齐不齐 / 源码比产物新没),
30
32
  就问工具:`workbench_diy_status`(`unit=<单元 id>`)会把这几件事一次说清并给出下一步 ——
31
33
  **别自己拼,也别再装一遍**;已经装过只是想上板,用 `workbench_diy_show`。
32
- 7. **验收** —— 逐条对着三张表走:「打开哪 → 看什么 → 点什么 → 应该看到什么」。
33
- 他说哪不对,**先取证再改**:`workbench_diy_panel` 一次看清面板上每张卡是读到了、读不到还是崩了
34
- (四步取证见 [REVIEW.md](REVIEW.md));改完让他看**同一处**;不给他看 diff、日志、代码。
35
- 逐条过他都说对了,再 `dsh-wb-module design accept` 把验收打勾(**没验完不许打**)。
34
+ 7. **自己验收** —— 逐条对着三张表,在**真卡片**上走「打开哪 → 看什么 → 点什么 → 应该看到什么」。
35
+ 要看 / 动**他那一屏**(浏览器那 14 把 + `logs_*`)得先开约定:
36
+ `workbench_diy_debug_start`(`unit=…`;会请他点头一次,他会被告知「调试中请留在那一屏」)——
37
+ **没有这场约定,上面那一族一律当场拒**;不看屏了先 `workbench_diy_debug_pause`,收工 `…_stop`。
38
+ (`workbench_diy_panel` / `_status` / `_probe` 不碰他那一屏,不用开。)
39
+ 走法(全表与参数在 [TOOLS.md](TOOLS.md) §13):
40
+ - **这一张卡长什么样**:`workbench_diy_screenshot`(要卡那一块就先 `eval` 量盒子、填 `clip`)+
41
+ `workbench_diy_html`(`--as json` 给结构树);
42
+ - **元素与语义**:`workbench_diy_find`(`--css '$card …'`、或 `--role` / `--name`)、`workbench_diy_state`;
43
+ - **动手**:`workbench_diy_click` / `_hover` / `_type` / `_fill` / `_keys` / `_scroll` / `_wait`。
44
+ 吃 CSS 的那几把有 `scope`(默认 `page`,`card` 才是这张卡里);portal 出来的那一层(下拉 / 菜单 /
45
+ 模态)要用 `scope: 'page'` + `--role menuitem` 按名字点;
46
+ - **派生读数**(对比度 / 被裁 / 可见文本):`workbench_diy_eval`(`in: 'card'` 注入卡根 `root`,脚本自己 `return`);
47
+ - **那一次读的收发包**:`workbench_diy_network`(`--follow` 臂上 → 做那件事 → `--detail <key>` 取 body);
48
+ - **控制台**:`workbench_diy_logs_watch` → 做那件事 → `workbench_diy_logs_read`。
49
+
50
+ 这些工具的参数**原样转发**给 opencli、返回体也是它的原样 —— 不知道某个子命令收什么,就
51
+ `args: ['<verb>', '--help']` 问它;事实在 `stdout` 里(`ok` 只管进程)。
52
+ 哪一条不对,**先取证再改**(`workbench_diy_panel` 一次看清面板上每张卡是读到了、读不到还是崩了;
53
+ 四步取证见 [REVIEW.md](REVIEW.md)),改完再走**同一处**。
54
+ 逐条都走通了,再 `dsh-wb-module design accept` 把验收打勾(**没走通的不许打勾**;走不通的如实说没验过)。
55
+ 审批那一格是**他的**:模块里要用户点头的写能力,得他真用一次时点 —— 你代不了,清单里写清
56
+ 「这条要你真用一次」,别打勾说验过。
57
+ 验完把结论告诉他:**装好了、面板上哪一格**(不用请他逐条点头 —— 他随时能自己点开看;
58
+ 他说哪不对,回到同一处取证再改)。不给他看 diff、日志、代码。
36
59
 
37
60
  ## 每个问题都带推荐答案
38
61
 
@@ -47,9 +70,10 @@ description: 在工作台里凭需求造一个模块(面板上的一块内容
47
70
  | 两条分工、三张表、怎么用 ASCII 跟他说话 | [DESIGN.md](DESIGN.md) |
48
71
  | 步骤 1 / 2 / 3 各自问什么、什么时候算做完 | [STEPS.md](STEPS.md) |
49
72
  | **数据从哪来**(干净机器上怎么找到那一条读) | [DATASOURCE.md](DATASOURCE.md) |
73
+ | **要 agent 真去改东西**(提交 / 发布 / 建单 / 改配置:写通道四步) | [WRITES.md](WRITES.md) |
50
74
  | 面板上该有什么、状态矩阵怎么写、界面习惯 | [PANEL.md](PANEL.md) |
51
75
  | **要做什么 → 用哪件 kit → 最小写法**(照抄面) | [KIT.md](KIT.md) |
52
- | 能力怎么划、传什么拿回什么、哪几件要点头 | [TOOLS.md](TOOLS.md) |
76
+ | 能力怎么划、传什么拿回什么、哪几件要点头(**工坊会话里那 29 把工具的全表在 §13**) | [TOOLS.md](TOOLS.md) |
53
77
  | agent 动完面板哪一格变、他正在干的时候看哪儿 | [LINKAGE.md](LINKAGE.md) |
54
78
  | 落点、文件形状、命令、两个面 | [BUILD.md](BUILD.md) |
55
79
  | 依赖、会被拦的写法、装、热换、发布 | [WIRING.md](WIRING.md) |
@@ -3,7 +3,7 @@
3
3
  [DESIGN.md](DESIGN.md) 是三张表的来历,本文是每一步的**问法**与**完成判据**。
4
4
 
5
5
  每一步定下来的东西都**当场落进**模块里的 `design.json`:面板 = `panel` / `states`、配置 = `config`、
6
- 能力 = `capabilities`、联动 = `linkage`、数据源 = `datasource` / `reads[]`、验收 = `acceptance`。
6
+ 能力 = `capabilities`、联动 = `linkage`、数据源 = `datasource` / `reads[]`、写通道 = `writes[]`(单条可写 `write{}`)、验收 = `acceptance`。
7
7
  `dsh-wb-module design check` 会把空着的格列出来 —— **只留在对话里等于没定**。
8
8
 
9
9
  ## 步骤 1 — 面板:他看什么,他配什么
@@ -12,11 +12,14 @@
12
12
 
13
13
  | 纪律(出处) | 固化形态 |
14
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` |
15
+ | 十三条判据([REVIEW.md](REVIEW.md)) | `dsh-wb-module check`(`--only <判据>` 单跑;`--only kit` 列这份外壳的 kit 成员;`--only arch` 跑模块自己的分层形状;`--only lint` 跑模块自己的语法卫生) |
16
+ | 落点、分层文件形状、六个脚本([BUILD.md](BUILD.md) §1–§2) | `dsh-wb-module init --template diy`(骨架带分层目录 + 一行 `arch.config.mjs` + 两行 `eslint.config.mjs`(`moduleLint({ rootDir: import.meta.dirname })`)+ 一条真读盘的数据源)+ `scripts/smoke.mjs` |
17
+ | 语法卫生红了、要自动修([REVIEW.md](REVIEW.md) 第十三条) | `dsh-wb-module lint [--fix]`(**`check` 永不动手改文件**;`--fix` 才写回) |
17
18
  | 装、上板、不重装([WIRING.md](WIRING.md) §3) | `workbench_diy_install` / `workbench_diy_show` / `workbench_diy_status`(工坊那一跳自己上板) |
18
19
  | 它到底起得来没(装配抛的错 / 声明被拒的原文) | `workbench_diy_status`("起得来"那一行,见 [REVIEW.md](REVIEW.md) 四步取证的第二步) |
19
- | 这一页的控制台 / 网络怎么取证([REVIEW.md](REVIEW.md) 四步取证) | `workbench_diy_logs_watch` / `_read` / `_stop`(钩子有上限:每页 200 条、单条 400 字);`workbench_diy_net_watch` / `_read` / `_stop`(最多 2000 行 / 15 分钟后自己收工;`_read key=…` 取 body)。**别用 `opencli browser <会话> console`**:那条命令拿到的是 stub,恒回空数组。网络那一头工作台走的是**隔离 opencli**(自己的 `HOME`),它的缓存与你自己 shell 里 `~/.opencli` 那份不是同一个 —— 核验要用工具,别看错文件 |
20
+ | 看 / 动**用户那一屏**之前要先取得约定(ADR-0055 §7「调试是一次约定」) | `workbench_diy_debug_start`(`unit=…`,**要用户点头一次**)—— 没有它,浏览器那 14 把 / `logs_*` **一律当场拒**(拒的时候一条命令都不发);不看屏了 `_pause`、收工 `_stop`(**收工顺手把网络捕获与日志钩子也收了**)。`_panel` / `_status` / `_probe` 不碰他那一屏,不用开。**全表与参数见 [TOOLS.md](TOOLS.md) §13** |
21
+ | 看 / 动那一屏的浏览器动作怎么发(枚举 opencli,argv 与结果原样转发) | 14 把:`workbench_diy_state` / `_html` / `_find` / `_click` / `_hover` / `_type` / `_fill` / `_keys` / `_scroll` / `_wait` / `_eval` / `_screenshot` / `_network` / `_dialog` —— 公共参数只有 `unit?` / `args?` / `timeoutMs?`(另有 `scope` / `in` / `clip` 只长在该长的那几把上;`$card` 在任何 `args` 里都替换)。**不知道参数就问 `--help`**(也照样转发);返回体是 opencli 的原样,事实在 `stdout` 里 |
22
+ | 这一页的控制台 / 网络怎么取证([REVIEW.md](REVIEW.md) 四步取证) | `workbench_diy_logs_watch` / `_read` / `_stop`(钩子有上限:每页 200 条、单条 400 字)—— **opencli 的 `console` 是空桩**(恒回空数组、还 exit 0),控制台只能我们自己做;网络是 `workbench_diy_network` 一把(`--follow` 臂上、`--failed` / `--since` 这些原样转发;收工 `_debug_stop` 顺手收),但**`--detail` 取不到 body** —— 缓存落在会话可写根之外,只能列请求 key;要 body 走 `_eval` 里的 `fetch`。网络那一头工作台走的是**隔离 opencli**(自己的 `HOME`),它的缓存与你自己 shell 里 `~/.opencli` 那份不是同一个 —— 核验要用工具,别看错文件 |
20
23
  | 面板上现在是什么样(每张卡读到没 / 崩没崩) | `workbench_diy_panel`(浏览器半侧上报,宿主按 unit 留最新一条) |
21
24
  | 空着的格就是还没定的事([DESIGN.md](DESIGN.md) §5) | `design.json` + `design check`(点名叫出空格)/ `render` / `accept` |
22
25
  | 发货前的清单([WIRING.md](WIRING.md) §5) | `dsh-wb-module publish`(门禁 + 打包清单 `files` 漏项,只 dry run) |
@@ -1,259 +1,145 @@
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
- 点头时,用户要看到的是**这次真的会发生什么**:
54
+ 命中任一就点头:① 改**平台上**的东西;② 改**这台机器**(装 / 卸 / 更新);③ 改**他自己的面板**(加 / 收 / 排序 / 清空)。
72
55
 
73
- - 哪一个对象、什么值 / 什么版本、到什么环境、影响几台;
74
- - 一句"这一步会真的做什么"(例:"这一步会真的合并分支");
75
- - **不是**"这个操作会写数据"这种空话。
76
-
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 查。
108
65
 
109
66
  ## 7 点头的全场景
110
67
 
111
68
  | 情况 | 用户看到什么 | agent 得到什么 |
112
69
  |---|---|---|
113
- | 预览(还没到真的做) | 什么都不弹 | 预览的话:将要做什么 |
114
- | 真的做,且料齐 | 弹出的问话:**这次会发生什么**(几行确定的事实)+ "允许吗" | 等他答 |
115
- | 料补不齐(没绑定 / 平台读不到 / 关键值缺) | **不弹**(他不该被问) | 停下的原因 → 去查 |
116
- | 他拒绝 | 弹窗关掉 | 被拒的原因 |
117
- | 他这一轮没回答就中断了 | 弹窗消失 | 这一轮结束 |
118
- | 他设了"后台不许问" | 不弹 | 自动不许做(后台不许问,就不许做) |
119
- | 这件事在他当前项目里没开 | 不弹 | 能力根本不在手上 |
70
+ | 预览 | 什么都不弹 | 预览的话:将要做什么 |
71
+ | 真的做,且料齐 | 问话:**这次会发生什么** + "允许吗" | 等他答 |
72
+ | 料补不齐 / 项目对不上 / 这件事没开 | **不弹**(他不该被问) | 停下的原因 / 说清是哪个项目 |
73
+ | 他拒绝 / 他点头之后中途变了 | 弹窗关掉 / 料是当时那一份 | 被拒的原因 / 失败原因要能照做 |
74
+ | 他这一轮没回答就中断 / 后台不许问 | 弹窗消失 / 不弹 | 这一轮结束 / 自动不许做 |
120
75
  | 一口气连着要好几件 | 一次一条,逐条问 | 逐条等 |
121
- | 项目对不上 | 不弹 | 说清是哪个项目的事 |
122
- | 他点头之后中途情况变了 | 弹窗里的料是当时那一份 | 失败原因要能照做 |
123
76
 
124
- 问答的按钮、快捷键、弹窗本身都是现成的(不是这个模块自己画);模块能决定的是**里面那块明细画什么**。那块明细要自己排版:**一行一个事实**,别一行塞三句。
77
+ 按钮、快捷键、弹窗都是现成的;模块只决定**里面那块明细画什么**:**一行一个事实**。
125
78
 
126
79
  ## 8 外部供给:这个能力要靠什么才跑得起来
127
80
 
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`**(否则用户只看到"没装",没有能照做的那条命令)。
81
+ 模块要的东西机器上不一定有(opencli 插件、内网源 npm 库)。**清单的 `env` 段就是说这件事的**:外壳的隔离运行时按它装、探,状态在 `workbench_modules_list` 里逐条看得到。形状照抄:`env: { command, libs: [{ name, registryEnv, registryDefault, purpose }], plugins: [{ name, commands, installSource }] }`。
82
+
83
+ - `libs[]` 四个字段都要:`registryEnv` 让**用户的源**覆盖 `registryDefault`(换源不用改代码),`purpose` 是给用户看的那半句"装它干嘛"。`installSource` 三种:`{kind:'npm', package, registryEnv}` / `{kind:'git', url}` / `{kind:'local', env}`。
84
+ - **没装好时那句话由你写**(外壳不知道你这个能力叫什么):`libInstallCommand({ need })`(`@dsh-workbench/shared/host`)连源与缓存目录一起拼好,**原样给用户、别自己拼 npm**;卡片上放进失败态([PANEL.md](PANEL.md) 三态里那条"照做")。
85
+ - 判据(`check --only contracts`):`env` 不能空;`libs` 缺 `purpose`/`registryEnv`、`registryDefault` 不是 `http(s)://` 或 `file://`、`installSource` 写错都红;**声明了 `libs` 就必须用 `libInstallCommand`**。
175
86
 
176
87
  ## 9 长任务:跑一会儿的事
177
88
 
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/`)与装机模块的源码都可以直接读。
89
+ 跑几十秒的事(构建、发布、批量删)做成**长任务**。宿主半侧用 `mount` deps 里的 `longTasks` 建注册表并挂回返回值:`longTasks.create({ key, run, step, outcome, view })` —— `run({ payload, onStep })` 里用 `onStep({ kind: 'start', text })` 报步骤、`return` 结果;`step`/`outcome`/`view` 各映射一段给界面;返回里带上 `longTasks: runs`。
90
+
91
+ 浏览器半侧用 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` 之一。
92
+
93
+ ## 10 用户自己按的那个入口 / 范例
94
+
95
+ 他自己按按钮发起的事**不问他**:最小确认条(将要做什么 + 取消 / 执行),按下去按钮变"正在做…",失败留在条里说原因;和 agent 走同一件事、同一进度、同一结果。范例(同形状、能跑):[SKILL.md](SKILL.md) 的「找不到一件事的时候」—— 模板与装机模块源码都能直接读。
228
96
 
229
97
  ## 12 同一把工具,在有些项目里不该出现
230
98
 
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` 可以接,它是**可选**的运行期收窄("这个能力还没配好,先按我自己的开关藏起来")。
99
+ 工具是**这台机器上的**,但项目不止一个:在 A 有意义的到 B 就是噪音。**别让工具自己判断再回"这个项目用不了"**,当场从清单里拿掉 —— `gate: { deniedTools(workspace), subscribe(listener) }`(挂进 `mount` 返回值):
100
+
101
+ - `deniedTools(workspace)` 回"这个项目里我的哪几个工具不存在"(名字跟 `registerTools()` 注册的一致;`workspace` 也可能是 `null`)。`subscribe(listener)`:你状态变了(用户把能力关了)时通知外壳重算 —— 不接要等下次重启。
102
+ - 外壳两道都做:清单里不出现 + 真调到时当场拒。它自己就这么用:工坊那一族 **29 把**(14 枚举 + 11 看 / 装 / 探 + 4 调试,见 §13)只在工坊会话里出现。
103
+
104
+ **"哪些工作区出现"不用你声明**(「模块开发者不能定义 scope」):他用 `workbench_panels_show` / `_hide` 放上 / 收起,卡片与**全部**工具同判。`deniedTools` 可选。
105
+
106
+ ## 13 工坊会话里那一族:29 把
107
+
108
+ §12 讲你写的模块;这一节是**工坊会话里 agent 手上真实有什么**。浏览器那一半照 opencli 的子命令枚举,参数只有 `args`(原样转发);为什么这么定见仓内 ADR-0062(这份指引不链仓内路径)。
109
+
110
+ ### 13.1 前置(别的都排它后面)
111
+
112
+ - 只在**工坊会话**里出现。看 / 动 / 网络 / 日志这一族要**先立调试约定** `workbench_diy_debug_start`(`unit=…`,请用户点头一次):**没有约定一律当场拒、零命令**;不看屏 `_debug_pause`、收工 `_debug_stop`(顺手收网络与日志钩子)。
113
+ - 还要**工坊那块板画在屏上**;会话钉死 `workbench`、每条带 `--window background`。`_list`/`_status`/`_panel`/`_recon`/`_probe`/`_spec`/`_install`/`_show` 不碰他那一屏,不用开。
114
+
115
+ ### 13.2 14 把浏览器工具(工具名 → argv;前缀都是 `workbench_diy_`)
116
+
117
+ `_state`→`state` · `_html`→`get html` · `_find`→`find` · `_click`→`click` · `_hover`→`hover` · `_type`→`type` · `_fill`→`fill` · `_keys`→`keys` · `_scroll`→`scroll` · `_wait`→`wait` · `_eval`→`eval` · `_screenshot`→`screenshot` · `_network`→`network` · `_dialog`→`dialog`
118
+
119
+ 干什么:名字直说;不直观的几个 —— `_state` URL + 标题 + 可点元素(ref 来源)、`_html` HTML(`--as json` 树)、`_find` 找元素回 `{matches_n, entries[]}`、`_network` 收发包(`--follow`;body 拿不到)、`_screenshot` 写文件(`clip` 只裁一块)。
120
+
121
+ **参数**(`scope` / `in` 默认都是 `page`):
122
+
123
+ - 公共:`unit?`(哪张卡;可省 —— 只剩一个模块就用它。**要卡作用域就得给**:写 `scope: 'card'` / `in: 'card'` 或用 `$card` 却不给 ⇒ **当场拒、零命令**)· `args?: string[]`(原样接在 `opencli browser workbench <verb…>` 后)· `timeoutMs?`(默认 30000;≤ 0 或非数也按默认)。
124
+ - `scope: 'card'` 只给吃 CSS 的**七把**:`_find`(`--css`)· `_html`(`--selector`,没给它我们补)· `_click`/`_hover`/`_type`/`_fill`(**位置参数 0**)· `_wait`(**只在 `wait selector`**,位置参数 1)—— `card` = 给那个目标挂 `#wb-card-<unit> .wb-card `。
125
+ - `in: 'card'` 只 `_eval`(注入卡根 `root`,**脚本自己 `return`**);`clip`(CSS px)只 `_screenshot`(拍完本地裁);`$card` **任何 `args` 里**都替换成 `#wb-card-<unit> .wb-card`。
126
+
127
+ **拿回**:`{ ok, exitCode, timedOut, truncated, stdout, stderr }` + `workshop`/`unit`/`verb`/`argv`(落到命令行的词);`_screenshot` 另给 `file`/`cropped`/`cropReason`。`ok` 只管进程;事实全在 `stdout`(报错也是一份 `{"error":{…}}`,`ok` 可能仍 `true`);不抠 JSON / 不翻中文 / 不做 ok 归一,**要参数先问 `--help`**。**"原样"有折损**:`args` 里空串 / 纯空白 / 非字符串**丢掉**、留下的词**前后 trim**、`stdout`/`stderr` 也 trim ⇒ **`_fill` 清空不了输入框**(`''` 被丢)、**`_type` 敲不了空格** —— 没有别的口子就如实说做不到。
128
+
129
+ ### 13.3 老工具 → 新命令
130
+
131
+ | 老 | 新 |
132
+ |---|---|
133
+ | `_look` | `state` + `get html` + `eval` + `screenshot`(卡图靠 `clip`,上游没有 selector) |
134
+ | `_reload` | `eval 'location.reload()'` |
135
+ | `_viewport` | `screenshot --width/--height` |
136
+ | `_select` | **没了**(上游只认原生 `<select>`);下拉两拍:卡内点触发器 → `scope: 'page'` + `--role menuitem` 按名字点 |
137
+ | `_net_watch` / `_net_read` / `_net_stop` | `network`(`--follow` 臂上、收工 `_debug_stop` 顺手收;**取 body 得走 `_eval` 的 `fetch`**,见 §13.4) |
138
+ | `_click` `_hover` `_type` `_fill` `_keys` `_scroll` | 同名,参数改成原样转发 |
139
+ | `_logs_*` | 同名(`console` 是空桩,控制台只能我们做) |
140
+
141
+ ### 13.4 如实边界
142
+
143
+ - `screenshot` **没有上游 selector**:卡图先 `eval` 量盒子再填 `clip`;不给路径我们补一个到工坊同级的 `shots/<unit|page>-<时间戳>.png`(否则 opencli 回 base64);要裁的块不在视口里就退回整屏图,原因在 `cropReason`。
144
+ - `state` **没有 `scope`**(卡内定位用 `find --css '$card …'`);**opencli 没有右键 / contextmenu**;ref 只在**一次快照**里有效,别跨调用存;`_dialog` 的 `accept|dismiss` 不校验;`_network --detail` **取不到 body**(缓存落在会话可写根之外)—— 要 body 走 `_eval` 里的 `fetch`。
145
+ - 不在这张表上的另外 15 把:`_list`/`_status`/`_install`/`_show`(台账)、`_panel`/`_status`(宿主运行态)、`_recon`/`_probe`(数据源)、`_spec`(问一条 opencli 命令的形状:`--help -f json` 原样回,**插件也适用**)、`_logs_*`(`console` 空桩)、`_debug_*`(约定本身)。
@@ -41,15 +41,16 @@ import 运行值会被**打进你的包**(第二份实例),而且浏览器
41
41
  哈希**,天然不会跟谁撞。要借外壳契约里的类,写 `:global(.wb-…)`,且那个类必须在契约里);
42
42
  **`src/**` 里零注释** —— 要说的写进 `README.md` 或 spec。
43
43
 
44
- ## 3 装上,看一眼
44
+ ## 3 装上、上板
45
45
 
46
46
  模块造好、门禁绿了,**装这一步你自己做** —— 用 `workbench_diy_install`(`unit=<单元 id>`):
47
47
 
48
48
  - 它只认**工坊里造好的**那个模块(按 unit 在工坊里查,不接任何路径)。
49
49
  - 装完顺手在**当前会话这个工作区**的面板上把它打开。
50
- - 装是这台机器上的持久变更 ⇒ 用户会看到一次点头,话里说清是哪个模块、会改什么。
51
- - 会话的审批被关成 `never` 时,这一步会被**直接拒**(不是用户点了拒绝,是没人能被问)——
52
- 让用户把审批开成 `ask`,或者在界面上点一次;判据与构建不受影响,只有这一步过不去。
50
+ - 装是这台机器上的持久变更,**但它不问审批**(工坊整族都是这个口径)—— 不会弹审批屏、也不用等他点头:
51
+ 别对他说"请你点一下确认"。
52
+ - 会话的审批被关成 `never` 时**拒的是"真回了 `ask`"的那些调用**(比如开始调试那次 `workbench_diy_debug_start`)——
53
+ 装 / 上板是 `allow`,`never` 下照样过,不用让用户去把审批开回来。判据与构建不受影响。
53
54
 
54
55
  两个必须记住的后果:
55
56
 
@@ -63,7 +64,8 @@ import 运行值会被**打进你的包**(第二份实例),而且浏览器
63
64
  实测会拼错(把"要重启才注册"读成"没装上",或者源码改了没 build 却去重装一遍)。
64
65
  - **已经装过、只是没上板**:`workbench_diy_show`(只上板,不装)。
65
66
 
66
- 工坊那七把 `workbench_diy_*` **只在工坊那个会话里**(工具清单按工作区收窄,别处连它都不在),
67
+ 工坊那一族 `workbench_diy_*`(29 把:14 把枚举的 opencli + 11 把看/装/探 + 4 把调试约定,
68
+ 全表见 [TOOLS.md](TOOLS.md) §13)**只在工坊那个会话里**(工具清单按工作区收窄,别处连它都不在),
67
69
  落在别的项目里调用会被当场拒 —— 这就是"DIY 模式"在工具面上的样子。
68
70
  普通项目里装别的东西是另一套工具:`workbench_modules_install` / `workbench_modules_uninstall` 等(见 TOOLS.md)。
69
71
 
@@ -0,0 +1,90 @@
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. **没有现成命令时才自己取证** —— 在**用户那一页**用 `workbench_diy_eval` 注入 hook 记
42
+ method / url / body / response,并**请他点一次真实的**(agent 自己点只限**测试环境 / 可撤销的目标**)。
43
+ 取证结果要固化成模块自己的写能力:`writes: true` + `needsApproval` + `approvalDetail`([TOOLS.md](TOOLS.md) §6)。
44
+
45
+ 4. **文档 / 问用户** —— 接口文档、别人给的 curl 样例都行;这属于**需求确认**那一拍
46
+ (照 [STEPS.md](STEPS.md) 那张表问他"你会看到什么",别问技术细节)。
47
+
48
+ ## 两条硬纪律
49
+
50
+ - **写命令只有少数带预览档**:隔离运行时那份 manifest 实测 **357 条写命令里只有 12 条**有
51
+ `yes` / `dryRun` / `confirm`(那 12 条:antigravity `delete`/`revert` · codex `archive` ·
52
+ gemini `deep-research` · grok `delete` · kimi `history-rename`/`sign-out` · pinterest 三条 delete ·
53
+ slock `task-delete` · twitter `list-delete`)。**`opencli cmdb publish` / `cmdb build` 都没有预览开关**
54
+ —— 调一次就是**真发布 / 真构建**。有预览档的先用预览档;没有的**不许 agent 试写**。
55
+ - **要新凭据 / 多步长流程 / 新依赖 ⇒ 工程域**(同 [DESIGN.md](DESIGN.md) §0)。
56
+
57
+ ## 出参的最终确认靠**回读**
58
+
59
+ 写完拿**同族的读**(或卡片那条投影)把那一格读回来,**别看"命令没报错"就当成功** ——
60
+ 平台可能收下了却没生效,也可能失败在"**可能已经写了**"那一档。写进模块时,
61
+ `design.json` 的 `writes[].readback` 就是这一格(哪条读 / 出参哪个字段)。
62
+
63
+ ## 边界(如实)
64
+
65
+ - **`browser network --detail` 在工坊会话里拿不到 body**:捕获缓存落在会话可写根之外
66
+ (`EPERM …/opencli/home/.opencli/cache/browser-network`),只能列请求 key。
67
+ 要 body 走 `workbench_diy_eval` 里的 `fetch` —— 用它自己的登录在页面里读,返回 JSON。
68
+ - **两套 opencli 真相**:PATH 上那份、你机器上 `~/.opencli` 那份、工作台**隔离运行时**那份
69
+ 可能不是同一代。工坊一律以**隔离运行时**(`$DSH_HOME/workbench/opencli/`)为准;
70
+ 核验用工具,别去看错文件。
71
+ - **取值枚举只在 help 文本里**(manifest 没有 `choices`),别指望 recon 的 `args` 带它。
72
+
73
+ ## 落进 `design.json`
74
+
75
+ 写通道填 `writes[]`(一个动作可以打几条写:先建后发布;只有一条写也可以写成 `write: {}`)。
76
+ 字段六个必填、一个可空;下表右边是 `design render` 渲染出来的**行名(逐字)**:
77
+
78
+ | 字段 | 渲染出来的行 |
79
+ |---|---|
80
+ | `command` | 哪条写(命令 / URL + 方法) |
81
+ | `params` | 参数(必填 / 取值枚举 / 上限) |
82
+ | `credentials` | 凭据从哪来 |
83
+ | `preview` | 有没有预览档(没有就写"没有,一次就是真的") |
84
+ | `readback` | 写完之后怎么回读确认(哪条读 / 出参哪个字段) |
85
+ | `failure` | 失败长什么样(含"可能已经写了") |
86
+ | `probeCommand`(可空) | 当初探明 / 预览用的那次调用(原文) |
87
+
88
+ `design render` 把它写进 README 的 **`### 写通道探明记录`** 那一节,位置在「数据源探明记录」与「验收」之间
89
+ (一条一张表;多条渲染成 `#### 写 N`)。`dsh-wb-module design check` 会把空着的格点名叫出来。
90
+ **没有写通道的模块不用写** —— 它不判"一条都没有",`check` 不红。
@@ -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)[];