@dsh-workbench/core 0.3.4 → 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 (44) hide show
  1. package/client.js +1243 -78
  2. package/dist/client/client.body.js +1243 -78
  3. package/dist/client/index.js +1243 -78
  4. package/dist/host/index.js +623 -841
  5. package/dist/types/app/client.d.ts +201 -60
  6. package/dist/types/modules/panels/model/diy-context.d.ts +4 -5
  7. package/dist/types/modules/panels/model/diy-observers.d.ts +2 -2
  8. package/dist/types/modules/panels/model/diy-page.d.ts +1 -1
  9. package/dist/types/modules/panels/unit.d.ts +3 -4
  10. package/dist/types/shared/api/context-chips.d.ts +20 -0
  11. package/dist/types/shared/api/host/diy-components.d.ts +17 -6
  12. package/dist/types/shared/api/host/diy-workshop.d.ts +0 -3
  13. package/dist/types/shared/api/host/preset-scope.d.ts +16 -0
  14. package/dist/types/shared/api/host/tool-scope.d.ts +6 -2
  15. package/dist/types/shared/api/host/workshop-root.d.ts +7 -0
  16. package/dist/types/shared/components/BlockerState.d.ts +12 -0
  17. package/dist/types/shared/components/PanelFoot.d.ts +9 -0
  18. package/dist/types/shared/components/Tip.d.ts +8 -0
  19. package/dist/types/shared/hooks/bundle-queries.d.ts +12 -0
  20. package/dist/types/shared/hooks/fact-transports.d.ts +4 -0
  21. package/dist/types/shared/hooks/panel-state-report.d.ts +12 -0
  22. package/dist/types/shared/hooks/use-api-resource.d.ts +22 -0
  23. package/dist/types/shared/hooks/use-blocker-actions.d.ts +8 -0
  24. package/dist/types/shared/hooks/use-capabilities.d.ts +55 -0
  25. package/dist/types/shared/hooks/use-capability-gate.d.ts +16 -0
  26. package/dist/types/shared/hooks/use-card-summary.d.ts +7 -0
  27. package/dist/types/shared/hooks/use-diy-debug.d.ts +13 -0
  28. package/dist/types/shared/hooks/use-diy-record.d.ts +13 -0
  29. package/dist/types/shared/hooks/use-facts.d.ts +10 -0
  30. package/dist/types/shared/hooks/use-host-write.d.ts +12 -0
  31. package/dist/types/shared/hooks/use-long-task.d.ts +8 -0
  32. package/dist/types/shared/hooks/use-unit-board.d.ts +17 -0
  33. package/dist/types/shared/hooks/use-visible-busy.d.ts +17 -0
  34. package/package.json +2 -2
  35. package/skills/workbench-module-diy/DATASOURCE.md +2 -2
  36. package/skills/workbench-module-diy/FAMILY.md +5 -4
  37. package/skills/workbench-module-diy/KIT.md +105 -100
  38. package/skills/workbench-module-diy/PANEL.md +19 -11
  39. package/skills/workbench-module-diy/REVIEW.md +4 -4
  40. package/skills/workbench-module-diy/SKILL.md +5 -4
  41. package/skills/workbench-module-diy/TOOLING.md +2 -2
  42. package/skills/workbench-module-diy/TOOLS.md +1 -1
  43. package/skills/workbench-module-diy/WIRING.md +2 -1
  44. package/skills/workbench-module-diy/HOOKS.md +0 -95
@@ -6,36 +6,71 @@ import { deflateSync, inflateSync } from "node:zlib";
6
6
  import { spawn } from "node:child_process";
7
7
  import os from "node:os";
8
8
  //#region ../shared/dist/config/constants.js
9
+ /** 外壳插件的包名:浏览器模块表与宿主按它注册、认领这枚插件。 */
9
10
  var PLUGIN_ID = "@dsh-workbench/core";
11
+ /** 与外壳同批发布的兄弟包名:`release.ts` 用它判一版三包是否都已在源上。 */
10
12
  var RELEASE_SIBLINGS = ["@dsh-workbench/shared", "@dsh-workbench/module-tools"];
13
+ /** 外壳宿主 HTTP API 的根前缀;prefs / actions / provision 等路由都挂在它下面。 */
11
14
  var API_PREFIX = "/workbench/api";
15
+ /** 模块偏好读写的路由前缀,挂在外壳 API 下:走写通道,拒写回 write-refused。 */
12
16
  var PREFS_PREFIX = `${API_PREFIX}/prefs`;
17
+ /** 事实(facts)SSE 事件流的端点路径;前端 `EventSource` 按它带 `since` 游标续传。 */
13
18
  var EVENTS_PATH = "/workbench/events";
19
+ /** 写动作的路由前缀,挂在外壳 API 下:模块的写通道 POST 到这里。 */
14
20
  var ACTIONS_PREFIX = `${API_PREFIX}/actions`;
21
+ /** 机器供给(provision)的路由前缀:只收 POST,装机时用它铺一份运行环境。 */
15
22
  var PROVISION_PREFIX = `${API_PREFIX}/provision`;
23
+ /** 面板运行态上报的路径段:崩溃与卡片收尾分别 POST 到 `panel-state/crash`、`panel-state/cards`。 */
16
24
  var PANEL_STATE_PATH = "panel-state";
25
+ /** DIY 调试动作的路径段;宿主按它登记 `/workbench/api/diy-debug` 前缀。 */
17
26
  var DIY_DEBUG_PATH = "diy-debug";
18
27
  `${DIY_DEBUG_PATH}`;
28
+ /** DIY 录制交接的路径段;宿主按它登记 `/workbench/api/diy-record` 前缀。 */
19
29
  var DIY_RECORD_PATH = "diy-record";
20
30
  `${DIY_RECORD_PATH}`;
31
+ /** 面板运行态的路由前缀,挂在外壳 API 下:上报口用它,拒报回 report-refused。 */
21
32
  var PANEL_STATE_PREFIX = `${API_PREFIX}/${PANEL_STATE_PATH}`;
33
+ /** 钉住的 opencli 插件版本:`opencli-runtime` 按它校验、取那一份。 */
22
34
  var OPENCLI_PIN = "1.8.8";
35
+ /** 面板套件的 dsh 服务名(`workbenchPanels`):宿主经它取套件公布的清单与订阅。 */
23
36
  var PANELS_SERVICE = "workbenchPanels";
37
+ /** 单元花名册服务的 dsh 服务名(`workbenchUnits`):模块经它认领自己的单元。 */
24
38
  var UNITS_SERVICE = "workbenchUnits";
39
+ /** 面板运行态服务的 dsh 服务名(`workbenchPanelState`):宿主注册,读口按它取。 */
25
40
  var PANEL_STATE_SERVICE = "workbenchPanelState";
41
+ /** DIY 调试约定服务的 dsh 服务名(`workbenchDiyDebug`):调试动作走它,不走 HTTP。 */
26
42
  var DIY_DEBUG_SERVICE = "workbenchDiyDebug";
43
+ /** DIY 录制交接服务的 dsh 服务名(`workbenchDiyRecord`):`diy-acts.ts` 经它收发录制三拍。 */
27
44
  var DIY_RECORD_SERVICE = "workbenchDiyRecord";
45
+ /** 会话投影服务的 dsh 服务名(`sessionProjections`):宿主经 `stateOf(session, 'agentPreset')` 读那一档。 */
46
+ var SESSION_PROJECTIONS_SERVICE = "sessionProjections";
47
+ /** 单次 opencli 调用的默认超时(毫秒);调用点没传 `timeoutMs` 时按它算。 */
28
48
  var OPENCLI_TIMEOUT_MS = 12e4;
49
+ /** 启动预热站点那一跳的超时(毫秒);超时按未热对待,不挡面板。 */
29
50
  var WARM_TIMEOUT_MS = 2e4;
51
+ /** 装 / 卸模块包的超时(毫秒):pnpm 与取包内容都按它算。 */
30
52
  var INSTALL_TIMEOUT_MS = 3e5;
53
+ /** 查 registry 版本那一次请求的超时(毫秒)。 */
31
54
  var VERSION_CHECK_TIMEOUT_MS = 1e4;
55
+ /** 单元探测结果的缓存有效期(毫秒)。 */
32
56
  var PROBE_TTL_MS = 3e4;
57
+ /** 跑本地 git 命令的超时(毫秒);短于对外的 `OPENCLI_TIMEOUT_MS`。 */
33
58
  var GIT_TIMEOUT_MS = 1e4;
34
59
  var MARKER = new RegExp(`@工作台:([a-z][a-z0-9-]*:[A-Za-z0-9._~%#/-]+)`, "g");
35
60
  var TRAILING_PUNCTUATION = /[.,;:!?]+$/;
61
+ /**
62
+ * 把引用拼回标记体里的那一段文本(不含 @ 与标记名)。
63
+ * @param ref 要拼的引用。
64
+ * @returns 形如 kind:part/part;分段里的 / 不转义,所以分段名本身不能含 /。
65
+ */
36
66
  function encodeRef(ref) {
37
67
  return `${ref.kind}:${ref.parts.join("/")}`;
38
68
  }
69
+ /**
70
+ * 把 kind:part/part 文本读回引用。
71
+ * @param text 待解析文本。
72
+ * @returns 没冒号、冒号在首尾、种类不是小写字母开头、或冒号后为空时给 undefined;不抛异常。
73
+ */
39
74
  function decodeRef(text) {
40
75
  const at = text.indexOf(":");
41
76
  if (at <= 0 || at === text.length - 1) return void 0;
@@ -48,6 +83,11 @@ function decodeRef(text) {
48
83
  parts
49
84
  };
50
85
  }
