@dsh-workbench/core 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/client.js +512 -42
  2. package/cordis.patch.yml +32 -0
  3. package/dist/client/client.body.js +512 -42
  4. package/dist/client/index.js +512 -42
  5. package/dist/host/index.js +2486 -323
  6. package/dist/types/app/client.d.ts +108 -3
  7. package/dist/types/app/layouts/approval-detail.d.ts +13 -0
  8. package/dist/types/app/layouts/sidebar/RecordFooterAction.d.ts +16 -0
  9. package/dist/types/app/layouts/sidebar/record-footer.d.ts +4 -0
  10. package/dist/types/modules/panels/client.d.ts +1 -0
  11. package/dist/types/modules/panels/model/diy-components.d.ts +3 -0
  12. package/dist/types/modules/panels/model/diy-components.test.d.ts +1 -0
  13. package/dist/types/modules/panels/model/diy-record-bar.test.d.ts +1 -0
  14. package/dist/types/modules/panels/model/diy-record-capture.d.ts +27 -0
  15. package/dist/types/modules/panels/model/diy-record-capture.test.d.ts +1 -0
  16. package/dist/types/modules/panels/model/diy-record-failure.test.d.ts +1 -0
  17. package/dist/types/modules/panels/model/diy-record-routes.d.ts +2 -0
  18. package/dist/types/modules/panels/model/diy-record-routes.test.d.ts +1 -0
  19. package/dist/types/modules/panels/model/diy-record-scripts.d.ts +6 -0
  20. package/dist/types/modules/panels/model/diy-record-text.d.ts +89 -0
  21. package/dist/types/modules/panels/model/diy-record-text.test.d.ts +1 -0
  22. package/dist/types/modules/panels/model/diy-record-tools.d.ts +2 -0
  23. package/dist/types/modules/panels/model/diy-record.d.ts +76 -0
  24. package/dist/types/modules/panels/model/diy-record.test.d.ts +1 -0
  25. package/dist/types/modules/panels/model/handoff.d.ts +1 -0
  26. package/dist/types/modules/panels/unit.d.ts +10 -4
  27. package/dist/types/shared/api/host/diy-acts.d.ts +4 -0
  28. package/dist/types/shared/api/host/diy-components.d.ts +54 -0
  29. package/dist/types/shared/api/host/diy-debug-act.d.ts +24 -7
  30. package/dist/types/shared/api/host/diy-record-act.d.ts +19 -0
  31. package/dist/types/shared/api/host/diy-workshop.d.ts +1 -0
  32. package/dist/types/shared/api/host/mount-units.d.ts +4 -0
  33. package/dist/types/shared/hooks/use-diy-record.d.ts +8 -0
  34. package/package.json +2 -2
  35. package/scripts/seed-workshop.sh +42 -2
  36. package/skills/workbench-module-diy/BUILD.md +4 -3
  37. package/skills/workbench-module-diy/DESIGN.md +26 -1
  38. package/skills/workbench-module-diy/FAMILY.md +55 -0
  39. package/skills/workbench-module-diy/HOOKS.md +95 -0
  40. package/skills/workbench-module-diy/KIT.md +16 -6
  41. package/skills/workbench-module-diy/PANEL.md +5 -2
  42. package/skills/workbench-module-diy/REVIEW.md +5 -4
  43. package/skills/workbench-module-diy/SKILL.md +12 -3
  44. package/skills/workbench-module-diy/TOOLING.md +5 -2
  45. package/skills/workbench-module-diy/TOOLS.md +6 -40
  46. package/skills/workbench-module-diy/WIRING.md +12 -5
  47. package/skills/workbench-module-diy/WRITES.md +47 -6
