@dsh-workbench/core 0.3.3 → 0.3.5

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 (71) hide show
  1. package/client.js +1749 -114
  2. package/cordis.patch.yml +32 -0
  3. package/dist/client/client.body.js +1749 -114
  4. package/dist/client/index.js +1749 -114
  5. package/dist/host/index.js +3101 -1156
  6. package/dist/types/app/client.d.ts +306 -60
  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-context.d.ts +4 -5
  14. package/dist/types/modules/panels/model/diy-observers.d.ts +2 -2
  15. package/dist/types/modules/panels/model/diy-page.d.ts +1 -1
  16. package/dist/types/modules/panels/model/diy-record-bar.test.d.ts +1 -0
  17. package/dist/types/modules/panels/model/diy-record-capture.d.ts +27 -0
  18. package/dist/types/modules/panels/model/diy-record-capture.test.d.ts +1 -0
  19. package/dist/types/modules/panels/model/diy-record-failure.test.d.ts +1 -0
  20. package/dist/types/modules/panels/model/diy-record-routes.d.ts +2 -0
  21. package/dist/types/modules/panels/model/diy-record-routes.test.d.ts +1 -0
  22. package/dist/types/modules/panels/model/diy-record-scripts.d.ts +6 -0
  23. package/dist/types/modules/panels/model/diy-record-text.d.ts +89 -0
  24. package/dist/types/modules/panels/model/diy-record-text.test.d.ts +1 -0
  25. package/dist/types/modules/panels/model/diy-record-tools.d.ts +2 -0
  26. package/dist/types/modules/panels/model/diy-record.d.ts +76 -0
  27. package/dist/types/modules/panels/model/diy-record.test.d.ts +1 -0
  28. package/dist/types/modules/panels/model/handoff.d.ts +1 -0
  29. package/dist/types/modules/panels/unit.d.ts +10 -5
  30. package/dist/types/shared/api/context-chips.d.ts +20 -0
  31. package/dist/types/shared/api/host/diy-acts.d.ts +4 -0
  32. package/dist/types/shared/api/host/diy-components.d.ts +65 -0
  33. package/dist/types/shared/api/host/diy-debug-act.d.ts +24 -7
  34. package/dist/types/shared/api/host/diy-record-act.d.ts +19 -0
  35. package/dist/types/shared/api/host/diy-workshop.d.ts +1 -3
  36. package/dist/types/shared/api/host/mount-units.d.ts +4 -0
  37. package/dist/types/shared/api/host/preset-scope.d.ts +16 -0
  38. package/dist/types/shared/api/host/tool-scope.d.ts +6 -2
  39. package/dist/types/shared/api/host/workshop-root.d.ts +7 -0
  40. package/dist/types/shared/components/BlockerState.d.ts +12 -0
  41. package/dist/types/shared/components/PanelFoot.d.ts +9 -0
  42. package/dist/types/shared/components/Tip.d.ts +8 -0
  43. package/dist/types/shared/hooks/bundle-queries.d.ts +12 -0
  44. package/dist/types/shared/hooks/fact-transports.d.ts +4 -0
  45. package/dist/types/shared/hooks/panel-state-report.d.ts +12 -0
  46. package/dist/types/shared/hooks/use-api-resource.d.ts +22 -0
  47. package/dist/types/shared/hooks/use-blocker-actions.d.ts +8 -0
  48. package/dist/types/shared/hooks/use-capabilities.d.ts +55 -0
  49. package/dist/types/shared/hooks/use-capability-gate.d.ts +16 -0
  50. package/dist/types/shared/hooks/use-card-summary.d.ts +7 -0
  51. package/dist/types/shared/hooks/use-diy-debug.d.ts +13 -0
  52. package/dist/types/shared/hooks/use-diy-record.d.ts +21 -0
  53. package/dist/types/shared/hooks/use-facts.d.ts +10 -0
  54. package/dist/types/shared/hooks/use-host-write.d.ts +12 -0
  55. package/dist/types/shared/hooks/use-long-task.d.ts +8 -0
  56. package/dist/types/shared/hooks/use-unit-board.d.ts +17 -0
  57. package/dist/types/shared/hooks/use-visible-busy.d.ts +17 -0
  58. package/package.json +2 -2
  59. package/scripts/seed-workshop.sh +42 -2
  60. package/skills/workbench-module-diy/BUILD.md +4 -3
  61. package/skills/workbench-module-diy/DATASOURCE.md +2 -2
  62. package/skills/workbench-module-diy/DESIGN.md +26 -1
  63. package/skills/workbench-module-diy/FAMILY.md +56 -0
  64. package/skills/workbench-module-diy/KIT.md +111 -96
  65. package/skills/workbench-module-diy/PANEL.md +24 -13
  66. package/skills/workbench-module-diy/REVIEW.md +9 -8
  67. package/skills/workbench-module-diy/SKILL.md +16 -6
  68. package/skills/workbench-module-diy/TOOLING.md +5 -2
  69. package/skills/workbench-module-diy/TOOLS.md +7 -41
  70. package/skills/workbench-module-diy/WIRING.md +14 -6
  71. package/skills/workbench-module-diy/WRITES.md +47 -6
@@ -42,7 +42,7 @@
42
42
  - 想长期用 → 给这个站点写 adapter:`opencli skills read opencli-adapter-author`(侦察 → 字段解码 →
43
43
  写 → verify);以后 adapter 失效:`opencli skills read opencli-autofix`。
44
44
 
