@dsh-workbench/core 0.3.4 → 0.3.6

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 (51) hide show
  1. package/client.js +1362 -85
  2. package/dist/client/client.body.js +1362 -85
  3. package/dist/client/index.js +1362 -85
  4. package/dist/host/index.js +1019 -869
  5. package/dist/types/app/client.d.ts +234 -60
  6. package/dist/types/modules/panels/client.d.ts +1 -1
  7. package/dist/types/modules/panels/model/approval.d.ts +8 -0
  8. package/dist/types/modules/panels/model/diy-context.d.ts +4 -5
  9. package/dist/types/modules/panels/model/diy-observers.d.ts +2 -2
  10. package/dist/types/modules/panels/model/diy-page.d.ts +1 -1
  11. package/dist/types/modules/panels/model/long-task-tools.d.ts +15 -0
  12. package/dist/types/modules/panels/model/long-task-tools.test.d.ts +1 -0
  13. package/dist/types/modules/panels/unit.d.ts +6 -5
  14. package/dist/types/shared/api/context-chips.d.ts +20 -0
  15. package/dist/types/shared/api/host/approval-call.d.ts +16 -0
  16. package/dist/types/shared/api/host/diy-components.d.ts +17 -6
  17. package/dist/types/shared/api/host/diy-workshop.d.ts +0 -3
  18. package/dist/types/shared/api/host/long-task-notice.d.ts +30 -0
  19. package/dist/types/shared/api/host/mount-units.d.ts +3 -1
  20. package/dist/types/shared/api/host/preset-scope.d.ts +16 -0
  21. package/dist/types/shared/api/host/tool-scope.d.ts +6 -2
  22. package/dist/types/shared/api/host/workshop-root.d.ts +7 -0
  23. package/dist/types/shared/components/BlockerState.d.ts +12 -0
  24. package/dist/types/shared/components/PanelFoot.d.ts +9 -0
  25. package/dist/types/shared/components/Tip.d.ts +8 -0
  26. package/dist/types/shared/hooks/bundle-queries.d.ts +12 -0
  27. package/dist/types/shared/hooks/fact-transports.d.ts +4 -0
  28. package/dist/types/shared/hooks/panel-state-report.d.ts +12 -0
  29. package/dist/types/shared/hooks/use-api-resource.d.ts +22 -0
  30. package/dist/types/shared/hooks/use-blocker-actions.d.ts +8 -0
  31. package/dist/types/shared/hooks/use-capabilities.d.ts +55 -0
  32. package/dist/types/shared/hooks/use-capability-gate.d.ts +16 -0
  33. package/dist/types/shared/hooks/use-card-summary.d.ts +7 -0
  34. package/dist/types/shared/hooks/use-diy-debug.d.ts +13 -0
  35. package/dist/types/shared/hooks/use-diy-record.d.ts +13 -0
  36. package/dist/types/shared/hooks/use-facts.d.ts +10 -0
  37. package/dist/types/shared/hooks/use-host-write.d.ts +12 -0
  38. package/dist/types/shared/hooks/use-long-task.d.ts +15 -1
  39. package/dist/types/shared/hooks/use-unit-board.d.ts +17 -0
  40. package/dist/types/shared/hooks/use-visible-busy.d.ts +17 -0
  41. package/package.json +2 -2
  42. package/skills/workbench-module-diy/DATASOURCE.md +2 -2
  43. package/skills/workbench-module-diy/FAMILY.md +5 -4
  44. package/skills/workbench-module-diy/KIT.md +105 -100
  45. package/skills/workbench-module-diy/PANEL.md +19 -11
  46. package/skills/workbench-module-diy/REVIEW.md +4 -4
  47. package/skills/workbench-module-diy/SKILL.md +5 -4
  48. package/skills/workbench-module-diy/TOOLING.md +7 -3
  49. package/skills/workbench-module-diy/TOOLS.md +20 -2
  50. package/skills/workbench-module-diy/WIRING.md +2 -1
  51. 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,48 @@ 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
  }
280
+ /**
281
+ * 停止那条写路由(走 actions 通道的资源名):`<单元>/runs/stop`。
282
+ * @param unit 单元 id。
283
+ * @returns <unit>/runs/stop。
284
+ */
285
+ function stopRouteOf(unit) {
286
+ return `${unit}/runs/stop`;
287
+ }
204
288
  //#endregion
205
289
  //#region ../shared/dist/lib/text.js
290
+ /**
291
+ * 把一个值裹成 shell 里的单引号字面量(值里的单引号一并转义),保证元字符按字面量传递。
292
+ * @param value - 任意值;不是字符串的先 `String()`。
293
+ * @returns 可安全拼进 `/bin/sh -c` 的一段文本 —— 含空格 / `$()` / `;` / 通配符的值都不会被当成语法。
294
+ */
206
295
  function quoteShellArg(value) {
207
296
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
208
297
  }
298
+ /**
299
+ * 取一段文本的末尾若干行(如命令输出),顺带去掉首尾空白。
300
+ * @param text - 原文;`null` / `undefined` 当空串。
301
+ * @param lines - 最多留几行,默认 6;0 给全篇、负数从开头砍掉那么多行(都是 `slice` 语义),调用方传正数。
302
+ * @returns 末尾若干行、去首尾空行后的文本;原文行数不够就全给,空输入给空串。
303
+ */
209
304
  function tailLines(text, lines = 6) {
210
305
  return String(text ?? "").trim().split("\n").slice(-lines).join("\n");
211
306
  }
@@ -223,6 +318,11 @@ function candidateStarts(raw) {
223
318
  }
224
319
  return starts;
225
320
  }
