@dsh-workbench/core 0.3.10 → 0.3.11

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 (82) hide show
  1. package/README.md +19 -7
  2. package/bin/dsh-workbench +2 -2
  3. package/client.js +1648 -973
  4. package/cordis.patch.yml +60 -35
  5. package/dist/client/client.body.js +1648 -973
  6. package/dist/client/index.js +1333 -658
  7. package/dist/host/index.js +2896 -2334
  8. package/dist/types/app/client.d.ts +2 -1
  9. package/dist/types/app/layouts/env-gate/boot-gate.d.ts +1 -1
  10. package/dist/types/app/layouts/market/use-module-versions.d.ts +13 -2
  11. package/dist/types/app/layouts/rightbar/MarketView.d.ts +6 -3
  12. package/dist/types/app/layouts/rightbar/market-act.d.ts +6 -3
  13. package/dist/types/app/layouts/rightbar/market-settle-hold.d.ts +2 -0
  14. package/dist/types/app/layouts/rightbar/market-settle.d.ts +20 -0
  15. package/dist/types/app/layouts/rightbar/market-update-verdict.d.ts +41 -0
  16. package/dist/types/app/layouts/rightbar/market-view-text.d.ts +4 -1
  17. package/dist/types/app/layouts/rightbar/store.d.ts +14 -0
  18. package/dist/types/app/layouts/sidebar/LongTaskDot.d.ts +12 -0
  19. package/dist/types/app/layouts/sidebar/LongTaskHover.d.ts +12 -0
  20. package/dist/types/app/layouts/sidebar/edition-boot.d.ts +24 -0
  21. package/dist/types/app/layouts/sidebar/edition.d.ts +22 -0
  22. package/dist/types/app/layouts/workbench-settings/ConfigSyncItem.d.ts +5 -0
  23. package/dist/types/app/layouts/workbench-settings/SettingsItem.d.ts +17 -0
  24. package/dist/types/app/layouts/workbench-settings/SidebarEditionItem.d.ts +11 -0
  25. package/dist/types/app/layouts/workbench-settings/WorkbenchSettingsSection.d.ts +11 -0
  26. package/dist/types/modules/panels/model/approval.d.ts +12 -4
  27. package/dist/types/modules/panels/model/diy-browser-tools.d.ts +1 -1
  28. package/dist/types/modules/panels/model/diy-context.d.ts +31 -14
  29. package/dist/types/modules/panels/model/diy-page.d.ts +3 -2
  30. package/dist/types/modules/panels/model/diy-record.d.ts +3 -3
  31. package/dist/types/modules/panels/model/diy-report.d.ts +6 -7
  32. package/dist/types/modules/panels/model/market-dependency-update.d.ts +21 -0
  33. package/dist/types/modules/panels/model/market-dispatch.d.ts +1 -0
  34. package/dist/types/modules/panels/model/market-types.d.ts +1 -0
  35. package/dist/types/modules/panels/model/market-update-bundle.d.ts +25 -0
  36. package/dist/types/modules/panels/model/market-verify.d.ts +31 -0
  37. package/dist/types/modules/panels/unit.d.ts +3 -4
  38. package/dist/types/shared/api/bundle-install.d.ts +13 -0
  39. package/dist/types/shared/api/host/approval-gate.d.ts +39 -0
  40. package/dist/types/shared/api/host/bundle-routes.d.ts +2 -1
  41. package/dist/types/shared/api/host/diy-artifacts.d.ts +14 -0
  42. package/dist/types/shared/api/host/diy-modules.d.ts +41 -0
  43. package/dist/types/shared/api/host/host-routes.d.ts +13 -0
  44. package/dist/types/shared/api/host/permission-mode.d.ts +9 -0
  45. package/dist/types/shared/api/host/runtime-seed.d.ts +2 -0
  46. package/dist/types/shared/api/host/self-update.d.ts +9 -2
  47. package/dist/types/shared/api/host/sidebar-edition.d.ts +46 -0
  48. package/dist/types/shared/api/host/workspace-root.d.ts +13 -0
  49. package/dist/types/shared/api/sidebar-edition.d.ts +14 -0
  50. package/dist/types/shared/api/unit-board.d.ts +20 -0
  51. package/dist/types/shared/api/write-outcome.d.ts +15 -0
  52. package/dist/types/shared/components/IconModules.d.ts +8 -0
  53. package/dist/types/shared/components/blocker-action.d.ts +16 -0
  54. package/dist/types/shared/hooks/sidebar-edition.d.ts +7 -0
  55. package/dist/types/shared/hooks/use-capabilities.d.ts +5 -0
  56. package/dist/types/shared/hooks/use-capability-gate.d.ts +1 -2
  57. package/dist/types/shared/hooks/use-host-write.d.ts +1 -1
  58. package/dist/types/shared/hooks/use-unit-board.d.ts +1 -1
  59. package/docs/HANDOFF.md +3 -3
  60. package/docs/OPERATIONS.md +16 -5
  61. package/package.json +3 -3
  62. package/preset/diy.patch.yml +2 -2
  63. package/profile/isolation.patch.yml +11 -0
  64. package/skills/workbench-module-diy/BUILD.md +62 -131
  65. package/skills/workbench-module-diy/COPY.md +89 -0
  66. package/skills/workbench-module-diy/DATASOURCE.md +84 -112
  67. package/skills/workbench-module-diy/DESIGN.md +75 -109
  68. package/skills/workbench-module-diy/FAMILY.md +70 -54
  69. package/skills/workbench-module-diy/KIT.md +77 -133
  70. package/skills/workbench-module-diy/LINKAGE.md +66 -105
  71. package/skills/workbench-module-diy/PANEL.md +79 -171
  72. package/skills/workbench-module-diy/REVIEW.md +67 -81
  73. package/skills/workbench-module-diy/SKILL.md +57 -96
  74. package/skills/workbench-module-diy/STEPS.md +66 -69
  75. package/skills/workbench-module-diy/TOOLING.md +34 -51
  76. package/skills/workbench-module-diy/TOOLS.md +110 -103
  77. package/skills/workbench-module-diy/WIRING.md +51 -83
  78. package/skills/workbench-module-diy/WRITES.md +89 -142
  79. package/dist/types/app/layouts/config-sync/ConfigSyncSection.d.ts +0 -5
  80. package/dist/types/shared/api/host/diy-workshop.d.ts +0 -23
  81. package/dist/types/shared/api/host/workshop-root.d.ts +0 -16
  82. /package/dist/types/shared/{api/host/diy-workshop.test.d.ts → components/blocker-action.test.d.ts} +0 -0