@@ -1,9 +1,9 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import fs, { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import path, { basename, delimiter, dirname, join, resolve, sep } from "node:path";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { deflateSync, inflateSync } from "node:zlib";
5
6
  import { spawn } from "node:child_process";
6
- import { fileURLToPath } from "node:url";
7
7
  import os from "node:os";
8
8
  //#region ../shared/dist/config/constants.js
9
9
  var PLUGIN_ID = "@dsh-workbench/core";
@@ -16,12 +16,15 @@ var PROVISION_PREFIX = `${API_PREFIX}/provision`;
16
16
  var PANEL_STATE_PATH = "panel-state";
17
17
  var DIY_DEBUG_PATH = "diy-debug";
18
18
  `${DIY_DEBUG_PATH}`;
19
+ var DIY_RECORD_PATH = "diy-record";
20
+ `${DIY_RECORD_PATH}`;
19
21
  var PANEL_STATE_PREFIX = `${API_PREFIX}/${PANEL_STATE_PATH}`;
20
22
  var OPENCLI_PIN = "1.8.8";
21
23
  var PANELS_SERVICE = "workbenchPanels";
22
24
  var UNITS_SERVICE = "workbenchUnits";
23
25
  var PANEL_STATE_SERVICE = "workbenchPanelState";
24
26
  var DIY_DEBUG_SERVICE = "workbenchDiyDebug";
27
+ var DIY_RECORD_SERVICE = "workbenchDiyRecord";
25
28
  var OPENCLI_TIMEOUT_MS = 12e4;
26
29
  var WARM_TIMEOUT_MS = 2e4;
27
30
  var INSTALL_TIMEOUT_MS = 3e5;
@@ -465,22 +468,22 @@ function boardSituation(probe, workshop) {
465
468
  function parseDebugIntent(value) {
466
469
  return value === "pause" || value === "resume" || value === "stop" ? value : null;
467
470
  }
468
- function alive(state, now) {
471
+ function alive$1(state, now) {
469
472
  if (state === null) return null;
470
473
  return now - state.at <= 144e5 ? state : null;
471
474
  }
472
- function ownerOf(state, sessionId, now) {
473
- const held = alive(state, now);
475
+ function ownerOf$1(state, sessionId, now) {
476
+ const held = alive$1(state, now);
474
477
  if (held === null) return "none";
475
478
  return held.sessionId === sessionId ? held : "not-owner";
476
479
  }
477
480
  function createDebugStore() {
478
481
  let held = null;
479
- const live = (now) => alive(held, now);
482
+ const live = (now) => alive$1(held, now);
480
483
  return {
481
484
  live,
482
485
  begin(pact) {
483
- const current = alive(held, pact.now);
486
+ const current = alive$1(held, pact.now);
484
487
  if (current !== null) return {
485
488
  ok: false,
486
489
  code: current.sessionId === pact.sessionId ? "already-held" : "held-by-other"
@@ -502,7 +505,7 @@ function createDebugStore() {
502
505
  };
503
506
  },
504
507
  pause(sessionId, now) {
505
- const found = ownerOf(held, sessionId, now);
508
+ const found = ownerOf$1(held, sessionId, now);
506
509
  if (typeof found === "string") return {
507
510
  ok: false,
508
511
  code: found
@@ -523,7 +526,7 @@ function createDebugStore() {
523
526
  };
524
527
  },
525
528
  resume(sessionId, now) {
526
- const found = ownerOf(held, sessionId, now);
529
+ const found = ownerOf$1(held, sessionId, now);
527
530
  if (typeof found === "string") return {
528
531
  ok: false,
529
532
  code: found
@@ -543,6 +546,113 @@ function createDebugStore() {
543
546
  state
544
547
  };
545
548
  },
549
+ stop(sessionId, now) {
550
+ const found = ownerOf$1(held, sessionId, now);
551
+ if (typeof found === "string") return {
552
+ ok: false,
553
+ code: found
554
+ };
555
+ held = null;
556
+ return {
557
+ ok: true,
558
+ state: found
559
+ };
560
+ }
561
+ };
562
+ }
563
+ function parseRecordIntent(raw) {
564
+ return raw === "peek" || raw === "halt" || raw === "stop" ? raw : null;
565
+ }
566
+ function alive(state, now) {
567
+ if (state === null) return null;
568
+ return now - state.at <= 9e5 ? state : null;
569
+ }
570
+ function ownerOf(state, sessionId, now) {
571
+ const held = alive(state, now);
572
+ if (held === null) return "none";
573
+ return held.sessionId === sessionId ? held : "not-owner";
574
+ }
575
+ function createRecordStore() {
576
+ let held = null;
577
+ const live = (now) => alive(held, now);
578
+ return {
579
+ live,
580
+ begin(pact) {
581
+ const current = alive(held, pact.now);
582
+ if (current !== null) return {
583
+ ok: false,
584
+ code: current.sessionId === pact.sessionId ? "already-held" : "held-by-other"
585
+ };
586
+ const state = {
587
+ phase: "armed",
588
+ sessionId: pact.sessionId,
589
+ workspace: pact.workspace,
590
+ unit: pact.unit,
591
+ name: pact.name,
592
+ url: pact.url,
593
+ title: pact.title,
594
+ action: pact.action,
595
+ expect: pact.expect,
596
+ captureFile: pact.captureFile,
597
+ startedAt: pact.now,
598
+ t0: null,
599
+ t1: null,
600
+ at: pact.now
601
+ };
602
+ held = state;
603
+ return {
604
+ ok: true,
605
+ state
606
+ };
607
+ },
608
+ started(sessionId, at, now) {
609
+ const found = ownerOf(held, sessionId, now);
610
+ if (typeof found === "string") return {
611
+ ok: false,
612
+ code: found
613
+ };
614
+ if (found.phase === "recording") return {
615
+ ok: false,
616
+ code: "already-recording"
617
+ };
618
+ if (found.phase !== "armed") return {
619
+ ok: false,
620
+ code: "not-armed"
621
+ };
622
+ const state = {
623
+ ...found,
624
+ phase: "recording",
625
+ t0: at,
626
+ at: now
627
+ };
628
+ held = state;
629
+ return {
630
+ ok: true,
631
+ state
632
+ };
633
+ },
634
+ finished(sessionId, at, now) {
635
+ const found = ownerOf(held, sessionId, now);
636
+ if (typeof found === "string") return {
637
+ ok: false,
638
+ code: found
639
+ };
640
+ if (found.phase !== "recording") return {
641
+ ok: false,
642
+ code: "not-recording"
643
+ };
644
+ const state = {
645
+ ...found,
646
+ phase: "finished",
647
+ t1: at,
648
+ at: now
649
+ };
650
+ held = state;
651
+ return {
652
+ ok: true,
653
+ state
654
+ };
655
+ },
546
656
  stop(sessionId, now) {
547
657
  const found = ownerOf(held, sessionId, now);
548
658
  if (typeof found === "string") return {
@@ -596,6 +706,7 @@ async function runShell(shell, command, options = {}) {
596
706
  //#endregion
597
707
  //#region ../shared/dist/host/browser-read.js
598
708
  var BROWSER_SCREEN_SESSION = "workbench";
709
+ var BROWSER_RECORD_SESSION = "workbench-rec";
599
710
  var DEFAULT_TIMEOUT_MS$2 = 25e3;
600
711
  var DEFAULT_RETRY_MS = 1200;
601
712
  var DEFAULT_SETTLE_MS$1 = 900;
@@ -1235,7 +1346,7 @@ var TARGET_CODE = {
1235
1346
  function asRecord$7(value) {
1236
1347
  return typeof value === "object" && value !== null ? value : {};
1237
1348
  }
1238
- function stringsOf$4(value) {
1349
+ function stringsOf$5(value) {
1239
1350
  if (!Array.isArray(value)) return void 0;
1240
1351
  const items = value.filter((item) => typeof item === "string" && item.trim() !== "");
1241
1352
  return items.length === 0 ? void 0 : items;
@@ -1291,7 +1402,7 @@ function parseProvisionBody(body) {
1291
1402
  request: requestOf$1(wanted, value.trim())
1292
1403
  };
1293
1404
  const registry = typeof raw.registry === "string" && raw.registry.trim() !== "" ? raw.registry.trim() : void 0;
1294
- const approvedBuilds = stringsOf$4(raw.approvedBuilds);
1405
+ const approvedBuilds = stringsOf$5(raw.approvedBuilds);
1295
1406
  const requestId = typeof raw.requestId === "string" && raw.requestId.trim() !== "" ? raw.requestId.trim() : void 0;
1296
1407
  return {
1297
1408
  ok: true,
@@ -1796,6 +1907,15 @@ function registerDiyDebugAct(ctx, resource, log) {
1796
1907
  wrote: () => log?.bump(["diy-debug"])
1797
1908
  });
1798
1909
  }
1910
+ function registerDiyRecordAct(ctx, resource, log) {
1911
+ return registerPost(ctx, {
1912
+ prefix: `${API_PREFIX}/${DIY_RECORD_PATH}`,
1913
+ what: "工作台 DIY 录制交接动作",
1914
+ refusedCode: "action-refused",
1915
+ resources: [resource],
1916
+ wrote: () => log?.bump(["diy-record"])
1917
+ });
1918
+ }
1799
1919
  //#endregion
1800
1920
  //#region ../shared/dist/host/long-task.js
1801
1921
  var LIVE = /* @__PURE__ */ new Set(["running", "needs-login"]);
@@ -2013,7 +2133,7 @@ function textOf$5(value) {
2013
2133
  const trimmed = value.trim();
2014
2134
  return trimmed === "" ? void 0 : trimmed;
2015
2135
  }
2016
- function entryOf(value) {
2136
+ function entryOf$1(value) {
2017
2137
  if (typeof value !== "object" || value === null) return void 0;
2018
2138
  const box = value;
2019
2139
  const opencliHome = textOf$5(box["opencliHome"]);
@@ -2039,7 +2159,7 @@ function parsePluginLedger(raw) {
2039
2159
  if (!Array.isArray(list)) return [];
2040
2160
  const entries = [];
2041
2161
  for (const item of list) {
2042
- const entry = entryOf(item);
2162
+ const entry = entryOf$1(item);
2043
2163
  if (entry === void 0) continue;
2044
2164
  if (entries.some((known) => ledgerKeyOf(known) === ledgerKeyOf(entry))) continue;
2045
2165
  entries.push(entry);
@@ -2210,11 +2330,14 @@ function withApprovalNote(parameters, options = {}) {
2210
2330
  ...required.length === 0 ? {} : { required }
2211
2331
  };
2212
2332
  }
2333
+ function oneLine(text) {
2334
+ return text.replace(/\s+/g, " ").trim();
2335
+ }
2213
2336
  function approvalNoteOf(call) {
2214
2337
  const raw = call.arguments;
2215
2338
  const value = typeof raw === "object" && raw !== null ? raw[APPROVAL_NOTE_ARG] : void 0;
2216
2339
  if (typeof value !== "string") return void 0;
2217
- const line = value.replace(/\s+/g, " ").trim();
2340
+ const line = oneLine(value);
2218
2341
  if (line === "") return void 0;
2219
2342
  return line.length > 40 ? `${line.slice(0, 40)}…` : line;
2220
2343
  }
@@ -2226,11 +2349,14 @@ function compose(detail, verdict, note) {
2226
2349
  en: `${detail.en} — ${verdict.question.en}`,
2227
2350
  zh: `${detail.zh} —— ${verdict.question.zh}`
2228
2351
  };
2229
- if (note === void 0) return base;
2230
- return {
2352
+ const line = note === void 0 ? base : {
2231
2353
  en: `${base.en} — ${note}`,
2232
2354
  zh: `${base.zh} —— ${note}`
2233
2355
  };
2356
+ return {
2357
+ en: oneLine(line.en),
2358
+ zh: oneLine(line.zh)
2359
+ };
2234
2360
  }
2235
2361
  async function approvalFor(call, context = {}) {
2236
2362
  if (context.writes !== true) return void 0;
@@ -2264,7 +2390,7 @@ async function approvalFor(call, context = {}) {
2264
2390
  return {
2265
2391
  kind: "ask",
2266
2392
  reason: verdict.reason,
2267
- displayReason: compose(facts, verdict, approvalNoteOf(call))
2393
+ displayReason: compose(facts, verdict, context.declaredNote === true ? approvalNoteOf(call) : void 0)
2268
2394
  };
2269
2395
  }
2270
2396
  //#endregion
@@ -2489,7 +2615,7 @@ var CANCELLATIONS = [
2489
2615
  function asRecord$6(value) {
2490
2616
  return typeof value === "object" && value !== null ? value : void 0;
2491
2617
  }
2492
- function asText$4(value) {
2618
+ function asText$5(value) {
2493
2619
  return typeof value === "string" ? value : "";
2494
2620
  }
2495
2621
  function pick(allowed, value) {
@@ -2519,9 +2645,9 @@ function asPackageResult(raw) {
2519
2645
  const kind = pick(FAILURE_KINDS, record.kind);
2520
2646
  return {
2521
2647
  exitCode: typeof record.exitCode === "number" ? record.exitCode : -1,
2522
- output: asText$4(record.output),
2648
+ output: asText$5(record.output),
2523
2649
  truncated: record.truncated === true,
2524
- logPath: asText$4(record.logPath),
2650
+ logPath: asText$5(record.logPath),
2525
2651
  ...kind === void 0 ? {} : { kind }
2526
2652
  };
2527
2653
  }
@@ -2536,14 +2662,14 @@ function asChange(raw) {
2536
2662
  const pendingBuilds = asTextArray(record.pendingBuilds);
2537
2663
  const restartReason = pick(RESTART_REASONS, record.restartReason);
2538
2664
  const failedAt = pick(["registry", "spec-host"], record.failedAt);
2539
- const bundle = asText$4(record.bundle);
2540
- const errorCode = error === void 0 ? "" : asText$4(error.code);
2541
- const errorDetail = error === void 0 ? "" : asText$4(error.diagnostic);
2665
+ const bundle = asText$5(record.bundle);
2666
+ const errorCode = error === void 0 ? "" : asText$5(error.code);
2667
+ const errorDetail = error === void 0 ? "" : asText$5(error.diagnostic);
2542
2668
  return {
2543
2669
  application,
2544
2670
  stage,
2545
2671
  changed: record.changed === true,
2546
- target: asText$4(record.target),
2672
+ target: asText$5(record.target),
2547
2673
  ...bundle === "" ? {} : { bundle },
2548
2674
  ...errorCode === "" ? {} : { errorCode },
2549
2675
  ...errorDetail === "" ? {} : { errorDetail },
@@ -2556,14 +2682,14 @@ function asChange(raw) {
2556
2682
  function asInspection(raw) {
2557
2683
  const record = asRecord$6(raw);
2558
2684
  if (record === void 0) return null;
2559
- const status = asText$4(record.status);
2685
+ const status = asText$5(record.status);
2560
2686
  if (status === "refused") {
2561
2687
  const problem = pick(PROBLEMS, record.problem) ?? "unknown";
2562
2688
  const registries = Array.isArray(record.registries) ? record.registries.map(asRegistry).filter((item) => item !== void 0) : [];
2563
2689
  return {
2564
2690
  status: "refused",
2565
2691
  problem,
2566
- reason: asText$4(record.reason),
2692
+ reason: asText$5(record.reason),
2567
2693
  ...registries.length === 0 ? {} : { registries }
2568
2694
  };
2569
2695
  }
@@ -2571,10 +2697,10 @@ function asInspection(raw) {
2571
2697
  const kind = pick(SPEC_KINDS, record.kind);
2572
2698
  const registry = asRegistry(record.registry);
2573
2699
  if (kind === void 0 || registry === void 0) return null;
2574
- const name = asText$4(record.name);
2575
- const version = asText$4(record.version);
2576
- const description = asText$4(record.description);
2577
- const host = asText$4(record.host);
2700
+ const name = asText$5(record.name);
2701
+ const version = asText$5(record.version);
2702
+ const description = asText$5(record.description);
2703
+ const host = asText$5(record.host);
2578
2704
  return {
2579
2705
  status: "accepted",
2580
2706
  kind,
@@ -2626,7 +2752,7 @@ var PACKAGE_NAME$1 = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
2626
2752
  function asRecord$5(value) {
2627
2753
  return typeof value === "object" && value !== null ? value : void 0;
2628
2754
  }
2629
- function asText$3(value) {
2755
+ function asText$4(value) {
2630
2756
  return typeof value === "string" ? value : "";
2631
2757
  }
2632
2758
  function isPackageName(value) {
@@ -2637,7 +2763,7 @@ function profileDirOf(ctx) {
2637
2763
  const dir = typeof profile?.dir === "string" ? profile.dir.trim() : "";
2638
2764
  return dir === "" ? null : dir;
2639
2765
  }
2640
- function stringsOf$3(value) {
2766
+ function stringsOf$4(value) {
2641
2767
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
2642
2768
  }
2643
2769
  function profileManifestOf(profileDir) {
@@ -2651,7 +2777,7 @@ function profileManifestOf(profileDir) {
2651
2777
  function profileModules(profileDir) {
2652
2778
  const record = profileManifestOf(profileDir);
2653
2779
  if (record === void 0) return [];
2654
- const bundles = stringsOf$3(asRecord$5(asRecord$5(record.dsh)?.profile)?.bundles);
2780
+ const bundles = stringsOf$4(asRecord$5(asRecord$5(record.dsh)?.profile)?.bundles);
2655
2781
  const dependencies = asRecord$5(record.dependencies) ?? {};
2656
2782
  const names = bundles.length > 0 ? bundles : Object.keys(dependencies);
2657
2783
  const found = [];
@@ -2662,7 +2788,7 @@ function profileModules(profileDir) {
2662
2788
  unit: read.unit,
2663
2789
  version: read.version,
2664
2790
  ...read.registry === void 0 ? {} : { registry: read.registry },
2665
- local: isLocalDependencySpec(asText$3(dependencies[read.pkg]))
2791
+ local: isLocalDependencySpec(asText$4(dependencies[read.pkg]))
2666
2792
  });
2667
2793
  }
2668
2794
  return found;
@@ -2757,18 +2883,18 @@ function readInstalledModule(profileDir, pkg) {
2757
2883
  pkg
2758
2884
  };
2759
2885
  const declared = asRecord$5(asRecord$5(record.dsh)?.workbench);
2760
- const unit = declared === void 0 ? "" : asText$3(declared.unit);
2886
+ const unit = declared === void 0 ? "" : asText$4(declared.unit);
2761
2887
  if (unit === "") return {
2762
2888
  kind: "not-module",
2763
2889
  pkg
2764
2890
  };
2765
- const declaredRegistry = asText$3(asRecord$5(record.publishConfig)?.registry);
2891
+ const declaredRegistry = asText$4(asRecord$5(record.publishConfig)?.registry);
2766
2892
  const registry = declaredRegistry === "" ? registryFromLockfile(profileDir, pkg) : declaredRegistry;
2767
2893
  return {
2768
2894
  kind: "module",
2769
2895
  pkg,
2770
2896
  unit,
2771
- version: asText$3(record.version),
2897
+ version: asText$4(record.version),
2772
2898
  ...registry === "" ? {} : { registry }
2773
2899
  };
2774
2900
  }
@@ -2786,11 +2912,11 @@ function readProfilePackage(profileDir, pkg) {
2786
2912
  }
2787
2913
  const record = asRecord$5(manifest);
2788
2914
  if (record === void 0) return void 0;
2789
- const version = asText$3(record.version);
2915
+ const version = asText$4(record.version);
2790
2916
  if (version === "") return void 0;
2791
- const declaredRegistry = asText$3(asRecord$5(record.publishConfig)?.registry);
2917
+ const declaredRegistry = asText$4(asRecord$5(record.publishConfig)?.registry);
2792
2918
  const registry = declaredRegistry === "" ? registryFromLockfile(profileDir, pkg) : declaredRegistry;
2793
- const local = isLocalDependencySpec(asText$3((asRecord$5(profileManifestOf(profileDir)?.dependencies) ?? {})[pkg]));
2919
+ const local = isLocalDependencySpec(asText$4((asRecord$5(profileManifestOf(profileDir)?.dependencies) ?? {})[pkg]));
2794
2920
  return {
2795
2921
  pkg,
2796
2922
  version,
@@ -2799,9 +2925,40 @@ function readProfilePackage(profileDir, pkg) {
2799
2925
  };
2800
2926
  }
2801
2927
  //#endregion
2928
+ //#region src/shared/api/host/opencli-command.ts
2929
+ var OPENCLI_PATH_FALLBACK = "opencli";
2930
+ function isolatedOpencliPathOf(ctx) {
2931
+ const homePath = ctx.get("dshHomePath");
2932
+ if (typeof homePath !== "function") return null;
2933
+ try {
2934
+ return join(homePath("workbench/opencli/bin/opencli"));
2935
+ } catch {
2936
+ return null;
2937
+ }
2938
+ }
2939
+ function opencliCommandOf(ctx) {
2940
+ const isolated = isolatedOpencliPathOf(ctx);
2941
+ const fromEnv = process.env.DSH_WORKBENCH_OPENCLI;
2942
+ if (fromEnv !== void 0 && fromEnv !== "") return {
2943
+ command: fromEnv,
2944
+ source: "env",
2945
+ isolated
2946
+ };
2947
+ if (isolated !== null && existsSync(isolated)) return {
2948
+ command: isolated,
2949
+ source: "isolated",
2950
+ isolated
2951
+ };
2952
+ return {
2953
+ command: OPENCLI_PATH_FALLBACK,
2954
+ source: "path",
2955
+ isolated
2956
+ };
2957
+ }
2958
+ //#endregion
2802
2959
  //#region src/shared/api/host/profile-registries.ts
2803
2960
  var REGISTRY_MARKER = "# dsh-workbench 写的:这一行的包只在这个源上(dsh 卸载没有源参数,pnpm 得问得到)";
2804
- function scopeOf$1(pkg) {
2961
+ function scopeOf$2(pkg) {
2805
2962
  const slash = pkg.indexOf("/");
2806
2963
  return pkg.startsWith("@") && slash > 1 ? pkg.slice(0, slash) : null;
2807
2964
  }
@@ -2841,7 +2998,7 @@ function sourcesFromLockfile(text) {
2841
2998
  if (name === null) continue;
2842
2999
  const tarball = TARBALL.exec(line);
2843
3000
  if (tarball === null) continue;
2844
- const scope = scopeOf$1(name);
3001
+ const scope = scopeOf$2(name);
2845
3002
  const root = registryRootOf(name, tarball[1] ?? "");
2846
3003
  name = null;
2847
3004
  if (scope === null || root === null) continue;
@@ -2930,7 +3087,7 @@ function workshopPathOf(ctx) {
2930
3087
  function asRecord$4(value) {
2931
3088
  return typeof value === "object" && value !== null ? value : void 0;
2932
3089
  }
2933
- function asText$2(value) {
3090
+ function asText$3(value) {
2934
3091
  return typeof value === "string" ? value.trim() : "";
2935
3092
  }
2936
3093
  function workshopPackagesDir(ctx) {
@@ -2953,17 +3110,26 @@ function workshopModules(packagesDir, installed = []) {
2953
3110
  }
2954
3111
  const record = asRecord$4(manifest);
2955
3112
  const declared = record === void 0 ? void 0 : asRecord$4(asRecord$4(record.dsh)?.workbench);
2956
- const unit = declared === void 0 ? "" : asText$2(declared.unit);
3113
+ const unit = declared === void 0 ? "" : asText$3(declared.unit);
2957
3114
  if (unit === "") return [];
2958
3115
  return [{
2959
3116
  unit,
2960
- name: asText$2(record?.name),
2961
- description: asText$2(record?.description),
3117
+ name: asText$3(record?.name),
3118
+ description: asText$3(record?.description),
2962
3119
  dir,
2963
3120
  installed: already.has(unit)
2964
3121
  }];
2965
3122
  }).sort((left, right) => left.unit.localeCompare(right.unit));
2966
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
+ }
2967
3133
  function inWorkshopSession(ctx, workspace) {
2968
3134
  const root = workshopPathOf(ctx);
2969
3135
  return root !== null && workspace !== null && insideWorkshop(root, workspace);
@@ -2977,20 +3143,20 @@ function workshopSyncOf(workshopRoot) {
2977
3143
  }
2978
3144
  if (typeof parsed !== "object" || parsed === null) return void 0;
2979
3145
  const record = parsed;
2980
- const state = asText$2(record["state"]);
3146
+ const state = asText$3(record["state"]);
2981
3147
  if (state !== "ok" && state !== "syncing" && state !== "failed") return void 0;
2982
- const reason = asText$2(record["reason"]);
3148
+ const reason = asText$3(record["reason"]);
2983
3149
  return {
2984
3150
  state,
2985
- version: asText$2(record["version"]),
2986
- at: asText$2(record["at"]),
3151
+ version: asText$3(record["version"]),
3152
+ at: asText$3(record["at"]),
2987
3153
  ...reason === "" ? {} : { reason }
2988
3154
  };
2989
3155
  }
2990
3156
  function workshopToolchainOf(workshopRoot) {
2991
3157
  const versionOf = (name) => {
2992
3158
  try {
2993
- return asText$2(JSON.parse(readFileSync(join(workshopRoot, "node_modules", "@dsh-workbench", name, "package.json"), "utf8")).version);
3159
+ return asText$3(JSON.parse(readFileSync(join(workshopRoot, "node_modules", "@dsh-workbench", name, "package.json"), "utf8")).version);
2994
3160
  } catch {
2995
3161
  return "";
2996
3162
  }
@@ -3025,6 +3191,7 @@ var MODULES_UPDATE_WORKBENCH_TOOL = "workbench_modules_update_workbench";
3025
3191
  var DIY_LIST_TOOL = "workbench_diy_list";
3026
3192
  var DIY_STATUS_TOOL = "workbench_diy_status";
3027
3193
  var DIY_PANEL_TOOL = "workbench_diy_panel";
3194
+ var DIY_COMPONENTS_TOOL = "workbench_diy_components";
3028
3195
  var DIY_RECON_TOOL = "workbench_diy_recon";
3029
3196
  var DIY_SPEC_TOOL = "workbench_diy_spec";
3030
3197
  var DIY_PROBE_TOOL = "workbench_diy_probe";
@@ -3047,6 +3214,10 @@ var DIY_DIALOG_TOOL = "workbench_diy_dialog";
3047
3214
  var DIY_LOGS_WATCH_TOOL = "workbench_diy_logs_watch";
3048
3215
  var DIY_LOGS_READ_TOOL = "workbench_diy_logs_read";
3049
3216
  var DIY_LOGS_STOP_TOOL = "workbench_diy_logs_stop";
3217
+ var DIY_RECORD_PREPARE_TOOL = "workbench_diy_record_prepare";
3218
+ var DIY_RECORD_READ_TOOL = "workbench_diy_record_read";
3219
+ var DIY_RECORD_DETAIL_TOOL = "workbench_diy_record_detail";
3220
+ var DIY_RECORD_STOP_TOOL = "workbench_diy_record_stop";
3050
3221
  var DIY_DEBUG_START_TOOL = "workbench_diy_debug_start";
3051
3222
  var DIY_DEBUG_PAUSE_TOOL = "workbench_diy_debug_pause";
3052
3223
  var DIY_DEBUG_RESUME_TOOL = "workbench_diy_debug_resume";
@@ -3057,6 +3228,7 @@ var READ_TOOLS = [
3057
3228
  DIY_LIST_TOOL,
3058
3229
  DIY_STATUS_TOOL,
3059
3230
  DIY_PANEL_TOOL,
3231
+ DIY_COMPONENTS_TOOL,
3060
3232
  DIY_RECON_TOOL,
3061
3233
  DIY_SPEC_TOOL,
3062
3234
  DIY_PROBE_TOOL,
@@ -3076,7 +3248,10 @@ var READ_TOOLS = [
3076
3248
  DIY_DIALOG_TOOL,
3077
3249
  DIY_LOGS_WATCH_TOOL,
3078
3250
  DIY_LOGS_READ_TOOL,
3079
- DIY_LOGS_STOP_TOOL
3251
+ DIY_LOGS_STOP_TOOL,
3252
+ DIY_RECORD_READ_TOOL,
3253
+ DIY_RECORD_DETAIL_TOOL,
3254
+ DIY_RECORD_STOP_TOOL
3080
3255
  ];
3081
3256
  var WRITE_TOOLS = [
3082
3257
  PANELS_SHOW_TOOL,
@@ -3094,6 +3269,7 @@ var WRITE_TOOLS = [
3094
3269
  MODULES_UPDATE_WORKBENCH_TOOL,
3095
3270
  DIY_INSTALL_TOOL,
3096
3271
  DIY_SHOW_TOOL,
3272
+ DIY_RECORD_PREPARE_TOOL,
3097
3273
  DIY_DEBUG_START_TOOL,
3098
3274
  DIY_DEBUG_PAUSE_TOOL,
3099
3275
  DIY_DEBUG_RESUME_TOOL,
@@ -3103,6 +3279,7 @@ var DIY_TOOLS = [
3103
3279
  DIY_LIST_TOOL,
3104
3280
  DIY_STATUS_TOOL,
3105
3281
  DIY_PANEL_TOOL,
3282
+ DIY_COMPONENTS_TOOL,
3106
3283
  DIY_RECON_TOOL,
3107
3284
  DIY_SPEC_TOOL,
3108
3285
  DIY_PROBE_TOOL,
@@ -3126,6 +3303,12 @@ var DIY_TOOLS = [
3126
3303
  DIY_LOGS_READ_TOOL,
3127
3304
  DIY_LOGS_STOP_TOOL
3128
3305
  ];
3306
+ var DIY_RECORD_TOOLS = [
3307
+ DIY_RECORD_PREPARE_TOOL,
3308
+ DIY_RECORD_READ_TOOL,
3309
+ DIY_RECORD_DETAIL_TOOL,
3310
+ DIY_RECORD_STOP_TOOL
3311
+ ];
3129
3312
  var DIY_DEBUG_TOOLS = [
3130
3313
  DIY_DEBUG_START_TOOL,
3131
3314
  DIY_DEBUG_PAUSE_TOOL,
@@ -3229,9 +3412,16 @@ var panelsApproval = (call) => {
3229
3412
  const starting = call.name === DIY_DEBUG_START_TOOL;
3230
3413
  const unit = argsOf$2(call)["unit"];
3231
3414
  const target = typeof unit === "string" && unit !== "" ? ` [${unit}]` : "";
3232
- 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. Allow?"));
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?"));
3233
3416
  }
3234
3417
  if (call.name === "workbench_diy_debug_pause" || call.name === "workbench_diy_debug_stop") return { kind: "allow" };
3418
+ if (call.name === "workbench_diy_record_prepare") {
3419
+ const raw = argsOf$2(call);
3420
+ const asked = typeof raw["url"] === "string" ? raw["url"].replace(/\s+/g, " ").trim() : "";
3421
+ const act = typeof raw["doing"] === "string" ? raw["doing"].replace(/\s+/g, " ").trim() : "";
3422
+ const title = pair("申请录制调试", "Request recording a debug session");
3423
+ return askOf(`${DIY_RECORD_PREPARE_TOOL} opens a browser window in front on ${asked} to record one real write` + (act === "" ? "" : ` (${act})`), title, title);
3424
+ }
3235
3425
  if (call.name !== void 0 && DIY_TOOLS.includes(call.name)) return { kind: "allow" };
3236
3426
  const raw = argsOf$2(call);
3237
3427
  const text = (key) => {
@@ -3439,35 +3629,305 @@ function withMoved(board, id, index, visible) {
3439
3629
  };
3440
3630
  }
3441
3631
  //#endregion
3442
- //#region src/shared/api/host/opencli-command.ts
3443
- var OPENCLI_PATH_FALLBACK = "opencli";
3444
- function isolatedOpencliPathOf(ctx) {
3445
- const homePath = ctx.get("dshHomePath");
3446
- if (typeof homePath !== "function") return null;
3632
+ //#region src/shared/api/host/diy-components.ts
3633
+ var SURFACE_TIMEOUT_MS = 3e4;
3634
+ var PRUNE_CHARS = 8192;
3635
+ var PRIMITIVES_PKG = "@deepseek-ai/dsh-client-ui-primitives";
3636
+ var MANIFEST_TAIL = "//# sourceMappingURL=";
3637
+ function scopeOf$1(raw) {
3638
+ if (raw === "primitives" || raw === "kit") return raw;
3639
+ return "all";
3640
+ }
3641
+ function wantsPrimitives(scope) {
3642
+ return scope !== "kit";
3643
+ }
3644
+ function wantsKit(scope) {
3645
+ return scope !== "primitives";
3646
+ }
3647
+ function toolchainBinOf(root) {
3648
+ const bin = join(root, "node_modules", ".bin", "dsh-wb-module");
3649
+ if (existsSync(bin)) return bin;
3650
+ const script = join(root, "node_modules", "@dsh-workbench", "module-tools", "bin", "dsh-wb-module.mjs");
3651
+ return existsSync(script) ? script : null;
3652
+ }
3653
+ function queryCommandOf(bin, only) {
3654
+ return `${quoteShellArg(bin)} check --only ${only} --json`;
3655
+ }
3656
+ async function runSurfaceQuery(shell, bin, only, root) {
3657
+ const result = await runShell(shell, queryCommandOf(bin, only), {
3658
+ workdir: root,
3659
+ timeoutMs: SURFACE_TIMEOUT_MS
3660
+ });
3661
+ const parsed = jsonOf$1(result.stdout);
3662
+ if (parsed !== null) return withoutProblems(parsed);
3663
+ if (result.exitCode !== 0) {
3664
+ const tail = `${result.stderr}\n${result.stdout}`.trim().split("\n").slice(-3).join(" | ");
3665
+ throw new Error(`工坊工具链的 \`check --only ${only} --json\` 没跑通(exit ${String(result.exitCode)})—— 这不是工具链坏了,是工坊里有模块过不了判据,所以连清单也没打出来${tail === "" ? "" : `:${tail}`}`);
3666
+ }
3667
+ throw new Error(`工坊工具链的 \`check --only ${only} --json\` 没回 JSON:${result.stdout.trim().split("\n").slice(-2).join(" | ")}`);
3668
+ }
3669
+ function jsonOf$1(stdout) {
3447
3670
  try {
3448
- return join(homePath("workbench/opencli/bin/opencli"));
3671
+ const parsed = JSON.parse(stdout);
3672
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
3449
3673
  } catch {
3450
3674
  return null;
3451
3675
  }
3452
3676
  }
3453
- function opencliCommandOf(ctx) {
3454
- const isolated = isolatedOpencliPathOf(ctx);
3455
- const fromEnv = process.env.DSH_WORKBENCH_OPENCLI;
3456
- if (fromEnv !== void 0 && fromEnv !== "") return {
3457
- command: fromEnv,
3458
- source: "env",
3459
- isolated
3460
- };
3461
- if (isolated !== null && existsSync(isolated)) return {
3462
- command: isolated,
3463
- source: "isolated",
3464
- isolated
3465
- };
3466
- return {
3467
- command: OPENCLI_PATH_FALLBACK,
3468
- source: "path",
3469
- isolated
3470
- };
3677
+ function withoutProblems(payload) {
3678
+ const rest = {};
3679
+ for (const [key, value] of Object.entries(payload)) if (key !== "problems") rest[key] = value;
3680
+ return rest;
3681
+ }
3682
+ function recordOf$1(value) {
3683
+ return typeof value === "object" && value !== null ? value : {};
3684
+ }
3685
+ function asText$2(value) {
3686
+ return typeof value === "string" ? value : "";
3687
+ }
3688
+ function stringsOf$3(value) {
3689
+ return Array.isArray(value) ? value.filter((one) => typeof one === "string") : [];
3690
+ }
3691
+ function filesOf$1(payload) {
3692
+ const files = recordOf$1(payload?.["files"]);
3693
+ const out = {};
3694
+ for (const [name, file] of Object.entries(files)) if (typeof file === "string") out[name] = file;
3695
+ return out;
3696
+ }
3697
+ function rootsOf(payload) {
3698
+ const roots = recordOf$1(payload?.["roots"]);
3699
+ const out = {};
3700
+ for (const [key, dir] of Object.entries(roots)) if (typeof dir === "string") out[key] = dir;
3701
+ return out;
3702
+ }
3703
+ function installedVersionOf(payload) {
3704
+ const version = recordOf$1(payload?.["installed"])["version"];
3705
+ return typeof version === "string" ? version : null;
3706
+ }
3707
+ function distance(left, right) {
3708
+ let previous = Array.from({ length: right.length + 1 }, (_unused, index) => index);
3709
+ for (let row = 1; row <= left.length; row += 1) {
3710
+ const current = [row];
3711
+ for (let column = 1; column <= right.length; column += 1) {
3712
+ const same = left[row - 1] === right[column - 1] ? 0 : 1;
3713
+ current.push(Math.min((previous[column] ?? 0) + 1, (current[column - 1] ?? 0) + 1, (previous[column - 1] ?? 0) + same));
3714
+ }
3715
+ previous = current;
3716
+ }
3717
+ return previous[right.length] ?? 0;
3718
+ }
3719
+ function nearNamesOf(name, names, limit = 8) {
3720
+ const wanted = name.toLowerCase();
3721
+ return names.flatMap((one) => {
3722
+ const lower = one.toLowerCase();
3723
+ if (lower.length < 3) return lower === wanted ? [{
3724
+ one,
3725
+ score: 0
3726
+ }] : [];
3727
+ if (lower.includes(wanted)) return [{
3728
+ one,
3729
+ score: 0
3730
+ }];
3731
+ if (wanted.includes(lower)) return [{
3732
+ one,
3733
+ score: 1
3734
+ }];
3735
+ const typos = distance(lower, wanted);
3736
+ return typos <= 2 ? [{
3737
+ one,
3738
+ score: typos + 1
3739
+ }] : [];
3740
+ }).sort((left, right) => left.score - right.score || left.one.length - right.one.length || left.one.localeCompare(right.one)).slice(0, limit).map((one) => one.one);
3741
+ }
3742
+ function findFileOf(name, query) {
3743
+ const primitives = filesOf$1(query.primitives)[name];
3744
+ if (primitives !== void 0) return {
3745
+ file: primitives,
3746
+ from: "primitives"
3747
+ };
3748
+ const kit = filesOf$1(query.kit)[name];
3749
+ return kit === void 0 ? null : {
3750
+ file: kit,
3751
+ from: "kit"
3752
+ };
3753
+ }
3754
+ function excerptOf(text, maxLines = 60) {
3755
+ const cutAt = text.indexOf(MANIFEST_TAIL);
3756
+ const body = (cutAt === -1 ? text : text.slice(0, cutAt)).trimEnd();
3757
+ const lines = body.split("\n");
3758
+ return lines.length <= maxLines ? body : `${lines.slice(0, maxLines).join("\n")}\n…(这份文件还有 ${String(lines.length - maxLines)} 行)`;
3759
+ }
3760
+ function excerptAt(file, maxLines = 60) {
3761
+ try {
3762
+ return excerptOf(readFileSync(file, "utf8"), maxLines);
3763
+ } catch {
3764
+ return "(打不开那份类型文件 —— 照上面的路径自己看一眼)";
3765
+ }
3766
+ }
3767
+ function skillDirOf(from) {
3768
+ let dir = from;
3769
+ for (let hop = 0; hop < 6; hop += 1) {
3770
+ const candidate = join(dir, "skills", "workbench-module-diy");
3771
+ if (existsSync(join(candidate, "HOOKS.md"))) return candidate;
3772
+ const parent = dirname(dir);
3773
+ if (parent === dir) break;
3774
+ dir = parent;
3775
+ }
3776
+ return null;
3777
+ }
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
+ function truncated(names, all) {
3805
+ if (all || names.length <= 12) return names.join(", ");
3806
+ return `${names.slice(0, 12).join(", ")} …+${String(names.length - 12)}`;
3807
+ }
3808
+ function byLocationOf(payload) {
3809
+ const roots = Object.values(rootsOf(payload)).sort((left, right) => right.length - left.length);
3810
+ const groups = /* @__PURE__ */ new Map();
3811
+ for (const [name, file] of Object.entries(filesOf$1(payload))) {
3812
+ const root = roots.find((one) => file === one || file.startsWith(`${one}/`));
3813
+ const where = (root === void 0 ? file.split("/").slice(-2).join("/") : file.slice(root.length + 1)).split("/").slice(0, -1).join("/") || ".";
3814
+ const bucket = groups.get(where);
3815
+ if (bucket === void 0) groups.set(where, [name]);
3816
+ else bucket.push(name);
3817
+ }
3818
+ return [...groups.entries()].map(([where, names]) => ({
3819
+ where,
3820
+ names: [...names].sort()
3821
+ })).sort((left, right) => right.names.length - left.names.length || left.where.localeCompare(right.where));
3822
+ }
3823
+ function isComponents(where) {
3824
+ return where.split("/").at(-1) === "components";
3825
+ }
3826
+ function renderSurfaceText(query, options) {
3827
+ const lines = [];
3828
+ const kitNames = Object.keys(filesOf$1(query.kit)).sort();
3829
+ if (query.kit !== void 0) {
3830
+ const groups = byLocationOf(query.kit);
3831
+ if (kitNames.length === 0 && groups.length === 0) {
3832
+ 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)}`);
3834
+ } 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
+ }
3844
+ }
3845
+ }
3846
+ if (query.primitives !== void 0) {
3847
+ 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);
3852
+ const covered = (Array.isArray(query.primitives["kitCovered"]) ? query.primitives["kitCovered"] : []).map((one) => recordOf$1(one)["name"]).filter((one) => typeof one === "string");
3853
+ if (version === null) {
3854
+ const want = asText$2(recordOf$1(recordOf$1(query.primitives["snippet"])["devDependency"])["version"]);
3855
+ 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(" · ")}`}`);
3857
+ if (only.length > 0 && version !== null) lines.push("(只有原语有的,用之前先在 dsh.client.external 里声明)");
3858
+ if (covered.length > 0) lines.push(`kit 里已有:${covered.join(", ")}`);
3859
+ }
3860
+ const roots = [...Object.entries(rootsOf(query.kit)).map(([key, dir]) => `${key}=${dir}`), ...Object.entries(rootsOf(query.primitives)).map(([key, dir]) => `${key}=${dir}`)];
3861
+ if (roots.length > 0) lines.push(`类型面:${roots.join(" · ")}`);
3862
+ lines.push(faceStateOf(query, "primitives") === "unreadable" ? "name=<件> 看它怎么用 · all=true 出全表(原语装过才能问原语那类)" : "name=<件> 看它怎么用 · all=true 出全表");
3863
+ const text = lines.join("\n");
3864
+ if (text.length <= 8192) return text;
3865
+ 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。`,
3868
+ "",
3869
+ text
3870
+ ].join("\n");
3871
+ }
3872
+ function listedNamesOf(query) {
3873
+ const out = /* @__PURE__ */ new Set([...Object.keys(filesOf$1(query.primitives)), ...Object.keys(filesOf$1(query.kit))]);
3874
+ for (const payload of [query.primitives, query.kit]) {
3875
+ if (payload === void 0) continue;
3876
+ for (const value of Object.values(recordOf$1(payload["buckets"]))) for (const one of stringsOf$3(value)) out.add(one);
3877
+ for (const value of Object.values(recordOf$1(payload["onlyPrimitives"]))) for (const one of stringsOf$3(value)) out.add(one);
3878
+ for (const one of Array.isArray(payload["kitCovered"]) ? payload["kitCovered"] : []) {
3879
+ const covered = asText$2(recordOf$1(one)["name"]);
3880
+ if (covered !== "") out.add(covered);
3881
+ }
3882
+ }
3883
+ return [...out].sort();
3884
+ }
3885
+ function faceStateOf(query, face) {
3886
+ const payload = face === "kit" ? query.kit : query.primitives;
3887
+ if (payload === void 0) return "not-queried";
3888
+ if (face === "primitives") {
3889
+ const named = Object.keys(filesOf$1(payload)).length > 0 || Object.keys(recordOf$1(payload["onlyPrimitives"])).length > 0;
3890
+ return installedVersionOf(payload) === null || !named ? "unreadable" : "ok";
3891
+ }
3892
+ return Object.keys(filesOf$1(payload)).length > 0 || Object.keys(rootsOf(payload)).length > 0 ? "ok" : "unreadable";
3893
+ }
3894
+ function installSnippetOf(query) {
3895
+ 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"]);
3898
+ return `devDependencies 加 "${name}": "${version === "" ? "<外壳钉的那版>" : version}" 再 pnpm install`;
3899
+ }
3900
+ function renderNameText(name, found, query) {
3901
+ if (found === null) {
3902
+ const every = listedNamesOf(query);
3903
+ const listed = every.includes(name);
3904
+ const near = nearNamesOf(name, every).filter((one) => one !== name);
3905
+ const kit = faceStateOf(query, "kit");
3906
+ const primitives = faceStateOf(query, "primitives");
3907
+ const said = [kit === "ok" ? "kit 里没有" : "", primitives === "ok" ? "原语里也没有" : ""].filter((one) => one !== "");
3908
+ const blind = [kit === "ok" ? "" : `kit 那一面${kit === "not-queried" ? "没查" : "读不到"}`, primitives === "ok" ? "" : `原语那一面${primitives === "not-queried" ? "没查" : "读不到(没装)"}`].filter((one) => one !== "");
3909
+ const how = primitives === "unreadable" ? `${installSnippetOf(query)},才敢说它在不在。` : kit === "unreadable" ? "把这份外壳装上(让 client.js 读得到)再问。" : "不给 scope 就都查。";
3910
+ const lines = [...listed ? [`「${name}」在清单里(${String(every.length)} 个名字里有它),但扫不到它的类型文件 —— 类型面读不到,或这代外壳的声明形状变了。`] : kit === "ok" && primitives === "ok" ? [`没有「${name}」这个东西(kit 与原语里都没有)。`] : said.length > 0 ? [`「${name}」${said.join(",")}。`, ...blind.length === 0 ? [] : [`${blind.join("、")} —— ${how}`]] : [`没法确认「${name}」在不在:${blind.join("、")} —— ${how}`]];
3911
+ if (near.length > 0) lines.push(`近似的名字:${near.join(", ")}`);
3912
+ return lines.join("\n");
3913
+ }
3914
+ const body = excerptAt(found.file);
3915
+ if (body.includes("/**")) return [
3916
+ `${name} → ${found.file}(${found.from === "kit" ? "kit" : "原语"})`,
3917
+ "",
3918
+ body,
3919
+ "",
3920
+ found.from === "kit" ? "(上面就是它的声明;@param 写着默认值。kit 件最小写法:KIT.md)" : "(上面就是它的声明;@param 写着默认值。原语件要先在 dsh.client.external 里声明它)"
3921
+ ].join("\n");
3922
+ const skillDir = skillDirOf(dirname(fileURLToPath(import.meta.url)));
3923
+ const doc = docSectionOf(name, skillDir);
3924
+ return [
3925
+ `${name} → ${found.file}(${found.from === "kit" ? "kit" : "原语"})`,
3926
+ "",
3927
+ body,
3928
+ "",
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}`
3930
+ ].join("\n");
3471
3931
  }
3472
3932
  var OUTPUT$2 = {
3473
3933
  schema: {
@@ -3584,6 +4044,74 @@ function panelEntriesOf(panels, workspace) {
3584
4044
  }
3585
4045
  }
3586
4046
  //#endregion
4047
+ //#region src/modules/panels/model/diy-components.ts
4048
+ function componentsTool(ctx) {
4049
+ return {
4050
+ name: DIY_COMPONENTS_TOOL,
4051
+ description: "工坊里能用什么:dsh 原语与外壳 kit 的清单、每件的类型文件在哪(给 name 连 JSDoc 与 prop 一起回)。",
4052
+ parameters: {
4053
+ type: "object",
4054
+ properties: {
4055
+ name: {
4056
+ type: "string",
4057
+ description: "要看的那件(例 Card;不填就给清单)。"
4058
+ },
4059
+ scope: {
4060
+ type: "string",
4061
+ enum: [
4062
+ "all",
4063
+ "primitives",
4064
+ "kit"
4065
+ ],
4066
+ description: "只看某一面(默认 all)。"
4067
+ },
4068
+ all: {
4069
+ type: "boolean",
4070
+ description: "列全表(默认每组前 12 个)。"
4071
+ }
4072
+ },
4073
+ additionalProperties: false
4074
+ },
4075
+ output: OUTPUT$2,
4076
+ async execute(raw, exec) {
4077
+ const { packagesDir } = workshopOf(ctx, exec);
4078
+ 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
+ const args = asRecord$3(raw);
4084
+ const scope = scopeOf$1(asText$1(args["scope"]));
4085
+ const name = asText$1(args["name"]);
4086
+ 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)]);
4088
+ const query = {
4089
+ ...primitives === void 0 ? {} : { primitives },
4090
+ ...kit === void 0 ? {} : { kit }
4091
+ };
4092
+ const found = name === "" ? null : findFileOf(name, query);
4093
+ const text = name === "" ? renderSurfaceText(query, {
4094
+ scope,
4095
+ all
4096
+ }) : renderNameText(name, found, query);
4097
+ return plainJson({
4098
+ workshop: root,
4099
+ scope,
4100
+ name,
4101
+ all,
4102
+ file: found?.file ?? null,
4103
+ from: found?.from ?? null,
4104
+ primitives: primitives ?? null,
4105
+ kit: kit ?? null,
4106
+ text
4107
+ });
4108
+ }
4109
+ };
4110
+ }
4111
+ function registerComponentsTool(ctx, tools) {
4112
+ return [tools.register(componentsTool(ctx))];
4113
+ }
4114
+ //#endregion
3587
4115
  //#region src/modules/panels/model/diy-scripts.ts
3588
4116
  var PAGE_PROBE = `(async () => {
3589
4117
  const answer = await fetch('/workbench/api/health').then((one) => one.json()).catch(() => null);
@@ -3842,7 +4370,7 @@ function renderDebugResume(state) {
3842
4370
  function renderDebugStop(state) {
3843
4371
  return `收工了:${state.unit} 的调试态清掉了(占用解除)。`;
3844
4372
  }
3845
- function moveOrThrow(move, held) {
4373
+ function moveOrThrow$1(move, held) {
3846
4374
  if (move.ok) return move.state;
3847
4375
  throw new Error(debugRefusalText(move.code, held ?? void 0));
3848
4376
  }
@@ -3850,7 +4378,7 @@ function registerDebugTools(ctx, tools, store) {
3850
4378
  const startTool = {
3851
4379
  name: DIY_DEBUG_START_TOOL,
3852
4380
  description: "开始调试:占用用户那一屏(要点头一次)。没有它,看 / 动 / 网络那一族一律当场拒。",
3853
- parameters: withApprovalNote({
4381
+ parameters: {
3854
4382
  type: "object",
3855
4383
  properties: { unit: {
3856
4384
  type: "string",
@@ -3858,7 +4386,7 @@ function registerDebugTools(ctx, tools, store) {
3858
4386
  } },
3859
4387
  required: ["unit"],
3860
4388
  additionalProperties: false
3861
- }, { required: true }),
4389
+ },
3862
4390
  output: OUTPUT$2,
3863
4391
  async execute(raw, exec) {
3864
4392
  const { packagesDir, workspace } = workshopOf(ctx, exec);
@@ -3872,7 +4400,7 @@ function registerDebugTools(ctx, tools, store) {
3872
4400
  requireBoard: false,
3873
4401
  requirePact: false
3874
4402
  });
3875
- const state = moveOrThrow(store.begin({
4403
+ const state = moveOrThrow$1(store.begin({
3876
4404
  sessionId: sessionIdOf(exec) ?? "",
3877
4405
  workspace: workspace ?? "",
3878
4406
  board: page.boardPath,
@@ -3898,7 +4426,7 @@ function registerDebugTools(ctx, tools, store) {
3898
4426
  async execute(_raw, exec) {
3899
4427
  const { packagesDir } = workshopOf(ctx, exec);
3900
4428
  const held = store.live(Date.now());
3901
- const state = moveOrThrow(store.pause(sessionIdOf(exec) ?? "", Date.now()), held);
4429
+ const state = moveOrThrow$1(store.pause(sessionIdOf(exec) ?? "", Date.now()), held);
3902
4430
  return plainJson({
3903
4431
  workshop: packagesDir,
3904
4432
  unit: state.unit,
@@ -3911,7 +4439,7 @@ function registerDebugTools(ctx, tools, store) {
3911
4439
  const resumeTool = {
3912
4440
  name: DIY_DEBUG_RESUME_TOOL,
3913
4441
  description: "继续调试(要点头一次):请用户回到「工作台DIY」那一屏。",
3914
- parameters: withApprovalNote(EMPTY, { required: true }),
4442
+ parameters: EMPTY,
3915
4443
  output: OUTPUT$2,
3916
4444
  async execute(_raw, exec) {
3917
4445
  const { packagesDir } = workshopOf(ctx, exec);
@@ -3921,7 +4449,7 @@ function registerDebugTools(ctx, tools, store) {
3921
4449
  requirePact: false,
3922
4450
  requireUnit: false
3923
4451
  });
3924
- const state = moveOrThrow(store.resume(sessionIdOf(exec) ?? "", Date.now()), held);
4452
+ const state = moveOrThrow$1(store.resume(sessionIdOf(exec) ?? "", Date.now()), held);
3925
4453
  return plainJson({
3926
4454
  workshop: packagesDir,
3927
4455
  unit: state.unit,
@@ -3940,7 +4468,7 @@ function registerDebugTools(ctx, tools, store) {
3940
4468
  async execute(_raw, exec) {
3941
4469
  const { packagesDir } = workshopOf(ctx, exec);
3942
4470
  const held = store.live(Date.now());
3943
- const state = moveOrThrow(store.stop(sessionIdOf(exec) ?? "", Date.now()), held);
4471
+ const state = moveOrThrow$1(store.stop(sessionIdOf(exec) ?? "", Date.now()), held);
3944
4472
  await store.settle?.();
3945
4473
  return plainJson({
3946
4474
  workshop: packagesDir,
@@ -3990,6 +4518,7 @@ async function stopNetworkExact(shell, file, timeoutMs = SWEEP_TIMEOUT_MS) {
3990
4518
  return (await runShell(shell, [
3991
4519
  `p="$(cat ${pidFile} 2>/dev/null)"`,
3992
4520
  `r="$(cat ${reaperFile} 2>/dev/null)"`,
4521
+ `[ -n "$p" ] && pkill -P "$p" 2>/dev/null`,
3993
4522
  `[ -n "$p" ] && kill "$p" 2>/dev/null`,
3994
4523
  `[ -n "$r" ] && pkill -P "$r" 2>/dev/null`,
3995
4524
  `[ -n "$r" ] && kill "$r" 2>/dev/null`,
@@ -5772,7 +6301,7 @@ function registerDiyTools(ctx, market, panels, debug) {
5772
6301
  const showTool = {
5773
6302
  name: DIY_SHOW_TOOL,
5774
6303
  description: "只上板(不装):把一个已经装好的工坊模块显示在当前项目的面板上。",
5775
- parameters: withApprovalNote({
6304
+ parameters: {
5776
6305
  type: "object",
5777
6306
  properties: { unit: {
5778
6307
  type: "string",
@@ -5780,7 +6309,7 @@ function registerDiyTools(ctx, market, panels, debug) {
5780
6309
  } },
5781
6310
  required: ["unit"],
5782
6311
  additionalProperties: false
5783
- }, { required: true }),
6312
+ },
5784
6313
  output: OUTPUT$2,
5785
6314
  async execute(raw, exec) {
5786
6315
  const { packagesDir, workspace } = workshopOf(ctx, exec);
@@ -5800,7 +6329,7 @@ function registerDiyTools(ctx, market, panels, debug) {
5800
6329
  const installTool = {
5801
6330
  name: DIY_INSTALL_TOOL,
5802
6331
  description: "把工坊里造好的模块装上并上到当前项目的面板。这是持久变更,一个模块只装一次:之后改源码只需 dsh-wb-module build(宿主侧秒级热换)。",
5803
- parameters: withApprovalNote({
6332
+ parameters: {
5804
6333
  type: "object",
5805
6334
  properties: { unit: {
5806
6335
  type: "string",
@@ -5808,121 +6337,1740 @@ function registerDiyTools(ctx, market, panels, debug) {
5808
6337
  } },
5809
6338
  required: ["unit"],
5810
6339
  additionalProperties: false
5811
- }, { required: true }),
6340
+ },
6341
+ output: OUTPUT$2,
6342
+ async execute(raw, exec) {
6343
+ const { packagesDir, workspace } = workshopOf(ctx, exec);
6344
+ const found = unitOf(packagesDir, raw, "workbench_diy_install");
6345
+ if (found.installed) {
6346
+ const panel = showOrReason(panels, found.unit, workspace);
6347
+ const text = ["它已经装过了(就是工坊里这一份)—— **不要再装**:改源码 + dsh-wb-module build(宿主侧与 client.js 都秒级热换 —— dsh-client-hmr 盯着每个 bundle 的 lib/client.js,重建了就换进已打开的页面)。", panel.ok ? `面板上已经打开它了(${workspace})—— 验收就走这一张卡(你自己走一遍)` : `面板上还没打开:${panel.reason}。调 workbench_diy_show(unit=${found.unit})`].join("\n");
6348
+ return plainJson({
6349
+ workshop: packagesDir,
6350
+ unit: found.unit,
6351
+ path: found.dir,
6352
+ alreadyInstalled: true,
6353
+ panel,
6354
+ text
6355
+ });
6356
+ }
6357
+ const outcome = await market.installBundle(found.dir);
6358
+ const verdict = verdictOf$1(outcome.change);
6359
+ const restartRequired = outcome.change.application === "restart-required";
6360
+ const panel = outcome.change.application === "applied" ? showOrReason(panels, found.unit, workspace) : {
6361
+ ok: false,
6362
+ reason: `安装结果是 ${outcome.change.application},还没生效`
6363
+ };
6364
+ const panelText = panel.ok ? `面板上已经打开它了(${workspace})—— 验收就走这一张卡(你自己走一遍)` : outcome.change.application === "applied" ? `面板上还没打开:${panel.reason}。调 workbench_diy_show(unit=${found.unit})试试。` : restartRequired ? `面板上还没打开(${panel.reason}):模块要**重启一次工作台**才注册进面板 —— 把这句话转给用户,重启后再调 workbench_diy_show(unit=${found.unit})。` : `面板上还没打开:${panel.reason}。先把上面那句"没装成"处理掉,再谈上板。`;
6365
+ const text = [
6366
+ `${verdict}:${found.unit}(${found.name})`,
6367
+ panelText,
6368
+ "之后只改源码 + dsh-wb-module build(宿主侧与 client.js 都秒级热换 —— dsh-client-hmr 盯着每个 bundle 的 lib/client.js,重建了就换进已打开的页面):不要再装一遍。"
6369
+ ].join("\n");
6370
+ return plainJson({
6371
+ workshop: packagesDir,
6372
+ unit: found.unit,
6373
+ path: found.dir,
6374
+ install: outcome.change,
6375
+ module: outcome.module,
6376
+ panel,
6377
+ text
6378
+ });
6379
+ }
6380
+ };
6381
+ return [
6382
+ tools.register(listTool),
6383
+ tools.register(statusTool),
6384
+ tools.register(panelTool),
6385
+ ...registerComponentsTool(ctx, tools),
6386
+ tools.register(reconTool),
6387
+ ...registerSpecTool(ctx, tools),
6388
+ tools.register(probeTool),
6389
+ ...registerLogTools(ctx, tools),
6390
+ ...registerBrowserTools(ctx, tools),
6391
+ ...registerDebugTools(ctx, tools, debug),
6392
+ tools.register(showTool),
6393
+ tools.register(installTool)
6394
+ ];
6395
+ }
6396
+ //#endregion
6397
+ //#region src/modules/panels/model/diy-observers.ts
6398
+ async function stopObservers(ctx, session = BROWSER_SCREEN_SESSION) {
6399
+ const packagesDir = workshopPackagesDir(ctx);
6400
+ const shell = ctx.get("shell");
6401
+ if (packagesDir === null || shell === void 0) return {
6402
+ net: false,
6403
+ logs: false
6404
+ };
6405
+ const file = join(dirname(packagesDir), "shots", `net-${session}.jsonl`);
6406
+ const command = opencliCommandOf(ctx).command;
6407
+ const [net, step] = await Promise.all([stopNetworkExact(shell, file), actBrowser(shell, command, session, ["eval", LOGS_STOP_SCRIPT], SWEEP_TIMEOUT_MS)]);
6408
+ return {
6409
+ net,
6410
+ logs: typeof step.payload?.["unhooked"] === "number"
6411
+ };
6412
+ }
6413
+ function withObserverCleanup(store, cleanup, logger) {
6414
+ let pending = Promise.resolve();
6415
+ let epoch = 0;
6416
+ const sweep = async (at) => {
6417
+ if (epoch !== at) {
6418
+ logger?.warn?.("收工那一拍:已经有新的调试约定开始了 —— 跳过收观察(新那一场自己收)");
6419
+ return;
6420
+ }
6421
+ try {
6422
+ const out = await cleanup();
6423
+ if (!out.net || !out.logs) logger?.warn?.(`收工那一拍没把观察收干净(网络 ${String(out.net)} / 日志 ${String(out.logs)})—— 网络那条最多 15 分钟自己收工,日志钩子随页面刷新消失`);
6424
+ } catch (error) {
6425
+ logger?.warn?.(`收工那一拍收观察抛了:${error instanceof Error ? error.message : String(error)}`);
6426
+ }
6427
+ };
6428
+ return {
6429
+ live: (now) => store.live(now),
6430
+ begin(pact) {
6431
+ epoch += 1;
6432
+ return store.begin(pact);
6433
+ },
6434
+ pause: (sessionId, now) => store.pause(sessionId, now),
6435
+ resume: (sessionId, now) => store.resume(sessionId, now),
6436
+ stop(sessionId, now) {
6437
+ const move = store.stop(sessionId, now);
6438
+ if (move.ok) {
6439
+ const at = epoch;
6440
+ pending = pending.then(() => sweep(at));
6441
+ }
6442
+ return move;
6443
+ },
6444
+ settle: () => pending
6445
+ };
6446
+ }
6447
+ //#endregion
6448
+ //#region src/modules/panels/model/diy-debug-routes.ts
6449
+ function diyDebugRoutes(store) {
6450
+ return { "panels/diy-debug": () => ({
6451
+ ok: true,
6452
+ state: store.live(Date.now())
6453
+ }) };
6454
+ }
6455
+ //#endregion
6456
+ //#region src/modules/panels/model/diy-record-routes.ts
6457
+ function diyRecordRoutes(store) {
6458
+ return { "panels/diy-record": () => ({
6459
+ ok: true,
6460
+ state: store.live(Date.now())
6461
+ }) };
6462
+ }
6463
+ //#endregion
6464
+ //#region src/shared/api/host/diy-debug-act.ts
6465
+ var DEBUG_NOTICE_KIND = "workbench-diy";
6466
+ function noticeMessage(text, summary, kind = DEBUG_NOTICE_KIND) {
6467
+ return {
6468
+ role: "user",
6469
+ id: randomUUID(),
6470
+ content: [{
6471
+ type: "text",
6472
+ text
6473
+ }],
6474
+ source: {
6475
+ kind,
6476
+ form: "notice",
6477
+ summary
6478
+ }
6479
+ };
6480
+ }
6481
+ function isNoticeMessage(value) {
6482
+ if (typeof value !== "object" || value === null) return false;
6483
+ const one = value;
6484
+ const source = one.source;
6485
+ return one.role === "user" && typeof one.id === "string" && Array.isArray(one.content) && typeof source === "object" && source !== null && typeof source.kind === "string";
6486
+ }
6487
+ function requireNoticeMessage(value) {
6488
+ if (!isNoticeMessage(value)) throw new Error("notice 得是 dsh 的 UserMessage 形状(role / id / content / source.kind)—— 裸字符串会写坏会话日志(agent/inbox/spliced requires message objects)");
6489
+ return value;
6490
+ }
6491
+ var INTENT_WORD$1 = {
6492
+ pause: "暂停",
6493
+ resume: "继续",
6494
+ stop: "结束"
6495
+ };
6496
+ function debugNoticeText(intent, state) {
6497
+ const head = `[工作台 DIY]用户按了「${INTENT_WORD$1[intent]}」:`;
6498
+ if (intent === "pause") return `${head}${state.unit} 这场调试暂停了 —— 现在别再把那一屏当目标(看 / 动 / 网络那一族发不出去)。要接着看屏就等用户按「继续」;在那之前照常改源码 / build / 跑门禁。`;
6499
+ if (intent === "resume") return `${head}${state.unit} 这场调试重新开始了 —— 那一屏又归你看。屏 / 板不对的话照旧会被拒,拒了就照实说清是哪种不对。`;
6500
+ return `${head}${state.unit} 这场调试收工了(占用解除)。要再调试就重新 start 一场。`;
6501
+ }
6502
+ function debugNoticeSummary(intent, state) {
6503
+ return `工作台 DIY:用户按了${INTENT_WORD$1[intent]}(${state.unit})`;
6504
+ }
6505
+ function debugNotice(intent, state) {
6506
+ const summary = debugNoticeSummary(intent, state);
6507
+ return {
6508
+ summary,
6509
+ message: noticeMessage(debugNoticeText(intent, state), summary)
6510
+ };
6511
+ }
6512
+ var NOTICE_MODE$1 = {
6513
+ resume: "wake",
6514
+ pause: "hold",
6515
+ stop: "drop"
6516
+ };
6517
+ var held = /* @__PURE__ */ new Map();
6518
+ function holdNotice(sessionId, message) {
6519
+ const checked = requireNoticeMessage(message);
6520
+ const list = held.get(sessionId);
6521
+ if (list === void 0) held.set(sessionId, [checked]);
6522
+ else list.push(checked);
6523
+ }
6524
+ function takeHeldNotices(sessionId) {
6525
+ const list = held.get(sessionId) ?? [];
6526
+ held.delete(sessionId);
6527
+ return list;
6528
+ }
6529
+ function deliverDebugNotice(agents, sessionId, message, mode = "wake") {
6530
+ const notice = requireNoticeMessage(message);
6531
+ const agent = agents?.get(sessionId);
6532
+ if (agent === void 0) {
6533
+ if (mode === "drop") return "none";
6534
+ holdNotice(sessionId, notice);
6535
+ return "held";
6536
+ }
6537
+ if (agent.status !== "idle") {
6538
+ agent.inject(notice);
6539
+ return "inject";
6540
+ }
6541
+ if (mode === "wake") {
6542
+ agent.followup(notice);
6543
+ return "followup";
6544
+ }
6545
+ if (mode === "hold") {
6546
+ holdNotice(sessionId, notice);
6547
+ return "held";
6548
+ }
6549
+ return "skipped-idle";
6550
+ }
6551
+ var REFUSAL$1 = {
6552
+ "already-paused": "这一场已经是暂停态了",
6553
+ "already-debugging": "这一场本来就在调试中",
6554
+ none: "现在没有调试态",
6555
+ "not-owner": "这场调试不是这个会话的"
6556
+ };
6557
+ function debugActResource(deps) {
6558
+ const now = () => deps.now?.() ?? Date.now();
6559
+ const settled = (intent, delivered, summary, state) => ({
6560
+ ok: true,
6561
+ intent,
6562
+ delivered,
6563
+ summary,
6564
+ state
6565
+ });
6566
+ return {
6567
+ resource: "act",
6568
+ handle(payload) {
6569
+ const intent = parseDebugIntent(payload?.intent);
6570
+ if (intent === null) return {
6571
+ ok: false,
6572
+ text: "不认识的意图(只认 pause / resume / stop)"
6573
+ };
6574
+ const store = deps.store();
6575
+ if (store === void 0) return {
6576
+ ok: false,
6577
+ text: "现在没有调试态 —— 没什么可暂停 / 继续 / 结束的"
6578
+ };
6579
+ const at = now();
6580
+ const held = store.live(at);
6581
+ if (held === null) return {
6582
+ ok: false,
6583
+ text: "现在没有调试态 —— 没什么可暂停 / 继续 / 结束的"
6584
+ };
6585
+ const move = intent === "pause" ? store.pause(held.sessionId, at) : intent === "resume" ? store.resume(held.sessionId, at) : store.stop(held.sessionId, at);
6586
+ if (!move.ok) return {
6587
+ ok: false,
6588
+ text: REFUSAL$1[move.code] ?? `按不动:${move.code}`
6589
+ };
6590
+ const notice = deps.notice?.(intent, move.state) ?? debugNotice(intent, move.state);
6591
+ const delivered = deliverDebugNotice(deps.agents(), held.sessionId, notice.message, NOTICE_MODE$1[intent]);
6592
+ if (intent !== "stop") return settled(intent, delivered, notice.summary, store.live(now()));
6593
+ return (store.settle?.() ?? Promise.resolve()).then(() => settled(intent, delivered, notice.summary, store.live(now())));
6594
+ }
6595
+ };
6596
+ }
6597
+ //#endregion
6598
+ //#region src/modules/panels/model/diy-record-scripts.ts
6599
+ var RECORD_KEY = "__dshDiyRecord";
6600
+ var RECORD_KEEP_KEY = "__dshDiyRecordKeep";
6601
+ var RECORD_OFF_KEY = "__dshDiyRecordOff";
6602
+ var RECORD_ARM_SCRIPT = `(() => {
6603
+ const KEY = ${JSON.stringify(RECORD_KEY)};
6604
+ const KEEP = ${JSON.stringify(RECORD_KEEP_KEY)};
6605
+ const OFF = ${JSON.stringify(RECORD_OFF_KEY)};
6606
+ const CAP = ${String(50)};
6607
+ const BODY_MAX = ${String(16384)};
6608
+ const blank = () => ({ phase: 'armed', t0: null, t1: null, href: location.href, dropped: 0, calls: [] });
6609
+ const readKeep = () => {
6610
+ try {
6611
+ const raw = sessionStorage.getItem(KEEP);
6612
+ const parsed = raw === null ? null : JSON.parse(raw);
6613
+ return parsed !== null && typeof parsed === 'object' ? parsed : blank();
6614
+ } catch (error) {
6615
+ return blank();
6616
+ }
6617
+ };
6618
+ const state = window[KEY] !== null && typeof window[KEY] === 'object' ? window[KEY] : readKeep();
6619
+ state.calls = Array.isArray(state.calls) ? state.calls : [];
6620
+ state.phase = typeof state.phase === 'string' ? state.phase : 'armed';
6621
+ window[KEY] = state;
6622
+ const keep = () => {
6623
+ try {
6624
+ sessionStorage.setItem(KEEP, JSON.stringify(state));
6625
+ } catch (error) {}
6626
+ };
6627
+ const slice = (value) => {
6628
+ try {
6629
+ if (value === undefined || value === null || value === '') return '';
6630
+ if (typeof value === 'string') return value.slice(0, BODY_MAX);
6631
+ if (typeof FormData === 'function' && value instanceof FormData) return '[form-data]';
6632
+ if (typeof Blob === 'function' && value instanceof Blob) return '[blob ' + String(value.size) + ']';
6633
+ if (typeof URLSearchParams === 'function' && value instanceof URLSearchParams) return String(value).slice(0, BODY_MAX);
6634
+ const json = JSON.stringify(value);
6635
+ return typeof json === 'string' ? json.slice(0, BODY_MAX) : '';
6636
+ } catch (error) {
6637
+ return '[unprintable]';
6638
+ }
6639
+ };
6640
+ const repaint = () => {
6641
+ if (typeof state.__repaint === 'function') state.__repaint();
6642
+ };
6643
+ const record = (method, url, body, status, ct, response) => {
6644
+ if (state.phase !== 'recording') return;
6645
+ state.calls.push({
6646
+ method: String(method === '' || method === undefined ? 'GET' : method).toUpperCase(),
6647
+ url: String(url === undefined || url === null ? '' : url),
6648
+ body: slice(body === undefined || body === null ? '' : body),
6649
+ status: typeof status === 'number' ? status : 0,
6650
+ ct: String(ct === undefined || ct === null ? '' : ct),
6651
+ response: slice(response === undefined || response === null ? '' : response),
6652
+ at: Date.now(),
6653
+ });
6654
+ if (state.calls.length > CAP) {
6655
+ state.dropped = (typeof state.dropped === 'number' ? state.dropped : 0) + (state.calls.length - CAP);
6656
+ state.calls.splice(0, state.calls.length - CAP);
6657
+ }
6658
+ keep();
6659
+ repaint();
6660
+ };
6661
+ const fresh = typeof window[OFF] !== 'function';
6662
+ if (fresh) {
6663
+ const realFetch = window.fetch;
6664
+ const proto = XMLHttpRequest.prototype;
6665
+ const realOpen = proto.open;
6666
+ const realSend = proto.send;
6667
+ const realBeacon = typeof navigator === 'object' && navigator !== null ? navigator.sendBeacon : undefined;
6668
+ const hasBeacon = typeof realBeacon === 'function';
6669
+ const ctOf = (xhr) => {
6670
+ try {
6671
+ return typeof xhr.getResponseHeader === 'function' ? xhr.getResponseHeader('content-type') || '' : '';
6672
+ } catch (error) {
6673
+ return '';
6674
+ }
6675
+ };
6676
+ const textOf = (xhr) => {
6677
+ try {
6678
+ return typeof xhr.responseText === 'string' ? xhr.responseText : '';
6679
+ } catch (error) {
6680
+ return '';
6681
+ }
6682
+ };
6683
+ window.fetch = function () {
6684
+ const args = arguments;
6685
+ const init = args[1] || {};
6686
+ const asked = args[0];
6687
+ const url = typeof asked === 'string' ? asked : asked !== null && typeof asked === 'object' ? asked.url : '';
6688
+ const method = init.method !== undefined ? init.method : asked !== null && typeof asked === 'object' ? asked.method : 'GET';
6689
+ const body = init.body === undefined ? '' : init.body;
6690
+ const done = realFetch.apply(this, args);
6691
+ try {
6692
+ done.then((response) => {
6693
+ try {
6694
+ const ct = response.headers.get('content-type') || '';
6695
+ const clone = response.clone();
6696
+ const target = response.url || url;
6697
+ clone.text().then((text) => record(method, target, body, response.status, ct, text)).catch(() => record(method, target, body, response.status, ct, ''));
6698
+ } catch (error) {}
6699
+ }).catch((error) => {
6700
+ try {
6701
+ record(method, url, body, 0, 'fetch-error', String(error !== null && typeof error === 'object' && error.message !== undefined ? error.message : error));
6702
+ } catch (inner) {}
6703
+ });
6704
+ } catch (error) {}
6705
+ return done;
6706
+ };
6707
+ proto.open = function (method, url) {
6708
+ this.__dshMethod = method;
6709
+ this.__dshUrl = url;
6710
+ return realOpen.apply(this, arguments);
6711
+ };
6712
+ proto.send = function (body) {
6713
+ const xhr = this;
6714
+ if (typeof xhr.__dshLoadEnd === 'function' && typeof xhr.removeEventListener === 'function') {
6715
+ try {
6716
+ xhr.removeEventListener('loadend', xhr.__dshLoadEnd);
6717
+ } catch (error) {}
6718
+ }
6719
+ const onEnd = () => {
6720
+ try {
6721
+ record(xhr.__dshMethod, xhr.__dshUrl, body, xhr.status, ctOf(xhr), textOf(xhr));
6722
+ } catch (error) {}
6723
+ };
6724
+ xhr.__dshLoadEnd = onEnd;
6725
+ xhr.addEventListener('loadend', onEnd);
6726
+ return realSend.apply(this, arguments);
6727
+ };
6728
+ if (hasBeacon) {
6729
+ navigator.sendBeacon = function (url, data) {
6730
+ try {
6731
+ record('POST', url, data === undefined || data === null ? '' : data, 0, 'beacon', '');
6732
+ } catch (error) {}
6733
+ return realBeacon.apply(navigator, arguments);
6734
+ };
6735
+ }
6736
+ const onResize = () => {
6737
+ if (typeof state.barX === 'number' && typeof state.barY === 'number') place(state.barX, state.barY);
6738
+ };
6739
+ window.addEventListener('resize', onResize);
6740
+ window[OFF] = () => {
6741
+ window.fetch = realFetch;
6742
+ proto.open = realOpen;
6743
+ proto.send = realSend;
6744
+ if (hasBeacon) {
6745
+ try {
6746
+ navigator.sendBeacon = realBeacon;
6747
+ } catch (error) {}
6748
+ }
6749
+ if (typeof window.removeEventListener === 'function') {
6750
+ try {
6751
+ window.removeEventListener('resize', onResize);
6752
+ } catch (error) {}
6753
+ }
6754
+ };
6755
+ }
6756
+ let bar = document.getElementById('__dsh-record-bar');
6757
+ const freshBar = bar === null;
6758
+ if (freshBar) {
6759
+ bar = document.createElement('div');
6760
+ bar.id = '__dsh-record-bar';
6761
+ document.documentElement.appendChild(bar);
6762
+ }
6763
+ bar.setAttribute('style', 'position:fixed;z-index:2147483647;right:16px;bottom:16px;display:flex;gap:8px;align-items:center;padding:8px 10px;border-radius:10px;background:#1f2430;color:#fff;box-shadow:0 6px 24px rgba(0,0,0,.35);font:12px/1.5 -apple-system,system-ui,sans-serif');
6764
+ let sheet = document.getElementById('__dsh-record-bar-style');
6765
+ if (sheet === null) {
6766
+ sheet = document.createElement('style');
6767
+ sheet.id = '__dsh-record-bar-style';
6768
+ sheet.textContent = '@keyframes __dshRecordBreath{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.25;transform:scale(.7)}}#__dsh-record-bar .__dsh-record-dot{animation:__dshRecordBreath 1.1s ease-in-out infinite}';
6769
+ const sheetHost = document.head === null || document.head === undefined ? document.documentElement : document.head;
6770
+ sheetHost.appendChild(sheet);
6771
+ }
6772
+ const place = (x, y, width, height) => {
6773
+ const rect = bar.getBoundingClientRect();
6774
+ const w = typeof width === 'number' ? width : rect.width;
6775
+ const h = typeof height === 'number' ? height : rect.height;
6776
+ const maxX = Math.max(0, (window.innerWidth || 0) - w);
6777
+ const maxY = Math.max(0, (window.innerHeight || 0) - h);
6778
+ const left = Math.min(Math.max(0, x), maxX);
6779
+ const top = Math.min(Math.max(0, y), maxY);
6780
+ state.barX = left;
6781
+ state.barY = top;
6782
+ bar.style.right = 'auto';
6783
+ bar.style.bottom = 'auto';
6784
+ bar.style.left = String(left) + 'px';
6785
+ bar.style.top = String(top) + 'px';
6786
+ };
6787
+ const grip = document.createElement('span');
6788
+ grip.setAttribute('role', 'button');
6789
+ grip.setAttribute('tabindex', '0');
6790
+ grip.setAttribute('title', '拖动浮条');
6791
+ grip.setAttribute('aria-label', '拖动浮条');
6792
+ grip.setAttribute('style', 'flex:0 0 auto;width:12px;height:18px;border-radius:3px;cursor:grab;touch-action:none;background-image:radial-gradient(circle,rgba(255,255,255,.5) 1.2px,transparent 1.3px);background-size:6px 6px;background-position:center center');
6793
+ const dot = document.createElement('span');
6794
+ dot.className = '__dsh-record-dot';
6795
+ dot.setAttribute('aria-hidden', 'true');
6796
+ dot.setAttribute('style', 'flex:0 0 auto;display:none;width:8px;height:8px;border-radius:999px;corner-shape:round;background:#ef4444');
6797
+ const label = document.createElement('span');
6798
+ label.setAttribute('style', 'flex:0 0 auto;white-space:nowrap;font-variant-numeric:tabular-nums');
6799
+ const start = document.createElement('button');
6800
+ const end = document.createElement('button');
6801
+ start.type = 'button';
6802
+ start.setAttribute('title', '开始录制');
6803
+ start.setAttribute('aria-label', '开始录制');
6804
+ start.innerHTML = '<svg width="13" height="13" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="1.5" y="4" width="9" height="8" rx="2"></rect><path d="M10.5 7.4 14.5 5v6l-4-2.4z"></path></svg>开始';
6805
+ end.type = 'button';
6806
+ end.setAttribute('title', '结束录制');
6807
+ end.setAttribute('aria-label', '结束录制');
6808
+ end.innerHTML = '<svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true" focusable="false"><rect x="2" y="2" width="8" height="8" rx="1.5" fill="currentColor"></rect></svg>结束';
6809
+ const look = (on) => 'display:inline-flex;align-items:center;gap:4px;height:24px;padding:0 8px;border:0;border-radius:6px;font-size:12px;line-height:1;font-family:inherit;white-space:nowrap;cursor:' + (on ? 'pointer' : 'default') + ';color:#fff;background:' + (on ? '#3b82f6' : '#334155');
6810
+ const paint = () => {
6811
+ const phase = state.phase;
6812
+ label.textContent = (phase === 'recording' ? '录制中 · ' : phase === 'finished' ? '录制完成 · ' : '录制 · 等开始 · ') + String(state.calls.length) + ' 条';
6813
+ dot.style.display = phase === 'recording' ? 'inline-block' : 'none';
6814
+ start.disabled = phase !== 'armed';
6815
+ end.disabled = phase !== 'recording';
6816
+ start.setAttribute('style', look(phase === 'armed'));
6817
+ end.setAttribute('style', look(phase === 'recording'));
6818
+ };
6819
+ state.__repaint = paint;
6820
+ const drag = { on: false, px: 0, py: 0, left: 0, top: 0, w: 0, h: 0 };
6821
+ const drop = () => {
6822
+ if (!drag.on) return;
6823
+ drag.on = false;
6824
+ grip.style.cursor = 'grab';
6825
+ keep();
6826
+ };
6827
+ grip.onpointerdown = (event) => {
6828
+ const rect = bar.getBoundingClientRect();
6829
+ drag.on = true;
6830
+ drag.px = event.clientX;
6831
+ drag.py = event.clientY;
6832
+ drag.left = rect.left;
6833
+ drag.top = rect.top;
6834
+ drag.w = rect.width;
6835
+ drag.h = rect.height;
6836
+ grip.style.cursor = 'grabbing';
6837
+ if (typeof grip.setPointerCapture === 'function') {
6838
+ try {
6839
+ grip.setPointerCapture(event.pointerId);
6840
+ } catch (error) {}
6841
+ }
6842
+ if (typeof event.preventDefault === 'function') event.preventDefault();
6843
+ };
6844
+ grip.onpointermove = (event) => {
6845
+ if (!drag.on) return;
6846
+ place(drag.left + event.clientX - drag.px, drag.top + event.clientY - drag.py, drag.w, drag.h);
6847
+ };
6848
+ grip.onpointerup = drop;
6849
+ grip.onpointercancel = drop;
6850
+ grip.onkeydown = (event) => {
6851
+ const step = event.shiftKey ? 40 : 16;
6852
+ const rect = bar.getBoundingClientRect();
6853
+ const left = event.key === 'ArrowLeft' ? rect.left - step : event.key === 'ArrowRight' ? rect.left + step : rect.left;
6854
+ const top = event.key === 'ArrowUp' ? rect.top - step : event.key === 'ArrowDown' ? rect.top + step : rect.top;
6855
+ if (left === rect.left && top === rect.top) return;
6856
+ place(left, top);
6857
+ keep();
6858
+ if (typeof event.preventDefault === 'function') event.preventDefault();
6859
+ };
6860
+ start.onclick = () => {
6861
+ if (state.phase !== 'armed') return;
6862
+ state.phase = 'recording';
6863
+ state.t0 = Date.now();
6864
+ state.t1 = null;
6865
+ state.href = location.href;
6866
+ state.calls = [];
6867
+ state.dropped = 0;
6868
+ keep();
6869
+ paint();
6870
+ };
6871
+ end.onclick = () => {
6872
+ state.phase = 'finished';
6873
+ state.t1 = Date.now();
6874
+ keep();
6875
+ paint();
6876
+ };
6877
+ bar.textContent = '';
6878
+ bar.appendChild(grip);
6879
+ bar.appendChild(dot);
6880
+ bar.appendChild(label);
6881
+ bar.appendChild(start);
6882
+ bar.appendChild(end);
6883
+ if (typeof state.barX === 'number' && typeof state.barY === 'number') place(state.barX, state.barY);
6884
+ paint();
6885
+ return { ok: true, fresh, phase: state.phase, t0: state.t0 === undefined ? null : state.t0, t1: state.t1 === undefined ? null : state.t1, href: state.href || location.href, size: state.calls.length, dropped: typeof state.dropped === 'number' ? state.dropped : 0 };
6886
+ })()`;
6887
+ var RECORD_READ_SCRIPT = `(() => {
6888
+ const KEY = ${JSON.stringify(RECORD_KEY)};
6889
+ const KEEP = ${JSON.stringify(RECORD_KEEP_KEY)};
6890
+ let state = window[KEY] !== null && typeof window[KEY] === 'object' ? window[KEY] : null;
6891
+ if (state === null) {
6892
+ try {
6893
+ const raw = sessionStorage.getItem(KEEP);
6894
+ state = raw === null ? null : JSON.parse(raw);
6895
+ } catch (error) {
6896
+ state = null;
6897
+ }
6898
+ }
6899
+ if (state === null || typeof state !== 'object') return { ok: false, code: 'not-armed', phase: 'none', calls: [] };
6900
+ return { ok: true, phase: typeof state.phase === 'string' ? state.phase : 'armed', t0: state.t0 === undefined ? null : state.t0, t1: state.t1 === undefined ? null : state.t1, href: typeof state.href === 'string' ? state.href : '', here: location.href, dropped: typeof state.dropped === 'number' ? state.dropped : 0, calls: Array.isArray(state.calls) ? state.calls : [] };
6901
+ })()`;
6902
+ var RECORD_STOP_SCRIPT = `(() => {
6903
+ const KEY = ${JSON.stringify(RECORD_KEY)};
6904
+ const KEEP = ${JSON.stringify(RECORD_KEEP_KEY)};
6905
+ const OFF = ${JSON.stringify(RECORD_OFF_KEY)};
6906
+ let state = window[KEY] !== null && typeof window[KEY] === 'object' ? window[KEY] : null;
6907
+ if (state === null) {
6908
+ try {
6909
+ const raw = sessionStorage.getItem(KEEP);
6910
+ state = raw === null ? null : JSON.parse(raw);
6911
+ } catch (error) {
6912
+ state = null;
6913
+ }
6914
+ }
6915
+ let unhooked = 0;
6916
+ if (typeof window[OFF] === 'function') {
6917
+ try {
6918
+ window[OFF]();
6919
+ unhooked = 1;
6920
+ } catch (error) {}
6921
+ }
6922
+ const bar = document.getElementById('__dsh-record-bar');
6923
+ if (bar !== null) bar.remove();
6924
+ const sheet = document.getElementById('__dsh-record-bar-style');
6925
+ if (sheet !== null) sheet.remove();
6926
+ try {
6927
+ delete window[KEY];
6928
+ } catch (error) {
6929
+ window[KEY] = undefined;
6930
+ }
6931
+ try {
6932
+ delete window[OFF];
6933
+ } catch (error) {
6934
+ window[OFF] = undefined;
6935
+ }
6936
+ try {
6937
+ sessionStorage.removeItem(KEEP);
6938
+ } catch (error) {}
6939
+ return { ok: true, unhooked, phase: state !== null && typeof state === 'object' ? state.phase : 'none', t0: state !== null && typeof state === 'object' && state.t0 !== undefined ? state.t0 : null, t1: state !== null && typeof state === 'object' && state.t1 !== undefined ? state.t1 : null, calls: state !== null && typeof state === 'object' && Array.isArray(state.calls) ? state.calls : [], dropped: state !== null && typeof state === 'object' && typeof state.dropped === 'number' ? state.dropped : 0 };
6940
+ })()`;
6941
+ //#endregion
6942
+ //#region src/modules/panels/model/diy-record-capture.ts
6943
+ var SEGMENT_SLACK_MS = 1500;
6944
+ function parsedOf(line) {
6945
+ const trimmed = line.trim();
6946
+ if (trimmed === "") return null;
6947
+ let parsed;
6948
+ try {
6949
+ parsed = JSON.parse(trimmed);
6950
+ } catch {
6951
+ return null;
6952
+ }
6953
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
6954
+ }
6955
+ function plain$1(value) {
6956
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
6957
+ }
6958
+ function bodyTextOf(value) {
6959
+ if (typeof value === "string") return value;
6960
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
6961
+ if (typeof value === "object" && value !== null) try {
6962
+ return JSON.stringify(value);
6963
+ } catch {
6964
+ return "";
6965
+ }
6966
+ return "";
6967
+ }
6968
+ function sizeOf(value) {
6969
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
6970
+ }
6971
+ function entryOf(row) {
6972
+ const url = row["url"];
6973
+ const method = row["method"];
6974
+ if (typeof url !== "string" || url === "") return null;
6975
+ if (typeof method !== "string" || method === "") return null;
6976
+ const stamp = typeof row["timestamp"] === "string" ? Date.parse(row["timestamp"]) : NaN;
6977
+ const request = plain$1(row["request"]);
6978
+ const requestBody = bodyTextOf(request["body"]);
6979
+ const responseBody = bodyTextOf(row["body"]);
6980
+ const requestFull = sizeOf(request["body_full_size"]);
6981
+ const responseFull = sizeOf(row["body_full_size"]);
6982
+ return {
6983
+ key: typeof row["key"] === "string" ? row["key"] : "",
6984
+ at: Number.isFinite(stamp) ? stamp : 0,
6985
+ method: method.toUpperCase(),
6986
+ url,
6987
+ status: sizeOf(row["status"]),
6988
+ ct: typeof row["ct"] === "string" ? row["ct"] : "",
6989
+ size: sizeOf(row["size"]),
6990
+ request: requestBody,
6991
+ response: responseBody,
6992
+ requestFullSize: requestFull || requestBody.length,
6993
+ responseFullSize: responseFull || responseBody.length,
6994
+ requestSizeExact: requestFull > 0,
6995
+ responseSizeExact: responseFull > 0,
6996
+ headers: Object.keys(request["headers"] === void 0 ? {} : plain$1(request["headers"])).length === 0 ? "" : bodyTextOf(request["headers"])
6997
+ };
6998
+ }
6999
+ function richerOf(left, right) {
7000
+ const leftScore = (left.status === 0 ? 0 : 1) + (left.response === "" ? 0 : 1) + (left.ct === "" ? 0 : 1);
7001
+ const best = (right.status === 0 ? 0 : 1) + (right.response === "" ? 0 : 1) + (right.ct === "" ? 0 : 1) > leftScore ? right : left;
7002
+ const other = best === left ? right : left;
7003
+ return {
7004
+ ...best,
7005
+ request: best.request === "" ? other.request : best.request,
7006
+ requestFullSize: best.request === "" ? other.requestFullSize : best.requestFullSize,
7007
+ requestSizeExact: best.requestSizeExact || other.requestSizeExact,
7008
+ response: best.response === "" ? other.response : best.response,
7009
+ responseFullSize: best.response === "" ? other.responseFullSize : best.responseFullSize,
7010
+ responseSizeExact: best.responseSizeExact || other.responseSizeExact,
7011
+ headers: best.headers === "" ? other.headers : best.headers
7012
+ };
7013
+ }
7014
+ function captureEntriesOf(text) {
7015
+ const out = [];
7016
+ const at = /* @__PURE__ */ new Map();
7017
+ const push = (one) => {
7018
+ if (one === null) return;
7019
+ const id = one.key === "" ? `${one.method} ${one.url}@${String(one.at)}` : `${one.key}@${String(one.at)}`;
7020
+ const found = at.get(id);
7021
+ if (found === void 0) {
7022
+ at.set(id, out.length);
7023
+ out.push(one);
7024
+ return;
7025
+ }
7026
+ const kept = out[found];
7027
+ if (kept !== void 0) out[found] = richerOf(kept, one);
7028
+ };
7029
+ for (const line of text.split("\n")) {
7030
+ const row = parsedOf(line);
7031
+ if (row === null) continue;
7032
+ const inside = row["entries"];
7033
+ if (Array.isArray(inside)) {
7034
+ for (const one of inside) push(entryOf(plain$1(one)));
7035
+ continue;
7036
+ }
7037
+ push(entryOf(row));
7038
+ }
7039
+ return out;
7040
+ }
7041
+ function segmentOf(entries, t0, t1) {
7042
+ if (t0 === null || t1 === null) return {
7043
+ inside: entries,
7044
+ outside: 0
7045
+ };
7046
+ const from = t0 - SEGMENT_SLACK_MS;
7047
+ const to = t1 + SEGMENT_SLACK_MS;
7048
+ const inside = [];
7049
+ let outside = 0;
7050
+ for (const one of entries) if (one.at === 0 || one.at >= from && one.at <= to) inside.push(one);
7051
+ else outside += 1;
7052
+ return {
7053
+ inside,
7054
+ outside
7055
+ };
7056
+ }
7057
+ function captureCallsOf(entries) {
7058
+ return entries.map((one) => ({
7059
+ key: one.key,
7060
+ method: one.method,
7061
+ url: one.url,
7062
+ body: one.request,
7063
+ status: one.status,
7064
+ ct: one.ct,
7065
+ response: one.response,
7066
+ at: one.at,
7067
+ source: "capture"
7068
+ }));
7069
+ }
7070
+ function callKeyOf(one) {
7071
+ return `${one.method} ${one.url}`;
7072
+ }
7073
+ function mergeCalls(hook, captured) {
7074
+ const out = [...hook];
7075
+ const hooks = hook.length;
7076
+ const claimed = /* @__PURE__ */ new Set();
7077
+ for (const one of captured) {
7078
+ const key = callKeyOf(one);
7079
+ let found = -1;
7080
+ for (let index = 0; index < hooks; index += 1) {
7081
+ if (claimed.has(index)) continue;
7082
+ const kept = out[index];
7083
+ if (kept === void 0 || callKeyOf(kept) !== key) continue;
7084
+ if (kept.key !== "" && kept.key !== one.key) continue;
7085
+ found = index;
7086
+ break;
7087
+ }
7088
+ if (found === -1) {
7089
+ out.push(one);
7090
+ continue;
7091
+ }
7092
+ claimed.add(found);
7093
+ const kept = out[found];
7094
+ if (kept === void 0) continue;
7095
+ out[found] = {
7096
+ ...kept,
7097
+ key: kept.key === "" ? one.key : kept.key,
7098
+ body: kept.body === "" ? one.body : kept.body,
7099
+ response: kept.response === "" ? one.response : kept.response
7100
+ };
7101
+ }
7102
+ return out.sort((left, right) => left.at - right.at);
7103
+ }
7104
+ function matchKey(one, want) {
7105
+ if (one.key === want) return true;
7106
+ const bare = want.replace(/#\d+$/, "");
7107
+ if (one.key.replace(/#\d+$/, "") === bare) return true;
7108
+ return callKeyOf(one) === bare;
7109
+ }
7110
+ function pickAllOf(entries, key) {
7111
+ const want = key.trim();
7112
+ if (want === "") return [];
7113
+ const exact = entries.filter((one) => one.key === want);
7114
+ if (exact.length > 0) return exact;
7115
+ return entries.filter((one) => matchKey(one, want));
7116
+ }
7117
+ function pickOf(entries, key, at = null) {
7118
+ const found = pickAllOf(entries, key);
7119
+ const first = found[0];
7120
+ if (first === void 0) return null;
7121
+ if (at === null) return first;
7122
+ let best = first;
7123
+ for (const one of found) if (Math.abs(one.at - at) < Math.abs(best.at - at)) best = one;
7124
+ return best;
7125
+ }
7126
+ //#endregion
7127
+ //#region src/modules/panels/model/diy-record-text.ts
7128
+ var CALLS_SHOWN = 12;
7129
+ function recordPageOf(payload) {
7130
+ const raw = payload ?? {};
7131
+ const calls = [];
7132
+ const rows = Array.isArray(raw["calls"]) ? raw["calls"] : [];
7133
+ for (const one of rows) {
7134
+ if (typeof one !== "object" || one === null) continue;
7135
+ const row = one;
7136
+ calls.push({
7137
+ key: "",
7138
+ method: typeof row["method"] === "string" ? row["method"] : "",
7139
+ url: typeof row["url"] === "string" ? row["url"] : "",
7140
+ body: typeof row["body"] === "string" ? row["body"] : "",
7141
+ status: typeof row["status"] === "number" ? row["status"] : 0,
7142
+ ct: typeof row["ct"] === "string" ? row["ct"] : "",
7143
+ response: typeof row["response"] === "string" ? row["response"] : "",
7144
+ at: typeof row["at"] === "number" ? row["at"] : 0,
7145
+ source: "hook"
7146
+ });
7147
+ }
7148
+ const phase = typeof raw["phase"] === "string" ? raw["phase"] : "none";
7149
+ return {
7150
+ ok: raw["ok"] === true,
7151
+ code: typeof raw["code"] === "string" ? raw["code"] : "",
7152
+ phase,
7153
+ t0: typeof raw["t0"] === "number" ? raw["t0"] : null,
7154
+ t1: typeof raw["t1"] === "number" ? raw["t1"] : null,
7155
+ href: typeof raw["href"] === "string" ? raw["href"] : "",
7156
+ here: typeof raw["here"] === "string" ? raw["here"] : "",
7157
+ dropped: typeof raw["dropped"] === "number" ? raw["dropped"] : 0,
7158
+ unhooked: raw["unhooked"] === 1 || raw["unhooked"] === true,
7159
+ calls
7160
+ };
7161
+ }
7162
+ function filterTextOf(expect) {
7163
+ const want = expect;
7164
+ if (want === null || want.method === "" && want.urlContains === "") return "";
7165
+ const parts = [];
7166
+ if (want.method !== "") parts.push(want.method);
7167
+ if (want.urlContains !== "") parts.push(`url 含 ${want.urlContains}`);
7168
+ return parts.join(" · ");
7169
+ }
7170
+ function probeCommandOf(calls) {
7171
+ const pick = calls.find((one) => one.body !== "");
7172
+ if (pick === void 0) return "";
7173
+ return `${pick.method} ${pick.url}\n${pick.body.slice(0, 400)}`;
7174
+ }
7175
+ function bodyMissingOf(calls) {
7176
+ return calls.length > 0 && calls.every((one) => one.body === "");
7177
+ }
7178
+ function recordRefusalText(code, held) {
7179
+ switch (code) {
7180
+ case "held-by-other": return `另一场录制还占着(${held === void 0 ? "另一个会话" : held.sessionId})—— 一台机器同时只允许一场:让它先 ${DIY_RECORD_STOP_TOOL},或等它收工。`;
7181
+ case "already-held": return `你这个会话已经持有一场录制(${held === void 0 ? "" : `${held.unit},${held.phase}`})—— 要换目标先 ${DIY_RECORD_STOP_TOOL};上一场录完了就再录一段(同一场不用重新审批)。`;
7182
+ case "not-owner": return "这一场录制不是你这个会话的 —— 谁开的谁收工。";
7183
+ case "none": return "现在没有录制中的那一场。";
7184
+ case "already-recording": return "已经在录了 —— 请他按「结束录制」,再调这一次。";
7185
+ case "not-armed": return "这一场已经录完了 —— 要再录一段就先起一段新的(同一场不用重新审批)。";
7186
+ case "not-recording": return "还没开始录(他还没按「开始录制」)—— 先请他按开始。";
7187
+ case "not-finished": return "这一场还没录完 —— 等他按「结束录制」再起下一段。";
7188
+ }
7189
+ }
7190
+ function recordMissingText() {
7191
+ return `现在没有录制中的那一场(${DIY_RECORD_PREPARE_TOOL} 起的那一场可能已经过期或收工了)—— 先调 ${DIY_RECORD_PREPARE_TOOL}。`;
7192
+ }
7193
+ function recordTalkOf() {
7194
+ return "已开始录制";
7195
+ }
7196
+ function noFilter(expect) {
7197
+ return expect === null || expect.method === "" && expect.urlContains === "";
7198
+ }
7199
+ function matchesExpect(one, expect) {
7200
+ if (noFilter(expect)) return false;
7201
+ const want = expect;
7202
+ if (want.method !== "" && one.method !== want.method) return false;
7203
+ if (want.urlContains !== "" && !one.url.includes(want.urlContains)) return false;
7204
+ return true;
7205
+ }
7206
+ function listRowsOf(calls, expect) {
7207
+ return calls.map((one, index) => {
7208
+ const hasRequest = one.body !== "";
7209
+ const hasResponse = one.response !== "";
7210
+ const likelyWrite = one.method !== "GET" || hasRequest;
7211
+ return {
7212
+ index,
7213
+ key: one.key,
7214
+ at: one.at,
7215
+ method: one.method,
7216
+ url: one.url,
7217
+ status: one.status,
7218
+ ct: one.ct,
7219
+ size: one.response.length === 0 ? 0 : one.response.length,
7220
+ source: one.source,
7221
+ hasRequest,
7222
+ hasResponse,
7223
+ likelyWrite,
7224
+ marked: matchesExpect(one, expect) || noFilter(expect) && likelyWrite
7225
+ };
7226
+ }).slice().sort((left, right) => Number(right.marked) - Number(left.marked) || left.at - right.at).map((one, index) => ({
7227
+ ...one,
7228
+ index
7229
+ }));
7230
+ }
7231
+ function sizeText(bytes) {
7232
+ if (bytes <= 0) return "无";
7233
+ if (bytes < 1024) return `${String(bytes)} B`;
7234
+ return `${(bytes / 1024).toFixed(1)} KB`;
7235
+ }
7236
+ function rowTextOf(one) {
7237
+ const mark = one.marked ? " ⭐" : " ";
7238
+ const where = one.source === "capture" ? "捕获" : "钩子";
7239
+ return `· [${String(one.index)}]${mark} ${one.method} ${one.url}(${where} · 入参 ${one.hasRequest ? "有" : "无"} · 出参 ${sizeText(one.size)})`;
7240
+ }
7241
+ function renderRecordRead(input) {
7242
+ if (!input.armed) return `那一页上还没有录制条(${DIY_RECORD_PREPARE_TOOL} 起的那一场可能被导航冲掉了)—— 先 ${DIY_RECORD_PREPARE_TOOL},或让他回到那一页再 ${DIY_RECORD_READ_TOOL}。`;
7243
+ if (input.phase === "armed") return "他还没按「开始录制」—— 等他按了再读。";
7244
+ if (input.phase === "recording") return `还在录(页面钩子上已经攒了 ${String(input.hookCaught)} 条)—— 等他按「结束录制」。`;
7245
+ const hook = String(input.hookCaught);
7246
+ const inside = String(input.capturedInSegment);
7247
+ const markShown = input.rows.filter((one) => one.marked).slice(0, CALLS_SHOWN);
7248
+ const restShown = input.rows.filter((one) => !one.marked).slice(-Math.max(0, CALLS_SHOWN - markShown.length));
7249
+ const shown = [...markShown, ...restShown];
7250
+ const lines = [];
7251
+ if (input.rows.length > 0) lines.push(`录完了:这一段一共 ${String(input.merged)} 条请求(页面钩子 ${hook} 条 · 会话捕获落在这个时间段里 ${inside} 条,合并去重后 ${String(input.merged)} 条)。下面 ⭐ 都列上、其余按时间取最近几条:`, ...shown.map(rowTextOf));
7252
+ else lines.push(input.capturedTruncated ? `录完了:这一段(就解析到的那部分而言)一条请求都没抓到(页面钩子 0 条 · 解析到的会话捕获里落在这个时间段里 0 条)—— 但捕获文件被截过,见下一行。` : "录完了:这一段一条请求都没抓到(页面钩子 0 条 · 会话捕获落在这个时间段里 0 条,两边都是 0 —— 不是筛掉的)。");
7253
+ if (input.capturedTruncated) lines.push(input.capturedTotalLines > input.capturedTailLimit ? `(捕获文件这一场一共落了 ${String(input.capturedTotalLines)} 行,这里只解析了最后 ${String(input.capturedTailLimit)} 行 —— 更早的没看;上面那几个"有几条 / 没落"的捕获数只对最后这段成立)` : `(捕获文件这一场一共落了 ${String(input.capturedTotalLines)} 行(没顶到 ${String(input.capturedTailLimit)} 行那顶帽),是**这一读本身被输出上限截了** —— 读到的只是最近一段,上面那几个捕获数只对读到的这段成立)`);
7254
+ if (input.dropped > 0) lines.push(`(缓冲区满了,最早丢了 ${String(input.dropped)} 条)`);
7255
+ if (input.captureRound === "fail") lines.push("(会话捕获最近一轮没跑成(opencli 那条报了错)—— 这一场可能只有页面钩子。)");
7256
+ else if (input.captureRound === "") lines.push("(会话捕获还没回过一轮 —— 再等一拍再看,别急着下\"没抓到\"的结论。)");
7257
+ if (input.capturedLines === 0 && !input.capturedTruncated) lines.push("(opencli 那条会话捕获一条都没落 —— 这次只有页面钩子。请求体抓不到时请他打开 DevTools 把那次请求复制成 curl)");
7258
+ if (input.nav) lines.push("(这一段里那一页导航过 —— 浮条重新注入过,导航前的那几条可能不在)");
7259
+ return lines.join("\n");
7260
+ }
7261
+ function clip(text, maxBytes) {
7262
+ if (maxBytes <= 0 || text.length <= maxBytes) return {
7263
+ text,
7264
+ cut: false
7265
+ };
7266
+ return {
7267
+ text: text.slice(0, maxBytes),
7268
+ cut: true
7269
+ };
7270
+ }
7271
+ function sizeLabel(size, exact) {
7272
+ return exact ? `${String(size)} 字节` : `${String(size)} 字符(opencli 没给 body_full_size,按字符数算)`;
7273
+ }
7274
+ function renderRecordDetail(input) {
7275
+ if (!input.found) return [`没找到这一条:${input.note}`, `可用的取法:② 还在这场录制里时用清单里的 key 或 index;③ 这一场已经收工的话,给 unit 让我去读 shots/rec-<unit>.jsonl.last;④ 都不行就让他打开 DevTools,把那次请求复制成 curl。`].join("\n");
7276
+ const req = clip(input.request, input.maxBytes);
7277
+ const res = clip(input.response, input.maxBytes);
7278
+ const status = input.status === 0 ? "回包还没到(status 0)" : String(input.status);
7279
+ const lines = [`${input.method} ${input.url} · ${status}${input.ct === "" ? "" : ` · ${input.ct}`}`, `(${input.where} · ${new Date(input.at).toISOString()})`];
7280
+ lines.push(req.text === "" ? "入参(request body):没有" : `入参(request body,原长 ${sizeLabel(input.requestFullSize, input.requestSizeExact)}${req.cut ? `,这里截到 ${String(input.maxBytes)}` : ""}):\n${req.text}`);
7281
+ lines.push(res.text === "" ? "出参(response body):没有" : `出参(response body,原长 ${sizeLabel(input.responseFullSize, input.responseSizeExact)}${res.cut ? `,这里截到 ${String(input.maxBytes)}` : ""}):\n${res.text}`);
7282
+ if (input.headers !== "") lines.push(`请求头(可能含凭据 —— 别落进 design.json):${input.headers}`);
7283
+ if (input.ambiguous > 1) lines.push(`(这个 key 在这一场的原文里出现 ${String(input.ambiguous)} 次 —— 跨轮会重名;带上清单里那一行的 at 才钉得住是哪一次。)`);
7284
+ if (input.note !== "") lines.push(`(${input.note})`);
7285
+ return lines.join("\n");
7286
+ }
7287
+ function moveOrThrow(move, held) {
7288
+ if (move.ok) return move.state;
7289
+ throw new Error(recordRefusalText(move.code, held ?? void 0));
7290
+ }
7291
+ function plain(value) {
7292
+ return typeof value === "object" && value !== null ? value : {};
7293
+ }
7294
+ function domainOf(url) {
7295
+ try {
7296
+ const host = new URL(url).hostname.toLowerCase();
7297
+ return host.startsWith("www.") ? host.slice(4) : host;
7298
+ } catch {
7299
+ return "";
7300
+ }
7301
+ }
7302
+ function readDomainsOf(design) {
7303
+ const root = plain(design);
7304
+ const many = root["reads"];
7305
+ const one = root["datasource"];
7306
+ const rows = Array.isArray(many) ? many : one === void 0 ? [] : [one];
7307
+ const found = /* @__PURE__ */ new Set();
7308
+ for (const row of rows) {
7309
+ const read = plain(row)["read"];
7310
+ if (typeof read !== "string") continue;
7311
+ for (const match of read.matchAll(/https?:\/\/[^\s"'`)\\]+/g)) {
7312
+ const domain = domainOf(match[0]);
7313
+ if (domain !== "") found.add(domain);
7314
+ }
7315
+ }
7316
+ return [...found];
7317
+ }
7318
+ //#endregion
7319
+ //#region src/modules/panels/model/diy-record.ts
7320
+ var OPEN_TIMEOUT_MS = 3e4;
7321
+ var START_TIMEOUT_MS = 8e3;
7322
+ var SETTLE_MS = 2e3;
7323
+ var WAIT_CAP_MS = 9e5;
7324
+ var POLL_MS = 1e3;
7325
+ var CAPTURE_TAIL_LINES = 2e4;
7326
+ var CAPTURE_POLL_S = 3;
7327
+ var CAPTURE_ARM_S = 2;
7328
+ var STATIC_URL_PATTERN = "^(data:)|(\\.(js|css|png|jpe?g|gif|svg|woff2?|ico|map|webp|mp4)(\\?|$))";
7329
+ var RECORD_WATCH_MS = 3e3;
7330
+ var RECORD_WATCH_TRIES = 5;
7331
+ var DROP_STATIC_JS = "let s=\"\";process.stdin.on(\"data\",(d)=>s+=d).on(\"end\",()=>{const re=new RegExp(process.argv[1],\"i\");for(const line of s.split(\"\\n\")){const t=line.trim();if(t===\"\")continue;let one;try{one=JSON.parse(t)}catch{process.stdout.write(t+\"\\n\");continue}if(Array.isArray(one.entries)){one.entries=one.entries.filter((e)=>!re.test(String((e||{}).url||\"\")));if(one.entries.length===0)continue}process.stdout.write(JSON.stringify(one)+\"\\n\")}});";
7332
+ var DETAIL_MAX_BYTES = 6e4;
7333
+ var HOST_PID = String(process.pid);
7334
+ var pauseFor = (ms) => new Promise((resolve) => {
7335
+ setTimeout(resolve, ms);
7336
+ });
7337
+ function waitOf(value) {
7338
+ return Math.min(Math.max(typeof value === "number" && Number.isFinite(value) ? Math.floor(value) : 0, 0), WAIT_CAP_MS);
7339
+ }
7340
+ function foreignDomainText(design, url) {
7341
+ const known = readDomainsOf(design);
7342
+ if (known.length === 0) return "";
7343
+ const domain = domainOf(url);
7344
+ if (domain === "" || known.includes(domain)) return "";
7345
+ return `这一页(${domain})跟这个模块已经探明的读源(${known.join(" / ")})不是一个域 —— 录下来的请求原文是要留在工坊里的,不许跨域录。换成它那个域的页面,或者先把这个模块的读源说清。`;
7346
+ }
7347
+ function captureFileOf(packagesDir, unit) {
7348
+ return join(dirname(packagesDir), "shots", `rec-${unit}.jsonl`);
7349
+ }
7350
+ function lastCaptureFileOf(file) {
7351
+ return `${file}.last`;
7352
+ }
7353
+ async function startRecordCapture(shell, command, session, file) {
7354
+ const bin = command === "" ? "opencli" : command;
7355
+ const arm = backgroundLine(bin, [
7356
+ "browser",
7357
+ session,
7358
+ "network",
7359
+ "--follow",
7360
+ "--all",
7361
+ "--window",
7362
+ "background"
7363
+ ]);
7364
+ const snap = backgroundLine(bin, [
7365
+ "browser",
7366
+ session,
7367
+ "network",
7368
+ "--all",
7369
+ "--raw",
7370
+ "--window",
7371
+ "background"
7372
+ ]);
7373
+ return (await runShell(shell, [
7374
+ `f=${quoteShellArg(file)}`,
7375
+ `r=${quoteShellArg(`${file}.round`)}`,
7376
+ ": > \"$f\"",
7377
+ "rm -f \"$f.last\" \"$f.first\" \"$r\"",
7378
+ `( ${arm} >> "$f" 2>/dev/null & p=$!; sleep ${String(CAPTURE_ARM_S)}; kill "$p" 2>/dev/null )`,
7379
+ `( while kill -0 ${HOST_PID} 2>/dev/null; do if ${snap} > "$r" 2>/dev/null; then printf ok > "$f.first"; else printf fail > "$f.first"; fi; if [ -s "$r" ]; then if command -v node >/dev/null 2>&1; then tr -d '\\n' < "$r" | node -e ${quoteShellArg(DROP_STATIC_JS)} ${quoteShellArg(STATIC_URL_PATTERN)} >> "$f"; else tr -d '\\n' < "$r" >> "$f"; printf '\\n' >> "$f"; fi; fi; rm -f "$r"; sleep ${String(CAPTURE_POLL_S)}; done & echo $! > "$f.pid" ) & echo $! > "$f.reaper"`,
7380
+ "kill -0 \"$(cat \"$f.pid\" 2>/dev/null)\" 2>/dev/null"
7381
+ ].join("; "), { timeoutMs: START_TIMEOUT_MS })).exitCode === 0;
7382
+ }
7383
+ async function releaseLeftoversOf(input) {
7384
+ await releaseQuietly({
7385
+ shell: input.shell,
7386
+ command: input.command,
7387
+ session: BROWSER_RECORD_SESSION,
7388
+ url: input.url,
7389
+ captureFile: input.captureFile
7390
+ });
7391
+ }
7392
+ async function watchFinishStepOf(input) {
7393
+ const live = input.store.live(Date.now());
7394
+ if (live === null) {
7395
+ await releaseLeftoversOf(input);
7396
+ return {
7397
+ stop: true,
7398
+ delivered: false
7399
+ };
7400
+ }
7401
+ if (live.sessionId !== input.sessionId) return {
7402
+ stop: true,
7403
+ delivered: false
7404
+ };
7405
+ if (live.phase === "finished") return {
7406
+ stop: false,
7407
+ delivered: false
7408
+ };
7409
+ let page;
7410
+ try {
7411
+ page = recordPageOf((await actBrowser(input.shell, input.command, BROWSER_RECORD_SESSION, ["eval", RECORD_READ_SCRIPT])).payload);
7412
+ } catch {
7413
+ return {
7414
+ stop: false,
7415
+ delivered: false
7416
+ };
7417
+ }
7418
+ const held = input.store.live(Date.now());
7419
+ if (held === null) {
7420
+ await releaseLeftoversOf(input);
7421
+ return {
7422
+ stop: true,
7423
+ delivered: false
7424
+ };
7425
+ }
7426
+ if (held.sessionId !== input.sessionId) return {
7427
+ stop: true,
7428
+ delivered: false
7429
+ };
7430
+ if (held.phase === "finished") return {
7431
+ stop: false,
7432
+ delivered: false
7433
+ };
7434
+ if (!page.ok) return {
7435
+ stop: false,
7436
+ delivered: false
7437
+ };
7438
+ if (page.phase === "finished" && page.t1 !== null) {
7439
+ const now = Date.now();
7440
+ if (held.phase === "recording") input.store.finished(input.sessionId, page.t1, now);
7441
+ const caught = page.calls.length;
7442
+ const summary = `工作台 DIY:他按了「结束录制」(${String(caught)} 条)`;
7443
+ deliverDebugNotice(input.ctx.get("agents"), input.sessionId, noticeMessage(`他按了「结束录制」(页面钩子上 ${String(caught)} 条)—— 去 ${DIY_RECORD_READ_TOOL} 取这一段清单,要某一条原文用 ${DIY_RECORD_DETAIL_TOOL}。`, summary), "wake");
7444
+ return {
7445
+ stop: false,
7446
+ delivered: true
7447
+ };
7448
+ }
7449
+ if (page.phase === "recording" && page.t0 !== null && held.phase === "armed") input.store.started(input.sessionId, page.t0, Date.now());
7450
+ return {
7451
+ stop: false,
7452
+ delivered: false
7453
+ };
7454
+ }
7455
+ function watchRecordFinish(input) {
7456
+ let misses = 0;
7457
+ const tick = () => {
7458
+ watchFinishStepOf(input).then((out) => {
7459
+ misses = 0;
7460
+ if (!out.stop) setTimeout(tick, RECORD_WATCH_MS);
7461
+ }).catch(() => {
7462
+ misses += 1;
7463
+ if (misses < RECORD_WATCH_TRIES) setTimeout(tick, RECORD_WATCH_MS);
7464
+ });
7465
+ };
7466
+ setTimeout(tick, RECORD_WATCH_MS);
7467
+ }
7468
+ async function captureLinesOf(shell, file, timeoutMs = START_TIMEOUT_MS) {
7469
+ const step = await runShell(shell, `wc -l < ${quoteShellArg(file)} 2>/dev/null || echo 0`, { timeoutMs });
7470
+ const value = Number.parseInt(step.stdout.trim() === "" ? "0" : step.stdout.trim(), 10);
7471
+ return Number.isFinite(value) ? value : 0;
7472
+ }
7473
+ async function captureRoundOf(shell, file, timeoutMs = START_TIMEOUT_MS) {
7474
+ const value = (await runShell(shell, `cat ${quoteShellArg(`${file}.first`)} 2>/dev/null || true`, { timeoutMs })).stdout.trim();
7475
+ return value === "ok" || value === "fail" ? value : "";
7476
+ }
7477
+ async function captureTextOf(shell, file, timeoutMs = START_TIMEOUT_MS) {
7478
+ const step = await runShell(shell, `tail -n ${String(CAPTURE_TAIL_LINES)} ${quoteShellArg(file)} 2>/dev/null || true`, { timeoutMs });
7479
+ return {
7480
+ text: step.stdout,
7481
+ truncated: step.truncated
7482
+ };
7483
+ }
7484
+ async function captureReadOf(shell, file, timeoutMs = START_TIMEOUT_MS) {
7485
+ const totalLines = await captureLinesOf(shell, file, timeoutMs);
7486
+ const read = await captureTextOf(shell, file, timeoutMs);
7487
+ const round = await captureRoundOf(shell, file, timeoutMs);
7488
+ return {
7489
+ text: read.text,
7490
+ totalLines,
7491
+ tailLimit: CAPTURE_TAIL_LINES,
7492
+ truncated: read.truncated || totalLines > CAPTURE_TAIL_LINES,
7493
+ round
7494
+ };
7495
+ }
7496
+ async function keepCaptureOf(shell, file, timeoutMs = START_TIMEOUT_MS) {
7497
+ return (await runShell(shell, `[ -f ${quoteShellArg(file)} ] && mv ${quoteShellArg(file)} ${quoteShellArg(lastCaptureFileOf(file))} && [ -s ${quoteShellArg(lastCaptureFileOf(file))} ]`, { timeoutMs })).exitCode === 0;
7498
+ }
7499
+ async function captureLinesWithKeyOf(shell, file, key, timeoutMs = START_TIMEOUT_MS) {
7500
+ const quoted = quoteShellArg(file);
7501
+ const compact = quoteShellArg(`"key":"${key}"`);
7502
+ const spaced = quoteShellArg(`"key": "${key}"`);
7503
+ return (await runShell(shell, [`[ -f ${quoted} ] || exit 0`, `grep -m 20 -F ${compact} ${quoted} 2>/dev/null || grep -m 20 -F ${spaced} ${quoted} 2>/dev/null || true`].join("; "), { timeoutMs })).stdout;
7504
+ }
7505
+ function bundleOf(capture, page, state) {
7506
+ const entries = captureEntriesOf(capture.text);
7507
+ const segment = segmentOf(entries, page.t0 ?? state.t0, page.t1 ?? state.t1);
7508
+ const merged = mergeCalls(page.calls, captureCallsOf(segment.inside));
7509
+ return {
7510
+ capture,
7511
+ entries,
7512
+ inside: segment.inside,
7513
+ outside: segment.outside,
7514
+ merged,
7515
+ rows: listRowsOf(merged, state.expect)
7516
+ };
7517
+ }
7518
+ function hookCallOf(page, key) {
7519
+ const want = key.trim();
7520
+ const bare = want.replace(/#\d+$/, "");
7521
+ for (const one of page.calls) {
7522
+ if (one.key !== "" && one.key === want) return one;
7523
+ if (`${one.method} ${one.url}` === bare) return one;
7524
+ }
7525
+ return null;
7526
+ }
7527
+ var DETAIL_WHERE_CAPTURE = "会话捕获(浏览器那一层,快照原文)";
7528
+ var DETAIL_WHERE_HOOK = "页面钩子(这一段)";
7529
+ function detailFactsOf(input) {
7530
+ return plainJson({
7531
+ ok: true,
7532
+ found: true,
7533
+ key: input.key,
7534
+ method: input.method,
7535
+ url: input.url,
7536
+ status: input.status,
7537
+ ct: input.ct,
7538
+ at: input.at,
7539
+ request: input.request,
7540
+ response: input.response,
7541
+ headers: input.headers,
7542
+ requestFullSize: input.requestFullSize,
7543
+ responseFullSize: input.responseFullSize,
7544
+ requestSizeExact: input.requestSizeExact,
7545
+ responseSizeExact: input.responseSizeExact,
7546
+ ambiguous: input.ambiguous,
7547
+ source: input.source,
7548
+ text: renderRecordDetail({
7549
+ found: true,
7550
+ where: input.where,
7551
+ method: input.method,
7552
+ url: input.url,
7553
+ status: input.status,
7554
+ ct: input.ct,
7555
+ at: input.at,
7556
+ request: input.request,
7557
+ response: input.response,
7558
+ headers: input.headers,
7559
+ requestFullSize: input.requestFullSize,
7560
+ responseFullSize: input.responseFullSize,
7561
+ requestSizeExact: input.requestSizeExact,
7562
+ responseSizeExact: input.responseSizeExact,
7563
+ ambiguous: input.ambiguous,
7564
+ source: input.source,
7565
+ maxBytes: input.maxBytes,
7566
+ note: ""
7567
+ })
7568
+ });
7569
+ }
7570
+ function detailOfEntry(entry, maxBytes, ambiguous = 1) {
7571
+ return detailFactsOf({
7572
+ where: DETAIL_WHERE_CAPTURE,
7573
+ key: entry.key,
7574
+ method: entry.method,
7575
+ url: entry.url,
7576
+ status: entry.status,
7577
+ ct: entry.ct,
7578
+ at: entry.at,
7579
+ request: entry.request,
7580
+ response: entry.response,
7581
+ headers: entry.headers,
7582
+ requestFullSize: entry.requestFullSize,
7583
+ responseFullSize: entry.responseFullSize,
7584
+ requestSizeExact: entry.requestSizeExact,
7585
+ responseSizeExact: entry.responseSizeExact,
7586
+ ambiguous,
7587
+ source: "capture",
7588
+ maxBytes
7589
+ });
7590
+ }
7591
+ function detailOfCall(call, maxBytes) {
7592
+ return detailFactsOf({
7593
+ where: DETAIL_WHERE_HOOK,
7594
+ key: call.key,
7595
+ method: call.method,
7596
+ url: call.url,
7597
+ status: call.status,
7598
+ ct: call.ct,
7599
+ at: call.at,
7600
+ request: call.body,
7601
+ response: call.response,
7602
+ headers: "",
7603
+ requestFullSize: call.body.length,
7604
+ responseFullSize: call.response.length,
7605
+ requestSizeExact: false,
7606
+ responseSizeExact: false,
7607
+ ambiguous: 0,
7608
+ source: "hook",
7609
+ maxBytes
7610
+ });
7611
+ }
7612
+ function tabsOf(raw) {
7613
+ const start = raw.indexOf("[");
7614
+ const end = raw.lastIndexOf("]");
7615
+ if (start === -1 || end <= start) return [];
7616
+ try {
7617
+ const parsed = JSON.parse(raw.slice(start, end + 1));
7618
+ if (!Array.isArray(parsed)) return [];
7619
+ return parsed.filter((one) => typeof one === "object" && one !== null);
7620
+ } catch {
7621
+ return [];
7622
+ }
7623
+ }
7624
+ async function closeRecordTab(shell, command, session, url) {
7625
+ const pages = tabsOf((await actBrowser(shell, command, session, ["tab", "list"])).stdout);
7626
+ const pick = pages.find((one) => typeof one["url"] === "string" && one["url"] === url) ?? pages[0];
7627
+ const page = pick === void 0 ? "" : typeof pick["page"] === "string" ? pick["page"] : "";
7628
+ if (page !== "") {
7629
+ if ((await actBrowser(shell, command, session, [
7630
+ "tab",
7631
+ "close",
7632
+ page
7633
+ ])).ok) return true;
7634
+ }
7635
+ return (await actBrowser(shell, command, session, ["close"])).ok;
7636
+ }
7637
+ async function releaseRecord(input) {
7638
+ const capture = await stopNetworkExact(input.shell, input.captureFile);
7639
+ return {
7640
+ tab: await closeRecordTab(input.shell, input.command, input.session, input.url),
7641
+ capture
7642
+ };
7643
+ }
7644
+ async function releaseQuietly(input) {
7645
+ try {
7646
+ return await releaseRecord(input);
7647
+ } catch {
7648
+ return {
7649
+ tab: false,
7650
+ capture: false
7651
+ };
7652
+ }
7653
+ }
7654
+ //#endregion
7655
+ //#region src/modules/panels/model/diy-record-tools.ts
7656
+ function registerRecordTools(ctx, store) {
7657
+ const tools = ctx.get("tools");
7658
+ if (tools === void 0) {
7659
+ ctx.logger?.info?.("[dsh-workbench] 没有 tools 服务,跳过录制交接工具注册");
7660
+ return [];
7661
+ }
7662
+ const prepareTool = {
7663
+ name: DIY_RECORD_PREPARE_TOOL,
7664
+ description: `开一扇浏览器窗口并顶到最前,让他自己录一次真实的写(开窗要点头一次);他按开始/结束后用 ${DIY_RECORD_READ_TOOL} 取那一段、${DIY_RECORD_STOP_TOOL} 收工。`,
7665
+ parameters: {
7666
+ type: "object",
7667
+ properties: {
7668
+ unit: {
7669
+ type: "string",
7670
+ description: "工坊里的 unit(workbench_diy_list 会给)。"
7671
+ },
7672
+ url: {
7673
+ type: "string",
7674
+ description: "那一页(他平时看这套系统的入口;与已探明的读源同 domain)。"
7675
+ },
7676
+ doing: {
7677
+ type: "string",
7678
+ description: "那一下叫什么,例:发布 / 提交(不是多路复用:这把工具只做录制这一件事)。"
7679
+ },
7680
+ expect: {
7681
+ type: "object",
7682
+ properties: {
7683
+ method: {
7684
+ type: "string",
7685
+ description: "想抓的那条的 HTTP 方法,例 POST(不填就按\"像写的那几条\"挑)。"
7686
+ },
7687
+ urlContains: {
7688
+ type: "string",
7689
+ description: "URL 里必须含的片段(不填就不筛)。"
7690
+ }
7691
+ },
7692
+ additionalProperties: false,
7693
+ description: "可选:你想抓的那条长什么样。"
7694
+ }
7695
+ },
7696
+ required: [
7697
+ "unit",
7698
+ "url",
7699
+ "doing"
7700
+ ],
7701
+ additionalProperties: false
7702
+ },
7703
+ output: OUTPUT$2,
7704
+ async execute(raw, exec) {
7705
+ const { packagesDir, workspace } = workshopOf(ctx, exec);
7706
+ const args = asRecord$3(raw);
7707
+ const unit = asText$1(args["unit"]);
7708
+ const modules = modulesOf(ctx, packagesDir).modules;
7709
+ if (unit === "") throw new Error(`要一个 unit(工坊里:${modules.map((one) => one.unit).join(", ") || "(空)"})`);
7710
+ const found = modules.find((one) => one.unit === unit);
7711
+ if (found === void 0) throw new Error(`工坊里没有单元 id 为「${unit}」的模块(可用的:${modules.map((one) => one.unit).join(", ") || "(空)"})`);
7712
+ const url = asText$1(args["url"]);
7713
+ if (url === "") throw new Error("要那一页的 url(他平时看这套系统的入口;先用 workbench_diy_recon 或问他\"你平时在哪看这个\")—— 我不猜是哪个站点。");
7714
+ const foreign = foreignDomainText(readDesignOfModule(found.dir), url);
7715
+ if (foreign !== "") throw new Error(foreign);
7716
+ const doing = asText$1(args["doing"]);
7717
+ const rawExpect = asRecord$3(args["expect"]);
7718
+ const method = asText$1(rawExpect["method"]).toUpperCase();
7719
+ const urlContains = asText$1(rawExpect["urlContains"]);
7720
+ const expect = method === "" && urlContains === "" ? null : {
7721
+ method,
7722
+ urlContains
7723
+ };
7724
+ const shell = ctx.get("shell");
7725
+ if (shell === void 0) throw new Error("这台机器上没有 shell 服务,开不了录制窗");
7726
+ const command = opencliCommandOf(ctx).command;
7727
+ const sessionId = sessionIdOf(exec) ?? "";
7728
+ const held = store.live(Date.now());
7729
+ const captureFile = captureFileOf(packagesDir, unit);
7730
+ moveOrThrow(store.begin({
7731
+ sessionId,
7732
+ workspace: workspace ?? "",
7733
+ unit,
7734
+ name: found.name,
7735
+ url,
7736
+ title: "",
7737
+ action: doing,
7738
+ expect,
7739
+ captureFile,
7740
+ now: Date.now()
7741
+ }), held);
7742
+ try {
7743
+ const opened = await runBrowserStep(shell, command, [
7744
+ "browser",
7745
+ BROWSER_RECORD_SESSION,
7746
+ "tab",
7747
+ "new",
7748
+ url,
7749
+ "--window",
7750
+ "foreground"
7751
+ ], OPEN_TIMEOUT_MS);
7752
+ if (!opened.ok) throw new Error(`开不了录制窗:${opened.stderr || opened.stdout || "opencli 报了个错"}。先让用户把工作台那一页切到前台(第一次要这样),或跑 opencli doctor 看扩展连没连上。`);
7753
+ await runBrowserStep(shell, command, [
7754
+ "browser",
7755
+ BROWSER_RECORD_SESSION,
7756
+ "wait",
7757
+ "time",
7758
+ String(SETTLE_MS / 1e3)
7759
+ ], OPEN_TIMEOUT_MS);
7760
+ const armed = await actBrowser(shell, command, BROWSER_RECORD_SESSION, ["eval", RECORD_ARM_SCRIPT]);
7761
+ const page = recordPageOf(armed.payload);
7762
+ if (!armed.ok || !page.ok) throw new Error(`浮条没装上:${armed.stderr || armed.stdout || "那一页还在加载"} —— 等他那一页站稳了再试。`);
7763
+ await stopNetworkExact(shell, captureFile);
7764
+ const capture = await startRecordCapture(shell, command, BROWSER_RECORD_SESSION, captureFile);
7765
+ watchRecordFinish({
7766
+ ctx,
7767
+ shell,
7768
+ command,
7769
+ sessionId,
7770
+ store,
7771
+ url,
7772
+ captureFile
7773
+ });
7774
+ const talk = recordTalkOf();
7775
+ return plainJson({
7776
+ workshop: packagesDir,
7777
+ ok: true,
7778
+ wrote: false,
7779
+ phase: page.phase,
7780
+ site: url,
7781
+ session: BROWSER_RECORD_SESSION,
7782
+ capture,
7783
+ captureFile,
7784
+ size: page.calls.length,
7785
+ talk,
7786
+ text: capture ? talk : `${talk}(opencli 那条捕获没起来 —— 这次只有页面钩子)`
7787
+ });
7788
+ } catch (error) {
7789
+ await releaseQuietly({
7790
+ shell,
7791
+ command,
7792
+ session: BROWSER_RECORD_SESSION,
7793
+ url,
7794
+ captureFile
7795
+ });
7796
+ store.stop(sessionId, Date.now());
7797
+ throw error;
7798
+ }
7799
+ }
7800
+ };
7801
+ const readTool = {
7802
+ name: DIY_RECORD_READ_TOOL,
7803
+ description: "读这一段录到的请求(他按「结束录制」之后用);没按开始 / 没装钩子都会说清,不假装是\"没有请求\"。",
7804
+ parameters: {
7805
+ type: "object",
7806
+ properties: { waitMs: {
7807
+ type: "integer",
7808
+ description: "可选:阻塞等他按「结束」最多这么久(毫秒,默认 0 = 立刻回;帽 15 分钟)。"
7809
+ } },
7810
+ additionalProperties: false
7811
+ },
7812
+ output: OUTPUT$2,
7813
+ async execute(raw, exec) {
7814
+ const { packagesDir } = workshopOf(ctx, exec);
7815
+ const shell = ctx.get("shell");
7816
+ if (shell === void 0) throw new Error("这台机器上没有 shell 服务,读不了录制");
7817
+ const sessionId = sessionIdOf(exec) ?? "";
7818
+ const held = store.live(Date.now());
7819
+ if (held === null) throw new Error(recordMissingText());
7820
+ if (held.sessionId !== sessionId) moveOrThrow({
7821
+ ok: false,
7822
+ code: "not-owner"
7823
+ }, held);
7824
+ const command = opencliCommandOf(ctx).command;
7825
+ const budget = waitOf(asRecord$3(raw)["waitMs"]);
7826
+ const opened = budget === 0 ? 0 : Date.now();
7827
+ const deadline = opened + budget;
7828
+ const sync = (page) => {
7829
+ if (page.ok && page.phase === "recording" && page.t0 !== null) {
7830
+ if (store.live(Date.now())?.phase === "armed") store.started(sessionId, page.t0, Date.now());
7831
+ }
7832
+ if (page.ok && page.phase === "finished" && page.t1 !== null) {
7833
+ if (store.live(Date.now())?.phase === "armed" && page.t0 !== null) store.started(sessionId, page.t0, Date.now());
7834
+ if (store.live(Date.now())?.phase === "recording") store.finished(sessionId, page.t1, Date.now());
7835
+ }
7836
+ };
7837
+ await actBrowser(shell, command, BROWSER_RECORD_SESSION, ["eval", RECORD_ARM_SCRIPT]);
7838
+ let step = await actBrowser(shell, command, BROWSER_RECORD_SESSION, ["eval", RECORD_READ_SCRIPT]);
7839
+ let page = recordPageOf(step.payload);
7840
+ sync(page);
7841
+ while (budget > 0 && page.phase !== "finished" && Date.now() < deadline) {
7842
+ await pauseFor(Math.min(POLL_MS, Math.max(0, deadline - Date.now())));
7843
+ step = await actBrowser(shell, command, BROWSER_RECORD_SESSION, ["eval", RECORD_READ_SCRIPT]);
7844
+ const next = recordPageOf(step.payload);
7845
+ if (next.ok) page = next;
7846
+ sync(page);
7847
+ }
7848
+ const timedOut = budget > 0 && page.phase !== "finished";
7849
+ const waitedMs = opened === 0 ? 0 : Date.now() - opened;
7850
+ if (timedOut) {
7851
+ const waited = String(Math.round(budget / 1e3));
7852
+ holdNotice(sessionId, noticeMessage(`录制还没结束(等了约 ${waited} 秒):他按了「结束」再 read 一次。`, `工作台 DIY:录制还没结束(等了约 ${waited} 秒)`));
7853
+ }
7854
+ const state = store.live(Date.now()) ?? held;
7855
+ const capture = await captureReadOf(shell, state.captureFile);
7856
+ const bundle = bundleOf(capture, page, state);
7857
+ const hookCaught = page.calls.length;
7858
+ const nav = page.href !== "" && page.here !== "" && page.href !== page.here;
7859
+ const noBody = bodyMissingOf(bundle.merged);
7860
+ const text = step.ok ? [renderRecordRead({
7861
+ phase: page.phase,
7862
+ armed: page.ok,
7863
+ hookCaught,
7864
+ rows: bundle.rows,
7865
+ merged: bundle.merged.length,
7866
+ capturedLines: bundle.entries.length,
7867
+ capturedInSegment: bundle.inside.length,
7868
+ capturedOutside: bundle.outside,
7869
+ capturedTotalLines: capture.totalLines,
7870
+ capturedTruncated: capture.truncated,
7871
+ capturedTailLimit: capture.tailLimit,
7872
+ dropped: page.dropped,
7873
+ nav,
7874
+ filter: filterTextOf(state.expect),
7875
+ captureRound: bundle.capture.round
7876
+ }), timedOut ? `还在录 —— 等他按「结束」(要等就带 waitMs,帽 ${String(WAIT_CAP_MS / 6e4)} 分钟)。` : ""].filter((one) => one !== "").join("\n") : `读不到那一页上的录制条:${step.stderr || step.stdout || "opencli 报了个错"}`;
7877
+ return plainJson({
7878
+ workshop: packagesDir,
7879
+ ok: step.ok && page.ok,
7880
+ phase: state.phase,
7881
+ t0: state.t0,
7882
+ t1: state.t1,
7883
+ rows: bundle.rows,
7884
+ dropped: page.dropped,
7885
+ captureLines: bundle.entries.length,
7886
+ capturedInSegment: bundle.inside.length,
7887
+ capturedOutside: bundle.outside,
7888
+ capturedTotalLines: capture.totalLines,
7889
+ capturedTruncated: capture.truncated,
7890
+ hookCaught,
7891
+ noBody,
7892
+ nav,
7893
+ waitedMs,
7894
+ timedOut,
7895
+ text
7896
+ });
7897
+ }
7898
+ };
7899
+ const detailTool = {
7900
+ name: DIY_RECORD_DETAIL_TOOL,
7901
+ description: "取录制这一段里某一条请求的原文:入参(request body)与出参(response body)。先用 record_read 拿清单(每条带 key),再用它挑一条 —— 清单不删条,\"那一条\"永远挑得到。",
7902
+ parameters: {
7903
+ type: "object",
7904
+ properties: {
7905
+ key: {
7906
+ type: "string",
7907
+ description: "清单里那一条的 key(原样给,含 #2 这种后缀)。"
7908
+ },
7909
+ index: {
7910
+ type: "integer",
7911
+ description: "清单里的序号(0 起);只在那一场还开着时可用。"
7912
+ },
7913
+ at: {
7914
+ type: "integer",
7915
+ description: "清单里那一行的 at(毫秒时间戳):同一个 key 跨轮会重名,带上它才钉得住是哪一次。"
7916
+ },
7917
+ unit: {
7918
+ type: "string",
7919
+ description: "那一场已经收工、只能去读 shots/rec-<unit>.jsonl.last 时给。"
7920
+ },
7921
+ maxBody: {
7922
+ type: "integer",
7923
+ description: `入参 / 出参各自最多回多少字符(默认 ${String(DETAIL_MAX_BYTES)},0 = 不限)。`
7924
+ }
7925
+ },
7926
+ additionalProperties: false
7927
+ },
5812
7928
  output: OUTPUT$2,
5813
7929
  async execute(raw, exec) {
5814
- const { packagesDir, workspace } = workshopOf(ctx, exec);
5815
- const found = unitOf(packagesDir, raw, "workbench_diy_install");
5816
- if (found.installed) {
5817
- const panel = showOrReason(panels, found.unit, workspace);
5818
- const text = ["它已经装过了(就是工坊里这一份)—— **不要再装**:改源码 + dsh-wb-module build(宿主侧与 client.js 都秒级热换 —— dsh-client-hmr 盯着每个 bundle 的 lib/client.js,重建了就换进已打开的页面)。", panel.ok ? `面板上已经打开它了(${workspace})—— 验收就走这一张卡(你自己走一遍)` : `面板上还没打开:${panel.reason}。调 workbench_diy_show(unit=${found.unit})`].join("\n");
5819
- return plainJson({
5820
- workshop: packagesDir,
5821
- unit: found.unit,
5822
- path: found.dir,
5823
- alreadyInstalled: true,
5824
- panel,
5825
- text
5826
- });
7930
+ const { packagesDir } = workshopOf(ctx, exec);
7931
+ const shell = ctx.get("shell");
7932
+ if (shell === void 0) throw new Error("这台机器上没有 shell 服务,取不了详情");
7933
+ const args = asRecord$3(raw);
7934
+ const unit = asText$1(args["unit"]);
7935
+ const askedKey = asText$1(args["key"]).trim();
7936
+ const rawIndex = args["index"];
7937
+ const index = typeof rawIndex === "number" && Number.isFinite(rawIndex) ? Math.floor(rawIndex) : null;
7938
+ const rawAt = args["at"];
7939
+ let at = typeof rawAt === "number" && Number.isFinite(rawAt) ? Math.floor(rawAt) : null;
7940
+ const rawMax = args["maxBody"];
7941
+ const maxBytes = typeof rawMax === "number" && Number.isFinite(rawMax) && rawMax >= 0 ? Math.floor(rawMax) : DETAIL_MAX_BYTES;
7942
+ const sessionId = sessionIdOf(exec) ?? "";
7943
+ const held = store.live(Date.now());
7944
+ if (held !== null && held.sessionId !== sessionId) moveOrThrow({
7945
+ ok: false,
7946
+ code: "not-owner"
7947
+ }, held);
7948
+ const file = held !== null ? held.captureFile : unit === "" ? "" : lastCaptureFileOf(captureFileOf(packagesDir, unit));
7949
+ if (file === "") throw new Error(`这一场录制已经不在了(收工 / 过期 / 不是你这个会话的)—— 要给一个 unit(我去读 shots/rec-<unit>.jsonl.last),或者重新 ${DIY_RECORD_PREPARE_TOOL} 一场。`);
7950
+ const command = opencliCommandOf(ctx).command;
7951
+ let key = askedKey;
7952
+ let page = null;
7953
+ if (key === "") {
7954
+ if (index === null) throw new Error("给一个 key(清单里那一条的)或 index(清单里的序号)。");
7955
+ if (held === null) throw new Error("收工之后只能用 key —— index 是\"那一场清单里\"的序号,这一场已经清掉了。");
7956
+ page = recordPageOf((await actBrowser(shell, command, BROWSER_RECORD_SESSION, ["eval", RECORD_READ_SCRIPT])).payload);
7957
+ const bundle = bundleOf(await captureReadOf(shell, file), page, held);
7958
+ const row = bundle.rows[index];
7959
+ if (row === void 0) throw new Error(`这一段清单里没有第 ${String(index)} 条(一共 ${String(bundle.rows.length)} 条)—— 先 ${DIY_RECORD_READ_TOOL} 拿一份新的清单。`);
7960
+ key = row.key;
7961
+ at = at ?? row.at;
7962
+ if (key === "") {
7963
+ const only = hookCallOf(page, `${row.method} ${row.url}`);
7964
+ if (only !== null) return detailOfCall(only, maxBytes);
7965
+ }
5827
7966
  }
5828
- const outcome = await market.installBundle(found.dir);
5829
- const verdict = verdictOf$1(outcome.change);
5830
- const restartRequired = outcome.change.application === "restart-required";
5831
- const panel = outcome.change.application === "applied" ? showOrReason(panels, found.unit, workspace) : {
7967
+ const candidates = pickAllOf(captureEntriesOf(await captureLinesWithKeyOf(shell, file, key)), key);
7968
+ const fromCapture = pickOf(candidates, key, at);
7969
+ if (fromCapture !== null) return detailOfEntry(fromCapture, maxBytes, candidates.length);
7970
+ const call = hookCallOf(page ?? recordPageOf((await actBrowser(shell, command, "workbench-rec", ["eval", RECORD_READ_SCRIPT])).payload), key);
7971
+ if (call !== null) return detailOfCall(call, maxBytes);
7972
+ return plainJson({
7973
+ workshop: packagesDir,
7974
+ ok: true,
7975
+ found: false,
7976
+ key,
7977
+ text: renderRecordDetail({
7978
+ found: false,
7979
+ where: "",
7980
+ method: "",
7981
+ url: "",
7982
+ status: 0,
7983
+ ct: "",
7984
+ at: 0,
7985
+ request: "",
7986
+ response: "",
7987
+ headers: "",
7988
+ requestFullSize: 0,
7989
+ responseFullSize: 0,
7990
+ requestSizeExact: false,
7991
+ responseSizeExact: false,
7992
+ ambiguous: 0,
7993
+ source: "",
7994
+ maxBytes,
7995
+ note: `「${key}」既不在这一场的快照原文(${file})里,也不在这一页的钩子那一份里`
7996
+ })
7997
+ });
7998
+ }
7999
+ };
8000
+ const stopTool = {
8001
+ name: DIY_RECORD_STOP_TOOL,
8002
+ description: "录制收工:卸浮条、停捕获、收掉那个标签、清状态(不动你的其他标签)。",
8003
+ parameters: {
8004
+ type: "object",
8005
+ properties: {},
8006
+ additionalProperties: false
8007
+ },
8008
+ output: OUTPUT$2,
8009
+ async execute(_raw, exec) {
8010
+ const { packagesDir } = workshopOf(ctx, exec);
8011
+ const shell = ctx.get("shell");
8012
+ if (shell === void 0) throw new Error("这台机器上没有 shell 服务,收不了工");
8013
+ const sessionId = sessionIdOf(exec) ?? "";
8014
+ const held = store.live(Date.now());
8015
+ if (held === null) throw new Error(recordMissingText());
8016
+ if (held.sessionId !== sessionId) moveOrThrow({
5832
8017
  ok: false,
5833
- reason: `安装结果是 ${outcome.change.application},还没生效`
5834
- };
5835
- const panelText = panel.ok ? `面板上已经打开它了(${workspace})—— 验收就走这一张卡(你自己走一遍)` : outcome.change.application === "applied" ? `面板上还没打开:${panel.reason}。调 workbench_diy_show(unit=${found.unit})试试。` : restartRequired ? `面板上还没打开(${panel.reason}):模块要**重启一次工作台**才注册进面板 —— 把这句话转给用户,重启后再调 workbench_diy_show(unit=${found.unit})。` : `面板上还没打开:${panel.reason}。先把上面那句"没装成"处理掉,再谈上板。`;
5836
- const text = [
5837
- `${verdict}:${found.unit}(${found.name})`,
5838
- panelText,
5839
- "之后只改源码 + dsh-wb-module build(宿主侧与 client.js 都秒级热换 —— dsh-client-hmr 盯着每个 bundle 的 lib/client.js,重建了就换进已打开的页面):不要再装一遍。"
5840
- ].join("\n");
8018
+ code: "not-owner"
8019
+ }, held);
8020
+ const command = opencliCommandOf(ctx).command;
8021
+ await actBrowser(shell, command, BROWSER_RECORD_SESSION, ["eval", RECORD_ARM_SCRIPT]);
8022
+ const page = recordPageOf((await actBrowser(shell, command, BROWSER_RECORD_SESSION, ["eval", RECORD_STOP_SCRIPT])).payload);
8023
+ const bundle = bundleOf(await captureReadOf(shell, held.captureFile), page, held);
8024
+ const hookCaught = page.calls.length;
8025
+ const noBody = bodyMissingOf(bundle.merged);
8026
+ const kept = await keepCaptureOf(shell, held.captureFile);
8027
+ const cleaned = await releaseQuietly({
8028
+ shell,
8029
+ command,
8030
+ session: BROWSER_RECORD_SESSION,
8031
+ url: held.url,
8032
+ captureFile: held.captureFile
8033
+ });
8034
+ store.stop(sessionId, Date.now());
8035
+ const probeCommand = probeCommandOf(bundle.merged);
8036
+ const lastFile = lastCaptureFileOf(held.captureFile);
5841
8037
  return plainJson({
5842
8038
  workshop: packagesDir,
5843
- unit: found.unit,
5844
- path: found.dir,
5845
- install: outcome.change,
5846
- module: outcome.module,
5847
- panel,
5848
- text
8039
+ ok: true,
8040
+ phase: "idle",
8041
+ rows: bundle.rows,
8042
+ lastFile: kept ? lastFile : "",
8043
+ dropped: page.dropped,
8044
+ captureLines: bundle.entries.length,
8045
+ capturedInSegment: bundle.inside.length,
8046
+ capturedOutside: bundle.outside,
8047
+ hookCaught,
8048
+ noBody,
8049
+ probeCommand,
8050
+ cleaned: {
8051
+ ...cleaned,
8052
+ hook: page.ok && page.unhooked
8053
+ },
8054
+ text: [
8055
+ `收工了:${held.unit} 的「${held.action}」这一段一共 ${String(bundle.merged.length)} 条请求(页面钩子 ${String(hookCaught)} 条 · 会话捕获这一段 ${String(bundle.inside.length)} 条)。`,
8056
+ kept ? `原文留在 ${lastFile}(下一场 prepare 会清掉它)—— 要再看某一条用 ${DIY_RECORD_DETAIL_TOOL}(给 unit + key;同一个 key 跨轮会重名,再带上清单里那行的 at 就钉住了)。` : "没留下原文(这一场的捕获文件是空的或没起来)—— 要哪一条就让他从 DevTools 复制 curl。",
8057
+ bundle.capture.round === "fail" ? "(会话捕获最近一轮没跑成(opencli 那条报了错)—— 这一场可能只有页面钩子。)" : "",
8058
+ cleaned.tab ? "录制窗那个标签收掉了。" : "那个标签没关掉(他自己关一下)。",
8059
+ cleaned.capture ? "" : "捕获那条没停干净(最多 15 分钟自己收工)。",
8060
+ probeCommand !== "" ? "这段原文可以直接落 design.json 的 writes[].probeCommand。" : noBody ? "只有方法 / URL,body 拿不到 —— 补到 body 再落 writes[].probeCommand;要体就让他从 DevTools 复制 curl,或在他那一页上用 _eval 里的 fetch 打一条同形的(先问过他)。" : "这一段的原文是空的 —— 别硬写进 design.json。",
8061
+ "不许复现这一段(没预览档的写一次就是真的)。"
8062
+ ].filter((one) => one !== "").join("\n")
5849
8063
  });
5850
8064
  }
5851
8065
  };
5852
8066
  return [
5853
- tools.register(listTool),
5854
- tools.register(statusTool),
5855
- tools.register(panelTool),
5856
- tools.register(reconTool),
5857
- ...registerSpecTool(ctx, tools),
5858
- tools.register(probeTool),
5859
- ...registerLogTools(ctx, tools),
5860
- ...registerBrowserTools(ctx, tools),
5861
- ...registerDebugTools(ctx, tools, debug),
5862
- tools.register(showTool),
5863
- tools.register(installTool)
8067
+ tools.register(prepareTool),
8068
+ tools.register(readTool),
8069
+ tools.register(detailTool),
8070
+ tools.register(stopTool)
5864
8071
  ];
5865
8072
  }
5866
8073
  //#endregion
5867
- //#region src/modules/panels/model/diy-observers.ts
5868
- async function stopObservers(ctx, session = BROWSER_SCREEN_SESSION) {
5869
- const packagesDir = workshopPackagesDir(ctx);
5870
- const shell = ctx.get("shell");
5871
- if (packagesDir === null || shell === void 0) return {
5872
- net: false,
5873
- logs: false
5874
- };
5875
- const file = join(dirname(packagesDir), "shots", `net-${session}.jsonl`);
5876
- const command = opencliCommandOf(ctx).command;
5877
- const [net, step] = await Promise.all([stopNetworkExact(shell, file), actBrowser(shell, command, session, ["eval", LOGS_STOP_SCRIPT], SWEEP_TIMEOUT_MS)]);
5878
- return {
5879
- net,
5880
- logs: typeof step.payload?.["unhooked"] === "number"
5881
- };
5882
- }
5883
- function withObserverCleanup(store, cleanup, logger) {
5884
- let pending = Promise.resolve();
5885
- let epoch = 0;
5886
- const sweep = async (at) => {
5887
- if (epoch !== at) {
5888
- logger?.warn?.("收工那一拍:已经有新的调试约定开始了 —— 跳过收观察(新那一场自己收)");
5889
- return;
5890
- }
5891
- try {
5892
- const out = await cleanup();
5893
- if (!out.net || !out.logs) logger?.warn?.(`收工那一拍没把观察收干净(网络 ${String(out.net)} / 日志 ${String(out.logs)})—— 网络那条最多 15 分钟自己收工,日志钩子随页面刷新消失`);
5894
- } catch (error) {
5895
- logger?.warn?.(`收工那一拍收观察抛了:${error instanceof Error ? error.message : String(error)}`);
5896
- }
5897
- };
5898
- return {
5899
- live: (now) => store.live(now),
5900
- begin(pact) {
5901
- epoch += 1;
5902
- return store.begin(pact);
5903
- },
5904
- pause: (sessionId, now) => store.pause(sessionId, now),
5905
- resume: (sessionId, now) => store.resume(sessionId, now),
5906
- stop(sessionId, now) {
5907
- const move = store.stop(sessionId, now);
5908
- if (move.ok) {
5909
- const at = epoch;
5910
- pending = pending.then(() => sweep(at));
5911
- }
5912
- return move;
5913
- },
5914
- settle: () => pending
5915
- };
5916
- }
5917
- //#endregion
5918
- //#region src/modules/panels/model/diy-debug-routes.ts
5919
- function diyDebugRoutes(store) {
5920
- return { "panels/diy-debug": () => ({
5921
- ok: true,
5922
- state: store.live(Date.now())
5923
- }) };
5924
- }
5925
- //#endregion
5926
8074
  //#region src/shared/api/host/release.ts
5927
8075
  function parseLedgerVersion(stdout, name) {
5928
8076
  const parsed = parseJsonLoose(stdout);
@@ -6813,7 +8961,7 @@ function createEnvDropper(options) {
6813
8961
  async function rememberInstalledSource(deps, manager, pkg, registry) {
6814
8962
  const { registries, logger } = deps;
6815
8963
  if (registries === void 0 || registry === void 0) return;
6816
- const scope = scopeOf$1(pkg);
8964
+ const scope = scopeOf$2(pkg);
6817
8965
  if (scope === null) return;
6818
8966
  const known = await manager.registries().catch(() => null);
6819
8967
  if (known !== null && sameRegistry(known.resolved, registry)) return;
@@ -7440,7 +9588,7 @@ function registerMarketTools(ctx, market) {
7440
9588
  return {
7441
9589
  name: spec.name,
7442
9590
  description: spec.description,
7443
- parameters: withApprovalNote({
9591
+ parameters: {
7444
9592
  type: "object",
7445
9593
  properties: { id: {
7446
9594
  type: "string",
@@ -7448,7 +9596,7 @@ function registerMarketTools(ctx, market) {
7448
9596
  } },
7449
9597
  required: ["id"],
7450
9598
  additionalProperties: false
7451
- }, { required: true }),
9599
+ },
7452
9600
  output: OUTPUT$1,
7453
9601
  async execute(raw) {
7454
9602
  const id = await idOrList(raw, spec.name);
@@ -7487,7 +9635,7 @@ function registerMarketTools(ctx, market) {
7487
9635
  const uninstallTool = {
7488
9636
  name: MODULES_UNINSTALL_TOOL,
7489
9637
  description: "卸模块:连模块包一起卸(只动工作台运行时);要换一份实现先卸再装。",
7490
- parameters: withApprovalNote({
9638
+ parameters: {
7491
9639
  type: "object",
7492
9640
  properties: { id: {
7493
9641
  type: "string",
@@ -7495,7 +9643,7 @@ function registerMarketTools(ctx, market) {
7495
9643
  } },
7496
9644
  required: ["id"],
7497
9645
  additionalProperties: false
7498
- }, { required: true }),
9646
+ },
7499
9647
  output: OUTPUT$1,
7500
9648
  async execute(raw) {
7501
9649
  const id = await idOrList(raw, MODULES_UNINSTALL_TOOL);
@@ -7510,7 +9658,7 @@ function registerMarketTools(ctx, market) {
7510
9658
  const installBundleTool = {
7511
9659
  name: MODULES_INSTALL_BUNDLE_TOOL,
7512
9660
  description: "按 spec 把模块包装进这份 profile(包名 / git 地址 / tarball / 本地路径);装完它要的插件也一起装。",
7513
- parameters: withApprovalNote({
9661
+ parameters: {
7514
9662
  type: "object",
7515
9663
  properties: {
7516
9664
  spec: {
@@ -7524,7 +9672,7 @@ function registerMarketTools(ctx, market) {
7524
9672
  },
7525
9673
  required: ["spec"],
7526
9674
  additionalProperties: false
7527
- }, { required: true }),
9675
+ },
7528
9676
  output: OUTPUT$1,
7529
9677
  async execute(raw) {
7530
9678
  const spec = textArg(raw, "spec");
@@ -7542,7 +9690,7 @@ function registerMarketTools(ctx, market) {
7542
9690
  const updateDependencyTool = {
7543
9691
  name: MODULES_UPDATE_DEPENDENCY_TOOL,
7544
9692
  description: "升共用依赖(工作台自己那份营生,如 plugin:coding):只有升级,用依赖 id。",
7545
- parameters: withApprovalNote({
9693
+ parameters: {
7546
9694
  type: "object",
7547
9695
  properties: { id: {
7548
9696
  type: "string",
@@ -7550,7 +9698,7 @@ function registerMarketTools(ctx, market) {
7550
9698
  } },
7551
9699
  required: ["id"],
7552
9700
  additionalProperties: false
7553
- }, { required: true }),
9701
+ },
7554
9702
  output: OUTPUT$1,
7555
9703
  async execute(raw) {
7556
9704
  const id = await idOrList(raw, MODULES_UPDATE_DEPENDENCY_TOOL);
@@ -7566,7 +9714,7 @@ function registerMarketTools(ctx, market) {
7566
9714
  return {
7567
9715
  name: spec.name,
7568
9716
  description: spec.description,
7569
- parameters: withApprovalNote({
9717
+ parameters: {
7570
9718
  type: "object",
7571
9719
  properties: { plugin: {
7572
9720
  type: "string",
@@ -7574,7 +9722,7 @@ function registerMarketTools(ctx, market) {
7574
9722
  } },
7575
9723
  required: ["plugin"],
7576
9724
  additionalProperties: false
7577
- }, { required: true }),
9725
+ },
7578
9726
  output: OUTPUT$1,
7579
9727
  async execute(raw) {
7580
9728
  const plugin = textArg(raw, "plugin");
@@ -7608,7 +9756,7 @@ function registerMarketTools(ctx, market) {
7608
9756
  const updateWorkbenchTool = {
7609
9757
  name: MODULES_UPDATE_WORKBENCH_TOOL,
7610
9758
  description: "把工作台自己更新到源上的新版(要重启时结果里会说;重启由用户来)。",
7611
- parameters: withApprovalNote(noParams, { required: true }),
9759
+ parameters: noParams,
7612
9760
  output: OUTPUT$1,
7613
9761
  async execute() {
7614
9762
  const outcome = await market.updateWorkbench("");
@@ -7687,6 +9835,7 @@ function applyPanelsPref(panels, payload) {
7687
9835
  //#region src/modules/panels/model/handoff.ts
7688
9836
  var PANELS_FACT = "panels";
7689
9837
  var DIY_DEBUG_FACT = "diy-debug";
9838
+ var DIY_RECORD_FACT = "diy-record";
7690
9839
  var CAPABILITY_FACT = "capability";
7691
9840
  var DEBUG_TOOLS = [
7692
9841
  DIY_DEBUG_START_TOOL,
@@ -7694,6 +9843,12 @@ var DEBUG_TOOLS = [
7694
9843
  DIY_DEBUG_RESUME_TOOL,
7695
9844
  DIY_DEBUG_STOP_TOOL
7696
9845
  ];
9846
+ var RECORD_TOOLS = [
9847
+ DIY_RECORD_PREPARE_TOOL,
9848
+ DIY_RECORD_READ_TOOL,
9849
+ DIY_RECORD_DETAIL_TOOL,
9850
+ DIY_RECORD_STOP_TOOL
9851
+ ];
7697
9852
  var PROVISION_TOOLS = {
7698
9853
  [MODULES_INSTALL_TOOL]: "install",
7699
9854
  [MODULES_UPDATE_TOOL]: "update",
@@ -7730,6 +9885,7 @@ function panelsFactsOf(call, manifests) {
7730
9885
  if (call.source === "workbench_modules_update_workbench") return [PANELS_FACT, CAPABILITY_FACT];
7731
9886
  if (call.source === "workbench_diy_list") return [PANELS_FACT];
7732
9887
  if (DEBUG_TOOLS.includes(call.source)) return [DIY_DEBUG_FACT];
9888
+ if (RECORD_TOOLS.includes(call.source)) return [DIY_RECORD_FACT];
7733
9889
  const request = requestOf(call);
7734
9890
  if (request === void 0) return void 0;
7735
9891
  return [
@@ -8066,9 +10222,6 @@ var ID_PARAM = {
8066
10222
  type: "string",
8067
10223
  description: "套件 id(如 cmdb)。"
8068
10224
  };
8069
- function noted(parameters) {
8070
- return withApprovalNote(parameters, { required: true });
8071
- }
8072
10225
  function listed(entries) {
8073
10226
  return entries.map((entry, index) => `${index + 1}. ${entry.id} · ${entry.shown ? "已启用" : "未启用"}${entry.hasCard ? "" : "(无卡片)"}${entry.source === "user" ? " · 用户改过" : ""}`).join("\n");
8074
10227
  }
@@ -8157,7 +10310,7 @@ function registerPanelTools(ctx, panels, values) {
8157
10310
  const showTool = {
8158
10311
  name: PANELS_SHOW_TOOL,
8159
10312
  description: "这个工作区要它:之前收起的也放回来,卡片与 agent 工具一起上线;那份板上写过\"不要\"的也划掉。",
8160
- parameters: noted({
10313
+ parameters: {
8161
10314
  type: "object",
8162
10315
  properties: {
8163
10316
  id: ID_PARAM,
@@ -8165,7 +10318,7 @@ function registerPanelTools(ctx, panels, values) {
8165
10318
  },
8166
10319
  required: ["id"],
8167
10320
  additionalProperties: false
8168
- }),
10321
+ },
8169
10322
  output: OUTPUT,
8170
10323
  async execute(raw, exec) {
8171
10324
  const id = idOf(raw);
@@ -8182,7 +10335,7 @@ function registerPanelTools(ctx, panels, values) {
8182
10335
  const hideTool = {
8183
10336
  name: PANELS_HIDE_TOOL,
8184
10337
  description: "这个工作区明确不要它:卡片与 agent 工具一起下线(机器板上开着的也照样收起);别的项目不受影响,要放回来用 workbench_panels_show。在机器板上 hide 是写下\"不要\"这一笔,比名单管用。",
8185
- parameters: noted({
10338
+ parameters: {
8186
10339
  type: "object",
8187
10340
  properties: {
8188
10341
  id: ID_PARAM,
@@ -8190,7 +10343,7 @@ function registerPanelTools(ctx, panels, values) {
8190
10343
  },
8191
10344
  required: ["id"],
8192
10345
  additionalProperties: false
8193
- }),
10346
+ },
8194
10347
  output: OUTPUT,
8195
10348
  async execute(raw, exec) {
8196
10349
  const id = idOf(raw);
@@ -8207,7 +10360,7 @@ function registerPanelTools(ctx, panels, values) {
8207
10360
  const moveTool = {
8208
10361
  name: PANELS_MOVE_TOOL,
8209
10362
  description: "调顺序:放到最前面(新开一行时排最后)。顺序只属于工作区;机器级默认板上没有顺序一说。",
8210
- parameters: noted({
10363
+ parameters: {
8211
10364
  type: "object",
8212
10365
  properties: {
8213
10366
  id: ID_PARAM,
@@ -8219,7 +10372,7 @@ function registerPanelTools(ctx, panels, values) {
8219
10372
  },
8220
10373
  required: ["id"],
8221
10374
  additionalProperties: false
8222
- }),
10375
+ },
8223
10376
  output: OUTPUT,
8224
10377
  async execute(raw, exec) {
8225
10378
  const id = idOf(raw);
@@ -8237,11 +10390,11 @@ function registerPanelTools(ctx, panels, values) {
8237
10390
  const resetTool = {
8238
10391
  name: PANELS_RESET_TOOL,
8239
10392
  description: "清掉这个工作区改过的行(改完全部回到跟着机器级默认板走);scope=machine 清的是整份机器级默认板。",
8240
- parameters: noted({
10393
+ parameters: {
8241
10394
  type: "object",
8242
10395
  properties: { scope: SCOPE_PARAM },
8243
10396
  additionalProperties: false
8244
- }),
10397
+ },
8245
10398
  output: OUTPUT,
8246
10399
  async execute(raw, exec) {
8247
10400
  const scope = scopeOf(raw);
@@ -8256,14 +10409,14 @@ function registerPanelTools(ctx, panels, values) {
8256
10409
  const followTool = {
8257
10410
  name: PANELS_FOLLOW_TOOL,
8258
10411
  description: "某一行放回跟随机器级默认板(带 id = 只那一行,不带 = 全部)。",
8259
- parameters: noted({
10412
+ parameters: {
8260
10413
  type: "object",
8261
10414
  properties: {
8262
10415
  id: ID_PARAM,
8263
10416
  scope: SCOPE_PARAM
8264
10417
  },
8265
10418
  additionalProperties: false
8266
- }),
10419
+ },
8267
10420
  output: OUTPUT,
8268
10421
  async execute(raw, exec) {
8269
10422
  const id = idOf(raw);
@@ -8306,6 +10459,24 @@ var OWNED = [{
8306
10459
  ctx.provide(PANELS_SERVICE, panels);
8307
10460
  const debug = withObserverCleanup(createDebugStore(), () => stopObservers(ctx), logger);
8308
10461
  ctx.provide(DIY_DEBUG_SERVICE, debug);
10462
+ const record = {
10463
+ ...createRecordStore(),
10464
+ release: (state) => {
10465
+ const shellNow = ctx.get("shell");
10466
+ if (shellNow === void 0) return Promise.resolve({
10467
+ tab: false,
10468
+ capture: false
10469
+ });
10470
+ return releaseQuietly({
10471
+ shell: shellNow,
10472
+ command: opencliCommandOf(ctx).command,
10473
+ session: BROWSER_RECORD_SESSION,
10474
+ url: state.url,
10475
+ captureFile: state.captureFile
10476
+ });
10477
+ }
10478
+ };
10479
+ ctx.provide(DIY_RECORD_SERVICE, record);
8309
10480
  const profileDir = profileDirOf(ctx);
8310
10481
  const shell = ctx.get("shell");
8311
10482
  const moduleManifests = () => {
@@ -8354,12 +10525,14 @@ var OWNED = [{
8354
10525
  ledger,
8355
10526
  ...shell === void 0 ? {} : { shell }
8356
10527
  }),
8357
- ...diyDebugRoutes(debug)
10528
+ ...diyDebugRoutes(debug),
10529
+ ...diyRecordRoutes(record)
8358
10530
  },
8359
10531
  registerTools: () => [
8360
10532
  ...registerPanelTools(ctx, panels, values),
8361
10533
  ...registerMarketTools(ctx, market),
8362
- ...registerDiyTools(ctx, market, panels, debug)
10534
+ ...registerDiyTools(ctx, market, panels, debug),
10535
+ ...registerRecordTools(ctx, record)
8363
10536
  ],
8364
10537
  factsOf: (call) => panelsFactsOf(call, manifests()),
8365
10538
  needsApproval: panelsApproval,
@@ -8370,7 +10543,11 @@ var OWNED = [{
8370
10543
  return [...manifests().flatMap((manifest) => owned.has(manifest.id) || resolveScope({
8371
10544
  id: manifest.id,
8372
10545
  boards
8373
- }) ? [] : manifest.tools.map((tool) => tool.name)), ...inWorkshopSession(ctx, workspace) ? [] : [...DIY_TOOLS, ...DIY_DEBUG_TOOLS]];
10546
+ }) ? [] : manifest.tools.map((tool) => tool.name)), ...inWorkshopSession(ctx, workspace) ? [] : [
10547
+ ...DIY_TOOLS,
10548
+ ...DIY_DEBUG_TOOLS,
10549
+ ...DIY_RECORD_TOOLS
10550
+ ]];
8374
10551
  },
8375
10552
  subscribe: (listener) => panels.subscribe(listener)
8376
10553
  },
@@ -8418,19 +10595,26 @@ function withRunsRoute(result, unit) {
8418
10595
  }
8419
10596
  };
8420
10597
  }
8421
- function notedRegistry(registry, writesOf, journal) {
10598
+ function declaresNote(definition) {
10599
+ const parameters = definition.parameters;
10600
+ if (typeof parameters !== "object" || parameters === null) return false;
10601
+ const properties = parameters["properties"];
10602
+ if (typeof properties !== "object" || properties === null) return false;
10603
+ return APPROVAL_NOTE_ARG in properties;
10604
+ }
10605
+ function declaredNotesOf(plans) {
10606
+ return new Set([...plans].flatMap((plan) => [...plan.declaredNotes]));
10607
+ }
10608
+ function journaledRegistry(registry, journal, declaredNotes) {
8422
10609
  return { register(definition) {
8423
- const noted = writesOf(definition.name) ? withApprovalNote(definition.parameters, { required: true }) : void 0;
8424
- const registered = registry.register(noted === void 0 || noted === definition.parameters ? definition : {
8425
- ...definition,
8426
- parameters: noted
8427
- });
10610
+ const registered = registry.register(definition);
8428
10611
  journal.push(registered);
10612
+ if (declaresNote(definition)) declaredNotes.add(definition.name);
8429
10613
  return registered;
8430
10614
  } };
8431
10615
  }
8432
- function scopedMountCtx(ctx, registry, writesOf, journal) {
8433
- const tools = notedRegistry(registry, writesOf, journal);
10616
+ function scopedMountCtx(ctx, registry, journal, declaredNotes) {
10617
+ const tools = journaledRegistry(registry, journal, declaredNotes);
8434
10618
  const get = (name) => {
8435
10619
  const value = ctx.get(name);
8436
10620
  return name === "tools" && value !== void 0 ? tools : value;
@@ -8574,6 +10758,7 @@ function mountUnits(units, deps) {
8574
10758
  const store = deps.store ?? createWorkspaceStore({ root: null });
8575
10759
  const ownedUnits = deps.ownedUnits ?? (() => []);
8576
10760
  const mounted = [];
10761
+ const declaredNotes = /* @__PURE__ */ new Set();
8577
10762
  const skipped = [];
8578
10763
  const longTaskDisposers = /* @__PURE__ */ new Map();
8579
10764
  const ledger = {
@@ -8591,9 +10776,8 @@ function mountUnits(units, deps) {
8591
10776
  }
8592
10777
  const unit = host.manifest.id;
8593
10778
  const tools = deps.ctx.get("tools");
8594
- const writes = toolUnits(deps.manifests());
8595
10779
  const toolJournal = [];
8596
- const mountCtx = tools === void 0 ? deps.ctx : scopedMountCtx(deps.ctx, tools, (name) => writes.get(name)?.writes === true, toolJournal);
10780
+ const mountCtx = tools === void 0 ? deps.ctx : scopedMountCtx(deps.ctx, tools, toolJournal, declaredNotes);
8597
10781
  const longTasks = deps.longTasks === void 0 ? void 0 : createLongTaskHost({
8598
10782
  scope: deps.longTasks.scope,
8599
10783
  unit,
@@ -8667,6 +10851,7 @@ function mountUnits(units, deps) {
8667
10851
  skipped,
8668
10852
  failed: ledger.failures,
8669
10853
  warmReads: collectWarmReads(alive),
10854
+ declaredNotes,
8670
10855
  disposeLongTasks: () => {
8671
10856
  for (const dispose of [...longTaskDisposers.values()]) dispose();
8672
10857
  longTaskDisposers.clear();
@@ -8703,125 +10888,105 @@ function createPostChannels(deps) {
8703
10888
  };
8704
10889
  }
8705
10890
  //#endregion
8706
- //#region src/shared/api/host/diy-debug-act.ts
8707
- var DEBUG_NOTICE_KIND = "workbench-diy";
10891
+ //#region src/shared/api/host/diy-record-act.ts
10892
+ var RECORD_NOTICE_KIND = "workbench-diy-record";
8708
10893
  var INTENT_WORD = {
8709
- pause: "暂停",
8710
- resume: "继续",
8711
- stop: "结束"
10894
+ peek: "看结果",
10895
+ halt: "停",
10896
+ stop: "收工"
8712
10897
  };
8713
- function debugNoticeText(intent, state) {
10898
+ function recordNoticeText(intent, state) {
8714
10899
  const head = `[工作台 DIY]用户按了「${INTENT_WORD[intent]}」:`;
8715
- if (intent === "pause") return `${head}${state.unit} 这场调试暂停了 —— 现在别再把那一屏当目标(看 / 动 / 网络那一族发不出去)。要接着看屏就等用户按「继续」;在那之前照常改源码 / build / 跑门禁。`;
8716
- if (intent === "resume") return `${head}${state.unit} 这场调试重新开始了 —— 那一屏又归你看。屏 / 板不对的话照旧会被拒,拒了就照实说清是哪种不对。`;
8717
- return `${head}${state.unit} 这场调试收工了(占用解除)。要再调试就重新 start 一场。`;
10900
+ if (intent === "peek") return state.phase === "finished" ? `${head}${state.unit} 这一段录制他做完了 —— 用 workbench_diy_record_read 取那一段(他不在了也照取),取过用 workbench_diy_record_stop 收工。` : `${head}${state.unit} 这一段现在还是 ${state.phase}(他还没按「结束录制」)—— 等他按完再 read;他要是说浮条没了,read 那一步会顺手把浮条重新装上。`;
10901
+ if (intent === "halt") return `${head}${state.unit} 这一段他停下来了(浮条可能被导航干掉过)。**先 read 再 stop**:read 会把那一页上的缓冲取出来,stop 之后就取不到了 —— 顺序反了就等于白录。`;
10902
+ return `${head}${state.unit} 这一场收了(占用解除)。已经 read 过的部分在你手上;没读的那部分没了 —— 要再录一段重新 prepare 一场。`;
8718
10903
  }
8719
- function debugNoticeSummary(intent, state) {
10904
+ function recordNoticeSummary(intent, state) {
8720
10905
  return `工作台 DIY:用户按了${INTENT_WORD[intent]}(${state.unit})`;
8721
10906
  }
8722
- function debugNotice(intent, state) {
8723
- const summary = debugNoticeSummary(intent, state);
10907
+ function recordNotice(intent, state) {
10908
+ const summary = recordNoticeSummary(intent, state);
8724
10909
  return {
8725
10910
  summary,
8726
- message: {
8727
- role: "user",
8728
- id: randomUUID(),
8729
- content: [{
8730
- type: "text",
8731
- text: debugNoticeText(intent, state)
8732
- }],
8733
- source: {
8734
- kind: DEBUG_NOTICE_KIND,
8735
- form: "notice",
8736
- summary
8737
- }
8738
- }
10911
+ message: noticeMessage(recordNoticeText(intent, state), summary, RECORD_NOTICE_KIND)
8739
10912
  };
8740
10913
  }
8741
10914
  var NOTICE_MODE = {
8742
- resume: "wake",
8743
- pause: "hold",
10915
+ peek: "wake",
10916
+ halt: "wake",
8744
10917
  stop: "drop"
8745
10918
  };
8746
- var held = /* @__PURE__ */ new Map();
8747
- function holdNotice(sessionId, message) {
8748
- const list = held.get(sessionId);
8749
- if (list === void 0) held.set(sessionId, [message]);
8750
- else list.push(message);
8751
- }
8752
- function takeHeldNotices(sessionId) {
8753
- const list = held.get(sessionId) ?? [];
8754
- held.delete(sessionId);
8755
- return list;
8756
- }
8757
- function deliverDebugNotice(agents, sessionId, message, mode = "wake") {
8758
- const agent = agents?.get(sessionId);
8759
- if (agent === void 0) {
8760
- if (mode === "drop") return "none";
8761
- holdNotice(sessionId, message);
8762
- return "held";
8763
- }
8764
- if (agent.status !== "idle") {
8765
- agent.inject(message);
8766
- return "inject";
8767
- }
8768
- if (mode === "wake") {
8769
- agent.followup(message);
8770
- return "followup";
8771
- }
8772
- if (mode === "hold") {
8773
- holdNotice(sessionId, message);
8774
- return "held";
10919
+ var REFUSAL = "现在没有录制中的那一场 —— 没什么可停 / 可收的";
10920
+ async function releaseHeld(store, held) {
10921
+ try {
10922
+ return await (store.release?.(held) ?? Promise.resolve({
10923
+ tab: false,
10924
+ capture: false
10925
+ }));
10926
+ } catch {
10927
+ return {
10928
+ tab: false,
10929
+ capture: false
10930
+ };
8775
10931
  }
8776
- return "skipped-idle";
8777
10932
  }
8778
- var REFUSAL = {
8779
- "already-paused": "这一场已经是暂停态了",
8780
- "already-debugging": "这一场本来就在调试中",
8781
- none: "现在没有调试态",
8782
- "not-owner": "这场调试不是这个会话的"
8783
- };
8784
- function debugActResource(deps) {
10933
+ function recordActResource(deps) {
8785
10934
  const now = () => deps.now?.() ?? Date.now();
8786
- const settled = (intent, delivered, summary, state) => ({
8787
- ok: true,
8788
- intent,
8789
- delivered,
8790
- summary,
8791
- state
8792
- });
8793
10935
  return {
8794
10936
  resource: "act",
8795
- handle(payload) {
8796
- const intent = parseDebugIntent(payload?.intent);
10937
+ async handle(payload) {
10938
+ const intent = parseRecordIntent(payload?.intent);
8797
10939
  if (intent === null) return {
8798
10940
  ok: false,
8799
- text: "不认识的意图(只认 pause / resume / stop)"
10941
+ text: "不认识的意图(只认 peek / halt / stop)"
8800
10942
  };
8801
10943
  const store = deps.store();
8802
- if (store === void 0) return {
8803
- ok: false,
8804
- text: "现在没有调试态 —— 没什么可暂停 / 继续 / 结束的"
8805
- };
8806
10944
  const at = now();
8807
- const held = store.live(at);
8808
- if (held === null) return {
10945
+ const held = store?.live(at) ?? null;
10946
+ if (store === void 0 || held === null) return {
8809
10947
  ok: false,
8810
- text: "现在没有调试态 —— 没什么可暂停 / 继续 / 结束的"
10948
+ text: REFUSAL
8811
10949
  };
8812
- const move = intent === "pause" ? store.pause(held.sessionId, at) : intent === "resume" ? store.resume(held.sessionId, at) : store.stop(held.sessionId, at);
8813
- if (!move.ok) return {
8814
- ok: false,
8815
- text: REFUSAL[move.code] ?? `按不动:${move.code}`
10950
+ const notice = deps.notice?.(intent, held) ?? recordNotice(intent, held);
10951
+ if (intent !== "stop") return {
10952
+ ok: true,
10953
+ intent,
10954
+ delivered: deliverDebugNotice(deps.agents(), held.sessionId, notice.message, NOTICE_MODE[intent]),
10955
+ summary: notice.summary,
10956
+ state: store.live(now())
10957
+ };
10958
+ const cleaned = await releaseHeld(store, held);
10959
+ const move = store.stop(held.sessionId, at);
10960
+ return {
10961
+ ok: true,
10962
+ intent,
10963
+ delivered: deliverDebugNotice(deps.agents(), held.sessionId, notice.message, NOTICE_MODE[intent]),
10964
+ summary: notice.summary,
10965
+ cleaned,
10966
+ refused: move.ok ? "" : move.code,
10967
+ state: store.live(now())
8816
10968
  };
8817
- const notice = deps.notice?.(intent, move.state) ?? debugNotice(intent, move.state);
8818
- const delivered = deliverDebugNotice(deps.agents(), held.sessionId, notice.message, NOTICE_MODE[intent]);
8819
- if (intent !== "stop") return settled(intent, delivered, notice.summary, store.live(now()));
8820
- return (store.settle?.() ?? Promise.resolve()).then(() => settled(intent, delivered, notice.summary, store.live(now())));
8821
10969
  }
8822
10970
  };
8823
10971
  }
8824
10972
  //#endregion
10973
+ //#region src/shared/api/host/diy-acts.ts
10974
+ function registerDiyActs(ctx, facts) {
10975
+ const log = { bump: (names) => facts.bump(names) };
10976
+ const offDebug = registerDiyDebugAct(ctx, debugActResource({
10977
+ store: () => ctx.get(DIY_DEBUG_SERVICE),
10978
+ agents: () => ctx.get("agents")
10979
+ }), log);
10980
+ const offRecord = registerDiyRecordAct(ctx, recordActResource({
10981
+ store: () => ctx.get(DIY_RECORD_SERVICE),
10982
+ agents: () => ctx.get("agents")
10983
+ }), log);
10984
+ return () => {
10985
+ offDebug();
10986
+ offRecord();
10987
+ };
10988
+ }
10989
+ //#endregion
8825
10990
  //#region src/shared/api/host/events.ts
8826
10991
  var HEARTBEAT_MS = 25e3;
8827
10992
  function frame(payload) {
@@ -11083,6 +13248,7 @@ function apply(ctx) {
11083
13248
  const factsOfUnit = (unit) => [...routesOfUnitMap.get(unit) ?? [], "capability"];
11084
13249
  const callFacts = (unit, call) => factsOfMap.get(unit)?.(call) ?? factsOfUnit(unit);
11085
13250
  const deniedNow = (workspace) => [...new Set([...mounted.values()].flatMap((item) => item.plan.gate?.deniedTools(workspace) ?? []))];
13251
+ const declaredNoteTools = () => declaredNotesOf([...mounted.values()].map((item) => item.plan));
11086
13252
  const wrote = (resource, write) => {
11087
13253
  const unit = ownersMap.get(resource);
11088
13254
  if (unit !== void 0) facts.bump(callFacts(unit, {
@@ -11314,10 +13480,12 @@ function apply(ctx) {
11314
13480
  ...workspace === void 0 ? {} : { workspace }
11315
13481
  };
11316
13482
  const policy = owner?.needsApproval;
13483
+ const declaredNote = name !== void 0 && declaredNoteTools().has(name);
11317
13484
  return await approvalFor(call, {
11318
13485
  detailOf,
11319
13486
  policy,
11320
- writes: tool?.writes ?? false
13487
+ writes: tool?.writes ?? false,
13488
+ declaredNote
11321
13489
  }) ?? decision;
11322
13490
  });
11323
13491
  return () => off?.();
@@ -11347,12 +13515,7 @@ function apply(ctx) {
11347
13515
  }, `${PLUGIN_ID}: 对话引用注入`);
11348
13516
  ctx.effect(() => registerFactStream(ctx, facts), `${PLUGIN_ID}: 变更流`);
11349
13517
  ctx.effect(() => registerPanelState(ctx, panelState), `${PLUGIN_ID}: 面板运行态上报(浏览器半侧报自己的取数与渲染收尾)`);
11350
- ctx.effect(() => {
11351
- return registerDiyDebugAct(ctx, debugActResource({
11352
- store: () => ctx.get(DIY_DEBUG_SERVICE),
11353
- agents: () => ctx.get("agents")
11354
- }), { bump: (names) => facts.bump(names) });
11355
- }, `${PLUGIN_ID}: DIY 调试的三个键(用户按的)`);
13518
+ ctx.effect(() => registerDiyActs(ctx, { bump: (names) => facts.bump(names) }), `${PLUGIN_ID}: DIY 的两族键(用户按的)`);
11356
13519
  ctx.effect(() => registerApi(ctx, routes), `${PLUGIN_ID}: HTTP 投影`);
11357
13520
  }
11358
13521
  //#endregion