@xbrowser/cli 1.21.0 → 1.23.0
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/README.md +21 -3
- package/dist/{browser-CNBSH53B.js → browser-BVRFSUXU.js} +3 -2
- package/dist/{browser-C2VEOJC2.js → browser-DSYUAE7H.js} +5 -4
- package/dist/{browser-VZOOFLAH.js → browser-Q67BJTI2.js} +4 -3
- package/dist/{cdp-driver-KFX4A6WR.js → cdp-driver-DC7EIUG5.js} +4 -3
- package/dist/{cdp-driver-7Z6ZAT5E.js → cdp-driver-T7D6H2RW.js} +335 -546
- package/dist/{cdp-driver-26LGCHXV.js → cdp-driver-WWO2UZDR.js} +3 -2
- package/dist/{chunk-Z5RTK4GW.js → chunk-2Q3GQRUP.js} +2 -2
- package/dist/{chunk-CLYZPSDR.js → chunk-4OU37CXP.js} +354 -576
- package/dist/{chunk-HWWG2ISJ.js → chunk-7OP2ISEY.js} +353 -575
- package/dist/{chunk-PIWNMC36.js → chunk-A3MORKYR.js} +1 -1
- package/dist/{chunk-3FWLW7FS.js → chunk-A7NEA4PA.js} +39 -1
- package/dist/{chunk-OMU63E6J.js → chunk-FBQZ3PVD.js} +5 -8
- package/dist/chunk-GYB5BOSP.js +602 -0
- package/dist/{chunk-ESJYANQO.js → chunk-IFKJO2UR.js} +360 -582
- package/dist/{chunk-NODRQGOK.js → chunk-IR6C24P7.js} +97 -0
- package/dist/{chunk-TEXCXIBW.js → chunk-LPW6QS4M.js} +5 -8
- package/dist/{chunk-3KZ34AUC.js → chunk-MYH6FIBZ.js} +2 -1
- package/dist/{chunk-AVSTNODR.js → chunk-T4WTKBWI.js} +2 -2
- package/dist/{chunk-HBHOKZPN.js → chunk-V6KGBU5J.js} +97 -0
- package/dist/{chunk-IWVG4XYZ.js → chunk-VCVDILH6.js} +2 -1
- package/dist/{chunk-NKW4A74J.js → chunk-XTUAUFL3.js} +5 -8
- package/dist/cli.js +497 -252
- package/dist/daemon-main.js +25 -19
- package/dist/{human-interaction-Y5IDH6LD.js → human-interaction-57VYIR56.js} +1 -1
- package/dist/{human-interaction-ZUTR5AC2.js → human-interaction-67K43B4O.js} +1 -1
- package/dist/{human-interaction-4YR2N6R6.js → human-interaction-MXST5ZQM.js} +1 -1
- package/dist/index.d.ts +52 -4
- package/dist/index.js +500 -256
- package/dist/keep-awake-M4KJS4B4.js +29 -0
- package/dist/{launcher-44STYRJC.js → launcher-IK2FKW6T.js} +1 -1
- package/dist/{launcher-EXJHHAUL.js → launcher-LUOMMGZC.js} +1 -1
- package/dist/{plugin-singleton-FCNDN3FX.js → plugin-singleton-GJO7EZXB.js} +1 -1
- package/dist/{recovery-NXC35EQN.js → recovery-6RI2FE3E.js} +1 -1
- package/dist/{recovery-MDWAVXE4.js → recovery-LGY25LIM.js} +1 -1
- package/dist/{recovery-L5GLDX4O.js → recovery-LMJW537X.js} +1 -1
- package/dist/{session-recorder-SLDBENVF.js → session-recorder-563ISSJK.js} +1 -1
- package/dist/{session-recorder-3BEVWHOK.js → session-recorder-GEZVQBKW.js} +1 -1
- package/dist/session-replayer-N665T3G5.js +932 -0
- package/dist/stealth-2AA2FSNN.js +29 -0
- package/dist/stealth-JOXGV34D.js +29 -0
- package/package.json +11 -7
- package/dist/session-replayer-EVD4HZBB.js +0 -413
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
detectAntiBot,
|
|
4
|
+
formatDetectionMessage
|
|
5
|
+
} from "./chunk-JKVUFP3G.js";
|
|
2
6
|
import {
|
|
3
7
|
forwardCommandLog,
|
|
4
8
|
forwardNetworkAnalyze,
|
|
@@ -45,7 +49,7 @@ import {
|
|
|
45
49
|
} from "./chunk-Q2DFJQGS.js";
|
|
46
50
|
import {
|
|
47
51
|
SessionRecorder
|
|
48
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-IR6C24P7.js";
|
|
49
53
|
import {
|
|
50
54
|
addKnownIssue,
|
|
51
55
|
getKnowledgePath,
|
|
@@ -66,21 +70,19 @@ import {
|
|
|
66
70
|
getAllSessions,
|
|
67
71
|
getBrowser,
|
|
68
72
|
getSessionById,
|
|
69
|
-
rand,
|
|
70
73
|
resolveLaunchOpts,
|
|
71
74
|
saveSessionDiskMeta,
|
|
72
|
-
setActivePage
|
|
75
|
+
setActivePage
|
|
76
|
+
} from "./chunk-IFKJO2UR.js";
|
|
77
|
+
import {
|
|
78
|
+
rand,
|
|
73
79
|
sleep,
|
|
74
80
|
wheelDelta
|
|
75
|
-
} from "./chunk-
|
|
76
|
-
import "./chunk-
|
|
81
|
+
} from "./chunk-GYB5BOSP.js";
|
|
82
|
+
import "./chunk-MYH6FIBZ.js";
|
|
77
83
|
import {
|
|
78
84
|
errMsg
|
|
79
85
|
} from "./chunk-GDKLH7ZY.js";
|
|
80
|
-
import {
|
|
81
|
-
detectAntiBot,
|
|
82
|
-
formatDetectionMessage
|
|
83
|
-
} from "./chunk-JKVUFP3G.js";
|
|
84
86
|
import "./chunk-KFQGP6VL.js";
|
|
85
87
|
|
|
86
88
|
// src/router.ts
|
|
@@ -7170,7 +7172,7 @@ import { join as join7 } from "path";
|
|
|
7170
7172
|
import { execSync } from "child_process";
|
|
7171
7173
|
var SHARED_PLUGIN_DEPENDENCIES = {
|
|
7172
7174
|
"zod": "^3.24.0",
|
|
7173
|
-
"@dyyz1993/xcli-core": "^0.
|
|
7175
|
+
"@dyyz1993/xcli-core": "^0.19.0"
|
|
7174
7176
|
};
|
|
7175
7177
|
function ensurePluginDependencies(pluginsDir) {
|
|
7176
7178
|
const zodPath = join7(pluginsDir, "node_modules", "zod");
|
|
@@ -8248,7 +8250,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
8248
8250
|
}
|
|
8249
8251
|
let targetPageOverride = null;
|
|
8250
8252
|
if (_target && extraOpts?.cdpEndpoint) {
|
|
8251
|
-
const { findTargetPage } = await import("./browser-
|
|
8253
|
+
const { findTargetPage } = await import("./browser-BVRFSUXU.js");
|
|
8252
8254
|
targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
|
|
8253
8255
|
if (!targetPageOverride) {
|
|
8254
8256
|
return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
|
|
@@ -8465,7 +8467,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
8465
8467
|
const errorMessage = errMsg(err);
|
|
8466
8468
|
if (session?.page && process.env.XBROWSER_RECOVERY && !extraOpts?._recoveryAttempted) {
|
|
8467
8469
|
try {
|
|
8468
|
-
const { attemptRecovery } = await import("./recovery-
|
|
8470
|
+
const { attemptRecovery } = await import("./recovery-6RI2FE3E.js");
|
|
8469
8471
|
const recovery = await attemptRecovery(
|
|
8470
8472
|
session.page,
|
|
8471
8473
|
sessionName,
|
|
@@ -9147,13 +9149,39 @@ async function installFromNpm(packageName, name, targetDir) {
|
|
|
9147
9149
|
import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync7, rmSync as rmSync3, cpSync as cpSync3 } from "fs";
|
|
9148
9150
|
import { resolve as resolve5, join as join10 } from "path";
|
|
9149
9151
|
import { tmpdir as tmpdir2 } from "os";
|
|
9150
|
-
import {
|
|
9152
|
+
import { execFile as execFile2 } from "child_process";
|
|
9151
9153
|
import { verifyPlugin as verifyPlugin3, safeCleanup as safeCleanup3 } from "@dyyz1993/xcli-core";
|
|
9154
|
+
var GIT_URL_RE = /^(?:https|git|git\+https|git\+ssh|ssh):\/\/[^\s"'`\\;<>()&]+$/i;
|
|
9155
|
+
var GIT_SCP_RE = /^git@[\w.-]+:[\w./~+-]+$/;
|
|
9156
|
+
function isValidGitUrl(url) {
|
|
9157
|
+
if (typeof url !== "string" || url.length === 0) return false;
|
|
9158
|
+
if (url.startsWith("-")) return false;
|
|
9159
|
+
if (/[\r\n\0]/.test(url)) return false;
|
|
9160
|
+
return GIT_URL_RE.test(url) || GIT_SCP_RE.test(url);
|
|
9161
|
+
}
|
|
9162
|
+
function execGit(args) {
|
|
9163
|
+
return new Promise((resolve10, reject) => {
|
|
9164
|
+
execFile2("git", args, (err, _stdout, stderr) => {
|
|
9165
|
+
if (err) {
|
|
9166
|
+
const stderrText = typeof stderr === "string" ? stderr : stderr?.toString("utf-8") ?? "";
|
|
9167
|
+
const detail = stderrText.trim() || err.message;
|
|
9168
|
+
reject(new Error(`git clone failed: ${detail}`));
|
|
9169
|
+
} else {
|
|
9170
|
+
resolve10();
|
|
9171
|
+
}
|
|
9172
|
+
});
|
|
9173
|
+
});
|
|
9174
|
+
}
|
|
9152
9175
|
async function installFromGit(gitUrl, name, targetDir) {
|
|
9176
|
+
if (!isValidGitUrl(gitUrl)) {
|
|
9177
|
+
throw new Error(
|
|
9178
|
+
`Invalid git URL: ${JSON.stringify(gitUrl).slice(0, 120)} \u2014 allowed forms: https://, git://, git+https://, git+ssh://, ssh://, git@host:path`
|
|
9179
|
+
);
|
|
9180
|
+
}
|
|
9153
9181
|
const tmpDir = join10(tmpdir2(), `xbrowser-git-${Date.now()}`);
|
|
9154
9182
|
let warnings = [];
|
|
9155
9183
|
try {
|
|
9156
|
-
|
|
9184
|
+
await execGit(["clone", "--depth", "1", "--", gitUrl, tmpDir]);
|
|
9157
9185
|
const verify = verifyPlugin3(tmpDir, { metadataField: "xbrowser" });
|
|
9158
9186
|
warnings = verify.warnings ?? [];
|
|
9159
9187
|
if (!verify.valid) {
|
|
@@ -10988,7 +11016,7 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
|
|
|
10988
11016
|
}
|
|
10989
11017
|
outputResult({ command: cmdDef.name, description: cmdDef.description, scope: cmdDef.scope, parameters: paramsList }, mode);
|
|
10990
11018
|
} else {
|
|
10991
|
-
|
|
11019
|
+
process.stdout.write(helpGenerator.generate(cmdDef, { color: true, emoji: false }));
|
|
10992
11020
|
}
|
|
10993
11021
|
} else {
|
|
10994
11022
|
outputError(`Unknown command: ${command}`);
|
|
@@ -11389,7 +11417,7 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
|
|
|
11389
11417
|
if (isEmptyResult) {
|
|
11390
11418
|
const hint = cdpEndpoint ? `\u53EF\u80FD\u672A\u8FDE\u63A5\u5230\u6D4F\u89C8\u5668\u3002\u8BF7\u786E\u8BA4 ${cdpEndpoint} \u4E0A\u6709 Chrome \u8FD0\u884C\uFF08--remote-debugging-port\uFF09\u3002` : "\u53EF\u80FD\u672A\u8FDE\u63A5\u5230\u6D4F\u89C8\u5668\u3002\u8BF7\u4F7F\u7528 --cdp <endpoint> \u8FDE\u63A5\uFF0C\u6216\u5B89\u88C5 cdp-tunnel \u590D\u7528\u5DF2\u6709 Chrome\u3002";
|
|
11391
11419
|
outputResult(result.data, mode);
|
|
11392
|
-
|
|
11420
|
+
outputError(`
|
|
11393
11421
|
\u26A0\uFE0F ${hint}`);
|
|
11394
11422
|
process.exit(1);
|
|
11395
11423
|
} else {
|
|
@@ -11406,8 +11434,9 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
|
|
|
11406
11434
|
try {
|
|
11407
11435
|
mkdirSync10(dirname6(outputFile), { recursive: true });
|
|
11408
11436
|
writeFileSync12(outputFile, content, "utf-8");
|
|
11409
|
-
|
|
11410
|
-
\u{1F4C4} Written to ${outputFile}
|
|
11437
|
+
process.stdout.write(`
|
|
11438
|
+
\u{1F4C4} Written to ${outputFile}
|
|
11439
|
+
`);
|
|
11411
11440
|
} catch (err) {
|
|
11412
11441
|
outputError(`Failed to write --output "${outputFile}": ${err instanceof Error ? err.message : String(err)}`);
|
|
11413
11442
|
}
|
|
@@ -11505,7 +11534,7 @@ async function handleSession(args, options, mode, _cdpEndpoint) {
|
|
|
11505
11534
|
break;
|
|
11506
11535
|
}
|
|
11507
11536
|
default:
|
|
11508
|
-
|
|
11537
|
+
process.stdout.write(handleSessionHelp() + "\n");
|
|
11509
11538
|
}
|
|
11510
11539
|
}
|
|
11511
11540
|
|
|
@@ -11679,19 +11708,24 @@ async function handleSearch(args, options, mode) {
|
|
|
11679
11708
|
outputEnvelope({ success: true, data: { results: deduped, total: deduped.length } }, { command: "plugin search" }, mode);
|
|
11680
11709
|
} else {
|
|
11681
11710
|
if (deduped.length === 0) {
|
|
11682
|
-
|
|
11711
|
+
process.stdout.write("No plugins found\n");
|
|
11683
11712
|
return;
|
|
11684
11713
|
}
|
|
11685
11714
|
for (const r of deduped) {
|
|
11686
11715
|
const src = r.source === "marketplace" ? "[marketplace]" : r.source === "local" ? "[local]" : "[npm]";
|
|
11687
11716
|
const slug = r.slug ? ` (${r.slug})` : "";
|
|
11688
|
-
|
|
11689
|
-
|
|
11690
|
-
if (r.
|
|
11691
|
-
|
|
11692
|
-
|
|
11717
|
+
process.stdout.write(` ${src} ${r.name}${slug}
|
|
11718
|
+
`);
|
|
11719
|
+
if (r.description) process.stdout.write(` ${r.description}
|
|
11720
|
+
`);
|
|
11721
|
+
if (r.version) process.stdout.write(` Version: ${r.version}
|
|
11722
|
+
`);
|
|
11723
|
+
if (r.downloads) process.stdout.write(` Downloads: ${r.downloads}
|
|
11724
|
+
`);
|
|
11725
|
+
process.stdout.write("\n");
|
|
11693
11726
|
}
|
|
11694
|
-
|
|
11727
|
+
process.stdout.write(`Total: ${deduped.length} plugins
|
|
11728
|
+
`);
|
|
11695
11729
|
}
|
|
11696
11730
|
}
|
|
11697
11731
|
async function handlePluginInfo(args, options, mode) {
|
|
@@ -11708,14 +11742,22 @@ async function handlePluginInfo(args, options, mode) {
|
|
|
11708
11742
|
outputEnvelope({ success: true, data: { source: "marketplace", ...d } }, { command: "plugin info" }, mode);
|
|
11709
11743
|
return;
|
|
11710
11744
|
}
|
|
11711
|
-
|
|
11712
|
-
|
|
11713
|
-
|
|
11714
|
-
|
|
11715
|
-
|
|
11716
|
-
|
|
11717
|
-
|
|
11718
|
-
|
|
11745
|
+
process.stdout.write(`\u540D\u79F0: ${d.name || ""}
|
|
11746
|
+
`);
|
|
11747
|
+
process.stdout.write(`\u7248\u672C: ${d.version || ""}
|
|
11748
|
+
`);
|
|
11749
|
+
process.stdout.write(`\u63CF\u8FF0: ${d.description || ""}
|
|
11750
|
+
`);
|
|
11751
|
+
process.stdout.write(`\u4F5C\u8005: ${d.author || ""}
|
|
11752
|
+
`);
|
|
11753
|
+
process.stdout.write(`\u547D\u4EE4: ${(d.commands || []).join(", ")}
|
|
11754
|
+
`);
|
|
11755
|
+
process.stdout.write(`\u4E0B\u8F7D\u91CF: ${d.downloads || 0}
|
|
11756
|
+
`);
|
|
11757
|
+
process.stdout.write(`\u6807\u7B7E: ${(d.tags || []).join(", ")}
|
|
11758
|
+
`);
|
|
11759
|
+
process.stdout.write(`\u7F51\u7AD9: ${(d.sites || []).join(", ")}
|
|
11760
|
+
`);
|
|
11719
11761
|
return;
|
|
11720
11762
|
}
|
|
11721
11763
|
} catch {
|
|
@@ -11734,19 +11776,25 @@ async function handlePluginInfo(args, options, mode) {
|
|
|
11734
11776
|
outputEnvelope({ success: true, data: { source: "npm", name: pkg.name, version: latest, description: pkg.description } }, { command: "plugin info" }, mode);
|
|
11735
11777
|
return;
|
|
11736
11778
|
}
|
|
11737
|
-
|
|
11738
|
-
|
|
11739
|
-
|
|
11779
|
+
process.stdout.write(`\u540D\u79F0: ${pkg.name || ""}
|
|
11780
|
+
`);
|
|
11781
|
+
process.stdout.write(`\u7248\u672C: ${latest}
|
|
11782
|
+
`);
|
|
11783
|
+
process.stdout.write(`\u63CF\u8FF0: ${pkg.description || ""}
|
|
11784
|
+
`);
|
|
11740
11785
|
const author = pkg.author;
|
|
11741
|
-
|
|
11742
|
-
|
|
11743
|
-
|
|
11786
|
+
process.stdout.write(`\u4F5C\u8005: ${typeof author === "string" ? author : author?.name || ""}
|
|
11787
|
+
`);
|
|
11788
|
+
process.stdout.write(`\u5173\u952E\u8BCD: ${(pkg.keywords || []).join(", ")}
|
|
11789
|
+
`);
|
|
11790
|
+
process.stdout.write(`\u8BB8\u53EF\u8BC1: ${pkg.license || ""}
|
|
11791
|
+
`);
|
|
11744
11792
|
return;
|
|
11745
11793
|
}
|
|
11746
11794
|
}
|
|
11747
|
-
|
|
11795
|
+
outputError(`\u63D2\u4EF6 '${slug}' \u672A\u627E\u5230`);
|
|
11748
11796
|
} catch (err) {
|
|
11749
|
-
|
|
11797
|
+
outputError("\u67E5\u8BE2\u5931\u8D25: " + errMsg(err));
|
|
11750
11798
|
}
|
|
11751
11799
|
}
|
|
11752
11800
|
async function handlePluginSchema(args, mode) {
|
|
@@ -11770,32 +11818,39 @@ async function handlePluginSchema(args, mode) {
|
|
|
11770
11818
|
}
|
|
11771
11819
|
}
|
|
11772
11820
|
function printPluginContract(contract) {
|
|
11773
|
-
|
|
11774
|
-
|
|
11775
|
-
|
|
11821
|
+
process.stdout.write(`${contract.plugin.name} contract v${contract.version}
|
|
11822
|
+
`);
|
|
11823
|
+
if (contract.plugin.description) process.stdout.write(contract.plugin.description + "\n");
|
|
11824
|
+
process.stdout.write("\n");
|
|
11776
11825
|
for (const command of contract.commands) {
|
|
11777
11826
|
printCommandContract(contract.plugin.name, command);
|
|
11778
11827
|
}
|
|
11779
11828
|
}
|
|
11780
11829
|
function printCommandContract(pluginName, command) {
|
|
11781
|
-
|
|
11782
|
-
|
|
11783
|
-
|
|
11830
|
+
process.stdout.write(`${pluginName} ${command.name}
|
|
11831
|
+
`);
|
|
11832
|
+
if (command.description) process.stdout.write(` ${command.description}
|
|
11833
|
+
`);
|
|
11834
|
+
process.stdout.write(` scope: ${command.scope}
|
|
11835
|
+
`);
|
|
11784
11836
|
if (command.capabilities.length > 0) {
|
|
11785
|
-
|
|
11837
|
+
process.stdout.write(` capabilities: ${command.capabilities.join(", ")}
|
|
11838
|
+
`);
|
|
11786
11839
|
}
|
|
11787
11840
|
if (command.positional.length > 0) {
|
|
11788
|
-
|
|
11841
|
+
process.stdout.write(` positional: ${command.positional.join(", ")}
|
|
11842
|
+
`);
|
|
11789
11843
|
}
|
|
11790
11844
|
if (command.form.fields.length > 0) {
|
|
11791
|
-
|
|
11845
|
+
process.stdout.write(" fields:\n");
|
|
11792
11846
|
for (const field of command.form.fields) {
|
|
11793
11847
|
const required = field.required ? "required" : "optional";
|
|
11794
11848
|
const choices = field.enum ? ` [${field.enum.join("|")}]` : "";
|
|
11795
|
-
|
|
11849
|
+
process.stdout.write(` --${field.name}: ${field.type}/${field.widget} ${required}${choices}
|
|
11850
|
+
`);
|
|
11796
11851
|
}
|
|
11797
11852
|
}
|
|
11798
|
-
|
|
11853
|
+
process.stdout.write("\n");
|
|
11799
11854
|
}
|
|
11800
11855
|
async function handlePlugin(args, options, mode) {
|
|
11801
11856
|
const sub = args[0];
|
|
@@ -11884,24 +11939,28 @@ async function handlePlugin(args, options, mode) {
|
|
|
11884
11939
|
outputEnvelope({ success: true, data: { plugins: enrichedPlugins } }, { command: "plugin list" }, mode);
|
|
11885
11940
|
} else {
|
|
11886
11941
|
if (enrichedPlugins.length === 0) {
|
|
11887
|
-
|
|
11942
|
+
process.stdout.write("No plugins installed\n");
|
|
11888
11943
|
return;
|
|
11889
11944
|
}
|
|
11890
11945
|
for (const p of enrichedPlugins) {
|
|
11891
11946
|
const loginTag = p.hasLogin ? p.loggedIn ? " [logged in]" : " [need login]" : "";
|
|
11892
11947
|
if (p.version && p.description) {
|
|
11893
|
-
|
|
11948
|
+
process.stdout.write(`${p.name} (${p.version}) - ${p.description}${loginTag}
|
|
11949
|
+
`);
|
|
11894
11950
|
} else {
|
|
11895
|
-
|
|
11951
|
+
process.stdout.write(`${p.name}${loginTag}
|
|
11952
|
+
`);
|
|
11896
11953
|
}
|
|
11897
11954
|
if (p.commands && p.commands.length > 0) {
|
|
11898
|
-
|
|
11955
|
+
process.stdout.write(` ${p.commands.join(", ")}
|
|
11956
|
+
`);
|
|
11899
11957
|
}
|
|
11900
11958
|
if (p.requiresLoginCommands.length > 0) {
|
|
11901
|
-
|
|
11959
|
+
process.stdout.write(` requires login: ${p.requiresLoginCommands.join(", ")}
|
|
11960
|
+
`);
|
|
11902
11961
|
}
|
|
11903
11962
|
}
|
|
11904
|
-
|
|
11963
|
+
process.stdout.write(`
|
|
11905
11964
|
Total: ${enrichedPlugins.length} plugins`);
|
|
11906
11965
|
}
|
|
11907
11966
|
break;
|
|
@@ -11938,7 +11997,7 @@ See docs/plugin-guide.md for the publishing workflow.`
|
|
|
11938
11997
|
);
|
|
11939
11998
|
break;
|
|
11940
11999
|
default:
|
|
11941
|
-
|
|
12000
|
+
process.stdout.write(handlePluginHelp() + "\n");
|
|
11942
12001
|
}
|
|
11943
12002
|
}
|
|
11944
12003
|
function handleCreate(args, options) {
|
|
@@ -11972,7 +12031,7 @@ function handleDaemon(args, options, mode) {
|
|
|
11972
12031
|
break;
|
|
11973
12032
|
}
|
|
11974
12033
|
default:
|
|
11975
|
-
|
|
12034
|
+
process.stdout.write("Daemon starts automatically. No manual action needed.\n");
|
|
11976
12035
|
}
|
|
11977
12036
|
}
|
|
11978
12037
|
|
|
@@ -12041,8 +12100,8 @@ async function handleRecord(args, options, mode) {
|
|
|
12041
12100
|
}, mode);
|
|
12042
12101
|
const md = SessionRecorder.readMarkdownSummary(sessionName);
|
|
12043
12102
|
if (md) {
|
|
12044
|
-
|
|
12045
|
-
|
|
12103
|
+
process.stdout.write("\n");
|
|
12104
|
+
process.stdout.write(md + "\n");
|
|
12046
12105
|
} else {
|
|
12047
12106
|
const summary = SessionRecorder.readSummary(sessionName);
|
|
12048
12107
|
if (summary) {
|
|
@@ -12101,25 +12160,30 @@ async function handleRecord(args, options, mode) {
|
|
|
12101
12160
|
break;
|
|
12102
12161
|
}
|
|
12103
12162
|
default:
|
|
12104
|
-
|
|
12105
|
-
|
|
12106
|
-
|
|
12107
|
-
|
|
12108
|
-
|
|
12109
|
-
|
|
12110
|
-
|
|
12111
|
-
|
|
12112
|
-
|
|
12163
|
+
process.stdout.write("Usage:\n");
|
|
12164
|
+
process.stdout.write(" xbrowser record start [--url <url>] [--session <name>]\n");
|
|
12165
|
+
process.stdout.write(" xbrowser record stop [--session <name>]\n");
|
|
12166
|
+
process.stdout.write(" xbrowser record status [--session <name>]\n");
|
|
12167
|
+
process.stdout.write(" xbrowser record summary [--session <name>] [--json]\n");
|
|
12168
|
+
process.stdout.write(' xbrowser record checkpoint --type <type> --hint "description" [--selector <sel>] [--session <name>]\n');
|
|
12169
|
+
process.stdout.write(" xbrowser record generate-plugin [--session <name>] [--name <plugin>] [--output <dir>]\n");
|
|
12170
|
+
process.stdout.write("\n");
|
|
12171
|
+
process.stdout.write("Checkpoint types: dialog, captcha, login, iframe, slider, custom\n");
|
|
12113
12172
|
}
|
|
12114
12173
|
}
|
|
12115
12174
|
function printRecordingSummary(summary, sessionName) {
|
|
12116
|
-
|
|
12117
|
-
|
|
12118
|
-
|
|
12119
|
-
|
|
12120
|
-
|
|
12121
|
-
|
|
12122
|
-
|
|
12175
|
+
process.stdout.write("\n");
|
|
12176
|
+
process.stdout.write("=== Recording Summary ===\n");
|
|
12177
|
+
process.stdout.write(` Start URL: ${summary.startUrl}
|
|
12178
|
+
`);
|
|
12179
|
+
process.stdout.write(` Duration: ${Math.round(summary.durationMs / 1e3)}s
|
|
12180
|
+
`);
|
|
12181
|
+
process.stdout.write(` Actions: ${summary.totalActions}
|
|
12182
|
+
`);
|
|
12183
|
+
process.stdout.write(` Network: ${summary.totalNetworkRequests}
|
|
12184
|
+
`);
|
|
12185
|
+
process.stdout.write(` Steps: ${summary.steps.length}
|
|
12186
|
+
`);
|
|
12123
12187
|
for (const step of summary.steps) {
|
|
12124
12188
|
const a = step.action;
|
|
12125
12189
|
const el = a.element;
|
|
@@ -12141,20 +12205,24 @@ function printRecordingSummary(summary, sessionName) {
|
|
|
12141
12205
|
}
|
|
12142
12206
|
const navInfo = step.contextChanges.find((c) => c.type === "navigate");
|
|
12143
12207
|
if (navInfo) desc += ` \u2192 navigate to ${navInfo.url?.substring(0, 80)}`;
|
|
12144
|
-
|
|
12208
|
+
process.stdout.write(` ${step.step}. ${desc}
|
|
12209
|
+
`);
|
|
12145
12210
|
if (a.clickContext) {
|
|
12146
12211
|
const ctx = a.clickContext;
|
|
12147
12212
|
if (ctx.appeared?.length > 0) {
|
|
12148
12213
|
for (const popup of ctx.appeared) {
|
|
12149
12214
|
const roleStr = popup.role ? ` [${popup.role}]` : "";
|
|
12150
|
-
|
|
12215
|
+
process.stdout.write(` \u21B3 ${popup.tag}${roleStr} "${(popup.text || "").substring(0, 60)}"
|
|
12216
|
+
`);
|
|
12151
12217
|
if (popup.items?.length > 0) {
|
|
12152
12218
|
for (const item of popup.items.slice(0, 10)) {
|
|
12153
12219
|
const disStr = item.disabled ? " [disabled]" : "";
|
|
12154
|
-
|
|
12220
|
+
process.stdout.write(` \u2022 ${item.text}${disStr}
|
|
12221
|
+
`);
|
|
12155
12222
|
}
|
|
12156
12223
|
if (popup.items.length > 10) {
|
|
12157
|
-
|
|
12224
|
+
process.stdout.write(` ... and ${popup.items.length - 10} more items
|
|
12225
|
+
`);
|
|
12158
12226
|
}
|
|
12159
12227
|
}
|
|
12160
12228
|
}
|
|
@@ -12167,31 +12235,41 @@ function printRecordingSummary(summary, sessionName) {
|
|
|
12167
12235
|
if (sc.ariaSelected !== void 0) parts.push(`selected=${sc.ariaSelected}`);
|
|
12168
12236
|
if (sc.dataState) parts.push(`state=${sc.dataState}`);
|
|
12169
12237
|
if (parts.length > 0) {
|
|
12170
|
-
|
|
12238
|
+
process.stdout.write(` \u21B3 state: <${sc.tag}> "${(sc.text || "").substring(0, 30)}" ${parts.join(", ")}
|
|
12239
|
+
`);
|
|
12171
12240
|
}
|
|
12172
12241
|
}
|
|
12173
12242
|
}
|
|
12174
12243
|
}
|
|
12175
12244
|
}
|
|
12176
|
-
|
|
12177
|
-
|
|
12245
|
+
process.stdout.write("\n");
|
|
12246
|
+
process.stdout.write(` Files: ${SessionRecorder.getRecordingsDir(sessionName)}/
|
|
12247
|
+
`);
|
|
12178
12248
|
if (summary.checkpoints && summary.checkpoints.length > 0) {
|
|
12179
|
-
|
|
12180
|
-
|
|
12249
|
+
process.stdout.write("\n");
|
|
12250
|
+
process.stdout.write(` Checkpoints (${summary.checkpoints.length}):
|
|
12251
|
+
`);
|
|
12181
12252
|
for (const cp of summary.checkpoints) {
|
|
12182
12253
|
const src = cp.source === "auto" ? "[auto]" : "[manual]";
|
|
12183
|
-
|
|
12184
|
-
|
|
12254
|
+
process.stdout.write(` ${cp.id}. ${src} [${cp.type}] ${cp.hint}
|
|
12255
|
+
`);
|
|
12256
|
+
if (cp.selector) process.stdout.write(` selector: ${cp.selector}
|
|
12257
|
+
`);
|
|
12185
12258
|
}
|
|
12186
12259
|
}
|
|
12187
12260
|
}
|
|
12188
12261
|
function printHumanReadableSummary(summary) {
|
|
12189
|
-
|
|
12190
|
-
|
|
12191
|
-
|
|
12192
|
-
|
|
12193
|
-
|
|
12194
|
-
|
|
12262
|
+
process.stdout.write(`Start URL: ${summary.startUrl}
|
|
12263
|
+
`);
|
|
12264
|
+
process.stdout.write(`Recorded: ${summary.recordedAt}
|
|
12265
|
+
`);
|
|
12266
|
+
process.stdout.write(`Duration: ${Math.round(summary.durationMs / 1e3)}s
|
|
12267
|
+
`);
|
|
12268
|
+
process.stdout.write(`Actions: ${summary.totalActions}
|
|
12269
|
+
`);
|
|
12270
|
+
process.stdout.write(`Network: ${summary.totalNetworkRequests}
|
|
12271
|
+
`);
|
|
12272
|
+
process.stdout.write("\n");
|
|
12195
12273
|
for (const step of summary.steps) {
|
|
12196
12274
|
const a = step.action;
|
|
12197
12275
|
const el = a.element;
|
|
@@ -12207,20 +12285,23 @@ function printHumanReadableSummary(summary) {
|
|
|
12207
12285
|
if (a.value) parts.push(`value="${a.value.substring(0, 50)}"`);
|
|
12208
12286
|
if (a.key) parts.push(`key=${a.key}`);
|
|
12209
12287
|
if (a.x !== void 0 && a.y !== void 0) parts.push(`@(${a.x},${a.y})`);
|
|
12210
|
-
|
|
12288
|
+
process.stdout.write(parts.join(" ") + "\n");
|
|
12211
12289
|
if (a.clickContext) {
|
|
12212
12290
|
const ctx = a.clickContext;
|
|
12213
12291
|
if (ctx.appeared?.length > 0) {
|
|
12214
12292
|
for (const popup of ctx.appeared) {
|
|
12215
12293
|
const roleStr = popup.role ? ` [${popup.role}]` : "";
|
|
12216
|
-
|
|
12294
|
+
process.stdout.write(` \u{1F4CB} ${popup.tag}${roleStr} "${(popup.text || "").substring(0, 60)}"
|
|
12295
|
+
`);
|
|
12217
12296
|
if (popup.items?.length > 0) {
|
|
12218
12297
|
for (const item of popup.items.slice(0, 10)) {
|
|
12219
12298
|
const disStr = item.disabled ? " [disabled]" : "";
|
|
12220
|
-
|
|
12299
|
+
process.stdout.write(` \u2022 ${item.text}${disStr}
|
|
12300
|
+
`);
|
|
12221
12301
|
}
|
|
12222
12302
|
if (popup.items.length > 10) {
|
|
12223
|
-
|
|
12303
|
+
process.stdout.write(` ... and ${popup.items.length - 10} more items
|
|
12304
|
+
`);
|
|
12224
12305
|
}
|
|
12225
12306
|
}
|
|
12226
12307
|
}
|
|
@@ -12233,40 +12314,50 @@ function printHumanReadableSummary(summary) {
|
|
|
12233
12314
|
if (sc.ariaSelected !== void 0) stateParts.push(`selected=${sc.ariaSelected}`);
|
|
12234
12315
|
if (sc.dataState) stateParts.push(`state=${sc.dataState}`);
|
|
12235
12316
|
if (stateParts.length > 0) {
|
|
12236
|
-
|
|
12317
|
+
process.stdout.write(` \u{1F504} <${sc.tag}> "${(sc.text || "").substring(0, 30)}" ${stateParts.join(", ")}
|
|
12318
|
+
`);
|
|
12237
12319
|
}
|
|
12238
12320
|
}
|
|
12239
12321
|
}
|
|
12240
12322
|
}
|
|
12241
12323
|
for (const net of step.network) {
|
|
12242
|
-
|
|
12324
|
+
process.stdout.write(` \u2192 ${net.method} ${net.path} [${net.status}] ${net.resourceType}
|
|
12325
|
+
`);
|
|
12243
12326
|
if (net.requestBody && typeof net.requestBody === "object") {
|
|
12244
12327
|
const bodyStr = JSON.stringify(net.requestBody);
|
|
12245
12328
|
if (bodyStr.length <= 200) {
|
|
12246
|
-
|
|
12329
|
+
process.stdout.write(` body: ${bodyStr}
|
|
12330
|
+
`);
|
|
12247
12331
|
} else {
|
|
12248
|
-
|
|
12332
|
+
process.stdout.write(` body: ${bodyStr.substring(0, 200)}... (${bodyStr.length} bytes)
|
|
12333
|
+
`);
|
|
12249
12334
|
}
|
|
12250
12335
|
}
|
|
12251
12336
|
}
|
|
12252
12337
|
for (const match of step.matchedInputs) {
|
|
12253
|
-
|
|
12338
|
+
process.stdout.write(` \u{1F517} input "${match.inputValue}" \u2192 network #${match.networkId} param "${match.paramName}"
|
|
12339
|
+
`);
|
|
12254
12340
|
}
|
|
12255
12341
|
for (const ctx of step.contextChanges) {
|
|
12256
12342
|
if (ctx.type === "navigate") {
|
|
12257
|
-
|
|
12343
|
+
process.stdout.write(` \u2197 navigate \u2192 ${ctx.url}
|
|
12344
|
+
`);
|
|
12258
12345
|
} else if (ctx.type === "new_tab") {
|
|
12259
|
-
|
|
12346
|
+
process.stdout.write(` \u2197 new tab: ${ctx.url}
|
|
12347
|
+
`);
|
|
12260
12348
|
}
|
|
12261
12349
|
}
|
|
12262
12350
|
}
|
|
12263
12351
|
if (summary.checkpoints && summary.checkpoints.length > 0) {
|
|
12264
|
-
|
|
12265
|
-
|
|
12352
|
+
process.stdout.write("\n");
|
|
12353
|
+
process.stdout.write(`Checkpoints (${summary.checkpoints.length}):
|
|
12354
|
+
`);
|
|
12266
12355
|
for (const cp of summary.checkpoints) {
|
|
12267
12356
|
const src = cp.source === "auto" ? "[auto]" : "[manual]";
|
|
12268
|
-
|
|
12269
|
-
|
|
12357
|
+
process.stdout.write(` ${cp.id}. ${src} [${cp.type}] ${cp.hint}
|
|
12358
|
+
`);
|
|
12359
|
+
if (cp.selector) process.stdout.write(` selector: ${cp.selector}
|
|
12360
|
+
`);
|
|
12270
12361
|
}
|
|
12271
12362
|
}
|
|
12272
12363
|
}
|
|
@@ -12292,13 +12383,25 @@ async function handleReplay(args, options, mode) {
|
|
|
12292
12383
|
}
|
|
12293
12384
|
return;
|
|
12294
12385
|
}
|
|
12386
|
+
if (result.ok && mode !== "json" && mode !== "yaml") {
|
|
12387
|
+
const healed = typeof result.healed === "number" ? result.healed : 0;
|
|
12388
|
+
if (healed > 0) {
|
|
12389
|
+
process.stdout.write(`
|
|
12390
|
+
Self-healed ${healed} action(s):
|
|
12391
|
+
`);
|
|
12392
|
+
for (const d of result.healedDetails ?? []) {
|
|
12393
|
+
process.stdout.write(` - step ${d.index + 1}: ${d.strategy}
|
|
12394
|
+
`);
|
|
12395
|
+
}
|
|
12396
|
+
}
|
|
12397
|
+
}
|
|
12295
12398
|
outputResult(result, mode);
|
|
12296
12399
|
}
|
|
12297
12400
|
async function handleConvert(args, _mode) {
|
|
12298
12401
|
const filePath = args[0];
|
|
12299
12402
|
const outputPath = args[1];
|
|
12300
12403
|
if (!filePath || !outputPath) {
|
|
12301
|
-
|
|
12404
|
+
outputError("Usage: xbrowser convert <recording.yaml> <output.{js,py,sh}>");
|
|
12302
12405
|
process.exit(1);
|
|
12303
12406
|
}
|
|
12304
12407
|
const fs3 = await import("fs");
|
|
@@ -12310,11 +12413,11 @@ async function handleConvert(args, _mode) {
|
|
|
12310
12413
|
const content = fs3.readFileSync(filePath, "utf-8");
|
|
12311
12414
|
recording = yaml.parse(content);
|
|
12312
12415
|
} catch (e) {
|
|
12313
|
-
|
|
12416
|
+
outputError(`Error: Failed to read "${filePath}": ${e instanceof Error ? e.message.split("\n")[0] : String(e)}`);
|
|
12314
12417
|
process.exit(1);
|
|
12315
12418
|
}
|
|
12316
12419
|
if (recording === null || typeof recording !== "object" || Array.isArray(recording)) {
|
|
12317
|
-
|
|
12420
|
+
outputError(`Error: "${filePath}" does not contain a valid recording (expected a YAML/JSON object with events or actions).`);
|
|
12318
12421
|
process.exit(1);
|
|
12319
12422
|
}
|
|
12320
12423
|
const rawActions = recording.actions;
|
|
@@ -12348,24 +12451,28 @@ async function handleConvert(args, _mode) {
|
|
|
12348
12451
|
fs3.writeFileSync(outputPath, script);
|
|
12349
12452
|
fs3.chmodSync(outputPath, 493);
|
|
12350
12453
|
const eventCount = (recordingTyped.events || []).length;
|
|
12351
|
-
|
|
12352
|
-
|
|
12353
|
-
|
|
12454
|
+
process.stdout.write(`Converted ${filePath} -> ${outputPath}
|
|
12455
|
+
`);
|
|
12456
|
+
process.stdout.write(` Events: ${eventCount}, Start URL: ${recordingTyped.startUrl}
|
|
12457
|
+
`);
|
|
12458
|
+
process.stdout.write(` Run: ${ext === ".py" ? "python" : ext === ".sh" ? "./" : "node"} ${outputPath}
|
|
12459
|
+
`);
|
|
12354
12460
|
}
|
|
12355
12461
|
async function handleExtract(args, _mode) {
|
|
12356
12462
|
const filePath = args[0];
|
|
12357
12463
|
if (!filePath) {
|
|
12358
|
-
|
|
12464
|
+
outputError("Usage: xbrowser extract <recording.yaml>");
|
|
12359
12465
|
process.exit(1);
|
|
12360
12466
|
}
|
|
12361
12467
|
const { extractAndSave, printExtractSummary } = await import("./extract-EUWPRSKH.js");
|
|
12362
12468
|
try {
|
|
12363
12469
|
const { summary, outputPath } = extractAndSave(filePath);
|
|
12364
12470
|
printExtractSummary(summary);
|
|
12365
|
-
|
|
12366
|
-
Saved LLM summary: ${outputPath}
|
|
12471
|
+
process.stdout.write(`
|
|
12472
|
+
Saved LLM summary: ${outputPath}
|
|
12473
|
+
`);
|
|
12367
12474
|
} catch (e) {
|
|
12368
|
-
|
|
12475
|
+
outputError(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
12369
12476
|
process.exit(1);
|
|
12370
12477
|
}
|
|
12371
12478
|
}
|
|
@@ -12373,7 +12480,7 @@ async function handleFilter(args, _mode, options) {
|
|
|
12373
12480
|
const filePath = args[0];
|
|
12374
12481
|
const outputPath = args[1];
|
|
12375
12482
|
if (!filePath || !outputPath) {
|
|
12376
|
-
|
|
12483
|
+
outputError("Usage: xbrowser filter <input.yaml> <output.yaml> [--exclude type1,type2]");
|
|
12377
12484
|
process.exit(1);
|
|
12378
12485
|
}
|
|
12379
12486
|
const { filterRecording, parseExcludeTypes } = await import("./filter-7YOPVPVC.js");
|
|
@@ -12385,15 +12492,17 @@ async function handleFilter(args, _mode, options) {
|
|
|
12385
12492
|
const excludeTypes = parseExcludeTypes(excludeArgs);
|
|
12386
12493
|
try {
|
|
12387
12494
|
const result = filterRecording(filePath, outputPath, excludeTypes);
|
|
12388
|
-
|
|
12389
|
-
|
|
12495
|
+
process.stdout.write(`Filtered ${filePath} -> ${outputPath}
|
|
12496
|
+
`);
|
|
12497
|
+
process.stdout.write(` Original: ${result.originalCount}, After: ${result.filteredCount}, Removed: ${result.removed} (${result.percentage}%)
|
|
12498
|
+
`);
|
|
12390
12499
|
} catch (e) {
|
|
12391
|
-
|
|
12500
|
+
outputError(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
12392
12501
|
process.exit(1);
|
|
12393
12502
|
}
|
|
12394
12503
|
}
|
|
12395
12504
|
async function handleGeneratePlugin(sessionName, pluginName, outputDir) {
|
|
12396
|
-
const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-
|
|
12505
|
+
const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-563ISSJK.js");
|
|
12397
12506
|
const { readSiteKnowledge: readSiteKnowledge2, toMarkdown } = await import("./site-knowledge-SYC6VCDB.js");
|
|
12398
12507
|
const { mkdirSync: mkdirSync10, writeFileSync: writeFileSync12 } = await import("fs");
|
|
12399
12508
|
const { join: join15 } = await import("path");
|
|
@@ -12417,21 +12526,30 @@ async function handleGeneratePlugin(sessionName, pluginName, outputDir) {
|
|
|
12417
12526
|
if (knowledgeMd) {
|
|
12418
12527
|
writeFileSync12(join15(finalOutputDir, "SITE_KNOWLEDGE.md"), knowledgeMd, "utf-8");
|
|
12419
12528
|
}
|
|
12420
|
-
|
|
12421
|
-
|
|
12422
|
-
|
|
12423
|
-
|
|
12424
|
-
|
|
12529
|
+
process.stdout.write("\n");
|
|
12530
|
+
process.stdout.write("=== Plugin Generated ===\n");
|
|
12531
|
+
process.stdout.write(` Plugin: ${finalPluginName}
|
|
12532
|
+
`);
|
|
12533
|
+
process.stdout.write(` Domain: ${domain}
|
|
12534
|
+
`);
|
|
12535
|
+
process.stdout.write(` Output: ${finalOutputDir}/index.ts
|
|
12536
|
+
`);
|
|
12425
12537
|
if (knowledgeMd) {
|
|
12426
|
-
|
|
12538
|
+
process.stdout.write(` Knowledge: ${finalOutputDir}/SITE_KNOWLEDGE.md
|
|
12539
|
+
`);
|
|
12427
12540
|
}
|
|
12428
|
-
|
|
12429
|
-
|
|
12430
|
-
|
|
12431
|
-
|
|
12432
|
-
|
|
12433
|
-
|
|
12434
|
-
|
|
12541
|
+
process.stdout.write(` Actions: ${data.actions.length}
|
|
12542
|
+
`);
|
|
12543
|
+
process.stdout.write(` APIs: ${data.network.filter((n) => n.contentType.includes("json") || n.url.includes("/api/")).length}
|
|
12544
|
+
`);
|
|
12545
|
+
process.stdout.write("\n");
|
|
12546
|
+
process.stdout.write("Next steps:\n");
|
|
12547
|
+
process.stdout.write(` 1. Review and edit: ${finalOutputDir}/index.ts
|
|
12548
|
+
`);
|
|
12549
|
+
process.stdout.write(` 2. Test: xbrowser ${finalPluginName} <command>
|
|
12550
|
+
`);
|
|
12551
|
+
process.stdout.write(` 3. Reference: ${finalOutputDir}/SITE_KNOWLEDGE.md (for LLM)
|
|
12552
|
+
`);
|
|
12435
12553
|
}
|
|
12436
12554
|
function generatePluginCode(pluginName, domain, data, _knowledgeMd) {
|
|
12437
12555
|
const pagePaths = /* @__PURE__ */ new Set();
|
|
@@ -12610,20 +12728,22 @@ async function handleRun(filePath, options) {
|
|
|
12610
12728
|
});
|
|
12611
12729
|
for (const step of chainResult.steps) {
|
|
12612
12730
|
if (step.success) {
|
|
12613
|
-
|
|
12731
|
+
process.stdout.write(`[OK] ${step.raw}
|
|
12732
|
+
`);
|
|
12614
12733
|
if (step.data && typeof step.data === "object") {
|
|
12615
12734
|
const d = step.data;
|
|
12616
12735
|
for (const [k, v] of Object.entries(d)) {
|
|
12617
12736
|
if (k !== "ok")
|
|
12618
|
-
|
|
12737
|
+
process.stdout.write(` ${k}: ${typeof v === "string" ? v : JSON.stringify(v)}
|
|
12738
|
+
`);
|
|
12619
12739
|
}
|
|
12620
12740
|
}
|
|
12621
12741
|
} else {
|
|
12622
|
-
|
|
12742
|
+
outputError(`[FAIL] ${step.raw}: ${step.message}`);
|
|
12623
12743
|
}
|
|
12624
12744
|
}
|
|
12625
12745
|
if (chainResult.stoppedReason) {
|
|
12626
|
-
|
|
12746
|
+
outputError(`Stopped: ${chainResult.stoppedReason}`);
|
|
12627
12747
|
}
|
|
12628
12748
|
if (!chainResult.success) process.exit(1);
|
|
12629
12749
|
}
|
|
@@ -12666,26 +12786,29 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
12666
12786
|
if (mode === "json") {
|
|
12667
12787
|
outputResult(result, mode);
|
|
12668
12788
|
} else {
|
|
12669
|
-
|
|
12789
|
+
process.stdout.write(`
|
|
12670
12790
|
Network captures (session: ${netSession})`);
|
|
12671
|
-
|
|
12791
|
+
process.stdout.write(` Total: ${result.total}, Showing: ${result.captures.length}
|
|
12672
12792
|
`);
|
|
12673
12793
|
for (const c of result.captures) {
|
|
12674
12794
|
const statusColor = c.status < 300 ? "\x1B[32m" : c.status < 400 ? "\x1B[33m" : "\x1B[31m";
|
|
12675
12795
|
const reset = "\x1B[0m";
|
|
12676
|
-
|
|
12796
|
+
process.stdout.write(` #${c.id} ${c.method.padEnd(6)} ${statusColor}${c.status}${reset} ${c.resourceType.padEnd(10)} ${c.path}
|
|
12797
|
+
`);
|
|
12677
12798
|
if (c.size > 0) {
|
|
12678
12799
|
const sizeStr = c.size > 1024 ? `${(c.size / 1024).toFixed(1)}KB` : `${c.size}B`;
|
|
12679
|
-
|
|
12800
|
+
process.stdout.write(` ${c.contentType.split(";")[0]} ${sizeStr}
|
|
12801
|
+
`);
|
|
12680
12802
|
}
|
|
12681
12803
|
}
|
|
12682
|
-
|
|
12804
|
+
process.stdout.write("\n");
|
|
12683
12805
|
}
|
|
12684
12806
|
break;
|
|
12685
12807
|
}
|
|
12686
12808
|
case "clear": {
|
|
12687
12809
|
await forwardNetworkClear(netSession);
|
|
12688
|
-
|
|
12810
|
+
process.stdout.write(`Network captures cleared for session: ${netSession}
|
|
12811
|
+
`);
|
|
12689
12812
|
break;
|
|
12690
12813
|
}
|
|
12691
12814
|
case "top": {
|
|
@@ -12695,21 +12818,23 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
12695
12818
|
if (mode === "json") {
|
|
12696
12819
|
outputResult(result, mode);
|
|
12697
12820
|
} else {
|
|
12698
|
-
|
|
12821
|
+
process.stdout.write(`
|
|
12699
12822
|
Top valued requests (session: ${netSession})`);
|
|
12700
|
-
|
|
12823
|
+
process.stdout.write(` Showing: ${result.entries.length}
|
|
12701
12824
|
`);
|
|
12702
12825
|
for (const e of result.entries) {
|
|
12703
12826
|
const scoreColor = e.score >= 50 ? "\x1B[32m" : e.score >= 20 ? "\x1B[33m" : "\x1B[90m";
|
|
12704
12827
|
const reset = "\x1B[0m";
|
|
12705
12828
|
const methodStr = e.method.padEnd(6);
|
|
12706
12829
|
const scoreStr = `${scoreColor}${e.score.toString().padStart(3)}${reset}`;
|
|
12707
|
-
|
|
12830
|
+
process.stdout.write(` ${scoreStr} ${methodStr} ${e.status} ${e.resourceType.padEnd(10)} ${e.path}
|
|
12831
|
+
`);
|
|
12708
12832
|
if (e.scoreBreakdown.content > 0) {
|
|
12709
|
-
|
|
12833
|
+
process.stdout.write(` ${e.contentType.split(";")[0]} ${e.size > 1024 ? (e.size / 1024).toFixed(1) + "KB" : e.size + "B"}
|
|
12834
|
+
`);
|
|
12710
12835
|
}
|
|
12711
12836
|
}
|
|
12712
|
-
|
|
12837
|
+
process.stdout.write("\n");
|
|
12713
12838
|
}
|
|
12714
12839
|
break;
|
|
12715
12840
|
}
|
|
@@ -12718,16 +12843,17 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
12718
12843
|
if (mode === "json") {
|
|
12719
12844
|
outputResult(logResult, mode);
|
|
12720
12845
|
} else {
|
|
12721
|
-
|
|
12846
|
+
process.stdout.write(`
|
|
12722
12847
|
Command log (session: ${netSession})`);
|
|
12723
|
-
|
|
12848
|
+
process.stdout.write(` Total: ${logResult.commands.length}
|
|
12724
12849
|
`);
|
|
12725
12850
|
for (const cmd of logResult.commands) {
|
|
12726
12851
|
const ts = new Date(cmd.timestamp).toISOString().substring(11, 19);
|
|
12727
12852
|
const paramsStr = Object.entries(cmd.params).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
12728
|
-
|
|
12853
|
+
process.stdout.write(` #${cmd.id} [${ts}] ${cmd.command} ${paramsStr}
|
|
12854
|
+
`);
|
|
12729
12855
|
}
|
|
12730
|
-
|
|
12856
|
+
process.stdout.write("\n");
|
|
12731
12857
|
}
|
|
12732
12858
|
break;
|
|
12733
12859
|
}
|
|
@@ -12743,28 +12869,31 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
12743
12869
|
outputResult(aroundResult, mode);
|
|
12744
12870
|
} else {
|
|
12745
12871
|
if (!aroundResult) {
|
|
12746
|
-
|
|
12872
|
+
process.stdout.write(" No command found with that ID\n");
|
|
12747
12873
|
break;
|
|
12748
12874
|
}
|
|
12749
12875
|
const cmd = aroundResult.command;
|
|
12750
12876
|
const ts = new Date(cmd.timestamp).toISOString().substring(11, 19);
|
|
12751
|
-
|
|
12877
|
+
process.stdout.write(`
|
|
12752
12878
|
Command: #${cmd.id} [${ts}] ${cmd.command}`);
|
|
12753
|
-
|
|
12879
|
+
process.stdout.write(` Window: \xB1${windowMs}ms
|
|
12754
12880
|
`);
|
|
12755
12881
|
const before = aroundResult.before;
|
|
12756
12882
|
const after = aroundResult.after;
|
|
12757
|
-
|
|
12883
|
+
process.stdout.write(` BEFORE (${before.length} requests):
|
|
12884
|
+
`);
|
|
12758
12885
|
for (const r of before.slice(0, 5)) {
|
|
12759
|
-
|
|
12886
|
+
process.stdout.write(` ${r.method} ${r.status} ${String(r.resourceType).padEnd(10)} ${r.path}
|
|
12887
|
+
`);
|
|
12760
12888
|
}
|
|
12761
|
-
|
|
12889
|
+
process.stdout.write(`
|
|
12762
12890
|
AFTER (${aroundResult.afterCount} requests):`);
|
|
12763
12891
|
for (const r of after.slice(0, 10)) {
|
|
12764
12892
|
const highlight = r.method !== "GET" ? " \u2190" : "";
|
|
12765
|
-
|
|
12893
|
+
process.stdout.write(` ${String(r.method).padEnd(6)} ${r.status} ${String(r.resourceType).padEnd(10)} ${r.path}${highlight}
|
|
12894
|
+
`);
|
|
12766
12895
|
}
|
|
12767
|
-
|
|
12896
|
+
process.stdout.write("\n");
|
|
12768
12897
|
}
|
|
12769
12898
|
break;
|
|
12770
12899
|
}
|
|
@@ -12773,9 +12902,9 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
12773
12902
|
if (mode === "json") {
|
|
12774
12903
|
outputResult(result, mode);
|
|
12775
12904
|
} else {
|
|
12776
|
-
|
|
12905
|
+
process.stdout.write(`
|
|
12777
12906
|
API Reusability Analysis (session: ${netSession})`);
|
|
12778
|
-
|
|
12907
|
+
process.stdout.write(` Total: ${result.total}, Analyzed: ${result.analyzed.length}
|
|
12779
12908
|
`);
|
|
12780
12909
|
const groups = { high: [], medium: [], low: [], unknown: [] };
|
|
12781
12910
|
for (const e of result.analyzed) {
|
|
@@ -12786,16 +12915,20 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
12786
12915
|
if (!items?.length) continue;
|
|
12787
12916
|
const color = level === "high" ? "\x1B[32m" : level === "medium" ? "\x1B[33m" : level === "low" ? "\x1B[31m" : "\x1B[90m";
|
|
12788
12917
|
const reset = "\x1B[0m";
|
|
12789
|
-
|
|
12918
|
+
process.stdout.write(` ${color}${level.toUpperCase()}${reset} (${items.length})
|
|
12919
|
+
`);
|
|
12790
12920
|
for (const e of items.slice(0, 5)) {
|
|
12791
12921
|
const scoreStr = `[${e.reusability.score.toString().padStart(3)}]`;
|
|
12792
|
-
|
|
12922
|
+
process.stdout.write(` ${e.method.padEnd(6)} ${e.status} ${scoreStr} ${e.path}
|
|
12923
|
+
`);
|
|
12793
12924
|
if (e.reusability.reasons.length > 0) {
|
|
12794
|
-
|
|
12925
|
+
process.stdout.write(` ${e.reusability.reasons.join(", ")}
|
|
12926
|
+
`);
|
|
12795
12927
|
}
|
|
12796
12928
|
}
|
|
12797
|
-
if (items.length > 5)
|
|
12798
|
-
|
|
12929
|
+
if (items.length > 5) process.stdout.write(` ... and ${items.length - 5} more
|
|
12930
|
+
`);
|
|
12931
|
+
process.stdout.write("\n");
|
|
12799
12932
|
}
|
|
12800
12933
|
}
|
|
12801
12934
|
break;
|
|
@@ -12814,12 +12947,12 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
12814
12947
|
if (mode === "json") {
|
|
12815
12948
|
outputResult(result, mode);
|
|
12816
12949
|
} else {
|
|
12817
|
-
|
|
12950
|
+
process.stdout.write(`
|
|
12818
12951
|
${result.method} ${result.url}`);
|
|
12819
|
-
|
|
12952
|
+
process.stdout.write(` Headers: ${result.headerCount}, Body: ${result.hasBody}
|
|
12820
12953
|
`);
|
|
12821
|
-
|
|
12822
|
-
|
|
12954
|
+
process.stdout.write(result.command + "\n");
|
|
12955
|
+
process.stdout.write("\n");
|
|
12823
12956
|
}
|
|
12824
12957
|
break;
|
|
12825
12958
|
}
|
|
@@ -12837,27 +12970,33 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
12837
12970
|
if (mode === "json") {
|
|
12838
12971
|
outputResult(result, mode);
|
|
12839
12972
|
} else {
|
|
12840
|
-
|
|
12973
|
+
process.stdout.write(`
|
|
12841
12974
|
Replay Result`);
|
|
12842
|
-
|
|
12975
|
+
process.stdout.write(` ${result.curlCommand?.split("\n")[0]?.trim()}
|
|
12843
12976
|
`);
|
|
12844
12977
|
const replay = result.replay;
|
|
12845
12978
|
if (replay?.error) {
|
|
12846
|
-
|
|
12979
|
+
process.stdout.write(` \x1B[31mFAILED\x1B[0m: ${replay.error}
|
|
12980
|
+
`);
|
|
12847
12981
|
} else if (replay) {
|
|
12848
12982
|
const statusColor = replay.status && replay.status < 300 ? "\x1B[32m" : "\x1B[31m";
|
|
12849
12983
|
const status = replay.status;
|
|
12850
12984
|
const size = replay.size;
|
|
12851
12985
|
const duration = replay.duration;
|
|
12852
|
-
|
|
12853
|
-
|
|
12854
|
-
|
|
12855
|
-
|
|
12986
|
+
process.stdout.write(` Status: ${statusColor}${status}\x1B[0m ${replay.statusText}
|
|
12987
|
+
`);
|
|
12988
|
+
process.stdout.write(` Size: ${size > 1024 ? (size / 1024).toFixed(1) + "KB" : size + "B"}
|
|
12989
|
+
`);
|
|
12990
|
+
process.stdout.write(` Duration: ${duration}ms
|
|
12991
|
+
`);
|
|
12992
|
+
process.stdout.write(` Body Match: ${replay.bodyMatch ? "\x1B[32mYes\x1B[0m" : "\x1B[33mNo\x1B[0m"}
|
|
12993
|
+
`);
|
|
12856
12994
|
if (status && status >= 400) {
|
|
12857
|
-
|
|
12995
|
+
process.stdout.write(` \x1B[33m\u26A0 API may require fresh signature/token\x1B[0m
|
|
12996
|
+
`);
|
|
12858
12997
|
}
|
|
12859
12998
|
}
|
|
12860
|
-
|
|
12999
|
+
process.stdout.write("\n");
|
|
12861
13000
|
}
|
|
12862
13001
|
break;
|
|
12863
13002
|
}
|
|
@@ -12876,40 +13015,47 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
12876
13015
|
outputResult(result, mode);
|
|
12877
13016
|
} else {
|
|
12878
13017
|
const c = result.capture;
|
|
12879
|
-
|
|
13018
|
+
process.stdout.write(`
|
|
12880
13019
|
Request #${c.id}`);
|
|
12881
|
-
|
|
12882
|
-
|
|
12883
|
-
|
|
13020
|
+
process.stdout.write(` ${c.method} ${c.url}
|
|
13021
|
+
`);
|
|
13022
|
+
process.stdout.write(` Status: ${c.status} | Size: ${c.size}B | Type: ${c.contentType}
|
|
13023
|
+
`);
|
|
13024
|
+
process.stdout.write(` Resource: ${c.resourceType}
|
|
13025
|
+
`);
|
|
12884
13026
|
if (c.requestHeaders) {
|
|
12885
|
-
|
|
13027
|
+
process.stdout.write(`
|
|
12886
13028
|
Request Headers:`);
|
|
12887
13029
|
for (const [k, v] of Object.entries(c.requestHeaders)) {
|
|
12888
|
-
|
|
13030
|
+
process.stdout.write(` ${k}: ${String(v).substring(0, 100)}
|
|
13031
|
+
`);
|
|
12889
13032
|
}
|
|
12890
13033
|
}
|
|
12891
13034
|
if (c.requestBody !== void 0) {
|
|
12892
|
-
|
|
13035
|
+
process.stdout.write(`
|
|
12893
13036
|
Request Body:`);
|
|
12894
13037
|
const bodyStr = typeof c.requestBody === "string" ? c.requestBody : JSON.stringify(c.requestBody, null, 2);
|
|
12895
13038
|
const lines = bodyStr.split("\n").slice(0, 20);
|
|
12896
|
-
for (const line of lines)
|
|
12897
|
-
|
|
13039
|
+
for (const line of lines) process.stdout.write(` ${line}
|
|
13040
|
+
`);
|
|
13041
|
+
if (bodyStr.split("\n").length > 20) process.stdout.write(" ...");
|
|
12898
13042
|
}
|
|
12899
|
-
|
|
13043
|
+
process.stdout.write(`
|
|
12900
13044
|
Response Headers:`);
|
|
12901
13045
|
for (const [k, v] of Object.entries(c.headers)) {
|
|
12902
|
-
|
|
13046
|
+
process.stdout.write(` ${k}: ${String(v).substring(0, 100)}
|
|
13047
|
+
`);
|
|
12903
13048
|
}
|
|
12904
13049
|
if (c.body !== void 0) {
|
|
12905
|
-
|
|
13050
|
+
process.stdout.write(`
|
|
12906
13051
|
Response Body:`);
|
|
12907
13052
|
const bodyStr = typeof c.body === "string" ? c.body : JSON.stringify(c.body, null, 2);
|
|
12908
13053
|
const lines = bodyStr.split("\n").slice(0, 20);
|
|
12909
|
-
for (const line of lines)
|
|
12910
|
-
|
|
13054
|
+
for (const line of lines) process.stdout.write(` ${line}
|
|
13055
|
+
`);
|
|
13056
|
+
if (bodyStr.split("\n").length > 20) process.stdout.write(" ...");
|
|
12911
13057
|
}
|
|
12912
|
-
|
|
13058
|
+
process.stdout.write("\n");
|
|
12913
13059
|
}
|
|
12914
13060
|
break;
|
|
12915
13061
|
}
|
|
@@ -12920,7 +13066,8 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
12920
13066
|
break;
|
|
12921
13067
|
}
|
|
12922
13068
|
await forwardNetworkLike(netSession, id);
|
|
12923
|
-
|
|
13069
|
+
process.stdout.write(`Marked #${id} as useful
|
|
13070
|
+
`);
|
|
12924
13071
|
break;
|
|
12925
13072
|
}
|
|
12926
13073
|
case "dislike": {
|
|
@@ -12930,7 +13077,8 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
12930
13077
|
break;
|
|
12931
13078
|
}
|
|
12932
13079
|
await forwardNetworkDislike(netSession, id);
|
|
12933
|
-
|
|
13080
|
+
process.stdout.write(`Marked #${id} as not useful
|
|
13081
|
+
`);
|
|
12934
13082
|
break;
|
|
12935
13083
|
}
|
|
12936
13084
|
case "export": {
|
|
@@ -12945,7 +13093,7 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
12945
13093
|
outputError(result.error);
|
|
12946
13094
|
break;
|
|
12947
13095
|
}
|
|
12948
|
-
|
|
13096
|
+
process.stdout.write(result.code + "\n");
|
|
12949
13097
|
break;
|
|
12950
13098
|
}
|
|
12951
13099
|
default:
|
|
@@ -12957,9 +13105,25 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
12957
13105
|
}
|
|
12958
13106
|
|
|
12959
13107
|
// src/cli/test-routes.ts
|
|
12960
|
-
import {
|
|
13108
|
+
import { execFile as execFile3 } from "child_process";
|
|
12961
13109
|
import { readFileSync as readFileSync11 } from "fs";
|
|
12962
13110
|
import { resolve as resolve9 } from "path";
|
|
13111
|
+
function execCli(args) {
|
|
13112
|
+
return new Promise((resolvePromise) => {
|
|
13113
|
+
execFile3(
|
|
13114
|
+
"npx",
|
|
13115
|
+
args,
|
|
13116
|
+
{ timeout: 65e3, env: { ...process.env, FORCE_COLOR: "0" } },
|
|
13117
|
+
(err, stdout, stderr) => {
|
|
13118
|
+
resolvePromise({
|
|
13119
|
+
stdout: typeof stdout === "string" ? stdout : stdout?.toString("utf-8") ?? "",
|
|
13120
|
+
stderr: typeof stderr === "string" ? stderr : stderr?.toString("utf-8") ?? "",
|
|
13121
|
+
error: err ?? null
|
|
13122
|
+
});
|
|
13123
|
+
}
|
|
13124
|
+
);
|
|
13125
|
+
});
|
|
13126
|
+
}
|
|
12963
13127
|
function findPluginPath(plugin) {
|
|
12964
13128
|
const candidates = [
|
|
12965
13129
|
resolve9(process.cwd(), ".xcli/plugins", plugin, "index.ts"),
|
|
@@ -13024,20 +13188,21 @@ function extractSchema(plugin, command) {
|
|
|
13024
13188
|
}
|
|
13025
13189
|
async function runTest(plugin, command, cmdArgs, options) {
|
|
13026
13190
|
const cdp = options.cdp || options.cdpEndpoint || "http://localhost:9221";
|
|
13027
|
-
const
|
|
13191
|
+
const passthroughArgs = cmdArgs.filter((a) => !a.startsWith("--cdp"));
|
|
13028
13192
|
const schema = extractSchema(plugin, command);
|
|
13029
|
-
const
|
|
13030
|
-
|
|
13031
|
-
|
|
13032
|
-
|
|
13033
|
-
|
|
13034
|
-
|
|
13035
|
-
|
|
13036
|
-
|
|
13037
|
-
|
|
13038
|
-
|
|
13039
|
-
|
|
13040
|
-
|
|
13193
|
+
const cliArgs = [
|
|
13194
|
+
"xbrowser",
|
|
13195
|
+
plugin,
|
|
13196
|
+
command,
|
|
13197
|
+
...passthroughArgs,
|
|
13198
|
+
"--cdp",
|
|
13199
|
+
String(cdp),
|
|
13200
|
+
"--json",
|
|
13201
|
+
"--timeout",
|
|
13202
|
+
"60000"
|
|
13203
|
+
];
|
|
13204
|
+
const { stdout, stderr, error } = await execCli(cliArgs);
|
|
13205
|
+
if (error) {
|
|
13041
13206
|
const jsonLine = stdout.split("\n").find((l) => {
|
|
13042
13207
|
try {
|
|
13043
13208
|
JSON.parse(l);
|
|
@@ -13056,11 +13221,10 @@ async function runTest(plugin, command, cmdArgs, options) {
|
|
|
13056
13221
|
} catch {
|
|
13057
13222
|
}
|
|
13058
13223
|
}
|
|
13059
|
-
const stderr = err.stderr?.toString() || "";
|
|
13060
13224
|
if (stdout.includes("captcha") || stderr.includes("captcha") || stdout.includes("CAPTCHA")) {
|
|
13061
13225
|
return { status: "CAPTCHA", message: "\u68C0\u6D4B\u5230\u9A8C\u8BC1\u7801", viewerUrl: "http://localhost:9224/preview/default" };
|
|
13062
13226
|
}
|
|
13063
|
-
return { status: "EXEC_ERROR", message: (
|
|
13227
|
+
return { status: "EXEC_ERROR", message: (error.message || "").slice(0, 200) || "\u6267\u884C\u5931\u8D25" };
|
|
13064
13228
|
}
|
|
13065
13229
|
const allLines = stdout.split("\n");
|
|
13066
13230
|
const jsonStart = allLines.findIndex((l) => l.trim().startsWith("{"));
|
|
@@ -13130,27 +13294,27 @@ async function handleTest(cmdArgs, options, mode, cdpEndpoint) {
|
|
|
13130
13294
|
const plugin = cmdArgs[0];
|
|
13131
13295
|
const command = cmdArgs[1];
|
|
13132
13296
|
if (!plugin || !command) {
|
|
13133
|
-
|
|
13134
|
-
|
|
13297
|
+
outputError("\u7528\u6CD5: xbrowser test <plugin> <command> [\u53C2\u6570...]");
|
|
13298
|
+
outputError("\u793A\u4F8B: xbrowser test doubao list --cdp 9221");
|
|
13135
13299
|
return;
|
|
13136
13300
|
}
|
|
13137
13301
|
const loader = await getPluginLoader();
|
|
13138
13302
|
const internalLoader = loader.getCore().loader;
|
|
13139
13303
|
const site = internalLoader.getSite(plugin);
|
|
13140
13304
|
if (!site) {
|
|
13141
|
-
|
|
13305
|
+
outputError(`\u63D2\u4EF6 "${plugin}" \u4E0D\u5B58\u5728`);
|
|
13142
13306
|
return;
|
|
13143
13307
|
}
|
|
13144
13308
|
const cmdEntry = site.getCommand(command);
|
|
13145
13309
|
if (!cmdEntry) {
|
|
13146
|
-
|
|
13310
|
+
outputError(`\u6307\u4EE4 "${command}" \u4E0D\u5B58\u5728`);
|
|
13147
13311
|
return;
|
|
13148
13312
|
}
|
|
13149
13313
|
const testArgs = cmdArgs.slice(2);
|
|
13150
13314
|
const mergedOptions = { ...options, cdp: cdpEndpoint || options.cdp };
|
|
13151
13315
|
const result = await runTest(plugin, command, testArgs, mergedOptions);
|
|
13152
13316
|
if (mode === "json") {
|
|
13153
|
-
|
|
13317
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
13154
13318
|
return;
|
|
13155
13319
|
}
|
|
13156
13320
|
const r = result;
|
|
@@ -13165,23 +13329,32 @@ async function handleTest(cmdArgs, options, mode, cdpEndpoint) {
|
|
|
13165
13329
|
};
|
|
13166
13330
|
const status = String(r.status);
|
|
13167
13331
|
const icon = icons[status] || "\u2753";
|
|
13168
|
-
|
|
13169
|
-
${icon} ${plugin}.${command}
|
|
13170
|
-
|
|
13332
|
+
process.stdout.write(`
|
|
13333
|
+
${icon} ${plugin}.${command}
|
|
13334
|
+
`);
|
|
13335
|
+
process.stdout.write(` \u72B6\u6001: ${status}
|
|
13336
|
+
`);
|
|
13171
13337
|
if (status === "OK") {
|
|
13172
|
-
if (r.count)
|
|
13173
|
-
|
|
13338
|
+
if (r.count) process.stdout.write(` \u6570\u636E: ${r.count} \u9879
|
|
13339
|
+
`);
|
|
13340
|
+
if (r.data) process.stdout.write(` \u9884\u89C8: ${String(r.data).slice(0, 150)}
|
|
13341
|
+
`);
|
|
13174
13342
|
} else if (status === "LOGIN_REQUIRED" || status === "CAPTCHA") {
|
|
13175
|
-
|
|
13176
|
-
|
|
13343
|
+
process.stdout.write(` \u4FE1\u606F: ${String(r.message)}
|
|
13344
|
+
`);
|
|
13345
|
+
process.stdout.write(` Viewer: ${String(r.viewerUrl)}`);
|
|
13177
13346
|
} else if (status === "SCHEMA_ERROR") {
|
|
13178
13347
|
const errs = r.errors;
|
|
13179
|
-
if (errs)
|
|
13348
|
+
if (errs) process.stdout.write(` \u9519\u8BEF: ${errs.join("; ")}
|
|
13349
|
+
`);
|
|
13180
13350
|
} else if (["NO_DATA", "BLOCKED"].includes(status)) {
|
|
13181
|
-
|
|
13182
|
-
|
|
13351
|
+
process.stdout.write(` \u4FE1\u606F: ${String(r.message)}
|
|
13352
|
+
`);
|
|
13353
|
+
if (r.viewerUrl) process.stdout.write(` Viewer: ${String(r.viewerUrl)}
|
|
13354
|
+
`);
|
|
13183
13355
|
} else {
|
|
13184
|
-
|
|
13356
|
+
process.stdout.write(` \u4FE1\u606F: ${String(r.message)}
|
|
13357
|
+
`);
|
|
13185
13358
|
}
|
|
13186
13359
|
}
|
|
13187
13360
|
|
|
@@ -13279,7 +13452,7 @@ Commands:
|
|
|
13279
13452
|
plugin list List plugins
|
|
13280
13453
|
plugin reload <name> Reload plugin
|
|
13281
13454
|
create <name> --template <type> Create plugin
|
|
13282
|
-
serve [--port <
|
|
13455
|
+
serve [--port <p>] [--host <h>] [--token <t>] [--cors-origins <csv>] Start HTTP server (loopback by default)
|
|
13283
13456
|
remote <url> [command] [--token <t>] Execute on remote server
|
|
13284
13457
|
run <file> Execute commands from file
|
|
13285
13458
|
viewer [--name <n>] Generate viewer URL
|
|
@@ -13400,10 +13573,26 @@ function validateAuth(authHeader, validTokens) {
|
|
|
13400
13573
|
function isAuthRequired(validTokens) {
|
|
13401
13574
|
return validTokens.length > 0;
|
|
13402
13575
|
}
|
|
13576
|
+
var IPV6_LOOPBACK = /* @__PURE__ */ new Set(["::1", "0:0:0:0:0:0:0:1"]);
|
|
13577
|
+
function isLoopbackHost(host) {
|
|
13578
|
+
const h = host.trim().toLowerCase().replace(/^\[/, "").replace(/\]$/, "");
|
|
13579
|
+
if (h === "localhost") return true;
|
|
13580
|
+
if (IPV6_LOOPBACK.has(h)) return true;
|
|
13581
|
+
const ipv4 = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
13582
|
+
return ipv4 !== null && ipv4[1] === "127";
|
|
13583
|
+
}
|
|
13584
|
+
function isLoopbackOrigin(origin) {
|
|
13585
|
+
try {
|
|
13586
|
+
const url = new URL(origin);
|
|
13587
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
13588
|
+
return isLoopbackHost(url.hostname);
|
|
13589
|
+
} catch {
|
|
13590
|
+
return false;
|
|
13591
|
+
}
|
|
13592
|
+
}
|
|
13403
13593
|
|
|
13404
13594
|
// src/server/router.ts
|
|
13405
|
-
var
|
|
13406
|
-
"Access-Control-Allow-Origin": "*",
|
|
13595
|
+
var CORS_BASE_HEADERS = {
|
|
13407
13596
|
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
13408
13597
|
"Access-Control-Allow-Headers": "Content-Type, Authorization"
|
|
13409
13598
|
};
|
|
@@ -13456,9 +13645,31 @@ function jsonResponse(statusCode, body, extraHeaders) {
|
|
|
13456
13645
|
return {
|
|
13457
13646
|
statusCode,
|
|
13458
13647
|
body,
|
|
13459
|
-
headers: {
|
|
13648
|
+
headers: { "Content-Type": "application/json", ...extraHeaders }
|
|
13649
|
+
};
|
|
13650
|
+
}
|
|
13651
|
+
function corsHeadersFor(origin, explicitOrigins) {
|
|
13652
|
+
if (!origin || !isOriginAllowed(origin, explicitOrigins)) return {};
|
|
13653
|
+
return {
|
|
13654
|
+
"Access-Control-Allow-Origin": origin,
|
|
13655
|
+
Vary: "Origin",
|
|
13656
|
+
...CORS_BASE_HEADERS
|
|
13460
13657
|
};
|
|
13461
13658
|
}
|
|
13659
|
+
function isOriginAllowed(origin, explicitOrigins) {
|
|
13660
|
+
if (explicitOrigins && explicitOrigins.length > 0) {
|
|
13661
|
+
return explicitOrigins.includes("*") || explicitOrigins.includes(origin);
|
|
13662
|
+
}
|
|
13663
|
+
return isLoopbackOrigin(origin);
|
|
13664
|
+
}
|
|
13665
|
+
function isJsonContentType(contentType) {
|
|
13666
|
+
return contentType.toLowerCase().split(";")[0].trim() === "application/json";
|
|
13667
|
+
}
|
|
13668
|
+
function applyCorsHeaders(response, origin, corsOrigins) {
|
|
13669
|
+
const cors = corsHeadersFor(origin, corsOrigins);
|
|
13670
|
+
if (Object.keys(cors).length === 0) return response;
|
|
13671
|
+
return { ...response, headers: { ...cors, ...response.headers ?? {} } };
|
|
13672
|
+
}
|
|
13462
13673
|
function errorResponse(statusCode, error, message) {
|
|
13463
13674
|
return jsonResponse(statusCode, { error, message, statusCode });
|
|
13464
13675
|
}
|
|
@@ -13576,13 +13787,30 @@ async function ensureSession(sessionName, url, cdpEndpoint) {
|
|
|
13576
13787
|
function isHealthCheckPath(pathname) {
|
|
13577
13788
|
return pathname === "/api/v1/health";
|
|
13578
13789
|
}
|
|
13579
|
-
async function route(method, url, headers, body) {
|
|
13790
|
+
async function route(method, url, headers, body, corsOrigins) {
|
|
13791
|
+
const response = await routeInner(method, url, headers, body, corsOrigins);
|
|
13792
|
+
return applyCorsHeaders(response, headers.origin, corsOrigins);
|
|
13793
|
+
}
|
|
13794
|
+
async function routeInner(method, url, headers, body, corsOrigins) {
|
|
13580
13795
|
const parsedUrl = new URL(url, "http://localhost");
|
|
13581
13796
|
const pathname = parsedUrl.pathname;
|
|
13582
13797
|
const query = parseQueryString(parsedUrl.search);
|
|
13583
13798
|
if (method === "OPTIONS") {
|
|
13584
13799
|
return jsonResponse(204, null);
|
|
13585
13800
|
}
|
|
13801
|
+
if (headers.origin && !isOriginAllowed(headers.origin, corsOrigins)) {
|
|
13802
|
+
return errorResponse(403, "FORBIDDEN", `Origin ${headers.origin} is not allowed to call this API`);
|
|
13803
|
+
}
|
|
13804
|
+
if (method === "POST" || method === "PUT") {
|
|
13805
|
+
const contentType = headers["content-type"];
|
|
13806
|
+
if (contentType && !isJsonContentType(contentType)) {
|
|
13807
|
+
return errorResponse(
|
|
13808
|
+
415,
|
|
13809
|
+
"UNSUPPORTED_MEDIA_TYPE",
|
|
13810
|
+
`Content-Type must be application/json, got "${contentType}"`
|
|
13811
|
+
);
|
|
13812
|
+
}
|
|
13813
|
+
}
|
|
13586
13814
|
const match = matchRoute(method, pathname);
|
|
13587
13815
|
if (!match) {
|
|
13588
13816
|
const pathMatch = matchRoute("GET", pathname) || matchRoute("POST", pathname) || matchRoute("DELETE", pathname);
|
|
@@ -13605,19 +13833,24 @@ async function route(method, url, headers, body) {
|
|
|
13605
13833
|
return errorResponse(500, "INTERNAL_ERROR", errMsg(err));
|
|
13606
13834
|
}
|
|
13607
13835
|
}
|
|
13608
|
-
async function handleRequest(req, res, validateAuthFn) {
|
|
13836
|
+
async function handleRequest(req, res, validateAuthFn, corsOrigins) {
|
|
13609
13837
|
const url = req.url || "/";
|
|
13610
13838
|
const method = (req.method || "GET").toUpperCase();
|
|
13611
13839
|
const pathname = new URL(url, "http://localhost").pathname;
|
|
13612
13840
|
if (method === "OPTIONS") {
|
|
13613
|
-
const response2 = await route(method, url, headersToObject(req.headers), null);
|
|
13841
|
+
const response2 = await route(method, url, headersToObject(req.headers), null, corsOrigins);
|
|
13614
13842
|
writeResponse(res, response2);
|
|
13615
13843
|
return;
|
|
13616
13844
|
}
|
|
13617
13845
|
if (validateAuthFn && !isHealthCheckPath(pathname)) {
|
|
13618
13846
|
const authHeader = req.headers["authorization"];
|
|
13619
13847
|
if (!validateAuthFn(authHeader)) {
|
|
13620
|
-
|
|
13848
|
+
const denied = applyCorsHeaders(
|
|
13849
|
+
errorResponse(401, "UNAUTHORIZED", "Invalid or missing authentication token"),
|
|
13850
|
+
req.headers.origin,
|
|
13851
|
+
corsOrigins
|
|
13852
|
+
);
|
|
13853
|
+
writeResponse(res, denied);
|
|
13621
13854
|
return;
|
|
13622
13855
|
}
|
|
13623
13856
|
}
|
|
@@ -13625,7 +13858,7 @@ async function handleRequest(req, res, validateAuthFn) {
|
|
|
13625
13858
|
if (method === "POST" || method === "PUT") {
|
|
13626
13859
|
body = await readBody(req);
|
|
13627
13860
|
}
|
|
13628
|
-
const response = await route(method, url, headersToObject(req.headers), body);
|
|
13861
|
+
const response = await route(method, url, headersToObject(req.headers), body, corsOrigins);
|
|
13629
13862
|
writeResponse(res, response);
|
|
13630
13863
|
}
|
|
13631
13864
|
function headersToObject(headers) {
|
|
@@ -13666,26 +13899,35 @@ var HTTPServer = class {
|
|
|
13666
13899
|
host;
|
|
13667
13900
|
server = null;
|
|
13668
13901
|
validTokens;
|
|
13902
|
+
corsOrigins;
|
|
13669
13903
|
constructor(config) {
|
|
13670
13904
|
this.port = config?.port ?? 9224;
|
|
13671
|
-
this.host = config?.host ?? "
|
|
13905
|
+
this.host = config?.host ?? "127.0.0.1";
|
|
13672
13906
|
this.validTokens = resolveTokens(config?.tokens);
|
|
13907
|
+
this.corsOrigins = config?.corsOrigins;
|
|
13673
13908
|
}
|
|
13674
13909
|
/**
|
|
13675
13910
|
* Start the HTTP server and begin listening for requests.
|
|
13676
13911
|
*
|
|
13677
13912
|
* @returns The actual port and host the server bound to.
|
|
13678
|
-
* @throws If the server is already running
|
|
13913
|
+
* @throws If the server is already running, if binding a non-loopback
|
|
13914
|
+
* host without an auth token, or if the port cannot be bound.
|
|
13679
13915
|
*/
|
|
13680
13916
|
async start() {
|
|
13681
13917
|
if (this.server) {
|
|
13682
13918
|
throw new Error("HTTP server is already running");
|
|
13683
13919
|
}
|
|
13920
|
+
if (!isLoopbackHost(this.host) && !isAuthRequired(this.validTokens)) {
|
|
13921
|
+
throw new Error(
|
|
13922
|
+
`Refusing to start on non-loopback host "${this.host}" without an auth token \u2014 the API exposes session creation and command execution. Bind loopback only (the default, or --host 127.0.0.1), or provide a token via --token or XBROWSER_SERVER_TOKEN.`
|
|
13923
|
+
);
|
|
13924
|
+
}
|
|
13684
13925
|
const authRequired = isAuthRequired(this.validTokens);
|
|
13685
13926
|
const tokens = this.validTokens;
|
|
13927
|
+
const corsOrigins = this.corsOrigins;
|
|
13686
13928
|
this.server = createServer((req, res) => {
|
|
13687
13929
|
const authFn = authRequired ? (authHeader) => validateAuth(authHeader, tokens) : void 0;
|
|
13688
|
-
handleRequest(req, res, authFn).catch((err) => {
|
|
13930
|
+
handleRequest(req, res, authFn, corsOrigins).catch((err) => {
|
|
13689
13931
|
const message = err instanceof Error ? err.message : String(err);
|
|
13690
13932
|
if (!res.headersSent) {
|
|
13691
13933
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
@@ -14156,7 +14398,7 @@ async function routeCommand(argvIn, stdinCommands) {
|
|
|
14156
14398
|
replay: "replay <file> [--slow-mo <ms>] [--stop-on-error]",
|
|
14157
14399
|
create: "create <name> [--template static|dynamic|login|api]",
|
|
14158
14400
|
run: "run <file>",
|
|
14159
|
-
serve: "serve [--port <port>] [--token <token>]",
|
|
14401
|
+
serve: "serve [--port <port>] [--host <host>] [--token <token>] [--cors-origins <csv>]",
|
|
14160
14402
|
remote: "remote <url> [command] [--token <token>]",
|
|
14161
14403
|
convert: "convert <file> [--to js|py|sh]",
|
|
14162
14404
|
extract: "extract <file> [--format json|yaml]",
|
|
@@ -14433,7 +14675,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
14433
14675
|
const targetPage = pages[cmdTabIndex];
|
|
14434
14676
|
await targetPage.bringToFront().catch(() => {
|
|
14435
14677
|
});
|
|
14436
|
-
const { setActivePage: setActivePage2 } = await import("./browser-
|
|
14678
|
+
const { setActivePage: setActivePage2 } = await import("./browser-BVRFSUXU.js");
|
|
14437
14679
|
setActivePage2(session, targetPage);
|
|
14438
14680
|
}
|
|
14439
14681
|
}
|
|
@@ -14555,7 +14797,10 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
14555
14797
|
async function handleServe(_args, options, mode) {
|
|
14556
14798
|
const port = options.port ? Number(options.port) : void 0;
|
|
14557
14799
|
const token = options.token;
|
|
14558
|
-
const
|
|
14800
|
+
const host = options.host;
|
|
14801
|
+
const corsOriginsRaw = options["cors-origins"];
|
|
14802
|
+
const corsOrigins = corsOriginsRaw ? corsOriginsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
14803
|
+
const httpServer = new HTTPServer({ port, host, tokens: token ? [token] : void 0, corsOrigins });
|
|
14559
14804
|
process.on("SIGINT", async () => {
|
|
14560
14805
|
await httpServer.stop();
|
|
14561
14806
|
return;
|
|
@@ -14709,7 +14954,7 @@ async function main() {
|
|
|
14709
14954
|
const command = process.argv[2];
|
|
14710
14955
|
const isLongRunning = command === "preview" || command === "serve";
|
|
14711
14956
|
if (!isLongRunning) {
|
|
14712
|
-
const { ensureProcessCanExit } = await import("./browser-
|
|
14957
|
+
const { ensureProcessCanExit } = await import("./browser-BVRFSUXU.js");
|
|
14713
14958
|
await ensureProcessCanExit().catch(() => {
|
|
14714
14959
|
});
|
|
14715
14960
|
process.exit(process.exitCode || exitCode);
|