@@ -1,138 +1,106 @@
1
- # kit 速查:要做什么 → 用哪一件 → 最小写法
1
+ # kit:选型、组合、最小写法
2
2
 
3
- `check --only kit` 列**名字**(这台机器上真有的,别照抄别的模块),**逐件用法在 `.d.ts` 的 JSDoc 里**
4
- (`workbench_diy_components name=<名字>` 一回就是它)。这一份给的是**跨件**的:怎么拼、纪律、排布。
3
+ `check --only kit` 列**当前安装的外壳真实有的**成员;逐件用法在 `.d.ts` 的 JSDoc(`workbench_diy_components name=<名>`)。
4
+ 本文件只给跨件规则与最小示例。全部从 `kit.components` / `kit.hooks` / `kit.lib` / `kit.api`
5
+ (`@dsh-workbench/core/client`)取;示例中 `h = createElement`(从 `react` 引)。
5
6
 
6
- **能拿 kit 的就别自己画。** 理由只有一条:主题、语言、悬停、禁用、焦点环、空态、被裁的处理
7
- 都只维护一份。自己画的不会跟着换主题,也不会跟着我们改口径 —— 这就是"同一个东西长得不一样"的来源。
8
-
9
- > 全部从 `const { … } = kit.components`(`@dsh-workbench/core/client`)拿。
10
- > 例子里的 `h` = `createElement`(`import { createElement as h } from 'react'`)。
7
+ 优先用 kit:kit 随外壳换主题、跟语言走。
11
8
 
12
9
  ## 1 卡片外壳
13
10
 
14
- **`Card`** —— 一张卡的壳:标题 + 刷新钮 + 卡片体。面板上每一格都是它。
11
+ - `Card`:标题 + 刷新 + 内容。面板每格都用它。
15
12
 
16
13
  ```tsx
17
14
  h(Card, { t: props.t, title: t('myunit.cardTitle'),
18
15
  refreshing: resource.phase === 'loading', onRefresh: props.onRefresh }, body)
19
16
  ```
20
17
 
21
- **`CardHead`** —— 你自己拼卡头时用它(`Card` 内部就是它)。prop 见 `.d.ts`;
22
- `platformHref` 只给这一颗就够,那颗 ↗ 立刻画出来。
23
-
24
- **`CardAnchor`** —— 外壳自己盖的锚点(`#wb-card-<unit>`)。**别自己写**,也别删。
18
+ - `CardHead`:自拼卡头时用;`platformHref` 给一个即出 ↗。
19
+ - `CardAnchor`:外壳锚点 `#wb-card-<unit>`;不写不删。
20
+ - `CardBoundary`:卡片错误边界;不要自己 try/catch 渲染。
25
21
 
26
- **`CardBoundary`** —— 卡片级错误边界。一张卡炸了不许把整块面板带走;**别自己 try/catch 包渲染**。
22
+ ## 2 状态
27
23
 
28
- ## 2 状态:这是下限最集中的地方
24
+ 五态:待机 / 在读 / 就绪 / 就绪但空 / 读不到。
29
25
 
30
- 先想清五态(`design.json` 的状态矩阵就是这五格):**待机 / 在读 / 就绪 / 就绪但空 / 读不到** —— 每一态都要有东西画。
31
-
32
- | 什么时候 | 用 |
26
+ | 场景 | 用 |
33
27
  |---|---|
34
- | 有内容了 | `StateBlock`(标题 + `hint` 一行 + 正文) |
35
- | 要他先选一个 / 正在读 / 读不到 | `CenteredState`(居中一屏) |
36
- | 就绪但空 | `EmptyState`(它只是 `CenteredState` 的简写) |
37
- | 在读、且你已经有大概形状 | `SkeletonRows`(骨架) |
38
- | 一句说明 / 一句报错 | `Note`(`tone: 'problem'` 是报错) |
39
-
40
- prop 与默认值看 `.d.ts`。
28
+ | 有内容 | `StateBlock`(标题 + `hint` + 正文) |
29
+ | 需先选 / 在读 / 读不到 | `CenteredState` |
30
+ | 就绪但空 | `EmptyState` |
31
+ | 在读且有形状 | `SkeletonRows` |
32
+ | 一句说明 / 报错 | `Note`(`tone: 'problem'`) |
41
33
 
42
34
  ```tsx
43
35
  h(CenteredState, { icon: null, title: t('myunit.loading'), hint: t('myunit.loadingHint'), compact: true })
44
36
  h(CenteredState, { icon: null, title: t('myunit.errorTitle'), detail: `${code}: ${message}`, hint: t('myunit.errorHint'), compact: true })
45
37
  h(EmptyState, { text: t('myunit.empty'), icon: h(IconPanelScreen, { size: 20 }), compact: true })
46
38
  h(StateBlock, { title: rows.title, hint: `3 条 · ${when}`, resolving: t('myunit.rows') }, table)
47
- h(Note, { children: t('myunit.hint') })
48
39
  ```
