@dsh-workbench/core 0.3.3 → 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 (47) hide show
  1. package/client.js +512 -42
  2. package/cordis.patch.yml +32 -0
  3. package/dist/client/client.body.js +512 -42
  4. package/dist/client/index.js +512 -42
  5. package/dist/host/index.js +2486 -323
  6. package/dist/types/app/client.d.ts +108 -3
  7. package/dist/types/app/layouts/approval-detail.d.ts +13 -0
  8. package/dist/types/app/layouts/sidebar/RecordFooterAction.d.ts +16 -0
  9. package/dist/types/app/layouts/sidebar/record-footer.d.ts +4 -0
  10. package/dist/types/modules/panels/client.d.ts +1 -0
  11. package/dist/types/modules/panels/model/diy-components.d.ts +3 -0
  12. package/dist/types/modules/panels/model/diy-components.test.d.ts +1 -0
  13. package/dist/types/modules/panels/model/diy-record-bar.test.d.ts +1 -0
  14. package/dist/types/modules/panels/model/diy-record-capture.d.ts +27 -0
  15. package/dist/types/modules/panels/model/diy-record-capture.test.d.ts +1 -0
  16. package/dist/types/modules/panels/model/diy-record-failure.test.d.ts +1 -0
  17. package/dist/types/modules/panels/model/diy-record-routes.d.ts +2 -0
  18. package/dist/types/modules/panels/model/diy-record-routes.test.d.ts +1 -0
  19. package/dist/types/modules/panels/model/diy-record-scripts.d.ts +6 -0
  20. package/dist/types/modules/panels/model/diy-record-text.d.ts +89 -0
  21. package/dist/types/modules/panels/model/diy-record-text.test.d.ts +1 -0
  22. package/dist/types/modules/panels/model/diy-record-tools.d.ts +2 -0
  23. package/dist/types/modules/panels/model/diy-record.d.ts +76 -0
  24. package/dist/types/modules/panels/model/diy-record.test.d.ts +1 -0
  25. package/dist/types/modules/panels/model/handoff.d.ts +1 -0
  26. package/dist/types/modules/panels/unit.d.ts +10 -4
  27. package/dist/types/shared/api/host/diy-acts.d.ts +4 -0
  28. package/dist/types/shared/api/host/diy-components.d.ts +54 -0
  29. package/dist/types/shared/api/host/diy-debug-act.d.ts +24 -7
  30. package/dist/types/shared/api/host/diy-record-act.d.ts +19 -0
  31. package/dist/types/shared/api/host/diy-workshop.d.ts +1 -0
  32. package/dist/types/shared/api/host/mount-units.d.ts +4 -0
  33. package/dist/types/shared/hooks/use-diy-record.d.ts +8 -0
  34. package/package.json +2 -2
  35. package/scripts/seed-workshop.sh +42 -2
  36. package/skills/workbench-module-diy/BUILD.md +4 -3
  37. package/skills/workbench-module-diy/DESIGN.md +26 -1
  38. package/skills/workbench-module-diy/FAMILY.md +55 -0
  39. package/skills/workbench-module-diy/HOOKS.md +95 -0
  40. package/skills/workbench-module-diy/KIT.md +16 -6
  41. package/skills/workbench-module-diy/PANEL.md +5 -2
  42. package/skills/workbench-module-diy/REVIEW.md +5 -4
  43. package/skills/workbench-module-diy/SKILL.md +12 -3
  44. package/skills/workbench-module-diy/TOOLING.md +5 -2
  45. package/skills/workbench-module-diy/TOOLS.md +6 -40
  46. package/skills/workbench-module-diy/WIRING.md +12 -5
  47. package/skills/workbench-module-diy/WRITES.md +47 -6