321
+ /**
322
+ * 宽松解析一段前后可能夹着人话的 JSON:整段不通就从行首的 `[` / `{` 起、截到最后一个闭合符再试。
323
+ * @param text - 原文;`null` / `undefined` 当空串。
324
+ * @returns 解析出的值(对象 / 数组 / 标量都原样);空输入或怎么都解析不出给 `undefined`,调用方据此报「输出认不出」。
325
+ */
226
326
  function parseJsonLoose(text) {
227
327
  const raw = String(text ?? "").trim();
228
328
  if (!raw) return void 0;
@@ -245,6 +345,9 @@ function parseJsonLoose(text) {
245
345
  var DAY = 864e5;
246
346
  30 * DAY;
247
347
  365 * DAY;
348
+ /**
349
+ * 上报文本字段的截断长度(2000 字符;workspace / reason / message 共用)。
350
+ */
248
351
  var PANEL_STATE_MAX_TEXT = 2e3;
249
352
  var PHASES = [
250
353
  "idle",
@@ -252,10 +355,10 @@ var PHASES = [
252
355
  "ready",
253
356
  "error"
254
357
  ];
255
- function asRecord$10(value) {
358
+ function asRecord$8(value) {
256
359
  return typeof value === "object" && value !== null ? value : void 0;
257
360
  }
258
- function textOf$8(value, max) {
361
+ function textOf$7(value, max) {
259
362
  if (typeof value !== "string") return "";
260
363
  const trimmed = value.trim();
261
364
  return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
@@ -266,23 +369,29 @@ function atOf(value) {
266
369
  return Math.trunc(value);
267
370
  }
268
371
  function errorOf(raw) {
269
- const record = asRecord$10(raw);
372
+ const record = asRecord$8(raw);
270
373
  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);
374
+ const code = textOf$7(record["code"], 120);
375
+ const message = textOf$7(record["message"], PANEL_STATE_MAX_TEXT);
273
376
  if (code === "" && message === "") return void 0;
274
377
  return {
275
378
  code: code === "" ? "unknown" : code,
276
379
  message
277
380
  };
278
381
  }
382
+ /**
383
+ * 收一批卡片读数上报。
384
+ * @param payload - `POST` 的请求体。
385
+ * @returns 至少收到一条能认的卡片才 `ok: true`;请求体不是对象、`workspace` 为空、`cards` 不是数组,
386
+ * 或一条都认不出(`unit` / `route` / `phase` / `at` 缺一)时 `ok: false` + 中文原因;认不出的单条静默跳过。
387
+ */
279
388
  function parseCardsReport(payload) {
280
- const record = asRecord$10(payload);
389
+ const record = asRecord$8(payload);
281
390
  if (record === void 0) return {
282
391
  ok: false,
283
392
  message: "请求体要是一个对象"
284
393
  };
285
- const workspace = textOf$8(record["workspace"], PANEL_STATE_MAX_TEXT);
394
+ const workspace = textOf$7(record["workspace"], PANEL_STATE_MAX_TEXT);
286
395
  if (workspace === "") return {
287
396
  ok: false,
288
397
  message: "workspace 不能是空的 —— 面板按工作区存"
@@ -294,11 +403,11 @@ function parseCardsReport(payload) {
294
403
  };
295
404
  const cards = [];
296
405
  for (const item of raw.slice(0, 32)) {
297
- const card = asRecord$10(item);
406
+ const card = asRecord$8(item);
298
407
  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);
408
+ const unit = textOf$7(card["unit"], 200);
409
+ const route = textOf$7(card["route"], 400);
410
+ const phase = textOf$7(card["phase"], 20);
302
411
  const at = atOf(card["at"]);
303
412
  if (unit === "" || route === "" || at === null || !PHASES.includes(phase)) continue;
304
413
  const error = errorOf(card["error"]);
@@ -322,15 +431,20 @@ function parseCardsReport(payload) {
322
431
  }
323
432
  };
324
433
  }
434
+ /**
435
+ * 收一次卡片崩溃上报。
436
+ * @param payload - `POST` 的请求体。
437
+ * @returns `workspace` / `unit` / `reason` 都不能空,缺一给 `ok: false` + 中文原因;`at` 缺省或不合法时取当前时间。
438
+ */
325
439
  function parseCrashReport(payload) {
326
- const record = asRecord$10(payload);
440
+ const record = asRecord$8(payload);
327
441
  if (record === void 0) return {
328
442
  ok: false,
329
443
  message: "请求体要是一个对象"
330
444
  };
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);
445
+ const workspace = textOf$7(record["workspace"], PANEL_STATE_MAX_TEXT);
446
+ const unit = textOf$7(record["unit"], 200);
447
+ const reason = textOf$7(record["reason"], PANEL_STATE_MAX_TEXT);
334
448
  if (workspace === "") return {
335
449
  ok: false,
336
450
  message: "workspace 不能是空的"
@@ -353,6 +467,10 @@ function parseCrashReport(payload) {
353
467
  }
354
468
  };
355
469
  }
470
+ /**
471
+ * 造一份面板运行态的留存(进程内存、按工作区分桶,带上面那几条上限的 LRU)。
472
+ * @returns 留存口:`cards` / `crash` 收上报,`snapshot(workspace)` 取全量;没存过的工作区(含 `null`)给一份空的。
473
+ */
356
474
  function createPanelStateStore() {
357
475
  const workspaces = /* @__PURE__ */ new Map();
358
476
  function trim(entry, max) {
@@ -443,17 +561,35 @@ function createPanelStateStore() {
443
561
  }
444
562
  //#endregion
445
563
  //#region ../shared/dist/lib/board-fact.js
564
+ /**
565
+ * 判一个路径落不落在这个工作区里(按路径前缀比,不是字符串包含)。
566
+ * @param root - 工作区根路径。
567
+ * @param target - 要判的路径。
568
+ * @returns 同一个路径或在其下给 `true`;`root` 末尾带不带斜杠都认(`/a` 不匹配 `/ab`)。
569
+ */
446
570
  function insideWorkshop$1(root, target) {
447
571
  if (target === root) return true;
448
572
  const base = root.endsWith("/") ? root : `${root}/`;
449
573
  return target.startsWith(base);
450
574
  }
575
+ /**
576
+ * 把一次探针读数归成椅面归属。
577
+ * @param probe - 探针读数。
578
+ * @param workshop - 当前工作区根路径;`null` 表示还不知道当前是哪个工作区。
579
+ * @returns 外壳不在、或板路径与工作区缺一 → `unknown`;右栏没开或没画出来 → `hidden`;板在本工作区 → `workshop`,否则 `other`。
580
+ */
451
581
  function boardFactOf(probe, workshop) {
452
582
  if (!probe.workbench) return "unknown";
453
583
  if (!probe.bar || !probe.open || !probe.drawn) return "hidden";
454
584
  if (probe.board === "" || workshop === null) return "unknown";
455
585
  return insideWorkshop$1(workshop, probe.board) ? "workshop" : "other";
456
586
  }
587
+ /**
588
+ * 把探针读数归成左栏那句人话的档位。
589
+ * @param probe - 探针读数。
590
+ * @param workshop - 当前工作区根路径;`null` 一律 `unknown`。
591
+ * @returns 椅面画着 → 本板 `ok` / 别板 `other-board`;没画着 → 页面已走 `off-workspace`,否则 `panel-closed`。
592
+ */
457
593
  function boardSituation(probe, workshop) {
458
594
  if (!probe.workbench || workshop === null) return "unknown";
459
595
  if (probe.bar && probe.open && probe.drawn) {
@@ -465,6 +601,11 @@ function boardSituation(probe, workshop) {
465
601
  }
466
602
  //#endregion
467
603
  //#region ../shared/dist/lib/diy-debug.js
604
+ /**
605
+ * 把工具入参收成人话动作。
606
+ * @param value - 任意入参值。
607
+ * @returns `'pause'` / `'resume'` / `'stop'` 原样;别的值(含 `undefined`、多余空白、大小写不同)给 `null`,由调用方拒。
608
+ */
468
609
  function parseDebugIntent(value) {
469
610
  return value === "pause" || value === "resume" || value === "stop" ? value : null;
470
611
  }
@@ -477,6 +618,10 @@ function ownerOf$1(state, sessionId, now) {
477
618
  if (held === null) return "none";
478
619
  return held.sessionId === sessionId ? held : "not-owner";
479
620
  }
621
+ /**
622
+ * 造一份新的调试约定存储(进程内存,同一时刻只可能有一份有效约定)。
623
+ * @returns 存取口;动作成不成都在返回的 `DebugMove` 里,不抛异常。
624
+ */
480
625
  function createDebugStore() {
481
626
  let held = null;
482
627
  const live = (now) => alive$1(held, now);
@@ -491,7 +636,8 @@ function createDebugStore() {
491
636
  const state = {
492
637
  phase: "debugging",
493
638
  sessionId: pact.sessionId,
494
- workspace: pact.workspace,
639
+ workspacePath: pact.workspacePath,
640
+ title: pact.title,
495
641
  board: pact.board,
496
642
  unit: pact.unit,
497
643
  name: pact.name,
@@ -560,6 +706,11 @@ function createDebugStore() {
560
706
  }
561
707
  };
562
708
  }
709
+ /**
710
+ * 把工具入参收成人话动作。
711
+ * @param raw - 任意入参值。
712
+ * @returns `'peek'` / `'halt'` / `'stop'` 原样;别的值(含 `undefined`、多余空白)给 `null`,由调用方拒。
713
+ */
563
714
  function parseRecordIntent(raw) {
564
715
  return raw === "peek" || raw === "halt" || raw === "stop" ? raw : null;
565
716
  }
@@ -572,6 +723,10 @@ function ownerOf(state, sessionId, now) {
572
723
  if (held === null) return "none";
573
724
  return held.sessionId === sessionId ? held : "not-owner";
574
725
  }
726
+ /**
727
+ * 造一份新的录制约定存储(进程内存,同一时刻只可能有一份有效约定)。
728
+ * @returns 存取口;动作成不成都在返回的 `RecordMove` 里,不抛异常。
729
+ */
575
730
  function createRecordStore() {
576
731
  let held = null;
577
732
  const live = (now) => alive(held, now);
@@ -706,481 +861,8 @@ async function runShell(shell, command, options = {}) {
706
861
  //#endregion
707
862
  //#region ../shared/dist/host/browser-read.js
708
863
  var BROWSER_SCREEN_SESSION = "workbench";
864
+ var BROWSER_READ_SESSION = "workbench-read";
709
865
  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
866
  //#endregion
1185
867
  //#region ../shared/dist/host/cli-error.js
1186
868
  function unquote(raw) {
@@ -1922,11 +1604,17 @@ var LIVE = /* @__PURE__ */ new Set(["running", "needs-login"]);
1922
1604
  var RECENT_MS = 6e5;
1923
1605
  function createLongTaskRegistry(deps) {
1924
1606
  const runs = /* @__PURE__ */ new Map();
1607
+ const controls = /* @__PURE__ */ new Map();
1925
1608
  const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
1926
1609
  let counter = 0;
1927
1610
  const nextId = deps.nextId ?? (() => `run-${Date.now()}-${counter += 1}`);
1928
1611
  const recentMs = deps.recentMs ?? RECENT_MS;
1929
1612
  const listeners = /* @__PURE__ */ new Set();
1613
+ const live = (id) => {
1614
+ const current = runs.get(id);
1615
+ if (current === void 0 || !LIVE.has(current.status)) return null;
1616
+ return current;
1617
+ };
1930
1618
  const notify = (change) => {
1931
1619
  for (const listener of [...listeners]) try {
1932
1620
  listener(change);
@@ -1961,7 +1649,10 @@ function createLongTaskRegistry(deps) {
1961
1649
  if (snapshot.workspace !== workspace) continue;
1962
1650
  newest.set(snapshot.target, snapshot);
1963
1651
  }
1964
- return [...newest.values()].filter((snapshot) => LIVE.has(snapshot.status) || Date.parse(snapshot.at) >= floor).reverse().map((snapshot) => deps.view(snapshot));
1652
+ return [...newest.values()].filter((snapshot) => LIVE.has(snapshot.status) || Date.parse(snapshot.at) >= floor).reverse().map((snapshot) => ({
1653
+ ...deps.view(snapshot),
1654
+ id: snapshot.id
1655
+ }));
1965
1656
  },
1966
1657
  start(options) {
1967
1658
  const running = this.active(options.workspace, options.target);
@@ -1976,15 +1667,16 @@ function createLongTaskRegistry(deps) {
1976
1667
  step: "",
1977
1668
  steps: [],
1978
1669
  startedAt,
1979
- at: startedAt
1670
+ at: startedAt,
1671
+ ...options.session === void 0 ? {} : { session: options.session }
1980
1672
  });
1981
1673
  notify({
1982
1674
  kind: "start",
1983
1675
  snapshot: runs.get(id)
1984
1676
  });
1985
1677
  const onStep = (step) => {
1986
- const current = runs.get(id);
1987
- if (current === void 0) return;
1678
+ const current = live(id);
1679
+ if (current === null) return;
1988
1680
  const steps = [...current.steps, `${step.kind === "fail" ? "✗ " : ""}${step.text}`].slice(-20);
1989
1681
  const extra = deps.step?.(step, current) ?? {};
1990
1682
  if (step.loginUrl !== void 0) {
@@ -2011,17 +1703,24 @@ function createLongTaskRegistry(deps) {
2011
1703
  step
2012
1704
  });
2013
1705
  };
1706
+ const control = new AbortController();
1707
+ controls.set(id, control);
2014
1708
  deps.run({
2015
1709
  payload: options.payload,
2016
- onStep
1710
+ onStep,
1711
+ signal: control.signal
2017
1712
  }).then((result) => {
2018
- const current = runs.get(id);
1713
+ const current = live(id);
1714
+ if (current === null) return;
1715
+ controls.delete(id);
2019
1716
  patch(id, {
2020
1717
  status: "ok",
2021
1718
  ...deps.outcome?.(result, current) ?? {},
2022
1719
  loginUrl: void 0
2023
1720
  }, { kind: "settle" });
2024
1721
  }).catch((error) => {
1722
+ if (live(id) === null) return;
1723
+ controls.delete(id);
2025
1724
  patch(id, {
2026
1725
  status: "failed",
2027
1726
  why: error instanceof Error ? error.message : String(error),
@@ -2030,6 +1729,21 @@ function createLongTaskRegistry(deps) {
2030
1729
  });
2031
1730
  return runs.get(id);
2032
1731
  },
1732
+ stop(id, by) {
1733
+ const current = live(id);
1734
+ if (current === null) return false;
1735
+ controls.get(id)?.abort();
1736
+ controls.delete(id);
1737
+ patch(id, {
1738
+ ...deps.stopped?.(current) ?? {},
1739
+ status: "stopped",
1740
+ loginUrl: void 0
1741
+ }, {
1742
+ kind: "settle",
1743
+ by
1744
+ });
1745
+ return true;
1746
+ },
2033
1747
  snapshot(id) {
2034
1748
  return runs.get(id) ?? null;
2035
1749
  },
@@ -2062,6 +1776,7 @@ function createLongTaskHost(options) {
2062
1776
  offs.push(registry.onChange((change) => {
2063
1777
  const named = [runsRouteOf(options.unit), ...facts?.(change) ?? []];
2064
1778
  options.bump([...new Set(named)]);
1779
+ options.notify?.(change);
2065
1780
  }));
2066
1781
  return registry;
2067
1782
  } },
@@ -2128,7 +1843,7 @@ var PLUGIN_LEDGER_FILE = "plugins.json";
2128
1843
  function ledgerKeyOf(entry) {
2129
1844
  return `${entry.opencliHome}\u0000${entry.package}`;
2130
1845
  }
2131
- function textOf$5(value) {
1846
+ function textOf$6(value) {
2132
1847
  if (typeof value !== "string") return void 0;
2133
1848
  const trimmed = value.trim();
2134
1849
  return trimmed === "" ? void 0 : trimmed;
@@ -2136,13 +1851,13 @@ function textOf$5(value) {
2136
1851
  function entryOf$1(value) {
2137
1852
  if (typeof value !== "object" || value === null) return void 0;
2138
1853
  const box = value;
2139
- const opencliHome = textOf$5(box["opencliHome"]);
2140
- const pkg = textOf$5(box["package"]);
2141
- const installedAt = textOf$5(box["installedAt"]);
1854
+ const opencliHome = textOf$6(box["opencliHome"]);
1855
+ const pkg = textOf$6(box["package"]);
1856
+ const installedAt = textOf$6(box["installedAt"]);
2142
1857
  if (opencliHome === void 0 || pkg === void 0 || installedAt === void 0) return void 0;
2143
- const name = textOf$5(box["name"]);
2144
- const registry = textOf$5(box["registry"]);
2145
- const releasedAt = textOf$5(box["releasedAt"]);
1858
+ const name = textOf$6(box["name"]);
1859
+ const registry = textOf$6(box["registry"]);
1860
+ const releasedAt = textOf$6(box["releasedAt"]);
2146
1861
  return {
2147
1862
  opencliHome,
2148
1863
  package: pkg,
@@ -2552,6 +2267,8 @@ function createWorkspaceStore(options) {
2552
2267
  }
2553
2268
  };
2554
2269
  }
2270
+ var WORKSHOP_RELATIVE = "workbench/diy";
2271
+ var WORKSHOP_TITLE = "工作台DIY";
2555
2272
  //#endregion
2556
2273
  //#region src/shared/api/bundle-install.ts
2557
2274
  var BUNDLE_REGISTRIES_ROUTE = "bundle-registries";
@@ -3068,110 +2785,6 @@ function createRegistryNotebook(dir) {
3068
2785
  };
3069
2786
  }
3070
2787
  //#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
2788
  //#region src/modules/panels/unit.ts
3176
2789
  var PANELS_LIST_TOOL = "workbench_panels_list";
3177
2790
  var PANELS_SHOW_TOOL = "workbench_panels_show";
@@ -3194,7 +2807,6 @@ var DIY_PANEL_TOOL = "workbench_diy_panel";
3194
2807
  var DIY_COMPONENTS_TOOL = "workbench_diy_components";
3195
2808
  var DIY_RECON_TOOL = "workbench_diy_recon";
3196
2809
  var DIY_SPEC_TOOL = "workbench_diy_spec";
3197
- var DIY_PROBE_TOOL = "workbench_diy_probe";
3198
2810
  var DIY_INSTALL_TOOL = "workbench_diy_install";
3199
2811
  var DIY_SHOW_TOOL = "workbench_diy_show";
3200
2812
  var DIY_STATE_TOOL = "workbench_diy_state";
@@ -3218,6 +2830,8 @@ var DIY_RECORD_PREPARE_TOOL = "workbench_diy_record_prepare";
3218
2830
  var DIY_RECORD_READ_TOOL = "workbench_diy_record_read";
3219
2831
  var DIY_RECORD_DETAIL_TOOL = "workbench_diy_record_detail";
3220
2832
  var DIY_RECORD_STOP_TOOL = "workbench_diy_record_stop";
2833
+ var LONG_TASKS_LIST_TOOL = "workbench_long_tasks_list";
2834
+ var LONG_TASKS_STOP_TOOL = "workbench_long_tasks_stop";
3221
2835
  var DIY_DEBUG_START_TOOL = "workbench_diy_debug_start";
3222
2836
  var DIY_DEBUG_PAUSE_TOOL = "workbench_diy_debug_pause";
3223
2837
  var DIY_DEBUG_RESUME_TOOL = "workbench_diy_debug_resume";
@@ -3231,7 +2845,6 @@ var READ_TOOLS = [
3231
2845
  DIY_COMPONENTS_TOOL,
3232
2846
  DIY_RECON_TOOL,
3233
2847
  DIY_SPEC_TOOL,
3234
- DIY_PROBE_TOOL,
3235
2848
  DIY_STATE_TOOL,
3236
2849
  DIY_HTML_TOOL,
3237
2850
  DIY_FIND_TOOL,
@@ -3251,7 +2864,8 @@ var READ_TOOLS = [
3251
2864
  DIY_LOGS_STOP_TOOL,
3252
2865
  DIY_RECORD_READ_TOOL,
3253
2866
  DIY_RECORD_DETAIL_TOOL,
3254
- DIY_RECORD_STOP_TOOL
2867
+ DIY_RECORD_STOP_TOOL,
2868
+ LONG_TASKS_LIST_TOOL
3255
2869
  ];
3256
2870
  var WRITE_TOOLS = [
3257
2871
  PANELS_SHOW_TOOL,
@@ -3273,7 +2887,8 @@ var WRITE_TOOLS = [
3273
2887
  DIY_DEBUG_START_TOOL,
3274
2888
  DIY_DEBUG_PAUSE_TOOL,
3275
2889
  DIY_DEBUG_RESUME_TOOL,
3276
- DIY_DEBUG_STOP_TOOL
2890
+ DIY_DEBUG_STOP_TOOL,
2891
+ LONG_TASKS_STOP_TOOL
3277
2892
  ];
3278
2893
  var DIY_TOOLS = [
3279
2894
  DIY_LIST_TOOL,
@@ -3282,7 +2897,6 @@ var DIY_TOOLS = [
3282
2897
  DIY_COMPONENTS_TOOL,
3283
2898
  DIY_RECON_TOOL,
3284
2899
  DIY_SPEC_TOOL,
3285
- DIY_PROBE_TOOL,
3286
2900
  DIY_INSTALL_TOOL,
3287
2901
  DIY_SHOW_TOOL,
3288
2902
  DIY_STATE_TOOL,
@@ -3333,6 +2947,7 @@ var panelsUnit = {
3333
2947
  //#endregion
3334
2948
  //#region src/modules/panels/model/approval.ts
3335
2949
  var SPEC_LIMIT = 80;
2950
+ var TARGET_LIMIT = 20;
3336
2951
  function missing(id) {
3337
2952
  return id === void 0 || id === "";
3338
2953
  }
@@ -3407,12 +3022,37 @@ function askOf(reason, question, fallback) {
3407
3022
  fallback
3408
3023
  };
3409
3024
  }
3025
+ function textOf$5(raw, key) {
3026
+ const value = raw[key];
3027
+ return typeof value === "string" ? value.trim() : "";
3028
+ }
3029
+ function clipped(text) {
3030
+ const chars = [...text];
3031
+ return chars.length > TARGET_LIMIT ? `${chars.slice(0, TARGET_LIMIT).join("")}…` : text;
3032
+ }
3033
+ function stopVerdict(call, runs) {
3034
+ const raw = argsOf$2(call);
3035
+ const unit = textOf$5(raw, "unit");
3036
+ const id = textOf$5(raw, "id");
3037
+ if (unit === "" || id === "") return { kind: "allow" };
3038
+ const run = runs?.().get(unit)?.snapshot(id) ?? null;
3039
+ if (runs !== void 0 && run === null) return { kind: "allow" };
3040
+ const session = run?.session ?? "";
3041
+ if (session !== "" && session === call.session) return { kind: "allow" };
3042
+ const target = run === null ? "" : run.target.trim();
3043
+ const where = target === "" ? `工作台 [${unit}] 那一趟长任务` : `工作台 [${unit}] 那一趟「${clipped(target)}」长任务`;
3044
+ const whereEn = target === "" ? `a long-running workbench job [${unit}]` : `a long-running workbench job [${unit}] "${clipped(target)}"`;
3045
+ const origin = session === "" ? "用户自己按的" : "别的会话起的";
3046
+ const originEn = session === "" ? "the user started" : "another session started";
3047
+ return askOf(`${LONG_TASKS_STOP_TOOL} ${unit} stops a long run someone else started`, pair(`会停掉${where},${origin},允许吗?`, `it stops ${whereEn} ${originEn}. Allow?`), pair(`agent 想停掉${where},${origin},允许吗?`, `The agent wants to stop ${whereEn} ${originEn}. Allow?`));
3048
+ }
3410
3049
  var panelsApproval = (call) => {
3050
+ if (call.name === "workbench_long_tasks_stop") return stopVerdict(call, void 0);
3411
3051
  if (call.name === "workbench_diy_debug_start" || call.name === "workbench_diy_debug_resume") {
3412
3052
  const starting = call.name === DIY_DEBUG_START_TOOL;
3413
3053
  const unit = argsOf$2(call)["unit"];
3414
3054
  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?"));
3055
+ 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
3056
  }
3417
3057
  if (call.name === "workbench_diy_debug_pause" || call.name === "workbench_diy_debug_stop") return { kind: "allow" };
3418
3058
  if (call.name === "workbench_diy_record_prepare") {
@@ -3479,6 +3119,15 @@ var panelsApproval = (call) => {
3479
3119
  default: return;
3480
3120
  }
3481
3121
  };
3122
+ /**
3123
+ * 面板族的审批策略:`panelsApproval` 再认长任务那一档(写成功要用户点头)。
3124
+ * `longTaskRuns` 用来查"这一趟是谁起的" —— 自己起的免问,别的会话起的 / 用户按的要问。
3125
+ * @param longTaskRuns 当前挂着的单元各自那把长任务注册表;没给就按"别人的"问(保守的那一侧)。
3126
+ * @returns 审批策略:认领的写工具回 ask / allow,不是本族的回 undefined。
3127
+ */
3128
+ function panelsApprovalFor(longTaskRuns) {
3129
+ return (call) => call.name === "workbench_long_tasks_stop" ? stopVerdict(call, longTaskRuns) : panelsApproval(call);
3130
+ }
3482
3131
  //#endregion
3483
3132
  //#region src/modules/panels/model/config.ts
3484
3133
  var EMPTY_BOARD = {
@@ -3629,6 +3278,191 @@ function withMoved(board, id, index, visible) {
3629
3278
  };
3630
3279
  }
3631
3280
  //#endregion
3281
+ //#region src/modules/panels/model/long-task-tools.ts
3282
+ var STATUS_WORDS = {
3283
+ running: "进行中",
3284
+ "needs-login": "要登录",
3285
+ stopped: "被停了",
3286
+ ok: "已完成",
3287
+ failed: "出错"
3288
+ };
3289
+ var NO_WORKSPACE = "不知道当前会话在哪个工作区 —— 长任务按工作区记,这条读不了。";
3290
+ var NO_RUNS = "这个工作区现在没有长任务在跑(也没有十分钟内刚收尾的)。";
3291
+ var NO_RUN = "没有这一趟 —— unit/id 对不上。";
3292
+ var STALE_REGISTRY = "这一份外壳还是上一版的注册表,停不了 —— 重新起一下工作台就能停";
3293
+ var LIST_OUTPUT = {
3294
+ schema: {
3295
+ type: "object",
3296
+ properties: {
3297
+ text: { type: "string" },
3298
+ runs: {
3299
+ type: "array",
3300
+ items: {
3301
+ type: "object",
3302
+ properties: {
3303
+ unit: { type: "string" },
3304
+ id: { type: "string" },
3305
+ target: { type: "string" },
3306
+ status: { type: "string" }
3307
+ },
3308
+ additionalProperties: true
3309
+ }
3310
+ }
3311
+ },
3312
+ additionalProperties: true
3313
+ },
3314
+ render: (_args, value) => [{
3315
+ type: "text",
3316
+ text: typeof value?.text === "string" ? value.text : JSON.stringify(value)
3317
+ }]
3318
+ };
3319
+ var STOP_OUTPUT = {
3320
+ schema: {
3321
+ type: "object",
3322
+ properties: {
3323
+ text: { type: "string" },
3324
+ stopped: { type: "boolean" }
3325
+ },
3326
+ additionalProperties: true
3327
+ },
3328
+ render: (_args, value) => [{
3329
+ type: "text",
3330
+ text: typeof value?.text === "string" ? value.text : JSON.stringify(value)
3331
+ }]
3332
+ };
3333
+ function str(value) {
3334
+ return typeof value === "string" ? value.trim() : "";
3335
+ }
3336
+ function arg(raw, key) {
3337
+ return str(typeof raw === "object" && raw !== null ? raw[key] : void 0);
3338
+ }
3339
+ function statusWord(status) {
3340
+ return STATUS_WORDS[status] ?? status;
3341
+ }
3342
+ function originOf(session, caller) {
3343
+ if (session === "") return "用户按的";
3344
+ return session === caller ? "我起的" : "别的会话起的";
3345
+ }
3346
+ function listed$1(runs, workspace) {
3347
+ const out = [];
3348
+ for (const [unit, registry] of runs?.() ?? []) for (const row of registry.current(workspace)) {
3349
+ const id = str(row["id"]);
3350
+ const run = id === "" ? null : registry.snapshot(id);
3351
+ out.push({
3352
+ unit,
3353
+ id,
3354
+ target: str(row["target"]) || str(run?.target),
3355
+ status: str(row["status"]) || str(run?.status),
3356
+ step: str(row["step"]) || str(row["why"]) || str(run?.step) || str(run?.why),
3357
+ session: str(row["session"]) || str(run?.session)
3358
+ });
3359
+ }
3360
+ return out;
3361
+ }
3362
+ function lineOf(run, index, caller) {
3363
+ const parts = [
3364
+ run.unit,
3365
+ run.target,
3366
+ statusWord(run.status),
3367
+ run.step,
3368
+ run.id === "" ? "" : `id=${run.id}`,
3369
+ originOf(run.session, caller)
3370
+ ];
3371
+ return `${String(index + 1)}. ${parts.filter((part) => part !== "").join(" · ")}`;
3372
+ }
3373
+ function renderList$1(runs, workspace, caller) {
3374
+ if (runs.length === 0) return NO_RUNS;
3375
+ return [`当前工作区:${workspace}`, ...runs.map((run, index) => lineOf(run, index, caller))].join("\n");
3376
+ }
3377
+ /**
3378
+ * 注册长任务那两把外壳工具:看得见(`workbench_long_tasks_list`)与停得了任何单元的一趟
3379
+ * (`workbench_long_tasks_stop`)。`longTaskRuns` 是晚绑定的取值器:两把都从它取注册表,
3380
+ * 所以单元晚挂上 / 被摘掉都跟得上;没给的话停那把一律回"没有这一趟"。
3381
+ * @param ctx 宿主上下文,`tools` 服务不在时什么都不注册。
3382
+ * @param longTaskRuns 当前挂着的单元各自那把长任务注册表。
3383
+ * @returns 注销函数(没有 tools 服务就是空数组)。
3384
+ */
3385
+ function registerLongTaskTools(ctx, longTaskRuns) {
3386
+ const tools = ctx.get("tools");
3387
+ if (tools === void 0) {
3388
+ ctx.logger?.info?.("[dsh-workbench] 没有 tools 服务,跳过外壳长任务工具注册");
3389
+ return [];
3390
+ }
3391
+ const listTool = {
3392
+ name: LONG_TASKS_LIST_TOOL,
3393
+ description: "看这个工作区在跑(或十分钟内刚收尾)的长任务:每行是 单元 · 目标 · 状态 · 当前一步 · id · 谁起的;要停哪一趟用 workbench_long_tasks_stop。",
3394
+ parameters: {
3395
+ type: "object",
3396
+ properties: {},
3397
+ additionalProperties: false
3398
+ },
3399
+ output: LIST_OUTPUT,
3400
+ async execute(_raw, exec) {
3401
+ const workspace = sessionWorkspace(exec);
3402
+ if (workspace === null) return {
3403
+ runs: [],
3404
+ text: NO_WORKSPACE
3405
+ };
3406
+ const runs = listed$1(longTaskRuns, workspace);
3407
+ return {
3408
+ runs,
3409
+ text: renderList$1(runs, workspace, sessionIdOf(exec))
3410
+ };
3411
+ }
3412
+ };
3413
+ const stopTool = {
3414
+ name: LONG_TASKS_STOP_TOOL,
3415
+ description: "停掉某一趟长任务(unit 与 id 从 workbench_long_tasks_list 拿)。只是让外壳给它发中断信号,平台侧那趟活儿的收尾看模块自己。",
3416
+ parameters: {
3417
+ type: "object",
3418
+ properties: {
3419
+ unit: {
3420
+ type: "string",
3421
+ description: "那一趟属于的单元 id(如 cmdb)。"
3422
+ },
3423
+ id: {
3424
+ type: "string",
3425
+ description: "那一趟的 id(列表里 id= 后面那串)。"
3426
+ }
3427
+ },
3428
+ required: ["unit", "id"],
3429
+ additionalProperties: false
3430
+ },
3431
+ output: STOP_OUTPUT,
3432
+ async execute(raw, exec) {
3433
+ const unit = arg(raw, "unit");
3434
+ const id = arg(raw, "id");
3435
+ if (unit === "" || id === "") return {
3436
+ text: NO_RUN,
3437
+ stopped: false
3438
+ };
3439
+ const registry = longTaskRuns?.().get(unit);
3440
+ if (registry === void 0 || typeof registry.stop !== "function") return {
3441
+ text: registry === void 0 ? NO_RUN : STALE_REGISTRY,
3442
+ stopped: false
3443
+ };
3444
+ const run = registry.snapshot(id);
3445
+ if (run === null) return {
3446
+ text: NO_RUN,
3447
+ stopped: false
3448
+ };
3449
+ if (run.workspace !== sessionWorkspace(exec)) return {
3450
+ text: `这一趟不在你当前的工作区(它在 ${run.workspace})—— 没动它。`,
3451
+ stopped: false
3452
+ };
3453
+ if (!registry.stop(id, sessionIdOf(exec) ?? void 0)) return {
3454
+ text: `这一趟已经不在跑了(${statusWord(run.status)})—— 没动它。`,
3455
+ stopped: false
3456
+ };
3457
+ return {
3458
+ text: `停了:${unit} · ${run.target}(${id})。这一趟的活儿收到中断信号 —— 它认不认、停在哪一步看它自己。`,
3459
+ stopped: true
3460
+ };
3461
+ }
3462
+ };
3463
+ return [tools.register(listTool), tools.register(stopTool)];
3464
+ }
3465
+ //#endregion
3632
3466
  //#region src/shared/api/host/diy-components.ts
3633
3467
  var SURFACE_TIMEOUT_MS = 3e4;
3634
3468
  var PRUNE_CHARS = 8192;
@@ -3650,21 +3484,38 @@ function toolchainBinOf(root) {
3650
3484
  const script = join(root, "node_modules", "@dsh-workbench", "module-tools", "bin", "dsh-wb-module.mjs");
3651
3485
  return existsSync(script) ? script : null;
3652
3486
  }
3487
+ function toolchainBinAmong(root, dirs) {
3488
+ const direct = toolchainBinOf(root);
3489
+ if (direct !== null) return direct;
3490
+ for (const dir of dirs) {
3491
+ const bin = toolchainBinOf(dir);
3492
+ if (bin !== null) return bin;
3493
+ }
3494
+ return null;
3495
+ }
3653
3496
  function queryCommandOf(bin, only) {
3654
3497
  return `${quoteShellArg(bin)} check --only ${only} --json`;
3655
3498
  }
3656
- async function runSurfaceQuery(shell, bin, only, root) {
3499
+ async function runSurfaceQuery(shell, bin, only, root, fallbackBin) {
3657
3500
  const result = await runShell(shell, queryCommandOf(bin, only), {
3658
3501
  workdir: root,
3659
3502
  timeoutMs: SURFACE_TIMEOUT_MS
3660
3503
  });
3661
3504
  const parsed = jsonOf$1(result.stdout);
3662
3505
  if (parsed !== null) return withoutProblems(parsed);
3506
+ const retry = fallbackBin === void 0 || fallbackBin === bin ? void 0 : fallbackBin;
3507
+ if (retry !== void 0) {
3508
+ const secondParsed = jsonOf$1((await runShell(shell, queryCommandOf(retry, only), {
3509
+ workdir: root,
3510
+ timeoutMs: SURFACE_TIMEOUT_MS
3511
+ })).stdout);
3512
+ if (secondParsed !== null) return withoutProblems(secondParsed);
3513
+ }
3663
3514
  if (result.exitCode !== 0) {
3664
3515
  const tail = `${result.stderr}\n${result.stdout}`.trim().split("\n").slice(-3).join(" | ");
3665
3516
  throw new Error(`工坊工具链的 \`check --only ${only} --json\` 没跑通(exit ${String(result.exitCode)})—— 这不是工具链坏了,是工坊里有模块过不了判据,所以连清单也没打出来${tail === "" ? "" : `:${tail}`}`);
3666
3517
  }
3667
- throw new Error(`工坊工具链的 \`check --only ${only} --json\` 没回 JSON:${result.stdout.trim().split("\n").slice(-2).join(" | ")}`);
3518
+ 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
3519
  }
3669
3520
  function jsonOf$1(stdout) {
3670
3521
  try {
@@ -3682,7 +3533,7 @@ function withoutProblems(payload) {
3682
3533
  function recordOf$1(value) {
3683
3534
  return typeof value === "object" && value !== null ? value : {};
3684
3535
  }
3685
- function asText$2(value) {
3536
+ function asText$3(value) {
3686
3537
  return typeof value === "string" ? value : "";
3687
3538
  }
3688
3539
  function stringsOf$3(value) {
@@ -3764,43 +3615,18 @@ function excerptAt(file, maxLines = 60) {
3764
3615
  return "(打不开那份类型文件 —— 照上面的路径自己看一眼)";
3765
3616
  }
3766
3617
  }
3618
+ /** 指引目录:从宿主产物往上找 `skills/workbench-module-diy`(找不到回 `null`)。 */
3767
3619
  function skillDirOf(from) {
3768
3620
  let dir = from;
3769
3621
  for (let hop = 0; hop < 6; hop += 1) {
3770
3622
  const candidate = join(dir, "skills", "workbench-module-diy");
3771
- if (existsSync(join(candidate, "HOOKS.md"))) return candidate;
3623
+ if (existsSync(candidate)) return candidate;
3772
3624
  const parent = dirname(dir);
3773
3625
  if (parent === dir) break;
3774
3626
  dir = parent;
3775
3627
  }
3776
3628
  return null;
3777
3629
  }
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
3630
  function truncated(names, all) {
3805
3631
  if (all || names.length <= 12) return names.join(", ");
3806
3632
  return `${names.slice(0, 12).join(", ")} …+${String(names.length - 12)}`;
@@ -3823,48 +3649,105 @@ function byLocationOf(payload) {
3823
3649
  function isComponents(where) {
3824
3650
  return where.split("/").at(-1) === "components";
3825
3651
  }
3652
+ function kitBucketsOf(payload) {
3653
+ const groups = byLocationOf(payload);
3654
+ const components = groups.filter((one) => isComponents(one.where)).flatMap((one) => one.names).sort();
3655
+ return [...components.length === 0 ? [] : [{
3656
+ label: "组件",
3657
+ key: "components",
3658
+ names: components
3659
+ }], ...groups.filter((one) => !isComponents(one.where)).map((one) => ({
3660
+ label: one.where,
3661
+ key: one.where,
3662
+ names: one.names
3663
+ }))];
3664
+ }
3665
+ function matchesBucket(where, bucket) {
3666
+ const wanted = bucket.trim().toLowerCase();
3667
+ if (wanted === "") return false;
3668
+ const one = where.trim().toLowerCase();
3669
+ return one === wanted || one.split("/").at(-1) === wanted;
3670
+ }
3671
+ function primitiveBucketsOf(payload) {
3672
+ return Object.entries(recordOf$1(payload["onlyPrimitives"])).map(([bucket, value]) => ({
3673
+ bucket,
3674
+ names: [...stringsOf$3(value)].sort()
3675
+ })).filter((one) => one.names.length > 0);
3676
+ }
3677
+ function bucketNamesOf(query) {
3678
+ const out = [];
3679
+ if (query.kit !== void 0) {
3680
+ const keys = kitBucketsOf(query.kit).map((one) => one.key);
3681
+ if (keys.length > 0) out.push({
3682
+ face: "kit",
3683
+ keys
3684
+ });
3685
+ }
3686
+ if (query.primitives !== void 0) {
3687
+ const keys = primitiveBucketsOf(query.primitives).map((one) => one.bucket);
3688
+ if (keys.length > 0) out.push({
3689
+ face: "primitives",
3690
+ keys
3691
+ });
3692
+ }
3693
+ return out;
3694
+ }
3695
+ var TWO_FACE_ALL = "⚠️ 两个面一起不给全表 —— 要全表就指名一面:scope=kit 或 scope=primitives。";
3696
+ function refusesEvery(scope, all, focused) {
3697
+ return all && scope === "all" && !focused;
3698
+ }
3826
3699
  function renderSurfaceText(query, options) {
3700
+ const bucket = (options.bucket ?? "").trim();
3701
+ const focused = bucket !== "";
3702
+ if (refusesEvery(options.scope, options.all, focused)) return TWO_FACE_ALL;
3703
+ const both = options.scope === "all";
3704
+ const every = options.all && !both;
3705
+ let hit = false;
3827
3706
  const lines = [];
3828
3707
  const kitNames = Object.keys(filesOf$1(query.kit)).sort();
3829
3708
  if (query.kit !== void 0) {
3830
3709
  const groups = byLocationOf(query.kit);
3831
3710
  if (kitNames.length === 0 && groups.length === 0) {
3832
3711
  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)}`);
3712
+ lines.push(anyNames.length === 0 ? "kit:读不到(没找到 client.js)" : `kit:类型面扫不到,只有名字 ${truncated(anyNames, every)}`);
3834
3713
  } 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
- }
3714
+ const buckets = kitBucketsOf(query.kit);
3715
+ lines.push(`kit ${kitNames.length}:${buckets.map((one) => `${one.label} ${one.names.length}`).join(" · ")}`);
3716
+ if (focused) {
3717
+ const matched = buckets.filter((one) => matchesBucket(one.key, bucket));
3718
+ for (const one of matched) lines.push(` ${one.label}(${one.names.length}):${one.names.join(", ")}`);
3719
+ if (matched.length > 0) hit = true;
3720
+ } else for (const one of buckets) lines.push(` ${one.label}(${one.names.length}):${truncated(one.names, every)}`);
3844
3721
  }
3845
3722
  }
3846
3723
  if (query.primitives !== void 0) {
3847
3724
  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);
3725
+ const only = primitiveBucketsOf(query.primitives);
3852
3726
  const covered = (Array.isArray(query.primitives["kitCovered"]) ? query.primitives["kitCovered"] : []).map((one) => recordOf$1(one)["name"]).filter((one) => typeof one === "string");
3853
3727
  if (version === null) {
3854
- const want = asText$2(recordOf$1(recordOf$1(query.primitives["snippet"])["devDependency"])["version"]);
3728
+ const want = asText$3(recordOf$1(recordOf$1(query.primitives["snippet"])["devDependency"])["version"]);
3855
3729
  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(" · ")}`}`);
3730
+ } else if (focused) {
3731
+ lines.push(`原语 ${version}${only.length === 0 ? "" : `:只有它有 ${only.map((one) => `${one.bucket} ${one.names.length}`).join(" · ")}`}`);
3732
+ const matched = only.filter((one) => matchesBucket(one.bucket, bucket));
3733
+ for (const one of matched) lines.push(` ${one.bucket}(${one.names.length}):${one.names.join(", ")}`);
3734
+ if (matched.length > 0) hit = true;
3735
+ } else lines.push(`原语 ${version}${only.length === 0 ? "" : `:只有它有 ${only.map((one) => `${one.bucket} ${truncated(one.names, every)}`).join(" · ")}`}`);
3857
3736
  if (only.length > 0 && version !== null) lines.push("(只有原语有的,用之前先在 dsh.client.external 里声明)");
3858
3737
  if (covered.length > 0) lines.push(`kit 里已有:${covered.join(", ")}`);
3859
3738
  }
3739
+ if (focused && !hit) {
3740
+ const keys = bucketNamesOf(query);
3741
+ lines.push(keys.length === 0 ? `没有「${bucket}」这一桶,这次也读不到任何桶。` : `没有「${bucket}」这一桶。可用的桶:${keys.map((one) => `${one.face} → ${one.keys.join(" / ")}`).join(";")}`);
3742
+ }
3860
3743
  const roots = [...Object.entries(rootsOf(query.kit)).map(([key, dir]) => `${key}=${dir}`), ...Object.entries(rootsOf(query.primitives)).map(([key, dir]) => `${key}=${dir}`)];
3861
3744
  if (roots.length > 0) lines.push(`类型面:${roots.join(" · ")}`);
3862
- lines.push(faceStateOf(query, "primitives") === "unreadable" ? "name=<件> 看它怎么用 · all=true 出全表(原语装过才能问原语那类)" : "name=<件> 看它怎么用 · all=true 出全表");
3863
3745
  const text = lines.join("\n");
3864
3746
  if (text.length <= 8192) return text;
3747
+ const commands = (options.scope === "all" ? ["kit", "primitives"] : [options.scope]).map((one) => `\`dsh-wb-module check --only ${one} --all\``).join(" 或 ");
3865
3748
  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。`,
3749
+ `⚠️ 这份清单 ${String(text.length)} 字符,超过 ${String(PRUNE_CHARS)} 会被修剪器裁掉中间 —— 按桶问装得下;要一份全的走 shell:${commands} 再 grep。`,
3750
+ " (shell 那份按 barrel 列,计数比这里的落点分组大 —— 两边口径不同。)",
3868
3751
  "",
3869
3752
  text
3870
3753
  ].join("\n");
@@ -3876,7 +3759,7 @@ function listedNamesOf(query) {
3876
3759
  for (const value of Object.values(recordOf$1(payload["buckets"]))) for (const one of stringsOf$3(value)) out.add(one);
3877
3760
  for (const value of Object.values(recordOf$1(payload["onlyPrimitives"]))) for (const one of stringsOf$3(value)) out.add(one);
3878
3761
  for (const one of Array.isArray(payload["kitCovered"]) ? payload["kitCovered"] : []) {
3879
- const covered = asText$2(recordOf$1(one)["name"]);
3762
+ const covered = asText$3(recordOf$1(one)["name"]);
3880
3763
  if (covered !== "") out.add(covered);
3881
3764
  }
3882
3765
  }
@@ -3893,8 +3776,8 @@ function faceStateOf(query, face) {
3893
3776
  }
3894
3777
  function installSnippetOf(query) {
3895
3778
  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"]);
3779
+ const name = asText$3(dependency["name"]) || "@deepseek-ai/dsh-client-ui-primitives";
3780
+ const version = asText$3(dependency["version"]);
3898
3781
  return `devDependencies 加 "${name}": "${version === "" ? "<外壳钉的那版>" : version}" 再 pnpm install`;
3899
3782
  }
3900
3783
  function renderNameText(name, found, query) {
@@ -3912,23 +3795,156 @@ function renderNameText(name, found, query) {
3912
3795
  return lines.join("\n");
3913
3796
  }
3914
3797
  const body = excerptAt(found.file);
3798
+ const head = `${name} → ${found.file}(${found.from === "kit" ? "kit" : "原语"})`;
3915
3799
  if (body.includes("/**")) return [
3916
- `${name} → ${found.file}(${found.from === "kit" ? "kit" : "原语"})`,
3800
+ head,
3917
3801
  "",
3918
3802
  body,
3919
3803
  "",
3920
- found.from === "kit" ? "(上面就是它的声明;@param 写着默认值。kit 件最小写法:KIT.md)" : "(上面就是它的声明;@param 写着默认值。原语件要先在 dsh.client.external 里声明它)"
3804
+ found.from === "kit" ? "(上面就是它的声明与 JSDoc —— 用法、默认值、边界都在里面。跨件的口径与最小写法看 KIT.md)" : "(上面就是它的声明与 JSDoc —— 用法与默认值都在里面。原语件要先在 dsh.client.external 里声明它)"
3921
3805
  ].join("\n");
3922
- const skillDir = skillDirOf(dirname(fileURLToPath(import.meta.url)));
3923
- const doc = docSectionOf(name, skillDir);
3806
+ const second = skillDirOf(dirname(fileURLToPath(import.meta.url))) === null ? "这份外壳连指引也没装 —— 升级它。" : `跨件的口径看 KIT.md(组件怎么拼)· LINKAGE.md / PANEL.md / TOOLS.md(取数 / 写 / 长任务)。`;
3924
3807
  return [
3925
- `${name} → ${found.file}(${found.from === "kit" ? "kit" : "原语"})`,
3808
+ head,
3926
3809
  "",
3927
3810
  body,
3928
3811
  "",
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}`
3812
+ found.from === "kit" ? `(声明没说明文字(没有 JSDoc)—— kit 的每个成员都该有,selfcheck 里有一条自检钉着;多半是你手上这份外壳比工具旧,升级外壳与工坊工具链再看。${second})` : `(声明没说明文字(没有 JSDoc)—— 装机那份原语本该自带;升级它再看。${second})`
3930
3813
  ].join("\n");
3931
3814
  }
3815
+ //#endregion
3816
+ //#region src/shared/api/host/diy.ts
3817
+ function workshopPathOf(ctx) {
3818
+ const homePath = ctx.get("dshHomePath");
3819
+ if (typeof homePath !== "function") return null;
3820
+ try {
3821
+ return homePath(WORKSHOP_RELATIVE);
3822
+ } catch {
3823
+ return null;
3824
+ }
3825
+ }
3826
+ //#endregion
3827
+ //#region src/shared/api/host/diy-workshop.ts
3828
+ function asRecord$4(value) {
3829
+ return typeof value === "object" && value !== null ? value : void 0;
3830
+ }
3831
+ function asText$2(value) {
3832
+ return typeof value === "string" ? value.trim() : "";
3833
+ }
3834
+ function insideWorkshop(root, target) {
3835
+ return insideWorkshop$1(existsSync(root) ? realpathSync(root) : resolve(root), existsSync(target) ? realpathSync(target) : resolve(target));
3836
+ }
3837
+ function workshopModules(packagesDir, installed = []) {
3838
+ if (!existsSync(packagesDir)) return [];
3839
+ const already = new Set(installed);
3840
+ return readdirSync(packagesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() || entry.isSymbolicLink()).flatMap((entry) => {
3841
+ const dir = join(packagesDir, entry.name);
3842
+ let manifest;
3843
+ try {
3844
+ manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
3845
+ } catch {
3846
+ return [];
3847
+ }
3848
+ const record = asRecord$4(manifest);
3849
+ const declared = record === void 0 ? void 0 : asRecord$4(asRecord$4(record.dsh)?.workbench);
3850
+ const unit = declared === void 0 ? "" : asText$2(declared.unit);
3851
+ if (unit === "") return [];
3852
+ return [{
3853
+ unit,
3854
+ name: asText$2(record?.name),
3855
+ description: asText$2(record?.description),
3856
+ dir,
3857
+ installed: already.has(unit)
3858
+ }];
3859
+ }).sort((left, right) => left.unit.localeCompare(right.unit));
3860
+ }
3861
+ function readDesignOfModule(dir) {
3862
+ const file = join(dir, "design.json");
3863
+ if (!existsSync(file)) return void 0;
3864
+ try {
3865
+ return JSON.parse(readFileSync(file, "utf8"));
3866
+ } catch {
3867
+ return;
3868
+ }
3869
+ }
3870
+ function workshopSyncOf(workshopRoot) {
3871
+ let parsed;
3872
+ try {
3873
+ parsed = JSON.parse(readFileSync(join(workshopRoot, ".sync.json"), "utf8"));
3874
+ } catch {
3875
+ return;
3876
+ }
3877
+ if (typeof parsed !== "object" || parsed === null) return void 0;
3878
+ const record = parsed;
3879
+ const state = asText$2(record["state"]);
3880
+ if (state !== "ok" && state !== "syncing" && state !== "failed") return void 0;
3881
+ const reason = asText$2(record["reason"]);
3882
+ return {
3883
+ state,
3884
+ version: asText$2(record["version"]),
3885
+ at: asText$2(record["at"]),
3886
+ ...reason === "" ? {} : { reason }
3887
+ };
3888
+ }
3889
+ function workshopToolchainOf(workshopRoot) {
3890
+ const versionOf = (name) => {
3891
+ try {
3892
+ return asText$2(JSON.parse(readFileSync(join(workshopRoot, "node_modules", "@dsh-workbench", name, "package.json"), "utf8")).version);
3893
+ } catch {
3894
+ return "";
3895
+ }
3896
+ };
3897
+ const core = versionOf("core");
3898
+ const moduleTools = versionOf("module-tools");
3899
+ const shared = versionOf("shared");
3900
+ if (core === "" || moduleTools === "" || shared === "") return void 0;
3901
+ return {
3902
+ core,
3903
+ moduleTools,
3904
+ shared
3905
+ };
3906
+ }
3907
+ //#endregion
3908
+ //#region src/shared/api/host/workshop-root.ts
3909
+ function rootAt(root, title) {
3910
+ return {
3911
+ root,
3912
+ packagesDir: join(root, "packages"),
3913
+ title
3914
+ };
3915
+ }
3916
+ async function resolveAt(registry, dir) {
3917
+ try {
3918
+ return await registry.resolveByPath(dir);
3919
+ } catch {
3920
+ return;
3921
+ }
3922
+ }
3923
+ function looksLikeWorkshop(dir) {
3924
+ return existsSync(join(dir, "packages")) || existsSync(join(dir, "pnpm-workspace.yaml"));
3925
+ }
3926
+ async function workshopRootOf(ctx, workspace) {
3927
+ const start = typeof workspace === "string" ? workspace.trim() : "";
3928
+ if (start === "") return null;
3929
+ const registry = ctx.get("workspaceRegistry");
3930
+ if (registry !== void 0) {
3931
+ let dir = start;
3932
+ for (;;) {
3933
+ const hit = await resolveAt(registry, dir);
3934
+ if (hit !== void 0) return rootAt(hit.path, hit.title);
3935
+ const parent = dirname(dir);
3936
+ if (parent === dir) break;
3937
+ dir = parent;
3938
+ }
3939
+ }
3940
+ let dir = start;
3941
+ for (;;) {
3942
+ if (looksLikeWorkshop(dir)) return rootAt(dir, basename(dir));
3943
+ const parent = dirname(dir);
3944
+ if (parent === dir) return null;
3945
+ dir = parent;
3946
+ }
3947
+ }
3932
3948
  var OUTPUT$2 = {
3933
3949
  schema: {
3934
3950
  type: "object",
@@ -3988,26 +4004,6 @@ function verdictOf$1(change) {
3988
4004
  const tail = (change.packageResult?.output ?? "").trim().split("\n").slice(-3).join(" | ");
3989
4005
  return `没装上(${change.errorCode ?? "failed"})${tail === "" ? "" : `:${tail}`}`;
3990
4006
  }
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
4007
  function showOrReason(panels, unit, workspace) {
4012
4008
  try {
4013
4009
  panels.show(unit, workspace);
@@ -4019,14 +4015,14 @@ function showOrReason(panels, unit, workspace) {
4019
4015
  };
4020
4016
  }
4021
4017
  }
4022
- function workshopOf(ctx, exec) {
4023
- const packagesDir = workshopPackagesDir(ctx);
4024
- if (packagesDir === null) throw new Error("算不出 DIY 工坊在哪(这台机器没给出 dshHomePath)—— 让用户跑一次 dsh-workbench setup");
4018
+ async function workshopOf(ctx, exec) {
4025
4019
  const session = sessionWorkspace(exec);
4026
- if (!inWorkshopSession(ctx, session)) throw new Error(`这把工具只在 DIY 工坊里用:当前会话不在工坊里(这个会话的工作区 ${session ?? "(不知道)"})—— 普通项目里装模块用 workbench_modules_install,工坊的入口是左栏那一行「工作台DIY」`);
4020
+ const root = await workshopRootOf(ctx, session);
4021
+ if (root === null) throw new Error(`这个工作区没有模块包可落地:DIY 工具把模块放在会话工作区的 packages/ 下(这个会话的工作区 ${session ?? "(不知道)"})—— 换到一个有模块包的工作区,或者按工程域在仓库里用 dsh-wb-module 新建一个模块包`);
4027
4022
  return {
4028
- packagesDir,
4029
- workspace: session
4023
+ packagesDir: root.packagesDir,
4024
+ workspace: root.root,
4025
+ title: root.title
4030
4026
  };
4031
4027
  }
4032
4028
  function modulesOf(ctx, packagesDir) {
@@ -4036,6 +4032,9 @@ function modulesOf(ctx, packagesDir) {
4036
4032
  modules: workshopModules(packagesDir, installed)
4037
4033
  };
4038
4034
  }
4035
+ function moduleDirsOf(packagesDir) {
4036
+ return workshopModules(packagesDir).map((one) => one.dir);
4037
+ }
4039
4038
  function panelEntriesOf(panels, workspace) {
4040
4039
  try {
4041
4040
  return panels.list(workspace);
@@ -4048,7 +4047,7 @@ function panelEntriesOf(panels, workspace) {
4048
4047
  function componentsTool(ctx) {
4049
4048
  return {
4050
4049
  name: DIY_COMPONENTS_TOOL,
4051
- description: "工坊里能用什么:dsh 原语与外壳 kit 的清单、每件的类型文件在哪(给 name 连 JSDoc 与 prop 一起回)。",
4050
+ description: "工坊里能用什么:dsh 原语与外壳 kit 的清单、每件的类型文件在哪(给 name 连 JSDoc 与 prop 一起回;给 bucket 把那一桶列全 —— 桶名见清单)。",
4052
4051
  parameters: {
4053
4052
  type: "object",
4054
4053
  properties: {
@@ -4058,33 +4057,48 @@ function componentsTool(ctx) {
4058
4057
  },
4059
4058
  scope: {
4060
4059
  type: "string",
4061
- enum: [
4062
- "all",
4063
- "primitives",
4064
- "kit"
4065
- ],
4066
- description: "只看某一面(默认 all)。"
4060
+ enum: ["kit", "primitives"],
4061
+ description: "只看某一面;不填就两个面都给目录。"
4062
+ },
4063
+ bucket: {
4064
+ type: "string",
4065
+ description: "只看某一桶并列全(例 components / icons / shared/hooks;桶名以清单里那份为准)。"
4067
4066
  },
4068
4067
  all: {
4069
4068
  type: "boolean",
4070
- description: "列全表(默认每组前 12 个)。"
4069
+ description: "列全表(默认每组前 12 个)—— 只在指名 scope 时生效;不指名 scope 时只回一句提示,不带目录。"
4071
4070
  }
4072
4071
  },
4073
4072
  additionalProperties: false
4074
4073
  },
4075
4074
  output: OUTPUT$2,
4076
4075
  async execute(raw, exec) {
4077
- const { packagesDir } = workshopOf(ctx, exec);
4076
+ const { packagesDir } = await workshopOf(ctx, exec);
4078
4077
  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
4078
  const args = asRecord$3(raw);
4084
4079
  const scope = scopeOf$1(asText$1(args["scope"]));
4085
4080
  const name = asText$1(args["name"]);
4081
+ const bucket = asText$1(args["bucket"]);
4086
4082
  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)]);
4083
+ if (name === "" && refusesEvery(scope, all, bucket !== "")) return plainJson({
4084
+ workshop: root,
4085
+ scope,
4086
+ name,
4087
+ bucket,
4088
+ all,
4089
+ file: null,
4090
+ from: null,
4091
+ primitives: null,
4092
+ kit: null,
4093
+ text: TWO_FACE_ALL
4094
+ });
4095
+ const bin = toolchainBinAmong(root, moduleDirsOf(packagesDir));
4096
+ if (bin === null) throw new Error(`工坊里还没装工具链(${join(root, "node_modules")} 与各模块自己的 node_modules 里都没有 dsh-wb-module)—— 在工坊根目录跑一次 pnpm install 就有了;workbench_diy_status 会告诉你卡在哪一步`);
4097
+ const shell = ctx.get("shell");
4098
+ if (shell === void 0) throw new Error("这台机器没给 shell 服务,读不了组件清单");
4099
+ const builtin = workshopPathOf(ctx);
4100
+ const fallbackBin = builtin === null ? void 0 : toolchainBinOf(builtin) ?? void 0;
4101
+ 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
4102
  const query = {
4089
4103
  ...primitives === void 0 ? {} : { primitives },
4090
4104
  ...kit === void 0 ? {} : { kit }
@@ -4092,12 +4106,14 @@ function componentsTool(ctx) {
4092
4106
  const found = name === "" ? null : findFileOf(name, query);
4093
4107
  const text = name === "" ? renderSurfaceText(query, {
4094
4108
  scope,
4095
- all
4109
+ all,
4110
+ bucket
4096
4111
  }) : renderNameText(name, found, query);
4097
4112
  return plainJson({
4098
4113
  workshop: root,
4099
4114
  scope,
4100
4115
  name,
4116
+ bucket,
4101
4117
  all,
4102
4118
  file: found?.file ?? null,
4103
4119
  from: found?.from ?? null,
@@ -4274,8 +4290,9 @@ function debugForeignPactText(held) {
4274
4290
  return `另一场调试正占着那块屏(${held.sessionId} 在调试 ${held.unit})—— 一台机器同时只允许一场:让它先 workbench_diy_debug_stop,或等它收工。`;
4275
4291
  }
4276
4292
  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」";
4293
+ const where = held.title === "" ? "这一场调试所在的工作区" : `「${held.title}」`;
4294
+ const what = situation === "off-workspace" ? `用户现在不在${where}(外壳选中的是别的工作区)` : situation === "panel-closed" ? "工作台面板没画在屏上(右栏收着,或那一列让给了别的面板)" : `你现在这一页画的不是${where}那块板`;
4295
+ const next = situation === "off-workspace" ? `让用户在左栏切回${where}` : situation === "panel-closed" ? "让用户把「工作台面板」切回来(或把右栏展开)" : `让用户把那一页切回${where}`;
4279
4296
  return `${what} —— 看 / 动 / 网络这一族现在发不出去,我不拿别的板当目标。${held.unit} 这场调试仍然**进行中**(我没有自动替你落 paused):${next};要停下来就自己 workbench_diy_debug_pause(不问审批),页面切回来了就接着看。`;
4280
4297
  }
4281
4298
  function boardProbeOf(page) {
@@ -4291,13 +4308,14 @@ function boardProbeOf(page) {
4291
4308
  function boardStateOf(page, workshop) {
4292
4309
  return boardFactOf(boardProbeOf(page), workshop);
4293
4310
  }
4294
- function boardGateText(situation, unit, page) {
4295
- if (situation === "off-workspace") return `用户现在不在 DIY 工作区 —— 外壳选中的是 ${page.where || "(问不出来是哪一块)"}。工坊那块板没在屏上,${unit} 的这张卡我看不到,也不拿别的板当目标。让用户在左栏切回「工作台DIY」;这之前我可以继续改源码 / build / 跑门禁。`;
4311
+ function boardGateText(situation, unit, page, title = "") {
4312
+ const where = title === "" ? "这一场调试所在的工作区" : `「${title}」`;
4313
+ if (situation === "off-workspace") return `用户现在不在${where} —— 外壳选中的是 ${page.where || "(问不出来是哪一块)"}。${where}那块板没在屏上,${unit} 的这张卡我看不到,也不拿别的板当目标。让用户在左栏切回${where};这之前我可以继续改源码 / build / 跑门禁。`;
4296
4314
  if (situation === "panel-closed") return `工作台面板没画在屏上(右栏收着,或者这一页让给了别的面板:文件预览、模块市场等)。让用户把「工作台面板」切回来(或把右栏展开),我再接着看 ${unit}。`;
4297
- return `你现在这一页画的是 ${page.board || "(不知道哪一块)"} 的面板,工坊那块板没在这一屏 —— ${unit} 的这张卡不在屏上,我不拿别的板当目标。请用户在左栏切回「工作台DIY」;这之前我可以继续改源码 / build / 跑门禁。`;
4315
+ return `你现在这一页画的是 ${page.board || "(不知道哪一块)"} 的面板,${where}那块板没在这一屏 —— ${unit} 的这张卡不在屏上,我不拿别的板当目标。请用户在左栏切回${where};这之前我可以继续改源码 / build / 跑门禁。`;
4298
4316
  }
4299
4317
  async function pageAt(ctx, exec, raw, options = {}) {
4300
- const { packagesDir } = workshopOf(ctx, exec);
4318
+ const { packagesDir, workspace, title } = await workshopOf(ctx, exec);
4301
4319
  const args = asRecord$3(raw);
4302
4320
  const asked = asText$1(args["unit"]);
4303
4321
  const modules = modulesOf(ctx, packagesDir).modules;
@@ -4320,11 +4338,10 @@ async function pageAt(ctx, exec, raw, options = {}) {
4320
4338
  const { page } = handshake;
4321
4339
  throw new Error(`你现在这一页不是工作台(${page.title || "(没有标题)"} ${page.url})—— 把工作台那一页切到前台,再喊我一次。我只看用户正在看的那一页。`);
4322
4340
  }
4323
- const workshop = workshopPathOf(ctx);
4324
- const board = boardStateOf(handshake.page, workshop);
4325
- const situation = boardSituation(boardProbeOf(handshake.page), workshop);
4341
+ const board = boardStateOf(handshake.page, workspace);
4342
+ const situation = boardSituation(boardProbeOf(handshake.page), workspace);
4326
4343
  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));
4344
+ if (options.requireBoard !== false && (board === "other" || board === "hidden")) throw new Error(boardGateText(situation, wanted, handshake.page, title));
4328
4345
  return {
4329
4346
  packagesDir,
4330
4347
  unit: wanted,
@@ -4365,7 +4382,7 @@ function renderDebugPause(state) {
4365
4382
  return [`调试暂停了(${state.unit})—— 动作族关闸,不再拿屏当目标。`, "改源码 / build / 跑门禁照常;要看屏再 workbench_diy_debug_resume(会再要用户点头一次)。"].join("\n");
4366
4383
  }
4367
4384
  function renderDebugResume(state) {
4368
- return [`继续调试 ${state.unit} —— 请用户回到「工作台DIY」那一屏。`, "卡不在屏上时我会当场拒(不拿别的板当目标)。"].join("\n");
4385
+ return [`继续调试 ${state.unit} —— 请用户回到「${state.title}」那一屏。`, "卡不在屏上时我会当场拒(不拿别的板当目标)。"].join("\n");
4369
4386
  }
4370
4387
  function renderDebugStop(state) {
4371
4388
  return `收工了:${state.unit} 的调试态清掉了(占用解除)。`;
@@ -4389,7 +4406,7 @@ function registerDebugTools(ctx, tools, store) {
4389
4406
  },
4390
4407
  output: OUTPUT$2,
4391
4408
  async execute(raw, exec) {
4392
- const { packagesDir, workspace } = workshopOf(ctx, exec);
4409
+ const { packagesDir, workspace, title } = await workshopOf(ctx, exec);
4393
4410
  const unit = asText$1(asRecord$3(raw)["unit"]);
4394
4411
  if (unit === "") {
4395
4412
  const { modules } = modulesOf(ctx, packagesDir);
@@ -4402,7 +4419,8 @@ function registerDebugTools(ctx, tools, store) {
4402
4419
  });
4403
4420
  const state = moveOrThrow$1(store.begin({
4404
4421
  sessionId: sessionIdOf(exec) ?? "",
4405
- workspace: workspace ?? "",
4422
+ workspacePath: workspace ?? "",
4423
+ title,
4406
4424
  board: page.boardPath,
4407
4425
  unit,
4408
4426
  name: modulesOf(ctx, packagesDir).modules.find((one) => one.unit === unit)?.name ?? unit,
@@ -4424,7 +4442,7 @@ function registerDebugTools(ctx, tools, store) {
4424
4442
  parameters: EMPTY,
4425
4443
  output: OUTPUT$2,
4426
4444
  async execute(_raw, exec) {
4427
- const { packagesDir } = workshopOf(ctx, exec);
4445
+ const { packagesDir } = await workshopOf(ctx, exec);
4428
4446
  const held = store.live(Date.now());
4429
4447
  const state = moveOrThrow$1(store.pause(sessionIdOf(exec) ?? "", Date.now()), held);
4430
4448
  return plainJson({
@@ -4438,11 +4456,11 @@ function registerDebugTools(ctx, tools, store) {
4438
4456
  };
4439
4457
  const resumeTool = {
4440
4458
  name: DIY_DEBUG_RESUME_TOOL,
4441
- description: "继续调试(要点头一次):请用户回到「工作台DIY」那一屏。",
4459
+ description: "继续调试(要点头一次):请用户回到这一场调试所在的那一屏。",
4442
4460
  parameters: EMPTY,
4443
4461
  output: OUTPUT$2,
4444
4462
  async execute(_raw, exec) {
4445
- const { packagesDir } = workshopOf(ctx, exec);
4463
+ const { packagesDir } = await workshopOf(ctx, exec);
4446
4464
  const held = store.live(Date.now());
4447
4465
  const page = await pageAt(ctx, exec, { unit: held?.unit ?? "" }, {
4448
4466
  requireBoard: false,
@@ -4466,7 +4484,7 @@ function registerDebugTools(ctx, tools, store) {
4466
4484
  parameters: EMPTY,
4467
4485
  output: OUTPUT$2,
4468
4486
  async execute(_raw, exec) {
4469
- const { packagesDir } = workshopOf(ctx, exec);
4487
+ const { packagesDir } = await workshopOf(ctx, exec);
4470
4488
  const held = store.live(Date.now());
4471
4489
  const state = moveOrThrow$1(store.stop(sessionIdOf(exec) ?? "", Date.now()), held);
4472
4490
  await store.settle?.();
@@ -5149,7 +5167,11 @@ function parametersOf(row) {
5149
5167
  },
5150
5168
  timeoutMs: {
5151
5169
  type: "number",
5152
- description: "超时毫秒(默认 30000;≤0 或非数也按默认算)。"
5170
+ description: "超时毫秒(默认 30000)。"
5171
+ },
5172
+ read: {
5173
+ type: "string",
5174
+ description: "只读线:给站点地址(不碰你那一屏、不要调试约定)。"
5153
5175
  }
5154
5176
  };
5155
5177
  if (row.css !== void 0) properties["scope"] = {
@@ -5217,7 +5239,80 @@ function needsCardOf(row, raw) {
5217
5239
  if (row.css !== void 0 && asText$1(raw["scope"]) === "card") return true;
5218
5240
  return stringsOf$2(raw["args"]).some((one) => one.includes(CARD_TOKEN));
5219
5241
  }
5242
+ function hostOf$1(url) {
5243
+ try {
5244
+ return new URL(url).host;
5245
+ } catch {
5246
+ return "";
5247
+ }
5248
+ }
5249
+ async function onSiteOnce(shell, command, site) {
5250
+ const step = await runBrowserStep(shell, command, [
5251
+ "browser",
5252
+ BROWSER_READ_SESSION,
5253
+ "get",
5254
+ "url",
5255
+ "--window",
5256
+ "background"
5257
+ ], LOOK_TIMEOUT_MS);
5258
+ const want = hostOf$1(site);
5259
+ if (want === "" || hostOf$1(step.stdout.trim()) === want) return false;
5260
+ await runBrowserStep(shell, command, [
5261
+ "browser",
5262
+ BROWSER_READ_SESSION,
5263
+ "open",
5264
+ site,
5265
+ "--window",
5266
+ "background"
5267
+ ], LOOK_TIMEOUT_MS);
5268
+ return true;
5269
+ }
5270
+ function loginHintOf(stdout, site) {
5271
+ const code = jsonOf(stdout)?.["code"];
5272
+ if (code === "not-logged-in") return `没登录:去 ${site} 登一次再重跑(读线的约定:脚本回 { ok: false, code: "not-logged-in" })。`;
5273
+ if (code === "not-on-site") return `还不在 ${site} 上:我开过一次了 —— 让用户在那扇窗里切过去登一次,再重跑。`;
5274
+ return "";
5275
+ }
5276
+ function readRefusalOf(row, raw) {
5277
+ 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 只在屏那一档能用。";
5278
+ return row.clip === true ? "只读线不截图裁剪 —— clip 是屏那一档的事(要图走 _screenshot)。" : "";
5279
+ }
5280
+ async function runReadTool(row, ctx, raw, site) {
5281
+ const refused = readRefusalOf(row, raw);
5282
+ if (refused !== "") throw new Error(refused);
5283
+ const shell = ctx.get("shell");
5284
+ if (shell === void 0) throw new Error("这台机器上没有 shell 服务,驱动不了浏览器");
5285
+ const command = opencliCommandOf(ctx).command;
5286
+ const requested = asNumber(raw["timeoutMs"]);
5287
+ const timeoutMs = requested !== void 0 && requested > 0 ? requested : DEFAULT_TIMEOUT_MS;
5288
+ const argv = stringsOf$2(raw["args"]);
5289
+ const opened = await onSiteOnce(shell, command, site);
5290
+ const step = await runBrowserStep(shell, command, [
5291
+ "browser",
5292
+ BROWSER_READ_SESSION,
5293
+ ...row.verb,
5294
+ ...argv,
5295
+ "--window",
5296
+ "background"
5297
+ ], timeoutMs);
5298
+ const hint = row.tool === "workbench_diy_eval" ? loginHintOf(step.stdout, site) : "";
5299
+ return {
5300
+ session: BROWSER_READ_SESSION,
5301
+ verb: row.verb.join(" "),
5302
+ argv,
5303
+ opened,
5304
+ ok: step.ok,
5305
+ exitCode: step.exitCode,
5306
+ timedOut: step.timedOut,
5307
+ truncated: step.truncated,
5308
+ stdout: hint === "" ? step.stdout : `${step.stdout}\n${hint}`,
5309
+ stderr: step.stderr
5310
+ };
5311
+ }
5220
5312
  async function runBrowserTool(row, ctx, raw, exec) {
5313
+ const args = asRecord$3(raw);
5314
+ const read = asText$1(args["read"]);
5315
+ if (read !== "") return runReadTool(row, ctx, args, read);
5221
5316
  const page = await pageAt(ctx, exec, raw, { requireUnit: false });
5222
5317
  if (needsCardOf(row, page.args) && page.unit === "") throw new Error("要卡作用域(scope=card / $card / in=card)就得给一个 unit —— 工坊里不止一个模块时,我不知道你说哪一张卡。");
5223
5318
  const requested = asNumber(page.args["timeoutMs"]);
@@ -5525,7 +5620,7 @@ function nextOf(answer) {
5525
5620
  }
5526
5621
  const plugin = answer.plugins[0];
5527
5622
  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)";
5623
+ return "这台机器上没有现成的 —— 两条路:① 给这个站点写 adapter:opencli skills read opencli-adapter-author;② 只读一次:workbench_diy_eval / _find 带 read=<地址>(在 workbench-read 上跑,借用户已登录的那扇窗)";
5529
5624
  }
5530
5625
  function signatureOf$1(args) {
5531
5626
  const shown = args.slice(0, ARG_LIMIT).map((one) => {
@@ -6055,7 +6150,7 @@ function registerDiyTools(ctx, market, panels, debug) {
6055
6150
  },
6056
6151
  output: OUTPUT$2,
6057
6152
  async execute(_raw, exec) {
6058
- const { packagesDir } = workshopOf(ctx, exec);
6153
+ const { packagesDir } = await workshopOf(ctx, exec);
6059
6154
  const { modules } = modulesOf(ctx, packagesDir);
6060
6155
  return plainJson({
6061
6156
  workshop: packagesDir,
@@ -6078,7 +6173,7 @@ function registerDiyTools(ctx, market, panels, debug) {
6078
6173
  },
6079
6174
  output: OUTPUT$2,
6080
6175
  async execute(raw, exec) {
6081
- const { packagesDir, workspace } = workshopOf(ctx, exec);
6176
+ const { packagesDir, workspace } = await workshopOf(ctx, exec);
6082
6177
  const { modules } = modulesOf(ctx, packagesDir);
6083
6178
  const registry = ctx.get(UNITS_SERVICE);
6084
6179
  const panelEntries = panelEntriesOf(panels, workspace);
@@ -6118,7 +6213,7 @@ function registerDiyTools(ctx, market, panels, debug) {
6118
6213
  },
6119
6214
  output: OUTPUT$2,
6120
6215
  async execute(raw, exec) {
6121
- const { packagesDir, workspace } = workshopOf(ctx, exec);
6216
+ const { packagesDir, workspace } = await workshopOf(ctx, exec);
6122
6217
  const { installed, modules } = modulesOf(ctx, packagesDir);
6123
6218
  const panelState = ctx.get(PANEL_STATE_SERVICE);
6124
6219
  const registry = ctx.get(UNITS_SERVICE);
@@ -6186,7 +6281,7 @@ function registerDiyTools(ctx, market, panels, debug) {
6186
6281
  },
6187
6282
  output: OUTPUT$2,
6188
6283
  async execute(raw, exec) {
6189
- const { packagesDir } = workshopOf(ctx, exec);
6284
+ const { packagesDir } = await workshopOf(ctx, exec);
6190
6285
  const args = asRecord$3(raw);
6191
6286
  const keywords = keywordsOf(asText$1(args["keyword"]), asText$1(args["url"]));
6192
6287
  if (keywords.length === 0) throw new Error("recon 要一个关键词(站点名 / 系统名 / 域名都行):先拿它去 opencli 自带的那份目录里找有没有现成的能力,别一上来就自己写 adapter;用户不知道 opencli 是什么,这一步别问他。");
@@ -6199,96 +6294,6 @@ function registerDiyTools(ctx, market, panels, debug) {
6199
6294
  });
6200
6295
  }
6201
6296
  };
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
6297
  function unitOf(packagesDir, raw, label) {
6293
6298
  const { modules } = modulesOf(ctx, packagesDir);
6294
6299
  const unit = asText$1(asRecord$3(raw)["unit"]);
@@ -6312,7 +6317,7 @@ function registerDiyTools(ctx, market, panels, debug) {
6312
6317
  },
6313
6318
  output: OUTPUT$2,
6314
6319
  async execute(raw, exec) {
6315
- const { packagesDir, workspace } = workshopOf(ctx, exec);
6320
+ const { packagesDir, workspace } = await workshopOf(ctx, exec);
6316
6321
  const found = unitOf(packagesDir, raw, "workbench_diy_show");
6317
6322
  if (!found.installed) throw new Error(`${found.unit} 还没装 —— 只上板的前提是它装过了。先用 workbench_diy_install(第一次)`);
6318
6323
  const panel = showOrReason(panels, found.unit, workspace);
@@ -6340,7 +6345,7 @@ function registerDiyTools(ctx, market, panels, debug) {
6340
6345
  },
6341
6346
  output: OUTPUT$2,
6342
6347
  async execute(raw, exec) {
6343
- const { packagesDir, workspace } = workshopOf(ctx, exec);
6348
+ const { packagesDir, workspace } = await workshopOf(ctx, exec);
6344
6349
  const found = unitOf(packagesDir, raw, "workbench_diy_install");
6345
6350
  if (found.installed) {
6346
6351
  const panel = showOrReason(panels, found.unit, workspace);
@@ -6385,7 +6390,6 @@ function registerDiyTools(ctx, market, panels, debug) {
6385
6390
  ...registerComponentsTool(ctx, tools),
6386
6391
  tools.register(reconTool),
6387
6392
  ...registerSpecTool(ctx, tools),
6388
- tools.register(probeTool),
6389
6393
  ...registerLogTools(ctx, tools),
6390
6394
  ...registerBrowserTools(ctx, tools),
6391
6395
  ...registerDebugTools(ctx, tools, debug),
@@ -6395,14 +6399,13 @@ function registerDiyTools(ctx, market, panels, debug) {
6395
6399
  }
6396
6400
  //#endregion
6397
6401
  //#region src/modules/panels/model/diy-observers.ts
6398
- async function stopObservers(ctx, session = BROWSER_SCREEN_SESSION) {
6399
- const packagesDir = workshopPackagesDir(ctx);
6402
+ async function stopObservers(ctx, workspace, session = BROWSER_SCREEN_SESSION) {
6400
6403
  const shell = ctx.get("shell");
6401
- if (packagesDir === null || shell === void 0) return {
6404
+ if (workspace === null || workspace === void 0 || workspace === "" || shell === void 0) return {
6402
6405
  net: false,
6403
6406
  logs: false
6404
6407
  };
6405
- const file = join(dirname(packagesDir), "shots", `net-${session}.jsonl`);
6408
+ const file = join(workspace, "shots", `net-${session}.jsonl`);
6406
6409
  const command = opencliCommandOf(ctx).command;
6407
6410
  const [net, step] = await Promise.all([stopNetworkExact(shell, file), actBrowser(shell, command, session, ["eval", LOGS_STOP_SCRIPT], SWEEP_TIMEOUT_MS)]);
6408
6411
  return {
@@ -6413,13 +6416,13 @@ async function stopObservers(ctx, session = BROWSER_SCREEN_SESSION) {
6413
6416
  function withObserverCleanup(store, cleanup, logger) {
6414
6417
  let pending = Promise.resolve();
6415
6418
  let epoch = 0;
6416
- const sweep = async (at) => {
6419
+ const sweep = async (at, workspace) => {
6417
6420
  if (epoch !== at) {
6418
6421
  logger?.warn?.("收工那一拍:已经有新的调试约定开始了 —— 跳过收观察(新那一场自己收)");
6419
6422
  return;
6420
6423
  }
6421
6424
  try {
6422
- const out = await cleanup();
6425
+ const out = await cleanup(workspace);
6423
6426
  if (!out.net || !out.logs) logger?.warn?.(`收工那一拍没把观察收干净(网络 ${String(out.net)} / 日志 ${String(out.logs)})—— 网络那条最多 15 分钟自己收工,日志钩子随页面刷新消失`);
6424
6427
  } catch (error) {
6425
6428
  logger?.warn?.(`收工那一拍收观察抛了:${error instanceof Error ? error.message : String(error)}`);
@@ -6437,7 +6440,8 @@ function withObserverCleanup(store, cleanup, logger) {
6437
6440
  const move = store.stop(sessionId, now);
6438
6441
  if (move.ok) {
6439
6442
  const at = epoch;
6440
- pending = pending.then(() => sweep(at));
6443
+ const workspace = move.state.workspacePath;
6444
+ pending = pending.then(() => sweep(at, workspace));
6441
6445
  }
6442
6446
  return move;
6443
6447
  },
@@ -6494,13 +6498,13 @@ var INTENT_WORD$1 = {
6494
6498
  stop: "结束"
6495
6499
  };
6496
6500
  function debugNoticeText(intent, state) {
6497
- const head = `[工作台 DIY]用户按了「${INTENT_WORD$1[intent]}」:`;
6501
+ const head = `[调试 · ${state.title === "" ? "工作区" : state.title}]用户按了「${INTENT_WORD$1[intent]}」:`;
6498
6502
  if (intent === "pause") return `${head}${state.unit} 这场调试暂停了 —— 现在别再把那一屏当目标(看 / 动 / 网络那一族发不出去)。要接着看屏就等用户按「继续」;在那之前照常改源码 / build / 跑门禁。`;
6499
6503
  if (intent === "resume") return `${head}${state.unit} 这场调试重新开始了 —— 那一屏又归你看。屏 / 板不对的话照旧会被拒,拒了就照实说清是哪种不对。`;
6500
6504
  return `${head}${state.unit} 这场调试收工了(占用解除)。要再调试就重新 start 一场。`;
6501
6505
  }
6502
6506
  function debugNoticeSummary(intent, state) {
6503
- return `工作台 DIY:用户按了${INTENT_WORD$1[intent]}(${state.unit})`;
6507
+ return `调试:用户按了${INTENT_WORD$1[intent]}(${state.unit})`;
6504
6508
  }
6505
6509
  function debugNotice(intent, state) {
6506
6510
  const summary = debugNoticeSummary(intent, state);
@@ -7702,7 +7706,7 @@ function registerRecordTools(ctx, store) {
7702
7706
  },
7703
7707
  output: OUTPUT$2,
7704
7708
  async execute(raw, exec) {
7705
- const { packagesDir, workspace } = workshopOf(ctx, exec);
7709
+ const { packagesDir, workspace } = await workshopOf(ctx, exec);
7706
7710
  const args = asRecord$3(raw);
7707
7711
  const unit = asText$1(args["unit"]);
7708
7712
  const modules = modulesOf(ctx, packagesDir).modules;
@@ -7811,7 +7815,7 @@ function registerRecordTools(ctx, store) {
7811
7815
  },
7812
7816
  output: OUTPUT$2,
7813
7817
  async execute(raw, exec) {
7814
- const { packagesDir } = workshopOf(ctx, exec);
7818
+ const { packagesDir } = await workshopOf(ctx, exec);
7815
7819
  const shell = ctx.get("shell");
7816
7820
  if (shell === void 0) throw new Error("这台机器上没有 shell 服务,读不了录制");
7817
7821
  const sessionId = sessionIdOf(exec) ?? "";
@@ -7927,7 +7931,7 @@ function registerRecordTools(ctx, store) {
7927
7931
  },
7928
7932
  output: OUTPUT$2,
7929
7933
  async execute(raw, exec) {
7930
- const { packagesDir } = workshopOf(ctx, exec);
7934
+ const { packagesDir } = await workshopOf(ctx, exec);
7931
7935
  const shell = ctx.get("shell");
7932
7936
  if (shell === void 0) throw new Error("这台机器上没有 shell 服务,取不了详情");
7933
7937
  const args = asRecord$3(raw);
@@ -8007,7 +8011,7 @@ function registerRecordTools(ctx, store) {
8007
8011
  },
8008
8012
  output: OUTPUT$2,
8009
8013
  async execute(_raw, exec) {
8010
- const { packagesDir } = workshopOf(ctx, exec);
8014
+ const { packagesDir } = await workshopOf(ctx, exec);
8011
8015
  const shell = ctx.get("shell");
8012
8016
  if (shell === void 0) throw new Error("这台机器上没有 shell 服务,收不了工");
8013
8017
  const sessionId = sessionIdOf(exec) ?? "";
@@ -9879,8 +9883,21 @@ function panelsHandoffEntry(target = {}) {
9879
9883
  key: `path:${path}`
9880
9884
  };
9881
9885
  }
9886
+ function listedRunUnits(result) {
9887
+ const outer = typeof result === "object" && result !== null ? result : {};
9888
+ const unwrapped = outer["value"];
9889
+ const runs = (typeof unwrapped === "object" && unwrapped !== null ? unwrapped : outer)["runs"];
9890
+ if (!Array.isArray(runs)) return [];
9891
+ const units = /* @__PURE__ */ new Set();
9892
+ for (const row of runs) {
9893
+ const unit = typeof row === "object" && row !== null ? row.unit : void 0;
9894
+ if (typeof unit === "string" && unit !== "") units.add(unit);
9895
+ }
9896
+ return [...units];
9897
+ }
9882
9898
  function panelsFactsOf(call, manifests) {
9883
9899
  if (call.source === "workbench_panels_list") return [PANELS_FACT];
9900
+ if (call.source === "workbench_long_tasks_list") return listedRunUnits(call.result).map(runsRouteOf);
9884
9901
  if (call.source === "workbench_modules_list") return [PANELS_FACT, CAPABILITY_FACT];
9885
9902
  if (call.source === "workbench_modules_update_workbench") return [PANELS_FACT, CAPABILITY_FACT];
9886
9903
  if (call.source === "workbench_diy_list") return [PANELS_FACT];
@@ -10444,7 +10461,7 @@ function registerPanelTools(ctx, panels, values) {
10444
10461
  var OWNED = [{
10445
10462
  unit: panelsUnit.id,
10446
10463
  manifest: panelsUnit,
10447
- mount({ ctx, logger, manifests, ownedUnits, installable, opencliCommand, probe, store, values }) {
10464
+ mount({ ctx, logger, manifests, ownedUnits, installable, opencliCommand, probe, store, values, longTaskRuns }) {
10448
10465
  const ledger = createPluginLedger({
10449
10466
  store,
10450
10467
  home: isolatedHome()
@@ -10457,7 +10474,7 @@ var OWNED = [{
10457
10474
  logger
10458
10475
  });
10459
10476
  ctx.provide(PANELS_SERVICE, panels);
10460
- const debug = withObserverCleanup(createDebugStore(), () => stopObservers(ctx), logger);
10477
+ const debug = withObserverCleanup(createDebugStore(), (workspace) => stopObservers(ctx, workspace), logger);
10461
10478
  ctx.provide(DIY_DEBUG_SERVICE, debug);
10462
10479
  const record = {
10463
10480
  ...createRecordStore(),
@@ -10532,18 +10549,19 @@ var OWNED = [{
10532
10549
  ...registerPanelTools(ctx, panels, values),
10533
10550
  ...registerMarketTools(ctx, market),
10534
10551
  ...registerDiyTools(ctx, market, panels, debug),
10535
- ...registerRecordTools(ctx, record)
10552
+ ...registerRecordTools(ctx, record),
10553
+ ...registerLongTaskTools(ctx, longTaskRuns)
10536
10554
  ],
10537
10555
  factsOf: (call) => panelsFactsOf(call, manifests()),
10538
- needsApproval: panelsApproval,
10556
+ needsApproval: panelsApprovalFor(longTaskRuns),
10539
10557
  gate: {
10540
- deniedTools: (workspace) => {
10541
- const boards = panels.boards(workspace);
10558
+ deniedTools: (context) => {
10559
+ const boards = panels.boards(context.workspace);
10542
10560
  const owned = new Set(ownedUnits());
10543
10561
  return [...manifests().flatMap((manifest) => owned.has(manifest.id) || resolveScope({
10544
10562
  id: manifest.id,
10545
10563
  boards
10546
- }) ? [] : manifest.tools.map((tool) => tool.name)), ...inWorkshopSession(ctx, workspace) ? [] : [
10564
+ }) ? [] : manifest.tools.map((tool) => tool.name)), ...context.preset === "workbench-diy" ? [] : [
10547
10565
  ...DIY_TOOLS,
10548
10566
  ...DIY_DEBUG_TOOLS,
10549
10567
  ...DIY_RECORD_TOOLS
@@ -10592,7 +10610,29 @@ function withRunsRoute(result, unit) {
10592
10610
  runs: path === "" ? [] : runs.current(path)
10593
10611
  };
10594
10612
  }
10595
- }
10613
+ },
10614
+ actions: [...result.actions ?? [], {
10615
+ resource: stopRouteOf(unit),
10616
+ handle(payload) {
10617
+ if (typeof runs.stop !== "function") return {
10618
+ ok: false,
10619
+ text: "这一份外壳还是上一版的注册表,停不了 —— 重新起一下工作台就能停"
10620
+ };
10621
+ const id = payload?.id;
10622
+ if (typeof id !== "string" || id.trim() === "") return {
10623
+ ok: false,
10624
+ text: "要停哪一趟?给一个 id 才成"
10625
+ };
10626
+ if (runs.snapshot(id) === null) return {
10627
+ ok: false,
10628
+ text: "没有这一趟 —— id 对不上,没什么可停的"
10629
+ };
10630
+ return {
10631
+ ok: true,
10632
+ stopped: runs.stop(id)
10633
+ };
10634
+ }
10635
+ }]
10596
10636
  };
10597
10637
  }
10598
10638
  function declaresNote(definition) {
@@ -10761,6 +10801,7 @@ function mountUnits(units, deps) {
10761
10801
  const declaredNotes = /* @__PURE__ */ new Set();
10762
10802
  const skipped = [];
10763
10803
  const longTaskDisposers = /* @__PURE__ */ new Map();
10804
+ const longTaskRuns = deps.longTaskRuns ?? /* @__PURE__ */ new Map();
10764
10805
  const ledger = {
10765
10806
  failed: /* @__PURE__ */ new Set(),
10766
10807
  failures: []
@@ -10781,7 +10822,8 @@ function mountUnits(units, deps) {
10781
10822
  const longTasks = deps.longTasks === void 0 ? void 0 : createLongTaskHost({
10782
10823
  scope: deps.longTasks.scope,
10783
10824
  unit,
10784
- bump: deps.longTasks.bump
10825
+ bump: deps.longTasks.bump,
10826
+ ...deps.longTasks.notify === void 0 ? {} : { notify: deps.longTasks.notify }
10785
10827
  });
10786
10828
  let result;
10787
10829
  try {
@@ -10796,6 +10838,7 @@ function mountUnits(units, deps) {
10796
10838
  ...deps.probe === void 0 ? {} : { probe: deps.probe },
10797
10839
  ...deps.values === void 0 ? {} : { values: deps.values },
10798
10840
  ...longTasks === void 0 ? {} : { longTasks: longTasks.host },
10841
+ longTaskRuns: () => longTaskRuns,
10799
10842
  store
10800
10843
  });
10801
10844
  } catch (error) {
@@ -10816,6 +10859,8 @@ function mountUnits(units, deps) {
10816
10859
  };
10817
10860
  mounted.push(item);
10818
10861
  if (longTasks !== void 0) longTaskDisposers.set(item, longTasks.dispose);
10862
+ const runs = item.result.longTasks;
10863
+ if (runs !== void 0) longTaskRuns.set(unit, runs);
10819
10864
  }
10820
10865
  const collected = collectRoutes(mounted, ledger);
10821
10866
  const prefs = collectNamed(mounted, ledger, (item) => item.result.prefs === void 0 ? [] : [item.result.prefs], "偏好资源");
@@ -10836,6 +10881,8 @@ function mountUnits(units, deps) {
10836
10881
  if (!ledger.failed.has(item)) continue;
10837
10882
  longTaskDisposers.get(item)?.();
10838
10883
  longTaskDisposers.delete(item);
10884
+ const runs = item.result.longTasks;
10885
+ if (runs !== void 0 && longTaskRuns.get(item.host.manifest.id) === runs) longTaskRuns.delete(item.host.manifest.id);
10839
10886
  }
10840
10887
  return {
10841
10888
  mounted: alive,
@@ -10855,6 +10902,10 @@ function mountUnits(units, deps) {
10855
10902
  disposeLongTasks: () => {
10856
10903
  for (const dispose of [...longTaskDisposers.values()]) dispose();
10857
10904
  longTaskDisposers.clear();
10905
+ for (const item of mounted) {
10906
+ const runs = item.result.longTasks;
10907
+ if (runs !== void 0 && longTaskRuns.get(item.host.manifest.id) === runs) longTaskRuns.delete(item.host.manifest.id);
10908
+ }
10858
10909
  }
10859
10910
  };
10860
10911
  }
@@ -13038,9 +13089,15 @@ function configSyncAction(deps) {
13038
13089
  function agentOf(value) {
13039
13090
  return typeof value === "object" && value !== null ? value : void 0;
13040
13091
  }
13041
- function workspaceOf(handle) {
13042
- const cwd = handle.session?.header?.cwd;
13043
- return typeof cwd === "string" && cwd !== "" ? cwd : null;
13092
+ function contextOf(handle, options) {
13093
+ const header = handle.session?.header;
13094
+ const cwd = header?.cwd;
13095
+ const declared = header?.agentPreset;
13096
+ const read = options.presetOf?.(handle);
13097
+ return {
13098
+ workspace: typeof cwd === "string" && cwd !== "" ? cwd : null,
13099
+ preset: read ?? (typeof declared === "string" && declared !== "" ? declared : null)
13100
+ };
13044
13101
  }
13045
13102
  function createToolScope(options) {
13046
13103
  const applied = /* @__PURE__ */ new WeakMap();
@@ -13057,9 +13114,9 @@ function createToolScope(options) {
13057
13114
  options.logger?.warn?.("[dsh-workbench] 拿不到 agent 作用域的工具面:这次不收窄(下次再试)");
13058
13115
  return;
13059
13116
  }
13060
- const workspace = workspaceOf(handle);
13061
- const deny = [...options.deniedTools(workspace)];
13062
- const signature = `${workspace ?? ""}\u0000${deny.join(",")}`;
13117
+ const context = contextOf(handle, options);
13118
+ const deny = [...options.deniedTools(context)];
13119
+ const signature = `${context.workspace ?? ""}\u0000${context.preset ?? ""}\u0000${deny.join(",")}`;
13063
13120
  const previous = applied.get(handle);
13064
13121
  if (previous !== void 0 && previous.signature === signature) return;
13065
13122
  if (previous === void 0) seen.add(new WeakRef(handle));
@@ -13069,12 +13126,13 @@ function createToolScope(options) {
13069
13126
  applied.delete(handle);
13070
13127
  return;
13071
13128
  }
13129
+ const where = context.preset === null ? context.workspace : `${context.workspace ?? "(未知工作区)"} · ${context.preset}`;
13072
13130
  try {
13073
13131
  applied.set(handle, {
13074
13132
  signature,
13075
13133
  dispose: tools.restrict({ deny })
13076
13134
  });
13077
- options.logger?.info?.(`[dsh-workbench] 按工作区收窄工具面:${workspace ?? "(未知工作区)"} 里收起 ${deny.length} 个工具(${deny.join(", ")})`);
13135
+ options.logger?.info?.(`[dsh-workbench] 按会话收窄工具面:${where ?? "(未知工作区)"} 里收起 ${deny.length} 个工具(${deny.join(", ")})`);
13078
13136
  } catch (error) {
13079
13137
  options.logger?.warn?.(`[dsh-workbench] 收窄工具面失败:${String(error)}`);
13080
13138
  }
@@ -13089,7 +13147,7 @@ function createToolScope(options) {
13089
13147
  const handle = agentOf(agent);
13090
13148
  const tools = assembly?.tools;
13091
13149
  if (handle === void 0 || tools === void 0) return assembly;
13092
- const deny = new Set(options.deniedTools(workspaceOf(handle)));
13150
+ const deny = new Set(options.deniedTools(contextOf(handle, options)));
13093
13151
  if (deny.size === 0) return assembly;
13094
13152
  const kept = tools.filter((tool) => !deny.has(tool.name));
13095
13153
  return kept.length === tools.length ? assembly : {
@@ -13116,6 +13174,41 @@ function createToolScope(options) {
13116
13174
  };
13117
13175
  }
13118
13176
  //#endregion
13177
+ //#region src/shared/api/host/preset-scope.ts
13178
+ var PRESET_KEY = "agentPreset";
13179
+ function presetOf(ctx, handle) {
13180
+ const projections = ctx.get(SESSION_PROJECTIONS_SERVICE);
13181
+ if (projections === void 0 || handle.session === void 0) return null;
13182
+ try {
13183
+ const preset = projections.stateOf(handle.session, PRESET_KEY);
13184
+ return typeof preset === "string" && preset !== "" ? preset : null;
13185
+ } catch (error) {
13186
+ ctx.logger?.warn?.(`[dsh-workbench] 读 preset 投影失败,退回会话创建事实:${String(error)}`);
13187
+ return null;
13188
+ }
13189
+ }
13190
+ function watchPreset(ctx, listener) {
13191
+ ctx.inject([SESSION_PROJECTIONS_SERVICE], (scoped) => {
13192
+ const projections = scoped.get(SESSION_PROJECTIONS_SERVICE);
13193
+ if (projections === void 0) return;
13194
+ scoped.effect(() => {
13195
+ const off = projections.onChanged((_session, key) => {
13196
+ if (key === PRESET_KEY) listener();
13197
+ });
13198
+ return () => off?.();
13199
+ }, "dsh-workbench: preset 选中就重算工具面(新会话屏是先建会话、再把选择 select 上去)");
13200
+ });
13201
+ }
13202
+ function createPresetScope(options) {
13203
+ const scope = createToolScope({
13204
+ deniedTools: options.deniedTools,
13205
+ presetOf: (handle) => presetOf(options.ctx, handle),
13206
+ logger: options.logger
13207
+ });
13208
+ watchPreset(options.ctx, () => scope.reapply());
13209
+ return scope;
13210
+ }
13211
+ //#endregion
13119
13212
  //#region src/shared/api/host/host-args.ts
13120
13213
  function asArgs(value) {
13121
13214
  return typeof value === "object" && value !== null ? value : {};
@@ -13124,6 +13217,64 @@ function warn(ctx, message) {
13124
13217
  ctx.logger?.warn?.(`[dsh-workbench] ${message}`);
13125
13218
  }
13126
13219
  //#endregion
13220
+ //#region src/shared/api/host/approval-call.ts
13221
+ /**
13222
+ * 把 dsh 交来的那次工具调用折成审批策略要看的那份 call:工作区与发起会话都从 `exec` 现取,取不到就不写那个键。
13223
+ * @param exec dsh 交来的执行上下文;没有就只有实参这一格。
13224
+ * @param name 工具名;`undefined` = 这次调用没名字,不写这个键。
13225
+ * @returns 交给 `approvalFor` 的 `ApprovalCall`。
13226
+ */
13227
+ function approvalCallOf(exec, name) {
13228
+ const workspace = sessionWorkspace(exec) ?? void 0;
13229
+ const session = sessionIdOf(exec) ?? void 0;
13230
+ return {
13231
+ ...name === void 0 ? {} : { name },
13232
+ arguments: exec?.arguments,
13233
+ ...workspace === void 0 ? {} : { workspace },
13234
+ ...session === void 0 ? {} : { session }
13235
+ };
13236
+ }
13237
+ //#endregion
13238
+ //#region src/shared/api/host/long-task-notice.ts
13239
+ /**
13240
+ * 这条 notice 的来源标识(`source.kind`)—— 长任务自己一格,与调试 / 录制分开。
13241
+ */
13242
+ var LONG_TASK_NOTICE_KIND = "workbench-long-task";
13243
+ /**
13244
+ * 判这一趟被停要不要回流给发起它的那个会话,并把话写好。
13245
+ * 只有"结算 + `stopped` + 这一趟记得会话 + 不是它自己停的"才回一条;跑完 / 失败 / 界面起的(没记会话)/ 自己停的回 `null`。
13246
+ * @param unit 这一趟属于哪个单元(进文案)。
13247
+ * @param change 长任务那一次变更;只有 `kind: 'settle'` 且 `snapshot.status === 'stopped'` 才可能成。
13248
+ * @returns 该发的那条(带收件会话),或者 `null`(不发)。
13249
+ */
13250
+ function longTaskStopNotice(unit, change) {
13251
+ if (change.kind !== "settle") return null;
13252
+ const snapshot = change.snapshot;
13253
+ if (snapshot.status !== "stopped") return null;
13254
+ const session = snapshot.session;
13255
+ if (session === void 0 || session === "") return null;
13256
+ if (change.by === session) return null;
13257
+ const target = snapshot.target;
13258
+ const summary = `长任务:${unit} 的「${target}」被停了`;
13259
+ return {
13260
+ session,
13261
+ summary,
13262
+ message: noticeMessage(`[长任务 · ${unit}]「${target}」那一趟被${change.by === void 0 ? "用户" : "别的会话"}停了 —— 别再把它当"还在跑";要接着做就重新起一趟(workbench_long_tasks_list 看当前这几趟)。`, summary, LONG_TASK_NOTICE_KIND)
13263
+ };
13264
+ }
13265
+ /**
13266
+ * 把"这一趟被停了"送出去:正在跑的会话 inject 一条、闲着的跳过、没有这个 agent 就当没发生(模式 `drop`)。
13267
+ * @param agents 会话表(宿主从 `ctx.get('agents')` 拿);拿不到就什么都不发。
13268
+ * @param unit 这一趟属于哪个单元。
13269
+ * @param change 长任务那一次变更。
13270
+ * @returns 送出去的结果;判据说不该发(`longTaskStopNotice` 回 `null`)就回 `null`。
13271
+ */
13272
+ function notifyLongTaskStop(agents, unit, change) {
13273
+ const notice = longTaskStopNotice(unit, change);
13274
+ if (notice === null) return null;
13275
+ return deliverDebugNotice(agents, notice.session, notice.message, "drop");
13276
+ }
13277
+ //#endregion
13127
13278
  //#region src/app/host.ts
13128
13279
  var name = "dsh-workbench";
13129
13280
  var inject = ["webServer"];
@@ -13219,8 +13370,9 @@ function apply(ctx) {
13219
13370
  };
13220
13371
  const store = createWorkspaceStore({ logger: ctx.logger });
13221
13372
  const unitsOfTool = () => toolUnits(liveManifests());
13222
- const toolScope = createToolScope({
13223
- deniedTools: (workspace) => deniedNow(workspace),
13373
+ const toolScope = createPresetScope({
13374
+ ctx,
13375
+ deniedTools: (context) => deniedNow(context),
13224
13376
  logger: ctx.logger
13225
13377
  });
13226
13378
  const pluginTools = shell === void 0 ? void 0 : createPluginUnits({
@@ -13242,12 +13394,13 @@ function apply(ctx) {
13242
13394
  });
13243
13395
  const mounted = /* @__PURE__ */ new Map();
13244
13396
  const mountFailures = /* @__PURE__ */ new Map();
13397
+ const longTaskRuns = /* @__PURE__ */ new Map();
13245
13398
  const factsOfMap = /* @__PURE__ */ new Map();
13246
13399
  const routesOfUnitMap = /* @__PURE__ */ new Map();
13247
13400
  const ownersMap = /* @__PURE__ */ new Map();
13248
13401
  const factsOfUnit = (unit) => [...routesOfUnitMap.get(unit) ?? [], "capability"];
13249
13402
  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) ?? []))];
13403
+ const deniedNow = (context) => [...new Set([...mounted.values()].flatMap((item) => item.plan.gate?.deniedTools(context) ?? []))];
13251
13404
  const declaredNoteTools = () => declaredNotesOf([...mounted.values()].map((item) => item.plan));
13252
13405
  const wrote = (resource, write) => {
13253
13406
  const unit = ownersMap.get(resource);
@@ -13294,8 +13447,10 @@ function apply(ctx) {
13294
13447
  values,
13295
13448
  longTasks: {
13296
13449
  scope: process.env["DSH_HOME"] ?? "",
13297
- bump: (names) => facts.bump(names)
13298
- }
13450
+ bump: (names) => facts.bump(names),
13451
+ notify: (change) => notifyLongTaskStop(ctx.get("agents"), host.unit, change)
13452
+ },
13453
+ longTaskRuns
13299
13454
  });
13300
13455
  } catch (error) {
13301
13456
  mountFailures.set(id, {
@@ -13473,12 +13628,7 @@ function apply(ctx) {
13473
13628
  const tool = name === void 0 ? void 0 : unitsOfTool().get(name);
13474
13629
  const owner = tool === void 0 ? void 0 : mounted.get(tool.unit)?.result;
13475
13630
  const detailOf = owner === void 0 ? void 0 : (call) => owner.approvalDetail?.(call);
13476
- const workspace = sessionWorkspace(exec) ?? void 0;
13477
- const call = {
13478
- ...name === void 0 ? {} : { name },
13479
- arguments: exec.arguments,
13480
- ...workspace === void 0 ? {} : { workspace }
13481
- };
13631
+ const call = approvalCallOf(exec, name);
13482
13632
  const policy = owner?.needsApproval;
13483
13633
  const declaredNote = name !== void 0 && declaredNoteTools().has(name);
13484
13634
  return await approvalFor(call, {