@xbrowser/cli 0.16.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -26
- package/dist/{browser-R7B255ML.js → browser-GITRHHFO.js} +4 -1
- package/dist/{browser-GWBH6OJK.js → browser-R56O3CW6.js} +3 -1
- package/dist/{browser-I2HJZ7IP.js → browser-ZJOZB5CR.js} +4 -2
- package/dist/cdp-driver-BE3FOMRN.js +2803 -0
- package/dist/cdp-driver-TOPYJIFL.js +47 -0
- package/dist/chunk-2SVQTI2O.js +2794 -0
- package/dist/{chunk-KDYXFLAC.js → chunk-ACFE6PKF.js} +1015 -121
- package/dist/chunk-BBMRDUYQ.js +260 -0
- package/dist/chunk-CAFNSGYM.js +4834 -0
- package/dist/{chunk-DTJRVA76.js → chunk-ETCO4SNK.js} +2 -2
- package/dist/{chunk-RS6YYWTK.js → chunk-JPA2ZT2R.js} +140 -72
- package/dist/chunk-JPHCY4TC.js +260 -0
- package/dist/chunk-KFQGP6VL.js +33 -0
- package/dist/{chunk-ITKPSIP7.js → chunk-MDAPTB7C.js} +6 -25
- package/dist/chunk-OZKD3W4X.js +417 -0
- package/dist/chunk-PPG4D2EW.js +2796 -0
- package/dist/{chunk-ATFTAKMN.js → chunk-Q4IGYTKR.js} +39 -7
- package/dist/{chunk-F3ZWFCJJ.js → chunk-QIK2I3VQ.js} +141 -72
- package/dist/chunk-WJRE55TN.js +83 -0
- package/dist/cli.js +2358 -1086
- package/dist/{convert-4DUWZIKH.js → convert-LB3GJTLR.js} +4 -2
- package/dist/{convert-EKQVHKB4.js → convert-R3XXYKC6.js} +2 -2
- package/dist/{daemon-client-GX2UYIW4.js → daemon-client-DRCUMNHK.js} +45 -72
- package/dist/{daemon-client-XWSSQBEA.js → daemon-client-UZZEHHIV.js} +8 -1
- package/dist/daemon-main.js +3067 -1688
- package/dist/{extract-JUOQQX4V.js → extract-2ZFW2MX7.js} +1 -1
- package/dist/{extract-EGRXZSSK.js → extract-BSYBM4MR.js} +2 -0
- package/dist/{filter-OLAE26HN.js → filter-KCFO4RSV.js} +2 -0
- package/dist/{filter-VID2GGZ7.js → filter-T7DSZ2X7.js} +1 -1
- package/dist/{human-interaction-W753RVJB.js → human-interaction-UKAS5ZXV.js} +2 -2
- package/dist/index.d.ts +745 -148
- package/dist/index.js +3488 -1719
- package/dist/launcher-QUJ4M2VS.js +19 -0
- package/dist/launcher-YARP45UY.js +19 -0
- package/dist/{network-store-YAF5OIBH.js → network-store-XGZ25FFC.js} +1 -0
- package/dist/{network-store-BN6QEZ7R.js → network-store-YVDNUREI.js} +1 -1
- package/dist/{parse-action-dsl-T3DYC33D.js → parse-action-dsl-UM333TL2.js} +1 -1
- package/dist/{proxy-WKGUCH2C.js → proxy-LV4BJ5RC.js} +1 -1
- package/dist/session-recorder-RTDGURIJ.js +8 -0
- package/dist/session-recorder-YI7YYM36.js +7 -0
- package/dist/session-replayer-GLTUICSD.js +276 -0
- package/dist/site-knowledge-SYC6VCDB.js +23 -0
- package/package.json +6 -6
- package/dist/chunk-2ONMTDLK.js +0 -2050
- package/dist/daemon-client-3IJD6X4B.js +0 -59
- package/dist/network-store-2S5HATEV.js +0 -194
- package/dist/parse-action-dsl-DRSPBALP.js +0 -72
- package/dist/screenshot-CWAWMXVA.js +0 -28
- package/dist/screenshot-MB6R7RSS.js +0 -26
- package/dist/session-recorder-ILSSV2UC.js +0 -6
- package/dist/session-recorder-XET3DNML.js +0 -7
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
SessionRecorder
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-ACFE6PKF.js";
|
|
5
|
+
import {
|
|
6
|
+
addKnownIssue,
|
|
7
|
+
getKnowledgePath,
|
|
8
|
+
init_site_knowledge,
|
|
9
|
+
listSiteKnowledge,
|
|
10
|
+
readSiteKnowledge,
|
|
11
|
+
readSiteKnowledgeMarkdown
|
|
12
|
+
} from "./chunk-OZKD3W4X.js";
|
|
5
13
|
import {
|
|
6
14
|
closeAllSessions,
|
|
7
15
|
closeEphemeralContext,
|
|
@@ -17,7 +25,8 @@ import {
|
|
|
17
25
|
resolveLaunchOpts,
|
|
18
26
|
saveSessionDiskMeta,
|
|
19
27
|
setActivePage
|
|
20
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-CAFNSGYM.js";
|
|
29
|
+
import "./chunk-BBMRDUYQ.js";
|
|
21
30
|
import {
|
|
22
31
|
forwardCommandLog,
|
|
23
32
|
forwardNetworkAnalyze,
|
|
@@ -38,7 +47,6 @@ import {
|
|
|
38
47
|
forwardRecordSummary,
|
|
39
48
|
forwardReplay,
|
|
40
49
|
forwardSessionClose,
|
|
41
|
-
forwardSessionCreate,
|
|
42
50
|
forwardSessionList,
|
|
43
51
|
forwardViewerCheckSelector,
|
|
44
52
|
getDaemonConfig,
|
|
@@ -46,10 +54,11 @@ import {
|
|
|
46
54
|
killAllDaemonProcesses,
|
|
47
55
|
startDaemonProcess,
|
|
48
56
|
stopDaemonProcess
|
|
49
|
-
} from "./chunk-
|
|
57
|
+
} from "./chunk-Q4IGYTKR.js";
|
|
58
|
+
import "./chunk-KFQGP6VL.js";
|
|
50
59
|
|
|
51
60
|
// src/router.ts
|
|
52
|
-
import { parseArgs, outputFormatter as outputFormatter2, isCommandResult as isCommandResult2, helpGenerator as helpGenerator2 } from "@dyyz1993/xcli-core";
|
|
61
|
+
import { parseArgs, outputFormatter as outputFormatter2, isCommandResult as isCommandResult2, helpGenerator as helpGenerator2, TipCollector as TipCollector2, normalizeTips as normalizeTips7, tip as makeTip } from "@dyyz1993/xcli-core";
|
|
53
62
|
|
|
54
63
|
// src/utils/positional-params.ts
|
|
55
64
|
import { unquote } from "@dyyz1993/xcli-core";
|
|
@@ -172,10 +181,13 @@ import {
|
|
|
172
181
|
ok as ok25,
|
|
173
182
|
fail as fail7,
|
|
174
183
|
isCommandResult,
|
|
184
|
+
CompositeStorage,
|
|
185
|
+
TipCollector,
|
|
186
|
+
normalizeTips as normalizeTips6,
|
|
175
187
|
configureArchiveStore,
|
|
176
188
|
appendCommandToArchive,
|
|
177
189
|
checkGuard,
|
|
178
|
-
|
|
190
|
+
unquote as unquote2
|
|
179
191
|
} from "@dyyz1993/xcli-core";
|
|
180
192
|
|
|
181
193
|
// src/commands/navigation.ts
|
|
@@ -322,10 +334,18 @@ var urlCommand = registerCommand({
|
|
|
322
334
|
return ok({ url: ctx.page.url() });
|
|
323
335
|
}
|
|
324
336
|
});
|
|
337
|
+
registerCommand({
|
|
338
|
+
name: "open",
|
|
339
|
+
description: "Navigate to URL (alias for goto)",
|
|
340
|
+
scope: "page",
|
|
341
|
+
parameters: gotoCommand.parameters,
|
|
342
|
+
result: gotoCommand.result,
|
|
343
|
+
handler: gotoCommand.handler
|
|
344
|
+
});
|
|
325
345
|
|
|
326
346
|
// src/commands/interaction.ts
|
|
327
347
|
import { z as z2 } from "zod";
|
|
328
|
-
import { ok as ok2 } from "@dyyz1993/xcli-core";
|
|
348
|
+
import { ok as ok2, normalizeTips } from "@dyyz1993/xcli-core";
|
|
329
349
|
|
|
330
350
|
// src/lib/captcha.ts
|
|
331
351
|
var CAPTCHA_SELECTORS = {
|
|
@@ -393,15 +413,15 @@ var clickCommand = registerCommand({
|
|
|
393
413
|
let detectedNewPage;
|
|
394
414
|
let cleanup;
|
|
395
415
|
if (ctx.browserContext?.on) {
|
|
396
|
-
const pagePromise = new Promise((
|
|
416
|
+
const pagePromise = new Promise((resolve10) => {
|
|
397
417
|
const timer = setTimeout(() => {
|
|
398
418
|
ctx.browserContext.off("page", handler);
|
|
399
|
-
|
|
419
|
+
resolve10(void 0);
|
|
400
420
|
}, 3e3);
|
|
401
421
|
const handler = (page2) => {
|
|
402
422
|
clearTimeout(timer);
|
|
403
423
|
ctx.browserContext.off("page", handler);
|
|
404
|
-
|
|
424
|
+
resolve10(page2);
|
|
405
425
|
};
|
|
406
426
|
ctx.browserContext.on("page", handler);
|
|
407
427
|
});
|
|
@@ -436,7 +456,7 @@ var clickCommand = registerCommand({
|
|
|
436
456
|
selector: p.selector,
|
|
437
457
|
newTab: { url: newUrl, title: newTitle }
|
|
438
458
|
});
|
|
439
|
-
result.tips = [`\u65B0 Tab \u5DF2\u6253\u5F00: ${newTitle ? newTitle + " \u2014 " : ""}${newUrl}`];
|
|
459
|
+
result.tips = normalizeTips([`\u65B0 Tab \u5DF2\u6253\u5F00: ${newTitle ? newTitle + " \u2014 " : ""}${newUrl}`]);
|
|
440
460
|
return result;
|
|
441
461
|
}
|
|
442
462
|
const captchaInfo = await detectCaptcha(page);
|
|
@@ -450,7 +470,7 @@ var clickCommand = registerCommand({
|
|
|
450
470
|
tips.push(solved ? "\u2705 CAPTCHA solved!" : "\u274C CAPTCHA timeout");
|
|
451
471
|
}
|
|
452
472
|
const result = ok2({ selector: p.selector, captcha: captchaInfo });
|
|
453
|
-
result.tips = tips;
|
|
473
|
+
result.tips = normalizeTips(tips);
|
|
454
474
|
return result;
|
|
455
475
|
}
|
|
456
476
|
return ok2({ selector: p.selector });
|
|
@@ -535,7 +555,7 @@ var pressCommand = registerCommand({
|
|
|
535
555
|
key: z2.string()
|
|
536
556
|
}),
|
|
537
557
|
handler: async (p, ctx) => {
|
|
538
|
-
await ctx.page.press(p.selector || "body", p.key, {
|
|
558
|
+
await ctx.page.press(p.selector || "body", p.key, { timeout: 1e4 });
|
|
539
559
|
return ok2({ key: p.key });
|
|
540
560
|
}
|
|
541
561
|
});
|
|
@@ -554,7 +574,7 @@ var selectCommand = registerCommand({
|
|
|
554
574
|
}),
|
|
555
575
|
handler: async (p, ctx) => {
|
|
556
576
|
const values = typeof p.value === "string" ? [p.value] : p.value;
|
|
557
|
-
await ctx.page.selectOption(p.selector, values
|
|
577
|
+
await ctx.page.selectOption(p.selector, values);
|
|
558
578
|
return ok2({ selector: p.selector, value: p.value });
|
|
559
579
|
}
|
|
560
580
|
});
|
|
@@ -570,7 +590,7 @@ var checkCommand = registerCommand({
|
|
|
570
590
|
selector: z2.string()
|
|
571
591
|
}),
|
|
572
592
|
handler: async (p, ctx) => {
|
|
573
|
-
await ctx.page.check(p.selector, {
|
|
593
|
+
await ctx.page.check(p.selector, { timeout: 1e4 });
|
|
574
594
|
return ok2({ selector: p.selector });
|
|
575
595
|
}
|
|
576
596
|
});
|
|
@@ -587,7 +607,7 @@ var hoverCommand = registerCommand({
|
|
|
587
607
|
selector: z2.string()
|
|
588
608
|
}),
|
|
589
609
|
handler: async (p, ctx) => {
|
|
590
|
-
await ctx.page.hover(p.selector, {
|
|
610
|
+
await ctx.page.hover(p.selector, { timeout: 1e4 });
|
|
591
611
|
return ok2({ selector: p.selector });
|
|
592
612
|
}
|
|
593
613
|
});
|
|
@@ -785,7 +805,7 @@ var mouseCommand = registerCommand({
|
|
|
785
805
|
|
|
786
806
|
// src/commands/evaluate.ts
|
|
787
807
|
import { z as z7 } from "zod";
|
|
788
|
-
import { ok as ok7 } from "@dyyz1993/xcli-core";
|
|
808
|
+
import { ok as ok7, normalizeTips as normalizeTips2 } from "@dyyz1993/xcli-core";
|
|
789
809
|
var evaluateCommand = registerCommand({
|
|
790
810
|
name: "eval",
|
|
791
811
|
description: "Evaluate JavaScript expression in the browser",
|
|
@@ -806,10 +826,10 @@ var evaluateCommand = registerCommand({
|
|
|
806
826
|
const result = await ctx.page.evaluate(p.expression);
|
|
807
827
|
const response = ok7({ result });
|
|
808
828
|
if (decision && decision.severity === "danger") {
|
|
809
|
-
response.tips = [
|
|
829
|
+
response.tips = normalizeTips2([
|
|
810
830
|
`\u26A0\uFE0F CDP Firewall: ${decision.reason}`,
|
|
811
831
|
`\u{1F4A1} Fix: ${decision.suggestion}`
|
|
812
|
-
];
|
|
832
|
+
]);
|
|
813
833
|
}
|
|
814
834
|
return response;
|
|
815
835
|
}
|
|
@@ -932,7 +952,19 @@ var clearLocalStorageCommand = registerCommand({
|
|
|
932
952
|
// src/commands/screenshot.ts
|
|
933
953
|
import { z as z9 } from "zod";
|
|
934
954
|
import { ok as ok9 } from "@dyyz1993/xcli-core";
|
|
935
|
-
import { writeFileSync } from "fs";
|
|
955
|
+
import { writeFileSync, mkdirSync } from "fs";
|
|
956
|
+
import { join } from "path";
|
|
957
|
+
import { homedir } from "os";
|
|
958
|
+
var SCREENSHOTS_DIR = join(homedir(), ".xbrowser", "screenshots");
|
|
959
|
+
function ensureScreenshotsDir() {
|
|
960
|
+
mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
|
961
|
+
}
|
|
962
|
+
function generateScreenshotPath(format) {
|
|
963
|
+
const timestamp = Date.now();
|
|
964
|
+
const random = Math.random().toString(36).slice(2, 8);
|
|
965
|
+
const ext = format === "jpeg" ? "jpg" : "png";
|
|
966
|
+
return join(SCREENSHOTS_DIR, `screenshot-${timestamp}-${random}.${ext}`);
|
|
967
|
+
}
|
|
936
968
|
var screenshotCommand = registerCommand({
|
|
937
969
|
name: "screenshot",
|
|
938
970
|
description: "Take a screenshot of the page or element",
|
|
@@ -942,7 +974,8 @@ var screenshotCommand = registerCommand({
|
|
|
942
974
|
selector: z9.string().optional(),
|
|
943
975
|
type: z9.enum(["png", "jpeg"]).optional(),
|
|
944
976
|
fullPage: z9.boolean().optional(),
|
|
945
|
-
output: z9.string().optional()
|
|
977
|
+
output: z9.string().optional(),
|
|
978
|
+
base64: z9.boolean().optional().describe("Return base64 data instead of file path")
|
|
946
979
|
}),
|
|
947
980
|
result: z9.union([
|
|
948
981
|
z9.object({
|
|
@@ -957,8 +990,9 @@ var screenshotCommand = registerCommand({
|
|
|
957
990
|
})
|
|
958
991
|
]),
|
|
959
992
|
handler: async (p, ctx) => {
|
|
993
|
+
const format = p.type || "png";
|
|
960
994
|
const options = {
|
|
961
|
-
type:
|
|
995
|
+
type: format,
|
|
962
996
|
fullPage: p.fullPage || false
|
|
963
997
|
};
|
|
964
998
|
let buffer;
|
|
@@ -971,13 +1005,23 @@ var screenshotCommand = registerCommand({
|
|
|
971
1005
|
writeFileSync(p.output, buffer);
|
|
972
1006
|
return ok9({
|
|
973
1007
|
output: p.output,
|
|
974
|
-
format
|
|
1008
|
+
format,
|
|
1009
|
+
size: buffer.length
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
if (p.base64) {
|
|
1013
|
+
return ok9({
|
|
1014
|
+
data: buffer.toString("base64"),
|
|
1015
|
+
format,
|
|
975
1016
|
size: buffer.length
|
|
976
1017
|
});
|
|
977
1018
|
}
|
|
1019
|
+
ensureScreenshotsDir();
|
|
1020
|
+
const screenshotPath = generateScreenshotPath(format);
|
|
1021
|
+
writeFileSync(screenshotPath, buffer);
|
|
978
1022
|
return ok9({
|
|
979
|
-
|
|
980
|
-
format
|
|
1023
|
+
output: screenshotPath,
|
|
1024
|
+
format,
|
|
981
1025
|
size: buffer.length
|
|
982
1026
|
});
|
|
983
1027
|
}
|
|
@@ -1159,7 +1203,7 @@ var consoleCheckCommand = registerCommand({
|
|
|
1159
1203
|
await page.goto(p.url, { waitUntil: "domcontentloaded" });
|
|
1160
1204
|
}
|
|
1161
1205
|
const messages = await page.evaluate((args) => {
|
|
1162
|
-
return new Promise((
|
|
1206
|
+
return new Promise((resolve10) => {
|
|
1163
1207
|
const collected = [];
|
|
1164
1208
|
const originalConsole = {
|
|
1165
1209
|
log: console.log,
|
|
@@ -1226,7 +1270,7 @@ ${a.stack || ""}`;
|
|
|
1226
1270
|
console.warn = originalConsole.warn;
|
|
1227
1271
|
console.error = originalConsole.error;
|
|
1228
1272
|
console.info = originalConsole.info;
|
|
1229
|
-
|
|
1273
|
+
resolve10(collected);
|
|
1230
1274
|
}, args.duration);
|
|
1231
1275
|
});
|
|
1232
1276
|
}, { duration: p.duration });
|
|
@@ -1575,6 +1619,19 @@ var healthCheckCommand = registerCommand({
|
|
|
1575
1619
|
// src/commands/actions.ts
|
|
1576
1620
|
import { z as z14 } from "zod";
|
|
1577
1621
|
import { ok as ok14 } from "@dyyz1993/xcli-core";
|
|
1622
|
+
import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
1623
|
+
import { join as join2 } from "path";
|
|
1624
|
+
import { homedir as homedir2 } from "os";
|
|
1625
|
+
var SCREENSHOTS_DIR2 = join2(homedir2(), ".xbrowser", "screenshots");
|
|
1626
|
+
function ensureScreenshotsDir2() {
|
|
1627
|
+
mkdirSync2(SCREENSHOTS_DIR2, { recursive: true });
|
|
1628
|
+
}
|
|
1629
|
+
function generateScreenshotPath2(format) {
|
|
1630
|
+
const timestamp = Date.now();
|
|
1631
|
+
const random = Math.random().toString(36).slice(2, 8);
|
|
1632
|
+
const ext = format === "jpeg" ? "jpg" : "png";
|
|
1633
|
+
return join2(SCREENSHOTS_DIR2, `screenshot-${timestamp}-${random}.${ext}`);
|
|
1634
|
+
}
|
|
1578
1635
|
var waitActionSchema = z14.object({
|
|
1579
1636
|
type: z14.literal("wait"),
|
|
1580
1637
|
milliseconds: z14.number().positive().optional(),
|
|
@@ -1592,7 +1649,8 @@ var screenshotActionSchema = z14.object({
|
|
|
1592
1649
|
type: z14.literal("screenshot"),
|
|
1593
1650
|
fullPage: z14.boolean().optional(),
|
|
1594
1651
|
quality: z14.number().min(1).max(100).optional(),
|
|
1595
|
-
viewport: z14.object({ width: z14.number().int().positive(), height: z14.number().int().positive() }).optional()
|
|
1652
|
+
viewport: z14.object({ width: z14.number().int().positive(), height: z14.number().int().positive() }).optional(),
|
|
1653
|
+
base64: z14.boolean().optional().describe("Return base64 data instead of file path")
|
|
1596
1654
|
});
|
|
1597
1655
|
var writeActionSchema = z14.object({
|
|
1598
1656
|
type: z14.literal("write"),
|
|
@@ -1638,7 +1696,7 @@ async function executeAction(page, action) {
|
|
|
1638
1696
|
if (action.selector) {
|
|
1639
1697
|
await page.waitForSelector(action.selector, { timeout: 3e4 });
|
|
1640
1698
|
} else if (action.milliseconds) {
|
|
1641
|
-
await new Promise((
|
|
1699
|
+
await new Promise((resolve10) => setTimeout(resolve10, action.milliseconds));
|
|
1642
1700
|
} else {
|
|
1643
1701
|
throw new Error("wait action requires either milliseconds or selector");
|
|
1644
1702
|
}
|
|
@@ -1659,7 +1717,13 @@ async function executeAction(page, action) {
|
|
|
1659
1717
|
quality: action.quality ?? 80,
|
|
1660
1718
|
...action.viewport ? { clip: { x: 0, y: 0, ...action.viewport } } : {}
|
|
1661
1719
|
});
|
|
1662
|
-
|
|
1720
|
+
if (action.base64) {
|
|
1721
|
+
return { type: "screenshot", result: buf.toString("base64"), base64: true };
|
|
1722
|
+
}
|
|
1723
|
+
ensureScreenshotsDir2();
|
|
1724
|
+
const screenshotPath = generateScreenshotPath2("jpg");
|
|
1725
|
+
writeFileSync2(screenshotPath, buf);
|
|
1726
|
+
return { type: "screenshot", result: screenshotPath };
|
|
1663
1727
|
}
|
|
1664
1728
|
case "write":
|
|
1665
1729
|
await page.keyboard.type(action.text);
|
|
@@ -1721,8 +1785,8 @@ var actionsCommand = registerCommand({
|
|
|
1721
1785
|
results.push(result);
|
|
1722
1786
|
}
|
|
1723
1787
|
})();
|
|
1724
|
-
const timeoutPromise = new Promise((
|
|
1725
|
-
setTimeout(
|
|
1788
|
+
const timeoutPromise = new Promise((resolve10) => {
|
|
1789
|
+
setTimeout(resolve10, timeoutMs);
|
|
1726
1790
|
});
|
|
1727
1791
|
await Promise.race([executionPromise, timeoutPromise]);
|
|
1728
1792
|
const title = await ctx.page.title();
|
|
@@ -2116,7 +2180,7 @@ var scrapeCommand = registerCommand({
|
|
|
2116
2180
|
await page.goto(p.url, { waitUntil: "commit", timeout: p.timeout });
|
|
2117
2181
|
await page.waitForSelector("body", { timeout: p.timeout }).catch(() => {
|
|
2118
2182
|
});
|
|
2119
|
-
await page.waitForLoadState("networkidle",
|
|
2183
|
+
await page.waitForLoadState("networkidle", Math.min(p.timeout, 8e3)).catch(() => {
|
|
2120
2184
|
});
|
|
2121
2185
|
await page.waitForTimeout(p.waitAfterLoad > 0 ? p.waitAfterLoad : 2e3);
|
|
2122
2186
|
if (p.selector) {
|
|
@@ -2396,11 +2460,11 @@ async function navigateForMap(page, url, timeout = 15e3) {
|
|
|
2396
2460
|
}
|
|
2397
2461
|
async function extractPageLinks(page, baseUrl) {
|
|
2398
2462
|
await navigateForMap(page, baseUrl);
|
|
2399
|
-
await new Promise((
|
|
2463
|
+
await new Promise((resolve10) => setTimeout(resolve10, 2e3));
|
|
2400
2464
|
await page.evaluate(() => {
|
|
2401
2465
|
window.scrollTo(0, document.body.scrollHeight);
|
|
2402
2466
|
});
|
|
2403
|
-
await new Promise((
|
|
2467
|
+
await new Promise((resolve10) => setTimeout(resolve10, 1e3));
|
|
2404
2468
|
const origin = new URL(baseUrl).origin;
|
|
2405
2469
|
const rawLinks = await page.evaluate((evalOrigin) => {
|
|
2406
2470
|
return Array.from(document.querySelectorAll("a[href]")).map((a) => {
|
|
@@ -3121,7 +3185,7 @@ async function fetchFullContent(urls, timeout, cdpEndpoint) {
|
|
|
3121
3185
|
const pg = await context.newPage();
|
|
3122
3186
|
try {
|
|
3123
3187
|
await pg.goto(url, { waitUntil: "domcontentloaded", timeout });
|
|
3124
|
-
await pg.waitForLoadState("networkidle",
|
|
3188
|
+
await pg.waitForLoadState("networkidle", timeout).catch(() => {
|
|
3125
3189
|
});
|
|
3126
3190
|
const html = await pg.content();
|
|
3127
3191
|
contentMap.set(url, htmlToMarkdown(html, { onlyMainContent: true }));
|
|
@@ -3517,7 +3581,7 @@ var networkCommand = registerCommand({
|
|
|
3517
3581
|
waitUntil: "domcontentloaded",
|
|
3518
3582
|
timeout: p.timeout
|
|
3519
3583
|
});
|
|
3520
|
-
await page.waitForLoadState("networkidle",
|
|
3584
|
+
await page.waitForLoadState("networkidle", p.timeout).catch(() => {
|
|
3521
3585
|
});
|
|
3522
3586
|
await page.waitForTimeout(p.wait);
|
|
3523
3587
|
const totalCount = captures.length;
|
|
@@ -3826,7 +3890,25 @@ var ENGINE_KEY_ENUM = z20.enum(ALL_ENGINE_KEYS);
|
|
|
3826
3890
|
|
|
3827
3891
|
// src/commands/snapshot.ts
|
|
3828
3892
|
import { z as z21 } from "zod";
|
|
3829
|
-
import { ok as ok20, fail as fail4 } from "@dyyz1993/xcli-core";
|
|
3893
|
+
import { ok as ok20, fail as fail4, normalizeTips as normalizeTips3 } from "@dyyz1993/xcli-core";
|
|
3894
|
+
|
|
3895
|
+
// src/runtime/ref-store.ts
|
|
3896
|
+
var sessions = /* @__PURE__ */ new Map();
|
|
3897
|
+
function normalizeAgentRef(ref) {
|
|
3898
|
+
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
3899
|
+
}
|
|
3900
|
+
function replaceRefs(sessionKey2, screenHash, targets) {
|
|
3901
|
+
sessions.set(sessionKey2, {
|
|
3902
|
+
screenHash,
|
|
3903
|
+
targets: new Map(targets.map((target) => [target.ref, target]))
|
|
3904
|
+
});
|
|
3905
|
+
}
|
|
3906
|
+
function getRefTarget(sessionKey2, ref) {
|
|
3907
|
+
const session = sessions.get(sessionKey2);
|
|
3908
|
+
const target = session?.targets.get(normalizeAgentRef(ref));
|
|
3909
|
+
if (!session || !target) return null;
|
|
3910
|
+
return { screenHash: session.screenHash, target };
|
|
3911
|
+
}
|
|
3830
3912
|
|
|
3831
3913
|
// src/utils/resolve-selector.ts
|
|
3832
3914
|
function buildElementSelector(el) {
|
|
@@ -3999,9 +4081,9 @@ async function resolveSelectors(page, ariaSnapshot) {
|
|
|
3999
4081
|
}
|
|
4000
4082
|
return results;
|
|
4001
4083
|
}
|
|
4002
|
-
var REF_ONLY =
|
|
4084
|
+
var REF_ONLY = /^@?(e\d+)$/;
|
|
4003
4085
|
var refCache = /* @__PURE__ */ new Map();
|
|
4004
|
-
async function resolveRefParams(page, params, selectorKeys, cache) {
|
|
4086
|
+
async function resolveRefParams(page, params, selectorKeys, cache, sessionId) {
|
|
4005
4087
|
const tips = [];
|
|
4006
4088
|
const newParams = { ...params };
|
|
4007
4089
|
if (!selectorKeys || selectorKeys.length === 0) {
|
|
@@ -4010,7 +4092,17 @@ async function resolveRefParams(page, params, selectorKeys, cache) {
|
|
|
4010
4092
|
for (const key of selectorKeys) {
|
|
4011
4093
|
const val = params[key];
|
|
4012
4094
|
if (typeof val !== "string" || !REF_ONLY.test(val)) continue;
|
|
4013
|
-
const
|
|
4095
|
+
const match = val.match(REF_ONLY);
|
|
4096
|
+
if (!match) continue;
|
|
4097
|
+
const ref = normalizeAgentRef(match[1]);
|
|
4098
|
+
if (sessionId) {
|
|
4099
|
+
const runtimeTarget = getRefTarget(sessionId, ref);
|
|
4100
|
+
if (runtimeTarget) {
|
|
4101
|
+
tips.push(`ref=@${ref} (${key}) => ${runtimeTarget.target.selector} (observe)`);
|
|
4102
|
+
newParams[key] = runtimeTarget.target.selector;
|
|
4103
|
+
continue;
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
4014
4106
|
const activeCache = cache ?? refCache;
|
|
4015
4107
|
const cached = activeCache.get(ref);
|
|
4016
4108
|
if (cached) {
|
|
@@ -4039,9 +4131,9 @@ async function resolveRefParams(page, params, selectorKeys, cache) {
|
|
|
4039
4131
|
}
|
|
4040
4132
|
|
|
4041
4133
|
// src/utils/site-semantics.ts
|
|
4042
|
-
import { writeFileSync as
|
|
4043
|
-
import { join, dirname } from "path";
|
|
4044
|
-
import { homedir } from "os";
|
|
4134
|
+
import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync, readFileSync } from "fs";
|
|
4135
|
+
import { join as join3, dirname } from "path";
|
|
4136
|
+
import { homedir as homedir3 } from "os";
|
|
4045
4137
|
import { stringify, parse } from "yaml";
|
|
4046
4138
|
import { execFile } from "child_process";
|
|
4047
4139
|
var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
@@ -4118,10 +4210,10 @@ function inferAction(role, label) {
|
|
|
4118
4210
|
return {};
|
|
4119
4211
|
}
|
|
4120
4212
|
function getSemanticsDir() {
|
|
4121
|
-
return
|
|
4213
|
+
return join3(homedir3(), ".xbrowser", "site-semantics");
|
|
4122
4214
|
}
|
|
4123
4215
|
function getSemanticsPath(domain) {
|
|
4124
|
-
return
|
|
4216
|
+
return join3(getSemanticsDir(), `${domain}.yaml`);
|
|
4125
4217
|
}
|
|
4126
4218
|
function extractDomain2(url) {
|
|
4127
4219
|
try {
|
|
@@ -4158,9 +4250,9 @@ function saveSemantics(domain, pagePath, url, elements) {
|
|
|
4158
4250
|
}
|
|
4159
4251
|
site.updated_at = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
4160
4252
|
if (!existsSync(dir)) {
|
|
4161
|
-
|
|
4253
|
+
mkdirSync3(dir, { recursive: true });
|
|
4162
4254
|
}
|
|
4163
|
-
|
|
4255
|
+
writeFileSync3(filePath, stringify(site, { lineWidth: 0 }));
|
|
4164
4256
|
}
|
|
4165
4257
|
function loadSemantics(domain) {
|
|
4166
4258
|
const filePath = getSemanticsPath(domain);
|
|
@@ -4230,25 +4322,25 @@ aria snapshot\uFF1A
|
|
|
4230
4322
|
async function analyzeWithLLM(ariaSnapshot) {
|
|
4231
4323
|
const piBin = process.env.PI_CLI_PATH || "pi";
|
|
4232
4324
|
const prompt = LLM_PROMPT.replace("{snapshot}", ariaSnapshot.slice(0, 4e3));
|
|
4233
|
-
return new Promise((
|
|
4325
|
+
return new Promise((resolve10) => {
|
|
4234
4326
|
execFile(
|
|
4235
4327
|
piBin,
|
|
4236
4328
|
["--provider", LLM_PROVIDER, "--model", LLM_MODEL, prompt],
|
|
4237
4329
|
{ timeout: LLM_TIMEOUT_MS, maxBuffer: 1024 * 1024 },
|
|
4238
4330
|
(err, stdout, _stderr) => {
|
|
4239
4331
|
if (err) {
|
|
4240
|
-
|
|
4332
|
+
resolve10(null);
|
|
4241
4333
|
return;
|
|
4242
4334
|
}
|
|
4243
4335
|
const output = (stdout || "").trim();
|
|
4244
4336
|
if (!output) {
|
|
4245
|
-
|
|
4337
|
+
resolve10(null);
|
|
4246
4338
|
return;
|
|
4247
4339
|
}
|
|
4248
4340
|
try {
|
|
4249
4341
|
const parsed = parse(output);
|
|
4250
4342
|
if (!parsed || typeof parsed !== "object") {
|
|
4251
|
-
|
|
4343
|
+
resolve10(null);
|
|
4252
4344
|
return;
|
|
4253
4345
|
}
|
|
4254
4346
|
const elements = {};
|
|
@@ -4263,9 +4355,9 @@ async function analyzeWithLLM(ariaSnapshot) {
|
|
|
4263
4355
|
};
|
|
4264
4356
|
}
|
|
4265
4357
|
}
|
|
4266
|
-
|
|
4358
|
+
resolve10(Object.keys(elements).length > 0 ? elements : null);
|
|
4267
4359
|
} catch {
|
|
4268
|
-
|
|
4360
|
+
resolve10(null);
|
|
4269
4361
|
}
|
|
4270
4362
|
}
|
|
4271
4363
|
);
|
|
@@ -4291,6 +4383,404 @@ async function enhanceSemanticsWithLLM(url, ariaSnapshot, ruleBasedElements) {
|
|
|
4291
4383
|
saveSemantics(domain, pathKey, url, llmElements);
|
|
4292
4384
|
}
|
|
4293
4385
|
|
|
4386
|
+
// src/runtime/agent-runtime.ts
|
|
4387
|
+
function sessionKey(sessionId) {
|
|
4388
|
+
return sessionId || "default";
|
|
4389
|
+
}
|
|
4390
|
+
async function observePage(page, sessionId, options = {}) {
|
|
4391
|
+
const [title, raw] = await Promise.all([
|
|
4392
|
+
page.title().catch(() => ""),
|
|
4393
|
+
page.evaluate(
|
|
4394
|
+
({ includeHidden, limit }) => {
|
|
4395
|
+
function hash(input) {
|
|
4396
|
+
let h = 2166136261;
|
|
4397
|
+
for (let i = 0; i < input.length; i++) {
|
|
4398
|
+
h ^= input.charCodeAt(i);
|
|
4399
|
+
h = Math.imul(h, 16777619);
|
|
4400
|
+
}
|
|
4401
|
+
return (h >>> 0).toString(16);
|
|
4402
|
+
}
|
|
4403
|
+
function cssEscape(value) {
|
|
4404
|
+
const css = globalThis.CSS;
|
|
4405
|
+
return css?.escape ? css.escape(value) : value.replace(/["\\#.:,[\]>+~*]/g, "\\$&");
|
|
4406
|
+
}
|
|
4407
|
+
function isUnique(selector2) {
|
|
4408
|
+
try {
|
|
4409
|
+
return document.querySelectorAll(selector2).length === 1;
|
|
4410
|
+
} catch {
|
|
4411
|
+
return false;
|
|
4412
|
+
}
|
|
4413
|
+
}
|
|
4414
|
+
function nthOfType(el) {
|
|
4415
|
+
const tag = el.tagName.toLowerCase();
|
|
4416
|
+
const parent = el.parentElement;
|
|
4417
|
+
if (!parent) return tag;
|
|
4418
|
+
const same = Array.from(parent.children).filter((child) => child.tagName === el.tagName);
|
|
4419
|
+
if (same.length === 1) return tag;
|
|
4420
|
+
return `${tag}:nth-of-type(${same.indexOf(el) + 1})`;
|
|
4421
|
+
}
|
|
4422
|
+
function selectorFor(el) {
|
|
4423
|
+
const tag = el.tagName.toLowerCase();
|
|
4424
|
+
const id = el.getAttribute("id");
|
|
4425
|
+
if (id) {
|
|
4426
|
+
const selector2 = `#${cssEscape(id)}`;
|
|
4427
|
+
if (isUnique(selector2)) return selector2;
|
|
4428
|
+
}
|
|
4429
|
+
for (const attr of ["data-testid", "data-test", "data-qa", "name", "aria-label"]) {
|
|
4430
|
+
const value = el.getAttribute(attr);
|
|
4431
|
+
if (!value) continue;
|
|
4432
|
+
const selector2 = `[${attr}="${cssEscape(value)}"]`;
|
|
4433
|
+
if (isUnique(selector2)) return selector2;
|
|
4434
|
+
const tagged = `${tag}${selector2}`;
|
|
4435
|
+
if (isUnique(tagged)) return tagged;
|
|
4436
|
+
}
|
|
4437
|
+
const classes = Array.from(el.classList).slice(0, 3);
|
|
4438
|
+
for (const cls of classes) {
|
|
4439
|
+
const selector2 = `${tag}.${cssEscape(cls)}`;
|
|
4440
|
+
if (isUnique(selector2)) return selector2;
|
|
4441
|
+
}
|
|
4442
|
+
const parts = [];
|
|
4443
|
+
let cur = el;
|
|
4444
|
+
while (cur && cur !== document.body && cur !== document.documentElement && parts.length < 6) {
|
|
4445
|
+
parts.unshift(nthOfType(cur));
|
|
4446
|
+
const selector2 = parts.join(" > ");
|
|
4447
|
+
if (isUnique(selector2)) return selector2;
|
|
4448
|
+
cur = cur.parentElement;
|
|
4449
|
+
}
|
|
4450
|
+
return parts.join(" > ") || tag;
|
|
4451
|
+
}
|
|
4452
|
+
function roleFor(el) {
|
|
4453
|
+
const explicit = el.getAttribute("role");
|
|
4454
|
+
if (explicit) return explicit;
|
|
4455
|
+
const tag = el.tagName.toLowerCase();
|
|
4456
|
+
if (tag === "a") return "link";
|
|
4457
|
+
if (tag === "button") return "button";
|
|
4458
|
+
if (tag === "select") return "combobox";
|
|
4459
|
+
if (tag === "textarea") return "textbox";
|
|
4460
|
+
if (tag === "input") {
|
|
4461
|
+
const type = (el.getAttribute("type") || "text").toLowerCase();
|
|
4462
|
+
if (type === "checkbox") return "checkbox";
|
|
4463
|
+
if (type === "radio") return "radio";
|
|
4464
|
+
if (type === "button" || type === "submit" || type === "reset") return "button";
|
|
4465
|
+
return "textbox";
|
|
4466
|
+
}
|
|
4467
|
+
return tag;
|
|
4468
|
+
}
|
|
4469
|
+
function textName(el) {
|
|
4470
|
+
const input = el;
|
|
4471
|
+
const direct = el.getAttribute("aria-label") || el.getAttribute("placeholder") || el.getAttribute("title") || input.value || el.textContent || "";
|
|
4472
|
+
return direct.replace(/\s+/g, " ").trim().slice(0, 120);
|
|
4473
|
+
}
|
|
4474
|
+
function actionsFor(role, editable) {
|
|
4475
|
+
const actions = [];
|
|
4476
|
+
if (editable) actions.push("fill", "type");
|
|
4477
|
+
if (role === "combobox") actions.push("select");
|
|
4478
|
+
if (role === "checkbox" || role === "radio") actions.push("check");
|
|
4479
|
+
actions.push("click", "hover");
|
|
4480
|
+
return Array.from(new Set(actions));
|
|
4481
|
+
}
|
|
4482
|
+
const selector = [
|
|
4483
|
+
"a[href]",
|
|
4484
|
+
"button",
|
|
4485
|
+
"input",
|
|
4486
|
+
"textarea",
|
|
4487
|
+
"select",
|
|
4488
|
+
"summary",
|
|
4489
|
+
"label",
|
|
4490
|
+
"[role]",
|
|
4491
|
+
"[tabindex]",
|
|
4492
|
+
'[contenteditable="true"]'
|
|
4493
|
+
].join(",");
|
|
4494
|
+
const candidates = Array.from(document.querySelectorAll(selector));
|
|
4495
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4496
|
+
const targets2 = [];
|
|
4497
|
+
for (const el of candidates) {
|
|
4498
|
+
const rect = el.getBoundingClientRect();
|
|
4499
|
+
const style = window.getComputedStyle(el);
|
|
4500
|
+
const visible = rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
|
|
4501
|
+
if (!includeHidden && !visible) continue;
|
|
4502
|
+
const selectorValue = selectorFor(el);
|
|
4503
|
+
if (!selectorValue || seen.has(selectorValue)) continue;
|
|
4504
|
+
seen.add(selectorValue);
|
|
4505
|
+
const input = el;
|
|
4506
|
+
const tag = el.tagName.toLowerCase();
|
|
4507
|
+
const role = roleFor(el);
|
|
4508
|
+
const editable = tag === "textarea" || tag === "select" || tag === "input" && !["button", "submit", "reset", "checkbox", "radio"].includes((input.type || "").toLowerCase()) || el.isContentEditable;
|
|
4509
|
+
const enabled = !input.disabled && el.getAttribute("aria-disabled") !== "true";
|
|
4510
|
+
const checked = typeof input.checked === "boolean" && ["checkbox", "radio"].includes((input.type || "").toLowerCase()) ? input.checked : void 0;
|
|
4511
|
+
targets2.push({
|
|
4512
|
+
selector: selectorValue,
|
|
4513
|
+
role,
|
|
4514
|
+
name: textName(el),
|
|
4515
|
+
tag,
|
|
4516
|
+
visible,
|
|
4517
|
+
enabled,
|
|
4518
|
+
editable,
|
|
4519
|
+
...checked !== void 0 ? { checked } : {},
|
|
4520
|
+
...editable && input.value ? { value: input.value.slice(0, 120) } : {},
|
|
4521
|
+
...visible ? { box: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) } } : {},
|
|
4522
|
+
actions: actionsFor(role, editable)
|
|
4523
|
+
});
|
|
4524
|
+
if (targets2.length >= limit) break;
|
|
4525
|
+
}
|
|
4526
|
+
const stateText = [
|
|
4527
|
+
location.href,
|
|
4528
|
+
document.title,
|
|
4529
|
+
document.body?.innerText?.slice(0, 5e3) || ""
|
|
4530
|
+
].join("\n");
|
|
4531
|
+
return { screenHash: hash(stateText), targets: targets2 };
|
|
4532
|
+
},
|
|
4533
|
+
{ includeHidden: !!options.includeHidden, limit: options.limit ?? 80 }
|
|
4534
|
+
)
|
|
4535
|
+
]);
|
|
4536
|
+
const targets = raw.targets.map((target, index) => ({
|
|
4537
|
+
ref: `e${index + 1}`,
|
|
4538
|
+
...target
|
|
4539
|
+
}));
|
|
4540
|
+
replaceRefs(sessionKey(sessionId), raw.screenHash, targets);
|
|
4541
|
+
return {
|
|
4542
|
+
url: page.url(),
|
|
4543
|
+
title,
|
|
4544
|
+
screenHash: raw.screenHash,
|
|
4545
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4546
|
+
targets
|
|
4547
|
+
};
|
|
4548
|
+
}
|
|
4549
|
+
function quoteName(name) {
|
|
4550
|
+
const cleaned = name.replace(/\s+/g, " ").trim();
|
|
4551
|
+
if (!cleaned) return "";
|
|
4552
|
+
return ` "${cleaned.replace(/"/g, '\\"')}"`;
|
|
4553
|
+
}
|
|
4554
|
+
function targetFlags(target) {
|
|
4555
|
+
const flags = [target.role || target.tag];
|
|
4556
|
+
if (!target.enabled) flags.push("disabled");
|
|
4557
|
+
if (target.editable) flags.push("editable");
|
|
4558
|
+
if (target.checked !== void 0) flags.push(target.checked ? "checked" : "unchecked");
|
|
4559
|
+
return flags.join(" ");
|
|
4560
|
+
}
|
|
4561
|
+
function buildSelectorMap(observation) {
|
|
4562
|
+
return Object.fromEntries(observation.targets.map((target) => [target.ref, target.selector]));
|
|
4563
|
+
}
|
|
4564
|
+
function formatObservationCompact(observation, options = {}) {
|
|
4565
|
+
const lines = [
|
|
4566
|
+
`Page: ${observation.title || "(untitled)"}`,
|
|
4567
|
+
`URL: ${observation.url}`,
|
|
4568
|
+
`Screen: ${observation.screenHash}`,
|
|
4569
|
+
""
|
|
4570
|
+
];
|
|
4571
|
+
if (observation.targets.length === 0) {
|
|
4572
|
+
lines.push("(no interactive targets)");
|
|
4573
|
+
} else {
|
|
4574
|
+
for (const target of observation.targets) {
|
|
4575
|
+
lines.push(`@${target.ref} [${targetFlags(target)}]${quoteName(target.name)}`);
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
if (options.selectors && observation.targets.length > 0) {
|
|
4579
|
+
lines.push("", "## Selectors");
|
|
4580
|
+
lines.push(observation.targets.map((target) => `${target.ref}: ${target.selector}`).join(" | "));
|
|
4581
|
+
}
|
|
4582
|
+
return lines.join("\n");
|
|
4583
|
+
}
|
|
4584
|
+
async function getPageScreenHash(page) {
|
|
4585
|
+
const observation = await page.evaluate(() => {
|
|
4586
|
+
let h = 2166136261;
|
|
4587
|
+
const input = [location.href, document.title, document.body?.innerText?.slice(0, 5e3) || ""].join("\n");
|
|
4588
|
+
for (let i = 0; i < input.length; i++) {
|
|
4589
|
+
h ^= input.charCodeAt(i);
|
|
4590
|
+
h = Math.imul(h, 16777619);
|
|
4591
|
+
}
|
|
4592
|
+
return (h >>> 0).toString(16);
|
|
4593
|
+
});
|
|
4594
|
+
return observation;
|
|
4595
|
+
}
|
|
4596
|
+
async function actionability(page, selector) {
|
|
4597
|
+
return await page.evaluate((sel) => {
|
|
4598
|
+
const el = document.querySelector(sel);
|
|
4599
|
+
if (!el) return { ok: false, reason: "not_found" };
|
|
4600
|
+
const rect = el.getBoundingClientRect();
|
|
4601
|
+
const style = window.getComputedStyle(el);
|
|
4602
|
+
const visible = rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
|
|
4603
|
+
if (!visible) return { ok: false, reason: "not_visible" };
|
|
4604
|
+
const input = el;
|
|
4605
|
+
const enabled = !input.disabled && el.getAttribute("aria-disabled") !== "true";
|
|
4606
|
+
if (!enabled) return { ok: false, reason: "disabled" };
|
|
4607
|
+
const cx = rect.left + rect.width / 2;
|
|
4608
|
+
const cy = rect.top + rect.height / 2;
|
|
4609
|
+
const hit = document.elementFromPoint(cx, cy);
|
|
4610
|
+
if (hit && hit !== el && !el.contains(hit) && !hit.contains(el)) {
|
|
4611
|
+
return { ok: false, reason: "covered" };
|
|
4612
|
+
}
|
|
4613
|
+
return { ok: true };
|
|
4614
|
+
}, selector);
|
|
4615
|
+
}
|
|
4616
|
+
async function actOnPage(page, sessionId, input) {
|
|
4617
|
+
const normalizedRef = input.ref ? normalizeAgentRef(input.ref) : void 0;
|
|
4618
|
+
const refMatch = normalizedRef ? getRefTarget(sessionKey(sessionId), normalizedRef) : null;
|
|
4619
|
+
const selector = input.selector || refMatch?.target.selector;
|
|
4620
|
+
if (!selector) {
|
|
4621
|
+
return {
|
|
4622
|
+
action: input.action,
|
|
4623
|
+
selector: "",
|
|
4624
|
+
ref: normalizedRef,
|
|
4625
|
+
success: false,
|
|
4626
|
+
reason: input.ref ? "unknown_ref" : "missing_target",
|
|
4627
|
+
message: normalizedRef ? `Ref "${input.ref}" not found. Run observe again.` : "Provide ref or selector."
|
|
4628
|
+
};
|
|
4629
|
+
}
|
|
4630
|
+
const hash = await getPageScreenHash(page).catch(() => void 0);
|
|
4631
|
+
const stale = !!(refMatch && hash && hash !== refMatch.screenHash);
|
|
4632
|
+
if (!input.force) {
|
|
4633
|
+
const check = await actionability(page, selector);
|
|
4634
|
+
if (!check.ok) {
|
|
4635
|
+
return {
|
|
4636
|
+
action: input.action,
|
|
4637
|
+
selector,
|
|
4638
|
+
ref: normalizedRef,
|
|
4639
|
+
success: false,
|
|
4640
|
+
reason: stale ? "stale_ref" : check.reason,
|
|
4641
|
+
message: stale ? `Ref "${input.ref}" may be stale. Run observe again.` : `Target is not actionable: ${check.reason}`,
|
|
4642
|
+
stale,
|
|
4643
|
+
screenHash: hash,
|
|
4644
|
+
target: refMatch?.target
|
|
4645
|
+
};
|
|
4646
|
+
}
|
|
4647
|
+
}
|
|
4648
|
+
const timeout = input.timeout ?? 1e4;
|
|
4649
|
+
try {
|
|
4650
|
+
switch (input.action) {
|
|
4651
|
+
case "click":
|
|
4652
|
+
await page.locator(selector).first().click({ timeout, force: !!input.force });
|
|
4653
|
+
break;
|
|
4654
|
+
case "fill":
|
|
4655
|
+
if (input.value === void 0) throw new Error("fill requires value");
|
|
4656
|
+
await page.locator(selector).first().fill(input.value, { timeout, force: !!input.force });
|
|
4657
|
+
break;
|
|
4658
|
+
case "type":
|
|
4659
|
+
if (input.value === void 0) throw new Error("type requires value");
|
|
4660
|
+
await page.locator(selector).first().pressSequentially(input.value, { timeout });
|
|
4661
|
+
break;
|
|
4662
|
+
case "press":
|
|
4663
|
+
if (!input.key) throw new Error("press requires key");
|
|
4664
|
+
await page.locator(selector).first().press(input.key, { timeout });
|
|
4665
|
+
break;
|
|
4666
|
+
case "select":
|
|
4667
|
+
if (input.value === void 0) throw new Error("select requires value");
|
|
4668
|
+
await page.locator(selector).first().selectOption(input.value);
|
|
4669
|
+
break;
|
|
4670
|
+
case "check":
|
|
4671
|
+
await page.locator(selector).first().check({ timeout });
|
|
4672
|
+
break;
|
|
4673
|
+
case "hover":
|
|
4674
|
+
await page.locator(selector).first().hover({ timeout });
|
|
4675
|
+
break;
|
|
4676
|
+
default: {
|
|
4677
|
+
const neverAction = input.action;
|
|
4678
|
+
throw new Error(`Unsupported action: ${neverAction}`);
|
|
4679
|
+
}
|
|
4680
|
+
}
|
|
4681
|
+
} catch (error) {
|
|
4682
|
+
return {
|
|
4683
|
+
action: input.action,
|
|
4684
|
+
selector,
|
|
4685
|
+
ref: normalizedRef,
|
|
4686
|
+
success: false,
|
|
4687
|
+
reason: "browser_error",
|
|
4688
|
+
message: error.message,
|
|
4689
|
+
stale,
|
|
4690
|
+
screenHash: hash,
|
|
4691
|
+
target: refMatch?.target
|
|
4692
|
+
};
|
|
4693
|
+
}
|
|
4694
|
+
return {
|
|
4695
|
+
action: input.action,
|
|
4696
|
+
selector,
|
|
4697
|
+
ref: normalizedRef,
|
|
4698
|
+
success: true,
|
|
4699
|
+
stale,
|
|
4700
|
+
screenHash: hash,
|
|
4701
|
+
target: refMatch?.target
|
|
4702
|
+
};
|
|
4703
|
+
}
|
|
4704
|
+
function matchUrlPattern(url, pattern) {
|
|
4705
|
+
if (pattern.includes("*")) {
|
|
4706
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
4707
|
+
return new RegExp(`^${escaped}$`).test(url);
|
|
4708
|
+
}
|
|
4709
|
+
return url.includes(pattern);
|
|
4710
|
+
}
|
|
4711
|
+
async function pollUntil(timeout, pollInterval, predicate) {
|
|
4712
|
+
const startedAt = Date.now();
|
|
4713
|
+
while (Date.now() - startedAt <= timeout) {
|
|
4714
|
+
if (await predicate()) return true;
|
|
4715
|
+
await new Promise((resolve10) => setTimeout(resolve10, pollInterval));
|
|
4716
|
+
}
|
|
4717
|
+
return false;
|
|
4718
|
+
}
|
|
4719
|
+
async function waitForPage(page, input) {
|
|
4720
|
+
const timeout = input.timeout ?? 3e4;
|
|
4721
|
+
const pollInterval = input.pollInterval ?? 200;
|
|
4722
|
+
const startedAt = Date.now();
|
|
4723
|
+
try {
|
|
4724
|
+
if (input.selector) {
|
|
4725
|
+
const state = input.state ?? "visible";
|
|
4726
|
+
await page.locator(input.selector).first().waitFor({ state, timeout });
|
|
4727
|
+
return { success: true, matched: "selector", timeout, elapsed: Date.now() - startedAt };
|
|
4728
|
+
}
|
|
4729
|
+
if (input.text) {
|
|
4730
|
+
await page.getByText(input.text).first().waitFor({ state: "visible", timeout });
|
|
4731
|
+
return { success: true, matched: "text", timeout, elapsed: Date.now() - startedAt };
|
|
4732
|
+
}
|
|
4733
|
+
if (input.url) {
|
|
4734
|
+
const matched = await pollUntil(timeout, pollInterval, async () => matchUrlPattern(page.url(), input.url));
|
|
4735
|
+
return {
|
|
4736
|
+
success: matched,
|
|
4737
|
+
matched: "url",
|
|
4738
|
+
timeout,
|
|
4739
|
+
elapsed: Date.now() - startedAt,
|
|
4740
|
+
...matched ? {} : { message: `Timed out waiting for URL pattern: ${input.url}` }
|
|
4741
|
+
};
|
|
4742
|
+
}
|
|
4743
|
+
if (input.load) {
|
|
4744
|
+
await page.waitForLoadState(input.load, timeout);
|
|
4745
|
+
return { success: true, matched: "load", timeout, elapsed: Date.now() - startedAt };
|
|
4746
|
+
}
|
|
4747
|
+
if (input.fn) {
|
|
4748
|
+
await page.waitForFunction(input.fn, void 0, { timeout });
|
|
4749
|
+
return { success: true, matched: "fn", timeout, elapsed: Date.now() - startedAt };
|
|
4750
|
+
}
|
|
4751
|
+
if (input.screenHashChanged) {
|
|
4752
|
+
let screenHash = await getPageScreenHash(page);
|
|
4753
|
+
const matched = await pollUntil(timeout, pollInterval, async () => {
|
|
4754
|
+
screenHash = await getPageScreenHash(page);
|
|
4755
|
+
return screenHash !== input.screenHashChanged;
|
|
4756
|
+
});
|
|
4757
|
+
return {
|
|
4758
|
+
success: matched,
|
|
4759
|
+
matched: "screenHashChanged",
|
|
4760
|
+
timeout,
|
|
4761
|
+
elapsed: Date.now() - startedAt,
|
|
4762
|
+
screenHash,
|
|
4763
|
+
...matched ? {} : { message: `Timed out waiting for screen hash to change: ${input.screenHashChanged}` }
|
|
4764
|
+
};
|
|
4765
|
+
}
|
|
4766
|
+
} catch (error) {
|
|
4767
|
+
return {
|
|
4768
|
+
success: false,
|
|
4769
|
+
matched: input.selector ? "selector" : input.text ? "text" : input.url ? "url" : input.load ? "load" : input.fn ? "fn" : "screenHashChanged",
|
|
4770
|
+
timeout,
|
|
4771
|
+
elapsed: Date.now() - startedAt,
|
|
4772
|
+
message: error.message
|
|
4773
|
+
};
|
|
4774
|
+
}
|
|
4775
|
+
return {
|
|
4776
|
+
success: false,
|
|
4777
|
+
matched: "selector",
|
|
4778
|
+
timeout,
|
|
4779
|
+
elapsed: Date.now() - startedAt,
|
|
4780
|
+
message: "Provide one wait predicate: selector, text, url, load, fn, or screenHashChanged."
|
|
4781
|
+
};
|
|
4782
|
+
}
|
|
4783
|
+
|
|
4294
4784
|
// src/commands/snapshot.ts
|
|
4295
4785
|
var snapshotCommand = registerCommand({
|
|
4296
4786
|
name: "snapshot",
|
|
@@ -4300,7 +4790,14 @@ var snapshotCommand = registerCommand({
|
|
|
4300
4790
|
parameters: z21.object({
|
|
4301
4791
|
type: z21.enum(["aria", "text", "dom", "all"]).default("aria").describe("Snapshot type: aria (accessibility tree), text (visible text), dom (element summary), all (combined)"),
|
|
4302
4792
|
selector: z21.string().optional().describe("Scope to a specific element"),
|
|
4303
|
-
depth: z21.number().optional().default(6).describe("Max depth for DOM/aria tree")
|
|
4793
|
+
depth: z21.number().optional().default(6).describe("Max depth for DOM/aria tree"),
|
|
4794
|
+
interactive: z21.boolean().optional().default(false).describe("Return interactive agent refs only"),
|
|
4795
|
+
interactiveOnly: z21.boolean().optional().default(false).describe("Alias for interactive"),
|
|
4796
|
+
i: z21.boolean().optional().default(false).describe("Short alias for interactive"),
|
|
4797
|
+
compact: z21.boolean().optional().default(false).describe("Include compact xbrowser style snapshot text"),
|
|
4798
|
+
c: z21.boolean().optional().default(false).describe("Short alias for compact"),
|
|
4799
|
+
selectors: z21.boolean().optional().default(false).describe("Include ref to CSS selector map"),
|
|
4800
|
+
all: z21.boolean().optional().default(false).describe("Include hidden interactive targets when using interactive snapshot")
|
|
4304
4801
|
}),
|
|
4305
4802
|
result: z21.object({
|
|
4306
4803
|
url: z21.string(),
|
|
@@ -4313,11 +4810,23 @@ var snapshotCommand = registerCommand({
|
|
|
4313
4810
|
const page = ctx.page;
|
|
4314
4811
|
const url = page.url();
|
|
4315
4812
|
const title = await page.title().catch(() => "");
|
|
4813
|
+
if (p.interactive || p.interactiveOnly || p.i || p.compact || p.c || p.selectors) {
|
|
4814
|
+
const observation = await observePage(page, ctx.sessionId, {
|
|
4815
|
+
includeHidden: p.all
|
|
4816
|
+
});
|
|
4817
|
+
if (p.selectors) observation.selectors = buildSelectorMap(observation);
|
|
4818
|
+
if (p.compact || p.c || p.interactive || p.interactiveOnly || p.i) {
|
|
4819
|
+
observation.compact = formatObservationCompact(observation, { selectors: p.selectors });
|
|
4820
|
+
}
|
|
4821
|
+
return ok20(observation, normalizeTips3([
|
|
4822
|
+
`refs refreshed for ${observation.targets.length} targets; use click @e1 or fill @e2 "text"`
|
|
4823
|
+
]));
|
|
4824
|
+
}
|
|
4316
4825
|
if (p.type === "aria") {
|
|
4317
4826
|
const aria = await captureAriaSnapshot(page, p.selector, p.depth);
|
|
4318
4827
|
const tips = await buildRefTips(page, aria);
|
|
4319
4828
|
persistSemantics(url, aria);
|
|
4320
|
-
return ok20({ url, title, aria }, tips);
|
|
4829
|
+
return ok20({ url, title, aria }, normalizeTips3(tips));
|
|
4321
4830
|
}
|
|
4322
4831
|
if (p.type === "text") {
|
|
4323
4832
|
const text = await captureTextSnapshot(page, p.selector);
|
|
@@ -4335,7 +4844,7 @@ var snapshotCommand = registerCommand({
|
|
|
4335
4844
|
]);
|
|
4336
4845
|
const tips = await buildRefTips(page, aria);
|
|
4337
4846
|
persistSemantics(url, aria);
|
|
4338
|
-
return ok20({ url, title, aria, text, dom }, tips);
|
|
4847
|
+
return ok20({ url, title, aria, text, dom }, normalizeTips3(tips));
|
|
4339
4848
|
}
|
|
4340
4849
|
return fail4(`Unknown snapshot type: ${p.type}`);
|
|
4341
4850
|
}
|
|
@@ -4363,10 +4872,10 @@ async function buildRefTips(page, aria) {
|
|
|
4363
4872
|
return [];
|
|
4364
4873
|
}
|
|
4365
4874
|
}
|
|
4366
|
-
async function captureAriaSnapshot(page, selector,
|
|
4875
|
+
async function captureAriaSnapshot(page, selector, _depth) {
|
|
4367
4876
|
try {
|
|
4368
4877
|
const locator = selector ? page.locator(selector).first() : page.locator("body");
|
|
4369
|
-
return await locator.ariaSnapshot(
|
|
4878
|
+
return await locator.ariaSnapshot();
|
|
4370
4879
|
} catch {
|
|
4371
4880
|
try {
|
|
4372
4881
|
return await page.locator("body").ariaSnapshot();
|
|
@@ -4377,7 +4886,7 @@ async function captureAriaSnapshot(page, selector, depth) {
|
|
|
4377
4886
|
}
|
|
4378
4887
|
async function captureTextSnapshot(page, selector) {
|
|
4379
4888
|
if (selector) {
|
|
4380
|
-
return await page.locator(selector).first().innerText(
|
|
4889
|
+
return await page.locator(selector).first().innerText().catch(() => "");
|
|
4381
4890
|
}
|
|
4382
4891
|
return await page.evaluate(() => document.body?.innerText || "").catch(() => "");
|
|
4383
4892
|
}
|
|
@@ -4413,22 +4922,113 @@ async function captureDomSnapshot(page, selector, maxDepth) {
|
|
|
4413
4922
|
).catch(() => ({ tag: "error" }));
|
|
4414
4923
|
}
|
|
4415
4924
|
|
|
4416
|
-
// src/commands/
|
|
4925
|
+
// src/commands/agent.ts
|
|
4417
4926
|
import { z as z22 } from "zod";
|
|
4418
|
-
import { ok as ok21,
|
|
4419
|
-
var
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4927
|
+
import { ok as ok21, normalizeTips as normalizeTips4 } from "@dyyz1993/xcli-core";
|
|
4928
|
+
var observeCommand = registerCommand({
|
|
4929
|
+
name: "observe",
|
|
4930
|
+
description: "Observe the current page as structured agent targets with session refs",
|
|
4931
|
+
scope: "page",
|
|
4932
|
+
parameters: z22.object({
|
|
4933
|
+
includeHidden: z22.boolean().optional().default(false).describe("Include hidden elements in the target list"),
|
|
4934
|
+
limit: z22.number().int().positive().max(300).optional().default(80).describe("Maximum number of targets to return"),
|
|
4935
|
+
compact: z22.boolean().optional().default(false).describe("Include compact xbrowser style snapshot text"),
|
|
4936
|
+
selectors: z22.boolean().optional().default(false).describe("Include ref to stable CSS selector map")
|
|
4937
|
+
}),
|
|
4938
|
+
result: z22.object({
|
|
4939
|
+
targets: z22.array(z22.record(z22.unknown())),
|
|
4940
|
+
selectors: z22.record(z22.unknown()).optional(),
|
|
4941
|
+
compact: z22.string().optional()
|
|
4942
|
+
}).passthrough(),
|
|
4943
|
+
handler: async (p, ctx) => {
|
|
4944
|
+
const observation = await observePage(ctx.page, ctx.sessionId, {
|
|
4945
|
+
includeHidden: p.includeHidden,
|
|
4946
|
+
limit: p.limit
|
|
4947
|
+
});
|
|
4948
|
+
if (p.selectors) observation.selectors = buildSelectorMap(observation);
|
|
4949
|
+
if (p.compact) observation.compact = formatObservationCompact(observation, { selectors: p.selectors });
|
|
4950
|
+
return ok21(observation, normalizeTips4([
|
|
4951
|
+
`refs refreshed for ${observation.targets.length} targets; use act --ref @e1 --action click or click @e1`
|
|
4952
|
+
]));
|
|
4953
|
+
}
|
|
4954
|
+
});
|
|
4955
|
+
var actCommand = registerCommand({
|
|
4956
|
+
name: "act",
|
|
4957
|
+
description: "Perform an agent action using an observe ref or explicit selector",
|
|
4958
|
+
scope: "element",
|
|
4959
|
+
selectorParams: ["selector"],
|
|
4960
|
+
parameters: z22.object({
|
|
4961
|
+
action: z22.enum(["click", "fill", "type", "press", "select", "check", "hover"]).default("click"),
|
|
4962
|
+
ref: z22.string().optional().describe("Session-scoped ref returned by observe, such as e1"),
|
|
4963
|
+
selector: z22.string().optional().describe("CSS selector fallback when no ref is available"),
|
|
4964
|
+
value: z22.string().optional().describe("Value for fill/type/select"),
|
|
4965
|
+
key: z22.string().optional().describe("Key for press"),
|
|
4966
|
+
force: z22.boolean().optional().default(false).describe("Bypass actionability checks"),
|
|
4967
|
+
timeout: z22.number().optional().default(1e4).describe("Playwright action timeout in milliseconds")
|
|
4968
|
+
}).refine((p) => !!p.ref || !!p.selector, {
|
|
4969
|
+
message: "Either ref or selector is required"
|
|
4970
|
+
}),
|
|
4971
|
+
handler: async (p, ctx) => {
|
|
4972
|
+
const result = await actOnPage(ctx.page, ctx.sessionId, { ...p });
|
|
4973
|
+
if (!result.success) {
|
|
4974
|
+
return {
|
|
4975
|
+
success: false,
|
|
4976
|
+
data: result,
|
|
4977
|
+
message: result.message || result.reason || "Action failed",
|
|
4978
|
+
tips: normalizeTips4(result.stale ? ["run observe again to refresh refs"] : [])
|
|
4979
|
+
};
|
|
4980
|
+
}
|
|
4981
|
+
return ok21(result, normalizeTips4(result.stale ? ["ref screen hash changed; run observe if the next action is uncertain"] : []));
|
|
4982
|
+
}
|
|
4983
|
+
});
|
|
4984
|
+
var waitForCommand = registerCommand({
|
|
4985
|
+
name: "waitFor",
|
|
4986
|
+
description: "Wait for agent predicates such as text, URL, load state, selector state, or screen hash changes",
|
|
4987
|
+
scope: "page",
|
|
4988
|
+
selectorParams: ["selector"],
|
|
4989
|
+
parameters: z22.object({
|
|
4990
|
+
selector: z22.string().optional().describe("CSS selector or observe ref to wait for"),
|
|
4991
|
+
state: z22.enum(["attached", "detached", "visible", "hidden"]).optional().default("visible"),
|
|
4992
|
+
text: z22.string().optional().describe("Visible text to wait for"),
|
|
4993
|
+
url: z22.string().optional().describe("URL substring or glob pattern to wait for"),
|
|
4994
|
+
load: z22.enum(["load", "domcontentloaded", "networkidle"]).optional().describe("Load state to wait for"),
|
|
4995
|
+
fn: z22.string().optional().describe("JavaScript predicate to wait for"),
|
|
4996
|
+
screenHashChanged: z22.string().optional().describe("Previous screenHash from observe"),
|
|
4997
|
+
timeout: z22.number().optional().default(3e4),
|
|
4998
|
+
pollInterval: z22.number().optional().default(200)
|
|
4999
|
+
}).refine((p) => [p.selector, p.text, p.url, p.load, p.fn, p.screenHashChanged].filter(Boolean).length === 1, {
|
|
5000
|
+
message: "Provide exactly one wait predicate: selector, text, url, load, fn, or screenHashChanged"
|
|
5001
|
+
}),
|
|
5002
|
+
handler: async (p, ctx) => {
|
|
5003
|
+
const result = await waitForPage(ctx.page, { ...p });
|
|
5004
|
+
if (!result.success) {
|
|
5005
|
+
return {
|
|
5006
|
+
success: false,
|
|
5007
|
+
data: result,
|
|
5008
|
+
message: result.message || `Timed out waiting for ${result.matched}`,
|
|
5009
|
+
tips: []
|
|
5010
|
+
};
|
|
5011
|
+
}
|
|
5012
|
+
return ok21(result);
|
|
5013
|
+
}
|
|
5014
|
+
});
|
|
5015
|
+
|
|
5016
|
+
// src/commands/tab.ts
|
|
5017
|
+
import { z as z23 } from "zod";
|
|
5018
|
+
import { ok as ok22, fail as fail5 } from "@dyyz1993/xcli-core";
|
|
5019
|
+
var TabParams = z23.object({
|
|
5020
|
+
subcommand: z23.enum(["list", "new", "close", "switch"]),
|
|
5021
|
+
url: z23.string().optional(),
|
|
5022
|
+
index: z23.number().int().min(0).optional()
|
|
4423
5023
|
});
|
|
4424
5024
|
var tabCommand = registerCommand({
|
|
4425
5025
|
name: "tab",
|
|
4426
5026
|
description: "Manage browser tabs: list, new, close, switch",
|
|
4427
5027
|
scope: "page",
|
|
4428
5028
|
parameters: TabParams,
|
|
4429
|
-
result:
|
|
4430
|
-
success:
|
|
4431
|
-
data:
|
|
5029
|
+
result: z23.object({
|
|
5030
|
+
success: z23.boolean(),
|
|
5031
|
+
data: z23.unknown()
|
|
4432
5032
|
}),
|
|
4433
5033
|
handler: async (p, ctx) => {
|
|
4434
5034
|
const pages = ctx.browserContext.pages();
|
|
@@ -4468,7 +5068,7 @@ function handleList(pages, ctx) {
|
|
|
4468
5068
|
active: i === currentIndex
|
|
4469
5069
|
};
|
|
4470
5070
|
});
|
|
4471
|
-
return
|
|
5071
|
+
return ok22({ tabs, total: tabs.length, activeIndex: currentIndex });
|
|
4472
5072
|
}
|
|
4473
5073
|
async function handleNew(p, _pages, ctx) {
|
|
4474
5074
|
const newPage = await ctx.browserContext.newPage();
|
|
@@ -4490,7 +5090,7 @@ async function handleNew(p, _pages, ctx) {
|
|
|
4490
5090
|
const title = await newPage.title().catch(() => "");
|
|
4491
5091
|
const allPages = ctx.browserContext.pages();
|
|
4492
5092
|
const newIndex = allPages.indexOf(newPage);
|
|
4493
|
-
return
|
|
5093
|
+
return ok22({
|
|
4494
5094
|
index: newIndex >= 0 ? newIndex : allPages.length - 1,
|
|
4495
5095
|
url: newPage.url(),
|
|
4496
5096
|
title,
|
|
@@ -4518,7 +5118,7 @@ async function handleClose(p, pages, ctx) {
|
|
|
4518
5118
|
}
|
|
4519
5119
|
ctx.page = newActivePage;
|
|
4520
5120
|
}
|
|
4521
|
-
return
|
|
5121
|
+
return ok22({
|
|
4522
5122
|
closedIndex: closeIndex,
|
|
4523
5123
|
total: remainingPages.length,
|
|
4524
5124
|
activeIndex: isActivePage ? closeIndex < remainingPages.length ? closeIndex : remainingPages.length - 1 : pages.indexOf(ctx.page)
|
|
@@ -4540,7 +5140,7 @@ async function handleSwitch(p, pages, ctx) {
|
|
|
4540
5140
|
}
|
|
4541
5141
|
ctx.page = targetPage;
|
|
4542
5142
|
const title = await targetPage.title().catch(() => "");
|
|
4543
|
-
return
|
|
5143
|
+
return ok22({
|
|
4544
5144
|
index: p.index,
|
|
4545
5145
|
url: targetPage.url(),
|
|
4546
5146
|
title,
|
|
@@ -4549,12 +5149,13 @@ async function handleSwitch(p, pages, ctx) {
|
|
|
4549
5149
|
}
|
|
4550
5150
|
|
|
4551
5151
|
// src/commands/addinitscript.ts
|
|
4552
|
-
import { z as
|
|
4553
|
-
import { ok as
|
|
5152
|
+
import { z as z24 } from "zod";
|
|
5153
|
+
import { ok as ok23 } from "@dyyz1993/xcli-core";
|
|
4554
5154
|
import { readFileSync as readFileSync2 } from "fs";
|
|
4555
5155
|
|
|
4556
5156
|
// src/chain-parser.ts
|
|
4557
|
-
import {
|
|
5157
|
+
import { splitCommand, parseCommandArgs } from "@dyyz1993/xcli-core";
|
|
5158
|
+
import { registerCommandDefinition } from "@dyyz1993/xcli-core";
|
|
4558
5159
|
function parseCommandChain(input, options) {
|
|
4559
5160
|
const result = [];
|
|
4560
5161
|
let currentPipeline = [];
|
|
@@ -4645,159 +5246,56 @@ function isSpaceAround(input, pos, tokenLen) {
|
|
|
4645
5246
|
const after = pos + tokenLen < input.length && input[pos + tokenLen] === " ";
|
|
4646
5247
|
return before && after;
|
|
4647
5248
|
}
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
if (v.startsWith("[") || v.startsWith("{")) {
|
|
4689
|
-
try {
|
|
4690
|
-
return JSON.parse(v);
|
|
4691
|
-
} catch {
|
|
4692
|
-
return v;
|
|
4693
|
-
}
|
|
4694
|
-
}
|
|
4695
|
-
return v;
|
|
4696
|
-
}
|
|
4697
|
-
function parseCommandArgs(name, args) {
|
|
4698
|
-
const definitions = getCommandDefinitions();
|
|
4699
|
-
const def = definitions[name];
|
|
4700
|
-
const positionalKeys = def ? def.positional : [];
|
|
4701
|
-
const params = {};
|
|
4702
|
-
let positionalIndex = 0;
|
|
4703
|
-
for (let i = 0; i < args.length; i++) {
|
|
4704
|
-
const raw = args[i];
|
|
4705
|
-
const arg = unquote2(raw);
|
|
4706
|
-
if (raw.startsWith("--")) {
|
|
4707
|
-
const key = raw.slice(2);
|
|
4708
|
-
const value = args[i + 1];
|
|
4709
|
-
if (value && !value.startsWith("-")) {
|
|
4710
|
-
params[key] = coerceValue2(value);
|
|
4711
|
-
i++;
|
|
4712
|
-
} else {
|
|
4713
|
-
params[key] = true;
|
|
4714
|
-
}
|
|
4715
|
-
} else if (raw.startsWith("-") && raw.length === 2) {
|
|
4716
|
-
const flag = raw[1];
|
|
4717
|
-
const mappedKey = SHORT_FLAG_MAP[flag];
|
|
4718
|
-
const value = args[i + 1];
|
|
4719
|
-
if (mappedKey && value && !value.startsWith("-")) {
|
|
4720
|
-
params[mappedKey] = coerceValue2(value);
|
|
4721
|
-
i++;
|
|
4722
|
-
} else if (value && !value.startsWith("-")) {
|
|
4723
|
-
params[flag] = coerceValue2(value);
|
|
4724
|
-
i++;
|
|
4725
|
-
} else {
|
|
4726
|
-
params[mappedKey || flag] = true;
|
|
4727
|
-
}
|
|
4728
|
-
} else {
|
|
4729
|
-
if (positionalIndex < positionalKeys.length) {
|
|
4730
|
-
const isLast = positionalIndex === positionalKeys.length - 1;
|
|
4731
|
-
if (isLast && name === "eval") {
|
|
4732
|
-
const remaining = args.slice(i).map(unquote2).join(" ");
|
|
4733
|
-
params[positionalKeys[positionalIndex]] = remaining;
|
|
4734
|
-
break;
|
|
4735
|
-
}
|
|
4736
|
-
params[positionalKeys[positionalIndex]] = arg;
|
|
4737
|
-
positionalIndex++;
|
|
4738
|
-
}
|
|
4739
|
-
}
|
|
4740
|
-
}
|
|
4741
|
-
return { command: name, params };
|
|
4742
|
-
}
|
|
4743
|
-
var commandDefCache = {
|
|
4744
|
-
goto: { positional: ["url"] },
|
|
4745
|
-
click: { positional: ["selector"] },
|
|
4746
|
-
fill: { positional: ["selector", "value"] },
|
|
4747
|
-
type: { positional: ["selector", "text"] },
|
|
4748
|
-
press: { positional: ["selector", "key"] },
|
|
4749
|
-
select: { positional: ["selector", "value"] },
|
|
4750
|
-
check: { positional: ["selector"] },
|
|
4751
|
-
uncheck: { positional: ["selector"] },
|
|
4752
|
-
hover: { positional: ["selector"] },
|
|
4753
|
-
dblclick: { positional: ["selector"] },
|
|
4754
|
-
wait: { positional: ["selector"] },
|
|
4755
|
-
screenshot: { positional: [] },
|
|
4756
|
-
eval: { positional: ["expression"] },
|
|
4757
|
-
scroll: { positional: ["direction"] },
|
|
4758
|
-
title: { positional: [] },
|
|
4759
|
-
url: { positional: [] },
|
|
4760
|
-
html: { positional: [] },
|
|
4761
|
-
text: { positional: [] },
|
|
4762
|
-
back: { positional: [] },
|
|
4763
|
-
forward: { positional: [] },
|
|
4764
|
-
refresh: { positional: [] },
|
|
4765
|
-
console: { positional: [] },
|
|
4766
|
-
network: { positional: [] },
|
|
4767
|
-
perf: { positional: [] },
|
|
4768
|
-
health: { positional: [] },
|
|
4769
|
-
scrape: { positional: ["url"] },
|
|
4770
|
-
structure: { positional: [] },
|
|
4771
|
-
"get-cookies": { positional: [] },
|
|
4772
|
-
"set-cookie": { positional: [] },
|
|
4773
|
-
"clear-cookies": { positional: [] },
|
|
4774
|
-
"get-local-storage": { positional: [] },
|
|
4775
|
-
"set-local-storage": { positional: [] },
|
|
4776
|
-
"clear-local-storage": { positional: [] },
|
|
4777
|
-
"set-viewport": { positional: [] },
|
|
4778
|
-
frames: { positional: [] },
|
|
4779
|
-
frame: { positional: ["selector"] },
|
|
4780
|
-
actions: { positional: ["url"] },
|
|
4781
|
-
find: { positional: ["strategy", "value"] },
|
|
4782
|
-
addinitscript: { positional: ["script"] },
|
|
4783
|
-
tab: { positional: ["subcommand"] }
|
|
4784
|
-
};
|
|
4785
|
-
function getCommandDefinitions() {
|
|
4786
|
-
return commandDefCache;
|
|
4787
|
-
}
|
|
4788
|
-
function registerCommandDefinition(name, positional) {
|
|
4789
|
-
commandDefCache[name] = { positional };
|
|
4790
|
-
}
|
|
5249
|
+
registerCommandDefinition("goto", ["url"]);
|
|
5250
|
+
registerCommandDefinition("click", ["selector"]);
|
|
5251
|
+
registerCommandDefinition("fill", ["selector", "value"]);
|
|
5252
|
+
registerCommandDefinition("type", ["selector", "text"]);
|
|
5253
|
+
registerCommandDefinition("press", ["selector", "key"]);
|
|
5254
|
+
registerCommandDefinition("select", ["selector", "value"]);
|
|
5255
|
+
registerCommandDefinition("check", ["selector"]);
|
|
5256
|
+
registerCommandDefinition("uncheck", ["selector"]);
|
|
5257
|
+
registerCommandDefinition("hover", ["selector"]);
|
|
5258
|
+
registerCommandDefinition("dblclick", ["selector"]);
|
|
5259
|
+
registerCommandDefinition("wait", ["selector"]);
|
|
5260
|
+
registerCommandDefinition("screenshot", []);
|
|
5261
|
+
registerCommandDefinition("eval", ["expression"]);
|
|
5262
|
+
registerCommandDefinition("scroll", ["direction"]);
|
|
5263
|
+
registerCommandDefinition("title", []);
|
|
5264
|
+
registerCommandDefinition("url", []);
|
|
5265
|
+
registerCommandDefinition("html", []);
|
|
5266
|
+
registerCommandDefinition("text", []);
|
|
5267
|
+
registerCommandDefinition("back", []);
|
|
5268
|
+
registerCommandDefinition("forward", []);
|
|
5269
|
+
registerCommandDefinition("refresh", []);
|
|
5270
|
+
registerCommandDefinition("console", []);
|
|
5271
|
+
registerCommandDefinition("network", []);
|
|
5272
|
+
registerCommandDefinition("perf", []);
|
|
5273
|
+
registerCommandDefinition("health", []);
|
|
5274
|
+
registerCommandDefinition("scrape", ["url"]);
|
|
5275
|
+
registerCommandDefinition("structure", []);
|
|
5276
|
+
registerCommandDefinition("get-cookies", []);
|
|
5277
|
+
registerCommandDefinition("set-cookie", []);
|
|
5278
|
+
registerCommandDefinition("clear-cookies", []);
|
|
5279
|
+
registerCommandDefinition("get-local-storage", []);
|
|
5280
|
+
registerCommandDefinition("set-local-storage", []);
|
|
5281
|
+
registerCommandDefinition("clear-local-storage", []);
|
|
5282
|
+
registerCommandDefinition("set-viewport", []);
|
|
5283
|
+
registerCommandDefinition("frames", []);
|
|
5284
|
+
registerCommandDefinition("frame", ["selector"]);
|
|
5285
|
+
registerCommandDefinition("actions", ["url"]);
|
|
5286
|
+
registerCommandDefinition("find", ["strategy", "value", "operation"]);
|
|
5287
|
+
registerCommandDefinition("addinitscript", ["script"]);
|
|
5288
|
+
registerCommandDefinition("tab", ["subcommand"]);
|
|
4791
5289
|
|
|
4792
5290
|
// src/commands/addinitscript.ts
|
|
4793
|
-
var InitScriptParams =
|
|
4794
|
-
script:
|
|
4795
|
-
file:
|
|
4796
|
-
stdin:
|
|
4797
|
-
name:
|
|
4798
|
-
list:
|
|
4799
|
-
remove:
|
|
4800
|
-
base64:
|
|
5291
|
+
var InitScriptParams = z24.object({
|
|
5292
|
+
script: z24.string().optional(),
|
|
5293
|
+
file: z24.string().optional(),
|
|
5294
|
+
stdin: z24.boolean().optional(),
|
|
5295
|
+
name: z24.string().optional(),
|
|
5296
|
+
list: z24.boolean().optional(),
|
|
5297
|
+
remove: z24.string().optional(),
|
|
5298
|
+
base64: z24.string().optional()
|
|
4801
5299
|
});
|
|
4802
5300
|
var registeredScripts = /* @__PURE__ */ new Map();
|
|
4803
5301
|
function resolveScriptContent(params) {
|
|
@@ -4817,12 +5315,12 @@ function resolveScriptContent(params) {
|
|
|
4817
5315
|
}
|
|
4818
5316
|
async function readStdin() {
|
|
4819
5317
|
const { createReadStream } = await import("fs");
|
|
4820
|
-
const { createInterface
|
|
4821
|
-
return new Promise((
|
|
5318
|
+
const { createInterface } = await import("readline");
|
|
5319
|
+
return new Promise((resolve10, reject) => {
|
|
4822
5320
|
const lines = [];
|
|
4823
|
-
const rl =
|
|
5321
|
+
const rl = createInterface({ input: createReadStream("/dev/stdin") });
|
|
4824
5322
|
rl.on("line", (line) => lines.push(line));
|
|
4825
|
-
rl.on("close", () =>
|
|
5323
|
+
rl.on("close", () => resolve10(lines.join("\n")));
|
|
4826
5324
|
rl.on("error", reject);
|
|
4827
5325
|
});
|
|
4828
5326
|
}
|
|
@@ -4831,6 +5329,15 @@ var addInitScriptCommand = registerCommand({
|
|
|
4831
5329
|
description: "Add an initialization script that runs on every page load",
|
|
4832
5330
|
scope: "page",
|
|
4833
5331
|
parameters: InitScriptParams,
|
|
5332
|
+
result: z24.object({
|
|
5333
|
+
scripts: z24.array(z24.object({ name: z24.string(), size: z24.number(), preview: z24.string() })).optional(),
|
|
5334
|
+
removed: z24.string().optional(),
|
|
5335
|
+
existed: z24.boolean().optional(),
|
|
5336
|
+
error: z24.string().optional(),
|
|
5337
|
+
registered: z24.string().optional(),
|
|
5338
|
+
hint: z24.string().optional(),
|
|
5339
|
+
executedImmediately: z24.boolean().optional()
|
|
5340
|
+
}).passthrough(),
|
|
4834
5341
|
handler: async (params, ctx) => {
|
|
4835
5342
|
if (params.list) {
|
|
4836
5343
|
const scripts = Array.from(registeredScripts.entries()).map(([n, content2]) => ({
|
|
@@ -4838,15 +5345,15 @@ var addInitScriptCommand = registerCommand({
|
|
|
4838
5345
|
size: content2.length,
|
|
4839
5346
|
preview: content2.slice(0, 80)
|
|
4840
5347
|
}));
|
|
4841
|
-
return
|
|
5348
|
+
return ok23({ scripts });
|
|
4842
5349
|
}
|
|
4843
5350
|
if (params.remove) {
|
|
4844
5351
|
const existed = registeredScripts.delete(params.remove);
|
|
4845
|
-
return
|
|
5352
|
+
return ok23({ removed: params.remove, existed });
|
|
4846
5353
|
}
|
|
4847
5354
|
let content = params.stdin ? await readStdin() : resolveScriptContent(params);
|
|
4848
5355
|
if (!content) {
|
|
4849
|
-
return
|
|
5356
|
+
return ok23({ error: "No script content provided. Use --script, --file, --stdin, or --base64" });
|
|
4850
5357
|
}
|
|
4851
5358
|
const scriptName = params.name ?? `script-${Date.now()}`;
|
|
4852
5359
|
registeredScripts.set(scriptName, content);
|
|
@@ -4854,74 +5361,134 @@ var addInitScriptCommand = registerCommand({
|
|
|
4854
5361
|
try {
|
|
4855
5362
|
await ctx.page.evaluate(content);
|
|
4856
5363
|
} catch {
|
|
4857
|
-
return
|
|
5364
|
+
return ok23({
|
|
4858
5365
|
registered: scriptName,
|
|
4859
5366
|
hint: "Script registered for future page loads; immediate execution skipped (page may not be ready)"
|
|
4860
5367
|
});
|
|
4861
5368
|
}
|
|
4862
|
-
return
|
|
5369
|
+
return ok23({ registered: scriptName, executedImmediately: true });
|
|
4863
5370
|
}
|
|
4864
5371
|
});
|
|
4865
5372
|
registerCommandDefinition("addinitscript", ["script"]);
|
|
4866
5373
|
|
|
4867
5374
|
// src/commands/find.ts
|
|
4868
|
-
import { z as
|
|
4869
|
-
import { ok as
|
|
5375
|
+
import { z as z25 } from "zod";
|
|
5376
|
+
import { ok as ok24, fail as fail6, normalizeTips as normalizeTips5 } from "@dyyz1993/xcli-core";
|
|
5377
|
+
var actionSchema2 = z25.enum(["click", "fill", "type", "select", "hover", "check"]);
|
|
4870
5378
|
var findCommand = registerCommand({
|
|
4871
5379
|
name: "find",
|
|
4872
5380
|
description: "Find elements by semantic strategy (text/role/label/placeholder/testid) and optionally perform an action",
|
|
4873
5381
|
scope: "page",
|
|
4874
|
-
parameters:
|
|
4875
|
-
strategy:
|
|
4876
|
-
value:
|
|
4877
|
-
name:
|
|
4878
|
-
exact:
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
5382
|
+
parameters: z25.object({
|
|
5383
|
+
strategy: z25.enum(["text", "role", "label", "placeholder", "testid", "alt", "title", "first", "last", "nth"]),
|
|
5384
|
+
value: z25.string(),
|
|
5385
|
+
name: z25.string().optional(),
|
|
5386
|
+
exact: z25.boolean().optional().default(false),
|
|
5387
|
+
operation: z25.string().optional().describe('Trailing operation syntax, e.g. click, fill "text", type "text"'),
|
|
5388
|
+
action: actionSchema2.optional().describe("Action to perform when not using trailing operation syntax"),
|
|
5389
|
+
actionValue: z25.string().optional().describe("Value for fill/type/select when using action"),
|
|
5390
|
+
index: z25.number().int().optional().describe("Index for nth strategy"),
|
|
5391
|
+
click: z25.boolean().optional().default(false),
|
|
5392
|
+
fill: z25.string().optional(),
|
|
5393
|
+
type: z25.string().optional(),
|
|
5394
|
+
select: z25.string().optional(),
|
|
5395
|
+
hover: z25.boolean().optional().default(false),
|
|
5396
|
+
check: z25.boolean().optional().default(false),
|
|
5397
|
+
timeout: z25.number().optional().default(1e4)
|
|
4884
5398
|
}),
|
|
4885
|
-
result:
|
|
4886
|
-
matched:
|
|
4887
|
-
selector:
|
|
4888
|
-
action:
|
|
5399
|
+
result: z25.object({
|
|
5400
|
+
matched: z25.number(),
|
|
5401
|
+
selector: z25.string(),
|
|
5402
|
+
action: z25.string().optional()
|
|
4889
5403
|
}),
|
|
4890
5404
|
handler: async (p, ctx) => {
|
|
4891
5405
|
const page = ctx.page;
|
|
4892
|
-
const
|
|
5406
|
+
const normalized = normalizeFindParams({ ...p });
|
|
5407
|
+
const parsedOperation = parseOperation(normalized.operation);
|
|
5408
|
+
const actionName = parsedOperation.action || p.action || inferLegacyAction(p);
|
|
5409
|
+
const actionValue = parsedOperation.value ?? p.actionValue ?? p.fill ?? p.type ?? p.select;
|
|
5410
|
+
const locator = buildLocator(page, normalized.strategy, normalized.value, {
|
|
4893
5411
|
name: p.name,
|
|
4894
|
-
exact: p.exact
|
|
5412
|
+
exact: p.exact,
|
|
5413
|
+
index: normalized.index
|
|
4895
5414
|
});
|
|
4896
5415
|
const count = await locator.count();
|
|
4897
5416
|
if (count === 0) {
|
|
4898
5417
|
return fail6(`No element found with ${p.strategy}="${p.value}"`);
|
|
4899
5418
|
}
|
|
4900
5419
|
const tips = [];
|
|
4901
|
-
const target = locator.
|
|
5420
|
+
const target = selectTarget(locator, p.strategy);
|
|
4902
5421
|
if (count > 1) {
|
|
4903
|
-
tips.push(`\u26A0\uFE0F Matched ${count} elements,
|
|
5422
|
+
tips.push(`\u26A0\uFE0F Matched ${count} elements, used first match. Use 'find nth <index> ${normalized.strategy} "${normalized.value}" ${actionName || "click"}' for a specific match.`);
|
|
4904
5423
|
}
|
|
4905
|
-
const selector = describeSelector(
|
|
4906
|
-
|
|
4907
|
-
if (p.click) {
|
|
5424
|
+
const selector = describeSelector(normalized.strategy, normalized.value, p.name);
|
|
5425
|
+
if (actionName === "click") {
|
|
4908
5426
|
await target.click({ timeout: p.timeout, force: true });
|
|
4909
|
-
action
|
|
4910
|
-
} else if (
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
5427
|
+
return okWithTips({ matched: count, selector, action: "click" }, tips);
|
|
5428
|
+
} else if (actionName === "fill") {
|
|
5429
|
+
if (actionValue === void 0) return fail6("find fill requires a value");
|
|
5430
|
+
await target.fill(actionValue, { timeout: p.timeout, force: true });
|
|
5431
|
+
return okWithTips({ matched: count, selector, action: `fill("${actionValue}")` }, tips);
|
|
5432
|
+
} else if (actionName === "type") {
|
|
5433
|
+
if (actionValue === void 0) return fail6("find type requires a value");
|
|
5434
|
+
await target.type(actionValue, { delay: 10, timeout: p.timeout });
|
|
5435
|
+
return okWithTips({ matched: count, selector, action: `type("${actionValue}")` }, tips);
|
|
5436
|
+
} else if (actionName === "select") {
|
|
5437
|
+
if (actionValue === void 0) return fail6("find select requires a value");
|
|
5438
|
+
await target.selectOption(actionValue);
|
|
5439
|
+
return okWithTips({ matched: count, selector, action: `select("${actionValue}")` }, tips);
|
|
5440
|
+
} else if (actionName === "hover") {
|
|
5441
|
+
await target.hover({ timeout: p.timeout, force: true });
|
|
5442
|
+
return okWithTips({ matched: count, selector, action: "hover" }, tips);
|
|
5443
|
+
} else if (actionName === "check") {
|
|
5444
|
+
await target.check({ timeout: p.timeout });
|
|
5445
|
+
return okWithTips({ matched: count, selector, action: "check" }, tips);
|
|
5446
|
+
}
|
|
5447
|
+
return okWithTips({ matched: count, selector }, tips);
|
|
4923
5448
|
}
|
|
4924
5449
|
});
|
|
5450
|
+
function okWithTips(data, tips) {
|
|
5451
|
+
const result = ok24(data);
|
|
5452
|
+
if (tips.length > 0) result.tips = normalizeTips5(tips);
|
|
5453
|
+
return result;
|
|
5454
|
+
}
|
|
5455
|
+
function parseOperation(operation) {
|
|
5456
|
+
if (!operation) return {};
|
|
5457
|
+
const match = operation.trim().match(/^(\S+)(?:\s+([\s\S]+))?$/);
|
|
5458
|
+
if (!match) return {};
|
|
5459
|
+
const maybeAction = match[1];
|
|
5460
|
+
const parsed = actionSchema2.safeParse(maybeAction);
|
|
5461
|
+
if (!parsed.success) return {};
|
|
5462
|
+
const rawValue = match[2];
|
|
5463
|
+
const value = rawValue?.replace(/^["']|["']$/g, "");
|
|
5464
|
+
return { action: parsed.data, ...value !== void 0 ? { value } : {} };
|
|
5465
|
+
}
|
|
5466
|
+
function normalizeFindParams(p) {
|
|
5467
|
+
if (p.strategy !== "nth") return { strategy: p.strategy, value: p.value, operation: p.operation, index: p.index };
|
|
5468
|
+
const parsedIndex = Number(p.value);
|
|
5469
|
+
if (!Number.isInteger(parsedIndex) || !p.operation) {
|
|
5470
|
+
return { strategy: p.strategy, value: p.value, operation: p.operation, index: p.index };
|
|
5471
|
+
}
|
|
5472
|
+
const match = p.operation.trim().match(/^(\S+)(?:\s+([\s\S]+))?$/);
|
|
5473
|
+
if (!match) {
|
|
5474
|
+
return { strategy: p.strategy, value: p.value, operation: p.operation, index: parsedIndex };
|
|
5475
|
+
}
|
|
5476
|
+
return {
|
|
5477
|
+
strategy: p.strategy,
|
|
5478
|
+
value: match[1].replace(/^["']|["']$/g, ""),
|
|
5479
|
+
...match[2] ? { operation: match[2] } : {},
|
|
5480
|
+
index: parsedIndex
|
|
5481
|
+
};
|
|
5482
|
+
}
|
|
5483
|
+
function inferLegacyAction(p) {
|
|
5484
|
+
if (p.click) return "click";
|
|
5485
|
+
if (p.fill !== void 0) return "fill";
|
|
5486
|
+
if (p.type !== void 0) return "type";
|
|
5487
|
+
if (p.select !== void 0) return "select";
|
|
5488
|
+
if (p.hover) return "hover";
|
|
5489
|
+
if (p.check) return "check";
|
|
5490
|
+
return void 0;
|
|
5491
|
+
}
|
|
4925
5492
|
function buildLocator(page, strategy, value, opts) {
|
|
4926
5493
|
switch (strategy) {
|
|
4927
5494
|
case "text":
|
|
@@ -4934,10 +5501,24 @@ function buildLocator(page, strategy, value, opts) {
|
|
|
4934
5501
|
return page.getByPlaceholder(value, { exact: opts.exact });
|
|
4935
5502
|
case "testid":
|
|
4936
5503
|
return page.getByTestId(value);
|
|
5504
|
+
case "alt":
|
|
5505
|
+
return page.getByAltText(value, { exact: opts.exact });
|
|
5506
|
+
case "title":
|
|
5507
|
+
return page.getByTitle(value, { exact: opts.exact });
|
|
5508
|
+
case "first":
|
|
5509
|
+
return page.locator(value).first();
|
|
5510
|
+
case "last":
|
|
5511
|
+
return page.locator(value).last();
|
|
5512
|
+
case "nth":
|
|
5513
|
+
return page.locator(value).nth(opts.index ?? 0);
|
|
4937
5514
|
default:
|
|
4938
5515
|
return page.getByText(value, { exact: opts.exact });
|
|
4939
5516
|
}
|
|
4940
5517
|
}
|
|
5518
|
+
function selectTarget(locator, strategy) {
|
|
5519
|
+
if (strategy === "first" || strategy === "last" || strategy === "nth") return locator;
|
|
5520
|
+
return locator.first();
|
|
5521
|
+
}
|
|
4941
5522
|
function describeSelector(strategy, value, name) {
|
|
4942
5523
|
switch (strategy) {
|
|
4943
5524
|
case "role":
|
|
@@ -4950,6 +5531,16 @@ function describeSelector(strategy, value, name) {
|
|
|
4950
5531
|
return `getByPlaceholder("${value}")`;
|
|
4951
5532
|
case "testid":
|
|
4952
5533
|
return `getByTestId("${value}")`;
|
|
5534
|
+
case "alt":
|
|
5535
|
+
return `getByAltText("${value}")`;
|
|
5536
|
+
case "title":
|
|
5537
|
+
return `getByTitle("${value}")`;
|
|
5538
|
+
case "first":
|
|
5539
|
+
return `first("${value}")`;
|
|
5540
|
+
case "last":
|
|
5541
|
+
return `last("${value}")`;
|
|
5542
|
+
case "nth":
|
|
5543
|
+
return `nth("${value}")`;
|
|
4953
5544
|
default:
|
|
4954
5545
|
return `${strategy}("${value}")`;
|
|
4955
5546
|
}
|
|
@@ -5072,7 +5663,7 @@ async function detectCaptcha2(page) {
|
|
|
5072
5663
|
}
|
|
5073
5664
|
async function detectWarningText(page) {
|
|
5074
5665
|
try {
|
|
5075
|
-
const pageText = await page.textContent("body"
|
|
5666
|
+
const pageText = await page.textContent("body").catch(() => "") || "";
|
|
5076
5667
|
const lowerText = pageText.toLowerCase();
|
|
5077
5668
|
for (const { text, severity } of WARNING_TEXTS) {
|
|
5078
5669
|
if (lowerText.includes(text.toLowerCase())) {
|
|
@@ -5113,16 +5704,11 @@ async function detectWebdriverExposure(page) {
|
|
|
5113
5704
|
try {
|
|
5114
5705
|
const webdriver = await page.evaluate(() => {
|
|
5115
5706
|
return {
|
|
5116
|
-
|
|
5117
|
-
webdriver: window.navigator?.webdriver,
|
|
5118
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5707
|
+
webdriver: window.navigator && window.navigator instanceof Object ? window.navigator.webdriver : void 0,
|
|
5119
5708
|
webdriverScriptFn: !!window.__webdriver_script_fn,
|
|
5120
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5121
5709
|
webdriverEvaluate: !!window.__webdriver_evaluate,
|
|
5122
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5123
5710
|
chrome: !!window.chrome,
|
|
5124
|
-
|
|
5125
|
-
permissions: window.navigator?.permissions
|
|
5711
|
+
permissions: window.navigator && window.navigator instanceof Object ? window.navigator.permissions : void 0
|
|
5126
5712
|
};
|
|
5127
5713
|
}).catch(() => null);
|
|
5128
5714
|
if (!webdriver) {
|
|
@@ -5202,380 +5788,26 @@ function formatDetectionMessage(result) {
|
|
|
5202
5788
|
Action: ${action}`;
|
|
5203
5789
|
}
|
|
5204
5790
|
|
|
5205
|
-
// src/
|
|
5206
|
-
import {
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
import { resolve as
|
|
5791
|
+
// src/plugin/loader.ts
|
|
5792
|
+
import {
|
|
5793
|
+
Core
|
|
5794
|
+
} from "@dyyz1993/xcli-core";
|
|
5795
|
+
import { resolve as resolve2 } from "path";
|
|
5796
|
+
import { existsSync as existsSync4, readdirSync } from "fs";
|
|
5797
|
+
import { homedir as homedir4 } from "os";
|
|
5210
5798
|
|
|
5211
|
-
// src/
|
|
5212
|
-
import {
|
|
5799
|
+
// src/plugin/metadata-parser.ts
|
|
5800
|
+
import { existsSync as existsSync2 } from "fs";
|
|
5213
5801
|
import { resolve } from "path";
|
|
5214
|
-
import { execSync } from "child_process";
|
|
5215
|
-
var DEVTO_NEW_URL = "https://dev.to/new";
|
|
5216
|
-
function ab(config) {
|
|
5217
|
-
const parts = ["agent-browser"];
|
|
5218
|
-
if (config.cdpEndpoint) parts.push("--cdp", config.cdpEndpoint);
|
|
5219
|
-
if (config.session) parts.push("--session", config.session);
|
|
5220
|
-
return parts.join(" ");
|
|
5221
|
-
}
|
|
5222
|
-
function extractTitleFromMarkdown(content) {
|
|
5223
|
-
const match = content.match(/^#\s+(.+)$/m);
|
|
5224
|
-
if (match) return match[1].trim();
|
|
5225
|
-
const lines = content.split("\n").filter((l) => l.trim().length > 0);
|
|
5226
|
-
return lines[0] ? lines[0].replace(/^#+\s*/, "").trim() : "Untitled";
|
|
5227
|
-
}
|
|
5228
|
-
function stripTitleFromMarkdown(content) {
|
|
5229
|
-
return content.replace(/^#\s+.+$\n?/m, "").trim();
|
|
5230
|
-
}
|
|
5231
|
-
async function publishToDevto(config) {
|
|
5232
|
-
const cli = ab(config);
|
|
5233
|
-
try {
|
|
5234
|
-
const filePath = resolve(config.file);
|
|
5235
|
-
const raw = readFileSync3(filePath, "utf-8");
|
|
5236
|
-
const title = config.title ?? extractTitleFromMarkdown(raw);
|
|
5237
|
-
const body = stripTitleFromMarkdown(raw);
|
|
5238
|
-
execSync(`${cli} open ${DEVTO_NEW_URL}`, { encoding: "utf-8", timeout: 3e4 });
|
|
5239
|
-
const snapshot = execSync(`${cli} snapshot -i -s body`, { encoding: "utf-8", timeout: 15e3 });
|
|
5240
|
-
if (snapshot.includes("Log in") && !snapshot.includes("Notifications")) {
|
|
5241
|
-
const viewer = execSync(`${cli} viewer --json`, { encoding: "utf-8", timeout: 1e4 }).trim();
|
|
5242
|
-
return {
|
|
5243
|
-
success: false,
|
|
5244
|
-
error: `Not logged in to Dev.to. Please log in via viewer: ${viewer}`,
|
|
5245
|
-
platform: "devto"
|
|
5246
|
-
};
|
|
5247
|
-
}
|
|
5248
|
-
execSync(`${cli} fill @e_title ${JSON.stringify(title)}`, { encoding: "utf-8", timeout: 1e4 });
|
|
5249
|
-
const escapedBody = JSON.stringify(body);
|
|
5250
|
-
execSync(`${cli} fill @e_content ${escapedBody}`, { encoding: "utf-8", timeout: 15e3 });
|
|
5251
|
-
if (config.tags) {
|
|
5252
|
-
const tags = config.tags.split(",").map((t) => t.trim()).slice(0, 4).join(", ");
|
|
5253
|
-
execSync(`${cli} find text "tags" click`, { encoding: "utf-8", timeout: 1e4 });
|
|
5254
|
-
execSync(`${cli} type ${JSON.stringify(tags)}`, { encoding: "utf-8", timeout: 1e4 });
|
|
5255
|
-
}
|
|
5256
|
-
execSync(`${cli} find text "Publish" click`, { encoding: "utf-8", timeout: 15e3 });
|
|
5257
|
-
const postUrl = execSync(`${cli} get url`, { encoding: "utf-8", timeout: 15e3 }).trim();
|
|
5258
|
-
if (postUrl && postUrl !== DEVTO_NEW_URL && postUrl.includes("dev.to")) {
|
|
5259
|
-
return { success: true, url: postUrl, platform: "devto" };
|
|
5260
|
-
}
|
|
5261
|
-
return { success: true, url: postUrl || void 0, platform: "devto" };
|
|
5262
|
-
} catch (err) {
|
|
5263
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
5264
|
-
return { success: false, error: message, platform: "devto" };
|
|
5265
|
-
}
|
|
5266
|
-
}
|
|
5267
5802
|
|
|
5268
|
-
// src/
|
|
5269
|
-
import { readFileSync as
|
|
5270
|
-
|
|
5271
|
-
import { execSync as execSync2 } from "child_process";
|
|
5272
|
-
var MEDIUM_NEW_URL = "https://medium.com/new-story";
|
|
5273
|
-
function ab2(config) {
|
|
5274
|
-
const parts = ["agent-browser"];
|
|
5275
|
-
if (config.cdpEndpoint) parts.push("--cdp", config.cdpEndpoint);
|
|
5276
|
-
if (config.session) parts.push("--session", config.session);
|
|
5277
|
-
return parts.join(" ");
|
|
5278
|
-
}
|
|
5279
|
-
function extractTitleFromMarkdown2(content) {
|
|
5280
|
-
const match = content.match(/^#\s+(.+)$/m);
|
|
5281
|
-
if (match) return match[1].trim();
|
|
5282
|
-
const lines = content.split("\n").filter((l) => l.trim().length > 0);
|
|
5283
|
-
return lines[0] ? lines[0].replace(/^#+\s*/, "").trim() : "Untitled";
|
|
5284
|
-
}
|
|
5285
|
-
function markdownToPlainText(content) {
|
|
5286
|
-
return content.replace(/^#+\s+.+$/gm, "").replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/`(.+?)`/g, "$1").replace(/\[(.+?)\]\(.+?\)/g, "$1").replace(/!\[.*?\]\(.+?\)/g, "").trim();
|
|
5287
|
-
}
|
|
5288
|
-
async function publishToMedium(config) {
|
|
5289
|
-
const cli = ab2(config);
|
|
5803
|
+
// src/utils/json-file.ts
|
|
5804
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
5805
|
+
function readJsonFile(filePath, defaultValue) {
|
|
5290
5806
|
try {
|
|
5291
|
-
const
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
execSync2(`${cli} open ${MEDIUM_NEW_URL}`, { encoding: "utf-8", timeout: 3e4 });
|
|
5296
|
-
const snapshot = execSync2(`${cli} snapshot -i -s body`, { encoding: "utf-8", timeout: 15e3 });
|
|
5297
|
-
if (snapshot.includes("Sign in") && !snapshot.includes("Write")) {
|
|
5298
|
-
const viewer = execSync2(`${cli} viewer --json`, { encoding: "utf-8", timeout: 1e4 }).trim();
|
|
5299
|
-
return {
|
|
5300
|
-
success: false,
|
|
5301
|
-
error: `Not logged in to Medium. Please log in via viewer: ${viewer}`,
|
|
5302
|
-
platform: "medium"
|
|
5303
|
-
};
|
|
5304
|
-
}
|
|
5305
|
-
execSync2(`${cli} click @e_title`, { encoding: "utf-8", timeout: 1e4 });
|
|
5306
|
-
execSync2(`${cli} type ${JSON.stringify(title)}`, { encoding: "utf-8", timeout: 1e4 });
|
|
5307
|
-
execSync2(`${cli} click @e_content`, { encoding: "utf-8", timeout: 1e4 });
|
|
5308
|
-
const lines = body.split("\n");
|
|
5309
|
-
for (const line of lines) {
|
|
5310
|
-
execSync2(`${cli} type ${JSON.stringify(line)}`, { encoding: "utf-8", timeout: 1e4 });
|
|
5311
|
-
execSync2(`${cli} keyboard Enter`, { encoding: "utf-8", timeout: 5e3 });
|
|
5312
|
-
}
|
|
5313
|
-
if (config.tags) {
|
|
5314
|
-
execSync2(`${cli} find text "Tags" click`, { encoding: "utf-8", timeout: 1e4 });
|
|
5315
|
-
const tags = config.tags.split(",").map((t) => t.trim()).join(", ");
|
|
5316
|
-
execSync2(`${cli} type ${JSON.stringify(tags)}`, { encoding: "utf-8", timeout: 1e4 });
|
|
5317
|
-
}
|
|
5318
|
-
execSync2(`${cli} find text "Publish" click`, { encoding: "utf-8", timeout: 15e3 });
|
|
5319
|
-
const postUrl = execSync2(`${cli} get url`, { encoding: "utf-8", timeout: 15e3 }).trim();
|
|
5320
|
-
return { success: true, url: postUrl || void 0, platform: "medium" };
|
|
5321
|
-
} catch (err) {
|
|
5322
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
5323
|
-
return { success: false, error: message, platform: "medium" };
|
|
5324
|
-
}
|
|
5325
|
-
}
|
|
5326
|
-
|
|
5327
|
-
// src/promo/csdn.ts
|
|
5328
|
-
import { readFileSync as readFileSync5 } from "fs";
|
|
5329
|
-
import { resolve as resolve3 } from "path";
|
|
5330
|
-
import { execSync as execSync3 } from "child_process";
|
|
5331
|
-
var CSDN_EDITOR_URL = "https://mp.csdn.net/mp_blog/creation/editor";
|
|
5332
|
-
function ab3(config) {
|
|
5333
|
-
const parts = ["agent-browser"];
|
|
5334
|
-
if (config.cdpEndpoint) parts.push("--cdp", config.cdpEndpoint);
|
|
5335
|
-
if (config.session) parts.push("--session", config.session);
|
|
5336
|
-
return parts.join(" ");
|
|
5337
|
-
}
|
|
5338
|
-
function extractTitleFromMarkdown3(content) {
|
|
5339
|
-
const match = content.match(/^#\s+(.+)$/m);
|
|
5340
|
-
if (match) return match[1].trim();
|
|
5341
|
-
const lines = content.split("\n").filter((l) => l.trim().length > 0);
|
|
5342
|
-
return lines[0] ? lines[0].replace(/^#+\s*/, "").trim() : "Untitled";
|
|
5343
|
-
}
|
|
5344
|
-
async function publishToCsdn(config) {
|
|
5345
|
-
const cli = ab3(config);
|
|
5346
|
-
try {
|
|
5347
|
-
const filePath = resolve3(config.file);
|
|
5348
|
-
const raw = readFileSync5(filePath, "utf-8");
|
|
5349
|
-
const title = config.title ?? extractTitleFromMarkdown3(raw);
|
|
5350
|
-
const body = raw.replace(/^#\s+.+$\n?/m, "").trim();
|
|
5351
|
-
execSync3(`${cli} open ${CSDN_EDITOR_URL}`, { encoding: "utf-8", timeout: 3e4 });
|
|
5352
|
-
const snapshot = execSync3(`${cli} snapshot -i -s body`, { encoding: "utf-8", timeout: 15e3 });
|
|
5353
|
-
if (snapshot.includes("\u767B\u5F55") && !snapshot.includes("\u5199\u6587\u7AE0")) {
|
|
5354
|
-
const viewer = execSync3(`${cli} viewer --json`, { encoding: "utf-8", timeout: 1e4 }).trim();
|
|
5355
|
-
return {
|
|
5356
|
-
success: false,
|
|
5357
|
-
error: `Not logged in to CSDN. Please log in via viewer: ${viewer}`,
|
|
5358
|
-
platform: "csdn"
|
|
5359
|
-
};
|
|
5360
|
-
}
|
|
5361
|
-
execSync3(`${cli} fill @e_title ${JSON.stringify(title)}`, { encoding: "utf-8", timeout: 1e4 });
|
|
5362
|
-
const escapedBody = JSON.stringify(body);
|
|
5363
|
-
execSync3(`${cli} fill @e_content ${escapedBody}`, { encoding: "utf-8", timeout: 15e3 });
|
|
5364
|
-
if (config.tags) {
|
|
5365
|
-
const tags = config.tags.split(",").map((t) => t.trim());
|
|
5366
|
-
for (const tag of tags) {
|
|
5367
|
-
execSync3(`${cli} find text "\u6DFB\u52A0\u6807\u7B7E" click`, { encoding: "utf-8", timeout: 1e4 });
|
|
5368
|
-
execSync3(`${cli} type ${JSON.stringify(tag)}`, { encoding: "utf-8", timeout: 1e4 });
|
|
5369
|
-
execSync3(`${cli} keyboard Enter`, { encoding: "utf-8", timeout: 5e3 });
|
|
5370
|
-
}
|
|
5371
|
-
}
|
|
5372
|
-
execSync3(`${cli} find text "\u53D1\u5E03" click`, { encoding: "utf-8", timeout: 15e3 });
|
|
5373
|
-
const postUrl = execSync3(`${cli} get url`, { encoding: "utf-8", timeout: 15e3 }).trim();
|
|
5374
|
-
return { success: true, url: postUrl || void 0, platform: "csdn" };
|
|
5375
|
-
} catch (err) {
|
|
5376
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
5377
|
-
return { success: false, error: message, platform: "csdn" };
|
|
5378
|
-
}
|
|
5379
|
-
}
|
|
5380
|
-
|
|
5381
|
-
// src/promo/juejin.ts
|
|
5382
|
-
import { readFileSync as readFileSync6 } from "fs";
|
|
5383
|
-
import { resolve as resolve4 } from "path";
|
|
5384
|
-
import { execSync as execSync4 } from "child_process";
|
|
5385
|
-
var JUEJIN_EDITOR_URL = "https://juejin.cn/editor/draft/new";
|
|
5386
|
-
function ab4(config) {
|
|
5387
|
-
const parts = ["agent-browser"];
|
|
5388
|
-
if (config.cdpEndpoint) parts.push("--cdp", config.cdpEndpoint);
|
|
5389
|
-
if (config.session) parts.push("--session", config.session);
|
|
5390
|
-
return parts.join(" ");
|
|
5391
|
-
}
|
|
5392
|
-
function extractTitleFromMarkdown4(content) {
|
|
5393
|
-
const match = content.match(/^#\s+(.+)$/m);
|
|
5394
|
-
if (match) return match[1].trim();
|
|
5395
|
-
const lines = content.split("\n").filter((l) => l.trim().length > 0);
|
|
5396
|
-
return lines[0] ? lines[0].replace(/^#+\s*/, "").trim() : "Untitled";
|
|
5397
|
-
}
|
|
5398
|
-
async function publishToJuejin(config) {
|
|
5399
|
-
const cli = ab4(config);
|
|
5400
|
-
try {
|
|
5401
|
-
const filePath = resolve4(config.file);
|
|
5402
|
-
const raw = readFileSync6(filePath, "utf-8");
|
|
5403
|
-
const title = config.title ?? extractTitleFromMarkdown4(raw);
|
|
5404
|
-
const body = raw.replace(/^#\s+.+$\n?/m, "").trim();
|
|
5405
|
-
execSync4(`${cli} open ${JUEJIN_EDITOR_URL}`, { encoding: "utf-8", timeout: 3e4 });
|
|
5406
|
-
const snapshot = execSync4(`${cli} snapshot -i -s body`, { encoding: "utf-8", timeout: 15e3 });
|
|
5407
|
-
if (snapshot.includes("\u767B\u5F55") && snapshot.includes("\u6CE8\u518C") && !snapshot.includes("\u521B\u4F5C\u8005\u4E2D\u5FC3")) {
|
|
5408
|
-
const viewer = execSync4(`${cli} viewer --json`, { encoding: "utf-8", timeout: 1e4 }).trim();
|
|
5409
|
-
return {
|
|
5410
|
-
success: false,
|
|
5411
|
-
error: `Not logged in to Juejin. Please log in via viewer: ${viewer}`,
|
|
5412
|
-
platform: "juejin"
|
|
5413
|
-
};
|
|
5414
|
-
}
|
|
5415
|
-
execSync4(`${cli} fill @e_title ${JSON.stringify(title)}`, { encoding: "utf-8", timeout: 1e4 });
|
|
5416
|
-
const escapedBody = JSON.stringify(body);
|
|
5417
|
-
execSync4(`${cli} fill @e_content ${escapedBody}`, { encoding: "utf-8", timeout: 15e3 });
|
|
5418
|
-
if (config.tags) {
|
|
5419
|
-
const tags = config.tags.split(",").map((t) => t.trim());
|
|
5420
|
-
for (const tag of tags) {
|
|
5421
|
-
execSync4(`${cli} find text "\u6DFB\u52A0\u6807\u7B7E" click`, { encoding: "utf-8", timeout: 1e4 });
|
|
5422
|
-
execSync4(`${cli} type ${JSON.stringify(tag)}`, { encoding: "utf-8", timeout: 1e4 });
|
|
5423
|
-
execSync4(`${cli} keyboard Enter`, { encoding: "utf-8", timeout: 5e3 });
|
|
5424
|
-
}
|
|
5425
|
-
}
|
|
5426
|
-
execSync4(`${cli} find text "\u53D1\u5E03" click`, { encoding: "utf-8", timeout: 15e3 });
|
|
5427
|
-
const postUrl = execSync4(`${cli} get url`, { encoding: "utf-8", timeout: 15e3 }).trim();
|
|
5428
|
-
return { success: true, url: postUrl || void 0, platform: "juejin" };
|
|
5429
|
-
} catch (err) {
|
|
5430
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
5431
|
-
return { success: false, error: message, platform: "juejin" };
|
|
5432
|
-
}
|
|
5433
|
-
}
|
|
5434
|
-
|
|
5435
|
-
// src/promo/quora.ts
|
|
5436
|
-
import { readFileSync as readFileSync7 } from "fs";
|
|
5437
|
-
import { resolve as resolve5 } from "path";
|
|
5438
|
-
import { execSync as execSync5 } from "child_process";
|
|
5439
|
-
function ab5(config) {
|
|
5440
|
-
const parts = ["agent-browser"];
|
|
5441
|
-
if (config.cdpEndpoint) parts.push("--cdp", config.cdpEndpoint);
|
|
5442
|
-
if (config.session) parts.push("--session", config.session);
|
|
5443
|
-
return parts.join(" ");
|
|
5444
|
-
}
|
|
5445
|
-
async function publishToQuora(config) {
|
|
5446
|
-
const cli = ab5(config);
|
|
5447
|
-
try {
|
|
5448
|
-
if (!config.search) {
|
|
5449
|
-
return {
|
|
5450
|
-
success: false,
|
|
5451
|
-
error: "Quora requires --search parameter to find relevant questions",
|
|
5452
|
-
platform: "quora"
|
|
5453
|
-
};
|
|
5454
|
-
}
|
|
5455
|
-
const filePath = resolve5(config.file);
|
|
5456
|
-
const raw = readFileSync7(filePath, "utf-8");
|
|
5457
|
-
const answer = raw.trim();
|
|
5458
|
-
const searchUrl = `https://www.quora.com/search?q=${encodeURIComponent(config.search)}`;
|
|
5459
|
-
execSync5(`${cli} open ${searchUrl}`, { encoding: "utf-8", timeout: 3e4 });
|
|
5460
|
-
const snapshot = execSync5(`${cli} snapshot -i -s body`, { encoding: "utf-8", timeout: 15e3 });
|
|
5461
|
-
if (!snapshot.includes("Add question")) {
|
|
5462
|
-
const viewer = execSync5(`${cli} viewer --json`, { encoding: "utf-8", timeout: 1e4 }).trim();
|
|
5463
|
-
return {
|
|
5464
|
-
success: false,
|
|
5465
|
-
error: `Not logged in to Quora. Please log in via viewer: ${viewer}`,
|
|
5466
|
-
platform: "quora"
|
|
5467
|
-
};
|
|
5468
|
-
}
|
|
5469
|
-
const answerMatch = snapshot.match(/Answer\s*(?:button|link)/i);
|
|
5470
|
-
if (!answerMatch) {
|
|
5471
|
-
return {
|
|
5472
|
-
success: false,
|
|
5473
|
-
error: "No answerable questions found for the given search query. Try a different search term.",
|
|
5474
|
-
platform: "quora"
|
|
5475
|
-
};
|
|
5476
|
-
}
|
|
5477
|
-
execSync5(`${cli} find text "Answer" click`, { encoding: "utf-8", timeout: 1e4 });
|
|
5478
|
-
const escapedAnswer = JSON.stringify(answer);
|
|
5479
|
-
execSync5(`${cli} fill @e_answer ${escapedAnswer}`, { encoding: "utf-8", timeout: 15e3 });
|
|
5480
|
-
execSync5(`${cli} find text "Submit" click`, { encoding: "utf-8", timeout: 15e3 });
|
|
5481
|
-
const postUrl = execSync5(`${cli} get url`, { encoding: "utf-8", timeout: 15e3 }).trim();
|
|
5482
|
-
return { success: true, url: postUrl || void 0, platform: "quora" };
|
|
5483
|
-
} catch (err) {
|
|
5484
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
5485
|
-
return { success: false, error: message, platform: "quora" };
|
|
5486
|
-
}
|
|
5487
|
-
}
|
|
5488
|
-
|
|
5489
|
-
// src/promo/index.ts
|
|
5490
|
-
var PUBLISHERS = {
|
|
5491
|
-
devto: publishToDevto,
|
|
5492
|
-
medium: publishToMedium,
|
|
5493
|
-
csdn: publishToCsdn,
|
|
5494
|
-
juejin: publishToJuejin,
|
|
5495
|
-
quora: publishToQuora
|
|
5496
|
-
};
|
|
5497
|
-
async function dispatchPromo(config) {
|
|
5498
|
-
const publisher = PUBLISHERS[config.platform];
|
|
5499
|
-
if (!publisher) {
|
|
5500
|
-
return {
|
|
5501
|
-
success: false,
|
|
5502
|
-
error: `Unknown platform: ${config.platform}. Supported: ${Object.keys(PUBLISHERS).join(", ")}`,
|
|
5503
|
-
platform: config.platform
|
|
5504
|
-
};
|
|
5505
|
-
}
|
|
5506
|
-
return publisher(config);
|
|
5507
|
-
}
|
|
5508
|
-
|
|
5509
|
-
// src/commands/promo.ts
|
|
5510
|
-
var promoParams = z25.object({
|
|
5511
|
-
platform: z25.enum(["devto", "medium", "csdn", "juejin", "quora"]).describe("Target platform for promotion"),
|
|
5512
|
-
file: z25.string().describe("Path to Markdown file to publish"),
|
|
5513
|
-
tags: z25.string().optional().describe("Comma-separated tags"),
|
|
5514
|
-
title: z25.string().optional().describe("Custom title (default: extracted from file first heading)"),
|
|
5515
|
-
search: z25.string().optional().describe("Quora: search query to find questions"),
|
|
5516
|
-
cdpEndpoint: z25.string().optional().describe("CDP endpoint for agent-browser"),
|
|
5517
|
-
session: z25.string().optional().describe("agent-browser session name")
|
|
5518
|
-
}).refine(
|
|
5519
|
-
(data) => data.platform !== "quora" || !!data.search,
|
|
5520
|
-
{ message: "Quora platform requires --search parameter" }
|
|
5521
|
-
).refine(
|
|
5522
|
-
(data) => existsSync2(resolve6(data.file)),
|
|
5523
|
-
{ message: "File does not exist" }
|
|
5524
|
-
);
|
|
5525
|
-
var promoCommand = registerCommand({
|
|
5526
|
-
name: "promo",
|
|
5527
|
-
description: "Publish promotional articles to various platforms (devto, medium, csdn, juejin, quora)",
|
|
5528
|
-
scope: "project",
|
|
5529
|
-
parameters: promoParams,
|
|
5530
|
-
result: z25.object({
|
|
5531
|
-
success: z25.boolean(),
|
|
5532
|
-
url: z25.string().optional(),
|
|
5533
|
-
error: z25.string().optional(),
|
|
5534
|
-
platform: z25.string()
|
|
5535
|
-
}),
|
|
5536
|
-
handler: async (p, _ctx) => {
|
|
5537
|
-
const filePath = resolve6(p.file);
|
|
5538
|
-
const content = readFileSync8(filePath, "utf-8");
|
|
5539
|
-
if (content.trim().length === 0) {
|
|
5540
|
-
return ok24({
|
|
5541
|
-
success: false,
|
|
5542
|
-
error: `File is empty: ${filePath}`,
|
|
5543
|
-
platform: p.platform
|
|
5544
|
-
});
|
|
5545
|
-
}
|
|
5546
|
-
const result = await dispatchPromo({
|
|
5547
|
-
platform: p.platform,
|
|
5548
|
-
file: filePath,
|
|
5549
|
-
tags: p.tags,
|
|
5550
|
-
title: p.title,
|
|
5551
|
-
search: p.search,
|
|
5552
|
-
cdpEndpoint: p.cdpEndpoint ?? _ctx.cdpEndpoint,
|
|
5553
|
-
session: p.session ?? p.platform
|
|
5554
|
-
});
|
|
5555
|
-
return ok24(result);
|
|
5556
|
-
}
|
|
5557
|
-
});
|
|
5558
|
-
|
|
5559
|
-
// src/plugin/loader.ts
|
|
5560
|
-
import {
|
|
5561
|
-
Core
|
|
5562
|
-
} from "@dyyz1993/xcli-core";
|
|
5563
|
-
import { resolve as resolve8 } from "path";
|
|
5564
|
-
import { existsSync as existsSync5, readdirSync } from "fs";
|
|
5565
|
-
import { homedir as homedir2 } from "os";
|
|
5566
|
-
|
|
5567
|
-
// src/plugin/metadata-parser.ts
|
|
5568
|
-
import { existsSync as existsSync3 } from "fs";
|
|
5569
|
-
import { resolve as resolve7 } from "path";
|
|
5570
|
-
|
|
5571
|
-
// src/utils/json-file.ts
|
|
5572
|
-
import { readFileSync as readFileSync9, writeFileSync as writeFileSync3 } from "fs";
|
|
5573
|
-
function readJsonFile(filePath, defaultValue) {
|
|
5574
|
-
try {
|
|
5575
|
-
const content = readFileSync9(filePath, "utf-8");
|
|
5576
|
-
return JSON.parse(content);
|
|
5577
|
-
} catch {
|
|
5578
|
-
return defaultValue;
|
|
5807
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
5808
|
+
return JSON.parse(content);
|
|
5809
|
+
} catch {
|
|
5810
|
+
return defaultValue;
|
|
5579
5811
|
}
|
|
5580
5812
|
}
|
|
5581
5813
|
|
|
@@ -5583,8 +5815,8 @@ function readJsonFile(filePath, defaultValue) {
|
|
|
5583
5815
|
var PluginMetadataParser = class {
|
|
5584
5816
|
static XBROWSER_KEYWORDS = ["xbrowser", "xbrowser-plugin"];
|
|
5585
5817
|
static parseFromPackageJson(pluginPath) {
|
|
5586
|
-
const packageJsonPath =
|
|
5587
|
-
if (!
|
|
5818
|
+
const packageJsonPath = resolve(pluginPath, "package.json");
|
|
5819
|
+
if (!existsSync2(packageJsonPath)) {
|
|
5588
5820
|
return null;
|
|
5589
5821
|
}
|
|
5590
5822
|
const packageJson = readJsonFile(packageJsonPath, null);
|
|
@@ -5648,20 +5880,20 @@ var PluginMetadataParser = class {
|
|
|
5648
5880
|
};
|
|
5649
5881
|
|
|
5650
5882
|
// src/plugin/ensure-deps.ts
|
|
5651
|
-
import { existsSync as
|
|
5652
|
-
import { join as
|
|
5653
|
-
import { execSync
|
|
5883
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
5884
|
+
import { join as join4 } from "path";
|
|
5885
|
+
import { execSync } from "child_process";
|
|
5654
5886
|
var SHARED_PLUGIN_DEPENDENCIES = {
|
|
5655
5887
|
"zod": "^3.24.0",
|
|
5656
5888
|
"@dyyz1993/xcli-core": "^0.12.1"
|
|
5657
5889
|
};
|
|
5658
5890
|
function ensurePluginDependencies(pluginsDir) {
|
|
5659
|
-
const zodPath =
|
|
5660
|
-
if (
|
|
5661
|
-
|
|
5662
|
-
const pkgPath =
|
|
5891
|
+
const zodPath = join4(pluginsDir, "node_modules", "zod");
|
|
5892
|
+
if (existsSync3(zodPath)) return;
|
|
5893
|
+
mkdirSync4(pluginsDir, { recursive: true });
|
|
5894
|
+
const pkgPath = join4(pluginsDir, "package.json");
|
|
5663
5895
|
let pkg2 = {};
|
|
5664
|
-
if (
|
|
5896
|
+
if (existsSync3(pkgPath)) {
|
|
5665
5897
|
try {
|
|
5666
5898
|
pkg2 = readJsonFile(pkgPath, {});
|
|
5667
5899
|
} catch {
|
|
@@ -5675,13 +5907,13 @@ function ensurePluginDependencies(pluginsDir) {
|
|
|
5675
5907
|
needsInstall = true;
|
|
5676
5908
|
}
|
|
5677
5909
|
}
|
|
5678
|
-
if (!needsInstall &&
|
|
5910
|
+
if (!needsInstall && existsSync3(join4(pluginsDir, "node_modules"))) return;
|
|
5679
5911
|
pkg2.dependencies = existingDeps;
|
|
5680
5912
|
pkg2.private = true;
|
|
5681
5913
|
pkg2.description = pkg2.description || "xbrowser plugins \u2014 shared dependencies";
|
|
5682
|
-
|
|
5914
|
+
writeFileSync5(pkgPath, JSON.stringify(pkg2, null, 2) + "\n", "utf-8");
|
|
5683
5915
|
try {
|
|
5684
|
-
|
|
5916
|
+
execSync("npm install --production --no-package-lock --no-fund --no-audit", {
|
|
5685
5917
|
cwd: pluginsDir,
|
|
5686
5918
|
stdio: "pipe",
|
|
5687
5919
|
timeout: 6e4,
|
|
@@ -5692,6 +5924,165 @@ function ensurePluginDependencies(pluginsDir) {
|
|
|
5692
5924
|
}
|
|
5693
5925
|
}
|
|
5694
5926
|
|
|
5927
|
+
// src/plugin/contract.ts
|
|
5928
|
+
import {
|
|
5929
|
+
unwrapZod,
|
|
5930
|
+
fieldsFromZodObjectReflected,
|
|
5931
|
+
zodTypeToContractType
|
|
5932
|
+
} from "@dyyz1993/xcli-core";
|
|
5933
|
+
function buildPluginContract(site) {
|
|
5934
|
+
const commands = site.getAllCommands().map((command) => buildCommandContract(site.getCommand?.(command.name) || command, {
|
|
5935
|
+
siteRequiresLogin: site.config?.requiresLogin
|
|
5936
|
+
}));
|
|
5937
|
+
return {
|
|
5938
|
+
version: 2,
|
|
5939
|
+
plugin: {
|
|
5940
|
+
name: site.name,
|
|
5941
|
+
url: site.url,
|
|
5942
|
+
description: site.config?.description,
|
|
5943
|
+
requiresLogin: site.config?.requiresLogin
|
|
5944
|
+
},
|
|
5945
|
+
commands
|
|
5946
|
+
};
|
|
5947
|
+
}
|
|
5948
|
+
function buildCommandContract(command, options = {}) {
|
|
5949
|
+
const extension = command.xbrowser || {};
|
|
5950
|
+
const inferredFields = fieldsFromZodObject(command.parameters);
|
|
5951
|
+
const fields = mergeFields(inferredFields, extension.form?.fields || []);
|
|
5952
|
+
const positional = extension.positional || fields.filter((field) => field.positional).map((field) => field.name);
|
|
5953
|
+
const requiresLogin = command.requiresLogin === true || options.siteRequiresLogin === true && command.name !== "login" && command.name !== "logout";
|
|
5954
|
+
const capabilities = extension.capabilities || inferCapabilities(command.scope || "project", requiresLogin);
|
|
5955
|
+
const outputSchema = command.result ? summarizeZod(command.result) : void 0;
|
|
5956
|
+
return {
|
|
5957
|
+
name: command.name,
|
|
5958
|
+
description: command.description || "",
|
|
5959
|
+
scope: command.scope || "project",
|
|
5960
|
+
requiresLogin,
|
|
5961
|
+
category: extension.category,
|
|
5962
|
+
capabilities,
|
|
5963
|
+
positional,
|
|
5964
|
+
form: {
|
|
5965
|
+
title: extension.form?.title || command.description || command.name,
|
|
5966
|
+
description: extension.form?.description,
|
|
5967
|
+
submitLabel: extension.form?.submitLabel || "Run",
|
|
5968
|
+
fields
|
|
5969
|
+
},
|
|
5970
|
+
output: extension.output || (outputSchema ? { schema: outputSchema } : void 0)
|
|
5971
|
+
};
|
|
5972
|
+
}
|
|
5973
|
+
function fieldsFromZodObject(schema) {
|
|
5974
|
+
const reflected = fieldsFromZodObjectReflected(schema);
|
|
5975
|
+
return reflected.map((field) => {
|
|
5976
|
+
const widget = widgetFor(field.type, field.enum);
|
|
5977
|
+
return {
|
|
5978
|
+
name: field.name,
|
|
5979
|
+
label: toLabel(field.name),
|
|
5980
|
+
type: field.type,
|
|
5981
|
+
widget,
|
|
5982
|
+
required: field.required,
|
|
5983
|
+
...field.description ? { description: field.description } : {},
|
|
5984
|
+
...field.default !== void 0 ? { default: field.default } : {},
|
|
5985
|
+
...field.enum ? { enum: field.enum } : {},
|
|
5986
|
+
...field.type === "array" ? { multiple: true } : {}
|
|
5987
|
+
};
|
|
5988
|
+
});
|
|
5989
|
+
}
|
|
5990
|
+
function mergeFields(inferred, overrides) {
|
|
5991
|
+
if (overrides.length === 0) return inferred;
|
|
5992
|
+
const byName = new Map(inferred.map((field) => [field.name, field]));
|
|
5993
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5994
|
+
const merged = [];
|
|
5995
|
+
for (const override of overrides) {
|
|
5996
|
+
if (!override.name) continue;
|
|
5997
|
+
const base = byName.get(override.name) || {
|
|
5998
|
+
name: override.name,
|
|
5999
|
+
label: toLabel(override.name),
|
|
6000
|
+
type: "string",
|
|
6001
|
+
widget: "text",
|
|
6002
|
+
required: false
|
|
6003
|
+
};
|
|
6004
|
+
merged.push({ ...base, ...override, name: override.name });
|
|
6005
|
+
seen.add(override.name);
|
|
6006
|
+
}
|
|
6007
|
+
for (const field of inferred) {
|
|
6008
|
+
if (!seen.has(field.name)) merged.push(field);
|
|
6009
|
+
}
|
|
6010
|
+
return merged;
|
|
6011
|
+
}
|
|
6012
|
+
function inferCapabilities(scope, requiresLogin) {
|
|
6013
|
+
const caps = [];
|
|
6014
|
+
if (scope === "page") caps.push("browser.page");
|
|
6015
|
+
if (scope === "browser") caps.push("browser.context");
|
|
6016
|
+
if (requiresLogin) caps.push("auth.login");
|
|
6017
|
+
return caps;
|
|
6018
|
+
}
|
|
6019
|
+
function widgetFor(type, enumValues) {
|
|
6020
|
+
if (enumValues) return "select";
|
|
6021
|
+
if (type === "boolean") return "checkbox";
|
|
6022
|
+
if (type === "number") return "number";
|
|
6023
|
+
if (type === "array") return "multi-select";
|
|
6024
|
+
if (type === "object") return "json";
|
|
6025
|
+
return "text";
|
|
6026
|
+
}
|
|
6027
|
+
function summarizeZod(schema) {
|
|
6028
|
+
const unwrapped = unwrapZod(schema);
|
|
6029
|
+
if (unwrapped.typeName === "ZodArray") {
|
|
6030
|
+
const def = unwrapped.schema?._def;
|
|
6031
|
+
return {
|
|
6032
|
+
type: "array",
|
|
6033
|
+
items: summarizeZod(def?.type || def?.innerType)
|
|
6034
|
+
};
|
|
6035
|
+
}
|
|
6036
|
+
const shape = getObjectShape(schema);
|
|
6037
|
+
if (!shape) {
|
|
6038
|
+
return {
|
|
6039
|
+
type: zodTypeToContractType(unwrapped.typeName),
|
|
6040
|
+
required: !unwrapped.optional,
|
|
6041
|
+
...unwrapped.description ? { description: unwrapped.description } : {}
|
|
6042
|
+
};
|
|
6043
|
+
}
|
|
6044
|
+
return Object.fromEntries(
|
|
6045
|
+
Object.entries(shape).map(([name, field]) => {
|
|
6046
|
+
const inner = unwrapZod(field);
|
|
6047
|
+
return [name, {
|
|
6048
|
+
type: zodTypeToContractType(inner.typeName),
|
|
6049
|
+
required: !inner.optional,
|
|
6050
|
+
...inner.description ? { description: inner.description } : {}
|
|
6051
|
+
}];
|
|
6052
|
+
})
|
|
6053
|
+
);
|
|
6054
|
+
}
|
|
6055
|
+
function getObjectShape(schema) {
|
|
6056
|
+
const zod = schema;
|
|
6057
|
+
const shapeOrFn = zod?.shape ?? zod?._def?.shape;
|
|
6058
|
+
if (!shapeOrFn) return void 0;
|
|
6059
|
+
return typeof shapeOrFn === "function" ? shapeOrFn() : shapeOrFn;
|
|
6060
|
+
}
|
|
6061
|
+
function toLabel(name) {
|
|
6062
|
+
return name.replace(/([A-Z])/g, " $1").replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim().replace(/^./, (char) => char.toUpperCase());
|
|
6063
|
+
}
|
|
6064
|
+
|
|
6065
|
+
// src/plugin/login-required-patch.ts
|
|
6066
|
+
import { SiteInstanceImpl } from "@dyyz1993/xcli-core";
|
|
6067
|
+
var patched = false;
|
|
6068
|
+
function patchLoginRequired() {
|
|
6069
|
+
if (patched) return;
|
|
6070
|
+
patched = true;
|
|
6071
|
+
const proto = SiteInstanceImpl.prototype;
|
|
6072
|
+
const original = proto.command;
|
|
6073
|
+
proto.command = function(name, cmd) {
|
|
6074
|
+
const result = original.call(this, name, cmd);
|
|
6075
|
+
const loginRequired = cmd.loginRequired;
|
|
6076
|
+
if (loginRequired) {
|
|
6077
|
+
const entry = this.commands.get(name);
|
|
6078
|
+
if (entry) {
|
|
6079
|
+
entry.loginRequired = loginRequired;
|
|
6080
|
+
}
|
|
6081
|
+
}
|
|
6082
|
+
return result;
|
|
6083
|
+
};
|
|
6084
|
+
}
|
|
6085
|
+
|
|
5695
6086
|
// src/plugin/loader.ts
|
|
5696
6087
|
var DEFAULT_PLUGIN_DIRS = [".xcli/plugins", "../.xcli/plugins"];
|
|
5697
6088
|
var XBrowserPluginLoader = class {
|
|
@@ -5699,6 +6090,7 @@ var XBrowserPluginLoader = class {
|
|
|
5699
6090
|
loader;
|
|
5700
6091
|
options;
|
|
5701
6092
|
constructor(options) {
|
|
6093
|
+
patchLoginRequired();
|
|
5702
6094
|
this.options = options ?? {};
|
|
5703
6095
|
const cwd = this.options.cwd || process.cwd();
|
|
5704
6096
|
const coreConfig = {
|
|
@@ -5709,7 +6101,7 @@ var XBrowserPluginLoader = class {
|
|
|
5709
6101
|
envPrefix: "XBROWSER",
|
|
5710
6102
|
pluginDirs: [
|
|
5711
6103
|
...DEFAULT_PLUGIN_DIRS,
|
|
5712
|
-
|
|
6104
|
+
resolve2(cwd, ".xcli/plugins")
|
|
5713
6105
|
]
|
|
5714
6106
|
};
|
|
5715
6107
|
this.core = new Core(coreConfig);
|
|
@@ -5734,6 +6126,13 @@ var XBrowserPluginLoader = class {
|
|
|
5734
6126
|
getLoadedPlugins() {
|
|
5735
6127
|
return this.loader.getLoadedPlugins();
|
|
5736
6128
|
}
|
|
6129
|
+
getPluginContract(siteName, commandName) {
|
|
6130
|
+
const site = this.core.loader.getSite(siteName);
|
|
6131
|
+
if (!site) return void 0;
|
|
6132
|
+
const contract = buildPluginContract(site);
|
|
6133
|
+
if (!commandName) return contract;
|
|
6134
|
+
return contract.commands.find((command) => command.name === commandName);
|
|
6135
|
+
}
|
|
5737
6136
|
async loadPlugin(pluginPath, id) {
|
|
5738
6137
|
return this.loader.loadPlugin(pluginPath, id);
|
|
5739
6138
|
}
|
|
@@ -5748,31 +6147,31 @@ var XBrowserPluginLoader = class {
|
|
|
5748
6147
|
}
|
|
5749
6148
|
async scanAndLoad() {
|
|
5750
6149
|
const cwd = this.options.cwd || process.cwd();
|
|
5751
|
-
const globalDir = this.options.globalDir ||
|
|
6150
|
+
const globalDir = this.options.globalDir || resolve2(homedir4(), ".xbrowser/plugins");
|
|
5752
6151
|
ensurePluginDependencies(globalDir);
|
|
5753
6152
|
const dirs = [
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
this.options.userDir ||
|
|
6153
|
+
resolve2(cwd, ".xcli/plugins"),
|
|
6154
|
+
resolve2(cwd, "../.xcli/plugins"),
|
|
6155
|
+
this.options.userDir || resolve2(homedir4(), ".xcli/plugins"),
|
|
5757
6156
|
globalDir
|
|
5758
6157
|
];
|
|
5759
6158
|
const loaded = [];
|
|
5760
6159
|
const seen = /* @__PURE__ */ new Set();
|
|
5761
6160
|
for (const dir of dirs) {
|
|
5762
|
-
if (!
|
|
6161
|
+
if (!existsSync4(dir)) continue;
|
|
5763
6162
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
5764
6163
|
for (const entry of entries) {
|
|
5765
6164
|
if (!entry.isDirectory()) continue;
|
|
5766
6165
|
if (seen.has(entry.name)) continue;
|
|
5767
6166
|
seen.add(entry.name);
|
|
5768
|
-
const pluginDir =
|
|
5769
|
-
let indexPath =
|
|
5770
|
-
if (!
|
|
5771
|
-
indexPath =
|
|
6167
|
+
const pluginDir = resolve2(dir, entry.name);
|
|
6168
|
+
let indexPath = resolve2(pluginDir, "index.js");
|
|
6169
|
+
if (!existsSync4(indexPath)) {
|
|
6170
|
+
indexPath = resolve2(pluginDir, "index.ts");
|
|
5772
6171
|
}
|
|
5773
|
-
if (!
|
|
6172
|
+
if (!existsSync4(indexPath)) continue;
|
|
5774
6173
|
try {
|
|
5775
|
-
if (!
|
|
6174
|
+
if (!existsSync4(resolve2(pluginDir, "package.json"))) {
|
|
5776
6175
|
console.warn(`\u26A0\uFE0F Plugin "${entry.name}" has no package.json. Use "xbrowser create ${entry.name} --template static" for proper structure.`);
|
|
5777
6176
|
} else {
|
|
5778
6177
|
const metadata = PluginMetadataParser.parseFromPackageJson(pluginDir);
|
|
@@ -5810,6 +6209,110 @@ async function getPluginLoader() {
|
|
|
5810
6209
|
return pluginLoader;
|
|
5811
6210
|
}
|
|
5812
6211
|
|
|
6212
|
+
// src/utils/viewer-url.ts
|
|
6213
|
+
function buildViewerUrl(sessionName = "default") {
|
|
6214
|
+
try {
|
|
6215
|
+
const status = getDaemonProcessStatus();
|
|
6216
|
+
if (!status.running) return void 0;
|
|
6217
|
+
const port = status.port || getDaemonConfig().basePort;
|
|
6218
|
+
return `http://localhost:${port}/preview/${encodeURIComponent(sessionName)}`;
|
|
6219
|
+
} catch {
|
|
6220
|
+
return void 0;
|
|
6221
|
+
}
|
|
6222
|
+
}
|
|
6223
|
+
|
|
6224
|
+
// src/plugin/login-guard.ts
|
|
6225
|
+
async function checkPluginLoginRequired(options) {
|
|
6226
|
+
const { site, command, commandName, ctx, page, sessionName } = options;
|
|
6227
|
+
if (commandName === "login" || commandName === "logout") return { ok: true };
|
|
6228
|
+
const loginConfig = site.config?.loginConfig;
|
|
6229
|
+
const requiresLogin = command.requiresLogin === true || site.config?.requiresLogin === true || loginConfig?.requiresLogin === true;
|
|
6230
|
+
if (!requiresLogin) return { ok: true };
|
|
6231
|
+
const pluginName = site.name || "plugin";
|
|
6232
|
+
if (typeof site.isLoggedIn === "function") {
|
|
6233
|
+
try {
|
|
6234
|
+
const loggedIn = await site.isLoggedIn(ctx);
|
|
6235
|
+
if (loggedIn) return { ok: true };
|
|
6236
|
+
return buildLoginRequired({
|
|
6237
|
+
plugin: pluginName,
|
|
6238
|
+
command: commandName,
|
|
6239
|
+
reason: "plugin isLoggedIn returned false",
|
|
6240
|
+
sessionName,
|
|
6241
|
+
loginConfig
|
|
6242
|
+
});
|
|
6243
|
+
} catch {
|
|
6244
|
+
}
|
|
6245
|
+
}
|
|
6246
|
+
if (page && loginConfig) {
|
|
6247
|
+
const generic = await detectLoginFromPage(page, loginConfig);
|
|
6248
|
+
if (generic === "logged-in") return { ok: true };
|
|
6249
|
+
if (generic === "logged-out") {
|
|
6250
|
+
return buildLoginRequired({
|
|
6251
|
+
plugin: pluginName,
|
|
6252
|
+
command: commandName,
|
|
6253
|
+
reason: "generic loginConfig detected logged-out page",
|
|
6254
|
+
sessionName,
|
|
6255
|
+
loginConfig
|
|
6256
|
+
});
|
|
6257
|
+
}
|
|
6258
|
+
}
|
|
6259
|
+
return { ok: true };
|
|
6260
|
+
}
|
|
6261
|
+
function buildLoginRequired(options) {
|
|
6262
|
+
const viewerUrl = buildViewerUrl(options.sessionName);
|
|
6263
|
+
const loginUrl = options.loginConfig?.loginUrl;
|
|
6264
|
+
const message = options.loginConfig?.loginPrompt || `Plugin "${options.plugin}" requires login before running "${options.command}".`;
|
|
6265
|
+
const tips = [
|
|
6266
|
+
message,
|
|
6267
|
+
...viewerUrl ? [`Open viewer to complete login: ${viewerUrl}`] : [],
|
|
6268
|
+
...loginUrl ? [`Login page: ${loginUrl}`] : [],
|
|
6269
|
+
`After login, retry: xbrowser ${options.plugin} ${options.command} --session ${options.sessionName}`
|
|
6270
|
+
];
|
|
6271
|
+
return {
|
|
6272
|
+
ok: false,
|
|
6273
|
+
data: {
|
|
6274
|
+
code: "LOGIN_REQUIRED",
|
|
6275
|
+
plugin: options.plugin,
|
|
6276
|
+
command: options.command,
|
|
6277
|
+
reason: options.reason,
|
|
6278
|
+
...viewerUrl ? { viewerUrl } : {},
|
|
6279
|
+
...loginUrl ? { loginUrl } : {}
|
|
6280
|
+
},
|
|
6281
|
+
message,
|
|
6282
|
+
tips
|
|
6283
|
+
};
|
|
6284
|
+
}
|
|
6285
|
+
async function detectLoginFromPage(page, config) {
|
|
6286
|
+
const url = page.url();
|
|
6287
|
+
if (config.loginUrls?.some((part) => url.includes(part))) return "logged-out";
|
|
6288
|
+
const result = await page.evaluate((cfg) => {
|
|
6289
|
+
const visible = (selector) => {
|
|
6290
|
+
try {
|
|
6291
|
+
const el = document.querySelector(selector);
|
|
6292
|
+
if (!el) return false;
|
|
6293
|
+
const rect = el.getBoundingClientRect();
|
|
6294
|
+
const style = window.getComputedStyle(el);
|
|
6295
|
+
return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
|
|
6296
|
+
} catch {
|
|
6297
|
+
return false;
|
|
6298
|
+
}
|
|
6299
|
+
};
|
|
6300
|
+
if (cfg.loggedInSelectors?.some(visible)) return "logged-in";
|
|
6301
|
+
if (cfg.loginSelectors?.some(visible)) return "logged-out";
|
|
6302
|
+
const bodyText = document.body?.innerText || "";
|
|
6303
|
+
const keywords = cfg.loginKeywords || [];
|
|
6304
|
+
if (keywords.length > 0 && keywords.every((keyword) => bodyText.includes(keyword))) {
|
|
6305
|
+
return "logged-out";
|
|
6306
|
+
}
|
|
6307
|
+
return "unknown";
|
|
6308
|
+
}, {
|
|
6309
|
+
loginSelectors: config.loginSelectors || [],
|
|
6310
|
+
loginKeywords: config.loginKeywords || [],
|
|
6311
|
+
loggedInSelectors: config.loggedInSelectors || []
|
|
6312
|
+
});
|
|
6313
|
+
return result;
|
|
6314
|
+
}
|
|
6315
|
+
|
|
5813
6316
|
// src/tips/dom-watcher.ts
|
|
5814
6317
|
var DOM_WATCHER_SCRIPT = `
|
|
5815
6318
|
(function() {
|
|
@@ -6189,7 +6692,7 @@ var TipsManager = class {
|
|
|
6189
6692
|
}
|
|
6190
6693
|
}
|
|
6191
6694
|
debounce() {
|
|
6192
|
-
return new Promise((
|
|
6695
|
+
return new Promise((resolve10) => setTimeout(resolve10, DEBOUNCE_MS));
|
|
6193
6696
|
}
|
|
6194
6697
|
formatTips(tips) {
|
|
6195
6698
|
return tips.map((tip) => {
|
|
@@ -6217,33 +6720,105 @@ function getTipsManager() {
|
|
|
6217
6720
|
}
|
|
6218
6721
|
|
|
6219
6722
|
// src/hooks/loader.ts
|
|
6220
|
-
var
|
|
6221
|
-
|
|
6723
|
+
var FAIL_KEYWORDS = [
|
|
6724
|
+
"\u767B\u5F55",
|
|
6725
|
+
"login",
|
|
6726
|
+
"Login",
|
|
6727
|
+
"\u672A\u767B\u5F55",
|
|
6728
|
+
"not logged in",
|
|
6729
|
+
"cdp",
|
|
6730
|
+
"CDP",
|
|
6731
|
+
"\u9A8C\u8BC1\u7801",
|
|
6732
|
+
"\u9A8C\u8BC1",
|
|
6733
|
+
"captcha",
|
|
6734
|
+
"\u9700\u8981\u767B\u5F55",
|
|
6735
|
+
"requires login",
|
|
6736
|
+
"blocked",
|
|
6737
|
+
"403",
|
|
6738
|
+
"404"
|
|
6739
|
+
];
|
|
6740
|
+
var HOOK_REGISTRY = {
|
|
6741
|
+
viewer: {
|
|
6742
|
+
name: "viewer",
|
|
6743
|
+
onAfterCommand: async (ctx) => {
|
|
6744
|
+
const result = ctx.result;
|
|
6745
|
+
if (!result || result.success !== false) return void 0;
|
|
6746
|
+
const msg = [
|
|
6747
|
+
result.message,
|
|
6748
|
+
...result.tips || []
|
|
6749
|
+
].filter(Boolean).join(" ").toLowerCase();
|
|
6750
|
+
if (!FAIL_KEYWORDS.some((k) => msg.includes(k))) return void 0;
|
|
6751
|
+
const viewerUrl = buildViewerUrl();
|
|
6752
|
+
if (!viewerUrl) return void 0;
|
|
6753
|
+
const tips = result.tips || [];
|
|
6754
|
+
if (!tips.some((t) => t.includes("viewer") || t.includes("Viewer"))) {
|
|
6755
|
+
tips.push(`Open viewer: ${viewerUrl}`);
|
|
6756
|
+
}
|
|
6757
|
+
result.tips = tips;
|
|
6758
|
+
result.viewerUrl = viewerUrl;
|
|
6759
|
+
return void 0;
|
|
6760
|
+
}
|
|
6761
|
+
},
|
|
6762
|
+
screenshot: {
|
|
6763
|
+
name: "screenshot",
|
|
6764
|
+
onAfterCommand: async (ctx) => {
|
|
6765
|
+
try {
|
|
6766
|
+
const buf = await ctx.page.screenshot({ type: "jpeg", quality: 40 }).catch(() => null);
|
|
6767
|
+
if (!buf) return;
|
|
6768
|
+
return { screenshot: { url: `data:image/jpeg;base64,${buf.toString("base64").slice(0, 50)}...` } };
|
|
6769
|
+
} catch {
|
|
6770
|
+
return;
|
|
6771
|
+
}
|
|
6772
|
+
}
|
|
6773
|
+
},
|
|
6774
|
+
recorder: {
|
|
6775
|
+
name: "recorder",
|
|
6776
|
+
onAfterCommand: async (ctx) => {
|
|
6777
|
+
const ctxAny = ctx;
|
|
6778
|
+
const logs = ctxAny.__commandLogs || [];
|
|
6779
|
+
logs.push({
|
|
6780
|
+
timestamp: Date.now(),
|
|
6781
|
+
command: ctx.command,
|
|
6782
|
+
params: JSON.parse(JSON.stringify(ctx.params)),
|
|
6783
|
+
duration: ctx.duration
|
|
6784
|
+
});
|
|
6785
|
+
ctxAny.__commandLogs = logs;
|
|
6786
|
+
return void 0;
|
|
6787
|
+
}
|
|
6788
|
+
}
|
|
6222
6789
|
};
|
|
6790
|
+
var customHooks = {};
|
|
6223
6791
|
async function loadHooks() {
|
|
6224
|
-
const
|
|
6225
|
-
if (!
|
|
6792
|
+
const env = process.env.XBROWSER_HOOKS;
|
|
6793
|
+
if (!env) return [];
|
|
6794
|
+
const names = env.split(",").map((n) => n.trim()).filter(Boolean);
|
|
6795
|
+
if (names.length === 0) return [];
|
|
6226
6796
|
const hooks = [];
|
|
6227
|
-
for (const name of names
|
|
6228
|
-
const
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6797
|
+
for (const name of names) {
|
|
6798
|
+
const hook = HOOK_REGISTRY[name];
|
|
6799
|
+
if (hook) {
|
|
6800
|
+
hooks.push(hook);
|
|
6801
|
+
continue;
|
|
6802
|
+
}
|
|
6803
|
+
const customFactory = customHooks[name];
|
|
6804
|
+
if (customFactory) {
|
|
6805
|
+
const customHook = await customFactory();
|
|
6806
|
+
if (customHook) hooks.push(customHook);
|
|
6232
6807
|
}
|
|
6233
6808
|
}
|
|
6234
6809
|
return hooks;
|
|
6235
6810
|
}
|
|
6236
6811
|
|
|
6237
6812
|
// src/executor.ts
|
|
6238
|
-
import { homedir as
|
|
6239
|
-
import { join as
|
|
6813
|
+
import { homedir as homedir5 } from "os";
|
|
6814
|
+
import { join as join5 } from "path";
|
|
6240
6815
|
var NAVIGATION_COMMANDS = /* @__PURE__ */ new Set(["goto", "back", "forward", "refresh"]);
|
|
6241
6816
|
var snapshotHintShown = /* @__PURE__ */ new WeakSet();
|
|
6242
|
-
var
|
|
6817
|
+
var CONFIG_DIR = join5(homedir5(), ".xbrowser");
|
|
6243
6818
|
var storageCache = /* @__PURE__ */ new Map();
|
|
6244
6819
|
function getPluginStorage(pluginName) {
|
|
6245
6820
|
if (!storageCache.has(pluginName)) {
|
|
6246
|
-
storageCache.set(pluginName, new
|
|
6821
|
+
storageCache.set(pluginName, new CompositeStorage(pluginName, CONFIG_DIR, "xbrowser"));
|
|
6247
6822
|
}
|
|
6248
6823
|
return storageCache.get(pluginName);
|
|
6249
6824
|
}
|
|
@@ -6251,7 +6826,7 @@ var archiveInitialized = false;
|
|
|
6251
6826
|
function ensureArchiveInit() {
|
|
6252
6827
|
if (!archiveInitialized) {
|
|
6253
6828
|
try {
|
|
6254
|
-
configureArchiveStore({ archiveDir:
|
|
6829
|
+
configureArchiveStore({ archiveDir: join5(homedir5(), ".xbrowser", "archives") });
|
|
6255
6830
|
} catch {
|
|
6256
6831
|
}
|
|
6257
6832
|
archiveInitialized = true;
|
|
@@ -6277,6 +6852,10 @@ async function guardCheck(commandName) {
|
|
|
6277
6852
|
function errorResult(message) {
|
|
6278
6853
|
return { ...fail7(message), duration: 0 };
|
|
6279
6854
|
}
|
|
6855
|
+
function tipsToMessages(tips) {
|
|
6856
|
+
if (!tips || tips.length === 0) return [];
|
|
6857
|
+
return tips.map((t) => typeof t === "string" ? t : t.message);
|
|
6858
|
+
}
|
|
6280
6859
|
var wsServer = null;
|
|
6281
6860
|
function streamCommandEvent(sessionId, message) {
|
|
6282
6861
|
if (!wsServer || !wsServer.getRunning()) return;
|
|
@@ -6304,7 +6883,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6304
6883
|
}
|
|
6305
6884
|
let targetPageOverride = null;
|
|
6306
6885
|
if (_target && extraOpts?.cdpEndpoint) {
|
|
6307
|
-
const { findTargetPage } = await import("./browser-
|
|
6886
|
+
const { findTargetPage } = await import("./browser-R56O3CW6.js");
|
|
6308
6887
|
targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
|
|
6309
6888
|
if (!targetPageOverride) {
|
|
6310
6889
|
return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
|
|
@@ -6321,7 +6900,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6321
6900
|
params = result.data;
|
|
6322
6901
|
}
|
|
6323
6902
|
if (command.scope !== "cli" && !process.env.XBROWSER_DAEMON_WORKER) {
|
|
6324
|
-
const { forwardExec } = await import("./daemon-client-
|
|
6903
|
+
const { forwardExec } = await import("./daemon-client-UZZEHHIV.js");
|
|
6325
6904
|
const result = await forwardExec(commandName, params, sessionName, extraOpts?.cdpEndpoint);
|
|
6326
6905
|
if (result) return result;
|
|
6327
6906
|
}
|
|
@@ -6329,32 +6908,35 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6329
6908
|
const existing = await findOrRestoreSession(sessionName, extraOpts?.cdpEndpoint);
|
|
6330
6909
|
if (existing) {
|
|
6331
6910
|
session = existing;
|
|
6332
|
-
if (
|
|
6911
|
+
if (session.page) {
|
|
6912
|
+
try {
|
|
6913
|
+
await Promise.race([
|
|
6914
|
+
session.page.evaluate(() => true),
|
|
6915
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 3e3))
|
|
6916
|
+
]);
|
|
6917
|
+
} catch {
|
|
6918
|
+
await closeSessionByName(session.name);
|
|
6919
|
+
session = void 0;
|
|
6920
|
+
}
|
|
6921
|
+
}
|
|
6922
|
+
if (session && targetPageOverride && session.page) {
|
|
6333
6923
|
const currentUrl = session.page.url();
|
|
6334
6924
|
if (currentUrl !== targetPageOverride.url) {
|
|
6335
6925
|
await session.page.goto(targetPageOverride.url, { waitUntil: "domcontentloaded", timeout: 15e3 }).catch(() => {
|
|
6336
6926
|
});
|
|
6337
6927
|
}
|
|
6338
6928
|
}
|
|
6339
|
-
} else if (
|
|
6929
|
+
} else if (command.scope !== "project") {
|
|
6340
6930
|
session = await createSession(sessionName, params.url, {
|
|
6341
6931
|
cdpEndpoint: extraOpts?.cdpEndpoint
|
|
6342
6932
|
});
|
|
6343
|
-
} else if (command.scope === "browser") {
|
|
6344
|
-
session = await createSession(sessionName, void 0, {
|
|
6345
|
-
cdpEndpoint: extraOpts?.cdpEndpoint
|
|
6346
|
-
});
|
|
6347
|
-
} else if (command.scope !== "project") {
|
|
6348
|
-
return errorResult(
|
|
6349
|
-
`Session '${sessionName}' not found. Run "xbrowser session open <url>" first.`
|
|
6350
|
-
);
|
|
6351
6933
|
}
|
|
6352
6934
|
const ctx = {
|
|
6353
6935
|
page: session?.page,
|
|
6354
6936
|
browser: session?.context.browser(),
|
|
6355
6937
|
browserContext: session?.context,
|
|
6356
6938
|
sessionId: session?.id,
|
|
6357
|
-
cdpEndpoint:
|
|
6939
|
+
cdpEndpoint: session?.cdpEndpoint || extraOpts?.cdpEndpoint,
|
|
6358
6940
|
args: [],
|
|
6359
6941
|
options: {},
|
|
6360
6942
|
cwd: process.cwd(),
|
|
@@ -6370,7 +6952,8 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6370
6952
|
},
|
|
6371
6953
|
config: {},
|
|
6372
6954
|
site: {},
|
|
6373
|
-
cliName: "xbrowser"
|
|
6955
|
+
cliName: "xbrowser",
|
|
6956
|
+
tips: new TipCollector()
|
|
6374
6957
|
};
|
|
6375
6958
|
const start = Date.now();
|
|
6376
6959
|
if (session) {
|
|
@@ -6389,9 +6972,9 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6389
6972
|
let refTips = [];
|
|
6390
6973
|
if (session?.page && command.selectorParams && command.selectorParams.length > 0) {
|
|
6391
6974
|
const cache = /* @__PURE__ */ new Map();
|
|
6392
|
-
const resolved = await resolveRefParams(session.page, params, command.selectorParams, cache);
|
|
6975
|
+
const resolved = await resolveRefParams(session.page, params, command.selectorParams, cache, session.id);
|
|
6393
6976
|
if (resolved.tips.length > 0) {
|
|
6394
|
-
refTips = resolved.tips;
|
|
6977
|
+
refTips = normalizeTips6(resolved.tips);
|
|
6395
6978
|
params = resolved.params;
|
|
6396
6979
|
}
|
|
6397
6980
|
}
|
|
@@ -6447,13 +7030,19 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6447
7030
|
snapshotHintShown.add(session);
|
|
6448
7031
|
snapshotHint = "\u{1F4A1} \u4F7F\u7528 snapshot \u547D\u4EE4\u83B7\u53D6\u9875\u9762\u5FEB\u7167\u548C ref \u7F16\u53F7\uFF0C\u7136\u540E\u7528 ref \u5FEB\u901F\u5B9A\u4F4D\u5143\u7D20\uFF08\u5982 click --selector e1\uFF09";
|
|
6449
7032
|
}
|
|
6450
|
-
const merged = [
|
|
7033
|
+
const merged = [
|
|
7034
|
+
...raw.tips || [],
|
|
7035
|
+
...normalizeTips6(smartTips),
|
|
7036
|
+
...snapshotHint ? normalizeTips6([snapshotHint]) : [],
|
|
7037
|
+
...refTips
|
|
7038
|
+
];
|
|
6451
7039
|
const isSuccess = raw.success !== false;
|
|
7040
|
+
const mergedOrRaw = merged.length > 0 ? merged : raw.tips || [];
|
|
6452
7041
|
recordArchive(session?.id, sessionName, {
|
|
6453
7042
|
step: 0,
|
|
6454
7043
|
command: commandName,
|
|
6455
7044
|
params,
|
|
6456
|
-
result: { success: isSuccess, data: raw.data, message: raw.message, tips:
|
|
7045
|
+
result: { success: isSuccess, data: raw.data, message: raw.message, tips: tipsToMessages(mergedOrRaw) },
|
|
6457
7046
|
toolCalls: [],
|
|
6458
7047
|
duration,
|
|
6459
7048
|
timestamp: start
|
|
@@ -6461,18 +7050,19 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6461
7050
|
if (isSuccess) {
|
|
6462
7051
|
return { ...ok25(raw.data, merged.length > 0 ? merged : raw.tips), duration, ...hookOutputs ? { hookOutputs } : {} };
|
|
6463
7052
|
}
|
|
6464
|
-
return { success: false, data: raw.data, message: raw.message, tips:
|
|
7053
|
+
return { success: false, data: raw.data, message: raw.message, tips: mergedOrRaw, duration, ...hookOutputs ? { hookOutputs } : {} };
|
|
6465
7054
|
}
|
|
7055
|
+
const smartTipNormalized = normalizeTips6(smartTips);
|
|
6466
7056
|
recordArchive(session?.id, sessionName, {
|
|
6467
7057
|
step: 0,
|
|
6468
7058
|
command: commandName,
|
|
6469
7059
|
params,
|
|
6470
|
-
result: { success: true, data: raw, tips:
|
|
7060
|
+
result: { success: true, data: raw, tips: tipsToMessages(smartTipNormalized) },
|
|
6471
7061
|
toolCalls: [],
|
|
6472
7062
|
duration,
|
|
6473
7063
|
timestamp: start
|
|
6474
7064
|
});
|
|
6475
|
-
return { ...ok25(raw,
|
|
7065
|
+
return { ...ok25(raw, smartTipNormalized), duration, ...hookOutputs ? { hookOutputs } : {} };
|
|
6476
7066
|
} catch (err) {
|
|
6477
7067
|
const end = Date.now();
|
|
6478
7068
|
const duration = end - start;
|
|
@@ -6522,8 +7112,8 @@ async function executeChain(input, options) {
|
|
|
6522
7112
|
});
|
|
6523
7113
|
}
|
|
6524
7114
|
try {
|
|
6525
|
-
for (const
|
|
6526
|
-
const { type, pipeline: commands } =
|
|
7115
|
+
for (const pipeline of pipelines) {
|
|
7116
|
+
const { type, pipeline: commands } = pipeline;
|
|
6527
7117
|
for (const cmdStr of commands) {
|
|
6528
7118
|
const parts = splitCommand(cmdStr);
|
|
6529
7119
|
if (parts.length === 0) continue;
|
|
@@ -6588,10 +7178,51 @@ async function executeChain(input, options) {
|
|
|
6588
7178
|
},
|
|
6589
7179
|
config: {},
|
|
6590
7180
|
site,
|
|
6591
|
-
cliName: "xbrowser"
|
|
7181
|
+
cliName: "xbrowser",
|
|
7182
|
+
tips: new TipCollector()
|
|
6592
7183
|
};
|
|
6593
7184
|
const start2 = Date.now();
|
|
6594
7185
|
try {
|
|
7186
|
+
const loginGuard = await checkPluginLoginRequired({
|
|
7187
|
+
site,
|
|
7188
|
+
command: cmdEntry,
|
|
7189
|
+
commandName: subCommand,
|
|
7190
|
+
ctx: pluginCtx,
|
|
7191
|
+
page: session?.page,
|
|
7192
|
+
sessionName
|
|
7193
|
+
});
|
|
7194
|
+
if (!loginGuard.ok) {
|
|
7195
|
+
const duration3 = Date.now() - start2;
|
|
7196
|
+
const data2 = loginGuard.data ?? null;
|
|
7197
|
+
recordArchive(session.id, sessionName, {
|
|
7198
|
+
step: results.length,
|
|
7199
|
+
command: `${cmdName} ${subCommand}`,
|
|
7200
|
+
params: pluginParams,
|
|
7201
|
+
result: { success: false, data: data2, message: loginGuard.message, tips: loginGuard.tips || [] },
|
|
7202
|
+
toolCalls: [],
|
|
7203
|
+
duration: duration3,
|
|
7204
|
+
timestamp: start2
|
|
7205
|
+
});
|
|
7206
|
+
results.push({
|
|
7207
|
+
command: `${cmdName} ${subCommand}`,
|
|
7208
|
+
raw: cmdStr,
|
|
7209
|
+
success: false,
|
|
7210
|
+
data: data2,
|
|
7211
|
+
message: loginGuard.message,
|
|
7212
|
+
tips: normalizeTips6(loginGuard.tips),
|
|
7213
|
+
duration: duration3
|
|
7214
|
+
});
|
|
7215
|
+
if (type === "and") {
|
|
7216
|
+
return {
|
|
7217
|
+
success: false,
|
|
7218
|
+
steps: results,
|
|
7219
|
+
totalDuration: Date.now() - totalStart,
|
|
7220
|
+
stoppedAt: results.length,
|
|
7221
|
+
stoppedReason: `Command '${cmdName} ${subCommand}' failed (&& chain): ${loginGuard.message}`
|
|
7222
|
+
};
|
|
7223
|
+
}
|
|
7224
|
+
continue;
|
|
7225
|
+
}
|
|
6595
7226
|
const hooks = await loadHooks();
|
|
6596
7227
|
if (hooks.length > 0) {
|
|
6597
7228
|
await Promise.all(hooks.map((h) => h.onBeforeCommand?.({ page: session.page, command: `${cmdName} ${subCommand}`, params: pluginParams })));
|
|
@@ -6612,7 +7243,7 @@ async function executeChain(input, options) {
|
|
|
6612
7243
|
step: results.length,
|
|
6613
7244
|
command: `${cmdName} ${subCommand}`,
|
|
6614
7245
|
params: pluginParams,
|
|
6615
|
-
result: { success: true, data, tips: raw?.tips
|
|
7246
|
+
result: { success: true, data, tips: tipsToMessages(raw?.tips) },
|
|
6616
7247
|
toolCalls: [],
|
|
6617
7248
|
duration: duration2,
|
|
6618
7249
|
timestamp: start2
|
|
@@ -6663,7 +7294,7 @@ async function executeChain(input, options) {
|
|
|
6663
7294
|
}
|
|
6664
7295
|
continue;
|
|
6665
7296
|
}
|
|
6666
|
-
const { params } = parseCommandArgs(cmdName, cmdArgs);
|
|
7297
|
+
const { params } = parseCommandArgs(cmdName, cmdArgs, unquote2);
|
|
6667
7298
|
if (cmdName === "goto" && params.url) {
|
|
6668
7299
|
const existing2 = await findOrRestoreSession(sessionName, options?.cdpEndpoint);
|
|
6669
7300
|
if (!existing2) {
|
|
@@ -6724,15 +7355,6 @@ function isChainInput(input) {
|
|
|
6724
7355
|
}
|
|
6725
7356
|
|
|
6726
7357
|
// src/session/session-client.ts
|
|
6727
|
-
function sessionToInfo(s) {
|
|
6728
|
-
return { id: s.id, name: s.name, url: s.page.url(), createdAt: s.createdAt, cdpEndpoint: s.cdpEndpoint };
|
|
6729
|
-
}
|
|
6730
|
-
async function openSession(name, url, options) {
|
|
6731
|
-
const session = await createSession(name, url, { cdpEndpoint: options?.cdpEndpoint });
|
|
6732
|
-
const info = sessionToInfo(session);
|
|
6733
|
-
saveSessionDiskMeta(name, info);
|
|
6734
|
-
return info;
|
|
6735
|
-
}
|
|
6736
7358
|
async function closeSession(name) {
|
|
6737
7359
|
await closeSessionByName(name);
|
|
6738
7360
|
}
|
|
@@ -6746,53 +7368,22 @@ function handleSessionHelp() {
|
|
|
6746
7368
|
"Usage: xbrowser session <command> [options]",
|
|
6747
7369
|
"",
|
|
6748
7370
|
"Commands:",
|
|
6749
|
-
" open <url> [--name <name>] Open browser and create session",
|
|
6750
7371
|
" close [--name <name>] Close session",
|
|
6751
7372
|
" list, ls List active sessions",
|
|
6752
7373
|
" kill [--name <name>] Kill session forcefully",
|
|
7374
|
+
" kill-all Kill all sessions and daemon",
|
|
6753
7375
|
"",
|
|
6754
7376
|
"Options:",
|
|
6755
7377
|
' --name <name> Session name (default: "default")',
|
|
6756
7378
|
"",
|
|
7379
|
+
"Note: Sessions are auto-created via --session global option.",
|
|
7380
|
+
"",
|
|
6757
7381
|
"Examples:",
|
|
6758
|
-
" xbrowser
|
|
6759
|
-
" xbrowser session open https://example.com --name mypage",
|
|
7382
|
+
" xbrowser goto https://example.com --session mypage",
|
|
6760
7383
|
" xbrowser session close --name mypage",
|
|
6761
7384
|
" xbrowser session list"
|
|
6762
7385
|
].join("\n");
|
|
6763
7386
|
}
|
|
6764
|
-
var sessionOpenBuiltin = {
|
|
6765
|
-
name: "session open",
|
|
6766
|
-
description: "Open browser and create session",
|
|
6767
|
-
help: {
|
|
6768
|
-
usage: "xbrowser session open <url> [--name <name>]",
|
|
6769
|
-
description: "Open URL and create a browser session",
|
|
6770
|
-
options: [{ name: "--name <name>", description: 'Session name (default: "default")' }],
|
|
6771
|
-
examples: [
|
|
6772
|
-
{ cmd: "xbrowser session open https://example.com", description: "Open example.com" },
|
|
6773
|
-
{
|
|
6774
|
-
cmd: "xbrowser session open https://example.com --name test",
|
|
6775
|
-
description: "Open with custom name"
|
|
6776
|
-
}
|
|
6777
|
-
]
|
|
6778
|
-
},
|
|
6779
|
-
execute: async (args, options) => {
|
|
6780
|
-
const [url] = args;
|
|
6781
|
-
const name = options["name"] || "default";
|
|
6782
|
-
if (!url) {
|
|
6783
|
-
console.log("Usage: xbrowser session open <url> [--name <name>]");
|
|
6784
|
-
process.exit(1);
|
|
6785
|
-
}
|
|
6786
|
-
try {
|
|
6787
|
-
const info = await openSession(name, url);
|
|
6788
|
-
console.log(`Session "${info.name}" opened: ${info.url}`);
|
|
6789
|
-
console.log(`ID: ${info.id}`);
|
|
6790
|
-
} catch (e) {
|
|
6791
|
-
console.error("Error:", e instanceof Error ? e.message : String(e));
|
|
6792
|
-
process.exit(1);
|
|
6793
|
-
}
|
|
6794
|
-
}
|
|
6795
|
-
};
|
|
6796
7387
|
var sessionCloseBuiltin = {
|
|
6797
7388
|
name: "session close",
|
|
6798
7389
|
description: "Close browser session",
|
|
@@ -6837,13 +7428,13 @@ var sessionListBuiltin = {
|
|
|
6837
7428
|
},
|
|
6838
7429
|
execute: async () => {
|
|
6839
7430
|
try {
|
|
6840
|
-
const
|
|
6841
|
-
if (
|
|
7431
|
+
const sessions2 = await listSessions();
|
|
7432
|
+
if (sessions2.length === 0) {
|
|
6842
7433
|
console.log("No active sessions");
|
|
6843
7434
|
return;
|
|
6844
7435
|
}
|
|
6845
7436
|
console.log("Active sessions:");
|
|
6846
|
-
for (const s of
|
|
7437
|
+
for (const s of sessions2) {
|
|
6847
7438
|
console.log(` ${s.name} (${s.id})`);
|
|
6848
7439
|
}
|
|
6849
7440
|
} catch (e) {
|
|
@@ -6874,22 +7465,17 @@ var sessionKillBuiltin = {
|
|
|
6874
7465
|
};
|
|
6875
7466
|
|
|
6876
7467
|
// src/config.ts
|
|
6877
|
-
import {
|
|
6878
|
-
import { join as
|
|
6879
|
-
import {
|
|
6880
|
-
function
|
|
6881
|
-
return
|
|
7468
|
+
import { homedir as homedir6, tmpdir } from "os";
|
|
7469
|
+
import { join as join6 } from "path";
|
|
7470
|
+
import { loadConfig as coreLoadConfig, saveConfig as coreSaveConfig } from "@dyyz1993/xcli-core";
|
|
7471
|
+
function getConfigSource() {
|
|
7472
|
+
return { configDir: join6(homedir6() || tmpdir(), ".xbrowser") };
|
|
6882
7473
|
}
|
|
6883
7474
|
function loadConfig() {
|
|
6884
|
-
|
|
6885
|
-
if (!existsSync6(configFile)) return {};
|
|
6886
|
-
return readJsonFile(configFile, {});
|
|
7475
|
+
return coreLoadConfig(getConfigSource());
|
|
6887
7476
|
}
|
|
6888
7477
|
function saveConfig(config) {
|
|
6889
|
-
|
|
6890
|
-
const configFile = getConfigFile();
|
|
6891
|
-
if (!existsSync6(dir)) mkdirSync3(dir, { recursive: true });
|
|
6892
|
-
writeFileSync5(configFile, JSON.stringify(config, null, 2), "utf-8");
|
|
7478
|
+
coreSaveConfig(getConfigSource(), config);
|
|
6893
7479
|
}
|
|
6894
7480
|
function getConfigValue(key) {
|
|
6895
7481
|
return loadConfig()[key];
|
|
@@ -6997,38 +7583,69 @@ var configBuiltin = {
|
|
|
6997
7583
|
|
|
6998
7584
|
// src/plugin/installer.ts
|
|
6999
7585
|
import {
|
|
7000
|
-
existsSync as
|
|
7001
|
-
readdirSync as
|
|
7586
|
+
existsSync as existsSync10,
|
|
7587
|
+
readdirSync as readdirSync2,
|
|
7002
7588
|
mkdirSync as mkdirSync8,
|
|
7003
|
-
rmSync as
|
|
7589
|
+
rmSync as rmSync6
|
|
7004
7590
|
} from "fs";
|
|
7005
|
-
import { resolve as
|
|
7006
|
-
import { homedir as
|
|
7591
|
+
import { resolve as resolve8, basename as basename2 } from "path";
|
|
7592
|
+
import { homedir as homedir7 } from "os";
|
|
7007
7593
|
|
|
7008
7594
|
// src/plugin/install-sources/local.ts
|
|
7009
|
-
import { existsSync as
|
|
7010
|
-
import { resolve as
|
|
7595
|
+
import { existsSync as existsSync5, cpSync, rmSync } from "fs";
|
|
7596
|
+
import { resolve as resolve3 } from "path";
|
|
7597
|
+
import { verifyPlugin, safeCleanup } from "@dyyz1993/xcli-core";
|
|
7598
|
+
async function installFromLocal(source, name, targetDir) {
|
|
7599
|
+
const srcPath = resolve3(source);
|
|
7600
|
+
if (!existsSync5(srcPath)) {
|
|
7601
|
+
throw new Error(`Local path does not exist: ${srcPath}`);
|
|
7602
|
+
}
|
|
7603
|
+
const tmpTarget = `${targetDir}-tmp-${Date.now()}`;
|
|
7604
|
+
let warnings = [];
|
|
7605
|
+
try {
|
|
7606
|
+
cpSync(srcPath, tmpTarget, { recursive: true });
|
|
7607
|
+
const verify = verifyPlugin(tmpTarget, { metadataField: "xbrowser" });
|
|
7608
|
+
warnings = verify.warnings ?? [];
|
|
7609
|
+
if (!verify.valid) {
|
|
7610
|
+
safeCleanup(tmpTarget);
|
|
7611
|
+
throw new Error(`Invalid plugin: ${verify.error}`);
|
|
7612
|
+
}
|
|
7613
|
+
if (existsSync5(targetDir)) {
|
|
7614
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
7615
|
+
}
|
|
7616
|
+
cpSync(tmpTarget, targetDir, { recursive: true, force: true });
|
|
7617
|
+
safeCleanup(tmpTarget);
|
|
7618
|
+
} catch (err) {
|
|
7619
|
+
safeCleanup(tmpTarget);
|
|
7620
|
+
throw err;
|
|
7621
|
+
}
|
|
7622
|
+
return {
|
|
7623
|
+
id: name,
|
|
7624
|
+
name,
|
|
7625
|
+
path: targetDir,
|
|
7626
|
+
source: "local",
|
|
7627
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7628
|
+
warnings
|
|
7629
|
+
};
|
|
7630
|
+
}
|
|
7011
7631
|
|
|
7012
|
-
// src/plugin/install-
|
|
7632
|
+
// src/plugin/install-sources/npm.ts
|
|
7633
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync6, rmSync as rmSync2, cpSync as cpSync2 } from "fs";
|
|
7634
|
+
import { resolve as resolve4, join as join7 } from "path";
|
|
7635
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
7013
7636
|
import {
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
createWriteStream
|
|
7021
|
-
} from "fs";
|
|
7022
|
-
import { resolve as resolve9 } from "path";
|
|
7023
|
-
import { execSync as execSync7 } from "child_process";
|
|
7024
|
-
import { pipeline } from "stream/promises";
|
|
7025
|
-
import { Readable } from "stream";
|
|
7637
|
+
downloadToFile,
|
|
7638
|
+
extractTarGz,
|
|
7639
|
+
flattenPackageRoot,
|
|
7640
|
+
verifyPlugin as verifyPlugin2,
|
|
7641
|
+
safeCleanup as safeCleanup2
|
|
7642
|
+
} from "@dyyz1993/xcli-core";
|
|
7026
7643
|
|
|
7027
7644
|
// src/utils/proxy-fetch.ts
|
|
7028
|
-
var
|
|
7645
|
+
var patched2 = false;
|
|
7029
7646
|
async function ensureProxyFetch() {
|
|
7030
|
-
if (
|
|
7031
|
-
|
|
7647
|
+
if (patched2) return;
|
|
7648
|
+
patched2 = true;
|
|
7032
7649
|
if (process.env.https_proxy && !process.env.HTTPS_PROXY) {
|
|
7033
7650
|
process.env.HTTPS_PROXY = process.env.https_proxy;
|
|
7034
7651
|
}
|
|
@@ -7068,113 +7685,7 @@ async function ensureProxyFetch() {
|
|
|
7068
7685
|
}
|
|
7069
7686
|
}
|
|
7070
7687
|
|
|
7071
|
-
// src/plugin/install-utils.ts
|
|
7072
|
-
async function downloadToFile(url, destPath) {
|
|
7073
|
-
await ensureProxyFetch();
|
|
7074
|
-
if (url.startsWith("file://")) {
|
|
7075
|
-
const filePath = decodeURIComponent(new URL(url).pathname);
|
|
7076
|
-
cpSync(filePath, destPath, { force: true });
|
|
7077
|
-
return;
|
|
7078
|
-
}
|
|
7079
|
-
const res = await fetch(url);
|
|
7080
|
-
if (!res.ok) {
|
|
7081
|
-
throw new Error(`Download failed: HTTP ${res.status} from ${url}`);
|
|
7082
|
-
}
|
|
7083
|
-
if (!res.body) {
|
|
7084
|
-
throw new Error(`No response body from ${url}`);
|
|
7085
|
-
}
|
|
7086
|
-
const nodeStream = Readable.fromWeb(res.body);
|
|
7087
|
-
await pipeline(nodeStream, createWriteStream(destPath));
|
|
7088
|
-
}
|
|
7089
|
-
function extractTarGz(tarballPath, targetDir) {
|
|
7090
|
-
mkdirSync4(targetDir, { recursive: true });
|
|
7091
|
-
execSync7(`tar -xzf "${tarballPath}" -C "${targetDir}"`, { stdio: "pipe" });
|
|
7092
|
-
}
|
|
7093
|
-
function flattenPackageRoot(targetDir) {
|
|
7094
|
-
const entries = readdirSync2(targetDir, { withFileTypes: true });
|
|
7095
|
-
const dirs = entries.filter((e) => e.isDirectory());
|
|
7096
|
-
const files = entries.filter((e) => !e.isDirectory());
|
|
7097
|
-
if (dirs.length === 1 && files.length === 0) {
|
|
7098
|
-
const pkgDir = resolve9(targetDir, dirs[0].name);
|
|
7099
|
-
const items = readdirSync2(pkgDir);
|
|
7100
|
-
for (const item of items) {
|
|
7101
|
-
const src = resolve9(pkgDir, item);
|
|
7102
|
-
const dst = resolve9(targetDir, item);
|
|
7103
|
-
cpSync(src, dst, { recursive: true, force: true });
|
|
7104
|
-
}
|
|
7105
|
-
rmSync(pkgDir, { recursive: true, force: true });
|
|
7106
|
-
}
|
|
7107
|
-
}
|
|
7108
|
-
async function verifyPlugin(dir) {
|
|
7109
|
-
const warnings = [];
|
|
7110
|
-
const indexPath = resolve9(dir, "index.ts");
|
|
7111
|
-
if (!existsSync7(indexPath)) {
|
|
7112
|
-
const indexJs = resolve9(dir, "index.js");
|
|
7113
|
-
if (!existsSync7(indexJs)) {
|
|
7114
|
-
return { valid: false, error: "No index.ts or index.js entry point found", warnings };
|
|
7115
|
-
}
|
|
7116
|
-
}
|
|
7117
|
-
const pkgPath = resolve9(dir, "package.json");
|
|
7118
|
-
if (!existsSync7(pkgPath)) {
|
|
7119
|
-
warnings.push("No package.json found");
|
|
7120
|
-
} else {
|
|
7121
|
-
try {
|
|
7122
|
-
const pkg2 = JSON.parse(readFileSync10(pkgPath, "utf-8"));
|
|
7123
|
-
if (!pkg2.xbrowser) {
|
|
7124
|
-
warnings.push("No xbrowser metadata in package.json");
|
|
7125
|
-
}
|
|
7126
|
-
} catch {
|
|
7127
|
-
warnings.push("Invalid package.json");
|
|
7128
|
-
}
|
|
7129
|
-
}
|
|
7130
|
-
return { valid: true, warnings };
|
|
7131
|
-
}
|
|
7132
|
-
function safeCleanup(dir) {
|
|
7133
|
-
try {
|
|
7134
|
-
rmSync(dir, { recursive: true, force: true });
|
|
7135
|
-
} catch {
|
|
7136
|
-
}
|
|
7137
|
-
}
|
|
7138
|
-
|
|
7139
|
-
// src/plugin/install-sources/local.ts
|
|
7140
|
-
async function installFromLocal(source, name, targetDir) {
|
|
7141
|
-
const srcPath = resolve10(source);
|
|
7142
|
-
if (!existsSync8(srcPath)) {
|
|
7143
|
-
throw new Error(`Local path does not exist: ${srcPath}`);
|
|
7144
|
-
}
|
|
7145
|
-
const tmpTarget = `${targetDir}-tmp-${Date.now()}`;
|
|
7146
|
-
let warnings = [];
|
|
7147
|
-
try {
|
|
7148
|
-
cpSync2(srcPath, tmpTarget, { recursive: true });
|
|
7149
|
-
const verify = await verifyPlugin(tmpTarget);
|
|
7150
|
-
warnings = verify.warnings ?? [];
|
|
7151
|
-
if (!verify.valid) {
|
|
7152
|
-
safeCleanup(tmpTarget);
|
|
7153
|
-
throw new Error(`Invalid plugin: ${verify.error}`);
|
|
7154
|
-
}
|
|
7155
|
-
if (existsSync8(targetDir)) {
|
|
7156
|
-
rmSync2(targetDir, { recursive: true, force: true });
|
|
7157
|
-
}
|
|
7158
|
-
cpSync2(tmpTarget, targetDir, { recursive: true, force: true });
|
|
7159
|
-
safeCleanup(tmpTarget);
|
|
7160
|
-
} catch (err) {
|
|
7161
|
-
safeCleanup(tmpTarget);
|
|
7162
|
-
throw err;
|
|
7163
|
-
}
|
|
7164
|
-
return {
|
|
7165
|
-
id: name,
|
|
7166
|
-
name,
|
|
7167
|
-
path: targetDir,
|
|
7168
|
-
source: "local",
|
|
7169
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7170
|
-
warnings
|
|
7171
|
-
};
|
|
7172
|
-
}
|
|
7173
|
-
|
|
7174
7688
|
// src/plugin/install-sources/npm.ts
|
|
7175
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync6, rmSync as rmSync3, cpSync as cpSync3 } from "fs";
|
|
7176
|
-
import { resolve as resolve11, join as join5 } from "path";
|
|
7177
|
-
import { tmpdir as tmpdir2 } from "os";
|
|
7178
7689
|
async function installFromNpm(packageName, name, targetDir) {
|
|
7179
7690
|
await ensureProxyFetch();
|
|
7180
7691
|
const encodedName = encodeURIComponent(packageName);
|
|
@@ -7192,34 +7703,34 @@ async function installFromNpm(packageName, name, targetDir) {
|
|
|
7192
7703
|
throw new Error(`No tarball URL for ${packageName}@${latestVersion}`);
|
|
7193
7704
|
}
|
|
7194
7705
|
const tarballUrl = versionMeta.dist.tarball;
|
|
7195
|
-
const tmpDir =
|
|
7706
|
+
const tmpDir = join7(tmpdir2(), `xbrowser-npm-${Date.now()}`);
|
|
7196
7707
|
mkdirSync5(tmpDir, { recursive: true });
|
|
7197
7708
|
let warnings = [];
|
|
7198
7709
|
try {
|
|
7199
|
-
const tarballPath =
|
|
7710
|
+
const tarballPath = join7(tmpDir, `${name}.tgz`);
|
|
7200
7711
|
await downloadToFile(tarballUrl, tarballPath);
|
|
7201
|
-
const extractDir =
|
|
7712
|
+
const extractDir = join7(tmpDir, "extracted");
|
|
7202
7713
|
extractTarGz(tarballPath, extractDir);
|
|
7203
7714
|
flattenPackageRoot(extractDir);
|
|
7204
|
-
const verify =
|
|
7715
|
+
const verify = verifyPlugin2(extractDir, { metadataField: "xbrowser" });
|
|
7205
7716
|
warnings = verify.warnings ?? [];
|
|
7206
7717
|
if (!verify.valid) {
|
|
7207
7718
|
throw new Error(`Invalid npm plugin: ${verify.error}`);
|
|
7208
7719
|
}
|
|
7209
|
-
if (
|
|
7210
|
-
|
|
7720
|
+
if (existsSync6(targetDir)) {
|
|
7721
|
+
rmSync2(targetDir, { recursive: true, force: true });
|
|
7211
7722
|
}
|
|
7212
|
-
|
|
7213
|
-
const pkgPath =
|
|
7214
|
-
if (
|
|
7215
|
-
const pkg2 = JSON.parse(
|
|
7723
|
+
cpSync2(extractDir, targetDir, { recursive: true, force: true });
|
|
7724
|
+
const pkgPath = resolve4(targetDir, "package.json");
|
|
7725
|
+
if (existsSync6(pkgPath)) {
|
|
7726
|
+
const pkg2 = JSON.parse(readFileSync4(pkgPath, "utf-8"));
|
|
7216
7727
|
if (!pkg2._npmSource) {
|
|
7217
7728
|
pkg2._npmSource = { name: packageName, version: latestVersion };
|
|
7218
7729
|
writeFileSync6(pkgPath, JSON.stringify(pkg2, null, 2));
|
|
7219
7730
|
}
|
|
7220
7731
|
}
|
|
7221
7732
|
} finally {
|
|
7222
|
-
|
|
7733
|
+
safeCleanup2(tmpDir);
|
|
7223
7734
|
}
|
|
7224
7735
|
return {
|
|
7225
7736
|
id: name,
|
|
@@ -7232,35 +7743,36 @@ async function installFromNpm(packageName, name, targetDir) {
|
|
|
7232
7743
|
}
|
|
7233
7744
|
|
|
7234
7745
|
// src/plugin/install-sources/git.ts
|
|
7235
|
-
import { existsSync as
|
|
7236
|
-
import { resolve as
|
|
7746
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync7, rmSync as rmSync3, cpSync as cpSync3 } from "fs";
|
|
7747
|
+
import { resolve as resolve5, join as join8 } from "path";
|
|
7237
7748
|
import { tmpdir as tmpdir3 } from "os";
|
|
7238
|
-
import { execSync as
|
|
7749
|
+
import { execSync as execSync2 } from "child_process";
|
|
7750
|
+
import { verifyPlugin as verifyPlugin3, safeCleanup as safeCleanup3 } from "@dyyz1993/xcli-core";
|
|
7239
7751
|
async function installFromGit(gitUrl, name, targetDir) {
|
|
7240
|
-
const tmpDir =
|
|
7752
|
+
const tmpDir = join8(tmpdir3(), `xbrowser-git-${Date.now()}`);
|
|
7241
7753
|
let warnings = [];
|
|
7242
7754
|
try {
|
|
7243
|
-
|
|
7244
|
-
const verify =
|
|
7755
|
+
execSync2(`git clone --depth 1 "${gitUrl}" "${tmpDir}"`, { stdio: "pipe" });
|
|
7756
|
+
const verify = verifyPlugin3(tmpDir, { metadataField: "xbrowser" });
|
|
7245
7757
|
warnings = verify.warnings ?? [];
|
|
7246
7758
|
if (!verify.valid) {
|
|
7247
7759
|
throw new Error(`Invalid git plugin: ${verify.error}`);
|
|
7248
7760
|
}
|
|
7249
|
-
if (
|
|
7250
|
-
|
|
7761
|
+
if (existsSync7(targetDir)) {
|
|
7762
|
+
rmSync3(targetDir, { recursive: true, force: true });
|
|
7251
7763
|
}
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
const pkgPath =
|
|
7255
|
-
if (
|
|
7256
|
-
const pkg2 = JSON.parse(
|
|
7764
|
+
cpSync3(tmpDir, targetDir, { recursive: true, force: true });
|
|
7765
|
+
rmSync3(resolve5(targetDir, ".git"), { recursive: true, force: true });
|
|
7766
|
+
const pkgPath = resolve5(targetDir, "package.json");
|
|
7767
|
+
if (existsSync7(pkgPath)) {
|
|
7768
|
+
const pkg2 = JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
7257
7769
|
if (!pkg2._gitSource) {
|
|
7258
7770
|
pkg2._gitSource = { url: gitUrl };
|
|
7259
7771
|
writeFileSync7(pkgPath, JSON.stringify(pkg2, null, 2));
|
|
7260
7772
|
}
|
|
7261
7773
|
}
|
|
7262
7774
|
} finally {
|
|
7263
|
-
|
|
7775
|
+
safeCleanup3(tmpDir);
|
|
7264
7776
|
}
|
|
7265
7777
|
return {
|
|
7266
7778
|
id: name,
|
|
@@ -7273,39 +7785,46 @@ async function installFromGit(gitUrl, name, targetDir) {
|
|
|
7273
7785
|
}
|
|
7274
7786
|
|
|
7275
7787
|
// src/plugin/install-sources/url.ts
|
|
7276
|
-
import { existsSync as
|
|
7277
|
-
import { resolve as
|
|
7788
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync8, rmSync as rmSync4, cpSync as cpSync4 } from "fs";
|
|
7789
|
+
import { resolve as resolve6, join as join9, basename } from "path";
|
|
7278
7790
|
import { tmpdir as tmpdir4 } from "os";
|
|
7791
|
+
import {
|
|
7792
|
+
downloadToFile as downloadToFile2,
|
|
7793
|
+
extractTarGz as extractTarGz2,
|
|
7794
|
+
flattenPackageRoot as flattenPackageRoot2,
|
|
7795
|
+
verifyPlugin as verifyPlugin4,
|
|
7796
|
+
safeCleanup as safeCleanup4
|
|
7797
|
+
} from "@dyyz1993/xcli-core";
|
|
7279
7798
|
async function installFromUrl(url, name, targetDir) {
|
|
7280
|
-
const tmpDir =
|
|
7799
|
+
const tmpDir = join9(tmpdir4(), `xbrowser-url-${Date.now()}`);
|
|
7281
7800
|
mkdirSync6(tmpDir, { recursive: true });
|
|
7282
7801
|
let warnings = [];
|
|
7283
7802
|
try {
|
|
7284
7803
|
const fileName = basename(new URL(url).pathname) || "plugin.tar.gz";
|
|
7285
|
-
const tarballPath =
|
|
7286
|
-
await
|
|
7287
|
-
const extractDir =
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
const verify =
|
|
7804
|
+
const tarballPath = join9(tmpDir, fileName);
|
|
7805
|
+
await downloadToFile2(url, tarballPath);
|
|
7806
|
+
const extractDir = join9(tmpDir, "extracted");
|
|
7807
|
+
extractTarGz2(tarballPath, extractDir);
|
|
7808
|
+
flattenPackageRoot2(extractDir);
|
|
7809
|
+
const verify = verifyPlugin4(extractDir, { metadataField: "xbrowser" });
|
|
7291
7810
|
warnings = verify.warnings ?? [];
|
|
7292
7811
|
if (!verify.valid) {
|
|
7293
7812
|
throw new Error(`Invalid plugin from URL: ${verify.error}`);
|
|
7294
7813
|
}
|
|
7295
|
-
if (
|
|
7296
|
-
|
|
7814
|
+
if (existsSync8(targetDir)) {
|
|
7815
|
+
rmSync4(targetDir, { recursive: true, force: true });
|
|
7297
7816
|
}
|
|
7298
|
-
|
|
7299
|
-
const pkgPath =
|
|
7300
|
-
if (
|
|
7301
|
-
const pkg2 = JSON.parse(
|
|
7817
|
+
cpSync4(extractDir, targetDir, { recursive: true, force: true });
|
|
7818
|
+
const pkgPath = resolve6(targetDir, "package.json");
|
|
7819
|
+
if (existsSync8(pkgPath)) {
|
|
7820
|
+
const pkg2 = JSON.parse(readFileSync6(pkgPath, "utf-8"));
|
|
7302
7821
|
if (!pkg2._urlSource) {
|
|
7303
7822
|
pkg2._urlSource = { url };
|
|
7304
7823
|
writeFileSync8(pkgPath, JSON.stringify(pkg2, null, 2));
|
|
7305
7824
|
}
|
|
7306
7825
|
}
|
|
7307
7826
|
} finally {
|
|
7308
|
-
|
|
7827
|
+
safeCleanup4(tmpDir);
|
|
7309
7828
|
}
|
|
7310
7829
|
return {
|
|
7311
7830
|
id: name,
|
|
@@ -7319,16 +7838,23 @@ async function installFromUrl(url, name, targetDir) {
|
|
|
7319
7838
|
|
|
7320
7839
|
// src/plugin/install-sources/marketplace.ts
|
|
7321
7840
|
import {
|
|
7322
|
-
existsSync as
|
|
7841
|
+
existsSync as existsSync9,
|
|
7323
7842
|
mkdirSync as mkdirSync7,
|
|
7324
7843
|
writeFileSync as writeFileSync9,
|
|
7325
|
-
readFileSync as
|
|
7326
|
-
rmSync as
|
|
7327
|
-
cpSync as
|
|
7844
|
+
readFileSync as readFileSync7,
|
|
7845
|
+
rmSync as rmSync5,
|
|
7846
|
+
cpSync as cpSync5
|
|
7328
7847
|
} from "fs";
|
|
7329
|
-
import { resolve as
|
|
7848
|
+
import { resolve as resolve7, join as join10, dirname as dirname2 } from "path";
|
|
7330
7849
|
import { tmpdir as tmpdir5 } from "os";
|
|
7331
7850
|
import { gunzipSync } from "zlib";
|
|
7851
|
+
import {
|
|
7852
|
+
downloadToFile as downloadToFile3,
|
|
7853
|
+
extractTarGz as extractTarGz3,
|
|
7854
|
+
flattenPackageRoot as flattenPackageRoot3,
|
|
7855
|
+
verifyPlugin as verifyPlugin5,
|
|
7856
|
+
safeCleanup as safeCleanup5
|
|
7857
|
+
} from "@dyyz1993/xcli-core";
|
|
7332
7858
|
async function installFromMarketplace(pluginsDir, slug, options) {
|
|
7333
7859
|
await ensureProxyFetch();
|
|
7334
7860
|
const baseUrl = getMarketplaceUrl();
|
|
@@ -7343,24 +7869,24 @@ async function installFromMarketplace(pluginsDir, slug, options) {
|
|
|
7343
7869
|
}
|
|
7344
7870
|
const plugin = detailData.data;
|
|
7345
7871
|
const name = options?.name || String(plugin.slug || slug);
|
|
7346
|
-
const targetDir =
|
|
7347
|
-
if (
|
|
7872
|
+
const targetDir = resolve7(pluginsDir, name);
|
|
7873
|
+
if (existsSync9(targetDir) && !options?.force) {
|
|
7348
7874
|
throw new Error(`Plugin "${name}" already exists. Use --force to overwrite.`);
|
|
7349
7875
|
}
|
|
7350
7876
|
mkdirSync7(targetDir, { recursive: true });
|
|
7351
|
-
const tmpDir =
|
|
7877
|
+
const tmpDir = join10(tmpdir5(), `xbrowser-marketplace-${Date.now()}`);
|
|
7352
7878
|
mkdirSync7(tmpDir, { recursive: true });
|
|
7353
7879
|
const realSlug = String(plugin.slug || slug);
|
|
7354
7880
|
try {
|
|
7355
7881
|
await downloadAndExtractMarketplaceTarball(baseUrl, realSlug, tmpDir, targetDir);
|
|
7356
7882
|
} finally {
|
|
7357
|
-
|
|
7883
|
+
safeCleanup5(tmpDir);
|
|
7358
7884
|
}
|
|
7359
7885
|
writeMarketplacePackageJson(plugin, slug, name, baseUrl, targetDir);
|
|
7360
7886
|
ensureIndexFile(plugin, name, targetDir);
|
|
7361
|
-
const verify =
|
|
7887
|
+
const verify = verifyPlugin5(targetDir, { metadataField: "xbrowser" });
|
|
7362
7888
|
if (!verify.valid) {
|
|
7363
|
-
|
|
7889
|
+
safeCleanup5(targetDir);
|
|
7364
7890
|
throw new Error(`Invalid marketplace plugin: ${verify.error}`);
|
|
7365
7891
|
}
|
|
7366
7892
|
const trackUrl = `${baseUrl}/api/plugins/${realSlug}/install`;
|
|
@@ -7379,12 +7905,12 @@ function isManifestArray(data) {
|
|
|
7379
7905
|
return Array.isArray(data) && data.length > 0 && typeof data[0].path === "string" && typeof data[0].content === "string";
|
|
7380
7906
|
}
|
|
7381
7907
|
function extractManifestToDir(manifest, targetDir) {
|
|
7382
|
-
if (
|
|
7383
|
-
|
|
7908
|
+
if (existsSync9(targetDir)) {
|
|
7909
|
+
rmSync5(targetDir, { recursive: true, force: true });
|
|
7384
7910
|
}
|
|
7385
7911
|
mkdirSync7(targetDir, { recursive: true });
|
|
7386
7912
|
for (const file of manifest) {
|
|
7387
|
-
const filePath =
|
|
7913
|
+
const filePath = resolve7(targetDir, file.path);
|
|
7388
7914
|
mkdirSync7(dirname2(filePath), { recursive: true });
|
|
7389
7915
|
writeFileSync9(filePath, Buffer.from(file.content, "base64"));
|
|
7390
7916
|
}
|
|
@@ -7411,21 +7937,21 @@ async function downloadAndExtractMarketplaceTarball(baseUrl, slug, tmpDir, targe
|
|
|
7411
7937
|
}
|
|
7412
7938
|
if (tarballRes.status === 302 || tarballRes.headers.get("location")) {
|
|
7413
7939
|
const redirectUrl = tarballRes.headers.get("location");
|
|
7414
|
-
const tarballPath =
|
|
7415
|
-
await
|
|
7416
|
-
const buffer =
|
|
7940
|
+
const tarballPath = join10(tmpDir, `${slug}.tar.gz`);
|
|
7941
|
+
await downloadToFile3(redirectUrl, tarballPath);
|
|
7942
|
+
const buffer = readFileSync7(tarballPath);
|
|
7417
7943
|
const manifest = tryParseAsGzippedManifest(buffer);
|
|
7418
7944
|
if (manifest) {
|
|
7419
7945
|
extractManifestToDir(manifest, targetDir);
|
|
7420
7946
|
return;
|
|
7421
7947
|
}
|
|
7422
|
-
const extractDir =
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
if (
|
|
7426
|
-
|
|
7948
|
+
const extractDir = join10(tmpDir, "extracted");
|
|
7949
|
+
extractTarGz3(tarballPath, extractDir);
|
|
7950
|
+
flattenPackageRoot3(extractDir);
|
|
7951
|
+
if (existsSync9(targetDir)) {
|
|
7952
|
+
rmSync5(targetDir, { recursive: true, force: true });
|
|
7427
7953
|
}
|
|
7428
|
-
|
|
7954
|
+
cpSync5(extractDir, targetDir, { recursive: true, force: true });
|
|
7429
7955
|
} else {
|
|
7430
7956
|
const buffer = Buffer.from(await tarballRes.arrayBuffer());
|
|
7431
7957
|
const manifest = tryParseAsGzippedManifest(buffer);
|
|
@@ -7433,16 +7959,16 @@ async function downloadAndExtractMarketplaceTarball(baseUrl, slug, tmpDir, targe
|
|
|
7433
7959
|
extractManifestToDir(manifest, targetDir);
|
|
7434
7960
|
return;
|
|
7435
7961
|
}
|
|
7436
|
-
const tarballPath =
|
|
7962
|
+
const tarballPath = join10(tmpDir, `${slug}.tar.gz`);
|
|
7437
7963
|
writeFileSync9(tarballPath, buffer);
|
|
7438
7964
|
try {
|
|
7439
|
-
const extractDir =
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
if (
|
|
7443
|
-
|
|
7965
|
+
const extractDir = join10(tmpDir, "extracted");
|
|
7966
|
+
extractTarGz3(tarballPath, extractDir);
|
|
7967
|
+
flattenPackageRoot3(extractDir);
|
|
7968
|
+
if (existsSync9(targetDir)) {
|
|
7969
|
+
rmSync5(targetDir, { recursive: true, force: true });
|
|
7444
7970
|
}
|
|
7445
|
-
|
|
7971
|
+
cpSync5(extractDir, targetDir, { recursive: true, force: true });
|
|
7446
7972
|
} catch {
|
|
7447
7973
|
throw new Error(
|
|
7448
7974
|
`Downloaded tarball for "${slug}" is neither a gzipped JSON manifest nor a valid tar.gz archive.`
|
|
@@ -7474,12 +8000,12 @@ function writeMarketplacePackageJson(plugin, slug, name, baseUrl, targetDir) {
|
|
|
7474
8000
|
url: baseUrl
|
|
7475
8001
|
}
|
|
7476
8002
|
};
|
|
7477
|
-
const pkgPath =
|
|
7478
|
-
if (!
|
|
8003
|
+
const pkgPath = resolve7(targetDir, "package.json");
|
|
8004
|
+
if (!existsSync9(pkgPath)) {
|
|
7479
8005
|
writeFileSync9(pkgPath, JSON.stringify(packageJson, null, 2));
|
|
7480
8006
|
} else {
|
|
7481
8007
|
try {
|
|
7482
|
-
const existing = JSON.parse(
|
|
8008
|
+
const existing = JSON.parse(readFileSync7(pkgPath, "utf-8"));
|
|
7483
8009
|
const merged = {
|
|
7484
8010
|
...existing,
|
|
7485
8011
|
xbrowser: { ...existing.xbrowser, ...packageJson.xbrowser },
|
|
@@ -7492,7 +8018,7 @@ function writeMarketplacePackageJson(plugin, slug, name, baseUrl, targetDir) {
|
|
|
7492
8018
|
}
|
|
7493
8019
|
}
|
|
7494
8020
|
function ensureIndexFile(plugin, name, targetDir) {
|
|
7495
|
-
if (!
|
|
8021
|
+
if (!existsSync9(resolve7(targetDir, "index.ts")) && !existsSync9(resolve7(targetDir, "index.js"))) {
|
|
7496
8022
|
const commands = plugin.commands || [];
|
|
7497
8023
|
const commandHandlers = commands.length > 0 ? commands.map((cmd) => {
|
|
7498
8024
|
return [
|
|
@@ -7508,7 +8034,7 @@ function ensureIndexFile(plugin, name, targetDir) {
|
|
|
7508
8034
|
` });`
|
|
7509
8035
|
].join("\n");
|
|
7510
8036
|
writeFileSync9(
|
|
7511
|
-
|
|
8037
|
+
resolve7(targetDir, "index.ts"),
|
|
7512
8038
|
[
|
|
7513
8039
|
`import type { XCLIAPI } from '@dyyz1993/xcli-core';`,
|
|
7514
8040
|
``,
|
|
@@ -7529,7 +8055,7 @@ function ensureIndexFile(plugin, name, targetDir) {
|
|
|
7529
8055
|
var PluginInstaller = class {
|
|
7530
8056
|
pluginsDir;
|
|
7531
8057
|
constructor(pluginsDir) {
|
|
7532
|
-
this.pluginsDir = pluginsDir ||
|
|
8058
|
+
this.pluginsDir = pluginsDir || resolve8(homedir7(), ".xbrowser/plugins");
|
|
7533
8059
|
}
|
|
7534
8060
|
getPluginsDir() {
|
|
7535
8061
|
return this.pluginsDir;
|
|
@@ -7545,8 +8071,8 @@ var PluginInstaller = class {
|
|
|
7545
8071
|
async install(source, options) {
|
|
7546
8072
|
const type = this.detectSourceType(source);
|
|
7547
8073
|
const name = options?.name || this.deriveName(source, type);
|
|
7548
|
-
const targetDir =
|
|
7549
|
-
if (
|
|
8074
|
+
const targetDir = resolve8(this.pluginsDir, name);
|
|
8075
|
+
if (existsSync10(targetDir) && !options?.force) {
|
|
7550
8076
|
throw new Error(`Plugin "${name}" already exists. Use --force to overwrite.`);
|
|
7551
8077
|
}
|
|
7552
8078
|
mkdirSync8(targetDir, { recursive: true });
|
|
@@ -7606,11 +8132,11 @@ var PluginInstaller = class {
|
|
|
7606
8132
|
* @throws If the plugin is not installed.
|
|
7607
8133
|
*/
|
|
7608
8134
|
async uninstall(name) {
|
|
7609
|
-
const targetDir =
|
|
7610
|
-
if (!
|
|
8135
|
+
const targetDir = resolve8(this.pluginsDir, name);
|
|
8136
|
+
if (!existsSync10(targetDir)) {
|
|
7611
8137
|
throw new Error(`Plugin "${name}" not found`);
|
|
7612
8138
|
}
|
|
7613
|
-
|
|
8139
|
+
rmSync6(targetDir, { recursive: true, force: true });
|
|
7614
8140
|
}
|
|
7615
8141
|
/**
|
|
7616
8142
|
* List all installed plugins with metadata.
|
|
@@ -7618,18 +8144,18 @@ var PluginInstaller = class {
|
|
|
7618
8144
|
* @returns Array of installed plugin information.
|
|
7619
8145
|
*/
|
|
7620
8146
|
async list(_options) {
|
|
7621
|
-
if (!
|
|
7622
|
-
const entries =
|
|
8147
|
+
if (!existsSync10(this.pluginsDir)) return [];
|
|
8148
|
+
const entries = readdirSync2(this.pluginsDir, { withFileTypes: true });
|
|
7623
8149
|
const plugins = [];
|
|
7624
8150
|
for (const entry of entries) {
|
|
7625
8151
|
if (!entry.isDirectory()) continue;
|
|
7626
|
-
const pluginPath =
|
|
7627
|
-
const indexPath =
|
|
7628
|
-
const indexJsPath =
|
|
7629
|
-
if (!
|
|
8152
|
+
const pluginPath = resolve8(this.pluginsDir, entry.name);
|
|
8153
|
+
const indexPath = resolve8(pluginPath, "index.ts");
|
|
8154
|
+
const indexJsPath = resolve8(pluginPath, "index.js");
|
|
8155
|
+
if (!existsSync10(indexPath) && !existsSync10(indexJsPath)) continue;
|
|
7630
8156
|
const metadata = PluginMetadataParser.parseFromPackageJson(pluginPath);
|
|
7631
8157
|
let source = "local";
|
|
7632
|
-
const pkg2 = readJsonFile(
|
|
8158
|
+
const pkg2 = readJsonFile(resolve8(pluginPath, "package.json"), {});
|
|
7633
8159
|
if (pkg2._marketplace) source = "marketplace";
|
|
7634
8160
|
else if (pkg2._npmSource) source = "npm";
|
|
7635
8161
|
else if (pkg2._gitSource) source = "git";
|
|
@@ -7653,10 +8179,10 @@ var PluginInstaller = class {
|
|
|
7653
8179
|
}
|
|
7654
8180
|
if (source.startsWith("file://")) {
|
|
7655
8181
|
const filePath = decodeURIComponent(new URL(source).pathname);
|
|
7656
|
-
if (
|
|
8182
|
+
if (existsSync10(filePath)) return "url";
|
|
7657
8183
|
}
|
|
7658
8184
|
if (source.endsWith(".git") || source.includes("github.com/")) return "git";
|
|
7659
|
-
if (
|
|
8185
|
+
if (existsSync10(resolve8(source))) return "local";
|
|
7660
8186
|
return "npm";
|
|
7661
8187
|
}
|
|
7662
8188
|
deriveName(source, type) {
|
|
@@ -7731,6 +8257,7 @@ function handlePluginHelp() {
|
|
|
7731
8257
|
" install <slug> --from-marketplace Install from marketplace",
|
|
7732
8258
|
" uninstall <name> Uninstall a plugin",
|
|
7733
8259
|
" list [--json] List installed plugins",
|
|
8260
|
+
" schema <name> [command] [--json] Show plugin contract and command forms",
|
|
7734
8261
|
" reload <name> Reload a plugin",
|
|
7735
8262
|
"",
|
|
7736
8263
|
"Examples:",
|
|
@@ -7739,6 +8266,7 @@ function handlePluginHelp() {
|
|
|
7739
8266
|
" xbrowser plugin install ./my-plugin",
|
|
7740
8267
|
" xbrowser plugin uninstall my-plugin",
|
|
7741
8268
|
" xbrowser plugin list",
|
|
8269
|
+
" xbrowser plugin schema my-plugin --json",
|
|
7742
8270
|
" xbrowser plugin reload my-plugin"
|
|
7743
8271
|
].join("\n");
|
|
7744
8272
|
}
|
|
@@ -8488,7 +9016,7 @@ var previewBuiltin = {
|
|
|
8488
9016
|
if (options.json) {
|
|
8489
9017
|
outputResult({ running: false }, "json");
|
|
8490
9018
|
} else {
|
|
8491
|
-
console.log("Daemon is not running.
|
|
9019
|
+
console.log("Daemon is not running. It will start automatically when needed.");
|
|
8492
9020
|
console.log("");
|
|
8493
9021
|
console.log("Preview is automatically available when the daemon is running.");
|
|
8494
9022
|
}
|
|
@@ -8512,9 +9040,199 @@ var previewBuiltin = {
|
|
|
8512
9040
|
}
|
|
8513
9041
|
};
|
|
8514
9042
|
|
|
9043
|
+
// src/builtins/knowledge.ts
|
|
9044
|
+
init_site_knowledge();
|
|
9045
|
+
import { existsSync as existsSync12 } from "fs";
|
|
9046
|
+
var knowledgeBuiltin = {
|
|
9047
|
+
name: "knowledge",
|
|
9048
|
+
description: "View LLM-readable site knowledge base (selectors, forms, APIs)",
|
|
9049
|
+
aliases: ["know"],
|
|
9050
|
+
help: {
|
|
9051
|
+
usage: "xbrowser knowledge <list|show|search|issue|path> [domain] [options]",
|
|
9052
|
+
description: "Manage auto-generated site knowledge from recordings. Knowledge is stored at ~/.xbrowser/knowledge/{domain}.md and is designed for LLM consumption.",
|
|
9053
|
+
options: [
|
|
9054
|
+
{ name: "list", description: "List all domains with knowledge bases" },
|
|
9055
|
+
{ name: "show <domain>", description: "Show full knowledge for a domain (markdown)" },
|
|
9056
|
+
{ name: "search <domain> <query>", description: "Search selectors/APIs by keyword" },
|
|
9057
|
+
{ name: "selectors <domain>", description: "List all selectors for a domain" },
|
|
9058
|
+
{ name: "api <domain>", description: "List all API endpoints for a domain" },
|
|
9059
|
+
{ name: "issue <domain> <text>", description: "Add a known issue to a domain" },
|
|
9060
|
+
{ name: "path <domain>", description: "Show file path for a domain's knowledge" }
|
|
9061
|
+
],
|
|
9062
|
+
examples: [
|
|
9063
|
+
{ cmd: "xbrowser knowledge list", description: "List all known sites" },
|
|
9064
|
+
{ cmd: "xbrowser knowledge show juejin.cn", description: "Show juejin.cn knowledge" },
|
|
9065
|
+
{ cmd: "xbrowser knowledge search juejin.cn publish", description: 'Find selectors related to "publish"' },
|
|
9066
|
+
{ cmd: "xbrowser knowledge selectors juejin.cn", description: "List all selectors" },
|
|
9067
|
+
{ cmd: "xbrowser knowledge api juejin.cn", description: "List API endpoints" },
|
|
9068
|
+
{ cmd: 'xbrowser knowledge issue juejin.cn "Title selector changed"', description: "Report an issue" }
|
|
9069
|
+
]
|
|
9070
|
+
},
|
|
9071
|
+
execute: async (args, _options, _ctx) => {
|
|
9072
|
+
const [subcommand, ...rest] = args;
|
|
9073
|
+
if (!subcommand || subcommand === "list") {
|
|
9074
|
+
const domains = listSiteKnowledge();
|
|
9075
|
+
if (domains.length === 0) {
|
|
9076
|
+
console.log("No site knowledge bases found.");
|
|
9077
|
+
console.log("Knowledge is auto-generated when you run `xbrowser record stop`.");
|
|
9078
|
+
return;
|
|
9079
|
+
}
|
|
9080
|
+
console.log("Site Knowledge Bases:");
|
|
9081
|
+
console.log("");
|
|
9082
|
+
for (const domain of domains) {
|
|
9083
|
+
const kb = readSiteKnowledge(domain);
|
|
9084
|
+
if (kb) {
|
|
9085
|
+
const pageCount = Object.keys(kb.pages).length;
|
|
9086
|
+
const selCount = Object.values(kb.pages).reduce((sum, p) => sum + p.selectors.length, 0);
|
|
9087
|
+
const apiCount = Object.keys(kb.apiEndpoints).length;
|
|
9088
|
+
console.log(
|
|
9089
|
+
` ${domain} \u2014 ${kb.recordingCount} recordings, ${pageCount} pages, ${selCount} selectors, ${apiCount} APIs`
|
|
9090
|
+
);
|
|
9091
|
+
}
|
|
9092
|
+
}
|
|
9093
|
+
return;
|
|
9094
|
+
}
|
|
9095
|
+
if (subcommand === "show") {
|
|
9096
|
+
const domain = rest[0];
|
|
9097
|
+
if (!domain) {
|
|
9098
|
+
console.error("Usage: xbrowser knowledge show <domain>");
|
|
9099
|
+
process.exit(1);
|
|
9100
|
+
}
|
|
9101
|
+
const md = readSiteKnowledgeMarkdown(domain);
|
|
9102
|
+
if (!md) {
|
|
9103
|
+
console.error(`No knowledge base found for ${domain}`);
|
|
9104
|
+
console.error("Run `xbrowser knowledge list` to see available domains.");
|
|
9105
|
+
process.exit(1);
|
|
9106
|
+
}
|
|
9107
|
+
console.log(md);
|
|
9108
|
+
return;
|
|
9109
|
+
}
|
|
9110
|
+
if (subcommand === "selectors") {
|
|
9111
|
+
const domain = rest[0];
|
|
9112
|
+
if (!domain) {
|
|
9113
|
+
console.error("Usage: xbrowser knowledge selectors <domain>");
|
|
9114
|
+
process.exit(1);
|
|
9115
|
+
}
|
|
9116
|
+
const kb = readSiteKnowledge(domain);
|
|
9117
|
+
if (!kb) {
|
|
9118
|
+
console.error(`No knowledge base found for ${domain}`);
|
|
9119
|
+
process.exit(1);
|
|
9120
|
+
}
|
|
9121
|
+
console.log(`Selectors for ${domain} (${kb.recordingCount} recordings):`);
|
|
9122
|
+
console.log("");
|
|
9123
|
+
for (const [pagePath, page] of Object.entries(kb.pages)) {
|
|
9124
|
+
if (page.selectors.length === 0) continue;
|
|
9125
|
+
console.log(` ${pagePath}:`);
|
|
9126
|
+
for (const sel of page.selectors) {
|
|
9127
|
+
const status = sel.status === "deprecated" ? " \u26A0\uFE0F" : "";
|
|
9128
|
+
console.log(
|
|
9129
|
+
` ${sel.selector.padEnd(30)} ${sel.tag.padEnd(8)} ${sel.actionType.padEnd(10)} ${sel.confidence.padEnd(6)} ${sel.timesSeen}x${status}`
|
|
9130
|
+
);
|
|
9131
|
+
if (sel.description) console.log(` \u2192 ${sel.description}`);
|
|
9132
|
+
}
|
|
9133
|
+
console.log("");
|
|
9134
|
+
}
|
|
9135
|
+
return;
|
|
9136
|
+
}
|
|
9137
|
+
if (subcommand === "api") {
|
|
9138
|
+
const domain = rest[0];
|
|
9139
|
+
if (!domain) {
|
|
9140
|
+
console.error("Usage: xbrowser knowledge api <domain>");
|
|
9141
|
+
process.exit(1);
|
|
9142
|
+
}
|
|
9143
|
+
const kb = readSiteKnowledge(domain);
|
|
9144
|
+
if (!kb) {
|
|
9145
|
+
console.error(`No knowledge base found for ${domain}`);
|
|
9146
|
+
process.exit(1);
|
|
9147
|
+
}
|
|
9148
|
+
const endpoints = Object.values(kb.apiEndpoints);
|
|
9149
|
+
if (endpoints.length === 0) {
|
|
9150
|
+
console.log(`No API endpoints recorded for ${domain}`);
|
|
9151
|
+
return;
|
|
9152
|
+
}
|
|
9153
|
+
console.log(`API Endpoints for ${domain}:`);
|
|
9154
|
+
console.log("");
|
|
9155
|
+
for (const ep of endpoints.sort((a, b) => b.timesSeen - a.timesSeen)) {
|
|
9156
|
+
const params = ep.params.length > 0 ? ep.params.join(", ") : "-";
|
|
9157
|
+
console.log(` ${ep.method} ${ep.path} (${ep.timesSeen}x)`);
|
|
9158
|
+
console.log(` Params: ${params}`);
|
|
9159
|
+
if (ep.responseFields.length > 0) {
|
|
9160
|
+
console.log(` Response: ${ep.responseFields.slice(0, 5).join(", ")}`);
|
|
9161
|
+
}
|
|
9162
|
+
console.log("");
|
|
9163
|
+
}
|
|
9164
|
+
return;
|
|
9165
|
+
}
|
|
9166
|
+
if (subcommand === "search") {
|
|
9167
|
+
const domain = rest[0];
|
|
9168
|
+
const query = rest.slice(1).join(" ").toLowerCase();
|
|
9169
|
+
if (!domain || !query) {
|
|
9170
|
+
console.error("Usage: xbrowser knowledge search <domain> <query>");
|
|
9171
|
+
process.exit(1);
|
|
9172
|
+
}
|
|
9173
|
+
const kb = readSiteKnowledge(domain);
|
|
9174
|
+
if (!kb) {
|
|
9175
|
+
console.error(`No knowledge base found for ${domain}`);
|
|
9176
|
+
process.exit(1);
|
|
9177
|
+
}
|
|
9178
|
+
console.log(`Search results for "${query}" in ${domain}:`);
|
|
9179
|
+
console.log("");
|
|
9180
|
+
let found = 0;
|
|
9181
|
+
for (const [pagePath, page] of Object.entries(kb.pages)) {
|
|
9182
|
+
const matches = page.selectors.filter(
|
|
9183
|
+
(s) => s.selector.toLowerCase().includes(query) || s.description.toLowerCase().includes(query) || (s.text || "").toLowerCase().includes(query)
|
|
9184
|
+
);
|
|
9185
|
+
for (const m of matches) {
|
|
9186
|
+
console.log(` [${pagePath}] ${m.selector} \u2192 ${m.description} (${m.actionType}, ${m.confidence}, ${m.timesSeen}x)`);
|
|
9187
|
+
found++;
|
|
9188
|
+
}
|
|
9189
|
+
}
|
|
9190
|
+
for (const ep of Object.values(kb.apiEndpoints)) {
|
|
9191
|
+
if (ep.path.toLowerCase().includes(query) || ep.params.some((p) => p.toLowerCase().includes(query))) {
|
|
9192
|
+
console.log(` [API] ${ep.method} ${ep.path} (${ep.timesSeen}x)`);
|
|
9193
|
+
found++;
|
|
9194
|
+
}
|
|
9195
|
+
}
|
|
9196
|
+
if (found === 0) {
|
|
9197
|
+
console.log(" No matches found.");
|
|
9198
|
+
}
|
|
9199
|
+
return;
|
|
9200
|
+
}
|
|
9201
|
+
if (subcommand === "issue") {
|
|
9202
|
+
const domain = rest[0];
|
|
9203
|
+
const text = rest.slice(1).join(" ");
|
|
9204
|
+
if (!domain || !text) {
|
|
9205
|
+
console.error("Usage: xbrowser knowledge issue <domain> <description>");
|
|
9206
|
+
process.exit(1);
|
|
9207
|
+
}
|
|
9208
|
+
if (!readSiteKnowledge(domain)) {
|
|
9209
|
+
console.error(`No knowledge base found for ${domain}`);
|
|
9210
|
+
process.exit(1);
|
|
9211
|
+
}
|
|
9212
|
+
addKnownIssue(domain, text);
|
|
9213
|
+
console.log(`Added issue to ${domain}: ${text}`);
|
|
9214
|
+
return;
|
|
9215
|
+
}
|
|
9216
|
+
if (subcommand === "path") {
|
|
9217
|
+
const domain = rest[0];
|
|
9218
|
+
if (!domain) {
|
|
9219
|
+
console.error("Usage: xbrowser knowledge path <domain>");
|
|
9220
|
+
process.exit(1);
|
|
9221
|
+
}
|
|
9222
|
+
const mdPath = getKnowledgePath(domain, "md");
|
|
9223
|
+
const jsonPath = getKnowledgePath(domain, "json");
|
|
9224
|
+
console.log(`Markdown: ${mdPath} (${existsSync12(mdPath) ? "exists" : "not found"})`);
|
|
9225
|
+
console.log(`JSON: ${jsonPath} (${existsSync12(jsonPath) ? "exists" : "not found"})`);
|
|
9226
|
+
return;
|
|
9227
|
+
}
|
|
9228
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
9229
|
+
console.error("Usage: xbrowser knowledge <list|show|selectors|api|search|issue|path>");
|
|
9230
|
+
process.exit(1);
|
|
9231
|
+
}
|
|
9232
|
+
};
|
|
9233
|
+
|
|
8515
9234
|
// src/builtins/index.ts
|
|
8516
9235
|
var allBuiltins = [
|
|
8517
|
-
sessionOpenBuiltin,
|
|
8518
9236
|
sessionCloseBuiltin,
|
|
8519
9237
|
sessionListBuiltin,
|
|
8520
9238
|
sessionKillBuiltin,
|
|
@@ -8525,7 +9243,8 @@ var allBuiltins = [
|
|
|
8525
9243
|
pluginListBuiltin,
|
|
8526
9244
|
pluginReloadBuiltin,
|
|
8527
9245
|
createBuiltin,
|
|
8528
|
-
previewBuiltin
|
|
9246
|
+
previewBuiltin,
|
|
9247
|
+
knowledgeBuiltin
|
|
8529
9248
|
];
|
|
8530
9249
|
|
|
8531
9250
|
// src/utils/selector.ts
|
|
@@ -8748,7 +9467,8 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
|
|
|
8748
9467
|
} else {
|
|
8749
9468
|
switch (command) {
|
|
8750
9469
|
case "goto":
|
|
8751
|
-
|
|
9470
|
+
case "open":
|
|
9471
|
+
if (!args[0]) outputError(`Usage: xbrowser ${command} <url>`);
|
|
8752
9472
|
cmdName = "goto";
|
|
8753
9473
|
params = {
|
|
8754
9474
|
url: /^https?:\/\//i.test(args[0]) || /^wss?:\/\//i.test(args[0]) ? args[0] : "https://" + args[0],
|
|
@@ -8760,7 +9480,9 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
|
|
|
8760
9480
|
params = {
|
|
8761
9481
|
fullPage: !!(options["full-page"] || options.fullPage),
|
|
8762
9482
|
type: options.type,
|
|
8763
|
-
selector: options.selector || options.s
|
|
9483
|
+
selector: options.selector || options.s,
|
|
9484
|
+
base64: !!options.base64,
|
|
9485
|
+
output: options.output || options.o
|
|
8764
9486
|
};
|
|
8765
9487
|
break;
|
|
8766
9488
|
case "eval":
|
|
@@ -8796,6 +9518,25 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
|
|
|
8796
9518
|
cmdName = "text";
|
|
8797
9519
|
params = { selector: options.selector || options.s };
|
|
8798
9520
|
break;
|
|
9521
|
+
case "find": {
|
|
9522
|
+
if (!args[0] || !args[1]) {
|
|
9523
|
+
outputError("Usage: xbrowser find <text|role|label|placeholder|testid|alt|title|first|last|nth> <value> [action] [--name <name>]");
|
|
9524
|
+
}
|
|
9525
|
+
const strategy = args[0];
|
|
9526
|
+
const value = args[1];
|
|
9527
|
+
const operation = args.slice(2).join(" ") || void 0;
|
|
9528
|
+
cmdName = "find";
|
|
9529
|
+
params = {
|
|
9530
|
+
strategy,
|
|
9531
|
+
value,
|
|
9532
|
+
...operation ? { operation } : {},
|
|
9533
|
+
name: options.name,
|
|
9534
|
+
exact: !!options.exact,
|
|
9535
|
+
timeout: options.timeout ? Number(options.timeout) : void 0,
|
|
9536
|
+
index: options.index ? Number(options.index) : void 0
|
|
9537
|
+
};
|
|
9538
|
+
break;
|
|
9539
|
+
}
|
|
8799
9540
|
case "back":
|
|
8800
9541
|
cmdName = "back";
|
|
8801
9542
|
params = {};
|
|
@@ -8818,7 +9559,7 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
|
|
|
8818
9559
|
let parsedActions;
|
|
8819
9560
|
if (options.action) {
|
|
8820
9561
|
const actionList = Array.isArray(options.action) ? options.action : [options.action];
|
|
8821
|
-
const { parseActionDsl } = await import("./parse-action-dsl-
|
|
9562
|
+
const { parseActionDsl } = await import("./parse-action-dsl-UM333TL2.js");
|
|
8822
9563
|
parsedActions = actionList.map((a) => parseActionDsl(a));
|
|
8823
9564
|
} else if (options["actions-file"]) {
|
|
8824
9565
|
const fs3 = await import("fs");
|
|
@@ -8946,36 +9687,27 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
|
|
|
8946
9687
|
}
|
|
8947
9688
|
|
|
8948
9689
|
// src/cli/session-routes.ts
|
|
8949
|
-
import { homedir as
|
|
8950
|
-
import { join as
|
|
8951
|
-
import { readdirSync as
|
|
9690
|
+
import { homedir as homedir8 } from "os";
|
|
9691
|
+
import { join as join12 } from "path";
|
|
9692
|
+
import { readdirSync as readdirSync3, rmSync as rmSync7 } from "fs";
|
|
8952
9693
|
function cleanSessionFiles() {
|
|
8953
|
-
const dir =
|
|
9694
|
+
const dir = join12(homedir8(), ".xbrowser", "sessions");
|
|
8954
9695
|
let count = 0;
|
|
8955
9696
|
try {
|
|
8956
|
-
for (const entry of
|
|
8957
|
-
const p =
|
|
8958
|
-
|
|
9697
|
+
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
9698
|
+
const p = join12(dir, entry.name);
|
|
9699
|
+
rmSync7(p, { recursive: true, force: true });
|
|
8959
9700
|
count++;
|
|
8960
9701
|
}
|
|
8961
9702
|
} catch {
|
|
8962
9703
|
}
|
|
8963
9704
|
return count;
|
|
8964
9705
|
}
|
|
8965
|
-
async function handleSession(args, options, mode,
|
|
9706
|
+
async function handleSession(args, options, mode, _cdpEndpoint) {
|
|
8966
9707
|
const sub = args[0];
|
|
8967
9708
|
switch (sub) {
|
|
8968
|
-
case "open": {
|
|
8969
|
-
const url = args[1];
|
|
8970
|
-
const name = options.name || process.env.XBROWSER_SESSION || "default";
|
|
8971
|
-
if (!url)
|
|
8972
|
-
outputError("Usage: xbrowser session open <url> [--name <name>] [--cdp <endpoint>]");
|
|
8973
|
-
const info = await forwardSessionCreate(name, url, cdpEndpoint);
|
|
8974
|
-
outputResult({ ok: true, ...info }, mode);
|
|
8975
|
-
break;
|
|
8976
|
-
}
|
|
8977
9709
|
case "close": {
|
|
8978
|
-
const name = options.name || process.env.XBROWSER_SESSION || "default";
|
|
9710
|
+
const name = options.session || options.name || process.env.XBROWSER_SESSION || "default";
|
|
8979
9711
|
try {
|
|
8980
9712
|
await forwardSessionClose(name);
|
|
8981
9713
|
} catch {
|
|
@@ -8987,16 +9719,16 @@ async function handleSession(args, options, mode, cdpEndpoint) {
|
|
|
8987
9719
|
case "list":
|
|
8988
9720
|
case "ls": {
|
|
8989
9721
|
try {
|
|
8990
|
-
const
|
|
8991
|
-
outputResult({ sessions }, mode);
|
|
9722
|
+
const sessions2 = await forwardSessionList();
|
|
9723
|
+
outputResult({ sessions: sessions2 }, mode);
|
|
8992
9724
|
} catch {
|
|
8993
|
-
const
|
|
8994
|
-
outputResult({ sessions }, mode);
|
|
9725
|
+
const sessions2 = await listSessions();
|
|
9726
|
+
outputResult({ sessions: sessions2 }, mode);
|
|
8995
9727
|
}
|
|
8996
9728
|
break;
|
|
8997
9729
|
}
|
|
8998
9730
|
case "kill": {
|
|
8999
|
-
const name = options.name || process.env.XBROWSER_SESSION || "default";
|
|
9731
|
+
const name = options.session || options.name || process.env.XBROWSER_SESSION || "default";
|
|
9000
9732
|
try {
|
|
9001
9733
|
await forwardSessionClose(name);
|
|
9002
9734
|
} catch {
|
|
@@ -9011,8 +9743,8 @@ async function handleSession(args, options, mode, cdpEndpoint) {
|
|
|
9011
9743
|
}
|
|
9012
9744
|
case "kill-all": {
|
|
9013
9745
|
try {
|
|
9014
|
-
const
|
|
9015
|
-
for (const s of
|
|
9746
|
+
const sessions2 = await forwardSessionList();
|
|
9747
|
+
for (const s of sessions2) {
|
|
9016
9748
|
try {
|
|
9017
9749
|
await forwardSessionClose(s.name);
|
|
9018
9750
|
} catch {
|
|
@@ -9039,15 +9771,28 @@ function getPluginLoader2() {
|
|
|
9039
9771
|
if (!pluginLoader3) pluginLoader3 = new XBrowserPluginLoader();
|
|
9040
9772
|
return pluginLoader3;
|
|
9041
9773
|
}
|
|
9042
|
-
async function
|
|
9774
|
+
async function buildRuntimePluginInfo() {
|
|
9043
9775
|
const loader = await getPluginLoader();
|
|
9044
9776
|
const sites = loader.getCore().loader.getSites();
|
|
9045
9777
|
const map = /* @__PURE__ */ new Map();
|
|
9046
9778
|
for (const site of sites) {
|
|
9047
|
-
const cmds = site.getAllCommands()
|
|
9048
|
-
|
|
9049
|
-
|
|
9779
|
+
const cmds = site.getAllCommands();
|
|
9780
|
+
const commandNames = cmds.map((c) => c.name);
|
|
9781
|
+
if (commandNames.length === 0) continue;
|
|
9782
|
+
const anySite = site;
|
|
9783
|
+
const hasLoginHandler = typeof anySite.hasLoginCommand === "function" && anySite.hasLoginCommand();
|
|
9784
|
+
const configRequiresLogin = !!site.config.requiresLogin;
|
|
9785
|
+
const hasLogin = hasLoginHandler || configRequiresLogin;
|
|
9786
|
+
let loggedIn = null;
|
|
9787
|
+
if (hasLogin) {
|
|
9788
|
+
try {
|
|
9789
|
+
loggedIn = await site.isLoggedIn();
|
|
9790
|
+
} catch {
|
|
9791
|
+
loggedIn = null;
|
|
9792
|
+
}
|
|
9050
9793
|
}
|
|
9794
|
+
const requiresLoginCommands = cmds.filter((c) => c.requiresLogin === true).map((c) => c.name);
|
|
9795
|
+
map.set(site.name, { commands: commandNames, hasLogin, loggedIn, requiresLoginCommands });
|
|
9051
9796
|
}
|
|
9052
9797
|
return map;
|
|
9053
9798
|
}
|
|
@@ -9211,6 +9956,54 @@ async function handlePluginInfo(args, options, mode) {
|
|
|
9211
9956
|
console.error("\u67E5\u8BE2\u5931\u8D25:", err.message);
|
|
9212
9957
|
}
|
|
9213
9958
|
}
|
|
9959
|
+
async function handlePluginSchema(args, mode) {
|
|
9960
|
+
const pluginName = args[0];
|
|
9961
|
+
const commandName = args[1];
|
|
9962
|
+
if (!pluginName) outputError("Usage: xbrowser plugin schema <name> [command] [--json]");
|
|
9963
|
+
const loader = await getPluginLoader();
|
|
9964
|
+
const contract = loader.getPluginContract(pluginName, commandName);
|
|
9965
|
+
if (!contract) {
|
|
9966
|
+
outputError(commandName ? `Command "${commandName}" not found in plugin "${pluginName}"` : `Plugin "${pluginName}" not found`);
|
|
9967
|
+
return;
|
|
9968
|
+
}
|
|
9969
|
+
if (mode === "json") {
|
|
9970
|
+
outputResult(contract, mode);
|
|
9971
|
+
return;
|
|
9972
|
+
}
|
|
9973
|
+
if ("commands" in contract) {
|
|
9974
|
+
printPluginContract(contract);
|
|
9975
|
+
} else {
|
|
9976
|
+
printCommandContract(pluginName, contract);
|
|
9977
|
+
}
|
|
9978
|
+
}
|
|
9979
|
+
function printPluginContract(contract) {
|
|
9980
|
+
console.log(`${contract.plugin.name} contract v${contract.version}`);
|
|
9981
|
+
if (contract.plugin.description) console.log(contract.plugin.description);
|
|
9982
|
+
console.log("");
|
|
9983
|
+
for (const command of contract.commands) {
|
|
9984
|
+
printCommandContract(contract.plugin.name, command);
|
|
9985
|
+
}
|
|
9986
|
+
}
|
|
9987
|
+
function printCommandContract(pluginName, command) {
|
|
9988
|
+
console.log(`${pluginName} ${command.name}`);
|
|
9989
|
+
if (command.description) console.log(` ${command.description}`);
|
|
9990
|
+
console.log(` scope: ${command.scope}`);
|
|
9991
|
+
if (command.capabilities.length > 0) {
|
|
9992
|
+
console.log(` capabilities: ${command.capabilities.join(", ")}`);
|
|
9993
|
+
}
|
|
9994
|
+
if (command.positional.length > 0) {
|
|
9995
|
+
console.log(` positional: ${command.positional.join(", ")}`);
|
|
9996
|
+
}
|
|
9997
|
+
if (command.form.fields.length > 0) {
|
|
9998
|
+
console.log(" fields:");
|
|
9999
|
+
for (const field of command.form.fields) {
|
|
10000
|
+
const required = field.required ? "required" : "optional";
|
|
10001
|
+
const choices = field.enum ? ` [${field.enum.join("|")}]` : "";
|
|
10002
|
+
console.log(` --${field.name}: ${field.type}/${field.widget} ${required}${choices}`);
|
|
10003
|
+
}
|
|
10004
|
+
}
|
|
10005
|
+
console.log("");
|
|
10006
|
+
}
|
|
9214
10007
|
async function handlePlugin(args, options, mode) {
|
|
9215
10008
|
const sub = args[0];
|
|
9216
10009
|
const subArgs = args.slice(1);
|
|
@@ -9251,17 +10044,20 @@ async function handlePlugin(args, options, mode) {
|
|
|
9251
10044
|
}
|
|
9252
10045
|
case "list": {
|
|
9253
10046
|
const plugins = await installer.list();
|
|
9254
|
-
const
|
|
10047
|
+
const runtimeInfo = await buildRuntimePluginInfo();
|
|
9255
10048
|
const enrichedPlugins = plugins.map((p) => {
|
|
9256
10049
|
const metadata = p.metadata;
|
|
9257
10050
|
const staticCommands = metadata?.commands;
|
|
9258
|
-
const
|
|
9259
|
-
const commands =
|
|
10051
|
+
const rt = runtimeInfo.get(p.name);
|
|
10052
|
+
const commands = rt?.commands || staticCommands;
|
|
9260
10053
|
return {
|
|
9261
10054
|
...p,
|
|
9262
10055
|
commands,
|
|
9263
10056
|
version: metadata?.version,
|
|
9264
|
-
description: metadata?.description
|
|
10057
|
+
description: metadata?.description,
|
|
10058
|
+
hasLogin: rt?.hasLogin ?? false,
|
|
10059
|
+
loggedIn: rt?.loggedIn ?? null,
|
|
10060
|
+
requiresLoginCommands: rt?.requiresLoginCommands ?? []
|
|
9265
10061
|
};
|
|
9266
10062
|
});
|
|
9267
10063
|
if (mode === "json") {
|
|
@@ -9272,20 +10068,27 @@ async function handlePlugin(args, options, mode) {
|
|
|
9272
10068
|
return;
|
|
9273
10069
|
}
|
|
9274
10070
|
for (const p of enrichedPlugins) {
|
|
10071
|
+
const loginTag = p.hasLogin ? p.loggedIn ? " [logged in]" : " [need login]" : "";
|
|
9275
10072
|
if (p.version && p.description) {
|
|
9276
|
-
console.log(`${p.name} (${p.version}) - ${p.description}`);
|
|
10073
|
+
console.log(`${p.name} (${p.version}) - ${p.description}${loginTag}`);
|
|
9277
10074
|
} else {
|
|
9278
|
-
console.log(p.name);
|
|
10075
|
+
console.log(`${p.name}${loginTag}`);
|
|
9279
10076
|
}
|
|
9280
10077
|
if (p.commands && p.commands.length > 0) {
|
|
9281
10078
|
console.log(` ${p.commands.join(", ")}`);
|
|
9282
10079
|
}
|
|
10080
|
+
if (p.requiresLoginCommands.length > 0) {
|
|
10081
|
+
console.log(` requires login: ${p.requiresLoginCommands.join(", ")}`);
|
|
10082
|
+
}
|
|
9283
10083
|
}
|
|
9284
10084
|
console.log(`
|
|
9285
10085
|
Total: ${enrichedPlugins.length} plugins`);
|
|
9286
10086
|
}
|
|
9287
10087
|
break;
|
|
9288
10088
|
}
|
|
10089
|
+
case "schema":
|
|
10090
|
+
await handlePluginSchema(subArgs, mode);
|
|
10091
|
+
break;
|
|
9289
10092
|
case "reload": {
|
|
9290
10093
|
const name = subArgs[0];
|
|
9291
10094
|
if (!name) outputError("Usage: xbrowser plugin reload <name>");
|
|
@@ -9343,7 +10146,7 @@ function handleDaemon(args, options, mode) {
|
|
|
9343
10146
|
break;
|
|
9344
10147
|
}
|
|
9345
10148
|
default:
|
|
9346
|
-
console.log("
|
|
10149
|
+
console.log("Daemon starts automatically. No manual action needed.");
|
|
9347
10150
|
}
|
|
9348
10151
|
}
|
|
9349
10152
|
|
|
@@ -9354,7 +10157,8 @@ async function handleRecord(args, options, mode) {
|
|
|
9354
10157
|
case "start": {
|
|
9355
10158
|
const url = options.url;
|
|
9356
10159
|
const sessionName = options.session || "default";
|
|
9357
|
-
const
|
|
10160
|
+
const cdpEndpoint = options.cdp;
|
|
10161
|
+
const result = await forwardRecordStart(sessionName, url, cdpEndpoint);
|
|
9358
10162
|
if (!result.ok) {
|
|
9359
10163
|
outputError(String(result.error || "Failed to start recording"));
|
|
9360
10164
|
return;
|
|
@@ -9438,6 +10242,13 @@ async function handleRecord(args, options, mode) {
|
|
|
9438
10242
|
outputResult(result, mode);
|
|
9439
10243
|
break;
|
|
9440
10244
|
}
|
|
10245
|
+
case "generate-plugin": {
|
|
10246
|
+
const sessionName = options.session || args[1] || "default";
|
|
10247
|
+
const pluginName = options.name || "";
|
|
10248
|
+
const outputDir = options.output || "";
|
|
10249
|
+
await handleGeneratePlugin(sessionName, pluginName, outputDir);
|
|
10250
|
+
break;
|
|
10251
|
+
}
|
|
9441
10252
|
default:
|
|
9442
10253
|
console.log("Usage:");
|
|
9443
10254
|
console.log(" xbrowser record start [--url <url>] [--session <name>]");
|
|
@@ -9445,6 +10256,7 @@ async function handleRecord(args, options, mode) {
|
|
|
9445
10256
|
console.log(" xbrowser record status [--session <name>]");
|
|
9446
10257
|
console.log(" xbrowser record summary [--session <name>] [--json]");
|
|
9447
10258
|
console.log(' xbrowser record checkpoint --type <type> --hint "description" [--selector <sel>] [--session <name>]');
|
|
10259
|
+
console.log(" xbrowser record generate-plugin [--session <name>] [--name <plugin>] [--output <dir>]");
|
|
9448
10260
|
console.log("");
|
|
9449
10261
|
console.log("Checkpoint types: dialog, captcha, login, iframe, slider, custom");
|
|
9450
10262
|
}
|
|
@@ -9633,7 +10445,7 @@ async function handleConvert(args, _mode) {
|
|
|
9633
10445
|
const fs3 = await import("fs");
|
|
9634
10446
|
const path3 = await import("path");
|
|
9635
10447
|
const { default: yaml } = await import("yaml");
|
|
9636
|
-
const { generateJSScript, generatePythonScript, generateBashScript } = await import("./convert-
|
|
10448
|
+
const { generateJSScript, generatePythonScript, generateBashScript } = await import("./convert-LB3GJTLR.js");
|
|
9637
10449
|
const content = fs3.readFileSync(filePath, "utf-8");
|
|
9638
10450
|
const recording = yaml.parse(content);
|
|
9639
10451
|
const ext = path3.extname(outputPath).toLowerCase();
|
|
@@ -9658,7 +10470,7 @@ async function handleExtract(args, _mode) {
|
|
|
9658
10470
|
console.error("Usage: xbrowser extract <recording.yaml>");
|
|
9659
10471
|
process.exit(1);
|
|
9660
10472
|
}
|
|
9661
|
-
const { extractAndSave, printExtractSummary } = await import("./extract-
|
|
10473
|
+
const { extractAndSave, printExtractSummary } = await import("./extract-BSYBM4MR.js");
|
|
9662
10474
|
const { summary, outputPath } = extractAndSave(filePath);
|
|
9663
10475
|
printExtractSummary(summary);
|
|
9664
10476
|
console.log(`
|
|
@@ -9671,33 +10483,196 @@ async function handleFilter(args, _mode) {
|
|
|
9671
10483
|
console.error("Usage: xbrowser filter <input.yaml> <output.yaml> [--exclude-types=type1,type2]");
|
|
9672
10484
|
process.exit(1);
|
|
9673
10485
|
}
|
|
9674
|
-
const { filterRecording, parseExcludeTypes } = await import("./filter-
|
|
10486
|
+
const { filterRecording, parseExcludeTypes } = await import("./filter-KCFO4RSV.js");
|
|
9675
10487
|
const excludeTypes = parseExcludeTypes(args.slice(2));
|
|
9676
10488
|
const result = filterRecording(filePath, outputPath, excludeTypes);
|
|
9677
10489
|
console.log(`Filtered ${filePath} -> ${outputPath}`);
|
|
9678
10490
|
console.log(` Original: ${result.originalCount}, After: ${result.filteredCount}, Removed: ${result.removed} (${result.percentage}%)`);
|
|
9679
10491
|
}
|
|
9680
|
-
|
|
9681
|
-
|
|
9682
|
-
|
|
9683
|
-
|
|
9684
|
-
|
|
9685
|
-
|
|
9686
|
-
|
|
9687
|
-
|
|
9688
|
-
|
|
9689
|
-
|
|
9690
|
-
|
|
9691
|
-
|
|
10492
|
+
async function handleGeneratePlugin(sessionName, pluginName, outputDir) {
|
|
10493
|
+
const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-RTDGURIJ.js");
|
|
10494
|
+
const { readSiteKnowledge: readSiteKnowledge2, toMarkdown } = await import("./site-knowledge-SYC6VCDB.js");
|
|
10495
|
+
const { mkdirSync: mkdirSync10, writeFileSync: writeFileSync11 } = await import("fs");
|
|
10496
|
+
const { join: join13 } = await import("path");
|
|
10497
|
+
const data = SessionRecorder2.readData(sessionName);
|
|
10498
|
+
if (!data) {
|
|
10499
|
+
outputError(`No recording found for session "${sessionName}". Run \`xbrowser record stop --session ${sessionName}\` first.`);
|
|
10500
|
+
return;
|
|
10501
|
+
}
|
|
10502
|
+
let domain = "unknown";
|
|
10503
|
+
try {
|
|
10504
|
+
domain = new URL(data.startUrl).hostname.replace(/^www\./, "");
|
|
10505
|
+
} catch {
|
|
10506
|
+
}
|
|
10507
|
+
const finalPluginName = pluginName || domain.split(".")[0] || "my-site";
|
|
10508
|
+
const finalOutputDir = outputDir || join13(process.cwd(), ".xcli", "plugins", finalPluginName);
|
|
10509
|
+
const knowledge = readSiteKnowledge2(domain);
|
|
10510
|
+
const knowledgeMd = knowledge ? toMarkdown(knowledge) : "";
|
|
10511
|
+
const pluginCode = generatePluginCode(finalPluginName, domain, data, knowledgeMd);
|
|
10512
|
+
mkdirSync10(join13(finalOutputDir), { recursive: true });
|
|
10513
|
+
writeFileSync11(join13(finalOutputDir, "index.ts"), pluginCode, "utf-8");
|
|
10514
|
+
if (knowledgeMd) {
|
|
10515
|
+
writeFileSync11(join13(finalOutputDir, "SITE_KNOWLEDGE.md"), knowledgeMd, "utf-8");
|
|
10516
|
+
}
|
|
10517
|
+
console.log("");
|
|
10518
|
+
console.log("=== Plugin Generated ===");
|
|
10519
|
+
console.log(` Plugin: ${finalPluginName}`);
|
|
10520
|
+
console.log(` Domain: ${domain}`);
|
|
10521
|
+
console.log(` Output: ${finalOutputDir}/index.ts`);
|
|
10522
|
+
if (knowledgeMd) {
|
|
10523
|
+
console.log(` Knowledge: ${finalOutputDir}/SITE_KNOWLEDGE.md`);
|
|
10524
|
+
}
|
|
10525
|
+
console.log(` Actions: ${data.actions.length}`);
|
|
10526
|
+
console.log(` APIs: ${data.network.filter((n) => n.contentType.includes("json") || n.url.includes("/api/")).length}`);
|
|
10527
|
+
console.log("");
|
|
10528
|
+
console.log("Next steps:");
|
|
10529
|
+
console.log(` 1. Review and edit: ${finalOutputDir}/index.ts`);
|
|
10530
|
+
console.log(` 2. Test: xbrowser ${finalPluginName} <command>`);
|
|
10531
|
+
console.log(` 3. Reference: ${finalOutputDir}/SITE_KNOWLEDGE.md (for LLM)`);
|
|
10532
|
+
}
|
|
10533
|
+
function generatePluginCode(pluginName, domain, data, _knowledgeMd) {
|
|
10534
|
+
const pagePaths = /* @__PURE__ */ new Set();
|
|
10535
|
+
for (const action of data.actions) {
|
|
10536
|
+
if (action.url) {
|
|
10537
|
+
try {
|
|
10538
|
+
pagePaths.add(new URL(action.url).pathname);
|
|
10539
|
+
} catch {
|
|
10540
|
+
}
|
|
10541
|
+
}
|
|
10542
|
+
}
|
|
10543
|
+
const clickSelectors = [];
|
|
10544
|
+
const inputSelectors = [];
|
|
10545
|
+
for (const action of data.actions) {
|
|
10546
|
+
const el = action.element;
|
|
10547
|
+
if (!el) continue;
|
|
10548
|
+
const sel = el.selector;
|
|
10549
|
+
if (!sel) continue;
|
|
10550
|
+
if (action.type === "click" && !clickSelectors.includes(sel)) {
|
|
10551
|
+
clickSelectors.push(sel);
|
|
9692
10552
|
}
|
|
10553
|
+
if (action.type === "input" && !inputSelectors.some((s) => s.selector === sel)) {
|
|
10554
|
+
inputSelectors.push({ selector: sel, placeholder: el.placeholder });
|
|
10555
|
+
}
|
|
10556
|
+
}
|
|
10557
|
+
const apis = data.network.filter(
|
|
10558
|
+
(n) => (n.contentType || "").includes("json") || n.url.includes("/api/")
|
|
10559
|
+
);
|
|
10560
|
+
const commands = [];
|
|
10561
|
+
commands.push(` site.command({
|
|
10562
|
+
name: 'open',
|
|
10563
|
+
description: 'Open ${domain}',
|
|
10564
|
+
scope: 'browser',
|
|
10565
|
+
handler: async (_p: Record<string, unknown>, ctx: CommandContext) => {
|
|
10566
|
+
const page = ensurePage(ctx);
|
|
10567
|
+
await page.goto('${data.startUrl}', { waitUntil: 'domcontentloaded' });
|
|
10568
|
+
return ok({ url: page.url() });
|
|
10569
|
+
},
|
|
10570
|
+
});`);
|
|
10571
|
+
if (inputSelectors.length > 0) {
|
|
10572
|
+
const params = inputSelectors.slice(0, 5).map(
|
|
10573
|
+
(s, i) => ` ${s.selector.replace(/[^a-zA-Z0-9]/g, "_").replace(/^_+/, "").toLowerCase() || `field${i}`}: z.string().describe('${s.placeholder || s.selector}'),`
|
|
10574
|
+
).join("\n");
|
|
10575
|
+
const fills = inputSelectors.slice(0, 5).map(
|
|
10576
|
+
(s) => ` await page.fill('${s.selector}', p.${s.selector.replace(/[^a-zA-Z0-9]/g, "_").replace(/^_+/, "").toLowerCase() || "field0"}');`
|
|
10577
|
+
).join("\n");
|
|
10578
|
+
commands.push(` site.command({
|
|
10579
|
+
name: 'fill',
|
|
10580
|
+
description: 'Fill form on ${domain}',
|
|
10581
|
+
scope: 'page',
|
|
10582
|
+
parameters: z.object({
|
|
10583
|
+
${params}
|
|
10584
|
+
}),
|
|
10585
|
+
handler: async (p: Record<string, unknown>, ctx: CommandContext) => {
|
|
10586
|
+
const page = ensurePage(ctx);
|
|
10587
|
+
${fills}
|
|
10588
|
+
return ok({ filled: true });
|
|
10589
|
+
},
|
|
10590
|
+
});`);
|
|
10591
|
+
}
|
|
10592
|
+
if (clickSelectors.length > 0) {
|
|
10593
|
+
commands.push(` site.command({
|
|
10594
|
+
name: 'click',
|
|
10595
|
+
description: 'Click element on ${domain}',
|
|
10596
|
+
scope: 'page',
|
|
10597
|
+
parameters: z.object({
|
|
10598
|
+
selector: z.string().describe('CSS selector of element to click'),
|
|
10599
|
+
}),
|
|
10600
|
+
handler: async (p: Record<string, unknown>, ctx: CommandContext) => {
|
|
10601
|
+
const page = ensurePage(ctx);
|
|
10602
|
+
await page.click(p.selector as string);
|
|
10603
|
+
return ok({ clicked: p.selector });
|
|
10604
|
+
},
|
|
10605
|
+
});`);
|
|
10606
|
+
}
|
|
10607
|
+
if (apis.length > 0) {
|
|
10608
|
+
commands.push(` site.command({
|
|
10609
|
+
name: 'scrape',
|
|
10610
|
+
description: 'Scrape data from ${domain}',
|
|
10611
|
+
scope: 'page',
|
|
10612
|
+
handler: async (_p: Record<string, unknown>, ctx: CommandContext) => {
|
|
10613
|
+
const page = ensurePage(ctx);
|
|
10614
|
+
const data = await page.evaluate(() => {
|
|
10615
|
+
return {
|
|
10616
|
+
title: document.title,
|
|
10617
|
+
url: location.href,
|
|
10618
|
+
content: document.body?.innerText?.substring(0, 5000) || '',
|
|
10619
|
+
};
|
|
10620
|
+
});
|
|
10621
|
+
return ok(data);
|
|
10622
|
+
},
|
|
10623
|
+
});`);
|
|
9693
10624
|
}
|
|
9694
|
-
return
|
|
10625
|
+
return `/**
|
|
10626
|
+
* ${pluginName} \u2014 Auto-generated plugin for ${domain}
|
|
10627
|
+
*
|
|
10628
|
+
* Generated from xbrowser recording session.
|
|
10629
|
+
* Review and customize before using in production.
|
|
10630
|
+
*
|
|
10631
|
+
* Site Knowledge: See SITE_KNOWLEDGE.md for LLM-readable selector/API reference.
|
|
10632
|
+
*/
|
|
10633
|
+
|
|
10634
|
+
import { z } from 'zod';
|
|
10635
|
+
import { ok } from '@dyyz1993/xcli-core';
|
|
10636
|
+
import { createSite, type CommandContext } from '@dyyz1993/xcli-core';
|
|
10637
|
+
|
|
10638
|
+
interface XBPage {
|
|
10639
|
+
url(): string;
|
|
10640
|
+
goto(url: string, opts?: Record<string, unknown>): Promise<unknown>;
|
|
10641
|
+
click(selector: string, opts?: Record<string, unknown>): Promise<unknown>;
|
|
10642
|
+
fill(selector: string, value: string, opts?: Record<string, unknown>): Promise<unknown>;
|
|
10643
|
+
evaluate<T>(fn: string | (() => T)): Promise<T>;
|
|
10644
|
+
}
|
|
10645
|
+
|
|
10646
|
+
function ensurePage(ctx: CommandContext): XBPage {
|
|
10647
|
+
const page = (ctx as Record<string, unknown>).page;
|
|
10648
|
+
if (!page) throw new Error('No active page. Start a session first.');
|
|
10649
|
+
return page as unknown as XBPage;
|
|
9695
10650
|
}
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
10651
|
+
|
|
10652
|
+
export default createSite({
|
|
10653
|
+
name: '${pluginName}',
|
|
10654
|
+
domain: '${domain}',
|
|
10655
|
+
description: 'Auto-generated plugin for ${domain} from recording',
|
|
10656
|
+
|
|
10657
|
+
login: {
|
|
10658
|
+
url: '${data.startUrl}',
|
|
10659
|
+
detect: async (ctx: CommandContext) => {
|
|
10660
|
+
const page = ensurePage(ctx);
|
|
10661
|
+
// TODO: Add login detection logic
|
|
10662
|
+
return false;
|
|
10663
|
+
},
|
|
10664
|
+
},
|
|
10665
|
+
|
|
10666
|
+
setup(site) {
|
|
10667
|
+
${commands.join("\n\n")}
|
|
10668
|
+
},
|
|
10669
|
+
});
|
|
10670
|
+
`;
|
|
9699
10671
|
}
|
|
9700
10672
|
|
|
10673
|
+
// src/stdin.ts
|
|
10674
|
+
import { readStdin as readStdin2, readCommandFile, splitFileLine } from "@dyyz1993/xcli-core";
|
|
10675
|
+
|
|
9701
10676
|
// src/cli/run-routes.ts
|
|
9702
10677
|
async function handleRun(filePath, options) {
|
|
9703
10678
|
let commands;
|
|
@@ -9741,10 +10716,10 @@ async function handleRun(filePath, options) {
|
|
|
9741
10716
|
async function handleViewer(_args, options, mode, _cdpEndpoint) {
|
|
9742
10717
|
const name = options.name || process.env.XBROWSER_SESSION || "default";
|
|
9743
10718
|
const selector = options.selector;
|
|
9744
|
-
|
|
10719
|
+
let status = getDaemonProcessStatus();
|
|
9745
10720
|
if (!status.running) {
|
|
9746
|
-
|
|
9747
|
-
|
|
10721
|
+
await startDaemonProcess();
|
|
10722
|
+
status = getDaemonProcessStatus();
|
|
9748
10723
|
}
|
|
9749
10724
|
const port = status.port || getDaemonConfig().basePort;
|
|
9750
10725
|
let url = `http://localhost:${port}/preview/${name}`;
|
|
@@ -10064,6 +11039,235 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
10064
11039
|
}
|
|
10065
11040
|
}
|
|
10066
11041
|
|
|
11042
|
+
// src/cli/test-routes.ts
|
|
11043
|
+
import { execSync as execSync3 } from "child_process";
|
|
11044
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
11045
|
+
import { resolve as resolve9 } from "path";
|
|
11046
|
+
function findPluginPath(plugin) {
|
|
11047
|
+
const candidates = [
|
|
11048
|
+
resolve9(process.cwd(), ".xcli/plugins", plugin, "index.ts"),
|
|
11049
|
+
resolve9(process.cwd(), "node_modules/@xbrowser/cli/.xcli/plugins", plugin, "index.ts")
|
|
11050
|
+
];
|
|
11051
|
+
for (const p of candidates) {
|
|
11052
|
+
try {
|
|
11053
|
+
readFileSync8(p, "utf-8");
|
|
11054
|
+
return p;
|
|
11055
|
+
} catch {
|
|
11056
|
+
}
|
|
11057
|
+
}
|
|
11058
|
+
return resolve9(process.cwd(), ".xcli/plugins", plugin, "index.ts");
|
|
11059
|
+
}
|
|
11060
|
+
function extractSchema(plugin, command) {
|
|
11061
|
+
const pluginPath = findPluginPath(plugin);
|
|
11062
|
+
let src;
|
|
11063
|
+
try {
|
|
11064
|
+
src = readFileSync8(pluginPath, "utf-8");
|
|
11065
|
+
} catch {
|
|
11066
|
+
return null;
|
|
11067
|
+
}
|
|
11068
|
+
const cmdIdx = src.indexOf(`.command('${command}'`);
|
|
11069
|
+
if (cmdIdx < 0) return null;
|
|
11070
|
+
const after = src.slice(cmdIdx);
|
|
11071
|
+
const resultIdx = after.indexOf("result:");
|
|
11072
|
+
if (resultIdx < 0) return null;
|
|
11073
|
+
let block = after.slice(resultIdx + 7);
|
|
11074
|
+
let depth = 0;
|
|
11075
|
+
let schemaStr = "";
|
|
11076
|
+
for (const ch of block) {
|
|
11077
|
+
if (ch === "{" || ch === "(" || ch === "[") depth++;
|
|
11078
|
+
if (ch === "}" || ch === ")" || ch === "]") {
|
|
11079
|
+
depth--;
|
|
11080
|
+
if (depth < 0) break;
|
|
11081
|
+
}
|
|
11082
|
+
if (depth === 0 && /\w/.test(ch) && schemaStr.trim().endsWith(",")) {
|
|
11083
|
+
break;
|
|
11084
|
+
}
|
|
11085
|
+
schemaStr += ch;
|
|
11086
|
+
}
|
|
11087
|
+
const objStart = schemaStr.indexOf("z.object({");
|
|
11088
|
+
const objEnd = objStart >= 0 ? schemaStr.indexOf("})", objStart) : -1;
|
|
11089
|
+
const objStr = objStart >= 0 && objEnd > objStart ? schemaStr.slice(objStart + 10, objEnd) : schemaStr;
|
|
11090
|
+
const fields = [];
|
|
11091
|
+
const SKIP_NAMES = /* @__PURE__ */ new Set(["passthrough", "optional", "describe", "default"]);
|
|
11092
|
+
const fieldRegex = /(\w+)\s*:\s*z\.(\w+)/g;
|
|
11093
|
+
let match;
|
|
11094
|
+
while ((match = fieldRegex.exec(objStr)) !== null) {
|
|
11095
|
+
const name = match[1];
|
|
11096
|
+
const type = match[2];
|
|
11097
|
+
if (SKIP_NAMES.has(name) || type === "union" || type === "enum") continue;
|
|
11098
|
+
const afterField = objStr.slice(match.index + match[0].length);
|
|
11099
|
+
const isOptional = afterField.trimStart().startsWith(".optional()");
|
|
11100
|
+
fields.push({
|
|
11101
|
+
name,
|
|
11102
|
+
type: type === "string" ? "string" : type === "number" ? "number" : type === "boolean" ? "boolean" : type === "array" ? "array" : type,
|
|
11103
|
+
optional: isOptional || name === "index"
|
|
11104
|
+
});
|
|
11105
|
+
}
|
|
11106
|
+
return fields.length > 0 ? fields : null;
|
|
11107
|
+
}
|
|
11108
|
+
async function runTest(plugin, command, cmdArgs, options) {
|
|
11109
|
+
const cdp = options.cdp || options.cdpEndpoint || "http://localhost:9221";
|
|
11110
|
+
const argsStr = cmdArgs.filter((a) => !a.startsWith("--cdp")).join(" ");
|
|
11111
|
+
const schema = extractSchema(plugin, command);
|
|
11112
|
+
const fullCmd = `npx xbrowser ${plugin} ${command} ${argsStr} --cdp ${cdp} --json --timeout 60000`;
|
|
11113
|
+
let stdout = "";
|
|
11114
|
+
try {
|
|
11115
|
+
stdout = execSync3(fullCmd, {
|
|
11116
|
+
timeout: 65e3,
|
|
11117
|
+
encoding: "utf-8",
|
|
11118
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
11119
|
+
env: { ...process.env, FORCE_COLOR: "0" }
|
|
11120
|
+
});
|
|
11121
|
+
} catch (e) {
|
|
11122
|
+
const err = e;
|
|
11123
|
+
stdout = err.stdout?.toString() || "";
|
|
11124
|
+
const jsonLine = stdout.split("\n").find((l) => {
|
|
11125
|
+
try {
|
|
11126
|
+
JSON.parse(l);
|
|
11127
|
+
return true;
|
|
11128
|
+
} catch {
|
|
11129
|
+
return false;
|
|
11130
|
+
}
|
|
11131
|
+
});
|
|
11132
|
+
if (jsonLine) {
|
|
11133
|
+
try {
|
|
11134
|
+
const parsed2 = JSON.parse(jsonLine);
|
|
11135
|
+
const code = parsed2?.data?.code || "";
|
|
11136
|
+
if (code === "LOGIN_REQUIRED") {
|
|
11137
|
+
return { status: "LOGIN_REQUIRED", message: parsed2.message || "\u9700\u8981\u767B\u5F55", viewerUrl: "http://localhost:9224/preview/default" };
|
|
11138
|
+
}
|
|
11139
|
+
} catch {
|
|
11140
|
+
}
|
|
11141
|
+
}
|
|
11142
|
+
const stderr = err.stderr?.toString() || "";
|
|
11143
|
+
if (stdout.includes("captcha") || stderr.includes("captcha") || stdout.includes("CAPTCHA")) {
|
|
11144
|
+
return { status: "CAPTCHA", message: "\u68C0\u6D4B\u5230\u9A8C\u8BC1\u7801", viewerUrl: "http://localhost:9224/preview/default" };
|
|
11145
|
+
}
|
|
11146
|
+
return { status: "EXEC_ERROR", message: (err.message || "").slice(0, 200) || "\u6267\u884C\u5931\u8D25" };
|
|
11147
|
+
}
|
|
11148
|
+
const allLines = stdout.split("\n");
|
|
11149
|
+
const jsonStart = allLines.findIndex((l) => l.trim().startsWith("{"));
|
|
11150
|
+
const jsonStr = jsonStart >= 0 ? allLines.slice(jsonStart).join("\n") : "";
|
|
11151
|
+
let parsed = {};
|
|
11152
|
+
try {
|
|
11153
|
+
parsed = JSON.parse(jsonStr);
|
|
11154
|
+
} catch {
|
|
11155
|
+
return { status: "EXEC_ERROR", message: "\u65E0\u6CD5\u89E3\u6790 CLI \u8F93\u51FA" };
|
|
11156
|
+
}
|
|
11157
|
+
const rawData = parsed.data;
|
|
11158
|
+
const rawTips = parsed.tips || [];
|
|
11159
|
+
if (parsed.success === false) {
|
|
11160
|
+
const code = rawData?.code || "";
|
|
11161
|
+
if (code === "LOGIN_REQUIRED") {
|
|
11162
|
+
return { status: "LOGIN_REQUIRED", message: parsed.message || "\u9700\u8981\u767B\u5F55", viewerUrl: "http://localhost:9224/preview/default" };
|
|
11163
|
+
}
|
|
11164
|
+
if (rawTips.join(" ").includes("captcha") || rawTips.join(" ").includes("CAPTCHA")) {
|
|
11165
|
+
return { status: "CAPTCHA", message: parsed.message || "\u9A8C\u8BC1\u7801", viewerUrl: "http://localhost:9224/preview/default" };
|
|
11166
|
+
}
|
|
11167
|
+
}
|
|
11168
|
+
const data = rawData;
|
|
11169
|
+
if (data === null || data === void 0) {
|
|
11170
|
+
const msg = parsed.message || "";
|
|
11171
|
+
const tips = rawTips.join(" ");
|
|
11172
|
+
const viewerUrl = parsed.viewerUrl || rawData?.viewerUrl || "";
|
|
11173
|
+
let status = "NO_DATA";
|
|
11174
|
+
if (msg.includes("block") || msg.includes("anti-bot") || msg.includes("captcha") || tips.includes("viewer")) {
|
|
11175
|
+
status = "BLOCKED";
|
|
11176
|
+
} else if (msg.includes("\u767B\u5F55") || msg.includes("login")) {
|
|
11177
|
+
status = "LOGIN_REQUIRED";
|
|
11178
|
+
}
|
|
11179
|
+
const ret = { status, message: msg || "\u6682\u65E0\u6570\u636E" };
|
|
11180
|
+
if (viewerUrl) ret.viewerUrl = viewerUrl;
|
|
11181
|
+
return ret;
|
|
11182
|
+
}
|
|
11183
|
+
if (!schema) {
|
|
11184
|
+
return { status: "OK", note: "\u65E0 result schema", data: JSON.stringify(data).slice(0, 200) };
|
|
11185
|
+
}
|
|
11186
|
+
const errors = [];
|
|
11187
|
+
const items = Array.isArray(data) ? data.slice(0, 3) : [data];
|
|
11188
|
+
for (const item of items) {
|
|
11189
|
+
if (typeof item !== "object" || item === null) {
|
|
11190
|
+
errors.push("\u6570\u636E\u9879\u4E0D\u662F\u5BF9\u8C61");
|
|
11191
|
+
continue;
|
|
11192
|
+
}
|
|
11193
|
+
for (const field of schema) {
|
|
11194
|
+
const val = item[field.name];
|
|
11195
|
+
if (val === void 0) {
|
|
11196
|
+
if (!field.optional) errors.push(`\u7F3A\u5C11: ${field.name}`);
|
|
11197
|
+
continue;
|
|
11198
|
+
}
|
|
11199
|
+
if (field.type === "array") {
|
|
11200
|
+
if (!Array.isArray(val)) errors.push(`${field.name}: \u671F\u671B array`);
|
|
11201
|
+
} else if (typeof val !== field.type) {
|
|
11202
|
+
errors.push(`${field.name}: \u671F\u671B ${field.type}, \u5B9E\u9645 ${typeof val}`);
|
|
11203
|
+
}
|
|
11204
|
+
}
|
|
11205
|
+
}
|
|
11206
|
+
if (errors.length > 0) {
|
|
11207
|
+
return { status: "SCHEMA_ERROR", errors, data: JSON.stringify(data).slice(0, 200) };
|
|
11208
|
+
}
|
|
11209
|
+
const count = Array.isArray(data) ? data.length : 1;
|
|
11210
|
+
return { status: "OK", count, data: JSON.stringify(data).slice(0, 200) };
|
|
11211
|
+
}
|
|
11212
|
+
async function handleTest(cmdArgs, options, mode, cdpEndpoint) {
|
|
11213
|
+
const plugin = cmdArgs[0];
|
|
11214
|
+
const command = cmdArgs[1];
|
|
11215
|
+
if (!plugin || !command) {
|
|
11216
|
+
console.error("\u7528\u6CD5: xbrowser test <plugin> <command> [\u53C2\u6570...]");
|
|
11217
|
+
console.error("\u793A\u4F8B: xbrowser test doubao list --cdp 9221");
|
|
11218
|
+
return;
|
|
11219
|
+
}
|
|
11220
|
+
const loader = await getPluginLoader();
|
|
11221
|
+
const internalLoader = loader.getCore().loader;
|
|
11222
|
+
const site = internalLoader.getSite(plugin);
|
|
11223
|
+
if (!site) {
|
|
11224
|
+
console.error(`\u63D2\u4EF6 "${plugin}" \u4E0D\u5B58\u5728`);
|
|
11225
|
+
return;
|
|
11226
|
+
}
|
|
11227
|
+
const cmdEntry = site.getCommand(command);
|
|
11228
|
+
if (!cmdEntry) {
|
|
11229
|
+
console.error(`\u6307\u4EE4 "${command}" \u4E0D\u5B58\u5728`);
|
|
11230
|
+
return;
|
|
11231
|
+
}
|
|
11232
|
+
const testArgs = cmdArgs.slice(2);
|
|
11233
|
+
const mergedOptions = { ...options, cdp: cdpEndpoint || options.cdp };
|
|
11234
|
+
const result = await runTest(plugin, command, testArgs, mergedOptions);
|
|
11235
|
+
if (mode === "json") {
|
|
11236
|
+
console.log(JSON.stringify(result, null, 2));
|
|
11237
|
+
return;
|
|
11238
|
+
}
|
|
11239
|
+
const r = result;
|
|
11240
|
+
const icons = {
|
|
11241
|
+
OK: "\u2705",
|
|
11242
|
+
LOGIN_REQUIRED: "\u{1F511}",
|
|
11243
|
+
CAPTCHA: "\u{1F6A8}",
|
|
11244
|
+
SCHEMA_ERROR: "\u274C",
|
|
11245
|
+
BLOCKED: "\u{1F6A7}",
|
|
11246
|
+
NO_DATA: "\u{1F4ED}",
|
|
11247
|
+
EXEC_ERROR: "\u{1F4A5}"
|
|
11248
|
+
};
|
|
11249
|
+
const status = String(r.status);
|
|
11250
|
+
const icon = icons[status] || "\u2753";
|
|
11251
|
+
console.log(`
|
|
11252
|
+
${icon} ${plugin}.${command}`);
|
|
11253
|
+
console.log(` \u72B6\u6001: ${status}`);
|
|
11254
|
+
if (status === "OK") {
|
|
11255
|
+
if (r.count) console.log(` \u6570\u636E: ${r.count} \u9879`);
|
|
11256
|
+
if (r.data) console.log(` \u9884\u89C8: ${String(r.data).slice(0, 150)}`);
|
|
11257
|
+
} else if (status === "LOGIN_REQUIRED" || status === "CAPTCHA") {
|
|
11258
|
+
console.log(` \u4FE1\u606F: ${String(r.message)}`);
|
|
11259
|
+
console.log(` Viewer: ${String(r.viewerUrl)}`);
|
|
11260
|
+
} else if (status === "SCHEMA_ERROR") {
|
|
11261
|
+
const errs = r.errors;
|
|
11262
|
+
if (errs) console.log(` \u9519\u8BEF: ${errs.join("; ")}`);
|
|
11263
|
+
} else if (["NO_DATA", "BLOCKED"].includes(status)) {
|
|
11264
|
+
console.log(` \u4FE1\u606F: ${String(r.message)}`);
|
|
11265
|
+
if (r.viewerUrl) console.log(` Viewer: ${String(r.viewerUrl)}`);
|
|
11266
|
+
} else {
|
|
11267
|
+
console.log(` \u4FE1\u606F: ${String(r.message)}`);
|
|
11268
|
+
}
|
|
11269
|
+
}
|
|
11270
|
+
|
|
10067
11271
|
// src/cli/help.ts
|
|
10068
11272
|
function showMainHelp() {
|
|
10069
11273
|
console.log(`
|
|
@@ -10077,11 +11281,11 @@ Usage:
|
|
|
10077
11281
|
xbrowser -e cmd1 -e cmd2 Execute multiple -e commands
|
|
10078
11282
|
|
|
10079
11283
|
Commands:
|
|
10080
|
-
session
|
|
10081
|
-
session close [--name <n>] Close session
|
|
11284
|
+
session close [--session <name>] Close session
|
|
10082
11285
|
session list List sessions
|
|
10083
|
-
session kill [--
|
|
11286
|
+
session kill [--session <name>] Kill session
|
|
10084
11287
|
goto <url> Navigate to URL
|
|
11288
|
+
open <url> Navigate to URL (alias for goto)
|
|
10085
11289
|
click <selector> Click element (-s <sel>)
|
|
10086
11290
|
fill <selector> <value> Fill input (-s <sel> -v <val>)
|
|
10087
11291
|
type <selector> <text> Type text (-s <sel> -v <text>)
|
|
@@ -10091,7 +11295,7 @@ Commands:
|
|
|
10091
11295
|
dblclick <selector> Double click (-s <sel>)
|
|
10092
11296
|
check <selector> Check checkbox (-s <sel>)
|
|
10093
11297
|
uncheck <selector> Uncheck checkbox (-s <sel>)
|
|
10094
|
-
screenshot [--full-page]
|
|
11298
|
+
screenshot [--full-page] [--base64] Take screenshot (saves to ~/.xbrowser/screenshots/; use --base64 for inline data)
|
|
10095
11299
|
eval <expression> Evaluate JS
|
|
10096
11300
|
wait <selector> [--timeout <ms>] Wait for element (-s <sel>)
|
|
10097
11301
|
scroll <direction> [--distance N] Scroll page
|
|
@@ -10113,9 +11317,6 @@ Commands:
|
|
|
10113
11317
|
plugin list List plugins
|
|
10114
11318
|
plugin reload <name> Reload plugin
|
|
10115
11319
|
create <name> --template <type> Create plugin
|
|
10116
|
-
daemon start [--port <port>] Start daemon
|
|
10117
|
-
daemon stop Stop daemon
|
|
10118
|
-
daemon status Check status
|
|
10119
11320
|
serve [--port <port>] [--token <t>] Start HTTP server for remote access
|
|
10120
11321
|
remote <url> [command] [--token <t>] Execute command on remote server
|
|
10121
11322
|
record start --url <url> Start recording
|
|
@@ -10326,14 +11527,14 @@ async function listCommands() {
|
|
|
10326
11527
|
return jsonResponse(200, { commands });
|
|
10327
11528
|
}
|
|
10328
11529
|
async function listSessions2() {
|
|
10329
|
-
const
|
|
11530
|
+
const sessions2 = getAllSessions().map((s) => ({
|
|
10330
11531
|
id: s.id,
|
|
10331
11532
|
name: s.name,
|
|
10332
11533
|
url: s.page?.url() ?? null,
|
|
10333
11534
|
createdAt: s.createdAt,
|
|
10334
11535
|
isCDP: s.isCDP ?? false
|
|
10335
11536
|
}));
|
|
10336
|
-
return jsonResponse(200, { sessions });
|
|
11537
|
+
return jsonResponse(200, { sessions: sessions2 });
|
|
10337
11538
|
}
|
|
10338
11539
|
async function createSessionHandler(req) {
|
|
10339
11540
|
const body = req.body;
|
|
@@ -10488,19 +11689,19 @@ function headersToObject(headers) {
|
|
|
10488
11689
|
return result;
|
|
10489
11690
|
}
|
|
10490
11691
|
function readBody(req) {
|
|
10491
|
-
return new Promise((
|
|
11692
|
+
return new Promise((resolve10, reject) => {
|
|
10492
11693
|
const chunks = [];
|
|
10493
11694
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
10494
11695
|
req.on("end", () => {
|
|
10495
11696
|
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
10496
11697
|
if (!raw) {
|
|
10497
|
-
|
|
11698
|
+
resolve10(null);
|
|
10498
11699
|
return;
|
|
10499
11700
|
}
|
|
10500
11701
|
try {
|
|
10501
|
-
|
|
11702
|
+
resolve10(JSON.parse(raw));
|
|
10502
11703
|
} catch {
|
|
10503
|
-
|
|
11704
|
+
resolve10(null);
|
|
10504
11705
|
}
|
|
10505
11706
|
});
|
|
10506
11707
|
req.on("error", reject);
|
|
@@ -10545,7 +11746,7 @@ var HTTPServer = class {
|
|
|
10545
11746
|
res.end(JSON.stringify({ error: "INTERNAL_ERROR", message, statusCode: 500 }));
|
|
10546
11747
|
});
|
|
10547
11748
|
});
|
|
10548
|
-
return new Promise((
|
|
11749
|
+
return new Promise((resolve10, reject) => {
|
|
10549
11750
|
const server = this.server;
|
|
10550
11751
|
server.on("error", (err) => {
|
|
10551
11752
|
this.server = null;
|
|
@@ -10557,7 +11758,7 @@ var HTTPServer = class {
|
|
|
10557
11758
|
this.port = addr.port;
|
|
10558
11759
|
}
|
|
10559
11760
|
console.log(`HTTP server listening on http://${this.host}:${this.port}`);
|
|
10560
|
-
|
|
11761
|
+
resolve10({ port: this.port, host: this.host });
|
|
10561
11762
|
});
|
|
10562
11763
|
});
|
|
10563
11764
|
}
|
|
@@ -10568,13 +11769,13 @@ var HTTPServer = class {
|
|
|
10568
11769
|
*/
|
|
10569
11770
|
async stop() {
|
|
10570
11771
|
if (!this.server) return;
|
|
10571
|
-
return new Promise((
|
|
11772
|
+
return new Promise((resolve10, reject) => {
|
|
10572
11773
|
this.server.close((err) => {
|
|
10573
11774
|
if (err) {
|
|
10574
11775
|
reject(err);
|
|
10575
11776
|
} else {
|
|
10576
11777
|
this.server = null;
|
|
10577
|
-
|
|
11778
|
+
resolve10();
|
|
10578
11779
|
}
|
|
10579
11780
|
});
|
|
10580
11781
|
});
|
|
@@ -10633,6 +11834,7 @@ function showCommandHelp(siteName, cmd, siteConfig, mode) {
|
|
|
10633
11834
|
command: c.name,
|
|
10634
11835
|
description: c.description,
|
|
10635
11836
|
scope: c.scope,
|
|
11837
|
+
...c.loginRequired ? { loginRequired: c.loginRequired } : {},
|
|
10636
11838
|
parameters: paramsList
|
|
10637
11839
|
}, mode);
|
|
10638
11840
|
} else {
|
|
@@ -10645,9 +11847,25 @@ function showCommandHelp(siteName, cmd, siteConfig, mode) {
|
|
|
10645
11847
|
examples: c.examples
|
|
10646
11848
|
}, { color: false, emoji: false });
|
|
10647
11849
|
console.log(text);
|
|
11850
|
+
if (c.loginRequired) {
|
|
11851
|
+
console.log(` Login: ${c.loginRequired}`);
|
|
11852
|
+
}
|
|
10648
11853
|
console.log("");
|
|
10649
11854
|
}
|
|
10650
11855
|
}
|
|
11856
|
+
function outputLoginRequired(result, mode) {
|
|
11857
|
+
if (mode === "json" || mode === "yaml") {
|
|
11858
|
+
console.log(outputFormatter2.format(result, { mode, color: false, emoji: false }));
|
|
11859
|
+
return;
|
|
11860
|
+
}
|
|
11861
|
+
const message = result.message || "Login required";
|
|
11862
|
+
console.error(message);
|
|
11863
|
+
for (const tip of result.tips || []) {
|
|
11864
|
+
const text = typeof tip === "string" ? tip : tip.message;
|
|
11865
|
+
if (text !== message) console.error(` \u{1F4A1} ${text}`);
|
|
11866
|
+
}
|
|
11867
|
+
process.exit(1);
|
|
11868
|
+
}
|
|
10651
11869
|
function extractZodFieldInfo(value) {
|
|
10652
11870
|
const field = value;
|
|
10653
11871
|
const fieldDef = field._def;
|
|
@@ -10711,7 +11929,7 @@ function extractCdpFromArgv(argv) {
|
|
|
10711
11929
|
if (argv[i] === "--cdp" && argv[i + 1]) return argv[i + 1];
|
|
10712
11930
|
if (typeof argv[i] === "string" && argv[i].startsWith("--cdp=")) return argv[i].slice(6);
|
|
10713
11931
|
}
|
|
10714
|
-
return
|
|
11932
|
+
return process.env.XBROWSER_CDP;
|
|
10715
11933
|
}
|
|
10716
11934
|
async function handleStdinMode(stdinCommands, argv) {
|
|
10717
11935
|
const chain = stdinCommands.join(" && ");
|
|
@@ -10774,7 +11992,7 @@ async function routeCommand(argv, stdinCommands) {
|
|
|
10774
11992
|
const cmdArgs = positional.slice(1);
|
|
10775
11993
|
const mode = options.json ? "json" : options.yaml ? "yaml" : "text";
|
|
10776
11994
|
const sessionName = options.session || process.env.XBROWSER_SESSION || "default";
|
|
10777
|
-
const cdpEndpoint = options.cdp;
|
|
11995
|
+
const cdpEndpoint = options.cdp || process.env.XBROWSER_CDP;
|
|
10778
11996
|
if (options.version || options.v) {
|
|
10779
11997
|
console.log(`xbrowser v${version}`);
|
|
10780
11998
|
return;
|
|
@@ -10907,12 +12125,21 @@ async function routeCommand(argv, stdinCommands) {
|
|
|
10907
12125
|
if (builtin) await builtin.execute(cmdArgs, options, { cwd: process.cwd() });
|
|
10908
12126
|
break;
|
|
10909
12127
|
}
|
|
12128
|
+
case "knowledge":
|
|
12129
|
+
case "know": {
|
|
12130
|
+
const builtin = allBuiltins.find((b) => b.name === "knowledge");
|
|
12131
|
+
if (builtin) await builtin.execute(cmdArgs, options, { cwd: process.cwd() });
|
|
12132
|
+
break;
|
|
12133
|
+
}
|
|
10910
12134
|
case "viewer":
|
|
10911
12135
|
await handleViewer(cmdArgs, options, mode, cdpEndpoint);
|
|
10912
12136
|
break;
|
|
10913
12137
|
case "help":
|
|
10914
12138
|
showMainHelp();
|
|
10915
12139
|
break;
|
|
12140
|
+
case "test":
|
|
12141
|
+
await handleTest(cmdArgs, options, mode, cdpEndpoint);
|
|
12142
|
+
break;
|
|
10916
12143
|
case "net":
|
|
10917
12144
|
await handleNetCommand(cmdArgs, options, mode, sessionName);
|
|
10918
12145
|
break;
|
|
@@ -11022,9 +12249,14 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
11022
12249
|
}
|
|
11023
12250
|
const needsBrowser = cmdEntry.scope === "page" || cmdEntry.scope === "browser";
|
|
11024
12251
|
if (needsBrowser && !process.env.XBROWSER_DAEMON_WORKER) {
|
|
11025
|
-
const { forwardExec } = await import("./daemon-client-
|
|
12252
|
+
const { forwardExec } = await import("./daemon-client-UZZEHHIV.js");
|
|
11026
12253
|
const userTimeout = typeof params.timeout === "number" && params.timeout > 0 ? params.timeout * 1e3 + 3e4 : void 0;
|
|
11027
|
-
const result = await forwardExec(command
|
|
12254
|
+
const result = await forwardExec(`${command}.${subCommand}`, params, sessionName, cdpEndpoint, userTimeout);
|
|
12255
|
+
const resultData = result && typeof result === "object" ? result.data : void 0;
|
|
12256
|
+
if (result && result.success === false && resultData?.code === "LOGIN_REQUIRED") {
|
|
12257
|
+
outputLoginRequired(result, mode);
|
|
12258
|
+
return;
|
|
12259
|
+
}
|
|
11028
12260
|
if (result && result.success !== false) {
|
|
11029
12261
|
if (isCommandResult2(result)) {
|
|
11030
12262
|
if (mode === "json" || mode === "yaml") {
|
|
@@ -11057,6 +12289,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
11057
12289
|
browser: needsBrowser ? session.context.browser() : null,
|
|
11058
12290
|
browserContext: needsBrowser ? session.context : null,
|
|
11059
12291
|
sessionId: needsBrowser ? session.id : "",
|
|
12292
|
+
cdpEndpoint: cdpEndpoint || (needsBrowser ? session?.cdpEndpoint : void 0),
|
|
11060
12293
|
storage: getPluginStorage(command),
|
|
11061
12294
|
output: { mode, showTips: true, color: true, emoji: true },
|
|
11062
12295
|
error: (msg) => {
|
|
@@ -11067,10 +12300,33 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
11067
12300
|
cliName: "xbrowser",
|
|
11068
12301
|
waitForHuman: async (_opts) => {
|
|
11069
12302
|
return { solved: false, timedOut: true };
|
|
11070
|
-
}
|
|
12303
|
+
},
|
|
12304
|
+
tips: new TipCollector2()
|
|
11071
12305
|
};
|
|
11072
12306
|
try {
|
|
11073
12307
|
const cmdStart = Date.now();
|
|
12308
|
+
const loginGuard = await checkPluginLoginRequired({
|
|
12309
|
+
site,
|
|
12310
|
+
command: cmdEntry,
|
|
12311
|
+
commandName: subCommand,
|
|
12312
|
+
ctx,
|
|
12313
|
+
page: needsBrowser ? session?.page : null,
|
|
12314
|
+
sessionName
|
|
12315
|
+
});
|
|
12316
|
+
if (!loginGuard.ok) {
|
|
12317
|
+
const result2 = {
|
|
12318
|
+
success: false,
|
|
12319
|
+
data: loginGuard.data ?? null,
|
|
12320
|
+
message: loginGuard.message,
|
|
12321
|
+
tips: normalizeTips7(loginGuard.tips)
|
|
12322
|
+
};
|
|
12323
|
+
if (mode === "json" || mode === "yaml") {
|
|
12324
|
+
outputLoginRequired(result2, mode);
|
|
12325
|
+
} else {
|
|
12326
|
+
outputLoginRequired(result2, mode);
|
|
12327
|
+
}
|
|
12328
|
+
return;
|
|
12329
|
+
}
|
|
11074
12330
|
const cmdHooks = await loadHooks();
|
|
11075
12331
|
if (cmdHooks.length > 0 && session?.page) {
|
|
11076
12332
|
await Promise.all(cmdHooks.map((h) => h.onBeforeCommand?.({ page: session.page, command: `${command} ${subCommand}`, params })));
|
|
@@ -11089,23 +12345,39 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
11089
12345
|
saveSessionDiskMeta(sessionName, { conversationUrl: convUrl, cdpEndpoint });
|
|
11090
12346
|
}
|
|
11091
12347
|
}
|
|
12348
|
+
let injectedViewerUrl;
|
|
12349
|
+
const LOGIN_FAIL_KEYWORDS = ["\u767B\u5F55", "login", "Login", "\u672A\u767B\u5F55", "not logged in", "cdp", "CDP", "\u9A8C\u8BC1\u7801", "\u9A8C\u8BC1", "captcha", "\u9700\u8981\u767B\u5F55", "requires login"];
|
|
12350
|
+
const tipTexts = (result.tips || []).map((t) => typeof t === "string" ? t : t.message);
|
|
12351
|
+
const isLoginFail = isCommandResult2(result) && result.success === false && [result.message, ...tipTexts].filter(Boolean).join(" ").toLowerCase().match(new RegExp(LOGIN_FAIL_KEYWORDS.join("|"), "i"));
|
|
12352
|
+
if (isLoginFail) {
|
|
12353
|
+
injectedViewerUrl = buildViewerUrl(sessionName);
|
|
12354
|
+
if (injectedViewerUrl) {
|
|
12355
|
+
result.tips = [...result.tips || [], makeTip.info(`Open viewer to complete login: ${injectedViewerUrl}`)];
|
|
12356
|
+
}
|
|
12357
|
+
}
|
|
11092
12358
|
const outputData = isCommandResult2(result) ? result.data : result && typeof result === "object" ? result.data ?? result : result;
|
|
11093
12359
|
const tips = isCommandResult2(result) ? result.tips : result && typeof result === "object" ? result.tips : void 0;
|
|
11094
12360
|
if (mode === "json" || mode === "yaml") {
|
|
11095
12361
|
const finalOutput = {
|
|
11096
12362
|
data: outputData
|
|
11097
12363
|
};
|
|
12364
|
+
if (injectedViewerUrl) {
|
|
12365
|
+
finalOutput.viewerUrl = injectedViewerUrl;
|
|
12366
|
+
}
|
|
12367
|
+
if (tips?.length) {
|
|
12368
|
+
finalOutput.tips = tips;
|
|
12369
|
+
}
|
|
11098
12370
|
if (hookOutputs.length > 0) {
|
|
11099
12371
|
finalOutput.hooks = hookOutputs;
|
|
11100
12372
|
}
|
|
11101
12373
|
console.log(outputFormatter2.format(finalOutput, { mode, color: false, emoji: false }));
|
|
11102
12374
|
if (tips?.length) {
|
|
11103
|
-
for (const tip of tips) console.error(`\u{1F4A1} ${tip}`);
|
|
12375
|
+
for (const tip of tips) console.error(`\u{1F4A1} ${typeof tip === "string" ? tip : tip.message}`);
|
|
11104
12376
|
}
|
|
11105
12377
|
} else {
|
|
11106
12378
|
console.log(outputFormatter2.format(outputData, { mode: "text", color: true, emoji: true }));
|
|
11107
12379
|
if (tips?.length) {
|
|
11108
|
-
for (const tip of tips) console.log(` \u{1F4A1} ${tip}`);
|
|
12380
|
+
for (const tip of tips) console.log(` \u{1F4A1} ${typeof tip === "string" ? tip : tip.message}`);
|
|
11109
12381
|
}
|
|
11110
12382
|
if (hookOutputs.length > 0) {
|
|
11111
12383
|
for (const ho of hookOutputs) {
|
|
@@ -11283,7 +12555,7 @@ async function main() {
|
|
|
11283
12555
|
const command = process.argv[2];
|
|
11284
12556
|
const isLongRunning = command === "preview" || command === "serve";
|
|
11285
12557
|
if (!isLongRunning) {
|
|
11286
|
-
const { ensureProcessCanExit } = await import("./browser-
|
|
12558
|
+
const { ensureProcessCanExit } = await import("./browser-R56O3CW6.js");
|
|
11287
12559
|
await ensureProcessCanExit().catch(() => {
|
|
11288
12560
|
});
|
|
11289
12561
|
process.exit(exitCode);
|