49
40
 
50
- **读不到就说读不到**:`detail` 里放原始错误码与那句话,**绝不显示 0**(0 是"真的是 0")。
41
+ 读不到:`detail` 放原始错误码与原文;不显示 0。
51
42
 
52
- ## 3 一行一行
43
+ ## 3 行与记录
53
44
 
54
- **`Rows` + `Row`** —— 键值行(左边名字、右边值)。**别自己写 `ul`/`li` 或 div 行**:
55
- 分隔线、行距、对齐、两端溢出都在这两件里。
45
+ - `Rows` + `Row`:键值行;不手写 `ul` / `li` / div 行。
56
46
 
57
47
  ```tsx
58
48
  h(Rows, null,
59
49
  ...items.map((one) => h(Row, { key: one.name, label: one.name, value: `${size} · ${when}` })))
60
50
  ```
61
51
 
62
- **`ActivityBlock` / `ActivityLine` / `ActivityAction`** —— "它做过什么"那一块(prop 见 `.d.ts`)。时间一律用它。
63
-
64
- **`LongTaskRow`** —— 长任务那一行(五态,prop 见 `.d.ts`);别自己画进度。传 `stop` 要宿主认 `signal`、peer 抬到带它那版([TOOLS.md](TOOLS.md) §9)。
65
-
66
- **`ApprovalDetail` / `ApprovalPlan`** —— 审批屏那两格(写操作才要点头);口径与那张 ASCII 见 [DESIGN.md](DESIGN.md) §3 末节。
52
+ - `ActivityBlock` / `ActivityLine` / `ActivityAction`:活动记录;时间用它。
53
+ - `LongTaskRow`:长任务行(五态);不画进度条。传 `stop` 需宿主响应 signal 且 peer 下限到位([TOOLS.md](TOOLS.md) §9)。
54
+ - `ApprovalDetail` / `ApprovalPlan`:审批屏两格;形状见 [DESIGN.md](DESIGN.md) §3 末节。
67
55
 
68
56
  ## 4 表单
69
57
 
70
- **`Form`** —— **整张表单**(面那一层):`fields` 配置进,行怎么排、错误摆哪、什么时候校验、提交怎么被挡住、
71
- 第一处错误怎么带进视口、操作区怎么排全归它。**一张完整的表单就用它**(口径见 §5,逐件看 `.d.ts`)。
58
+ `Form` 管理整张表单:字段、校验时机、错误位置、首错聚焦、操作区。
72
59
 
73
60
  ```tsx
74
61
  h(Form<{ branch: string }>, {
75
- fields: [
76
- { key: 'branch', label: t('myunit.branch'), value: draft.branch, onChange: onBranch, // 值与改写同一处
77
- validate: (v) => (v === '' ? t('myunit.branchRequired') : undefined),
78
- control: (b) => h(TextField, { id: b.id, name: b.name, invalid: b.invalid,
79
- disabled: b.disabled, value: b.value, onChange: (e) => b.onChange(e.currentTarget.value) }) },
80
- ],
81
- submit: { label: t('myunit.save'), busyLabel: t('myunit.saving') }, // cancel / extra 见 `.d.ts`
82
- onSubmit: save, // 有错就不调它;values 就是这个类型
62
+ fields: [{
63
+ key: 'branch', label: t('myunit.branch'), value: draft.branch, onChange: onBranch, // 值与改写同格
64
+ validate: (v) => (v === '' ? t('myunit.branchRequired') : undefined),
65
+ control: (b) => h(TextField, { id: b.id, name: b.name, invalid: b.invalid,
66
+ disabled: b.disabled, value: b.value, onChange: (e) => b.onChange(e.currentTarget.value) }),
67
+ }],
68
+ submit: { label: t('myunit.save'), busyLabel: t('myunit.saving') },
69
+ onSubmit: save,
83
70
  })
84
71
  ```
85
72
 
86
- **`Form<Values>` 把每一格的键与值类型钉住**(不写类型参数 = 默认档、不钉,每格要自己收窄);
87
- **值与它的改写写在同一格**(`value` + `onChange`,`control` 从绑定取 `binding.value` / `binding.onChange`
88
- —— `Form` 不持有值);**会 `visible: false` 的键在 `Values` 里写可选**(`branch?: string` ⇒ 值是
89
- `string | undefined`)。`validate` 是同步纯函数、第二参是整张值表(跨字段读得到别的格);**控件永远由
90
- `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` 组**)。
98
- **顺序 / 间距 / 换行都归它** —— `primary` 永远在右、`danger` 紧挨它左侧、一个区只许一颗 `primary`、
99
- 窄了自动折行且 `primary` 落在右下角;`busy` 自带 `Spinner`。**别自己写 `gap` / `margin` / 顺序,
100
- 也别借 `.wb-formActions` 槽类手摆**(F3 红)。`danger` 只标"破坏性"(**永远不当填充的 `primary`**);
101
- **二次确认那半归调用点**。
73
+ 规则:`Form<Values>` 钉键与值类型;值与 `onChange` 同格;`visible: false` 的键在 `Values` 中可选;
74
+ `validate` 同步纯函数、第二参为整张值表;控件只经 `control` 提供。
102
75
 
