@dsh-workbench/core 0.3.1 → 0.3.3

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 (39) hide show
  1. package/client.js +177 -107
  2. package/dist/client/client.body.js +177 -107
  3. package/dist/client/index.js +177 -107
  4. package/dist/host/index.js +832 -1461
  5. package/dist/types/app/client.d.ts +15 -3
  6. package/dist/types/app/layouts/panel-hint.d.ts +3 -0
  7. package/dist/types/app/layouts/rightbar/market-hint.d.ts +16 -0
  8. package/dist/types/app/layouts/rightbar/market-rows.d.ts +2 -3
  9. package/dist/types/modules/panels/model/diy-browser-act.d.ts +2 -39
  10. package/dist/types/modules/panels/model/diy-browser-tools.d.ts +39 -0
  11. package/dist/types/modules/panels/model/diy-browser.d.ts +1 -14
  12. package/dist/types/modules/panels/model/diy-observers.d.ts +11 -0
  13. package/dist/types/modules/panels/model/diy-observers.test.d.ts +1 -0
  14. package/dist/types/modules/panels/model/diy-page.d.ts +1 -0
  15. package/dist/types/modules/panels/model/diy-scripts.d.ts +0 -63
  16. package/dist/types/modules/panels/model/diy-spec.d.ts +5 -0
  17. package/dist/types/modules/panels/model/diy-spec.test.d.ts +1 -0
  18. package/dist/types/modules/panels/unit.d.ts +13 -11
  19. package/dist/types/shared/api/host/diy-recon.d.ts +10 -0
  20. package/package.json +5 -5
  21. package/scripts/seed-workshop.sh +3 -1
  22. package/skills/workbench-module-diy/BUILD.md +37 -16
  23. package/skills/workbench-module-diy/DATASOURCE.md +11 -2
  24. package/skills/workbench-module-diy/DESIGN.md +15 -8
  25. package/skills/workbench-module-diy/KIT.md +2 -0
  26. package/skills/workbench-module-diy/PANEL.md +3 -2
  27. package/skills/workbench-module-diy/REVIEW.md +115 -48
  28. package/skills/workbench-module-diy/SKILL.md +33 -9
  29. package/skills/workbench-module-diy/STEPS.md +1 -1
  30. package/skills/workbench-module-diy/TOOLING.md +6 -3
  31. package/skills/workbench-module-diy/TOOLS.md +90 -204
  32. package/skills/workbench-module-diy/WIRING.md +7 -5
  33. package/skills/workbench-module-diy/WRITES.md +90 -0
  34. package/dist/types/modules/panels/model/diy-act.d.ts +0 -4
  35. package/dist/types/modules/panels/model/diy-look.d.ts +0 -15
  36. package/dist/types/modules/panels/model/diy-net.d.ts +0 -3
  37. /package/dist/types/modules/panels/model/{diy-act.test.d.ts → diy-browser-act.test.d.ts} +0 -0
  38. /package/dist/types/modules/panels/model/{diy-look.test.d.ts → diy-browser-tools-shot.test.d.ts} +0 -0
  39. /package/dist/types/modules/panels/model/{diy-net.test.d.ts → diy-browser-tools.test.d.ts} +0 -0
