@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.
@@ -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-DK6QGYNC.js");
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 site = this.core.loader.getSite(siteName);
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
- return this.loader.loadPlugin(pluginPath, id);
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
- return { success: false, data: raw.data, message: raw.message, tips: mergedOrRaw, duration, ...hookOutputs ? { hookOutputs } : {} };
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-67DMH2OM.js");
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 site = internalLoader.getSite(cmdName);
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
- const pluginParams = parsePluginParams(pluginArgs, cmdEntry.parameters);
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 site = this.core.loader.getSite(siteName);
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
- return this.loader.loadPlugin(pluginPath, id);
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
- return { success: false, data: raw.data, message: raw.message, tips: mergedOrRaw, duration, ...hookOutputs ? { hookOutputs } : {} };
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-6RI2FE3E.js");
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 site = internalLoader.getSite(cmdName);
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
- const pluginParams = parsePluginParams(pluginArgs, cmdEntry.parameters);
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: JSON.stringify(payload),
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 site = this.core.loader.getSite(siteName);
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
- return this.loader.loadPlugin(pluginPath, id);
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: JSON.stringify(payload),
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: JSON.stringify(payload),
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-TBSS7ES4.js";
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).then(async (r) => {
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).then(async (r) => {
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).then(async (r) => {
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).then(async (r) => {
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 site = internalLoader.getSite(command);
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-HU4F5KN7.js");
6284
+ const { startMcpStdio } = await import("./server-KSHSG4TY.js");
6268
6285
  startMcpStdio();
6269
6286
  }
6270
6287
 
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-JKVUFP3G.js";
5
5
  import {
6
6
  getPluginLoader
7
- } from "./chunk-A3MORKYR.js";
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
- return { success: false, data: raw.data, message: raw.message, tips: mergedOrRaw, duration, ...hookOutputs ? { hookOutputs } : {} };
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-LGY25LIM.js");
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 site = internalLoader.getSite(cmdName);
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
- const pluginParams = parsePluginParams(pluginArgs, cmdEntry.parameters);
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-LGY25LIM.js");
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-GJO7EZXB.js");
9242
+ const { resetPluginLoader } = await import("./plugin-singleton-GGSBI3EG.js");
9196
9243
  resetPluginLoader();
9197
- const loader = await import("./plugin-singleton-GJO7EZXB.js").then((m) => m.getPluginLoader());
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
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  HumanInteractionManager
3
- } from "./chunk-FBQZ3PVD.js";
3
+ } from "./chunk-PGKV24QN.js";
4
4
  import "./chunk-HBMEFSTB.js";
5
5
  import "./chunk-KFQGP6VL.js";
6
6
  export {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  HumanInteractionManager
3
- } from "./chunk-LPW6QS4M.js";
3
+ } from "./chunk-SOPN4XDK.js";
4
4
  import "./chunk-ZTHE5RBZ.js";
5
5
  import "./chunk-KFQGP6VL.js";
6
6
  export {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  HumanInteractionManager
3
- } from "./chunk-W44UXZSC.js";
3
+ } from "./chunk-FNXA6FDM.js";
4
4
  import "./chunk-ZTHE5RBZ.js";
5
5
  import "./chunk-KFQGP6VL.js";
6
6
  export {
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-LFX24QJZ.js";
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-W44UXZSC.js";
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).then(async (r) => {
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).then(async (r) => {
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).then(async (r) => {
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).then(async (r) => {
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 site = internalLoader.getSite(command);
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-DZUIBVVR.js");
6380
+ const { startMcpStdio } = await import("./server-NXXU3UGI.js");
6364
6381
  startMcpStdio();
6365
6382
  }
6366
6383
 
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getPluginLoader,
3
3
  resetPluginLoader
4
- } from "./chunk-A3MORKYR.js";
4
+ } from "./chunk-OSZNJT4U.js";
5
5
  import "./chunk-KFQGP6VL.js";
6
6
  export {
7
7
  getPluginLoader,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  HumanInteractionManager
3
- } from "./chunk-FBQZ3PVD.js";
3
+ } from "./chunk-PGKV24QN.js";
4
4
  import {
5
5
  buildViewerUrl
6
6
  } from "./chunk-TZPKFUBT.js";
@@ -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";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  executeChain,
3
3
  executeCommand
4
- } from "./chunk-TBSS7ES4.js";
4
+ } from "./chunk-AYNFXG7V.js";
5
5
  import "./chunk-JKVUFP3G.js";
6
6
  import "./chunk-3OD76SUE.js";
7
7
  import "./chunk-ZTHE5RBZ.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  executeChain,
3
3
  executeCommand
4
- } from "./chunk-LFX24QJZ.js";
4
+ } from "./chunk-5PZXCIDR.js";
5
5
  import "./chunk-2Q3GQRUP.js";
6
6
  import "./chunk-JKVUFP3G.js";
7
7
  import "./chunk-3OD76SUE.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xbrowser/cli",
3
- "version": "1.23.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
+ }