103
- **别手写 `<button>` / `<input>` / `<select>`**(dsh 有 `Button`,kit 有上面这几件;手写的判据会红)。
104
- `<textarea>` 例外(dsh 与 kit 都没有多行输入)。
76
+ 控件:`Field`(未进 `FormRow` 时包标签与说明)· `TextField`(`invalid`)· `ChoiceGroup`(`multi`)·
77
+ `SelectField`(常规下拉;`variant: 'ghost'` 无边框;非原生 select)· `SaveBar`(取消 / 保存)。
78
+ 不手写 `<button>` / `<input>` / `<select>`;`<textarea>` 例外。
105
79
 
106
- **`ConfirmBar`** —— 「先问一句再动手」那一条(标题 / 说明 / 键值 / 失败那句 / 动作区一次收口):
107
- 键值走 `Rows` / `Row`、错走 `Note tone="problem"`、动作走 `FormActions` —— **别自己拼这四个槽**。
108
- **要不要问归调用点**(`danger` 只标"破坏性")。口径见 ADR-0073。
80
+ `FormActions`:操作区。`primary` 靠右、`danger` 在其左、每区一个 `primary`、窄屏折行;`busy` 带 `Spinner`。
81
+ 不写 `gap` / `margin` / 顺序,不借 `.wb-formActions` 槽类(F3 红)。`danger` 仅标破坏性;二次确认归调用点。
109
82
 
110
- 怎么排(标签位置、错误态、尺寸)见 §5。
83
+ `ConfirmBar`:确认条。键值走 `Rows`,错误走 `Note tone="problem"`,动作走 `FormActions`;不重拼这四槽。
84
+ 是否询问归调用点。
111
85
 
112
- ## 4′ hook 怎么搭(逐件看 `.d.ts`)
86
+ ## 4′ hooks
113
87
 
114
- kit 的 hook 也分四个场合,**别自己 `useState` + `setTimeout` 造一套**(面板要同一套口径):
115
-
116
- | 场合 | 用它 |
88
+ | 场景 | 用 |
117
89
  |---|---|
118
- | 卡片里的一次读(自带同 key 合流 / 事实订阅 / 上报面板) | `useApiResource`(非 hook 的本体是 `requestResource`,**用了就一定要 `release`**) |
119
- | 写完让面板重取 / 听事实变化 | `useFactInvalidate` · `useFactRevision` · `subscribeFactRevision`(非 hook,记得退订) |
120
- | "至少转一会儿"的忙态 | `useVisibleBusy`(投影那两件 `busyView` / `heldPhase`) |
121
- | 一格**卡片**起步(gate + 忙 + 探针 + 上报 + blocker 一次叠好) | `useUnitBoard`(`read(gated)` 里自己调 `useApiResource`) |
122
- | 只要一格的能力门 / 整张能力表 | `useCapabilityGate` / `useCapabilityMap` |
123
- | 写(**别在组件里 `fetch`**) / 轮询长任务 | `useHostWrite` / `useLongTask`(画用 `LongTaskRow`;判「还在跑」`longTaskIsLive`、写完让这一格立刻重读 `refresh()`) |
90
+ | 卡片一次读(同 key 合流 / 事实订阅 / 上报) | `useApiResource`(非 hook 为 `requestResource`,用后必须 `release`) |
91
+ | 写后重取 / 听事实变化 | `useFactInvalidate` · `useFactRevision` · `subscribeFactRevision`(非 hook 要退订) |
92
+ | 最短忙态 | `useVisibleBusy`(`busyView` / `heldPhase`) |
93
+ | 整卡起步(gate + 忙 + 探针 + 上报 + blocker) | `useUnitBoard`(`read(gated)` 内调 `useApiResource`) |
94
+ | 单点 / 整表能力门 | `useCapabilityGate` / `useCapabilityMap` |
95
+ | 写(组件内不 fetch)/ 长任务轮询 | `useHostWrite` / `useLongTask`(`LongTaskRow`;`longTaskIsLive`;写后 `refresh()`) |
124
96
 
125
- 取数那条链的图见 [LINKAGE.md](LINKAGE.md) §8,写通道与配置见 [PANEL.md](PANEL.md) §5。
97
+ 取数链见 [LINKAGE.md](LINKAGE.md) §8;配置见 [PANEL.md](PANEL.md) §5。
126
98
 
127
- ## 5 表单这一层:`Form`(面)→ 行 → 控件
99
+ ## 5 表单三层:面 → 行 → 控件
128
100
 
129
- 表单三层:**面**(`Form`,写法见 §4:值 / 校验 / 操作区全归它)→ **行**(`FormRows` + `FormRow`)→
130
- **控件**(`TextField` / `SelectField` / `ChoiceGroup`)。**一张表单一个面**。**别再自己摆操作区** ——
131
- 它自己组 `[...extra, ...cancel, submit(primary)]`(提交钮永远在右;「一会在左一会在右」就是自己摆的结果);
132
- 其余口径(时机 / 第一处错误 / `busy` / `disabled`)看它的 JSDoc 与 ADR-0068 / ADR-0074。
101
+ 一张表单一个 `Form`;不要另摆操作区(`Form` 组 `[...extra, ...cancel, submit(primary)]`)。
133
102
 
134
- `FormRows` + `FormRow` + `FormActions` 留给**不是整张表单**的地方:单行(一格字段嵌在别处)、
135
- 非表单的一组动作(工具栏、卡头那几颗钮)—— 那里才自己拼这三件:
103
+ `FormRows` + `FormRow` + `FormActions` 仅用于非整表场景:单行嵌入、工具栏、卡头按钮。
136
104
 
137
105
  ```tsx
138
106
  h(FormRows, null,
@@ -140,64 +108,40 @@ h(FormRows, null,
140
108
  h(SelectField, { value, options, onChange: setMode })))
141
109
  ```
142
110
 
