@dsh-workbench/core 0.1.14 → 0.1.15

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 (42) hide show
  1. package/bin/dsh-workbench +16 -0
  2. package/client.js +163 -89
  3. package/cordis.patch.yml +18 -0
  4. package/dist/client/client.body.js +163 -89
  5. package/dist/client/index.js +163 -89
  6. package/dist/host/index.js +1300 -93
  7. package/dist/types/app/__tests__/workbench-styles.test.d.ts +1 -0
  8. package/dist/types/app/client.d.ts +39 -15
  9. package/dist/types/app/layouts/rightbar/board-columns.d.ts +8 -0
  10. package/dist/types/app/layouts/rightbar/board-columns.test.d.ts +1 -0
  11. package/dist/types/app/layouts/rightbar/market-rows.d.ts +2 -0
  12. package/dist/types/app/layouts/workbench-styles.d.ts +2 -0
  13. package/dist/types/modules/panels/__tests__/diy-harness.d.ts +51 -0
  14. package/dist/types/modules/panels/__tests__/diy-tools-status.test.d.ts +1 -0
  15. package/dist/types/modules/panels/__tests__/diy-tools.test.d.ts +1 -0
  16. package/dist/types/modules/panels/model/diy-tools.d.ts +4 -0
  17. package/dist/types/modules/panels/unit.d.ts +1 -0
  18. package/dist/types/shared/api/bundle-install.d.ts +1 -0
  19. package/dist/types/shared/api/host/diy-facts.d.ts +9 -0
  20. package/dist/types/shared/api/host/diy-facts.test.d.ts +1 -0
  21. package/dist/types/shared/api/host/diy-recon-fs.d.ts +3 -0
  22. package/dist/types/shared/api/host/diy-recon-fs.test.d.ts +1 -0
  23. package/dist/types/shared/api/host/diy-recon.d.ts +39 -0
  24. package/dist/types/shared/api/host/diy-recon.test.d.ts +1 -0
  25. package/dist/types/shared/api/host/diy-workshop.d.ts +12 -0
  26. package/dist/types/shared/api/host/diy-workshop.test.d.ts +1 -0
  27. package/dist/types/shared/api/host/installed-module.d.ts +2 -0
  28. package/dist/types/shared/api/host/opencli-command.d.ts +10 -0
  29. package/dist/types/shared/api/host/opencli-command.test.d.ts +1 -0
  30. package/package.json +2 -3
  31. package/scripts/seed-profile.sh +4 -113
  32. package/skills/workbench-module-diy/BUILD.md +36 -66
  33. package/skills/workbench-module-diy/DATASOURCE.md +80 -12
  34. package/skills/workbench-module-diy/DESIGN.md +27 -2
  35. package/skills/workbench-module-diy/LINKAGE.md +49 -19
  36. package/skills/workbench-module-diy/PANEL.md +78 -4
  37. package/skills/workbench-module-diy/REVIEW.md +6 -5
  38. package/skills/workbench-module-diy/SKILL.md +32 -12
  39. package/skills/workbench-module-diy/STEPS.md +4 -0
  40. package/skills/workbench-module-diy/TOOLING.md +47 -0
  41. package/skills/workbench-module-diy/TOOLS.md +147 -5
  42. package/skills/workbench-module-diy/WIRING.md +88 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/core",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "个人工作台:Agent 工作台外壳(左栏 + 常驻对话 + 自研右侧数据面板)。",
5
5
  "type": "module",
6
6
  "icon": "./icon.svg",
@@ -86,7 +86,7 @@
86
86
  "access": "public"
87
87
  },
88
88
  "dependencies": {
89
- "@dsh-workbench/shared": "0.1.14"
89
+ "@dsh-workbench/shared": "0.1.15"
90
90
  },
