@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.
- package/client.js +177 -107
- package/dist/client/client.body.js +177 -107
- package/dist/client/index.js +177 -107
- package/dist/host/index.js +832 -1461
- package/dist/types/app/client.d.ts +15 -3
- package/dist/types/app/layouts/panel-hint.d.ts +3 -0
- package/dist/types/app/layouts/rightbar/market-hint.d.ts +16 -0
- package/dist/types/app/layouts/rightbar/market-rows.d.ts +2 -3
- package/dist/types/modules/panels/model/diy-browser-act.d.ts +2 -39
- package/dist/types/modules/panels/model/diy-browser-tools.d.ts +39 -0
- package/dist/types/modules/panels/model/diy-browser.d.ts +1 -14
- package/dist/types/modules/panels/model/diy-observers.d.ts +11 -0
- package/dist/types/modules/panels/model/diy-observers.test.d.ts +1 -0
- package/dist/types/modules/panels/model/diy-page.d.ts +1 -0
- package/dist/types/modules/panels/model/diy-scripts.d.ts +0 -63
- package/dist/types/modules/panels/model/diy-spec.d.ts +5 -0
- package/dist/types/modules/panels/model/diy-spec.test.d.ts +1 -0
- package/dist/types/modules/panels/unit.d.ts +13 -11
- package/dist/types/shared/api/host/diy-recon.d.ts +10 -0
- package/package.json +5 -5
- package/scripts/seed-workshop.sh +3 -1
- package/skills/workbench-module-diy/BUILD.md +37 -16
- package/skills/workbench-module-diy/DATASOURCE.md +11 -2
- package/skills/workbench-module-diy/DESIGN.md +15 -8
- package/skills/workbench-module-diy/KIT.md +2 -0
- package/skills/workbench-module-diy/PANEL.md +3 -2
- package/skills/workbench-module-diy/REVIEW.md +115 -48
- package/skills/workbench-module-diy/SKILL.md +33 -9
- package/skills/workbench-module-diy/STEPS.md +1 -1
- package/skills/workbench-module-diy/TOOLING.md +6 -3
- package/skills/workbench-module-diy/TOOLS.md +90 -204
- package/skills/workbench-module-diy/WIRING.md +7 -5
- package/skills/workbench-module-diy/WRITES.md +90 -0
- package/dist/types/modules/panels/model/diy-act.d.ts +0 -4
- package/dist/types/modules/panels/model/diy-look.d.ts +0 -15
- package/dist/types/modules/panels/model/diy-net.d.ts +0 -3
- /package/dist/types/modules/panels/model/{diy-act.test.d.ts → diy-browser-act.test.d.ts} +0 -0
- /package/dist/types/modules/panels/model/{diy-look.test.d.ts → diy-browser-tools-shot.test.d.ts} +0 -0
- /package/dist/types/modules/panels/model/{diy-net.test.d.ts → diy-browser-tools.test.d.ts} +0 -0
package/dist/host/index.js
CHANGED
|
@@ -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$
|
|
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$
|
|
269
|
-
const message = textOf$
|
|
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$
|
|
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$
|
|
297
|
-
const route = textOf$
|
|
298
|
-
const phase = textOf$
|
|
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$
|
|
329
|
-
const unit = textOf$
|
|
330
|
-
const reason = textOf$
|
|
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
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
875
|
-
tag: textOf$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
2017
|
-
const pkg = textOf$
|
|
2018
|
-
const installedAt = textOf$
|
|
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$
|
|
2021
|
-
const registry = textOf$
|
|
2022
|
-
const releasedAt = textOf$
|
|
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
|
|
3038
|
-
var
|
|
3039
|
-
var
|
|
3040
|
-
var
|
|
3041
|
-
var
|
|
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
|
-
|
|
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
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
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
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
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
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
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
|
-
|
|
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 [
|
|
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: "
|
|
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
|
-
|
|
4574
|
-
|
|
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
|
-
|
|
4681
|
-
"
|
|
4682
|
-
"
|
|
4683
|
-
"-
|
|
4684
|
-
|
|
4685
|
-
quoteShellArg(
|
|
4686
|
-
|
|
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
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
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
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
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
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
}
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
"
|
|
5087
|
-
|
|
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
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
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
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
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
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
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
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
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
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
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
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
4563
|
+
{
|
|
4564
|
+
tool: DIY_KEYS_TOOL,
|
|
4565
|
+
verb: ["keys"],
|
|
4566
|
+
description: "给焦点发按键。",
|
|
4567
|
+
argsHelp: "opencli browser keys 的参数(如 Enter / Meta+K)。"
|
|
5261
4568
|
},
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
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
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
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
|
-
|
|
5277
|
-
|
|
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
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
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
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
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
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
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
|
-
|
|
4643
|
+
y: {
|
|
5444
4644
|
type: "number",
|
|
5445
|
-
description: "
|
|
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: "
|
|
4649
|
+
description: "宽(CSS px)。"
|
|
5513
4650
|
},
|
|
5514
4651
|
height: {
|
|
5515
4652
|
type: "number",
|
|
5516
|
-
description: "
|
|
4653
|
+
description: "高(CSS px)。"
|
|
5517
4654
|
}
|
|
5518
|
-
},
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
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
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
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
|
|
5696
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
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: "
|
|
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})——
|
|
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})——
|
|
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})——
|
|
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
|
-
...
|
|
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
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
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
|
}
|