143
- - **标签归 `FormRow`**(默认在左,`layout: 'stacked'` 才上下堆叠);**控件只给数据与语义 prop**,
144
- 进了 `FormRow` 就别再给控件自己的 `label` / `hint`(会多一个标签、还退回上下堆叠)。
145
- - **`hint` 与 `problem` 二选一**:有错先显示错。
146
-
147
- **错误态**:位置与长相归 `FormRow.problem` + 控件的 `invalid`;**算错的逻辑留在模块**(纯函数 + 文案,
148
- 交给 `FormField.validate`)—— **别指望外壳给校验引擎**(不做 `rules` / 异步 / 联动),
149
- **"提交过没有"与聚焦第一处错也归 `Form`**(别自己记布尔、别自己调 `revealFirstProblem`)。
150
-
151
- **选择类用哪件**:候选多 / 要搜索 / 要分组 / 名单可能读不到 ⇒ `SelectField`;候选少、要一眼看全 / 多选 ⇒
152
- `ChoiceGroup`(`loading` / `emptyText` / `variant` 两件一个意思)。**loading 那颗一律是 `Spinner`**。
153
-
154
- **尺寸口径**:字段控件默认 **28**,别自己调高;行距只有一档、标签列宽也归 `FormRow(s)`
155
- (长标签不折行、同一组共用一个标签列,别自己给标签写宽度);**控件的宽度也别自己写**
156
- (字段控件自己吃满控件槽那一格,ADR-0052 §13)。非行的子项(如分组标题)自动通栏。
111
+ - 标签归 `FormRow`(默认左置;`layout: 'stacked'` 堆叠)。进了 `FormRow` 就不再给控件 `label` / `hint`。
112
+ `hint` 与 `problem` 二选一,错误优先。
113
+ - 错误:位置与外观归 `FormRow.problem` + 控件 `invalid`;校验逻辑留在模块(纯函数 + 文案,交给 `validate`)。
114
+ "是否提交过"与首错聚焦归 `Form`。
115
+ - 选择:候选多 / 需搜索 / 需分组 / 可能读不到 → `SelectField`;候选少 / 要全览 / 多选 → `ChoiceGroup`。
116
+ loading 一律 `Spinner`。
117
+ - 尺寸:字段控件默认 28;行距与标签列宽归 `FormRow(s)`;控件宽度归控件槽。非行子项自动通栏。
157
118
 
158
- **反例(别这么写)**:
159
-
160
- ```tsx
161
- h('span', { className: styles.label }, …) // ← 手写标签行:F1 红
162
- h('div', { className: styles.form }, …) // ← 自己管行距(.form { gap }):F2 红
163
- h('form', …) // ← 裸 <form> / 手写行槽类:F4 红(表单归 Form)
164
- ```
119
+ 反例:手写标签行(F1)、自管行距(F2)、裸 `<form>` / 手写行槽类(F4)。
165
120
 
166
- **时点**:这一层随外壳发版后模块才能用(`check --only kit` 拿 peer 下限那份外壳比,不抬必红)。
167
- **`Form` / `ConfirmBar` 是新成员**:要用得等外壳**发一版含它的版本**,再把 peer 下限抬到**那一版**
168
- (版本随发版填,别照抄别的件);`FormRow(s)` / `FormActions` 的下限是另一件事。
121
+ 时点:`Form` / `ConfirmBar` 需外壳发版含它们后,把 peer 下限抬到该版。
169
122
 
170
- ## 6 别的
123
+ ## 6 其他
171
124
 
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` 拿,**别自己画图标**。
125
+ - `Link`:唯一外链(`h(Link, { href, children })`);不去箭头、文案不带 `→`、不手写 `<a>`。
126
+ - `SettingsSection` / `Stack` / `BlockerState` / `Tip`:设置节 / 竖排 / 阻断屏 / Tooltip(纯图必须有它或 `aria-label`)。
127
+ - 图标:`IconPanelScreen` / `IconModuleMark` / `IconNpm` / `IconOpencli`;其余从 dsh `Icon*Regular` 取,不自绘。
128
+ - 真机造不出的状态:`plugin.test.ts` 覆盖五态。
180
129
 
181
- ## 7 三条纪律
130
+ ## 7 规则
182
131
 
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` 里写一句为什么。
187
- 3. **自己写 CSS 只写布局**(flex / grid / gap / 对齐)。要一个新的"长得像控件"的东西时,
188
- 先问一句:kit 里是不是已经有了?
132
+ 1. 调用点只给数据与语义;外观类名只允许布局槽(通栏、外边距、网格位置)。
133
+ 2. 字号与颜色走 kit 与令牌(`--dsw-*` / `--wb-*`)。重点数字用 `--dsw-font-xl-24` + `tabular-nums`;
134
+ 刻度外字面值在模块 `README.md` 说明([PANEL.md](PANEL.md) §6)。
135
+ 3. 自写 CSS 只写布局(flex / grid / gap / 对齐)。
189
136
 
190
- ## 8 kit 里没有、但你确定要标准控件
137
+ ## 8 需要 kit 没有的标准控件
191
138
 
192
- **先问工具**:DIY 会话里调 `workbench_diy_components`(不填给清单;`name=Button` 回它的类型与声明)。工程域用 `dsh-wb-module check --only primitives`
193
- (`--all` 全表 / `--json` 机器档)—— 分桶导出 + 与 kit 求交(`kit 里已有` 的优先用)+
194
- 该写进 `package.json` 的那两行;骨架默认已备好。
195
- **要看某一件**:`--file <名字>`(`--files | grep <名字>`)。
139
+ 查:`workbench_diy_components`(DIY 会话)或 `check --only primitives`(`--all` / `--json` / `--file <名>`)。
140
+ 写入:
196
141
 
