@xbrowser/cli 1.22.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 +20 -3
- package/dist/{chunk-PIWNMC36.js → chunk-A3MORKYR.js} +1 -1
- package/dist/{chunk-OMU63E6J.js → chunk-FBQZ3PVD.js} +5 -8
- package/dist/{chunk-TEXCXIBW.js → chunk-LPW6QS4M.js} +5 -8
- package/dist/{chunk-NKW4A74J.js → chunk-XTUAUFL3.js} +5 -8
- package/dist/cli.js +475 -242
- package/dist/daemon-main.js +6 -6
- 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 +15 -3
- package/dist/index.js +477 -244
- 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-replayer-XM5L7LFD.js → session-replayer-N665T3G5.js} +22 -17
- package/package.json +11 -7
package/dist/cli.js
CHANGED
|
@@ -7172,7 +7172,7 @@ import { join as join7 } from "path";
|
|
|
7172
7172
|
import { execSync } from "child_process";
|
|
7173
7173
|
var SHARED_PLUGIN_DEPENDENCIES = {
|
|
7174
7174
|
"zod": "^3.24.0",
|
|
7175
|
-
"@dyyz1993/xcli-core": "^0.
|
|
7175
|
+
"@dyyz1993/xcli-core": "^0.19.0"
|
|
7176
7176
|
};
|
|
7177
7177
|
function ensurePluginDependencies(pluginsDir) {
|
|
7178
7178
|
const zodPath = join7(pluginsDir, "node_modules", "zod");
|
|
@@ -8467,7 +8467,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
8467
8467
|
const errorMessage = errMsg(err);
|
|
8468
8468
|
if (session?.page && process.env.XBROWSER_RECOVERY && !extraOpts?._recoveryAttempted) {
|
|
8469
8469
|
try {
|
|
8470
|
-
const { attemptRecovery } = await import("./recovery-
|
|
8470
|
+
const { attemptRecovery } = await import("./recovery-6RI2FE3E.js");
|
|
8471
8471
|
const recovery = await attemptRecovery(
|
|
8472
8472
|
session.page,
|
|
8473
8473
|
sessionName,
|
|
@@ -9149,13 +9149,39 @@ async function installFromNpm(packageName, name, targetDir) {
|
|
|
9149
9149
|
import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync7, rmSync as rmSync3, cpSync as cpSync3 } from "fs";
|
|
9150
9150
|
import { resolve as resolve5, join as join10 } from "path";
|
|
9151
9151
|
import { tmpdir as tmpdir2 } from "os";
|
|
9152
|
-
import {
|
|
9152
|
+
import { execFile as execFile2 } from "child_process";
|
|
9153
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
|
+
}
|
|
9154
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
|
+
}
|
|
9155
9181
|
const tmpDir = join10(tmpdir2(), `xbrowser-git-${Date.now()}`);
|
|
9156
9182
|
let warnings = [];
|
|
9157
9183
|
try {
|
|
9158
|
-
|
|
9184
|
+
await execGit(["clone", "--depth", "1", "--", gitUrl, tmpDir]);
|
|
9159
9185
|
const verify = verifyPlugin3(tmpDir, { metadataField: "xbrowser" });
|
|
9160
9186
|
warnings = verify.warnings ?? [];
|
|
9161
9187
|
if (!verify.valid) {
|
|
@@ -10990,7 +11016,7 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
|
|
|
10990
11016
|
}
|
|
10991
11017
|
outputResult({ command: cmdDef.name, description: cmdDef.description, scope: cmdDef.scope, parameters: paramsList }, mode);
|
|
10992
11018
|
} else {
|
|
10993
|
-
|
|
11019
|
+
process.stdout.write(helpGenerator.generate(cmdDef, { color: true, emoji: false }));
|
|
10994
11020
|
}
|
|
10995
11021
|
} else {
|
|
10996
11022
|
outputError(`Unknown command: ${command}`);
|
|
@@ -11391,7 +11417,7 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
|
|
|
11391
11417
|
if (isEmptyResult) {
|
|
11392
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";
|
|
11393
11419
|
outputResult(result.data, mode);
|
|
11394
|
-
|
|
11420
|
+
outputError(`
|
|
11395
11421
|
\u26A0\uFE0F ${hint}`);
|
|
11396
11422
|
process.exit(1);
|
|
11397
11423
|
} else {
|
|
@@ -11408,8 +11434,9 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
|
|
|
11408
11434
|
try {
|
|
11409
11435
|
mkdirSync10(dirname6(outputFile), { recursive: true });
|
|
11410
11436
|
writeFileSync12(outputFile, content, "utf-8");
|
|
11411
|
-
|
|
11412
|
-
\u{1F4C4} Written to ${outputFile}
|
|
11437
|
+
process.stdout.write(`
|
|
11438
|
+
\u{1F4C4} Written to ${outputFile}
|
|
11439
|
+
`);
|
|
11413
11440
|
} catch (err) {
|
|
11414
11441
|
outputError(`Failed to write --output "${outputFile}": ${err instanceof Error ? err.message : String(err)}`);
|
|
11415
11442
|
}
|
|
@@ -11507,7 +11534,7 @@ async function handleSession(args, options, mode, _cdpEndpoint) {
|
|
|
11507
11534
|
break;
|
|
11508
11535
|
}
|
|
11509
11536
|
default:
|
|
11510
|
-
|
|
11537
|
+
process.stdout.write(handleSessionHelp() + "\n");
|
|
11511
11538
|
}
|
|
11512
11539
|
}
|
|
11513
11540
|
|
|
@@ -11681,19 +11708,24 @@ async function handleSearch(args, options, mode) {
|
|
|
11681
11708
|
outputEnvelope({ success: true, data: { results: deduped, total: deduped.length } }, { command: "plugin search" }, mode);
|
|
11682
11709
|
} else {
|
|
11683
11710
|
if (deduped.length === 0) {
|
|
11684
|
-
|
|
11711
|
+
process.stdout.write("No plugins found\n");
|
|
11685
11712
|
return;
|
|
11686
11713
|
}
|
|
11687
11714
|
for (const r of deduped) {
|
|
11688
11715
|
const src = r.source === "marketplace" ? "[marketplace]" : r.source === "local" ? "[local]" : "[npm]";
|
|
11689
11716
|
const slug = r.slug ? ` (${r.slug})` : "";
|
|
11690
|
-
|
|
11691
|
-
|
|
11692
|
-
if (r.
|
|
11693
|
-
|
|
11694
|
-
|
|
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");
|
|
11695
11726
|
}
|
|
11696
|
-
|
|
11727
|
+
process.stdout.write(`Total: ${deduped.length} plugins
|
|
11728
|
+
`);
|
|
11697
11729
|
}
|
|
11698
11730
|
}
|
|
11699
11731
|
async function handlePluginInfo(args, options, mode) {
|
|
@@ -11710,14 +11742,22 @@ async function handlePluginInfo(args, options, mode) {
|
|
|
11710
11742
|
outputEnvelope({ success: true, data: { source: "marketplace", ...d } }, { command: "plugin info" }, mode);
|
|
11711
11743
|
return;
|
|
11712
11744
|
}
|
|
11713
|
-
|
|
11714
|
-
|
|
11715
|
-
|
|
11716
|
-
|
|
11717
|
-
|
|
11718
|
-
|
|
11719
|
-
|
|
11720
|
-
|
|
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
|
+
`);
|
|
11721
11761
|
return;
|
|
11722
11762
|
}
|
|
11723
11763
|
} catch {
|
|
@@ -11736,19 +11776,25 @@ async function handlePluginInfo(args, options, mode) {
|
|
|
11736
11776
|
outputEnvelope({ success: true, data: { source: "npm", name: pkg.name, version: latest, description: pkg.description } }, { command: "plugin info" }, mode);
|
|
11737
11777
|
return;
|
|
11738
11778
|
}
|
|
11739
|
-
|
|
11740
|
-
|
|
11741
|
-
|
|
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
|
+
`);
|
|
11742
11785
|
const author = pkg.author;
|
|
11743
|
-
|
|
11744
|
-
|
|
11745
|
-
|
|
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
|
+
`);
|
|
11746
11792
|
return;
|
|
11747
11793
|
}
|
|
11748
11794
|
}
|
|
11749
|
-
|
|
11795
|
+
outputError(`\u63D2\u4EF6 '${slug}' \u672A\u627E\u5230`);
|
|
11750
11796
|
} catch (err) {
|
|
11751
|
-
|
|
11797
|
+
outputError("\u67E5\u8BE2\u5931\u8D25: " + errMsg(err));
|
|
11752
11798
|
}
|
|
11753
11799
|
}
|
|
11754
11800
|
async function handlePluginSchema(args, mode) {
|
|
@@ -11772,32 +11818,39 @@ async function handlePluginSchema(args, mode) {
|
|
|
11772
11818
|
}
|
|
11773
11819
|
}
|
|
11774
11820
|
function printPluginContract(contract) {
|
|
11775
|
-
|
|
11776
|
-
|
|
11777
|
-
|
|
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");
|
|
11778
11825
|
for (const command of contract.commands) {
|
|
11779
11826
|
printCommandContract(contract.plugin.name, command);
|
|
11780
11827
|
}
|
|
11781
11828
|
}
|
|
11782
11829
|
function printCommandContract(pluginName, command) {
|
|
11783
|
-
|
|
11784
|
-
|
|
11785
|
-
|
|
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
|
+
`);
|
|
11786
11836
|
if (command.capabilities.length > 0) {
|
|
11787
|
-
|
|
11837
|
+
process.stdout.write(` capabilities: ${command.capabilities.join(", ")}
|
|
11838
|
+
`);
|
|
11788
11839
|
}
|
|
11789
11840
|
if (command.positional.length > 0) {
|
|
11790
|
-
|
|
11841
|
+
process.stdout.write(` positional: ${command.positional.join(", ")}
|
|
11842
|
+
`);
|
|
11791
11843
|
}
|
|
11792
11844
|
if (command.form.fields.length > 0) {
|
|
11793
|
-
|
|
11845
|
+
process.stdout.write(" fields:\n");
|
|
11794
11846
|
for (const field of command.form.fields) {
|
|
11795
11847
|
const required = field.required ? "required" : "optional";
|
|
11796
11848
|
const choices = field.enum ? ` [${field.enum.join("|")}]` : "";
|
|
11797
|
-
|
|
11849
|
+
process.stdout.write(` --${field.name}: ${field.type}/${field.widget} ${required}${choices}
|
|
11850
|
+
`);
|
|
11798
11851
|
}
|
|
11799
11852
|
}
|
|
11800
|
-
|
|
11853
|
+
process.stdout.write("\n");
|
|
11801
11854
|
}
|
|
11802
11855
|
async function handlePlugin(args, options, mode) {
|
|
11803
11856
|
const sub = args[0];
|
|
@@ -11886,24 +11939,28 @@ async function handlePlugin(args, options, mode) {
|
|
|
11886
11939
|
outputEnvelope({ success: true, data: { plugins: enrichedPlugins } }, { command: "plugin list" }, mode);
|
|
11887
11940
|
} else {
|
|
11888
11941
|
if (enrichedPlugins.length === 0) {
|
|
11889
|
-
|
|
11942
|
+
process.stdout.write("No plugins installed\n");
|
|
11890
11943
|
return;
|
|
11891
11944
|
}
|
|
11892
11945
|
for (const p of enrichedPlugins) {
|
|
11893
11946
|
const loginTag = p.hasLogin ? p.loggedIn ? " [logged in]" : " [need login]" : "";
|
|
11894
11947
|
if (p.version && p.description) {
|
|
11895
|
-
|
|
11948
|
+
process.stdout.write(`${p.name} (${p.version}) - ${p.description}${loginTag}
|
|
11949
|
+
`);
|
|
11896
11950
|
} else {
|
|
11897
|
-
|
|
11951
|
+
process.stdout.write(`${p.name}${loginTag}
|
|
11952
|
+
`);
|
|
11898
11953
|
}
|
|
11899
11954
|
if (p.commands && p.commands.length > 0) {
|
|
11900
|
-
|
|
11955
|
+
process.stdout.write(` ${p.commands.join(", ")}
|
|
11956
|
+
`);
|
|
11901
11957
|
}
|
|
11902
11958
|
if (p.requiresLoginCommands.length > 0) {
|
|
11903
|
-
|
|
11959
|
+
process.stdout.write(` requires login: ${p.requiresLoginCommands.join(", ")}
|
|
11960
|
+
`);
|
|
11904
11961
|
}
|
|
11905
11962
|
}
|
|
11906
|
-
|
|
11963
|
+
process.stdout.write(`
|
|
11907
11964
|
Total: ${enrichedPlugins.length} plugins`);
|
|
11908
11965
|
}
|
|
11909
11966
|
break;
|
|
@@ -11940,7 +11997,7 @@ See docs/plugin-guide.md for the publishing workflow.`
|
|
|
11940
11997
|
);
|
|
11941
11998
|
break;
|
|
11942
11999
|
default:
|
|
11943
|
-
|
|
12000
|
+
process.stdout.write(handlePluginHelp() + "\n");
|
|
11944
12001
|
}
|
|
11945
12002
|
}
|
|
11946
12003
|
function handleCreate(args, options) {
|
|
@@ -11974,7 +12031,7 @@ function handleDaemon(args, options, mode) {
|
|
|
11974
12031
|
break;
|
|
11975
12032
|
}
|
|
11976
12033
|
default:
|
|
11977
|
-
|
|
12034
|
+
process.stdout.write("Daemon starts automatically. No manual action needed.\n");
|
|
11978
12035
|
}
|
|
11979
12036
|
}
|
|
11980
12037
|
|
|
@@ -12043,8 +12100,8 @@ async function handleRecord(args, options, mode) {
|
|
|
12043
12100
|
}, mode);
|
|
12044
12101
|
const md = SessionRecorder.readMarkdownSummary(sessionName);
|
|
12045
12102
|
if (md) {
|
|
12046
|
-
|
|
12047
|
-
|
|
12103
|
+
process.stdout.write("\n");
|
|
12104
|
+
process.stdout.write(md + "\n");
|
|
12048
12105
|
} else {
|
|
12049
12106
|
const summary = SessionRecorder.readSummary(sessionName);
|
|
12050
12107
|
if (summary) {
|
|
@@ -12103,25 +12160,30 @@ async function handleRecord(args, options, mode) {
|
|
|
12103
12160
|
break;
|
|
12104
12161
|
}
|
|
12105
12162
|
default:
|
|
12106
|
-
|
|
12107
|
-
|
|
12108
|
-
|
|
12109
|
-
|
|
12110
|
-
|
|
12111
|
-
|
|
12112
|
-
|
|
12113
|
-
|
|
12114
|
-
|
|
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");
|
|
12115
12172
|
}
|
|
12116
12173
|
}
|
|
12117
12174
|
function printRecordingSummary(summary, sessionName) {
|
|
12118
|
-
|
|
12119
|
-
|
|
12120
|
-
|
|
12121
|
-
|
|
12122
|
-
|
|
12123
|
-
|
|
12124
|
-
|
|
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
|
+
`);
|
|
12125
12187
|
for (const step of summary.steps) {
|
|
12126
12188
|
const a = step.action;
|
|
12127
12189
|
const el = a.element;
|
|
@@ -12143,20 +12205,24 @@ function printRecordingSummary(summary, sessionName) {
|
|
|
12143
12205
|
}
|
|
12144
12206
|
const navInfo = step.contextChanges.find((c) => c.type === "navigate");
|
|
12145
12207
|
if (navInfo) desc += ` \u2192 navigate to ${navInfo.url?.substring(0, 80)}`;
|
|
12146
|
-
|
|
12208
|
+
process.stdout.write(` ${step.step}. ${desc}
|
|
12209
|
+
`);
|
|
12147
12210
|
if (a.clickContext) {
|
|
12148
12211
|
const ctx = a.clickContext;
|
|
12149
12212
|
if (ctx.appeared?.length > 0) {
|
|
12150
12213
|
for (const popup of ctx.appeared) {
|
|
12151
12214
|
const roleStr = popup.role ? ` [${popup.role}]` : "";
|
|
12152
|
-
|
|
12215
|
+
process.stdout.write(` \u21B3 ${popup.tag}${roleStr} "${(popup.text || "").substring(0, 60)}"
|
|
12216
|
+
`);
|
|
12153
12217
|
if (popup.items?.length > 0) {
|
|
12154
12218
|
for (const item of popup.items.slice(0, 10)) {
|
|
12155
12219
|
const disStr = item.disabled ? " [disabled]" : "";
|
|
12156
|
-
|
|
12220
|
+
process.stdout.write(` \u2022 ${item.text}${disStr}
|
|
12221
|
+
`);
|
|
12157
12222
|
}
|
|
12158
12223
|
if (popup.items.length > 10) {
|
|
12159
|
-
|
|
12224
|
+
process.stdout.write(` ... and ${popup.items.length - 10} more items
|
|
12225
|
+
`);
|
|
12160
12226
|
}
|
|
12161
12227
|
}
|
|
12162
12228
|
}
|
|
@@ -12169,31 +12235,41 @@ function printRecordingSummary(summary, sessionName) {
|
|
|
12169
12235
|
if (sc.ariaSelected !== void 0) parts.push(`selected=${sc.ariaSelected}`);
|
|
12170
12236
|
if (sc.dataState) parts.push(`state=${sc.dataState}`);
|
|
12171
12237
|
if (parts.length > 0) {
|
|
12172
|
-
|
|
12238
|
+
process.stdout.write(` \u21B3 state: <${sc.tag}> "${(sc.text || "").substring(0, 30)}" ${parts.join(", ")}
|
|
12239
|
+
`);
|
|
12173
12240
|
}
|
|
12174
12241
|
}
|
|
12175
12242
|
}
|
|
12176
12243
|
}
|
|
12177
12244
|
}
|
|
12178
|
-
|
|
12179
|
-
|
|
12245
|
+
process.stdout.write("\n");
|
|
12246
|
+
process.stdout.write(` Files: ${SessionRecorder.getRecordingsDir(sessionName)}/
|
|
12247
|
+
`);
|
|
12180
12248
|
if (summary.checkpoints && summary.checkpoints.length > 0) {
|
|
12181
|
-
|
|
12182
|
-
|
|
12249
|
+
process.stdout.write("\n");
|
|
12250
|
+
process.stdout.write(` Checkpoints (${summary.checkpoints.length}):
|
|
12251
|
+
`);
|
|
12183
12252
|
for (const cp of summary.checkpoints) {
|
|
12184
12253
|
const src = cp.source === "auto" ? "[auto]" : "[manual]";
|
|
12185
|
-
|
|
12186
|
-
|
|
12254
|
+
process.stdout.write(` ${cp.id}. ${src} [${cp.type}] ${cp.hint}
|
|
12255
|
+
`);
|
|
12256
|
+
if (cp.selector) process.stdout.write(` selector: ${cp.selector}
|
|
12257
|
+
`);
|
|
12187
12258
|
}
|
|
12188
12259
|
}
|
|
12189
12260
|
}
|
|
12190
12261
|
function printHumanReadableSummary(summary) {
|
|
12191
|
-
|
|
12192
|
-
|
|
12193
|
-
|
|
12194
|
-
|
|
12195
|
-
|
|
12196
|
-
|
|
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");
|
|
12197
12273
|
for (const step of summary.steps) {
|
|
12198
12274
|
const a = step.action;
|
|
12199
12275
|
const el = a.element;
|
|
@@ -12209,20 +12285,23 @@ function printHumanReadableSummary(summary) {
|
|
|
12209
12285
|
if (a.value) parts.push(`value="${a.value.substring(0, 50)}"`);
|
|
12210
12286
|
if (a.key) parts.push(`key=${a.key}`);
|
|
12211
12287
|
if (a.x !== void 0 && a.y !== void 0) parts.push(`@(${a.x},${a.y})`);
|
|
12212
|
-
|
|
12288
|
+
process.stdout.write(parts.join(" ") + "\n");
|
|
12213
12289
|
if (a.clickContext) {
|
|
12214
12290
|
const ctx = a.clickContext;
|
|
12215
12291
|
if (ctx.appeared?.length > 0) {
|
|
12216
12292
|
for (const popup of ctx.appeared) {
|
|
12217
12293
|
const roleStr = popup.role ? ` [${popup.role}]` : "";
|
|
12218
|
-
|
|
12294
|
+
process.stdout.write(` \u{1F4CB} ${popup.tag}${roleStr} "${(popup.text || "").substring(0, 60)}"
|
|
12295
|
+
`);
|
|
12219
12296
|
if (popup.items?.length > 0) {
|
|
12220
12297
|
for (const item of popup.items.slice(0, 10)) {
|
|
12221
12298
|
const disStr = item.disabled ? " [disabled]" : "";
|
|
12222
|
-
|
|
12299
|
+
process.stdout.write(` \u2022 ${item.text}${disStr}
|
|
12300
|
+
`);
|
|
12223
12301
|
}
|
|
12224
12302
|
if (popup.items.length > 10) {
|
|
12225
|
-
|
|
12303
|
+
process.stdout.write(` ... and ${popup.items.length - 10} more items
|
|
12304
|
+
`);
|
|
12226
12305
|
}
|
|
12227
12306
|
}
|
|
12228
12307
|
}
|
|
@@ -12235,40 +12314,50 @@ function printHumanReadableSummary(summary) {
|
|
|
12235
12314
|
if (sc.ariaSelected !== void 0) stateParts.push(`selected=${sc.ariaSelected}`);
|
|
12236
12315
|
if (sc.dataState) stateParts.push(`state=${sc.dataState}`);
|
|
12237
12316
|
if (stateParts.length > 0) {
|
|
12238
|
-
|
|
12317
|
+
process.stdout.write(` \u{1F504} <${sc.tag}> "${(sc.text || "").substring(0, 30)}" ${stateParts.join(", ")}
|
|
12318
|
+
`);
|
|
12239
12319
|
}
|
|
12240
12320
|
}
|
|
12241
12321
|
}
|
|
12242
12322
|
}
|
|
12243
12323
|
for (const net of step.network) {
|
|
12244
|
-
|
|
12324
|
+
process.stdout.write(` \u2192 ${net.method} ${net.path} [${net.status}] ${net.resourceType}
|
|
12325
|
+
`);
|
|
12245
12326
|
if (net.requestBody && typeof net.requestBody === "object") {
|
|
12246
12327
|
const bodyStr = JSON.stringify(net.requestBody);
|
|
12247
12328
|
if (bodyStr.length <= 200) {
|
|
12248
|
-
|
|
12329
|
+
process.stdout.write(` body: ${bodyStr}
|
|
12330
|
+
`);
|
|
12249
12331
|
} else {
|
|
12250
|
-
|
|
12332
|
+
process.stdout.write(` body: ${bodyStr.substring(0, 200)}... (${bodyStr.length} bytes)
|
|
12333
|
+
`);
|
|
12251
12334
|
}
|
|
12252
12335
|
}
|
|
12253
12336
|
}
|
|
12254
12337
|
for (const match of step.matchedInputs) {
|
|
12255
|
-
|
|
12338
|
+
process.stdout.write(` \u{1F517} input "${match.inputValue}" \u2192 network #${match.networkId} param "${match.paramName}"
|
|
12339
|
+
`);
|
|
12256
12340
|
}
|
|
12257
12341
|
for (const ctx of step.contextChanges) {
|
|
12258
12342
|
if (ctx.type === "navigate") {
|
|
12259
|
-
|
|
12343
|
+
process.stdout.write(` \u2197 navigate \u2192 ${ctx.url}
|
|
12344
|
+
`);
|
|
12260
12345
|
} else if (ctx.type === "new_tab") {
|
|
12261
|
-
|
|
12346
|
+
process.stdout.write(` \u2197 new tab: ${ctx.url}
|
|
12347
|
+
`);
|
|
12262
12348
|
}
|
|
12263
12349
|
}
|
|
12264
12350
|
}
|
|
12265
12351
|
if (summary.checkpoints && summary.checkpoints.length > 0) {
|
|
12266
|
-
|
|
12267
|
-
|
|
12352
|
+
process.stdout.write("\n");
|
|
12353
|
+
process.stdout.write(`Checkpoints (${summary.checkpoints.length}):
|
|
12354
|
+
`);
|
|
12268
12355
|
for (const cp of summary.checkpoints) {
|
|
12269
12356
|
const src = cp.source === "auto" ? "[auto]" : "[manual]";
|
|
12270
|
-
|
|
12271
|
-
|
|
12357
|
+
process.stdout.write(` ${cp.id}. ${src} [${cp.type}] ${cp.hint}
|
|
12358
|
+
`);
|
|
12359
|
+
if (cp.selector) process.stdout.write(` selector: ${cp.selector}
|
|
12360
|
+
`);
|
|
12272
12361
|
}
|
|
12273
12362
|
}
|
|
12274
12363
|
}
|
|
@@ -12297,10 +12386,12 @@ async function handleReplay(args, options, mode) {
|
|
|
12297
12386
|
if (result.ok && mode !== "json" && mode !== "yaml") {
|
|
12298
12387
|
const healed = typeof result.healed === "number" ? result.healed : 0;
|
|
12299
12388
|
if (healed > 0) {
|
|
12300
|
-
|
|
12301
|
-
Self-healed ${healed} action(s)
|
|
12389
|
+
process.stdout.write(`
|
|
12390
|
+
Self-healed ${healed} action(s):
|
|
12391
|
+
`);
|
|
12302
12392
|
for (const d of result.healedDetails ?? []) {
|
|
12303
|
-
|
|
12393
|
+
process.stdout.write(` - step ${d.index + 1}: ${d.strategy}
|
|
12394
|
+
`);
|
|
12304
12395
|
}
|
|
12305
12396
|
}
|
|
12306
12397
|
}
|
|
@@ -12310,7 +12401,7 @@ async function handleConvert(args, _mode) {
|
|
|
12310
12401
|
const filePath = args[0];
|
|
12311
12402
|
const outputPath = args[1];
|
|
12312
12403
|
if (!filePath || !outputPath) {
|
|
12313
|
-
|
|
12404
|
+
outputError("Usage: xbrowser convert <recording.yaml> <output.{js,py,sh}>");
|
|
12314
12405
|
process.exit(1);
|
|
12315
12406
|
}
|
|
12316
12407
|
const fs3 = await import("fs");
|
|
@@ -12322,11 +12413,11 @@ async function handleConvert(args, _mode) {
|
|
|
12322
12413
|
const content = fs3.readFileSync(filePath, "utf-8");
|
|
12323
12414
|
recording = yaml.parse(content);
|
|
12324
12415
|
} catch (e) {
|
|
12325
|
-
|
|
12416
|
+
outputError(`Error: Failed to read "${filePath}": ${e instanceof Error ? e.message.split("\n")[0] : String(e)}`);
|
|
12326
12417
|
process.exit(1);
|
|
12327
12418
|
}
|
|
12328
12419
|
if (recording === null || typeof recording !== "object" || Array.isArray(recording)) {
|
|
12329
|
-
|
|
12420
|
+
outputError(`Error: "${filePath}" does not contain a valid recording (expected a YAML/JSON object with events or actions).`);
|
|
12330
12421
|
process.exit(1);
|
|
12331
12422
|
}
|
|
12332
12423
|
const rawActions = recording.actions;
|
|
@@ -12360,24 +12451,28 @@ async function handleConvert(args, _mode) {
|
|
|
12360
12451
|
fs3.writeFileSync(outputPath, script);
|
|
12361
12452
|
fs3.chmodSync(outputPath, 493);
|
|
12362
12453
|
const eventCount = (recordingTyped.events || []).length;
|
|
12363
|
-
|
|
12364
|
-
|
|
12365
|
-
|
|
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
|
+
`);
|
|
12366
12460
|
}
|
|
12367
12461
|
async function handleExtract(args, _mode) {
|
|
12368
12462
|
const filePath = args[0];
|
|
12369
12463
|
if (!filePath) {
|
|
12370
|
-
|
|
12464
|
+
outputError("Usage: xbrowser extract <recording.yaml>");
|
|
12371
12465
|
process.exit(1);
|
|
12372
12466
|
}
|
|
12373
12467
|
const { extractAndSave, printExtractSummary } = await import("./extract-EUWPRSKH.js");
|
|
12374
12468
|
try {
|
|
12375
12469
|
const { summary, outputPath } = extractAndSave(filePath);
|
|
12376
12470
|
printExtractSummary(summary);
|
|
12377
|
-
|
|
12378
|
-
Saved LLM summary: ${outputPath}
|
|
12471
|
+
process.stdout.write(`
|
|
12472
|
+
Saved LLM summary: ${outputPath}
|
|
12473
|
+
`);
|
|
12379
12474
|
} catch (e) {
|
|
12380
|
-
|
|
12475
|
+
outputError(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
12381
12476
|
process.exit(1);
|
|
12382
12477
|
}
|
|
12383
12478
|
}
|
|
@@ -12385,7 +12480,7 @@ async function handleFilter(args, _mode, options) {
|
|
|
12385
12480
|
const filePath = args[0];
|
|
12386
12481
|
const outputPath = args[1];
|
|
12387
12482
|
if (!filePath || !outputPath) {
|
|
12388
|
-
|
|
12483
|
+
outputError("Usage: xbrowser filter <input.yaml> <output.yaml> [--exclude type1,type2]");
|
|
12389
12484
|
process.exit(1);
|
|
12390
12485
|
}
|
|
12391
12486
|
const { filterRecording, parseExcludeTypes } = await import("./filter-7YOPVPVC.js");
|
|
@@ -12397,10 +12492,12 @@ async function handleFilter(args, _mode, options) {
|
|
|
12397
12492
|
const excludeTypes = parseExcludeTypes(excludeArgs);
|
|
12398
12493
|
try {
|
|
12399
12494
|
const result = filterRecording(filePath, outputPath, excludeTypes);
|
|
12400
|
-
|
|
12401
|
-
|
|
12495
|
+
process.stdout.write(`Filtered ${filePath} -> ${outputPath}
|
|
12496
|
+
`);
|
|
12497
|
+
process.stdout.write(` Original: ${result.originalCount}, After: ${result.filteredCount}, Removed: ${result.removed} (${result.percentage}%)
|
|
12498
|
+
`);
|
|
12402
12499
|
} catch (e) {
|
|
12403
|
-
|
|
12500
|
+
outputError(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
12404
12501
|
process.exit(1);
|
|
12405
12502
|
}
|
|
12406
12503
|
}
|
|
@@ -12429,21 +12526,30 @@ async function handleGeneratePlugin(sessionName, pluginName, outputDir) {
|
|
|
12429
12526
|
if (knowledgeMd) {
|
|
12430
12527
|
writeFileSync12(join15(finalOutputDir, "SITE_KNOWLEDGE.md"), knowledgeMd, "utf-8");
|
|
12431
12528
|
}
|
|
12432
|
-
|
|
12433
|
-
|
|
12434
|
-
|
|
12435
|
-
|
|
12436
|
-
|
|
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
|
+
`);
|
|
12437
12537
|
if (knowledgeMd) {
|
|
12438
|
-
|
|
12538
|
+
process.stdout.write(` Knowledge: ${finalOutputDir}/SITE_KNOWLEDGE.md
|
|
12539
|
+
`);
|
|
12439
12540
|
}
|
|
12440
|
-
|
|
12441
|
-
|
|
12442
|
-
|
|
12443
|
-
|
|
12444
|
-
|
|
12445
|
-
|
|
12446
|
-
|
|
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
|
+
`);
|
|
12447
12553
|
}
|
|
12448
12554
|
function generatePluginCode(pluginName, domain, data, _knowledgeMd) {
|
|
12449
12555
|
const pagePaths = /* @__PURE__ */ new Set();
|
|
@@ -12622,20 +12728,22 @@ async function handleRun(filePath, options) {
|
|
|
12622
12728
|
});
|
|
12623
12729
|
for (const step of chainResult.steps) {
|
|
12624
12730
|
if (step.success) {
|
|
12625
|
-
|
|
12731
|
+
process.stdout.write(`[OK] ${step.raw}
|
|
12732
|
+
`);
|
|
12626
12733
|
if (step.data && typeof step.data === "object") {
|
|
12627
12734
|
const d = step.data;
|
|
12628
12735
|
for (const [k, v] of Object.entries(d)) {
|
|
12629
12736
|
if (k !== "ok")
|
|
12630
|
-
|
|
12737
|
+
process.stdout.write(` ${k}: ${typeof v === "string" ? v : JSON.stringify(v)}
|
|
12738
|
+
`);
|
|
12631
12739
|
}
|
|
12632
12740
|
}
|
|
12633
12741
|
} else {
|
|
12634
|
-
|
|
12742
|
+
outputError(`[FAIL] ${step.raw}: ${step.message}`);
|
|
12635
12743
|
}
|
|
12636
12744
|
}
|
|
12637
12745
|
if (chainResult.stoppedReason) {
|
|
12638
|
-
|
|
12746
|
+
outputError(`Stopped: ${chainResult.stoppedReason}`);
|
|
12639
12747
|
}
|
|
12640
12748
|
if (!chainResult.success) process.exit(1);
|
|
12641
12749
|
}
|
|
@@ -12678,26 +12786,29 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
12678
12786
|
if (mode === "json") {
|
|
12679
12787
|
outputResult(result, mode);
|
|
12680
12788
|
} else {
|
|
12681
|
-
|
|
12789
|
+
process.stdout.write(`
|
|
12682
12790
|
Network captures (session: ${netSession})`);
|
|
12683
|
-
|
|
12791
|
+
process.stdout.write(` Total: ${result.total}, Showing: ${result.captures.length}
|
|
12684
12792
|
`);
|
|
12685
12793
|
for (const c of result.captures) {
|
|
12686
12794
|
const statusColor = c.status < 300 ? "\x1B[32m" : c.status < 400 ? "\x1B[33m" : "\x1B[31m";
|
|
12687
12795
|
const reset = "\x1B[0m";
|
|
12688
|
-
|
|
12796
|
+
process.stdout.write(` #${c.id} ${c.method.padEnd(6)} ${statusColor}${c.status}${reset} ${c.resourceType.padEnd(10)} ${c.path}
|
|
12797
|
+
`);
|
|
12689
12798
|
if (c.size > 0) {
|
|
12690
12799
|
const sizeStr = c.size > 1024 ? `${(c.size / 1024).toFixed(1)}KB` : `${c.size}B`;
|
|
12691
|
-
|
|
12800
|
+
process.stdout.write(` ${c.contentType.split(";")[0]} ${sizeStr}
|
|
12801
|
+
`);
|
|
12692
12802
|
}
|
|
12693
12803
|
}
|
|
12694
|
-
|
|
12804
|
+
process.stdout.write("\n");
|
|
12695
12805
|
}
|
|
12696
12806
|
break;
|
|
12697
12807
|
}
|
|
12698
12808
|
case "clear": {
|
|
12699
12809
|
await forwardNetworkClear(netSession);
|
|
12700
|
-
|
|
12810
|
+
process.stdout.write(`Network captures cleared for session: ${netSession}
|
|
12811
|
+
`);
|
|
12701
12812
|
break;
|
|
12702
12813
|
}
|
|
12703
12814
|
case "top": {
|
|
@@ -12707,21 +12818,23 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
12707
12818
|
if (mode === "json") {
|
|
12708
12819
|
outputResult(result, mode);
|
|
12709
12820
|
} else {
|
|
12710
|
-
|
|
12821
|
+
process.stdout.write(`
|
|
12711
12822
|
Top valued requests (session: ${netSession})`);
|
|
12712
|
-
|
|
12823
|
+
process.stdout.write(` Showing: ${result.entries.length}
|
|
12713
12824
|
`);
|
|
12714
12825
|
for (const e of result.entries) {
|
|
12715
12826
|
const scoreColor = e.score >= 50 ? "\x1B[32m" : e.score >= 20 ? "\x1B[33m" : "\x1B[90m";
|
|
12716
12827
|
const reset = "\x1B[0m";
|
|
12717
12828
|
const methodStr = e.method.padEnd(6);
|
|
12718
12829
|
const scoreStr = `${scoreColor}${e.score.toString().padStart(3)}${reset}`;
|
|
12719
|
-
|
|
12830
|
+
process.stdout.write(` ${scoreStr} ${methodStr} ${e.status} ${e.resourceType.padEnd(10)} ${e.path}
|
|
12831
|
+
`);
|
|
12720
12832
|
if (e.scoreBreakdown.content > 0) {
|
|
12721
|
-
|
|
12833
|
+
process.stdout.write(` ${e.contentType.split(";")[0]} ${e.size > 1024 ? (e.size / 1024).toFixed(1) + "KB" : e.size + "B"}
|
|
12834
|
+
`);
|
|
12722
12835
|
}
|
|
12723
12836
|
}
|
|
12724
|
-
|
|
12837
|
+
process.stdout.write("\n");
|
|
12725
12838
|
}
|
|
12726
12839
|
break;
|
|
12727
12840
|
}
|
|
@@ -12730,16 +12843,17 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
12730
12843
|
if (mode === "json") {
|
|
12731
12844
|
outputResult(logResult, mode);
|
|
12732
12845
|
} else {
|
|
12733
|
-
|
|
12846
|
+
process.stdout.write(`
|
|
12734
12847
|
Command log (session: ${netSession})`);
|
|
12735
|
-
|
|
12848
|
+
process.stdout.write(` Total: ${logResult.commands.length}
|
|
12736
12849
|
`);
|
|
12737
12850
|
for (const cmd of logResult.commands) {
|
|
12738
12851
|
const ts = new Date(cmd.timestamp).toISOString().substring(11, 19);
|
|
12739
12852
|
const paramsStr = Object.entries(cmd.params).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
12740
|
-
|
|
12853
|
+
process.stdout.write(` #${cmd.id} [${ts}] ${cmd.command} ${paramsStr}
|
|
12854
|
+
`);
|
|
12741
12855
|
}
|
|
12742
|
-
|
|
12856
|
+
process.stdout.write("\n");
|
|
12743
12857
|
}
|
|
12744
12858
|
break;
|
|
12745
12859
|
}
|
|
@@ -12755,28 +12869,31 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
12755
12869
|
outputResult(aroundResult, mode);
|
|
12756
12870
|
} else {
|
|
12757
12871
|
if (!aroundResult) {
|
|
12758
|
-
|
|
12872
|
+
process.stdout.write(" No command found with that ID\n");
|
|
12759
12873
|
break;
|
|
12760
12874
|
}
|
|
12761
12875
|
const cmd = aroundResult.command;
|
|
12762
12876
|
const ts = new Date(cmd.timestamp).toISOString().substring(11, 19);
|
|
12763
|
-
|
|
12877
|
+
process.stdout.write(`
|
|
12764
12878
|
Command: #${cmd.id} [${ts}] ${cmd.command}`);
|
|
12765
|
-
|
|
12879
|
+
process.stdout.write(` Window: \xB1${windowMs}ms
|
|
12766
12880
|
`);
|
|
12767
12881
|
const before = aroundResult.before;
|
|
12768
12882
|
const after = aroundResult.after;
|
|
12769
|
-
|
|
12883
|
+
process.stdout.write(` BEFORE (${before.length} requests):
|
|
12884
|
+
`);
|
|
12770
12885
|
for (const r of before.slice(0, 5)) {
|
|
12771
|
-
|
|
12886
|
+
process.stdout.write(` ${r.method} ${r.status} ${String(r.resourceType).padEnd(10)} ${r.path}
|
|
12887
|
+
`);
|
|
12772
12888
|
}
|
|
12773
|
-
|
|
12889
|
+
process.stdout.write(`
|
|
12774
12890
|
AFTER (${aroundResult.afterCount} requests):`);
|
|
12775
12891
|
for (const r of after.slice(0, 10)) {
|
|
12776
12892
|
const highlight = r.method !== "GET" ? " \u2190" : "";
|
|
12777
|
-
|
|
12893
|
+
process.stdout.write(` ${String(r.method).padEnd(6)} ${r.status} ${String(r.resourceType).padEnd(10)} ${r.path}${highlight}
|
|
12894
|
+
`);
|
|
12778
12895
|
}
|
|
12779
|
-
|
|
12896
|
+
process.stdout.write("\n");
|
|
12780
12897
|
}
|
|
12781
12898
|
break;
|
|
12782
12899
|
}
|
|
@@ -12785,9 +12902,9 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
12785
12902
|
if (mode === "json") {
|
|
12786
12903
|
outputResult(result, mode);
|
|
12787
12904
|
} else {
|
|
12788
|
-
|
|
12905
|
+
process.stdout.write(`
|
|
12789
12906
|
API Reusability Analysis (session: ${netSession})`);
|
|
12790
|
-
|
|
12907
|
+
process.stdout.write(` Total: ${result.total}, Analyzed: ${result.analyzed.length}
|
|
12791
12908
|
`);
|
|
12792
12909
|
const groups = { high: [], medium: [], low: [], unknown: [] };
|
|
12793
12910
|
for (const e of result.analyzed) {
|
|
@@ -12798,16 +12915,20 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
12798
12915
|
if (!items?.length) continue;
|
|
12799
12916
|
const color = level === "high" ? "\x1B[32m" : level === "medium" ? "\x1B[33m" : level === "low" ? "\x1B[31m" : "\x1B[90m";
|
|
12800
12917
|
const reset = "\x1B[0m";
|
|
12801
|
-
|
|
12918
|
+
process.stdout.write(` ${color}${level.toUpperCase()}${reset} (${items.length})
|
|
12919
|
+
`);
|
|
12802
12920
|
for (const e of items.slice(0, 5)) {
|
|
12803
12921
|
const scoreStr = `[${e.reusability.score.toString().padStart(3)}]`;
|
|
12804
|
-
|
|
12922
|
+
process.stdout.write(` ${e.method.padEnd(6)} ${e.status} ${scoreStr} ${e.path}
|
|
12923
|
+
`);
|
|
12805
12924
|
if (e.reusability.reasons.length > 0) {
|
|
12806
|
-
|
|
12925
|
+
process.stdout.write(` ${e.reusability.reasons.join(", ")}
|
|
12926
|
+
`);
|
|
12807
12927
|
}
|
|
12808
12928
|
}
|
|
12809
|
-
if (items.length > 5)
|
|
12810
|
-
|
|
12929
|
+
if (items.length > 5) process.stdout.write(` ... and ${items.length - 5} more
|
|
12930
|
+
`);
|
|
12931
|
+
process.stdout.write("\n");
|
|
12811
12932
|
}
|
|
12812
12933
|
}
|
|
12813
12934
|
break;
|
|
@@ -12826,12 +12947,12 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
12826
12947
|
if (mode === "json") {
|
|
12827
12948
|
outputResult(result, mode);
|
|
12828
12949
|
} else {
|
|
12829
|
-
|
|
12950
|
+
process.stdout.write(`
|
|
12830
12951
|
${result.method} ${result.url}`);
|
|
12831
|
-
|
|
12952
|
+
process.stdout.write(` Headers: ${result.headerCount}, Body: ${result.hasBody}
|
|
12832
12953
|
`);
|
|
12833
|
-
|
|
12834
|
-
|
|
12954
|
+
process.stdout.write(result.command + "\n");
|
|
12955
|
+
process.stdout.write("\n");
|
|
12835
12956
|
}
|
|
12836
12957
|
break;
|
|
12837
12958
|
}
|
|
@@ -12849,27 +12970,33 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
12849
12970
|
if (mode === "json") {
|
|
12850
12971
|
outputResult(result, mode);
|
|
12851
12972
|
} else {
|
|
12852
|
-
|
|
12973
|
+
process.stdout.write(`
|
|
12853
12974
|
Replay Result`);
|
|
12854
|
-
|
|
12975
|
+
process.stdout.write(` ${result.curlCommand?.split("\n")[0]?.trim()}
|
|
12855
12976
|
`);
|
|
12856
12977
|
const replay = result.replay;
|
|
12857
12978
|
if (replay?.error) {
|
|
12858
|
-
|
|
12979
|
+
process.stdout.write(` \x1B[31mFAILED\x1B[0m: ${replay.error}
|
|
12980
|
+
`);
|
|
12859
12981
|
} else if (replay) {
|
|
12860
12982
|
const statusColor = replay.status && replay.status < 300 ? "\x1B[32m" : "\x1B[31m";
|
|
12861
12983
|
const status = replay.status;
|
|
12862
12984
|
const size = replay.size;
|
|
12863
12985
|
const duration = replay.duration;
|
|
12864
|
-
|
|
12865
|
-
|
|
12866
|
-
|
|
12867
|
-
|
|
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
|
+
`);
|
|
12868
12994
|
if (status && status >= 400) {
|
|
12869
|
-
|
|
12995
|
+
process.stdout.write(` \x1B[33m\u26A0 API may require fresh signature/token\x1B[0m
|
|
12996
|
+
`);
|
|
12870
12997
|
}
|
|
12871
12998
|
}
|
|
12872
|
-
|
|
12999
|
+
process.stdout.write("\n");
|
|
12873
13000
|
}
|
|
12874
13001
|
break;
|
|
12875
13002
|
}
|
|
@@ -12888,40 +13015,47 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
12888
13015
|
outputResult(result, mode);
|
|
12889
13016
|
} else {
|
|
12890
13017
|
const c = result.capture;
|
|
12891
|
-
|
|
13018
|
+
process.stdout.write(`
|
|
12892
13019
|
Request #${c.id}`);
|
|
12893
|
-
|
|
12894
|
-
|
|
12895
|
-
|
|
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
|
+
`);
|
|
12896
13026
|
if (c.requestHeaders) {
|
|
12897
|
-
|
|
13027
|
+
process.stdout.write(`
|
|
12898
13028
|
Request Headers:`);
|
|
12899
13029
|
for (const [k, v] of Object.entries(c.requestHeaders)) {
|
|
12900
|
-
|
|
13030
|
+
process.stdout.write(` ${k}: ${String(v).substring(0, 100)}
|
|
13031
|
+
`);
|
|
12901
13032
|
}
|
|
12902
13033
|
}
|
|
12903
13034
|
if (c.requestBody !== void 0) {
|
|
12904
|
-
|
|
13035
|
+
process.stdout.write(`
|
|
12905
13036
|
Request Body:`);
|
|
12906
13037
|
const bodyStr = typeof c.requestBody === "string" ? c.requestBody : JSON.stringify(c.requestBody, null, 2);
|
|
12907
13038
|
const lines = bodyStr.split("\n").slice(0, 20);
|
|
12908
|
-
for (const line of lines)
|
|
12909
|
-
|
|
13039
|
+
for (const line of lines) process.stdout.write(` ${line}
|
|
13040
|
+
`);
|
|
13041
|
+
if (bodyStr.split("\n").length > 20) process.stdout.write(" ...");
|
|
12910
13042
|
}
|
|
12911
|
-
|
|
13043
|
+
process.stdout.write(`
|
|
12912
13044
|
Response Headers:`);
|
|
12913
13045
|
for (const [k, v] of Object.entries(c.headers)) {
|
|
12914
|
-
|
|
13046
|
+
process.stdout.write(` ${k}: ${String(v).substring(0, 100)}
|
|
13047
|
+
`);
|
|
12915
13048
|
}
|
|
12916
13049
|
if (c.body !== void 0) {
|
|
12917
|
-
|
|
13050
|
+
process.stdout.write(`
|
|
12918
13051
|
Response Body:`);
|
|
12919
13052
|
const bodyStr = typeof c.body === "string" ? c.body : JSON.stringify(c.body, null, 2);
|
|
12920
13053
|
const lines = bodyStr.split("\n").slice(0, 20);
|
|
12921
|
-
for (const line of lines)
|
|
12922
|
-
|
|
13054
|
+
for (const line of lines) process.stdout.write(` ${line}
|
|
13055
|
+
`);
|
|
13056
|
+
if (bodyStr.split("\n").length > 20) process.stdout.write(" ...");
|
|
12923
13057
|
}
|
|
12924
|
-
|
|
13058
|
+
process.stdout.write("\n");
|
|
12925
13059
|
}
|
|
12926
13060
|
break;
|
|
12927
13061
|
}
|
|
@@ -12932,7 +13066,8 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
12932
13066
|
break;
|
|
12933
13067
|
}
|
|
12934
13068
|
await forwardNetworkLike(netSession, id);
|
|
12935
|
-
|
|
13069
|
+
process.stdout.write(`Marked #${id} as useful
|
|
13070
|
+
`);
|
|
12936
13071
|
break;
|
|
12937
13072
|
}
|
|
12938
13073
|
case "dislike": {
|
|
@@ -12942,7 +13077,8 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
12942
13077
|
break;
|
|
12943
13078
|
}
|
|
12944
13079
|
await forwardNetworkDislike(netSession, id);
|
|
12945
|
-
|
|
13080
|
+
process.stdout.write(`Marked #${id} as not useful
|
|
13081
|
+
`);
|
|
12946
13082
|
break;
|
|
12947
13083
|
}
|
|
12948
13084
|
case "export": {
|
|
@@ -12957,7 +13093,7 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
12957
13093
|
outputError(result.error);
|
|
12958
13094
|
break;
|
|
12959
13095
|
}
|
|
12960
|
-
|
|
13096
|
+
process.stdout.write(result.code + "\n");
|
|
12961
13097
|
break;
|
|
12962
13098
|
}
|
|
12963
13099
|
default:
|
|
@@ -12969,9 +13105,25 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
12969
13105
|
}
|
|
12970
13106
|
|
|
12971
13107
|
// src/cli/test-routes.ts
|
|
12972
|
-
import {
|
|
13108
|
+
import { execFile as execFile3 } from "child_process";
|
|
12973
13109
|
import { readFileSync as readFileSync11 } from "fs";
|
|
12974
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
|
+
}
|
|
12975
13127
|
function findPluginPath(plugin) {
|
|
12976
13128
|
const candidates = [
|
|
12977
13129
|
resolve9(process.cwd(), ".xcli/plugins", plugin, "index.ts"),
|
|
@@ -13036,20 +13188,21 @@ function extractSchema(plugin, command) {
|
|
|
13036
13188
|
}
|
|
13037
13189
|
async function runTest(plugin, command, cmdArgs, options) {
|
|
13038
13190
|
const cdp = options.cdp || options.cdpEndpoint || "http://localhost:9221";
|
|
13039
|
-
const
|
|
13191
|
+
const passthroughArgs = cmdArgs.filter((a) => !a.startsWith("--cdp"));
|
|
13040
13192
|
const schema = extractSchema(plugin, command);
|
|
13041
|
-
const
|
|
13042
|
-
|
|
13043
|
-
|
|
13044
|
-
|
|
13045
|
-
|
|
13046
|
-
|
|
13047
|
-
|
|
13048
|
-
|
|
13049
|
-
|
|
13050
|
-
|
|
13051
|
-
|
|
13052
|
-
|
|
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) {
|
|
13053
13206
|
const jsonLine = stdout.split("\n").find((l) => {
|
|
13054
13207
|
try {
|
|
13055
13208
|
JSON.parse(l);
|
|
@@ -13068,11 +13221,10 @@ async function runTest(plugin, command, cmdArgs, options) {
|
|
|
13068
13221
|
} catch {
|
|
13069
13222
|
}
|
|
13070
13223
|
}
|
|
13071
|
-
const stderr = err.stderr?.toString() || "";
|
|
13072
13224
|
if (stdout.includes("captcha") || stderr.includes("captcha") || stdout.includes("CAPTCHA")) {
|
|
13073
13225
|
return { status: "CAPTCHA", message: "\u68C0\u6D4B\u5230\u9A8C\u8BC1\u7801", viewerUrl: "http://localhost:9224/preview/default" };
|
|
13074
13226
|
}
|
|
13075
|
-
return { status: "EXEC_ERROR", message: (
|
|
13227
|
+
return { status: "EXEC_ERROR", message: (error.message || "").slice(0, 200) || "\u6267\u884C\u5931\u8D25" };
|
|
13076
13228
|
}
|
|
13077
13229
|
const allLines = stdout.split("\n");
|
|
13078
13230
|
const jsonStart = allLines.findIndex((l) => l.trim().startsWith("{"));
|
|
@@ -13142,27 +13294,27 @@ async function handleTest(cmdArgs, options, mode, cdpEndpoint) {
|
|
|
13142
13294
|
const plugin = cmdArgs[0];
|
|
13143
13295
|
const command = cmdArgs[1];
|
|
13144
13296
|
if (!plugin || !command) {
|
|
13145
|
-
|
|
13146
|
-
|
|
13297
|
+
outputError("\u7528\u6CD5: xbrowser test <plugin> <command> [\u53C2\u6570...]");
|
|
13298
|
+
outputError("\u793A\u4F8B: xbrowser test doubao list --cdp 9221");
|
|
13147
13299
|
return;
|
|
13148
13300
|
}
|
|
13149
13301
|
const loader = await getPluginLoader();
|
|
13150
13302
|
const internalLoader = loader.getCore().loader;
|
|
13151
13303
|
const site = internalLoader.getSite(plugin);
|
|
13152
13304
|
if (!site) {
|
|
13153
|
-
|
|
13305
|
+
outputError(`\u63D2\u4EF6 "${plugin}" \u4E0D\u5B58\u5728`);
|
|
13154
13306
|
return;
|
|
13155
13307
|
}
|
|
13156
13308
|
const cmdEntry = site.getCommand(command);
|
|
13157
13309
|
if (!cmdEntry) {
|
|
13158
|
-
|
|
13310
|
+
outputError(`\u6307\u4EE4 "${command}" \u4E0D\u5B58\u5728`);
|
|
13159
13311
|
return;
|
|
13160
13312
|
}
|
|
13161
13313
|
const testArgs = cmdArgs.slice(2);
|
|
13162
13314
|
const mergedOptions = { ...options, cdp: cdpEndpoint || options.cdp };
|
|
13163
13315
|
const result = await runTest(plugin, command, testArgs, mergedOptions);
|
|
13164
13316
|
if (mode === "json") {
|
|
13165
|
-
|
|
13317
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
13166
13318
|
return;
|
|
13167
13319
|
}
|
|
13168
13320
|
const r = result;
|
|
@@ -13177,23 +13329,32 @@ async function handleTest(cmdArgs, options, mode, cdpEndpoint) {
|
|
|
13177
13329
|
};
|
|
13178
13330
|
const status = String(r.status);
|
|
13179
13331
|
const icon = icons[status] || "\u2753";
|
|
13180
|
-
|
|
13181
|
-
${icon} ${plugin}.${command}
|
|
13182
|
-
|
|
13332
|
+
process.stdout.write(`
|
|
13333
|
+
${icon} ${plugin}.${command}
|
|
13334
|
+
`);
|
|
13335
|
+
process.stdout.write(` \u72B6\u6001: ${status}
|
|
13336
|
+
`);
|
|
13183
13337
|
if (status === "OK") {
|
|
13184
|
-
if (r.count)
|
|
13185
|
-
|
|
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
|
+
`);
|
|
13186
13342
|
} else if (status === "LOGIN_REQUIRED" || status === "CAPTCHA") {
|
|
13187
|
-
|
|
13188
|
-
|
|
13343
|
+
process.stdout.write(` \u4FE1\u606F: ${String(r.message)}
|
|
13344
|
+
`);
|
|
13345
|
+
process.stdout.write(` Viewer: ${String(r.viewerUrl)}`);
|
|
13189
13346
|
} else if (status === "SCHEMA_ERROR") {
|
|
13190
13347
|
const errs = r.errors;
|
|
13191
|
-
if (errs)
|
|
13348
|
+
if (errs) process.stdout.write(` \u9519\u8BEF: ${errs.join("; ")}
|
|
13349
|
+
`);
|
|
13192
13350
|
} else if (["NO_DATA", "BLOCKED"].includes(status)) {
|
|
13193
|
-
|
|
13194
|
-
|
|
13351
|
+
process.stdout.write(` \u4FE1\u606F: ${String(r.message)}
|
|
13352
|
+
`);
|
|
13353
|
+
if (r.viewerUrl) process.stdout.write(` Viewer: ${String(r.viewerUrl)}
|
|
13354
|
+
`);
|
|
13195
13355
|
} else {
|
|
13196
|
-
|
|
13356
|
+
process.stdout.write(` \u4FE1\u606F: ${String(r.message)}
|
|
13357
|
+
`);
|
|
13197
13358
|
}
|
|
13198
13359
|
}
|
|
13199
13360
|
|
|
@@ -13291,7 +13452,7 @@ Commands:
|
|
|
13291
13452
|
plugin list List plugins
|
|
13292
13453
|
plugin reload <name> Reload plugin
|
|
13293
13454
|
create <name> --template <type> Create plugin
|
|
13294
|
-
serve [--port <
|
|
13455
|
+
serve [--port <p>] [--host <h>] [--token <t>] [--cors-origins <csv>] Start HTTP server (loopback by default)
|
|
13295
13456
|
remote <url> [command] [--token <t>] Execute on remote server
|
|
13296
13457
|
run <file> Execute commands from file
|
|
13297
13458
|
viewer [--name <n>] Generate viewer URL
|
|
@@ -13412,10 +13573,26 @@ function validateAuth(authHeader, validTokens) {
|
|
|
13412
13573
|
function isAuthRequired(validTokens) {
|
|
13413
13574
|
return validTokens.length > 0;
|
|
13414
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
|
+
}
|
|
13415
13593
|
|
|
13416
13594
|
// src/server/router.ts
|
|
13417
|
-
var
|
|
13418
|
-
"Access-Control-Allow-Origin": "*",
|
|
13595
|
+
var CORS_BASE_HEADERS = {
|
|
13419
13596
|
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
13420
13597
|
"Access-Control-Allow-Headers": "Content-Type, Authorization"
|
|
13421
13598
|
};
|
|
@@ -13468,9 +13645,31 @@ function jsonResponse(statusCode, body, extraHeaders) {
|
|
|
13468
13645
|
return {
|
|
13469
13646
|
statusCode,
|
|
13470
13647
|
body,
|
|
13471
|
-
headers: {
|
|
13648
|
+
headers: { "Content-Type": "application/json", ...extraHeaders }
|
|
13472
13649
|
};
|
|
13473
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
|
|
13657
|
+
};
|
|
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
|
+
}
|
|
13474
13673
|
function errorResponse(statusCode, error, message) {
|
|
13475
13674
|
return jsonResponse(statusCode, { error, message, statusCode });
|
|
13476
13675
|
}
|
|
@@ -13588,13 +13787,30 @@ async function ensureSession(sessionName, url, cdpEndpoint) {
|
|
|
13588
13787
|
function isHealthCheckPath(pathname) {
|
|
13589
13788
|
return pathname === "/api/v1/health";
|
|
13590
13789
|
}
|
|
13591
|
-
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) {
|
|
13592
13795
|
const parsedUrl = new URL(url, "http://localhost");
|
|
13593
13796
|
const pathname = parsedUrl.pathname;
|
|
13594
13797
|
const query = parseQueryString(parsedUrl.search);
|
|
13595
13798
|
if (method === "OPTIONS") {
|
|
13596
13799
|
return jsonResponse(204, null);
|
|
13597
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
|
+
}
|
|
13598
13814
|
const match = matchRoute(method, pathname);
|
|
13599
13815
|
if (!match) {
|
|
13600
13816
|
const pathMatch = matchRoute("GET", pathname) || matchRoute("POST", pathname) || matchRoute("DELETE", pathname);
|
|
@@ -13617,19 +13833,24 @@ async function route(method, url, headers, body) {
|
|
|
13617
13833
|
return errorResponse(500, "INTERNAL_ERROR", errMsg(err));
|
|
13618
13834
|
}
|
|
13619
13835
|
}
|
|
13620
|
-
async function handleRequest(req, res, validateAuthFn) {
|
|
13836
|
+
async function handleRequest(req, res, validateAuthFn, corsOrigins) {
|
|
13621
13837
|
const url = req.url || "/";
|
|
13622
13838
|
const method = (req.method || "GET").toUpperCase();
|
|
13623
13839
|
const pathname = new URL(url, "http://localhost").pathname;
|
|
13624
13840
|
if (method === "OPTIONS") {
|
|
13625
|
-
const response2 = await route(method, url, headersToObject(req.headers), null);
|
|
13841
|
+
const response2 = await route(method, url, headersToObject(req.headers), null, corsOrigins);
|
|
13626
13842
|
writeResponse(res, response2);
|
|
13627
13843
|
return;
|
|
13628
13844
|
}
|
|
13629
13845
|
if (validateAuthFn && !isHealthCheckPath(pathname)) {
|
|
13630
13846
|
const authHeader = req.headers["authorization"];
|
|
13631
13847
|
if (!validateAuthFn(authHeader)) {
|
|
13632
|
-
|
|
13848
|
+
const denied = applyCorsHeaders(
|
|
13849
|
+
errorResponse(401, "UNAUTHORIZED", "Invalid or missing authentication token"),
|
|
13850
|
+
req.headers.origin,
|
|
13851
|
+
corsOrigins
|
|
13852
|
+
);
|
|
13853
|
+
writeResponse(res, denied);
|
|
13633
13854
|
return;
|
|
13634
13855
|
}
|
|
13635
13856
|
}
|
|
@@ -13637,7 +13858,7 @@ async function handleRequest(req, res, validateAuthFn) {
|
|
|
13637
13858
|
if (method === "POST" || method === "PUT") {
|
|
13638
13859
|
body = await readBody(req);
|
|
13639
13860
|
}
|
|
13640
|
-
const response = await route(method, url, headersToObject(req.headers), body);
|
|
13861
|
+
const response = await route(method, url, headersToObject(req.headers), body, corsOrigins);
|
|
13641
13862
|
writeResponse(res, response);
|
|
13642
13863
|
}
|
|
13643
13864
|
function headersToObject(headers) {
|
|
@@ -13678,26 +13899,35 @@ var HTTPServer = class {
|
|
|
13678
13899
|
host;
|
|
13679
13900
|
server = null;
|
|
13680
13901
|
validTokens;
|
|
13902
|
+
corsOrigins;
|
|
13681
13903
|
constructor(config) {
|
|
13682
13904
|
this.port = config?.port ?? 9224;
|
|
13683
|
-
this.host = config?.host ?? "
|
|
13905
|
+
this.host = config?.host ?? "127.0.0.1";
|
|
13684
13906
|
this.validTokens = resolveTokens(config?.tokens);
|
|
13907
|
+
this.corsOrigins = config?.corsOrigins;
|
|
13685
13908
|
}
|
|
13686
13909
|
/**
|
|
13687
13910
|
* Start the HTTP server and begin listening for requests.
|
|
13688
13911
|
*
|
|
13689
13912
|
* @returns The actual port and host the server bound to.
|
|
13690
|
-
* @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.
|
|
13691
13915
|
*/
|
|
13692
13916
|
async start() {
|
|
13693
13917
|
if (this.server) {
|
|
13694
13918
|
throw new Error("HTTP server is already running");
|
|
13695
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
|
+
}
|
|
13696
13925
|
const authRequired = isAuthRequired(this.validTokens);
|
|
13697
13926
|
const tokens = this.validTokens;
|
|
13927
|
+
const corsOrigins = this.corsOrigins;
|
|
13698
13928
|
this.server = createServer((req, res) => {
|
|
13699
13929
|
const authFn = authRequired ? (authHeader) => validateAuth(authHeader, tokens) : void 0;
|
|
13700
|
-
handleRequest(req, res, authFn).catch((err) => {
|
|
13930
|
+
handleRequest(req, res, authFn, corsOrigins).catch((err) => {
|
|
13701
13931
|
const message = err instanceof Error ? err.message : String(err);
|
|
13702
13932
|
if (!res.headersSent) {
|
|
13703
13933
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
@@ -14168,7 +14398,7 @@ async function routeCommand(argvIn, stdinCommands) {
|
|
|
14168
14398
|
replay: "replay <file> [--slow-mo <ms>] [--stop-on-error]",
|
|
14169
14399
|
create: "create <name> [--template static|dynamic|login|api]",
|
|
14170
14400
|
run: "run <file>",
|
|
14171
|
-
serve: "serve [--port <port>] [--token <token>]",
|
|
14401
|
+
serve: "serve [--port <port>] [--host <host>] [--token <token>] [--cors-origins <csv>]",
|
|
14172
14402
|
remote: "remote <url> [command] [--token <token>]",
|
|
14173
14403
|
convert: "convert <file> [--to js|py|sh]",
|
|
14174
14404
|
extract: "extract <file> [--format json|yaml]",
|
|
@@ -14567,7 +14797,10 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
14567
14797
|
async function handleServe(_args, options, mode) {
|
|
14568
14798
|
const port = options.port ? Number(options.port) : void 0;
|
|
14569
14799
|
const token = options.token;
|
|
14570
|
-
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 });
|
|
14571
14804
|
process.on("SIGINT", async () => {
|
|
14572
14805
|
await httpServer.stop();
|
|
14573
14806
|
return;
|