@dsh-workbench/core 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/client.js +689 -149
  2. package/cordis.patch.yml +32 -0
  3. package/dist/client/client.body.js +689 -149
  4. package/dist/client/index.js +689 -149
  5. package/dist/host/index.js +3638 -2104
  6. package/dist/types/app/client.d.ts +120 -3
  7. package/dist/types/app/layouts/approval-detail.d.ts +13 -0
  8. package/dist/types/app/layouts/panel-hint.d.ts +3 -0
  9. package/dist/types/app/layouts/rightbar/market-hint.d.ts +16 -0
  10. package/dist/types/app/layouts/rightbar/market-rows.d.ts +2 -3
  11. package/dist/types/app/layouts/sidebar/RecordFooterAction.d.ts +16 -0
  12. package/dist/types/app/layouts/sidebar/record-footer.d.ts +4 -0
  13. package/dist/types/modules/panels/client.d.ts +1 -0
  14. package/dist/types/modules/panels/model/diy-browser-act.d.ts +2 -39
  15. package/dist/types/modules/panels/model/diy-browser-tools.d.ts +39 -0
  16. package/dist/types/modules/panels/model/diy-browser.d.ts +1 -14
  17. package/dist/types/modules/panels/model/diy-components.d.ts +3 -0
  18. package/dist/types/modules/panels/model/diy-components.test.d.ts +1 -0
  19. package/dist/types/modules/panels/model/diy-observers.d.ts +11 -0
  20. package/dist/types/modules/panels/model/diy-observers.test.d.ts +1 -0
  21. package/dist/types/modules/panels/model/diy-page.d.ts +1 -0
  22. package/dist/types/modules/panels/model/diy-record-bar.test.d.ts +1 -0
  23. package/dist/types/modules/panels/model/diy-record-capture.d.ts +27 -0
  24. package/dist/types/modules/panels/model/diy-record-capture.test.d.ts +1 -0
  25. package/dist/types/modules/panels/model/diy-record-failure.test.d.ts +1 -0
  26. package/dist/types/modules/panels/model/diy-record-routes.d.ts +2 -0
  27. package/dist/types/modules/panels/model/diy-record-routes.test.d.ts +1 -0
  28. package/dist/types/modules/panels/model/diy-record-scripts.d.ts +6 -0
  29. package/dist/types/modules/panels/model/diy-record-text.d.ts +89 -0
  30. package/dist/types/modules/panels/model/diy-record-text.test.d.ts +1 -0
  31. package/dist/types/modules/panels/model/diy-record-tools.d.ts +2 -0
  32. package/dist/types/modules/panels/model/diy-record.d.ts +76 -0
  33. package/dist/types/modules/panels/model/diy-record.test.d.ts +1 -0
  34. package/dist/types/modules/panels/model/diy-scripts.d.ts +0 -63
  35. package/dist/types/modules/panels/model/diy-spec.d.ts +5 -0
  36. package/dist/types/modules/panels/model/diy-spec.test.d.ts +1 -0
  37. package/dist/types/modules/panels/model/handoff.d.ts +1 -0
  38. package/dist/types/modules/panels/unit.d.ts +20 -12
  39. package/dist/types/shared/api/host/diy-acts.d.ts +4 -0
  40. package/dist/types/shared/api/host/diy-components.d.ts +54 -0
  41. package/dist/types/shared/api/host/diy-debug-act.d.ts +24 -7
  42. package/dist/types/shared/api/host/diy-recon.d.ts +10 -0
  43. package/dist/types/shared/api/host/diy-record-act.d.ts +19 -0
  44. package/dist/types/shared/api/host/diy-workshop.d.ts +1 -0
  45. package/dist/types/shared/api/host/mount-units.d.ts +4 -0
  46. package/dist/types/shared/hooks/use-diy-record.d.ts +8 -0
  47. package/package.json +3 -3
  48. package/scripts/seed-workshop.sh +42 -2
  49. package/skills/workbench-module-diy/BUILD.md +39 -17
  50. package/skills/workbench-module-diy/DATASOURCE.md +4 -1
  51. package/skills/workbench-module-diy/DESIGN.md +40 -8
  52. package/skills/workbench-module-diy/FAMILY.md +55 -0
  53. package/skills/workbench-module-diy/HOOKS.md +95 -0
  54. package/skills/workbench-module-diy/KIT.md +18 -6
  55. package/skills/workbench-module-diy/PANEL.md +9 -5
  56. package/skills/workbench-module-diy/REVIEW.md +117 -49
  57. package/skills/workbench-module-diy/SKILL.md +42 -9
  58. package/skills/workbench-module-diy/STEPS.md +1 -1
  59. package/skills/workbench-module-diy/TOOLING.md +10 -4
  60. package/skills/workbench-module-diy/TOOLS.md +56 -204
  61. package/skills/workbench-module-diy/WIRING.md +17 -8
  62. package/skills/workbench-module-diy/WRITES.md +131 -0
  63. package/dist/types/modules/panels/model/diy-act.d.ts +0 -4
  64. package/dist/types/modules/panels/model/diy-look.d.ts +0 -15
  65. package/dist/types/modules/panels/model/diy-net.d.ts +0 -3
  66. /package/dist/types/modules/panels/model/{diy-act.test.d.ts → diy-browser-act.test.d.ts} +0 -0
  67. /package/dist/types/modules/panels/model/{diy-look.test.d.ts → diy-browser-tools-shot.test.d.ts} +0 -0
  68. /package/dist/types/modules/panels/model/{diy-net.test.d.ts → diy-browser-tools.test.d.ts} +0 -0
@@ -1,9 +1,26 @@
1
1
  import type { DebugIntent, DebugState, DebugStore, PostResource } from '@dsh-workbench/shared/host';
2
2
  export declare const DEBUG_NOTICE_KIND = "workbench-diy";
3
+ export interface NoticeTextBlock {
4
+ readonly type: 'text';
5
+ readonly text: string;
6
+ }
7
+ export interface NoticeSource {
8
+ readonly kind: string;
9
+ readonly form: 'notice';
10
+ readonly summary: string;
11
+ }
12
+ export interface NoticeMessage {
13
+ readonly role: 'user';
14
+ readonly id: string;
15
+ readonly content: readonly NoticeTextBlock[];
16
+ readonly source: NoticeSource;
17
+ }
18
+ export declare function noticeMessage(text: string, summary: string, kind?: string): NoticeMessage;
19
+ export declare function isNoticeMessage(value: unknown): value is NoticeMessage;
3
20
  export interface NoticeAgent {
4
21
  readonly status: string;
5
- inject(message: unknown): void;
6
- followup(message: unknown): void;
22
+ inject(message: NoticeMessage): void;
23
+ followup(message: NoticeMessage): void;
7
24
  }