86
+ /**
87
+ * 从一段文本里按出现顺序捞出所有标记引用,同一个引用只留第一次。
88
+ * @param text 任意文本;null / undefined 当空串。
89
+ * @returns 解析不出来的标记会被丢掉,所以结果只含合法引用,可能为空数组。
90
+ */
51
91
  function parseMarkers(text) {
52
92
  const seen = /* @__PURE__ */ new Set();
53
93
  const refs = [];
@@ -63,10 +103,21 @@ function parseMarkers(text) {
63
103
  }
64
104
  //#endregion
65
105
  //#region ../shared/dist/api/unit.js
106
+ /**
107
+ * 判这个模块要不要往外装东西。
108
+ * @param manifest 模块清单。
109
+ * @returns 插件与共用库两张表都空时给 false。
110
+ */
66
111
  function hasEnvSupply(manifest) {
67
112
  return (manifest.env?.plugins ?? []).length > 0 || (manifest.env?.libs ?? []).length > 0;
68
113
  }
114
+ /**
115
+ * 外壳愿意认的协议版本;不在表里的模块当场被拒,不猜语义。
116
+ */
69
117
  var SUPPORTED_API_VERSIONS = [1];
118
+ /**
119
+ * 协议认得的全部键;写了别的键不会被采纳(外壳静默忽略),所以字段名拼错不会有提示。
120
+ */
70
121
  var DECLARATION_KEYS = [
71
122
  "apiVersion",
72
123
  "unit",
@@ -75,6 +126,9 @@ var DECLARATION_KEYS = [
75
126
  "host",
76
127
  "env"
77
128
  ];
129
+ /**
130
+ * 协议要求必写的键;env 不在其中(没有环境需要就不写)。
131
+ */
78
132
  var DECLARATION_REQUIRED_KEYS = [
79
133
  "apiVersion",
80
134
  "unit",
@@ -106,6 +160,12 @@ function stringArrayProblems(at, value) {
106
160
  if (!Array.isArray(value)) return [`${at} 要是一串字符串,现在是 ${JSON.stringify(value)}`];
107
161
  return value.flatMap((item, index) => text$1(item) === "" ? [`${at}[${index}] 不是非空字符串:${JSON.stringify(item)}`] : []);
108
162
  }
163
+ /**
164
+ * 查清单里 dsh.workbench 那一段的形状,回一串给人看的问题(中文,带字段路径)。
165
+ * @param subject 报错时用的主语,一般是包名。
166
+ * @param raw 清单里那段原始值,可能什么都不是。
167
+ * @returns 空数组表示形状过得去;版本不对就当场返回(后面的字段无从判起)。
168
+ */
109
169
  function declarationProblemsOf(subject, raw) {
110
170
  if (!isRecord(raw)) return [`${subject}: dsh.workbench 不是一个对象 —— ${JSON.stringify(raw)}`];
111
171
  const version = raw.apiVersion;
@@ -167,6 +227,12 @@ function declarationProblemsOf(subject, raw) {
167
227
  if (raw.env !== void 0 && !isRecord(raw.env)) problems.push(`${subject}: dsh.workbench.env 要是对象(command / plugins / libs)—— 语义由门禁的 env 判据查,这里只查形状`);
168
228
  return problems;
169
229
  }
230
+ /**
231
+ * 读一份清单:先查形状,过了再把声明规整成 manifest。
232
+ * @param subject 报错时用的主语。
233
+ * @param raw 清单里 dsh.workbench 那段原始值。
234
+ * @returns 有问题时只有 problems;没问题时 declaration 与 manifest 都给,manifest 里的字符串已 trim。
235
+ */
170
236
  function readDeclaration(subject, raw) {
171
237
  const problems = declarationProblemsOf(subject, raw);
172
238
  if (problems.length > 0) return { problems };
@@ -193,19 +259,40 @@ function readDeclaration(subject, raw) {
193
259
  }
194
260
  };
195
261
  }
262
+ /**
263
+ * 由单元 id 拼出它那一族事实的范围名。
264
+ * @param unit 单元 id,不带后缀。
265
+ * @returns <unit>* —— 永远是范围名,不是某一条事实。
266
+ */
196
267
  function unitFactScope(unit) {
197
268
  return `${unit}*`;
198
269
  }
199
270
  //#endregion
200
271
  //#region ../shared/dist/api/long-task.js
272
+ /**
273
+ * 单元的长任务读口路由。
274
+ * @param unit 单元 id。
275
+ * @returns <unit>/runs。
276
+ */
201
277
  function runsRouteOf(unit) {
202
278
  return `${unit}/runs`;
203
279
  }
204
280
  //#endregion
205
281
  //#region ../shared/dist/lib/text.js
282
+ /**
283
+ * 把一个值裹成 shell 里的单引号字面量(值里的单引号一并转义),保证元字符按字面量传递。
284
+ * @param value - 任意值;不是字符串的先 `String()`。
285
+ * @returns 可安全拼进 `/bin/sh -c` 的一段文本 —— 含空格 / `$()` / `;` / 通配符的值都不会被当成语法。
286
+ */
206
287
  function quoteShellArg(value) {
207
288
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
208
289
  }
290
+ /**
291
+ * 取一段文本的末尾若干行(如命令输出),顺带去掉首尾空白。
292
+ * @param text - 原文;`null` / `undefined` 当空串。
293
+ * @param lines - 最多留几行,默认 6;0 给全篇、负数从开头砍掉那么多行(都是 `slice` 语义),调用方传正数。
294
+ * @returns 末尾若干行、去首尾空行后的文本;原文行数不够就全给,空输入给空串。
295
+ */
209
296
  function tailLines(text, lines = 6) {
210
297
  return String(text ?? "").trim().split("\n").slice(-lines).join("\n");
211
298
  }
@@ -223,6 +310,11 @@ function candidateStarts(raw) {
223
310
  }
224
311
  return starts;
225
312
  }
313
+ /**
314
+ * 宽松解析一段前后可能夹着人话的 JSON:整段不通就从行首的 `[` / `{` 起、截到最后一个闭合符再试。
315
+ * @param text - 原文;`null` / `undefined` 当空串。
316
+ * @returns 解析出的值(对象 / 数组 / 标量都原样);空输入或怎么都解析不出给 `undefined`,调用方据此报「输出认不出」。
317
+ */
226
318
  function parseJsonLoose(text) {
227
319
  const raw = String(text ?? "").trim();
228
320
  if (!raw) return void 0;
@@ -245,6 +337,9 @@ function parseJsonLoose(text) {
245
337
  var DAY = 864e5;
246
338
  30 * DAY;
247
339
  365 * DAY;
340
+ /**
341
+ * 上报文本字段的截断长度(2000 字符;workspace / reason / message 共用)。
342
+ */
248
343
  var PANEL_STATE_MAX_TEXT = 2e3;
249
344
  var PHASES = [
250
345
  "idle",
@@ -252,10 +347,10 @@ var PHASES = [
252
347
  "ready",
253
348
  "error"
254
349
  ];
255
- function asRecord$10(value) {
350
+ function asRecord$8(value) {
256
351
  return typeof value === "object" && value !== null ? value : void 0;
257
352
  }
258
- function textOf$8(value, max) {
353
+ function textOf$6(value, max) {
259
354
  if (typeof value !== "string") return "";
260
355
  const trimmed = value.trim();
261
356
  return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
@@ -266,23 +361,29 @@ function atOf(value) {
266
361
  return Math.trunc(value);
267
362
  }
268
363
  function errorOf(raw) {
269
- const record = asRecord$10(raw);
364
+ const record = asRecord$8(raw);
270
365
  if (record === void 0) return void 0;
271
- const code = textOf$8(record["code"], 120);
272
- const message = textOf$8(record["message"], PANEL_STATE_MAX_TEXT);
366
+ const code = textOf$6(record["code"], 120);
367
+ const message = textOf$6(record["message"], PANEL_STATE_MAX_TEXT);
273
368
  if (code === "" && message === "") return void 0;
274
369
  return {
275
370
  code: code === "" ? "unknown" : code,
276
371
  message
277
372
  };
278
373
  }
374
+ /**
375
+ * 收一批卡片读数上报。
376
+ * @param payload - `POST` 的请求体。
377
+ * @returns 至少收到一条能认的卡片才 `ok: true`;请求体不是对象、`workspace` 为空、`cards` 不是数组,
378
+ * 或一条都认不出(`unit` / `route` / `phase` / `at` 缺一)时 `ok: false` + 中文原因;认不出的单条静默跳过。
379
+ */
279
380
  function parseCardsReport(payload) {
280
- const record = asRecord$10(payload);
381
+ const record = asRecord$8(payload);
281
382
  if (record === void 0) return {
282
383
  ok: false,
283
384
  message: "请求体要是一个对象"
284
385
  };
285
- const workspace = textOf$8(record["workspace"], PANEL_STATE_MAX_TEXT);
386
+ const workspace = textOf$6(record["workspace"], PANEL_STATE_MAX_TEXT);
286
387
  if (workspace === "") return {
287
388
  ok: false,
288
389
  message: "workspace 不能是空的 —— 面板按工作区存"
@@ -294,11 +395,11 @@ function parseCardsReport(payload) {
294
395
  };
295
396
  const cards = [];
296
397
  for (const item of raw.slice(0, 32)) {
297
- const card = asRecord$10(item);
398
+ const card = asRecord$8(item);
298
399
  if (card === void 0) continue;
299
- const unit = textOf$8(card["unit"], 200);
300
- const route = textOf$8(card["route"], 400);
301
- const phase = textOf$8(card["phase"], 20);
400
+ const unit = textOf$6(card["unit"], 200);
401
+ const route = textOf$6(card["route"], 400);
402
+ const phase = textOf$6(card["phase"], 20);
302
403
  const at = atOf(card["at"]);
303
404
  if (unit === "" || route === "" || at === null || !PHASES.includes(phase)) continue;
304
405
  const error = errorOf(card["error"]);
@@ -322,15 +423,20 @@ function parseCardsReport(payload) {
322
423
  }
323
424
  };
324
425
  }
426
+ /**
427
+ * 收一次卡片崩溃上报。
428
+ * @param payload - `POST` 的请求体。
429
+ * @returns `workspace` / `unit` / `reason` 都不能空,缺一给 `ok: false` + 中文原因;`at` 缺省或不合法时取当前时间。
430
+ */
325
431
  function parseCrashReport(payload) {
326
- const record = asRecord$10(payload);
432
+ const record = asRecord$8(payload);
327
433
  if (record === void 0) return {
328
434
  ok: false,
329
435
  message: "请求体要是一个对象"
330
436
  };
331
- const workspace = textOf$8(record["workspace"], PANEL_STATE_MAX_TEXT);
332
- const unit = textOf$8(record["unit"], 200);
333
- const reason = textOf$8(record["reason"], PANEL_STATE_MAX_TEXT);
437
+ const workspace = textOf$6(record["workspace"], PANEL_STATE_MAX_TEXT);
438
+ const unit = textOf$6(record["unit"], 200);
439
+ const reason = textOf$6(record["reason"], PANEL_STATE_MAX_TEXT);
334
440
  if (workspace === "") return {
335
441
  ok: false,
336
442
  message: "workspace 不能是空的"
@@ -353,6 +459,10 @@ function parseCrashReport(payload) {
353
459
  }
354
460
  };
355
461
  }
462
+ /**
463
+ * 造一份面板运行态的留存(进程内存、按工作区分桶,带上面那几条上限的 LRU)。
464
+ * @returns 留存口:`cards` / `crash` 收上报,`snapshot(workspace)` 取全量;没存过的工作区(含 `null`)给一份空的。
465
+ */
356
466
  function createPanelStateStore() {
357
467
  const workspaces = /* @__PURE__ */ new Map();
358
468
  function trim(entry, max) {
@@ -443,17 +553,35 @@ function createPanelStateStore() {
443
553
  }
444
554
  //#endregion
445
555
  //#region ../shared/dist/lib/board-fact.js
556
+ /**
557
+ * 判一个路径落不落在这个工作区里(按路径前缀比,不是字符串包含)。
558
+ * @param root - 工作区根路径。
559
+ * @param target - 要判的路径。
560
+ * @returns 同一个路径或在其下给 `true`;`root` 末尾带不带斜杠都认(`/a` 不匹配 `/ab`)。
561
+ */
446
562
  function insideWorkshop$1(root, target) {
447
563
  if (target === root) return true;
448
564
  const base = root.endsWith("/") ? root : `${root}/`;
449
565
  return target.startsWith(base);
450
566
  }
567
+ /**
568
+ * 把一次探针读数归成椅面归属。
569
+ * @param probe - 探针读数。
570
+ * @param workshop - 当前工作区根路径;`null` 表示还不知道当前是哪个工作区。
571
+ * @returns 外壳不在、或板路径与工作区缺一 → `unknown`;右栏没开或没画出来 → `hidden`;板在本工作区 → `workshop`,否则 `other`。
572
+ */
451
573
  function boardFactOf(probe, workshop) {
452
574
  if (!probe.workbench) return "unknown";
453
575
  if (!probe.bar || !probe.open || !probe.drawn) return "hidden";
454
576
  if (probe.board === "" || workshop === null) return "unknown";
455
577
  return insideWorkshop$1(workshop, probe.board) ? "workshop" : "other";
456
578
  }
579
+ /**
580
+ * 把探针读数归成左栏那句人话的档位。
581
+ * @param probe - 探针读数。
582
+ * @param workshop - 当前工作区根路径;`null` 一律 `unknown`。
583
+ * @returns 椅面画着 → 本板 `ok` / 别板 `other-board`;没画着 → 页面已走 `off-workspace`,否则 `panel-closed`。
584
+ */
457
585
  function boardSituation(probe, workshop) {
458
586
  if (!probe.workbench || workshop === null) return "unknown";
459
587
  if (probe.bar && probe.open && probe.drawn) {
@@ -465,6 +593,11 @@ function boardSituation(probe, workshop) {
465
593
  }
466
594
  //#endregion
467
595
  //#region ../shared/dist/lib/diy-debug.js
596
+ /**
597
+ * 把工具入参收成人话动作。
598
+ * @param value - 任意入参值。
599
+ * @returns `'pause'` / `'resume'` / `'stop'` 原样;别的值(含 `undefined`、多余空白、大小写不同)给 `null`,由调用方拒。
600
+ */
468
601
  function parseDebugIntent(value) {
469
602
  return value === "pause" || value === "resume" || value === "stop" ? value : null;
470
603
  }
@@ -477,6 +610,10 @@ function ownerOf$1(state, sessionId, now) {
477
610
  if (held === null) return "none";
478
611
  return held.sessionId === sessionId ? held : "not-owner";
479
612
  }
613
+ /**
614
+ * 造一份新的调试约定存储(进程内存,同一时刻只可能有一份有效约定)。
615
+ * @returns 存取口;动作成不成都在返回的 `DebugMove` 里,不抛异常。
616
+ */
480
617
  function createDebugStore() {
481
618
  let held = null;
482
619
  const live = (now) => alive$1(held, now);
@@ -491,7 +628,8 @@ function createDebugStore() {
491
628
  const state = {
492
629
  phase: "debugging",
493
630
  sessionId: pact.sessionId,
494
- workspace: pact.workspace,
631
+ workspacePath: pact.workspacePath,
632
+ title: pact.title,
495
633
  board: pact.board,
496
634
  unit: pact.unit,
497
635
  name: pact.name,
@@ -560,6 +698,11 @@ function createDebugStore() {
560
698
  }
561
699
  };
562
700
  }
701
+ /**
702
+ * 把工具入参收成人话动作。
703
+ * @param raw - 任意入参值。
704
+ * @returns `'peek'` / `'halt'` / `'stop'` 原样;别的值(含 `undefined`、多余空白)给 `null`,由调用方拒。
705
+ */
563
706
  function parseRecordIntent(raw) {
564
707
  return raw === "peek" || raw === "halt" || raw === "stop" ? raw : null;
565
708
  }
@@ -572,6 +715,10 @@ function ownerOf(state, sessionId, now) {
572
715
  if (held === null) return "none";
573
716
  return held.sessionId === sessionId ? held : "not-owner";
574
717
  }
718
+ /**
719
+ * 造一份新的录制约定存储(进程内存,同一时刻只可能有一份有效约定)。
720
+ * @returns 存取口;动作成不成都在返回的 `RecordMove` 里,不抛异常。
721
+ */
575
722
  function createRecordStore() {
576
723
  let held = null;
577
724
  const live = (now) => alive(held, now);
@@ -706,481 +853,8 @@ async function runShell(shell, command, options = {}) {
706
853
  //#endregion
707
854
  //#region ../shared/dist/host/browser-read.js
708
855
  var BROWSER_SCREEN_SESSION = "workbench";
856
+ var BROWSER_READ_SESSION = "workbench-read";
709
857
  var BROWSER_RECORD_SESSION = "workbench-rec";
710
- var DEFAULT_TIMEOUT_MS$2 = 25e3;
711
- var DEFAULT_RETRY_MS = 1200;
712
- var DEFAULT_SETTLE_MS$1 = 900;
713
- var AUTH_CODES = /* @__PURE__ */ new Set([
714
- "NOT_LOGGED_IN",
715
- "NOT_LOGIN",
716
- "AUTH_FAILED",
717
- "AUTH_REQUIRED",
718
- "UNAUTHENTICATED",
719
- "UNAUTHORIZED",
720
- "TOKEN_EXPIRED"
721
- ]);
722
- var PERMISSION_CODES = /* @__PURE__ */ new Set([
723
- "FORBIDDEN",
724
- "PERMISSION_DENIED",
725
- "NO_PERMISSION",
726
- "EACCES"
727
- ]);
728
- function asRecord$9(value) {
729
- return typeof value === "object" && value !== null ? value : void 0;
730
- }
731
- function textOf$7(value) {
732
- return typeof value === "string" ? value.trim() : "";
733
- }
734
- function codeOf(page) {
735
- const record = asRecord$9(page);
736
- return record === void 0 ? "" : textOf$7(record["code"]);
737
- }
738
- function messageOf$2(page) {
739
- const record = asRecord$9(page);
740
- return record === void 0 ? "" : textOf$7(record["message"]);
741
- }
742
- function hintOf$1(page) {
743
- const record = asRecord$9(page);
744
- const hint = record === void 0 ? "" : textOf$7(record["hint"]);
745
- return hint === "" ? void 0 : hint;
746
- }
747
- function pageFailed(page) {
748
- const record = asRecord$9(page);
749
- return record !== void 0 && record["ok"] === false;
750
- }
751
- function blockerFor(code) {
752
- const normalized = code.trim().toUpperCase().replace(/-/g, "_");
753
- if (normalized === "") return void 0;
754
- const matches = (set) => [...set].some((one) => normalized === one || normalized.endsWith(`_${one}`));
755
- if (matches(PERMISSION_CODES)) return "permission-denied";
756
- if (matches(AUTH_CODES) || normalized.includes("UNAUTH")) return "auth-required";
757
- }
758
- function sleep$1(ms) {
759
- return new Promise((resolve) => setTimeout(resolve, ms));
760
- }
761
- async function shellRun(shell, command, argv, timeoutMs) {
762
- const result = await runShell(shell, backgroundLine(command, argv), { timeoutMs });
763
- if (result.timedOut) return {
764
- kind: "timeout",
765
- detail: `没在 ${Math.round(timeoutMs / 1e3)} 秒内返回`,
766
- stderr: result.stderr
767
- };
768
- if (result.exitCode !== 0) return {
769
- kind: "failed",
770
- detail: tailLines(result.stderr || result.stdout, 4),
771
- stderr: result.stderr
772
- };
773
- return {
774
- kind: "ok",
775
- stdout: result.stdout
776
- };
777
- }
778
- async function evalOnce(shell, command, session, script, timeoutMs, retryMs) {
779
- const run = async () => {
780
- let attempt;
781
- try {
782
- attempt = await shellRun(shell, command, [
783
- "browser",
784
- session,
785
- "eval",
786
- script,
787
- "--window",
788
- "background"
789
- ], timeoutMs);
790
- } catch (error) {
791
- return {
792
- kind: "failure",
793
- result: {
794
- ok: false,
795
- code: "cli-exec-failed",
796
- message: "读不了:opencli 起不来",
797
- hint: "确认这台机器上 opencli 能跑(工作台会自己把它挂好)",
798
- detail: error instanceof Error ? error.message : String(error)
799
- }
800
- };
801
- }
802
- if (attempt.kind === "timeout") return {
803
- kind: "failure",
804
- result: {
805
- ok: false,
806
- code: "cli-timeout",
807
- message: "这次没读到(等超时了)",
808
- hint: "稍后重试",
809
- detail: attempt.detail
810
- }
811
- };
812
- if (attempt.kind === "failed") {
813
- const normalized = attempt.stderr.trim().toUpperCase().replace(/-/g, "_");
814
- const forbidden = [...PERMISSION_CODES].some((one) => normalized.includes(one));
815
- return {
816
- kind: "failure",
817
- result: {
818
- ok: false,
819
- code: forbidden ? "permission-denied" : "cli-exec-failed",
820
- message: forbidden ? "没有权限在这台机器上读" : "读不了(命令失败)",
821
- hint: forbidden ? "换一个有权限的账号,或让用户开权限" : "把下面这段原始输出看一眼再重试",
822
- detail: attempt.detail
823
- }
824
- };
825
- }
826
- const value = parseJsonLoose(attempt.stdout);
827
- if (value === void 0) return {
828
- kind: "failure",
829
- result: {
830
- ok: false,
831
- code: "cli-exec-failed",
832
- message: "读到了、但不是 JSON",
833
- hint: "脚本必须回 JSON",
834
- detail: tailLines(attempt.stdout, 4)
835
- }
836
- };
837
- return {
838
- kind: "value",
839
- value
840
- };
841
- };
842
- const first = await run();
843
- if (first.kind === "value") return first;
844
- if (first.result.code !== "cli-exec-failed") return first;
845
- await sleep$1(retryMs);
846
- return run();
847
- }
848
- async function openOnce$1(shell, command, session, url, timeoutMs) {
849
- let attempt;
850
- try {
851
- attempt = await shellRun(shell, command, [
852
- "browser",
853
- session,
854
- "open",
855
- url,
856
- "--window",
857
- "background"
858
- ], timeoutMs);
859
- } catch (error) {
860
- return {
861
- ok: false,
862
- code: "cli-exec-failed",
863
- message: "打不开那个页面",
864
- hint: "确认 opencli 能跑起来",
865
- detail: error instanceof Error ? error.message : String(error),
866
- helpUrl: url
867
- };
868
- }
869
- if (attempt.kind === "ok") return void 0;
870
- return {
871
- ok: false,
872
- code: attempt.kind === "timeout" ? "cli-timeout" : "cli-exec-failed",
873
- message: "打不开那个页面",
874
- hint: "在浏览器里手动打开一次,然后重试",
875
- detail: attempt.detail,
876
- helpUrl: url
877
- };
878
- }
879
- async function browserRead(options) {
880
- const { shell, script, url, loginUrl, open = true } = options;
881
- const command = options.command ?? "opencli";
882
- const session = options.session ?? "workbench-read";
883
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS$2;
884
- const retryMs = options.retryMs ?? DEFAULT_RETRY_MS;
885
- const settleMs = options.settleMs ?? DEFAULT_SETTLE_MS$1;
886
- const at = Date.now();
887
- const first = await evalOnce(shell, command, session, script, timeoutMs, retryMs);
888
- if (first.kind === "failure") return {
889
- ...first.result,
890
- at
891
- };
892
- let page = first.value;
893
- if (codeOf(page) === "not-on-site") {
894
- if (!open || url === void 0) return {
895
- ok: false,
896
- code: "cli-exec-failed",
897
- at,
898
- message: "没停在那套系统上",
899
- hint: "先在那个浏览器里把它打开,再读一次",
900
- detail: JSON.stringify(page),
901
- ...url === void 0 ? {} : { helpUrl: url }
902
- };
903
- const opened = await openOnce$1(shell, command, session, url, timeoutMs);
904
- if (opened !== void 0) return {
905
- ...opened,
906
- at
907
- };
908
- await sleep$1(settleMs);
909
- const second = await evalOnce(shell, command, session, script, timeoutMs, retryMs);
910
- if (second.kind === "failure") return {
911
- ...second.result,
912
- at
913
- };
914
- page = second.value;
915
- if (codeOf(page) === "not-on-site") return {
916
- ok: false,
917
- code: "cli-exec-failed",
918
- at,
919
- message: "打开了页面、又读了一遍,仍然不在那套系统上",
920
- hint: "确认这个地址就是那套系统(或者它跳到了别的域)",
921
- detail: JSON.stringify(page),
922
- helpUrl: url
923
- };
924
- }
925
- if (!pageFailed(page)) return {
926
- ok: true,
927
- code: "ok",
928
- at,
929
- value: page,
930
- message: "读到了"
931
- };
932
- const pageCode = codeOf(page) || "read-failed";
933
- const message = messageOf$2(page) || `读不到(${pageCode})`;
934
- const blocker = blockerFor(pageCode);
935
- if (blocker === "auth-required") return {
936
- ok: false,
937
- code: blocker,
938
- at,
939
- pageCode,
940
- message,
941
- hint: "去登录一次,再重试",
942
- detail: JSON.stringify(page),
943
- ...loginUrl === void 0 && url === void 0 ? {} : { helpUrl: loginUrl ?? url }
944
- };
945
- if (blocker === "permission-denied") return {
946
- ok: false,
947
- code: blocker,
948
- at,
949
- pageCode,
950
- message,
951
- hint: "这个账号没有权限",
952
- detail: JSON.stringify(page),
953
- ...loginUrl === void 0 && url === void 0 ? {} : { helpUrl: loginUrl ?? url }
954
- };
955
- return {
956
- ok: false,
957
- code: pageCode,
958
- at,
959
- pageCode,
960
- message,
961
- detail: JSON.stringify(page),
962
- ...hintOf$1(page) === void 0 ? {} : { hint: hintOf$1(page) }
963
- };
964
- }
965
- //#endregion
966
- //#region ../shared/dist/host/browser-find.js
967
- var DEFAULT_TIMEOUT_MS$1 = 25e3;
968
- var DEFAULT_SETTLE_MS = 2500;
969
- var URL_TIMEOUT_MS = 8e3;
970
- var DEFAULT_LIMIT = 50;
971
- var DEFAULT_TEXT_MAX = 200;
972
- function asRecord$8(value) {
973
- return typeof value === "object" && value !== null ? value : void 0;
974
- }
975
- function textOf$6(value) {
976
- return typeof value === "string" ? value.trim() : "";
977
- }
978
- function entriesOf(value) {
979
- const record = asRecord$8(value);
980
- const raw = record === void 0 ? void 0 : record["entries"];
981
- if (!Array.isArray(raw)) return [];
982
- const entries = [];
983
- for (const one of raw) {
984
- const item = asRecord$8(one);
985
- if (item === void 0) continue;
986
- const nth = typeof item["nth"] === "number" ? item["nth"] : entries.length;
987
- entries.push({
988
- text: textOf$6(item["text"]),
989
- tag: textOf$6(item["tag"]),
990
- nth
991
- });
992
- }
993
- return entries;
994
- }
995
- function reasonOf(stdout, stderr) {
996
- const parsed = asRecord$8(parseJsonLoose(stdout));
997
- const error = parsed === void 0 ? void 0 : asRecord$8(parsed["error"]);
998
- const message = error === void 0 ? "" : textOf$6(error["message"]);
999
- if (message !== "") return message;
1000
- const lines = (stderr.trim() === "" ? stdout : stderr).split("\n").map((one) => one.trim()).filter((one) => one !== "");
1001
- return (lines[lines.length - 1] ?? "").slice(-200);
1002
- }
1003
- function hintOf(stdout) {
1004
- const parsed = asRecord$8(parseJsonLoose(stdout));
1005
- const error = parsed === void 0 ? void 0 : asRecord$8(parsed["error"]);
1006
- const hint = error === void 0 ? "" : textOf$6(error["hint"]);
1007
- return hint === "" ? void 0 : hint;
1008
- }
1009
- function sameOrigin(left, right) {
1010
- try {
1011
- return new URL(left).origin === new URL(right).origin;
1012
- } catch {
1013
- return false;
1014
- }
1015
- }
1016
- function urlOf(stdout) {
1017
- const bare = (stdout.split("\n").map((one) => one.trim()).filter((one) => one !== "")[0] ?? "").replace(/^url:\s*/i, "").trim().replace(/^["']+|["']+$/g, "");
1018
- if (bare.startsWith("{")) {
1019
- const parsed = asRecord$8(parseJsonLoose(bare));
1020
- const url = parsed === void 0 ? "" : textOf$6(parsed["url"]);
1021
- if (url !== "") return url;
1022
- }
1023
- return bare;
1024
- }
1025
- async function currentUrl(shell, command, session) {
1026
- const line = backgroundLine(command, [
1027
- "browser",
1028
- session,
1029
- "get",
1030
- "url",
1031
- "--window",
1032
- "background"
1033
- ]);
1034
- try {
1035
- const result = await runShell(shell, line, { timeoutMs: URL_TIMEOUT_MS });
1036
- if (result.exitCode !== 0) return "";
1037
- return urlOf(result.stdout);
1038
- } catch {
1039
- return "";
1040
- }
1041
- }
1042
- function sleep(ms) {
1043
- return new Promise((resolve) => setTimeout(resolve, ms));
1044
- }
1045
- async function findOnce(shell, command, session, css, limit, textMax, timeoutMs) {
1046
- const result = await runShell(shell, backgroundLine(command, [
1047
- "browser",
1048
- session,
1049
- "find",
1050
- "--css",
1051
- css,
1052
- "--limit",
1053
- String(limit),
1054
- "--text-max",
1055
- String(textMax),
1056
- "--window",
1057
- "background"
1058
- ]), { timeoutMs });
1059
- if (result.timedOut) return {
1060
- kind: "timeout",
1061
- detail: `没在 ${Math.round(timeoutMs / 1e3)} 秒内返回`
1062
- };
1063
- if (result.exitCode !== 0) return {
1064
- kind: "failed",
1065
- detail: reasonOf(result.stdout, result.stderr),
1066
- ...hintOf(result.stdout) === void 0 ? {} : { hint: hintOf(result.stdout) }
1067
- };
1068
- return {
1069
- kind: "ok",
1070
- entries: entriesOf(parseJsonLoose(result.stdout)),
1071
- stdout: result.stdout
1072
- };
1073
- }
1074
- async function openOnce(shell, command, session, url, timeoutMs) {
1075
- const line = backgroundLine(command, [
1076
- "browser",
1077
- session,
1078
- "open",
1079
- url,
1080
- "--window",
1081
- "background"
1082
- ]);
1083
- let result;
1084
- try {
1085
- result = await runShell(shell, line, { timeoutMs });
1086
- } catch (error) {
1087
- return error instanceof Error ? error.message : String(error);
1088
- }
1089
- if (result.timedOut) return `没在 ${Math.round(timeoutMs / 1e3)} 秒内打开`;
1090
- if (result.exitCode !== 0) {
1091
- const reason = reasonOf(result.stdout, result.stderr);
1092
- return reason === "" ? `opencli 打开失败(退出码 ${String(result.exitCode ?? "?")})` : reason;
1093
- }
1094
- }
1095
- async function browserFind(options) {
1096
- const { shell, css, url, open = true } = options;
1097
- const command = options.command ?? "opencli";
1098
- const session = options.session ?? "workbench-read";
1099
- const limit = options.limit ?? DEFAULT_LIMIT;
1100
- const textMax = options.textMax ?? DEFAULT_TEXT_MAX;
1101
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS$1;
1102
- const settleMs = options.settleMs ?? DEFAULT_SETTLE_MS;
1103
- const at = Date.now();
1104
- const find = async () => {
1105
- try {
1106
- return await findOnce(shell, command, session, css, limit, textMax, timeoutMs);
1107
- } catch (error) {
1108
- return {
1109
- kind: "failed",
1110
- detail: error instanceof Error ? error.message : String(error),
1111
- hint: "确认这台机器上 opencli 能跑(工作台会自己把它挂好)"
1112
- };
1113
- }
1114
- };
1115
- const canMove = open && url !== void 0;
1116
- if (canMove) {
1117
- const here = await currentUrl(shell, command, session);
1118
- if (here !== "" && !sameOrigin(here, url)) {
1119
- const opened = await openOnce(shell, command, session, url, timeoutMs);
1120
- if (opened !== void 0) return {
1121
- ok: false,
1122
- code: "cli-exec-failed",
1123
- at,
1124
- entries: [],
1125
- count: 0,
1126
- message: "打不开那个页面",
1127
- hint: "在浏览器里手动打开一次,然后重试",
1128
- detail: opened,
1129
- helpUrl: url
1130
- };
1131
- await sleep(settleMs);
1132
- }
1133
- }
1134
- let outcome = await find();
1135
- if (outcome === void 0) return {
1136
- ok: false,
1137
- code: "cli-exec-failed",
1138
- at,
1139
- entries: [],
1140
- count: 0,
1141
- message: "没找到元素"
1142
- };
1143
- if (outcome.kind === "failed" && canMove) {
1144
- if (await openOnce(shell, command, session, url, timeoutMs) === void 0) {
1145
- await sleep(settleMs);
1146
- const second = await find();
1147
- if (second !== void 0) outcome = second;
1148
- }
1149
- }
1150
- if (outcome.kind === "timeout") return {
1151
- ok: false,
1152
- code: "cli-timeout",
1153
- at,
1154
- entries: [],
1155
- count: 0,
1156
- message: "这次没找到元素(等超时了)",
1157
- hint: "稍后重试",
1158
- detail: outcome.detail,
1159
- ...url === void 0 ? {} : { helpUrl: url }
1160
- };
1161
- if (outcome.kind === "failed") {
1162
- const detail = outcome.detail;
1163
- return {
1164
- ok: false,
1165
- code: "cli-exec-failed",
1166
- at,
1167
- entries: [],
1168
- count: 0,
1169
- message: detail === "" ? "没找到元素(opencli 命令失败)" : `没找到元素:${detail}`,
1170
- ...outcome.hint === void 0 ? {} : { hint: outcome.hint },
1171
- ...detail === "" ? {} : { detail },
1172
- ...url === void 0 ? {} : { helpUrl: url }
1173
- };
1174
- }
1175
- return {
1176
- ok: true,
1177
- code: "ok",
1178
- at,
1179
- entries: outcome.entries,
1180
- count: outcome.entries.length,
1181
- message: outcome.entries.length === 0 ? "页面上没有匹配的元素" : `找到 ${String(outcome.entries.length)} 个元素`
1182
- };
1183
- }
1184
858
  //#endregion
1185
859
  //#region ../shared/dist/host/cli-error.js
1186
860
  function unquote(raw) {
@@ -2552,6 +2226,8 @@ function createWorkspaceStore(options) {
2552
2226
  }
2553
2227
  };
2554
2228
  }
2229
+ var WORKSHOP_RELATIVE = "workbench/diy";
2230
+ var WORKSHOP_TITLE = "工作台DIY";
2555
2231
  //#endregion
2556
2232
  //#region src/shared/api/bundle-install.ts
2557
2233
  var BUNDLE_REGISTRIES_ROUTE = "bundle-registries";
@@ -3068,110 +2744,6 @@ function createRegistryNotebook(dir) {
3068
2744
  };
3069
2745
  }
3070
2746
  //#endregion
3071
- //#region src/shared/api/diy.ts
3072
- var WORKSHOP_RELATIVE = "workbench/diy";
3073
- var WORKSHOP_TITLE = "工作台DIY";
3074
- //#endregion
3075
- //#region src/shared/api/host/diy.ts
3076
- function workshopPathOf(ctx) {
3077
- const homePath = ctx.get("dshHomePath");
3078
- if (typeof homePath !== "function") return null;
3079
- try {
3080
- return homePath(WORKSHOP_RELATIVE);
3081
- } catch {
3082
- return null;
3083
- }
3084
- }
3085
- //#endregion
3086
- //#region src/shared/api/host/diy-workshop.ts
3087
- function asRecord$4(value) {
3088
- return typeof value === "object" && value !== null ? value : void 0;
3089
- }
3090
- function asText$3(value) {
3091
- return typeof value === "string" ? value.trim() : "";
3092
- }
3093
- function workshopPackagesDir(ctx) {
3094
- const root = workshopPathOf(ctx);
3095
- return root === null ? null : join(root, "packages");
3096
- }
3097
- function insideWorkshop(root, target) {
3098
- return insideWorkshop$1(existsSync(root) ? realpathSync(root) : resolve(root), existsSync(target) ? realpathSync(target) : resolve(target));
3099
- }
3100
- function workshopModules(packagesDir, installed = []) {
3101
- if (!existsSync(packagesDir)) return [];
3102
- const already = new Set(installed);
3103
- return readdirSync(packagesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() || entry.isSymbolicLink()).flatMap((entry) => {
3104
- const dir = join(packagesDir, entry.name);
3105
- let manifest;
3106
- try {
3107
- manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
3108
- } catch {
3109
- return [];
3110
- }
3111
- const record = asRecord$4(manifest);
3112
- const declared = record === void 0 ? void 0 : asRecord$4(asRecord$4(record.dsh)?.workbench);
3113
- const unit = declared === void 0 ? "" : asText$3(declared.unit);
3114
- if (unit === "") return [];
3115
- return [{
3116
- unit,
3117
- name: asText$3(record?.name),
3118
- description: asText$3(record?.description),
3119
- dir,
3120
- installed: already.has(unit)
3121
- }];
3122
- }).sort((left, right) => left.unit.localeCompare(right.unit));
3123
- }
3124
- function readDesignOfModule(dir) {
3125
- const file = join(dir, "design.json");
3126
- if (!existsSync(file)) return void 0;
3127
- try {
3128
- return JSON.parse(readFileSync(file, "utf8"));
3129
- } catch {
3130
- return;
3131
- }
3132
- }
3133
- function inWorkshopSession(ctx, workspace) {
3134
- const root = workshopPathOf(ctx);
3135
- return root !== null && workspace !== null && insideWorkshop(root, workspace);
3136
- }
3137
- function workshopSyncOf(workshopRoot) {
3138
- let parsed;
3139
- try {
3140
- parsed = JSON.parse(readFileSync(join(workshopRoot, ".sync.json"), "utf8"));
3141
- } catch {
3142
- return;
3143
- }
3144
- if (typeof parsed !== "object" || parsed === null) return void 0;
3145
- const record = parsed;
3146
- const state = asText$3(record["state"]);
3147
- if (state !== "ok" && state !== "syncing" && state !== "failed") return void 0;
3148
- const reason = asText$3(record["reason"]);
3149
- return {
3150
- state,
3151
- version: asText$3(record["version"]),
3152
- at: asText$3(record["at"]),
3153
- ...reason === "" ? {} : { reason }
3154
- };
3155
- }
3156
- function workshopToolchainOf(workshopRoot) {
3157
- const versionOf = (name) => {
3158
- try {
3159
- return asText$3(JSON.parse(readFileSync(join(workshopRoot, "node_modules", "@dsh-workbench", name, "package.json"), "utf8")).version);
3160
- } catch {
3161
- return "";
3162
- }
3163
- };
3164
- const core = versionOf("core");
3165
- const moduleTools = versionOf("module-tools");
3166
- const shared = versionOf("shared");
3167
- if (core === "" || moduleTools === "" || shared === "") return void 0;
3168
- return {
3169
- core,
3170
- moduleTools,
3171
- shared
3172
- };
3173
- }
3174
- //#endregion
3175
2747
  //#region src/modules/panels/unit.ts
3176
2748
  var PANELS_LIST_TOOL = "workbench_panels_list";
3177
2749
  var PANELS_SHOW_TOOL = "workbench_panels_show";
@@ -3194,7 +2766,6 @@ var DIY_PANEL_TOOL = "workbench_diy_panel";
3194
2766
  var DIY_COMPONENTS_TOOL = "workbench_diy_components";
3195
2767
  var DIY_RECON_TOOL = "workbench_diy_recon";
3196
2768
  var DIY_SPEC_TOOL = "workbench_diy_spec";
3197
- var DIY_PROBE_TOOL = "workbench_diy_probe";
3198
2769
  var DIY_INSTALL_TOOL = "workbench_diy_install";
3199
2770
  var DIY_SHOW_TOOL = "workbench_diy_show";
3200
2771
  var DIY_STATE_TOOL = "workbench_diy_state";
@@ -3231,7 +2802,6 @@ var READ_TOOLS = [
3231
2802
  DIY_COMPONENTS_TOOL,
3232
2803
  DIY_RECON_TOOL,
3233
2804
  DIY_SPEC_TOOL,
3234
- DIY_PROBE_TOOL,
3235
2805
  DIY_STATE_TOOL,
3236
2806
  DIY_HTML_TOOL,
3237
2807
  DIY_FIND_TOOL,
@@ -3282,7 +2852,6 @@ var DIY_TOOLS = [
3282
2852
  DIY_COMPONENTS_TOOL,
3283
2853
  DIY_RECON_TOOL,
3284
2854
  DIY_SPEC_TOOL,
3285
- DIY_PROBE_TOOL,
3286
2855
  DIY_INSTALL_TOOL,
3287
2856
  DIY_SHOW_TOOL,
3288
2857
  DIY_STATE_TOOL,
@@ -3412,7 +2981,7 @@ var panelsApproval = (call) => {
3412
2981
  const starting = call.name === DIY_DEBUG_START_TOOL;
3413
2982
  const unit = argsOf$2(call)["unit"];
3414
2983
  const target = typeof unit === "string" && unit !== "" ? ` [${unit}]` : "";
3415
- return askOf(`${call.name}${target} takes over the user's screen to debug a workbench module`, pair(starting ? "要在「工作台DIY」那块板上开始调试 —— 调试中请留在那一屏,允许吗?" : "要接着调试 —— 请回到「工作台DIY」那一屏,允许吗?", starting ? "it takes over your screen to debug on the workshop board — please stay on that screen. Allow?" : "it needs your screen again to keep debugging — please go back to the workshop board. Allow?"), pair(starting ? "agent 要占用你的屏幕开始调试工作台模块,允许吗?" : "agent 要接着占用你的屏幕,继续调试工作台模块,允许吗?", starting ? "The agent wants to take over your screen to start debugging a workbench module. Allow?" : "The agent wants your screen again to keep debugging a workbench module. Allow?"));
2984
+ return askOf(`${call.name}${target} takes over the user's screen to debug a workbench module`, pair(starting ? "要在这个工作区的那块板上开始调试 —— 调试中请留在那一屏,允许吗?" : "要接着调试 —— 请回到那一屏,允许吗?", starting ? "it takes over your screen to debug on this workspace's board — please stay on that screen. Allow?" : "it needs your screen again to keep debugging — please go back to that screen. Allow?"), pair(starting ? "agent 要占用你的屏幕开始调试工作台模块,允许吗?" : "agent 要接着占用你的屏幕,继续调试工作台模块,允许吗?", starting ? "The agent wants to take over your screen to start debugging a workbench module. Allow?" : "The agent wants your screen again to keep debugging a workbench module. Allow?"));
3416
2985
  }
3417
2986
  if (call.name === "workbench_diy_debug_pause" || call.name === "workbench_diy_debug_stop") return { kind: "allow" };
3418
2987
  if (call.name === "workbench_diy_record_prepare") {
@@ -3650,21 +3219,38 @@ function toolchainBinOf(root) {
3650
3219
  const script = join(root, "node_modules", "@dsh-workbench", "module-tools", "bin", "dsh-wb-module.mjs");
3651
3220
  return existsSync(script) ? script : null;
3652
3221
  }
3222
+ function toolchainBinAmong(root, dirs) {
3223
+ const direct = toolchainBinOf(root);
3224
+ if (direct !== null) return direct;
3225
+ for (const dir of dirs) {
3226
+ const bin = toolchainBinOf(dir);
3227
+ if (bin !== null) return bin;
3228
+ }
3229
+ return null;
3230
+ }
3653
3231
  function queryCommandOf(bin, only) {
3654
3232
  return `${quoteShellArg(bin)} check --only ${only} --json`;
3655
3233
  }
3656
- async function runSurfaceQuery(shell, bin, only, root) {
3234
+ async function runSurfaceQuery(shell, bin, only, root, fallbackBin) {
3657
3235
  const result = await runShell(shell, queryCommandOf(bin, only), {
3658
3236
  workdir: root,
3659
3237
  timeoutMs: SURFACE_TIMEOUT_MS
3660
3238
  });
3661
3239
  const parsed = jsonOf$1(result.stdout);
3662
3240
  if (parsed !== null) return withoutProblems(parsed);
3241
+ const retry = fallbackBin === void 0 || fallbackBin === bin ? void 0 : fallbackBin;
3242
+ if (retry !== void 0) {
3243
+ const secondParsed = jsonOf$1((await runShell(shell, queryCommandOf(retry, only), {
3244
+ workdir: root,
3245
+ timeoutMs: SURFACE_TIMEOUT_MS
3246
+ })).stdout);
3247
+ if (secondParsed !== null) return withoutProblems(secondParsed);
3248
+ }
3663
3249
  if (result.exitCode !== 0) {
3664
3250
  const tail = `${result.stderr}\n${result.stdout}`.trim().split("\n").slice(-3).join(" | ");
3665
3251
  throw new Error(`工坊工具链的 \`check --only ${only} --json\` 没跑通(exit ${String(result.exitCode)})—— 这不是工具链坏了,是工坊里有模块过不了判据,所以连清单也没打出来${tail === "" ? "" : `:${tail}`}`);
3666
3252
  }
3667
- throw new Error(`工坊工具链的 \`check --only ${only} --json\` 没回 JSON:${result.stdout.trim().split("\n").slice(-2).join(" | ")}`);
3253
+ throw new Error(`工坊工具链的 \`check --only ${only} --json\` 没回 JSON:${result.stdout.trim().split("\n").slice(-2).join(" | ")}` + (retry === void 0 ? ` —— 这份工具链可能太旧(没有 --json):在 ${root} 里把 @dsh-workbench/module-tools 升到 ^0.3.4(pnpm install)` : " —— 连跑着的那份外壳自带的工具链也没回 JSON,这条得问人"));
3668
3254
  }
3669
3255
  function jsonOf$1(stdout) {
3670
3256
  try {
@@ -3682,7 +3268,7 @@ function withoutProblems(payload) {
3682
3268
  function recordOf$1(value) {
3683
3269
  return typeof value === "object" && value !== null ? value : {};
3684
3270
  }
3685
- function asText$2(value) {
3271
+ function asText$3(value) {
3686
3272
  return typeof value === "string" ? value : "";
3687
3273
  }
3688
3274
  function stringsOf$3(value) {
@@ -3764,43 +3350,18 @@ function excerptAt(file, maxLines = 60) {
3764
3350
  return "(打不开那份类型文件 —— 照上面的路径自己看一眼)";
3765
3351
  }
3766
3352
  }
3353
+ /** 指引目录:从宿主产物往上找 `skills/workbench-module-diy`(找不到回 `null`)。 */
3767
3354
  function skillDirOf(from) {
3768
3355
  let dir = from;
3769
3356
  for (let hop = 0; hop < 6; hop += 1) {
3770
3357
  const candidate = join(dir, "skills", "workbench-module-diy");
3771
- if (existsSync(join(candidate, "HOOKS.md"))) return candidate;
3358
+ if (existsSync(candidate)) return candidate;
3772
3359
  const parent = dirname(dir);
3773
3360
  if (parent === dir) break;
3774
3361
  dir = parent;
3775
3362
  }
3776
3363
  return null;
3777
3364
  }
3778
- function symbolsIn(heading) {
3779
- return [...heading.matchAll(/`([^`]+)`/g)].map((one) => (one[1] ?? "").trim()).map((one) => one.replace(/[<({[].*$/u, "").trim()).map((one) => one.replace(/[.::,,\s].*$/u, "").trim()).filter((one) => /^[\w$]+$/u.test(one));
3780
- }
3781
- function sectionOf(text, name) {
3782
- const rows = text.split("\n");
3783
- const at = rows.findIndex((row) => row.startsWith("### ") && symbolsIn(row).includes(name));
3784
- if (at === -1) return "";
3785
- const rest = rows.slice(at + 1).findIndex((row) => row.startsWith("## ") || row.startsWith("### "));
3786
- return rows.slice(at, rest === -1 ? rows.length : at + 1 + rest).join("\n").trim();
3787
- }
3788
- function docSectionOf(name, dir) {
3789
- if (dir === null) return null;
3790
- for (const doc of ["HOOKS.md", "KIT.md"]) {
3791
- const file = join(dir, doc);
3792
- try {
3793
- const lines = sectionOf(readFileSync(file, "utf8"), name);
3794
- if (lines !== "") return {
3795
- file,
3796
- lines
3797
- };
3798
- } catch {
3799
- continue;
3800
- }
3801
- }
3802
- return null;
3803
- }
3804
3365
  function truncated(names, all) {
3805
3366
  if (all || names.length <= 12) return names.join(", ");
3806
3367
  return `${names.slice(0, 12).join(", ")} …+${String(names.length - 12)}`;
@@ -3823,48 +3384,105 @@ function byLocationOf(payload) {
3823
3384
  function isComponents(where) {
3824
3385
  return where.split("/").at(-1) === "components";
3825
3386
  }
3387
+ function kitBucketsOf(payload) {
3388
+ const groups = byLocationOf(payload);
3389
+ const components = groups.filter((one) => isComponents(one.where)).flatMap((one) => one.names).sort();
3390
+ return [...components.length === 0 ? [] : [{
3391
+ label: "组件",
3392
+ key: "components",
3393
+ names: components
3394
+ }], ...groups.filter((one) => !isComponents(one.where)).map((one) => ({
3395
+ label: one.where,
3396
+ key: one.where,
3397
+ names: one.names
3398
+ }))];
3399
+ }
3400
+ function matchesBucket(where, bucket) {
3401
+ const wanted = bucket.trim().toLowerCase();
3402
+ if (wanted === "") return false;
3403
+ const one = where.trim().toLowerCase();
3404
+ return one === wanted || one.split("/").at(-1) === wanted;
3405
+ }
3406
+ function primitiveBucketsOf(payload) {
3407
+ return Object.entries(recordOf$1(payload["onlyPrimitives"])).map(([bucket, value]) => ({
3408
+ bucket,
3409
+ names: [...stringsOf$3(value)].sort()
3410
+ })).filter((one) => one.names.length > 0);
3411
+ }
3412
+ function bucketNamesOf(query) {
3413
+ const out = [];
3414
+ if (query.kit !== void 0) {
3415
+ const keys = kitBucketsOf(query.kit).map((one) => one.key);
3416
+ if (keys.length > 0) out.push({
3417
+ face: "kit",
3418
+ keys
3419
+ });
3420
+ }
3421
+ if (query.primitives !== void 0) {
3422
+ const keys = primitiveBucketsOf(query.primitives).map((one) => one.bucket);
3423
+ if (keys.length > 0) out.push({
3424
+ face: "primitives",
3425
+ keys
3426
+ });
3427
+ }
3428
+ return out;
3429
+ }
3430
+ var TWO_FACE_ALL = "⚠️ 两个面一起不给全表 —— 要全表就指名一面:scope=kit 或 scope=primitives。";
3431
+ function refusesEvery(scope, all, focused) {
3432
+ return all && scope === "all" && !focused;
3433
+ }
3826
3434
  function renderSurfaceText(query, options) {
3435
+ const bucket = (options.bucket ?? "").trim();
3436
+ const focused = bucket !== "";
3437
+ if (refusesEvery(options.scope, options.all, focused)) return TWO_FACE_ALL;
3438
+ const both = options.scope === "all";
3439
+ const every = options.all && !both;
3440
+ let hit = false;
3827
3441
  const lines = [];
3828
3442
  const kitNames = Object.keys(filesOf$1(query.kit)).sort();
3829
3443
  if (query.kit !== void 0) {
3830
3444
  const groups = byLocationOf(query.kit);
3831
3445
  if (kitNames.length === 0 && groups.length === 0) {
3832
3446
  const anyNames = [...new Set(Object.values(recordOf$1(query.kit["buckets"])).flatMap((value) => stringsOf$3(value)))].sort();
3833
- lines.push(anyNames.length === 0 ? "kit:读不到(没找到 client.js)" : `kit:类型面扫不到,只有名字 ${truncated(anyNames, options.all)}`);
3447
+ lines.push(anyNames.length === 0 ? "kit:读不到(没找到 client.js)" : `kit:类型面扫不到,只有名字 ${truncated(anyNames, every)}`);
3834
3448
  } else {
3835
- const componentNames = groups.filter((one) => isComponents(one.where)).flatMap((one) => one.names).sort();
3836
- const countOf = (one) => `${isComponents(one.where) ? "组件" : one.where} ${one.names.length}`;
3837
- const counts = [...componentNames.length === 0 ? [] : [`组件 ${componentNames.length}`], ...groups.filter((one) => !isComponents(one.where)).map(countOf)];
3838
- lines.push(`kit ${kitNames.length}:${counts.join(" · ")}`);
3839
- if (componentNames.length > 0) lines.push(` 组件(${componentNames.length}):${truncated(componentNames, options.all)}`);
3840
- for (const group of groups) {
3841
- if (isComponents(group.where)) continue;
3842
- lines.push(` ${group.where}(${group.names.length}):${truncated(group.names, options.all)}`);
3843
- }
3449
+ const buckets = kitBucketsOf(query.kit);
3450
+ lines.push(`kit ${kitNames.length}:${buckets.map((one) => `${one.label} ${one.names.length}`).join(" · ")}`);
3451
+ if (focused) {
3452
+ const matched = buckets.filter((one) => matchesBucket(one.key, bucket));
3453
+ for (const one of matched) lines.push(` ${one.label}(${one.names.length}):${one.names.join(", ")}`);
3454
+ if (matched.length > 0) hit = true;
3455
+ } else for (const one of buckets) lines.push(` ${one.label}(${one.names.length}):${truncated(one.names, every)}`);
3844
3456
  }
3845
3457
  }
3846
3458
  if (query.primitives !== void 0) {
3847
3459
  const version = installedVersionOf(query.primitives);
3848
- const only = Object.entries(recordOf$1(query.primitives["onlyPrimitives"])).map(([bucket, value]) => ({
3849
- bucket,
3850
- names: [...stringsOf$3(value)].sort()
3851
- })).filter((one) => one.names.length > 0);
3460
+ const only = primitiveBucketsOf(query.primitives);
3852
3461
  const covered = (Array.isArray(query.primitives["kitCovered"]) ? query.primitives["kitCovered"] : []).map((one) => recordOf$1(one)["name"]).filter((one) => typeof one === "string");
3853
3462
  if (version === null) {
3854
- const want = asText$2(recordOf$1(recordOf$1(query.primitives["snippet"])["devDependency"])["version"]);
3463
+ const want = asText$3(recordOf$1(recordOf$1(query.primitives["snippet"])["devDependency"])["version"]);
3855
3464
  lines.push(`原语:没装 —— devDependencies 加 "${PRIMITIVES_PKG}": "${want === "" ? "<外壳钉的那版>" : want}" 再 pnpm install`);
3856
- } else lines.push(`原语 ${version}${only.length === 0 ? "" : `:只有它有 ${only.map((one) => `${one.bucket} ${truncated(one.names, options.all)}`).join(" · ")}`}`);
3465
+ } else if (focused) {
3466
+ lines.push(`原语 ${version}${only.length === 0 ? "" : `:只有它有 ${only.map((one) => `${one.bucket} ${one.names.length}`).join(" · ")}`}`);
3467
+ const matched = only.filter((one) => matchesBucket(one.bucket, bucket));
3468
+ for (const one of matched) lines.push(` ${one.bucket}(${one.names.length}):${one.names.join(", ")}`);
3469
+ if (matched.length > 0) hit = true;
3470
+ } else lines.push(`原语 ${version}${only.length === 0 ? "" : `:只有它有 ${only.map((one) => `${one.bucket} ${truncated(one.names, every)}`).join(" · ")}`}`);
3857
3471
  if (only.length > 0 && version !== null) lines.push("(只有原语有的,用之前先在 dsh.client.external 里声明)");
3858
3472
  if (covered.length > 0) lines.push(`kit 里已有:${covered.join(", ")}`);
3859
3473
  }
3474
+ if (focused && !hit) {
3475
+ const keys = bucketNamesOf(query);
3476
+ lines.push(keys.length === 0 ? `没有「${bucket}」这一桶,这次也读不到任何桶。` : `没有「${bucket}」这一桶。可用的桶:${keys.map((one) => `${one.face} → ${one.keys.join(" / ")}`).join(";")}`);
3477
+ }
3860
3478
  const roots = [...Object.entries(rootsOf(query.kit)).map(([key, dir]) => `${key}=${dir}`), ...Object.entries(rootsOf(query.primitives)).map(([key, dir]) => `${key}=${dir}`)];
3861
3479
  if (roots.length > 0) lines.push(`类型面:${roots.join(" · ")}`);
3862
- lines.push(faceStateOf(query, "primitives") === "unreadable" ? "name=<件> 看它怎么用 · all=true 出全表(原语装过才能问原语那类)" : "name=<件> 看它怎么用 · all=true 出全表");
3863
3480
  const text = lines.join("\n");
3864
3481
  if (text.length <= 8192) return text;
3482
+ const commands = (options.scope === "all" ? ["kit", "primitives"] : [options.scope]).map((one) => `\`dsh-wb-module check --only ${one} --all\``).join(" 或 ");
3865
3483
  return [
3866
- `⚠️ 这份清单 ${String(text.length)} 字符,超过 ${String(PRUNE_CHARS)} 会被修剪器裁掉中间:`,
3867
- ` 一件件问(name=<件>)或到 shell 里跑 \`dsh-wb-module check --only ${options.scope === "all" ? "primitives|kit" : options.scope} --all\` 再 grep。`,
3484
+ `⚠️ 这份清单 ${String(text.length)} 字符,超过 ${String(PRUNE_CHARS)} 会被修剪器裁掉中间 —— 按桶问装得下;要一份全的走 shell:${commands} 再 grep。`,
3485
+ " (shell 那份按 barrel 列,计数比这里的落点分组大 —— 两边口径不同。)",
3868
3486
  "",
3869
3487
  text
3870
3488
  ].join("\n");
@@ -3876,7 +3494,7 @@ function listedNamesOf(query) {
3876
3494
  for (const value of Object.values(recordOf$1(payload["buckets"]))) for (const one of stringsOf$3(value)) out.add(one);
3877
3495
  for (const value of Object.values(recordOf$1(payload["onlyPrimitives"]))) for (const one of stringsOf$3(value)) out.add(one);
3878
3496
  for (const one of Array.isArray(payload["kitCovered"]) ? payload["kitCovered"] : []) {
3879
- const covered = asText$2(recordOf$1(one)["name"]);
3497
+ const covered = asText$3(recordOf$1(one)["name"]);
3880
3498
  if (covered !== "") out.add(covered);
3881
3499
  }
3882
3500
  }
@@ -3893,8 +3511,8 @@ function faceStateOf(query, face) {
3893
3511
  }
3894
3512
  function installSnippetOf(query) {
3895
3513
  const dependency = recordOf$1(recordOf$1(query.primitives?.["snippet"])["devDependency"]);
3896
- const name = asText$2(dependency["name"]) || "@deepseek-ai/dsh-client-ui-primitives";
3897
- const version = asText$2(dependency["version"]);
3514
+ const name = asText$3(dependency["name"]) || "@deepseek-ai/dsh-client-ui-primitives";
3515
+ const version = asText$3(dependency["version"]);
3898
3516
  return `devDependencies 加 "${name}": "${version === "" ? "<外壳钉的那版>" : version}" 再 pnpm install`;
3899
3517
  }
3900
3518
  function renderNameText(name, found, query) {
@@ -3912,23 +3530,156 @@ function renderNameText(name, found, query) {
3912
3530
  return lines.join("\n");
3913
3531
  }
3914
3532
  const body = excerptAt(found.file);
3533
+ const head = `${name} → ${found.file}(${found.from === "kit" ? "kit" : "原语"})`;
3915
3534
  if (body.includes("/**")) return [
3916
- `${name} → ${found.file}(${found.from === "kit" ? "kit" : "原语"})`,
3535
+ head,
3917
3536
  "",
3918
3537
  body,
3919
3538
  "",
3920
- found.from === "kit" ? "(上面就是它的声明;@param 写着默认值。kit 件最小写法:KIT.md)" : "(上面就是它的声明;@param 写着默认值。原语件要先在 dsh.client.external 里声明它)"
3539
+ found.from === "kit" ? "(上面就是它的声明与 JSDoc —— 用法、默认值、边界都在里面。跨件的口径与最小写法看 KIT.md)" : "(上面就是它的声明与 JSDoc —— 用法与默认值都在里面。原语件要先在 dsh.client.external 里声明它)"
3921
3540
  ].join("\n");
3922
- const skillDir = skillDirOf(dirname(fileURLToPath(import.meta.url)));
3923
- const doc = docSectionOf(name, skillDir);
3541
+ const second = skillDirOf(dirname(fileURLToPath(import.meta.url))) === null ? "这份外壳连指引也没装 —— 升级它。" : `跨件的口径看 KIT.md(组件怎么拼)· LINKAGE.md / PANEL.md / TOOLS.md(取数 / 写 / 长任务)。`;
3924
3542
  return [
3925
- `${name} → ${found.file}(${found.from === "kit" ? "kit" : "原语"})`,
3543
+ head,
3926
3544
  "",
3927
3545
  body,
3928
3546
  "",
3929
- doc === null ? `(声明没说明文字;${skillDir === null ? "指引也没装" : `指引里没有专门讲「${name}」的一节`} —— 源码零注释,tsc 不 emit JSDoc。用法看 KIT.md / HOOKS.md / LINKAGE.md / PANEL.md / TOOLS.md。)` : `(声明没说明文字(零注释);语义出自 ${basename(doc.file)}:)\n\n${doc.lines}`
3547
+ found.from === "kit" ? `(声明没说明文字(没有 JSDoc)—— kit 的每个成员都该有,selfcheck 里有一条自检钉着;多半是你手上这份外壳比工具旧,升级外壳与工坊工具链再看。${second})` : `(声明没说明文字(没有 JSDoc)—— 装机那份原语本该自带;升级它再看。${second})`
3930
3548
  ].join("\n");
3931
3549
  }
3550
+ //#endregion
3551
+ //#region src/shared/api/host/diy.ts
3552
+ function workshopPathOf(ctx) {
3553
+ const homePath = ctx.get("dshHomePath");
3554
+ if (typeof homePath !== "function") return null;
3555
+ try {
3556
+ return homePath(WORKSHOP_RELATIVE);
3557
+ } catch {
3558
+ return null;
3559
+ }
3560
+ }
3561
+ //#endregion
3562
+ //#region src/shared/api/host/diy-workshop.ts
3563
+ function asRecord$4(value) {
3564
+ return typeof value === "object" && value !== null ? value : void 0;
3565
+ }
3566
+ function asText$2(value) {
3567
+ return typeof value === "string" ? value.trim() : "";
3568
+ }
3569
+ function insideWorkshop(root, target) {
3570
+ return insideWorkshop$1(existsSync(root) ? realpathSync(root) : resolve(root), existsSync(target) ? realpathSync(target) : resolve(target));
3571
+ }
3572
+ function workshopModules(packagesDir, installed = []) {
3573
+ if (!existsSync(packagesDir)) return [];
3574
+ const already = new Set(installed);
3575
+ return readdirSync(packagesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() || entry.isSymbolicLink()).flatMap((entry) => {
3576
+ const dir = join(packagesDir, entry.name);
3577
+ let manifest;
3578
+ try {
3579
+ manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
3580
+ } catch {
3581
+ return [];
3582
+ }
3583
+ const record = asRecord$4(manifest);
3584
+ const declared = record === void 0 ? void 0 : asRecord$4(asRecord$4(record.dsh)?.workbench);
3585
+ const unit = declared === void 0 ? "" : asText$2(declared.unit);
3586
+ if (unit === "") return [];
3587
+ return [{
3588
+ unit,
3589
+ name: asText$2(record?.name),
3590
+ description: asText$2(record?.description),
3591
+ dir,
3592
+ installed: already.has(unit)
3593
+ }];
3594
+ }).sort((left, right) => left.unit.localeCompare(right.unit));
3595
+ }
3596
+ function readDesignOfModule(dir) {
3597
+ const file = join(dir, "design.json");
3598
+ if (!existsSync(file)) return void 0;
3599
+ try {
3600
+ return JSON.parse(readFileSync(file, "utf8"));
3601
+ } catch {
3602
+ return;
3603
+ }
3604
+ }
3605
+ function workshopSyncOf(workshopRoot) {
3606
+ let parsed;
3607
+ try {
3608
+ parsed = JSON.parse(readFileSync(join(workshopRoot, ".sync.json"), "utf8"));
3609
+ } catch {
3610
+ return;
3611
+ }
3612
+ if (typeof parsed !== "object" || parsed === null) return void 0;
3613
+ const record = parsed;
3614
+ const state = asText$2(record["state"]);
3615
+ if (state !== "ok" && state !== "syncing" && state !== "failed") return void 0;
3616
+ const reason = asText$2(record["reason"]);
3617
+ return {
3618
+ state,
3619
+ version: asText$2(record["version"]),
3620
+ at: asText$2(record["at"]),
3621
+ ...reason === "" ? {} : { reason }
3622
+ };
3623
+ }
3624
+ function workshopToolchainOf(workshopRoot) {
3625
+ const versionOf = (name) => {
3626
+ try {
3627
+ return asText$2(JSON.parse(readFileSync(join(workshopRoot, "node_modules", "@dsh-workbench", name, "package.json"), "utf8")).version);
3628
+ } catch {
3629
+ return "";
3630
+ }
3631
+ };
3632
+ const core = versionOf("core");
3633
+ const moduleTools = versionOf("module-tools");
3634
+ const shared = versionOf("shared");
3635
+ if (core === "" || moduleTools === "" || shared === "") return void 0;
3636
+ return {
3637
+ core,
3638
+ moduleTools,
3639
+ shared
3640
+ };
3641
+ }
3642
+ //#endregion
3643
+ //#region src/shared/api/host/workshop-root.ts
3644
+ function rootAt(root, title) {
3645
+ return {
3646
+ root,
3647
+ packagesDir: join(root, "packages"),
3648
+ title
3649
+ };
3650
+ }
3651
+ async function resolveAt(registry, dir) {
3652
+ try {
3653
+ return await registry.resolveByPath(dir);
3654
+ } catch {
3655
+ return;
3656
+ }
3657
+ }
3658
+ function looksLikeWorkshop(dir) {
3659
+ return existsSync(join(dir, "packages")) || existsSync(join(dir, "pnpm-workspace.yaml"));
3660
+ }
3661
+ async function workshopRootOf(ctx, workspace) {
3662
+ const start = typeof workspace === "string" ? workspace.trim() : "";
3663
+ if (start === "") return null;
3664
+ const registry = ctx.get("workspaceRegistry");
3665
+ if (registry !== void 0) {
3666
+ let dir = start;
3667
+ for (;;) {
3668
+ const hit = await resolveAt(registry, dir);
3669
+ if (hit !== void 0) return rootAt(hit.path, hit.title);
3670
+ const parent = dirname(dir);
3671
+ if (parent === dir) break;
3672
+ dir = parent;
3673
+ }
3674
+ }
3675
+ let dir = start;
3676
+ for (;;) {
3677
+ if (looksLikeWorkshop(dir)) return rootAt(dir, basename(dir));
3678
+ const parent = dirname(dir);
3679
+ if (parent === dir) return null;
3680
+ dir = parent;
3681
+ }
3682
+ }
3932
3683
  var OUTPUT$2 = {
3933
3684
  schema: {
3934
3685
  type: "object",
@@ -3988,26 +3739,6 @@ function verdictOf$1(change) {
3988
3739
  const tail = (change.packageResult?.output ?? "").trim().split("\n").slice(-3).join(" | ");
3989
3740
  return `没装上(${change.errorCode ?? "failed"})${tail === "" ? "" : `:${tail}`}`;
3990
3741
  }
3991
- function renderProbe(out) {
3992
- if (out.ok) return `读到了 —— 这就是那条读的返回值,把它固化成模块宿主半侧的那一条:\n${(JSON.stringify(out.value, null, 2) ?? "undefined").slice(0, 2e3)}`;
3993
- const lines = [`没读到(${out.code}):${out.message}`];
3994
- if (out.hint !== void 0) lines.push(`照做:${out.hint}`);
3995
- if (out.helpUrl !== void 0) lines.push(`入口:${out.helpUrl}`);
3996
- if (out.pageCode !== void 0) lines.push(`页面自己回的码:${out.pageCode}`);
3997
- return lines.join("\n");
3998
- }
3999
- function renderFind(out) {
4000
- if (!out.ok) {
4001
- const lines = [`没找到(${out.code}):${out.message}`];
4002
- if (out.hint !== void 0) lines.push(`照做:${out.hint}`);
4003
- if (out.helpUrl !== void 0) lines.push(`入口:${out.helpUrl}`);
4004
- return lines.join("\n");
4005
- }
4006
- if (out.count === 0) return "找到了 0 个元素 —— 选择器没匹配到东西。先确认停在哪一页(probe 会自己问页面),再把选择器放宽一点(例如 table tr 而不是某个 id)。";
4007
- const head = out.entries.slice(0, 20).map((one, index) => `${String(index + 1)}. ${one.text.replace(/\n+/g, " | ").slice(0, 200)}`);
4008
- const more = out.entries.length > 20 ? `\n… 还有 ${String(out.entries.length - 20)} 个` : "";
4009
- return `找到了 ${String(out.count)} 个元素 —— 每行的单元格按换行拼在 text 里;把它固化成模块宿主半侧的那一条(host 的 browserFind + 切格 + 按列序取值):\n${head.join("\n")}${more}`;
4010
- }
4011
3742
  function showOrReason(panels, unit, workspace) {
4012
3743
  try {
4013
3744
  panels.show(unit, workspace);
@@ -4019,14 +3750,14 @@ function showOrReason(panels, unit, workspace) {
4019
3750
  };
4020
3751
  }
4021
3752
  }
4022
- function workshopOf(ctx, exec) {
4023
- const packagesDir = workshopPackagesDir(ctx);
4024
- if (packagesDir === null) throw new Error("算不出 DIY 工坊在哪(这台机器没给出 dshHomePath)—— 让用户跑一次 dsh-workbench setup");
3753
+ async function workshopOf(ctx, exec) {
4025
3754
  const session = sessionWorkspace(exec);
4026
- if (!inWorkshopSession(ctx, session)) throw new Error(`这把工具只在 DIY 工坊里用:当前会话不在工坊里(这个会话的工作区 ${session ?? "(不知道)"})—— 普通项目里装模块用 workbench_modules_install,工坊的入口是左栏那一行「工作台DIY」`);
3755
+ const root = await workshopRootOf(ctx, session);
3756
+ if (root === null) throw new Error(`这个工作区没有模块包可落地:DIY 工具把模块放在会话工作区的 packages/ 下(这个会话的工作区 ${session ?? "(不知道)"})—— 换到一个有模块包的工作区,或者按工程域在仓库里用 dsh-wb-module 新建一个模块包`);
4027
3757
  return {
4028
- packagesDir,
4029
- workspace: session
3758
+ packagesDir: root.packagesDir,
3759
+ workspace: root.root,
3760
+ title: root.title
4030
3761
  };
4031
3762
  }
4032
3763
  function modulesOf(ctx, packagesDir) {
@@ -4036,6 +3767,9 @@ function modulesOf(ctx, packagesDir) {
4036
3767
  modules: workshopModules(packagesDir, installed)
4037
3768
  };
4038
3769
  }
3770
+ function moduleDirsOf(packagesDir) {
3771
+ return workshopModules(packagesDir).map((one) => one.dir);
3772
+ }
4039
3773
  function panelEntriesOf(panels, workspace) {
4040
3774
  try {
4041
3775
  return panels.list(workspace);
@@ -4048,7 +3782,7 @@ function panelEntriesOf(panels, workspace) {
4048
3782
  function componentsTool(ctx) {
4049
3783
  return {
4050
3784
  name: DIY_COMPONENTS_TOOL,
4051
- description: "工坊里能用什么:dsh 原语与外壳 kit 的清单、每件的类型文件在哪(给 name 连 JSDoc 与 prop 一起回)。",
3785
+ description: "工坊里能用什么:dsh 原语与外壳 kit 的清单、每件的类型文件在哪(给 name 连 JSDoc 与 prop 一起回;给 bucket 把那一桶列全 —— 桶名见清单)。",
4052
3786
  parameters: {
4053
3787
  type: "object",
4054
3788
  properties: {
@@ -4058,33 +3792,48 @@ function componentsTool(ctx) {
4058
3792
  },
4059
3793
  scope: {
4060
3794
  type: "string",
4061
- enum: [
4062
- "all",
4063
- "primitives",
4064
- "kit"
4065
- ],
4066
- description: "只看某一面(默认 all)。"
3795
+ enum: ["kit", "primitives"],
3796
+ description: "只看某一面;不填就两个面都给目录。"
3797
+ },
3798
+ bucket: {
3799
+ type: "string",
3800
+ description: "只看某一桶并列全(例 components / icons / shared/hooks;桶名以清单里那份为准)。"
4067
3801
  },
4068
3802
  all: {
4069
3803
  type: "boolean",
4070
- description: "列全表(默认每组前 12 个)。"
3804
+ description: "列全表(默认每组前 12 个)—— 只在指名 scope 时生效;不指名 scope 时只回一句提示,不带目录。"
4071
3805
  }
4072
3806
  },
4073
3807
  additionalProperties: false
4074
3808
  },
4075
3809
  output: OUTPUT$2,
4076
3810
  async execute(raw, exec) {
4077
- const { packagesDir } = workshopOf(ctx, exec);
3811
+ const { packagesDir } = await workshopOf(ctx, exec);
4078
3812
  const root = dirname(packagesDir);
4079
- const bin = toolchainBinOf(root);
4080
- if (bin === null) throw new Error(`工坊里还没装工具链(${join(root, "node_modules")} 里没有 dsh-wb-module)—— 在工坊根目录跑一次 pnpm install 就有了;workbench_diy_status 会告诉你卡在哪一步`);
4081
- const shell = ctx.get("shell");
4082
- if (shell === void 0) throw new Error("这台机器没给 shell 服务,读不了组件清单");
4083
3813
  const args = asRecord$3(raw);
4084
3814
  const scope = scopeOf$1(asText$1(args["scope"]));
4085
3815
  const name = asText$1(args["name"]);
3816
+ const bucket = asText$1(args["bucket"]);
4086
3817
  const all = args["all"] === true;
4087
- const [primitives, kit] = await Promise.all([wantsPrimitives(scope) ? runSurfaceQuery(shell, bin, "primitives", root) : Promise.resolve(void 0), wantsKit(scope) ? runSurfaceQuery(shell, bin, "kit", root) : Promise.resolve(void 0)]);
3818
+ if (name === "" && refusesEvery(scope, all, bucket !== "")) return plainJson({
3819
+ workshop: root,
3820
+ scope,
3821
+ name,
3822
+ bucket,
3823
+ all,
3824
+ file: null,
3825
+ from: null,
3826
+ primitives: null,
3827
+ kit: null,
3828
+ text: TWO_FACE_ALL
3829
+ });
3830
+ const bin = toolchainBinAmong(root, moduleDirsOf(packagesDir));
3831
+ if (bin === null) throw new Error(`工坊里还没装工具链(${join(root, "node_modules")} 与各模块自己的 node_modules 里都没有 dsh-wb-module)—— 在工坊根目录跑一次 pnpm install 就有了;workbench_diy_status 会告诉你卡在哪一步`);
3832
+ const shell = ctx.get("shell");
3833
+ if (shell === void 0) throw new Error("这台机器没给 shell 服务,读不了组件清单");
3834
+ const builtin = workshopPathOf(ctx);
3835
+ const fallbackBin = builtin === null ? void 0 : toolchainBinOf(builtin) ?? void 0;
3836
+ const [primitives, kit] = await Promise.all([wantsPrimitives(scope) ? runSurfaceQuery(shell, bin, "primitives", root, fallbackBin) : Promise.resolve(void 0), wantsKit(scope) ? runSurfaceQuery(shell, bin, "kit", root, fallbackBin) : Promise.resolve(void 0)]);
4088
3837
  const query = {
4089
3838
  ...primitives === void 0 ? {} : { primitives },
4090
3839
  ...kit === void 0 ? {} : { kit }
@@ -4092,12 +3841,14 @@ function componentsTool(ctx) {
4092
3841
  const found = name === "" ? null : findFileOf(name, query);
4093
3842
  const text = name === "" ? renderSurfaceText(query, {
4094
3843
  scope,
4095
- all
3844
+ all,
3845
+ bucket
4096
3846
  }) : renderNameText(name, found, query);
4097
3847
  return plainJson({
4098
3848
  workshop: root,
4099
3849
  scope,
4100
3850
  name,
3851
+ bucket,
4101
3852
  all,
4102
3853
  file: found?.file ?? null,
4103
3854
  from: found?.from ?? null,
@@ -4274,8 +4025,9 @@ function debugForeignPactText(held) {
4274
4025
  return `另一场调试正占着那块屏(${held.sessionId} 在调试 ${held.unit})—— 一台机器同时只允许一场:让它先 workbench_diy_debug_stop,或等它收工。`;
4275
4026
  }
4276
4027
  function debugOffBoardText(held, situation = "other-board") {
4277
- const what = situation === "off-workspace" ? "用户现在不在 DIY 工作区(外壳选中的是别的工作区)" : situation === "panel-closed" ? "工作台面板没画在屏上(右栏收着,或那一列让给了别的面板)" : "你现在这一页画的不是工坊那块板";
4278
- const next = situation === "off-workspace" ? "让用户在左栏切回「工作台DIY」那个工作区" : situation === "panel-closed" ? "让用户把「工作台面板」切回来(或把右栏展开)" : "让用户把那一页切回「工作台DIY」";
4028
+ const where = held.title === "" ? "这一场调试所在的工作区" : `「${held.title}」`;
4029
+ const what = situation === "off-workspace" ? `用户现在不在${where}(外壳选中的是别的工作区)` : situation === "panel-closed" ? "工作台面板没画在屏上(右栏收着,或那一列让给了别的面板)" : `你现在这一页画的不是${where}那块板`;
4030
+ const next = situation === "off-workspace" ? `让用户在左栏切回${where}` : situation === "panel-closed" ? "让用户把「工作台面板」切回来(或把右栏展开)" : `让用户把那一页切回${where}`;
4279
4031
  return `${what} —— 看 / 动 / 网络这一族现在发不出去,我不拿别的板当目标。${held.unit} 这场调试仍然**进行中**(我没有自动替你落 paused):${next};要停下来就自己 workbench_diy_debug_pause(不问审批),页面切回来了就接着看。`;
4280
4032
  }
4281
4033
  function boardProbeOf(page) {
@@ -4291,13 +4043,14 @@ function boardProbeOf(page) {
4291
4043
  function boardStateOf(page, workshop) {
4292
4044
  return boardFactOf(boardProbeOf(page), workshop);
4293
4045
  }
4294
- function boardGateText(situation, unit, page) {
4295
- if (situation === "off-workspace") return `用户现在不在 DIY 工作区 —— 外壳选中的是 ${page.where || "(问不出来是哪一块)"}。工坊那块板没在屏上,${unit} 的这张卡我看不到,也不拿别的板当目标。让用户在左栏切回「工作台DIY」;这之前我可以继续改源码 / build / 跑门禁。`;
4046
+ function boardGateText(situation, unit, page, title = "") {
4047
+ const where = title === "" ? "这一场调试所在的工作区" : `「${title}」`;
4048
+ if (situation === "off-workspace") return `用户现在不在${where} —— 外壳选中的是 ${page.where || "(问不出来是哪一块)"}。${where}那块板没在屏上,${unit} 的这张卡我看不到,也不拿别的板当目标。让用户在左栏切回${where};这之前我可以继续改源码 / build / 跑门禁。`;
4296
4049
  if (situation === "panel-closed") return `工作台面板没画在屏上(右栏收着,或者这一页让给了别的面板:文件预览、模块市场等)。让用户把「工作台面板」切回来(或把右栏展开),我再接着看 ${unit}。`;
4297
- return `你现在这一页画的是 ${page.board || "(不知道哪一块)"} 的面板,工坊那块板没在这一屏 —— ${unit} 的这张卡不在屏上,我不拿别的板当目标。请用户在左栏切回「工作台DIY」;这之前我可以继续改源码 / build / 跑门禁。`;
4050
+ return `你现在这一页画的是 ${page.board || "(不知道哪一块)"} 的面板,${where}那块板没在这一屏 —— ${unit} 的这张卡不在屏上,我不拿别的板当目标。请用户在左栏切回${where};这之前我可以继续改源码 / build / 跑门禁。`;
4298
4051
  }
4299
4052
  async function pageAt(ctx, exec, raw, options = {}) {
4300
- const { packagesDir } = workshopOf(ctx, exec);
4053
+ const { packagesDir, workspace, title } = await workshopOf(ctx, exec);
4301
4054
  const args = asRecord$3(raw);
4302
4055
  const asked = asText$1(args["unit"]);
4303
4056
  const modules = modulesOf(ctx, packagesDir).modules;
@@ -4320,11 +4073,10 @@ async function pageAt(ctx, exec, raw, options = {}) {
4320
4073
  const { page } = handshake;
4321
4074
  throw new Error(`你现在这一页不是工作台(${page.title || "(没有标题)"} ${page.url})—— 把工作台那一页切到前台,再喊我一次。我只看用户正在看的那一页。`);
4322
4075
  }
4323
- const workshop = workshopPathOf(ctx);
4324
- const board = boardStateOf(handshake.page, workshop);
4325
- const situation = boardSituation(boardProbeOf(handshake.page), workshop);
4076
+ const board = boardStateOf(handshake.page, workspace);
4077
+ const situation = boardSituation(boardProbeOf(handshake.page), workspace);
4326
4078
  if (options.requirePact !== false && held !== null && (board === "other" || board === "hidden")) throw new Error(debugOffBoardText(held, situation));
4327
- if (options.requireBoard !== false && (board === "other" || board === "hidden")) throw new Error(boardGateText(situation, wanted, handshake.page));
4079
+ if (options.requireBoard !== false && (board === "other" || board === "hidden")) throw new Error(boardGateText(situation, wanted, handshake.page, title));
4328
4080
  return {
4329
4081
  packagesDir,
4330
4082
  unit: wanted,
@@ -4365,7 +4117,7 @@ function renderDebugPause(state) {
4365
4117
  return [`调试暂停了(${state.unit})—— 动作族关闸,不再拿屏当目标。`, "改源码 / build / 跑门禁照常;要看屏再 workbench_diy_debug_resume(会再要用户点头一次)。"].join("\n");
4366
4118
  }
4367
4119
  function renderDebugResume(state) {
4368
- return [`继续调试 ${state.unit} —— 请用户回到「工作台DIY」那一屏。`, "卡不在屏上时我会当场拒(不拿别的板当目标)。"].join("\n");
4120
+ return [`继续调试 ${state.unit} —— 请用户回到「${state.title}」那一屏。`, "卡不在屏上时我会当场拒(不拿别的板当目标)。"].join("\n");
4369
4121
  }
4370
4122
  function renderDebugStop(state) {
4371
4123
  return `收工了:${state.unit} 的调试态清掉了(占用解除)。`;
@@ -4389,7 +4141,7 @@ function registerDebugTools(ctx, tools, store) {
4389
4141
  },
4390
4142
  output: OUTPUT$2,
4391
4143
  async execute(raw, exec) {
4392
- const { packagesDir, workspace } = workshopOf(ctx, exec);
4144
+ const { packagesDir, workspace, title } = await workshopOf(ctx, exec);
4393
4145
  const unit = asText$1(asRecord$3(raw)["unit"]);
4394
4146
  if (unit === "") {
4395
4147
  const { modules } = modulesOf(ctx, packagesDir);
@@ -4402,7 +4154,8 @@ function registerDebugTools(ctx, tools, store) {
4402
4154
  });
4403
4155
  const state = moveOrThrow$1(store.begin({
4404
4156
  sessionId: sessionIdOf(exec) ?? "",
4405
- workspace: workspace ?? "",
4157
+ workspacePath: workspace ?? "",
4158
+ title,
4406
4159
  board: page.boardPath,
4407
4160
  unit,
4408
4161
  name: modulesOf(ctx, packagesDir).modules.find((one) => one.unit === unit)?.name ?? unit,
@@ -4424,7 +4177,7 @@ function registerDebugTools(ctx, tools, store) {
4424
4177
  parameters: EMPTY,
4425
4178
  output: OUTPUT$2,
4426
4179
  async execute(_raw, exec) {
4427
- const { packagesDir } = workshopOf(ctx, exec);
4180
+ const { packagesDir } = await workshopOf(ctx, exec);
4428
4181
  const held = store.live(Date.now());
4429
4182
  const state = moveOrThrow$1(store.pause(sessionIdOf(exec) ?? "", Date.now()), held);
4430
4183
  return plainJson({
@@ -4438,11 +4191,11 @@ function registerDebugTools(ctx, tools, store) {
4438
4191
  };
4439
4192
  const resumeTool = {
4440
4193
  name: DIY_DEBUG_RESUME_TOOL,
4441
- description: "继续调试(要点头一次):请用户回到「工作台DIY」那一屏。",
4194
+ description: "继续调试(要点头一次):请用户回到这一场调试所在的那一屏。",
4442
4195
  parameters: EMPTY,
4443
4196
  output: OUTPUT$2,
4444
4197
  async execute(_raw, exec) {
4445
- const { packagesDir } = workshopOf(ctx, exec);
4198
+ const { packagesDir } = await workshopOf(ctx, exec);
4446
4199
  const held = store.live(Date.now());
4447
4200
  const page = await pageAt(ctx, exec, { unit: held?.unit ?? "" }, {
4448
4201
  requireBoard: false,
@@ -4466,7 +4219,7 @@ function registerDebugTools(ctx, tools, store) {
4466
4219
  parameters: EMPTY,
4467
4220
  output: OUTPUT$2,
4468
4221
  async execute(_raw, exec) {
4469
- const { packagesDir } = workshopOf(ctx, exec);
4222
+ const { packagesDir } = await workshopOf(ctx, exec);
4470
4223
  const held = store.live(Date.now());
4471
4224
  const state = moveOrThrow$1(store.stop(sessionIdOf(exec) ?? "", Date.now()), held);
4472
4225
  await store.settle?.();
@@ -5149,7 +4902,11 @@ function parametersOf(row) {
5149
4902
  },
5150
4903
  timeoutMs: {
5151
4904
  type: "number",
5152
- description: "超时毫秒(默认 30000;≤0 或非数也按默认算)。"
4905
+ description: "超时毫秒(默认 30000)。"
4906
+ },
4907
+ read: {
4908
+ type: "string",
4909
+ description: "只读线:给站点地址(不碰你那一屏、不要调试约定)。"
5153
4910
  }
5154
4911
  };
5155
4912
  if (row.css !== void 0) properties["scope"] = {
@@ -5217,7 +4974,80 @@ function needsCardOf(row, raw) {
5217
4974
  if (row.css !== void 0 && asText$1(raw["scope"]) === "card") return true;
5218
4975
  return stringsOf$2(raw["args"]).some((one) => one.includes(CARD_TOKEN));
5219
4976
  }
4977
+ function hostOf$1(url) {
4978
+ try {
4979
+ return new URL(url).host;
4980
+ } catch {
4981
+ return "";
4982
+ }
4983
+ }
4984
+ async function onSiteOnce(shell, command, site) {
4985
+ const step = await runBrowserStep(shell, command, [
4986
+ "browser",
4987
+ BROWSER_READ_SESSION,
4988
+ "get",
4989
+ "url",
4990
+ "--window",
4991
+ "background"
4992
+ ], LOOK_TIMEOUT_MS);
4993
+ const want = hostOf$1(site);
4994
+ if (want === "" || hostOf$1(step.stdout.trim()) === want) return false;
4995
+ await runBrowserStep(shell, command, [
4996
+ "browser",
4997
+ BROWSER_READ_SESSION,
4998
+ "open",
4999
+ site,
5000
+ "--window",
5001
+ "background"
5002
+ ], LOOK_TIMEOUT_MS);
5003
+ return true;
5004
+ }
5005
+ function loginHintOf(stdout, site) {
5006
+ const code = jsonOf(stdout)?.["code"];
5007
+ if (code === "not-logged-in") return `没登录:去 ${site} 登一次再重跑(读线的约定:脚本回 { ok: false, code: "not-logged-in" })。`;
5008
+ if (code === "not-on-site") return `还不在 ${site} 上:我开过一次了 —— 让用户在那扇窗里切过去登一次,再重跑。`;
5009
+ return "";
5010
+ }
5011
+ function readRefusalOf(row, raw) {
5012
+ if (asText$1(raw["unit"]) !== "" || asText$1(raw["scope"]) === "card" || asText$1(raw["in"]) === "card" || stringsOf$2(raw["args"]).some((one) => one.includes("$card"))) return "只读线上没有「用户那张卡」:unit / scope=card / in=card / $card 只在屏那一档能用。";
5013
+ return row.clip === true ? "只读线不截图裁剪 —— clip 是屏那一档的事(要图走 _screenshot)。" : "";
5014
+ }
5015
+ async function runReadTool(row, ctx, raw, site) {
5016
+ const refused = readRefusalOf(row, raw);
5017
+ if (refused !== "") throw new Error(refused);
5018
+ const shell = ctx.get("shell");
5019
+ if (shell === void 0) throw new Error("这台机器上没有 shell 服务,驱动不了浏览器");
5020
+ const command = opencliCommandOf(ctx).command;
5021
+ const requested = asNumber(raw["timeoutMs"]);
5022
+ const timeoutMs = requested !== void 0 && requested > 0 ? requested : DEFAULT_TIMEOUT_MS;
5023
+ const argv = stringsOf$2(raw["args"]);
5024
+ const opened = await onSiteOnce(shell, command, site);
5025
+ const step = await runBrowserStep(shell, command, [
5026
+ "browser",
5027
+ BROWSER_READ_SESSION,
5028
+ ...row.verb,
5029
+ ...argv,
5030
+ "--window",
5031
+ "background"
5032
+ ], timeoutMs);
5033
+ const hint = row.tool === "workbench_diy_eval" ? loginHintOf(step.stdout, site) : "";
5034
+ return {
5035
+ session: BROWSER_READ_SESSION,
5036
+ verb: row.verb.join(" "),
5037
+ argv,
5038
+ opened,
5039
+ ok: step.ok,
5040
+ exitCode: step.exitCode,
5041
+ timedOut: step.timedOut,
5042
+ truncated: step.truncated,
5043
+ stdout: hint === "" ? step.stdout : `${step.stdout}\n${hint}`,
5044
+ stderr: step.stderr
5045
+ };
5046
+ }
5220
5047
  async function runBrowserTool(row, ctx, raw, exec) {
5048
+ const args = asRecord$3(raw);
5049
+ const read = asText$1(args["read"]);
5050
+ if (read !== "") return runReadTool(row, ctx, args, read);
5221
5051
  const page = await pageAt(ctx, exec, raw, { requireUnit: false });
5222
5052
  if (needsCardOf(row, page.args) && page.unit === "") throw new Error("要卡作用域(scope=card / $card / in=card)就得给一个 unit —— 工坊里不止一个模块时,我不知道你说哪一张卡。");
5223
5053
  const requested = asNumber(page.args["timeoutMs"]);
@@ -5525,7 +5355,7 @@ function nextOf(answer) {
5525
5355
  }
5526
5356
  const plugin = answer.plugins[0];
5527
5357
  if (plugin !== void 0) return `这台机器另装的插件 ${plugin.name} 里有匹配:opencli ${plugin.name} --help 看全部命令`;
5528
- return "这台机器上没有现成的 —— 两条路:① 给这个站点写 adapter:opencli skills read opencli-adapter-author;② 只读一次:workbench_diy_probe(script 读接口 / css 读 DOM)";
5358
+ return "这台机器上没有现成的 —— 两条路:① 给这个站点写 adapter:opencli skills read opencli-adapter-author;② 只读一次:workbench_diy_eval / _find 带 read=<地址>(在 workbench-read 上跑,借用户已登录的那扇窗)";
5529
5359
  }
5530
5360
  function signatureOf$1(args) {
5531
5361
  const shown = args.slice(0, ARG_LIMIT).map((one) => {
@@ -6055,7 +5885,7 @@ function registerDiyTools(ctx, market, panels, debug) {
6055
5885
  },
6056
5886
  output: OUTPUT$2,
6057
5887
  async execute(_raw, exec) {
6058
- const { packagesDir } = workshopOf(ctx, exec);
5888
+ const { packagesDir } = await workshopOf(ctx, exec);
6059
5889
  const { modules } = modulesOf(ctx, packagesDir);
6060
5890
  return plainJson({
6061
5891
  workshop: packagesDir,
@@ -6078,7 +5908,7 @@ function registerDiyTools(ctx, market, panels, debug) {
6078
5908
  },
6079
5909
  output: OUTPUT$2,
6080
5910
  async execute(raw, exec) {
6081
- const { packagesDir, workspace } = workshopOf(ctx, exec);
5911
+ const { packagesDir, workspace } = await workshopOf(ctx, exec);
6082
5912
  const { modules } = modulesOf(ctx, packagesDir);
6083
5913
  const registry = ctx.get(UNITS_SERVICE);
6084
5914
  const panelEntries = panelEntriesOf(panels, workspace);
@@ -6118,7 +5948,7 @@ function registerDiyTools(ctx, market, panels, debug) {
6118
5948
  },
6119
5949
  output: OUTPUT$2,
6120
5950
  async execute(raw, exec) {
6121
- const { packagesDir, workspace } = workshopOf(ctx, exec);
5951
+ const { packagesDir, workspace } = await workshopOf(ctx, exec);
6122
5952
  const { installed, modules } = modulesOf(ctx, packagesDir);
6123
5953
  const panelState = ctx.get(PANEL_STATE_SERVICE);
6124
5954
  const registry = ctx.get(UNITS_SERVICE);
@@ -6186,7 +6016,7 @@ function registerDiyTools(ctx, market, panels, debug) {
6186
6016
  },
6187
6017
  output: OUTPUT$2,
6188
6018
  async execute(raw, exec) {
6189
- const { packagesDir } = workshopOf(ctx, exec);
6019
+ const { packagesDir } = await workshopOf(ctx, exec);
6190
6020
  const args = asRecord$3(raw);
6191
6021
  const keywords = keywordsOf(asText$1(args["keyword"]), asText$1(args["url"]));
6192
6022
  if (keywords.length === 0) throw new Error("recon 要一个关键词(站点名 / 系统名 / 域名都行):先拿它去 opencli 自带的那份目录里找有没有现成的能力,别一上来就自己写 adapter;用户不知道 opencli 是什么,这一步别问他。");
@@ -6199,96 +6029,6 @@ function registerDiyTools(ctx, market, panels, debug) {
6199
6029
  });
6200
6030
  }
6201
6031
  };
6202
- const probeTool = {
6203
- name: DIY_PROBE_TOOL,
6204
- description: "用用户浏览器里现有的登录探一条读:script 读接口 / css 读 DOM。不在站点上会自己打开 url。",
6205
- parameters: {
6206
- type: "object",
6207
- properties: {
6208
- script: {
6209
- type: "string",
6210
- description: "与 css 二选一:页面里跑一段回 JSON 的脚本。不在站点回 { ok: false, code: \"not-on-site\" },登录过期回 \"not-logged-in\",成功回 { ok: true, ...数据 }。"
6211
- },
6212
- css: {
6213
- type: "string",
6214
- description: "与 script 二选一:给一个 CSS 选择器,宿主回匹配到的元素文本(每行单元格按换行拼在 text 里)。"
6215
- },
6216
- limit: {
6217
- type: "number",
6218
- description: "配 css 用:最多回几个元素(默认 50)。"
6219
- },
6220
- textMax: {
6221
- type: "number",
6222
- description: "配 css 用:每个元素文本最多留几个字(默认 200)。"
6223
- },
6224
- url: {
6225
- type: "string",
6226
- description: "那套系统的地址(不在站点上时自己打开它再读一遍)。"
6227
- },
6228
- loginUrl: {
6229
- type: "string",
6230
- description: "要登录时那颗\"去登录\"该去哪(默认用 url)。"
6231
- },
6232
- session: {
6233
- type: "string",
6234
- description: "会话名(默认读那条,一般不用填)。"
6235
- },
6236
- open: {
6237
- type: "boolean",
6238
- description: "不在站点上时要不要顺手打开(默认要)。"
6239
- }
6240
- },
6241
- additionalProperties: false
6242
- },
6243
- output: OUTPUT$2,
6244
- async execute(raw, exec) {
6245
- const { packagesDir } = workshopOf(ctx, exec);
6246
- const args = asRecord$3(raw);
6247
- const script = asText$1(args["script"]);
6248
- const css = asText$1(args["css"]);
6249
- if (script === "" && css === "") throw new Error("probe 要 script(页面脚本,回 JSON)或 css(读 DOM 那一行的选择器)之一。script 的约定:不在那个站点上就回 { ok: false, code: \"not-on-site\" }(我会自己把它打开再读一遍);登录过期回 { ok: false, code: \"not-logged-in\" };成功回 { ok: true, ...数据 }。页面里压根没有接口时给 css,别写 document.querySelectorAll(宿主产物不许出现 DOM 字样)。");
6250
- const shell = ctx.get("shell");
6251
- if (shell === void 0) throw new Error("这台机器上没有 shell 服务,probe 跑不了");
6252
- const url = asText$1(args["url"]);
6253
- const loginUrl = asText$1(args["loginUrl"]);
6254
- const session = asText$1(args["session"]);
6255
- if (session === "workbench") throw new Error(`"${BROWSER_SCREEN_SESSION}" 是用户那一屏那条会话 —— probe 只读自己的线(不填就落在读那条上,想给自己一条专线就传自己的 unit)。要看 / 动他那一屏走 workbench_diy_state / _find / _click 那一族,而且要先 workbench_diy_debug_start(会请他点头一次)。`);
6256
- const open = typeof args["open"] === "boolean" ? args["open"] : void 0;
6257
- if (css !== "") {
6258
- const limit = asNumber(args["limit"]);
6259
- const textMax = asNumber(args["textMax"]);
6260
- const found = await browserFind({
6261
- shell,
6262
- css,
6263
- command: opencliCommandOf(ctx).command,
6264
- ...url === "" ? {} : { url },
6265
- ...session === "" ? {} : { session },
6266
- ...open === void 0 ? {} : { open },
6267
- ...limit === void 0 ? {} : { limit },
6268
- ...textMax === void 0 ? {} : { textMax }
6269
- });
6270
- return plainJson({
6271
- workshop: packagesDir,
6272
- ...found,
6273
- text: renderFind(found)
6274
- });
6275
- }
6276
- const out = await browserRead({
6277
- shell,
6278
- script,
6279
- command: opencliCommandOf(ctx).command,
6280
- ...url === "" ? {} : { url },
6281
- ...loginUrl === "" ? {} : { loginUrl },
6282
- ...session === "" ? {} : { session },
6283
- ...open === void 0 ? {} : { open }
6284
- });
6285
- return plainJson({
6286
- workshop: packagesDir,
6287
- ...out,
6288
- text: renderProbe(out)
6289
- });
6290
- }
6291
- };
6292
6032
  function unitOf(packagesDir, raw, label) {
6293
6033
  const { modules } = modulesOf(ctx, packagesDir);
6294
6034
  const unit = asText$1(asRecord$3(raw)["unit"]);
@@ -6312,7 +6052,7 @@ function registerDiyTools(ctx, market, panels, debug) {
6312
6052
  },
6313
6053
  output: OUTPUT$2,
6314
6054
  async execute(raw, exec) {
6315
- const { packagesDir, workspace } = workshopOf(ctx, exec);
6055
+ const { packagesDir, workspace } = await workshopOf(ctx, exec);
6316
6056
  const found = unitOf(packagesDir, raw, "workbench_diy_show");
6317
6057
  if (!found.installed) throw new Error(`${found.unit} 还没装 —— 只上板的前提是它装过了。先用 workbench_diy_install(第一次)`);
6318
6058
  const panel = showOrReason(panels, found.unit, workspace);
@@ -6340,7 +6080,7 @@ function registerDiyTools(ctx, market, panels, debug) {
6340
6080
  },
6341
6081
  output: OUTPUT$2,
6342
6082
  async execute(raw, exec) {
6343
- const { packagesDir, workspace } = workshopOf(ctx, exec);
6083
+ const { packagesDir, workspace } = await workshopOf(ctx, exec);
6344
6084
  const found = unitOf(packagesDir, raw, "workbench_diy_install");
6345
6085
  if (found.installed) {
6346
6086
  const panel = showOrReason(panels, found.unit, workspace);
@@ -6385,7 +6125,6 @@ function registerDiyTools(ctx, market, panels, debug) {
6385
6125
  ...registerComponentsTool(ctx, tools),
6386
6126
  tools.register(reconTool),
6387
6127
  ...registerSpecTool(ctx, tools),
6388
- tools.register(probeTool),
6389
6128
  ...registerLogTools(ctx, tools),
6390
6129
  ...registerBrowserTools(ctx, tools),
6391
6130
  ...registerDebugTools(ctx, tools, debug),
@@ -6395,14 +6134,13 @@ function registerDiyTools(ctx, market, panels, debug) {
6395
6134
  }
6396
6135
  //#endregion
6397
6136
  //#region src/modules/panels/model/diy-observers.ts
6398
- async function stopObservers(ctx, session = BROWSER_SCREEN_SESSION) {
6399
- const packagesDir = workshopPackagesDir(ctx);
6137
+ async function stopObservers(ctx, workspace, session = BROWSER_SCREEN_SESSION) {
6400
6138
  const shell = ctx.get("shell");
6401
- if (packagesDir === null || shell === void 0) return {
6139
+ if (workspace === null || workspace === void 0 || workspace === "" || shell === void 0) return {
6402
6140
  net: false,
6403
6141
  logs: false
6404
6142
  };
6405
- const file = join(dirname(packagesDir), "shots", `net-${session}.jsonl`);
6143
+ const file = join(workspace, "shots", `net-${session}.jsonl`);
6406
6144
  const command = opencliCommandOf(ctx).command;
6407
6145
  const [net, step] = await Promise.all([stopNetworkExact(shell, file), actBrowser(shell, command, session, ["eval", LOGS_STOP_SCRIPT], SWEEP_TIMEOUT_MS)]);
6408
6146
  return {
@@ -6413,13 +6151,13 @@ async function stopObservers(ctx, session = BROWSER_SCREEN_SESSION) {
6413
6151
  function withObserverCleanup(store, cleanup, logger) {
6414
6152
  let pending = Promise.resolve();
6415
6153
  let epoch = 0;
6416
- const sweep = async (at) => {
6154
+ const sweep = async (at, workspace) => {
6417
6155
  if (epoch !== at) {
6418
6156
  logger?.warn?.("收工那一拍:已经有新的调试约定开始了 —— 跳过收观察(新那一场自己收)");
6419
6157
  return;
6420
6158
  }
6421
6159
  try {
6422
- const out = await cleanup();
6160
+ const out = await cleanup(workspace);
6423
6161
  if (!out.net || !out.logs) logger?.warn?.(`收工那一拍没把观察收干净(网络 ${String(out.net)} / 日志 ${String(out.logs)})—— 网络那条最多 15 分钟自己收工,日志钩子随页面刷新消失`);
6424
6162
  } catch (error) {
6425
6163
  logger?.warn?.(`收工那一拍收观察抛了:${error instanceof Error ? error.message : String(error)}`);
@@ -6437,7 +6175,8 @@ function withObserverCleanup(store, cleanup, logger) {
6437
6175
  const move = store.stop(sessionId, now);
6438
6176
  if (move.ok) {
6439
6177
  const at = epoch;
6440
- pending = pending.then(() => sweep(at));
6178
+ const workspace = move.state.workspacePath;
6179
+ pending = pending.then(() => sweep(at, workspace));
6441
6180
  }
6442
6181
  return move;
6443
6182
  },
@@ -6494,13 +6233,13 @@ var INTENT_WORD$1 = {
6494
6233
  stop: "结束"
6495
6234
  };
6496
6235
  function debugNoticeText(intent, state) {
6497
- const head = `[工作台 DIY]用户按了「${INTENT_WORD$1[intent]}」:`;
6236
+ const head = `[调试 · ${state.title === "" ? "工作区" : state.title}]用户按了「${INTENT_WORD$1[intent]}」:`;
6498
6237
  if (intent === "pause") return `${head}${state.unit} 这场调试暂停了 —— 现在别再把那一屏当目标(看 / 动 / 网络那一族发不出去)。要接着看屏就等用户按「继续」;在那之前照常改源码 / build / 跑门禁。`;
6499
6238
  if (intent === "resume") return `${head}${state.unit} 这场调试重新开始了 —— 那一屏又归你看。屏 / 板不对的话照旧会被拒,拒了就照实说清是哪种不对。`;
6500
6239
  return `${head}${state.unit} 这场调试收工了(占用解除)。要再调试就重新 start 一场。`;
6501
6240
  }
6502
6241
  function debugNoticeSummary(intent, state) {
6503
- return `工作台 DIY:用户按了${INTENT_WORD$1[intent]}(${state.unit})`;
6242
+ return `调试:用户按了${INTENT_WORD$1[intent]}(${state.unit})`;
6504
6243
  }
6505
6244
  function debugNotice(intent, state) {
6506
6245
  const summary = debugNoticeSummary(intent, state);
@@ -7702,7 +7441,7 @@ function registerRecordTools(ctx, store) {
7702
7441
  },
7703
7442
  output: OUTPUT$2,
7704
7443
  async execute(raw, exec) {
7705
- const { packagesDir, workspace } = workshopOf(ctx, exec);
7444
+ const { packagesDir, workspace } = await workshopOf(ctx, exec);
7706
7445
  const args = asRecord$3(raw);
7707
7446
  const unit = asText$1(args["unit"]);
7708
7447
  const modules = modulesOf(ctx, packagesDir).modules;
@@ -7811,7 +7550,7 @@ function registerRecordTools(ctx, store) {
7811
7550
  },
7812
7551
  output: OUTPUT$2,
7813
7552
  async execute(raw, exec) {
7814
- const { packagesDir } = workshopOf(ctx, exec);
7553
+ const { packagesDir } = await workshopOf(ctx, exec);
7815
7554
  const shell = ctx.get("shell");
7816
7555
  if (shell === void 0) throw new Error("这台机器上没有 shell 服务,读不了录制");
7817
7556
  const sessionId = sessionIdOf(exec) ?? "";
@@ -7927,7 +7666,7 @@ function registerRecordTools(ctx, store) {
7927
7666
  },
7928
7667
  output: OUTPUT$2,
7929
7668
  async execute(raw, exec) {
7930
- const { packagesDir } = workshopOf(ctx, exec);
7669
+ const { packagesDir } = await workshopOf(ctx, exec);
7931
7670
  const shell = ctx.get("shell");
7932
7671
  if (shell === void 0) throw new Error("这台机器上没有 shell 服务,取不了详情");
7933
7672
  const args = asRecord$3(raw);
@@ -8007,7 +7746,7 @@ function registerRecordTools(ctx, store) {
8007
7746
  },
8008
7747
  output: OUTPUT$2,
8009
7748
  async execute(_raw, exec) {
8010
- const { packagesDir } = workshopOf(ctx, exec);
7749
+ const { packagesDir } = await workshopOf(ctx, exec);
8011
7750
  const shell = ctx.get("shell");
8012
7751
  if (shell === void 0) throw new Error("这台机器上没有 shell 服务,收不了工");
8013
7752
  const sessionId = sessionIdOf(exec) ?? "";
@@ -10457,7 +10196,7 @@ var OWNED = [{
10457
10196
  logger
10458
10197
  });
10459
10198
  ctx.provide(PANELS_SERVICE, panels);
10460
- const debug = withObserverCleanup(createDebugStore(), () => stopObservers(ctx), logger);
10199
+ const debug = withObserverCleanup(createDebugStore(), (workspace) => stopObservers(ctx, workspace), logger);
10461
10200
  ctx.provide(DIY_DEBUG_SERVICE, debug);
10462
10201
  const record = {
10463
10202
  ...createRecordStore(),
@@ -10537,13 +10276,13 @@ var OWNED = [{
10537
10276
  factsOf: (call) => panelsFactsOf(call, manifests()),
10538
10277
  needsApproval: panelsApproval,
10539
10278
  gate: {
10540
- deniedTools: (workspace) => {
10541
- const boards = panels.boards(workspace);
10279
+ deniedTools: (context) => {
10280
+ const boards = panels.boards(context.workspace);
10542
10281
  const owned = new Set(ownedUnits());
10543
10282
  return [...manifests().flatMap((manifest) => owned.has(manifest.id) || resolveScope({
10544
10283
  id: manifest.id,
10545
10284
  boards
10546
- }) ? [] : manifest.tools.map((tool) => tool.name)), ...inWorkshopSession(ctx, workspace) ? [] : [
10285
+ }) ? [] : manifest.tools.map((tool) => tool.name)), ...context.preset === "workbench-diy" ? [] : [
10547
10286
  ...DIY_TOOLS,
10548
10287
  ...DIY_DEBUG_TOOLS,
10549
10288
  ...DIY_RECORD_TOOLS
@@ -13038,9 +12777,15 @@ function configSyncAction(deps) {
13038
12777
  function agentOf(value) {
13039
12778
  return typeof value === "object" && value !== null ? value : void 0;
13040
12779
  }
13041
- function workspaceOf(handle) {
13042
- const cwd = handle.session?.header?.cwd;
13043
- return typeof cwd === "string" && cwd !== "" ? cwd : null;
12780
+ function contextOf(handle, options) {
12781
+ const header = handle.session?.header;
12782
+ const cwd = header?.cwd;
12783
+ const declared = header?.agentPreset;
12784
+ const read = options.presetOf?.(handle);
12785
+ return {
12786
+ workspace: typeof cwd === "string" && cwd !== "" ? cwd : null,
12787
+ preset: read ?? (typeof declared === "string" && declared !== "" ? declared : null)
12788
+ };
13044
12789
  }
13045
12790
  function createToolScope(options) {
13046
12791
  const applied = /* @__PURE__ */ new WeakMap();
@@ -13057,9 +12802,9 @@ function createToolScope(options) {
13057
12802
  options.logger?.warn?.("[dsh-workbench] 拿不到 agent 作用域的工具面:这次不收窄(下次再试)");
13058
12803
  return;
13059
12804
  }
13060
- const workspace = workspaceOf(handle);
13061
- const deny = [...options.deniedTools(workspace)];
13062
- const signature = `${workspace ?? ""}\u0000${deny.join(",")}`;
12805
+ const context = contextOf(handle, options);
12806
+ const deny = [...options.deniedTools(context)];
12807
+ const signature = `${context.workspace ?? ""}\u0000${context.preset ?? ""}\u0000${deny.join(",")}`;
13063
12808
  const previous = applied.get(handle);
13064
12809
  if (previous !== void 0 && previous.signature === signature) return;
13065
12810
  if (previous === void 0) seen.add(new WeakRef(handle));
@@ -13069,12 +12814,13 @@ function createToolScope(options) {
13069
12814
  applied.delete(handle);
13070
12815
  return;
13071
12816
  }
12817
+ const where = context.preset === null ? context.workspace : `${context.workspace ?? "(未知工作区)"} · ${context.preset}`;
13072
12818
  try {
13073
12819
  applied.set(handle, {
13074
12820
  signature,
13075
12821
  dispose: tools.restrict({ deny })
13076
12822
  });
13077
- options.logger?.info?.(`[dsh-workbench] 按工作区收窄工具面:${workspace ?? "(未知工作区)"} 里收起 ${deny.length} 个工具(${deny.join(", ")})`);
12823
+ options.logger?.info?.(`[dsh-workbench] 按会话收窄工具面:${where ?? "(未知工作区)"} 里收起 ${deny.length} 个工具(${deny.join(", ")})`);
13078
12824
  } catch (error) {
13079
12825
  options.logger?.warn?.(`[dsh-workbench] 收窄工具面失败:${String(error)}`);
13080
12826
  }
@@ -13089,7 +12835,7 @@ function createToolScope(options) {
13089
12835
  const handle = agentOf(agent);
13090
12836
  const tools = assembly?.tools;
13091
12837
  if (handle === void 0 || tools === void 0) return assembly;
13092
- const deny = new Set(options.deniedTools(workspaceOf(handle)));
12838
+ const deny = new Set(options.deniedTools(contextOf(handle, options)));
13093
12839
  if (deny.size === 0) return assembly;
13094
12840
  const kept = tools.filter((tool) => !deny.has(tool.name));
13095
12841
  return kept.length === tools.length ? assembly : {
@@ -13116,6 +12862,41 @@ function createToolScope(options) {
13116
12862
  };
13117
12863
  }
13118
12864
  //#endregion
12865
+ //#region src/shared/api/host/preset-scope.ts
12866
+ var PRESET_KEY = "agentPreset";
12867
+ function presetOf(ctx, handle) {
12868
+ const projections = ctx.get(SESSION_PROJECTIONS_SERVICE);
12869
+ if (projections === void 0 || handle.session === void 0) return null;
12870
+ try {
12871
+ const preset = projections.stateOf(handle.session, PRESET_KEY);
12872
+ return typeof preset === "string" && preset !== "" ? preset : null;
12873
+ } catch (error) {
12874
+ ctx.logger?.warn?.(`[dsh-workbench] 读 preset 投影失败,退回会话创建事实:${String(error)}`);
12875
+ return null;
12876
+ }
12877
+ }
12878
+ function watchPreset(ctx, listener) {
12879
+ ctx.inject([SESSION_PROJECTIONS_SERVICE], (scoped) => {
12880
+ const projections = scoped.get(SESSION_PROJECTIONS_SERVICE);
12881
+ if (projections === void 0) return;
12882
+ scoped.effect(() => {
12883
+ const off = projections.onChanged((_session, key) => {
12884
+ if (key === PRESET_KEY) listener();
12885
+ });
12886
+ return () => off?.();
12887
+ }, "dsh-workbench: preset 选中就重算工具面(新会话屏是先建会话、再把选择 select 上去)");
12888
+ });
12889
+ }
12890
+ function createPresetScope(options) {
12891
+ const scope = createToolScope({
12892
+ deniedTools: options.deniedTools,
12893
+ presetOf: (handle) => presetOf(options.ctx, handle),
12894
+ logger: options.logger
12895
+ });
12896
+ watchPreset(options.ctx, () => scope.reapply());
12897
+ return scope;
12898
+ }
12899
+ //#endregion
13119
12900
  //#region src/shared/api/host/host-args.ts
13120
12901
  function asArgs(value) {
13121
12902
  return typeof value === "object" && value !== null ? value : {};
@@ -13219,8 +13000,9 @@ function apply(ctx) {
13219
13000
  };
13220
13001
  const store = createWorkspaceStore({ logger: ctx.logger });
13221
13002
  const unitsOfTool = () => toolUnits(liveManifests());
13222
- const toolScope = createToolScope({
13223
- deniedTools: (workspace) => deniedNow(workspace),
13003
+ const toolScope = createPresetScope({
13004
+ ctx,
13005
+ deniedTools: (context) => deniedNow(context),
13224
13006
  logger: ctx.logger
13225
13007
  });
13226
13008
  const pluginTools = shell === void 0 ? void 0 : createPluginUnits({
@@ -13247,7 +13029,7 @@ function apply(ctx) {
13247
13029
  const ownersMap = /* @__PURE__ */ new Map();
13248
13030
  const factsOfUnit = (unit) => [...routesOfUnitMap.get(unit) ?? [], "capability"];
13249
13031
  const callFacts = (unit, call) => factsOfMap.get(unit)?.(call) ?? factsOfUnit(unit);
13250
- const deniedNow = (workspace) => [...new Set([...mounted.values()].flatMap((item) => item.plan.gate?.deniedTools(workspace) ?? []))];
13032
+ const deniedNow = (context) => [...new Set([...mounted.values()].flatMap((item) => item.plan.gate?.deniedTools(context) ?? []))];
13251
13033
  const declaredNoteTools = () => declaredNotesOf([...mounted.values()].map((item) => item.plan));
13252
13034
  const wrote = (resource, write) => {
13253
13035
  const unit = ownersMap.get(resource);