197
142
  ```jsonc
198
143
  "dsh": { "client": { "external": [ "@dsh-workbench/core/client", "@deepseek-ai/dsh-client-ui-primitives" ] } },
199
- "devDependencies": { "@deepseek-ai/dsh-client-ui-primitives": "<外壳钉的那版,精确版>" }
144
+ "devDependencies": { "@deepseek-ai/dsh-client-ui-primitives": "<与外壳一致的精确版>" }
200
145
  ```
201
146
 
202
- 改完 `pnpm install` 一次;三条会红的边界(只在浏览器侧引 / 版本精确对齐 / 能拿 kit 的别引)与
203
- 宿主侧的契约都见 [PANEL.md](PANEL.md) §9。
147
+ 改后 `pnpm install`。边界见 [PANEL.md](PANEL.md) §9。
@@ -1,142 +1,103 @@
1
- # 联动:agent 做完,用户看到什么
1
+ # 联动:谁在动、何时变、哪一格变
2
2
 
3
- 步骤 3 的参考。写的是用户什么时候看到哪一格变,不写这条链怎么接。
3
+ 步骤 3 的参考。实现见 §8。
4
4
 
5
- ## 1 什么时候面板该动
5
+ ## 1 触发规则
6
6
 
7
- | 谁在动 | 用户看到什么 |
7
+ | 触发 | 面板 |
8
8
  |---|---|
9
- | agent **读**了面板这一格的数据 | **那一格**变(按最窄的那个切片重读),值跟对话里那份一样 |
10
- | agent 读的是面板上没有的东西(明细、日志、某一档子集) | **不动**(面板没有那一格) |
11
- | agent 只预览、还没真的做 | **不动**(预览不改任何东西) |
12
- | agent 真的做了 | 相关的那一格**变成新值** |
13
- | 他自己按的动作 | 那一格**立刻变**,并在原地看到"正在做 / 做完了" |
14
- | 他自己改了偏好 | 保存后立刻按新的显示 |
15
- | 别的窗口 / 另一个会话也在改 | 他这里也会变(不需要他刷新) |
9
+ | agent 读面板这一格 | 该格按最窄切片重读;值与对话一致 |
10
+ | agent 读面板外的东西(明细 / 日志 / 子集) | 不变 |
11
+ | agent 只预览 | 不变 |
12
+ | agent 真写 | 相关格变新值 |
13
+ | 用户自按动作 | 该格立即变,原地显示进行中 / 完成 |
14
+ | 用户改偏好 | 保存后按新偏好显示 |
15
+ | 其他窗口 / 会话改动 | 跟随变,无需刷新 |
16
16
 
17
- ⚠️ **"读 → 也变"这条是用户定的口径**:理由是「agent 调用工具的时候面板数据也受益」—— agent 刚在对话里
18
- 念了数字,面板得是同一份,否则他到底信哪个。
17
+ 一次说清变哪一级:整板 / 一张卡 / 一行。"刷新一下"不合格。只动该动的格,粒度落到"某投影 + 最窄参数"
18
+ (`cmdb/builds?env=dev`);否则会整卡重读。
19
19
 
20
- **一次说清动的是哪一格**:整块板、某一张卡、还是某一行。写"刷新一下"不算说清 —— 那会让实现的人整卡重读。
20
+ ## 2 进行中
21
21
 
22
- **只动该动的那一格**:agent 在一个环境做了一次,不该让别的环境、别的列、整张卡一起重新读一遍(读一次很慢,他会看到整块退回"在读")。**颗粒度就是这句话在实现里的样子**:动的那一格要能落到"某条投影 + 最窄的那个参数"(`cmdb/builds?env=dev` 这种),写进联动表里。
22
+ - 显示可读的步骤名,不用内部步骤号。
23
+ - 要登录:给登录入口;登录后续跑,不重来。
24
+ - 失败:卡在哪步、一句原因、原始输出(折叠)、一条可照做的路。
25
+ - 不画进度条、不画流程控制台。
23
26
 
24
- ## 2 agent 正在干的时候
27
+ 中途停止可续:用户说"接着做"即从上次继续。
25
28
 
26
- - 显示到**哪一步**("正在合并分支"这种他能读懂的一步,不是内部步骤名);
27
- - 要**登录**就给他一条去登录的路,登录完这次继续,不用他重来;
28
- - **失败**给他:卡在哪一步、一句能读的原因、原始输出(折叠起来),以及一条能照做的路;
29
- - **不画进度条、不画流程控制台**:状态 + 链接 + 原始输出就够。
29
+ ## 3 "交给 agent"的内容
30
30
 
31
- 一次做一半就停也不是问题:他回来说一句"接着做",能接着上一次继续,不用从头再来。
31
+ - 哪一类 + 哪个对象 + 多少(按用户点的那一行,不是全量)。
32
+ - 一句"用哪个能力取明细"(agent 打不开面板的 ↗)。
33
+ - 不塞明细本身。
34
+ - 保持用户点的分类;认不出就报错,不退回全量。
32
35
 
33
- ## 3 他点"交给 agent"之后,agent 拿到什么
36
+ ## 4 收起模块
34
37
 
35
- - **哪一类 + 哪个对象 + 多少**(他点的是哪一行,agent 就该拿到哪一类,不能四行点了都给它同一份全部);
36
- - 加一句**该用哪个能力往下拿明细**(agent 打不开面板上那个 ↗,明细只能它自己再问一次);
37
- - **明细本身不要塞进去**(一页几十行按会话付费,而 agent 拿到那句指引自己会去要);
38
- - 他点的那一类要跟着走(点"圈复杂度"就只给圈复杂度,不能悄悄退回整份);
39
- - 认不出的那一类要**当场说错**,不许默默退回整份(他要的是这一块,给全部比报错更糟)。
38
+ 收起 = 该项目内 agent 也失去该能力。收起时说明;收起后能力清单更新,对话显示"能力有更新";其他项目不受影响。
40
39
 
