@dsh-workbench/core 0.3.5 → 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.
@@ -277,6 +277,14 @@ function unitFactScope(unit) {
277
277
  function runsRouteOf(unit) {
278
278
  return `${unit}/runs`;
279
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
+ }
280
288
  //#endregion
281
289
  //#region ../shared/dist/lib/text.js
282
290
  /**
@@ -350,7 +358,7 @@ var PHASES = [
350
358
  function asRecord$8(value) {
351
359
  return typeof value === "object" && value !== null ? value : void 0;
352
360
  }
353
- function textOf$6(value, max) {
361
+ function textOf$7(value, max) {
354
362
  if (typeof value !== "string") return "";
355
363
  const trimmed = value.trim();
356
364
  return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
@@ -363,8 +371,8 @@ function atOf(value) {
363
371
  function errorOf(raw) {
364
372
  const record = asRecord$8(raw);
365
373
  if (record === void 0) return void 0;
366
- const code = textOf$6(record["code"], 120);
367
- const message = textOf$6(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);
368
376
  if (code === "" && message === "") return void 0;
369
377
  return {
370
378
  code: code === "" ? "unknown" : code,
@@ -383,7 +391,7 @@ function parseCardsReport(payload) {
383
391
  ok: false,
384
392
  message: "请求体要是一个对象"
385
393
  };
386
- const workspace = textOf$6(record["workspace"], PANEL_STATE_MAX_TEXT);
394
+ const workspace = textOf$7(record["workspace"], PANEL_STATE_MAX_TEXT);
387
395
  if (workspace === "") return {
388
396
  ok: false,
389
397
  message: "workspace 不能是空的 —— 面板按工作区存"
@@ -397,9 +405,9 @@ function parseCardsReport(payload) {
397
405
  for (const item of raw.slice(0, 32)) {
398
406
  const card = asRecord$8(item);
399
407
  if (card === void 0) continue;
400
- const unit = textOf$6(card["unit"], 200);
401
- const route = textOf$6(card["route"], 400);
402
- const phase = textOf$6(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);
403
411
  const at = atOf(card["at"]);
404
412
  if (unit === "" || route === "" || at === null || !PHASES.includes(phase)) continue;
405
413
  const error = errorOf(card["error"]);
@@ -434,9 +442,9 @@ function parseCrashReport(payload) {
434
442
  ok: false,
435
443
  message: "请求体要是一个对象"
436
444
  };
437
- const workspace = textOf$6(record["workspace"], PANEL_STATE_MAX_TEXT);
438
- const unit = textOf$6(record["unit"], 200);
439
- const reason = textOf$6(record["reason"], PANEL_STATE_MAX_TEXT);
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);
440
448
  if (workspace === "") return {
441
449
  ok: false,
442
450
  message: "workspace 不能是空的"
@@ -1596,11 +1604,17 @@ var LIVE = /* @__PURE__ */ new Set(["running", "needs-login"]);
1596
1604
  var RECENT_MS = 6e5;
1597
1605
  function createLongTaskRegistry(deps) {
1598
1606
  const runs = /* @__PURE__ */ new Map();
1607
+ const controls = /* @__PURE__ */ new Map();
1599
1608
  const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
1600
1609
  let counter = 0;
1601
1610
  const nextId = deps.nextId ?? (() => `run-${Date.now()}-${counter += 1}`);
1602
1611
  const recentMs = deps.recentMs ?? RECENT_MS;
1603
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
+ };
1604
1618
  const notify = (change) => {
1605
1619
  for (const listener of [...listeners]) try {
1606
1620
  listener(change);
@@ -1635,7 +1649,10 @@ function createLongTaskRegistry(deps) {
1635
1649
  if (snapshot.workspace !== workspace) continue;
1636
1650
  newest.set(snapshot.target, snapshot);
1637
1651
  }
1638
- 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
+ }));
1639
1656
  },
1640
1657
  start(options) {
1641
1658
  const running = this.active(options.workspace, options.target);
@@ -1650,15 +1667,16 @@ function createLongTaskRegistry(deps) {
1650
1667
  step: "",
1651
1668
  steps: [],
1652
1669
  startedAt,
1653
- at: startedAt
1670
+ at: startedAt,
1671
+ ...options.session === void 0 ? {} : { session: options.session }
1654
1672
  });
1655
1673
  notify({
1656
1674
  kind: "start",
1657
1675
  snapshot: runs.get(id)
1658
1676
  });
