@xbrowser/cli 1.23.1 → 1.24.1
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/dist/{chunk-LFX24QJZ.js → chunk-5PZXCIDR.js} +81 -8
- package/dist/{chunk-TBSS7ES4.js → chunk-AYNFXG7V.js} +80 -7
- package/dist/{chunk-LPW6QS4M.js → chunk-FNXA6FDM.js} +39 -1
- package/dist/{chunk-A3MORKYR.js → chunk-OSZNJT4U.js} +29 -3
- package/dist/{chunk-FBQZ3PVD.js → chunk-PGKV24QN.js} +39 -1
- package/dist/{chunk-W44UXZSC.js → chunk-SOPN4XDK.js} +39 -1
- package/dist/cli.js +40 -23
- package/dist/daemon-main.js +55 -8
- package/dist/{human-interaction-MXST5ZQM.js → human-interaction-CTZ4IWXJ.js} +1 -1
- package/dist/{human-interaction-67K43B4O.js → human-interaction-HJCK45SL.js} +1 -1
- package/dist/{human-interaction-DK6QGYNC.js → human-interaction-V3VDGHHI.js} +1 -1
- package/dist/index.d.ts +28 -0
- package/dist/index.js +41 -24
- package/dist/{plugin-singleton-GJO7EZXB.js → plugin-singleton-GGSBI3EG.js} +1 -1
- package/dist/{recovery-LGY25LIM.js → recovery-5C4FBA5O.js} +1 -1
- package/dist/{recovery-67DMH2OM.js → recovery-EBKORYEU.js} +3 -3
- package/dist/{recovery-6RI2FE3E.js → recovery-IMZUGWSS.js} +3 -3
- package/dist/{server-HU4F5KN7.js → server-KSHSG4TY.js} +1 -1
- package/dist/{server-DZUIBVVR.js → server-NXXU3UGI.js} +1 -1
- package/package.json +2 -2
|
@@ -7123,7 +7123,7 @@ function attachWaitForHuman(ctx, getOrCreateWSServer) {
|
|
|
7123
7123
|
if (!ctx.page) {
|
|
7124
7124
|
throw new Error("waitForHuman requires an active page");
|
|
7125
7125
|
}
|
|
7126
|
-
const { HumanInteractionManager } = await import("./human-interaction-
|
|
7126
|
+
const { HumanInteractionManager } = await import("./human-interaction-HJCK45SL.js");
|
|
7127
7127
|
const wsServer2 = await getOrCreateWSServer(ctx.browserContext);
|
|
7128
7128
|
const manager = new HumanInteractionManager(wsServer2, ctx.page);
|
|
7129
7129
|
return manager.waitForHuman(options);
|
|
@@ -7146,7 +7146,7 @@ function setWSServerCache(browserContext, server) {
|
|
|
7146
7146
|
import {
|
|
7147
7147
|
Core
|
|
7148
7148
|
} from "@dyyz1993/xcli-core";
|
|
7149
|
-
import { resolve as resolve2 } from "path";
|
|
7149
|
+
import { resolve as resolve2, basename, dirname as dirname3 } from "path";
|
|
7150
7150
|
import { existsSync as existsSync6, readdirSync } from "fs";
|
|
7151
7151
|
import { homedir as homedir4 } from "os";
|
|
7152
7152
|
|
|
@@ -7450,6 +7450,8 @@ var XBrowserPluginLoader = class {
|
|
|
7450
7450
|
core;
|
|
7451
7451
|
loader;
|
|
7452
7452
|
options;
|
|
7453
|
+
/** 目录名 → 该插件注册的 site 名(site.name ≠ 目录名时的命令别名兜底) */
|
|
7454
|
+
dirSiteAliases = /* @__PURE__ */ new Map();
|
|
7453
7455
|
constructor(options) {
|
|
7454
7456
|
patchLoginRequired();
|
|
7455
7457
|
this.options = options ?? {};
|
|
@@ -7487,15 +7489,39 @@ var XBrowserPluginLoader = class {
|
|
|
7487
7489
|
getLoadedPlugins() {
|
|
7488
7490
|
return this.loader.getLoadedPlugins();
|
|
7489
7491
|
}
|
|
7492
|
+
/**
|
|
7493
|
+
* 目录名别名兜底(小白用户实测踩坑):`plugin list` 显示目录名,但命令路由
|
|
7494
|
+
* 按 site.name 匹配——两者不一致时(如目录 alibaba-1688、site 名 1688),
|
|
7495
|
+
* 用户按 list 显示的名字敲命令会 "Unknown command"。映射在 scanAndLoad/
|
|
7496
|
+
* loadPlugin 时通过加载前后 site 快照 diff 记录(instance.siteName 返回的是
|
|
7497
|
+
* 插件 id 而非 site 名,不可用)。
|
|
7498
|
+
*/
|
|
7499
|
+
resolveSiteName(name) {
|
|
7500
|
+
if (this.core.loader.getSite(name)) return name;
|
|
7501
|
+
const aliased = this.dirSiteAliases.get(name);
|
|
7502
|
+
if (aliased && this.core.loader.getSite(aliased)) return aliased;
|
|
7503
|
+
return void 0;
|
|
7504
|
+
}
|
|
7505
|
+
/** 记录一次插件加载引入的 site:加载前后快照 diff(同插件可注册多个 site) */
|
|
7506
|
+
recordSiteAliases(pluginDirName, before) {
|
|
7507
|
+
for (const site of this.core.loader.getSites()) {
|
|
7508
|
+
if (!before.has(site.name)) this.dirSiteAliases.set(pluginDirName, site.name);
|
|
7509
|
+
}
|
|
7510
|
+
}
|
|
7490
7511
|
getPluginContract(siteName, commandName) {
|
|
7491
|
-
const
|
|
7512
|
+
const resolved = this.resolveSiteName(siteName);
|
|
7513
|
+
if (!resolved) return void 0;
|
|
7514
|
+
const site = this.core.loader.getSite(resolved);
|
|
7492
7515
|
if (!site) return void 0;
|
|
7493
7516
|
const contract = buildPluginContract(site);
|
|
7494
7517
|
if (!commandName) return contract;
|
|
7495
7518
|
return contract.commands.find((command) => command.name === commandName);
|
|
7496
7519
|
}
|
|
7497
7520
|
async loadPlugin(pluginPath, id) {
|
|
7498
|
-
|
|
7521
|
+
const before = new Set(this.core.loader.getSites().map((s) => s.name));
|
|
7522
|
+
const instance = await this.loader.loadPlugin(pluginPath, id);
|
|
7523
|
+
this.recordSiteAliases(basename(dirname3(pluginPath)), before);
|
|
7524
|
+
return instance;
|
|
7499
7525
|
}
|
|
7500
7526
|
async unloadPlugin(id) {
|
|
7501
7527
|
return this.loader.unloadPlugin(id);
|
|
@@ -7558,6 +7584,35 @@ var XBrowserPluginLoader = class {
|
|
|
7558
7584
|
}
|
|
7559
7585
|
};
|
|
7560
7586
|
|
|
7587
|
+
// src/fail-codes.ts
|
|
7588
|
+
var FAIL_CODES = {
|
|
7589
|
+
selectorNotFound: "RETRYABLE: selector-not-found",
|
|
7590
|
+
pageNotReady: "RETRYABLE: page-not-ready",
|
|
7591
|
+
elementGone: "RETRYABLE: element-gone",
|
|
7592
|
+
captcha: "HUMAN: captcha",
|
|
7593
|
+
loginWall: "HUMAN: login-wall",
|
|
7594
|
+
permissionDenied: "FATAL: permission-denied",
|
|
7595
|
+
targetGone: "FATAL: target-gone",
|
|
7596
|
+
unknown: "FATAL: unknown"
|
|
7597
|
+
};
|
|
7598
|
+
function classifyFailure(message) {
|
|
7599
|
+
const msg = (message || "").toLowerCase();
|
|
7600
|
+
const pick = (code) => ({
|
|
7601
|
+
code,
|
|
7602
|
+
level: code.split(":")[0],
|
|
7603
|
+
message: message || code
|
|
7604
|
+
});
|
|
7605
|
+
if (/captcha|验证码|人机校验|recaptcha|hcaptcha|turnstile/.test(msg)) return pick(FAIL_CODES.captcha);
|
|
7606
|
+
if (/login|登录|signin|sign in|未登录|login-required|LOGIN_REQUIRED/.test(msg)) return pick(FAIL_CODES.loginWall);
|
|
7607
|
+
if (/permission|权限|not allowed|forbidden|EACCES/.test(msg)) return pick(FAIL_CODES.permissionDenied);
|
|
7608
|
+
if (/selector|选择器|element not found|未找到元素|no such element|queryselector returned null/.test(msg))
|
|
7609
|
+
return pick(FAIL_CODES.selectorNotFound);
|
|
7610
|
+
if (/timeout|超时|timed out|waiting for/.test(msg)) return pick(FAIL_CODES.pageNotReady);
|
|
7611
|
+
if (/net::err|404|navigation failed|页面不存在|target closed|element-gone/.test(msg))
|
|
7612
|
+
return pick(FAIL_CODES.targetGone);
|
|
7613
|
+
return pick(FAIL_CODES.unknown);
|
|
7614
|
+
}
|
|
7615
|
+
|
|
7561
7616
|
// src/executor.ts
|
|
7562
7617
|
import {
|
|
7563
7618
|
ok as ok30,
|
|
@@ -8705,7 +8760,16 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
8705
8760
|
if (isSuccess) {
|
|
8706
8761
|
return { ...ok30(raw.data, merged.length > 0 ? merged : raw.tips), duration, ...hookOutputs ? { hookOutputs } : {} };
|
|
8707
8762
|
}
|
|
8708
|
-
|
|
8763
|
+
const fc = classifyFailure(raw.message);
|
|
8764
|
+
return {
|
|
8765
|
+
success: false,
|
|
8766
|
+
data: raw.data,
|
|
8767
|
+
message: raw.message,
|
|
8768
|
+
tips: mergedOrRaw,
|
|
8769
|
+
duration,
|
|
8770
|
+
error: { code: fc.code, level: fc.level },
|
|
8771
|
+
...hookOutputs ? { hookOutputs } : {}
|
|
8772
|
+
};
|
|
8709
8773
|
}
|
|
8710
8774
|
const smartTipNormalized = normalizeTips6(smartTips);
|
|
8711
8775
|
recordArchive(session?.id, sessionName, {
|
|
@@ -8724,7 +8788,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
8724
8788
|
const errorMessage = errMsg(err);
|
|
8725
8789
|
if (session?.page && process.env.XBROWSER_RECOVERY && !extraOpts?._recoveryAttempted) {
|
|
8726
8790
|
try {
|
|
8727
|
-
const { attemptRecovery } = await import("./recovery-
|
|
8791
|
+
const { attemptRecovery } = await import("./recovery-IMZUGWSS.js");
|
|
8728
8792
|
const recovery = await attemptRecovery(
|
|
8729
8793
|
session.page,
|
|
8730
8794
|
sessionName,
|
|
@@ -8799,7 +8863,8 @@ async function executeChain(input, options) {
|
|
|
8799
8863
|
const cmdArgs = parts.slice(1);
|
|
8800
8864
|
const loader = await getPluginLoader();
|
|
8801
8865
|
const internalLoader = loader.getCore().loader;
|
|
8802
|
-
const
|
|
8866
|
+
const resolvedName = loader.resolveSiteName(cmdName);
|
|
8867
|
+
const site = resolvedName ? internalLoader.getSite(resolvedName) : void 0;
|
|
8803
8868
|
if (site) {
|
|
8804
8869
|
const subCommand = cmdArgs[0];
|
|
8805
8870
|
if (!subCommand) {
|
|
@@ -8840,7 +8905,15 @@ async function executeChain(input, options) {
|
|
|
8840
8905
|
continue;
|
|
8841
8906
|
}
|
|
8842
8907
|
const pluginArgs = cmdArgs.slice(1);
|
|
8843
|
-
|
|
8908
|
+
let pluginParams = parsePluginParams(pluginArgs, cmdEntry.parameters);
|
|
8909
|
+
if (cmdEntry.parameters) {
|
|
8910
|
+
const schemaCheck = cmdEntry.parameters.safeParse(pluginParams);
|
|
8911
|
+
if (schemaCheck.success) {
|
|
8912
|
+
for (const [k, v] of Object.entries(schemaCheck.data)) {
|
|
8913
|
+
if (!(k in pluginParams)) pluginParams[k] = v;
|
|
8914
|
+
}
|
|
8915
|
+
}
|
|
8916
|
+
}
|
|
8844
8917
|
const pluginCtx = {
|
|
8845
8918
|
args: pluginArgs,
|
|
8846
8919
|
options: pluginParams,
|
|
@@ -30,6 +30,35 @@ import {
|
|
|
30
30
|
errMsg
|
|
31
31
|
} from "./chunk-GDKLH7ZY.js";
|
|
32
32
|
|
|
33
|
+
// src/fail-codes.ts
|
|
34
|
+
var FAIL_CODES = {
|
|
35
|
+
selectorNotFound: "RETRYABLE: selector-not-found",
|
|
36
|
+
pageNotReady: "RETRYABLE: page-not-ready",
|
|
37
|
+
elementGone: "RETRYABLE: element-gone",
|
|
38
|
+
captcha: "HUMAN: captcha",
|
|
39
|
+
loginWall: "HUMAN: login-wall",
|
|
40
|
+
permissionDenied: "FATAL: permission-denied",
|
|
41
|
+
targetGone: "FATAL: target-gone",
|
|
42
|
+
unknown: "FATAL: unknown"
|
|
43
|
+
};
|
|
44
|
+
function classifyFailure(message) {
|
|
45
|
+
const msg = (message || "").toLowerCase();
|
|
46
|
+
const pick = (code) => ({
|
|
47
|
+
code,
|
|
48
|
+
level: code.split(":")[0],
|
|
49
|
+
message: message || code
|
|
50
|
+
});
|
|
51
|
+
if (/captcha|验证码|人机校验|recaptcha|hcaptcha|turnstile/.test(msg)) return pick(FAIL_CODES.captcha);
|
|
52
|
+
if (/login|登录|signin|sign in|未登录|login-required|LOGIN_REQUIRED/.test(msg)) return pick(FAIL_CODES.loginWall);
|
|
53
|
+
if (/permission|权限|not allowed|forbidden|EACCES/.test(msg)) return pick(FAIL_CODES.permissionDenied);
|
|
54
|
+
if (/selector|选择器|element not found|未找到元素|no such element|queryselector returned null/.test(msg))
|
|
55
|
+
return pick(FAIL_CODES.selectorNotFound);
|
|
56
|
+
if (/timeout|超时|timed out|waiting for/.test(msg)) return pick(FAIL_CODES.pageNotReady);
|
|
57
|
+
if (/net::err|404|navigation failed|页面不存在|target closed|element-gone/.test(msg))
|
|
58
|
+
return pick(FAIL_CODES.targetGone);
|
|
59
|
+
return pick(FAIL_CODES.unknown);
|
|
60
|
+
}
|
|
61
|
+
|
|
33
62
|
// src/executor.ts
|
|
34
63
|
import {
|
|
35
64
|
ok as ok30,
|
|
@@ -7016,7 +7045,7 @@ function attachDetectAntiBot(ctx) {
|
|
|
7016
7045
|
import {
|
|
7017
7046
|
Core
|
|
7018
7047
|
} from "@dyyz1993/xcli-core";
|
|
7019
|
-
import { resolve as resolve2 } from "path";
|
|
7048
|
+
import { resolve as resolve2, basename, dirname as dirname3 } from "path";
|
|
7020
7049
|
import { existsSync as existsSync6, readdirSync } from "fs";
|
|
7021
7050
|
import { homedir as homedir5 } from "os";
|
|
7022
7051
|
|
|
@@ -7320,6 +7349,8 @@ var XBrowserPluginLoader = class {
|
|
|
7320
7349
|
core;
|
|
7321
7350
|
loader;
|
|
7322
7351
|
options;
|
|
7352
|
+
/** 目录名 → 该插件注册的 site 名(site.name ≠ 目录名时的命令别名兜底) */
|
|
7353
|
+
dirSiteAliases = /* @__PURE__ */ new Map();
|
|
7323
7354
|
constructor(options) {
|
|
7324
7355
|
patchLoginRequired();
|
|
7325
7356
|
this.options = options ?? {};
|
|
@@ -7357,15 +7388,39 @@ var XBrowserPluginLoader = class {
|
|
|
7357
7388
|
getLoadedPlugins() {
|
|
7358
7389
|
return this.loader.getLoadedPlugins();
|
|
7359
7390
|
}
|
|
7391
|
+
/**
|
|
7392
|
+
* 目录名别名兜底(小白用户实测踩坑):`plugin list` 显示目录名,但命令路由
|
|
7393
|
+
* 按 site.name 匹配——两者不一致时(如目录 alibaba-1688、site 名 1688),
|
|
7394
|
+
* 用户按 list 显示的名字敲命令会 "Unknown command"。映射在 scanAndLoad/
|
|
7395
|
+
* loadPlugin 时通过加载前后 site 快照 diff 记录(instance.siteName 返回的是
|
|
7396
|
+
* 插件 id 而非 site 名,不可用)。
|
|
7397
|
+
*/
|
|
7398
|
+
resolveSiteName(name) {
|
|
7399
|
+
if (this.core.loader.getSite(name)) return name;
|
|
7400
|
+
const aliased = this.dirSiteAliases.get(name);
|
|
7401
|
+
if (aliased && this.core.loader.getSite(aliased)) return aliased;
|
|
7402
|
+
return void 0;
|
|
7403
|
+
}
|
|
7404
|
+
/** 记录一次插件加载引入的 site:加载前后快照 diff(同插件可注册多个 site) */
|
|
7405
|
+
recordSiteAliases(pluginDirName, before) {
|
|
7406
|
+
for (const site of this.core.loader.getSites()) {
|
|
7407
|
+
if (!before.has(site.name)) this.dirSiteAliases.set(pluginDirName, site.name);
|
|
7408
|
+
}
|
|
7409
|
+
}
|
|
7360
7410
|
getPluginContract(siteName, commandName) {
|
|
7361
|
-
const
|
|
7411
|
+
const resolved = this.resolveSiteName(siteName);
|
|
7412
|
+
if (!resolved) return void 0;
|
|
7413
|
+
const site = this.core.loader.getSite(resolved);
|
|
7362
7414
|
if (!site) return void 0;
|
|
7363
7415
|
const contract = buildPluginContract(site);
|
|
7364
7416
|
if (!commandName) return contract;
|
|
7365
7417
|
return contract.commands.find((command) => command.name === commandName);
|
|
7366
7418
|
}
|
|
7367
7419
|
async loadPlugin(pluginPath, id) {
|
|
7368
|
-
|
|
7420
|
+
const before = new Set(this.core.loader.getSites().map((s) => s.name));
|
|
7421
|
+
const instance = await this.loader.loadPlugin(pluginPath, id);
|
|
7422
|
+
this.recordSiteAliases(basename(dirname3(pluginPath)), before);
|
|
7423
|
+
return instance;
|
|
7369
7424
|
}
|
|
7370
7425
|
async unloadPlugin(id) {
|
|
7371
7426
|
return this.loader.unloadPlugin(id);
|
|
@@ -8385,7 +8440,16 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
8385
8440
|
if (isSuccess) {
|
|
8386
8441
|
return { ...ok30(raw.data, merged.length > 0 ? merged : raw.tips), duration, ...hookOutputs ? { hookOutputs } : {} };
|
|
8387
8442
|
}
|
|
8388
|
-
|
|
8443
|
+
const fc = classifyFailure(raw.message);
|
|
8444
|
+
return {
|
|
8445
|
+
success: false,
|
|
8446
|
+
data: raw.data,
|
|
8447
|
+
message: raw.message,
|
|
8448
|
+
tips: mergedOrRaw,
|
|
8449
|
+
duration,
|
|
8450
|
+
error: { code: fc.code, level: fc.level },
|
|
8451
|
+
...hookOutputs ? { hookOutputs } : {}
|
|
8452
|
+
};
|
|
8389
8453
|
}
|
|
8390
8454
|
const smartTipNormalized = normalizeTips6(smartTips);
|
|
8391
8455
|
recordArchive(session?.id, sessionName, {
|
|
@@ -8404,7 +8468,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
8404
8468
|
const errorMessage = errMsg(err);
|
|
8405
8469
|
if (session?.page && process.env.XBROWSER_RECOVERY && !extraOpts?._recoveryAttempted) {
|
|
8406
8470
|
try {
|
|
8407
|
-
const { attemptRecovery } = await import("./recovery-
|
|
8471
|
+
const { attemptRecovery } = await import("./recovery-EBKORYEU.js");
|
|
8408
8472
|
const recovery = await attemptRecovery(
|
|
8409
8473
|
session.page,
|
|
8410
8474
|
sessionName,
|
|
@@ -8479,7 +8543,8 @@ async function executeChain(input, options) {
|
|
|
8479
8543
|
const cmdArgs = parts.slice(1);
|
|
8480
8544
|
const loader = await getPluginLoader();
|
|
8481
8545
|
const internalLoader = loader.getCore().loader;
|
|
8482
|
-
const
|
|
8546
|
+
const resolvedName = loader.resolveSiteName(cmdName);
|
|
8547
|
+
const site = resolvedName ? internalLoader.getSite(resolvedName) : void 0;
|
|
8483
8548
|
if (site) {
|
|
8484
8549
|
const subCommand = cmdArgs[0];
|
|
8485
8550
|
if (!subCommand) {
|
|
@@ -8520,7 +8585,15 @@ async function executeChain(input, options) {
|
|
|
8520
8585
|
continue;
|
|
8521
8586
|
}
|
|
8522
8587
|
const pluginArgs = cmdArgs.slice(1);
|
|
8523
|
-
|
|
8588
|
+
let pluginParams = parsePluginParams(pluginArgs, cmdEntry.parameters);
|
|
8589
|
+
if (cmdEntry.parameters) {
|
|
8590
|
+
const schemaCheck = cmdEntry.parameters.safeParse(pluginParams);
|
|
8591
|
+
if (schemaCheck.success) {
|
|
8592
|
+
for (const [k, v] of Object.entries(schemaCheck.data)) {
|
|
8593
|
+
if (!(k in pluginParams)) pluginParams[k] = v;
|
|
8594
|
+
}
|
|
8595
|
+
}
|
|
8596
|
+
}
|
|
8524
8597
|
const pluginCtx = {
|
|
8525
8598
|
args: pluginArgs,
|
|
8526
8599
|
options: pluginParams,
|
|
@@ -251,6 +251,7 @@ var CaptchaDetector = class {
|
|
|
251
251
|
};
|
|
252
252
|
|
|
253
253
|
// src/webhook.ts
|
|
254
|
+
import * as os from "os";
|
|
254
255
|
var WebhookNotifier = class {
|
|
255
256
|
url;
|
|
256
257
|
constructor(url) {
|
|
@@ -264,11 +265,15 @@ var WebhookNotifier = class {
|
|
|
264
265
|
*/
|
|
265
266
|
async notify(payload) {
|
|
266
267
|
if (!this.url) return false;
|
|
268
|
+
if (this.isDrelEndpoint(this.url)) {
|
|
269
|
+
payload = { ...payload, previewUrl: this.lanify(payload.previewUrl) };
|
|
270
|
+
}
|
|
267
271
|
try {
|
|
272
|
+
const body = this.isDrelEndpoint(this.url) ? JSON.stringify(this.toDrelPayload(payload)) : JSON.stringify(payload);
|
|
268
273
|
const response = await fetch(this.url, {
|
|
269
274
|
method: "POST",
|
|
270
275
|
headers: { "Content-Type": "application/json" },
|
|
271
|
-
body
|
|
276
|
+
body,
|
|
272
277
|
signal: AbortSignal.timeout(5e3)
|
|
273
278
|
});
|
|
274
279
|
return response.ok;
|
|
@@ -276,6 +281,39 @@ var WebhookNotifier = class {
|
|
|
276
281
|
return false;
|
|
277
282
|
}
|
|
278
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* previewUrl 里的 localhost/127.0.0.1 替换为本机局域网 IPv4——
|
|
286
|
+
* 推送目标是手机,localhost 在手机上不可达;同一 Wi-Fi 下局域网 IP 可达。
|
|
287
|
+
*/
|
|
288
|
+
lanify(url) {
|
|
289
|
+
if (!url) return url;
|
|
290
|
+
if (!url.includes("localhost") && !url.includes("127.0.0.1")) return url;
|
|
291
|
+
const ips = Object.values(os.networkInterfaces()).flat().filter((i) => !!i && i.family === "IPv4" && !i.internal);
|
|
292
|
+
if (!ips.length) return url;
|
|
293
|
+
return url.replace("localhost", ips[0].address).replace("127.0.0.1", ips[0].address);
|
|
294
|
+
}
|
|
295
|
+
/** Drel 推送端点识别(api.drel.app/<token>) */
|
|
296
|
+
isDrelEndpoint(url) {
|
|
297
|
+
return url.includes("api.drel.app") || url.includes("drel.app");
|
|
298
|
+
}
|
|
299
|
+
toDrelPayload(payload) {
|
|
300
|
+
const eventLabel = {
|
|
301
|
+
"captcha-detected": "\u26A0\uFE0F \u68C0\u6D4B\u5230\u9A8C\u8BC1\u7801",
|
|
302
|
+
"captcha-resolved": "\u2705 \u9A8C\u8BC1\u7801\u5DF2\u89E3\u51B3",
|
|
303
|
+
"session-started": "\u25B6\uFE0F \u4F1A\u8BDD\u5F00\u59CB",
|
|
304
|
+
"session-ended": "\u23F9 \u4F1A\u8BDD\u7ED3\u675F"
|
|
305
|
+
};
|
|
306
|
+
const parts = [];
|
|
307
|
+
if (payload.reason) parts.push(payload.reason);
|
|
308
|
+
if (payload.targetUrl) parts.push(`\u76EE\u6807: ${payload.targetUrl}`);
|
|
309
|
+
if (payload.sessionId) parts.push(`\u4F1A\u8BDD: ${payload.sessionId}`);
|
|
310
|
+
if (payload.timeout) parts.push(`\u8D85\u65F6: ${payload.timeout}s`);
|
|
311
|
+
return {
|
|
312
|
+
title: payload.title || `${eventLabel[payload.event] || payload.event} \u2014 \u9700\u8981\u4F60\u4ECB\u5165`,
|
|
313
|
+
body: payload.body || parts.join("\n") || payload.event,
|
|
314
|
+
...payload.previewUrl ? { url: payload.previewUrl } : {}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
279
317
|
};
|
|
280
318
|
|
|
281
319
|
// src/human-interaction.ts
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
Core
|
|
4
4
|
} from "@dyyz1993/xcli-core";
|
|
5
|
-
import { resolve as resolve2 } from "path";
|
|
5
|
+
import { resolve as resolve2, basename, dirname } from "path";
|
|
6
6
|
import { existsSync as existsSync3, readdirSync } from "fs";
|
|
7
7
|
import { homedir } from "os";
|
|
8
8
|
|
|
@@ -306,6 +306,8 @@ var XBrowserPluginLoader = class {
|
|
|
306
306
|
core;
|
|
307
307
|
loader;
|
|
308
308
|
options;
|
|
309
|
+
/** 目录名 → 该插件注册的 site 名(site.name ≠ 目录名时的命令别名兜底) */
|
|
310
|
+
dirSiteAliases = /* @__PURE__ */ new Map();
|
|
309
311
|
constructor(options) {
|
|
310
312
|
patchLoginRequired();
|
|
311
313
|
this.options = options ?? {};
|
|
@@ -343,15 +345,39 @@ var XBrowserPluginLoader = class {
|
|
|
343
345
|
getLoadedPlugins() {
|
|
344
346
|
return this.loader.getLoadedPlugins();
|
|
345
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* 目录名别名兜底(小白用户实测踩坑):`plugin list` 显示目录名,但命令路由
|
|
350
|
+
* 按 site.name 匹配——两者不一致时(如目录 alibaba-1688、site 名 1688),
|
|
351
|
+
* 用户按 list 显示的名字敲命令会 "Unknown command"。映射在 scanAndLoad/
|
|
352
|
+
* loadPlugin 时通过加载前后 site 快照 diff 记录(instance.siteName 返回的是
|
|
353
|
+
* 插件 id 而非 site 名,不可用)。
|
|
354
|
+
*/
|
|
355
|
+
resolveSiteName(name) {
|
|
356
|
+
if (this.core.loader.getSite(name)) return name;
|
|
357
|
+
const aliased = this.dirSiteAliases.get(name);
|
|
358
|
+
if (aliased && this.core.loader.getSite(aliased)) return aliased;
|
|
359
|
+
return void 0;
|
|
360
|
+
}
|
|
361
|
+
/** 记录一次插件加载引入的 site:加载前后快照 diff(同插件可注册多个 site) */
|
|
362
|
+
recordSiteAliases(pluginDirName, before) {
|
|
363
|
+
for (const site of this.core.loader.getSites()) {
|
|
364
|
+
if (!before.has(site.name)) this.dirSiteAliases.set(pluginDirName, site.name);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
346
367
|
getPluginContract(siteName, commandName) {
|
|
347
|
-
const
|
|
368
|
+
const resolved = this.resolveSiteName(siteName);
|
|
369
|
+
if (!resolved) return void 0;
|
|
370
|
+
const site = this.core.loader.getSite(resolved);
|
|
348
371
|
if (!site) return void 0;
|
|
349
372
|
const contract = buildPluginContract(site);
|
|
350
373
|
if (!commandName) return contract;
|
|
351
374
|
return contract.commands.find((command) => command.name === commandName);
|
|
352
375
|
}
|
|
353
376
|
async loadPlugin(pluginPath, id) {
|
|
354
|
-
|
|
377
|
+
const before = new Set(this.core.loader.getSites().map((s) => s.name));
|
|
378
|
+
const instance = await this.loader.loadPlugin(pluginPath, id);
|
|
379
|
+
this.recordSiteAliases(basename(dirname(pluginPath)), before);
|
|
380
|
+
return instance;
|
|
355
381
|
}
|
|
356
382
|
async unloadPlugin(id) {
|
|
357
383
|
return this.loader.unloadPlugin(id);
|
|
@@ -111,6 +111,7 @@ var CaptchaDetector = class {
|
|
|
111
111
|
};
|
|
112
112
|
|
|
113
113
|
// src/webhook.ts
|
|
114
|
+
import * as os from "os";
|
|
114
115
|
var WebhookNotifier = class {
|
|
115
116
|
url;
|
|
116
117
|
constructor(url) {
|
|
@@ -124,11 +125,15 @@ var WebhookNotifier = class {
|
|
|
124
125
|
*/
|
|
125
126
|
async notify(payload) {
|
|
126
127
|
if (!this.url) return false;
|
|
128
|
+
if (this.isDrelEndpoint(this.url)) {
|
|
129
|
+
payload = { ...payload, previewUrl: this.lanify(payload.previewUrl) };
|
|
130
|
+
}
|
|
127
131
|
try {
|
|
132
|
+
const body = this.isDrelEndpoint(this.url) ? JSON.stringify(this.toDrelPayload(payload)) : JSON.stringify(payload);
|
|
128
133
|
const response = await fetch(this.url, {
|
|
129
134
|
method: "POST",
|
|
130
135
|
headers: { "Content-Type": "application/json" },
|
|
131
|
-
body
|
|
136
|
+
body,
|
|
132
137
|
signal: AbortSignal.timeout(5e3)
|
|
133
138
|
});
|
|
134
139
|
return response.ok;
|
|
@@ -136,6 +141,39 @@ var WebhookNotifier = class {
|
|
|
136
141
|
return false;
|
|
137
142
|
}
|
|
138
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* previewUrl 里的 localhost/127.0.0.1 替换为本机局域网 IPv4——
|
|
146
|
+
* 推送目标是手机,localhost 在手机上不可达;同一 Wi-Fi 下局域网 IP 可达。
|
|
147
|
+
*/
|
|
148
|
+
lanify(url) {
|
|
149
|
+
if (!url) return url;
|
|
150
|
+
if (!url.includes("localhost") && !url.includes("127.0.0.1")) return url;
|
|
151
|
+
const ips = Object.values(os.networkInterfaces()).flat().filter((i) => !!i && i.family === "IPv4" && !i.internal);
|
|
152
|
+
if (!ips.length) return url;
|
|
153
|
+
return url.replace("localhost", ips[0].address).replace("127.0.0.1", ips[0].address);
|
|
154
|
+
}
|
|
155
|
+
/** Drel 推送端点识别(api.drel.app/<token>) */
|
|
156
|
+
isDrelEndpoint(url) {
|
|
157
|
+
return url.includes("api.drel.app") || url.includes("drel.app");
|
|
158
|
+
}
|
|
159
|
+
toDrelPayload(payload) {
|
|
160
|
+
const eventLabel = {
|
|
161
|
+
"captcha-detected": "\u26A0\uFE0F \u68C0\u6D4B\u5230\u9A8C\u8BC1\u7801",
|
|
162
|
+
"captcha-resolved": "\u2705 \u9A8C\u8BC1\u7801\u5DF2\u89E3\u51B3",
|
|
163
|
+
"session-started": "\u25B6\uFE0F \u4F1A\u8BDD\u5F00\u59CB",
|
|
164
|
+
"session-ended": "\u23F9 \u4F1A\u8BDD\u7ED3\u675F"
|
|
165
|
+
};
|
|
166
|
+
const parts = [];
|
|
167
|
+
if (payload.reason) parts.push(payload.reason);
|
|
168
|
+
if (payload.targetUrl) parts.push(`\u76EE\u6807: ${payload.targetUrl}`);
|
|
169
|
+
if (payload.sessionId) parts.push(`\u4F1A\u8BDD: ${payload.sessionId}`);
|
|
170
|
+
if (payload.timeout) parts.push(`\u8D85\u65F6: ${payload.timeout}s`);
|
|
171
|
+
return {
|
|
172
|
+
title: payload.title || `${eventLabel[payload.event] || payload.event} \u2014 \u9700\u8981\u4F60\u4ECB\u5165`,
|
|
173
|
+
body: payload.body || parts.join("\n") || payload.event,
|
|
174
|
+
...payload.previewUrl ? { url: payload.previewUrl } : {}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
139
177
|
};
|
|
140
178
|
|
|
141
179
|
// src/human-interaction.ts
|
|
@@ -251,6 +251,7 @@ var CaptchaDetector = class {
|
|
|
251
251
|
};
|
|
252
252
|
|
|
253
253
|
// src/webhook.ts
|
|
254
|
+
import * as os from "os";
|
|
254
255
|
var WebhookNotifier = class {
|
|
255
256
|
url;
|
|
256
257
|
constructor(url) {
|
|
@@ -264,11 +265,15 @@ var WebhookNotifier = class {
|
|
|
264
265
|
*/
|
|
265
266
|
async notify(payload) {
|
|
266
267
|
if (!this.url) return false;
|
|
268
|
+
if (this.isDrelEndpoint(this.url)) {
|
|
269
|
+
payload = { ...payload, previewUrl: this.lanify(payload.previewUrl) };
|
|
270
|
+
}
|
|
267
271
|
try {
|
|
272
|
+
const body = this.isDrelEndpoint(this.url) ? JSON.stringify(this.toDrelPayload(payload)) : JSON.stringify(payload);
|
|
268
273
|
const response = await fetch(this.url, {
|
|
269
274
|
method: "POST",
|
|
270
275
|
headers: { "Content-Type": "application/json" },
|
|
271
|
-
body
|
|
276
|
+
body,
|
|
272
277
|
signal: AbortSignal.timeout(5e3)
|
|
273
278
|
});
|
|
274
279
|
return response.ok;
|
|
@@ -276,6 +281,39 @@ var WebhookNotifier = class {
|
|
|
276
281
|
return false;
|
|
277
282
|
}
|
|
278
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* previewUrl 里的 localhost/127.0.0.1 替换为本机局域网 IPv4——
|
|
286
|
+
* 推送目标是手机,localhost 在手机上不可达;同一 Wi-Fi 下局域网 IP 可达。
|
|
287
|
+
*/
|
|
288
|
+
lanify(url) {
|
|
289
|
+
if (!url) return url;
|
|
290
|
+
if (!url.includes("localhost") && !url.includes("127.0.0.1")) return url;
|
|
291
|
+
const ips = Object.values(os.networkInterfaces()).flat().filter((i) => !!i && i.family === "IPv4" && !i.internal);
|
|
292
|
+
if (!ips.length) return url;
|
|
293
|
+
return url.replace("localhost", ips[0].address).replace("127.0.0.1", ips[0].address);
|
|
294
|
+
}
|
|
295
|
+
/** Drel 推送端点识别(api.drel.app/<token>) */
|
|
296
|
+
isDrelEndpoint(url) {
|
|
297
|
+
return url.includes("api.drel.app") || url.includes("drel.app");
|
|
298
|
+
}
|
|
299
|
+
toDrelPayload(payload) {
|
|
300
|
+
const eventLabel = {
|
|
301
|
+
"captcha-detected": "\u26A0\uFE0F \u68C0\u6D4B\u5230\u9A8C\u8BC1\u7801",
|
|
302
|
+
"captcha-resolved": "\u2705 \u9A8C\u8BC1\u7801\u5DF2\u89E3\u51B3",
|
|
303
|
+
"session-started": "\u25B6\uFE0F \u4F1A\u8BDD\u5F00\u59CB",
|
|
304
|
+
"session-ended": "\u23F9 \u4F1A\u8BDD\u7ED3\u675F"
|
|
305
|
+
};
|
|
306
|
+
const parts = [];
|
|
307
|
+
if (payload.reason) parts.push(payload.reason);
|
|
308
|
+
if (payload.targetUrl) parts.push(`\u76EE\u6807: ${payload.targetUrl}`);
|
|
309
|
+
if (payload.sessionId) parts.push(`\u4F1A\u8BDD: ${payload.sessionId}`);
|
|
310
|
+
if (payload.timeout) parts.push(`\u8D85\u65F6: ${payload.timeout}s`);
|
|
311
|
+
return {
|
|
312
|
+
title: payload.title || `${eventLabel[payload.event] || payload.event} \u2014 \u9700\u8981\u4F60\u4ECB\u5165`,
|
|
313
|
+
body: payload.body || parts.join("\n") || payload.event,
|
|
314
|
+
...payload.previewUrl ? { url: payload.previewUrl } : {}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
279
317
|
};
|
|
280
318
|
|
|
281
319
|
// src/human-interaction.ts
|
package/dist/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
loadHooks,
|
|
17
17
|
parsePluginParams,
|
|
18
18
|
readJsonFile
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-AYNFXG7V.js";
|
|
20
20
|
import "./chunk-JKVUFP3G.js";
|
|
21
21
|
import {
|
|
22
22
|
forwardCommandLog,
|
|
@@ -629,6 +629,9 @@ async function installFromMarketplace(pluginsDir, slug, options) {
|
|
|
629
629
|
const realSlug = String(plugin.slug || slug);
|
|
630
630
|
try {
|
|
631
631
|
await downloadAndExtractMarketplaceTarball(baseUrl, realSlug, tmpDir, targetDir);
|
|
632
|
+
} catch (err) {
|
|
633
|
+
safeCleanup5(targetDir);
|
|
634
|
+
throw err;
|
|
632
635
|
} finally {
|
|
633
636
|
safeCleanup5(tmpDir);
|
|
634
637
|
}
|
|
@@ -802,6 +805,7 @@ function ensureIndexFile(plugin, name, targetDir) {
|
|
|
802
805
|
}
|
|
803
806
|
|
|
804
807
|
// src/plugin/installer.ts
|
|
808
|
+
import { safeCleanup as safeCleanup6 } from "@dyyz1993/xcli-core";
|
|
805
809
|
var PluginInstaller = class {
|
|
806
810
|
pluginsDir;
|
|
807
811
|
constructor(pluginsDir) {
|
|
@@ -827,31 +831,29 @@ var PluginInstaller = class {
|
|
|
827
831
|
}
|
|
828
832
|
mkdirSync4(targetDir, { recursive: true });
|
|
829
833
|
const resolvedSource = type === "npm" ? await resolveNpmPackageWithFallback(source) : source;
|
|
834
|
+
const withCleanup = async (fn) => {
|
|
835
|
+
try {
|
|
836
|
+
const r = await fn();
|
|
837
|
+
await this.fixSharedDeps(targetDir);
|
|
838
|
+
ensurePluginDependencies(this.pluginsDir);
|
|
839
|
+
if (!existsSync6(resolve6(targetDir, "index.ts")) && !existsSync6(resolve6(targetDir, "index.js"))) {
|
|
840
|
+
throw new Error("installed package has no index.ts/js entry");
|
|
841
|
+
}
|
|
842
|
+
return r;
|
|
843
|
+
} catch (err) {
|
|
844
|
+
safeCleanup6(targetDir);
|
|
845
|
+
throw err;
|
|
846
|
+
}
|
|
847
|
+
};
|
|
830
848
|
switch (type) {
|
|
831
849
|
case "local":
|
|
832
|
-
return await installFromLocal(source, name, targetDir)
|
|
833
|
-
await this.fixSharedDeps(targetDir);
|
|
834
|
-
ensurePluginDependencies(this.pluginsDir);
|
|
835
|
-
return r;
|
|
836
|
-
});
|
|
850
|
+
return await withCleanup(() => installFromLocal(source, name, targetDir));
|
|
837
851
|
case "npm":
|
|
838
|
-
return await installFromNpm(resolvedSource, name, targetDir)
|
|
839
|
-
await this.fixSharedDeps(targetDir);
|
|
840
|
-
ensurePluginDependencies(this.pluginsDir);
|
|
841
|
-
return r;
|
|
842
|
-
});
|
|
852
|
+
return await withCleanup(() => installFromNpm(resolvedSource, name, targetDir));
|
|
843
853
|
case "git":
|
|
844
|
-
return await installFromGit(source, name, targetDir)
|
|
845
|
-
await this.fixSharedDeps(targetDir);
|
|
846
|
-
ensurePluginDependencies(this.pluginsDir);
|
|
847
|
-
return r;
|
|
848
|
-
});
|
|
854
|
+
return await withCleanup(() => installFromGit(source, name, targetDir));
|
|
849
855
|
case "url":
|
|
850
|
-
return await installFromUrl(source, name, targetDir)
|
|
851
|
-
await this.fixSharedDeps(targetDir);
|
|
852
|
-
ensurePluginDependencies(this.pluginsDir);
|
|
853
|
-
return r;
|
|
854
|
-
});
|
|
856
|
+
return await withCleanup(() => installFromUrl(source, name, targetDir));
|
|
855
857
|
}
|
|
856
858
|
}
|
|
857
859
|
/**
|
|
@@ -5816,7 +5818,14 @@ async function routeCommand(argvIn, stdinCommands) {
|
|
|
5816
5818
|
default: {
|
|
5817
5819
|
const loader = await getPluginLoader();
|
|
5818
5820
|
const internalLoader = loader.getCore().loader;
|
|
5819
|
-
const
|
|
5821
|
+
const resolvedSiteName = loader.resolveSiteName(command);
|
|
5822
|
+
const site = resolvedSiteName ? internalLoader.getSite(resolvedSiteName) : void 0;
|
|
5823
|
+
if (site && resolvedSiteName !== command) {
|
|
5824
|
+
if (mode !== "json" && mode !== "yaml") {
|
|
5825
|
+
process.stderr.write(`\u2139\uFE0F "${command}" \u7684\u547D\u4EE4\u540D\u662F "${resolvedSiteName}"\uFF08\u63D2\u4EF6\u76EE\u5F55\u540D\u4E0E\u7AD9\u70B9\u540D\u4E0D\u4E00\u81F4\uFF0C\u5DF2\u81EA\u52A8\u6620\u5C04\uFF09
|
|
5826
|
+
`);
|
|
5827
|
+
}
|
|
5828
|
+
}
|
|
5820
5829
|
if (!site) {
|
|
5821
5830
|
const globalFlagSet = /* @__PURE__ */ new Set(["--session", "--cdp", "--json", "--yaml", "--help", "-h", "--version", "--output", "-o"]);
|
|
5822
5831
|
const cleanParts = [];
|
|
@@ -5957,6 +5966,14 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
5957
5966
|
if (options.target && !params._target) {
|
|
5958
5967
|
params._target = options.target;
|
|
5959
5968
|
}
|
|
5969
|
+
if (cmdEntry.parameters) {
|
|
5970
|
+
const schemaFill = cmdEntry.parameters.safeParse(params);
|
|
5971
|
+
if (schemaFill.success) {
|
|
5972
|
+
for (const [k, v] of Object.entries(schemaFill.data)) {
|
|
5973
|
+
if (!(k in params)) params[k] = v;
|
|
5974
|
+
}
|
|
5975
|
+
}
|
|
5976
|
+
}
|
|
5960
5977
|
const needsBrowser = cmdEntry.scope === "page" || cmdEntry.scope === "browser";
|
|
5961
5978
|
if (needsBrowser && !process.env.XBROWSER_DAEMON_WORKER) {
|
|
5962
5979
|
const { forwardPluginExec } = await import("./daemon-client-HEGAXWGK.js");
|
|
@@ -6264,7 +6281,7 @@ async function handleRemote(args, options, mode) {
|
|
|
6264
6281
|
}
|
|
6265
6282
|
}
|
|
6266
6283
|
async function startMcpServer() {
|
|
6267
|
-
const { startMcpStdio } = await import("./server-
|
|
6284
|
+
const { startMcpStdio } = await import("./server-KSHSG4TY.js");
|
|
6268
6285
|
startMcpStdio();
|
|
6269
6286
|
}
|
|
6270
6287
|
|
package/dist/daemon-main.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "./chunk-JKVUFP3G.js";
|
|
5
5
|
import {
|
|
6
6
|
getPluginLoader
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-OSZNJT4U.js";
|
|
8
8
|
import {
|
|
9
9
|
version
|
|
10
10
|
} from "./chunk-43TEMG4U.js";
|
|
@@ -69,6 +69,35 @@ import {
|
|
|
69
69
|
removeSession
|
|
70
70
|
} from "@dyyz1993/xcli-core";
|
|
71
71
|
|
|
72
|
+
// src/fail-codes.ts
|
|
73
|
+
var FAIL_CODES = {
|
|
74
|
+
selectorNotFound: "RETRYABLE: selector-not-found",
|
|
75
|
+
pageNotReady: "RETRYABLE: page-not-ready",
|
|
76
|
+
elementGone: "RETRYABLE: element-gone",
|
|
77
|
+
captcha: "HUMAN: captcha",
|
|
78
|
+
loginWall: "HUMAN: login-wall",
|
|
79
|
+
permissionDenied: "FATAL: permission-denied",
|
|
80
|
+
targetGone: "FATAL: target-gone",
|
|
81
|
+
unknown: "FATAL: unknown"
|
|
82
|
+
};
|
|
83
|
+
function classifyFailure(message) {
|
|
84
|
+
const msg = (message || "").toLowerCase();
|
|
85
|
+
const pick = (code) => ({
|
|
86
|
+
code,
|
|
87
|
+
level: code.split(":")[0],
|
|
88
|
+
message: message || code
|
|
89
|
+
});
|
|
90
|
+
if (/captcha|验证码|人机校验|recaptcha|hcaptcha|turnstile/.test(msg)) return pick(FAIL_CODES.captcha);
|
|
91
|
+
if (/login|登录|signin|sign in|未登录|login-required|LOGIN_REQUIRED/.test(msg)) return pick(FAIL_CODES.loginWall);
|
|
92
|
+
if (/permission|权限|not allowed|forbidden|EACCES/.test(msg)) return pick(FAIL_CODES.permissionDenied);
|
|
93
|
+
if (/selector|选择器|element not found|未找到元素|no such element|queryselector returned null/.test(msg))
|
|
94
|
+
return pick(FAIL_CODES.selectorNotFound);
|
|
95
|
+
if (/timeout|超时|timed out|waiting for/.test(msg)) return pick(FAIL_CODES.pageNotReady);
|
|
96
|
+
if (/net::err|404|navigation failed|页面不存在|target closed|element-gone/.test(msg))
|
|
97
|
+
return pick(FAIL_CODES.targetGone);
|
|
98
|
+
return pick(FAIL_CODES.unknown);
|
|
99
|
+
}
|
|
100
|
+
|
|
72
101
|
// src/executor.ts
|
|
73
102
|
import {
|
|
74
103
|
ok as ok30,
|
|
@@ -7966,7 +7995,16 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
7966
7995
|
if (isSuccess) {
|
|
7967
7996
|
return { ...ok30(raw.data, merged.length > 0 ? merged : raw.tips), duration, ...hookOutputs ? { hookOutputs } : {} };
|
|
7968
7997
|
}
|
|
7969
|
-
|
|
7998
|
+
const fc = classifyFailure(raw.message);
|
|
7999
|
+
return {
|
|
8000
|
+
success: false,
|
|
8001
|
+
data: raw.data,
|
|
8002
|
+
message: raw.message,
|
|
8003
|
+
tips: mergedOrRaw,
|
|
8004
|
+
duration,
|
|
8005
|
+
error: { code: fc.code, level: fc.level },
|
|
8006
|
+
...hookOutputs ? { hookOutputs } : {}
|
|
8007
|
+
};
|
|
7970
8008
|
}
|
|
7971
8009
|
const smartTipNormalized = normalizeTips6(smartTips);
|
|
7972
8010
|
recordArchive(session?.id, sessionName, {
|
|
@@ -7985,7 +8023,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
7985
8023
|
const errorMessage = errMsg(err);
|
|
7986
8024
|
if (session?.page && process.env.XBROWSER_RECOVERY && !extraOpts?._recoveryAttempted) {
|
|
7987
8025
|
try {
|
|
7988
|
-
const { attemptRecovery } = await import("./recovery-
|
|
8026
|
+
const { attemptRecovery } = await import("./recovery-5C4FBA5O.js");
|
|
7989
8027
|
const recovery = await attemptRecovery(
|
|
7990
8028
|
session.page,
|
|
7991
8029
|
sessionName,
|
|
@@ -8060,7 +8098,8 @@ async function executeChain(input, options) {
|
|
|
8060
8098
|
const cmdArgs = parts.slice(1);
|
|
8061
8099
|
const loader = await getPluginLoader();
|
|
8062
8100
|
const internalLoader = loader.getCore().loader;
|
|
8063
|
-
const
|
|
8101
|
+
const resolvedName = loader.resolveSiteName(cmdName);
|
|
8102
|
+
const site = resolvedName ? internalLoader.getSite(resolvedName) : void 0;
|
|
8064
8103
|
if (site) {
|
|
8065
8104
|
const subCommand = cmdArgs[0];
|
|
8066
8105
|
if (!subCommand) {
|
|
@@ -8101,7 +8140,15 @@ async function executeChain(input, options) {
|
|
|
8101
8140
|
continue;
|
|
8102
8141
|
}
|
|
8103
8142
|
const pluginArgs = cmdArgs.slice(1);
|
|
8104
|
-
|
|
8143
|
+
let pluginParams = parsePluginParams(pluginArgs, cmdEntry.parameters);
|
|
8144
|
+
if (cmdEntry.parameters) {
|
|
8145
|
+
const schemaCheck = cmdEntry.parameters.safeParse(pluginParams);
|
|
8146
|
+
if (schemaCheck.success) {
|
|
8147
|
+
for (const [k, v] of Object.entries(schemaCheck.data)) {
|
|
8148
|
+
if (!(k in pluginParams)) pluginParams[k] = v;
|
|
8149
|
+
}
|
|
8150
|
+
}
|
|
8151
|
+
}
|
|
8105
8152
|
const pluginCtx = {
|
|
8106
8153
|
args: pluginArgs,
|
|
8107
8154
|
options: pluginParams,
|
|
@@ -9025,7 +9072,7 @@ function createRPCHandler() {
|
|
|
9025
9072
|
return result;
|
|
9026
9073
|
} catch (err) {
|
|
9027
9074
|
const errorMessage = errMsg(err);
|
|
9028
|
-
const { attemptRecovery } = await import("./recovery-
|
|
9075
|
+
const { attemptRecovery } = await import("./recovery-5C4FBA5O.js");
|
|
9029
9076
|
const recovery = await attemptRecovery(
|
|
9030
9077
|
session?.page,
|
|
9031
9078
|
sessionName,
|
|
@@ -9192,9 +9239,9 @@ function createRPCHandler() {
|
|
|
9192
9239
|
return result;
|
|
9193
9240
|
}
|
|
9194
9241
|
async function handlePluginsReload() {
|
|
9195
|
-
const { resetPluginLoader } = await import("./plugin-singleton-
|
|
9242
|
+
const { resetPluginLoader } = await import("./plugin-singleton-GGSBI3EG.js");
|
|
9196
9243
|
resetPluginLoader();
|
|
9197
|
-
const loader = await import("./plugin-singleton-
|
|
9244
|
+
const loader = await import("./plugin-singleton-GGSBI3EG.js").then((m) => m.getPluginLoader());
|
|
9198
9245
|
const sites = loader.getCore().loader.getSites();
|
|
9199
9246
|
return { ok: true, plugins: sites.length };
|
|
9200
9247
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -836,6 +836,10 @@ interface ExecutionResult {
|
|
|
836
836
|
duration: number;
|
|
837
837
|
tips?: Tip[];
|
|
838
838
|
hookOutputs?: HookOutput[];
|
|
839
|
+
error?: {
|
|
840
|
+
code: string;
|
|
841
|
+
level: string;
|
|
842
|
+
};
|
|
839
843
|
}
|
|
840
844
|
/**
|
|
841
845
|
* Result of a single step within a command chain execution.
|
|
@@ -1630,6 +1634,8 @@ declare class XBrowserPluginLoader {
|
|
|
1630
1634
|
private core;
|
|
1631
1635
|
private loader;
|
|
1632
1636
|
private options;
|
|
1637
|
+
/** 目录名 → 该插件注册的 site 名(site.name ≠ 目录名时的命令别名兜底) */
|
|
1638
|
+
private dirSiteAliases;
|
|
1633
1639
|
constructor(options?: PluginLoaderOptions);
|
|
1634
1640
|
getAPI(): XCLIAPI;
|
|
1635
1641
|
/**
|
|
@@ -1640,6 +1646,16 @@ declare class XBrowserPluginLoader {
|
|
|
1640
1646
|
getPlugin(id: string): PluginInstance | undefined;
|
|
1641
1647
|
getPluginStatus(id: string): PluginStatus;
|
|
1642
1648
|
getLoadedPlugins(): PluginInstance[];
|
|
1649
|
+
/**
|
|
1650
|
+
* 目录名别名兜底(小白用户实测踩坑):`plugin list` 显示目录名,但命令路由
|
|
1651
|
+
* 按 site.name 匹配——两者不一致时(如目录 alibaba-1688、site 名 1688),
|
|
1652
|
+
* 用户按 list 显示的名字敲命令会 "Unknown command"。映射在 scanAndLoad/
|
|
1653
|
+
* loadPlugin 时通过加载前后 site 快照 diff 记录(instance.siteName 返回的是
|
|
1654
|
+
* 插件 id 而非 site 名,不可用)。
|
|
1655
|
+
*/
|
|
1656
|
+
resolveSiteName(name: string): string | undefined;
|
|
1657
|
+
/** 记录一次插件加载引入的 site:加载前后快照 diff(同插件可注册多个 site) */
|
|
1658
|
+
private recordSiteAliases;
|
|
1643
1659
|
getPluginContract(siteName: string, commandName?: string): PluginContract | PluginCommandContract | undefined;
|
|
1644
1660
|
loadPlugin(pluginPath: string, id?: string): Promise<PluginInstance>;
|
|
1645
1661
|
unloadPlugin(id: string): Promise<void>;
|
|
@@ -2488,6 +2504,10 @@ interface WebhookPayload {
|
|
|
2488
2504
|
previewUrl?: string;
|
|
2489
2505
|
targetUrl?: string;
|
|
2490
2506
|
timeout?: number;
|
|
2507
|
+
/** Drel 等推送服务的通知标题(可选;缺省由 event 推导) */
|
|
2508
|
+
title?: string;
|
|
2509
|
+
/** Drel 等推送服务的通知正文(可选;缺省由其余字段拼装) */
|
|
2510
|
+
body?: string;
|
|
2491
2511
|
}
|
|
2492
2512
|
/**
|
|
2493
2513
|
* Sends webhook notifications for browser automation lifecycle events.
|
|
@@ -2505,6 +2525,14 @@ declare class WebhookNotifier {
|
|
|
2505
2525
|
* @returns `true` if the request succeeded (HTTP 2xx), `false` otherwise.
|
|
2506
2526
|
*/
|
|
2507
2527
|
notify(payload: WebhookPayload): Promise<boolean>;
|
|
2528
|
+
/**
|
|
2529
|
+
* previewUrl 里的 localhost/127.0.0.1 替换为本机局域网 IPv4——
|
|
2530
|
+
* 推送目标是手机,localhost 在手机上不可达;同一 Wi-Fi 下局域网 IP 可达。
|
|
2531
|
+
*/
|
|
2532
|
+
private lanify;
|
|
2533
|
+
/** Drel 推送端点识别(api.drel.app/<token>) */
|
|
2534
|
+
private isDrelEndpoint;
|
|
2535
|
+
private toDrelPayload;
|
|
2508
2536
|
}
|
|
2509
2537
|
|
|
2510
2538
|
interface CaptchaConfig {
|
package/dist/index.js
CHANGED
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
splitCommand,
|
|
49
49
|
waitForAIResponse,
|
|
50
50
|
waitForPage
|
|
51
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-5PZXCIDR.js";
|
|
52
52
|
import {
|
|
53
53
|
SessionRecorder
|
|
54
54
|
} from "./chunk-IR6C24P7.js";
|
|
@@ -97,7 +97,7 @@ import {
|
|
|
97
97
|
HumanInteractionManager,
|
|
98
98
|
ScreencastCapturer,
|
|
99
99
|
WebhookNotifier
|
|
100
|
-
} from "./chunk-
|
|
100
|
+
} from "./chunk-SOPN4XDK.js";
|
|
101
101
|
import {
|
|
102
102
|
NPM_REGISTRY_URL,
|
|
103
103
|
NPM_SCOPE,
|
|
@@ -720,6 +720,9 @@ async function installFromMarketplace(pluginsDir, slug, options) {
|
|
|
720
720
|
const realSlug = String(plugin.slug || slug);
|
|
721
721
|
try {
|
|
722
722
|
await downloadAndExtractMarketplaceTarball(baseUrl, realSlug, tmpDir, targetDir);
|
|
723
|
+
} catch (err) {
|
|
724
|
+
safeCleanup5(targetDir);
|
|
725
|
+
throw err;
|
|
723
726
|
} finally {
|
|
724
727
|
safeCleanup5(tmpDir);
|
|
725
728
|
}
|
|
@@ -893,6 +896,7 @@ function ensureIndexFile(plugin, name, targetDir) {
|
|
|
893
896
|
}
|
|
894
897
|
|
|
895
898
|
// src/plugin/installer.ts
|
|
899
|
+
import { safeCleanup as safeCleanup6 } from "@dyyz1993/xcli-core";
|
|
896
900
|
var PluginInstaller = class {
|
|
897
901
|
pluginsDir;
|
|
898
902
|
constructor(pluginsDir) {
|
|
@@ -918,31 +922,29 @@ var PluginInstaller = class {
|
|
|
918
922
|
}
|
|
919
923
|
mkdirSync4(targetDir, { recursive: true });
|
|
920
924
|
const resolvedSource = type === "npm" ? await resolveNpmPackageWithFallback(source) : source;
|
|
925
|
+
const withCleanup = async (fn) => {
|
|
926
|
+
try {
|
|
927
|
+
const r = await fn();
|
|
928
|
+
await this.fixSharedDeps(targetDir);
|
|
929
|
+
ensurePluginDependencies(this.pluginsDir);
|
|
930
|
+
if (!existsSync6(resolve6(targetDir, "index.ts")) && !existsSync6(resolve6(targetDir, "index.js"))) {
|
|
931
|
+
throw new Error("installed package has no index.ts/js entry");
|
|
932
|
+
}
|
|
933
|
+
return r;
|
|
934
|
+
} catch (err) {
|
|
935
|
+
safeCleanup6(targetDir);
|
|
936
|
+
throw err;
|
|
937
|
+
}
|
|
938
|
+
};
|
|
921
939
|
switch (type) {
|
|
922
940
|
case "local":
|
|
923
|
-
return await installFromLocal(source, name, targetDir)
|
|
924
|
-
await this.fixSharedDeps(targetDir);
|
|
925
|
-
ensurePluginDependencies(this.pluginsDir);
|
|
926
|
-
return r;
|
|
927
|
-
});
|
|
941
|
+
return await withCleanup(() => installFromLocal(source, name, targetDir));
|
|
928
942
|
case "npm":
|
|
929
|
-
return await installFromNpm(resolvedSource, name, targetDir)
|
|
930
|
-
await this.fixSharedDeps(targetDir);
|
|
931
|
-
ensurePluginDependencies(this.pluginsDir);
|
|
932
|
-
return r;
|
|
933
|
-
});
|
|
943
|
+
return await withCleanup(() => installFromNpm(resolvedSource, name, targetDir));
|
|
934
944
|
case "git":
|
|
935
|
-
return await installFromGit(source, name, targetDir)
|
|
936
|
-
await this.fixSharedDeps(targetDir);
|
|
937
|
-
ensurePluginDependencies(this.pluginsDir);
|
|
938
|
-
return r;
|
|
939
|
-
});
|
|
945
|
+
return await withCleanup(() => installFromGit(source, name, targetDir));
|
|
940
946
|
case "url":
|
|
941
|
-
return await installFromUrl(source, name, targetDir)
|
|
942
|
-
await this.fixSharedDeps(targetDir);
|
|
943
|
-
ensurePluginDependencies(this.pluginsDir);
|
|
944
|
-
return r;
|
|
945
|
-
});
|
|
947
|
+
return await withCleanup(() => installFromUrl(source, name, targetDir));
|
|
946
948
|
}
|
|
947
949
|
}
|
|
948
950
|
/**
|
|
@@ -5912,7 +5914,14 @@ async function routeCommand(argvIn, stdinCommands) {
|
|
|
5912
5914
|
default: {
|
|
5913
5915
|
const loader = await getPluginLoader();
|
|
5914
5916
|
const internalLoader = loader.getCore().loader;
|
|
5915
|
-
const
|
|
5917
|
+
const resolvedSiteName = loader.resolveSiteName(command);
|
|
5918
|
+
const site = resolvedSiteName ? internalLoader.getSite(resolvedSiteName) : void 0;
|
|
5919
|
+
if (site && resolvedSiteName !== command) {
|
|
5920
|
+
if (mode !== "json" && mode !== "yaml") {
|
|
5921
|
+
process.stderr.write(`\u2139\uFE0F "${command}" \u7684\u547D\u4EE4\u540D\u662F "${resolvedSiteName}"\uFF08\u63D2\u4EF6\u76EE\u5F55\u540D\u4E0E\u7AD9\u70B9\u540D\u4E0D\u4E00\u81F4\uFF0C\u5DF2\u81EA\u52A8\u6620\u5C04\uFF09
|
|
5922
|
+
`);
|
|
5923
|
+
}
|
|
5924
|
+
}
|
|
5916
5925
|
if (!site) {
|
|
5917
5926
|
const globalFlagSet = /* @__PURE__ */ new Set(["--session", "--cdp", "--json", "--yaml", "--help", "-h", "--version", "--output", "-o"]);
|
|
5918
5927
|
const cleanParts = [];
|
|
@@ -6053,6 +6062,14 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
6053
6062
|
if (options.target && !params._target) {
|
|
6054
6063
|
params._target = options.target;
|
|
6055
6064
|
}
|
|
6065
|
+
if (cmdEntry.parameters) {
|
|
6066
|
+
const schemaFill = cmdEntry.parameters.safeParse(params);
|
|
6067
|
+
if (schemaFill.success) {
|
|
6068
|
+
for (const [k, v] of Object.entries(schemaFill.data)) {
|
|
6069
|
+
if (!(k in params)) params[k] = v;
|
|
6070
|
+
}
|
|
6071
|
+
}
|
|
6072
|
+
}
|
|
6056
6073
|
const needsBrowser = cmdEntry.scope === "page" || cmdEntry.scope === "browser";
|
|
6057
6074
|
if (needsBrowser && !process.env.XBROWSER_DAEMON_WORKER) {
|
|
6058
6075
|
const { forwardPluginExec } = await import("./daemon-client-HEGAXWGK.js");
|
|
@@ -6360,7 +6377,7 @@ async function handleRemote(args, options, mode) {
|
|
|
6360
6377
|
}
|
|
6361
6378
|
}
|
|
6362
6379
|
async function startMcpServer() {
|
|
6363
|
-
const { startMcpStdio } = await import("./server-
|
|
6380
|
+
const { startMcpStdio } = await import("./server-NXXU3UGI.js");
|
|
6364
6381
|
startMcpStdio();
|
|
6365
6382
|
}
|
|
6366
6383
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HumanInteractionManager
|
|
3
|
+
} from "./chunk-FNXA6FDM.js";
|
|
1
4
|
import {
|
|
2
5
|
buildViewerUrl
|
|
3
6
|
} from "./chunk-3OD76SUE.js";
|
|
4
|
-
import {
|
|
5
|
-
HumanInteractionManager
|
|
6
|
-
} from "./chunk-W44UXZSC.js";
|
|
7
7
|
import "./chunk-ZTHE5RBZ.js";
|
|
8
8
|
import "./chunk-Q2DFJQGS.js";
|
|
9
9
|
import "./chunk-KFQGP6VL.js";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
HumanInteractionManager
|
|
3
|
-
} from "./chunk-LPW6QS4M.js";
|
|
4
1
|
import {
|
|
5
2
|
buildViewerUrl
|
|
6
3
|
} from "./chunk-3OD76SUE.js";
|
|
4
|
+
import {
|
|
5
|
+
HumanInteractionManager
|
|
6
|
+
} from "./chunk-SOPN4XDK.js";
|
|
7
7
|
import "./chunk-ZTHE5RBZ.js";
|
|
8
8
|
import "./chunk-Q2DFJQGS.js";
|
|
9
9
|
import "./chunk-KFQGP6VL.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xbrowser/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.1",
|
|
4
4
|
"description": "Browser automation CLI for web scraping, headless browsing, SEO analysis, and AI agent workflows. A command-line alternative to Playwright, Puppeteer, and Selenium.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -130,4 +130,4 @@
|
|
|
130
130
|
"optionalDependencies": {
|
|
131
131
|
"undici": "^7.29.1"
|
|
132
132
|
}
|
|
133
|
-
}
|
|
133
|
+
}
|