41
- ## 4 收起模块意味着什么
40
+ ## 5 不陈旧
42
41
 
43
- 把一块面板收起来 = **这个项目里 agent 也做不了这件事**。
42
+ 覆盖三种:切项目 → 立刻显示新项目;其他窗口 / 会话改动 → 跟随;程序刚更新 → 不因"我比你还新"而不收更新。
43
+ 允许秒级延迟,但要说明;不得停在旧值等手动刷新。
44
44
 
45
- - 这一条要在**点头收起的时候**就说清("这个项目里它的能力也一起下线");
46
- - 收起之后 agent 手上的能力清单会变,用户会在对话里看到一句"能力有更新";
47
- - 别的项目不受影响。
45
+ ## 6 联动表(步骤 3 产出)
48
46
 
49
- ## 5 面板凭什么不旧
50
-
51
- 三种情况都要有答案:
52
-
53
- 1. **他切了项目** —— 看到的立刻是新项目的数据;
54
- 2. **别的窗口 / 另一个会话也改了** —— 他这里会跟着变;
55
- 3. **程序刚更新过(开发时最常见)** —— 不会因为"我比你还新"这种判断而永远收不到更新。
56
-
57
- 允许有延迟(几秒),但延迟要说清;**不允许停在旧值上、只有手动点刷新才动**。
58
-
59
- ## 6 联动场景表(步骤 3 的产出)
60
-
61
- | 谁在动 | 面板什么时候变 | 哪一格变 | 用户看到什么 |
47
+ | 谁在动 | 何时变 | 哪一格 | 看到什么 |
62
48
  |---|---|---|---|
63
- | agent 读了面板这一格的数据 | 读完自动 | 那一格(最窄那个切片) | 那一格出已有的加载态(约半秒)→ 与对话同值 |
64
- | agent 读了面板上没有的那份 | 不变 | — | 对话里一段结果 |
65
- | agent 预览 | 不变 | — | 对话里"将要做什么" |
66
- | agent 真的做 | 做完之后自动 | 相关那一格 | 新值 |
67
- | 他自己按动作 | 立刻 + 做完再更新 | 那一格 | 确认条 → "正在做" → 结果 |
68
- | 他改偏好 | 保存后 | 整张卡按新偏好 | 回到看板、数据已换 |
69
- | agent 改他的面板 | 自动 | 面板多了 / 少了 / 换了顺序 | 界面变了 + 对话里"能力有更新" |
70
- | 装 / 卸能力 | 自动 | 相关的那几处 | 状态变 |
71
- | 一件长事在跑 | 自动刷新进度 | 那张卡的进度那一行 | 到哪一步 / 要登录 / 失败原因 |
72
- | 别的窗口也在改 | 自动 | 同"真的做" | 同"真的做" |
73
- | 程序刚更新过 | 全部重读一次 | 整块 | 不会退回旧值 |
74
- | 他自己点刷新 | 立刻 | 那一份 | 转圈,然后新值 |
75
- | 他切项目 | 立刻 | 整块 | 新项目的数据 |
76
- | 模块在这个项目被收起 | 自动 | 少一张卡 | 对话里"能力有更新" |
77
-
78
- ## 7 读工具也要说清"它对应面板哪一格"
79
-
80
- 用户会在对话里问到面板上就有的那格数字 —— 两处必须对得上(否则他得决定信哪个)。
81
- 所以每把**读**工具在设计时就要回答三句:
82
-
83
- - 它读的那份数据,是面板上**哪一格**?
84
- - 那一格**最窄的切片**是什么(哪个环境的哪一列 / 哪一档筛选 / 就是整张卡)?
85
- - 手里那份**是不是就是**那一格的值?
86
-
87
- 第三种答案决定面板怎么动:
88
-
89
- | 它读的是 | 面板 |
49
+ | agent 读面板这一格 | 读完 | 该格(最窄切片) | 加载态(约半秒)→ 与对话同值 |
50
+ | agent 读面板外 | 不变 | — | 对话内结果 |
51
+ | agent 预览 | 不变 | — | 对话内"将要做什么" |
52
+ | agent 真写 | 完成 | 相关格 | 新值 |
53
+ | 用户自按 | 立即 + 完成再更新 | 该格 | 确认条 → 进行中 → 结果 |
54
+ | 用户改偏好 | 保存后 | 整卡 | 回看板、数据已换 |
55
+ | agent 改面板 / 装卸能力 | 自动 | 增删或顺序,或相关几处 | 界面变化 + "能力有更新" |
56
+ | 长任务运行 | 自动推进度 | 卡的进度行 | 到哪步 / 要登录 / 失败原因 |
57
+ | 其他窗口 / 程序更新 | 自动 | 同"真写" / 整板 | 同"真写" / 不回旧值 |
58
+ | 切项目 / 点刷新 | 立即 | 整板 / 该份 | 新项目 / 转圈后新值 |
59
+ | 模块在项目内被收起 | 自动 | 少一张卡 | "能力有更新" |
60
+
61
+ ## 7 读工具对应哪一格
62
+
63
+ 每把读工具回答三句,写进能力清单:
64
+
65
+ 1. 读的数据是面板哪一格?
66
+ 2. 该格最窄切片是什么(哪个环境哪一列 / 哪档筛选 / 整卡)?
67
+ 3. 手里这份是否就是该格的值?
68
+
69
+ | 读的范围 | 面板 |
90
70
  |---|---|
