@dsh-workbench/core 0.2.10 → 0.2.12

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 (60) hide show
  1. package/client.js +785 -2361
  2. package/dist/board.css +0 -12
  3. package/dist/client/client.body.js +785 -2361
  4. package/dist/client/index.js +786 -2362
  5. package/dist/host/index.js +2902 -859
  6. package/dist/types/app/client.d.ts +42 -52
  7. package/dist/types/app/layouts/rightbar/board-mock.d.ts +1 -1
  8. package/dist/types/app/layouts/rightbar/board-plan.d.ts +2 -12
  9. package/dist/types/app/layouts/rightbar/board-shape.d.ts +14 -0
  10. package/dist/types/app/layouts/rightbar/kit-preview-controls.d.ts +10 -0
  11. package/dist/types/app/layouts/rightbar/kit-preview-frame.d.ts +14 -0
  12. package/dist/types/app/layouts/rightbar/kit-preview-rows.d.ts +5 -0
  13. package/dist/types/app/layouts/rightbar/kit-preview.d.ts +3 -0
  14. package/dist/types/app/layouts/rightbar/market-row-icon.d.ts +1 -1
  15. package/dist/types/app/layouts/rightbar/market-section-icon.d.ts +1 -1
  16. package/dist/types/app/layouts/sidebar/workspace-groups.d.ts +0 -1
  17. package/dist/types/app/layouts/sidebar/workspace-items.d.ts +2 -3
  18. package/dist/types/dev/MockStrip.d.ts +15 -0
  19. package/dist/types/dev/surface-types.d.ts +53 -0
  20. package/dist/types/dev/surface.d.ts +2 -0
  21. package/dist/types/dev/surface.stub.d.ts +2 -0
  22. package/dist/types/modules/panels/__tests__/diy-harness.d.ts +32 -0
  23. package/dist/types/modules/panels/__tests__/market-tools-harness.d.ts +1 -1
  24. package/dist/types/modules/panels/__tests__/scope-support.d.ts +1 -1
  25. package/dist/types/modules/panels/model/diy-act.d.ts +4 -0
  26. package/dist/types/modules/panels/model/diy-act.test.d.ts +1 -0
  27. package/dist/types/modules/panels/model/diy-browser-act.d.ts +47 -0
  28. package/dist/types/modules/panels/model/diy-browser.d.ts +47 -0
  29. package/dist/types/modules/panels/model/diy-browser.test.d.ts +1 -0
  30. package/dist/types/modules/panels/model/diy-context.d.ts +102 -0
  31. package/dist/types/modules/panels/model/diy-context.test.d.ts +1 -0
  32. package/dist/types/modules/panels/model/diy-logs.d.ts +18 -0
  33. package/dist/types/modules/panels/model/diy-logs.test.d.ts +1 -0
  34. package/dist/types/modules/panels/model/diy-look.d.ts +14 -0
  35. package/dist/types/modules/panels/model/diy-look.test.d.ts +1 -0
  36. package/dist/types/modules/panels/model/diy-net.d.ts +3 -0
  37. package/dist/types/modules/panels/model/diy-net.test.d.ts +1 -0
  38. package/dist/types/modules/panels/model/diy-page.d.ts +15 -0
  39. package/dist/types/modules/panels/model/diy-page.test.d.ts +1 -0
  40. package/dist/types/modules/panels/model/diy-schema.d.ts +9 -0
  41. package/dist/types/modules/panels/model/diy-schema.test.d.ts +1 -0
  42. package/dist/types/modules/panels/model/diy-scripts.d.ts +64 -0
  43. package/dist/types/modules/panels/unit.d.ts +43 -3
  44. package/dist/types/shared/api/diy.d.ts +4 -0
  45. package/dist/types/shared/api/host/__tests__/png-fixtures.d.ts +17 -0
  46. package/dist/types/shared/api/host/mount-units.d.ts +6 -0
  47. package/dist/types/shared/api/host/png-crop.d.ts +21 -0
  48. package/dist/types/shared/api/host/png-crop.test.d.ts +1 -0
  49. package/package.json +2 -2
  50. package/skills/workbench-module-diy/BUILD.md +4 -3
  51. package/skills/workbench-module-diy/DATASOURCE.md +4 -4
  52. package/skills/workbench-module-diy/DESIGN.md +1 -1
  53. package/skills/workbench-module-diy/KIT.md +121 -0
  54. package/skills/workbench-module-diy/PANEL.md +12 -2
  55. package/skills/workbench-module-diy/REVIEW.md +12 -6
  56. package/skills/workbench-module-diy/SKILL.md +7 -6
  57. package/skills/workbench-module-diy/TOOLING.md +5 -4
  58. package/skills/workbench-module-diy/TOOLS.md +3 -3
  59. package/skills/workbench-module-diy/WIRING.md +6 -6
  60. package/dist/types/app/layouts/sidebar/WorkspaceSearchField.d.ts +0 -9
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import fs, { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import path, { basename, delimiter, dirname, join, resolve, sep } from "node:path";
4
+ import { deflateSync, inflateSync } from "node:zlib";
4
5
  import os from "node:os";
5
6
  import { spawn } from "node:child_process";
6
7
  import { fileURLToPath } from "node:url";
@@ -247,7 +248,7 @@ var PHASES = [
247
248
  function asRecord$10(value) {
248
249
  return typeof value === "object" && value !== null ? value : void 0;
249
250
  }
250
- function textOf$7(value, max) {
251
+ function textOf$9(value, max) {
251
252
  if (typeof value !== "string") return "";
252
253
  const trimmed = value.trim();
253
254
  return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
@@ -260,8 +261,8 @@ function atOf(value) {
260
261
  function errorOf(raw) {
261
262
  const record = asRecord$10(raw);
262
263
  if (record === void 0) return void 0;
263
- const code = textOf$7(record["code"], 120);
264
- const message = textOf$7(record["message"], PANEL_STATE_MAX_TEXT);
264
+ const code = textOf$9(record["code"], 120);
265
+ const message = textOf$9(record["message"], PANEL_STATE_MAX_TEXT);
265
266
  if (code === "" && message === "") return void 0;
266
267
  return {
267
268
  code: code === "" ? "unknown" : code,
@@ -274,7 +275,7 @@ function parseCardsReport(payload) {
274
275
  ok: false,
275
276
  message: "请求体要是一个对象"
276
277
  };
277
- const workspace = textOf$7(record["workspace"], PANEL_STATE_MAX_TEXT);
278
+ const workspace = textOf$9(record["workspace"], PANEL_STATE_MAX_TEXT);
278
279
  if (workspace === "") return {
279
280
  ok: false,
280
281
  message: "workspace 不能是空的 —— 面板按工作区存"
@@ -288,9 +289,9 @@ function parseCardsReport(payload) {
288
289
  for (const item of raw.slice(0, 32)) {
289
290
  const card = asRecord$10(item);
290
291
  if (card === void 0) continue;
291
- const unit = textOf$7(card["unit"], 200);
292
- const route = textOf$7(card["route"], 400);
293
- const phase = textOf$7(card["phase"], 20);
292
+ const unit = textOf$9(card["unit"], 200);
293
+ const route = textOf$9(card["route"], 400);
294
+ const phase = textOf$9(card["phase"], 20);
294
295
  const at = atOf(card["at"]);
295
296
  if (unit === "" || route === "" || at === null || !PHASES.includes(phase)) continue;
296
297
  const error = errorOf(card["error"]);
@@ -320,9 +321,9 @@ function parseCrashReport(payload) {
320
321
  ok: false,
321
322
  message: "请求体要是一个对象"
322
323
  };
323
- const workspace = textOf$7(record["workspace"], PANEL_STATE_MAX_TEXT);
324
- const unit = textOf$7(record["unit"], 200);
325
- const reason = textOf$7(record["reason"], PANEL_STATE_MAX_TEXT);
324
+ const workspace = textOf$9(record["workspace"], PANEL_STATE_MAX_TEXT);
325
+ const unit = textOf$9(record["unit"], 200);
326
+ const reason = textOf$9(record["reason"], PANEL_STATE_MAX_TEXT);
326
327
  if (workspace === "") return {
327
328
  ok: false,
328
329
  message: "workspace 不能是空的"
@@ -568,20 +569,20 @@ var PERMISSION_CODES = /* @__PURE__ */ new Set([
568
569
  function asRecord$9(value) {
569
570
  return typeof value === "object" && value !== null ? value : void 0;
570
571
  }
571
- function textOf$6(value) {
572
+ function textOf$8(value) {
572
573
  return typeof value === "string" ? value.trim() : "";
573
574
  }
574
575
  function codeOf(page) {
575
576
  const record = asRecord$9(page);
576
- return record === void 0 ? "" : textOf$6(record["code"]);
577
+ return record === void 0 ? "" : textOf$8(record["code"]);
577
578
  }
578
579
  function messageOf$2(page) {
579
580
  const record = asRecord$9(page);
580
- return record === void 0 ? "" : textOf$6(record["message"]);
581
+ return record === void 0 ? "" : textOf$8(record["message"]);
581
582
  }
582
583
  function hintOf$1(page) {
583
584
  const record = asRecord$9(page);
584
- const hint = record === void 0 ? "" : textOf$6(record["hint"]);
585
+ const hint = record === void 0 ? "" : textOf$8(record["hint"]);
585
586
  return hint === "" ? void 0 : hint;
586
587
  }
587
588
  function pageFailed(page) {
@@ -812,7 +813,7 @@ var DEFAULT_TEXT_MAX = 200;
812
813
  function asRecord$8(value) {
813
814
  return typeof value === "object" && value !== null ? value : void 0;
814
815
  }
815
- function textOf$5(value) {
816
+ function textOf$7(value) {
816
817
  return typeof value === "string" ? value.trim() : "";
817
818
  }
818
819
  function entriesOf(value) {
@@ -825,8 +826,8 @@ function entriesOf(value) {
825
826
  if (item === void 0) continue;
826
827
  const nth = typeof item["nth"] === "number" ? item["nth"] : entries.length;
827
828
  entries.push({
828
- text: textOf$5(item["text"]),
829
- tag: textOf$5(item["tag"]),
829
+ text: textOf$7(item["text"]),
830
+ tag: textOf$7(item["tag"]),
830
831
  nth
831
832
  });
832
833
  }
@@ -835,7 +836,7 @@ function entriesOf(value) {
835
836
  function reasonOf(stdout, stderr) {
836
837
  const parsed = asRecord$8(parseJsonLoose(stdout));
837
838
  const error = parsed === void 0 ? void 0 : asRecord$8(parsed["error"]);
838
- const message = error === void 0 ? "" : textOf$5(error["message"]);
839
+ const message = error === void 0 ? "" : textOf$7(error["message"]);
839
840
  if (message !== "") return message;
840
841
  const lines = (stderr.trim() === "" ? stdout : stderr).split("\n").map((one) => one.trim()).filter((one) => one !== "");
841
842
  return (lines[lines.length - 1] ?? "").slice(-200);
@@ -843,7 +844,7 @@ function reasonOf(stdout, stderr) {
843
844
  function hintOf(stdout) {
844
845
  const parsed = asRecord$8(parseJsonLoose(stdout));
845
846
  const error = parsed === void 0 ? void 0 : asRecord$8(parsed["error"]);
846
- const hint = error === void 0 ? "" : textOf$5(error["hint"]);
847
+ const hint = error === void 0 ? "" : textOf$7(error["hint"]);
847
848
  return hint === "" ? void 0 : hint;
848
849
  }
849
850
  function sameOrigin(left, right) {
@@ -857,7 +858,7 @@ function urlOf(stdout) {
857
858
  const bare = (stdout.split("\n").map((one) => one.trim()).filter((one) => one !== "")[0] ?? "").replace(/^url:\s*/i, "").trim().replace(/^["']+|["']+$/g, "");
858
859
  if (bare.startsWith("{")) {
859
860
  const parsed = asRecord$8(parseJsonLoose(bare));
860
- const url = parsed === void 0 ? "" : textOf$5(parsed["url"]);
861
+ const url = parsed === void 0 ? "" : textOf$7(parsed["url"]);
861
862
  if (url !== "") return url;
862
863
  }
863
864
  return bare;
@@ -1108,7 +1109,7 @@ var TARGET_CODE = {
1108
1109
  function asRecord$7(value) {
1109
1110
  return typeof value === "object" && value !== null ? value : {};
1110
1111
  }
1111
- function stringsOf$3(value) {
1112
+ function stringsOf$4(value) {
1112
1113
  if (!Array.isArray(value)) return void 0;
1113
1114
  const items = value.filter((item) => typeof item === "string" && item.trim() !== "");
1114
1115
  return items.length === 0 ? void 0 : items;
@@ -1164,7 +1165,7 @@ function parseProvisionBody(body) {
1164
1165
  request: requestOf$1(wanted, value.trim())
1165
1166
  };
1166
1167
  const registry = typeof raw.registry === "string" && raw.registry.trim() !== "" ? raw.registry.trim() : void 0;
1167
- const approvedBuilds = stringsOf$3(raw.approvedBuilds);
1168
+ const approvedBuilds = stringsOf$4(raw.approvedBuilds);
1168
1169
  const requestId = typeof raw.requestId === "string" && raw.requestId.trim() !== "" ? raw.requestId.trim() : void 0;
1169
1170
  return {
1170
1171
  ok: true,
@@ -1872,7 +1873,7 @@ var PLUGIN_LEDGER_FILE = "plugins.json";
1872
1873
  function ledgerKeyOf(entry) {
1873
1874
  return `${entry.opencliHome}\u0000${entry.package}`;
1874
1875
  }
1875
- function textOf$4(value) {
1876
+ function textOf$6(value) {
1876
1877
  if (typeof value !== "string") return void 0;
1877
1878
  const trimmed = value.trim();
1878
1879
  return trimmed === "" ? void 0 : trimmed;
@@ -1880,13 +1881,13 @@ function textOf$4(value) {
1880
1881
  function entryOf(value) {
1881
1882
  if (typeof value !== "object" || value === null) return void 0;
1882
1883
  const box = value;
1883
- const opencliHome = textOf$4(box["opencliHome"]);
1884
- const pkg = textOf$4(box["package"]);
1885
- const installedAt = textOf$4(box["installedAt"]);
1884
+ const opencliHome = textOf$6(box["opencliHome"]);
1885
+ const pkg = textOf$6(box["package"]);
1886
+ const installedAt = textOf$6(box["installedAt"]);
1886
1887
  if (opencliHome === void 0 || pkg === void 0 || installedAt === void 0) return void 0;
1887
- const name = textOf$4(box["name"]);
1888
- const registry = textOf$4(box["registry"]);
1889
- const releasedAt = textOf$4(box["releasedAt"]);
1888
+ const name = textOf$6(box["name"]);
1889
+ const registry = textOf$6(box["registry"]);
1890
+ const releasedAt = textOf$6(box["releasedAt"]);
1890
1891
  return {
1891
1892
  opencliHome,
1892
1893
  package: pkg,
@@ -2107,7 +2108,7 @@ async function approvalFor(call, context = {}) {
2107
2108
  reason: `问"要不要审批"的时候套件的策略抛了错,已经停下:${messageOf$1(error)}`
2108
2109
  };
2109
2110
  }
2110
- if (verdict?.kind === "read") return void 0;
2111
+ if (verdict?.kind === "read" || verdict?.kind === "allow") return void 0;
2111
2112
  if (verdict?.kind !== "ask") return {
2112
2113
  kind: "deny",
2113
2114
  reason: `${call.name ?? "这把工具"} 在清单里声明会写东西,但它的套件没认领这次调用 —— 停下:在套件的 needsApproval 里补一条审批(含"这次会发生什么"),别让写静默过去`
@@ -2497,7 +2498,7 @@ function profileDirOf(ctx) {
2497
2498
  const dir = typeof profile?.dir === "string" ? profile.dir.trim() : "";
2498
2499
  return dir === "" ? null : dir;
2499
2500
  }
2500
- function stringsOf$2(value) {
2501
+ function stringsOf$3(value) {
2501
2502
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
2502
2503
  }
2503
2504
  function profileManifestOf(profileDir) {
@@ -2511,7 +2512,7 @@ function profileManifestOf(profileDir) {
2511
2512
  function profileModules(profileDir) {
2512
2513
  const record = profileManifestOf(profileDir);
2513
2514
  if (record === void 0) return [];
2514
- const bundles = stringsOf$2(asRecord$5(asRecord$5(record.dsh)?.profile)?.bundles);
2515
+ const bundles = stringsOf$3(asRecord$5(asRecord$5(record.dsh)?.profile)?.bundles);
2515
2516
  const dependencies = asRecord$5(record.dependencies) ?? {};
2516
2517
  const names = bundles.length > 0 ? bundles : Object.keys(dependencies);
2517
2518
  const found = [];
@@ -2661,7 +2662,7 @@ function readProfilePackage(profileDir, pkg) {
2661
2662
  //#endregion
2662
2663
  //#region src/shared/api/host/profile-registries.ts
2663
2664
  var REGISTRY_MARKER = "# dsh-workbench 写的:这一行的包只在这个源上(dsh 卸载没有源参数,pnpm 得问得到)";
2664
- function scopeOf(pkg) {
2665
+ function scopeOf$1(pkg) {
2665
2666
  const slash = pkg.indexOf("/");
2666
2667
  return pkg.startsWith("@") && slash > 1 ? pkg.slice(0, slash) : null;
2667
2668
  }
@@ -2701,7 +2702,7 @@ function sourcesFromLockfile(text) {
2701
2702
  if (name === null) continue;
2702
2703
  const tarball = TARBALL.exec(line);
2703
2704
  if (tarball === null) continue;
2704
- const scope = scopeOf(name);
2705
+ const scope = scopeOf$1(name);
2705
2706
  const root = registryRootOf(name, tarball[1] ?? "");
2706
2707
  name = null;
2707
2708
  if (scope === null || root === null) continue;
@@ -2832,26 +2833,121 @@ function inWorkshopSession(ctx, workspace) {
2832
2833
  }
2833
2834
  //#endregion
2834
2835
  //#region src/modules/panels/unit.ts
2835
- var PANELS_TOOL = "workbench_panels";
2836
- var MODULES_TOOL = "workbench_modules";
2837
- var DIY_TOOL = "workbench_diy";
2836
+ var PANELS_LIST_TOOL = "workbench_panels_list";
2837
+ var PANELS_SHOW_TOOL = "workbench_panels_show";
2838
+ var PANELS_HIDE_TOOL = "workbench_panels_hide";
2839
+ var PANELS_MOVE_TOOL = "workbench_panels_move";
2840
+ var PANELS_RESET_TOOL = "workbench_panels_reset";
2841
+ var PANELS_FOLLOW_TOOL = "workbench_panels_follow";
2842
+ var MODULES_LIST_TOOL = "workbench_modules_list";
2843
+ var MODULES_INSTALL_TOOL = "workbench_modules_install";
2844
+ var MODULES_UPDATE_TOOL = "workbench_modules_update";
2845
+ var MODULES_UNINSTALL_TOOL = "workbench_modules_uninstall";
2846
+ var MODULES_INSTALL_BUNDLE_TOOL = "workbench_modules_install_bundle";
2847
+ var MODULES_UPDATE_DEPENDENCY_TOOL = "workbench_modules_update_dependency";
2848
+ var MODULES_UPDATE_PLUGIN_TOOL = "workbench_modules_update_plugin";
2849
+ var MODULES_UNINSTALL_PLUGIN_TOOL = "workbench_modules_uninstall_plugin";
2850
+ var MODULES_UPDATE_WORKBENCH_TOOL = "workbench_modules_update_workbench";
2851
+ var DIY_LIST_TOOL = "workbench_diy_list";
2852
+ var DIY_STATUS_TOOL = "workbench_diy_status";
2853
+ var DIY_PANEL_TOOL = "workbench_diy_panel";
2854
+ var DIY_RECON_TOOL = "workbench_diy_recon";
2855
+ var DIY_PROBE_TOOL = "workbench_diy_probe";
2856
+ var DIY_LOOK_TOOL = "workbench_diy_look";
2857
+ var DIY_INSTALL_TOOL = "workbench_diy_install";
2858
+ var DIY_SHOW_TOOL = "workbench_diy_show";
2859
+ var DIY_CLICK_TOOL = "workbench_diy_click";
2860
+ var DIY_TYPE_TOOL = "workbench_diy_type";
2861
+ var DIY_FILL_TOOL = "workbench_diy_fill";
2862
+ var DIY_KEYS_TOOL = "workbench_diy_keys";
2863
+ var DIY_HOVER_TOOL = "workbench_diy_hover";
2864
+ var DIY_SELECT_TOOL = "workbench_diy_select";
2865
+ var DIY_SCROLL_TOOL = "workbench_diy_scroll";
2866
+ var DIY_RELOAD_TOOL = "workbench_diy_reload";
2867
+ var DIY_VIEWPORT_TOOL = "workbench_diy_viewport";
2868
+ var DIY_NET_WATCH_TOOL = "workbench_diy_net_watch";
2869
+ var DIY_NET_READ_TOOL = "workbench_diy_net_read";
2870
+ var DIY_NET_STOP_TOOL = "workbench_diy_net_stop";
2871
+ var DIY_LOGS_WATCH_TOOL = "workbench_diy_logs_watch";
2872
+ var DIY_LOGS_READ_TOOL = "workbench_diy_logs_read";
2873
+ var DIY_LOGS_STOP_TOOL = "workbench_diy_logs_stop";
2874
+ var READ_TOOLS = [
2875
+ PANELS_LIST_TOOL,
2876
+ MODULES_LIST_TOOL,
2877
+ DIY_LIST_TOOL,
2878
+ DIY_STATUS_TOOL,
2879
+ DIY_PANEL_TOOL,
2880
+ DIY_RECON_TOOL,
2881
+ DIY_PROBE_TOOL,
2882
+ DIY_LOOK_TOOL,
2883
+ DIY_CLICK_TOOL,
2884
+ DIY_TYPE_TOOL,
2885
+ DIY_FILL_TOOL,
2886
+ DIY_KEYS_TOOL,
2887
+ DIY_HOVER_TOOL,
2888
+ DIY_SELECT_TOOL,
2889
+ DIY_SCROLL_TOOL,
2890
+ DIY_RELOAD_TOOL,
2891
+ DIY_VIEWPORT_TOOL,
2892
+ DIY_NET_WATCH_TOOL,
2893
+ DIY_NET_READ_TOOL,
2894
+ DIY_NET_STOP_TOOL,
2895
+ DIY_LOGS_WATCH_TOOL,
2896
+ DIY_LOGS_READ_TOOL,
2897
+ DIY_LOGS_STOP_TOOL
2898
+ ];
2899
+ var WRITE_TOOLS = [
2900
+ PANELS_SHOW_TOOL,
2901
+ PANELS_HIDE_TOOL,
2902
+ PANELS_MOVE_TOOL,
2903
+ PANELS_RESET_TOOL,
2904
+ PANELS_FOLLOW_TOOL,
2905
+ MODULES_INSTALL_TOOL,
2906
+ MODULES_UPDATE_TOOL,
2907
+ MODULES_UNINSTALL_TOOL,
2908
+ MODULES_INSTALL_BUNDLE_TOOL,
2909
+ MODULES_UPDATE_DEPENDENCY_TOOL,
2910
+ MODULES_UPDATE_PLUGIN_TOOL,
2911
+ MODULES_UNINSTALL_PLUGIN_TOOL,
2912
+ MODULES_UPDATE_WORKBENCH_TOOL,
2913
+ DIY_INSTALL_TOOL,
2914
+ DIY_SHOW_TOOL
2915
+ ];
2916
+ var DIY_TOOLS = [
2917
+ DIY_LIST_TOOL,
2918
+ DIY_STATUS_TOOL,
2919
+ DIY_PANEL_TOOL,
2920
+ DIY_RECON_TOOL,
2921
+ DIY_PROBE_TOOL,
2922
+ DIY_LOOK_TOOL,
2923
+ DIY_INSTALL_TOOL,
2924
+ DIY_SHOW_TOOL,
2925
+ DIY_CLICK_TOOL,
2926
+ DIY_TYPE_TOOL,
2927
+ DIY_FILL_TOOL,
2928
+ DIY_KEYS_TOOL,
2929
+ DIY_HOVER_TOOL,
2930
+ DIY_SELECT_TOOL,
2931
+ DIY_SCROLL_TOOL,
2932
+ DIY_RELOAD_TOOL,
2933
+ DIY_VIEWPORT_TOOL,
2934
+ DIY_NET_WATCH_TOOL,
2935
+ DIY_NET_READ_TOOL,
2936
+ DIY_NET_STOP_TOOL,
2937
+ DIY_LOGS_WATCH_TOOL,
2938
+ DIY_LOGS_READ_TOOL,
2939
+ DIY_LOGS_STOP_TOOL
2940
+ ];
2838
2941
  var panelsUnit = {
2839
2942
  id: "panels",
2840
2943
  card: null,
2841
- tools: [
2842
- {
2843
- name: PANELS_TOOL,
2844
- writes: true
2845
- },
2846
- {
2847
- name: MODULES_TOOL,
2848
- writes: true
2849
- },
2850
- {
2851
- name: DIY_TOOL,
2852
- writes: true
2853
- }
2854
- ],
2944
+ tools: [...READ_TOOLS.map((name) => ({
2945
+ name,
2946
+ writes: false
2947
+ })), ...WRITE_TOOLS.map((name) => ({
2948
+ name,
2949
+ writes: true
2950
+ }))],
2855
2951
  host: {
2856
2952
  inject: [],
2857
2953
  optional: ["tools", "shell"]
@@ -2859,14 +2955,6 @@ var panelsUnit = {
2859
2955
  };
2860
2956
  //#endregion
2861
2957
  //#region src/modules/panels/model/approval.ts
2862
- var READ = { kind: "read" };
2863
- var READ_ONLY_ACTIONS = /* @__PURE__ */ new Set([
2864
- "list",
2865
- "status",
2866
- "probe",
2867
- "recon",
2868
- ""
2869
- ]);
2870
2958
  var SPEC_LIMIT = 80;
2871
2959
  function missing(id) {
2872
2960
  return id === void 0 || id === "";
@@ -2874,6 +2962,12 @@ function missing(id) {
2874
2962
  function pad(token) {
2875
2963
  return token.startsWith("[") ? ` ${token} ` : token;
2876
2964
  }
2965
+ function pair(zh, en) {
2966
+ return {
2967
+ zh,
2968
+ en
2969
+ };
2970
+ }
2877
2971
  function workbenchModule(id) {
2878
2972
  return missing(id) ? {
2879
2973
  zh: "工作台模块",
@@ -2883,12 +2977,6 @@ function workbenchModule(id) {
2883
2977
  en: `the workbench [${id}] module`
2884
2978
  };
2885
2979
  }
2886
- function anyWorkbenchModule(id) {
2887
- return missing(id) ? {
2888
- zh: "工作台的一个模块",
2889
- en: "a workbench module"
2890
- } : workbenchModule(id);
2891
- }
2892
2980
  function workbenchPlugin(id) {
2893
2981
  return missing(id) ? {
2894
2982
  zh: "工作台的 opencli 插件",
@@ -2916,18 +3004,24 @@ function workbenchPanelRow(id) {
2916
3004
  en: `the workbench [${id}] module row`
2917
3005
  };
2918
3006
  }
2919
- function actionOf$1(call) {
2920
- const raw = call.arguments;
2921
- if (typeof raw !== "object" || raw === null) return {};
2922
- const value = raw;
2923
- const target = typeof value.plugin === "string" ? value.plugin : typeof value.name === "string" ? value.name : typeof value.id === "string" ? value.id : typeof value.unit === "string" ? value.unit : void 0;
2924
- const spec = typeof value.spec === "string" ? value.spec.replace(/\s+/g, " ").trim() : "";
2925
- return {
2926
- ...typeof value.action === "string" ? { action: value.action } : {},
2927
- ...target === void 0 ? {} : { id: target },
2928
- ...spec === "" ? {} : { spec: spec.length > SPEC_LIMIT ? `${spec.slice(0, SPEC_LIMIT)}…` : spec }
3007
+ function specOf(raw) {
3008
+ const spec = typeof raw["spec"] === "string" ? raw["spec"].replace(/\s+/g, " ").trim() : "";
3009
+ if (spec === "") return "";
3010
+ return spec.length > SPEC_LIMIT ? `${spec.slice(0, SPEC_LIMIT)}…` : spec;
3011
+ }
3012
+ function boardName(scope) {
3013
+ return scope === "machine" ? {
3014
+ zh: "机器级默认板",
3015
+ en: "the machine-level board"
3016
+ } : {
3017
+ zh: "当前项目的面板",
3018
+ en: "this project's panel"
2929
3019
  };
2930
3020
  }
3021
+ function argsOf$1(call) {
3022
+ const raw = call.arguments;
3023
+ return typeof raw === "object" && raw !== null ? raw : {};
3024
+ }
2931
3025
  function askOf(reason, question, fallback) {
2932
3026
  return {
2933
3027
  kind: "ask",
@@ -2937,152 +3031,62 @@ function askOf(reason, question, fallback) {
2937
3031
  };
2938
3032
  }
2939
3033
  var panelsApproval = (call) => {
2940
- const { action, id, spec } = actionOf$1(call);
2941
- const readAction = action !== void 0 && action !== "" && READ_ONLY_ACTIONS.has(action);
2942
- if (call.name === "workbench_modules") {
2943
- if (readAction) return READ;
2944
- if (action === "update-plugin" || action === "uninstall-plugin") {
2945
- const verb = action === "update-plugin" ? "更新" : "卸载";
2946
- const verbEn = action === "update-plugin" ? "update" : "uninstall";
2947
- const target = workbenchPlugin(id);
2948
- return askOf(`workbench_modules ${action} ${id ?? ""} changes an opencli plugin on this machine`, {
2949
- en: `it ${verbEn}s ${target.en}. Allow?`,
2950
- zh: `会${verb}${target.zh},允许吗?`
2951
- }, {
2952
- en: `The agent wants to ${verbEn} ${target.en}. Allow?`,
2953
- zh: `agent 想${verb}${target.zh},允许吗?`
2954
- });
2955
- }
2956
- if (action === "update-workbench") return askOf("workbench_modules update-workbench replaces the workbench itself with the new version from the registry", {
2957
- en: "it updates the workbench itself to the new version from the registry. Allow?",
2958
- zh: "会把工作台本身更新到源上的新版,允许吗?"
2959
- }, {
2960
- en: "The agent wants to update the workbench itself to the new version from the registry. Allow?",
2961
- zh: "agent 想把工作台本身更新到源上的新版,允许吗?"
2962
- });
2963
- if (action === "update-dependency") {
2964
- const target = workbenchDependency(id);
2965
- return askOf(`workbench_modules update-dependency ${id ?? ""} upgrades a shared dependency on this machine`, {
2966
- en: `it upgrades ${target.en}. Allow?`,
2967
- zh: `会升级${target.zh},允许吗?`
2968
- }, {
2969
- en: `The agent wants to upgrade ${target.en}. Allow?`,
2970
- zh: `agent 想升级${target.zh},允许吗?`
2971
- });
2972
- }
2973
- if (action === "install-bundle" && spec !== void 0) return askOf(`workbench_modules install-bundle ${spec} installs a workbench module from a spec`, {
2974
- en: `it installs the workbench module from [${spec}]. Allow?`,
2975
- zh: `会安装工作台模块 [${spec}],允许吗?`
2976
- }, {
2977
- en: `The agent wants to install the workbench module from [${spec}]. Allow?`,
2978
- zh: `agent 想安装工作台模块 [${spec}],允许吗?`
2979
- });
2980
- if (action === "install") {
3034
+ if (call.name !== void 0 && DIY_TOOLS.includes(call.name)) return { kind: "allow" };
3035
+ const raw = argsOf$1(call);
3036
+ const text = (key) => {
3037
+ const value = raw[key];
3038
+ return typeof value === "string" && value !== "" ? value : void 0;
3039
+ };
3040
+ const id = text("id") ?? text("unit");
3041
+ const plugin = text("plugin");
3042
+ const spec = specOf(raw);
3043
+ const scope = text("scope");
3044
+ const board = boardName(scope);
3045
+ switch (call.name) {
3046
+ case PANELS_SHOW_TOOL: {
3047
+ const row = workbenchPanelRow(id);
3048
+ return askOf(`${PANELS_SHOW_TOOL} ${id ?? ""} puts a workbench module row on this project's board`, pair(`会把${row.zh}加回${board.zh},允许吗?`, `it puts ${row.en} back on ${board.en}. Allow?`), pair(`agent 想把${row.zh}加到${board.zh},允许吗?`, `The agent wants to put ${row.en} on ${board.en}. Allow?`));
3049
+ }
3050
+ case PANELS_HIDE_TOOL: {
3051
+ const row = workbenchPanelRow(id);
3052
+ return askOf(`${PANELS_HIDE_TOOL} ${id ?? ""} takes a workbench module row off this project's board`, pair(`会把${row.zh}从${board.zh}上收起,允许吗?`, `it takes ${row.en} off ${board.en} — its card and its tools go too. Allow?`), pair(`agent 想把${row.zh}收起,允许吗?`, `The agent wants to take ${row.en} off the board. Allow?`));
3053
+ }
3054
+ case PANELS_MOVE_TOOL: {
3055
+ const row = workbenchPanelRow(id);
3056
+ return askOf(`${PANELS_MOVE_TOOL} ${id ?? ""} reorders the workbench panel`, pair(`会把${row.zh}移到${board.zh}最前面,允许吗?`, `it moves ${row.en} to the front of ${board.en}. Allow?`), pair(`agent 想把${row.zh}移到最前面,允许吗?`, `The agent wants to move ${row.en} to the front. Allow?`));
3057
+ }
3058
+ case PANELS_RESET_TOOL: return askOf(`${PANELS_RESET_TOOL} clears the rows this project changed on the workbench panel`, pair(scope === "machine" ? "会清空工作台那份机器级默认板,允许吗?" : "会清空工作台在当前项目的面板,允许吗?", `it clears the rows this project changed on the workbench ${board.en}. Allow?`), pair(scope === "machine" ? "agent 想清空工作台那份机器级默认板,允许吗?" : "agent 想清空工作台在当前项目的面板,允许吗?", `The agent wants to clear this project's rows on the workbench ${board.en}. Allow?`));
3059
+ case PANELS_FOLLOW_TOOL: {
3060
+ const row = workbenchPanelRow(id);
3061
+ return askOf(`${PANELS_FOLLOW_TOOL} ${id ?? ""} puts a workbench panel row back to following the machine-level board`, pair(`会把${row.zh}改回跟随机器级默认板,允许吗?`, `it puts ${row.en} back to following the machine-level board. Allow?`), pair(`agent 想把${row.zh}改回跟随机器级默认板,允许吗?`, `The agent wants to put ${row.en} back to following the machine-level board. Allow?`));
3062
+ }
3063
+ case MODULES_INSTALL_TOOL:
3064
+ case MODULES_UPDATE_TOOL: {
3065
+ const install = call.name === MODULES_INSTALL_TOOL;
3066
+ const verb = install ? "装" : "升";
3067
+ const verbEn = install ? "installs" : "upgrades";
3068
+ return askOf(`${call.name} ${id ?? ""} changes the opencli plugins and libraries a workbench module declares`, pair(`会${verb}${workbenchModule(id).zh}要的插件与库,不换模块包本身,允许吗?`, `it ${verbEn} the plugins the ${workbenchModule(id).en} declares, without replacing its package. Allow?`), pair(`agent 想${verb}${workbenchModule(id).zh}要的插件与库,允许吗?`, `The agent wants to ${install ? "install" : "upgrade"} the plugins ${workbenchModule(id).en} declares. Allow?`));
3069
+ }
3070
+ case MODULES_UNINSTALL_TOOL: {
2981
3071
  const target = workbenchModule(id);
2982
- return askOf(`workbench_modules install ${id ?? ""} installs a workbench module on this machine`, {
2983
- en: `it installs ${target.en}. Allow?`,
2984
- zh: `会安装${target.zh},允许吗?`
2985
- }, {
2986
- en: `The agent wants to install ${target.en}. Allow?`,
2987
- zh: `agent 想安装${target.zh},允许吗?`
2988
- });
3072
+ return askOf(`${MODULES_UNINSTALL_TOOL} ${id ?? ""} removes a workbench module from this machine`, pair(`会把${target.zh}连它的包一起卸掉,允许吗?`, `it uninstalls ${target.en}, package included. Allow?`), pair(`agent 想卸载${target.zh},允许吗?`, `The agent wants to uninstall ${target.en}. Allow?`));
2989
3073
  }
2990
- if (action === "update") {
2991
- const target = workbenchModule(id);
2992
- return askOf(`workbench_modules update ${id ?? ""} updates a workbench module on this machine`, {
2993
- en: `it updates ${target.en}. Allow?`,
2994
- zh: `会更新${target.zh},允许吗?`
2995
- }, {
2996
- en: `The agent wants to update ${target.en}. Allow?`,
2997
- zh: `agent 想更新${target.zh},允许吗?`
2998
- });
3074
+ case MODULES_INSTALL_BUNDLE_TOOL: return askOf(`${MODULES_INSTALL_BUNDLE_TOOL} ${spec} installs a module package into this profile`, pair(`会把 [${spec}] 装成工作台的模块包,允许吗?`, `it installs [${spec}] as a workbench module package. Allow?`), pair(`agent 想装工作台的模块包 [${spec}],允许吗?`, `The agent wants to install the workbench module package [${spec}]. Allow?`));
3075
+ case MODULES_UPDATE_DEPENDENCY_TOOL: {
3076
+ const target = workbenchDependency(id);
3077
+ return askOf(`${MODULES_UPDATE_DEPENDENCY_TOOL} ${id ?? ""} upgrades a shared dependency on this machine`, pair(`会升级${target.zh},允许吗?`, `it upgrades ${target.en}. Allow?`), pair(`agent 想升级${target.zh},允许吗?`, `The agent wants to upgrade ${target.en}. Allow?`));
2999
3078
  }
3000
- if (action === "uninstall") {
3001
- const target = workbenchModule(id);
3002
- return askOf(`workbench_modules uninstall ${id ?? ""} removes a workbench module from this machine`, {
3003
- en: `it uninstalls ${target.en}. Allow?`,
3004
- zh: `会卸载${target.zh},允许吗?`
3005
- }, {
3006
- en: `The agent wants to uninstall ${target.en}. Allow?`,
3007
- zh: `agent 想卸载${target.zh},允许吗?`
3008
- });
3079
+ case MODULES_UPDATE_PLUGIN_TOOL: {
3080
+ const target = workbenchPlugin(plugin);
3081
+ return askOf(`${MODULES_UPDATE_PLUGIN_TOOL} ${plugin ?? ""} updates an opencli plugin on this machine`, pair(`会更新${target.zh},允许吗?`, `it updates ${target.en}. Allow?`), pair(`agent 想更新${target.zh},允许吗?`, `The agent wants to update ${target.en}. Allow?`));
3009
3082
  }
3010
- return askOf(`workbench_modules ${action ?? "(unknown action)"} ${id ?? ""} changes plugins on this machine`.trim(), {
3011
- en: "it changes the plugins installed in the workbench. Allow?",
3012
- zh: "会改动工作台装的插件,允许吗?"
3013
- }, {
3014
- en: "The agent wants to change the plugins installed in the workbench. Allow?",
3015
- zh: "agent 想改动工作台装的插件,允许吗?"
3016
- });
3017
- }
3018
- if (call.name === "workbench_diy") {
3019
- if (readAction) return READ;
3020
- if (action === "open") {
3021
- const target = anyWorkbenchModule(id);
3022
- return askOf("workbench_diy open puts an already-installed module on this project panel", {
3023
- en: `it shows ${target.en} on this project's panel. Allow?`,
3024
- zh: `会把${target.zh}显示在当前项目的面板上,允许吗?`
3025
- }, {
3026
- en: `The agent wants to show ${target.en} on this project's panel. Allow?`,
3027
- zh: `agent 想把${target.zh}显示在当前项目的面板上,允许吗?`
3028
- });
3083
+ case MODULES_UNINSTALL_PLUGIN_TOOL: {
3084
+ const target = workbenchPlugin(plugin);
3085
+ return askOf(`${MODULES_UNINSTALL_PLUGIN_TOOL} ${plugin ?? ""} uninstalls an opencli plugin on this machine`, pair(`会卸载${target.zh},允许吗?`, `it uninstalls ${target.en}. Allow?`), pair(`agent 想卸载${target.zh},允许吗?`, `The agent wants to uninstall ${target.en}. Allow?`));
3029
3086
  }
3030
- const target = workbenchModule(id);
3031
- return askOf(`workbench_diy ${action ?? "(unknown action)"} installs a module built in the workshop and opens it on this project's panel`, {
3032
- en: `it installs ${target.en} and opens it on this project's panel. Allow?`,
3033
- zh: `会安装${target.zh},并上到当前项目的面板,允许吗?`
3034
- }, {
3035
- en: `The agent wants to install ${target.en} and open it on this project's panel. Allow?`,
3036
- zh: `agent 想安装${target.zh},并上到当前项目的面板,允许吗?`
3037
- });
3038
- }
3039
- if (call.name !== "workbench_panels") return void 0;
3040
- if (readAction) return READ;
3041
- if (action === "hide") {
3042
- const target = anyWorkbenchModule(id);
3043
- return askOf("workbench_panels hide removes a module from the panel", {
3044
- en: `it takes ${target.en} off this project's panel. Allow?`,
3045
- zh: `会把${target.zh}从当前项目的面板上收起来,允许吗?`
3046
- }, {
3047
- en: `The agent wants to take ${target.en} off this project's panel. Allow?`,
3048
- zh: `agent 想把${target.zh}从当前项目的面板上收起来,允许吗?`
3049
- });
3050
- }
3051
- if (action === "move") {
3052
- const target = anyWorkbenchModule(id);
3053
- return askOf("workbench_panels move reorders the panel", {
3054
- en: `it moves ${target.en} to the front of this project's panel. Allow?`,
3055
- zh: `会把${target.zh}移到当前项目面板的最前面,允许吗?`
3056
- }, {
3057
- en: `The agent wants to move ${target.en} to the front of this project's panel. Allow?`,
3058
- zh: `agent 想把${target.zh}移到当前项目面板的最前面,允许吗?`
3059
- });
3060
- }
3061
- if (action === "reset") return askOf("workbench_panels reset clears the panel", {
3062
- en: `it clears the workbench panel for this project. Allow?`,
3063
- zh: "会清空工作台在当前项目的面板,允许吗?"
3064
- }, {
3065
- en: "The agent wants to clear the workbench panel for this project. Allow?",
3066
- zh: "agent 想清空工作台在当前项目的面板,允许吗?"
3067
- });
3068
- if (action === "follow") {
3069
- const target = workbenchPanelRow(id);
3070
- return askOf("workbench_panels follow puts a row back to following the machine-level board", {
3071
- en: `it puts ${target.en} back to following the machine-level board. Allow?`,
3072
- zh: `会把${target.zh}改回跟随机器级默认板,允许吗?`
3073
- }, {
3074
- en: `The agent wants to put ${target.en} back to following the machine-level board. Allow?`,
3075
- zh: `agent 想把${target.zh}改回跟随机器级默认板,允许吗?`
3076
- });
3087
+ case MODULES_UPDATE_WORKBENCH_TOOL: return askOf(`${MODULES_UPDATE_WORKBENCH_TOOL} replaces the workbench itself with the new version from the registry`, pair("会把工作台本身更新到源上的新版,允许吗?", "it updates the workbench itself to the new version from the registry. Allow?"), pair("agent 想把工作台本身更新到源上的新版,允许吗?", "The agent wants to update the workbench itself from the registry. Allow?"));
3088
+ default: return;
3077
3089
  }
3078
- const target = anyWorkbenchModule(id);
3079
- return askOf(`workbench_panels ${action ?? "(unknown action)"} changes the user's panel`, {
3080
- en: `it adds ${target.en} to this project's panel. Allow?`,
3081
- zh: `会把${target.zh}加到当前项目的面板上,允许吗?`
3082
- }, {
3083
- en: `The agent wants to add ${target.en} to this project's panel. Allow?`,
3084
- zh: `agent 想把${target.zh}加到当前项目的面板上,允许吗?`
3085
- });
3086
3090
  };
3087
3091
  //#endregion
3088
3092
  //#region src/modules/panels/model/config.ts
@@ -3264,116 +3268,1983 @@ function opencliCommandOf(ctx) {
3264
3268
  isolated
3265
3269
  };
3266
3270
  }
3267
- //#endregion
3268
- //#region src/shared/api/host/diy-recon.ts
3269
- var SITE_LIMIT = 6;
3270
- var COMMAND_LIMIT = 12;
3271
- var SHOWN_LIMIT = 6;
3272
- var PLUGIN_PREFIX = "opencli-plugin-";
3273
- function text(value) {
3271
+ var OUTPUT$2 = {
3272
+ schema: {
3273
+ type: "object",
3274
+ properties: {
3275
+ text: { type: "string" },
3276
+ workshop: { type: "string" },
3277
+ modules: {
3278
+ type: "array",
3279
+ items: {
3280
+ type: "object",
3281
+ additionalProperties: true
3282
+ }
3283
+ },
3284
+ unit: { type: "string" },
3285
+ path: { type: "string" },
3286
+ install: {
3287
+ type: "object",
3288
+ additionalProperties: true
3289
+ },
3290
+ alreadyInstalled: { type: "boolean" },
3291
+ panel: {
3292
+ type: "object",
3293
+ additionalProperties: true
3294
+ }
3295
+ },
3296
+ additionalProperties: true
3297
+ },
3298
+ render: (_args, value) => {
3299
+ const payload = value;
3300
+ return [{
3301
+ type: "text",
3302
+ text: typeof payload?.text === "string" ? payload.text : JSON.stringify(value)
3303
+ }];
3304
+ }
3305
+ };
3306
+ function asRecord$3(value) {
3307
+ return typeof value === "object" && value !== null ? value : {};
3308
+ }
3309
+ function asNumber(value) {
3310
+ if (typeof value === "number" && Number.isFinite(value)) return value;
3311
+ if (typeof value === "string" && value.trim() !== "") {
3312
+ const parsed = Number(value);
3313
+ if (Number.isFinite(parsed)) return parsed;
3314
+ }
3315
+ }
3316
+ function asText$1(value) {
3274
3317
  return typeof value === "string" ? value.trim() : "";
3275
3318
  }
3276
- function stringsOf$1(value) {
3277
- return Array.isArray(value) ? value.filter((one) => typeof one === "string") : [];
3319
+ function plainJson(value) {
3320
+ return JSON.parse(JSON.stringify(value));
3278
3321
  }
3279
- function manifestCommandsOf(raw) {
3280
- let parsed;
3281
- try {
3282
- parsed = JSON.parse(raw);
3283
- } catch {
3284
- return [];
3322
+ function verdictOf$1(change) {
3323
+ if (change.application === "applied") return "装好了,已生效";
3324
+ if (change.application === "restart-required") return "装好了,要重启一次才生效(装新模块、同名重装、替换已有包都走这一支)";
3325
+ if (change.application === "overridden") return "被更高层的声明盖住了,这次没生效";
3326
+ if (change.application === "cancelled") return "安装被取消,没有生效";
3327
+ const tail = (change.packageResult?.output ?? "").trim().split("\n").slice(-3).join(" | ");
3328
+ return `没装上(${change.errorCode ?? "failed"})${tail === "" ? "" : `:${tail}`}`;
3329
+ }
3330
+ function renderProbe(out) {
3331
+ if (out.ok) return `读到了 —— 这就是那条读的返回值,把它固化成模块宿主半侧的那一条:\n${(JSON.stringify(out.value, null, 2) ?? "undefined").slice(0, 2e3)}`;
3332
+ const lines = [`没读到(${out.code}):${out.message}`];
3333
+ if (out.hint !== void 0) lines.push(`照做:${out.hint}`);
3334
+ if (out.helpUrl !== void 0) lines.push(`入口:${out.helpUrl}`);
3335
+ if (out.pageCode !== void 0) lines.push(`页面自己回的码:${out.pageCode}`);
3336
+ return lines.join("\n");
3337
+ }
3338
+ function renderFind(out) {
3339
+ if (!out.ok) {
3340
+ const lines = [`没找到(${out.code}):${out.message}`];
3341
+ if (out.hint !== void 0) lines.push(`照做:${out.hint}`);
3342
+ if (out.helpUrl !== void 0) lines.push(`入口:${out.helpUrl}`);
3343
+ return lines.join("\n");
3285
3344
  }
3286
- if (!Array.isArray(parsed)) return [];
3287
- const commands = [];
3288
- for (const one of parsed) {
3289
- if (typeof one !== "object" || one === null) continue;
3290
- const row = one;
3291
- const declared = text(row["command"]);
3292
- const site = text(row["site"]) || declared.split("/")[0] || "";
3293
- const name = text(row["name"]) || declared.split("/")[1] || "";
3294
- const command = declared === "" ? site === "" || name === "" ? "" : `${site}/${name}` : declared;
3295
- if (command === "" || site === "") continue;
3296
- const access = text(row["access"]);
3297
- commands.push({
3298
- command,
3299
- site,
3300
- name: name === "" ? command.slice(command.indexOf("/") + 1) : name,
3301
- domain: text(row["domain"]).toLowerCase(),
3302
- description: text(row["description"]),
3303
- access: access === "" ? "read" : access,
3304
- strategy: text(row["strategy"]).toLowerCase(),
3305
- browser: row["browser"] === true,
3306
- example: text(row["example"]),
3307
- columns: stringsOf$1(row["columns"])
3308
- });
3345
+ if (out.count === 0) return "找到了 0 个元素 —— 选择器没匹配到东西。先确认停在哪一页(probe 会自己问页面),再把选择器放宽一点(例如 table tr 而不是某个 id)。";
3346
+ const head = out.entries.slice(0, 20).map((one, index) => `${String(index + 1)}. ${one.text.replace(/\n+/g, " | ").slice(0, 200)}`);
3347
+ const more = out.entries.length > 20 ? `\n… 还有 ${String(out.entries.length - 20)} 个` : "";
3348
+ return `找到了 ${String(out.count)} 个元素 —— 每行的单元格按换行拼在 text 里;把它固化成模块宿主半侧的那一条(host 的 browserFind + 切格 + 按列序取值):\n${head.join("\n")}${more}`;
3349
+ }
3350
+ function showOrReason(panels, unit, workspace) {
3351
+ try {
3352
+ panels.show(unit, workspace);
3353
+ return { ok: true };
3354
+ } catch (error) {
3355
+ return {
3356
+ ok: false,
3357
+ reason: error instanceof Error ? error.message : String(error)
3358
+ };
3309
3359
  }
3310
- return commands;
3311
3360
  }
3312
- function pluginNamesOf(entries) {
3313
- return entries.filter((one) => one.startsWith(PLUGIN_PREFIX)).map((one) => one.slice(15)).filter((one) => one !== "").sort();
3361
+ function workshopOf(ctx, exec) {
3362
+ const packagesDir = workshopPackagesDir(ctx);
3363
+ if (packagesDir === null) throw new Error("算不出 DIY 工坊在哪(这台机器没给出 dshHomePath)—— 让用户跑一次 dsh-workbench setup");
3364
+ const session = sessionWorkspace(exec);
3365
+ if (!inWorkshopSession(ctx, session)) throw new Error(`这把工具只在 DIY 工坊里用:当前会话不在工坊里(这个会话的工作区 ${session ?? "(不知道)"})—— 普通项目里装模块用 workbench_modules_install,工坊的入口是左栏那一行「工作台DIY」`);
3366
+ return {
3367
+ packagesDir,
3368
+ workspace: session
3369
+ };
3314
3370
  }
3315
- function pluginCommandsOf(files) {
3316
- return files.filter((one) => one.endsWith(".js")).map((one) => one.slice(0, -3)).filter((one) => one !== "index" && !one.startsWith("_")).sort();
3371
+ function modulesOf(ctx, packagesDir) {
3372
+ const installed = profileModules(profileDirOf(ctx)).map((row) => row.unit);
3373
+ return {
3374
+ installed,
3375
+ modules: workshopModules(packagesDir, installed)
3376
+ };
3317
3377
  }
3318
- function hostOf(url) {
3319
- const trimmed = url.trim();
3320
- if (trimmed === "") return "";
3378
+ function panelEntriesOf(panels, workspace) {
3321
3379
  try {
3322
- return new URL(trimmed).hostname.toLowerCase().replace(/^www\./, "");
3380
+ return panels.list(workspace);
3323
3381
  } catch {
3324
- return "";
3382
+ return [];
3325
3383
  }
3326
3384
  }
3327
- function keywordsOf(keyword, url) {
3328
- const words = /* @__PURE__ */ new Set();
3329
- const trimmed = keyword.trim().toLowerCase();
3330
- if (trimmed !== "") words.add(trimmed);
3331
- const host = hostOf(url);
3332
- if (host !== "") words.add(host);
3333
- return [...words];
3385
+ //#endregion
3386
+ //#region src/shared/api/host/png-crop.ts
3387
+ var SIGNATURE = Buffer.from([
3388
+ 137,
3389
+ 80,
3390
+ 78,
3391
+ 71,
3392
+ 13,
3393
+ 10,
3394
+ 26,
3395
+ 10
3396
+ ]);
3397
+ var CHANNELS = {
3398
+ 0: 1,
3399
+ 2: 3,
3400
+ 4: 2,
3401
+ 6: 4
3402
+ };
3403
+ var CRC_TABLE = (() => {
3404
+ const table = /* @__PURE__ */ new Int32Array(256);
3405
+ for (let index = 0; index < 256; index += 1) {
3406
+ let value = index;
3407
+ for (let bit = 0; bit < 8; bit += 1) value = (value & 1) === 1 ? 3988292384 ^ value >>> 1 : value >>> 1;
3408
+ table[index] = value;
3409
+ }
3410
+ return table;
3411
+ })();
3412
+ function crc32(bytes) {
3413
+ let value = -1;
3414
+ for (const byte of bytes) value = CRC_TABLE[(value ^ byte) & 255] ^ value >>> 8;
3415
+ return (value ^ -1) >>> 0;
3416
+ }
3417
+ function chunkOf(type, data) {
3418
+ const head = Buffer.alloc(4);
3419
+ head.writeUInt32BE(data.length, 0);
3420
+ const body = Buffer.concat([Buffer.from(type, "latin1"), data]);
3421
+ const tail = Buffer.alloc(4);
3422
+ tail.writeUInt32BE(crc32(body), 0);
3423
+ return Buffer.concat([
3424
+ head,
3425
+ body,
3426
+ tail
3427
+ ]);
3334
3428
  }
3335
- function scoreOf(command, word) {
3336
- const site = command.site.toLowerCase();
3337
- const name = command.name.toLowerCase();
3338
- if (word === "") return 0;
3339
- if (site === word) return 100;
3340
- if (site.includes(word)) return 80;
3341
- if (command.domain !== "" && command.domain.includes(word)) return 70;
3342
- if (site.length >= 3 && word.includes(site)) return 45;
3343
- if (name === word) return 65;
3344
- if (name.includes(word)) return 55;
3345
- if (command.command.toLowerCase().includes(word)) return 50;
3346
- if (command.description.toLowerCase().includes(word)) return 30;
3347
- return 0;
3429
+ function paethOf(left, above, corner) {
3430
+ const estimate = left + above - corner;
3431
+ const toLeft = Math.abs(estimate - left);
3432
+ const toAbove = Math.abs(estimate - above);
3433
+ const toCorner = Math.abs(estimate - corner);
3434
+ if (toLeft <= toAbove && toLeft <= toCorner) return left;
3435
+ return toAbove <= toCorner ? above : corner;
3436
+ }
3437
+ function headerOf(bytes) {
3438
+ if (bytes.length < 8 || !bytes.subarray(0, 8).equals(SIGNATURE)) return { reason: "不是一张 PNG" };
3439
+ let at = 8;
3440
+ let raw;
3441
+ const parts = [];
3442
+ while (at + 8 <= bytes.length) {
3443
+ const length = bytes.readUInt32BE(at);
3444
+ const type = bytes.toString("latin1", at + 4, at + 8);
3445
+ const data = bytes.subarray(at + 8, at + 8 + length);
3446
+ if (type === "IHDR") raw = {
3447
+ width: data.readUInt32BE(0),
3448
+ height: data.readUInt32BE(4),
3449
+ depth: data[8] ?? 0,
3450
+ color: data[9] ?? 0,
3451
+ interlace: data[12] ?? 0
3452
+ };
3453
+ else if (type === "IDAT") parts.push(Buffer.from(data));
3454
+ else if (type === "IEND") break;
3455
+ at += 12 + length;
3456
+ }
3457
+ if (raw === void 0) return { reason: "这张 PNG 没有 IHDR" };
3458
+ const channels = CHANNELS[raw.color] ?? 0;
3459
+ if (raw.depth !== 8 || channels === 0) return { reason: `这张 PNG 的形状不认(位深 ${String(raw.depth)} / 颜色类型 ${String(raw.color)})` };
3460
+ if (raw.interlace !== 0) return { reason: "这张 PNG 是隔行的,不认" };
3461
+ return { header: {
3462
+ ...raw,
3463
+ channels
3464
+ } };
3348
3465
  }
3349
- function reconOf(commands, plugins, keywords) {
3350
- const keyword = keywords[0] ?? "";
3351
- const best = /* @__PURE__ */ new Map();
3352
- for (const command of commands) {
3353
- let score = 0;
3354
- for (const word of keywords) score = Math.max(score, scoreOf(command, word));
3355
- if (score === 0) continue;
3356
- const current = best.get(command.command);
3357
- if (current === void 0 || current.score < score) best.set(command.command, {
3358
- command,
3359
- score
3360
- });
3466
+ function rowsOf$1(header, parts, keep) {
3467
+ const stride = header.width * header.channels;
3468
+ const inflated = inflateSync(Buffer.concat(parts));
3469
+ const above = Buffer.alloc(stride);
3470
+ const current = Buffer.alloc(stride);
3471
+ const out = Buffer.alloc(stride * keep);
3472
+ let at = 0;
3473
+ for (let y = 0; y < header.height; y += 1) {
3474
+ const filter = inflated[at] ?? 0;
3475
+ const line = inflated.subarray(at + 1, at + 1 + stride);
3476
+ at += 1 + stride;
3477
+ for (let i = 0; i < stride; i += 1) {
3478
+ const source = line[i] ?? 0;
3479
+ const left = i >= header.channels ? current[i - header.channels] ?? 0 : 0;
3480
+ const up = above[i] ?? 0;
3481
+ const corner = i >= header.channels ? above[i - header.channels] ?? 0 : 0;
3482
+ const value = filter === 0 ? source : filter === 1 ? source + left : filter === 2 ? source + up : filter === 3 ? source + (left + up >> 1) : source + paethOf(left, up, corner);
3483
+ current[i] = value & 255;
3484
+ }
3485
+ if (y < keep) current.copy(out, y * stride);
3486
+ above.set(current);
3487
+ if (y + 1 >= keep) break;
3361
3488
  }
3362
- const grouped = /* @__PURE__ */ new Map();
3363
- for (const entry of best.values()) {
3364
- const group = grouped.get(entry.command.site) ?? {
3365
- best: 0,
3366
- rows: []
3489
+ return out;
3490
+ }
3491
+ function encodeOf(width, height, color, pixels) {
3492
+ const stride = width * (CHANNELS[color] ?? 3);
3493
+ const raw = Buffer.alloc((stride + 1) * height);
3494
+ for (let y = 0; y < height; y += 1) {
3495
+ raw[y * (stride + 1)] = 0;
3496
+ pixels.copy(raw, y * (stride + 1) + 1, y * stride, (y + 1) * stride);
3497
+ }
3498
+ const ihdr = Buffer.alloc(13);
3499
+ ihdr.writeUInt32BE(width, 0);
3500
+ ihdr.writeUInt32BE(height, 4);
3501
+ ihdr[8] = 8;
3502
+ ihdr[9] = color;
3503
+ return Buffer.concat([
3504
+ SIGNATURE,
3505
+ chunkOf("IHDR", ihdr),
3506
+ chunkOf("IDAT", deflateSync(raw, { level: 9 })),
3507
+ chunkOf("IEND", Buffer.alloc(0))
3508
+ ]);
3509
+ }
3510
+ function cropPng(bytes, rect) {
3511
+ const found = headerOf(bytes);
3512
+ if ("reason" in found) return {
3513
+ ok: false,
3514
+ bytes: Buffer.alloc(0),
3515
+ width: 0,
3516
+ height: 0,
3517
+ clamped: false,
3518
+ reason: found.reason
3519
+ };
3520
+ const { header } = found;
3521
+ const x = Math.max(0, Math.min(header.width - 1, Math.trunc(rect.x)));
3522
+ const y = Math.max(0, Math.min(header.height - 1, Math.trunc(rect.y)));
3523
+ const width = Math.max(1, Math.min(header.width - x, Math.trunc(rect.width)));
3524
+ const height = Math.max(1, Math.min(header.height - y, Math.trunc(rect.height)));
3525
+ const clamped = x !== Math.trunc(rect.x) || y !== Math.trunc(rect.y) || width !== Math.trunc(rect.width) || height !== Math.trunc(rect.height);
3526
+ const parts = [];
3527
+ let at = 8;
3528
+ while (at + 8 <= bytes.length) {
3529
+ const length = bytes.readUInt32BE(at);
3530
+ const type = bytes.toString("latin1", at + 4, at + 8);
3531
+ if (type === "IDAT") parts.push(Buffer.from(bytes.subarray(at + 8, at + 8 + length)));
3532
+ if (type === "IEND") break;
3533
+ at += 12 + length;
3534
+ }
3535
+ const stride = header.width * header.channels;
3536
+ const rows = rowsOf$1(header, parts, y + height);
3537
+ const outStride = width * header.channels;
3538
+ const pixels = Buffer.alloc(outStride * height);
3539
+ for (let row = 0; row < height; row += 1) {
3540
+ const from = (y + row) * stride + x * header.channels;
3541
+ rows.copy(pixels, row * outStride, from, from + outStride);
3542
+ }
3543
+ return {
3544
+ ok: true,
3545
+ bytes: encodeOf(width, height, header.color, pixels),
3546
+ width,
3547
+ height,
3548
+ clamped,
3549
+ reason: clamped ? "矩形超出图外,已经按边界收进来" : ""
3550
+ };
3551
+ }
3552
+ function cropPngFile(input, output, rect) {
3553
+ let source;
3554
+ try {
3555
+ source = readFileSync(input);
3556
+ } catch (error) {
3557
+ return {
3558
+ ok: false,
3559
+ clamped: false,
3560
+ reason: `读不到那张截图:${error instanceof Error ? error.message : String(error)}`
3367
3561
  };
3368
- group.best = Math.max(group.best, entry.score);
3369
- group.rows.push(entry);
3370
- grouped.set(entry.command.site, group);
3371
3562
  }
3372
- const base = {
3373
- keyword,
3374
- sites: [...grouped.entries()].sort((left, right) => right[1].best - left[1].best || left[0].localeCompare(right[0])).slice(0, SITE_LIMIT).map(([site, group]) => {
3375
- const picked = [...group.rows].sort((left, right) => (left.command.access === "write" ? 1 : 0) - (right.command.access === "write" ? 1 : 0) || right.score - left.score || left.command.command.localeCompare(right.command.command)).slice(0, COMMAND_LIMIT).map((one) => one.command);
3376
- const first = picked[0];
3563
+ const cropped = cropPng(source, rect);
3564
+ if (!cropped.ok) return {
3565
+ ok: false,
3566
+ clamped: false,
3567
+ reason: cropped.reason
3568
+ };
3569
+ try {
3570
+ writeFileSync(output, cropped.bytes);
3571
+ } catch (error) {
3572
+ return {
3573
+ ok: false,
3574
+ clamped: false,
3575
+ reason: `裁好的图写不回去:${error instanceof Error ? error.message : String(error)}`
3576
+ };
3577
+ }
3578
+ return {
3579
+ ok: true,
3580
+ clamped: cropped.clamped,
3581
+ reason: cropped.reason
3582
+ };
3583
+ }
3584
+ //#endregion
3585
+ //#region src/modules/panels/model/diy-scripts.ts
3586
+ function textOf$5(value) {
3587
+ return typeof value === "string" ? value.trim() : "";
3588
+ }
3589
+ function numberIn$1(value, fallback) {
3590
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
3591
+ }
3592
+ var PAGE_PROBE = `(async () => {
3593
+ const answer = await fetch('/workbench/api/health').then((one) => one.json()).catch(() => null);
3594
+ return JSON.stringify({
3595
+ plugin: answer === null ? null : answer.plugin,
3596
+ pid: answer === null ? null : answer.pid,
3597
+ url: location.href,
3598
+ title: document.title,
3599
+ });
3600
+ })()`;
3601
+ function cardSelectorOf(unit) {
3602
+ return `#wb-card-${unit}`;
3603
+ }
3604
+ var DOM_MODES = [
3605
+ "none",
3606
+ "outline",
3607
+ "html"
3608
+ ];
3609
+ var DEFAULT_PROPS = [
3610
+ "display",
3611
+ "flexDirection",
3612
+ "gap",
3613
+ "padding",
3614
+ "width",
3615
+ "height",
3616
+ "overflow",
3617
+ "backgroundColor",
3618
+ "color",
3619
+ "borderRadius",
3620
+ "fontSize",
3621
+ "lineHeight",
3622
+ "fontFamily",
3623
+ "boxSizing",
3624
+ "--dsw-radius-xl",
3625
+ "--dsw-alias-bg-layer-1",
3626
+ "--dsw-alias-label-primary"
3627
+ ];
3628
+ var DEFAULT_PARTS = [".wb-cardHead", ".wb-cardBody"];
3629
+ var MAX_PROPS = 40;
3630
+ var MAX_PARTS = 6;
3631
+ function cleanNames(value, cap) {
3632
+ if (!Array.isArray(value)) return [];
3633
+ const out = [];
3634
+ for (const one of value) {
3635
+ if (typeof one !== "string") continue;
3636
+ const trimmed = one.trim();
3637
+ if (trimmed === "" || trimmed.length > 80 || out.includes(trimmed)) continue;
3638
+ out.push(trimmed);
3639
+ if (out.length >= cap) break;
3640
+ }
3641
+ return out;
3642
+ }
3643
+ function pickOf(raw) {
3644
+ const props = cleanNames(raw["props"], MAX_PROPS);
3645
+ const parts = cleanNames(raw["parts"], MAX_PARTS);
3646
+ const asked = textOf$5(raw["dom"]);
3647
+ return {
3648
+ props: props.length === 0 ? [...DEFAULT_PROPS] : props,
3649
+ parts: parts.length === 0 ? [...DEFAULT_PARTS] : parts,
3650
+ dom: DOM_MODES.includes(asked) ? asked : "outline"
3651
+ };
3652
+ }
3653
+ var NO_PICK = {
3654
+ card: {},
3655
+ parts: {}
3656
+ };
3657
+ var EMPTY_CARD = {
3658
+ ok: false,
3659
+ anchored: false,
3660
+ width: 0,
3661
+ height: 0,
3662
+ head: "",
3663
+ text: "",
3664
+ clipped: false,
3665
+ cards: 0,
3666
+ rect: {
3667
+ x: 0,
3668
+ y: 0,
3669
+ width: 0,
3670
+ height: 0
3671
+ },
3672
+ dpr: 1,
3673
+ viewport: {
3674
+ width: 0,
3675
+ height: 0
3676
+ },
3677
+ pick: NO_PICK,
3678
+ slot: {
3679
+ parentWidth: 0,
3680
+ parentDisplay: "",
3681
+ columns: "",
3682
+ index: 0,
3683
+ cards: 0,
3684
+ viewportHeight: 0,
3685
+ scrollHeight: 0
3686
+ },
3687
+ outline: [],
3688
+ html: ""
3689
+ };
3690
+ function measureScriptOf(unit, pick) {
3691
+ return `(() => {
3692
+ const props = ${JSON.stringify([...pick.props])};
3693
+ const parts = ${JSON.stringify([...pick.parts])};
3694
+ const wantDom = ${JSON.stringify(pick.dom)};
3695
+ const kebab = (one) => (one.startsWith('--') ? one : one.replace(/[A-Z]/g, (hit) => '-' + hit.toLowerCase()));
3696
+ const scrub = (one) => (one ?? '').replace(/\\s+/g, ' ').trim();
3697
+ const read = (el) => {
3698
+ if (el === null || el === undefined) return {};
3699
+ const computed = getComputedStyle(el);
3700
+ const out = {};
3701
+ for (const one of props) out[one] = computed.getPropertyValue(kebab(one)).trim().slice(0, 200);
3702
+ return out;
3703
+ };
3704
+ const anchor = document.querySelector(${JSON.stringify(cardSelectorOf(unit))});
3705
+ const all = [...document.querySelectorAll('.wb-card')];
3706
+ if (anchor === null) return JSON.stringify({ ok: false, cards: all.length, anchored: false });
3707
+ const card = anchor.classList.contains('wb-card') ? anchor : anchor.querySelector('.wb-card');
3708
+ if (card === null) return JSON.stringify({ ok: false, cards: all.length, anchored: true });
3709
+ const box = card.getBoundingClientRect();
3710
+ const head = card.querySelector('.wb-cardTitle');
3711
+ const text = scrub(card.textContent);
3712
+ const shown = scrub(card.innerText);
3713
+ const clipped = [...card.querySelectorAll('*')].some((one) => one.scrollWidth > one.clientWidth + 1);
3714
+ const picked = {};
3715
+ for (const one of parts) {
3716
+ let found = null;
3717
+ try {
3718
+ found = card.querySelector(one);
3719
+ } catch {
3720
+ found = null;
3721
+ }
3722
+ picked[one] = found === null ? {} : read(found);
3723
+ }
3724
+ const parent = card.parentElement === anchor ? anchor.parentElement : card.parentElement;
3725
+ let scrollable = parent;
3726
+ while (scrollable !== null && !/(auto|scroll)/.test(getComputedStyle(scrollable).overflowY)) {
3727
+ scrollable = scrollable.parentElement;
3728
+ }
3729
+ const outline = [];
3730
+ if (wantDom === 'outline') {
3731
+ const walk = (el, depth) => {
3732
+ if (depth > 3 || outline.length >= 40) return;
3733
+ const classed =
3734
+ typeof el.className === 'string'
3735
+ ? el.className.split(/\\s+/).filter((one) => one.startsWith('wb-')).slice(0, 2).join('.')
3736
+ : '';
3737
+ outline.push({
3738
+ depth,
3739
+ tag: el.tagName.toLowerCase(),
3740
+ cls: classed,
3741
+ role: el.getAttribute('role') ?? '',
3742
+ name: el.getAttribute('aria-label') ?? '',
3743
+ text: el.children.length === 0 ? scrub(el.textContent).slice(0, 30) : '',
3744
+ });
3745
+ for (const child of [...el.children]) walk(child, depth + 1);
3746
+ };
3747
+ walk(card, 0);
3748
+ }
3749
+ return JSON.stringify({
3750
+ ok: true,
3751
+ cards: all.length,
3752
+ width: Math.ceil(box.width),
3753
+ height: Math.ceil(box.height),
3754
+ x: Math.round(box.x),
3755
+ y: Math.round(box.y),
3756
+ head: head === null ? '' : scrub(head.textContent),
3757
+ text: (shown === '' ? text : shown).slice(0, 600),
3758
+ clipped,
3759
+ dpr: window.devicePixelRatio || 1,
3760
+ vw: Math.round(window.innerWidth),
3761
+ vh: Math.round(window.innerHeight),
3762
+ slot: {
3763
+ parentWidth: parent === null ? 0 : Math.round(parent.getBoundingClientRect().width),
3764
+ parentDisplay: parent === null ? '' : getComputedStyle(parent).display,
3765
+ columns: parent === null ? '' : getComputedStyle(parent).gridTemplateColumns,
3766
+ index: all.indexOf(card),
3767
+ cards: all.length,
3768
+ viewportHeight: scrollable === null ? 0 : scrollable.clientHeight,
3769
+ scrollHeight: scrollable === null ? 0 : scrollable.scrollHeight,
3770
+ },
3771
+ outline,
3772
+ html: wantDom === 'html' ? card.outerHTML.slice(0, 4000) : '',
3773
+ pick: { card: read(card), parts: picked },
3774
+ });
3775
+ })()`;
3776
+ }
3777
+ function nodesOf(value) {
3778
+ if (!Array.isArray(value)) return [];
3779
+ const out = [];
3780
+ for (const one of value) {
3781
+ if (typeof one !== "object" || one === null) continue;
3782
+ const record = one;
3783
+ const depth = numberIn$1(record["depth"], -1);
3784
+ const tag = textOf$5(record["tag"]);
3785
+ if (depth < 0 || tag === "") continue;
3786
+ out.push({
3787
+ depth,
3788
+ tag,
3789
+ cls: textOf$5(record["cls"]),
3790
+ role: textOf$5(record["role"]),
3791
+ name: textOf$5(record["name"]),
3792
+ text: textOf$5(record["text"])
3793
+ });
3794
+ }
3795
+ return out;
3796
+ }
3797
+ function slotOf(value) {
3798
+ const record = typeof value === "object" && value !== null ? value : {};
3799
+ return {
3800
+ parentWidth: numberIn$1(record["parentWidth"], 0),
3801
+ parentDisplay: textOf$5(record["parentDisplay"]),
3802
+ columns: textOf$5(record["columns"]),
3803
+ index: numberIn$1(record["index"], 0),
3804
+ cards: numberIn$1(record["cards"], 0),
3805
+ viewportHeight: numberIn$1(record["viewportHeight"], 0),
3806
+ scrollHeight: numberIn$1(record["scrollHeight"], 0)
3807
+ };
3808
+ }
3809
+ function stringsOf$2(value) {
3810
+ if (typeof value !== "object" || value === null) return {};
3811
+ const out = {};
3812
+ for (const [key, raw] of Object.entries(value)) {
3813
+ const text = textOf$5(raw);
3814
+ if (text !== "") out[key] = text;
3815
+ }
3816
+ return out;
3817
+ }
3818
+ function pickFactsOf(value) {
3819
+ if (typeof value !== "object" || value === null) return NO_PICK;
3820
+ const record = value;
3821
+ const raw = record["parts"];
3822
+ const parts = {};
3823
+ if (typeof raw === "object" && raw !== null) for (const [key, one] of Object.entries(raw)) parts[key] = stringsOf$2(one);
3824
+ return {
3825
+ card: stringsOf$2(record["card"]),
3826
+ parts
3827
+ };
3828
+ }
3829
+ //#endregion
3830
+ //#region src/modules/panels/model/diy-browser.ts
3831
+ var LOOK_TIMEOUT_MS = 3e4;
3832
+ async function runBrowserStep(shell, command, argv, timeoutMs = LOOK_TIMEOUT_MS) {
3833
+ const result = await runShell(shell, backgroundLine(command, argv), { timeoutMs });
3834
+ return {
3835
+ ok: result.exitCode === 0 && !result.timedOut,
3836
+ stdout: result.stdout.trim(),
3837
+ stderr: result.stderr.trim(),
3838
+ exitCode: result.exitCode,
3839
+ timedOut: result.timedOut
3840
+ };
3841
+ }
3842
+ function jsonOf(raw) {
3843
+ const start = raw.indexOf("{");
3844
+ const end = raw.lastIndexOf("}");
3845
+ if (start === -1 || end <= start) return void 0;
3846
+ try {
3847
+ const parsed = JSON.parse(raw.slice(start, end + 1));
3848
+ return typeof parsed === "object" && parsed !== null ? parsed : void 0;
3849
+ } catch {
3850
+ return;
3851
+ }
3852
+ }
3853
+ function textOf$4(value) {
3854
+ return typeof value === "string" ? value.trim() : "";
3855
+ }
3856
+ function numberIn(value, fallback) {
3857
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
3858
+ }
3859
+ async function bindCurrentTab(shell, command, session) {
3860
+ const step = await runBrowserStep(shell, command, [
3861
+ "browser",
3862
+ session,
3863
+ "bind",
3864
+ "--window",
3865
+ "background"
3866
+ ]);
3867
+ const parsed = jsonOf(step.stdout);
3868
+ if (parsed === void 0) return {
3869
+ ok: false,
3870
+ url: "",
3871
+ title: "",
3872
+ reason: step.timedOut ? "没在 30 秒内返回" : step.stderr || step.stdout
3873
+ };
3874
+ const error = parsed["error"];
3875
+ if (typeof error === "object" && error !== null) {
3876
+ const message = textOf$4(error["message"]);
3877
+ return {
3878
+ ok: false,
3879
+ url: "",
3880
+ title: "",
3881
+ reason: message === "" ? "opencli 报了个错" : message
3882
+ };
3883
+ }
3884
+ return {
3885
+ ok: true,
3886
+ url: textOf$4(parsed["url"]),
3887
+ title: textOf$4(parsed["title"]),
3888
+ reason: ""
3889
+ };
3890
+ }
3891
+ async function workbenchPageOf(shell, command, session) {
3892
+ const parsed = jsonOf((await runBrowserStep(shell, command, [
3893
+ "browser",
3894
+ session,
3895
+ "eval",
3896
+ PAGE_PROBE,
3897
+ "--window",
3898
+ "background"
3899
+ ])).stdout);
3900
+ if (parsed === void 0) return {
3901
+ ok: false,
3902
+ url: "",
3903
+ title: ""
3904
+ };
3905
+ return {
3906
+ ok: textOf$4(parsed["plugin"]) === "@dsh-workbench/core",
3907
+ url: textOf$4(parsed["url"]),
3908
+ title: textOf$4(parsed["title"])
3909
+ };
3910
+ }
3911
+ async function writeShotsDir(shell, dir) {
3912
+ const result = await runShell(shell, [
3913
+ "mkdir",
3914
+ "-p",
3915
+ dir
3916
+ ].map((one) => `'${one}'`).join(" "), { timeoutMs: LOOK_TIMEOUT_MS });
3917
+ return result.exitCode === 0 && !result.timedOut;
3918
+ }
3919
+ async function readyPageOf(shell, command, session) {
3920
+ const already = await workbenchPageOf(shell, command, session);
3921
+ if (already.ok) return {
3922
+ ok: true,
3923
+ bound: false,
3924
+ page: already,
3925
+ reason: ""
3926
+ };
3927
+ const bound = await bindCurrentTab(shell, command, session);
3928
+ if (!bound.ok) return {
3929
+ ok: false,
3930
+ bound: false,
3931
+ page: already,
3932
+ reason: bound.reason
3933
+ };
3934
+ const after = await workbenchPageOf(shell, command, session);
3935
+ return {
3936
+ ok: after.ok,
3937
+ bound: true,
3938
+ page: after,
3939
+ reason: ""
3940
+ };
3941
+ }
3942
+ async function measureCard(shell, command, session, unit, pick) {
3943
+ const parsed = jsonOf((await runBrowserStep(shell, command, [
3944
+ "browser",
3945
+ session,
3946
+ "eval",
3947
+ measureScriptOf(unit, pick),
3948
+ "--window",
3949
+ "background"
3950
+ ])).stdout);
3951
+ if (parsed === void 0 || parsed["ok"] !== true) return {
3952
+ ...EMPTY_CARD,
3953
+ cards: numberIn(parsed?.["cards"], 0),
3954
+ anchored: parsed?.["anchored"] === true
3955
+ };
3956
+ const width = numberIn(parsed["width"], 0);
3957
+ const height = numberIn(parsed["height"], 0);
3958
+ return {
3959
+ ok: true,
3960
+ anchored: true,
3961
+ width,
3962
+ height,
3963
+ head: textOf$4(parsed["head"]),
3964
+ text: textOf$4(parsed["text"]),
3965
+ clipped: parsed["clipped"] === true,
3966
+ cards: numberIn(parsed["cards"], 0),
3967
+ rect: {
3968
+ x: numberIn(parsed["x"], 0),
3969
+ y: numberIn(parsed["y"], 0),
3970
+ width,
3971
+ height
3972
+ },
3973
+ dpr: numberIn(parsed["dpr"], 1) || 1,
3974
+ viewport: {
3975
+ width: numberIn(parsed["vw"], 0),
3976
+ height: numberIn(parsed["vh"], 0)
3977
+ },
3978
+ pick: pickFactsOf(parsed["pick"]),
3979
+ slot: slotOf(parsed["slot"]),
3980
+ outline: nodesOf(parsed["outline"]),
3981
+ html: textOf$4(parsed["html"])
3982
+ };
3983
+ }
3984
+ function insideViewportOf(view) {
3985
+ return view.rect.x >= 0 && view.rect.y >= 0 && view.rect.x + view.rect.width <= view.viewport.width && view.rect.y + view.rect.height <= view.viewport.height;
3986
+ }
3987
+ function deviceRectOf(view) {
3988
+ return {
3989
+ x: Math.max(0, Math.round(view.rect.x * view.dpr)),
3990
+ y: Math.max(0, Math.round(view.rect.y * view.dpr)),
3991
+ width: Math.max(1, Math.round(view.rect.width * view.dpr)),
3992
+ height: Math.max(1, Math.round(view.rect.height * view.dpr))
3993
+ };
3994
+ }
3995
+ function cropFile(file, view) {
3996
+ const crop = cropPngFile(file, file, deviceRectOf(view));
3997
+ return {
3998
+ cropped: crop.ok,
3999
+ reason: crop.reason
4000
+ };
4001
+ }
4002
+ async function shootCard(shell, command, session, unit, file, view) {
4003
+ const step = await runBrowserStep(shell, command, [
4004
+ "browser",
4005
+ session,
4006
+ "screenshot",
4007
+ file,
4008
+ "--window",
4009
+ "background"
4010
+ ]);
4011
+ if (!step.ok) return {
4012
+ ok: false,
4013
+ file: "",
4014
+ width: 0,
4015
+ height: 0,
4016
+ cropped: false,
4017
+ reason: step.timedOut ? "截图没在 30 秒内回来" : step.stderr || step.stdout
4018
+ };
4019
+ if (!insideViewportOf(view)) return {
4020
+ ok: true,
4021
+ file,
4022
+ width: Math.round(view.rect.width),
4023
+ height: Math.round(view.rect.height),
4024
+ cropped: false,
4025
+ reason: `${unit} 这张卡有一部分在视口外面(卡片 ${String(Math.round(view.rect.width))}×${String(Math.round(view.rect.height))},视口 ${String(view.viewport.width)}×${String(view.viewport.height)})—— 滚过一次还是放不下,这一张是整屏图,卡片在 ${String(Math.round(view.rect.x))},${String(Math.round(view.rect.y))}。`
4026
+ };
4027
+ const crop = cropFile(file, view);
4028
+ return {
4029
+ ok: true,
4030
+ file,
4031
+ width: Math.round(view.rect.width),
4032
+ height: Math.round(view.rect.height),
4033
+ cropped: crop.cropped,
4034
+ reason: crop.cropped ? crop.reason : `${crop.reason} —— 给的是整屏图:卡片在 ${String(Math.round(view.rect.x))},${String(Math.round(view.rect.y))}`
4035
+ };
4036
+ }
4037
+ //#endregion
4038
+ //#region src/modules/panels/model/diy-page.ts
4039
+ async function pageAt(ctx, exec, raw, options = {}) {
4040
+ const { packagesDir } = workshopOf(ctx, exec);
4041
+ const args = asRecord$3(raw);
4042
+ const asked = asText$1(args["unit"]);
4043
+ const modules = modulesOf(ctx, packagesDir).modules;
4044
+ const wanted = asked === "" && modules.length === 1 ? modules[0]?.unit ?? "" : asked;
4045
+ if (wanted === "" && options.requireUnit !== false) throw new Error(`看一眼要一个 unit(工坊里:${modules.map((one) => one.unit).join(", ") || "(空)"})`);
4046
+ const shell = ctx.get("shell");
4047
+ if (shell === void 0) throw new Error("这台机器上没有 shell 服务,动不了浏览器");
4048
+ const command = opencliCommandOf(ctx).command;
4049
+ const session = asText$1(args["session"]) || "workbench";
4050
+ const handshake = await readyPageOf(shell, command, session);
4051
+ if (!handshake.ok && !handshake.bound) throw new Error(`连不上浏览器:${handshake.reason}。先让用户把工作台那一页切到前台(只有第一次要这样);还不行就跑 opencli doctor 看扩展连没连上。`);
4052
+ if (!handshake.ok && options.requireWorkbench !== false) {
4053
+ const { page } = handshake;
4054
+ throw new Error(`你现在这一页不是工作台(${page.title || "(没有标题)"} ${page.url})—— 把工作台那一页切到前台,再喊我一次。我只看用户正在看的那一页。`);
4055
+ }
4056
+ return {
4057
+ packagesDir,
4058
+ unit: wanted,
4059
+ args,
4060
+ shell,
4061
+ command,
4062
+ session,
4063
+ bound: handshake.bound
4064
+ };
4065
+ }
4066
+ //#endregion
4067
+ //#region src/modules/panels/model/diy-browser-act.ts
4068
+ var ACT_TIMEOUT_MS = 15e3;
4069
+ function actTargetOf(raw) {
4070
+ const nth = raw["nth"];
4071
+ return {
4072
+ selector: textOf$4(raw["selector"]),
4073
+ role: textOf$4(raw["role"]),
4074
+ name: textOf$4(raw["name"]),
4075
+ testid: textOf$4(raw["testid"]),
4076
+ nth: typeof nth === "number" && Number.isFinite(nth) ? Math.max(0, Math.trunc(nth)) : null,
4077
+ page: textOf$4(raw["scope"]) === "page"
4078
+ };
4079
+ }
4080
+ function scopedSelectorOf(unit, target) {
4081
+ if (target.selector === "") return "";
4082
+ return target.page ? target.selector : `${cardSelectorOf(unit)} ${target.selector}`;
4083
+ }
4084
+ var SCOPE_ATTR = "data-diy-scope";
4085
+ function semanticOptionsOf(target) {
4086
+ const out = [];
4087
+ if (target.role !== "") out.push("--role", target.role);
4088
+ if (target.name !== "") out.push("--name", target.name);
4089
+ if (target.testid !== "") out.push("--testid", target.testid);
4090
+ return out;
4091
+ }
4092
+ function targetOptionsOf(target) {
4093
+ const out = [...semanticOptionsOf(target)];
4094
+ if (target.nth !== null) out.push("--nth", String(target.nth));
4095
+ return out;
4096
+ }
4097
+ function refsOf(payload) {
4098
+ const entries = payload?.["entries"];
4099
+ if (!Array.isArray(entries)) return [];
4100
+ const out = [];
4101
+ for (const one of entries) {
4102
+ if (typeof one !== "object" || one === null) continue;
4103
+ const ref = one["ref"];
4104
+ if (typeof ref === "number") out.push(ref);
4105
+ }
4106
+ return out;
4107
+ }
4108
+ function totalMatchesOf(payload) {
4109
+ const total = payload?.["matches_n"];
4110
+ return typeof total === "number" ? total : 0;
4111
+ }
4112
+ function scopeMarkScriptOf(unit, refs, nth, token) {
4113
+ return `(() => {
4114
+ const card = document.querySelector(${JSON.stringify(cardSelectorOf(unit))});
4115
+ const refs = ${JSON.stringify(refs)};
4116
+ if (card === null) return { ok: false, code: 'no-card', inside: 0, total: refs.length };
4117
+ const found = [];
4118
+ for (const ref of refs) {
4119
+ const el = document.querySelector('[data-opencli-ref="' + ref + '"]');
4120
+ if (el !== null && card.contains(el)) found.push(el);
4121
+ }
4122
+ for (const stale of card.querySelectorAll('[${SCOPE_ATTR}]')) stale.removeAttribute('${SCOPE_ATTR}');
4123
+ const pick = found[${String(nth)}];
4124
+ if (pick === undefined) return { ok: false, code: 'not-in-card', inside: found.length, total: refs.length };
4125
+ pick.setAttribute('${SCOPE_ATTR}', ${JSON.stringify(token)});
4126
+ return { ok: true, code: '', inside: found.length, total: refs.length };
4127
+ })()`;
4128
+ }
4129
+ function scopeClearScriptOf(token) {
4130
+ return `(() => {
4131
+ const marked = document.querySelectorAll('[${SCOPE_ATTR}="${token}"]');
4132
+ const removed = marked.length;
4133
+ for (const el of marked) el.removeAttribute('${SCOPE_ATTR}');
4134
+ return { ok: true, removed };
4135
+ })()`;
4136
+ }
4137
+ function scopedMarkerSelectorOf(unit, token) {
4138
+ return `${cardSelectorOf(unit)} [${SCOPE_ATTR}="${token}"]`;
4139
+ }
4140
+ async function actBrowser(shell, command, session, argv, timeoutMs = ACT_TIMEOUT_MS) {
4141
+ const step = await runBrowserStep(shell, command, [
4142
+ "browser",
4143
+ session,
4144
+ ...argv,
4145
+ "--window",
4146
+ "background"
4147
+ ], timeoutMs);
4148
+ return {
4149
+ ...step,
4150
+ payload: jsonOf(step.stdout)
4151
+ };
4152
+ }
4153
+ function actErrorOf(step) {
4154
+ if (step.timedOut) return `opencli 没在 ${String(Math.round(ACT_TIMEOUT_MS / 1e3))} 秒内回来`;
4155
+ const error = step.payload?.["error"];
4156
+ if (typeof error === "object" && error !== null) {
4157
+ const message = textOf$4(error["message"]);
4158
+ if (message !== "") return message;
4159
+ }
4160
+ if (step.ok) return "";
4161
+ return step.stderr || step.stdout || "opencli 报了个错";
4162
+ }
4163
+ function matchesOf(step) {
4164
+ const raw = step.payload?.["matches_n"];
4165
+ return typeof raw === "number" && Number.isFinite(raw) ? raw : -1;
4166
+ }
4167
+ async function scrollIntoView(shell, command, session, unit, selector) {
4168
+ const asked = selector === "" ? ".wb-card" : selector;
4169
+ return actBrowser(shell, command, session, ["eval", `(() => {
4170
+ const anchor = document.querySelector(${JSON.stringify(cardSelectorOf(unit))});
4171
+ const sel = ${JSON.stringify(asked)};
4172
+ if (anchor === null) return JSON.stringify({ ok: false, found: false });
4173
+ const el = anchor.matches(sel) ? anchor : anchor.querySelector(sel);
4174
+ if (el === null) return JSON.stringify({ ok: false, found: false });
4175
+ el.scrollIntoView({ block: 'center', inline: 'nearest' });
4176
+ const box = el.getBoundingClientRect();
4177
+ return JSON.stringify({ ok: true, found: true, x: Math.round(box.x), y: Math.round(box.y), width: Math.ceil(box.width), height: Math.ceil(box.height) });
4178
+ })()`]);
4179
+ }
4180
+ async function reloadPage(shell, command, session) {
4181
+ return actBrowser(shell, command, session, ["eval", `(() => { setTimeout(() => location.reload(), 30); return JSON.stringify({ ok: true }); })()`]);
4182
+ }
4183
+ async function shootAtWidth(shell, command, session, file, width, height) {
4184
+ const step = await actBrowser(shell, command, session, [
4185
+ "screenshot",
4186
+ file,
4187
+ "--width",
4188
+ String(width),
4189
+ ...height > 0 ? ["--height", String(height)] : []
4190
+ ], LOOK_TIMEOUT_MS);
4191
+ return {
4192
+ ok: step.ok,
4193
+ file: step.ok ? file : "",
4194
+ width,
4195
+ height,
4196
+ reason: step.ok ? "" : step.timedOut ? "截图没在 30 秒内回来" : step.stderr || step.stdout
4197
+ };
4198
+ }
4199
+ var NET_CAP_LINES = 2e3;
4200
+ function followerPatternOf(session) {
4201
+ return `[b]rowser ${session} network --follow`;
4202
+ }
4203
+ function killFollowerLine(session, file) {
4204
+ const pidFile = quoteShellArg(`${file}.pid`);
4205
+ const reaperFile = quoteShellArg(`${file}.reaper`);
4206
+ return [
4207
+ `kill "$(cat ${pidFile} 2>/dev/null)" 2>/dev/null`,
4208
+ `pkill -f ${quoteShellArg(followerPatternOf(session))} 2>/dev/null`,
4209
+ `pkill -P "$(cat ${reaperFile} 2>/dev/null)" 2>/dev/null`,
4210
+ `kill "$(cat ${reaperFile} 2>/dev/null)" 2>/dev/null`
4211
+ ].join("; ");
4212
+ }
4213
+ async function armNetwork(shell, command, session, file) {
4214
+ const capture = `${backgroundLine(command, [
4215
+ "browser",
4216
+ session,
4217
+ "network",
4218
+ "--follow",
4219
+ "--window",
4220
+ "background"
4221
+ ])} 2>&1 | head -n ${String(NET_CAP_LINES)} > ${quoteShellArg(file)}`;
4222
+ const reaper = `sleep ${String(900)}; ${killFollowerLine(session, file)}`;
4223
+ const step = await runShell(shell, [
4224
+ "rm",
4225
+ "-f",
4226
+ quoteShellArg(file),
4227
+ quoteShellArg(`${file}.pid`),
4228
+ quoteShellArg(`${file}.reaper`),
4229
+ ";",
4230
+ "nohup",
4231
+ "sh",
4232
+ "-c",
4233
+ quoteShellArg(capture),
4234
+ ">",
4235
+ "/dev/null",
4236
+ "2>&1",
4237
+ "&",
4238
+ "echo",
4239
+ "$!",
4240
+ ">",
4241
+ quoteShellArg(`${file}.pid`),
4242
+ ";",
4243
+ "nohup",
4244
+ "sh",
4245
+ "-c",
4246
+ quoteShellArg(reaper),
4247
+ ">",
4248
+ "/dev/null",
4249
+ "2>&1",
4250
+ "&",
4251
+ "echo",
4252
+ "$!",
4253
+ ">",
4254
+ quoteShellArg(`${file}.reaper`),
4255
+ ";",
4256
+ "sleep",
4257
+ "0.6",
4258
+ ";",
4259
+ "cat",
4260
+ quoteShellArg(`${file}.pid`)
4261
+ ].join(" "), { timeoutMs: ACT_TIMEOUT_MS });
4262
+ const pid = step.stdout.trim().split("\n").slice(-1)[0] ?? "";
4263
+ return {
4264
+ ok: step.exitCode === 0 && pid !== "",
4265
+ pid,
4266
+ reason: step.exitCode === 0 ? "" : step.stderr.trim() || step.stdout.trim()
4267
+ };
4268
+ }
4269
+ async function stopNetwork(shell, session, file) {
4270
+ return (await runShell(shell, [
4271
+ killFollowerLine(session, file),
4272
+ ";",
4273
+ "rm",
4274
+ "-f",
4275
+ quoteShellArg(`${file}.pid`),
4276
+ quoteShellArg(`${file}.reaper`)
4277
+ ].join(" "), { timeoutMs: ACT_TIMEOUT_MS })).exitCode === 0;
4278
+ }
4279
+ async function readNetwork(shell, file, lines) {
4280
+ const step = await runShell(shell, `tail -n ${String(lines)} ${quoteShellArg(file)}`, { timeoutMs: ACT_TIMEOUT_MS });
4281
+ const out = [];
4282
+ for (const raw of step.stdout.split("\n")) {
4283
+ const parsed = jsonOf(raw);
4284
+ if (parsed !== void 0) out.push(parsed);
4285
+ }
4286
+ return out;
4287
+ }
4288
+ //#endregion
4289
+ //#region src/modules/panels/model/diy-look.ts
4290
+ function registerLookTools(ctx, tools) {
4291
+ const lookTool = {
4292
+ name: DIY_LOOK_TOOL,
4293
+ description: "看一眼这张卡:只截它这一张 + 按你点的那几项读它的样式与字。要用户把工作台那一页切到前台。",
4294
+ parameters: {
4295
+ type: "object",
4296
+ properties: {
4297
+ unit: {
4298
+ type: "string",
4299
+ description: "工坊里的 unit(不填 = 工坊里只有一个就用它)。"
4300
+ },
4301
+ props: {
4302
+ type: "array",
4303
+ items: { type: "string" },
4304
+ description: "要读的 CSS 属性(含 --令牌;不填给默认档)。最多 40 个。"
4305
+ },
4306
+ parts: {
4307
+ type: "array",
4308
+ items: { type: "string" },
4309
+ description: "卡片里再看哪几块(选择器,限卡片内;不填 = 卡头与卡片体)。最多 6 个。"
4310
+ },
4311
+ dom: {
4312
+ type: "string",
4313
+ description: "结构快照:outline(默认,tag+类名+role+文本的浅层大纲)| html(卡片子树原始 HTML)| none。"
4314
+ },
4315
+ session: {
4316
+ type: "string",
4317
+ description: "会话名(默认 workbench,一般不用填)。"
4318
+ }
4319
+ },
4320
+ additionalProperties: false
4321
+ },
4322
+ output: OUTPUT$2,
4323
+ async execute(raw, exec) {
4324
+ const { packagesDir, unit, shell, command, session } = await pageAt(ctx, exec, raw);
4325
+ const pick = pickOf(asRecord$3(raw));
4326
+ let view = await measureCard(shell, command, session, unit, pick);
4327
+ if (!view.ok) return plainJson({
4328
+ workshop: packagesDir,
4329
+ unit,
4330
+ ok: false,
4331
+ cards: view.cards,
4332
+ text: renderMissingCard(unit, view.cards, view.anchored)
4333
+ });
4334
+ if (!insideViewportOf(view)) {
4335
+ await scrollIntoView(shell, command, session, unit, "");
4336
+ view = await measureCard(shell, command, session, unit, pick);
4337
+ }
4338
+ const file = join(dirname(packagesDir), "shots", `${unit}-${String(Date.now())}.png`);
4339
+ await writeShotsDir(shell, dirname(file));
4340
+ const shot = await shootCard(shell, command, session, unit, file, view);
4341
+ return plainJson({
4342
+ workshop: packagesDir,
4343
+ unit,
4344
+ ok: shot.ok,
4345
+ file: shot.file,
4346
+ rect: view.rect,
4347
+ cropped: shot.cropped,
4348
+ clipped: view.clipped,
4349
+ head: view.head,
4350
+ pick: view.pick,
4351
+ slot: view.slot,
4352
+ outline: view.outline,
4353
+ html: view.html,
4354
+ text: renderLook(unit, view, shot, pick)
4355
+ });
4356
+ }
4357
+ };
4358
+ return [tools.register(lookTool)];
4359
+ }
4360
+ function rgbOf(value) {
4361
+ const hit = /rgba?\((\d+)[,\s]+(\d+)[,\s]+(\d+)/.exec(value);
4362
+ if (hit === null) return void 0;
4363
+ return [
4364
+ Number(hit[1]),
4365
+ Number(hit[2]),
4366
+ Number(hit[3])
4367
+ ];
4368
+ }
4369
+ function luminanceOf(rgb) {
4370
+ const channel = (one) => {
4371
+ const scaled = one / 255;
4372
+ return scaled <= .03928 ? scaled / 12.92 : ((scaled + .055) / 1.055) ** 2.4;
4373
+ };
4374
+ return .2126 * channel(rgb[0]) + .7152 * channel(rgb[1]) + .0722 * channel(rgb[2]);
4375
+ }
4376
+ function contrastOf(foreground, background) {
4377
+ const front = rgbOf(foreground);
4378
+ const back = rgbOf(background);
4379
+ if (front === void 0 || back === void 0) return void 0;
4380
+ const one = luminanceOf(front);
4381
+ const other = luminanceOf(back);
4382
+ const ratio = (Math.max(one, other) + .05) / (Math.min(one, other) + .05);
4383
+ return Math.round(ratio * 100) / 100;
4384
+ }
4385
+ function styleLineOf(view, pick) {
4386
+ return pick.props.filter((one) => (view.pick.card[one] ?? "") !== "").map((one) => `${one}=${view.pick.card[one] ?? ""}`).join(" · ");
4387
+ }
4388
+ function partsLineOf(view, pick) {
4389
+ return pick.parts.map((one) => {
4390
+ const found = view.pick.parts[one] ?? {};
4391
+ const inside = Object.entries(found).map(([key, value]) => `${key}=${value}`).join(" ");
4392
+ return `${one}${inside === "" ? "(页面里没有这个)" : ` ${inside}`}`;
4393
+ }).join("\n");
4394
+ }
4395
+ function slotText(slot, width) {
4396
+ const columns = slot.columns === "" || slot.columns === "none" ? "一列" : `多列(${slot.columns})`;
4397
+ const fit = slot.parentWidth === 0 ? "" : `、这张卡占 ${String(width)}/${String(slot.parentWidth)}px`;
4398
+ return `它在第 ${String(slot.index + 1)}/${String(slot.cards)} 张;卡片格子 ${String(slot.parentWidth)}px 宽、${columns}${fit};滚动容器可视 ${String(slot.viewportHeight)}px / 内容 ${String(slot.scrollHeight)}px`;
4399
+ }
4400
+ function slotLineOf(view) {
4401
+ return slotText(view.slot, view.width);
4402
+ }
4403
+ function outlineText(view) {
4404
+ return view.outline.map((one) => {
4405
+ const bits = [`${" ".repeat(one.depth)}${one.tag}${one.cls === "" ? "" : `.${one.cls}`}`];
4406
+ if (one.role !== "") bits.push(`role=${one.role}`);
4407
+ if (one.name !== "") bits.push(`name="${one.name}"`);
4408
+ if (one.text !== "") bits.push(`"${one.text}"`);
4409
+ return bits.join(" ");
4410
+ }).join("\n");
4411
+ }
4412
+ function contrastLineOf(view) {
4413
+ const ratio = contrastOf((view.pick.parts[".wb-cardBody"] ?? view.pick.card)["color"] ?? "", view.pick.card["backgroundColor"] ?? "");
4414
+ if (ratio === void 0) return "";
4415
+ return ratio >= 4.5 ? `正文对比度 ${String(ratio)}(够)` : `正文对比度 ${String(ratio)} —— 低于 4.5,正文会看不清(要改)`;
4416
+ }
4417
+ function renderLook(unit, view, shot, pick) {
4418
+ const where = `面板里 ${String(view.rect.x)},${String(view.rect.y)}(${String(view.width)}×${String(view.height)})`;
4419
+ return [
4420
+ shot.ok ? `看到了 ${unit} 这张卡:${where}` : `没拍成:${shot.reason}`,
4421
+ ...shot.ok ? [shot.file] : [],
4422
+ ...shot.ok && shot.reason !== "" ? [shot.reason] : [],
4423
+ ...view.head === "" ? [] : [`卡头:${view.head}`],
4424
+ ...view.text === "" ? [] : [`卡片上写着:${view.text}`],
4425
+ `格子里:${slotLineOf(view)}`,
4426
+ ...styleLineOf(view, pick) === "" ? [] : [`你点的那几项:${styleLineOf(view, pick)}`],
4427
+ ...[partsLineOf(view, pick)].filter((one) => one !== ""),
4428
+ ...contrastLineOf(view) === "" ? [] : [contrastLineOf(view)],
4429
+ ...pick.dom === "outline" && view.outline.length > 0 ? ["结构:", outlineText(view)] : [],
4430
+ ...pick.dom === "html" ? [`原始 HTML:${String(view.html.length)} 字符(在返回的 html 字段里)`] : [],
4431
+ ...view.clipped ? ["有内容被裁掉了(某一格比它自己的宽度还宽)—— 这是要改的信号"] : []
4432
+ ].join("\n");
4433
+ }
4434
+ function renderMissingCard(unit, cards, anchored) {
4435
+ if (anchored) return `面板里给 ${unit} 留了位置(锚点在),但里面没有卡片 —— 它的 View 没画出来:要么渲染时抛了(看 workbench_diy_panel 的运行态上报与 workbench_diy_status),要么它返回了空 / 没套卡壳。你现在这一页面板里一共 ${String(cards)} 张卡。`;
4436
+ return `你现在这一页的面板里没有 ${unit} 这张卡(面板里现在有 ${String(cards)} 张卡)。三件事,按顺序试:① workbench_diy_show(unit=${unit})把它上板;② 右栏可能没停在「工作台面板」那一列 —— 让用户切回来;③ 页面上跑的外壳可能还是旧的一份(锚点由外壳盖)—— 让用户刷新一次页面。`;
4437
+ }
4438
+ //#endregion
4439
+ //#region src/modules/panels/model/diy-logs.ts
4440
+ var LOGS_KEY = "__dshDiyLogs";
4441
+ var LOGS_CAP = 200;
4442
+ var LOGS_TEXT_MAX = 400;
4443
+ var LEVELS = [
4444
+ "log",
4445
+ "info",
4446
+ "warn",
4447
+ "error",
4448
+ "debug"
4449
+ ];
4450
+ var LOGS_ARM_SCRIPT = `(() => {
4451
+ const KEY = ${JSON.stringify(LOGS_KEY)};
4452
+ const CAP = ${String(LOGS_CAP)};
4453
+ const TEXT_MAX = ${String(LOGS_TEXT_MAX)};
4454
+ const LEVELS = ${JSON.stringify(LEVELS)};
4455
+ let buf = window[KEY];
4456
+ if (!Array.isArray(buf)) {
4457
+ buf = [];
4458
+ window[KEY] = buf;
4459
+ }
4460
+ const textOf = (value) => {
4461
+ try {
4462
+ if (typeof value === 'string') return value;
4463
+ const json = JSON.stringify(value);
4464
+ return typeof json === 'string' ? json : String(value);
4465
+ } catch (error) {
4466
+ return '[unprintable]';
4467
+ }
4468
+ };
4469
+ let hooked = 0;
4470
+ for (const level of LEVELS) {
4471
+ const current = console[level];
4472
+ if (typeof current === 'function' && current.__dshDiy === true) continue;
4473
+ const real = typeof current === 'function' ? current.bind(console) : () => {};
4474
+ const patched = (...args) => {
4475
+ try {
4476
+ buf.push({ level, text: args.map(textOf).join(' ').slice(0, TEXT_MAX), at: Date.now() });
4477
+ if (buf.length > CAP) buf.splice(0, buf.length - CAP);
4478
+ } catch (error) {}
4479
+ return real(...args);
4480
+ };
4481
+ patched.__dshDiy = true;
4482
+ patched.__real = typeof current === 'function' ? current : null;
4483
+ console[level] = patched;
4484
+ hooked += 1;
4485
+ }
4486
+ return { ok: true, cap: CAP, levels: LEVELS.length, hooked, size: buf.length };
4487
+ })()`;
4488
+ var LOGS_READ_SCRIPT = `(() => {
4489
+ const buf = window[${JSON.stringify(LOGS_KEY)}];
4490
+ if (!Array.isArray(buf)) return { ok: false, code: 'not-armed', seen: 0, lines: [] };
4491
+ return { ok: true, seen: buf.length, lines: buf.slice(-${String(LOGS_CAP)}) };
4492
+ })()`;
4493
+ var LOGS_STOP_SCRIPT = `(() => {
4494
+ const KEY = ${JSON.stringify(LOGS_KEY)};
4495
+ const LEVELS = ${JSON.stringify(LEVELS)};
4496
+ const buf = window[KEY];
4497
+ const dropped = Array.isArray(buf) ? buf.length : 0;
4498
+ let unhooked = 0;
4499
+ for (const level of LEVELS) {
4500
+ const current = console[level];
4501
+ if (typeof current !== 'function' || current.__dshDiy !== true) continue;
4502
+ if (typeof current.__real === 'function') console[level] = current.__real;
4503
+ else delete console[level];
4504
+ unhooked += 1;
4505
+ }
4506
+ try {
4507
+ delete window[KEY];
4508
+ } catch (error) {
4509
+ window[KEY] = undefined;
4510
+ }
4511
+ return { ok: true, unhooked, dropped };
4512
+ })()`;
4513
+ function logsReadOf(payload) {
4514
+ const raw = payload ?? {};
4515
+ const lines = Array.isArray(raw["lines"]) ? raw["lines"] : [];
4516
+ const kept = [];
4517
+ for (const one of lines) {
4518
+ if (typeof one !== "object" || one === null) continue;
4519
+ const record = one;
4520
+ const text = typeof record["text"] === "string" ? record["text"] : "";
4521
+ if (text === "") continue;
4522
+ kept.push({
4523
+ level: typeof record["level"] === "string" && record["level"] !== "" ? record["level"] : "log",
4524
+ text,
4525
+ at: typeof record["at"] === "number" ? record["at"] : 0
4526
+ });
4527
+ }
4528
+ const seen = typeof raw["seen"] === "number" ? raw["seen"] : kept.length;
4529
+ return {
4530
+ ok: raw["ok"] === true,
4531
+ code: typeof raw["code"] === "string" ? raw["code"] : "",
4532
+ seen: Math.max(seen, kept.length),
4533
+ lines: kept
4534
+ };
4535
+ }
4536
+ function renderLogs(read) {
4537
+ if (!read.ok) return "这一页上还没有钩子(看不到控制台)—— 先 workbench_diy_logs_watch 装上,做完你要看的那件事,再 workbench_diy_logs_read。";
4538
+ if (read.lines.length === 0) return "钩子装上了,但这一页到现在一条 console.* 都没打过。";
4539
+ const shown = read.lines.slice(-50);
4540
+ const more = read.seen > shown.length ? `(攒了 ${String(read.seen)} 条,这里只列最近 ${String(shown.length)} 条)` : "";
4541
+ return [`这一页最近 ${String(shown.length)} 条控制台${more}:`, ...shown.map((one) => `[${one.level}] ${one.text}`)].join("\n");
4542
+ }
4543
+ function registerLogTools(ctx, tools) {
4544
+ const schema = {
4545
+ type: "object",
4546
+ properties: { session: {
4547
+ type: "string",
4548
+ description: "会话名(默认 workbench,一般不用填)。"
4549
+ } },
4550
+ additionalProperties: false
4551
+ };
4552
+ async function run(exec, raw, script) {
4553
+ const page = await pageAt(ctx, exec, raw, {
4554
+ requireUnit: false,
4555
+ requireWorkbench: false
4556
+ });
4557
+ return {
4558
+ page,
4559
+ step: await actBrowser(page.shell, page.command, page.session, ["eval", script])
4560
+ };
4561
+ }
4562
+ const watchTool = {
4563
+ name: DIY_LOGS_WATCH_TOOL,
4564
+ description: "把这一页的控制台听上(每页最多 200 条、单条截 400 字,随页面刷新消失)。",
4565
+ parameters: schema,
4566
+ output: OUTPUT$2,
4567
+ async execute(raw, exec) {
4568
+ const { page, step } = await run(exec, raw, LOGS_ARM_SCRIPT);
4569
+ const error = actErrorOf(step);
4570
+ const hooked = typeof step.payload?.["hooked"] === "number" ? step.payload["hooked"] : 0;
4571
+ const size = typeof step.payload?.["size"] === "number" ? step.payload["size"] : 0;
4572
+ const text = error !== "" ? `没听上:${error}` : hooked === 0 ? `已经在听了(钩子还在,攒着 ${String(size)} 条)。做完那件事用 ${DIY_LOGS_READ_TOOL} 读。` : `听上了(装了 ${String(hooked)} 个钩子,最多 ${String(LOGS_CAP)} 条):去做你要看的那件事,回头用 ${DIY_LOGS_READ_TOOL} 读;读完用 ${DIY_LOGS_STOP_TOOL} 卸掉。`;
4573
+ return plainJson({
4574
+ workshop: page.packagesDir,
4575
+ ok: step.ok && error === "",
4576
+ hooked,
4577
+ cap: LOGS_CAP,
4578
+ text
4579
+ });
4580
+ }
4581
+ };
4582
+ const readTool = {
4583
+ name: DIY_LOGS_READ_TOOL,
4584
+ description: "读刚才听下来的控制台(最近 50 条;没装钩子会说清,不假装是\"没有报错\")。",
4585
+ parameters: schema,
4586
+ output: OUTPUT$2,
4587
+ async execute(raw, exec) {
4588
+ const { page, step } = await run(exec, raw, LOGS_READ_SCRIPT);
4589
+ const error = actErrorOf(step);
4590
+ const read = logsReadOf(step.payload);
4591
+ const text = error !== "" ? `读不到:${error}` : renderLogs(read);
4592
+ return plainJson({
4593
+ workshop: page.packagesDir,
4594
+ ok: step.ok && error === "",
4595
+ seen: read.seen,
4596
+ lines: read.lines.slice(-50),
4597
+ text
4598
+ });
4599
+ }
4600
+ };
4601
+ const stopTool = {
4602
+ name: DIY_LOGS_STOP_TOOL,
4603
+ description: "控制台收听收工:卸掉钩子、扔掉攒下的记录。",
4604
+ parameters: schema,
4605
+ output: OUTPUT$2,
4606
+ async execute(raw, exec) {
4607
+ const { page, step } = await run(exec, raw, LOGS_STOP_SCRIPT);
4608
+ const error = actErrorOf(step);
4609
+ const unhooked = typeof step.payload?.["unhooked"] === "number" ? step.payload["unhooked"] : 0;
4610
+ const dropped = typeof step.payload?.["dropped"] === "number" ? step.payload["dropped"] : 0;
4611
+ const text = error !== "" ? `没卸干净:${error}` : `收工了:卸掉 ${String(unhooked)} 个钩子、扔掉 ${String(dropped)} 条记录。`;
4612
+ return plainJson({
4613
+ workshop: page.packagesDir,
4614
+ ok: step.ok && error === "",
4615
+ unhooked,
4616
+ dropped,
4617
+ text
4618
+ });
4619
+ }
4620
+ };
4621
+ return [
4622
+ tools.register(watchTool),
4623
+ tools.register(readTool),
4624
+ tools.register(stopTool)
4625
+ ];
4626
+ }
4627
+ //#endregion
4628
+ //#region src/modules/panels/model/diy-schema.ts
4629
+ var UNIT_PROP = {
4630
+ type: "string",
4631
+ description: "工坊里的 unit(不填 = 工坊里只有一个就用它)。"
4632
+ };
4633
+ var SESSION_PROP = {
4634
+ type: "string",
4635
+ description: "会话名(默认 workbench,一般不用填)。"
4636
+ };
4637
+ function pageSchema(extra = {}, required = []) {
4638
+ return {
4639
+ type: "object",
4640
+ properties: {
4641
+ unit: UNIT_PROP,
4642
+ ...extra,
4643
+ session: SESSION_PROP
4644
+ },
4645
+ ...required.length === 0 ? {} : { required },
4646
+ additionalProperties: false
4647
+ };
4648
+ }
4649
+ //#endregion
4650
+ //#region src/modules/panels/model/diy-net.ts
4651
+ var SIBLING_TRIES = 6;
4652
+ var NET_BODY_MAX = 2e4;
4653
+ function nearestKeyOf(payload, key) {
4654
+ const entries = payload?.["entries"];
4655
+ if (!Array.isArray(entries)) return "";
4656
+ let recent = "";
4657
+ for (const one of entries) {
4658
+ if (typeof one !== "object" || one === null) continue;
4659
+ const candidate = one["key"];
4660
+ if (typeof candidate !== "string" || candidate === "") continue;
4661
+ if (candidate === key) return key;
4662
+ if (candidate.startsWith(`${key}#`)) recent = candidate;
4663
+ }
4664
+ return recent;
4665
+ }
4666
+ function registerNetTools(ctx, tools) {
4667
+ async function netDetail(page, key) {
4668
+ return await actBrowser(page.shell, page.command, page.session, [
4669
+ "network",
4670
+ "--detail",
4671
+ key,
4672
+ "--max-body",
4673
+ String(NET_BODY_MAX)
4674
+ ]);
4675
+ }
4676
+ async function netFileOf(exec, raw) {
4677
+ const page = await pageAt(ctx, exec, raw, {
4678
+ requireWorkbench: false,
4679
+ requireUnit: false
4680
+ });
4681
+ return {
4682
+ page,
4683
+ file: join(dirname(page.packagesDir), "shots", `net-${page.session}.jsonl`)
4684
+ };
4685
+ }
4686
+ const netWatchTool = {
4687
+ name: DIY_NET_WATCH_TOOL,
4688
+ description: `把网络捕获臂上(opencli 的 network 不臂上就收不到):随后你在这页上做的事都会被记下(最多 ${String(NET_CAP_LINES)} 行 / ${String(Math.round(15))} 分钟后自己收工)。`,
4689
+ parameters: pageSchema(),
4690
+ output: OUTPUT$2,
4691
+ async execute(raw, exec) {
4692
+ const { page, file } = await netFileOf(exec, raw);
4693
+ await writeShotsDir(page.shell, dirname(file));
4694
+ await stopNetwork(page.shell, page.session, file);
4695
+ const armed = await armNetwork(page.shell, page.command, page.session, file);
4696
+ return plainJson({
4697
+ workshop: page.packagesDir,
4698
+ unit: page.unit,
4699
+ ok: armed.ok,
4700
+ pid: armed.pid,
4701
+ capLines: NET_CAP_LINES,
4702
+ capSeconds: 900,
4703
+ text: armed.ok ? `捕获臂上了(pid ${armed.pid};最多 ${String(NET_CAP_LINES)} 行、${String(900)} 秒后自己收工)。现在去做你要做的事,回头用 ${DIY_NET_READ_TOOL} 读;读完 ${DIY_NET_STOP_TOOL} 可以提前收工。` : `臂不上:${armed.reason}`
4704
+ });
4705
+ }
4706
+ };
4707
+ const netReadTool = {
4708
+ name: DIY_NET_READ_TOOL,
4709
+ description: "读刚才臂上之后记下的网络(给 key 就取那一条的完整 body)。",
4710
+ parameters: pageSchema({
4711
+ lines: {
4712
+ type: "number",
4713
+ description: "读最近几条(默认 60)。"
4714
+ },
4715
+ failed: {
4716
+ type: "boolean",
4717
+ description: "只看失败请求(0 或 >= 400)。"
4718
+ },
4719
+ grep: {
4720
+ type: "string",
4721
+ description: "再按 URL 里的一段词收。"
4722
+ },
4723
+ key: {
4724
+ type: "string",
4725
+ description: "给 key = 取这一条请求的完整 body(read 的返回里带 key)。"
4726
+ }
4727
+ }),
4728
+ output: OUTPUT$2,
4729
+ async execute(raw, exec) {
4730
+ const { page, file } = await netFileOf(exec, raw);
4731
+ const key = asText$1(page.args["key"]);
4732
+ if (key !== "") {
4733
+ const tried = [key];
4734
+ let used = key;
4735
+ let detail = await netDetail(page, key);
4736
+ for (let n = 2; n <= SIBLING_TRIES && !detail.ok; n += 1) {
4737
+ const sibling = `${key}#${String(n)}`;
4738
+ const attempt = await netDetail(page, sibling);
4739
+ tried.push(sibling);
4740
+ if (attempt.ok) {
4741
+ detail = attempt;
4742
+ used = sibling;
4743
+ }
4744
+ }
4745
+ let refreshed = false;
4746
+ if (!detail.ok) {
4747
+ const capture = await actBrowser(page.shell, page.command, page.session, ["network"]);
4748
+ const grabbed = Array.isArray(capture.payload?.["entries"]) ? capture.payload["entries"].length : 0;
4749
+ refreshed = capture.ok && grabbed > 0;
4750
+ if (refreshed) {
4751
+ const retry = await netDetail(page, key);
4752
+ tried.push(key);
4753
+ if (retry.ok) {
4754
+ detail = retry;
4755
+ used = key;
4756
+ } else {
4757
+ const alt = nearestKeyOf(capture.payload, key);
4758
+ if (alt !== "" && alt !== key) {
4759
+ const attempt = await netDetail(page, alt);
4760
+ tried.push(alt);
4761
+ if (attempt.ok) {
4762
+ detail = attempt;
4763
+ used = alt;
4764
+ }
4765
+ }
4766
+ }
4767
+ }
4768
+ }
4769
+ return plainJson({
4770
+ workshop: page.packagesDir,
4771
+ unit: page.unit,
4772
+ ok: detail.ok,
4773
+ key: used,
4774
+ tried,
4775
+ refreshed,
4776
+ detail: detail.payload,
4777
+ text: detail.ok ? `${used} 的完整 body 在 detail.body 里${used === key ? "" : `(给的 key 不在缓存里,取了同名的 ${used})`}${refreshed ? "(先跑了一次性抓取把缓存落下来)" : ""}。` : `没取到:${actErrorOf(detail)} —— 试过 ${String(tried.length)} 个 key(同名请求在缓存里会带 #2/#3…),缓存里都没有${refreshed ? "" : ",而刚才那次补抓一个请求也没抓到"}。重抓一次(${DIY_NET_WATCH_TOOL} → 做那件事 → ${DIY_NET_READ_TOOL})之后趁热再取;要的只是这一次接口的返回值的话,用 workbench_diy_probe 在页面里直接读更快。`
4778
+ });
4779
+ }
4780
+ const lines = asNumber(page.args["lines"]) ?? 60;
4781
+ const onlyFailed = page.args["failed"] === true;
4782
+ const grep = asText$1(page.args["grep"]);
4783
+ const all = await readNetwork(page.shell, file, Math.max(1, Math.min(500, lines)));
4784
+ const entries = all.filter((one) => {
4785
+ const status = typeof one["status"] === "number" ? one["status"] : 0;
4786
+ if (onlyFailed && !(status === 0 || status >= 400)) return false;
4787
+ if (grep !== "" && !String(one["url"] ?? "").includes(grep)) return false;
4788
+ return true;
4789
+ });
4790
+ return plainJson({
4791
+ workshop: page.packagesDir,
4792
+ unit: page.unit,
4793
+ ok: true,
4794
+ count: entries.length,
4795
+ seen: all.length,
4796
+ entries: entries.slice(-40),
4797
+ text: all.length === 0 ? "这份捕获是空的 —— 先用 workbench_diy_net_watch 臂上,再去做那件事,然后回来读。" : `${String(entries.length)}/${String(all.length)} 条(最近的在后面)。每条的 key 可以拿去取完整 body:${DIY_NET_READ_TOOL} key="<某条的 key>"。`
4798
+ });
4799
+ }
4800
+ };
4801
+ const netStopTool = {
4802
+ name: DIY_NET_STOP_TOOL,
4803
+ description: "网络捕获收工(把臂上的那条 follower 停掉)。",
4804
+ parameters: pageSchema(),
4805
+ output: OUTPUT$2,
4806
+ async execute(raw, exec) {
4807
+ const { page, file } = await netFileOf(exec, raw);
4808
+ const stopped = await stopNetwork(page.shell, page.session, file);
4809
+ return plainJson({
4810
+ workshop: page.packagesDir,
4811
+ unit: page.unit,
4812
+ ok: stopped,
4813
+ text: stopped ? `捕获收工了(follower 与看门狗都停掉)。` : `收工那一下没跑成(命令退出码非 0)—— 再调一次 ${DIY_NET_STOP_TOOL};还不行看一眼 shots/ 那份 .pid / .reaper。`
4814
+ });
4815
+ }
4816
+ };
4817
+ return [
4818
+ tools.register(netWatchTool),
4819
+ tools.register(netReadTool),
4820
+ tools.register(netStopTool)
4821
+ ];
4822
+ }
4823
+ //#endregion
4824
+ //#region src/modules/panels/model/diy-act.ts
4825
+ var TARGET_PROPS = {
4826
+ selector: {
4827
+ type: "string",
4828
+ description: "卡内的 CSS 选择器(默认只在这张卡里找;scope=page 才是整页)。"
4829
+ },
4830
+ role: {
4831
+ type: "string",
4832
+ description: "语义定位:角色(button / link / textbox / option…)。"
4833
+ },
4834
+ name: {
4835
+ type: "string",
4836
+ description: "语义定位:无障碍名(aria-label / 可见文字)包含这段。"
4837
+ },
4838
+ testid: {
4839
+ type: "string",
4840
+ description: "语义定位:data-testid 包含这段。"
4841
+ },
4842
+ nth: {
4843
+ type: "number",
4844
+ description: "匹配到多个时取第几个(0 起)。"
4845
+ },
4846
+ scope: {
4847
+ type: "string",
4848
+ description: "card(默认,只在这张卡里找)| page(整页找)。"
4849
+ }
4850
+ };
4851
+ function targetSchema(extra = {}, required = []) {
4852
+ return {
4853
+ type: "object",
4854
+ properties: {
4855
+ unit: UNIT_PROP,
4856
+ ...TARGET_PROPS,
4857
+ ...extra,
4858
+ session: SESSION_PROP
4859
+ },
4860
+ ...required.length === 0 ? {} : { required },
4861
+ additionalProperties: false
4862
+ };
4863
+ }
4864
+ function hasTarget(target) {
4865
+ return target.selector !== "" || target.role !== "" || target.name !== "" || target.testid !== "";
4866
+ }
4867
+ function describeTarget(target, selector) {
4868
+ if (selector !== "") return `\`${selector}\``;
4869
+ const bits = [];
4870
+ if (target.role !== "") bits.push(`role=${target.role}`);
4871
+ if (target.name !== "") bits.push(`name=${target.name}`);
4872
+ if (target.testid !== "") bits.push(`testid=${target.testid}`);
4873
+ return bits.join(" · ");
4874
+ }
4875
+ function renderAct(verb, target, selector, step) {
4876
+ const matches = matchesOf(step);
4877
+ if (matches === 0) return `没找到要${verb}的东西(${describeTarget(target, selector)})—— 用 workbench_diy_look 的 dom=outline 看一眼这张卡里到底有什么。`;
4878
+ const error = actErrorOf(step);
4879
+ if (error !== "") return `${verb}没成:${error}`;
4880
+ const many = matches > 1 ? `(匹配 ${String(matches)} 个,用的是第 ${String((target.nth ?? 0) + 1)} 个)` : "";
4881
+ return `${verb}了 ${describeTarget(target, selector)}${many}。看一眼 workbench_diy_look 就知道这一下有没有生效。`;
4882
+ }
4883
+ function registerActTools(ctx, tools) {
4884
+ async function resolveTarget(page, target, verb) {
4885
+ if (target.selector !== "") return {
4886
+ selector: scopedSelectorOf(page.unit, target),
4887
+ options: [],
4888
+ token: "",
4889
+ via: "css"
4890
+ };
4891
+ const semantic = semanticOptionsOf(target);
4892
+ if (target.page || semantic.length === 0) return {
4893
+ selector: "",
4894
+ options: targetOptionsOf(target),
4895
+ token: "",
4896
+ via: "page-semantic"
4897
+ };
4898
+ const found = await actBrowser(page.shell, page.command, page.session, [
4899
+ "find",
4900
+ ...semantic,
4901
+ "--limit",
4902
+ "200"
4903
+ ]);
4904
+ const refs = refsOf(found.payload);
4905
+ const total = totalMatchesOf(found.payload) || refs.length;
4906
+ if (refs.length === 0) throw new Error(`${verb}谁?这一页上没有匹配 ${describeTarget(target, "")} 的东西 —— 用 workbench_diy_look 的 dom=outline 看一眼这张卡里有什么,或者换成卡内 CSS(selector)。`);
4907
+ const token = `diy-${Date.now().toString(36)}`;
4908
+ const nth = target.nth ?? 0;
4909
+ const marked = await actBrowser(page.shell, page.command, page.session, ["eval", scopeMarkScriptOf(page.unit, refs, nth, token)]);
4910
+ if (marked.payload?.["ok"] !== true) {
4911
+ const inside = marked.payload?.["inside"];
4912
+ const why = marked.payload?.["code"] === "no-card" ? `这一页上没有 ${page.unit} 这张卡` : `卡内只有 ${String(typeof inside === "number" ? inside : 0)} 个匹配,拿不出第 ${String(nth + 1)} 个`;
4913
+ throw new Error(`${describeTarget(target, "")} 这一页上匹配了 ${String(total)} 个,但${why} —— 是不是 unit 给错了?也可以在 selector 里点名卡内 CSS,或 scope=page 明说要整页那一个。`);
4914
+ }
4915
+ return {
4916
+ selector: scopedMarkerSelectorOf(page.unit, token),
4917
+ options: [],
4918
+ token,
4919
+ via: "card-semantic"
4920
+ };
4921
+ }
4922
+ async function runTargeted(exec, raw, verb, build, needSelector = false) {
4923
+ const page = await pageAt(ctx, exec, raw);
4924
+ const target = actTargetOf(page.args);
4925
+ if (!hasTarget(target)) throw new Error(`${verb}谁?给一个 selector(卡内 CSS),或 role / name / testid 之一。`);
4926
+ const resolved = await resolveTarget(page, target, verb);
4927
+ if (needSelector && resolved.selector === "") throw new Error(`${verb}要一个 selector:输入框得点名才敢动手(语义定位请去掉 scope=page)。`);
4928
+ const step = await actBrowser(page.shell, page.command, page.session, [...build(resolved.selector, target), ...resolved.options]);
4929
+ if (resolved.token !== "") await actBrowser(page.shell, page.command, page.session, ["eval", scopeClearScriptOf(resolved.token)]);
4930
+ return plainJson({
4931
+ workshop: page.packagesDir,
4932
+ unit: page.unit,
4933
+ ok: step.ok,
4934
+ verb,
4935
+ via: resolved.via,
4936
+ matches: matchesOf(step),
4937
+ text: renderAct(verb, target, target.selector === "" ? "" : resolved.selector, step) + (resolved.via === "card-semantic" ? `(语义定位收进了 ${page.unit} 这张卡)` : "")
4938
+ });
4939
+ }
4940
+ const clickTool = {
4941
+ name: DIY_CLICK_TOOL,
4942
+ description: "在这张卡里点一下(按钮 / 链接 / 行)。默认只在这张卡里找目标,找不到就直说。",
4943
+ parameters: targetSchema(),
4944
+ output: OUTPUT$2,
4945
+ execute: (raw, exec) => runTargeted(exec, raw, "点", (selector) => selector === "" ? ["click"] : ["click", selector])
4946
+ };
4947
+ const typeTool = {
4948
+ name: DIY_TYPE_TOOL,
4949
+ description: "往这张卡里的某个输入框敲字(先点它再敲)—— 要一个 selector 与 value。",
4950
+ parameters: targetSchema({ value: {
4951
+ type: "string",
4952
+ description: "要敲进去的字。"
4953
+ } }, ["selector", "value"]),
4954
+ output: OUTPUT$2,
4955
+ execute: (raw, exec) => runTargeted(exec, raw, "敲", (selector) => [
4956
+ "type",
4957
+ selector,
4958
+ asText$1(asRecord$3(raw)["value"])
4959
+ ], true)
4960
+ };
4961
+ const fillTool = {
4962
+ name: DIY_FILL_TOOL,
4963
+ description: "把这张卡里某个输入框的值直接设成这段字并核对(不改别处)—— 要一个 selector 与 value。",
4964
+ parameters: targetSchema({ value: {
4965
+ type: "string",
4966
+ description: "要设进去的值。"
4967
+ } }, ["selector", "value"]),
4968
+ output: OUTPUT$2,
4969
+ execute: (raw, exec) => runTargeted(exec, raw, "填", (selector) => [
4970
+ "fill",
4971
+ selector,
4972
+ asText$1(asRecord$3(raw)["value"])
4973
+ ], true)
4974
+ };
4975
+ const keysTool = {
4976
+ name: DIY_KEYS_TOOL,
4977
+ description: "在这一页上按一个键(Enter / Escape / Tab / Control+a)—— 收下拉、提交、挪焦点用它。",
4978
+ parameters: pageSchema({ key: {
4979
+ type: "string",
4980
+ description: "要按的键。"
4981
+ } }, ["key"]),
4982
+ output: OUTPUT$2,
4983
+ async execute(raw, exec) {
4984
+ const page = await pageAt(ctx, exec, raw, { requireUnit: false });
4985
+ const key = asText$1(page.args["key"]);
4986
+ if (key === "") throw new Error("按哪个键?给 key(Enter / Escape / Tab / Control+a)。");
4987
+ const step = await actBrowser(page.shell, page.command, page.session, ["keys", key]);
4988
+ const error = actErrorOf(step);
4989
+ return plainJson({
4990
+ workshop: page.packagesDir,
4991
+ unit: page.unit,
4992
+ ok: step.ok,
4993
+ verb: "keys",
4994
+ text: error === "" ? `按了 ${key}。` : `按键没成:${error}`
4995
+ });
4996
+ }
4997
+ };
4998
+ const hoverTool = {
4999
+ name: DIY_HOVER_TOOL,
5000
+ description: "把鼠标移到这张卡里的某个东西上(悬停才出来的东西靠它)。",
5001
+ parameters: targetSchema(),
5002
+ output: OUTPUT$2,
5003
+ execute: (raw, exec) => runTargeted(exec, raw, "悬停", (selector) => selector === "" ? ["hover"] : ["hover", selector])
5004
+ };
5005
+ const selectTool = {
5006
+ name: DIY_SELECT_TOOL,
5007
+ description: "在这张卡的下拉里选一项 —— 要一个 selector 与选项文字。",
5008
+ parameters: targetSchema({ value: {
5009
+ type: "string",
5010
+ description: "选项文字(或 value)。"
5011
+ } }, ["selector", "value"]),
5012
+ output: OUTPUT$2,
5013
+ execute: (raw, exec) => runTargeted(exec, raw, "选", (selector) => [
5014
+ "select",
5015
+ selector,
5016
+ asText$1(asRecord$3(raw)["value"])
5017
+ ], true)
5018
+ };
5019
+ const scrollTool = {
5020
+ name: DIY_SCROLL_TOOL,
5021
+ description: "滚这一页(direction + amount),或把这张卡 / 卡里某块滚到看得见(into)。",
5022
+ parameters: pageSchema({
5023
+ direction: {
5024
+ type: "string",
5025
+ description: "up | down(不给 into 时用它)。"
5026
+ },
5027
+ amount: {
5028
+ type: "number",
5029
+ description: "滚多少像素(默认 500)。"
5030
+ },
5031
+ into: {
5032
+ type: "string",
5033
+ description: "滚到看得见:留空 = 这张卡;填卡内 CSS = 卡里那一块。"
5034
+ }
5035
+ }),
5036
+ output: OUTPUT$2,
5037
+ async execute(raw, exec) {
5038
+ const page = await pageAt(ctx, exec, raw);
5039
+ const into = page.args["into"];
5040
+ if (into !== void 0) {
5041
+ const step = await scrollIntoView(page.shell, page.command, page.session, page.unit, asText$1(into));
5042
+ const found = step.payload?.["found"] === true;
5043
+ return plainJson({
5044
+ workshop: page.packagesDir,
5045
+ unit: page.unit,
5046
+ ok: step.ok && found,
5047
+ verb: "scroll",
5048
+ text: actErrorOf(step) !== "" ? `滚不动:${actErrorOf(step)}` : found ? "滚到看得见了。" : `卡里没有要滚的那一块(${asText$1(into) || "卡片本身"})。`
5049
+ });
5050
+ }
5051
+ const direction = asText$1(page.args["direction"]) || "down";
5052
+ const amount = asNumber(page.args["amount"]) ?? 500;
5053
+ const step = await actBrowser(page.shell, page.command, page.session, [
5054
+ "scroll",
5055
+ direction,
5056
+ "--amount",
5057
+ String(amount)
5058
+ ]);
5059
+ const error = actErrorOf(step);
5060
+ return plainJson({
5061
+ workshop: page.packagesDir,
5062
+ unit: page.unit,
5063
+ ok: step.ok,
5064
+ verb: "scroll",
5065
+ text: error === "" ? `这一页往${direction === "up" ? "上" : "下"}滚了 ${String(amount)}px。` : `滚不动:${error}`
5066
+ });
5067
+ }
5068
+ };
5069
+ const reloadTool = {
5070
+ name: DIY_RELOAD_TOOL,
5071
+ description: "刷新用户正开着的这一页(页面上跑的外壳/共享包是旧的一份时用它;卡片自己会热更新,一般不用)。",
5072
+ parameters: pageSchema(),
5073
+ output: OUTPUT$2,
5074
+ async execute(raw, exec) {
5075
+ const page = await pageAt(ctx, exec, raw, {
5076
+ requireWorkbench: false,
5077
+ requireUnit: false
5078
+ });
5079
+ const step = await reloadPage(page.shell, page.command, page.session);
5080
+ const error = actErrorOf(step);
5081
+ return plainJson({
5082
+ workshop: page.packagesDir,
5083
+ unit: page.unit,
5084
+ ok: step.ok,
5085
+ verb: "reload",
5086
+ text: error === "" ? "让这一页自己刷新了(等两秒再看)。" : `刷新没成:${error}`
5087
+ });
5088
+ }
5089
+ };
5090
+ const viewportTool = {
5091
+ name: DIY_VIEWPORT_TOOL,
5092
+ description: "按某个宽度重拍一张整屏图(看窄屏/宽屏下这张卡挤不挤)—— 只在这一拍里换视口。",
5093
+ parameters: pageSchema({
5094
+ width: {
5095
+ type: "number",
5096
+ description: "视口宽度(CSS px,例如 390)。"
5097
+ },
5098
+ height: {
5099
+ type: "number",
5100
+ description: "视口高度(可不要)。"
5101
+ }
5102
+ }, ["width"]),
5103
+ output: OUTPUT$2,
5104
+ async execute(raw, exec) {
5105
+ const page = await pageAt(ctx, exec, raw, { requireUnit: false });
5106
+ const width = asNumber(page.args["width"]);
5107
+ if (width === void 0 || width <= 0) throw new Error("多宽?给 width(CSS px)。");
5108
+ const height = asNumber(page.args["height"]) ?? 0;
5109
+ const file = join(dirname(page.packagesDir), "shots", `${page.unit || "page"}-${String(width)}-${String(Date.now())}.png`);
5110
+ await writeShotsDir(page.shell, dirname(file));
5111
+ const shot = await shootAtWidth(page.shell, page.command, page.session, file, width, height);
5112
+ return plainJson({
5113
+ workshop: page.packagesDir,
5114
+ unit: page.unit,
5115
+ ok: shot.ok,
5116
+ file: shot.file,
5117
+ width,
5118
+ viewport: {
5119
+ width,
5120
+ height
5121
+ },
5122
+ text: shot.ok ? `按 ${String(width)}px 宽重拍了一张:${shot.file}(页面按新视口回流,只在这一拍里生效)。` : `没拍成:${shot.reason}`
5123
+ });
5124
+ }
5125
+ };
5126
+ return [
5127
+ tools.register(clickTool),
5128
+ tools.register(typeTool),
5129
+ tools.register(fillTool),
5130
+ tools.register(keysTool),
5131
+ tools.register(hoverTool),
5132
+ tools.register(selectTool),
5133
+ tools.register(scrollTool),
5134
+ tools.register(reloadTool),
5135
+ tools.register(viewportTool)
5136
+ ];
5137
+ }
5138
+ //#endregion
5139
+ //#region src/shared/api/host/diy-recon.ts
5140
+ var SITE_LIMIT = 6;
5141
+ var COMMAND_LIMIT = 12;
5142
+ var SHOWN_LIMIT = 6;
5143
+ var PLUGIN_PREFIX = "opencli-plugin-";
5144
+ function text(value) {
5145
+ return typeof value === "string" ? value.trim() : "";
5146
+ }
5147
+ function stringsOf$1(value) {
5148
+ return Array.isArray(value) ? value.filter((one) => typeof one === "string") : [];
5149
+ }
5150
+ function manifestCommandsOf(raw) {
5151
+ let parsed;
5152
+ try {
5153
+ parsed = JSON.parse(raw);
5154
+ } catch {
5155
+ return [];
5156
+ }
5157
+ if (!Array.isArray(parsed)) return [];
5158
+ const commands = [];
5159
+ for (const one of parsed) {
5160
+ if (typeof one !== "object" || one === null) continue;
5161
+ const row = one;
5162
+ const declared = text(row["command"]);
5163
+ const site = text(row["site"]) || declared.split("/")[0] || "";
5164
+ const name = text(row["name"]) || declared.split("/")[1] || "";
5165
+ const command = declared === "" ? site === "" || name === "" ? "" : `${site}/${name}` : declared;
5166
+ if (command === "" || site === "") continue;
5167
+ const access = text(row["access"]);
5168
+ commands.push({
5169
+ command,
5170
+ site,
5171
+ name: name === "" ? command.slice(command.indexOf("/") + 1) : name,
5172
+ domain: text(row["domain"]).toLowerCase(),
5173
+ description: text(row["description"]),
5174
+ access: access === "" ? "read" : access,
5175
+ strategy: text(row["strategy"]).toLowerCase(),
5176
+ browser: row["browser"] === true,
5177
+ example: text(row["example"]),
5178
+ columns: stringsOf$1(row["columns"])
5179
+ });
5180
+ }
5181
+ return commands;
5182
+ }
5183
+ function pluginNamesOf(entries) {
5184
+ return entries.filter((one) => one.startsWith(PLUGIN_PREFIX)).map((one) => one.slice(15)).filter((one) => one !== "").sort();
5185
+ }
5186
+ function pluginCommandsOf(files) {
5187
+ return files.filter((one) => one.endsWith(".js")).map((one) => one.slice(0, -3)).filter((one) => one !== "index" && !one.startsWith("_")).sort();
5188
+ }
5189
+ function hostOf(url) {
5190
+ const trimmed = url.trim();
5191
+ if (trimmed === "") return "";
5192
+ try {
5193
+ return new URL(trimmed).hostname.toLowerCase().replace(/^www\./, "");
5194
+ } catch {
5195
+ return "";
5196
+ }
5197
+ }
5198
+ function keywordsOf(keyword, url) {
5199
+ const words = /* @__PURE__ */ new Set();
5200
+ const trimmed = keyword.trim().toLowerCase();
5201
+ if (trimmed !== "") words.add(trimmed);
5202
+ const host = hostOf(url);
5203
+ if (host !== "") words.add(host);
5204
+ return [...words];
5205
+ }
5206
+ function scoreOf(command, word) {
5207
+ const site = command.site.toLowerCase();
5208
+ const name = command.name.toLowerCase();
5209
+ if (word === "") return 0;
5210
+ if (site === word) return 100;
5211
+ if (site.includes(word)) return 80;
5212
+ if (command.domain !== "" && command.domain.includes(word)) return 70;
5213
+ if (site.length >= 3 && word.includes(site)) return 45;
5214
+ if (name === word) return 65;
5215
+ if (name.includes(word)) return 55;
5216
+ if (command.command.toLowerCase().includes(word)) return 50;
5217
+ if (command.description.toLowerCase().includes(word)) return 30;
5218
+ return 0;
5219
+ }
5220
+ function reconOf(commands, plugins, keywords) {
5221
+ const keyword = keywords[0] ?? "";
5222
+ const best = /* @__PURE__ */ new Map();
5223
+ for (const command of commands) {
5224
+ let score = 0;
5225
+ for (const word of keywords) score = Math.max(score, scoreOf(command, word));
5226
+ if (score === 0) continue;
5227
+ const current = best.get(command.command);
5228
+ if (current === void 0 || current.score < score) best.set(command.command, {
5229
+ command,
5230
+ score
5231
+ });
5232
+ }
5233
+ const grouped = /* @__PURE__ */ new Map();
5234
+ for (const entry of best.values()) {
5235
+ const group = grouped.get(entry.command.site) ?? {
5236
+ best: 0,
5237
+ rows: []
5238
+ };
5239
+ group.best = Math.max(group.best, entry.score);
5240
+ group.rows.push(entry);
5241
+ grouped.set(entry.command.site, group);
5242
+ }
5243
+ const base = {
5244
+ keyword,
5245
+ sites: [...grouped.entries()].sort((left, right) => right[1].best - left[1].best || left[0].localeCompare(right[0])).slice(0, SITE_LIMIT).map(([site, group]) => {
5246
+ const picked = [...group.rows].sort((left, right) => (left.command.access === "write" ? 1 : 0) - (right.command.access === "write" ? 1 : 0) || right.score - left.score || left.command.command.localeCompare(right.command.command)).slice(0, COMMAND_LIMIT).map((one) => one.command);
5247
+ const first = picked[0];
3377
5248
  return {
3378
5249
  site,
3379
5250
  domain: first?.domain ?? "",
@@ -3407,7 +5278,7 @@ function nextOf(answer) {
3407
5278
  }
3408
5279
  const plugin = answer.plugins[0];
3409
5280
  if (plugin !== void 0) return `这台机器另装的插件 ${plugin.name} 里有匹配:opencli ${plugin.name} --help 看全部命令`;
3410
- return "这台机器上没有现成的 —— 两条路:① 给这个站点写 adapter:opencli skills read opencli-adapter-author;② 只读一次:workbench_diy action=probe(script 读接口 / css 读 DOM)";
5281
+ return "这台机器上没有现成的 —— 两条路:① 给这个站点写 adapter:opencli skills read opencli-adapter-author;② 只读一次:workbench_diy_probe(script 读接口 / css 读 DOM)";
3411
5282
  }
3412
5283
  function textOf$3(answer) {
3413
5284
  const lines = [];
@@ -3575,7 +5446,7 @@ function renderPanel(options) {
3575
5446
  function statusOf(options) {
3576
5447
  const { dir, unit, name, installed, known, open, health } = options;
3577
5448
  const facts = moduleBuildFacts(dir);
3578
- const next = health.state === "failed" ? `它起不来:${health.reason ?? "(没给原因)"} —— 先把这一条修掉(改模块源码 → build;改了 package.json / cordis.patch.yml 要重启工作台),别去重装` : !installed ? "装:action=install(第一次,会让用户点头一次)" : !known ? "面板还不认得这个单元 —— 重启一次工作台(新装的模块要重启才注册进面板),重启后再 action=open" : !open ? "上板:action=open(只上板,不重装)" : !facts.builtAll ? "还没构建:pnpm exec dsh-wb-module build" : facts.stale ? "源码比产物新:pnpm exec dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新页面)" : "都在生效:改源码 → build(宿主侧秒级热换;改了 client.js 刷新页面),不要再装一遍";
5449
+ const next = health.state === "failed" ? `它起不来:${health.reason ?? "(没给原因)"} —— 先把这一条修掉(改模块源码 → build;改了 package.json / cordis.patch.yml 要重启工作台),别去重装` : !installed ? "装:workbench_diy_install(第一次,会让用户点头一次)" : !known ? "面板还不认得这个单元 —— 重启一次工作台(新装的模块要重启才注册进面板),重启后再 workbench_diy_show" : !open ? "上板:workbench_diy_show(只上板,不重装)" : !facts.builtAll ? "还没构建:pnpm exec dsh-wb-module build" : facts.stale ? "源码比产物新:pnpm exec dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新页面)" : "都在生效:改源码 → build(宿主侧秒级热换;改了 client.js 刷新页面),不要再装一遍";
3579
5450
  return {
3580
5451
  unit,
3581
5452
  name,
@@ -3607,342 +5478,319 @@ function renderStatus(packagesDir, lines) {
3607
5478
  function renderList(packagesDir, modules) {
3608
5479
  if (modules.length === 0) return `工坊里还没有模块(${packagesDir})—— 在里面造一个:pnpm exec dsh-wb-module init <名字> --dir <工坊>/packages/module-<名字> --template diy`;
3609
5480
  const lines = modules.map((one) => `${one.unit} · ${one.installed ? "已装" : "没装"} · ${one.name}` + (one.description === "" ? "" : `\n${" ".repeat(one.unit.length)} ${one.description}`));
3610
- const footer = modules.filter((one) => !one.installed).length === 0 ? "都已装过 —— **不要再装**:改源码 + dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面)。" : "装:workbench_diy(action=install, unit=<上面那个 unit>)";
5481
+ const footer = modules.filter((one) => !one.installed).length === 0 ? "都已装过 —— **不要再装**:改源码 + dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面)。" : "装:workbench_diy_install(unit=<上面那个 unit>)";
3611
5482
  return `工坊:${packagesDir}\n${lines.join("\n")}\n\n${footer}`;
3612
5483
  }
3613
5484
  //#endregion
3614
5485
  //#region src/modules/panels/model/diy-tools.ts
3615
- var ACTIONS$2 = [
3616
- "list",
3617
- "status",
3618
- "panel",
3619
- "probe",
3620
- "recon",
3621
- "install",
3622
- "open"
3623
- ];
3624
- var OUTPUT_SCHEMA$2 = {
3625
- type: "object",
3626
- properties: {
3627
- text: { type: "string" },
3628
- workshop: { type: "string" },
3629
- modules: {
3630
- type: "array",
3631
- items: {
3632
- type: "object",
3633
- additionalProperties: true
3634
- }
3635
- },
3636
- unit: { type: "string" },
3637
- path: { type: "string" },
3638
- install: {
3639
- type: "object",
3640
- additionalProperties: true
3641
- },
3642
- alreadyInstalled: { type: "boolean" },
3643
- panel: {
3644
- type: "object",
3645
- additionalProperties: true
3646
- }
3647
- },
3648
- additionalProperties: true
3649
- };
3650
- function asRecord$3(value) {
3651
- return typeof value === "object" && value !== null ? value : void 0;
3652
- }
3653
- function asNumber(value) {
3654
- if (typeof value === "number" && Number.isFinite(value)) return value;
3655
- if (typeof value === "string" && value.trim() !== "") {
3656
- const parsed = Number(value);
3657
- if (Number.isFinite(parsed)) return parsed;
3658
- }
3659
- }
3660
- function asText$1(value) {
3661
- return typeof value === "string" ? value.trim() : "";
3662
- }
3663
- function plainJson(value) {
3664
- return JSON.parse(JSON.stringify(value));
3665
- }
3666
- function verdictOf$1(change) {
3667
- if (change.application === "applied") return "装好了,已生效";
3668
- if (change.application === "restart-required") return "装好了,要重启一次才生效(装新模块、同名重装、替换已有包都走这一支)";
3669
- if (change.application === "overridden") return "被更高层的声明盖住了,这次没生效";
3670
- if (change.application === "cancelled") return "安装被取消,没有生效";
3671
- const tail = (change.packageResult?.output ?? "").trim().split("\n").slice(-3).join(" | ");
3672
- return `没装上(${change.errorCode ?? "failed"})${tail === "" ? "" : `:${tail}`}`;
3673
- }
3674
- function renderProbe(out) {
3675
- if (out.ok) return `读到了 —— 这就是那条读的返回值,把它固化成模块宿主半侧的那一条:\n${(JSON.stringify(out.value, null, 2) ?? "undefined").slice(0, 2e3)}`;
3676
- const lines = [`没读到(${out.code}):${out.message}`];
3677
- if (out.hint !== void 0) lines.push(`照做:${out.hint}`);
3678
- if (out.helpUrl !== void 0) lines.push(`入口:${out.helpUrl}`);
3679
- if (out.pageCode !== void 0) lines.push(`页面自己回的码:${out.pageCode}`);
3680
- return lines.join("\n");
3681
- }
3682
- function renderFind(out) {
3683
- if (!out.ok) {
3684
- const lines = [`没找到(${out.code}):${out.message}`];
3685
- if (out.hint !== void 0) lines.push(`照做:${out.hint}`);
3686
- if (out.helpUrl !== void 0) lines.push(`入口:${out.helpUrl}`);
3687
- return lines.join("\n");
3688
- }
3689
- if (out.count === 0) return "找到了 0 个元素 —— 选择器没匹配到东西。先确认停在哪一页(probe 会自己问页面),再把选择器放宽一点(例如 table tr 而不是某个 id)。";
3690
- const head = out.entries.slice(0, 20).map((one, index) => `${String(index + 1)}. ${one.text.replace(/\n+/g, " | ").slice(0, 200)}`);
3691
- const more = out.entries.length > 20 ? `\n… 还有 ${String(out.entries.length - 20)} 个` : "";
3692
- return `找到了 ${String(out.count)} 个元素 —— 每行的单元格按换行拼在 text 里;把它固化成模块宿主半侧的那一条(host 的 browserFind + 切格 + 按列序取值):\n${head.join("\n")}${more}`;
3693
- }
3694
- function showOrReason(panels, unit, workspace) {
3695
- try {
3696
- panels.show(unit, workspace);
3697
- return { ok: true };
3698
- } catch (error) {
3699
- return {
3700
- ok: false,
3701
- reason: error instanceof Error ? error.message : String(error)
3702
- };
3703
- }
3704
- }
3705
5486
  function registerDiyTools(ctx, market, panels) {
3706
5487
  const tools = ctx.get("tools");
3707
5488
  if (tools === void 0) {
3708
5489
  ctx.logger?.info?.("[dsh-workbench] 没有 tools 服务,跳过 DIY 工坊工具注册");
3709
5490
  return [];
3710
5491
  }
3711
- const tool = {
3712
- name: "workbench_diy",
3713
- description: "DIY 工坊里的模块:看它走到哪一步,或把它装上 / 上板。只认工坊里造好的模块 —— 普通项目里装模块用 workbench_modules。status 是入口,照它给的\"下一步\"走。install 是这台机器上的持久变更,一个模块一辈子只装一次:之后改源码只需 dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面)。",
3714
- parameters: withApprovalNote({
5492
+ const listTool = {
5493
+ name: DIY_LIST_TOOL,
5494
+ description: "DIY 工坊里有哪些模块、装没装 —— 先跑它拿到 unit。普通项目里装模块用 workbench_modules_install。",
5495
+ parameters: {
5496
+ type: "object",
5497
+ properties: {},
5498
+ additionalProperties: false
5499
+ },
5500
+ output: OUTPUT$2,
5501
+ async execute(_raw, exec) {
5502
+ const { packagesDir } = workshopOf(ctx, exec);
5503
+ const { modules } = modulesOf(ctx, packagesDir);
5504
+ return plainJson({
5505
+ workshop: packagesDir,
5506
+ modules,
5507
+ text: renderList(packagesDir, modules)
5508
+ });
5509
+ }
5510
+ };
5511
+ const statusTool = {
5512
+ name: DIY_STATUS_TOOL,
5513
+ description: "某个模块走到哪一步:装没装、面板认不认得、起得来没,以及下一步做什么。",
5514
+ parameters: {
5515
+ type: "object",
5516
+ properties: { unit: {
5517
+ type: "string",
5518
+ description: "单元 id(workbench_diy_list 会给)。"
5519
+ } },
5520
+ required: ["unit"],
5521
+ additionalProperties: false
5522
+ },
5523
+ output: OUTPUT$2,
5524
+ async execute(raw, exec) {
5525
+ const { packagesDir, workspace } = workshopOf(ctx, exec);
5526
+ const { modules } = modulesOf(ctx, packagesDir);
5527
+ const registry = ctx.get(UNITS_SERVICE);
5528
+ const panelEntries = panelEntriesOf(panels, workspace);
5529
+ const wanted = asText$1(asRecord$3(raw)["unit"]);
5530
+ const picked = wanted === "" ? modules : modules.filter((one) => one.unit === wanted);
5531
+ if (picked.length === 0) throw new Error(`工坊里没有单元 id 为「${wanted}」的模块(可用的:${modules.map((one) => one.unit).join(", ") || "(空)"})`);
5532
+ const rows = picked.map((one) => statusOf({
5533
+ dir: one.dir,
5534
+ unit: one.unit,
5535
+ name: one.name,
5536
+ installed: one.installed,
5537
+ known: panelEntries.some((entry) => entry.id === one.unit),
5538
+ open: panels.isEnabled(one.unit, workspace),
5539
+ health: healthOf({
5540
+ registry,
5541
+ unit: one.unit,
5542
+ installed: one.installed
5543
+ })
5544
+ }));
5545
+ return plainJson({
5546
+ workshop: packagesDir,
5547
+ status: rows,
5548
+ text: renderStatus(packagesDir, rows)
5549
+ });
5550
+ }
5551
+ };
5552
+ const panelTool = {
5553
+ name: DIY_PANEL_TOOL,
5554
+ description: "面板此刻的真实样子(每张卡读到没 / 崩没崩)—— 用户说\"卡片不对 / 一片空白\"就先调它。",
5555
+ parameters: {
5556
+ type: "object",
5557
+ properties: { unit: {
5558
+ type: "string",
5559
+ description: "只看某一个单元(不填 = 全看)。"
5560
+ } },
5561
+ additionalProperties: false
5562
+ },
5563
+ output: OUTPUT$2,
5564
+ async execute(raw, exec) {
5565
+ const { packagesDir, workspace } = workshopOf(ctx, exec);
5566
+ const { installed, modules } = modulesOf(ctx, packagesDir);
5567
+ const panelState = ctx.get(PANEL_STATE_SERVICE);
5568
+ const registry = ctx.get(UNITS_SERVICE);
5569
+ const panelEntries = panelEntriesOf(panels, workspace);
5570
+ const snapshot = panelState?.snapshot(workspace) ?? {
5571
+ workspace,
5572
+ at: 0,
5573
+ units: []
5574
+ };
5575
+ const wanted = asText$1(asRecord$3(raw)["unit"]);
5576
+ const board = panelEntries.filter((entry) => entry.shown && entry.hasCard).map((entry) => entry.id);
5577
+ const nameOf = new Map(modules.map((one) => [one.unit, one.name]));
5578
+ const reported = new Map(snapshot.units.map((one) => [one.unit, one]));
5579
+ const rows = [.../* @__PURE__ */ new Set([
5580
+ ...board,
5581
+ ...reported.keys(),
5582
+ ...nameOf.keys()
5583
+ ])].filter((unit) => wanted === "" || unit === wanted).map((unit) => {
5584
+ const report = reported.get(unit);
5585
+ return {
5586
+ unit,
5587
+ name: nameOf.get(unit) ?? "",
5588
+ health: healthOf({
5589
+ registry,
5590
+ unit,
5591
+ installed: installed.includes(unit)
5592
+ }),
5593
+ ...report === void 0 ? {} : { report }
5594
+ };
5595
+ });
5596
+ return plainJson({
5597
+ workshop: packagesDir,
5598
+ workspace,
5599
+ board,
5600
+ report: snapshot,
5601
+ panels: rows,
5602
+ text: renderPanel({
5603
+ packagesDir,
5604
+ workspace,
5605
+ rows,
5606
+ board,
5607
+ at: snapshot.at,
5608
+ now: Date.now()
5609
+ })
5610
+ });
5611
+ }
5612
+ };
5613
+ const reconTool = {
5614
+ name: DIY_RECON_TOOL,
5615
+ description: "先查 opencli 有没有现成的(自带 adapter / 本机插件)—— opencli 是什么自己查,别问用户。",
5616
+ parameters: {
3715
5617
  type: "object",
3716
5618
  properties: {
3717
- action: {
5619
+ keyword: {
3718
5620
  type: "string",
3719
- description: "list=工坊里有哪些模块(默认);status=某个模块走到哪一步、下一步做什么;panel=面板此刻的真实样子(宿主回的原文都在)—— 用户说\"卡片不对 / 一片空白 / 一直转圈\"就先调它;recon=先查 opencli 有没有现成的(自带 adapter / 本机插件)—— opencli 是什么自己查,别问用户;probe=用用户浏览器里现有的登录探一条读(script 或 css);open=只上板(不装);install=装上并上板(第一次,会让用户点头)。",
3720
- enum: [...ACTIONS$2]
5621
+ description: "站点名 / 系统名 / 域名(英文名或域名最准,例:bilibili、arxiv.org、cmdb)。"
3721
5622
  },
3722
- unit: {
5623
+ url: {
3723
5624
  type: "string",
3724
- description: "status / panel / open / install 填:单元 id(list 会给)。"
3725
- },
5625
+ description: "那套系统的地址(从它取域名)。"
5626
+ }
5627
+ },
5628
+ required: ["keyword"],
5629
+ additionalProperties: false
5630
+ },
5631
+ output: OUTPUT$2,
5632
+ async execute(raw, exec) {
5633
+ const { packagesDir } = workshopOf(ctx, exec);
5634
+ const args = asRecord$3(raw);
5635
+ const keywords = keywordsOf(asText$1(args["keyword"]), asText$1(args["url"]));
5636
+ if (keywords.length === 0) throw new Error("recon 要一个关键词(站点名 / 系统名 / 域名都行):先拿它去 opencli 自带的那份目录里找有没有现成的能力,别一上来就自己写 adapter;用户不知道 opencli 是什么,这一步别问他。");
5637
+ const catalog = reconCatalogOf();
5638
+ if (catalog === void 0) throw new Error(`读不到 opencli 那份目录(${cliCatalogPath()})—— 隔离运行时还没装好;照做:${isolatedInstallCommand()}`);
5639
+ return plainJson({
5640
+ workshop: packagesDir,
5641
+ ok: true,
5642
+ ...reconOf(catalog, installedPluginsOf(), keywords)
5643
+ });
5644
+ }
5645
+ };
5646
+ const probeTool = {
5647
+ name: DIY_PROBE_TOOL,
5648
+ description: "用用户浏览器里现有的登录探一条读:script 读接口 / css 读 DOM。不在站点上会自己打开 url。",
5649
+ parameters: {
5650
+ type: "object",
5651
+ properties: {
3726
5652
  script: {
3727
5653
  type: "string",
3728
- description: "probe 要(与 css 二选一):读接口用这个 —— 在页面里跑一段回 JSON 的脚本。不在站点就回 { ok: false, code: \"not-on-site\" };登录过期回 { ok: false, code: \"not-logged-in\" };成功回 { ok: true, ...数据 }。"
5654
+ description: "与 css 二选一:页面里跑一段回 JSON 的脚本。不在站点回 { ok: false, code: \"not-on-site\" },登录过期回 \"not-logged-in\",成功回 { ok: true, ...数据 }。"
3729
5655
  },
3730
5656
  css: {
3731
5657
  type: "string",
3732
- description: "probe 要(与 script 二选一):读 DOM 用这个 —— 给一个 CSS 选择器,宿主回匹配到的元素文本(每行的单元格按换行拼在 text 里);不在站点会自己打开 url 再读一遍。"
5658
+ description: "与 script 二选一:给一个 CSS 选择器,宿主回匹配到的元素文本(每行单元格按换行拼在 text 里)。"
3733
5659
  },
3734
5660
  limit: {
3735
5661
  type: "number",
3736
- description: "probe 配 css 用:最多回几个元素(默认 50)。"
5662
+ description: "配 css 用:最多回几个元素(默认 50)。"
3737
5663
  },
3738
5664
  textMax: {
3739
5665
  type: "number",
3740
- description: "probe 配 css 用:每个元素文本最多留几个字(默认 200)。"
5666
+ description: "配 css 用:每个元素文本最多留几个字(默认 200)。"
3741
5667
  },
3742
5668
  url: {
3743
5669
  type: "string",
3744
- description: "probe / recon 用:那套系统的地址(probe 不在站点上时自己打开它再读一遍;recon 从它取域名)。"
3745
- },
3746
- keyword: {
3747
- type: "string",
3748
- description: "recon 要:站点名 / 系统名 / 域名(英文名或域名最准,例:bilibili、arxiv.org、cmdb)。"
5670
+ description: "那套系统的地址(不在站点上时自己打开它再读一遍)。"
3749
5671
  },
3750
5672
  loginUrl: {
3751
5673
  type: "string",
3752
- description: "probe 用:要登录时那颗\"去登录\"该去哪(默认用 url)。"
5674
+ description: "要登录时那颗\"去登录\"该去哪(默认用 url)。"
3753
5675
  },
3754
5676
  session: {
3755
5677
  type: "string",
3756
- description: "probe 用:会话名(默认 workbench,一般不用填)。"
5678
+ description: "会话名(默认 workbench,一般不用填)。"
3757
5679
  },
3758
5680
  open: {
3759
5681
  type: "boolean",
3760
- description: "probe 用:不在站点上时要不要顺手打开(默认要)。"
5682
+ description: "不在站点上时要不要顺手打开(默认要)。"
3761
5683
  }
3762
5684
  },
3763
- required: ["action"],
3764
5685
  additionalProperties: false
3765
- }, { required: true }),
3766
- output: {
3767
- schema: OUTPUT_SCHEMA$2,
3768
- render: (_args, value) => {
3769
- const payload = value;
3770
- return [{
3771
- type: "text",
3772
- text: typeof payload?.text === "string" ? payload.text : JSON.stringify(value)
3773
- }];
3774
- }
3775
5686
  },
5687
+ output: OUTPUT$2,
3776
5688
  async execute(raw, exec) {
3777
- const args = asRecord$3(raw) ?? {};
3778
- const action = asText$1(args.action) || "list";
3779
- if (!ACTIONS$2.includes(action)) throw new Error(`不认识的 action:${action}(可用:${ACTIONS$2.join(" / ")})`);
3780
- const packagesDir = workshopPackagesDir(ctx);
3781
- if (packagesDir === null) throw new Error("算不出 DIY 工坊在哪(这台机器没给出 dshHomePath)—— 让用户跑一次 dsh-workbench setup");
3782
- const session = sessionWorkspace(exec);
3783
- if (!inWorkshopSession(ctx, session)) throw new Error(`这把工具只在 DIY 工坊里用:当前会话不在工坊里(这个会话的工作区 ${session ?? "(不知道)"})—— 普通项目里装模块用 workbench_modules,工坊的入口是左栏那一行「工作台DIY」`);
3784
- const workspace = session;
3785
- const installed = profileModules(profileDirOf(ctx)).map((row) => row.unit);
3786
- const modules = workshopModules(packagesDir, installed);
3787
- const registry = ctx.get(UNITS_SERVICE);
3788
- const panelState = ctx.get(PANEL_STATE_SERVICE);
3789
- if (action === "list") return plainJson({
3790
- workshop: packagesDir,
3791
- modules,
3792
- text: renderList(packagesDir, modules)
3793
- });
3794
- const panelEntries = (() => {
3795
- try {
3796
- return panels.list(workspace);
3797
- } catch {
3798
- return [];
3799
- }
3800
- })();
3801
- const statusOfUnit = (one) => statusOf({
3802
- dir: one.dir,
3803
- unit: one.unit,
3804
- name: one.name,
3805
- installed: one.installed,
3806
- known: panelEntries.some((entry) => entry.id === one.unit),
3807
- open: panels.isEnabled(one.unit, workspace),
3808
- health: healthOf({
3809
- registry,
3810
- unit: one.unit,
3811
- installed: one.installed
3812
- })
3813
- });
3814
- if (action === "status") {
3815
- const wanted = asText$1(args.unit);
3816
- const picked = wanted === "" ? modules : modules.filter((one) => one.unit === wanted);
3817
- if (wanted !== "" && picked.length === 0) throw new Error(`工坊里没有单元 id 为「${wanted}」的模块(可用的:${modules.map((one) => one.unit).join(", ") || "(空)"})`);
3818
- const rows = picked.map(statusOfUnit);
3819
- return plainJson({
3820
- workshop: packagesDir,
3821
- status: rows,
3822
- text: renderStatus(packagesDir, rows)
3823
- });
3824
- }
3825
- if (action === "panel") {
3826
- const snapshot = panelState?.snapshot(workspace) ?? {
3827
- workspace,
3828
- at: 0,
3829
- units: []
3830
- };
3831
- const wanted = asText$1(args.unit);
3832
- const board = panelEntries.filter((entry) => entry.shown && entry.hasCard).map((entry) => entry.id);
3833
- const nameOf = new Map(modules.map((one) => [one.unit, one.name]));
3834
- const reported = new Map(snapshot.units.map((one) => [one.unit, one]));
3835
- const rows = [.../* @__PURE__ */ new Set([
3836
- ...board,
3837
- ...reported.keys(),
3838
- ...nameOf.keys()
3839
- ])].filter((unit) => wanted === "" || unit === wanted).map((unit) => {
3840
- const report = reported.get(unit);
3841
- return {
3842
- unit,
3843
- name: nameOf.get(unit) ?? "",
3844
- health: healthOf({
3845
- registry,
3846
- unit,
3847
- installed: installed.includes(unit)
3848
- }),
3849
- ...report === void 0 ? {} : { report }
3850
- };
3851
- });
3852
- return plainJson({
3853
- workshop: packagesDir,
3854
- workspace,
3855
- board,
3856
- report: snapshot,
3857
- panels: rows,
3858
- text: renderPanel({
3859
- packagesDir,
3860
- workspace,
3861
- rows,
3862
- board,
3863
- at: snapshot.at,
3864
- now: Date.now()
3865
- })
3866
- });
3867
- }
3868
- if (action === "recon") {
3869
- const keywords = keywordsOf(asText$1(args.keyword), asText$1(args.url));
3870
- if (keywords.length === 0) throw new Error("recon 要一个关键词(站点名 / 系统名 / 域名都行):先拿它去 opencli 自带的那份目录里找有没有现成的能力,别一上来就自己写 adapter;用户不知道 opencli 是什么,这一步别问他。");
3871
- const catalog = reconCatalogOf();
3872
- if (catalog === void 0) throw new Error(`读不到 opencli 那份目录(${cliCatalogPath()})—— 隔离运行时还没装好;照做:${isolatedInstallCommand()}`);
3873
- return plainJson({
3874
- workshop: packagesDir,
3875
- ok: true,
3876
- ...reconOf(catalog, installedPluginsOf(), keywords)
3877
- });
3878
- }
3879
- if (action === "probe") {
3880
- const script = asText$1(args.script);
3881
- const css = asText$1(args.css);
3882
- if (script === "" && css === "") throw new Error("probe 要 script(页面脚本,回 JSON)或 css(读 DOM 那一行的选择器)之一。script 的约定:不在那个站点上就回 { ok: false, code: \"not-on-site\" }(我会自己把它打开再读一遍);登录过期回 { ok: false, code: \"not-logged-in\" };成功回 { ok: true, ...数据 }。页面里压根没有接口时给 css,别写 document.querySelectorAll(宿主产物不许出现 DOM 字样)。");
3883
- const shell = ctx.get("shell");
3884
- if (shell === void 0) throw new Error("这台机器上没有 shell 服务,probe 跑不了");
3885
- const url = asText$1(args.url);
3886
- const loginUrl = asText$1(args.loginUrl);
3887
- const session = asText$1(args.session);
3888
- const open = typeof args.open === "boolean" ? args.open : void 0;
3889
- if (css !== "") {
3890
- const limit = asNumber(args.limit);
3891
- const textMax = asNumber(args.textMax);
3892
- const found = await browserFind({
3893
- shell,
3894
- css,
3895
- command: opencliCommandOf(ctx).command,
3896
- ...url === "" ? {} : { url },
3897
- ...session === "" ? {} : { session },
3898
- ...open === void 0 ? {} : { open },
3899
- ...limit === void 0 ? {} : { limit },
3900
- ...textMax === void 0 ? {} : { textMax }
3901
- });
3902
- return plainJson({
3903
- workshop: packagesDir,
3904
- ...found,
3905
- text: renderFind(found)
3906
- });
3907
- }
3908
- const out = await browserRead({
5689
+ const { packagesDir } = workshopOf(ctx, exec);
5690
+ const args = asRecord$3(raw);
5691
+ const script = asText$1(args["script"]);
5692
+ const css = asText$1(args["css"]);
5693
+ if (script === "" && css === "") throw new Error("probe 要 script(页面脚本,回 JSON)或 css(读 DOM 那一行的选择器)之一。script 的约定:不在那个站点上就回 { ok: false, code: \"not-on-site\" }(我会自己把它打开再读一遍);登录过期回 { ok: false, code: \"not-logged-in\" };成功回 { ok: true, ...数据 }。页面里压根没有接口时给 css,别写 document.querySelectorAll(宿主产物不许出现 DOM 字样)。");
5694
+ const shell = ctx.get("shell");
5695
+ if (shell === void 0) throw new Error("这台机器上没有 shell 服务,probe 跑不了");
5696
+ const url = asText$1(args["url"]);
5697
+ const loginUrl = asText$1(args["loginUrl"]);
5698
+ const session = asText$1(args["session"]);
5699
+ const open = typeof args["open"] === "boolean" ? args["open"] : void 0;
5700
+ if (css !== "") {
5701
+ const limit = asNumber(args["limit"]);
5702
+ const textMax = asNumber(args["textMax"]);
5703
+ const found = await browserFind({
3909
5704
  shell,
3910
- script,
5705
+ css,
3911
5706
  command: opencliCommandOf(ctx).command,
3912
5707
  ...url === "" ? {} : { url },
3913
- ...loginUrl === "" ? {} : { loginUrl },
3914
5708
  ...session === "" ? {} : { session },
3915
- ...open === void 0 ? {} : { open }
3916
- });
3917
- return plainJson({
3918
- workshop: packagesDir,
3919
- ...out,
3920
- text: renderProbe(out)
5709
+ ...open === void 0 ? {} : { open },
5710
+ ...limit === void 0 ? {} : { limit },
5711
+ ...textMax === void 0 ? {} : { textMax }
3921
5712
  });
3922
- }
3923
- const unit = asText$1(args.unit);
3924
- if (unit === "") throw new Error(`install 要一个 unit(工坊里:${modules.map((one) => one.unit).join(", ") || "(空)"})`);
3925
- const found = modules.find((one) => one.unit === unit);
3926
- if (found === void 0) throw new Error(`工坊里没有单元 id 为「${unit}」的模块(可用的:${modules.map((one) => one.unit).join(", ") || "(空)"})—— 先装上的前提是它在工坊里造好了`);
3927
- if (!insideWorkshop(packagesDir, found.dir)) throw new Error(`这个模块不在工坊里(${found.dir})—— 只装 ${packagesDir} 之下的模块`);
3928
- if (action === "open") {
3929
- if (!found.installed) throw new Error(`${unit} 还没装 —— open 只负责上板。先用 action=install 装上(第一次会让用户点头一次)`);
3930
- const panel = showOrReason(panels, unit, workspace);
3931
- const text = panel.ok ? `面板上已经打开它了(${workspace})—— 让用户看一眼` : `面板上还没打开:${panel.reason}。这就是它的下一步(status 会给出同样的判断)`;
3932
5713
  return plainJson({
3933
5714
  workshop: packagesDir,
3934
- unit,
3935
- path: found.dir,
3936
- panel,
3937
- text
5715
+ ...found,
5716
+ text: renderFind(found)
3938
5717
  });
3939
5718
  }
5719
+ const out = await browserRead({
5720
+ shell,
5721
+ script,
5722
+ command: opencliCommandOf(ctx).command,
5723
+ ...url === "" ? {} : { url },
5724
+ ...loginUrl === "" ? {} : { loginUrl },
5725
+ ...session === "" ? {} : { session },
5726
+ ...open === void 0 ? {} : { open }
5727
+ });
5728
+ return plainJson({
5729
+ workshop: packagesDir,
5730
+ ...out,
5731
+ text: renderProbe(out)
5732
+ });
5733
+ }
5734
+ };
5735
+ function unitOf(packagesDir, raw, label) {
5736
+ const { modules } = modulesOf(ctx, packagesDir);
5737
+ const unit = asText$1(asRecord$3(raw)["unit"]);
5738
+ if (unit === "") throw new Error(`${label} 要一个 unit(工坊里:${modules.map((one) => one.unit).join(", ") || "(空)"})`);
5739
+ const found = modules.find((one) => one.unit === unit);
5740
+ if (found === void 0) throw new Error(`工坊里没有单元 id 为「${unit}」的模块(可用的:${modules.map((one) => one.unit).join(", ") || "(空)"})—— 先装上的前提是它在工坊里造好了`);
5741
+ if (!insideWorkshop(packagesDir, found.dir)) throw new Error(`这个模块不在工坊里(${found.dir})—— 只装 ${packagesDir} 之下的模块`);
5742
+ return found;
5743
+ }
5744
+ const showTool = {
5745
+ name: DIY_SHOW_TOOL,
5746
+ description: "只上板(不装):把一个已经装好的工坊模块显示在当前项目的面板上。",
5747
+ parameters: withApprovalNote({
5748
+ type: "object",
5749
+ properties: { unit: {
5750
+ type: "string",
5751
+ description: "单元 id(workbench_diy_list 会给)。"
5752
+ } },
5753
+ required: ["unit"],
5754
+ additionalProperties: false
5755
+ }, { required: true }),
5756
+ output: OUTPUT$2,
5757
+ async execute(raw, exec) {
5758
+ const { packagesDir, workspace } = workshopOf(ctx, exec);
5759
+ const found = unitOf(packagesDir, raw, "workbench_diy_show");
5760
+ if (!found.installed) throw new Error(`${found.unit} 还没装 —— 只上板的前提是它装过了。先用 workbench_diy_install(第一次会让用户点头一次)`);
5761
+ const panel = showOrReason(panels, found.unit, workspace);
5762
+ const text = panel.ok ? `面板上已经打开它了(${workspace})—— 让用户看一眼` : `面板上还没打开:${panel.reason}。这就是它的下一步(workbench_diy_status 会给出同样的判断)`;
5763
+ return plainJson({
5764
+ workshop: packagesDir,
5765
+ unit: found.unit,
5766
+ path: found.dir,
5767
+ panel,
5768
+ text
5769
+ });
5770
+ }
5771
+ };
5772
+ const installTool = {
5773
+ name: DIY_INSTALL_TOOL,
5774
+ description: "把工坊里造好的模块装上并上到当前项目的面板。这是持久变更,一个模块只装一次:之后改源码只需 dsh-wb-module build(宿主侧秒级热换)。",
5775
+ parameters: withApprovalNote({
5776
+ type: "object",
5777
+ properties: { unit: {
5778
+ type: "string",
5779
+ description: "单元 id(workbench_diy_list 会给)。"
5780
+ } },
5781
+ required: ["unit"],
5782
+ additionalProperties: false
5783
+ }, { required: true }),
5784
+ output: OUTPUT$2,
5785
+ async execute(raw, exec) {
5786
+ const { packagesDir, workspace } = workshopOf(ctx, exec);
5787
+ const found = unitOf(packagesDir, raw, "workbench_diy_install");
3940
5788
  if (found.installed) {
3941
- const panel = showOrReason(panels, unit, workspace);
3942
- const text = [`它已经装过了(就是工坊里这一份)—— **不要再装**:改源码 + dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面)。`, panel.ok ? `面板上已经打开它了(${workspace})—— 让用户看一眼` : `面板上还没打开:${panel.reason}。调 workbench_panels(action=show, id=${unit})`].join("\n");
5789
+ const panel = showOrReason(panels, found.unit, workspace);
5790
+ 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");
3943
5791
  return plainJson({
3944
5792
  workshop: packagesDir,
3945
- unit,
5793
+ unit: found.unit,
3946
5794
  path: found.dir,
3947
5795
  alreadyInstalled: true,
3948
5796
  panel,
@@ -3952,19 +5800,19 @@ function registerDiyTools(ctx, market, panels) {
3952
5800
  const outcome = await market.installBundle(found.dir);
3953
5801
  const verdict = verdictOf$1(outcome.change);
3954
5802
  const restartRequired = outcome.change.application === "restart-required";
3955
- const panel = outcome.change.application === "applied" ? showOrReason(panels, unit, workspace) : {
5803
+ const panel = outcome.change.application === "applied" ? showOrReason(panels, found.unit, workspace) : {
3956
5804
  ok: false,
3957
5805
  reason: `安装结果是 ${outcome.change.application},还没生效`
3958
5806
  };
3959
- const panelText = panel.ok ? `面板上已经打开它了(${workspace})—— 让用户看一眼` : outcome.change.application === "applied" ? `面板上还没打开:${panel.reason}。调 workbench_panels(action=show, id=${unit})试试。` : restartRequired ? `面板上还没打开(${panel.reason}):模块要**重启一次工作台**才注册进面板 —— 把这句话转给用户,重启后再调 workbench_panels(action=show, id=${unit})。` : `面板上还没打开:${panel.reason}。先把上面那句"没装成"处理掉,再谈上板。`;
5807
+ 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}。先把上面那句"没装成"处理掉,再谈上板。`;
3960
5808
  const text = [
3961
- `${verdict}:${unit}(${found.name})`,
5809
+ `${verdict}:${found.unit}(${found.name})`,
3962
5810
  panelText,
3963
- "之后只改源码 + dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面):不要再装一遍。"
5811
+ "之后只改源码 + dsh-wb-module build(宿主侧与 client.js 都秒级热换 —— dsh-client-hmr 盯着每个 bundle 的 lib/client.js,重建了就换进已打开的页面):不要再装一遍。"
3964
5812
  ].join("\n");
3965
5813
  return plainJson({
3966
5814
  workshop: packagesDir,
3967
- unit,
5815
+ unit: found.unit,
3968
5816
  path: found.dir,
3969
5817
  install: outcome.change,
3970
5818
  module: outcome.module,
@@ -3973,7 +5821,19 @@ function registerDiyTools(ctx, market, panels) {
3973
5821
  });
3974
5822
  }
3975
5823
  };
3976
- return [tools.register(tool)];
5824
+ return [
5825
+ tools.register(listTool),
5826
+ tools.register(statusTool),
5827
+ tools.register(panelTool),
5828
+ tools.register(reconTool),
5829
+ tools.register(probeTool),
5830
+ ...registerLookTools(ctx, tools),
5831
+ ...registerLogTools(ctx, tools),
5832
+ ...registerNetTools(ctx, tools),
5833
+ ...registerActTools(ctx, tools),
5834
+ tools.register(showTool),
5835
+ tools.register(installTool)
5836
+ ];
3977
5837
  }
3978
5838
  //#endregion
3979
5839
  //#region src/shared/api/host/release.ts
@@ -5136,7 +6996,7 @@ function createEnvDropper(options) {
5136
6996
  async function rememberInstalledSource(deps, manager, pkg, registry) {
5137
6997
  const { registries, logger } = deps;
5138
6998
  if (registries === void 0 || registry === void 0) return;
5139
- const scope = scopeOf(pkg);
6999
+ const scope = scopeOf$1(pkg);
5140
7000
  if (scope === null) return;
5141
7001
  const known = await manager.registries().catch(() => null);
5142
7002
  if (known !== null && sameRegistry(known.resolved, registry)) return;
@@ -5694,27 +7554,41 @@ function failureText(change) {
5694
7554
  function describeInstall(spec, module, supply, change) {
5695
7555
  return `${change.application === "applied" ? "装好了,已生效" : change.application === "restart-required" ? "装好了,要重启一次才生效" : `没成功:${failureText(change)}`}:${module.kind === "module" ? `${module.unit}(${module.pkg})` : module.pkg}${supply.state === "installed" ? ";它要的 opencli 插件也装上了" : supply.state === "pending-restart" ? ";重启后再装它要的 opencli 插件" : supply.state === "not-a-module" ? ";但它不是工作台模块(没有 dsh.workbench 声明,不会出现在面板上)" : supply.state === "failed" ? `;但它要的插件没装上:${supply.reason}` : ""}(spec:${spec})`;
5696
7556
  }
5697
- var OUTPUT_SCHEMA$1 = {
5698
- type: "object",
5699
- properties: {
5700
- text: { type: "string" },
5701
- modules: {
5702
- type: "array",
5703
- items: {
5704
- type: "object",
5705
- additionalProperties: true
7557
+ var OUTPUT$1 = {
7558
+ schema: {
7559
+ type: "object",
7560
+ properties: {
7561
+ text: { type: "string" },
7562
+ modules: {
7563
+ type: "array",
7564
+ items: {
7565
+ type: "object",
7566
+ additionalProperties: true
7567
+ }
7568
+ },
7569
+ shared: {
7570
+ type: "array",
7571
+ items: {
7572
+ type: "object",
7573
+ additionalProperties: true
7574
+ }
5706
7575
  }
5707
7576
  },
5708
- shared: {
5709
- type: "array",
5710
- items: {
5711
- type: "object",
5712
- additionalProperties: true
5713
- }
5714
- }
7577
+ additionalProperties: true
5715
7578
  },
5716
- additionalProperties: true
7579
+ render: (_args, value) => {
7580
+ const payload = value;
7581
+ return [{
7582
+ type: "text",
7583
+ text: typeof payload?.text === "string" ? payload.text : JSON.stringify(value)
7584
+ }];
7585
+ }
5717
7586
  };
7587
+ function textArg(raw, key) {
7588
+ if (typeof raw !== "object" || raw === null) return "";
7589
+ const value = raw[key];
7590
+ return typeof value === "string" ? value.trim() : "";
7591
+ }
5718
7592
  function describePluginUninstall(pkg, outcome) {
5719
7593
  if (outcome.removed) return `已卸载 opencli 插件 ${pkg}:没人再用它,文件也删了。`;
5720
7594
  return `已从「你装的插件」里去掉 ${pkg}。${outcome.kept === void 0 ? "" : pluginKeptText(outcome.kept)}`;
@@ -5728,98 +7602,167 @@ function usersOf(shared, names) {
5728
7602
  if (hit === void 0 || hit.usedBy.length === 0) return "";
5729
7603
  return `它被 ${hit.usedBy.join("、")} 用着(动的是它们的供给,它们的状态跟着重探了一遍)`;
5730
7604
  }
5731
- var ACTIONS$1 = [
5732
- "list",
5733
- "install",
5734
- "update",
5735
- "uninstall",
5736
- "install-bundle",
5737
- "update-dependency",
5738
- "update-plugin",
5739
- "uninstall-plugin",
5740
- "update-workbench"
5741
- ];
5742
7605
  function registerMarketTools(ctx, market) {
5743
7606
  const tools = ctx.get("tools");
5744
7607
  if (tools === void 0) {
5745
7608
  ctx.logger?.info?.("[dsh-workbench] 没有 tools 服务,跳过模块市场工具注册");
5746
7609
  return [];
5747
7610
  }
5748
- const tool = {
5749
- name: "workbench_modules",
5750
- description: "看 / 装 / 升 / 卸工作台的面板模块,以及用户自己装的 opencli 插件。install-bundle 按 spec 把模块包装进 profile;按模块 id 的 install / update 只装、升它声明的那份外部供给(opencli 插件与库),不换模块包本身;uninstall 连模块包一起卸。update-workbench 把工作台自己更新到源上的新版(一般自己换上,要重启时结果里会说;重启由用户来)。卸模块只动工作台运行时,不影响终端那套 dsh;共用依赖只有升级。",
7611
+ const noParams = {
7612
+ type: "object",
7613
+ properties: {},
7614
+ additionalProperties: false
7615
+ };
7616
+ async function idOrList(raw, label) {
7617
+ const id = textArg(raw, "id");
7618
+ if (id !== "") return id;
7619
+ const { modules, shared } = await market.list();
7620
+ throw new Error(`${label} 需要一个 id(例如 coding-issues)。可安装的模块:${modules.map((entry) => entry.unit).join(", ") || "(空)"};共用依赖:${shared.map((dep) => dep.id).join(", ") || "(空)"}`);
7621
+ }
7622
+ function moduleSupplyTool(spec) {
7623
+ return {
7624
+ name: spec.name,
7625
+ description: spec.description,
7626
+ parameters: withApprovalNote({
7627
+ type: "object",
7628
+ properties: { id: {
7629
+ type: "string",
7630
+ description: "模块 id(如 coding-issues)。"
7631
+ } },
7632
+ required: ["id"],
7633
+ additionalProperties: false
7634
+ }, { required: true }),
7635
+ output: OUTPUT$1,
7636
+ async execute(raw) {
7637
+ const id = await idOrList(raw, spec.name);
7638
+ const entry = spec.install ? await market.install(id) : await market.update(id);
7639
+ return {
7640
+ module: entry,
7641
+ text: `${spec.install ? "已安装" : "已更新"} ${id}:${stateOf(entry)}`
7642
+ };
7643
+ }
7644
+ };
7645
+ }
7646
+ const listTool = {
7647
+ name: MODULES_LIST_TOOL,
7648
+ description: "看模块市场与已装:每个模块装没装、可用不可用、要哪些 opencli 插件;还有共用依赖(只有升级)。",
7649
+ parameters: noParams,
7650
+ output: OUTPUT$1,
7651
+ async execute() {
7652
+ const { modules, shared } = await market.list();
7653
+ return {
7654
+ modules,
7655
+ shared,
7656
+ text: render$1(modules, shared)
7657
+ };
7658
+ }
7659
+ };
7660
+ const installTool = moduleSupplyTool({
7661
+ name: MODULES_INSTALL_TOOL,
7662
+ description: "按模块 id 装它声明的那份外部供给(opencli 插件与库),不换模块包本身。",
7663
+ install: true
7664
+ });
7665
+ const updateTool = moduleSupplyTool({
7666
+ name: MODULES_UPDATE_TOOL,
7667
+ description: "按模块 id 升它声明的那份外部供给(opencli 插件与库),不换模块包本身。",
7668
+ install: false
7669
+ });
7670
+ const uninstallTool = {
7671
+ name: MODULES_UNINSTALL_TOOL,
7672
+ description: "卸模块:连模块包一起卸(只动工作台运行时);要换一份实现先卸再装。",
7673
+ parameters: withApprovalNote({
7674
+ type: "object",
7675
+ properties: { id: {
7676
+ type: "string",
7677
+ description: "模块 id(如 coding-issues)。"
7678
+ } },
7679
+ required: ["id"],
7680
+ additionalProperties: false
7681
+ }, { required: true }),
7682
+ output: OUTPUT$1,
7683
+ async execute(raw) {
7684
+ const id = await idOrList(raw, MODULES_UNINSTALL_TOOL);
7685
+ const outcome = await market.uninstall(id);
7686
+ const kept = outcome.keptFor.length === 0 ? "" : `(还有模块在面板上要用其中一份安装,所以那份留着:${outcome.keptFor.join("、")})`;
7687
+ return {
7688
+ module: outcome.entry,
7689
+ text: `已卸载 ${id}:${stateOf(outcome.entry)}${kept}`
7690
+ };
7691
+ }
7692
+ };
7693
+ const installBundleTool = {
7694
+ name: MODULES_INSTALL_BUNDLE_TOOL,
7695
+ description: "按 spec 把模块包装进这份 profile(包名 / git 地址 / tarball / 本地路径);装完它要的插件也一起装。",
5751
7696
  parameters: withApprovalNote({
5752
7697
  type: "object",
5753
7698
  properties: {
5754
- action: {
5755
- type: "string",
5756
- description: "list=看状态(默认);install / update / uninstall 要 id;install-bundle 要 spec(换源给 registry);update-dependency 要 id;update-plugin / uninstall-plugin 要 plugin。",
5757
- enum: [...ACTIONS$1]
5758
- },
5759
- id: {
5760
- type: "string",
5761
- description: "模块 id(如 coding-issues);update-dependency 用共用依赖 id(如 plugin:coding)。"
5762
- },
5763
7699
  spec: {
5764
7700
  type: "string",
5765
- description: "包名(可带 @版本),也可以是 git 地址 / tarball / 本地绝对路径。"
7701
+ description: "包名(可带 @版本)/ git 地址 / tarball / 本地路径。"
5766
7702
  },
5767
7703
  registry: {
5768
7704
  type: "string",
5769
- description: "去哪个源装(不填就用这份 profile 自己配的源)。"
5770
- },
5771
- plugin: {
5772
- type: "string",
5773
- description: "opencli 插件的包名或插件名(如 cmdb 或 opencli-plugin-cmdb)。"
7705
+ description: "去哪个源装(不填用 profile 自己配的源)。"
5774
7706
  }
5775
7707
  },
5776
- required: ["action"],
7708
+ required: ["spec"],
5777
7709
  additionalProperties: false
5778
7710
  }, { required: true }),
5779
- output: {
5780
- schema: OUTPUT_SCHEMA$1,
5781
- render: (_args, value) => {
5782
- const payload = value;
5783
- return [{
5784
- type: "text",
5785
- text: typeof payload?.text === "string" ? payload.text : JSON.stringify(value)
5786
- }];
5787
- }
5788
- },
5789
- async execute(args) {
5790
- const action = args?.action ?? "list";
5791
- if (!ACTIONS$1.includes(action)) throw new Error(`不认识的 action:${action}(可用:${ACTIONS$1.join(" / ")})`);
5792
- if (action === "list") {
5793
- const { modules, shared } = await market.list();
5794
- return {
5795
- modules,
5796
- shared,
5797
- text: render$1(modules, shared)
5798
- };
5799
- }
5800
- if (action === "install-bundle") {
5801
- const spec = args?.spec?.trim() ?? "";
5802
- if (spec === "") throw new Error("install-bundle 需要一个 spec:包名(可带版本,如 @scope/module@1.2.3),也可以是 git 地址 / tarball / 本地绝对路径。");
5803
- const registry = args?.registry?.trim();
5804
- const outcome = await market.installBundle(spec, registry === void 0 || registry === "" ? {} : { registry });
5805
- return {
5806
- change: outcome.change,
5807
- module: outcome.module,
5808
- supply: outcome.supply,
5809
- text: describeInstall(spec, outcome.module, outcome.supply, outcome.change)
5810
- };
5811
- }
5812
- if (action === "update-workbench") {
5813
- const outcome = await market.updateWorkbench("");
5814
- return {
5815
- change: outcome.change,
5816
- text: describeWorkbenchUpdate(outcome.change)
5817
- };
5818
- }
5819
- if (action === "update-plugin" || action === "uninstall-plugin") {
5820
- const plugin = args?.plugin?.trim() ?? "";
5821
- if (plugin === "") throw new Error(`${action} 需要一个 plugin:opencli 插件的包名或插件名 —— 就是市场「已安装 opencli 插件」那一段里的名字。`);
5822
- if (action === "update-plugin") {
7711
+ output: OUTPUT$1,
7712
+ async execute(raw) {
7713
+ const spec = textArg(raw, "spec");
7714
+ if (spec === "") throw new Error("install-bundle 需要一个 spec:包名(可带版本,如 @scope/module@1.2.3),也可以是 git 地址 / tarball / 本地绝对路径。");
7715
+ const registry = textArg(raw, "registry");
7716
+ const outcome = await market.installBundle(spec, registry === "" ? {} : { registry });
7717
+ return {
7718
+ change: outcome.change,
7719
+ module: outcome.module,
7720
+ supply: outcome.supply,
7721
+ text: describeInstall(spec, outcome.module, outcome.supply, outcome.change)
7722
+ };
7723
+ }
7724
+ };
7725
+ const updateDependencyTool = {
7726
+ name: MODULES_UPDATE_DEPENDENCY_TOOL,
7727
+ description: "升共用依赖(工作台自己那份营生,如 plugin:coding):只有升级,用依赖 id。",
7728
+ parameters: withApprovalNote({
7729
+ type: "object",
7730
+ properties: { id: {
7731
+ type: "string",
7732
+ description: "共用依赖 id(如 plugin:coding)。"
7733
+ } },
7734
+ required: ["id"],
7735
+ additionalProperties: false
7736
+ }, { required: true }),
7737
+ output: OUTPUT$1,
7738
+ async execute(raw) {
7739
+ const id = await idOrList(raw, MODULES_UPDATE_DEPENDENCY_TOOL);
7740
+ const shared = await market.updateDependency(id);
7741
+ const dep = shared.find((item) => item.id === id);
7742
+ return {
7743
+ shared,
7744
+ text: `已更新共用依赖 ${id}${dep === void 0 ? "" : `:${stateOfDependency(dep)}`}`
7745
+ };
7746
+ }
7747
+ };
7748
+ function pluginTool(spec) {
7749
+ return {
7750
+ name: spec.name,
7751
+ description: spec.description,
7752
+ parameters: withApprovalNote({
7753
+ type: "object",
7754
+ properties: { plugin: {
7755
+ type: "string",
7756
+ description: "opencli 插件的包名或插件名(如 cmdb)。"
7757
+ } },
7758
+ required: ["plugin"],
7759
+ additionalProperties: false
7760
+ }, { required: true }),
7761
+ output: OUTPUT$1,
7762
+ async execute(raw) {
7763
+ const plugin = textArg(raw, "plugin");
7764
+ if (plugin === "") throw new Error(`${spec.name} 需要一个 plugin:opencli 插件的包名或插件名 —— 就是市场「已安装 opencli 插件」那一段里的名字。`);
7765
+ if (spec.update) {
5823
7766
  const updated = await market.updatePlugin(plugin);
5824
7767
  const users = usersOf((await market.list()).shared, [updated.name, plugin]);
5825
7768
  return { text: `已更新 opencli 插件 ${plugin}${users === "" ? "。" : `;${users}。`}` };
@@ -5833,35 +7776,42 @@ function registerMarketTools(ctx, market) {
5833
7776
  text: `${describePluginUninstall(plugin, outcome)}${users === "" ? "" : `${users}。`}`
5834
7777
  };
5835
7778
  }
5836
- const id = args?.id;
5837
- if (id === void 0 || id.trim() === "") {
5838
- const { modules, shared } = await market.list();
5839
- throw new Error(`${action} 需要一个 id(例如 coding-issues)。可安装的模块:${modules.map((entry) => entry.unit).join(", ") || "(空)"};共用依赖:${shared.map((dep) => dep.id).join(", ") || "(空)"}`);
5840
- }
5841
- if (action === "update-dependency") {
5842
- const shared = await market.updateDependency(id);
5843
- const dep = shared.find((item) => item.id === id);
5844
- return {
5845
- shared,
5846
- text: `已更新共用依赖 ${id}${dep === void 0 ? "" : `:${stateOfDependency(dep)}`}`
5847
- };
5848
- }
5849
- const outcome = action === "install" ? {
5850
- entry: await market.install(id),
5851
- keptFor: []
5852
- } : action === "update" ? {
5853
- entry: await market.update(id),
5854
- keptFor: []
5855
- } : await market.uninstall(id);
5856
- const verb = action === "uninstall" ? "已卸载" : action === "update" ? "已更新" : "已安装";
5857
- const kept = outcome.keptFor.length === 0 ? "" : `(还有模块在面板上要用其中一份安装,所以那份留着:${outcome.keptFor.join("、")})`;
7779
+ };
7780
+ }
7781
+ const updatePluginTool = pluginTool({
7782
+ name: MODULES_UPDATE_PLUGIN_TOOL,
7783
+ description: "更新用户装的 opencli 插件(按包名或插件名);老插件跑不动了先试它。",
7784
+ update: true
7785
+ });
7786
+ const uninstallPluginTool = pluginTool({
7787
+ name: MODULES_UNINSTALL_PLUGIN_TOOL,
7788
+ description: "卸用户装的 opencli 插件:没人再用它时文件也删;还有模块用着就只从名单去掉。",
7789
+ update: false
7790
+ });
7791
+ const updateWorkbenchTool = {
7792
+ name: MODULES_UPDATE_WORKBENCH_TOOL,
7793
+ description: "把工作台自己更新到源上的新版(要重启时结果里会说;重启由用户来)。",
7794
+ parameters: withApprovalNote(noParams, { required: true }),
7795
+ output: OUTPUT$1,
7796
+ async execute() {
7797
+ const outcome = await market.updateWorkbench("");
5858
7798
  return {
5859
- module: outcome.entry,
5860
- text: `${verb} ${id}:${stateOf(outcome.entry)}${kept}`
7799
+ change: outcome.change,
7800
+ text: describeWorkbenchUpdate(outcome.change)
5861
7801
  };
5862
7802
  }
5863
7803
  };
5864
- return [tools.register(tool)];
7804
+ return [
7805
+ tools.register(listTool),
7806
+ tools.register(installTool),
7807
+ tools.register(updateTool),
7808
+ tools.register(uninstallTool),
7809
+ tools.register(installBundleTool),
7810
+ tools.register(updateDependencyTool),
7811
+ tools.register(updatePluginTool),
7812
+ tools.register(uninstallPluginTool),
7813
+ tools.register(updateWorkbenchTool)
7814
+ ];
5865
7815
  }
5866
7816
  //#endregion
5867
7817
  //#region src/modules/panels/model/prefs.ts
@@ -5920,23 +7870,26 @@ function applyPanelsPref(panels, payload) {
5920
7870
  //#region src/modules/panels/model/handoff.ts
5921
7871
  var PANELS_FACT = "panels";
5922
7872
  var CAPABILITY_FACT = "capability";
7873
+ var PROVISION_TOOLS = {
7874
+ [MODULES_INSTALL_TOOL]: "install",
7875
+ [MODULES_UPDATE_TOOL]: "update",
7876
+ [MODULES_UNINSTALL_TOOL]: "uninstall",
7877
+ [MODULES_UPDATE_DEPENDENCY_TOOL]: "update-dependency",
7878
+ [DIY_INSTALL_TOOL]: "install"
7879
+ };
5923
7880
  function textOf$2(value) {
5924
7881
  return typeof value === "string" ? value.trim() : "";
5925
7882
  }
5926
- function actionOf(call) {
5927
- return textOf$2(call.args["action"]) || "list";
5928
- }
5929
7883
  function requestOf(call) {
5930
- const action = actionOf(call);
5931
- if (!PROVISION_ACTIONS.includes(action)) return void 0;
7884
+ const action = PROVISION_TOOLS[call.source];
7885
+ if (action === void 0) return void 0;
5932
7886
  const target = textOf$2(call.args["unit"]) || textOf$2(call.args["id"]);
5933
7887
  if (target === "") return void 0;
5934
- const wanted = action;
5935
- return isDependencyAction(wanted) ? {
5936
- action: wanted,
7888
+ return isDependencyAction(action) ? {
7889
+ action,
5937
7890
  id: target
5938
7891
  } : {
5939
- action: wanted,
7892
+ action,
5940
7893
  unit: target
5941
7894
  };
5942
7895
  }
@@ -5948,11 +7901,10 @@ function panelsHandoffEntry(target = {}) {
5948
7901
  };
5949
7902
  }
5950
7903
  function panelsFactsOf(call, manifests) {
5951
- if (call.source === "workbench_panels") return actionOf(call) === "list" ? [PANELS_FACT] : void 0;
5952
- if (call.source === "workbench_diy" && actionOf(call) === "list") return [PANELS_FACT];
5953
- if (call.source !== "workbench_modules" && call.source !== "workbench_diy") return void 0;
5954
- if (actionOf(call) === "list") return [PANELS_FACT, CAPABILITY_FACT];
5955
- if (actionOf(call) === "update-workbench") return [PANELS_FACT, CAPABILITY_FACT];
7904
+ if (call.source === "workbench_panels_list") return [PANELS_FACT];
7905
+ if (call.source === "workbench_modules_list") return [PANELS_FACT, CAPABILITY_FACT];
7906
+ if (call.source === "workbench_modules_update_workbench") return [PANELS_FACT, CAPABILITY_FACT];
7907
+ if (call.source === "workbench_diy_list") return [PANELS_FACT];
5956
7908
  const request = requestOf(call);
5957
7909
  if (request === void 0) return void 0;
5958
7910
  return [
@@ -6280,16 +8232,28 @@ function pluginRoutes(options) {
6280
8232
  //#region src/modules/panels/model/tools.ts
6281
8233
  var MACHINE_LABEL = "机器级默认板 —— 还没配过的工作区默认开这些:";
6282
8234
  var MACHINE_CAVEAT = "机器级默认板只是还没配过的工作区的默认值:已经自己配过的工作区保持自己的选择,不会被它改。";
8235
+ var SCOPE_PARAM = {
8236
+ type: "string",
8237
+ description: "workspace(默认)=只动当前工作区;machine=动机器级默认板(只影响还没配过的工作区)。",
8238
+ enum: ["workspace", "machine"]
8239
+ };
8240
+ var ID_PARAM = {
8241
+ type: "string",
8242
+ description: "套件 id(如 cmdb)。"
8243
+ };
8244
+ function noted(parameters) {
8245
+ return withApprovalNote(parameters, { required: true });
8246
+ }
6283
8247
  function listed(entries) {
6284
8248
  return entries.map((entry, index) => `${index + 1}. ${entry.id} · ${entry.shown ? "已启用" : "未启用"}${entry.hasCard ? "" : "(无卡片)"}${entry.source === "user" ? " · 用户改过" : ""}`).join("\n");
6285
8249
  }
6286
8250
  function render(entries, workspace, scope) {
6287
8251
  if (scope === "machine") {
6288
- if (entries.length === 0) return "机器级默认板是空的(还没配过的工作区默认什么卡片都不开)——用 show 放一个上去。";
8252
+ if (entries.length === 0) return "机器级默认板是空的(还没配过的工作区默认什么卡片都不开)——用 workbench_panels_show 放一个上去。";
6289
8253
  return `${MACHINE_LABEL}\n${listed(entries)}`;
6290
8254
  }
6291
8255
  const where = workspace === null ? "(不知道当前会话在哪个工作区)" : workspace;
6292
- if (entries.length === 0) return `这个工作区的面板是空的(${where})——用 show 放一个上去。`;
8256
+ if (entries.length === 0) return `这个工作区的面板是空的(${where})——用 workbench_panels_show 放一个上去。`;
6293
8257
  return `当前工作区:${where}\n${listed(entries)}`;
6294
8258
  }
6295
8259
  function parseScope(raw) {
@@ -6297,141 +8261,205 @@ function parseScope(raw) {
6297
8261
  if (typeof raw === "string" && BOARD_TARGETS.includes(raw)) return raw;
6298
8262
  throw new Error(`不认识的 scope:${String(raw)}(可用:workspace / machine)`);
6299
8263
  }
6300
- var OUTPUT_SCHEMA = {
6301
- type: "object",
6302
- properties: {
6303
- text: { type: "string" },
6304
- units: {
6305
- type: "array",
6306
- items: {
6307
- type: "object",
6308
- properties: {
6309
- id: { type: "string" },
6310
- shown: { type: "boolean" },
6311
- order: { type: "integer" },
6312
- hasCard: { type: "boolean" },
6313
- source: { type: "string" }
6314
- },
6315
- additionalProperties: true
8264
+ var OUTPUT = {
8265
+ schema: {
8266
+ type: "object",
8267
+ properties: {
8268
+ text: { type: "string" },
8269
+ units: {
8270
+ type: "array",
8271
+ items: {
8272
+ type: "object",
8273
+ properties: {
8274
+ id: { type: "string" },
8275
+ shown: { type: "boolean" },
8276
+ order: { type: "integer" },
8277
+ hasCard: { type: "boolean" },
8278
+ source: { type: "string" }
8279
+ },
8280
+ additionalProperties: true
8281
+ }
6316
8282
  }
6317
- }
8283
+ },
8284
+ additionalProperties: true
6318
8285
  },
6319
- additionalProperties: true
8286
+ render: (_args, value) => {
8287
+ const payload = value;
8288
+ return [{
8289
+ type: "text",
8290
+ text: typeof payload?.text === "string" ? payload.text : JSON.stringify(value)
8291
+ }];
8292
+ }
6320
8293
  };
8294
+ function idOf(raw) {
8295
+ if (typeof raw !== "object" || raw === null) return void 0;
8296
+ const value = raw["id"];
8297
+ return typeof value === "string" ? value : void 0;
8298
+ }
8299
+ function scopeOf(raw) {
8300
+ if (typeof raw !== "object" || raw === null) return parseScope(void 0);
8301
+ return parseScope(raw["scope"]);
8302
+ }
6321
8303
  function registerPanelTools(ctx, panels, values) {
6322
8304
  const tools = ctx.get("tools");
6323
8305
  if (tools === void 0) {
6324
8306
  ctx.logger?.info?.("[dsh-workbench] 没有 tools 服务,跳过户表面板工具注册");
6325
8307
  return [];
6326
8308
  }
6327
- const tool = {
6328
- name: "workbench_panels",
6329
- description: "看或调右侧面板:哪些套件开着、什么顺序。两份板:本工作区那份优先,没提过的行看机器级默认板 —— 那只是还没配过的工作区的默认值(已经自己配过的工作区保持自己的选择);本工作区只记改过的那几行,所以那份板以后改了,没改过的工作区跟着变。收起 = 这个工作区明确不要它:卡片与 agent 工具一起在这个工作区下线,连机器级默认板上开着的套件也照样收起;别的项目不受影响、不删数据,要放回来用 show。在机器级默认板上 hide 是写下\"不要它\"这一笔 —— 比名单更管用,show 会把这一笔划掉。顺序只属于工作区:没排过的按各模块自己声明的次序(开着的在前、关着的在后),show 或 move 过就以那次为准。",
6330
- parameters: withApprovalNote({
8309
+ const listTool = {
8310
+ name: PANELS_LIST_TOOL,
8311
+ description: "看现在这份板:哪些套件开着、什么顺序。两份板:本工作区那份优先,没提过的行看机器级默认板 ——那只是还没配过的工作区的默认值(自己配过的保持自己的选择)。",
8312
+ parameters: {
8313
+ type: "object",
8314
+ properties: { scope: SCOPE_PARAM },
8315
+ additionalProperties: false
8316
+ },
8317
+ output: OUTPUT,
8318
+ async execute(raw, exec) {
8319
+ const scope = scopeOf(raw);
8320
+ const workspace = sessionWorkspace(exec);
8321
+ const units = panels.list(workspace, scope);
8322
+ if (scope === "workspace") {
8323
+ const entry = panelsHandoffEntry(workspace === null ? {} : { path: workspace });
8324
+ if (entry !== void 0) values?.hand(entry, units);
8325
+ }
8326
+ return {
8327
+ units,
8328
+ text: render(units, workspace, scope)
8329
+ };
8330
+ }
8331
+ };
8332
+ const showTool = {
8333
+ name: PANELS_SHOW_TOOL,
8334
+ description: "这个工作区要它:之前收起的也放回来,卡片与 agent 工具一起上线;那份板上写过\"不要\"的也划掉。",
8335
+ parameters: noted({
6331
8336
  type: "object",
6332
8337
  properties: {
6333
- action: {
6334
- type: "string",
6335
- description: "list=看现在这份板(默认);show=这个工作区要它(之前收起的也放回来);hide=这个工作区明确不要它;move=调顺序(新开一行时排最后);reset=清掉这个工作区改过的行(改完全部回到跟着机器级默认板走);follow=某一行放回跟随(带 id = 只那一行,不带 = 全部)。",
6336
- enum: [
6337
- "list",
6338
- "show",
6339
- "hide",
6340
- "move",
6341
- "reset",
6342
- "follow"
6343
- ]
6344
- },
6345
- id: {
6346
- type: "string",
6347
- description: "show / hide / move 必填:要动的套件 id。"
6348
- },
8338
+ id: ID_PARAM,
8339
+ scope: SCOPE_PARAM
8340
+ },
8341
+ required: ["id"],
8342
+ additionalProperties: false
8343
+ }),
8344
+ output: OUTPUT,
8345
+ async execute(raw, exec) {
8346
+ const id = idOf(raw);
8347
+ const scope = scopeOf(raw);
8348
+ const workspace = sessionWorkspace(exec);
8349
+ if (id === void 0 || id === "") throw new Error(`workbench_panels_show 需要一个 id(例如 coding-issues)。当前套件:${panels.list(workspace, scope).map((entry) => entry.id).join(", ")}`);
8350
+ const units = panels.show(id, workspace, scope);
8351
+ return {
8352
+ units,
8353
+ text: scope === "machine" ? `已把 ${id} 放进机器级默认板 —— 还没配过的工作区会带上它(卡片与工具一起上线);那份板上要是给它写过"不要",这一下也划掉了。${MACHINE_CAVEAT}\n${render(units, workspace, scope)}` : `已启用 ${id} —— 这个工作区要它:之前收起的也放回来了,卡片与工具一起上线。\n${render(units, workspace, scope)}`
8354
+ };
8355
+ }
8356
+ };
8357
+ const hideTool = {
8358
+ name: PANELS_HIDE_TOOL,
8359
+ description: "这个工作区明确不要它:卡片与 agent 工具一起下线(机器板上开着的也照样收起);别的项目不受影响,要放回来用 workbench_panels_show。在机器板上 hide 是写下\"不要\"这一笔,比名单管用。",
8360
+ parameters: noted({
8361
+ type: "object",
8362
+ properties: {
8363
+ id: ID_PARAM,
8364
+ scope: SCOPE_PARAM
8365
+ },
8366
+ required: ["id"],
8367
+ additionalProperties: false
8368
+ }),
8369
+ output: OUTPUT,
8370
+ async execute(raw, exec) {
8371
+ const id = idOf(raw);
8372
+ const scope = scopeOf(raw);
8373
+ const workspace = sessionWorkspace(exec);
8374
+ if (id === void 0 || id === "") throw new Error(`workbench_panels_hide 需要一个 id(例如 coding-issues)。当前套件:${panels.list(workspace, scope).map((entry) => entry.id).join(", ")}`);
8375
+ const units = panels.hide(id, workspace, scope);
8376
+ return {
8377
+ units,
8378
+ text: scope === "machine" ? `已把 ${id} 从机器级默认板划掉,并在那份板上明确写下"不要它" —— 还没配过的工作区从此看不到它(卡片与工具一起下线);名单里还列着它也一样不算数,要放回来用 workbench_panels_show。${MACHINE_CAVEAT}\n${render(units, workspace, scope)}` : `已收起 ${id} —— 这个工作区明确不要它:卡片与工具一起下线(默认在机器上也开着的套件同样收起),别的项目不受影响;要放回来用 workbench_panels_show。\n${render(units, workspace, scope)}`
8379
+ };
8380
+ }
8381
+ };
8382
+ const moveTool = {
8383
+ name: PANELS_MOVE_TOOL,
8384
+ description: "调顺序:放到最前面(新开一行时排最后)。顺序只属于工作区;机器级默认板上没有顺序一说。",
8385
+ parameters: noted({
8386
+ type: "object",
8387
+ properties: {
8388
+ id: ID_PARAM,
6349
8389
  index: {
6350
8390
  type: "integer",
6351
- description: "move 的位置,0 基(数的是当前开着的那几行)。"
8391
+ description: "位置,0 基(数的是当前开着的那几行)。"
6352
8392
  },
6353
- scope: {
6354
- type: "string",
6355
- description: "workspace(默认)=只动当前工作区;machine=动那份机器级默认板(只影响还没配过的工作区;没有顺序一说,move 会报错)。",
6356
- enum: ["workspace", "machine"]
6357
- }
8393
+ scope: SCOPE_PARAM
6358
8394
  },
6359
- required: ["action"],
8395
+ required: ["id"],
6360
8396
  additionalProperties: false
6361
- }, { required: true }),
6362
- output: {
6363
- schema: OUTPUT_SCHEMA,
6364
- render: (_args, value) => {
6365
- const payload = value;
6366
- return [{
6367
- type: "text",
6368
- text: typeof payload?.text === "string" ? payload.text : JSON.stringify(value)
6369
- }];
6370
- }
6371
- },
6372
- async execute(args, exec) {
6373
- const action = args?.action ?? "list";
6374
- const id = args?.id;
6375
- const scope = parseScope(args?.scope);
8397
+ }),
8398
+ output: OUTPUT,
8399
+ async execute(raw, exec) {
8400
+ const id = idOf(raw);
8401
+ const scope = scopeOf(raw);
6376
8402
  const workspace = sessionWorkspace(exec);
6377
- const list = () => panels.list(workspace, scope);
6378
- if (![
6379
- "list",
6380
- "show",
6381
- "hide",
6382
- "move",
6383
- "reset",
6384
- "follow"
6385
- ].includes(action)) throw new Error(`不认识的 action:${action}(可用:list / show / hide / move / reset / follow)`);
6386
- if (action === "list") {
6387
- const units = list();
6388
- if (scope === "workspace") {
6389
- const entry = panelsHandoffEntry(workspace === null ? {} : { path: workspace });
6390
- if (entry !== void 0) values?.hand(entry, units);
6391
- }
6392
- return {
6393
- units,
6394
- text: render(units, workspace, scope)
6395
- };
6396
- }
6397
- if (action === "follow") {
6398
- if (scope === "machine") throw new Error("机器级默认板没有\"放回跟随\"一说:它就是那份默认本身,直接 show / hide 写它。");
6399
- const units = panels.follow(workspace, id);
6400
- return {
6401
- units,
6402
- text: id === void 0 ? `这个工作区改过的行全部清掉 —— 每一行都回到跟着机器级默认板走(那份板以后改了,这里跟着变)。\n${render(units, workspace, scope)}` : `${id} 放回跟随 —— 它不再由这个工作区定,改跟机器级默认板走。\n${render(units, workspace, scope)}`
6403
- };
6404
- }
6405
- if (action === "reset") {
6406
- const units = panels.reset(workspace, scope);
6407
- return {
6408
- units,
6409
- text: scope === "machine" ? `机器级默认板已清空 —— 从今往后,没在这一行改过的工作区默认什么都不开。${MACHINE_CAVEAT}\n${render(units, workspace, scope)}` : `这个工作区改过的行已全部清掉 —— 每一行都回到跟着机器级默认板走。\n${render(units, workspace, scope)}`
6410
- };
6411
- }
6412
- if (id === void 0) throw new Error(`${action} 需要一个 id(例如 coding-issues)。当前套件:${list().map((entry) => entry.id).join(", ")}`);
6413
- if (action === "show") {
6414
- const units = panels.show(id, workspace, scope);
6415
- return {
6416
- units,
6417
- text: scope === "machine" ? `已把 ${id} 放进机器级默认板 —— 还没配过的工作区会带上它(卡片与工具一起上线);那份板上要是给它写过"不要",这一下也划掉了。${MACHINE_CAVEAT}\n${render(units, workspace, scope)}` : `已启用 ${id} —— 这个工作区要它:之前收起的也放回来了,卡片与工具一起上线。\n${render(units, workspace, scope)}`
6418
- };
6419
- }
6420
- if (action === "hide") {
6421
- const units = panels.hide(id, workspace, scope);
6422
- return {
6423
- units,
6424
- text: scope === "machine" ? `已把 ${id} 从机器级默认板划掉,并在那份板上明确写下"不要它" —— 还没配过的工作区从此看不到它(卡片与工具一起下线);名单里还列着它也一样不算数,要放回来用 show。${MACHINE_CAVEAT}\n${render(units, workspace, scope)}` : `已收起 ${id} —— 这个工作区明确不要它:卡片与工具一起下线(默认在机器上也开着的套件同样收起),别的项目不受影响;要放回来用 show。\n${render(units, workspace, scope)}`
6425
- };
6426
- }
6427
- const units = panels.move(id, args?.index ?? 0, workspace, scope);
8403
+ if (id === void 0 || id === "") throw new Error(`workbench_panels_move 需要一个 id(例如 coding-issues)。当前套件:${panels.list(workspace, scope).map((entry) => entry.id).join(", ")}`);
8404
+ const index = typeof raw === "object" && raw !== null && typeof raw.index === "number" ? raw.index : 0;
8405
+ const units = panels.move(id, index, workspace, scope);
8406
+ return {
8407
+ units,
8408
+ text: scope === "machine" ? `已把 ${id} 放到机器级默认板第 ${index + 1} 位 —— 只影响还没配过的工作区。${MACHINE_CAVEAT}\n${render(units, workspace, scope)}` : `已把 ${id} 放到第 ${index + 1} 位。\n${render(units, workspace, scope)}`
8409
+ };
8410
+ }
8411
+ };
8412
+ const resetTool = {
8413
+ name: PANELS_RESET_TOOL,
8414
+ description: "清掉这个工作区改过的行(改完全部回到跟着机器级默认板走);scope=machine 清的是整份机器级默认板。",
8415
+ parameters: noted({
8416
+ type: "object",
8417
+ properties: { scope: SCOPE_PARAM },
8418
+ additionalProperties: false
8419
+ }),
8420
+ output: OUTPUT,
8421
+ async execute(raw, exec) {
8422
+ const scope = scopeOf(raw);
8423
+ const workspace = sessionWorkspace(exec);
8424
+ const units = panels.reset(workspace, scope);
8425
+ return {
8426
+ units,
8427
+ text: scope === "machine" ? `机器级默认板已清空 —— 从今往后,没在这一行改过的工作区默认什么都不开。${MACHINE_CAVEAT}\n${render(units, workspace, scope)}` : `这个工作区改过的行已全部清掉 —— 每一行都回到跟着机器级默认板走。\n${render(units, workspace, scope)}`
8428
+ };
8429
+ }
8430
+ };
8431
+ const followTool = {
8432
+ name: PANELS_FOLLOW_TOOL,
8433
+ description: "某一行放回跟随机器级默认板(带 id = 只那一行,不带 = 全部)。",
8434
+ parameters: noted({
8435
+ type: "object",
8436
+ properties: {
8437
+ id: ID_PARAM,
8438
+ scope: SCOPE_PARAM
8439
+ },
8440
+ additionalProperties: false
8441
+ }),
8442
+ output: OUTPUT,
8443
+ async execute(raw, exec) {
8444
+ const id = idOf(raw);
8445
+ const scope = scopeOf(raw);
8446
+ const workspace = sessionWorkspace(exec);
8447
+ if (scope === "machine") throw new Error("机器级默认板没有\"放回跟随\"一说:它就是那份默认本身,直接 show / hide 写它。");
8448
+ const units = panels.follow(workspace, id);
6428
8449
  return {
6429
8450
  units,
6430
- text: scope === "machine" ? `已把 ${id} 放到机器级默认板第 ${(args?.index ?? 0) + 1} 位 —— 只影响还没配过的工作区。${MACHINE_CAVEAT}\n${render(units, workspace, scope)}` : `已把 ${id} 放到第 ${(args?.index ?? 0) + 1} 位。\n${render(units, workspace, scope)}`
8451
+ text: id === void 0 ? `这个工作区改过的行全部清掉 —— 每一行都回到跟着机器级默认板走(那份板以后改了,这里跟着变)。\n${render(units, workspace, scope)}` : `${id} 放回跟随 —— 它不再由这个工作区定,改跟机器级默认板走。\n${render(units, workspace, scope)}`
6431
8452
  };
6432
8453
  }
6433
8454
  };
6434
- return [tools.register(tool)];
8455
+ return [
8456
+ tools.register(listTool),
8457
+ tools.register(showTool),
8458
+ tools.register(hideTool),
8459
+ tools.register(moveTool),
8460
+ tools.register(resetTool),
8461
+ tools.register(followTool)
8462
+ ];
6435
8463
  }
6436
8464
  //#endregion
6437
8465
  //#region src/app/units/host.ts
@@ -6514,7 +8542,7 @@ var OWNED = [{
6514
8542
  return [...manifests().flatMap((manifest) => owned.has(manifest.id) || resolveScope({
6515
8543
  id: manifest.id,
6516
8544
  boards
6517
- }) ? [] : manifest.tools.map((tool) => tool.name)), ...inWorkshopSession(ctx, workspace) ? [] : [DIY_TOOL]];
8545
+ }) ? [] : manifest.tools.map((tool) => tool.name)), ...inWorkshopSession(ctx, workspace) ? [] : [...DIY_TOOLS]];
6518
8546
  },
6519
8547
  subscribe: (listener) => panels.subscribe(listener)
6520
8548
  },
@@ -6562,18 +8590,19 @@ function withRunsRoute(result, unit) {
6562
8590
  }
6563
8591
  };
6564
8592
  }
6565
- function notedRegistry(registry, writesOf) {
8593
+ function notedRegistry(registry, writesOf, journal) {
6566
8594
  return { register(definition) {
6567
- if (!writesOf(definition.name)) return registry.register(definition);
6568
- const noted = withApprovalNote(definition.parameters, { required: true });
6569
- return registry.register(noted === definition.parameters ? definition : {
8595
+ const noted = writesOf(definition.name) ? withApprovalNote(definition.parameters, { required: true }) : void 0;
8596
+ const registered = registry.register(noted === void 0 || noted === definition.parameters ? definition : {
6570
8597
  ...definition,
6571
8598
  parameters: noted
6572
8599
  });
8600
+ journal.push(registered);
8601
+ return registered;
6573
8602
  } };
6574
8603
  }
6575
- function scopedMountCtx(ctx, registry, writesOf) {
6576
- const tools = notedRegistry(registry, writesOf);
8604
+ function scopedMountCtx(ctx, registry, writesOf, journal) {
8605
+ const tools = notedRegistry(registry, writesOf, journal);
6577
8606
  const get = (name) => {
6578
8607
  const value = ctx.get(name);
6579
8608
  return name === "tools" && value !== void 0 ? tools : value;
@@ -6588,6 +8617,12 @@ function failureReasonOf(error) {
6588
8617
  if (error instanceof Error) return error.message;
6589
8618
  return typeof error === "string" ? error : String(error);
6590
8619
  }
8620
+ function applyPlanFailures(failures, sinks) {
8621
+ for (const failure of failures) {
8622
+ sinks.note(failure.id, failure.reason);
8623
+ sinks.warn(`[dsh-workbench] ${failure.id} 套件起不来,已摘掉它这一份:${failure.reason}`);
8624
+ }
8625
+ }
6591
8626
  function fail(ledger, item, reason) {
6592
8627
  ledger.failed.add(item);
6593
8628
  ledger.failures.push({
@@ -6729,7 +8764,8 @@ function mountUnits(units, deps) {
6729
8764
  const unit = host.manifest.id;
6730
8765
  const tools = deps.ctx.get("tools");
6731
8766
  const writes = toolUnits(deps.manifests());
6732
- const mountCtx = tools === void 0 ? deps.ctx : scopedMountCtx(deps.ctx, tools, (name) => writes.get(name)?.writes === true);
8767
+ const toolJournal = [];
8768
+ const mountCtx = tools === void 0 ? deps.ctx : scopedMountCtx(deps.ctx, tools, (name) => writes.get(name)?.writes === true, toolJournal);
6733
8769
  const longTasks = deps.longTasks === void 0 ? void 0 : createLongTaskHost({
6734
8770
  scope: deps.longTasks.scope,
6735
8771
  unit,
@@ -6761,7 +8797,10 @@ function mountUnits(units, deps) {
6761
8797
  const item = {
6762
8798
  host,
6763
8799
  readiness,
6764
- result: withRunsRoute(result, unit)
8800
+ result: withRunsRoute(result, unit),
8801
+ rollbackTools: () => {
8802
+ for (const dispose of toolJournal.splice(0)) dispose();
8803
+ }
6765
8804
  };
6766
8805
  mounted.push(item);
6767
8806
  if (longTasks !== void 0) longTaskDisposers.set(item, longTasks.dispose);
@@ -9153,13 +11192,16 @@ function apply(ctx) {
9153
11192
  return;
9154
11193
  }
9155
11194
  for (const item of one.skipped) ctx.logger?.warn?.(`[dsh-workbench] ${item.id} 套件缺硬依赖,跳过:${item.missing.join(", ")}`);
9156
- for (const failure of one.failed) {
9157
- mountFailures.set(failure.id, {
9158
- reason: failure.reason,
9159
- host
9160
- });
9161
- ctx.logger?.warn?.(`[dsh-workbench] ${failure.id} 套件起不来,已摘掉它这一份:${failure.reason}`);
9162
- }
11195
+ applyPlanFailures(one.failed, {
11196
+ note: (unit, reason) => {
11197
+ mountFailures.set(unit, {
11198
+ reason,
11199
+ host
11200
+ });
11201
+ units.fail(unit, reason);
11202
+ },
11203
+ warn: (message) => ctx.logger?.warn?.(message)
11204
+ });
9163
11205
  const slot = one.mounted[0];
9164
11206
  if (slot === void 0) {
9165
11207
  one.disposeLongTasks();
@@ -9183,6 +11225,7 @@ function apply(ctx) {
9183
11225
  mountFailures.delete(id);
9184
11226
  units.recovered(id);
9185
11227
  } catch (error) {
11228
+ slot.rollbackTools();
9186
11229
  for (const route of added) delete routes[route];
9187
11230
  for (const kind of one.contextRenderers.keys()) renderers.delete(kind);
9188
11231
  for (const unit of one.factsOf.keys()) factsOfMap.delete(unit);