@@ -0,0 +1,55 @@
1
+ # 工坊会话里那一族:33 把(agent 手上真实有什么)
2
+
3
+ [TOOLS.md](TOOLS.md) 讲**你写的模块**的能力怎么写;这一份讲**工坊会话里 agent 手上真实有什么**。
4
+ 浏览器那一半照 opencli 的子命令枚举,参数只有 `args`(原样转发);为什么这么定见仓内 ADR-0062(这份指引不链仓内路径)。
5
+
6
+ ## 1 前置(别的都排它后面)
7
+
8
+ - 只在**工坊会话**里出现,且**那块板画在屏上**;看 / 动 / 网络 / 日志那一族还要**先立调试约定** `workbench_diy_debug_start`(请用户点头一次):**没有约定一律当场拒、零命令**;不看屏 `_debug_pause`、收工 `_debug_stop`(顺手收网络与日志钩子)。
9
+ - 会话钉死 `workbench`、每条带 `--window background`;`_list`/`_status`/`_panel`/`_components`/`_recon`/`_probe`/`_spec`/`_install`/`_show` 不碰他那一屏,不用开。**写那一族(录制交接,见 §5)不走这条路**:它自己开一条 `workbench-rec` 会话,`_record_prepare` 要点头一次,`_record_read` / `_record_detail` / `_record_stop` 不问。
10
+
11
+ ## 2 14 把浏览器工具(工具名 → argv;前缀都是 `workbench_diy_`)
12
+
13
+ `_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`
14
+
15
+ 干什么:名字直说;不直观的几个 —— `_state` URL + 标题 + 可点元素(ref 来源)、`_html` HTML(`--as json` 树)、`_find` 回 `{matches_n, entries[]}`、`_network` 收发包(`--follow`;body 拿不到)、`_screenshot` 写文件(`clip` 只裁一块)。
16
+
17
+ **参数**(`scope` / `in` 默认都是 `page`):
18
+
19
+ - 公共:`unit?`(哪张卡;可省 —— 只剩一个模块就用它。**要卡作用域就得给**:写 `scope: 'card'` / `in: 'card'` 或用 `$card` 却不给 ⇒ **当场拒、零命令**)· `args?: string[]`(原样接在 `opencli browser workbench <verb…>` 后)· `timeoutMs?`(默认 30000;≤ 0 或非数也按默认)。
20
+ - `scope: 'card'` 只给吃 CSS 的**七把**:`_find`(`--css`)· `_html`(`--selector`,没给它我们补)· `_click`/`_hover`/`_type`/`_fill`(**位置参数 0**)· `_wait`(**只在 `wait selector`**,位置参数 1)—— `card` = 给那个目标挂 `#wb-card-<unit> .wb-card `。
21
+ - `in: 'card'` 只 `_eval`(注入卡根 `root`,**脚本自己 `return`**);`clip`(CSS px)只 `_screenshot`(拍完本地裁);`$card` **任何 `args` 里**都替换成 `#wb-card-<unit> .wb-card`。
22
+
23
+ **拿回**:`{ 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` 敲不了空格**,没有别的口子就如实说做不到。
24
+
25
+ ## 3 老工具 → 新命令
26
+
27
+ | 老 | 新 |
28
+ |---|---|
29
+ | `_look` | `state` + `get html` + `eval` + `screenshot`(卡图靠 `clip`,上游没有 selector) |
30
+ | `_reload` / `_viewport` | `eval 'location.reload()'` / `screenshot --width/--height` |
31
+ | `_select` | **没了**(上游只认原生 `<select>`);下拉两拍:卡内点触发器 → `scope: 'page'` + `--role menuitem` 按名字点 |
32
+ | `_net_*` | `network`(`--follow` 臂上、收工 `_debug_stop` 顺手收;**取 body 走 `_eval` 的 `fetch`**) |
33
+ | `_click` `_hover` `_type` `_fill` `_keys` `_scroll` / `_logs_*` | 同名,参数改成原样转发(`console` 是空桩,控制台只能我们做) |
34
+
35
+ ## 4 如实边界
36
+
37
+ - `screenshot` **没有上游 selector**:卡图先 `eval` 量盒子再填 `clip`;不给路径我们补到工坊同级的 `shots/<unit|page>-<时间戳>.png`(否则回 base64);要裁的块不在视口里就退回整屏图,原因在 `cropReason`。
38
+ - `state` **没有 `scope`**(卡内定位用 `find --css '$card …'`);**没有右键 / contextmenu**;ref 只在**一次快照**里有效,别跨调用存;`_dialog` 的 `accept|dismiss` 不校验;`_network --detail` **取不到 body**(缓存落在会话可写根之外)—— 走 `_eval` 里的 `fetch`。
39
+ - 录制交接那条**会话捕获**(`_record_prepare` 起的:先短暂 `network --follow` 只做"臂上",然后每 3 秒一次**非跟随** `browser <session> network --all --raw`,**落盘前先按 URL 去掉静态资源与 `data:`**(含 node 就过滤、没有就原样落),一轮一行追加到工坊同级的 `shots/rec-<unit>.jsonl`)**带 body**:非跟随那条会把 live 缓冲排空,连 `request.body`(入参)与 `body`(出参)一起打出来;跟随那条只打形状(只有方法 / URL / 状态 / ct / size / 时间),所以只拿它臂上、装完就杀。它盖**整场**、在浏览器那一层,比页面钩子全(钩子只认那一页自己的 `fetch`/`XHR` —— 应用提前把 `window.fetch` 存走、或在 iframe / Worker / WebSocket 里发,它就看不见)。**不许改成"用 opencli 默认过滤"**:那层是**按响应 ct 白名单**判的(只留 json/xml/text-plain/javascript),"按下写就立刻按结束"那条**回包还没到、ct 为空**的写会被当场丢掉 —— 实测 127 条里它只留 33 条、那条写没了;按 URL 去静态则是 127 → 30 条、那条写还在。
40
+ - 另外 20 把:台账 `_list`/`_status`/`_install`/`_show`、组件与 kit 清单 `_components`(不填给清单与类型面根,`name=Button` 回它的类型文件与声明)、宿主运行态 `_panel`/`_status`、数据源 `_recon`/`_probe`、`_spec`(问一条 opencli 命令的形状,**插件也适用**)、`_logs_*`、约定本身 `_debug_*`、录制交接 `_record_*`(§5)。
41
+
42
+ ## 5 录制交接(四把):抓那一次"在外站页面上点出来的写",清单 → 挑一条 → 看原文
43
+
44
+ | 工具 | 干什么 |
45
+ |---|---|
46
+ | `_record_prepare` | 开一扇窗**顶到最前**(**要用户点头一次**)、装浮条、臂上捕获。参数 `unit` / `url` / `doing`,可选 `expect:{method,urlContains}`;**不给 `url` 当场拒**,**跨域也当场拒**(那个模块的 `design.json` 说过读源时,只许同域录;还没说过就不拦)。返回带 `wrote: false` |
47
+ | `_record_read` | 取那一段的**清单**:`rows`(每行 `index` / `key` / `at` / `method` / `url` / `status` / `ct` / `size` / `source` / `hasRequest` / `hasResponse` / `likelyWrite` / `marked`)· `hookCaught`(钩子这一段**原始**条数)· `capturedLines` / `capturedInSegment` / `capturedOutside` · `dropped` · `noBody` · `nav` · `captureRound`(会话捕获**最近一轮**成没成:`ok` / `fail` / 空 = 还没回)。**清单一条都不删、也不回体**:页面钩子(`[t0,t1]` 段)与会话捕获(整场按段切)合并,**同名只配掉一条钩子行**(缺的 key / 入参 / 出参由捕获那条补上);捕获里两条同 method+url 是**两次真调用,都留着**。**`expect` 不再是过滤器**:它只给命中的行标 ⭐,⭐ 排在前面(不然只列最后十几条时它可能不在正文里);不设 expect 时 ⭐ = 非 GET 或带 request body。要哪一条的原文就带它的 `key`(或 `index`)去 `_record_detail`。没按开始 / 还在录都如实说,不假装"没有请求"。可选 `waitMs`(默认 0 = 立刻回,帽 15 分钟):等不到就回 `timedOut` 并挂一条 notice 叫醒你 —— **一场就是一个周期(开始 → 结束)**,结束之后要再录就**重新 `_record_prepare` 一场**(同场没有"再按一次开始"这回事)。**他按「结束录制」不用他再跟你说一声**:录制期间宿主每 3 秒自己看一眼那一页(浮条在目标页里、推不到宿主;宿主能叫醒你的通道只有 `deliverDebugNotice`),一看到 `finished` 就推状态并发一条 notice 叫你来 `read` |
48
+ | `_record_detail` | 取**某一条**的原文:入参(`request.body`)与出参(`response body`),外加请求头(可能含凭据,别落进 `design.json`)。参数 `key`(清单里那一条,原样给、含 `#2` 这种后缀)或 `index`(清单里的序号,只在那一场还开着时可用);**同一个 key 跨轮会重名**(opencli 的 key 计数器每批重置),所以再带上清单里那行的 `at` 才钉得住是哪一次(不给就取最早那条,并在正文里说重名几次)。可选 `unit` —— 那一场**收工之后**要给 unit,我去读 `shots/rec-<unit>.jsonl.last`(`_record_stop` 会把原文留成 `.last`,下一场 prepare 开录时清掉它)。取不到时如实说"两边都没有",并给兜底:DevTools 复制 curl。可选 `maxBody`(默认 60000 字符,0 = 不限) |
49
+ | `_record_stop` | 停捕获、收掉那个标签、清状态(**收工前先读一次捕获文件**,与 read 同一套切段 / 合并;**原文另存成 `.last`**,所以收工之后还能 `_record_detail`;没挪成就不说"原文留在…")。回一段能直接落 `design.json` 的原文(`probeCommand`)—— **只认第一条带 body 的**;一条都没有就回空串,并如实说"只有方法 / URL,body 拿不到 —— 补到 body 再落"(没 body 的写命令落下去是一条**会打错的**命令)。**先 read(或先 detail 完)再 stop** |
50
+
51
+ 左栏底部那一格给他的键(等出手 `[取消]` / 录制中 `[停]`)只**给 agent 发一条消息**,不直接改页面那一半(`[停]` 有意不动状态);**录完那一档没有键** —— agent 自己来取、自己收工。
52
+ 一场过期(TTL 15 分钟)或台账被换走,**不会把采集循环留在地上**:看门那一拍看到台账没了就停捕获 + 收标签,
53
+ 循环本身也被宿主 pid 兜着(宿主重起后孤儿循环自己退);`prepare` 开新一场前也先对同一落点收一次。
54
+ 细则、三颗键与如实边界(`fetch`/XHR 之外抓不到、50 条 / 16 KB 上限、一次一场 15 分钟)
55
+ 都在 [WRITES.md](WRITES.md) 步骤 3;纪律同那一份的「三条写纪律」。
@@ -0,0 +1,95 @@
1
+ # HOOKS:kit 里那些 hook 怎么用
2
+
3
+ **为什么有这份**:外壳与共享包的源码按"零注释"规矩写,emit 出来的 `<名字>.d.ts` 里**没有说明文字** ——
4
+ 只有参数名与类型。组件的 prop 一般看得出来,hook 的语义看不出来(什么时候调、取数什么时候重来、
5
+ "在忙"为什么还要挂一会儿)。这份就是那些语义的出处。
6
+ **原语那 5 个 hook 不在这里**(`useAnchoredPosition` / `useAnchoredMaxHeight` / `useCodeHighlighter` /
7
+ `useDismissOnOutsidePointer` / `useModalLayer`):它们的 `.d.ts` 自带 JSDoc。
8
+
9
+ 问清单、或要某一件的类型文件:`workbench_diy_components`(`name=useVisibleBusy`)。
10
+
11
+ ## 0 两条路,别绕开
12
+
13
+ - **取数**:卡片里所有读都走 `useApiResource`。它自带三件事 —— 同 key 合流、事实通道订阅("这条变了"自动重取)、
14
+ 把读数上报面板(`workbench_diy_panel` 看的就是它)。
15
+ - **状态**:`useVisibleBusy`(在忙 / 安定)+ `useCapabilityGate`(被能力挡住没)+ `useUnitBoard`(把两者与
16
+ 卡片读数叠成一组)。**别自己 `useState` + `setTimeout` 造一套**:面板要的是同一套口径。
17
+
18
+ ## 1 取数与事实
19
+
20
+ ### `useApiResource(route, query, refreshToken, options?)` → `{ phase, value?, error?, at? }`
21
+
22
+ 详见 [LINKAGE.md](LINKAGE.md) §8。`phase` 四态 `idle` / `loading` / `ready` / `error`;`value` 是
23
+ `ApiResponse<T>`(`ok` / `data`);`options`:`enabled`(前置条件不满足就别发)、`key`(自定义身份)、
24
+ `fact`(订哪条事实,默认 `factOfRoute(route)`)、`report: false`(不上报面板 —— 夹具 / 后台探针用)。
25
+
26
+ ### `useFactRevision(fact)` → 版本号 | `useFactInvalidate()` → `(fact?) => void`
27
+
28
+ 详见 [LINKAGE.md](LINKAGE.md) §8。前者是"这条事实的版本",喂给 `useApiResource`(默认订阅已经接好);
29
+ 后者是**写完之后**把这条标脏 —— 不标,面板就停在旧值。
30
+
31
+ ### `subscribeFactRevision(fact, listener)` → 退订函数
32
+
33
+ 非 hook:给"不在渲染里"的地方用(宿主事件、定时器里看到的旧值)。返回值就是退订函数,**记得退订**。
34
+ 渲染里要版本号用 `useFactRevision`,别自己 `useEffect` + `subscribe` 绕一圈。
35
+
36
+ ### `resourceIdentity(route, query, override?)` | `readKeyOf(identity, refreshToken, factRev)`
37
+
38
+ 都不是 hook:前者是"这次读的身份"(`route` + 排序后的 query;给了 `override` 就用它);后者把身份与两个
39
+ 版本号拼成**合流的 key**。要自己管缓存时才用得上。
40
+
41
+ ### `requestResource(route, query, key)` → `{ task, release }`
42
+
43
+ 非 hook 的取数本体:**同 key 只发一次**(并发调用共享同一个 `task`),最后一个 `release()` 才 abort。
44
+ `useApiResource` 内部就是它 + 事实订阅 + 上报。用它就**一定要 `release`**(放 `finally`),否则那条请求永不取消。
45
+
46
+ ## 2 忙与安定
47
+
48
+ ### `useVisibleBusy(active, floorMs)` → `{ busy, settling }`
49
+
50
+ **最短可见时长**:`active` 变真立刻 `busy`;变假时若这次忙还没到 `floorMs`,`busy` 先挂着,这期间
51
+ `settling: true`("读数在回来的路上")。`floorMs` 传共享包的 `MIN_BUSY_MS`,别自己编。
52
+ 为什么:取数几毫秒就回来时不挂一会儿,用户看到的是"闪一下",比不转还难读(ADR-0022)。
53
+
54
+ ### `busyView(active, visible)` | `heldPhase(phase, refreshing)`
55
+
56
+ 两个纯函数(不是 hook,好测):`busyView` 是"真忙还是在收尾"的投影,`useVisibleBusy` 用的就是它;
57
+ `heldPhase` 把"正在刷新"折进 `phase`(`refreshing && phase === 'ready'` ⇒ `'loading'`),
58
+ 让"旧值还显示着、新的在路上"这种态有名字。
59
+
60
+ ## 3 卡片那一格与能力门
61
+
62
+ ### `useUnitBoard({ unit, gate, refreshToken, onRefresh, onSummary, read, t, enabled?, platformBlocker? })`
63
+
64
+ **卡片起步就用它**:把"gate 挡着就不读"、忙 / 安定、探针(被能力挡住或读数报错时自动 `forceProbe`)、
65
+ `useCardSummary`(把 `{ refreshing, at }` 交给调用点上报面板)、以及 blocker 的拆分
66
+ (`capabilityBlocker` / `errorBlocker`)一次叠好。`read(gated)` 是你自己的读数函数(里面调 `useApiResource`);
67
+ 返回 `feed`(含 `state`)、`capability`、`gated`、`refresh`、`forceProbe`、`summary` 与那几个 blocker 字段。
68
+
69
+ ### `useCapabilityGate(unit, refreshToken, options?)` → `{ capability, gated, forceProbe }`
70
+
71
+ 这一格的**能力**(`design.json` 里声明的那份)与"挡住了没"。`options.mode` 换门的口径(默认 `'capability'`),
72
+ `options.fixture` 给夹具。`forceProbe()` 是"现在就去探一次"。
73
+
74
+ ### `useCapabilityMap(refreshToken)` → `{ capabilities, checkedAt, checking, forceProbe, reload }`
75
+
76
+ **全部**单元的能力表(不是一格):设置页 / 体检那一类界面用。`checking` 是探针在跑,`reload()` 重取整张表。
77
+
78
+ ### `useCardSummary(onSummary, summary)`
79
+
80
+ 把 `{ refreshing, at }` 在变化时交给调用点(`onSummary` 要**稳定引用**,一般 `useCallback`)——
81
+ 面板那行"最近一次读是什么时候"就是这么上去的。`useUnitBoard` 已经接好;自己搭卡片才直接用。
82
+
83
+ ## 4 写与长任务
84
+
85
+ ### `useHostWrite({ channel, resource?, networkText })` → `{ busy, failed, detail, send, fail, clear }`
86
+
87
+ 详见 [PANEL.md](PANEL.md) §5(写通道与配置持久化)。`send(body, { silent })` 回 `WriteOutcome`
88
+ (`ok` / 人话 message / detail),失败自动落到 `failed` + `detail`(界面直接显示,别自己拼);`silent` 是不打扰;
89
+ `fail` 是"本地就知道不行"时自己塞一条。**写只走这条**:别在组件里直接 `fetch`。
90
+
91
+ ### `useLongTask({ unit, workspace, mocked?, enabled?, pollMs? })` → `{ runs }`
92
+
93
+ 详见 [TOOLS.md](TOOLS.md) §9。轮询这一单元的长任务:有在跑的(`running` / `needs-login`)按
94
+ `longTaskPollMs` 快档轮,空闲慢档;`workspace` 为 null 或 `mocked` 时不轮。画用 `<LongTaskRow …/>`,
95
+ 点用 `longTaskDot(status)`。
@@ -70,9 +70,7 @@ h(Rows, null,
70
70
  **`LongTaskRow`** —— 长任务那一行:`status`(四态)+ `headline` + `problem` + `links` + `steps` +
71
71
  `raw`。跑得久的事(发布、扫描、迁移)都用它,别自己画进度。
72
72
 
73
- **`ApprovalDetail` / `ApprovalPlan`** —— 审批屏上那两格(要用户点头的写操作)。
74
-
75
- ## 4 表单
73
+ **`ApprovalDetail` / `ApprovalPlan`** —— 审批屏那两格(写操作才要点头);口径与那张 ASCII 见 [DESIGN.md](DESIGN.md) §3 末节。## 4 表单
76
74
 
77
75
  **`Field`** —— 标签 + 输入的排布(`label` / `hint`),输入框放里面。
78
76
 
@@ -184,6 +182,18 @@ h('div', { className: styles.form }, …) // ← .form { gap:
184
182
 
185
183
  ## 8 kit 里没有、但你确定要标准控件
186
184
 
187
- 见 [PANEL.md](PANEL.md) §9:可以引 dsh 的客户端原语,但**只在浏览器侧**、并在
188
- `package.json` 的 `dsh.client.external` 里声明 `@deepseek-ai/dsh-client-ui-primitives`
189
- (宿主侧一个字都不许碰 —— `check` 的源码边界判据会红)。能拿 kit 的时候仍然别自己画。
185
+ **先问工具**:DIY 会话里调 `workbench_diy_components`(不填给清单与类型面根;`name=Button`
186
+ 直接回它的类型文件与那份声明)。工程域用 `dsh-wb-module check --only primitives`
187
+ (`--all` 全表 / `--json` 机器档)—— 分桶导出 + 与 kit 求交(`kit 里已有` 的优先用)+
188
+ 该写进 `package.json` 的那两行。骨架默认已备好。
189
+ **要看某一件的 prop / 用法**:`--file <名字>` 给绝对路径(`kit` 那条同形:
190
+ `check --only kit --file Card`),`--files | grep <名字>` 出全表 —— 每件都有一个同名(或就近)的 `.d.ts`。
191
+
192
+ ```jsonc
193
+ "dsh": { "client": { "external": [ "@dsh-workbench/core/client", "@deepseek-ai/dsh-client-ui-primitives" ] } },
194
+ "devDependencies": { "@deepseek-ai/dsh-client-ui-primitives": "<外壳钉的那版,精确版>" }
195
+ ```
196
+
197
+ 改完 `pnpm install` 一次(不装 `pnpm typecheck` 报"找不到模块")。三条会红的边界:只在浏览器侧引
198
+ (宿主侧碰它 `source` 判红);版本对齐外壳钉的那份(`init` 盖的就是它,别用 `^`);能拿 kit 的别引。
199
+ 宿主侧的契约按需自己声明([PANEL.md](PANEL.md) §9)。
@@ -197,10 +197,13 @@ agent 那一侧也要能通过模块自己的能力读到同一份偏好。
197
197
  ## 9 界面边界:kit 不够用时怎么办
198
198
 
199
199
  **先从 kit 拿**(`@dsh-workbench/core/client`,`check --only kit` 会列全这份外壳有什么)。
200
- kit 里确实没有、而你要的又是标准控件(确认条、选择器、设置区块那种)时,可以引 dsh 的客户端原语:
200
+ kit 里确实没有、而你要的又是标准控件(按钮、选择器、设置区块那种)时,可以引 dsh 的客户端原语 ——
201
+ **有哪些**问 `dsh-wb-module check --only primitives`(它还会把"kit 里已有"的那批挑出来):
201
202
 
202
203
  * 只在**浏览器侧**引(`src/client.tsx` 与 `src/components/**` 那一侧);
203
- * 在 `package.json` 的 `dsh.client.external` 里**声明** `@deepseek-ai/dsh-client-ui-primitives`;
204
+ * 在 `package.json` 的 `dsh.client.external` 里**声明** `@deepseek-ai/dsh-client-ui-primitives`,
205
+ 同时把 `devDependencies` 里那行**精确版**对齐到外壳钉的那版(骨架默认已经两处都写好了;
206
+ 声明完记得 `pnpm install`,否则 `pnpm typecheck` 找不到它);
204
207
  * 宿主侧(`src/host.ts`)**一个字都不许碰** —— `check` 的源码边界判据会红;宿主侧 import
205
208
  `src/components/**` 还会被 `arch` 按跨运行时(S23)判红 —— 组件整片算 client 面。
206
209
 
@@ -7,8 +7,8 @@
7
7
  | `contracts` | dsh 组合期**不校验** `package.json` 声明;清单 ↔ 代码 / 底稿 ↔ 清单漂了没人拦 | 报哪条改哪条:`unit` 撞了换名;`dsh.workbenchFaces` 补两个**源码**入口;四个脚本补全;私有就 `private: true`。`dsh.workbench.tools` 与 `registerTools()` 注册的**不重不漏**;写了 `design.json` 就对**键集合**(只对名字);生成物过期跑 `dsh-wb-module sync` |
8
8
  | `protocol` | `dsh.workbench` 缺版本 / 形状不对 / **多一个键** —— 外壳会**静默忽略**生字段 | 补 `"apiVersion": 1`;`unit`/`card`/`tools`/`host` 写全(没卡片 `null`、没工具 `[]`);删协议外的键(`faces` 住 `dsh.workbenchFaces`,`scope` **不要写**);字段清单见 module-tools README「协议」 |
9
9
  | `bundles` | 两个运行时互串:浏览器产物带 node 内置 / 宿主产物带 DOM、react | 各自别 import 对方那侧;只通过服务名相接 |
10
- | `source` | 非种子词的 `@deepseek-ai/*`、宿主侧碰 dsh 浏览器包、`@/` 指到工作台源码 | 宿主侧只用 Node 侧;dsh 原语一律走 `kit`;`@/` 只指自己 `src/` |
11
- | `primitives` | 声明 `external` 却没产物物证 ⇒ 原语被**内联**成第二份实例 | 在 `dsh.client.external` 显式声明;确认产物里是 `require(...)` |
10
+ | `source` | 非种子词的 `@deepseek-ai/*`、宿主侧碰 dsh 浏览器包、`@/` 指到工作台源码 | 宿主侧只用 Node 侧;dsh 的浏览器包(种子词)只在**只被 client 入口可达**的文件里用;`@/` 只指自己 `src/` |
11
+ | `primitives` | 声明 `external` 却没产物物证 ⇒ 原语被**内联**成第二份实例 | `dsh.client.external` 里声明;`devDependencies` 精确版对齐外壳钉的那份;产物里要是 `require(...)`(清单:`check --only primitives`) |
12
12
  | `kit` | 用了 peer 下限那份外壳**还没有**的成员 ⇒ 卡片首帧 `Element type is invalid`,整块面板被卸 | 只用 peer 范围里真有的成员;下限提到你实际用的那档 |
13
13
  | `cards` | 卡片在外壳侧输入下崩 / 空白 / 变脏(八态 × `zh`/`en` 各一遍;走**真壳**取数) | 五态都画得出(加载 / 空 / 阻断);外套 `kit.components.Card`;`titleKey` 与卡片问过的每个键 `zh`/`en` 都有落点。判据**不断言**画的是哪一态 —— 那归 `plugin.test.ts` |
14
14
  | `standalone` | "只有工作台源码在场才编得过" ⇒ 发出去就散架 | 别引用工作台源码;`tsconfig` 的 `paths` 用**相对路径**(`"./src/*"`) |
@@ -106,13 +106,14 @@ export default [
106
106
 
107
107
  判据自己也有正反例,正常不误报。但**骨架刚 `init` 出来、一行没改却红了** —— 那是模板 / 工具坏了,不是你的。
108
108
 
109
- ## 三条"不是判据、但更常踩"的
109
+ ## 四条"不是判据、但更常踩"的
110
110
 
111
111
  (细节在 [WIRING.md](WIRING.md) / [BUILD.md](BUILD.md))
112
112
 
113
113
  1. **装上去没反应** → 先问 `workbench_diy_status`(`unit=…`)。[WIRING.md](WIRING.md) §3
114
114
  2. **改完没热换** → 宿主侧秒级热换;客户端刷新页面。[WIRING.md](WIRING.md) §4
115
115
  3. **"再装一遍"** → 日常迭代只重建、不重装。[WIRING.md](WIRING.md) §3
116
+ 4. **引了 dsh 原语却 `tsc` 报"找不到模块"** → 两处声明 + `pnpm install`([KIT.md](KIT.md) §8)。
116
117
 
117
118
  ## 用户说"卡片不对"时:四步取证
118
119
 
@@ -124,4 +125,4 @@ export default [
124
125
  3. **自己把那条读跑一遍**:卡片画的就是模块那条宿主路由的返回值 —— `curl '<工作台地址>/workbench/api/<unit>/<route>'`(默认 `http://127.0.0.1:3081`)。接口对而卡片不对 ⇒ 渲染侧;接口就错 ⇒ 宿主侧(照 [DATASOURCE.md](DATASOURCE.md) 重探)。要**浏览器里那一次**的原文,用 `workbench_diy_probe`(`script` 里 `fetch` 自己那条读)。
125
126
  4. **最后才看用户那一页**:要控制台走三拍 —— `workbench_diy_logs_watch` → 做那件你怀疑会报错的事 → `workbench_diy_logs_read`(读完 `workbench_diy_logs_stop` 卸掉;钩子上限:每页 200 条、单条 400 字、随刷新消失)。**别用 `opencli browser <会话> console`** —— 那是个 stub(恒回空数组、还 exit 0)。要在页面里跑自己的代码用 `workbench_diy_probe`(**读会话**那页);看**用户那一页**是什么、有哪些可点的用 `workbench_diy_state`。宿主侧热换 / 装载的报错在**起工作台那个终端**里。
126
127
 
127
- 两条纪律:**别让用户截图或替你转述报错**;**验收就是你自己拿这一族走** —— 逐条对着三张表走「打开哪 → 看什么 → 点什么 → 应该看到什么」,`screenshot`/`html`/`find`/`eval` 的读数、动作族的返回、`workbench_diy_network` 的收发包、`logs_*` 的报错都算证据;走通再 `design accept`,走不通的如实说没验过(用户不必在场,也不用请他逐条点头)。工具参数与返回体都是 opencli 的原样(见 [TOOLS.md](TOOLS.md) §13)。面板运行态是**用户那一页自己报上来的**,第 1 步没有内容时,先让他把工作台切到工坊那个工作区、刷新一次页面再看。
128
+ 两条纪律:**别让用户截图或替你转述报错**;**验收就是你自己拿这一族走** —— 逐条对着三张表走「打开哪 → 看什么 → 点什么 → 应该看到什么」,`screenshot`/`html`/`find`/`eval` 的读数、动作族的返回、`workbench_diy_network` 的收发包、`logs_*` 的报错都算证据;走通再 `design accept`,走不通的如实说没验过(用户不必在场,也不用请他逐条点头)。工具参数与返回体都是 opencli 的原样(见 [FAMILY.md](FAMILY.md))。面板运行态是**用户那一页自己报上来的**,第 1 步没有内容时,先让他把工作台切到工坊那个工作区、刷新一次页面再看。
@@ -18,11 +18,15 @@ description: 在工作台里凭需求造一个模块(面板上的一块内容
18
18
  都在那把工具里;探明后**固化成一条读**(`@dsh-workbench/shared/host` 的 `browserRead`)。顺手把三态写掉。
19
19
  要 agent **真去改东西**(提交 / 发布 / 建单 / 改配置)的,读完 [WRITES.md](WRITES.md) 再动手:
20
20
  四步(现成写命令 → 入参 / 出参来源 → 没有预览档的不许试写 → 写完后回读确认)。
21
+ 有写工具就**顺手把审批屏那一屏也画进图里**([DESIGN.md](DESIGN.md) §3 末尾那节 + KIT.md §3):
22
+ 标题一句 + `ApprovalPlan` 详情表 —— 那两格的形状是固定的(仓内 ADR-0065,这份指引不链仓内路径)。
21
23
  4. **从模板改** —— [BUILD.md](BUILD.md) §1–§2:在工坊里 `dsh-wb-module init` 一个骨架,
22
24
  把 `design.json` 里的**三张表与数据源记录先改成你问出来的那份**(`design check` 会点名叫出还空着哪一格),
23
25
  再改数据源与渲染。
24
26
  **不从零写**。
25
27
  命令写成 `pnpm exec dsh-wb-module …`(工坊里 PATH 上也有同名入口,但 `pnpm exec` 到处都对)。
28
+ `init` 完先在工坊里跑一次 `pnpm install`:模板默认带了 dsh 原语那个 devDependency
29
+ ([KIT.md](KIT.md) §8),不装 `pnpm typecheck` 会报"找不到模块"。
26
30
  5. **自己过门禁** —— `dsh-wb-module build` + `check` + `design check`;
27
31
  红了自己修([REVIEW.md](REVIEW.md) 有对照),不抛给他、也不贴报错给他看。
28
32
  6. **装上、上板** —— `workbench_diy_install`(`unit=<单元 id>`)装上:
@@ -36,7 +40,7 @@ description: 在工作台里凭需求造一个模块(面板上的一块内容
36
40
  `workbench_diy_debug_start`(`unit=…`;会请他点头一次,他会被告知「调试中请留在那一屏」)——
37
41
  **没有这场约定,上面那一族一律当场拒**;不看屏了先 `workbench_diy_debug_pause`,收工 `…_stop`。
38
42
  (`workbench_diy_panel` / `_status` / `_probe` 不碰他那一屏,不用开。)
39
- 走法(全表与参数在 [TOOLS.md](TOOLS.md) §13):
43
+ 走法(全表与参数在 [FAMILY.md](FAMILY.md)):
40
44
  - **这一张卡长什么样**:`workbench_diy_screenshot`(要卡那一块就先 `eval` 量盒子、填 `clip`)+
41
45
  `workbench_diy_html`(`--as json` 给结构树);
42
46
  - **元素与语义**:`workbench_diy_find`(`--css '$card …'`、或 `--role` / `--name`)、`workbench_diy_state`;
@@ -73,7 +77,10 @@ description: 在工作台里凭需求造一个模块(面板上的一块内容
73
77
  | **要 agent 真去改东西**(提交 / 发布 / 建单 / 改配置:写通道四步) | [WRITES.md](WRITES.md) |
74
78
  | 面板上该有什么、状态矩阵怎么写、界面习惯 | [PANEL.md](PANEL.md) |
75
79
  | **要做什么 → 用哪件 kit → 最小写法**(照抄面) | [KIT.md](KIT.md) |
76
- | 能力怎么划、传什么拿回什么、哪几件要点头(**工坊会话里那 29 把工具的全表在 §13**) | [TOOLS.md](TOOLS.md) |
80
+ | **kit / dsh 原语里到底有什么、某一件怎么用**(DIY 会话里直接调这把工具) | `workbench_diy_components`(`name=Button` 回它的类型文件与声明;工程域用 `check --only kit` · `--only primitives`)· [KIT.md](KIT.md) §8 |
81
+ | **kit 那些 hook 怎么用**(什么时候调、取数什么时候重来、"在忙"为什么还要挂一会儿) | [HOOKS.md](HOOKS.md) |
82
+ | 能力怎么划、传什么拿回什么、哪几件要点头 | [TOOLS.md](TOOLS.md) |
83
+ | **工坊会话里那 33 把的全表**(枚举 opencli 的 14 把 + 看/装/探 + 调试约定 + 录制交接、参数与如实边界) | [FAMILY.md](FAMILY.md) |
77
84
  | agent 动完面板哪一格变、他正在干的时候看哪儿 | [LINKAGE.md](LINKAGE.md) |
78
85
  | 落点、文件形状、命令、两个面 | [BUILD.md](BUILD.md) |
79
86
  | 依赖、会被拦的写法、装、热换、发布 | [WIRING.md](WIRING.md) |
@@ -84,7 +91,9 @@ description: 在工作台里凭需求造一个模块(面板上的一块内容
84
91
  ## 找不到一件事的时候,按这个顺序找
85
92
 
86
93
  1. **先跑门禁**:`dsh-wb-module check --only <那一条>` 的报错原文最准。
87
- 「外壳的 kit 里到底有什么组件 / hooks」也问它:`check --only kit` 会把这份外壳的 kit 成员列出来。
94
+ 「外壳的 kit 里到底有什么组件 / hooks」「dsh 原语里有什么、某一件怎么用」在工坊会话里直接调
95
+ `workbench_diy_components`(不填给清单,`name=Button` 回它的类型文件与声明);工程域用
96
+ `check --only kit` · `check --only primitives`(`--all` 全表、`--json` 机器档)。
88
97
  2. **再看模板**:已经有一个同形状、能跑的最小实现,比任何描述都准 ——
89
98
  在你工坊的 `node_modules/@dsh-workbench/module-tools/template-diy/`(带真数据源的样板)
90
99
  与 `template/`(空壳)里,原文都是可读的。
@@ -16,8 +16,10 @@
16
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
17
  | 语法卫生红了、要自动修([REVIEW.md](REVIEW.md) 第十三条) | `dsh-wb-module lint [--fix]`(**`check` 永不动手改文件**;`--fix` 才写回) |
18
18
  | 装、上板、不重装([WIRING.md](WIRING.md) §3) | `workbench_diy_install` / `workbench_diy_show` / `workbench_diy_status`(工坊那一跳自己上板) |
19
+ | 想知道 dsh 原语 / 这份外壳的 kit 里有什么、某一件怎么用([KIT.md](KIT.md) §8) | `workbench_diy_components`(不填给清单 + 类型面根;`name=Button` 回它的类型文件与那份声明——JSDoc 里写着默认值与语义;工程域用 `dsh-wb-module check --only primitives --file <名字>`) |
19
20
  | 它到底起得来没(装配抛的错 / 声明被拒的原文) | `workbench_diy_status`("起得来"那一行,见 [REVIEW.md](REVIEW.md) 四步取证的第二步) |
20
- | 看 / 动**用户那一屏**之前要先取得约定(ADR-0055 §7「调试是一次约定」) | `workbench_diy_debug_start`(`unit=…`,**要用户点头一次**)—— 没有它,浏览器那 14 把 / `logs_*` **一律当场拒**(拒的时候一条命令都不发);不看屏了 `_pause`、收工 `_stop`(**收工顺手把网络捕获与日志钩子也收了**)。`_panel` / `_status` / `_probe` 不碰他那一屏,不用开。**全表与参数见 [TOOLS.md](TOOLS.md) §13** |
21
+ | 看 / 动**用户那一屏**之前要先取得约定(ADR-0055 §7「调试是一次约定」) | `workbench_diy_debug_start`(`unit=…`,**要用户点头一次**)—— 没有它,浏览器那 14 把 / `logs_*` **一律当场拒**(拒的时候一条命令都不发);不看屏了 `_pause`、收工 `_stop`(**收工顺手把网络捕获与日志钩子也收了**)。`_panel` / `_status` / `_probe` 不碰他那一屏,不用开。**全表与参数见 [FAMILY.md](FAMILY.md)** |
22
+ | 抓那一次**在外站页面上点出来的写**(他的写只发生一次,也不在我们那条会话里) | `_record_prepare`(`unit` / `url` / `doing`,**要用户点头一次**)→ 请他按「开始录制」→ 做**一次**那动作 → 按「结束录制」→ `_record_read` 取那一段 → `_record_stop` 收工(**先 read 再 stop**)。它不走调试约定,只碰自己的 `workbench-rec` 会话;三颗键、边界与实话见 [FAMILY.md](FAMILY.md) §5 · [WRITES.md](WRITES.md) 步骤 3 |
21
23
  | 看 / 动那一屏的浏览器动作怎么发(枚举 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
24
  | 这一页的控制台 / 网络怎么取证([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` 那份不是同一个 —— 核验要用工具,别看错文件 |
23
25
  | 面板上现在是什么样(每张卡读到没 / 崩没崩) | `workbench_diy_panel`(浏览器半侧上报,宿主按 unit 留最新一条) |
@@ -29,7 +31,8 @@
29
31
  | 这个能力要靠什么才跑得起来([TOOLS.md](TOOLS.md) §8) | `manifest.env` + `libInstallCommand`,`check --only contracts` 查 |
30
32
  | 配置存不存([PANEL.md](PANEL.md) §5.1) | `config[].persistence`(`prefs` / `view`)+ `check --only contracts` |
31
33
  | agent 动完面板哪一格变([LINKAGE.md](LINKAGE.md) §8) | `factsOf` + `factOfRoute` / `context` / `warmReads` / `values`,`check --only contracts` 只查形状 |
32
- | kit 不够用时能不能引 dsh 的界面原语([PANEL.md](PANEL.md) §9) | 只在客户端侧引 + `dsh.client.external` 里声明;宿主侧一个字都不许碰(`check --only source`) |
34
+ | kit 不够用时能不能引 dsh 的界面原语([PANEL.md](PANEL.md) §9 · [KIT.md](KIT.md) §8) | 只在客户端侧引 + `dsh.client.external` 里声明 + `devDependencies` 精确版对齐外壳钉的那份;宿主侧一个字都不许碰(`check --only source` / `--only primitives`) |
35
+ | dsh 原语里到底有什么(名字 / 哪几件 kit 已有 / 该写哪两行 / **这一件在哪份类型文件**) | `dsh-wb-module check --only primitives`(单跑当查询口;`--all` 全表、`--file <名字>` 给绝对路径、`--files \| grep` 全表、`--json` 机器档);kit 那条同形(`--only kit --file Card`)—— 取装机那份 `lib/types/**/*.d.ts`,不手抄清单 |
33
36
  | DOM 读怎么读([DATASOURCE.md](DATASOURCE.md) 第 2 步) | `probe`(探)+ `browserFind`(固化)—— 切格 / 按列取值是两个纯函数 |
34
37
  | 链接只有一副样子([PANEL.md](PANEL.md) §6) | kit 的 `Link` 是唯一一份(**没有 `icon` 口子**),`check --only style` 拦裸 `<a>` |
35
38
  | 输入框不用自己配样式([PANEL.md](PANEL.md) §5.1) | `TextField` 的默认内边距 + kit 的 `Field`(标签 + 提示的排布) |
@@ -62,6 +62,7 @@
62
62
  - `key`(lowerCamelCase):给了就跑一次 `dsh-wb-module sync` 生成 `src/declared-tools.ts`,代码写 `DECLARED_TOOLS.rebuild.name` —— 工具名字面量只剩清单这一处(键写错编译错;生成物过期 / 半写 key / 又写字面量都红)。
63
63
  - `needsApproval` 回 `{ kind: 'read' }` = 不问,`{ kind: 'ask', question, fallback }` = 要问;**`writes: true` 却不认领 = 当场 deny**。`check --only contracts` 先替你看:缺 `needsApproval`、zh/en 不齐、`approvalDetail` 没给 → 红。
64
64
  - `question` 是问句、`fallback` 是拼不出细节时的兜底,都要 zh/en;`approvalDetail` 是 §7 那段明细(一行一个事实),拿不到料回 `{ blocked: '…' }` = **不弹**。缺东西先自己走一遍预览摆事实,补不齐就停下把原因交给 agent 查。
65
+ - **那屏的样子是定死的**([DESIGN.md](DESIGN.md) §3 末节):标题**只有一句**(`approvalDetail` 那块明细 ≤20 字一行,且**只给没有详情表的能力**);**事实与长内容走客户端面的 `approval.render`**,用外壳的 `ApprovalPlan` 画成两列表 —— 详情座那边等宽、能换行,路径 / diff / 多字段都放得下。宿主算的料到不了浏览器侧,事实要从**工具参数**重建。
65
66
 
66
67
  ## 7 点头的全场景
67
68
 
@@ -99,47 +100,12 @@
99
100
  工具是**这台机器上的**,但项目不止一个:在 A 有意义的到 B 就是噪音。**别让工具自己判断再回"这个项目用不了"**,当场从清单里拿掉 —— `gate: { deniedTools(workspace), subscribe(listener) }`(挂进 `mount` 返回值):
100
101
 
101
102
  - `deniedTools(workspace)` 回"这个项目里我的哪几个工具不存在"(名字跟 `registerTools()` 注册的一致;`workspace` 也可能是 `null`)。`subscribe(listener)`:你状态变了(用户把能力关了)时通知外壳重算 —— 不接要等下次重启。
102
- - 外壳两道都做:清单里不出现 + 真调到时当场拒。它自己就这么用:工坊那一族 **29 把**(14 枚举 + 11 看 / 装 / 探 + 4 调试,见 §13)只在工坊会话里出现。
103
+ - 外壳两道都做:清单里不出现 + 真调到时当场拒。它自己就这么用:工坊那一族 **33 把**(14 枚举 + 12 看 / 装 / 探 + 4 调试 + 3 录制交接,见 [FAMILY.md](FAMILY.md))只在工坊会话里出现。
103
104
 
104
105
  **"哪些工作区出现"不用你声明**(「模块开发者不能定义 scope」):他用 `workbench_panels_show` / `_hide` 放上 / 收起,卡片与**全部**工具同判。`deniedTools` 可选。
105
106
 
106
- ## 13 工坊会话里那一族:29 把
107
+ ## 13 工坊会话里那一族:33 把
107
108
 
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_*`(约定本身)。
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
 
@@ -64,8 +69,8 @@ import 运行值会被**打进你的包**(第二份实例),而且浏览器
64
69
  实测会拼错(把"要重启才注册"读成"没装上",或者源码改了没 build 却去重装一遍)。
65
70
  - **已经装过、只是没上板**:`workbench_diy_show`(只上板,不装)。
66
71
 
67
- 工坊那一族 `workbench_diy_*`(29 把:14 把枚举的 opencli + 11 把看/装/探 + 4 把调试约定,
68
- 全表见 [TOOLS.md](TOOLS.md) §13)**只在工坊那个会话里**(工具清单按工作区收窄,别处连它都不在),
72
+ 工坊那一族 `workbench_diy_*`(33 把:14 把枚举的 opencli + 12 把看/装/探 + 4 把调试约定 + 3 把录制交接,
73
+ 全表见 [FAMILY.md](FAMILY.md))**只在工坊那个会话里**(工具清单按工作区收窄,别处连它都不在),
69
74
  落在别的项目里调用会被当场拒 —— 这就是"DIY 模式"在工具面上的样子。
70
75
  普通项目里装别的东西是另一套工具:`workbench_modules_install` / `workbench_modules_uninstall` 等(见 TOOLS.md)。
71
76
 
@@ -74,8 +79,10 @@ import 运行值会被**打进你的包**(第二份实例),而且浏览器
74
79
  装完之后**不要再装一遍**。改源码 → `dsh-wb-module build` → 用户面板上那张卡自己变。
75
80
 
76
81
  - **宿主侧**(`src/host.ts` / `balance.ts` … → `dist/host.js`):**秒级热换**,不重启、不刷新
77
- (加一行探针 → build → 3 秒内接口就返回新内容)。`cordis.patch.yml` 里的 `followSymlinks: false`
78
- 不能去掉:`dsh-hmr` 跟着软链会把文件描述符用光。
82
+ (加一行探针 → build → 3 秒内接口就返回新内容)。`cordis.patch.yml` 里 hmr 那两件都不能去掉:
83
+ `followSymlinks: false`(跟着软链会把文件描述符用光),以及 `ignored` 里那两条 `*(../)`
84
+ extglob(工坊那个根在外壳包目录之外,相对路径以 `..` 打头,而 picomatch 的 `**` 不吃 `..` ——
85
+ 少了它工坊 `node_modules` 的实目录会整棵进监听表)。为什么、实测数字与边界见仓内 ADR-0034 §3。
79
86
  - **客户端侧**(`src/client.tsx` / CSS → `client.js`):浏览器**不会自己**换,**刷新一下页面**即可
80
87
  (实测:重建后 60 秒页面没有任何反应)。`*.module.css` 的改动是就地改写那一份 `<style>`,刷新即生效。
81
88
  - **改 `cordis.patch.yml`、改 `package.json` 里的 `dsh.client` / `exports["./client"]` 需要重启。**
@@ -38,8 +38,32 @@
38
38
  - `opencli <site> <name> --help`(`-f yaml` 更好读)—— 补**取值枚举**:manifest 里没有 `choices`,
39
39
  枚举只在 help 文本里;`_spec` 那条路回的是 JSON,`-f yaml` 适合人眼扫。
40
40
 
41
- 3. **没有现成命令时才自己取证** —— 在**用户那一页**用 `workbench_diy_eval` 注入 hook 记
42
- method / url / body / response,并**请他点一次真实的**(agent 自己点只限**测试环境 / 可撤销的目标**)。
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 自己点只限**测试环境 / 可撤销的目标**(同下面第一条纪律)。
43
67
  取证结果要固化成模块自己的写能力:`writes: true` + `needsApproval` + `approvalDetail`([TOOLS.md](TOOLS.md) §6)。
44
68
 
45
69
  4. **文档 / 问用户** —— 接口文档、别人给的 curl 样例都行;这属于**需求确认**那一拍
@@ -54,6 +78,17 @@
54
78
  —— 调一次就是**真发布 / 真构建**。有预览档的先用预览档;没有的**不许 agent 试写**。
55
79
  - **要新凭据 / 多步长流程 / 新依赖 ⇒ 工程域**(同 [DESIGN.md](DESIGN.md) §0)。
56
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
+
57
92
  ## 出参的最终确认靠**回读**
58
93
 
59
94
  写完拿**同族的读**(或卡片那条投影)把那一格读回来,**别看"命令没报错"就当成功** ——
@@ -62,9 +97,11 @@
62
97
 
63
98
  ## 边界(如实)
64
99
 
65
- - **`browser network --detail` 在工坊会话里拿不到 body**:捕获缓存落在会话可写根之外
66
- (`EPERM …/opencli/home/.opencli/cache/browser-network`),只能列请求 key。
67
- 要 body 走 `workbench_diy_eval` 里的 `fetch` —— 用它自己的登录在页面里读,返回 JSON。
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>` 生效。
68
105
  - **两套 opencli 真相**:PATH 上那份、你机器上 `~/.opencli` 那份、工作台**隔离运行时**那份
69
106
  可能不是同一代。工坊一律以**隔离运行时**(`$DSH_HOME/workbench/opencli/`)为准;
70
107
  核验用工具,别去看错文件。
@@ -73,7 +110,7 @@
73
110
  ## 落进 `design.json`
74
111
 
75
112
  写通道填 `writes[]`(一个动作可以打几条写:先建后发布;只有一条写也可以写成 `write: {}`)。
76
- 字段六个必填、一个可空;下表右边是 `design render` 渲染出来的**行名(逐字)**:
113
+ 字段**七个必填**、一个可空;下表右边是 `design render` 渲染出来的**行名(逐字)**:
77
114
 
78
115
  | 字段 | 渲染出来的行 |
79
116
  |---|---|
@@ -82,9 +119,13 @@
82
119
  | `credentials` | 凭据从哪来 |
83
120
  | `preview` | 有没有预览档(没有就写"没有,一次就是真的") |
84
121
  | `readback` | 写完之后怎么回读确认(哪条读 / 出参哪个字段) |
122
+ | `undo` | 配对的那个撤销 / 删除(没有就写"没有配对,一次就是真的")← 第三条写纪律那一格 |
85
123
  | `failure` | 失败长什么样(含"可能已经写了") |
86
124
  | `probeCommand`(可空) | 当初探明 / 预览用的那次调用(原文) |
87
125
 
88
126
  `design render` 把它写进 README 的 **`### 写通道探明记录`** 那一节,位置在「数据源探明记录」与「验收」之间
89
127
  (一条一张表;多条渲染成 `#### 写 N`)。`dsh-wb-module design check` 会把空着的格点名叫出来。
90
128
  **没有写通道的模块不用写** —— 它不判"一条都没有",`check` 不红。
129
+
130
+ `undo` 是 2026-10-19 加的(照 opencli 的 `write-without-delete-pair`):**旧模块**跑 `design check`
131
+ 会多出一格 `write.undo` 的空格 —— 补一句"没有配对,一次就是真的"(或真配对那条命令)就过,别的不用动。