45
- **④ 才轮到探** —— 用工具:`workbench_diy_probe`(`url=<那套系统的地址>, script=<一段回 JSON 的脚本>`;
45
+ **④ 才轮到探** —— 用工具:`workbench_diy_eval`(`read=<那套系统的地址>, args=["<一段回 JSON 的脚本>"]`;
46
46
  页面里没有接口就给 `css=<选择器>`,走的是同一条固定 DOM 读,`{entries:[{text}]}` 直接回给你)。
47
47
  它用**用户浏览器里现有的登录**去读,并把"读不到"归一成阻断屏认的那几个码 —— 两拍(不在站点上就自己
48
48
  打开 `url` 再读一遍)、刚重启时的重试、平台码到阻断码的映射**都在工具里**,不用你记。
@@ -72,7 +72,7 @@
72
72
  想给自己一条专线(不跟别的模块抢同一个 tab)就传自己的名字:`session: "<unit>"`。
73
73
  - **页面里没有接口时(DOM 读)不要用 `eval` 去 `document.querySelectorAll`**:产物边界判据会拦
74
74
  (宿主产物里不许出现 `document.` / `window.` 字样),而且那样解析只能窝在字符串里、测不了。
75
- 探的时候 `workbench_diy_probe`(`url=…, css=<选择器>`)就是这个口子;
75
+ 探的时候 `workbench_diy_find`(`read=…, args=["--css", "<选择器>"]`)就是这个口子;
76
76
  固化时走 `browserFind`,它内部用的就是 `opencli browser <session> find --css '<选择器>' --limit N --text-max M`,
77
77
  回 `{ entries: [{ text, tag, nth }] }` —— **一行的单元格按换行拼在 `text` 里**(`美元\n668.3\n…`),
78
78
  所以"切格 + 按列序取值"是两个纯函数,能单测。很多公开站点一条 XHR 都不发,就该走这条。
@@ -75,13 +75,38 @@
75
75
  画不成的只有这几类,写词(并说清意思):纯动作("点一下去平台")、agent 的下一步、
76
76
  时间性的词("不变 / 自己变 / 立刻")、纯映射表(能力名 ↔ 用户会怎么说)。
77
77
 
78
+ ### 审批屏:写工具族必答,而且那两格是固定的
79
+
80
+ 有"要用户点头"的写工具,就把**审批屏**也画进图里。它的形状不是自由的(仓内 ADR-0065,这份指引不链仓内路径):
81
+
82
+ ```
83
+ ┌─ card(dsh 的壳)─────────────────────────────────────────┐
84
+ │ ● 等待审批 │ ← dsh 画的
85
+ ├───────────────────────────────────────────────────────────┤
86
+ │ <标题:一句,≤40 字,…,允许吗?> │ ← 你写的 question / fallback
87
+ │ <详情:ApprovalPlan 的两列表 + warn> │ ← 你写的 approval.render
88
+ ├───────────────────────────────────────────────────────────┤
89
+ │ [ 拒绝 ] [ 允许一次 ] │
90
+ └───────────────────────────────────────────────────────────┘
91
+ ```
92
+
93
+ 四条口径(同一份:[KIT.md](KIT.md) §3):
94
+
95
+ 1. **标题只有一句**:`…,允许吗?` 收尾并点名"工作台"(工作台自己的动作可用 `申请…` 陈述句式);
96
+ 装配后 ≤108 字(含数据)。
97
+ 2. **事实放详情**,用外壳的 `ApprovalPlan`(`approval: { tools, render }`):等宽、能换行、能滚,
98
+ 长 URL / diff / 多字段都住这儿。
99
+ 3. **别把事实塞进标题**:宿主那句 `approvalDetail`(明细)只留给**没有详情表**的族,≤20 字一行 ——
100
+ 你既然画了详情表,它就是空的。
101
+ 4. **附注(`approval_note`)不是你的**:只有 opencli 插件命令那族才必填;模块定义的工具默认没有那一格。
102
+
78
103
  ## 4 做完的判据
79
104
 
80
105
  - 全程**用户只说过需求相关的话**:没有一次被问"用什么、放在哪、叫什么、怎么测"。
81
106
  - 三张表没有空格(`dsh-wb-module design check` 绿);表里每一格,都在真卡片上见过或点过,
82
107
  验收清单逐条打上了勾(`design accept`)。
83
108
  - **验收归谁按域分**:**DIY 域(工坊会话)的验收是 agent 自己走的** —— 调试族
84
- (`workbench_diy_debug_start` + 浏览器那 14 把枚举工具 + `logs_*`,全表见 [TOOLS.md](TOOLS.md) §13)
109
+ (`workbench_diy_debug_start` + 浏览器那 14 把枚举工具 + `logs_*`,全表见 [FAMILY.md](FAMILY.md))
85
110
  就是取证面;用户不必在场、也不必逐条点头,走完把"装好了、面板上哪一格"告诉他即可。
86
111
  **工程域没有那一族**,仍按「打开 :3081 → 右栏第 N 张卡 → 点 X → 应该看到 Y」请他看一次。
87
112
  - 要用户点头的**写能力(审批)那一拍仍是他的**:他真用一次时点,agent 代不了 ——
@@ -0,0 +1,56 @@
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`/`_spec`/`_install`/`_show` 不碰他那一屏,不用开;`_find`/`_eval` 这些带 `read=` 时也**不碰**(走只读线,见 §2 末)。**写那一族(录制交接,见 §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)· `read?: string`(只读线档,见本节末)。
20
+ - `scope: 'card'` 只给吃 CSS 的**七把**:`_find`(`--css`)· `_html`(`--selector`,没给它我们补)· `_click`/`_hover`/`_type`/`_fill`(**位置参数 0**)· `_wait`(**只在 `wait selector`**,位置参数 1)—— `card` = 给那个目标挂 `#wb-card-<unit> .wb-card `。
21
+ - **只读线档**:任何一把带上 `read: <站点地址>` 就跑在**另一条会话** `workbench-read` 上 —— **不要调试约定、不要板在屏上、不认卡作用域**(`unit` / `scope: 'card'` / `in: 'card'` / `$card` 逐个当场拒、零命令),也**不要求会话工作区里有模块包**(它一行都没碰工作区);结果与屏那一档**同一组字段**(`ok`/`exitCode`/`timedOut`/`truncated`/`stdout`/`stderr` 原样),另带 `session` 与 `opened`。`opened` = 我替你开过一次站点:跑之前先 `get url` 比域名,不在那个站点上就 `open <read>` 一次再跑。`_eval` 上沿用读线约定:脚本回 `{ ok: false, code: "not-logged-in" }` 时输出后补一句「去 <read> 登一次」(只补说明,结果字段一个不改)。**这一档取代了老的 `_probe`**(2026-10-20;来龙去脉在本仓 `docs/adr/0062` §6 与 `.scratch/diy-read-line/spec.md` —— 那两份不随包出货,只给改外壳的人):老 `_probe` 那层结构化 `entries` 没有替代 —— `_find` 的 stdout 自己解析。
22
+ - `in: 'card'` 只 `_eval`(注入卡根 `root`,**脚本自己 `return`**);`clip`(CSS px)只 `_screenshot`(拍完本地裁);`$card` **任何 `args` 里**都替换成 `#wb-card-<unit> .wb-card`。
23
+
24
+ **拿回**:`{ ok, exitCode, timedOut, truncated, stdout, stderr }` + `workshop`/`unit`/`verb`/`argv`(`read=` 那一档改成 `session`/`opened`:只读线不碰工作区,所以没有 `workshop`/`unit`);`_screenshot` 另给 `file`/`cropped`/`cropReason`。`ok` 只管进程,事实全在 `stdout`(报错也是一份 `{"error":{…}}`,`ok` 可能仍 `true`);不抠 JSON / 不翻中文 / 不做 ok 归一,**要参数先问 `--help`**。**"原样"有折损**:`args` 里空串 / 纯空白 / 非字符串丢掉、留下的词前后 trim、`stdout`/`stderr` 也 trim ⇒ **`_fill` 清空不了输入框**、**`_type` 敲不了空格**,没有别的口子就如实说做不到。
25
+
26
+ ## 3 老工具 → 新命令
27
+
28
+ | 老 | 新 |
29
+ |---|---|
30
+ | `_look` | `state` + `get html` + `eval` + `screenshot`(卡图靠 `clip`,上游没有 selector) |
31
+ | `_reload` / `_viewport` | `eval 'location.reload()'` / `screenshot --width/--height` |
32
+ | `_select` | **没了**(上游只认原生 `<select>`);下拉两拍:卡内点触发器 → `scope: 'page'` + `--role menuitem` 按名字点 |
33
+ | `_net_*` | `network`(`--follow` 臂上、收工 `_debug_stop` 顺手收;**取 body 走 `_eval` 的 `fetch`**) |
34
+ | `_click` `_hover` `_type` `_fill` `_keys` `_scroll` / `_logs_*` | 同名,参数改成原样转发(`console` 是空桩,控制台只能我们做) |
35
+
36
+ ## 4 如实边界
37
+
38
+ - `screenshot` **没有上游 selector**:卡图先 `eval` 量盒子再填 `clip`;不给路径我们补到工坊同级的 `shots/<unit|page>-<时间戳>.png`(否则回 base64);要裁的块不在视口里就退回整屏图,原因在 `cropReason`。
39
+ - `state` **没有 `scope`**(卡内定位用 `find --css '$card …'`);**没有右键 / contextmenu**;ref 只在**一次快照**里有效,别跨调用存;`_dialog` 的 `accept|dismiss` 不校验;`_network --detail` **取不到 body**(缓存落在会话可写根之外)—— 走 `_eval` 里的 `fetch`。
40
+ - 录制交接那条**会话捕获**(`_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 条、那条写还在。
41
+ - 另外 19 把:台账 `_list`/`_status`/`_install`/`_show`、组件与 kit 清单 `_components`(不填给清单与类型面根,`name=Button` 回它的类型文件与声明)、宿主运行态 `_panel`/`_status`、数据源 `_recon`、`_spec`(问一条 opencli 命令的形状,**插件也适用**)、`_logs_*`、约定本身 `_debug_*`、录制交接 `_record_*`(§5)。
42
+
43
+ ## 5 录制交接(四把):抓那一次"在外站页面上点出来的写",清单 → 挑一条 → 看原文
44
+
45
+ | 工具 | 干什么 |
46
+ |---|---|
47
+ | `_record_prepare` | 开一扇窗**顶到最前**(**要用户点头一次**)、装浮条、臂上捕获。参数 `unit` / `url` / `doing`,可选 `expect:{method,urlContains}`;**不给 `url` 当场拒**,**跨域也当场拒**(那个模块的 `design.json` 说过读源时,只许同域录;还没说过就不拦)。返回带 `wrote: false` |
48
+ | `_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` |
49
+ | `_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 = 不限) |
50
+ | `_record_stop` | 停捕获、收掉那个标签、清状态(**收工前先读一次捕获文件**,与 read 同一套切段 / 合并;**原文另存成 `.last`**,所以收工之后还能 `_record_detail`;没挪成就不说"原文留在…")。回一段能直接落 `design.json` 的原文(`probeCommand`)—— **只认第一条带 body 的**;一条都没有就回空串,并如实说"只有方法 / URL,body 拿不到 —— 补到 body 再落"(没 body 的写命令落下去是一条**会打错的**命令)。**先 read(或先 detail 完)再 stop** |
51
+
52
+ 左栏底部那一格给他的键(等出手 `[取消]` / 录制中 `[停]`)只**给 agent 发一条消息**,不直接改页面那一半(`[停]` 有意不动状态);**录完那一档没有键** —— agent 自己来取、自己收工。
53
+ 一场过期(TTL 15 分钟)或台账被换走,**不会把采集循环留在地上**:看门那一拍看到台账没了就停捕获 + 收标签,
54
+ 循环本身也被宿主 pid 兜着(宿主重起后孤儿循环自己退);`prepare` 开新一场前也先对同一落点收一次。
55
+ 细则、三颗键与如实边界(`fetch`/XHR 之外抓不到、50 条 / 16 KB 上限、一次一场 15 分钟)
56
+ 都在 [WRITES.md](WRITES.md) 步骤 3;纪律同那一份的「三条写纪律」。
@@ -1,7 +1,7 @@
1
1
  # kit 速查:要做什么 → 用哪一件 → 最小写法
2
2
 
3
- `dsh-wb-module check --only kit` 列的是**名字**,这一份给的是**用法**。两处都别看漏:
4
- 名字那份是这台机器上**真有的**(别照抄别的模块),这一份是**怎么拼**。
3
+ `check --only kit` 列**名字**(这台机器上真有的,别照抄别的模块),**逐件用法在 `.d.ts` 的 JSDoc 里**
4
+ (`workbench_diy_components name=<名字>` 一回就是它)。这一份给的是**跨件**的:怎么拼、纪律、排布。
5
5
 
6
6
  **能拿 kit 的就别自己画。** 理由只有一条:主题、语言、悬停、禁用、焦点环、空态、被裁的处理
7
7
  都只维护一份。自己画的不会跟着换主题,也不会跟着我们改口径 —— 这就是"同一个东西长得不一样"的来源。
@@ -14,17 +14,12 @@
14
14
  **`Card`** —— 一张卡的壳:标题 + 刷新钮 + 卡片体。面板上每一格都是它。
15
15
 
16
16
  ```tsx
17
- h(Card, {
18
- t: props.t,
19
- title: t('myunit.cardTitle'),
20
- refreshing: resource.phase === 'loading',
21
- onRefresh: props.onRefresh,
22
- }, body)
17
+ h(Card, { t: props.t, title: t('myunit.cardTitle'),
18
+ refreshing: resource.phase === 'loading', onRefresh: props.onRefresh }, body)
23
19
  ```
24
20
 
25
- **`CardHead`** —— 你自己拼卡头时用它(`Card` 内部就是它):`t` / `title` / `meta` / `refreshing` /
26
- `onRefresh` / `gear` / `onToggleGear`(右上那颗齿轮展开设置面)/ **`platformHref`**(跳整张卡对应的
27
- 那个系统:只给这一颗就够,那颗 ↗ 立刻画出来)。
21
+ **`CardHead`** —— 你自己拼卡头时用它(`Card` 内部就是它)。prop 见 `.d.ts`;
22
+ `platformHref` 只给这一颗就够,那颗 ↗ 立刻画出来。
28
23
 
29
24
  **`CardAnchor`** —— 外壳自己盖的锚点(`#wb-card-<unit>`)。**别自己写**,也别删。
30
25
 
@@ -32,8 +27,7 @@ h(Card, {
32
27
 
33
28
  ## 2 状态:这是下限最集中的地方
34
29
 
35
- 先想清五态(`design.json` 的状态矩阵就是这五格):**待机 / 在读 / 就绪 / 就绪但空 / 读不到**。
36
- 每一态都要有东西画,一个不落。
30
+ 先想清五态(`design.json` 的状态矩阵就是这五格):**待机 / 在读 / 就绪 / 就绪但空 / 读不到** —— 每一态都要有东西画。
37
31
 
38
32
  | 什么时候 | 用 |
39
33
  |---|---|
@@ -43,6 +37,8 @@ h(Card, {
43
37
  | 在读、且你已经有大概形状 | `SkeletonRows`(骨架) |
44
38
  | 一句说明 / 一句报错 | `Note`(`tone: 'problem'` 是报错) |
45
39
 
40
+ prop 与默认值看 `.d.ts`;下面是**怎么拼**。
41
+
46
42
  ```tsx
47
43
  h(CenteredState, { icon: null, title: t('myunit.loading'), hint: t('myunit.loadingHint'), compact: true })
48
44
  h(CenteredState, { icon: null, title: t('myunit.errorTitle'), detail: `${code}: ${message}`, hint: t('myunit.errorHint'), compact: true })
@@ -51,7 +47,7 @@ h(StateBlock, { title: rows.title, hint: `3 条 · ${when}`, resolving: t('myuni
51
47
  h(Note, { children: t('myunit.hint') })
52
48
  ```
53
49
 
54
- **读不到就说读不到**:`detail` 里放原始错误码与那句话,**绝不显示 0**(0 是"真的是 0",不是"没读到")。
50
+ **读不到就说读不到**:`detail` 里放原始错误码与那句话,**绝不显示 0**(0 是"真的是 0")。
55
51
 
56
52
  ## 3 一行一行
57
53
 
@@ -63,127 +59,146 @@ h(Rows, null,
63
59
  ...items.map((one) => h(Row, { key: one.name, label: one.name, value: `${size} · ${when}` })))
64
60
  ```
65
61
 
66
- **`ActivityBlock` / `ActivityLine` / `ActivityAction`** —— "它做过什么"那一块:
67
- `ActivityLine` 有 `dot` / `text` / `at`(时间)/ `actions`;`ActivityAction` 是行尾那个动作
68
- (`label` / `icon` / `href` 或 `onClick` / `failed`)。时间一律用它,别自己拼。
62
+ **`ActivityBlock` / `ActivityLine` / `ActivityAction`** —— "它做过什么"那一块(prop 见 `.d.ts`)。时间一律用它。
69
63
 
70
- **`LongTaskRow`** —— 长任务那一行:`status`(四态)+ `headline` + `problem` + `links` + `steps` +
71
- `raw`。跑得久的事(发布、扫描、迁移)都用它,别自己画进度。
64
+ **`LongTaskRow`** —— 长任务那一行(四态 + 步骤 + 原文,prop 见 `.d.ts`);
65
+ 跑得久的事(发布、扫描、迁移)都用它,别自己画进度。
72
66
 
73
- **`ApprovalDetail` / `ApprovalPlan`** —— 审批屏上那两格(要用户点头的写操作)。
67
+ **`ApprovalDetail` / `ApprovalPlan`** —— 审批屏那两格(写操作才要点头);口径与那张 ASCII 见 [DESIGN.md](DESIGN.md) §3 末节。
74
68
 
75
69
  ## 4 表单
76
70
 
77
- **`Field`** —— 标签 + 输入的排布(`label` / `hint`),输入框放里面。
71
+ **`Form`** —— **整张表单**(面那一层):`fields` 配置进,行怎么排、错误摆哪、什么时候校验、提交怎么被挡住、
72
+ 第一处错误怎么带进视口、操作区怎么排全归它。**一张完整的表单就用它**(口径见 §5,逐件看 `.d.ts`)。
78
73
 
79
74
  ```tsx
80
- h(Field, { label: t('myunit.mode'), hint: t('myunit.modeHint') },
81
- h(TextField, { value, onChange: (event) => setMode(event.target.value) }))
75
+ h(Form, {
76
+ fields: [
77
+ { key: 'branch', label: t('myunit.branch'), value: branch, // 值只读给校验看,读写仍归你
78
+ validate: (v) => (v === '' ? t('myunit.branchRequired') : undefined),
79
+ control: (b) => h(TextField, { id: b.id, name: b.name, invalid: b.invalid,
80
+ disabled: b.disabled, value: branch, onChange: (e) => setBranch(e.currentTarget.value) }) },
81
+ ],
82
+ submit: { label: t('myunit.save'), busyLabel: t('myunit.saving') },
83
+ cancel: { label: t('myunit.cancel'), onClick: onCancel }, // 排在提交钮左边
84
+ extra: [{ key: 'retry', label: t('myunit.retry'), onClick: onRetry }], // 其余动作排最左
85
+ onSubmit: save, // 有错就不调它
86
+ })
82
87
  ```
83
88
 
84
- **`TextField`** —— 单行输入(`invalid` 标红;其余是原生 input 的 props)。
85
- **`ChoiceGroup`** —— 单选 / 多选(`multi` 区分;`value` 或 `values` + `onChange`;选项 `{ value, label, description? }`;`disabled` 在整组上)。
86
- **`SelectField`** —— 下拉:默认是**常规下拉框**(跟 `TextField` 一套),无边框那种给 `variant: 'ghost'`;
87
- 搜索、分组、禁用项、加载态、空态、页脚都是它的 prop。
88
- (它是 dsh 那套按钮 + portal 菜单,**不是原生 `<select>`** —— 验收时 agent 要驱动它得走两拍:
89
- 卡内点触发器 → `scope: page` + `--role menuitem` 按名字点;`workbench_diy_select` 那把工具已经不在了。)
90
- **`SaveBar`** —— 设置面底下那条"取消 / 保存"(`busy` / `disabled` / `labels` / `onCancel` / `onSave`)。
91
- **`FormActions`** —— **操作区**(一组按钮):`actions: { key, label, kind?: 'default' | 'danger' | 'primary', onClick, disabled?, busy? }[]`。
89
+ **值仍归调用点**(`Form` 一个值都不存):`binding` 只递 `id` / `name` / `invalid` / `disabled`,
90
+ `value` 与 `onChange` 由 `control` 里自己接。**控件永远由 `control` 给** —— 没有"给个 `options` 自己选控件"的 DSL。
91
+
92
+ **控件那几件** —— `Field`(没进 `FormRow` 时兜住标签 + 说明)· `TextField`(`invalid` 标红)·
93
+ `ChoiceGroup`(`multi` 区分多选)· `SelectField`(默认常规下拉框,`variant: 'ghost'` 是无边框那档,
94
+ **不是原生 `<select>`**)· `SaveBar`(设置面底下那条"取消 / 保存"):都是只给数据与语义 prop 的哑组件,
95
+ **逐件看 `.d.ts`**。
96
+
97
+ **`FormActions`** —— **操作区**(一组按钮,形状见 `.d.ts`;**整张表单里由 `Form` 组**)。
92
98
  **顺序 / 间距 / 换行都归它** —— `primary` 永远在右、`danger` 紧挨它左侧、一个区只许一颗 `primary`、
93
- 窄了自动折行且 `primary` 落在右下角;`busy` 自带 `Spinner`。调用点只给"有哪些动作、哪个是主要、点了做什么":
94
- **别自己写 `gap` / `margin` / 顺序,也别借 `.wb-formActions` 槽类手摆**(`check --only style` 的 F3 会红)。
99
+ 窄了自动折行且 `primary` 落在右下角;`busy` 自带 `Spinner`。**别自己写 `gap` / `margin` / 顺序,
100
+ 也别借 `.wb-formActions` 槽类手摆**(F3 红)。`danger` 只标"破坏性"(**永远不当填充的 `primary`**);
101
+ **二次确认那半归调用点**。
95
102
 
96
- ```tsx
97
- h(FormActions, { actions: [
98
- { key: 'retry', label: t('myunit.retry'), onClick: onRetry, disabled: busy },
99
- { key: 'confirm', label: t('myunit.confirm'), kind: 'primary', onClick: submit, disabled: busy },
100
- ] })
101
- ```
103
+ **别手写 `<button>` / `<input>` / `<select>`**(dsh 有 `Button`,kit 有上面这几件;手写的判据会红)。
104
+ `<textarea>` 例外(dsh 与 kit 都没有多行输入)。
105
+
106
+ 怎么排成一行(标签位置、错误态、尺寸)见 §5。
107
+
108
+ ## 4′ hook 怎么搭(逐件看 `.d.ts`)
102
109
 
103
- `danger` 只标"这是破坏性的"(渲成描边 + 危险色,**永远不当填充的 `primary`**);**二次确认那半归调用点**
104
- (先问一句或弹窗都行)。
110
+ kit 的 hook 也分四个场合,**别自己 `useState` + `setTimeout` 造一套**(面板要同一套口径):
105
111
 
106
- **别手写 `<button>` / `<input>` / `<select>`**:dsh 有 `Button`(`variant` / `size`),
107
- kit 有上面这几件;手写的会拿到浏览器默认样式,而且**判据会红**(`check --only style`)。
108
- `<textarea>` 例外(dsh 与 kit 都没有多行输入)——真要用就直接写,其余形态别开这个口子。
112
+ | 场合 | 用它 |
113
+ |---|---|
114
+ | 卡片里的一次读(自带同 key 合流 / 事实订阅 / 上报面板) | `useApiResource`(非 hook 的本体是 `requestResource`,**用了就一定要 `release`**) |
115
+ | 写完让面板重取 / 听事实变化 | `useFactInvalidate` · `useFactRevision` · `subscribeFactRevision`(非 hook,记得退订) |
116
+ | "至少转一会儿"的忙态 | `useVisibleBusy`(投影那两件 `busyView` / `heldPhase`) |
117
+ | 一格**卡片**起步(gate + 忙 + 探针 + 上报 + blocker 一次叠好) | `useUnitBoard`(`read(gated)` 里自己调 `useApiResource`) |
118
+ | 只要一格的能力门 / 整张能力表 | `useCapabilityGate` / `useCapabilityMap` |
119
+ | 写(**别在组件里 `fetch`**) / 轮询长任务 | `useHostWrite` / `useLongTask`(画用 `LongTaskRow`、点用 `longTaskDot`) |
120
+
121
+ 取数那条链的图见 [LINKAGE.md](LINKAGE.md) §8,写通道与配置见 [PANEL.md](PANEL.md) §5。
109
122
 
110
- 控件怎么排成一行(标签位置、错误态、尺寸)见 §5。
123
+ ## 5 表单这一层:`Form`(面)→ 行 → 控件
111
124
 
112
- ## 5 表单这一层:面 → 行 → 控件
125
+ 表单三层:**面**(`Form`)→ **行**(`FormRows` + `FormRow`)→ **控件**(`TextField` / `SelectField` /
126
+ `ChoiceGroup`)。**一张表单一个面**。
113
127
 
114
- 表单三层:**面**(`SettingsSection` / `SaveBar`)→ **行**(`FormRows` + `FormRow`)→
115
- **控件**(`TextField` / `SelectField` / `ChoiceGroup`)。**一行一个字段**,行这一层只有一个出处。
128
+ **整张表单用 `Form`**(写法见 §4):`fields` 配置进;**值仍归调用点**(`value` / `onChange` 由 `control`
129
+ 里自己接,`binding` 只递 `id` / `name` / `invalid` / `disabled`);**控件永远由 `control` 给**(没有"给个
130
+ `options` 自己选控件"的 DSL);`validate` 是同步纯函数;`visible: false` 的那格当它不存在(不渲染 /
131
+ 不校验 / 不进 `values`)。**别再自己摆操作区** —— 它自己组 `[...extra, ...cancel, submit(primary)]`
132
+ (提交钮永远在右;「一会在左一会在右」就是自己摆的结果)。其余口径(时机 / 第一处错误 / `busy` /
133
+ `disabled` / `key`)看它的 JSDoc 与 ADR-0068。
134
+
135
+ `FormRows` + `FormRow` + `FormActions` 留给**不是整张表单**的地方:单行(一格字段嵌在别处,比如卡片里
136
+ 那一条设定)、非表单的一组动作(工具栏、卡头那几颗钮)。那里才自己拼这三件:
116
137
 
117
138
  ```tsx
118
139
  h(FormRows, null,
119
140
  h(FormRow, { label: t('myunit.mode'), hint: t('myunit.modeHint') },
120
- h(SelectField, { value, options, onChange: setMode })),
121
- h(FormRow, { label: t('myunit.env'), problem: problemOfEnv },
122
- h(ChoiceGroup, { label: t('myunit.env'), name: 'env', options, value: env, onChange: setEnv })))
141
+ h(SelectField, { value, options, onChange: setMode })))
123
142
  ```
124
143
 
125
- - **标签归 `FormRow`**(`label`):默认标签在左,`layout: 'stacked'` 才是上下堆叠(`Field` 那种排布)。
126
- **控件只给数据与语义 prop**,不写外观类名(§7 第 1 条)。进了 `FormRow` 就别再给控件自己的
127
- `label` / `hint`(`SelectField` 那两件是没 `FormRow` 时的兜底,重复给会多一个标签、还退回上下堆叠)。
128
- - **`hint` 与 `problem` 二选一**:给了 `problem` 就不画 `hint` —— 同一格不叠两段话,有错先显示错。
144
+ - **标签归 `FormRow`**(默认在左,`layout: 'stacked'` 才上下堆叠);**控件只给数据与语义 prop**,
145
+ 进了 `FormRow` 就别再给控件自己的 `label` / `hint`(会多一个标签、还退回上下堆叠)。
146
+ - **`hint` 与 `problem` 二选一**:有错先显示错。
129
147
 
130
- **错误态**:位置与长相由 `FormRow.problem` + 控件的 `invalid` 定(`SelectField` / `TextField` 同形,
131
- `ChoiceGroup` 见它自己的档)。**校验逻辑留在模块**:只写"算错误"的纯函数与文案
132
- (例:cmdb 的 `CmdbFormProblem` 那类),**别指望外壳给校验引擎**(不做 `rules` / `validate`、
133
- 不做联动、不持有值)。提交失败后调 `kit.lib.revealFirstProblem(root)`(`root` = 表单容器)——
134
- 把第一处错误带进视口、并聚焦它里面的控件。
148
+ **错误态**:位置与长相归 `FormRow.problem` + 控件的 `invalid`;**算错的逻辑留在模块**(纯函数 + 文案,
149
+ 交给 `FormField.validate`)—— **别指望外壳给校验引擎**(不做 `rules` / 异步 / 联动、不持有值),
150
+ **"提交过没有"与聚焦第一处错也归 `Form`**(别自己记布尔、别自己调 `revealFirstProblem`)。
135
151
 
136
- **选择类用哪件**:候选多 / 要搜索 / 要分组 / 名单可能读不到 ⇒ `SelectField`(`search` 默认开、
137
- `emptyText`、`loading` + `loadingText`、`fallback`、`variant: 'boxed' | 'ghost'`);
138
- 候选少、要一眼看全 / 多选 ⇒ `ChoiceGroup`(`multi`、`variant`、`loading` + `loadingText`、
139
- `emptyText`、`options[].description`)。两件的 `loading` / `loadingText` / `emptyText` / `variant`
140
- 是一个意思;**loading 那颗一律是 `Spinner`**(`h(Spinner, { size: 14 })`)—— **别自己写转圈**。
152
+ **选择类用哪件**:候选多 / 要搜索 / 要分组 / 名单可能读不到 ⇒ `SelectField`;候选少、要一眼看全 / 多选 ⇒
153
+ `ChoiceGroup`(`loading` / `emptyText` / `variant` 两件一个意思)。**loading 那颗一律是 `Spinner`**。
141
154
 
142
- **尺寸口径**:字段控件默认 **28**(dsh 紧凑档),**别自己调高**;行距**只有一档**、归 `FormRow(s)`,
143
- 模块里不该出现管行距的 `gap` / `margin`(横向的、卡片/清单网格的不算)。标签列也归它:
144
- **长标签不折行;同一 `FormRows` 共用一个标签列(列宽由最长的标签定),别自己给标签写宽度**
145
- (非行的子项,比如分组标题 `<p>`,自动通栏)。
155
+ **尺寸口径**:字段控件默认 **28**,别自己调高;行距只有一档、标签列宽也归 `FormRow(s)`
156
+ (长标签不折行、同一组共用一个标签列,别自己给标签写宽度)。非行的子项(如分组标题)自动通栏。
146
157
 
147
158
  **反例(别这么写)**:
148
159
 
149
160
  ```tsx
150
- h('span', { className: styles.label }, t('myunit.mode')) // ← 紧邻控件的手写标签行:`check --only style` 红
151
- h('div', { className: styles.form }, …) // ← .form { gap: 8px }:自己管行距也红
161
+ h('span', { className: styles.label }, …) // ← 手写标签行:F1 红
162
+ h('div', { className: styles.form }, …) // ← 自己管行距(.form { gap }):F2 红
163
+ h('form', …) // ← 裸 <form> / 手写行槽类:F4 红(表单归 Form)
152
164
  ```
153
165
 
154
- `<span className={styles.label}>` 紧邻控件、或给表单容器自己写 `gap` —— 这正是 `check --only style` 那条
155
- 棘轮(F1 手写行 / F2 自己管行距)盯的东西:这么写直接红。
156
-
157
- **时点**:这一层随外壳**发版**后模块才能用 —— 模块的 `peerDependencies` 下限得先有它
158
- (`check --only kit` 拿 peer 下限那份外壳比,不抬必红)。包下限是 `^0.2.14`。
166
+ **时点**:这一层随外壳发版后模块才能用(`check --only kit` 拿 peer 下限那份外壳比,不抬必红)。
167
+ **`Form` 是新成员**:要用它得等外壳**发一版含它的版本**,再把 `@dsh-workbench/core` 的 peer
168
+ 下限抬到**那一版**(版本号随发版填,别照抄别的件);`FormRow(s)` / `FormActions` 的下限是另一件事。
159
169
 
160
170
  ## 6 别的
161
171
 
162
- - **`Link`** —— **唯一**的外链写法:`h(Link, { href, children })`。自带新窗口 + `rel="noopener noreferrer"`
163
- + 正文色 + hover 下划线 + 那颗小箭头。**别去掉箭头、别在文案里自带 `→`、别手写 `<a>`**(判据红)。
164
- - **`SettingsSection`** —— 一屏设置里的一节(`label` / `title` / `action`)。
165
- - **`Stack`** —— 竖排 + 统一间距(懒得分组时的默认容器)。
166
- - **`BlockerState`** —— 阻断屏:要登录 / 没权限 / 能力没装好那一类(`blocker` / `t` / `onRetry` / `onOpenMarket`)。
167
- - **`Tip`** —— Tooltip:图标钮与只有图的地方**必须**有它(或 `aria-label`)。
168
- - **真机上造不出来的那几态**:**拿你自己那份 `plugin.test.ts` 把五态各喂一遍**就够;
169
- `check --only cards` 还会拿**七种外壳侧输入**各喂一遍(它只问"退化得干不干净",
170
- 不断言"画的是哪一态那一屏")。
171
- - **图标** —— `IconPanelScreen`(面板/卡片那类)、`IconModuleMark`(我们的模块类型标)、
172
- `IconNpm` / `IconOpencli`(包类型);其余图形一律从 dsh 的 `Icon*Regular` 拿,**别自己画图标**。
172
+ - **`Link`** —— **唯一**的外链写法(`h(Link, { href, children })`):**别去掉箭头、别在文案里自带 `→`、
173
+ 别手写 `<a>`**(判据红)。
174
+ - **`SettingsSection` / `Stack` / `BlockerState` / `Tip`** —— 一屏设置里的一节 / 竖排间距 / 阻断屏 /
175
+ Tooltip(图标钮与只有图的地方**必须**有它或 `aria-label`);prop 看 `.d.ts`。
176
+ - **真机上造不出来的那几态**:拿你自己那份 `plugin.test.ts` 把五态各喂一遍(`check --only cards`
177
+ 拿七种外壳侧输入各喂一遍,只问"退化得干不干净")。
178
+ - **图标** —— `IconPanelScreen` / `IconModuleMark` / `IconNpm` / `IconOpencli`;其余图形一律从 dsh 的
179
+ `Icon*Regular` 拿,**别自己画图标**。
173
180
 
174
181
  ## 7 三条纪律
175
182
 
176
- 1. **调用点只给数据与语义**:`h(Card, { title, refreshing, onRefresh }, …)`。别从外面塞外观类名
177
- —— 只有布局槽例外(通栏、外边距、网格位置)。
178
- 2. **字号与颜色走 kit 与令牌**(`--dsw-*` / `--wb-*`),不写字面值。**重点可以大**:
179
- 一个"余额 / 总数"那种要突出的数字,用刻度里最大那档 `--dsw-font-xl-24`(24/32、600),
180
- 数字加 `font-variant-numeric: tabular-nums`(见 [PANEL.md](PANEL.md) §6)。
181
- 刻度外非要一个字面值(比如更大的标题数)时,在模块 `README.md` 里写一句为什么。
183
+ 1. **调用点只给数据与语义**。别从外面塞外观类名 —— 只有布局槽例外(通栏、外边距、网格位置)。
184
+ 2. **字号与颜色走 kit 与令牌**(`--dsw-*` / `--wb-*`),不写字面值。**重点可以大**:要突出的数字用
185
+ `--dsw-font-xl-24`(24/32、600)加 `font-variant-numeric: tabular-nums`(见 [PANEL.md](PANEL.md) §6);
186
+ 刻度外非要字面值时,在模块 `README.md` 里写一句为什么。
182
187
  3. **自己写 CSS 只写布局**(flex / grid / gap / 对齐)。要一个新的"长得像控件"的东西时,
183
188
  先问一句:kit 里是不是已经有了?
184
189
 
185
190
  ## 8 kit 里没有、但你确定要标准控件
186
191
 
187
- 见 [PANEL.md](PANEL.md) §9:可以引 dsh 的客户端原语,但**只在浏览器侧**、并在
188
- `package.json` 的 `dsh.client.external` 里声明 `@deepseek-ai/dsh-client-ui-primitives`
189
- (宿主侧一个字都不许碰 —— `check` 的源码边界判据会红)。能拿 kit 的时候仍然别自己画。
192
+ **先问工具**:DIY 会话里调 `workbench_diy_components`(不填给清单与类型面根;`name=Button`
193
+ 回它的类型文件与声明)。工程域用 `dsh-wb-module check --only primitives`
194
+ (`--all` 全表 / `--json` 机器档)—— 分桶导出 + 与 kit 求交(`kit 里已有` 的优先用)+
195
+ 该写进 `package.json` 的那两行;骨架默认已备好。
196
+ **要看某一件**:`--file <名字>`(`--files | grep <名字>` 全表)。
197
+
198
+ ```jsonc
199
+ "dsh": { "client": { "external": [ "@dsh-workbench/core/client", "@deepseek-ai/dsh-client-ui-primitives" ] } },
200
+ "devDependencies": { "@deepseek-ai/dsh-client-ui-primitives": "<外壳钉的那版,精确版>" }
201
+ ```
202
+
203
+ 改完 `pnpm install` 一次;三条会红的边界(只在浏览器侧引 / 版本精确对齐 / 能拿 kit 的别引)与
204
+ 宿主侧的契约都见 [PANEL.md](PANEL.md) §9。
@@ -90,12 +90,11 @@ return {
90
90
  ```
91
91
 
92
92
  **浏览器半侧**:写走 `prefs` 通道,读还是模块自己的只读口(别为配置另开一条)。
93
- `Field`(标签 + 提示 + 输入框的排布)与 `wb-textField` 的内边距都是外壳给的默认,**模块不需要为"能看"写 CSS**:
93
+ 排版与内边距都是外壳给的默认(`Field` / `TextField`),**模块不需要为"能看"写 CSS**:
94
94
 
95
95
  ```tsx
96
96
  const write = useHostWrite({ channel: 'prefs', resource: 'your-module' });
97
- // Field + TextField / ChoiceGroup,外面套 SettingsSection,保存用 SaveBar;失败留在原地
98
- // (输入框的内边距和"标签 + 提示"的排布由外壳给默认,模块不用自己写 CSS)
97
+ // 控件用 kit 的(Field + TextField / ChoiceGroup),外面套 SettingsSection,保存用 SaveBar;失败留在原地
99
98
  write.send({ ignored, timeoutSeconds, workspace });
100
99
  ```
101
100
 
@@ -120,19 +119,28 @@ agent 那一侧也要能通过模块自己的能力读到同一份偏好。
120
119
  - **默认收起**:历史、明细折叠,露出最近一条;只有一条时不给折叠三角(没有可展开的东西)。
121
120
  - **屏幕上的字越少越好**:他能看懂的(红点绿点、数字)就不用再写一段规则解释;删掉的东西不要留一行说明。
122
121
  - **默认状态点**只有四种意思:在做 / 做完了 / 出错 / 没在跑。
123
- - **外链一律用 kit 的 `Link`**(`h(Link, { href: … }, 文案)`),而且**它自带的样子就是唯一一份**:
124
- 新窗口 + `rel="noopener noreferrer"` + 12px 正文色 + hover 下划线 + 自带那个外链小箭头。
125
- **别去掉箭头、别换箭头、别自己调**(没有 `icon` 这个口子)—— 全仓的链接必须长得一样。
122
+ - **外链一律用 kit 的 `Link`**(`h(Link, { href: … }, 文案)`):新窗口 + `rel` + 自带外链箭头,
123
+ **它自带的样子就是唯一一份,别去掉箭头、别换箭头、别自己调**(没有 `icon` 这个口子)——
124
+ 全仓的链接必须长得一样。
126
125
  模块源码里手写 `<a>` / `h('a', …)` 会被「样式归属」判据**直接判红**。
127
126
  **文案里也不许自带箭头**(`'去网关看账单 →'` 这种)—— 箭头由组件给,自带的话页面上就是两颗。
128
127
  它内部已经把 dsh 的图标原语包好了,所以模块**不用**为它声明 `client.external`。
129
- - **"去看某处"用哪个**:跳**整张卡对应的系统** → 卡头的图标按钮(`CardHead.platformHref`:只有图标 + Tooltip。
130
- **给这一个就够了**,那颗 ↗ 立刻就画;`platform` / `platformTitle` 是可选的,只决定 Tooltip 与
131
- `aria-label` 上的字 —— 少给一个**不会**少画那颗 ↗);
128
+ - **哪几种手写 `<a>` 不算 `Link`(判据不该咬、也别硬迁)**:判据钉的是**"手写 `<a>` 这件事本身"**
129
+ (`Link` 是唯一出处),下面三类**有意**留着,因为它们**不是"正文链接"**——
130
+ - **图标链**(icon-only 的 `<a>`,靠 `title` / `aria-label` 顶着):`Link` 一定会多画一颗 ↗,
131
+ 套进去就是两颗,尺寸与槽位它也表达不了 ⇒ 留 `<a>`。
132
+ - **按钮皮的链**(`.anchorButton` 那类 28 高 / 描边的"看起来像按钮、实际是链接"):**必须是链接的动作
133
+ 留真 `<a>`** —— `href` 带来的是中键新开、右键复制链接、悬停看 URL;塞成
134
+ `onClick: () => window.open(…)` 的按钮,这三件一起丢。
135
+ - **整行链**(一行整体可点,`flex: 1` / `color: inherit`):它是**那条行的布局槽**,不是正文里的字。
136
+ **要放宽这一条就走棘轮**:那张已知豁免表(`module-tools` 的 `style` 判据里,`ANCHOR_EXEMPT`)
137
+ **只许减不许加**(迁一处删一行);真要加一格,那是**改判据口径**,得写清"为什么 `Link` 表达不了它"。
138
+ 口径的权威那份是 `style.mjs` 里 `BARE_ANCHOR` 那一段注释(`ANCHOR_EXEMPT` 上面)
139
+ 与 `module-tools/README.md` 的 `style` 那一行。
140
+ - **"去看某处"用哪个**:跳**整张卡对应的系统** → 卡头那颗图标按钮(`CardHead.platformHref`,给这一个就够);
132
141
  跳**一个具体页面 / 一条具体记录** → `Link`(正文里的文字链接);**触发一个动作**(刷新、重建、去登录)→
133
142
  dsh 的 `Button`,或阻断屏里的 `action`。区别在"跳去哪"和"它是不是一个动作",不在好看不好看。
134
- - **标签 + 输入框的排布用 `Field`**,输入框用 `TextField` —— 内边距与排版是外壳给的默认,
135
- 别为"能看"写模块自己的 CSS(见 §5.1)。
143
+ - **标签 + 输入框用 `Field` + `TextField`**(排布与内边距是外壳的默认,别自己写 CSS,见 §5.1)。
136
144
  - **要做什么用哪件,照 [KIT.md](KIT.md) 抄**("要做什么 → 用哪件 → 最小写法"的表)。
137
145
  下面几条是口径,KIT.md 是写法。
138
146
  - **字号与重点**:走那把刻度(`--dsw-font-*`:11 / 12 / 13 / 14 / 16 / 20 / 24),
@@ -197,10 +205,13 @@ agent 那一侧也要能通过模块自己的能力读到同一份偏好。
197
205
  ## 9 界面边界:kit 不够用时怎么办
198
206
 
199
207
  **先从 kit 拿**(`@dsh-workbench/core/client`,`check --only kit` 会列全这份外壳有什么)。
200
- kit 里确实没有、而你要的又是标准控件(确认条、选择器、设置区块那种)时,可以引 dsh 的客户端原语:
208
+ kit 里确实没有、而你要的又是标准控件(按钮、选择器、设置区块那种)时,可以引 dsh 的客户端原语 ——
209
+ **有哪些**问 `dsh-wb-module check --only primitives`(它还会把"kit 里已有"的那批挑出来):
201
210
 
202
211
  * 只在**浏览器侧**引(`src/client.tsx` 与 `src/components/**` 那一侧);
203
- * 在 `package.json` 的 `dsh.client.external` 里**声明** `@deepseek-ai/dsh-client-ui-primitives`;
212
+ * 在 `package.json` 的 `dsh.client.external` 里**声明** `@deepseek-ai/dsh-client-ui-primitives`,
213
+ 同时把 `devDependencies` 里那行**精确版**对齐到外壳钉的那版(骨架默认已经两处都写好了;
214
+ 声明完记得 `pnpm install`,否则 `pnpm typecheck` 找不到它);
204
215
  * 宿主侧(`src/host.ts`)**一个字都不许碰** —— `check` 的源码边界判据会红;宿主侧 import
205
216
  `src/components/**` 还会被 `arch` 按跨运行时(S23)判红 —— 组件整片算 client 面。
206
217
 
@@ -7,14 +7,14 @@
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/*"`) |
15
15
  | `markers` | 内网域名 / 企业内部名泄进要公开发布的包 | 别写进包里;要配就落到模块自己的配置面 |
16
16
  | `alias` | `@/` 伸进工作台源码 ⇒ 装的人手上没那份源码 | `@/` 只许指自己 `src/`;跨包走包名 |
17
- | `style` | 模块定义 `.wb-*` 覆盖外壳类;手写「标签+控件」行 / 操作区 | 样式走 `*.module.css`(不带 `wb-` 前缀);借外壳类写 `:global(.wb-…)`;行与操作区用 kit 的 `FormRow(s)` / `FormActions`([KIT.md](KIT.md) §5) |
17
+ | `style` | 模块定义 `.wb-*` 覆盖外壳类;手写行 / 操作区 / **整张表单**(F1–F4) | 样式走 `*.module.css`(不带 `wb-` 前缀);借外壳类写 `:global(.wb-…)`;行与操作区用 kit 的 `FormRow(s)` / `FormActions`,整张表单用 `Form`([KIT.md](KIT.md) §5)。**F4**:源码里出现裸 `<form`、或手写行槽类(`wb-formRow` / `wb-formRows` / `wb-field`)⇒ 红(口径见 ADR-0068) |
18
18
  | `arch` | 结构退化(巨文件 / 反向依赖 / 跨运行时 / 表外目录 / 模型层没测试) | 见「第十二条 `arch`」 |
19
19
  | `lint` | 单文件语法卫生(`as any` / 残留 `console`、`debugger` / 内联颜色、`!important` / react-hooks) | 见「第十三条 `lint`」 |
20
20
 
@@ -106,22 +106,23 @@ 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
 
119
120
  他说"一片空白 / 一直转圈 / 数字不对"时,**先取证再改代码**;前一步有结论就停在那儿修:
120
121
 
121
122
  1. **`workbench_diy_panel`** —— 这个工作区面板现在什么样:哪几张卡、每张最后一次取数是读到了还是读不到(带宿主回的码与原文)、崩过没(原因 + 次数)。有答案就别往下走。
122
- 1.5 **看这一张卡**(`workbench_diy_screenshot` / `workbench_diy_html` / `workbench_diy_find` / `workbench_diy_eval`)—— 打的是**用户当前那一页**上工坊那块板里这张卡(`unit=…`):`screenshot` 给图(要卡那一块先量盒子、填 `clip`)、`html --as json` 给结构树、`find` 给元素与 `ref`、`eval` 算派生读数(对比度 / 可见文本;`in: 'card'` 注入卡根 `root`,脚本自己 `return`)。配合上一步把"看起来不对"说成"哪一处不对"。**这一族(浏览器那 14 把 + `logs_*`)要先 `workbench_diy_debug_start`**(`unit=…`;会请用户点头一次)—— 没有这场约定一律当场拒;不看屏先 `_pause`,收工 `_stop`。(`_panel` / `_status` / `_probe` 不碰他那一屏,不用开。)
123
+ 1.5 **看这一张卡**(`workbench_diy_screenshot` / `_html` / `_find` / `_eval`)—— 打的是**用户当前那一页**上工坊那块板里这张卡(`unit=…`;四把的入参与拿回见 [FAMILY.md](FAMILY.md) §2),配合上一步把"看起来不对"说成"哪一处不对"。**这一族(浏览器那 14 把 + `logs_*`)要先 `workbench_diy_debug_start`**(`unit=…`;会请用户点头一次)—— 没有这场约定一律当场拒;不看屏先 `_pause`,收工 `_stop`。(`_panel` / `_status` 不碰他那一屏,不用开;`_find` / `_eval` 带 `read=` 时也不碰。)
123
124
  2. **`workbench_diy_status`(`unit=…`)** —— 看"起得来"那一行:模块起不来时原因就在那里(装配抛的错 / 声明被拒的原文)。
124
- 3. **自己把那条读跑一遍**:卡片画的就是模块那条宿主路由的返回值 —— `curl '<工作台地址>/workbench/api/<unit>/<route>'`(默认 `http://127.0.0.1:3081`)。接口对而卡片不对 ⇒ 渲染侧;接口就错 ⇒ 宿主侧(照 [DATASOURCE.md](DATASOURCE.md) 重探)。要**浏览器里那一次**的原文,用 `workbench_diy_probe`(`script` 里 `fetch` 自己那条读)。
125
- 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`。宿主侧热换 / 装载的报错在**起工作台那个终端**里。
125
+ 3. **自己把那条读跑一遍**:卡片画的就是模块那条宿主路由的返回值 —— `curl '<工作台地址>/workbench/api/<unit>/<route>'`(默认 `http://127.0.0.1:3081`)。接口对而卡片不对 ⇒ 渲染侧;接口就错 ⇒ 宿主侧(照 [DATASOURCE.md](DATASOURCE.md) 重探)。要**浏览器里那一次**的原文,用 `workbench_diy_eval`(`read=<站点地址>`,脚本里 `fetch` 自己那条读)。
126
+ 4. **最后才看用户那一页**:要控制台走三拍 —— `workbench_diy_logs_watch` → 做那件你怀疑会报错的事 → `workbench_diy_logs_read`(读完 `_logs_stop` 卸掉;上限:每页 200 条 / 单条 400 字、随刷新消失)。**别用 `opencli browser <会话> console`** —— 那是个 stub(恒回空数组、还 exit 0)。要在页面里跑自己的代码用 `workbench_diy_eval` 的 `read=` 档;看**用户那一页**有什么可点的用 `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 步没有内容时,先让他把工作台切到工坊那个工作区、刷新一次页面再看。