@@ -252,7 +252,7 @@ var PHASES = [
252
252
  function asRecord$10(value) {
253
253
  return typeof value === "object" && value !== null ? value : void 0;
254
254
  }
255
- function textOf$9(value, max) {
255
+ function textOf$8(value, max) {
256
256
  if (typeof value !== "string") return "";
257
257
  const trimmed = value.trim();
258
258
  return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
@@ -265,8 +265,8 @@ function atOf(value) {
265
265
  function errorOf(raw) {
266
266
  const record = asRecord$10(raw);
267
267
  if (record === void 0) return void 0;
268
- const code = textOf$9(record["code"], 120);
269
- const message = textOf$9(record["message"], PANEL_STATE_MAX_TEXT);
268
+ const code = textOf$8(record["code"], 120);
269
+ const message = textOf$8(record["message"], PANEL_STATE_MAX_TEXT);
270
270
  if (code === "" && message === "") return void 0;
271
271
  return {
272
272
  code: code === "" ? "unknown" : code,
@@ -279,7 +279,7 @@ function parseCardsReport(payload) {
279
279
  ok: false,
280
280
  message: "请求体要是一个对象"
281
281
  };
282
- const workspace = textOf$9(record["workspace"], PANEL_STATE_MAX_TEXT);
282
+ const workspace = textOf$8(record["workspace"], PANEL_STATE_MAX_TEXT);
283
283
  if (workspace === "") return {
284
284
  ok: false,
285
285
  message: "workspace 不能是空的 —— 面板按工作区存"
@@ -293,9 +293,9 @@ function parseCardsReport(payload) {
293
293
  for (const item of raw.slice(0, 32)) {
294
294
  const card = asRecord$10(item);
295
295
  if (card === void 0) continue;
296
- const unit = textOf$9(card["unit"], 200);
297
- const route = textOf$9(card["route"], 400);
298
- const phase = textOf$9(card["phase"], 20);
296
+ const unit = textOf$8(card["unit"], 200);
297
+ const route = textOf$8(card["route"], 400);
298
+ const phase = textOf$8(card["phase"], 20);
299
299
  const at = atOf(card["at"]);
300
300
  if (unit === "" || route === "" || at === null || !PHASES.includes(phase)) continue;
301
301
  const error = errorOf(card["error"]);
@@ -325,9 +325,9 @@ function parseCrashReport(payload) {
325
325
  ok: false,
326
326
  message: "请求体要是一个对象"
327
327
  };
328
- const workspace = textOf$9(record["workspace"], PANEL_STATE_MAX_TEXT);
329
- const unit = textOf$9(record["unit"], 200);
330
- const reason = textOf$9(record["reason"], PANEL_STATE_MAX_TEXT);
328
+ const workspace = textOf$8(record["workspace"], PANEL_STATE_MAX_TEXT);
329
+ const unit = textOf$8(record["unit"], 200);
330
+ const reason = textOf$8(record["reason"], PANEL_STATE_MAX_TEXT);
331
331
  if (workspace === "") return {
332
332
  ok: false,
333
333
  message: "workspace 不能是空的"
@@ -593,7 +593,10 @@ async function runShell(shell, command, options = {}) {
593
593
  truncated: result.stdout.truncated || result.stderr.truncated
594
594
  };
595
595
  }
596
- var DEFAULT_TIMEOUT_MS$1 = 25e3;
596
+ //#endregion
597
+ //#region ../shared/dist/host/browser-read.js
598
+ var BROWSER_SCREEN_SESSION = "workbench";
599
+ var DEFAULT_TIMEOUT_MS$2 = 25e3;
597
600
  var DEFAULT_RETRY_MS = 1200;
598
601
  var DEFAULT_SETTLE_MS$1 = 900;
599
602
  var AUTH_CODES = /* @__PURE__ */ new Set([
@@ -614,20 +617,20 @@ var PERMISSION_CODES = /* @__PURE__ */ new Set([
614
617
  function asRecord$9(value) {
615
618
  return typeof value === "object" && value !== null ? value : void 0;
616
619
  }
617
- function textOf$8(value) {
620
+ function textOf$7(value) {
618
621
  return typeof value === "string" ? value.trim() : "";
619
622
  }
620
623
  function codeOf(page) {
621
624
  const record = asRecord$9(page);
622
- return record === void 0 ? "" : textOf$8(record["code"]);
625
+ return record === void 0 ? "" : textOf$7(record["code"]);
623
626
  }
624
627
  function messageOf$2(page) {
625
628
  const record = asRecord$9(page);
626
- return record === void 0 ? "" : textOf$8(record["message"]);
629
+ return record === void 0 ? "" : textOf$7(record["message"]);
627
630
  }
628
631
  function hintOf$1(page) {
629
632
  const record = asRecord$9(page);
630
- const hint = record === void 0 ? "" : textOf$8(record["hint"]);
633
+ const hint = record === void 0 ? "" : textOf$7(record["hint"]);
631
634
  return hint === "" ? void 0 : hint;
632
635
  }
633
636
  function pageFailed(page) {
@@ -765,8 +768,8 @@ async function openOnce$1(shell, command, session, url, timeoutMs) {
765
768
  async function browserRead(options) {
766
769
  const { shell, script, url, loginUrl, open = true } = options;
767
770
  const command = options.command ?? "opencli";
768
- const session = options.session ?? "workbench";
769
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS$1;
771
+ const session = options.session ?? "workbench-read";
772
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS$2;
770
773
  const retryMs = options.retryMs ?? DEFAULT_RETRY_MS;
771
774
  const settleMs = options.settleMs ?? DEFAULT_SETTLE_MS$1;
772
775
  const at = Date.now();
@@ -850,7 +853,7 @@ async function browserRead(options) {
850
853
  }
851
854
  //#endregion
852
855
  //#region ../shared/dist/host/browser-find.js
853
- var DEFAULT_TIMEOUT_MS = 25e3;
856
+ var DEFAULT_TIMEOUT_MS$1 = 25e3;
854
857
  var DEFAULT_SETTLE_MS = 2500;
855
858
  var URL_TIMEOUT_MS = 8e3;
856
859
  var DEFAULT_LIMIT = 50;
@@ -858,7 +861,7 @@ var DEFAULT_TEXT_MAX = 200;
858
861
  function asRecord$8(value) {
859
862
  return typeof value === "object" && value !== null ? value : void 0;
860
863
  }
861
- function textOf$7(value) {
864
+ function textOf$6(value) {
862
865
  return typeof value === "string" ? value.trim() : "";
863
866
  }
864
867
  function entriesOf(value) {
@@ -871,8 +874,8 @@ function entriesOf(value) {
871
874
  if (item === void 0) continue;
872
875
  const nth = typeof item["nth"] === "number" ? item["nth"] : entries.length;
873
876
  entries.push({
874
- text: textOf$7(item["text"]),
875
- tag: textOf$7(item["tag"]),
877
+ text: textOf$6(item["text"]),
878
+ tag: textOf$6(item["tag"]),
876
879
  nth
877
880
  });
878
881
  }
@@ -881,7 +884,7 @@ function entriesOf(value) {
881
884
  function reasonOf(stdout, stderr) {
882
885
  const parsed = asRecord$8(parseJsonLoose(stdout));
883
886
  const error = parsed === void 0 ? void 0 : asRecord$8(parsed["error"]);
884
- const message = error === void 0 ? "" : textOf$7(error["message"]);
887
+ const message = error === void 0 ? "" : textOf$6(error["message"]);
885
888
  if (message !== "") return message;
886
889
  const lines = (stderr.trim() === "" ? stdout : stderr).split("\n").map((one) => one.trim()).filter((one) => one !== "");
887
890
  return (lines[lines.length - 1] ?? "").slice(-200);
@@ -889,7 +892,7 @@ function reasonOf(stdout, stderr) {
889
892
  function hintOf(stdout) {
890
893
  const parsed = asRecord$8(parseJsonLoose(stdout));
891
894
  const error = parsed === void 0 ? void 0 : asRecord$8(parsed["error"]);
892
- const hint = error === void 0 ? "" : textOf$7(error["hint"]);
895
+ const hint = error === void 0 ? "" : textOf$6(error["hint"]);
893
896
  return hint === "" ? void 0 : hint;
894
897
  }
895
898
  function sameOrigin(left, right) {
@@ -903,7 +906,7 @@ function urlOf(stdout) {
903
906
  const bare = (stdout.split("\n").map((one) => one.trim()).filter((one) => one !== "")[0] ?? "").replace(/^url:\s*/i, "").trim().replace(/^["']+|["']+$/g, "");
904
907
  if (bare.startsWith("{")) {
905
908
  const parsed = asRecord$8(parseJsonLoose(bare));
906
- const url = parsed === void 0 ? "" : textOf$7(parsed["url"]);
909
+ const url = parsed === void 0 ? "" : textOf$6(parsed["url"]);
907
910
  if (url !== "") return url;
908
911
  }
909
912
  return bare;
@@ -981,10 +984,10 @@ async function openOnce(shell, command, session, url, timeoutMs) {
981
984
  async function browserFind(options) {
982
985
  const { shell, css, url, open = true } = options;
983
986
  const command = options.command ?? "opencli";
984
- const session = options.session ?? "workbench";
987
+ const session = options.session ?? "workbench-read";
985
988
  const limit = options.limit ?? DEFAULT_LIMIT;
986
989
  const textMax = options.textMax ?? DEFAULT_TEXT_MAX;
987
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
990
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS$1;
988
991
  const settleMs = options.settleMs ?? DEFAULT_SETTLE_MS;
989
992
  const at = Date.now();
990
993
  const find = async () => {
@@ -2005,7 +2008,7 @@ var PLUGIN_LEDGER_FILE = "plugins.json";
2005
2008
  function ledgerKeyOf(entry) {
2006
2009
  return `${entry.opencliHome}\u0000${entry.package}`;
2007
2010
  }
2008
- function textOf$6(value) {
2011
+ function textOf$5(value) {
2009
2012
  if (typeof value !== "string") return void 0;
2010
2013
  const trimmed = value.trim();
2011
2014
  return trimmed === "" ? void 0 : trimmed;
@@ -2013,13 +2016,13 @@ function textOf$6(value) {
2013
2016
  function entryOf(value) {
2014
2017
  if (typeof value !== "object" || value === null) return void 0;
2015
2018
  const box = value;
2016
- const opencliHome = textOf$6(box["opencliHome"]);
2017
- const pkg = textOf$6(box["package"]);
2018
- const installedAt = textOf$6(box["installedAt"]);
2019
+ const opencliHome = textOf$5(box["opencliHome"]);
2020
+ const pkg = textOf$5(box["package"]);
2021
+ const installedAt = textOf$5(box["installedAt"]);
2019
2022
  if (opencliHome === void 0 || pkg === void 0 || installedAt === void 0) return void 0;
2020
- const name = textOf$6(box["name"]);
2021
- const registry = textOf$6(box["registry"]);
2022
- const releasedAt = textOf$6(box["releasedAt"]);
2023
+ const name = textOf$5(box["name"]);
2024
+ const registry = textOf$5(box["registry"]);
2025
+ const releasedAt = textOf$5(box["releasedAt"]);
2023
2026
  return {
2024
2027
  opencliHome,
2025
2028
  package: pkg,
@@ -3023,22 +3026,24 @@ var DIY_LIST_TOOL = "workbench_diy_list";
3023
3026
  var DIY_STATUS_TOOL = "workbench_diy_status";
3024
3027
  var DIY_PANEL_TOOL = "workbench_diy_panel";
3025
3028
  var DIY_RECON_TOOL = "workbench_diy_recon";
3029
+ var DIY_SPEC_TOOL = "workbench_diy_spec";
3026
3030
  var DIY_PROBE_TOOL = "workbench_diy_probe";
3027
- var DIY_LOOK_TOOL = "workbench_diy_look";
3028
3031
  var DIY_INSTALL_TOOL = "workbench_diy_install";
3029
3032
  var DIY_SHOW_TOOL = "workbench_diy_show";
3033
+ var DIY_STATE_TOOL = "workbench_diy_state";
3034
+ var DIY_HTML_TOOL = "workbench_diy_html";
3035
+ var DIY_FIND_TOOL = "workbench_diy_find";
3030
3036
  var DIY_CLICK_TOOL = "workbench_diy_click";
3037
+ var DIY_HOVER_TOOL = "workbench_diy_hover";
3031
3038
  var DIY_TYPE_TOOL = "workbench_diy_type";
3032
3039
  var DIY_FILL_TOOL = "workbench_diy_fill";
3033
3040
  var DIY_KEYS_TOOL = "workbench_diy_keys";
3034
- var DIY_HOVER_TOOL = "workbench_diy_hover";
3035
- var DIY_SELECT_TOOL = "workbench_diy_select";
3036
3041
  var DIY_SCROLL_TOOL = "workbench_diy_scroll";
3037
- var DIY_RELOAD_TOOL = "workbench_diy_reload";
3038
- var DIY_VIEWPORT_TOOL = "workbench_diy_viewport";
3039
- var DIY_NET_WATCH_TOOL = "workbench_diy_net_watch";
3040
- var DIY_NET_READ_TOOL = "workbench_diy_net_read";
3041
- var DIY_NET_STOP_TOOL = "workbench_diy_net_stop";
3042
+ var DIY_WAIT_TOOL = "workbench_diy_wait";
3043
+ var DIY_EVAL_TOOL = "workbench_diy_eval";
3044
+ var DIY_SCREENSHOT_TOOL = "workbench_diy_screenshot";
3045
+ var DIY_NETWORK_TOOL = "workbench_diy_network";
3046
+ var DIY_DIALOG_TOOL = "workbench_diy_dialog";
3042
3047
  var DIY_LOGS_WATCH_TOOL = "workbench_diy_logs_watch";
3043
3048
  var DIY_LOGS_READ_TOOL = "workbench_diy_logs_read";
3044
3049
  var DIY_LOGS_STOP_TOOL = "workbench_diy_logs_stop";
@@ -3053,20 +3058,22 @@ var READ_TOOLS = [
3053
3058
  DIY_STATUS_TOOL,
3054
3059
  DIY_PANEL_TOOL,
3055
3060
  DIY_RECON_TOOL,
3061
+ DIY_SPEC_TOOL,
3056
3062
  DIY_PROBE_TOOL,
3057
- DIY_LOOK_TOOL,
3063
+ DIY_STATE_TOOL,
3064
+ DIY_HTML_TOOL,
3065
+ DIY_FIND_TOOL,
3058
3066
  DIY_CLICK_TOOL,
3067
+ DIY_HOVER_TOOL,
3059
3068
  DIY_TYPE_TOOL,
3060
3069
  DIY_FILL_TOOL,
3061
3070
  DIY_KEYS_TOOL,
3062
- DIY_HOVER_TOOL,
3063
- DIY_SELECT_TOOL,
3064
3071
  DIY_SCROLL_TOOL,
3065
- DIY_RELOAD_TOOL,
3066
- DIY_VIEWPORT_TOOL,
3067
- DIY_NET_WATCH_TOOL,
3068
- DIY_NET_READ_TOOL,
3069
- DIY_NET_STOP_TOOL,
3072
+ DIY_WAIT_TOOL,
3073
+ DIY_EVAL_TOOL,
3074
+ DIY_SCREENSHOT_TOOL,
3075
+ DIY_NETWORK_TOOL,
3076
+ DIY_DIALOG_TOOL,
3070
3077
  DIY_LOGS_WATCH_TOOL,
3071
3078
  DIY_LOGS_READ_TOOL,
3072
3079
  DIY_LOGS_STOP_TOOL
@@ -3097,22 +3104,24 @@ var DIY_TOOLS = [
3097
3104
  DIY_STATUS_TOOL,
3098
3105
  DIY_PANEL_TOOL,
3099
3106
  DIY_RECON_TOOL,
3107
+ DIY_SPEC_TOOL,
3100
3108
  DIY_PROBE_TOOL,
3101
- DIY_LOOK_TOOL,
3102
3109
  DIY_INSTALL_TOOL,
3103
3110
  DIY_SHOW_TOOL,
3111
+ DIY_STATE_TOOL,
3112
+ DIY_HTML_TOOL,
3113
+ DIY_FIND_TOOL,
3104
3114
  DIY_CLICK_TOOL,
3115
+ DIY_HOVER_TOOL,
3105
3116
  DIY_TYPE_TOOL,
3106
3117
  DIY_FILL_TOOL,
3107
3118
  DIY_KEYS_TOOL,
3108
- DIY_HOVER_TOOL,
3109
- DIY_SELECT_TOOL,
3110
3119
  DIY_SCROLL_TOOL,
3111
- DIY_RELOAD_TOOL,
3112
- DIY_VIEWPORT_TOOL,
3113
- DIY_NET_WATCH_TOOL,
3114
- DIY_NET_READ_TOOL,
3115
- DIY_NET_STOP_TOOL,
3120
+ DIY_WAIT_TOOL,
3121
+ DIY_EVAL_TOOL,
3122
+ DIY_SCREENSHOT_TOOL,
3123
+ DIY_NETWORK_TOOL,
3124
+ DIY_DIALOG_TOOL,
3116
3125
  DIY_LOGS_WATCH_TOOL,
3117
3126
  DIY_LOGS_READ_TOOL,
3118
3127
  DIY_LOGS_STOP_TOOL
@@ -3203,7 +3212,7 @@ function boardName(scope) {
3203
3212
  en: "this project's panel"
3204
3213
  };
3205
3214
  }
3206
- function argsOf$1(call) {
3215
+ function argsOf$2(call) {
3207
3216
  const raw = call.arguments;
3208
3217
  return typeof raw === "object" && raw !== null ? raw : {};
3209
3218
  }
@@ -3218,13 +3227,13 @@ function askOf(reason, question, fallback) {
3218
3227
  var panelsApproval = (call) => {
3219
3228
  if (call.name === "workbench_diy_debug_start" || call.name === "workbench_diy_debug_resume") {
3220
3229
  const starting = call.name === DIY_DEBUG_START_TOOL;
3221
- const unit = argsOf$1(call)["unit"];
3230
+ const unit = argsOf$2(call)["unit"];
3222
3231
  const target = typeof unit === "string" && unit !== "" ? ` [${unit}]` : "";
3223
3232
  return askOf(`${call.name}${target} takes over the user's screen to debug a workbench module`, pair(starting ? "要在「工作台DIY」那块板上开始调试 —— 调试中请留在那一屏,允许吗?" : "要接着调试 —— 请回到「工作台DIY」那一屏,允许吗?", starting ? "it takes over your screen to debug on the workshop board — please stay on that screen. Allow?" : "it needs your screen again to keep debugging — please go back to the workshop board. Allow?"), pair(starting ? "agent 要占用你的屏幕开始调试工作台模块,允许吗?" : "agent 要接着占用你的屏幕调试,允许吗?", starting ? "The agent wants to take over your screen to start debugging a workbench module. Allow?" : "The agent wants your screen again to keep debugging. Allow?"));
3224
3233
  }
3225
3234
  if (call.name === "workbench_diy_debug_pause" || call.name === "workbench_diy_debug_stop") return { kind: "allow" };
3226
3235
  if (call.name !== void 0 && DIY_TOOLS.includes(call.name)) return { kind: "allow" };
3227
- const raw = argsOf$1(call);
3236
+ const raw = argsOf$2(call);
3228
3237
  const text = (key) => {
3229
3238
  const value = raw[key];
3230
3239
  return typeof value === "string" && value !== "" ? value : void 0;
@@ -3575,212 +3584,7 @@ function panelEntriesOf(panels, workspace) {
3575
3584
  }
3576
3585
  }
3577
3586
  //#endregion
3578
- //#region src/shared/api/host/png-crop.ts
3579
- var SIGNATURE = Buffer.from([
3580
- 137,
3581
- 80,
3582
- 78,
3583
- 71,
3584
- 13,
3585
- 10,
3586
- 26,
3587
- 10
3588
- ]);
3589
- var CHANNELS = {
3590
- 0: 1,
3591
- 2: 3,
3592
- 4: 2,
3593
- 6: 4
3594
- };
3595
- var CRC_TABLE = (() => {
3596
- const table = /* @__PURE__ */ new Int32Array(256);
3597
- for (let index = 0; index < 256; index += 1) {
3598
- let value = index;
3599
- for (let bit = 0; bit < 8; bit += 1) value = (value & 1) === 1 ? 3988292384 ^ value >>> 1 : value >>> 1;
3600
- table[index] = value;
3601
- }
3602
- return table;
3603
- })();
3604
- function crc32(bytes) {
3605
- let value = -1;
3606
- for (const byte of bytes) value = CRC_TABLE[(value ^ byte) & 255] ^ value >>> 8;
3607
- return (value ^ -1) >>> 0;
3608
- }
3609
- function chunkOf(type, data) {
3610
- const head = Buffer.alloc(4);
3611
- head.writeUInt32BE(data.length, 0);
3612
- const body = Buffer.concat([Buffer.from(type, "latin1"), data]);
3613
- const tail = Buffer.alloc(4);
3614
- tail.writeUInt32BE(crc32(body), 0);
3615
- return Buffer.concat([
3616
- head,
3617
- body,
3618
- tail
3619
- ]);
3620
- }
3621
- function paethOf(left, above, corner) {
3622
- const estimate = left + above - corner;
3623
- const toLeft = Math.abs(estimate - left);
3624
- const toAbove = Math.abs(estimate - above);
3625
- const toCorner = Math.abs(estimate - corner);
3626
- if (toLeft <= toAbove && toLeft <= toCorner) return left;
3627
- return toAbove <= toCorner ? above : corner;
3628
- }
3629
- function headerOf(bytes) {
3630
- if (bytes.length < 8 || !bytes.subarray(0, 8).equals(SIGNATURE)) return { reason: "不是一张 PNG" };
3631
- let at = 8;
3632
- let raw;
3633
- const parts = [];
3634
- while (at + 8 <= bytes.length) {
3635
- const length = bytes.readUInt32BE(at);
3636
- const type = bytes.toString("latin1", at + 4, at + 8);
3637
- const data = bytes.subarray(at + 8, at + 8 + length);
3638
- if (type === "IHDR") raw = {
3639
- width: data.readUInt32BE(0),
3640
- height: data.readUInt32BE(4),
3641
- depth: data[8] ?? 0,
3642
- color: data[9] ?? 0,
3643
- interlace: data[12] ?? 0
3644
- };
3645
- else if (type === "IDAT") parts.push(Buffer.from(data));
3646
- else if (type === "IEND") break;
3647
- at += 12 + length;
3648
- }
3649
- if (raw === void 0) return { reason: "这张 PNG 没有 IHDR" };
3650
- const channels = CHANNELS[raw.color] ?? 0;
3651
- if (raw.depth !== 8 || channels === 0) return { reason: `这张 PNG 的形状不认(位深 ${String(raw.depth)} / 颜色类型 ${String(raw.color)})` };
3652
- if (raw.interlace !== 0) return { reason: "这张 PNG 是隔行的,不认" };
3653
- return { header: {
3654
- ...raw,
3655
- channels
3656
- } };
3657
- }
3658
- function rowsOf$1(header, parts, keep) {
3659
- const stride = header.width * header.channels;
3660
- const inflated = inflateSync(Buffer.concat(parts));
3661
- const above = Buffer.alloc(stride);
3662
- const current = Buffer.alloc(stride);
3663
- const out = Buffer.alloc(stride * keep);
3664
- let at = 0;
3665
- for (let y = 0; y < header.height; y += 1) {
3666
- const filter = inflated[at] ?? 0;
3667
- const line = inflated.subarray(at + 1, at + 1 + stride);
3668
- at += 1 + stride;
3669
- for (let i = 0; i < stride; i += 1) {
3670
- const source = line[i] ?? 0;
3671
- const left = i >= header.channels ? current[i - header.channels] ?? 0 : 0;
3672
- const up = above[i] ?? 0;
3673
- const corner = i >= header.channels ? above[i - header.channels] ?? 0 : 0;
3674
- const value = filter === 0 ? source : filter === 1 ? source + left : filter === 2 ? source + up : filter === 3 ? source + (left + up >> 1) : source + paethOf(left, up, corner);
3675
- current[i] = value & 255;
3676
- }
3677
- if (y < keep) current.copy(out, y * stride);
3678
- above.set(current);
3679
- if (y + 1 >= keep) break;
3680
- }
3681
- return out;
3682
- }
3683
- function encodeOf(width, height, color, pixels) {
3684
- const stride = width * (CHANNELS[color] ?? 3);
3685
- const raw = Buffer.alloc((stride + 1) * height);
3686
- for (let y = 0; y < height; y += 1) {
3687
- raw[y * (stride + 1)] = 0;
3688
- pixels.copy(raw, y * (stride + 1) + 1, y * stride, (y + 1) * stride);
3689
- }
3690
- const ihdr = Buffer.alloc(13);
3691
- ihdr.writeUInt32BE(width, 0);
3692
- ihdr.writeUInt32BE(height, 4);
3693
- ihdr[8] = 8;
3694
- ihdr[9] = color;
3695
- return Buffer.concat([
3696
- SIGNATURE,
3697
- chunkOf("IHDR", ihdr),
3698
- chunkOf("IDAT", deflateSync(raw, { level: 9 })),
3699
- chunkOf("IEND", Buffer.alloc(0))
3700
- ]);
3701
- }
3702
- function cropPng(bytes, rect) {
3703
- const found = headerOf(bytes);
3704
- if ("reason" in found) return {
3705
- ok: false,
3706
- bytes: Buffer.alloc(0),
3707
- width: 0,
3708
- height: 0,
3709
- clamped: false,
3710
- reason: found.reason
3711
- };
3712
- const { header } = found;
3713
- const x = Math.max(0, Math.min(header.width - 1, Math.trunc(rect.x)));
3714
- const y = Math.max(0, Math.min(header.height - 1, Math.trunc(rect.y)));
3715
- const width = Math.max(1, Math.min(header.width - x, Math.trunc(rect.width)));
3716
- const height = Math.max(1, Math.min(header.height - y, Math.trunc(rect.height)));
3717
- const clamped = x !== Math.trunc(rect.x) || y !== Math.trunc(rect.y) || width !== Math.trunc(rect.width) || height !== Math.trunc(rect.height);
3718
- const parts = [];
3719
- let at = 8;
3720
- while (at + 8 <= bytes.length) {
3721
- const length = bytes.readUInt32BE(at);
3722
- const type = bytes.toString("latin1", at + 4, at + 8);
3723
- if (type === "IDAT") parts.push(Buffer.from(bytes.subarray(at + 8, at + 8 + length)));
3724
- if (type === "IEND") break;
3725
- at += 12 + length;
3726
- }
3727
- const stride = header.width * header.channels;
3728
- const rows = rowsOf$1(header, parts, y + height);
3729
- const outStride = width * header.channels;
3730
- const pixels = Buffer.alloc(outStride * height);
3731
- for (let row = 0; row < height; row += 1) {
3732
- const from = (y + row) * stride + x * header.channels;
3733
- rows.copy(pixels, row * outStride, from, from + outStride);
3734
- }
3735
- return {
3736
- ok: true,
3737
- bytes: encodeOf(width, height, header.color, pixels),
3738
- width,
3739
- height,
3740
- clamped,
3741
- reason: clamped ? "矩形超出图外,已经按边界收进来" : ""
3742
- };
3743
- }
3744
- function cropPngFile(input, output, rect) {
3745
- let source;
3746
- try {
3747
- source = readFileSync(input);
3748
- } catch (error) {
3749
- return {
3750
- ok: false,
3751
- clamped: false,
3752
- reason: `读不到那张截图:${error instanceof Error ? error.message : String(error)}`
3753
- };
3754
- }
3755
- const cropped = cropPng(source, rect);
3756
- if (!cropped.ok) return {
3757
- ok: false,
3758
- clamped: false,
3759
- reason: cropped.reason
3760
- };
3761
- try {
3762
- writeFileSync(output, cropped.bytes);
3763
- } catch (error) {
3764
- return {
3765
- ok: false,
3766
- clamped: false,
3767
- reason: `裁好的图写不回去:${error instanceof Error ? error.message : String(error)}`
3768
- };
3769
- }
3770
- return {
3771
- ok: true,
3772
- clamped: cropped.clamped,
3773
- reason: cropped.reason
3774
- };
3775
- }
3776
- //#endregion
3777
3587
  //#region src/modules/panels/model/diy-scripts.ts
3778
- function textOf$5(value) {
3779
- return typeof value === "string" ? value.trim() : "";
3780
- }
3781
- function numberIn$1(value, fallback) {
3782
- return typeof value === "number" && Number.isFinite(value) ? value : fallback;
3783
- }
3784
3588
  var PAGE_PROBE = `(async () => {
3785
3589
  const answer = await fetch('/workbench/api/health').then((one) => one.json()).catch(() => null);
3786
3590
  const root = document.querySelector('.wb-root');
@@ -3810,247 +3614,18 @@ var PAGE_PROBE = `(async () => {
3810
3614
  function cardSelectorOf(unit) {
3811
3615
  return `#wb-card-${unit}`;
3812
3616
  }
3813
- var DOM_MODES = [
3814
- "none",
3815
- "outline",
3816
- "html"
3817
- ];
3818
- var DEFAULT_PROPS = [
3819
- "display",
3820
- "flexDirection",
3821
- "gap",
3822
- "padding",
3823
- "width",
3824
- "height",
3825
- "overflow",
3826
- "backgroundColor",
3827
- "color",
3828
- "borderRadius",
3829
- "fontSize",
3830
- "lineHeight",
3831
- "fontFamily",
3832
- "boxSizing",
3833
- "--dsw-radius-xl",
3834
- "--dsw-alias-bg-layer-1",
3835
- "--dsw-alias-label-primary"
3836
- ];
3837
- var DEFAULT_PARTS = [".wb-cardHead", ".wb-cardBody"];
3838
- var MAX_PROPS = 40;
3839
- var MAX_PARTS = 6;
3840
- function cleanNames(value, cap) {
3841
- if (!Array.isArray(value)) return [];
3842
- const out = [];
3843
- for (const one of value) {
3844
- if (typeof one !== "string") continue;
3845
- const trimmed = one.trim();
3846
- if (trimmed === "" || trimmed.length > 80 || out.includes(trimmed)) continue;
3847
- out.push(trimmed);
3848
- if (out.length >= cap) break;
3849
- }
3850
- return out;
3851
- }
3852
- function pickOf(raw) {
3853
- const props = cleanNames(raw["props"], MAX_PROPS);
3854
- const parts = cleanNames(raw["parts"], MAX_PARTS);
3855
- const asked = textOf$5(raw["dom"]);
3617
+ //#endregion
3618
+ //#region src/modules/panels/model/diy-browser.ts
3619
+ var LOOK_TIMEOUT_MS = 3e4;
3620
+ async function runBrowserStep(shell, command, argv, timeoutMs = LOOK_TIMEOUT_MS) {
3621
+ const result = await runShell(shell, backgroundLine(command, argv), { timeoutMs });
3856
3622
  return {
3857
- props: props.length === 0 ? [...DEFAULT_PROPS] : props,
3858
- parts: parts.length === 0 ? [...DEFAULT_PARTS] : parts,
3859
- dom: DOM_MODES.includes(asked) ? asked : "outline"
3860
- };
3861
- }
3862
- var NO_PICK = {
3863
- card: {},
3864
- parts: {}
3865
- };
3866
- var EMPTY_CARD = {
3867
- ok: false,
3868
- anchored: false,
3869
- visible: false,
3870
- width: 0,
3871
- height: 0,
3872
- head: "",
3873
- text: "",
3874
- clipped: false,
3875
- cards: 0,
3876
- rect: {
3877
- x: 0,
3878
- y: 0,
3879
- width: 0,
3880
- height: 0
3881
- },
3882
- dpr: 1,
3883
- viewport: {
3884
- width: 0,
3885
- height: 0
3886
- },
3887
- pick: NO_PICK,
3888
- slot: {
3889
- parentWidth: 0,
3890
- parentDisplay: "",
3891
- columns: "",
3892
- index: 0,
3893
- cards: 0,
3894
- viewportHeight: 0,
3895
- scrollHeight: 0
3896
- },
3897
- outline: [],
3898
- html: ""
3899
- };
3900
- function measureScriptOf(unit, pick) {
3901
- return `(() => {
3902
- const props = ${JSON.stringify([...pick.props])};
3903
- const parts = ${JSON.stringify([...pick.parts])};
3904
- const wantDom = ${JSON.stringify(pick.dom)};
3905
- const kebab = (one) => (one.startsWith('--') ? one : one.replace(/[A-Z]/g, (hit) => '-' + hit.toLowerCase()));
3906
- const scrub = (one) => (one ?? '').replace(/\\s+/g, ' ').trim();
3907
- const read = (el) => {
3908
- if (el === null || el === undefined) return {};
3909
- const computed = getComputedStyle(el);
3910
- const out = {};
3911
- for (const one of props) out[one] = computed.getPropertyValue(kebab(one)).trim().slice(0, 200);
3912
- return out;
3913
- };
3914
- const anchor = document.querySelector(${JSON.stringify(cardSelectorOf(unit))});
3915
- const all = [...document.querySelectorAll('.wb-card')];
3916
- if (anchor === null) return JSON.stringify({ ok: false, cards: all.length, anchored: false, visible: false });
3917
- const card = anchor.classList.contains('wb-card') ? anchor : anchor.querySelector('.wb-card');
3918
- if (card === null) return JSON.stringify({ ok: false, cards: all.length, anchored: true, visible: false });
3919
- const box = card.getBoundingClientRect();
3920
- const painted = getComputedStyle(card);
3921
- const visible =
3922
- painted.visibility !== 'hidden' && painted.display !== 'none' && box.width > 0 && box.height > 0;
3923
- const head = card.querySelector('.wb-cardTitle');
3924
- const text = scrub(card.textContent);
3925
- const shown = scrub(card.innerText);
3926
- const clipped = [...card.querySelectorAll('*')].some((one) => one.scrollWidth > one.clientWidth + 1);
3927
- const picked = {};
3928
- for (const one of parts) {
3929
- let found = null;
3930
- try {
3931
- found = card.querySelector(one);
3932
- } catch {
3933
- found = null;
3934
- }
3935
- picked[one] = found === null ? {} : read(found);
3936
- }
3937
- const parent = card.parentElement === anchor ? anchor.parentElement : card.parentElement;
3938
- let scrollable = parent;
3939
- while (scrollable !== null && !/(auto|scroll)/.test(getComputedStyle(scrollable).overflowY)) {
3940
- scrollable = scrollable.parentElement;
3941
- }
3942
- const outline = [];
3943
- if (wantDom === 'outline') {
3944
- const walk = (el, depth) => {
3945
- if (depth > 3 || outline.length >= 40) return;
3946
- const classed =
3947
- typeof el.className === 'string'
3948
- ? el.className.split(/\\s+/).filter((one) => one.startsWith('wb-')).slice(0, 2).join('.')
3949
- : '';
3950
- outline.push({
3951
- depth,
3952
- tag: el.tagName.toLowerCase(),
3953
- cls: classed,
3954
- role: el.getAttribute('role') ?? '',
3955
- name: el.getAttribute('aria-label') ?? '',
3956
- text: el.children.length === 0 ? scrub(el.textContent).slice(0, 30) : '',
3957
- });
3958
- for (const child of [...el.children]) walk(child, depth + 1);
3959
- };
3960
- walk(card, 0);
3961
- }
3962
- return JSON.stringify({
3963
- ok: true,
3964
- cards: all.length,
3965
- visible,
3966
- width: Math.ceil(box.width),
3967
- height: Math.ceil(box.height),
3968
- x: Math.round(box.x),
3969
- y: Math.round(box.y),
3970
- head: head === null ? '' : scrub(head.textContent),
3971
- text: (shown === '' ? text : shown).slice(0, 600),
3972
- clipped,
3973
- dpr: window.devicePixelRatio || 1,
3974
- vw: Math.round(window.innerWidth),
3975
- vh: Math.round(window.innerHeight),
3976
- slot: {
3977
- parentWidth: parent === null ? 0 : Math.round(parent.getBoundingClientRect().width),
3978
- parentDisplay: parent === null ? '' : getComputedStyle(parent).display,
3979
- columns: parent === null ? '' : getComputedStyle(parent).gridTemplateColumns,
3980
- index: all.indexOf(card),
3981
- cards: all.length,
3982
- viewportHeight: scrollable === null ? 0 : scrollable.clientHeight,
3983
- scrollHeight: scrollable === null ? 0 : scrollable.scrollHeight,
3984
- },
3985
- outline,
3986
- html: wantDom === 'html' ? card.outerHTML.slice(0, 4000) : '',
3987
- pick: { card: read(card), parts: picked },
3988
- });
3989
- })()`;
3990
- }
3991
- function nodesOf(value) {
3992
- if (!Array.isArray(value)) return [];
3993
- const out = [];
3994
- for (const one of value) {
3995
- if (typeof one !== "object" || one === null) continue;
3996
- const record = one;
3997
- const depth = numberIn$1(record["depth"], -1);
3998
- const tag = textOf$5(record["tag"]);
3999
- if (depth < 0 || tag === "") continue;
4000
- out.push({
4001
- depth,
4002
- tag,
4003
- cls: textOf$5(record["cls"]),
4004
- role: textOf$5(record["role"]),
4005
- name: textOf$5(record["name"]),
4006
- text: textOf$5(record["text"])
4007
- });
4008
- }
4009
- return out;
4010
- }
4011
- function slotOf(value) {
4012
- const record = typeof value === "object" && value !== null ? value : {};
4013
- return {
4014
- parentWidth: numberIn$1(record["parentWidth"], 0),
4015
- parentDisplay: textOf$5(record["parentDisplay"]),
4016
- columns: textOf$5(record["columns"]),
4017
- index: numberIn$1(record["index"], 0),
4018
- cards: numberIn$1(record["cards"], 0),
4019
- viewportHeight: numberIn$1(record["viewportHeight"], 0),
4020
- scrollHeight: numberIn$1(record["scrollHeight"], 0)
4021
- };
4022
- }
4023
- function stringsOf$2(value) {
4024
- if (typeof value !== "object" || value === null) return {};
4025
- const out = {};
4026
- for (const [key, raw] of Object.entries(value)) {
4027
- const text = textOf$5(raw);
4028
- if (text !== "") out[key] = text;
4029
- }
4030
- return out;
4031
- }
4032
- function pickFactsOf(value) {
4033
- if (typeof value !== "object" || value === null) return NO_PICK;
4034
- const record = value;
4035
- const raw = record["parts"];
4036
- const parts = {};
4037
- if (typeof raw === "object" && raw !== null) for (const [key, one] of Object.entries(raw)) parts[key] = stringsOf$2(one);
4038
- return {
4039
- card: stringsOf$2(record["card"]),
4040
- parts
4041
- };
4042
- }
4043
- //#endregion
4044
- //#region src/modules/panels/model/diy-browser.ts
4045
- var LOOK_TIMEOUT_MS = 3e4;
4046
- async function runBrowserStep(shell, command, argv, timeoutMs = LOOK_TIMEOUT_MS) {
4047
- const result = await runShell(shell, backgroundLine(command, argv), { timeoutMs });
4048
- return {
4049
- ok: result.exitCode === 0 && !result.timedOut,
4050
- stdout: result.stdout.trim(),
4051
- stderr: result.stderr.trim(),
4052
- exitCode: result.exitCode,
4053
- timedOut: result.timedOut
3623
+ ok: result.exitCode === 0 && !result.timedOut,
3624
+ stdout: result.stdout.trim(),
3625
+ stderr: result.stderr.trim(),
3626
+ exitCode: result.exitCode,
3627
+ timedOut: result.timedOut,
3628
+ truncated: result.truncated
4054
3629
  };
4055
3630
  }
4056
3631
  function jsonOf(raw) {
@@ -4067,9 +3642,6 @@ function jsonOf(raw) {
4067
3642
  function textOf$4(value) {
4068
3643
  return typeof value === "string" ? value.trim() : "";
4069
3644
  }
4070
- function numberIn(value, fallback) {
4071
- return typeof value === "number" && Number.isFinite(value) ? value : fallback;
4072
- }
4073
3645
  async function bindCurrentTab(shell, command, session) {
4074
3646
  const step = await runBrowserStep(shell, command, [
4075
3647
  "browser",
@@ -4162,108 +3734,14 @@ async function readyPageOf(shell, command, session) {
4162
3734
  reason: ""
4163
3735
  };
4164
3736
  }
4165
- async function measureCard(shell, command, session, unit, pick) {
4166
- const parsed = jsonOf((await runBrowserStep(shell, command, [
4167
- "browser",
4168
- session,
4169
- "eval",
4170
- measureScriptOf(unit, pick),
4171
- "--window",
4172
- "background"
4173
- ])).stdout);
4174
- if (parsed === void 0 || parsed["ok"] !== true) return {
4175
- ...EMPTY_CARD,
4176
- cards: numberIn(parsed?.["cards"], 0),
4177
- anchored: parsed?.["anchored"] === true,
4178
- visible: parsed?.["visible"] === true
4179
- };
4180
- const width = numberIn(parsed["width"], 0);
4181
- const height = numberIn(parsed["height"], 0);
4182
- return {
4183
- ok: true,
4184
- anchored: true,
4185
- visible: parsed["visible"] === true,
4186
- width,
4187
- height,
4188
- head: textOf$4(parsed["head"]),
4189
- text: textOf$4(parsed["text"]),
4190
- clipped: parsed["clipped"] === true,
4191
- cards: numberIn(parsed["cards"], 0),
4192
- rect: {
4193
- x: numberIn(parsed["x"], 0),
4194
- y: numberIn(parsed["y"], 0),
4195
- width,
4196
- height
4197
- },
4198
- dpr: numberIn(parsed["dpr"], 1) || 1,
4199
- viewport: {
4200
- width: numberIn(parsed["vw"], 0),
4201
- height: numberIn(parsed["vh"], 0)
4202
- },
4203
- pick: pickFactsOf(parsed["pick"]),
4204
- slot: slotOf(parsed["slot"]),
4205
- outline: nodesOf(parsed["outline"]),
4206
- html: textOf$4(parsed["html"])
4207
- };
4208
- }
4209
- function insideViewportOf(view) {
4210
- 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;
4211
- }
4212
- function deviceRectOf(view) {
4213
- return {
4214
- x: Math.max(0, Math.round(view.rect.x * view.dpr)),
4215
- y: Math.max(0, Math.round(view.rect.y * view.dpr)),
4216
- width: Math.max(1, Math.round(view.rect.width * view.dpr)),
4217
- height: Math.max(1, Math.round(view.rect.height * view.dpr))
4218
- };
4219
- }
4220
- function cropFile(file, view) {
4221
- const crop = cropPngFile(file, file, deviceRectOf(view));
4222
- return {
4223
- cropped: crop.ok,
4224
- reason: crop.reason
4225
- };
4226
- }
4227
- async function shootCard(shell, command, session, unit, file, view) {
4228
- const step = await runBrowserStep(shell, command, [
4229
- "browser",
4230
- session,
4231
- "screenshot",
4232
- file,
4233
- "--window",
4234
- "background"
4235
- ]);
4236
- if (!step.ok) return {
4237
- ok: false,
4238
- file: "",
4239
- width: 0,
4240
- height: 0,
4241
- cropped: false,
4242
- reason: step.timedOut ? "截图没在 30 秒内回来" : step.stderr || step.stdout
4243
- };
4244
- if (!insideViewportOf(view)) return {
4245
- ok: true,
4246
- file,
4247
- width: Math.round(view.rect.width),
4248
- height: Math.round(view.rect.height),
4249
- cropped: false,
4250
- 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))}。`
4251
- };
4252
- const crop = cropFile(file, view);
4253
- return {
4254
- ok: true,
4255
- file,
4256
- width: Math.round(view.rect.width),
4257
- height: Math.round(view.rect.height),
4258
- cropped: crop.cropped,
4259
- reason: crop.cropped ? crop.reason : `${crop.reason} —— 给的是整屏图:卡片在 ${String(Math.round(view.rect.x))},${String(Math.round(view.rect.y))}`
4260
- };
4261
- }
4262
3737
  //#endregion
4263
3738
  //#region src/modules/panels/model/diy-page.ts
4264
3739
  function debugPausedText(held) {
4265
3740
  return `调试已暂停(${held.unit})—— 动作族关闸,不再拿屏当目标。要接着看先 workbench_diy_debug_resume(会再要用户点头一次);这之前可以改源码 / build / 跑门禁。`;
4266
3741
  }
3742
+ function debugMissingPactText() {
3743
+ return "现在没有调试约定 —— 看 / 动 / 网络 / 日志这一族只在约定里发得出去。先 workbench_diy_debug_start(unit=…;会请用户点头一次,批了他会被告知「调试中请留在那一屏」),再接着看;收工用 workbench_diy_debug_stop。";
3744
+ }
4267
3745
  function debugForeignPactText(held) {
4268
3746
  return `另一场调试正占着那块屏(${held.sessionId} 在调试 ${held.unit})—— 一台机器同时只允许一场:让它先 workbench_diy_debug_stop,或等它收工。`;
4269
3747
  }
@@ -4299,6 +3777,13 @@ async function pageAt(ctx, exec, raw, options = {}) {
4299
3777
  if (wanted === "" && options.requireUnit !== false) throw new Error(`看一眼要一个 unit(工坊里:${modules.map((one) => one.unit).join(", ") || "(空)"})`);
4300
3778
  const shell = ctx.get("shell");
4301
3779
  if (shell === void 0) throw new Error("这台机器上没有 shell 服务,动不了浏览器");
3780
+ const held = ctx.get("workbenchDiyDebug")?.live(Date.now()) ?? null;
3781
+ const sessionId = sessionIdOf(exec);
3782
+ if (options.requirePact !== false) {
3783
+ if (held === null) throw new Error(debugMissingPactText());
3784
+ if (held.sessionId !== sessionId) throw new Error(debugForeignPactText(held));
3785
+ if (held.phase === "paused") throw new Error(debugPausedText(held));
3786
+ }
4302
3787
  const command = opencliCommandOf(ctx).command;
4303
3788
  const session = asText$1(args["session"]) || "workbench";
4304
3789
  const handshake = await readyPageOf(shell, command, session);
@@ -4310,13 +3795,7 @@ async function pageAt(ctx, exec, raw, options = {}) {
4310
3795
  const workshop = workshopPathOf(ctx);
4311
3796
  const board = boardStateOf(handshake.page, workshop);
4312
3797
  const situation = boardSituation(boardProbeOf(handshake.page), workshop);
4313
- const held = ctx.get("workbenchDiyDebug")?.live(Date.now()) ?? null;
4314
- const sessionId = sessionIdOf(exec);
4315
- if (options.requirePact !== false && held !== null) {
4316
- if (held.sessionId !== sessionId) throw new Error(debugForeignPactText(held));
4317
- if (held.phase === "paused") throw new Error(debugPausedText(held));
4318
- if (board === "other" || board === "hidden") throw new Error(debugOffBoardText(held, situation));
4319
- }
3798
+ if (options.requirePact !== false && held !== null && (board === "other" || board === "hidden")) throw new Error(debugOffBoardText(held, situation));
4320
3799
  if (options.requireBoard !== false && (board === "other" || board === "hidden")) throw new Error(boardGateText(situation, wanted, handshake.page));
4321
3800
  return {
4322
3801
  packagesDir,
@@ -4348,7 +3827,11 @@ function debugRefusalText(code, held) {
4348
3827
  }
4349
3828
  }
4350
3829
  function renderDebugStart(state) {
4351
- return [`开始调试 ${state.unit}(声明的目标:${state.board === "" ? "用户那一屏" : state.board})。`, "现在看 / 动 / 网络那一族才把屏当目标 —— 收工前记得 workbench_diy_debug_stop。"].join("\n");
3830
+ return [
3831
+ `开始调试 ${state.unit}(声明的目标:${state.board === "" ? "用户那一屏" : state.board})。`,
3832
+ "看 / 动 / 网络 / 日志那一族现在才放行(没有这场约定,它们一律当场拒)。",
3833
+ "不看屏时先 workbench_diy_debug_pause;收工记得 workbench_diy_debug_stop。"
3834
+ ].join("\n");
4352
3835
  }
4353
3836
  function renderDebugPause(state) {
4354
3837
  return [`调试暂停了(${state.unit})—— 动作族关闸,不再拿屏当目标。`, "改源码 / build / 跑门禁照常;要看屏再 workbench_diy_debug_resume(会再要用户点头一次)。"].join("\n");
@@ -4366,7 +3849,7 @@ function moveOrThrow(move, held) {
4366
3849
  function registerDebugTools(ctx, tools, store) {
4367
3850
  const startTool = {
4368
3851
  name: DIY_DEBUG_START_TOOL,
4369
- description: "开始调试:占用用户那一屏(要点头一次)。声明 unit 之后,看 / 动那一族才把屏当目标。",
3852
+ description: "开始调试:占用用户那一屏(要点头一次)。没有它,看 / 动 / 网络那一族一律当场拒。",
4370
3853
  parameters: withApprovalNote({
4371
3854
  type: "object",
4372
3855
  properties: { unit: {
@@ -4458,11 +3941,12 @@ function registerDebugTools(ctx, tools, store) {
4458
3941
  const { packagesDir } = workshopOf(ctx, exec);
4459
3942
  const held = store.live(Date.now());
4460
3943
  const state = moveOrThrow(store.stop(sessionIdOf(exec) ?? "", Date.now()), held);
3944
+ await store.settle?.();
4461
3945
  return plainJson({
4462
3946
  workshop: packagesDir,
4463
3947
  unit: state.unit,
4464
3948
  phase: "idle",
4465
- text: renderDebugStop(state)
3949
+ text: `${renderDebugStop(state)}顺手把这一场的观察收一收(网络捕获 / 日志钩子 —— 没开着的不用管)。`
4466
3950
  });
4467
3951
  }
4468
3952
  };
@@ -4476,77 +3960,6 @@ function registerDebugTools(ctx, tools, store) {
4476
3960
  //#endregion
4477
3961
  //#region src/modules/panels/model/diy-browser-act.ts
4478
3962
  var ACT_TIMEOUT_MS = 15e3;
4479
- function actTargetOf(raw) {
4480
- const nth = raw["nth"];
4481
- return {
4482
- selector: textOf$4(raw["selector"]),
4483
- role: textOf$4(raw["role"]),
4484
- name: textOf$4(raw["name"]),
4485
- testid: textOf$4(raw["testid"]),
4486
- nth: typeof nth === "number" && Number.isFinite(nth) ? Math.max(0, Math.trunc(nth)) : null,
4487
- page: textOf$4(raw["scope"]) === "page"
4488
- };
4489
- }
4490
- function scopedSelectorOf(unit, target) {
4491
- if (target.selector === "") return "";
4492
- return target.page ? target.selector : `${cardSelectorOf(unit)} ${target.selector}`;
4493
- }
4494
- var SCOPE_ATTR = "data-diy-scope";
4495
- function semanticOptionsOf(target) {
4496
- const out = [];
4497
- if (target.role !== "") out.push("--role", target.role);
4498
- if (target.name !== "") out.push("--name", target.name);
4499
- if (target.testid !== "") out.push("--testid", target.testid);
4500
- return out;
4501
- }
4502
- function targetOptionsOf(target) {
4503
- const out = [...semanticOptionsOf(target)];
4504
- if (target.nth !== null) out.push("--nth", String(target.nth));
4505
- return out;
4506
- }
4507
- function refsOf(payload) {
4508
- const entries = payload?.["entries"];
4509
- if (!Array.isArray(entries)) return [];
4510
- const out = [];
4511
- for (const one of entries) {
4512
- if (typeof one !== "object" || one === null) continue;
4513
- const ref = one["ref"];
4514
- if (typeof ref === "number") out.push(ref);
4515
- }
4516
- return out;
4517
- }
4518
- function totalMatchesOf(payload) {
4519
- const total = payload?.["matches_n"];
4520
- return typeof total === "number" ? total : 0;
4521
- }
4522
- function scopeMarkScriptOf(unit, refs, nth, token) {
4523
- return `(() => {
4524
- const card = document.querySelector(${JSON.stringify(cardSelectorOf(unit))});
4525
- const refs = ${JSON.stringify(refs)};
4526
- if (card === null) return { ok: false, code: 'no-card', inside: 0, total: refs.length };
4527
- const found = [];
4528
- for (const ref of refs) {
4529
- const el = document.querySelector('[data-opencli-ref="' + ref + '"]');
4530
- if (el !== null && card.contains(el)) found.push(el);
4531
- }
4532
- for (const stale of card.querySelectorAll('[${SCOPE_ATTR}]')) stale.removeAttribute('${SCOPE_ATTR}');
4533
- const pick = found[${String(nth)}];
4534
- if (pick === undefined) return { ok: false, code: 'not-in-card', inside: found.length, total: refs.length };
4535
- pick.setAttribute('${SCOPE_ATTR}', ${JSON.stringify(token)});
4536
- return { ok: true, code: '', inside: found.length, total: refs.length };
4537
- })()`;
4538
- }
4539
- function scopeClearScriptOf(token) {
4540
- return `(() => {
4541
- const marked = document.querySelectorAll('[${SCOPE_ATTR}="${token}"]');
4542
- const removed = marked.length;
4543
- for (const el of marked) el.removeAttribute('${SCOPE_ATTR}');
4544
- return { ok: true, removed };
4545
- })()`;
4546
- }
4547
- function scopedMarkerSelectorOf(unit, token) {
4548
- return `${cardSelectorOf(unit)} [${SCOPE_ATTR}="${token}"]`;
4549
- }
4550
3963
  async function actBrowser(shell, command, session, argv, timeoutMs = ACT_TIMEOUT_MS) {
4551
3964
  const step = await runBrowserStep(shell, command, [
4552
3965
  "browser",
@@ -4570,286 +3983,19 @@ function actErrorOf(step) {
4570
3983
  if (step.ok) return "";
4571
3984
  return step.stderr || step.stdout || "opencli 报了个错";
4572
3985
  }
4573
- function matchesOf(step) {
4574
- const raw = step.payload?.["matches_n"];
4575
- return typeof raw === "number" && Number.isFinite(raw) ? raw : -1;
4576
- }
4577
- async function scrollIntoView(shell, command, session, unit, selector) {
4578
- const asked = selector === "" ? ".wb-card" : selector;
4579
- return actBrowser(shell, command, session, ["eval", `(() => {
4580
- const anchor = document.querySelector(${JSON.stringify(cardSelectorOf(unit))});
4581
- const sel = ${JSON.stringify(asked)};
4582
- if (anchor === null) return JSON.stringify({ ok: false, found: false });
4583
- const el = anchor.matches(sel) ? anchor : anchor.querySelector(sel);
4584
- if (el === null) return JSON.stringify({ ok: false, found: false });
4585
- el.scrollIntoView({ block: 'center', inline: 'nearest' });
4586
- const box = el.getBoundingClientRect();
4587
- 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) });
4588
- })()`]);
4589
- }
4590
- async function reloadPage(shell, command, session) {
4591
- return actBrowser(shell, command, session, ["eval", `(() => { setTimeout(() => location.reload(), 30); return JSON.stringify({ ok: true }); })()`]);
4592
- }
4593
- async function shootAtWidth(shell, command, session, file, width = 0, height = 0) {
4594
- const step = await actBrowser(shell, command, session, [
4595
- "screenshot",
4596
- file,
4597
- ...width > 0 ? ["--width", String(width)] : [],
4598
- ...height > 0 ? ["--height", String(height)] : []
4599
- ], LOOK_TIMEOUT_MS);
4600
- return {
4601
- ok: step.ok,
4602
- file: step.ok ? file : "",
4603
- width,
4604
- height,
4605
- reason: step.ok ? "" : step.timedOut ? "截图没在 30 秒内回来" : step.stderr || step.stdout
4606
- };
4607
- }
4608
- var NET_CAP_LINES = 2e3;
4609
- function followerPatternOf(session) {
4610
- return `[b]rowser ${session} network --follow`;
4611
- }
4612
- function killFollowerLine(session, file) {
3986
+ var SWEEP_TIMEOUT_MS = 4e3;
3987
+ async function stopNetworkExact(shell, file, timeoutMs = SWEEP_TIMEOUT_MS) {
4613
3988
  const pidFile = quoteShellArg(`${file}.pid`);
4614
3989
  const reaperFile = quoteShellArg(`${file}.reaper`);
4615
- return [
4616
- `kill "$(cat ${pidFile} 2>/dev/null)" 2>/dev/null`,
4617
- `pkill -f ${quoteShellArg(followerPatternOf(session))} 2>/dev/null`,
4618
- `pkill -P "$(cat ${reaperFile} 2>/dev/null)" 2>/dev/null`,
4619
- `kill "$(cat ${reaperFile} 2>/dev/null)" 2>/dev/null`
4620
- ].join("; ");
4621
- }
4622
- async function armNetwork(shell, command, session, file) {
4623
- const capture = `${backgroundLine(command, [
4624
- "browser",
4625
- session,
4626
- "network",
4627
- "--follow",
4628
- "--window",
4629
- "background"
4630
- ])} 2>&1 | head -n ${String(NET_CAP_LINES)} > ${quoteShellArg(file)}`;
4631
- const reaper = `sleep ${String(900)}; ${killFollowerLine(session, file)}`;
4632
- const step = await runShell(shell, [
4633
- "rm",
4634
- "-f",
4635
- quoteShellArg(file),
4636
- quoteShellArg(`${file}.pid`),
4637
- quoteShellArg(`${file}.reaper`),
4638
- ";",
4639
- "nohup",
4640
- "sh",
4641
- "-c",
4642
- quoteShellArg(capture),
4643
- ">",
4644
- "/dev/null",
4645
- "2>&1",
4646
- "&",
4647
- "echo",
4648
- "$!",
4649
- ">",
4650
- quoteShellArg(`${file}.pid`),
4651
- ";",
4652
- "nohup",
4653
- "sh",
4654
- "-c",
4655
- quoteShellArg(reaper),
4656
- ">",
4657
- "/dev/null",
4658
- "2>&1",
4659
- "&",
4660
- "echo",
4661
- "$!",
4662
- ">",
4663
- quoteShellArg(`${file}.reaper`),
4664
- ";",
4665
- "sleep",
4666
- "0.6",
4667
- ";",
4668
- "cat",
4669
- quoteShellArg(`${file}.pid`)
4670
- ].join(" "), { timeoutMs: ACT_TIMEOUT_MS });
4671
- const pid = step.stdout.trim().split("\n").slice(-1)[0] ?? "";
4672
- return {
4673
- ok: step.exitCode === 0 && pid !== "",
4674
- pid,
4675
- reason: step.exitCode === 0 ? "" : step.stderr.trim() || step.stdout.trim()
4676
- };
4677
- }
4678
- async function stopNetwork(shell, session, file) {
4679
3990
  return (await runShell(shell, [
4680
- killFollowerLine(session, file),
4681
- ";",
4682
- "rm",
4683
- "-f",
4684
- quoteShellArg(`${file}.pid`),
4685
- quoteShellArg(`${file}.reaper`)
4686
- ].join(" "), { timeoutMs: ACT_TIMEOUT_MS })).exitCode === 0;
4687
- }
4688
- async function readNetwork(shell, file, lines) {
4689
- const step = await runShell(shell, `tail -n ${String(lines)} ${quoteShellArg(file)}`, { timeoutMs: ACT_TIMEOUT_MS });
4690
- const out = [];
4691
- for (const raw of step.stdout.split("\n")) {
4692
- const parsed = jsonOf(raw);
4693
- if (parsed !== void 0) out.push(parsed);
4694
- }
4695
- return out;
4696
- }
4697
- //#endregion
4698
- //#region src/modules/panels/model/diy-look.ts
4699
- function registerLookTools(ctx, tools) {
4700
- const lookTool = {
4701
- name: DIY_LOOK_TOOL,
4702
- description: "看一眼这张卡:只截它这一张 + 按你点的那几项读它的样式与字。要用户把工作台那一页切到前台。",
4703
- parameters: {
4704
- type: "object",
4705
- properties: {
4706
- unit: {
4707
- type: "string",
4708
- description: "工坊里的 unit(不填 = 工坊里只有一个就用它)。"
4709
- },
4710
- props: {
4711
- type: "array",
4712
- items: { type: "string" },
4713
- description: "要读的 CSS 属性(含 --令牌;不填给默认档)。最多 40 个。"
4714
- },
4715
- parts: {
4716
- type: "array",
4717
- items: { type: "string" },
4718
- description: "卡片里再看哪几块(选择器,限卡片内;不填 = 卡头与卡片体)。最多 6 个。"
4719
- },
4720
- dom: {
4721
- type: "string",
4722
- description: "结构快照:outline(默认,tag+类名+role+文本的浅层大纲)| html(卡片子树原始 HTML)| none。"
4723
- },
4724
- session: {
4725
- type: "string",
4726
- description: "会话名(默认 workbench,一般不用填)。"
4727
- }
4728
- },
4729
- additionalProperties: false
4730
- },
4731
- output: OUTPUT$2,
4732
- async execute(raw, exec) {
4733
- const { packagesDir, unit, shell, command, session, board } = await pageAt(ctx, exec, raw);
4734
- const pick = pickOf(asRecord$3(raw));
4735
- let view = await measureCard(shell, command, session, unit, pick);
4736
- if (!view.ok || !view.visible) {
4737
- const state = view.ok ? "hidden" : board;
4738
- return plainJson({
4739
- workshop: packagesDir,
4740
- unit,
4741
- ok: false,
4742
- board: state,
4743
- cards: view.cards,
4744
- text: renderMissingCard(unit, view, state)
4745
- });
4746
- }
4747
- if (!insideViewportOf(view)) {
4748
- await scrollIntoView(shell, command, session, unit, "");
4749
- view = await measureCard(shell, command, session, unit, pick);
4750
- }
4751
- const file = join(dirname(packagesDir), "shots", `${unit}-${String(Date.now())}.png`);
4752
- await writeShotsDir(shell, dirname(file));
4753
- const shot = await shootCard(shell, command, session, unit, file, view);
4754
- return plainJson({
4755
- workshop: packagesDir,
4756
- unit,
4757
- ok: shot.ok,
4758
- file: shot.file,
4759
- rect: view.rect,
4760
- cropped: shot.cropped,
4761
- clipped: view.clipped,
4762
- head: view.head,
4763
- pick: view.pick,
4764
- slot: view.slot,
4765
- outline: view.outline,
4766
- html: view.html,
4767
- text: renderLook(unit, view, shot, pick)
4768
- });
4769
- }
4770
- };
4771
- return [tools.register(lookTool)];
4772
- }
4773
- function rgbOf(value) {
4774
- const hit = /rgba?\((\d+)[,\s]+(\d+)[,\s]+(\d+)/.exec(value);
4775
- if (hit === null) return void 0;
4776
- return [
4777
- Number(hit[1]),
4778
- Number(hit[2]),
4779
- Number(hit[3])
4780
- ];
4781
- }
4782
- function luminanceOf(rgb) {
4783
- const channel = (one) => {
4784
- const scaled = one / 255;
4785
- return scaled <= .03928 ? scaled / 12.92 : ((scaled + .055) / 1.055) ** 2.4;
4786
- };
4787
- return .2126 * channel(rgb[0]) + .7152 * channel(rgb[1]) + .0722 * channel(rgb[2]);
4788
- }
4789
- function contrastOf(foreground, background) {
4790
- const front = rgbOf(foreground);
4791
- const back = rgbOf(background);
4792
- if (front === void 0 || back === void 0) return void 0;
4793
- const one = luminanceOf(front);
4794
- const other = luminanceOf(back);
4795
- const ratio = (Math.max(one, other) + .05) / (Math.min(one, other) + .05);
4796
- return Math.round(ratio * 100) / 100;
4797
- }
4798
- function styleLineOf(view, pick) {
4799
- return pick.props.filter((one) => (view.pick.card[one] ?? "") !== "").map((one) => `${one}=${view.pick.card[one] ?? ""}`).join(" · ");
4800
- }
4801
- function partsLineOf(view, pick) {
4802
- return pick.parts.map((one) => {
4803
- const found = view.pick.parts[one] ?? {};
4804
- const inside = Object.entries(found).map(([key, value]) => `${key}=${value}`).join(" ");
4805
- return `${one}${inside === "" ? "(页面里没有这个)" : ` ${inside}`}`;
4806
- }).join("\n");
4807
- }
4808
- function slotText(slot, width) {
4809
- const columns = slot.columns === "" || slot.columns === "none" ? "一列" : `多列(${slot.columns})`;
4810
- const fit = slot.parentWidth === 0 ? "" : `、这张卡占 ${String(width)}/${String(slot.parentWidth)}px`;
4811
- return `它在第 ${String(slot.index + 1)}/${String(slot.cards)} 张;卡片格子 ${String(slot.parentWidth)}px 宽、${columns}${fit};滚动容器可视 ${String(slot.viewportHeight)}px / 内容 ${String(slot.scrollHeight)}px`;
4812
- }
4813
- function slotLineOf(view) {
4814
- return slotText(view.slot, view.width);
4815
- }
4816
- function outlineText(view) {
4817
- return view.outline.map((one) => {
4818
- const bits = [`${" ".repeat(one.depth)}${one.tag}${one.cls === "" ? "" : `.${one.cls}`}`];
4819
- if (one.role !== "") bits.push(`role=${one.role}`);
4820
- if (one.name !== "") bits.push(`name="${one.name}"`);
4821
- if (one.text !== "") bits.push(`"${one.text}"`);
4822
- return bits.join(" ");
4823
- }).join("\n");
4824
- }
4825
- function contrastLineOf(view) {
4826
- const ratio = contrastOf((view.pick.parts[".wb-cardBody"] ?? view.pick.card)["color"] ?? "", view.pick.card["backgroundColor"] ?? "");
4827
- if (ratio === void 0) return "";
4828
- return ratio >= 4.5 ? `正文对比度 ${String(ratio)}(够)` : `正文对比度 ${String(ratio)} —— 低于 4.5,正文会看不清(要改)`;
4829
- }
4830
- function renderLook(unit, view, shot, pick) {
4831
- const where = `面板里 ${String(view.rect.x)},${String(view.rect.y)}(${String(view.width)}×${String(view.height)})`;
4832
- return [
4833
- shot.ok ? `看到了 ${unit} 这张卡:${where}` : `没拍成:${shot.reason}`,
4834
- ...shot.ok ? [shot.file] : [],
4835
- ...shot.ok && shot.reason !== "" ? [shot.reason] : [],
4836
- ...view.head === "" ? [] : [`卡头:${view.head}`],
4837
- ...view.text === "" ? [] : [`卡片上写着:${view.text}`],
4838
- `格子里:${slotLineOf(view)}`,
4839
- ...styleLineOf(view, pick) === "" ? [] : [`你点的那几项:${styleLineOf(view, pick)}`],
4840
- ...[partsLineOf(view, pick)].filter((one) => one !== ""),
4841
- ...contrastLineOf(view) === "" ? [] : [contrastLineOf(view)],
4842
- ...pick.dom === "outline" && view.outline.length > 0 ? ["结构:", outlineText(view)] : [],
4843
- ...pick.dom === "html" ? [`原始 HTML:${String(view.html.length)} 字符(在返回的 html 字段里)`] : [],
4844
- ...view.clipped ? ["有内容被裁掉了(某一格比它自己的宽度还宽)—— 这是要改的信号"] : []
4845
- ].join("\n");
4846
- }
4847
- function renderMissingCard(unit, view, board) {
4848
- if (board === "other") return `你现在这一页画的是别的工作区的面板,工坊那块板没在这一屏 —— ${unit} 的卡不在屏上,我不拿别的板当目标。请用户在左栏切回「工作台DIY」,或把这张卡上到他正看着的那块板上。`;
4849
- if (board === "hidden") return `右栏收着 —— 面板没画在屏上(锚点还在页面里,但看不见它),量不到也不该拍 ${unit}。让用户把面板展开、或切回「工作台DIY」,我再接着看。`;
4850
- if (view.anchored) return `面板里给 ${unit} 留了位置(锚点在),但里面没有卡片 —— 它的 View 没画出来:要么渲染时抛了(看 workbench_diy_panel 的运行态上报与 workbench_diy_status),要么它返回了空 / 没套卡壳。你现在这一页面板里一共 ${String(view.cards)} 张卡。`;
4851
- const stale = board === "unknown" ? "③ 页面上跑的外壳可能还是旧的一份(锚点由外壳盖)—— 让用户刷新一次页面。" : "";
4852
- return `你现在这一页的面板里没有 ${unit} 这张卡(面板里现在有 ${String(view.cards)} 张卡)。按顺序试:① workbench_diy_show(unit=${unit})把它上板;② 右栏可能没停在「工作台面板」那一列 —— 让用户切回来。${stale}`;
3991
+ `p="$(cat ${pidFile} 2>/dev/null)"`,
3992
+ `r="$(cat ${reaperFile} 2>/dev/null)"`,
3993
+ `[ -n "$p" ] && kill "$p" 2>/dev/null`,
3994
+ `[ -n "$r" ] && pkill -P "$r" 2>/dev/null`,
3995
+ `[ -n "$r" ] && kill "$r" 2>/dev/null`,
3996
+ `if [ "$(cat ${pidFile} 2>/dev/null)" = "$p" ]; then rm -f ${quoteShellArg(file)} ${pidFile} ${reaperFile}; fi`,
3997
+ "true"
3998
+ ].join("; "), { timeoutMs })).exitCode === 0;
4853
3999
  }
4854
4000
  //#endregion
4855
4001
  //#region src/modules/panels/model/diy-logs.ts
@@ -5041,531 +4187,678 @@ function registerLogTools(ctx, tools) {
5041
4187
  ];
5042
4188
  }
5043
4189
  //#endregion
4190
+ //#region src/shared/api/host/png-crop.ts
4191
+ var SIGNATURE = Buffer.from([
4192
+ 137,
4193
+ 80,
4194
+ 78,
4195
+ 71,
4196
+ 13,
4197
+ 10,
4198
+ 26,
4199
+ 10
4200
+ ]);
4201
+ var CHANNELS = {
4202
+ 0: 1,
4203
+ 2: 3,
4204
+ 4: 2,
4205
+ 6: 4
4206
+ };
4207
+ var CRC_TABLE = (() => {
4208
+ const table = /* @__PURE__ */ new Int32Array(256);
4209
+ for (let index = 0; index < 256; index += 1) {
4210
+ let value = index;
4211
+ for (let bit = 0; bit < 8; bit += 1) value = (value & 1) === 1 ? 3988292384 ^ value >>> 1 : value >>> 1;
4212
+ table[index] = value;
4213
+ }
4214
+ return table;
4215
+ })();
4216
+ function crc32(bytes) {
4217
+ let value = -1;
4218
+ for (const byte of bytes) value = CRC_TABLE[(value ^ byte) & 255] ^ value >>> 8;
4219
+ return (value ^ -1) >>> 0;
4220
+ }
4221
+ function chunkOf(type, data) {
4222
+ const head = Buffer.alloc(4);
4223
+ head.writeUInt32BE(data.length, 0);
4224
+ const body = Buffer.concat([Buffer.from(type, "latin1"), data]);
4225
+ const tail = Buffer.alloc(4);
4226
+ tail.writeUInt32BE(crc32(body), 0);
4227
+ return Buffer.concat([
4228
+ head,
4229
+ body,
4230
+ tail
4231
+ ]);
4232
+ }
4233
+ function paethOf(left, above, corner) {
4234
+ const estimate = left + above - corner;
4235
+ const toLeft = Math.abs(estimate - left);
4236
+ const toAbove = Math.abs(estimate - above);
4237
+ const toCorner = Math.abs(estimate - corner);
4238
+ if (toLeft <= toAbove && toLeft <= toCorner) return left;
4239
+ return toAbove <= toCorner ? above : corner;
4240
+ }
4241
+ function headerOf(bytes) {
4242
+ if (bytes.length < 8 || !bytes.subarray(0, 8).equals(SIGNATURE)) return { reason: "不是一张 PNG" };
4243
+ let at = 8;
4244
+ let raw;
4245
+ const parts = [];
4246
+ while (at + 8 <= bytes.length) {
4247
+ const length = bytes.readUInt32BE(at);
4248
+ const type = bytes.toString("latin1", at + 4, at + 8);
4249
+ const data = bytes.subarray(at + 8, at + 8 + length);
4250
+ if (type === "IHDR") raw = {
4251
+ width: data.readUInt32BE(0),
4252
+ height: data.readUInt32BE(4),
4253
+ depth: data[8] ?? 0,
4254
+ color: data[9] ?? 0,
4255
+ interlace: data[12] ?? 0
4256
+ };
4257
+ else if (type === "IDAT") parts.push(Buffer.from(data));
4258
+ else if (type === "IEND") break;
4259
+ at += 12 + length;
4260
+ }
4261
+ if (raw === void 0) return { reason: "这张 PNG 没有 IHDR" };
4262
+ const channels = CHANNELS[raw.color] ?? 0;
4263
+ if (raw.depth !== 8 || channels === 0) return { reason: `这张 PNG 的形状不认(位深 ${String(raw.depth)} / 颜色类型 ${String(raw.color)})` };
4264
+ if (raw.interlace !== 0) return { reason: "这张 PNG 是隔行的,不认" };
4265
+ return { header: {
4266
+ ...raw,
4267
+ channels
4268
+ } };
4269
+ }
4270
+ function rowsOf$1(header, parts, keep) {
4271
+ const stride = header.width * header.channels;
4272
+ const inflated = inflateSync(Buffer.concat(parts));
4273
+ const above = Buffer.alloc(stride);
4274
+ const current = Buffer.alloc(stride);
4275
+ const out = Buffer.alloc(stride * keep);
4276
+ let at = 0;
4277
+ for (let y = 0; y < header.height; y += 1) {
4278
+ const filter = inflated[at] ?? 0;
4279
+ const line = inflated.subarray(at + 1, at + 1 + stride);
4280
+ at += 1 + stride;
4281
+ for (let i = 0; i < stride; i += 1) {
4282
+ const source = line[i] ?? 0;
4283
+ const left = i >= header.channels ? current[i - header.channels] ?? 0 : 0;
4284
+ const up = above[i] ?? 0;
4285
+ const corner = i >= header.channels ? above[i - header.channels] ?? 0 : 0;
4286
+ const value = filter === 0 ? source : filter === 1 ? source + left : filter === 2 ? source + up : filter === 3 ? source + (left + up >> 1) : source + paethOf(left, up, corner);
4287
+ current[i] = value & 255;
4288
+ }
4289
+ if (y < keep) current.copy(out, y * stride);
4290
+ above.set(current);
4291
+ if (y + 1 >= keep) break;
4292
+ }
4293
+ return out;
4294
+ }
4295
+ function encodeOf(width, height, color, pixels) {
4296
+ const stride = width * (CHANNELS[color] ?? 3);
4297
+ const raw = Buffer.alloc((stride + 1) * height);
4298
+ for (let y = 0; y < height; y += 1) {
4299
+ raw[y * (stride + 1)] = 0;
4300
+ pixels.copy(raw, y * (stride + 1) + 1, y * stride, (y + 1) * stride);
4301
+ }
4302
+ const ihdr = Buffer.alloc(13);
4303
+ ihdr.writeUInt32BE(width, 0);
4304
+ ihdr.writeUInt32BE(height, 4);
4305
+ ihdr[8] = 8;
4306
+ ihdr[9] = color;
4307
+ return Buffer.concat([
4308
+ SIGNATURE,
4309
+ chunkOf("IHDR", ihdr),
4310
+ chunkOf("IDAT", deflateSync(raw, { level: 9 })),
4311
+ chunkOf("IEND", Buffer.alloc(0))
4312
+ ]);
4313
+ }
4314
+ function cropPng(bytes, rect) {
4315
+ const found = headerOf(bytes);
4316
+ if ("reason" in found) return {
4317
+ ok: false,
4318
+ bytes: Buffer.alloc(0),
4319
+ width: 0,
4320
+ height: 0,
4321
+ clamped: false,
4322
+ reason: found.reason
4323
+ };
4324
+ const { header } = found;
4325
+ const x = Math.max(0, Math.min(header.width - 1, Math.trunc(rect.x)));
4326
+ const y = Math.max(0, Math.min(header.height - 1, Math.trunc(rect.y)));
4327
+ const width = Math.max(1, Math.min(header.width - x, Math.trunc(rect.width)));
4328
+ const height = Math.max(1, Math.min(header.height - y, Math.trunc(rect.height)));
4329
+ const clamped = x !== Math.trunc(rect.x) || y !== Math.trunc(rect.y) || width !== Math.trunc(rect.width) || height !== Math.trunc(rect.height);
4330
+ const parts = [];
4331
+ let at = 8;
4332
+ while (at + 8 <= bytes.length) {
4333
+ const length = bytes.readUInt32BE(at);
4334
+ const type = bytes.toString("latin1", at + 4, at + 8);
4335
+ if (type === "IDAT") parts.push(Buffer.from(bytes.subarray(at + 8, at + 8 + length)));
4336
+ if (type === "IEND") break;
4337
+ at += 12 + length;
4338
+ }
4339
+ const stride = header.width * header.channels;
4340
+ const rows = rowsOf$1(header, parts, y + height);
4341
+ const outStride = width * header.channels;
4342
+ const pixels = Buffer.alloc(outStride * height);
4343
+ for (let row = 0; row < height; row += 1) {
4344
+ const from = (y + row) * stride + x * header.channels;
4345
+ rows.copy(pixels, row * outStride, from, from + outStride);
4346
+ }
4347
+ return {
4348
+ ok: true,
4349
+ bytes: encodeOf(width, height, header.color, pixels),
4350
+ width,
4351
+ height,
4352
+ clamped,
4353
+ reason: clamped ? "矩形超出图外,已经按边界收进来" : ""
4354
+ };
4355
+ }
4356
+ function cropPngFile(input, output, rect) {
4357
+ let source;
4358
+ try {
4359
+ source = readFileSync(input);
4360
+ } catch (error) {
4361
+ return {
4362
+ ok: false,
4363
+ clamped: false,
4364
+ reason: `读不到那张截图:${error instanceof Error ? error.message : String(error)}`
4365
+ };
4366
+ }
4367
+ const cropped = cropPng(source, rect);
4368
+ if (!cropped.ok) return {
4369
+ ok: false,
4370
+ clamped: false,
4371
+ reason: cropped.reason
4372
+ };
4373
+ try {
4374
+ writeFileSync(output, cropped.bytes);
4375
+ } catch (error) {
4376
+ return {
4377
+ ok: false,
4378
+ clamped: false,
4379
+ reason: `裁好的图写不回去:${error instanceof Error ? error.message : String(error)}`
4380
+ };
4381
+ }
4382
+ return {
4383
+ ok: true,
4384
+ clamped: cropped.clamped,
4385
+ reason: cropped.reason
4386
+ };
4387
+ }
4388
+ //#endregion
5044
4389
  //#region src/modules/panels/model/diy-schema.ts
5045
4390
  var UNIT_PROP = {
5046
4391
  type: "string",
5047
4392
  description: "工坊里的 unit(不填 = 工坊里只有一个就用它)。"
5048
4393
  };
5049
- var SESSION_PROP = {
5050
- type: "string",
5051
- description: "会话名(默认 workbench,一般不用填)。"
5052
- };
5053
- function pageSchema(extra = {}, required = []) {
4394
+ //#endregion
4395
+ //#region src/modules/panels/model/diy-browser-tools.ts
4396
+ var DEFAULT_TIMEOUT_MS = 3e4;
4397
+ var CARD_TAIL = " .wb-card";
4398
+ var VIEWPORT_SCRIPT = "({ dpr: window.devicePixelRatio, width: window.innerWidth, height: window.innerHeight })";
4399
+ var CARD_TOKEN = "$card";
4400
+ function cardSelectorIn(unit) {
4401
+ return `${cardSelectorOf(unit)}${CARD_TAIL}`;
4402
+ }
4403
+ function stringsOf$2(value) {
4404
+ if (!Array.isArray(value)) return [];
4405
+ return value.map((one) => asText$1(one)).filter((one) => one !== "");
4406
+ }
4407
+ function withToken(argv, card) {
4408
+ return argv.map((one) => one.split(CARD_TOKEN).join(card));
4409
+ }
4410
+ function prefixValue(argv, flag, card) {
4411
+ const at = argv.indexOf(flag);
4412
+ if (at !== -1) {
4413
+ const value = argv[at + 1];
4414
+ if (value === void 0 || value.startsWith("-")) return [
4415
+ ...argv.slice(0, at + 1),
4416
+ card,
4417
+ ...argv.slice(at + 1)
4418
+ ];
4419
+ return [
4420
+ ...argv.slice(0, at + 1),
4421
+ `${card} ${value}`,
4422
+ ...argv.slice(at + 2)
4423
+ ];
4424
+ }
4425
+ const joined = `${flag}=`;
4426
+ const eq = argv.findIndex((one) => one.startsWith(joined));
4427
+ if (eq !== -1) return argv.map((one, index) => index === eq ? `${joined}${card} ${one.slice(joined.length)}` : one);
4428
+ return [
4429
+ ...argv,
4430
+ flag,
4431
+ card
4432
+ ];
4433
+ }
4434
+ function prefixPositional(argv, at, card, onlyWhenFirst) {
4435
+ if (onlyWhenFirst !== void 0 && argv[0] !== onlyWhenFirst) return [...argv];
4436
+ const value = argv[at];
4437
+ if (value === void 0 || value.startsWith("-")) return [...argv];
4438
+ return [
4439
+ ...argv.slice(0, at),
4440
+ `${card} ${value}`,
4441
+ ...argv.slice(at + 1)
4442
+ ];
4443
+ }
4444
+ function evalWrapped(script, card) {
4445
+ return `(() => { const root = document.querySelector(${JSON.stringify(card)}); if (root === null) return null;\n${script}\n})()`;
4446
+ }
4447
+ function wrapFirst(argv, card) {
4448
+ const first = argv[0];
4449
+ if (first === void 0 || first.startsWith("-")) return [...argv];
4450
+ return [evalWrapped(first, card), ...argv.slice(1)];
4451
+ }
4452
+ function argvOf(row, raw, unit) {
4453
+ const card = cardSelectorIn(unit);
4454
+ const declared = stringsOf$2(raw["args"]);
4455
+ const tokens = withToken(row.evalInCard === true && asText$1(raw["in"]) === "card" ? wrapFirst(declared, card) : declared, card);
4456
+ if (row.css === void 0 || asText$1(raw["scope"]) !== "card") return tokens;
4457
+ if (row.css.kind === "flag") return prefixValue(tokens, row.css.flag, card);
4458
+ return prefixPositional(tokens, row.css.at, card, row.css.onlyWhenFirst);
4459
+ }
4460
+ function finite(value) {
4461
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
4462
+ }
4463
+ function clipOf(value) {
4464
+ if (typeof value !== "object" || value === null) return void 0;
4465
+ const raw = value;
4466
+ const x = finite(raw["x"]);
4467
+ const y = finite(raw["y"]);
4468
+ const width = finite(raw["width"]);
4469
+ const height = finite(raw["height"]);
4470
+ if (x === void 0 || y === void 0 || width === void 0 || height === void 0) return void 0;
4471
+ if (x < 0 || y < 0 || width <= 0 || height <= 0) return void 0;
4472
+ return {
4473
+ x,
4474
+ y,
4475
+ width,
4476
+ height
4477
+ };
4478
+ }
4479
+ function deviceClipRect(clip, dpr) {
4480
+ const scale = Number.isFinite(dpr) && dpr > 0 ? dpr : 1;
5054
4481
  return {
5055
- type: "object",
5056
- properties: {
5057
- unit: UNIT_PROP,
5058
- ...extra,
5059
- session: SESSION_PROP
5060
- },
5061
- ...required.length === 0 ? {} : { required },
5062
- additionalProperties: false
4482
+ x: Math.max(0, Math.round(clip.x * scale)),
4483
+ y: Math.max(0, Math.round(clip.y * scale)),
4484
+ width: Math.max(1, Math.round(clip.width * scale)),
4485
+ height: Math.max(1, Math.round(clip.height * scale))
5063
4486
  };
5064
4487
  }
5065
- //#endregion
5066
- //#region src/modules/panels/model/diy-net.ts
5067
- var SIBLING_TRIES = 6;
5068
- var NET_BODY_MAX = 2e4;
5069
- function nearestKeyOf(payload, key) {
5070
- const entries = payload?.["entries"];
5071
- if (!Array.isArray(entries)) return "";
5072
- let recent = "";
5073
- for (const one of entries) {
5074
- if (typeof one !== "object" || one === null) continue;
5075
- const candidate = one["key"];
5076
- if (typeof candidate !== "string" || candidate === "") continue;
5077
- if (candidate === key) return key;
5078
- if (candidate.startsWith(`${key}#`)) recent = candidate;
5079
- }
5080
- return recent;
5081
- }
5082
- function registerNetTools(ctx, tools) {
5083
- async function netDetail(page, key) {
5084
- return await actBrowser(page.shell, page.command, page.session, [
5085
- "network",
5086
- "--detail",
5087
- key,
5088
- "--max-body",
5089
- String(NET_BODY_MAX)
5090
- ]);
5091
- }
5092
- async function netFileOf(exec, raw) {
5093
- const page = await pageAt(ctx, exec, raw, {
5094
- requireWorkbench: false,
5095
- requireUnit: false
5096
- });
5097
- return {
5098
- page,
5099
- file: join(dirname(page.packagesDir), "shots", `net-${page.session}.jsonl`)
5100
- };
5101
- }
5102
- const netWatchTool = {
5103
- name: DIY_NET_WATCH_TOOL,
5104
- description: `把网络捕获臂上(opencli 的 network 不臂上就收不到):随后你在这页上做的事都会被记下(最多 ${String(NET_CAP_LINES)} 行 / ${String(Math.round(15))} 分钟后自己收工)。`,
5105
- parameters: pageSchema(),
5106
- output: OUTPUT$2,
5107
- async execute(raw, exec) {
5108
- const { page, file } = await netFileOf(exec, raw);
5109
- await writeShotsDir(page.shell, dirname(file));
5110
- await stopNetwork(page.shell, page.session, file);
5111
- const armed = await armNetwork(page.shell, page.command, page.session, file);
5112
- return plainJson({
5113
- workshop: page.packagesDir,
5114
- unit: page.unit,
5115
- ok: armed.ok,
5116
- pid: armed.pid,
5117
- capLines: NET_CAP_LINES,
5118
- capSeconds: 900,
5119
- text: armed.ok ? `捕获臂上了(pid ${armed.pid};最多 ${String(NET_CAP_LINES)} 行、${String(900)} 秒后自己收工)。现在去做你要做的事,回头用 ${DIY_NET_READ_TOOL} 读;读完 ${DIY_NET_STOP_TOOL} 可以提前收工。` : `臂不上:${armed.reason}`
5120
- });
4488
+ function clipNote(clip, viewport) {
4489
+ if (viewport.width <= 0 || viewport.height <= 0) return "没读到视口尺寸(那条量 dpr / innerWidth 的 eval 没回来)—— 给的是整屏图,裁不了";
4490
+ if (clip.x + clip.width <= viewport.width && clip.y + clip.height <= viewport.height && viewport.width > 0) return "";
4491
+ return `要裁的那块 ${String(clip.x)},${String(clip.y)} ${String(clip.width)}×${String(clip.height)} 不在视口里(视口 ${String(viewport.width)}×${String(viewport.height)})—— 给的是整屏图`;
4492
+ }
4493
+ function defaultShotPath(packagesDir, unit, stamp) {
4494
+ return join(dirname(packagesDir), "shots", `${unit === "" ? "page" : unit}-${String(stamp)}.png`);
4495
+ }
4496
+ var BROWSER_TOOLS = [
4497
+ {
4498
+ tool: DIY_STATE_TOOL,
4499
+ verb: ["state"],
4500
+ description: "这一页有什么:URL / 标题 / 可点的元素与索引。",
4501
+ argsHelp: "opencli browser state 的参数(如 --source ax)。"
4502
+ },
4503
+ {
4504
+ tool: DIY_HTML_TOOL,
4505
+ verb: ["get", "html"],
4506
+ description: "取 DOM:默认整页 HTML,--as json 给 {tag,attrs,text,children} 树。",
4507
+ argsHelp: "opencli browser get html 的参数(--selector / --as json / --depth / --children-max / --text-max)。",
4508
+ css: {
4509
+ kind: "flag",
4510
+ flag: "--selector"
5121
4511
  }
5122
- };
5123
- const netReadTool = {
5124
- name: DIY_NET_READ_TOOL,
5125
- description: "读刚才臂上之后记下的网络(给 key 就取那一条的完整 body)。",
5126
- parameters: pageSchema({
5127
- lines: {
5128
- type: "number",
5129
- description: "读最近几条(默认 60)。"
5130
- },
5131
- failed: {
5132
- type: "boolean",
5133
- description: "只看失败请求(0 或 >= 400)。"
5134
- },
5135
- grep: {
5136
- type: "string",
5137
- description: "再按 URL 里的一段词收。"
5138
- },
5139
- key: {
5140
- type: "string",
5141
- description: "给 key = 取这一条请求的完整 body(read 的返回里带 key)。"
5142
- }
5143
- }),
5144
- output: OUTPUT$2,
5145
- async execute(raw, exec) {
5146
- const { page, file } = await netFileOf(exec, raw);
5147
- const key = asText$1(page.args["key"]);
5148
- if (key !== "") {
5149
- const tried = [key];
5150
- let used = key;
5151
- let detail = await netDetail(page, key);
5152
- for (let n = 2; n <= SIBLING_TRIES && !detail.ok; n += 1) {
5153
- const sibling = `${key}#${String(n)}`;
5154
- const attempt = await netDetail(page, sibling);
5155
- tried.push(sibling);
5156
- if (attempt.ok) {
5157
- detail = attempt;
5158
- used = sibling;
5159
- }
5160
- }
5161
- let refreshed = false;
5162
- if (!detail.ok) {
5163
- const capture = await actBrowser(page.shell, page.command, page.session, ["network"]);
5164
- const grabbed = Array.isArray(capture.payload?.["entries"]) ? capture.payload["entries"].length : 0;
5165
- refreshed = capture.ok && grabbed > 0;
5166
- if (refreshed) {
5167
- const retry = await netDetail(page, key);
5168
- tried.push(key);
5169
- if (retry.ok) {
5170
- detail = retry;
5171
- used = key;
5172
- } else {
5173
- const alt = nearestKeyOf(capture.payload, key);
5174
- if (alt !== "" && alt !== key) {
5175
- const attempt = await netDetail(page, alt);
5176
- tried.push(alt);
5177
- if (attempt.ok) {
5178
- detail = attempt;
5179
- used = alt;
5180
- }
5181
- }
5182
- }
5183
- }
5184
- }
5185
- return plainJson({
5186
- workshop: page.packagesDir,
5187
- unit: page.unit,
5188
- ok: detail.ok,
5189
- key: used,
5190
- tried,
5191
- refreshed,
5192
- detail: detail.payload,
5193
- 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 在页面里直接读更快。`
5194
- });
5195
- }
5196
- const lines = asNumber(page.args["lines"]) ?? 60;
5197
- const onlyFailed = page.args["failed"] === true;
5198
- const grep = asText$1(page.args["grep"]);
5199
- const all = await readNetwork(page.shell, file, Math.max(1, Math.min(500, lines)));
5200
- const entries = all.filter((one) => {
5201
- const status = typeof one["status"] === "number" ? one["status"] : 0;
5202
- if (onlyFailed && !(status === 0 || status >= 400)) return false;
5203
- if (grep !== "" && !String(one["url"] ?? "").includes(grep)) return false;
5204
- return true;
5205
- });
5206
- return plainJson({
5207
- workshop: page.packagesDir,
5208
- unit: page.unit,
5209
- ok: true,
5210
- count: entries.length,
5211
- seen: all.length,
5212
- entries: entries.slice(-40),
5213
- text: all.length === 0 ? "这份捕获是空的 —— 先用 workbench_diy_net_watch 臂上,再去做那件事,然后回来读。" : `${String(entries.length)}/${String(all.length)} 条(最近的在后面)。每条的 key 可以拿去取完整 body:${DIY_NET_READ_TOOL} key="<某条的 key>"。`
5214
- });
4512
+ },
4513
+ {
4514
+ tool: DIY_FIND_TOOL,
4515
+ verb: ["find"],
4516
+ description: "按 CSS 或语义找元素,回 {matches_n, entries[]}(ref 只在那一次快照里有效)。",
4517
+ argsHelp: "opencli browser find 的参数(--css / --role / --name / --testid / --limit / --text-max)。",
4518
+ css: {
4519
+ kind: "flag",
4520
+ flag: "--css"
5215
4521
  }
5216
- };
5217
- const netStopTool = {
5218
- name: DIY_NET_STOP_TOOL,
5219
- description: "网络捕获收工(把臂上的那条 follower 停掉)。",
5220
- parameters: pageSchema(),
5221
- output: OUTPUT$2,
5222
- async execute(raw, exec) {
5223
- const { page, file } = await netFileOf(exec, raw);
5224
- const stopped = await stopNetwork(page.shell, page.session, file);
5225
- return plainJson({
5226
- workshop: page.packagesDir,
5227
- unit: page.unit,
5228
- ok: stopped,
5229
- text: stopped ? `捕获收工了(follower 与看门狗都停掉)。` : `收工那一下没跑成(命令退出码非 0)—— 再调一次 ${DIY_NET_STOP_TOOL};还不行看一眼 shots/ 那份 .pid / .reaper。`
5230
- });
4522
+ },
4523
+ {
4524
+ tool: DIY_CLICK_TOOL,
4525
+ verb: ["click"],
4526
+ description: "点一个元素。",
4527
+ argsHelp: "opencli browser click 的参数(选择器 / --role / --name / --nth)。",
4528
+ css: {
4529
+ kind: "positional",
4530
+ at: 0
5231
4531
  }
5232
- };
5233
- return [
5234
- tools.register(netWatchTool),
5235
- tools.register(netReadTool),
5236
- tools.register(netStopTool)
5237
- ];
5238
- }
5239
- //#endregion
5240
- //#region src/modules/panels/model/diy-act.ts
5241
- var TARGET_PROPS = {
5242
- selector: {
5243
- type: "string",
5244
- description: "卡内的 CSS 选择器(默认只在这张卡里找;scope=page 才是整页)。"
5245
4532
  },
5246
- role: {
5247
- type: "string",
5248
- description: "语义定位:角色(button / link / textbox / option…)。"
4533
+ {
4534
+ tool: DIY_HOVER_TOOL,
4535
+ verb: ["hover"],
4536
+ description: "把鼠标移到元素上。",
4537
+ argsHelp: "opencli browser hover 的参数(选择器 / --role / --name)。",
4538
+ css: {
4539
+ kind: "positional",
4540
+ at: 0
4541
+ }
5249
4542
  },
5250
- name: {
5251
- type: "string",
5252
- description: "语义定位:无障碍名(aria-label / 可见文字)包含这段。"
4543
+ {
4544
+ tool: DIY_TYPE_TOOL,
4545
+ verb: ["type"],
4546
+ description: "点一下再敲字。",
4547
+ argsHelp: "opencli browser type 的参数(选择器在前,然后是字)。",
4548
+ css: {
4549
+ kind: "positional",
4550
+ at: 0
4551
+ }
5253
4552
  },
5254
- testid: {
5255
- type: "string",
5256
- description: "语义定位:data-testid 包含这段。"
4553
+ {
4554
+ tool: DIY_FILL_TOOL,
4555
+ verb: ["fill"],
4556
+ description: "直接把值设进去。",
4557
+ argsHelp: "opencli browser fill 的参数(选择器在前,然后是值)。",
4558
+ css: {
4559
+ kind: "positional",
4560
+ at: 0
4561
+ }
5257
4562
  },
5258
- nth: {
5259
- type: "number",
5260
- description: "匹配到多个时取第几个(0 起)。"
4563
+ {
4564
+ tool: DIY_KEYS_TOOL,
4565
+ verb: ["keys"],
4566
+ description: "给焦点发按键。",
4567
+ argsHelp: "opencli browser keys 的参数(如 Enter / Meta+K)。"
5261
4568
  },
5262
- scope: {
5263
- type: "string",
5264
- description: "card(默认,只在这张卡里找)| page(整页找)。"
4569
+ {
4570
+ tool: DIY_SCROLL_TOOL,
4571
+ verb: ["scroll"],
4572
+ description: "滚一屏。",
4573
+ argsHelp: "opencli browser scroll 的参数(up|down / --amount)。"
4574
+ },
4575
+ {
4576
+ tool: DIY_WAIT_TOOL,
4577
+ verb: ["wait"],
4578
+ description: "等一件事:选择器 / 文字 / 时间 / XHR / 下载。",
4579
+ argsHelp: "opencli browser wait 的参数(selector|text|time|xhr|download + --timeout)。",
4580
+ css: {
4581
+ kind: "positional",
4582
+ at: 1,
4583
+ onlyWhenFirst: "selector"
4584
+ }
4585
+ },
4586
+ {
4587
+ tool: DIY_EVAL_TOOL,
4588
+ verb: ["eval"],
4589
+ description: "在页面里跑一段 JS(return 什么就回什么)。",
4590
+ argsHelp: "opencli browser eval 的参数:一段 JS(in=card 时可用注入的 root)。",
4591
+ evalInCard: true
4592
+ },
4593
+ {
4594
+ tool: DIY_SCREENSHOT_TOOL,
4595
+ verb: ["screenshot"],
4596
+ description: "拍一张图(写文件;clip 给 CSS px 就只裁那一块)。",
4597
+ argsHelp: "opencli browser screenshot 的参数:文件路径在前,然后 --full-page / --width / --height / --annotate。",
4598
+ clip: true
4599
+ },
4600
+ {
4601
+ tool: DIY_NETWORK_TOOL,
4602
+ verb: ["network"],
4603
+ description: "看网络:捕获到的请求形状(URL / 方法 / key);body 取不到,要 body 用 _eval 里 fetch 打一条。",
4604
+ argsHelp: "opencli browser network 的参数(--since / --until / --detail / --filter / --all / --raw / --follow)。"
4605
+ },
4606
+ {
4607
+ tool: DIY_DIALOG_TOOL,
4608
+ verb: ["dialog"],
4609
+ description: "处理 JS 弹窗。",
4610
+ argsHelp: "opencli browser dialog 的参数(accept | dismiss)。"
5265
4611
  }
5266
- };
5267
- function targetSchema(extra = {}, required = []) {
5268
- return {
5269
- type: "object",
5270
- properties: {
5271
- unit: UNIT_PROP,
5272
- ...TARGET_PROPS,
5273
- ...extra,
5274
- session: SESSION_PROP
4612
+ ];
4613
+ function parametersOf(row) {
4614
+ const properties = {
4615
+ unit: UNIT_PROP,
4616
+ args: {
4617
+ type: "array",
4618
+ items: { type: "string" },
4619
+ description: row.argsHelp
5275
4620
  },
5276
- ...required.length === 0 ? {} : { required },
5277
- additionalProperties: false
5278
- };
5279
- }
5280
- function hasTarget(target) {
5281
- return target.selector !== "" || target.role !== "" || target.name !== "" || target.testid !== "";
5282
- }
5283
- function describeTarget(target, selector) {
5284
- if (selector !== "") return `\`${selector}\``;
5285
- const bits = [];
5286
- if (target.role !== "") bits.push(`role=${target.role}`);
5287
- if (target.name !== "") bits.push(`name=${target.name}`);
5288
- if (target.testid !== "") bits.push(`testid=${target.testid}`);
5289
- return bits.join(" · ");
5290
- }
5291
- function renderAct(verb, target, selector, step) {
5292
- const matches = matchesOf(step);
5293
- if (matches === 0) return `没找到要${verb}的东西(${describeTarget(target, selector)})—— 用 workbench_diy_look 的 dom=outline 看一眼这张卡里到底有什么。`;
5294
- const error = actErrorOf(step);
5295
- if (error !== "") return `${verb}没成:${error}`;
5296
- const many = matches > 1 ? `(匹配 ${String(matches)} 个,用的是第 ${String((target.nth ?? 0) + 1)} 个)` : "";
5297
- return `${verb}了 ${describeTarget(target, selector)}${many}。看一眼 workbench_diy_look 就知道这一下有没有生效。`;
5298
- }
5299
- function registerActTools(ctx, tools) {
5300
- async function resolveTarget(page, target, verb) {
5301
- if (target.selector !== "") return {
5302
- selector: scopedSelectorOf(page.unit, target),
5303
- options: [],
5304
- token: "",
5305
- via: "css"
5306
- };
5307
- const semantic = semanticOptionsOf(target);
5308
- if (target.page || semantic.length === 0) return {
5309
- selector: "",
5310
- options: targetOptionsOf(target),
5311
- token: "",
5312
- via: "page-semantic"
5313
- };
5314
- const found = await actBrowser(page.shell, page.command, page.session, [
5315
- "find",
5316
- ...semantic,
5317
- "--limit",
5318
- "200"
5319
- ]);
5320
- const refs = refsOf(found.payload);
5321
- const total = totalMatchesOf(found.payload) || refs.length;
5322
- if (refs.length === 0) throw new Error(`${verb}谁?这一页上没有匹配 ${describeTarget(target, "")} 的东西 —— 用 workbench_diy_look 的 dom=outline 看一眼这张卡里有什么,或者换成卡内 CSS(selector)。`);
5323
- const token = `diy-${Date.now().toString(36)}`;
5324
- const nth = target.nth ?? 0;
5325
- const marked = await actBrowser(page.shell, page.command, page.session, ["eval", scopeMarkScriptOf(page.unit, refs, nth, token)]);
5326
- if (marked.payload?.["ok"] !== true) {
5327
- const inside = marked.payload?.["inside"];
5328
- const why = marked.payload?.["code"] === "no-card" ? `这一页上没有 ${page.unit} 这张卡` : `卡内只有 ${String(typeof inside === "number" ? inside : 0)} 个匹配,拿不出第 ${String(nth + 1)} 个`;
5329
- throw new Error(`${describeTarget(target, "")} 这一页上匹配了 ${String(total)} 个,但${why} —— 是不是 unit 给错了?也可以在 selector 里点名卡内 CSS,或 scope=page 明说要整页那一个。`);
4621
+ timeoutMs: {
4622
+ type: "number",
4623
+ description: "超时毫秒(默认 30000;≤0 或非数也按默认算)。"
5330
4624
  }
5331
- return {
5332
- selector: scopedMarkerSelectorOf(page.unit, token),
5333
- options: [],
5334
- token,
5335
- via: "card-semantic"
5336
- };
5337
- }
5338
- async function runTargeted(exec, raw, verb, build, needSelector = false) {
5339
- const page = await pageAt(ctx, exec, raw);
5340
- const target = actTargetOf(page.args);
5341
- if (!hasTarget(target)) throw new Error(`${verb}谁?给一个 selector(卡内 CSS),或 role / name / testid 之一。`);
5342
- const resolved = await resolveTarget(page, target, verb);
5343
- if (needSelector && resolved.selector === "") throw new Error(`${verb}要一个 selector:输入框得点名才敢动手(语义定位请去掉 scope=page)。`);
5344
- const step = await actBrowser(page.shell, page.command, page.session, [...build(resolved.selector, target), ...resolved.options]);
5345
- if (resolved.token !== "") await actBrowser(page.shell, page.command, page.session, ["eval", scopeClearScriptOf(resolved.token)]);
5346
- return plainJson({
5347
- workshop: page.packagesDir,
5348
- unit: page.unit,
5349
- ok: step.ok,
5350
- verb,
5351
- via: resolved.via,
5352
- matches: matchesOf(step),
5353
- text: renderAct(verb, target, target.selector === "" ? "" : resolved.selector, step) + (resolved.via === "card-semantic" ? `(语义定位收进了 ${page.unit} 这张卡)` : "")
5354
- });
5355
- }
5356
- const clickTool = {
5357
- name: DIY_CLICK_TOOL,
5358
- description: "在这张卡里点一下(按钮 / 链接 / 行)。默认只在这张卡里找目标,找不到就直说。",
5359
- parameters: targetSchema(),
5360
- output: OUTPUT$2,
5361
- execute: (raw, exec) => runTargeted(exec, raw, "点", (selector) => selector === "" ? ["click"] : ["click", selector])
5362
4625
  };
5363
- const typeTool = {
5364
- name: DIY_TYPE_TOOL,
5365
- description: "往这张卡里的某个输入框敲字(先点它再敲)—— 要一个 selector 与 value。",
5366
- parameters: targetSchema({ value: {
5367
- type: "string",
5368
- description: "要敲进去的字。"
5369
- } }, ["selector", "value"]),
5370
- output: OUTPUT$2,
5371
- execute: (raw, exec) => runTargeted(exec, raw, "敲", (selector) => [
5372
- "type",
5373
- selector,
5374
- asText$1(asRecord$3(raw)["value"])
5375
- ], true)
5376
- };
5377
- const fillTool = {
5378
- name: DIY_FILL_TOOL,
5379
- description: "把这张卡里某个输入框的值直接设成这段字并核对(不改别处)—— 要一个 selector 与 value。",
5380
- parameters: targetSchema({ value: {
5381
- type: "string",
5382
- description: "要设进去的值。"
5383
- } }, ["selector", "value"]),
5384
- output: OUTPUT$2,
5385
- execute: (raw, exec) => runTargeted(exec, raw, "填", (selector) => [
5386
- "fill",
5387
- selector,
5388
- asText$1(asRecord$3(raw)["value"])
5389
- ], true)
5390
- };
5391
- const keysTool = {
5392
- name: DIY_KEYS_TOOL,
5393
- description: "在这一页上按一个键(Enter / Escape / Tab / Control+a)—— 收下拉、提交、挪焦点用它。",
5394
- parameters: pageSchema({ key: {
5395
- type: "string",
5396
- description: "要按的键。"
5397
- } }, ["key"]),
5398
- output: OUTPUT$2,
5399
- async execute(raw, exec) {
5400
- const page = await pageAt(ctx, exec, raw, { requireUnit: false });
5401
- const key = asText$1(page.args["key"]);
5402
- if (key === "") throw new Error("按哪个键?给 key(Enter / Escape / Tab / Control+a)。");
5403
- const step = await actBrowser(page.shell, page.command, page.session, ["keys", key]);
5404
- const error = actErrorOf(step);
5405
- return plainJson({
5406
- workshop: page.packagesDir,
5407
- unit: page.unit,
5408
- ok: step.ok,
5409
- verb: "keys",
5410
- text: error === "" ? `按了 ${key}。` : `按键没成:${error}`
5411
- });
5412
- }
4626
+ if (row.css !== void 0) properties["scope"] = {
4627
+ type: "string",
4628
+ enum: ["page", "card"],
4629
+ description: "card = 把 CSS 目标收进这张卡;默认 page(原样转发)。"
5413
4630
  };
5414
- const hoverTool = {
5415
- name: DIY_HOVER_TOOL,
5416
- description: "把鼠标移到这张卡里的某个东西上(悬停才出来的东西靠它)。",
5417
- parameters: targetSchema(),
5418
- output: OUTPUT$2,
5419
- execute: (raw, exec) => runTargeted(exec, raw, "悬停", (selector) => selector === "" ? ["hover"] : ["hover", selector])
4631
+ if (row.evalInCard === true) properties["in"] = {
4632
+ type: "string",
4633
+ enum: ["page", "card"],
4634
+ description: "card = 脚本在卡根里跑(注入 root;脚本自己 return)。"
5420
4635
  };
5421
- const selectTool = {
5422
- name: DIY_SELECT_TOOL,
5423
- description: "在这张卡的下拉里选一项 —— 要一个 selector 与选项文字。",
5424
- parameters: targetSchema({ value: {
5425
- type: "string",
5426
- description: "选项文字(或 value)。"
5427
- } }, ["selector", "value"]),
5428
- output: OUTPUT$2,
5429
- execute: (raw, exec) => runTargeted(exec, raw, "选", (selector) => [
5430
- "select",
5431
- selector,
5432
- asText$1(asRecord$3(raw)["value"])
5433
- ], true)
5434
- };
5435
- const scrollTool = {
5436
- name: DIY_SCROLL_TOOL,
5437
- description: "滚这一页(direction + amount),或把这张卡 / 卡里某块滚到看得见(into)。",
5438
- parameters: pageSchema({
5439
- direction: {
5440
- type: "string",
5441
- description: "up | down(不给 into 时用它)。"
4636
+ if (row.clip === true) properties["clip"] = {
4637
+ type: "object",
4638
+ properties: {
4639
+ x: {
4640
+ type: "number",
4641
+ description: "左边(CSS px)。"
5442
4642
  },
5443
- amount: {
4643
+ y: {
5444
4644
  type: "number",
5445
- description: "滚多少像素(默认 500)。"
4645
+ description: "上边(CSS px)。"
5446
4646
  },
5447
- into: {
5448
- type: "string",
5449
- description: "滚到看得见:留空 = 这张卡;填卡内 CSS = 卡里那一块。"
5450
- }
5451
- }),
5452
- output: OUTPUT$2,
5453
- async execute(raw, exec) {
5454
- const page = await pageAt(ctx, exec, raw);
5455
- const into = page.args["into"];
5456
- if (into !== void 0) {
5457
- const step = await scrollIntoView(page.shell, page.command, page.session, page.unit, asText$1(into));
5458
- const found = step.payload?.["found"] === true;
5459
- return plainJson({
5460
- workshop: page.packagesDir,
5461
- unit: page.unit,
5462
- ok: step.ok && found,
5463
- verb: "scroll",
5464
- text: actErrorOf(step) !== "" ? `滚不动:${actErrorOf(step)}` : found ? "滚到看得见了。" : `卡里没有要滚的那一块(${asText$1(into) || "卡片本身"})。`
5465
- });
5466
- }
5467
- const direction = asText$1(page.args["direction"]) || "down";
5468
- const amount = asNumber(page.args["amount"]) ?? 500;
5469
- const step = await actBrowser(page.shell, page.command, page.session, [
5470
- "scroll",
5471
- direction,
5472
- "--amount",
5473
- String(amount)
5474
- ]);
5475
- const error = actErrorOf(step);
5476
- return plainJson({
5477
- workshop: page.packagesDir,
5478
- unit: page.unit,
5479
- ok: step.ok,
5480
- verb: "scroll",
5481
- text: error === "" ? `这一页往${direction === "up" ? "上" : "下"}滚了 ${String(amount)}px。` : `滚不动:${error}`
5482
- });
5483
- }
5484
- };
5485
- const reloadTool = {
5486
- name: DIY_RELOAD_TOOL,
5487
- description: "刷新用户正开着的这一页(页面上跑的外壳/共享包是旧的一份时用它;卡片自己会热更新,一般不用)。",
5488
- parameters: pageSchema(),
5489
- output: OUTPUT$2,
5490
- async execute(raw, exec) {
5491
- const page = await pageAt(ctx, exec, raw, {
5492
- requireWorkbench: false,
5493
- requireUnit: false
5494
- });
5495
- const step = await reloadPage(page.shell, page.command, page.session);
5496
- const error = actErrorOf(step);
5497
- return plainJson({
5498
- workshop: page.packagesDir,
5499
- unit: page.unit,
5500
- ok: step.ok,
5501
- verb: "reload",
5502
- text: error === "" ? "让这一页自己刷新了(等两秒再看)。" : `刷新没成:${error}`
5503
- });
5504
- }
5505
- };
5506
- const viewportTool = {
5507
- name: DIY_VIEWPORT_TOOL,
5508
- description: "拍整屏图。不给宽高 = 按用户当前窗口拍;给了才换视口 —— 会回流一次。",
5509
- parameters: pageSchema({
5510
4647
  width: {
5511
4648
  type: "number",
5512
- description: "视口宽度(CSS px,如 390);不填按当前窗口。"
4649
+ description: "宽(CSS px)。"
5513
4650
  },
5514
4651
  height: {
5515
4652
  type: "number",
5516
- description: "视口高度(可不要)。"
4653
+ description: "高(CSS px)。"
5517
4654
  }
5518
- }, []),
5519
- output: OUTPUT$2,
5520
- async execute(raw, exec) {
5521
- const page = await pageAt(ctx, exec, raw, { requireUnit: false });
5522
- const width = asNumber(page.args["width"]) ?? 0;
5523
- if (width < 0) throw new Error("width 得是正的 CSS px;不给就按用户当前窗口拍。");
5524
- const height = asNumber(page.args["height"]) ?? 0;
5525
- const overrode = width > 0 || height > 0;
5526
- const stamp = overrode ? `${String(width)}x${String(height)}` : "now";
5527
- const file = join(dirname(page.packagesDir), "shots", `${page.unit || "page"}-${stamp}-${String(Date.now())}.png`);
4655
+ },
4656
+ required: [
4657
+ "x",
4658
+ "y",
4659
+ "width",
4660
+ "height"
4661
+ ],
4662
+ additionalProperties: false,
4663
+ description: "只要这一块:拍完本地裁;不在视口里就退回整屏图。"
4664
+ };
4665
+ return {
4666
+ type: "object",
4667
+ properties,
4668
+ additionalProperties: false
4669
+ };
4670
+ }
4671
+ async function viewportOf(page) {
4672
+ const parsed = jsonOf((await runBrowserStep(page.shell, page.command, [
4673
+ "browser",
4674
+ page.session,
4675
+ "eval",
4676
+ VIEWPORT_SCRIPT,
4677
+ "--window",
4678
+ "background"
4679
+ ], LOOK_TIMEOUT_MS)).stdout);
4680
+ return {
4681
+ dpr: asNumber(parsed?.["dpr"]) ?? 1,
4682
+ width: asNumber(parsed?.["width"]) ?? 0,
4683
+ height: asNumber(parsed?.["height"]) ?? 0
4684
+ };
4685
+ }
4686
+ function needsCardOf(row, raw) {
4687
+ if (row.evalInCard === true && asText$1(raw["in"]) === "card") return true;
4688
+ if (row.css !== void 0 && asText$1(raw["scope"]) === "card") return true;
4689
+ return stringsOf$2(raw["args"]).some((one) => one.includes(CARD_TOKEN));
4690
+ }
4691
+ async function runBrowserTool(row, ctx, raw, exec) {
4692
+ const page = await pageAt(ctx, exec, raw, { requireUnit: false });
4693
+ if (needsCardOf(row, page.args) && page.unit === "") throw new Error("要卡作用域(scope=card / $card / in=card)就得给一个 unit —— 工坊里不止一个模块时,我不知道你说哪一张卡。");
4694
+ const requested = asNumber(page.args["timeoutMs"]);
4695
+ const timeoutMs = requested !== void 0 && requested > 0 ? requested : DEFAULT_TIMEOUT_MS;
4696
+ const argv = argvOf(row, page.args, page.unit);
4697
+ const clip = row.clip === true ? clipOf(page.args["clip"]) : void 0;
4698
+ const clipAsked = row.clip === true && page.args["clip"] !== void 0;
4699
+ let file = "";
4700
+ let finalArgv = argv;
4701
+ if (row.clip === true) {
4702
+ const given = argv[0] !== void 0 && !argv[0].startsWith("-") ? argv[0] : "";
4703
+ if (given === "") {
4704
+ file = defaultShotPath(page.packagesDir, page.unit, Date.now());
4705
+ finalArgv = [file, ...argv];
5528
4706
  await writeShotsDir(page.shell, dirname(file));
5529
- const shot = await shootAtWidth(page.shell, page.command, page.session, file, width, height);
4707
+ } else file = given;
4708
+ }
4709
+ const step = await runBrowserStep(page.shell, page.command, [
4710
+ "browser",
4711
+ page.session,
4712
+ ...row.verb,
4713
+ ...finalArgv,
4714
+ "--window",
4715
+ "background"
4716
+ ], timeoutMs);
4717
+ const shot = {};
4718
+ if (row.clip === true) {
4719
+ shot["file"] = step.ok ? file : "";
4720
+ if (clipAsked && clip === void 0) {
4721
+ shot["cropped"] = false;
4722
+ shot["cropReason"] = "clip 不是完整矩形(要 x/y ≥ 0,width/height > 0)—— 给的是整屏图";
4723
+ } else if (clip !== void 0 && step.ok) {
4724
+ const viewport = await viewportOf(page);
4725
+ const note = clipNote(clip, viewport);
4726
+ if (note === "") {
4727
+ const crop = cropPngFile(file, file, deviceClipRect(clip, viewport.dpr));
4728
+ shot["cropped"] = crop.ok;
4729
+ shot["cropReason"] = crop.reason;
4730
+ } else {
4731
+ shot["cropped"] = false;
4732
+ shot["cropReason"] = note;
4733
+ }
4734
+ }
4735
+ }
4736
+ return {
4737
+ workshop: page.packagesDir,
4738
+ unit: page.unit,
4739
+ verb: row.verb.join(" "),
4740
+ argv: finalArgv,
4741
+ ok: step.ok,
4742
+ exitCode: step.exitCode,
4743
+ timedOut: step.timedOut,
4744
+ truncated: step.truncated,
4745
+ stdout: step.stdout,
4746
+ stderr: step.stderr,
4747
+ ...shot
4748
+ };
4749
+ }
4750
+ function registerBrowserTools(ctx, tools) {
4751
+ return BROWSER_TOOLS.map((row) => {
4752
+ const definition = {
4753
+ name: row.tool,
4754
+ description: row.description,
4755
+ parameters: parametersOf(row),
4756
+ output: OUTPUT$2,
4757
+ execute: async (raw, exec) => plainJson(await runBrowserTool(row, ctx, raw, exec))
4758
+ };
4759
+ return tools.register(definition);
4760
+ });
4761
+ }
4762
+ //#endregion
4763
+ //#region src/modules/panels/model/diy-spec.ts
4764
+ var SPEC_TIMEOUT_MS = 2e4;
4765
+ function specArgvOf(command) {
4766
+ const parts = command.trim().replace(/\//g, " ").split(/\s+/).filter((one) => one !== "");
4767
+ if (parts.length !== 2) return [];
4768
+ return [
4769
+ parts[0] ?? "",
4770
+ parts[1] ?? "",
4771
+ "--help",
4772
+ "-f",
4773
+ "json"
4774
+ ];
4775
+ }
4776
+ function specTool(ctx) {
4777
+ return {
4778
+ name: DIY_SPEC_TOOL,
4779
+ description: "问一条 opencli 命令的形状(入参 / 出参列 / access / 预览档):`--help -f json` 原样回,插件与内置都能问。",
4780
+ parameters: {
4781
+ type: "object",
4782
+ properties: {
4783
+ command: {
4784
+ type: "string",
4785
+ description: "site 与名字两个词,例 `cmdb publish`(`cmdb/publish` 也认)。"
4786
+ },
4787
+ timeoutMs: {
4788
+ type: "number",
4789
+ description: "超时毫秒(默认 20000)。"
4790
+ }
4791
+ },
4792
+ required: ["command"],
4793
+ additionalProperties: false
4794
+ },
4795
+ output: OUTPUT$2,
4796
+ async execute(raw, _exec) {
4797
+ const args = asRecord$3(raw);
4798
+ const argv = specArgvOf(asText$1(args["command"]));
4799
+ if (argv.length === 0) throw new Error("给一条命令,site 与名字两个词 —— 例 `cmdb publish`(`cmdb/publish` 也认)。");
4800
+ const shell = ctx.get("shell");
4801
+ if (shell === void 0) throw new Error("这台机器上没有 shell 服务,问不了 opencli");
4802
+ const asked = asNumber(args["timeoutMs"]);
4803
+ const timeoutMs = asked !== void 0 && asked > 0 ? asked : SPEC_TIMEOUT_MS;
4804
+ const result = await runShell(shell, backgroundLine(opencliCommandOf(ctx).command, argv), { timeoutMs });
4805
+ const spec = jsonOf(result.stdout);
5530
4806
  return plainJson({
5531
- workshop: page.packagesDir,
5532
- unit: page.unit,
5533
- ok: shot.ok,
5534
- file: shot.file,
5535
- overrode,
5536
- width: overrode ? width : null,
5537
- viewport: overrode ? {
5538
- width,
5539
- height
5540
- } : null,
5541
- text: shot.ok ? overrode ? `重拍了一张:${shot.file}(临时换了视口${width > 0 ? ` 宽 ${String(width)}px` : ""}${height > 0 ? ` 高 ${String(height)}px` : ""},你那一屏会回流一次、拍完就恢复)。` : `按你当前窗口拍了一张:${shot.file}(没动你的视口)。` : `没拍成:${shot.reason}`
4807
+ ok: result.exitCode === 0 && !result.timedOut,
4808
+ exitCode: result.exitCode,
4809
+ timedOut: result.timedOut,
4810
+ truncated: result.truncated,
4811
+ stdout: result.stdout.trim(),
4812
+ stderr: result.stderr.trim(),
4813
+ ...spec === void 0 ? {} : { spec }
5542
4814
  });
5543
4815
  }
5544
4816
  };
5545
- return [
5546
- tools.register(clickTool),
5547
- tools.register(typeTool),
5548
- tools.register(fillTool),
5549
- tools.register(keysTool),
5550
- tools.register(hoverTool),
5551
- tools.register(selectTool),
5552
- tools.register(scrollTool),
5553
- tools.register(reloadTool),
5554
- tools.register(viewportTool)
5555
- ];
4817
+ }
4818
+ function registerSpecTool(ctx, tools) {
4819
+ return [tools.register(specTool(ctx))];
5556
4820
  }
5557
4821
  //#endregion
5558
4822
  //#region src/shared/api/host/diy-recon.ts
5559
4823
  var SITE_LIMIT = 6;
5560
4824
  var COMMAND_LIMIT = 12;
5561
4825
  var SHOWN_LIMIT = 6;
4826
+ var ARG_LIMIT = 6;
5562
4827
  var PLUGIN_PREFIX = "opencli-plugin-";
4828
+ var PREVIEW_ARGS = [
4829
+ "yes",
4830
+ "dryrun",
4831
+ "confirm"
4832
+ ];
5563
4833
  function text(value) {
5564
4834
  return typeof value === "string" ? value.trim() : "";
5565
4835
  }
5566
4836
  function stringsOf$1(value) {
5567
4837
  return Array.isArray(value) ? value.filter((one) => typeof one === "string") : [];
5568
4838
  }
4839
+ function argsOf$1(value) {
4840
+ if (!Array.isArray(value)) return [];
4841
+ const out = [];
4842
+ for (const one of value) {
4843
+ if (typeof one !== "object" || one === null) continue;
4844
+ const row = one;
4845
+ const name = text(row["name"]);
4846
+ if (name === "") continue;
4847
+ const fallback = row["default"];
4848
+ out.push({
4849
+ name,
4850
+ type: text(row["type"]),
4851
+ required: row["required"] === true,
4852
+ value: typeof fallback === "number" || typeof fallback === "boolean" ? String(fallback) : text(fallback),
4853
+ help: text(row["help"])
4854
+ });
4855
+ }
4856
+ return out;
4857
+ }
4858
+ function invocableOf(command) {
4859
+ if (command.site === "") return command.command;
4860
+ return command.name === "" ? `opencli ${command.site}` : `opencli ${command.site} ${command.name}`;
4861
+ }
5569
4862
  function manifestCommandsOf(raw) {
5570
4863
  let parsed;
5571
4864
  try {
@@ -5584,6 +4877,7 @@ function manifestCommandsOf(raw) {
5584
4877
  const command = declared === "" ? site === "" || name === "" ? "" : `${site}/${name}` : declared;
5585
4878
  if (command === "" || site === "") continue;
5586
4879
  const access = text(row["access"]);
4880
+ const args = argsOf$1(row["args"]);
5587
4881
  commands.push({
5588
4882
  command,
5589
4883
  site,
@@ -5594,7 +4888,9 @@ function manifestCommandsOf(raw) {
5594
4888
  strategy: text(row["strategy"]).toLowerCase(),
5595
4889
  browser: row["browser"] === true,
5596
4890
  example: text(row["example"]),
5597
- columns: stringsOf$1(row["columns"])
4891
+ columns: stringsOf$1(row["columns"]),
4892
+ args,
4893
+ preview: args.some((one) => PREVIEW_ARGS.includes(one.name.toLowerCase()))
5598
4894
  });
5599
4895
  }
5600
4896
  return commands;
@@ -5692,13 +4988,29 @@ function reconOf(commands, plugins, keywords) {
5692
4988
  function nextOf(answer) {
5693
4989
  const site = answer.sites[0];
5694
4990
  if (site !== void 0) {
5695
- const pick = site.commands.find((one) => one.access !== "write") ?? site.commands[0];
5696
- return `直接复用:${pick === void 0 ? `opencli ${site.site}` : pick.example === "" ? `opencli ${pick.command}` : pick.example}(这台机器上就能跑,不用装、不用点头)`;
4991
+ const read = site.commands.find((one) => one.access !== "write");
4992
+ const pick = read ?? site.commands[0];
4993
+ if (pick === void 0) return `opencli ${site.site} --help 看全部命令`;
4994
+ if (read === void 0) return `这台机器上只匹配到写命令:${invocableOf(pick)} —— 先 \`--help\` 看入参;带 \`--dryRun\` / \`--yes\` 的先用预览档;没有预览档的不许试写(那一次就是真的)。写取证的完整口径见 WRITES.md。`;
4995
+ return `直接复用:${pick.example === "" ? invocableOf(pick) : pick.example}(这台机器上就能跑,不用装、不用点头)`;
5697
4996
  }
5698
4997
  const plugin = answer.plugins[0];
5699
4998
  if (plugin !== void 0) return `这台机器另装的插件 ${plugin.name} 里有匹配:opencli ${plugin.name} --help 看全部命令`;
5700
4999
  return "这台机器上没有现成的 —— 两条路:① 给这个站点写 adapter:opencli skills read opencli-adapter-author;② 只读一次:workbench_diy_probe(script 读接口 / css 读 DOM)";
5701
5000
  }
5001
+ function signatureOf$1(args) {
5002
+ const shown = args.slice(0, ARG_LIMIT).map((one) => {
5003
+ if (one.required) return `--${one.name}`;
5004
+ return one.value === "" ? `--${one.name}` : `--${one.name}=${one.value}`;
5005
+ });
5006
+ const rest = args.length - shown.length;
5007
+ const tail = rest > 0 ? ` …+${String(rest)}` : "";
5008
+ return `${shown.join(" ")}${tail}`;
5009
+ }
5010
+ function labelOf(command) {
5011
+ if (command.access !== "write") return "读";
5012
+ return command.preview ? "写(可预览)" : "写(无预览档,一次就是真的)";
5013
+ }
5702
5014
  function textOf$3(answer) {
5703
5015
  const lines = [];
5704
5016
  if (answer.sites.length > 0) {
@@ -5709,10 +5021,15 @@ function textOf$3(answer) {
5709
5021
  lines.push(`· ${site.site}${where} · ${site.strategy === "" ? "—" : site.strategy} · ${chrome}`);
5710
5022
  for (const command of site.commands.slice(0, SHOWN_LIMIT)) {
5711
5023
  const what = command.description === "" ? "" : ` —— ${command.description.slice(0, 60)}`;
5712
- lines.push(` ${command.access === "write" ? "写" : "读"} opencli ${command.command}${what}`);
5024
+ const args = command.access === "write" && command.args.length > 0 ? ` | 入参:${signatureOf$1(command.args)}` : "";
5025
+ lines.push(` ${labelOf(command)} ${invocableOf(command)}${what}${args}`);
5713
5026
  }
5714
5027
  const rest = site.commands.length - SHOWN_LIMIT;
5715
- if (rest > 0) lines.push(` … 还有 ${String(rest)} 条:opencli ${site.site} --help`);
5028
+ if (rest > 0) {
5029
+ const writes = site.commands.slice(SHOWN_LIMIT).filter((one) => one.access === "write").length;
5030
+ const tail = writes === 0 ? "" : `(其中写 ${String(writes)} 条)`;
5031
+ lines.push(` … 还有 ${String(rest)} 条${tail}:opencli ${site.site} --help`);
5032
+ }
5716
5033
  }
5717
5034
  }
5718
5035
  if (answer.plugins.length > 0) lines.push(`这台机器另装的插件里匹配到:${answer.plugins.map((one) => `${one.name}(${String(one.commands.length)} 条命令)`).join("、")}`);
@@ -6146,7 +5463,7 @@ function renderPanel(options) {
6146
5463
  function statusOf(options) {
6147
5464
  const { dir, unit, name, installed, known, open, health } = options;
6148
5465
  const facts = moduleBuildFacts(dir);
6149
- 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 刷新页面),不要再装一遍";
5466
+ 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 刷新页面),不要再装一遍";
6150
5467
  return {
6151
5468
  unit,
6152
5469
  name,
@@ -6385,7 +5702,7 @@ function registerDiyTools(ctx, market, panels, debug) {
6385
5702
  },
6386
5703
  session: {
6387
5704
  type: "string",
6388
- description: "会话名(默认 workbench,一般不用填)。"
5705
+ description: "会话名(默认读那条,一般不用填)。"
6389
5706
  },
6390
5707
  open: {
6391
5708
  type: "boolean",
@@ -6406,6 +5723,7 @@ function registerDiyTools(ctx, market, panels, debug) {
6406
5723
  const url = asText$1(args["url"]);
6407
5724
  const loginUrl = asText$1(args["loginUrl"]);
6408
5725
  const session = asText$1(args["session"]);
5726
+ if (session === "workbench") throw new Error(`"${BROWSER_SCREEN_SESSION}" 是用户那一屏那条会话 —— probe 只读自己的线(不填就落在读那条上,想给自己一条专线就传自己的 unit)。要看 / 动他那一屏走 workbench_diy_state / _find / _click 那一族,而且要先 workbench_diy_debug_start(会请他点头一次)。`);
6409
5727
  const open = typeof args["open"] === "boolean" ? args["open"] : void 0;
6410
5728
  if (css !== "") {
6411
5729
  const limit = asNumber(args["limit"]);
@@ -6467,9 +5785,9 @@ function registerDiyTools(ctx, market, panels, debug) {
6467
5785
  async execute(raw, exec) {
6468
5786
  const { packagesDir, workspace } = workshopOf(ctx, exec);
6469
5787
  const found = unitOf(packagesDir, raw, "workbench_diy_show");
6470
- if (!found.installed) throw new Error(`${found.unit} 还没装 —— 只上板的前提是它装过了。先用 workbench_diy_install(第一次会让用户点头一次)`);
5788
+ if (!found.installed) throw new Error(`${found.unit} 还没装 —— 只上板的前提是它装过了。先用 workbench_diy_install(第一次)`);
6471
5789
  const panel = showOrReason(panels, found.unit, workspace);
6472
- const text = panel.ok ? `面板上已经打开它了(${workspace})—— 让用户看一眼` : `面板上还没打开:${panel.reason}。这就是它的下一步(workbench_diy_status 会给出同样的判断)`;
5790
+ const text = panel.ok ? `面板上已经打开它了(${workspace})—— 验收就走这一张卡(你自己走一遍)` : `面板上还没打开:${panel.reason}。这就是它的下一步(workbench_diy_status 会给出同样的判断)`;
6473
5791
  return plainJson({
6474
5792
  workshop: packagesDir,
6475
5793
  unit: found.unit,
@@ -6497,7 +5815,7 @@ function registerDiyTools(ctx, market, panels, debug) {
6497
5815
  const found = unitOf(packagesDir, raw, "workbench_diy_install");
6498
5816
  if (found.installed) {
6499
5817
  const panel = showOrReason(panels, found.unit, workspace);
6500
- 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");
5818
+ const text = ["它已经装过了(就是工坊里这一份)—— **不要再装**:改源码 + dsh-wb-module build(宿主侧与 client.js 都秒级热换 —— dsh-client-hmr 盯着每个 bundle 的 lib/client.js,重建了就换进已打开的页面)。", panel.ok ? `面板上已经打开它了(${workspace})—— 验收就走这一张卡(你自己走一遍)` : `面板上还没打开:${panel.reason}。调 workbench_diy_show(unit=${found.unit})`].join("\n");
6501
5819
  return plainJson({
6502
5820
  workshop: packagesDir,
6503
5821
  unit: found.unit,
@@ -6514,7 +5832,7 @@ function registerDiyTools(ctx, market, panels, debug) {
6514
5832
  ok: false,
6515
5833
  reason: `安装结果是 ${outcome.change.application},还没生效`
6516
5834
  };
6517
- 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}。先把上面那句"没装成"处理掉,再谈上板。`;
5835
+ const panelText = panel.ok ? `面板上已经打开它了(${workspace})—— 验收就走这一张卡(你自己走一遍)` : outcome.change.application === "applied" ? `面板上还没打开:${panel.reason}。调 workbench_diy_show(unit=${found.unit})试试。` : restartRequired ? `面板上还没打开(${panel.reason}):模块要**重启一次工作台**才注册进面板 —— 把这句话转给用户,重启后再调 workbench_diy_show(unit=${found.unit})。` : `面板上还没打开:${panel.reason}。先把上面那句"没装成"处理掉,再谈上板。`;
6518
5836
  const text = [
6519
5837
  `${verdict}:${found.unit}(${found.name})`,
6520
5838
  panelText,
@@ -6536,17 +5854,67 @@ function registerDiyTools(ctx, market, panels, debug) {
6536
5854
  tools.register(statusTool),
6537
5855
  tools.register(panelTool),
6538
5856
  tools.register(reconTool),
5857
+ ...registerSpecTool(ctx, tools),
6539
5858
  tools.register(probeTool),
6540
- ...registerLookTools(ctx, tools),
6541
5859
  ...registerLogTools(ctx, tools),
6542
- ...registerNetTools(ctx, tools),
6543
- ...registerActTools(ctx, tools),
5860
+ ...registerBrowserTools(ctx, tools),
6544
5861
  ...registerDebugTools(ctx, tools, debug),
6545
5862
  tools.register(showTool),
6546
5863
  tools.register(installTool)
6547
5864
  ];
6548
5865
  }
6549
5866
  //#endregion
5867
+ //#region src/modules/panels/model/diy-observers.ts
5868
+ async function stopObservers(ctx, session = BROWSER_SCREEN_SESSION) {
5869
+ const packagesDir = workshopPackagesDir(ctx);
5870
+ const shell = ctx.get("shell");
5871
+ if (packagesDir === null || shell === void 0) return {
5872
+ net: false,
5873
+ logs: false
5874
+ };
5875
+ const file = join(dirname(packagesDir), "shots", `net-${session}.jsonl`);
5876
+ const command = opencliCommandOf(ctx).command;
5877
+ const [net, step] = await Promise.all([stopNetworkExact(shell, file), actBrowser(shell, command, session, ["eval", LOGS_STOP_SCRIPT], SWEEP_TIMEOUT_MS)]);
5878
+ return {
5879
+ net,
5880
+ logs: typeof step.payload?.["unhooked"] === "number"
5881
+ };
5882
+ }
5883
+ function withObserverCleanup(store, cleanup, logger) {
5884
+ let pending = Promise.resolve();
5885
+ let epoch = 0;
5886
+ const sweep = async (at) => {
5887
+ if (epoch !== at) {
5888
+ logger?.warn?.("收工那一拍:已经有新的调试约定开始了 —— 跳过收观察(新那一场自己收)");
5889
+ return;
5890
+ }
5891
+ try {
5892
+ const out = await cleanup();
5893
+ if (!out.net || !out.logs) logger?.warn?.(`收工那一拍没把观察收干净(网络 ${String(out.net)} / 日志 ${String(out.logs)})—— 网络那条最多 15 分钟自己收工,日志钩子随页面刷新消失`);
5894
+ } catch (error) {
5895
+ logger?.warn?.(`收工那一拍收观察抛了:${error instanceof Error ? error.message : String(error)}`);
5896
+ }
5897
+ };
5898
+ return {
5899
+ live: (now) => store.live(now),
5900
+ begin(pact) {
5901
+ epoch += 1;
5902
+ return store.begin(pact);
5903
+ },
5904
+ pause: (sessionId, now) => store.pause(sessionId, now),
5905
+ resume: (sessionId, now) => store.resume(sessionId, now),
5906
+ stop(sessionId, now) {
5907
+ const move = store.stop(sessionId, now);
5908
+ if (move.ok) {
5909
+ const at = epoch;
5910
+ pending = pending.then(() => sweep(at));
5911
+ }
5912
+ return move;
5913
+ },
5914
+ settle: () => pending
5915
+ };
5916
+ }
5917
+ //#endregion
6550
5918
  //#region src/modules/panels/model/diy-debug-routes.ts
6551
5919
  function diyDebugRoutes(store) {
6552
5920
  return { "panels/diy-debug": () => ({
@@ -8936,7 +8304,7 @@ var OWNED = [{
8936
8304
  logger
8937
8305
  });
8938
8306
  ctx.provide(PANELS_SERVICE, panels);
8939
- const debug = createDebugStore();
8307
+ const debug = withObserverCleanup(createDebugStore(), () => stopObservers(ctx), logger);
8940
8308
  ctx.provide(DIY_DEBUG_SERVICE, debug);
8941
8309
  const profileDir = profileDirOf(ctx);
8942
8310
  const shell = ctx.get("shell");
@@ -9415,6 +8783,13 @@ var REFUSAL = {
9415
8783
  };
9416
8784
  function debugActResource(deps) {
9417
8785
  const now = () => deps.now?.() ?? Date.now();
8786
+ const settled = (intent, delivered, summary, state) => ({
8787
+ ok: true,
8788
+ intent,
8789
+ delivered,
8790
+ summary,
8791
+ state
8792
+ });
9418
8793
  return {
9419
8794
  resource: "act",
9420
8795
  handle(payload) {
@@ -9440,13 +8815,9 @@ function debugActResource(deps) {
9440
8815
  text: REFUSAL[move.code] ?? `按不动:${move.code}`
9441
8816
  };
9442
8817
  const notice = deps.notice?.(intent, move.state) ?? debugNotice(intent, move.state);
9443
- return {
9444
- ok: true,
9445
- intent,
9446
- delivered: deliverDebugNotice(deps.agents(), held.sessionId, notice.message, NOTICE_MODE[intent]),
9447
- summary: notice.summary,
9448
- state: store.live(now())
9449
- };
8818
+ const delivered = deliverDebugNotice(deps.agents(), held.sessionId, notice.message, NOTICE_MODE[intent]);
8819
+ if (intent !== "stop") return settled(intent, delivered, notice.summary, store.live(now()));
8820
+ return (store.settle?.() ?? Promise.resolve()).then(() => settled(intent, delivered, notice.summary, store.live(now())));
9450
8821
  }
9451
8822
  };
9452
8823
  }