91
91
  "scripts": {
92
92
  "build": "node scripts/build.mjs && node scripts/build-types.mjs",
@@ -95,7 +95,6 @@
95
95
  "typecheck": "tsc --noEmit",
96
96
  "arch": "arch-guard",
97
97
  "selfcheck": "node scripts/selfcheck.mjs",
98
- "check:host": "node scripts/check-host.mjs",
99
98
  "check:runtimes": "node scripts/check-runtimes.mjs",
100
99
  "check": "npm run typecheck && npm run coverage && npm run arch && npm run check:runtimes && npm run selfcheck",
101
100
  "test": "vitest run",
@@ -170,119 +170,10 @@ fi
170
170
 
171
171
  # ── 第 6 步:建 DIY 工坊 ────────────────────────────────────────────────
172
172
  #
173
- # 工坊是 DIY 模式的落点:agent 在那里造模块(一个模块一个目录),
174
- # 而 **DIY 会话的 cwd 就是它** —— dsh 的沙箱把"会话 cwd"当作 workspace-write 的边界,
175
- # 所以源码写在工坊里天然放行,不需要给用户开 danger-full-access。
176
- #
177
- # 为什么它是一个 pnpm workspace 而不是随便一个目录:模块要构建就得有
178
- # module-tools / core / shared / react / typescript / vitest;把它做成 workspace 根,
179
- # 依赖只装一次,每个模块只是 packages/ 下多一个目录。而且 module-tools 的发现逻辑
180
- # 认 `pnpm-workspace.yaml`,于是模块单独跑 check 时能找到同仓的外壳(kit / standalone 两条判据
181
- # 才不会静默跳过 —— 跳过等于没验)。
182
- #
183
- # 两个身份同 bin/dsh-workbench:**仓库副本**把三个 @dsh-workbench/* 用 link 指进本仓
184
- # (改工具链立刻生效),**安装包副本**从源上按版本装。
185
- DIY_DIR="$DSH_HOME/workbench/diy"
186
- mkdir -p "$DIY_DIR/packages"
187
-
188
- if [ -n "$GIT_TOP" ]; then
189
- CORE_SPEC="link:$REPO_DIR"
190
- TOOLS_SPEC="link:$(cd "$REPO_DIR/../module-tools" && pwd)"
191
- SHARED_SPEC="link:$(cd "$REPO_DIR/../shared" && pwd)"
192
- else
193
- CORE_SPEC="@dsh-workbench/core@^$(node -p "require('$REPO_DIR/package.json').version" 2>/dev/null || echo 0)"
194
- TOOLS_SPEC="@dsh-workbench/module-tools@^$(node -p "require('$REPO_DIR/package.json').version" 2>/dev/null || echo 0)"
195
- SHARED_SPEC="@dsh-workbench/shared@^$(node -p "require('$REPO_DIR/package.json').version" 2>/dev/null || echo 0)"
196
- fi
197
-
198
- # 工坊必须**钉住自己那一代 pnpm**(与仓库根钉的同一个)。
199
- #
200
- # 不钉的话它用用户机器上那个 pnpm —— 实测这台是 11.21.0,而仓库钉的是 10.32.1,
201
- # 而**两代的设置住址与键名都不一样**:`store-dir` 在 pnpm 10 从 `.npmrc` 读、
202
- # 在 pnpm 11 只认 `pnpm-workspace.yaml` 里的 `storeDir`;放行依赖构建的键
203
- # 在 pnpm 10 是 `onlyBuiltDependencies`、11 是 `allowBuilds`(11 还会往
204
- # pnpm-workspace.yaml 里写一行 `esbuild: set this to true or false` 占位)。
205
- # 结果是同一份工坊配置在这台机器上对、在另一台上错,而且错法都是静默或半静默的。
206
- # 钉了之后两边一致,且这一整轮验证过的就是这一代。
207
- PNPM_PIN=""
208
- if [ -n "$GIT_TOP" ] && [ -f "$GIT_TOP/package.json" ]; then
209
- PNPM_PIN="$(node -p "require('$GIT_TOP/package.json').packageManager || ''" 2>/dev/null || true)"
210
- fi
211
- PNPM_PIN="${PNPM_PIN:-pnpm@10.32.1}"
212
-
213
- if [ -f "$DIY_DIR/package.json" ]; then
214
- echo "→ DIY 工坊已存在,跳过建骨架(要重来就先删掉 $DIY_DIR)"
215
- else
216
- echo "→ 建 DIY 工坊 $DIY_DIR"
217
- cat >"$DIY_DIR/package.json" <<EOF
218
- {
219
- "name": "dsh-workbench-diy",
220
- "version": "0.0.0",
221
- "private": true,
222
- "description": "DIY 工坊:工作台模块在这里造。这个目录由 dsh-workbench 的 setup 生成,手改会被覆盖。",
223
- "packageManager": "$PNPM_PIN",
224
- "devDependencies": {
225
- "@dsh-workbench/core": "$CORE_SPEC",
226
- "@dsh-workbench/module-tools": "$TOOLS_SPEC",
227
- "@dsh-workbench/shared": "$SHARED_SPEC",
228
- "@types/node": "^24.19.0",
229
- "@types/react": "^19.3.0",
230
- "esbuild": "^0.28.2",
231
- "react": "^19.3.0",
232
- "typescript": "5.9",
233
- "vitest": "^5.0.2"
234
- }
235
- }
236
- EOF
237
- cat >"$DIY_DIR/pnpm-workspace.yaml" <<'EOF'
238
- packages:
239
- - "packages/*"
240
-
241
- # esbuild 的构建脚本要放行:pnpm 默认不跑依赖的 postinstall,而模块的构建就是 esbuild。
242
- # 不放行的话每次 install 都会打一行
243
- # `[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: esbuild@0.28.2` —— 看着像失败,
244
- # 其实退出码是 0(实测踩过)。
245
- # 钉在 10.32.1 之后这个键名是对的(同一代 pnpm 也从 package.json 的 `pnpm` 段读它,
246
- # 仓库根就是这么放的);换 pnpm 11 会变成 `allowBuilds`,所以上面那句「钉住 pnpm」是前提。
247
- onlyBuiltDependencies:
248
- - esbuild
249
- EOF
250
- cat >"$DIY_DIR/.npmrc" <<EOF
251
- # 工坊自己的 store 与 cache。
252
- #
253
- # 为什么必须指进来:agent 的 shell 跑在**会话沙箱**里,可写根就是会话 cwd(= 这个目录)。
254
- # 而 pnpm 默认的 store(例如 ~/Library/pnpm/store/v10)与 npm 默认的 cache(~/.npm)
255
- # 都在可写根**之外** —— 不指进来的话,agent 在会话里跑一次 pnpm install 会被沙箱拒(EPERM),
256
- # 而"给模块加一个依赖"只有这一条路。setup 时的安装由人跑,不受沙箱限制,但用的也是这份配置,
257
- # 于是装一次之后 agent 那边也能继续装。
258
- store-dir=$DIY_DIR/.pnpm-store
259
- cache-dir=$DIY_DIR/.npm-cache
260
- EOF
261
- cat >"$DIY_DIR/README.md" <<'EOF'
262
- # DIY 工坊
263
-
264
- 工作台模块在这里造。**一个模块一个目录**,都住在 `packages/` 下。
265
-
266
- - 这个目录是「DIY 会话」的工作目录 —— 在对话里让 agent 造模块,它就写在这里。
267
- - 依赖已经装好(`@dsh-workbench/core` / `module-tools` / `shared` / react / typescript / vitest)。
268
- - 造一个新模块:`dsh-wb-module init <名字> --dir packages/module-<名字>`
269
- - 每个模块自己的四件事:`pnpm build` / `pnpm typecheck` / `pnpm test` / `pnpm selfcheck`,
270
- 外加 `dsh-wb-module check` 的十条判据。
271
-
272
- 不要手改这个目录下的 `package.json` / `pnpm-workspace.yaml` / `.npmrc`:
273
- 它们是 `dsh-workbench setup` 生成的,模块要加依赖就加在**模块自己**的 `package.json` 里。
274
- EOF
275
- fi
276
-
277
- if [ -d "$DIY_DIR/node_modules" ]; then
278
- echo "→ 工坊依赖已装,跳过"
279
- elif ! command -v pnpm >/dev/null 2>&1; then
280
- echo "→ ⚠️ PATH 上没有 pnpm,工坊依赖没装。装好 pnpm 后跑一次:" >&2
281
- echo " cd $DIY_DIR && pnpm install" >&2
282
- else
283
- echo "→ 装工坊依赖(第一次会下载 react / typescript / vitest,几百 MB,之后不再下)"
284
- (cd "$DIY_DIR" && pnpm install 2>&1 | tail -6)
285
- fi
173
+ # 工坊是 DIY 模式的落点:agent 在那里造模块(一个模块一个目录),而 **DIY 会话的 cwd 就是它**。
174
+ # 骨架 + 依赖 + 稳定入口都交给 `scripts/seed-workshop.sh` —— 启动器(bin/dsh-workbench)
175
+ # 每次启动也调它(幂等),所以"干净机器上工坊是空的"这件事不再需要人来处理。
176
+ bash "$REPO_DIR/scripts/seed-workshop.sh"
286
177
 
287
178
  # 装 bundle 的目标:开发态是这个仓库(link),安装包态是源上那个包。
288
179
  # 判据是「git 顶层在不在」—— npm pack 的产物里不含 .git(见 package.json 的 files)。
@@ -1,6 +1,7 @@
1
- # 实现参考(DIY 域):把模块造成一个独立包,装进工坊
1
+ # 实现参考(DIY 域):把模块造成一个独立包
2
2
 
3
- 本文只管"怎么落"。设计口径看 [DESIGN.md](DESIGN.md) 与 [STEPS.md](STEPS.md);
3
+ 本文只管"怎么落":工坊、骨架、文件、命令、两个面。接上外壳与交付(依赖 / 禁区 / 装 / 热换 / 发布)
4
+ 在 [WIRING.md](WIRING.md)。设计口径看 [DESIGN.md](DESIGN.md) 与 [STEPS.md](STEPS.md);
4
5
  门禁红了看 [REVIEW.md](REVIEW.md)。
5
6
 
6
7
  ## 0 一个模块就是一个独立的 dsh 插件包
@@ -28,10 +29,18 @@ $DSH_HOME/workbench/diy/ ← 工坊(一个 pnpm workspace,依赖
28
29
 
29
30
  一个模块一个目录,一个目录一个包。**不要**把源码写到别处,也不要去动工坊根上的配置。
30
31
 
32
+ ⚠️ **骨架不在就先补上**:启动器会自愈(少了才建),没建起来时跑一次 `dsh-workbench setup`(幂等)
33
+ 再回来跑模块命令 —— 没有骨架时门禁会把"模块不在任何 workspace 里"讲成"声明写错了",那是症状。
34
+
35
+ 工坊里的 `@dsh-workbench/*` **只有一份**(工坊根的 `link:` + 一份 `pnpm.overrides` 钉着):模块自己的
36
+ `pnpm build` / `pnpm exec dsh-wb-module` 和工坊根的 `.bin/dsh-wb-module` 是同一个二进制,
37
+ `check --only kit` 比的也是用户**真正在跑**的那份外壳。别在模块里另装一份——两代并存是静默的
38
+ (你会在模块里看到旧模板、旧报错,而工坊根那份什么都说得好好的)。
39
+
31
40
  ## 2 从模板改,不从零写
32
41
 
33
42
  ```bash
34
- dsh-wb-module init <unit> --dir <工坊>/packages/module-<unit> --description "<一句话>"
43
+ pnpm exec dsh-wb-module init <unit> --dir <工坊>/packages/module-<unit> --template diy --description "<一句话>"
35
44
  ```
36
45
 
37
46
  `<unit>` 是小写短横线标识(如 `my-thing`),它同时是单元 id、包名后缀、卡片样式前缀。
@@ -48,7 +57,7 @@ dsh-wb-module init <unit> --dir <工坊>/packages/module-<unit> --description "<
48
57
 
49
58
  ⚠️ `diy` 那份是**给别人照抄的**,所以它自己也必须开箱全绿 —— 门禁会**两个模板各 init 一遍、各跑一遍全套**。
50
59
 
51
- 十一个文件各有其职:
60
+ 骨架里的文件各有其职(`minimal` 十一份,`diy` 多一份 `design.json`,见下面那张 ⚠️):
52
61
 
53
62
  | 文件 | 职责 |
54
63
  |---|---|
@@ -62,12 +71,17 @@ dsh-wb-module init <unit> --dir <工坊>/packages/module-<unit> --description "<
62
71
  | `scripts/smoke.mjs` | `selfcheck`:把 `client.js` 当浏览器加载器跑一遍,看卡片真进没进外壳 |
63
72
  | `tsconfig.json` | `paths` 里 `@/modules/<unit>/*` → `./src/*`,值必须是**相对路径** |
64
73
  | `types/globals.d.ts` | 经典 JSX 运行时的全局类型垫片 |
74
+ | `design.json` | 三张表 + 状态矩阵 + 数据源记录 + 验收清单(**唯一真相**;README 里那一段由它生成) |
65
75
  | `README.md` | 这个模块自己的说明 |
66
76
 
67
77
  改哪一个:**画什么改 `src/client.tsx`,文案改 `src/messages.ts`**,需要能力再动 `src/host.ts`。
68
78
  模板已经把卡片套好了外壳的卡壳(`kit.components.Card`)、三态与测试;你只改里面。
69
79
 
70
- ## 3 三个命令 + 四个脚本
80
+ ⚠️ **三张表落在 `design.json`**(`diy` 骨架自带一份填好的,`minimal` 里没有 —— 用它就照
81
+ [DESIGN.md](DESIGN.md) §5 建一个):把它改成你刚问出来的那份,**再改代码**。
82
+ README 里 `<!-- dsh-wb:design:start/end -->` 之间那一段**由它生成**(`design render`)—— 手改会判红。
83
+
84
+ ## 3 命令与脚本
71
85
 
72
86
  ```bash
73
87
  dsh-wb-module build # 出 client.js 与 dist/host.js
@@ -76,16 +90,30 @@ dsh-wb-module check --only cards # 单跑一条(contracts / bundles / sourc
76
90
  # kit / cards / standalone / markers / alias / style)
77
91
  ```
78
92
 
79
- `package.json` 里那四个脚本都要留着(契约判据会查):
93
+ 三张表的填/查/生成/打勾见 [DESIGN.md](DESIGN.md) §5。
94
+
95
+ ⚠️ 写成 `pnpm exec dsh-wb-module …`(工坊里 PATH 上也有同名入口,由启动器挂);
96
+ 模块目录里更省事的是那几个脚本(`pnpm build` / `pnpm selfcheck`)。
97
+
98
+ `package.json` 里那前四个脚本都要留着(契约判据会查):
80
99
 
81
100
  ```bash
82
101
  pnpm build # 构建
83
102
  pnpm typecheck # tsc --noEmit
84
103
  pnpm test # vitest
85
- pnpm selfcheck # 构建 + 把 client.js 当加载器跑一遍
104
+ pnpm selfcheck # 构建 + 把 client.js 当加载器跑一遍(`diy` 骨架里还多一条 `design check`)
86
105
  ```
87
106
 
88
107
  **自己的门禁自己过,红了别抛给用户。** 十条判据在 [REVIEW.md](REVIEW.md) 里逐条翻了人话。
108
+ ⚠️ **`check` 不替你跑 `pnpm test`**(它只看形状与产物):状态级的渲染、换算、边界都在你自己的
109
+ `test` 里 —— 骨架开箱就带一份,改了数据源与渲染就顺手把它跑一遍,别让红线留在包里。
110
+ ⚠️ **工坊里恒有两条是「不适用」,那不是「通过」**:`独立构建` 扫的是 `packages/*` 里的外壳源码
111
+ (工坊里没有),`公开包无内部标识` 只扫要发出去的包(DIY 模块一律 `private`)。汇总行下面会把
112
+ 这次没查的那几条逐条列出来;**要验它们,回工作台仓库根跑一次 `dsh-wb-module check`**。
113
+ 所以「8 通过 · 2 不适用」不等于十条全绿 —— 别把这句话念给用户听。
114
+
115
+ 装上板那一步不是命令,是一把 agent 工具:`workbench_diy`(`action=list` 看工坊里有什么、`action=status, unit=<单元 id>`
116
+ 看它现在走到哪一步、`action=install` 装上并上板、`action=open` 只上板)—— 见 §7。
89
117
 
90
118
  ## 4 两个面的形状
91
119
 
@@ -118,62 +146,4 @@ export function apply(ctx) {
118
146
  **卡片必须在拿不到数据时也画得出来**(第一次读、卡在审批、平台没登录)。
119
147
  空数据下抛异常的卡片 = 用户眼里的"卡片一直不出来"。这是 `check:cards` 守的那条。
120
148
 
121
- ## 5 依赖从哪拿
122
-
123
- | 要什么 | 从哪拿 |
124
- |---|---|
125
- | 宿主侧要用的纯函数与常量(`param`、平台解析、工作区存储…) | `@dsh-workbench/shared/host`(写进 `dependencies`) |
126
- | 浏览器侧要的一切(hooks、组件、图标、文案工具) | `@dsh-workbench/core/client` 的 `kit` |
127
- | 类型 | `import type` 随便用(编译时擦除,不会进产物) |
128
- | 外壳本体 | `@dsh-workbench/core`(peer 依赖,装的时候要比版本) |
129
-
130
- ⚠️ **浏览器侧不许 `import` `@dsh-workbench/shared` 的运行值**(两个入口都不行):
131
- 它**不在**客户端构建的 external 默认表里(`module-tools/src/build.mjs`),
132
- import 运行值会被**打进你的包**(第二份实例),而且浏览器运行时也解析不到它。
133
- 宿主侧正相反:`@dsh-workbench/shared` 与 `/host` 都在 external 里,直接 import 才对。
134
-
135
- **单例只能有一份**:kit 里的 hooks 与容器组件**不许被复制进模块包**
136
- (各打一份的症状不是报错,而是"agent 改完面板不自己刷新")。它们只能从 `kit` 拿。
137
-
138
- ## 6 四条会被门禁拦住的写法
139
-
140
- | 别这么写 | 为什么 |
141
- |---|---|
142
- | 从 DOM + CSS 手搓一个 dsh 已经有的东西(按钮、弹窗、输入框、图标) | 重画就是第二份材质与交互(悬停、禁用、焦点环、色板),迟早跟 dsh 漂。**先从 `kit` 拿**;kit 里没有再自己写,并在 spec 里记理由 |
143
- | `import … from '@deepseek-ai/dsh-client-ui-primitives'` | 模块**不直接** import dsh 的包;dsh 原语由外壳经 `kit` 交给你。直接引会被判红(那是第二份实例) |
144
- | `import … from '@/shared/…'`、`../../workbench/src/…` | 装你的人手上没有工作台的源码。`@/` 只许指**你自己**的 `src/` |
145
- | 在公开包里写内网地址 / 内网域名 / 企业内部名 | 门禁会扫出来并指到行 |
146
-
147
- 另外两条不是"被拦",是纪律:**样式自带前缀 `wb-<unit>-`**(外壳的 `wb-*` 是全局类名,覆盖它等于两份规则打架);
148
- **`src/**` 里零注释** —— 要说的写进 `README.md` 或 spec。
149
-
150
- ## 7 装上,看一眼
151
-
152
- 模块造好、门禁绿了,**不要自己往 profile 里装**:工作台的 profile 目录不在你的可写根里。
153
- 把这件事交给用户点一下「装上看看」—— 那一跳在宿主侧完成(装 + 顺手在面板里打开它)。
154
-
155
- 两个必须记住的后果:
156
-
157
- - **那一跳一辈子只该走一次。** 包名一旦在 profile 里,再装一次只会得到"要重启"
158
- (同名重装一定要求重启,而且那一支连重载都不做)。
159
- - **装 ≠ 上板**:面板默认是空的,得在面板配置里把它打开 —— 这就是那一跳顺手做掉的第二件事。
160
-
161
- ## 8 改一行,看到它变
162
-
163
- 装完之后**不要再装一遍**。改源码 → `dsh-wb-module build` → 用户面板上那张卡自己变,
164
- 不重启、不刷新。为了让它成立:
165
-
166
- - **构建必须原地覆写产物**(现在是)。别为了"原子写"改成临时文件 + rename ——
167
- 宿主的监听只认 `change`,不认 `rename`,改了会**静默**不热换。
168
- - **改 `cordis.patch.yml`、改 `package.json` 里的 `dsh.client` / `exports["./client"]` 需要重启。**
169
- 这些是"模块怎么被装上"的声明,不是模块的内容。
170
- - 重启操作只有在极少数情况下才需要用户做。真需要时,说清**为什么**、**要重启哪个**。
171
-
172
- ## 9 发布
173
-
174
- **工坊里的模块一律 `private: true`,不发到任何源。** 模型写的代码在用户的机器上执行,
175
- 这是他自己要的东西、他自己的机器;把 `private` 摘掉是另一个决定,不属于 DIY。
176
-
177
- 真要给别人用:把 `private` 去掉,并**显式**写上发布目标
178
- (`publishConfig.access: "public"`,或指向内网源的 `publishConfig.registry`)——
179
- 两样都不写等于打算发到公共源,门禁会拦。
149
+ → 拿到依赖、装上、改一行、发出去:[WIRING.md](WIRING.md)。
@@ -7,31 +7,83 @@
7
7
 
8
8
  | 情况 | 判 |
9
9
  |---|---|
10
- | 他已经在浏览器里登录过那套系统 | **DIY 域** —— 用 opencli 复用他的登录去探 |
10
+ | 他已经在浏览器里登录过那套系统 | **DIY 域** —— 用 opencli 复用他的登录去探(下面四步) |
11
11
  | 要**新凭据**(新账号 / 密钥 / token) | **工程域** —— 停下来,说清要谁去配什么 |
12
- | 要起新服务 / 新进程 / 加新依赖 | **工程域** |
13
- | 数据在本地文件 / 一条已有命令 / 一个不要凭据的 GET 上 | **DIY 域** —— 直接造 |
14
12
 
15
13
  判的是「**要不要新凭据**」,不是「要不要接新平台」。复用已有登录的探查,正是把一大类需求留在 DIY 域的原因。
14
+ 其余几类(本地文件 / 已有命令 / 不要凭据的 GET → 直接造;新服务 / 新依赖 / 改外壳 → 工程域):[DESIGN.md](DESIGN.md) §0 那张全表。
16
15
 
17
16
  ## 四步
18
17
 
19
18
  1. **问出他在看什么(用他的词)** —— "你平时是在哪个网页 / 哪个软件里看这个的?" 记下他说的入口。
20
19
  **不要**问"你们用 argocd 还是 jenkins" —— 他多半不知道,也答不上来。
21
- 2. **探** —— 用 `opencli`。**它随工作台预装**:隔离运行时在 `$DSH_HOME/workbench/opencli/`,
22
- 启动体检自己装,**不碰**用户本机的 `~/.opencli`(工作台已经把它挂成命令,`bin/opencli-isolated` 是那层壳)。
23
- 目标是找到**承载数据的那一处**:列表页本身,或它背后的那一个 XHR。记下:命令 / URL、需要的参数、凭据从哪来。
24
- - 数据通常来自 **opencli 的某个插件**(`opencli <plugin> …`)—— 先看这台机器上哪些插件能用,
25
- **别自己拼接口**。
26
- - 隔离运行时没装好时它会自己说"opencli 的隔离运行时还没装好" —— 把体检那屏的原话转给用户,
27
- 别硬编 URL 猜(那正是"看起来能用、其实读不到"的来源)。
20
+ 2. **数据从哪来 —— 四层,从上往下试**(**用户不知道 opencli 是什么,这一整步是你的活**:
21
+ 别问他"你用哪个插件 / 有没有 API",他说的是"我想看 X",把 X 翻成关键词是你的活)。
22
+
23
+ ① **先看 opencli 自带的**:`workbench_diy`(`action=recon, keyword=<站点名 / 系统名 / 域名>`,
24
+ 也可以直接给 `url` 让它取域名)。它回这台机器上**已经能用**的能力:站点、命令、`strategy`
25
+ (PUBLIC / COOKIE / INTERCEPT / UI)以及**要不要用户的 Chrome 登录**,还有能直接照抄的那一行。
26
+ **有就直接复用:不用装、不用点头。** 官方那份发现指引:`opencli skills read opencli-usage`。
27
+ - **命中了要看全**:一个词常常翻出好几组(打 `arxiv` 会同时带出 `hf`、`semanticscholar`),
28
+ 只看第一组就会漏掉能互补的命令(引用数、相关推荐那类);
29
+ - **挑的时候优先 `public`**:不用他的 Chrome、不用登录、不用点头,卡也最容易验;
30
+ `cookie` / `ui` / `intercept` 的先把"他有没有登录"这件事摆到明面上。
31
+ - **一张卡背后可以有多条读**(甚至跨站点):design.json 用 `reads: [ … ]` 一条一行写全,
32
+ 每条各自的"参数 / 凭据 / 没登录长什么样 / 超时长什么样"都别省 —— 卡片上要按条给失败态。
33
+ ② **再看这台机器另装的插件**:recon 会一并列出来(另一面是 `opencli plugin list`)。用
34
+ `opencli <插件> <命令>`,参数用 `--help` 看。
35
+ ③ **官方有、这台机器没有** → 才考虑装:社区插件 `opencli plugin install github:user/repo`、
36
+ 外部 CLI `opencli external install <名字>`,或在模块清单里声明 `manifest.env.plugins`
37
+ (`installSource`:npm 包 / git / 本地)。**装是这台机器上的持久变更、而且会跑别人的代码 → 要用户
38
+ 点头**,优先内网或官方源。
39
+ ④ **都没有** → 才自己开发,而且两条官方流程都能用:
40
+ - 只读一次 → 下面那条 `probe`;
41
+ - 想长期用 → 给这个站点写 adapter:`opencli skills read opencli-adapter-author`(侦察 → 字段解码 →
42
+ 写 → verify);以后 adapter 失效:`opencli skills read opencli-autofix`。
43
+
44
+ **④ 才轮到探** —— 用工具:`workbench_diy`(`action=probe, url=<那套系统的地址>, script=<一段回 JSON 的脚本>`;
45
+ 页面里没有接口就给 `css=<选择器>`,走的是同一条固定 DOM 读,`{entries:[{text}]}` 直接回给你)。
46
+ 它用**用户浏览器里现有的登录**去读,并把"读不到"归一成阻断屏认的那几个码 —— 两拍(不在站点上就自己
47
+ 打开 `url` 再读一遍)、刚重启时的重试、平台码到阻断码的映射**都在工具里**,不用你记。
48
+ 目标是找到**承载数据的那一处**:列表页本身,或它背后的那一个 XHR。记下:URL、需要的参数、凭据从哪来。
49
+ 要手工看别的东西(列请求、点页面)时才直接用 `opencli`。
50
+ - **不知道数据在哪**时先跑一条侦察脚本(`script`):`performance.getEntriesByType('resource')`
51
+ 里筛 `/api/`、`.json` —— 有一串就是**接口读**(顺它去 fetch);一条都没有、页面上却有一整块
52
+ 表格 / 列表,那就是 **DOM 读**(换 `css` 读它)。这个判断一次就够,别反复试。
53
+ - 手工用 `opencli` 时记住:**它随工作台预装**(隔离运行时在 `$DSH_HOME/workbench/opencli/`,
54
+ 启动体检自己装,**不碰**用户本机的 `~/.opencli`);没装好时它会自己说"opencli 的隔离运行时
55
+ 还没装好" —— 把体检那屏的原话转给用户,别硬编 URL 猜(那正是"看起来能用、其实读不到"的来源)。
56
+ - 脚本的三条约定(工具就按这个判):不在站点上回 `{ ok: false, code: "not-on-site" }`;登录过期回
57
+ `{ ok: false, code: "not-logged-in" }`(工具归一成 `auth-required`);成功回 `{ ok: true, ...数据 }`。
58
+ - **`browser network --detail` 在会话里拿不到 body**:捕获缓存写在会话可写根之外
59
+ (`EPERM … opencli/home/.opencli/cache/browser-network`),只能列请求 key,`--detail` 永远
60
+ `cache_missing`。换这条:`opencli browser <session> eval "<一段 fetch 脚本>"` —— 在页面里用
61
+ 他的登录去读,返回 JSON。它既能当探数的手段,也常常**直接就是那条固化读**。
62
+ - **新 session 的 `eval` 会静默开一个 `about:blank` 标签页**(不是报错)—— 它落在 opencli 自己的后台容器窗里,不抢焦点、也不进用户当前窗。所以"读之前先判在不在
63
+ 那个站点"必须写在脚本第一行:`if (location.origin !== "https://…") return { ok: false, code: "not-on-site" }`。
64
+ 固化读的完整形状是**两拍**:不在站点上 → `open` 那个页 → 等一会儿(约 1 秒)再 eval 一遍。
65
+ 每一拍都由 shared 的 `backgroundLine` 拼(前缀 `env OPENCLI_WINDOW=background`)—— 别绕开它直接拼
66
+ `opencli browser …`:漏一拍就是把用户的 Chrome 顶到面前一次(见
67
+ [ADR-0036](../../../../docs/adr/0036-opencli-always-background.md))。
68
+ - **页面里没有接口时(DOM 读)不要用 `eval` 去 `document.querySelectorAll`**:产物边界判据会拦
69
+ (宿主产物里不许出现 `document.` / `window.` 字样),而且那样解析只能窝在字符串里、测不了。
70
+ 探的时候 `workbench_diy`(`action=probe, url=…, css=<选择器>`)就是这个口子;
71
+ 固化时走 `browserFind`,它内部用的就是 `opencli browser <session> find --css '<选择器>' --limit N --text-max M`,
72
+ 回 `{ entries: [{ text, tag, nth }] }` —— **一行的单元格按换行拼在 `text` 里**(`美元\n668.3\n…`),
73
+ 所以"切格 + 按列序取值"是两个纯函数,能单测。很多公开站点一条 XHR 都不发,就该走这条。
74
+ - **工作台刚重启时桥还没起来**:第一次读会以"命令跑不起来"失败(不是"数据读不到")——
75
+ 等一秒再读一次就好。超时**不**重试:那已经等满了。这条与上面两条一样,`browserRead` 里都写好了。
28
76
  3. **试一条最小读** —— 先证明"能拿到",再写模块:
29
77
  - 拿得到结构化输出 → 记下字段形状
30
78
  - 要登录但已过期 → 记下"未登录"长什么样
31
79
  - 超时 / 被墙 → 记下这两种失败的形状
32
80
  这一步的产出是**一条可重复的命令或请求**,不是"我探过了"。
33
- 4. **固化进模块** —— 把它写进模块的**宿主半侧**(一条命令,或只读路由里的一个 HTTP GET)。
34
- 之后卡片只依赖**这一条**;渲染怎么改都不动它。
81
+ 4. **固化进模块** —— 先把它照抄进 `design.json`(只有一条读写 `datasource{}`;一条卡有好几条读写 `reads[]`,一条一行;字段一样:哪条读 / 参数 / 凭据 /
82
+ 没登录长什么样 / 超时长什么样 / 当初那次调用的原文),再写进模块的**宿主半侧**:用 `@dsh-workbench/shared/host` 导出的
83
+ `browserRead({ shell, script, url, loginUrl })`(**接口读**:探明的那一段脚本原样搬进去就行 ——
84
+ 两拍、重试、平台码映射、`message` / `hint` / `detail` / `helpUrl` 四个字段都在它里面)或
85
+ `browserFind({ shell, css, url, session })`(**DOM 读**:它替你问"现在停在哪一页"(`browser get url`),
86
+ 不在就 `open` 再读一遍,失败时把 opencli 自己说的原因带出来),或者只读路由里的一个 HTTP GET。之后卡片只依赖**这一条**;渲染怎么改都不动它。
35
87
 
36
88
  `opencli` 是**侦察兵,不是日常通道**:探明之后,日常读必须是一条固定的、便宜的、能解释的调用。
37
89
 
@@ -47,6 +99,22 @@
47
99
 
48
100
  空数据下抛异常的卡片 = 用户眼里的"卡片一直不出来"。这条 `check:cards` 会拦(见 [REVIEW.md](REVIEW.md))。
49
101
 
102
+ ## 错误码不是随便写的(阻断屏按它换文案)
103
+
104
+ 外壳的阻断屏只认下面这几个码,`helpUrl` 才是把"重试"变成"去登录"的那一步。平台自己回什么码
105
+ (例:`AUTH_UNAUTHORIZED`)**别直接透传**,先映射到这张表里的一个:
106
+
107
+ | 你回的 code | 用户看到 |
108
+ |---|---|
109
+ | `auth-required` + `helpUrl` | "需要登录 XX" + 一颗**去登录** + 一颗**重试**(`helpUrl` 缺了就只剩重试) |
110
+ | `cli-timeout` | **一条细提示**(不换整卡)+ 重试 |
111
+ | `cli-exec-failed` | 整卡换成"读不到" + 重试,`hint` 写清他该去确认什么 |
112
+ | `permission-denied` + `helpUrl` | "没有权限" + 打开权限页 + 重试 |
113
+ | 其它 | 整卡换成你给的 `message` + 重试 |
114
+
115
+ 三个字段各有各的位置:`message` 是标题、`hint` 是下面那句能照做的话、`detail` 是折叠起来的原始
116
+ 输出 —— 写全了用户才知道"这一屏为什么长这样、下一步点哪"。
117
+
50
118
  ## 你自己的门禁会查
51
119
 
52
120
  - 数据源写在**宿主半侧**,卡片只消费它的返回;
@@ -47,6 +47,8 @@
47
47
  **三张表既是要问清的需求,也是最后的验收单** —— 表里每一格,他都要能在界面上见过或点过。
48
48
  表里不许有空格,也不许有「其他」那一行。
49
49
 
50
+ 它的落点是模块里的 `design.json`(命令与形状见本文 §5)—— **只留在对话里等于没定**。
51
+
50
52
  三步各自要问什么、要定什么、完成判据是什么:[STEPS.md](STEPS.md);
51
53
  每一步的细节:[PANEL.md](PANEL.md) · [TOOLS.md](TOOLS.md) · [LINKAGE.md](LINKAGE.md)。
52
54
 
@@ -67,7 +69,6 @@
67
69
  骨架是几行灰条;弹窗里哪一行是谁给的),表里用**图号**引用(A / B / C…)。
68
70
  **给状态起个名字、配个图标,都不算图例** —— 用户看不懂 `⛔` 是什么,
69
71
  但一眼看得懂"整张卡被换成了这一屏"。
70
- 画不成画面的(动作、agent 的下一步、时间性的"不变 / 自己变")才写词,并把意思说清。
71
72
 
72
73
  定稿前扫一遍全文,凡是**描述"看到 / 听到 / 发生"的句子**,都问一句:**这句能不能画成 ASCII?** 能就画。
73
74
  画不成的只有这几类,写词(并说清意思):纯动作("点一下去平台")、agent 的下一步、
@@ -76,7 +77,31 @@
76
77
  ## 4 做完的判据(都看用户那一侧)
77
78
 
78
79
  - 全程**用户只说过需求相关的话**:没有一次被问"用什么、放在哪、叫什么、怎么测"。
79
- - 三张表没有空格;表里每一格,他都在界面上见过或点过。
80
+ - 三张表没有空格(`dsh-wb-module design check` 绿);表里每一格,他都在界面上见过或点过,
81
+ 验收清单逐条打上了勾(`design accept`)。
80
82
  - 点头那条流程他真走过一次(允许或拒绝都算)。
81
83
  - 门禁是绿的 —— 那是 agent 自己的门槛,不用他关心。
82
84
  - 下次他再说一句"改一下",不用从头问:设计留下的记录还在。
85
+
86
+ ## 5 落点:`design.json`(机器形状)
87
+
88
+ 三张表 + 状态矩阵 + 数据源记录 + 验收清单住在模块根目录的 `design.json`,README 里那一段**由它生成**
89
+ (夹在 `<!-- dsh-wb:design:start/end -->` 之间,别手改 —— 下一次 `check` 会判红):
90
+
91
+ ```bash
92
+ pnpm exec dsh-wb-module design check # 空着的格?README 落后了?都在这儿报(红 = 还有没定的事)
93
+ pnpm exec dsh-wb-module design render # 改完 design.json,把 README 那一段重新生成
94
+ pnpm exec dsh-wb-module design accept # 用户逐条点头之后,把验收清单打勾
95
+ ```
96
+
97
+ 形状:`unit` / `name` / `oneLine` + `panel[]`(这一格 / 结论 / 去哪)+ `states[]`(**五态一个都不能少**,
98
+ key 固定 `idle|loading|ready|empty|unreadable`)+ `config[]` + `capabilities[]` + `linkage[]` +
99
+ `datasource{}`(只有一条读)或 `reads[]`(多条读,一条一行;字段一样)+ `acceptance[]`。
100
+ `config` / `capabilities` **允许空数组**("这份模块故意没有能力"是一种决定);其余空着就是没定的事。
101
+ `link` 这类**可选格**留空渲染成 `—`,不算空格。
102
+
103
+ 五态那五行是矩阵的行,**不能省**;但某一态对这份模块确实不适用时,写一句为什么就行 ——
104
+ `{ "key": "idle", "na": "读的是账号,不随项目变:没选项目时这张卡压根不渲染" }`,
105
+ 那一行不填 `sees` / `canDo` / `agentCan`,表里渲染成「(这份模块不适用)…」。
106
+ `capabilities` 是空数组时,`states[].agentCan` 也不用逐格填(渲染成 `—`):这份模块故意没有
107
+ agent 能力,那几格本来就没有内容可写,硬填只会逼出一句同义反复。
@@ -14,11 +14,8 @@
14
14
  | 他自己改了偏好 | 保存后立刻按新的显示 |
15
15
  | 别的窗口 / 另一个会话也在改 | 他这里也会变(不需要他刷新) |
16
16
 
17
- ⚠️ **第一条是后加的口径(2026-10-01)**:以前写的是"agent 只是看一眼 → 不动"。改成"看一眼也让
18
- **那一格**变",理由是用户口径:「agent 调用工具的时候面板数据也受益」—— agent 刚在对话里念了数字,
19
- 面板得是同一份,否则他到底信哪个。三档说清:**读了这一格 → 变**;**读了面板没有的 → 不动**;
20
- **只预览 → 不动**。机制与逐工具的表:工作台仓库里的 `docs/adr/0023-read-tools-claim-facts.md`
21
- (工程域的实现落点见工作台仓库 `.agents/skills/module-design/IMPLEMENTATION.md` §5)。
17
+ ⚠️ **"读 → 也变"这条是用户定的口径**:理由是「agent 调用工具的时候面板数据也受益」—— agent 刚在对话里
18
+ 念了数字,面板得是同一份,否则他到底信哪个。
22
19
 
23
20
  **一次说清动的是哪一格**:整块板、某一张卡、还是某一行。写"刷新一下"不算说清 —— 那会让实现的人整卡重读。
24
21
 
@@ -97,17 +94,50 @@
97
94
 
98
95
  三句都写进能力清单(用户看的是"哪一格会跟着动")。
99
96
 
100
- → 实现与逐工具那张表:本文 §1 · `docs/adr/0023-read-tools-claim-facts.md` §2.3
101
- (工程域的实现落点见工作台仓库 `.agents/skills/module-design/IMPLEMENTATION.md` §5)。
102
-
103
- ## 8 范例
104
-
105
- - **读工具也让面板动**(哪一格动、颗粒度到哪、什么值能直接交、什么值只能点名):
106
- `docs/adr/0023-read-tools-claim-facts.md` §2.3 是逐工具那张表;
107
- `docs/adr/0021-agent-tools-hand-their-value-to-the-panel.md`(值交接),
108
- `docs/adr/0022-refresh-has-a-visible-floor.md`(刷新要看得见)。
109
- - 收起模块与"能力有更新"这件事的来龙去脉:`docs/adr/0020-tool-surface-narrows-at-assembly.md`。
110
- - "改完面板为什么自己会新":`docs/adr/0012-facts-are-routes.md`。
111
- - 用户视角的落地形态:`.scratch/tool-value-handoff/spec.md`(逐套件的联动验收)、
112
- `.scratch/cmdb-module/spec.md` §6(联动;长事在跑的那一行在 §6.1、图 R–V 在 §6.2)、
113
- `.scratch/coding-analysis/spec.md` §6(每一颗"交给 agent"各带哪一类 + 宿主注入的正文)。
97
+ → 实现与逐工具那张表:本文 §1 · 工作台仓库里的 `docs/adr/0023-read-tools-claim-facts.md` §2.3
98
+ (工程域的实现落点见 `.agents/skills/module-design/IMPLEMENTATION.md` §5)。
99
+
100
+ ## 8 宿主侧的四个口子(实现形状)
101
+
102
+ 上面说的"哪一格变",落到代码就四个可选字段;**不写 `factsOf` 的模块,面板那一格不会自己新**。
103
+
104
+ ```ts
105
+ factsOf: (call) => (call.source === REBUILD_TOOL ? [factOfRoute(HEALTH_ROUTE)] : undefined),
106
+ ```
107
+
108
+ * `call.source` 是**能力名**、`call.args` 是它这次收到的参数;返回"这次动到了哪几条事实",
109
+ 返回 `[]` = 涉及但没有具体事实,`undefined` = 跟面板无关(例:明细、日志那种面板上没有的东西)。
110
+ * 事实名不要自己拼字符串:用 `factOfRoute('<unit>/<路由>')`(`@dsh-workbench/shared`)——
111
+ **卡片读的那条路由对应哪条事实,就是它算出来的那条**。
112
+ * **读能力也要认领**(§1 与 §7 的口径):agent 只是读了一次,那一格也该重读。
113
+ * 长任务动完也要认领:`longTasks.create({ …, facts: () => [factOfRoute(HEALTH_ROUTE)] })`。
114
+
115
+ ```tsx
116
+ const rev = useFactRevision(factOfRoute(HEALTH_ROUTE));
117
+ const resource = useApiResource(ROUTE, query, rev, {});
118
+ ```
119
+
120
+ 卡片的 `useApiResource` **默认就订阅** `factOfRoute(route)`(`use-api-resource.ts` 里那句
121
+ `options.fact ?? factOfRoute(route)`),所以多数情况你什么都不用接;用户自己按的那个动作做完,
122
+ 用 `useFactInvalidate()()` 把这条事实标脏(cmdb 的发布就是这么做的)。
123
+
124
+ ```ts
125
+ context: { kinds: ['module'], render: async (ref) => `模块 ${ref.parts[0]}:…` },
126
+ warmReads: () => [['<unit>', '<target>']],
127
+ ```
128
+
129
+ * `context`:agent 上下文里引用你这一格时(例:`diy-doctor/module/fx-rate`),由你把它渲染成人话;
130
+ `kinds` 列出你认哪几类,**参数形状不对就 `throw`**(说清"形状不对",别默默回一段错的)。
131
+ 判据只查形状(`kinds` 非空、`render` 是函数),**不会拿假参数调它** —— 那些参数只有真引用才有。
132
+ * `warmReads`:**只给"走 opencli 适配器"的读用**。条目就是那条 opencli 命令的 argv,外壳在启动的
133
+ preflight 里拼成 `opencli <argv…> --site-session persistent -f json` 先跑一遍(把站点登录态与首屏焐热)。
134
+ 例:成熟模块 cmdb 的 `[['cmdb', 'enterprises']]`(`module-cmdb/src/model/warm.ts`,还带测试)。
135
+ **本机读盘、browser bridge、直接打接口的读一律不要写这里** —— 写了外壳会去跑一条不存在的命令,
136
+ preflight 只会给个 warn;这些读没有"冷启动"可省,焐不热任何东西。工坊里四个模块就是刻意不写的。
137
+ * `values`(`deps.values`,`ValueHandoff`):把一次写的结果交给别的单元 —— `values.hand({fact, key}, 值)`,
138
+ 另一边 `values.take({fact, key})` 取;`key` 自己起(例:一次发布的结果给联动那一格用)。
139
+
140
+ ## 9 范例
141
+
142
+ 范例与来龙去脉都在**工作台仓库里**:`.agents/skills/module-design/EXAMPLES.md` §3
143
+ (逐工具那张表、值交接、刷新下限、收起模块的来龙去脉)—— 工坊会话里够不到那儿。