1659
1677
  const onStep = (step) => {
1660
- const current = runs.get(id);
1661
- if (current === void 0) return;
1678
+ const current = live(id);
1679
+ if (current === null) return;
1662
1680
  const steps = [...current.steps, `${step.kind === "fail" ? "✗ " : ""}${step.text}`].slice(-20);
1663
1681
  const extra = deps.step?.(step, current) ?? {};
1664
1682
  if (step.loginUrl !== void 0) {
@@ -1685,17 +1703,24 @@ function createLongTaskRegistry(deps) {
1685
1703
  step
1686
1704
  });
1687
1705
  };
1706
+ const control = new AbortController();
1707
+ controls.set(id, control);
1688
1708
  deps.run({
1689
1709
  payload: options.payload,
1690
- onStep
1710
+ onStep,
1711
+ signal: control.signal
1691
1712
  }).then((result) => {
1692
- const current = runs.get(id);
1713
+ const current = live(id);
1714
+ if (current === null) return;
1715
+ controls.delete(id);
1693
1716
  patch(id, {
1694
1717
  status: "ok",
1695
1718
  ...deps.outcome?.(result, current) ?? {},
1696
1719
  loginUrl: void 0
1697
1720
  }, { kind: "settle" });
1698
1721
  }).catch((error) => {
1722
+ if (live(id) === null) return;
1723
+ controls.delete(id);
1699
1724
  patch(id, {
1700
1725
  status: "failed",
1701
1726
  why: error instanceof Error ? error.message : String(error),
@@ -1704,6 +1729,21 @@ function createLongTaskRegistry(deps) {
1704
1729
  });
1705
1730
  return runs.get(id);
1706
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
+ },
1707
1747
  snapshot(id) {
1708
1748
  return runs.get(id) ?? null;
1709
1749
  },
@@ -1736,6 +1776,7 @@ function createLongTaskHost(options) {
1736
1776
  offs.push(registry.onChange((change) => {
1737
1777
  const named = [runsRouteOf(options.unit), ...facts?.(change) ?? []];
1738
1778
  options.bump([...new Set(named)]);
1779
+ options.notify?.(change);
1739
1780
  }));
1740
1781
  return registry;
1741
1782
  } },
@@ -1802,7 +1843,7 @@ var PLUGIN_LEDGER_FILE = "plugins.json";
1802
1843
  function ledgerKeyOf(entry) {
1803
1844
  return `${entry.opencliHome}\u0000${entry.package}`;
1804
1845
  }
1805
- function textOf$5(value) {
1846
+ function textOf$6(value) {
1806
1847
  if (typeof value !== "string") return void 0;
1807
1848
  const trimmed = value.trim();
1808
1849
  return trimmed === "" ? void 0 : trimmed;
@@ -1810,13 +1851,13 @@ function textOf$5(value) {
1810
1851
  function entryOf$1(value) {
1811
1852
  if (typeof value !== "object" || value === null) return void 0;
1812
1853
  const box = value;
1813
- const opencliHome = textOf$5(box["opencliHome"]);
1814
- const pkg = textOf$5(box["package"]);
1815
- 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"]);
1816
1857
  if (opencliHome === void 0 || pkg === void 0 || installedAt === void 0) return void 0;
1817
- const name = textOf$5(box["name"]);
1818
- const registry = textOf$5(box["registry"]);
1819
- 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"]);
1820
1861
  return {
1821
1862
  opencliHome,
1822
1863
  package: pkg,
@@ -2789,6 +2830,8 @@ var DIY_RECORD_PREPARE_TOOL = "workbench_diy_record_prepare";
2789
2830
  var DIY_RECORD_READ_TOOL = "workbench_diy_record_read";
2790
2831
  var DIY_RECORD_DETAIL_TOOL = "workbench_diy_record_detail";
2791
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";
2792
2835
  var DIY_DEBUG_START_TOOL = "workbench_diy_debug_start";
2793
2836
  var DIY_DEBUG_PAUSE_TOOL = "workbench_diy_debug_pause";
2794
2837
  var DIY_DEBUG_RESUME_TOOL = "workbench_diy_debug_resume";
@@ -2821,7 +2864,8 @@ var READ_TOOLS = [
2821
2864
  DIY_LOGS_STOP_TOOL,
2822
2865
  DIY_RECORD_READ_TOOL,
2823
2866
  DIY_RECORD_DETAIL_TOOL,
2824
- DIY_RECORD_STOP_TOOL
2867
+ DIY_RECORD_STOP_TOOL,
2868
+ LONG_TASKS_LIST_TOOL
2825
2869
  ];
2826
2870
  var WRITE_TOOLS = [
2827
2871
  PANELS_SHOW_TOOL,
@@ -2843,7 +2887,8 @@ var WRITE_TOOLS = [
2843
2887
  DIY_DEBUG_START_TOOL,
2844
2888
  DIY_DEBUG_PAUSE_TOOL,
2845
2889
  DIY_DEBUG_RESUME_TOOL,
2846
- DIY_DEBUG_STOP_TOOL
2890
+ DIY_DEBUG_STOP_TOOL,
2891
+ LONG_TASKS_STOP_TOOL
2847
2892
  ];
2848
2893
  var DIY_TOOLS = [
2849
2894
  DIY_LIST_TOOL,
@@ -2902,6 +2947,7 @@ var panelsUnit = {
2902
2947
  //#endregion
2903
2948
  //#region src/modules/panels/model/approval.ts
2904
2949
  var SPEC_LIMIT = 80;
2950
+ var TARGET_LIMIT = 20;
2905
2951
  function missing(id) {
2906
2952
  return id === void 0 || id === "";
2907
2953
  }
@@ -2976,7 +3022,32 @@ function askOf(reason, question, fallback) {
2976
3022
  fallback
2977
3023
  };
2978
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
+ }
2979
3049
  var panelsApproval = (call) => {
3050
+ if (call.name === "workbench_long_tasks_stop") return stopVerdict(call, void 0);
2980
3051
  if (call.name === "workbench_diy_debug_start" || call.name === "workbench_diy_debug_resume") {
2981
3052
  const starting = call.name === DIY_DEBUG_START_TOOL;
2982
3053
  const unit = argsOf$2(call)["unit"];
@@ -3048,6 +3119,15 @@ var panelsApproval = (call) => {
3048
3119
  default: return;
3049
3120
  }
3050
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
+ }
3051
3131
  //#endregion
3052
3132
  //#region src/modules/panels/model/config.ts
3053
3133
  var EMPTY_BOARD = {
@@ -3198,6 +3278,191 @@ function withMoved(board, id, index, visible) {
3198
3278
  };
3199
3279
  }
3200
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
3201
3466
  //#region src/shared/api/host/diy-components.ts
3202
3467
  var SURFACE_TIMEOUT_MS = 3e4;
3203
3468
  var PRUNE_CHARS = 8192;
@@ -9618,8 +9883,21 @@ function panelsHandoffEntry(target = {}) {
9618
9883
  key: `path:${path}`
9619
9884
  };
9620
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
+ }
9621
9898
  function panelsFactsOf(call, manifests) {
9622
9899
  if (call.source === "workbench_panels_list") return [PANELS_FACT];
9900
+ if (call.source === "workbench_long_tasks_list") return listedRunUnits(call.result).map(runsRouteOf);
9623
9901
  if (call.source === "workbench_modules_list") return [PANELS_FACT, CAPABILITY_FACT];
9624
9902
  if (call.source === "workbench_modules_update_workbench") return [PANELS_FACT, CAPABILITY_FACT];
9625
9903
  if (call.source === "workbench_diy_list") return [PANELS_FACT];
@@ -10183,7 +10461,7 @@ function registerPanelTools(ctx, panels, values) {
10183
10461
  var OWNED = [{
10184
10462
  unit: panelsUnit.id,
10185
10463
  manifest: panelsUnit,
10186
- mount({ ctx, logger, manifests, ownedUnits, installable, opencliCommand, probe, store, values }) {
10464
+ mount({ ctx, logger, manifests, ownedUnits, installable, opencliCommand, probe, store, values, longTaskRuns }) {
10187
10465
  const ledger = createPluginLedger({
10188
10466
  store,
10189
10467
  home: isolatedHome()
@@ -10271,10 +10549,11 @@ var OWNED = [{
10271
10549
  ...registerPanelTools(ctx, panels, values),
10272
10550
  ...registerMarketTools(ctx, market),
10273
10551
  ...registerDiyTools(ctx, market, panels, debug),
10274
- ...registerRecordTools(ctx, record)
10552
+ ...registerRecordTools(ctx, record),
10553
+ ...registerLongTaskTools(ctx, longTaskRuns)
10275
10554
  ],
10276
10555
  factsOf: (call) => panelsFactsOf(call, manifests()),
10277
- needsApproval: panelsApproval,
10556
+ needsApproval: panelsApprovalFor(longTaskRuns),
10278
10557
  gate: {
10279
10558
  deniedTools: (context) => {
10280
10559
  const boards = panels.boards(context.workspace);
@@ -10331,7 +10610,29 @@ function withRunsRoute(result, unit) {
10331
10610
  runs: path === "" ? [] : runs.current(path)
10332
10611
  };
10333
10612
  }
10334
- }
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
+ }]
10335
10636
  };
10336
10637
  }
10337
10638
  function declaresNote(definition) {
@@ -10500,6 +10801,7 @@ function mountUnits(units, deps) {
10500
10801
  const declaredNotes = /* @__PURE__ */ new Set();
10501
10802
  const skipped = [];
10502
10803
  const longTaskDisposers = /* @__PURE__ */ new Map();
10804
+ const longTaskRuns = deps.longTaskRuns ?? /* @__PURE__ */ new Map();
10503
10805
  const ledger = {
10504
10806
  failed: /* @__PURE__ */ new Set(),
10505
10807
  failures: []
@@ -10520,7 +10822,8 @@ function mountUnits(units, deps) {
10520
10822
  const longTasks = deps.longTasks === void 0 ? void 0 : createLongTaskHost({
10521
10823
  scope: deps.longTasks.scope,
10522
10824
  unit,
10523
- bump: deps.longTasks.bump
10825
+ bump: deps.longTasks.bump,
10826
+ ...deps.longTasks.notify === void 0 ? {} : { notify: deps.longTasks.notify }
10524
10827
  });
10525
10828
  let result;
10526
10829
  try {
@@ -10535,6 +10838,7 @@ function mountUnits(units, deps) {
10535
10838
  ...deps.probe === void 0 ? {} : { probe: deps.probe },
10536
10839
  ...deps.values === void 0 ? {} : { values: deps.values },
10537
10840
  ...longTasks === void 0 ? {} : { longTasks: longTasks.host },
10841
+ longTaskRuns: () => longTaskRuns,
10538
10842
  store
10539
10843
  });
10540
10844
  } catch (error) {
@@ -10555,6 +10859,8 @@ function mountUnits(units, deps) {
10555
10859
  };
10556
10860
  mounted.push(item);
10557
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);
10558
10864
  }
10559
10865
  const collected = collectRoutes(mounted, ledger);
10560
10866
  const prefs = collectNamed(mounted, ledger, (item) => item.result.prefs === void 0 ? [] : [item.result.prefs], "偏好资源");
@@ -10575,6 +10881,8 @@ function mountUnits(units, deps) {
10575
10881
  if (!ledger.failed.has(item)) continue;
10576
10882
  longTaskDisposers.get(item)?.();
10577
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);
10578
10886
  }
10579
10887
  return {
10580
10888
  mounted: alive,
@@ -10594,6 +10902,10 @@ function mountUnits(units, deps) {
10594
10902
  disposeLongTasks: () => {
10595
10903
  for (const dispose of [...longTaskDisposers.values()]) dispose();
10596
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
+ }
10597
10909
  }
10598
10910
  };
10599
10911
  }
@@ -12905,6 +13217,64 @@ function warn(ctx, message) {
12905
13217
  ctx.logger?.warn?.(`[dsh-workbench] ${message}`);
12906
13218
  }
12907
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
12908
13278
  //#region src/app/host.ts
12909
13279
  var name = "dsh-workbench";
12910
13280
  var inject = ["webServer"];
@@ -13024,6 +13394,7 @@ function apply(ctx) {
13024
13394
  });
13025
13395
  const mounted = /* @__PURE__ */ new Map();
13026
13396
  const mountFailures = /* @__PURE__ */ new Map();
13397
+ const longTaskRuns = /* @__PURE__ */ new Map();
13027
13398
  const factsOfMap = /* @__PURE__ */ new Map();
13028
13399
  const routesOfUnitMap = /* @__PURE__ */ new Map();
13029
13400
  const ownersMap = /* @__PURE__ */ new Map();
@@ -13076,8 +13447,10 @@ function apply(ctx) {
13076
13447
  values,
13077
13448
  longTasks: {
13078
13449
  scope: process.env["DSH_HOME"] ?? "",
13079
- bump: (names) => facts.bump(names)
13080
- }
13450
+ bump: (names) => facts.bump(names),
13451
+ notify: (change) => notifyLongTaskStop(ctx.get("agents"), host.unit, change)
13452
+ },
13453
+ longTaskRuns
13081
13454
  });
13082
13455
  } catch (error) {
13083
13456
  mountFailures.set(id, {
@@ -13255,12 +13628,7 @@ function apply(ctx) {
13255
13628
  const tool = name === void 0 ? void 0 : unitsOfTool().get(name);
13256
13629
  const owner = tool === void 0 ? void 0 : mounted.get(tool.unit)?.result;
13257
13630
  const detailOf = owner === void 0 ? void 0 : (call) => owner.approvalDetail?.(call);
13258
- const workspace = sessionWorkspace(exec) ?? void 0;
13259
- const call = {
13260
- ...name === void 0 ? {} : { name },
13261
- arguments: exec.arguments,
13262
- ...workspace === void 0 ? {} : { workspace }
13263
- };
13631
+ const call = approvalCallOf(exec, name);
13264
13632
  const policy = owner?.needsApproval;
13265
13633
  const declaredNote = name !== void 0 && declaredNoteTools().has(name);
13266
13634
  return await approvalFor(call, {