91
- | 面板这一格范围内的数据 | 那一格重读;**手里那份就是那一格的值**时可以直接交出去(面板不再打一遍平台) |
92
- | 面板上没有的东西(明细 / 日志 / 单条) | **不动**(面板没有那一格) |
93
- | 面板某一格的**子集**(只读 TODO、limit 更小) | **不动**,而且**不许**把子集当那一格交出去(那是错值)—— 要对得上就让 agent 读全量那一档 |
94
-
95
- 三句都写进能力清单(用户看的是"哪一格会跟着动")。
96
-
97
- → 实现:本文 §1 与 §7。
71
+ | 该格范围内 | 该格重读;手里就是该格值时可直接交出(不再重打平台) |
72
+ | 面板外(明细 / 日志 / 单条) | 不变 |
73
+ | 该格的子集(只读 TODO、更小 limit) | 不变;不得当该格交出(错值)—— 要一致就读全量档 |
98
74
 
99
- ## 8 宿主侧的四个口子(实现形状)
75
+ ## 8 宿主侧实现
100
76
 
101
- 上面说的"哪一格变",落到代码就四个可选字段;**不写 `factsOf` 的模块,面板那一格不会自己新**。
77
+ 四个可选字段。不写 `factsOf` 的模块,面板格不会自己新。
102
78
 
103
79
  ```ts
104
80
  factsOf: (call) => (call.source === REBUILD_TOOL ? [factOfRoute(HEALTH_ROUTE)] : undefined),
105
81
  ```
106
82
 
107
- * `call.source` 是**能力名**、`call.args` 是它这次收到的参数;返回"这次动到了哪几条事实",
108
- 返回 `[]` = 涉及但没有具体事实,`undefined` = 跟面板无关(例:明细、日志那种面板上没有的东西)。
109
- * 事实名不要自己拼字符串:用 `factOfRoute('<unit>/<路由>')`(`@dsh-workbench/shared`)——
110
- **卡片读的那条路由对应哪条事实,就是它算出来的那条**。
111
- * **读能力也要认领**(§1 与 §7 的口径):agent 只是读了一次,那一格也该重读。
112
- * 长任务动完也要认领:`longTasks.create({ …, facts: () => [factOfRoute(HEALTH_ROUTE)] })`。
83
+ - `call.source` = 能力名,`call.args` = 本次参数。返回本次涉及的事实:`[]` = 涉及但无具体事实;
84
+ `undefined` = 与面板无关。
85
+ - 事实名用 `factOfRoute('<unit>/<路由>')`(`kit.api.factOfRoute`),不要手拼。读能力也认领;长任务写
86
+ `longTasks.create({ …, facts: () => [factOfRoute(HEALTH_ROUTE)] })`。
113
87
 
114
88
  ```tsx
115
89
  const rev = useFactRevision(factOfRoute(HEALTH_ROUTE));
116
90
  const resource = useApiResource(ROUTE, query, rev, {});
117
91
  ```
118
92
 
119
- 卡片的 `useApiResource` **默认就订阅** `factOfRoute(route)`(`use-api-resource.ts` 里那句
120
- `options.fact ?? factOfRoute(route)`),所以多数情况你什么都不用接;用户自己按的那个动作做完,
121
- 用 `useFactInvalidate()()` 把这条事实标脏。
93
+ `useApiResource` 默认订阅 `factOfRoute(route)`;用户自按动作完成后用 `useFactInvalidate()()` 标脏。
122
94
 
123
95
  ```ts
124
96
  context: { kinds: ['module'], render: async (ref) => `模块 ${ref.parts[0]}:…` },
125
97
  warmReads: () => [['<unit>', '<target>']],
126
98
  ```
127
99
 
128
- * `context`:agent 上下文里引用你这一格时(例:`diy-doctor/module/fx-rate`),由你把它渲染成人话;
129
- `kinds` 列出你认哪几类,**参数形状不对就 `throw`**(说清"形状不对",别默默回一段错的)。
130
- 判据只查形状(`kinds` 非空、`render` 是函数),**不会拿假参数调它** —— 那些参数只有真引用才有。
131
- * `warmReads`:**只给"走 opencli 适配器"的读用**。条目就是那条 opencli 命令的 argv,外壳在启动的
132
- preflight 里拼成 `opencli <argv…> --site-session persistent -f json` 先跑一遍(把站点登录态与首屏焐热)。
133
- 例:`[['cmdb', 'enterprises']]`(一条 opencli 命令的 argv,还带测试)。
134
- **本机读盘、browser bridge、直接打接口的读一律不要写这里** —— 写了外壳会去跑一条不存在的命令,
135
- preflight 只会给个 warn;这些读没有"冷启动"可省,焐不热任何东西。
136
- * `values`(`deps.values`,`ValueHandoff`):把一次写的结果交给别的单元 —— `values.hand({fact, key}, 值)`,
137
- 另一边 `values.take({fact, key})` 取;`key` 自己起(例:一次发布的结果给联动那一格用)。
138
-
139
- ## 9 范例
140
-
141
- 要一个"同形状、能跑"的样子:[SKILL.md](SKILL.md) 的「找不到一件事的时候」—— 模板
142
- (`template-diy/` / `template/`)与装机模块的源码都可以直接读。
100
+ - `context`:把 agent 上下文里的引用渲染成人话;`kinds` 列出识别的类型;形状不对就 `throw`。
101
+ - `warmReads`:只给走 opencli 适配器的读。条目是该 opencli 命令的 argv,启动 preflight 先跑一遍。
102
+ 本地读盘、browser bridge、直连接口的读不要写。
103
+ - `values`:跨单元传值。`values.hand({fact, key}, 值)` / `values.take({fact, key})`。