8
25
  export interface AgentsLookup {
9
26
  get(id: string): NoticeAgent | undefined;
@@ -11,20 +28,20 @@ export interface AgentsLookup {
11
28
  export declare function debugNoticeText(intent: DebugIntent, state: DebugState): string;
12
29
  export declare function debugNoticeSummary(intent: DebugIntent, state: DebugState): string;
13
30
  export declare function debugNotice(intent: DebugIntent, state: DebugState): {
14
- message: unknown;
31
+ message: NoticeMessage;
15
32
  summary: string;
16
33
  };
17
34
  export type NoticeMode = 'wake' | 'hold' | 'drop';
18
35
  export type NoticeDelivery = 'followup' | 'inject' | 'held' | 'none' | 'skipped-idle';
19
- export declare function holdNotice(sessionId: string, message: unknown): void;
20
- export declare function takeHeldNotices(sessionId: string): unknown[];
21
- export declare function deliverDebugNotice(agents: AgentsLookup | undefined, sessionId: string, message: unknown, mode?: NoticeMode): NoticeDelivery;
36
+ export declare function holdNotice(sessionId: string, message: NoticeMessage): void;
37
+ export declare function takeHeldNotices(sessionId: string): NoticeMessage[];
38
+ export declare function deliverDebugNotice(agents: AgentsLookup | undefined, sessionId: string, message: NoticeMessage, mode?: NoticeMode): NoticeDelivery;
22
39
  export interface DebugActDeps {
23
40
  readonly store: () => DebugStore | undefined;
24
41
  readonly agents: () => AgentsLookup | undefined;
25
42
  readonly now?: (() => number) | undefined;
26
43
  readonly notice?: ((intent: DebugIntent, state: DebugState) => {
27
- message: unknown;
44
+ message: NoticeMessage;
28
45
  summary: string;
29
46
  }) | undefined;
30
47
  }
@@ -1,3 +1,10 @@
1
+ export interface ReconArg {
2
+ readonly name: string;
3
+ readonly type: string;
4
+ readonly required: boolean;
5
+ readonly value: string;
6
+ readonly help: string;
7
+ }
1
8
  export interface ReconCommand {
2
9
  readonly command: string;
3
10
  readonly site: string;
@@ -9,6 +16,8 @@ export interface ReconCommand {
9
16
  readonly browser: boolean;
10
17
  readonly example: string;
11
18
  readonly columns: readonly string[];
19
+ readonly args: readonly ReconArg[];
20
+ readonly preview: boolean;
12
21
  }
13
22
  export interface ReconSite {
14
23
  readonly site: string;
@@ -28,6 +37,7 @@ export interface ReconAnswer {
28
37
  readonly next: string;
29
38
  readonly text: string;
30
39
  }
40
+ export declare function invocableOf(command: ReconCommand): string;
31
41
  export declare function manifestCommandsOf(raw: string): readonly ReconCommand[];
32
42
  export declare function pluginNamesOf(entries: readonly string[]): readonly string[];
33
43
  export declare function pluginCommandsOf(files: readonly string[]): readonly string[];
@@ -0,0 +1,19 @@
1
+ import type { PostResource, RecordIntent, RecordState, RecordStore } from '@dsh-workbench/shared/host';
2
+ import { type AgentsLookup, type NoticeMessage } from './diy-debug-act';
3
+ export declare const RECORD_NOTICE_KIND = "workbench-diy-record";
4
+ export declare function recordNoticeText(intent: RecordIntent, state: RecordState): string;
5
+ export declare function recordNoticeSummary(intent: RecordIntent, state: RecordState): string;
6
+ export declare function recordNotice(intent: RecordIntent, state: RecordState): {
7
+ message: NoticeMessage;
8
+ summary: string;
9
+ };
10
+ export interface RecordActDeps {
11
+ readonly store: () => RecordStore | undefined;
12
+ readonly agents: () => AgentsLookup | undefined;
13
+ readonly now?: (() => number) | undefined;
14
+ readonly notice?: ((intent: RecordIntent, state: RecordState) => {
15
+ message: NoticeMessage;
16
+ summary: string;
17
+ }) | undefined;
18
+ }
19
+ export declare function recordActResource(deps: RecordActDeps): PostResource;
@@ -9,6 +9,7 @@ export interface WorkshopModule {
9
9
  export declare function workshopPackagesDir(ctx: DshHostContext): string | null;
10
10
  export declare function insideWorkshop(root: string, target: string): boolean;
11
11
  export declare function workshopModules(packagesDir: string, installed?: readonly string[]): readonly WorkshopModule[];
12
+ export declare function readDesignOfModule(dir: string): unknown;
12
13
  export declare function inWorkshopSession(ctx: DshHostContext, workspace: string | null): boolean;
13
14
  export interface WorkshopSync {
14
15
  readonly state: 'ok' | 'syncing' | 'failed';
@@ -4,6 +4,9 @@ import { type RouteTable } from '@dsh-workbench/shared/host';
4
4
  import type { ContextRenderer, UnitMountDeps, UnitMountResult, UnitCall } from '@dsh-workbench/shared/host';
5
5
  import type { HostUnit } from './unit-registry';
6
6
  import { type UnitReadiness } from './unit-readiness';
7
+ export declare function declaredNotesOf(plans: Iterable<{
8
+ readonly declaredNotes: ReadonlySet<string>;
9
+ }>): ReadonlySet<string>;
7
10
  export interface UnitFailure {
8
11
  readonly id: string;
9
12
  readonly reason: string;
@@ -35,6 +38,7 @@ export interface UnitsPlan {
35
38
  }>;
36
39
  readonly failed: readonly UnitFailure[];
37
40
  readonly warmReads: () => readonly (readonly string[])[];
41
+ readonly declaredNotes: ReadonlySet<string>;
38
42
  readonly disposeLongTasks: () => void;
39
43
  }
40
44
  interface NamedEntry {
@@ -0,0 +1,8 @@
1
+ import type { RecordState } from '@dsh-workbench/shared';
2
+ import type { HostWrite } from './use-host-write';
3
+ export declare const DIY_RECORD_ROUTE = "panels/diy-record";
4
+ export declare const DIY_RECORD_FACT = "diy-record";
5
+ export declare function useDiyRecordState(): RecordState | null;
6
+ export declare function useDiyRecordAct(): HostWrite<{
7
+ text?: string;
8
+ }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/core",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "个人工作台:Agent 工作台外壳(左栏 + 常驻对话 + 自研右侧数据面板)。",
5
5
  "type": "module",
6
6
  "icon": "./icon.svg",
@@ -42,7 +42,7 @@
42
42
  "title": "Workbench",
43
43
  "description": "个人工作台"
44
44
  },
45
- "//scripts": "包内不跑 check:docs —— 那份文档管理块住在仓库根的 docs/ARCHITECTURE.md,由根 `pnpm run check` 用 --config 指到本包的 arch.config.mjs 校验。check 与 prepublishOnly 都走 -w 跑根那一整套:包内自己串一份会漏掉 lint(react-hooks 那几条规则只在这里跑)、check:docs 与模块工具包的十一条判据 —— 2026-10-16 实测踩到(渲染期写 ref,包内全绿、根上一跑就红)。",
45
+ "//scripts": "包内不跑 check:docs —— 那份文档管理块住在仓库根的 docs/ARCHITECTURE.md,由根 `pnpm run check` 用 --config 指到本包的 arch.config.mjs 校验。check 与 prepublishOnly 都走 -w 跑根那一整套:包内自己串一份会漏掉 lint(react-hooks 那几条规则只在这里跑)、check:docs 与模块工具包的十三条判据 —— 2026-10-16 实测踩到(渲染期写 ref,包内全绿、根上一跑就红)。",
46
46
  "//dsh.bundle.patch": "一份 preset 就是一个补丁层:它 insert 一行 @deepseek-ai/dsh-agent-preset,config.plugins 是整表插件清单。diy.patch.yml 是**生成的**(scripts/gen-diy-preset.mjs,从 dsh 出厂那份 standard.patch.yml 派生),selfcheck 有一条判据盯着它有没有过期。",
47
47
  "dsh": {
48
48
  "bundle": {
@@ -87,7 +87,7 @@
87
87
  "access": "public"
88
88
  },
89
89
  "dependencies": {
90
- "@dsh-workbench/shared": "0.3.2"
90
+ "@dsh-workbench/shared": "0.3.4"
91
91
  },
92
92
  "scripts": {
93
93
  "build": "node scripts/build.mjs && node scripts/build-types.mjs",
@@ -102,6 +102,24 @@ target_version() {
102
102
  printf '%s\n' "$version"
103
103
  }
104
104
 
105
+ # 外壳 manifest 里钉的那份 dsh UI 版本(模块模板默认带的那个 devDependency,见
106
+ # `.scratch/dsh-primitives-face/spec.md` §3.1)。源优先**基准**(正在跑的那份外壳),
107
+ # 没有才退回跑脚本这份副本 —— 与 link_workshop_skills 同一条口径。
108
+ # 读不到就留空:调用方按"不写这一项"处理(模块 `init` 那边有自己的兜底常量与一句人话)。
109
+ shell_primitives_version() {
110
+ local root="$SHELL_ROOT"
111
+ [ -n "$root" ] && [ -f "$root/package.json" ] || root="$REPO_DIR"
112
+ [ -f "$root/package.json" ] || return 0
113
+ node -e '
114
+ const fs = require("node:fs");
115
+ try {
116
+ const pinned = JSON.parse(fs.readFileSync(process.argv[1], "utf8"))
117
+ .devDependencies?.["@deepseek-ai/dsh-client-ui-primitives"];
118
+ process.stdout.write(typeof pinned === "string" ? pinned : "");
119
+ } catch {}
120
+ ' "$root/package.json" 2>/dev/null || true
121
+ }
122
+
105
123
  # 三个包的装法**只有一处真相**:仓库副本 link 进本仓,安装副本按目标版本装。
106
124
  # skeleton 与 sync 都调它 —— 两份规则漂开的症状是"根清单是 link、overrides 是 ^",
107
125
  # 于是同一个工坊里两代并存(正是要防的那件事),而且不报错。
@@ -124,6 +142,15 @@ compute_specs() {
124
142
  TOOLS_SPEC="^$version"
125
143
  SHARED_SPEC="^$version"
126
144
  fi
145
+ # dsh 原语(模块模板默认带的那个):**精确版**,与模块自己那份 devDependency 对齐 ——
146
+ # 类型面要对齐对账过的那一版(`.scratch/dsh-primitives-face/spec.md` §3.1)。
147
+ # 空 = 读不到外壳 manifest,就**不写这一项**(模块 `init` 有自己的兜底常量)。
148
+ local primitives_version
149
+ primitives_version="$(shell_primitives_version)"
150
+ PRIMITIVES_SPEC=""
151
+ if [ -n "$primitives_version" ]; then
152
+ PRIMITIVES_SPEC="$primitives_version"
153
+ fi
127
154
  }
128
155
 
129
156
  # 日志**只有一处**去处:$DSH_HOME/workbench/seed.log(冻结接口)。
@@ -395,8 +422,8 @@ run_sync() {
395
422
  log_line "→ ⚠️ 自愈失败:${reason}"
396
423
  return 1
397
424
  fi
398
- node - "$DIY_DIR/package.json" "$CORE_SPEC" "$TOOLS_SPEC" "$SHARED_SPEC" <<'NODE'
399
- const [file, core, tools, shared] = process.argv.slice(2);
425
+ node - "$DIY_DIR/package.json" "$CORE_SPEC" "$TOOLS_SPEC" "$SHARED_SPEC" "$PRIMITIVES_SPEC" <<'NODE'
426
+ const [file, core, tools, shared, primitives] = process.argv.slice(2);
400
427
  const fs = require('node:fs');
401
428
  const manifest = JSON.parse(fs.readFileSync(file, 'utf8'));
402
429
  const want = {
@@ -405,6 +432,11 @@ const want = {
405
432
  '@dsh-workbench/shared': shared,
406
433
  };
407
434
  manifest.devDependencies = { ...(manifest.devDependencies ?? {}), ...want };
435
+ // dsh 原语(模块模板默认带的那个):只进 devDependencies,**不进 overrides** ——
436
+ // 它不是 @dsh-workbench 的包,覆盖它没有意义(那是"多个模块各装一份"的另一种病)。
437
+ if (primitives !== undefined && primitives !== '') {
438
+ manifest.devDependencies['@deepseek-ai/dsh-client-ui-primitives'] = primitives;
439
+ }
408
440
  manifest.pnpm = {
409
441
  ...(manifest.pnpm ?? {}),
410
442
  overrides: { ...(manifest.pnpm?.overrides ?? {}), ...want },
@@ -556,6 +588,13 @@ if [ -f "$DIY_DIR/package.json" ]; then
556
588
  echo "→ DIY 工坊已存在,跳过建骨架(要重来就先删掉 ${DIY_DIR})"
557
589
  else
558
590
  echo "→ 建 DIY 工坊 $DIY_DIR"
591
+ # 原语那一行可能没有(读不到外壳 manifest 时不写):空行在 JSON 里无害。
592
+ # ⚠️ 这里是 `if` 不是 `[ … ] && …`:后者在 `set -e` 下,测试为假时整条语句返回 1,
593
+ # 脚本会**当场退出**(骨架只建到一半、自愈也不起了)—— 实测被 ④ 那一格抓到。
594
+ PRIMITIVES_LINE=""
595
+ if [ -n "$PRIMITIVES_SPEC" ]; then
596
+ PRIMITIVES_LINE=" \"@deepseek-ai/dsh-client-ui-primitives\": \"$PRIMITIVES_SPEC\","
597
+ fi
559
598
  cat >"$DIY_DIR/package.json" <<EOF
560
599
  {
561
600
  "name": "dsh-workbench-diy",
@@ -567,6 +606,7 @@ else
567
606
  "@dsh-workbench/core": "$CORE_SPEC",
568
607
  "@dsh-workbench/module-tools": "$TOOLS_SPEC",
569
608
  "@dsh-workbench/shared": "$SHARED_SPEC",
609
+ ${PRIMITIVES_LINE}
570
610
  "@types/node": "^24.19.0",
571
611
  "@types/react": "^19.3.0",
572
612
  "esbuild": "^0.28.2",
@@ -51,7 +51,7 @@ pnpm exec dsh-wb-module init <unit> --dir <工坊>/packages/module-<unit> --temp
51
51
  `<unit>` 是小写短横线标识(如 `my-thing`),它同时是单元 id、包名后缀、卡片样式前缀。
52
52
  **它必须全局唯一** —— 和工作台里其它模块、和外壳里已有的单元都不能撞;撞了装上去当场抛。
53
53
 
54
- 生成的骨架是**能直接跑的**(十一条判据 + 三个脚本开箱全绿;门禁还在,模板改坏了当场报)。
54
+ 生成的骨架是**能直接跑的**(十三条判据 + 六个脚本开箱全绿;门禁还在,模板改坏了当场报)。
55
55
 
56
56
  **两个模板,按需要挑**(加 `--template <名字>`):
57
57
 
@@ -62,25 +62,36 @@ pnpm exec dsh-wb-module init <unit> --dir <工坊>/packages/module-<unit> --temp
62
62
 
63
63
  ⚠️ `diy` 那份是**给别人照抄的**,所以它自己也必须开箱全绿 —— 门禁会**两个模板各 init 一遍、各跑一遍全套**。
64
64
 
65
- 骨架里的文件各有其职(`minimal` 十一份,`diy` 多一份 `design.json`,见下面那张 ⚠️):
65
+ 骨架是**分层多文件**的(不是单文件):目录**按需要出现** —— 空壳可以只有 `components/`,
66
+ 有状态逻辑才需要 `model/`(`diy` 两样都有,`minimal` 不硬造 `model/`)。骨架里的文件各有其职
67
+ (`minimal` 是空壳那几份,`diy` 多一份 `design.json`、`src/model/`、`src/components/OverviewCard.test.ts` 与 `types/styles.d.ts`,见下面那张 ⚠️):
66
68
 
67
69
  | 文件 | 职责 |
68
70
  |---|---|
69
- | `package.json` | 自述清单:包名、`dsh.bundle.patch`、`dsh.client`、`dsh.workbench.unit`、`dsh.workbenchFaces{host,client}`(工具链字段,门禁必填)、四个脚本、peer 依赖 |
71
+ | `package.json` | 自述清单:包名、`dsh.bundle.patch`、`dsh.client`(`external` 默认带 `@dsh-workbench/core/client` 与 dsh 原语那行)、`dsh.workbench.unit`、`dsh.workbenchFaces{host,client}`(工具链字段,门禁必填)、六个脚本、peer 依赖;原语那行由 `init` 盖精确版([KIT.md](KIT.md) §8) |
72
+ | `arch.config.mjs` | 模块自己的**架构方言**:**只有一行**工厂调用 `moduleArch()`(从 `@dsh-workbench/module-tools/arch` 来)。层序表 / 两个运行时 / 阈值只有那一份,随工具链同代 |
73
+ | `eslint.config.mjs` | 模块自己的**语法卫生方言**:**只有两行** —— `moduleLint({ rootDir: import.meta.dirname })`(从 `@dsh-workbench/module-tools/lint` 来;`rootDir` 把 tsconfig 根钉在这个模块上,多份配置并存时 typescript-eslint 才猜得出)。规则 / ignores 只有那一份,随工具链同代;要放行某条就从 `moduleLint({ rules, ignores })` 开口子,别改方言那些块(判据比整份 8 块,逐块点名),也别手写一份自己的(自己 import 方言再照抄整份则分不出来) |
70
74
  | `cordis.patch.yml` | 把自己插进 profile 的那一行 |
71
- | `src/host.ts` | 宿主面:`manifest` + `mount()`(路由 / 工具) |
72
- | `src/client.tsx` | 浏览器面:`manifest` + `card` |
75
+ | `src/model/**` | 纯逻辑:类型、换算、状态机(`diy` 是 `overview.ts` + 配对测试)。两个运行时都能读,不碰 react / kit / dsh / `node:*` |
76
+ | `src/components/**` | 浏览器侧的视图件,**样式与它同目录**(`X.module.css`;`diy` 是 `OverviewCard.tsx`)。整片算 client 面,宿主侧碰不得 |
77
+ | `src/host.ts` | 宿主面:`manifest` + `mount()`(路由 / 工具);类型从 `src/model/` 引,同一个概念不留两处真相 |
78
+ | `src/client.tsx` | 浏览器面:`manifest` + `card` —— **只剩面**(注册 + 把 props 交给 `components/` 那个组件) |
73
79
  | `src/messages.ts` | zh / en 文案;键必须带 `<unit>.` 前缀,两种语言同构 |
74
- | `src/plugin.test.ts` | 单测:两侧都把自己的面注册进了外壳 |
80
+ | `src/plugin.test.ts` | 单测:两侧都把自己的面注册进了外壳(`diy` 还多一条宿主路由) |
81
+ | `src/components/X.test.ts` | 组件自己的渲染断言(`diy` 把 `OverviewCard` 的五态各画一遍拆在这儿。**测试文件不吃方言的 S16「文件 200 行」**(2026-10-19 起内置豁免,后缀式与 `__tests__/` 两种命名都算),所以并回一个文件也不会红 —— 拆开只为读着顺) |
75
82
  | `vitest.config.ts` | 测试配置(`@/modules/<unit>/` 的别名) |
76
83
  | `scripts/smoke.mjs` | `selfcheck`:把 `client.js` 当浏览器加载器跑一遍,看卡片真进没进外壳 |
77
84
  | `tsconfig.json` | `paths` 里 `@/modules/<unit>/*` → `./src/*`,值必须是**相对路径** |
78
85
  | `types/globals.d.ts` | 经典 JSX 运行时的全局类型垫片 |
86
+ | `types/styles.d.ts` | CSS Modules 的类名表声明(`declare module '*.module.css'`)。`globals.d.ts` 顶层有 import、是模块,那里的 `declare module` 会变成模块增强而不是环境声明 —— 用了 `X.module.css` 就必须有这一份(`diy` 有) |
79
87
  | `design.json` | 三张表 + 状态矩阵 + 数据源记录 + 验收清单(**唯一真相**;README 里那一段由它生成) |
80
88
  | `README.md` | 这个模块自己的说明 |
81
89
 
82
- 改哪一个:**画什么改 `src/client.tsx`,文案改 `src/messages.ts`**,需要能力再动 `src/host.ts`。
83
- 模板已经把卡片套好了外壳的卡壳(`kit.components.Card`)、三态与测试;你只改里面。
90
+ 改哪一个:**画什么改 `src/components/*.tsx` + 它同目录的 `*.module.css`,纯逻辑(换算 / 状态机 / 类型)
91
+ 改 `src/model/**`,文案改 `src/messages.ts`;`src/client.tsx` 只留注册那一层**,需要能力再动 `src/host.ts`。
92
+ 模板已经把组件套好了外壳的卡壳(`kit.components.Card`)、三态与测试;你只改里面。
93
+ **宿主侧不许 import `src/components/**`** —— 那是跨运行时,`arch` 判据当场红(层号拦不住它:
94
+ components 第 2 层、host 第 10 层)。
84
95
 
85
96
  ⚠️ **三张表落在 `design.json`**(`diy` 骨架自带一份填好的,`minimal` 里没有 —— 用它就照
86
97
  [DESIGN.md](DESIGN.md) §5 建一个):把它改成你刚问出来的那份,**再改代码**。
@@ -90,9 +101,11 @@ README 里 `<!-- dsh-wb:design:start/end -->` 之间那一段**由它生成**(
90
101
 
91
102
  ```bash
92
103
  dsh-wb-module build # 出 client.js 与 dist/host.js
93
- dsh-wb-module check # 十一条判据,全跑
94
- dsh-wb-module check --only cards # 单跑一条(contracts / bundles / source / primitives /
95
- # kit / cards / standalone / markers / alias / style)
104
+ dsh-wb-module check # 十三条判据,全跑
105
+ dsh-wb-module check --only cards # 单跑一条(contracts / protocol / bundles / source / arch / lint /
106
+ # primitives / kit / cards / standalone / markers / style / alias)
107
+ dsh-wb-module check --only primitives [--all|--json] # 查询口:dsh 原语里有什么
108
+ dsh-wb-module lint [--fix] # 跑模块自己那份 eslint 配置;--fix 写回
96
109
  ```
97
110
 
98
111
  三张表的填/查/生成/打勾见 [DESIGN.md](DESIGN.md) §5。
@@ -100,25 +113,34 @@ dsh-wb-module check --only cards # 单跑一条(contracts / bundles / sourc
100
113
  ⚠️ 写成 `pnpm exec dsh-wb-module …`(工坊里 PATH 上也有同名入口,由启动器挂);
101
114
  模块目录里更省事的是那几个脚本(`pnpm build` / `pnpm selfcheck`)。
102
115
 
103
- `package.json` 里那前四个脚本都要留着(契约判据会查):
116
+ `package.json` 里那前四个脚本都要留着(契约判据会查);`check` 是第五条(`arch` 方言的 M09 要这个脚本名);`lint` 是第六条(`lint` 判据要它):
104
117
 
105
118
  ```bash
106
119
  pnpm build # 构建
107
- pnpm typecheck # tsc --noEmit
120
+ pnpm typecheck # tsc --noEmit(init 后先 pnpm install)
108
121
  pnpm test # vitest
109
122
  pnpm selfcheck # 构建 + 把 client.js 当加载器跑一遍(`diy` 骨架里还多一条 `design check`)
123
+ pnpm check # dsh-wb-module check:十三条判据
124
+ pnpm lint # dsh-wb-module lint:语法卫生(`pnpm lint -- --fix` 写回)
110
125
  ```
111
126
 
112
- **自己的门禁自己过,红了别抛给用户。** 十一条判据在 [REVIEW.md](REVIEW.md) 里逐条翻了人话。
127
+ **自己的门禁自己过,红了别抛给用户。** 十三条判据在 [REVIEW.md](REVIEW.md) 里逐条翻了人话。
113
128
  ⚠️ **`check` 不替你跑 `pnpm test`**(它只看形状与产物):状态级的渲染、换算、边界都在你自己的
114
129
  `test` 里 —— 骨架开箱就带一份,改了数据源与渲染就顺手把它跑一遍,别让红线留在包里。
115
130
  ⚠️ **工坊里恒有两条是「不适用」,那不是「通过」**:`独立构建` 要外壳源码在场才跑得起来,
116
131
  `公开包无内部标识` 只扫要发出去的包(DIY 模块一律 `private`)。汇总行下面会把
117
- 这次没查的那几条逐条列出来;所以「8 通过 · 2 不适用」不等于十一条全绿 —— 别把这句话念给用户听。
132
+ 这次没查的那几条逐条列出来;所以「8 通过 · 2 不适用」不等于十三条全绿 —— 别把这句话念给用户听。
118
133
 
119
- 装上板那一步不是命令,是一套 agent 工具(一个动词一把):`workbench_diy_list` 看工坊里有什么、
134
+ 装上板那一步不是命令,是一套 agent 工具:`workbench_diy_list` 看工坊里有什么、
120
135
  `workbench_diy_status`(`unit=<单元 id>`)看它现在走到哪一步、`workbench_diy_install` 装上并上板、
121
- `workbench_diy_show` 只上板 —— 见 §7。**上完板看一眼**:`workbench_diy_look`(只截那张卡 + 按你点的项读样式)—— 用户那一页不会被改动,但他得把工作台切到前台。卡片有没有报错先看 `workbench_diy_panel` 的「崩过 / 读到了」;真要这一页的控制台,用 `workbench_diy_logs_watch` → 做那件事 → `workbench_diy_logs_read`(钩子有上限,见 [REVIEW.md](REVIEW.md) 四步取证的 4)。
136
+ `workbench_diy_show` 只上板 —— 见 §7。**上完板自己逐条走一遍(这就是验收那一拍)**:浏览器那 14 把
137
+ (`state` / `get html` / `find` / `click` / `hover` / `type` / `fill` / `keys` / `scroll` / `wait` / `eval` /
138
+ `screenshot` / `network` / `dialog`,argv 与结果原样转发,全表见 [FAMILY.md](FAMILY.md))打的是
139
+ **用户当前那一页**上工坊那块板里的那张卡 —— 用户那一页不会被换掉,但他得把工作台切到前台。
140
+ **看屏 / 动屏那一族(浏览器那 14 把 / `logs_*`)要先 `workbench_diy_debug_start`**(`unit=…`;
141
+ 会请他点头一次)—— 没有这场约定它们一律当场拒;不看屏了先 `_pause`、收工 `_stop`。
142
+ 卡片有没有报错先看 `workbench_diy_panel` 的「崩过 / 读到了」;真要这一页的控制台,用
143
+ `workbench_diy_logs_watch` → 做那件事 → `workbench_diy_logs_read`(钩子有上限,见 [REVIEW.md](REVIEW.md) 四步取证的 4)。
122
144
 
123
145
  ## 4 两个面的形状
124
146
 
@@ -31,7 +31,8 @@
31
31
  - **一张卡背后可以有多条读**(甚至跨站点):design.json 用 `reads: [ … ]` 一条一行写全,
32
32
  每条各自的"参数 / 凭据 / 没登录长什么样 / 超时长什么样"都别省 —— 卡片上要按条给失败态。
33
33
  ② **再看这台机器另装的插件**:recon 会一并列出来(另一面是 `opencli plugin list`)。用
34
- `opencli <插件> <命令>`,参数用 `--help` 看。
34
+ `opencli <插件> <命令>`,参数用 `--help` 看 —— 也可以 `workbench_diy_spec`(`command='<site> <name>'`)
35
+ 一次问全:它把 `--help -f json` **原样**回(插件与内置都能问)。
35
36
  ③ **官方有、这台机器没有** → 才考虑装:社区插件 `opencli plugin install github:user/repo`、
36
37
  外部 CLI `opencli external install <名字>`,或在模块清单里声明 `manifest.env.plugins`
37
38
  (`installSource`:npm 包 / git / 本地)。**装是这台机器上的持久变更、而且会跑别人的代码 → 要用户
@@ -90,6 +91,8 @@
90
91
  `browserFind({ shell, css, url, session })`(**DOM 读**:它替你问"现在停在哪一页"(`browser get url`),
91
92
  不在就 `open` 再读一遍,失败时把 opencli 自己说的原因带出来),或者只读路由里的一个 HTTP GET。之后卡片只依赖**这一条**;渲染怎么改都不动它。
92
93
 
94
+ 这一步只管**读**。要 agent 真去**改**东西(提交 / 发布 / 建单 / 改配置),那半边走 [WRITES.md](WRITES.md)。
95
+
93
96
  `opencli` 是**侦察兵,不是日常通道**:探明之后,日常读必须是一条固定的、便宜的、能解释的调用。
94
97
 
95
98
  ## 固化时一并写掉三态
@@ -1,7 +1,7 @@
1
1
  # 设计口径:先说清他要什么,再谈怎么做
2
2
 
3
3
  这一步与落点无关 —— 无论模块最后落进工作台外壳里,还是落进一个独立模块包,
4
- **问法、产出、验收都是同一套**。工程域与 DIY 域共用这一份。
4
+ **问法、产出是同一套;验收归谁按域分**(见 §4)。工程域与 DIY 域共用这一份。
5
5
 
6
6
  ## 0 先判域
7
7
 
@@ -27,7 +27,7 @@
27
27
  | 谁 | 管什么 |
28
28
  |---|---|
29
29
  | **用户** | 他看得见、能决定的事:要看到什么、要能自己配什么、要 agent 替他做什么、哪些要他点头、话怎么说、取舍 |
30
- | **agent** | 一切实现:怎么落、叫什么、放在哪、用什么、怎么验 —— **这些不问用户** |
30
+ | **agent** | 一切实现与验收(验收按域分,见 §4):怎么落、叫什么、放在哪、用什么、怎么在真卡片上逐条验 —— **这些不问用户** |
31
31
 
32
32
  四条对话纪律:
33
33
 
@@ -44,7 +44,8 @@
44
44
  | 2 agent | 他要 agent 替他做什么?哪几件他必须先点头? | 能力清单 + 全场景 + 点头场景 | 每件事 × 每种情况:agent 得到什么 / 用户看到什么 |
45
45
  | 3 联动 | 他凭什么相信面板是新的? | 联动场景 | 每种「谁在动」×「面板什么时候变 / 变成什么样 / 用户看到什么」 |
46
46
 
47
- **三张表既是要问清的需求,也是最后的验收单** —— 表里每一格,他都要能在界面上见过或点过。
47
+ **三张表既是要问清的需求,也是最后的验收单** —— 表里每一格,都要能在真卡片上见过或点过
48
+ (DIY 域由 agent 自己走,工程域请他看一次,见 §4)。
48
49
  表里不许有空格,也不许有「其他」那一行。
49
50
 
50
51
  它的落点是模块里的 `design.json`(命令与形状见本文 §5)—— **只留在对话里等于没定**。
@@ -74,12 +75,42 @@
74
75
  画不成的只有这几类,写词(并说清意思):纯动作("点一下去平台")、agent 的下一步、
75
76
  时间性的词("不变 / 自己变 / 立刻")、纯映射表(能力名 ↔ 用户会怎么说)。
76
77
 
77
- ## 4 做完的判据(都看用户那一侧)
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
+
103
+ ## 4 做完的判据
78
104
 
79
105
  - 全程**用户只说过需求相关的话**:没有一次被问"用什么、放在哪、叫什么、怎么测"。
80
- - 三张表没有空格(`dsh-wb-module design check` 绿);表里每一格,他都在界面上见过或点过,
106
+ - 三张表没有空格(`dsh-wb-module design check` 绿);表里每一格,都在真卡片上见过或点过,
81
107
  验收清单逐条打上了勾(`design accept`)。
82
- - 点头那条流程他真走过一次(允许或拒绝都算)。
108
+ - **验收归谁按域分**:**DIY 域(工坊会话)的验收是 agent 自己走的** —— 调试族
109
+ (`workbench_diy_debug_start` + 浏览器那 14 把枚举工具 + `logs_*`,全表见 [FAMILY.md](FAMILY.md))
110
+ 就是取证面;用户不必在场、也不必逐条点头,走完把"装好了、面板上哪一格"告诉他即可。
111
+ **工程域没有那一族**,仍按「打开 :3081 → 右栏第 N 张卡 → 点 X → 应该看到 Y」请他看一次。
112
+ - 要用户点头的**写能力(审批)那一拍仍是他的**:他真用一次时点,agent 代不了 ——
113
+ 清单里写清"这条要你真用一次",别替他打勾。
83
114
  - 门禁是绿的 —— 那是 agent 自己的门槛,不用他关心。
84
115
  - 下次他再说一句"改一下",不用从头问:设计留下的记录还在。
85
116
 
@@ -91,13 +122,14 @@
91
122
  ```bash
92
123
  pnpm exec dsh-wb-module design check # 空着的格?README 落后了?都在这儿报(红 = 还有没定的事)
93
124
  pnpm exec dsh-wb-module design render # 改完 design.json,把 README 那一段重新生成
94
- pnpm exec dsh-wb-module design accept # 用户逐条点头之后,把验收清单打勾
125
+ pnpm exec dsh-wb-module design accept # agent 自己在真卡片上逐条走通之后,把验收清单打勾
95
126
  ```
96
127
 
97
128
  形状:`unit` / `name` / `oneLine` + `panel[]`(这一格 / 结论 / 去哪)+ `states[]`(**五态一个都不能少**,
98
129
  key 固定 `idle|loading|ready|empty|unreadable`)+ `config[]` + `capabilities[]` + `linkage[]` +
99
- `datasource{}`(只有一条读)或 `reads[]`(多条读,一条一行;字段一样)+ `acceptance[]`。
130
+ `datasource{}`(只有一条读)或 `reads[]`(多条读,一条一行;字段一样)+ `writes[]`(**写通道**:一个动作可以打几条写,一条一行;只有一条写也可写 `write{}`;**没有写通道的模块不写就行**)+ `acceptance[]`。
100
131
  `config` / `capabilities` **允许空数组**("这份模块故意没有能力"是一种决定);其余空着就是没定的事。
132
+ (写通道的字段与取证口径见 [WRITES.md](WRITES.md);README 里那一节叫「写通道探明记录」,位置在「数据源探明记录」与「验收」之间。)
101
133
  `link` 这类**可选格**留空渲染成 `—`,不算空格。
102
134
 
103
135
  `design.json` 是**设计底稿**:只装人话。"这条能力在哪些工作区出现"**哪里都别写** ——
@@ -0,0 +1,55 @@
1
+ # 工坊会话里那一族:33 把(agent 手上真实有什么)
2
+
3
+ [TOOLS.md](TOOLS.md) 讲**你写的模块**的能力怎么写;这一份讲**工坊会话里 agent 手上真实有什么**。
4
+ 浏览器那一半照 opencli 的子命令枚举,参数只有 `args`(原样转发);为什么这么定见仓内 ADR-0062(这份指引不链仓内路径)。
5
+
6
+ ## 1 前置(别的都排它后面)
7
+
8
+ - 只在**工坊会话**里出现,且**那块板画在屏上**;看 / 动 / 网络 / 日志那一族还要**先立调试约定** `workbench_diy_debug_start`(请用户点头一次):**没有约定一律当场拒、零命令**;不看屏 `_debug_pause`、收工 `_debug_stop`(顺手收网络与日志钩子)。
9
+ - 会话钉死 `workbench`、每条带 `--window background`;`_list`/`_status`/`_panel`/`_components`/`_recon`/`_probe`/`_spec`/`_install`/`_show` 不碰他那一屏,不用开。**写那一族(录制交接,见 §5)不走这条路**:它自己开一条 `workbench-rec` 会话,`_record_prepare` 要点头一次,`_record_read` / `_record_detail` / `_record_stop` 不问。
10
+
11
+ ## 2 14 把浏览器工具(工具名 → argv;前缀都是 `workbench_diy_`)
12
+
13
+ `_state`→`state` · `_html`→`get html` · `_find`→`find` · `_click`→`click` · `_hover`→`hover` · `_type`→`type` · `_fill`→`fill` · `_keys`→`keys` · `_scroll`→`scroll` · `_wait`→`wait` · `_eval`→`eval` · `_screenshot`→`screenshot` · `_network`→`network` · `_dialog`→`dialog`
14
+
15
+ 干什么:名字直说;不直观的几个 —— `_state` URL + 标题 + 可点元素(ref 来源)、`_html` HTML(`--as json` 树)、`_find` 回 `{matches_n, entries[]}`、`_network` 收发包(`--follow`;body 拿不到)、`_screenshot` 写文件(`clip` 只裁一块)。
16
+
17
+ **参数**(`scope` / `in` 默认都是 `page`):
18
+
19
+ - 公共:`unit?`(哪张卡;可省 —— 只剩一个模块就用它。**要卡作用域就得给**:写 `scope: 'card'` / `in: 'card'` 或用 `$card` 却不给 ⇒ **当场拒、零命令**)· `args?: string[]`(原样接在 `opencli browser workbench <verb…>` 后)· `timeoutMs?`(默认 30000;≤ 0 或非数也按默认)。
20
+ - `scope: 'card'` 只给吃 CSS 的**七把**:`_find`(`--css`)· `_html`(`--selector`,没给它我们补)· `_click`/`_hover`/`_type`/`_fill`(**位置参数 0**)· `_wait`(**只在 `wait selector`**,位置参数 1)—— `card` = 给那个目标挂 `#wb-card-<unit> .wb-card `。
21
+ - `in: 'card'` 只 `_eval`(注入卡根 `root`,**脚本自己 `return`**);`clip`(CSS px)只 `_screenshot`(拍完本地裁);`$card` **任何 `args` 里**都替换成 `#wb-card-<unit> .wb-card`。
22
+
23
+ **拿回**:`{ ok, exitCode, timedOut, truncated, stdout, stderr }` + `workshop`/`unit`/`verb`/`argv`;`_screenshot` 另给 `file`/`cropped`/`cropReason`。`ok` 只管进程,事实全在 `stdout`(报错也是一份 `{"error":{…}}`,`ok` 可能仍 `true`);不抠 JSON / 不翻中文 / 不做 ok 归一,**要参数先问 `--help`**。**"原样"有折损**:`args` 里空串 / 纯空白 / 非字符串丢掉、留下的词前后 trim、`stdout`/`stderr` 也 trim ⇒ **`_fill` 清空不了输入框**、**`_type` 敲不了空格**,没有别的口子就如实说做不到。
24
+
25
+ ## 3 老工具 → 新命令
26
+
27
+ | 老 | 新 |
28
+ |---|---|
29
+ | `_look` | `state` + `get html` + `eval` + `screenshot`(卡图靠 `clip`,上游没有 selector) |
30
+ | `_reload` / `_viewport` | `eval 'location.reload()'` / `screenshot --width/--height` |
31
+ | `_select` | **没了**(上游只认原生 `<select>`);下拉两拍:卡内点触发器 → `scope: 'page'` + `--role menuitem` 按名字点 |
32
+ | `_net_*` | `network`(`--follow` 臂上、收工 `_debug_stop` 顺手收;**取 body 走 `_eval` 的 `fetch`**) |
33
+ | `_click` `_hover` `_type` `_fill` `_keys` `_scroll` / `_logs_*` | 同名,参数改成原样转发(`console` 是空桩,控制台只能我们做) |
34
+
35
+ ## 4 如实边界
36
+
37
+ - `screenshot` **没有上游 selector**:卡图先 `eval` 量盒子再填 `clip`;不给路径我们补到工坊同级的 `shots/<unit|page>-<时间戳>.png`(否则回 base64);要裁的块不在视口里就退回整屏图,原因在 `cropReason`。
38
+ - `state` **没有 `scope`**(卡内定位用 `find --css '$card …'`);**没有右键 / contextmenu**;ref 只在**一次快照**里有效,别跨调用存;`_dialog` 的 `accept|dismiss` 不校验;`_network --detail` **取不到 body**(缓存落在会话可写根之外)—— 走 `_eval` 里的 `fetch`。
39
+ - 录制交接那条**会话捕获**(`_record_prepare` 起的:先短暂 `network --follow` 只做"臂上",然后每 3 秒一次**非跟随** `browser <session> network --all --raw`,**落盘前先按 URL 去掉静态资源与 `data:`**(含 node 就过滤、没有就原样落),一轮一行追加到工坊同级的 `shots/rec-<unit>.jsonl`)**带 body**:非跟随那条会把 live 缓冲排空,连 `request.body`(入参)与 `body`(出参)一起打出来;跟随那条只打形状(只有方法 / URL / 状态 / ct / size / 时间),所以只拿它臂上、装完就杀。它盖**整场**、在浏览器那一层,比页面钩子全(钩子只认那一页自己的 `fetch`/`XHR` —— 应用提前把 `window.fetch` 存走、或在 iframe / Worker / WebSocket 里发,它就看不见)。**不许改成"用 opencli 默认过滤"**:那层是**按响应 ct 白名单**判的(只留 json/xml/text-plain/javascript),"按下写就立刻按结束"那条**回包还没到、ct 为空**的写会被当场丢掉 —— 实测 127 条里它只留 33 条、那条写没了;按 URL 去静态则是 127 → 30 条、那条写还在。
40
+ - 另外 20 把:台账 `_list`/`_status`/`_install`/`_show`、组件与 kit 清单 `_components`(不填给清单与类型面根,`name=Button` 回它的类型文件与声明)、宿主运行态 `_panel`/`_status`、数据源 `_recon`/`_probe`、`_spec`(问一条 opencli 命令的形状,**插件也适用**)、`_logs_*`、约定本身 `_debug_*`、录制交接 `_record_*`(§5)。
41
+
42
+ ## 5 录制交接(四把):抓那一次"在外站页面上点出来的写",清单 → 挑一条 → 看原文
43
+
44
+ | 工具 | 干什么 |
45
+ |---|---|
46
+ | `_record_prepare` | 开一扇窗**顶到最前**(**要用户点头一次**)、装浮条、臂上捕获。参数 `unit` / `url` / `doing`,可选 `expect:{method,urlContains}`;**不给 `url` 当场拒**,**跨域也当场拒**(那个模块的 `design.json` 说过读源时,只许同域录;还没说过就不拦)。返回带 `wrote: false` |
47
+ | `_record_read` | 取那一段的**清单**:`rows`(每行 `index` / `key` / `at` / `method` / `url` / `status` / `ct` / `size` / `source` / `hasRequest` / `hasResponse` / `likelyWrite` / `marked`)· `hookCaught`(钩子这一段**原始**条数)· `capturedLines` / `capturedInSegment` / `capturedOutside` · `dropped` · `noBody` · `nav` · `captureRound`(会话捕获**最近一轮**成没成:`ok` / `fail` / 空 = 还没回)。**清单一条都不删、也不回体**:页面钩子(`[t0,t1]` 段)与会话捕获(整场按段切)合并,**同名只配掉一条钩子行**(缺的 key / 入参 / 出参由捕获那条补上);捕获里两条同 method+url 是**两次真调用,都留着**。**`expect` 不再是过滤器**:它只给命中的行标 ⭐,⭐ 排在前面(不然只列最后十几条时它可能不在正文里);不设 expect 时 ⭐ = 非 GET 或带 request body。要哪一条的原文就带它的 `key`(或 `index`)去 `_record_detail`。没按开始 / 还在录都如实说,不假装"没有请求"。可选 `waitMs`(默认 0 = 立刻回,帽 15 分钟):等不到就回 `timedOut` 并挂一条 notice 叫醒你 —— **一场就是一个周期(开始 → 结束)**,结束之后要再录就**重新 `_record_prepare` 一场**(同场没有"再按一次开始"这回事)。**他按「结束录制」不用他再跟你说一声**:录制期间宿主每 3 秒自己看一眼那一页(浮条在目标页里、推不到宿主;宿主能叫醒你的通道只有 `deliverDebugNotice`),一看到 `finished` 就推状态并发一条 notice 叫你来 `read` |
48
+ | `_record_detail` | 取**某一条**的原文:入参(`request.body`)与出参(`response body`),外加请求头(可能含凭据,别落进 `design.json`)。参数 `key`(清单里那一条,原样给、含 `#2` 这种后缀)或 `index`(清单里的序号,只在那一场还开着时可用);**同一个 key 跨轮会重名**(opencli 的 key 计数器每批重置),所以再带上清单里那行的 `at` 才钉得住是哪一次(不给就取最早那条,并在正文里说重名几次)。可选 `unit` —— 那一场**收工之后**要给 unit,我去读 `shots/rec-<unit>.jsonl.last`(`_record_stop` 会把原文留成 `.last`,下一场 prepare 开录时清掉它)。取不到时如实说"两边都没有",并给兜底:DevTools 复制 curl。可选 `maxBody`(默认 60000 字符,0 = 不限) |
49
+ | `_record_stop` | 停捕获、收掉那个标签、清状态(**收工前先读一次捕获文件**,与 read 同一套切段 / 合并;**原文另存成 `.last`**,所以收工之后还能 `_record_detail`;没挪成就不说"原文留在…")。回一段能直接落 `design.json` 的原文(`probeCommand`)—— **只认第一条带 body 的**;一条都没有就回空串,并如实说"只有方法 / URL,body 拿不到 —— 补到 body 再落"(没 body 的写命令落下去是一条**会打错的**命令)。**先 read(或先 detail 完)再 stop** |
50
+
51
+ 左栏底部那一格给他的键(等出手 `[取消]` / 录制中 `[停]`)只**给 agent 发一条消息**,不直接改页面那一半(`[停]` 有意不动状态);**录完那一档没有键** —— agent 自己来取、自己收工。
52
+ 一场过期(TTL 15 分钟)或台账被换走,**不会把采集循环留在地上**:看门那一拍看到台账没了就停捕获 + 收标签,
53
+ 循环本身也被宿主 pid 兜着(宿主重起后孤儿循环自己退);`prepare` 开新一场前也先对同一落点收一次。
54
+ 细则、三颗键与如实边界(`fetch`/XHR 之外抓不到、50 条 / 16 KB 上限、一次一场 15 分钟)
55
+ 都在 [WRITES.md](WRITES.md) 步骤 3;纪律同那一份的「三条写纪律」。