@xbrowser/cli 1.0.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-GURRY444.js → browser-GITRHHFO.js} +4 -3
- package/dist/{browser-DSVV4GHS.js → browser-R56O3CW6.js} +3 -3
- package/dist/{browser-53KUFEEM.js → browser-ZJOZB5CR.js} +4 -4
- package/dist/{cdp-driver-MNPR3HZH.js → cdp-driver-BE3FOMRN.js} +324 -58
- package/dist/{cdp-driver-SSXUGXP6.js → cdp-driver-TOPYJIFL.js} +3 -3
- package/dist/chunk-2SVQTI2O.js +2794 -0
- package/dist/{chunk-2MFXKN32.js → chunk-ACFE6PKF.js} +1013 -119
- package/dist/chunk-BBMRDUYQ.js +260 -0
- package/dist/{chunk-E4O5ZU3H.js → chunk-CAFNSGYM.js} +393 -95
- package/dist/{chunk-DTJRVA76.js → chunk-ETCO4SNK.js} +2 -2
- package/dist/{chunk-YKOHDEFV.js → chunk-JPA2ZT2R.js} +69 -36
- package/dist/{chunk-T4J4C2NZ.js → chunk-JPHCY4TC.js} +12 -2
- 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-42RPMJ76.js → chunk-PPG4D2EW.js} +325 -59
- package/dist/{chunk-IDVD44ED.js → chunk-Q4IGYTKR.js} +19 -7
- package/dist/{chunk-2BQZIT3S.js → chunk-QIK2I3VQ.js} +86 -2501
- package/dist/chunk-WJRE55TN.js +83 -0
- package/dist/cli.js +1435 -1077
- package/dist/{convert-EGFYNICZ.js → convert-LB3GJTLR.js} +3 -3
- package/dist/{convert-EKQVHKB4.js → convert-R3XXYKC6.js} +2 -2
- package/dist/{daemon-client-3VM7VU7O.js → daemon-client-DRCUMNHK.js} +25 -74
- package/dist/{daemon-client-YAVQ343A.js → daemon-client-UZZEHHIV.js} +2 -2
- package/dist/daemon-main.js +2200 -1691
- package/dist/{extract-JUOQQX4V.js → extract-2ZFW2MX7.js} +1 -1
- package/dist/{extract-L2IW3IUB.js → extract-BSYBM4MR.js} +1 -1
- package/dist/{filter-HC4RA7JY.js → filter-KCFO4RSV.js} +1 -1
- 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 +165 -108
- package/dist/index.js +2531 -1680
- package/dist/launcher-QUJ4M2VS.js +19 -0
- package/dist/{launcher-KA7J32K5.js → launcher-YARP45UY.js} +1 -1
- package/dist/{network-store-66A2RATI.js → network-store-XGZ25FFC.js} +1 -1
- 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 +5 -4
- package/dist/screenshot-CWAWMXVA.js +0 -28
- package/dist/session-recorder-MA75PKTQ.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,8 +25,8 @@ import {
|
|
|
17
25
|
resolveLaunchOpts,
|
|
18
26
|
saveSessionDiskMeta,
|
|
19
27
|
setActivePage
|
|
20
|
-
} from "./chunk-
|
|
21
|
-
import "./chunk-
|
|
28
|
+
} from "./chunk-CAFNSGYM.js";
|
|
29
|
+
import "./chunk-BBMRDUYQ.js";
|
|
22
30
|
import {
|
|
23
31
|
forwardCommandLog,
|
|
24
32
|
forwardNetworkAnalyze,
|
|
@@ -39,7 +47,6 @@ import {
|
|
|
39
47
|
forwardRecordSummary,
|
|
40
48
|
forwardReplay,
|
|
41
49
|
forwardSessionClose,
|
|
42
|
-
forwardSessionCreate,
|
|
43
50
|
forwardSessionList,
|
|
44
51
|
forwardViewerCheckSelector,
|
|
45
52
|
getDaemonConfig,
|
|
@@ -47,11 +54,11 @@ import {
|
|
|
47
54
|
killAllDaemonProcesses,
|
|
48
55
|
startDaemonProcess,
|
|
49
56
|
stopDaemonProcess
|
|
50
|
-
} from "./chunk-
|
|
51
|
-
import "./chunk-
|
|
57
|
+
} from "./chunk-Q4IGYTKR.js";
|
|
58
|
+
import "./chunk-KFQGP6VL.js";
|
|
52
59
|
|
|
53
60
|
// src/router.ts
|
|
54
|
-
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";
|
|
55
62
|
|
|
56
63
|
// src/utils/positional-params.ts
|
|
57
64
|
import { unquote } from "@dyyz1993/xcli-core";
|
|
@@ -171,13 +178,16 @@ var version = pkg.version;
|
|
|
171
178
|
|
|
172
179
|
// src/executor.ts
|
|
173
180
|
import {
|
|
174
|
-
ok as
|
|
181
|
+
ok as ok25,
|
|
175
182
|
fail as fail7,
|
|
176
183
|
isCommandResult,
|
|
184
|
+
CompositeStorage,
|
|
185
|
+
TipCollector,
|
|
186
|
+
normalizeTips as normalizeTips6,
|
|
177
187
|
configureArchiveStore,
|
|
178
188
|
appendCommandToArchive,
|
|
179
189
|
checkGuard,
|
|
180
|
-
|
|
190
|
+
unquote as unquote2
|
|
181
191
|
} from "@dyyz1993/xcli-core";
|
|
182
192
|
|
|
183
193
|
// src/commands/navigation.ts
|
|
@@ -324,10 +334,18 @@ var urlCommand = registerCommand({
|
|
|
324
334
|
return ok({ url: ctx.page.url() });
|
|
325
335
|
}
|
|
326
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
|
+
});
|
|
327
345
|
|
|
328
346
|
// src/commands/interaction.ts
|
|
329
347
|
import { z as z2 } from "zod";
|
|
330
|
-
import { ok as ok2 } from "@dyyz1993/xcli-core";
|
|
348
|
+
import { ok as ok2, normalizeTips } from "@dyyz1993/xcli-core";
|
|
331
349
|
|
|
332
350
|
// src/lib/captcha.ts
|
|
333
351
|
var CAPTCHA_SELECTORS = {
|
|
@@ -395,15 +413,15 @@ var clickCommand = registerCommand({
|
|
|
395
413
|
let detectedNewPage;
|
|
396
414
|
let cleanup;
|
|
397
415
|
if (ctx.browserContext?.on) {
|
|
398
|
-
const pagePromise = new Promise((
|
|
416
|
+
const pagePromise = new Promise((resolve10) => {
|
|
399
417
|
const timer = setTimeout(() => {
|
|
400
418
|
ctx.browserContext.off("page", handler);
|
|
401
|
-
|
|
419
|
+
resolve10(void 0);
|
|
402
420
|
}, 3e3);
|
|
403
421
|
const handler = (page2) => {
|
|
404
422
|
clearTimeout(timer);
|
|
405
423
|
ctx.browserContext.off("page", handler);
|
|
406
|
-
|
|
424
|
+
resolve10(page2);
|
|
407
425
|
};
|
|
408
426
|
ctx.browserContext.on("page", handler);
|
|
409
427
|
});
|
|
@@ -438,7 +456,7 @@ var clickCommand = registerCommand({
|
|
|
438
456
|
selector: p.selector,
|
|
439
457
|
newTab: { url: newUrl, title: newTitle }
|
|
440
458
|
});
|
|
441
|
-
result.tips = [`\u65B0 Tab \u5DF2\u6253\u5F00: ${newTitle ? newTitle + " \u2014 " : ""}${newUrl}`];
|
|
459
|
+
result.tips = normalizeTips([`\u65B0 Tab \u5DF2\u6253\u5F00: ${newTitle ? newTitle + " \u2014 " : ""}${newUrl}`]);
|
|
442
460
|
return result;
|
|
443
461
|
}
|
|
444
462
|
const captchaInfo = await detectCaptcha(page);
|
|
@@ -452,7 +470,7 @@ var clickCommand = registerCommand({
|
|
|
452
470
|
tips.push(solved ? "\u2705 CAPTCHA solved!" : "\u274C CAPTCHA timeout");
|
|
453
471
|
}
|
|
454
472
|
const result = ok2({ selector: p.selector, captcha: captchaInfo });
|
|
455
|
-
result.tips = tips;
|
|
473
|
+
result.tips = normalizeTips(tips);
|
|
456
474
|
return result;
|
|
457
475
|
}
|
|
458
476
|
return ok2({ selector: p.selector });
|
|
@@ -787,7 +805,7 @@ var mouseCommand = registerCommand({
|
|
|
787
805
|
|
|
788
806
|
// src/commands/evaluate.ts
|
|
789
807
|
import { z as z7 } from "zod";
|
|
790
|
-
import { ok as ok7 } from "@dyyz1993/xcli-core";
|
|
808
|
+
import { ok as ok7, normalizeTips as normalizeTips2 } from "@dyyz1993/xcli-core";
|
|
791
809
|
var evaluateCommand = registerCommand({
|
|
792
810
|
name: "eval",
|
|
793
811
|
description: "Evaluate JavaScript expression in the browser",
|
|
@@ -808,10 +826,10 @@ var evaluateCommand = registerCommand({
|
|
|
808
826
|
const result = await ctx.page.evaluate(p.expression);
|
|
809
827
|
const response = ok7({ result });
|
|
810
828
|
if (decision && decision.severity === "danger") {
|
|
811
|
-
response.tips = [
|
|
829
|
+
response.tips = normalizeTips2([
|
|
812
830
|
`\u26A0\uFE0F CDP Firewall: ${decision.reason}`,
|
|
813
831
|
`\u{1F4A1} Fix: ${decision.suggestion}`
|
|
814
|
-
];
|
|
832
|
+
]);
|
|
815
833
|
}
|
|
816
834
|
return response;
|
|
817
835
|
}
|
|
@@ -934,7 +952,19 @@ var clearLocalStorageCommand = registerCommand({
|
|
|
934
952
|
// src/commands/screenshot.ts
|
|
935
953
|
import { z as z9 } from "zod";
|
|
936
954
|
import { ok as ok9 } from "@dyyz1993/xcli-core";
|
|
937
|
-
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
|
+
}
|
|
938
968
|
var screenshotCommand = registerCommand({
|
|
939
969
|
name: "screenshot",
|
|
940
970
|
description: "Take a screenshot of the page or element",
|
|
@@ -944,7 +974,8 @@ var screenshotCommand = registerCommand({
|
|
|
944
974
|
selector: z9.string().optional(),
|
|
945
975
|
type: z9.enum(["png", "jpeg"]).optional(),
|
|
946
976
|
fullPage: z9.boolean().optional(),
|
|
947
|
-
output: z9.string().optional()
|
|
977
|
+
output: z9.string().optional(),
|
|
978
|
+
base64: z9.boolean().optional().describe("Return base64 data instead of file path")
|
|
948
979
|
}),
|
|
949
980
|
result: z9.union([
|
|
950
981
|
z9.object({
|
|
@@ -959,8 +990,9 @@ var screenshotCommand = registerCommand({
|
|
|
959
990
|
})
|
|
960
991
|
]),
|
|
961
992
|
handler: async (p, ctx) => {
|
|
993
|
+
const format = p.type || "png";
|
|
962
994
|
const options = {
|
|
963
|
-
type:
|
|
995
|
+
type: format,
|
|
964
996
|
fullPage: p.fullPage || false
|
|
965
997
|
};
|
|
966
998
|
let buffer;
|
|
@@ -973,13 +1005,23 @@ var screenshotCommand = registerCommand({
|
|
|
973
1005
|
writeFileSync(p.output, buffer);
|
|
974
1006
|
return ok9({
|
|
975
1007
|
output: p.output,
|
|
976
|
-
format
|
|
1008
|
+
format,
|
|
1009
|
+
size: buffer.length
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
if (p.base64) {
|
|
1013
|
+
return ok9({
|
|
1014
|
+
data: buffer.toString("base64"),
|
|
1015
|
+
format,
|
|
977
1016
|
size: buffer.length
|
|
978
1017
|
});
|
|
979
1018
|
}
|
|
1019
|
+
ensureScreenshotsDir();
|
|
1020
|
+
const screenshotPath = generateScreenshotPath(format);
|
|
1021
|
+
writeFileSync(screenshotPath, buffer);
|
|
980
1022
|
return ok9({
|
|
981
|
-
|
|
982
|
-
format
|
|
1023
|
+
output: screenshotPath,
|
|
1024
|
+
format,
|
|
983
1025
|
size: buffer.length
|
|
984
1026
|
});
|
|
985
1027
|
}
|
|
@@ -1161,7 +1203,7 @@ var consoleCheckCommand = registerCommand({
|
|
|
1161
1203
|
await page.goto(p.url, { waitUntil: "domcontentloaded" });
|
|
1162
1204
|
}
|
|
1163
1205
|
const messages = await page.evaluate((args) => {
|
|
1164
|
-
return new Promise((
|
|
1206
|
+
return new Promise((resolve10) => {
|
|
1165
1207
|
const collected = [];
|
|
1166
1208
|
const originalConsole = {
|
|
1167
1209
|
log: console.log,
|
|
@@ -1228,7 +1270,7 @@ ${a.stack || ""}`;
|
|
|
1228
1270
|
console.warn = originalConsole.warn;
|
|
1229
1271
|
console.error = originalConsole.error;
|
|
1230
1272
|
console.info = originalConsole.info;
|
|
1231
|
-
|
|
1273
|
+
resolve10(collected);
|
|
1232
1274
|
}, args.duration);
|
|
1233
1275
|
});
|
|
1234
1276
|
}, { duration: p.duration });
|
|
@@ -1577,6 +1619,19 @@ var healthCheckCommand = registerCommand({
|
|
|
1577
1619
|
// src/commands/actions.ts
|
|
1578
1620
|
import { z as z14 } from "zod";
|
|
1579
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
|
+
}
|
|
1580
1635
|
var waitActionSchema = z14.object({
|
|
1581
1636
|
type: z14.literal("wait"),
|
|
1582
1637
|
milliseconds: z14.number().positive().optional(),
|
|
@@ -1594,7 +1649,8 @@ var screenshotActionSchema = z14.object({
|
|
|
1594
1649
|
type: z14.literal("screenshot"),
|
|
1595
1650
|
fullPage: z14.boolean().optional(),
|
|
1596
1651
|
quality: z14.number().min(1).max(100).optional(),
|
|
1597
|
-
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")
|
|
1598
1654
|
});
|
|
1599
1655
|
var writeActionSchema = z14.object({
|
|
1600
1656
|
type: z14.literal("write"),
|
|
@@ -1640,7 +1696,7 @@ async function executeAction(page, action) {
|
|
|
1640
1696
|
if (action.selector) {
|
|
1641
1697
|
await page.waitForSelector(action.selector, { timeout: 3e4 });
|
|
1642
1698
|
} else if (action.milliseconds) {
|
|
1643
|
-
await new Promise((
|
|
1699
|
+
await new Promise((resolve10) => setTimeout(resolve10, action.milliseconds));
|
|
1644
1700
|
} else {
|
|
1645
1701
|
throw new Error("wait action requires either milliseconds or selector");
|
|
1646
1702
|
}
|
|
@@ -1661,7 +1717,13 @@ async function executeAction(page, action) {
|
|
|
1661
1717
|
quality: action.quality ?? 80,
|
|
1662
1718
|
...action.viewport ? { clip: { x: 0, y: 0, ...action.viewport } } : {}
|
|
1663
1719
|
});
|
|
1664
|
-
|
|
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 };
|
|
1665
1727
|
}
|
|
1666
1728
|
case "write":
|
|
1667
1729
|
await page.keyboard.type(action.text);
|
|
@@ -1723,8 +1785,8 @@ var actionsCommand = registerCommand({
|
|
|
1723
1785
|
results.push(result);
|
|
1724
1786
|
}
|
|
1725
1787
|
})();
|
|
1726
|
-
const timeoutPromise = new Promise((
|
|
1727
|
-
setTimeout(
|
|
1788
|
+
const timeoutPromise = new Promise((resolve10) => {
|
|
1789
|
+
setTimeout(resolve10, timeoutMs);
|
|
1728
1790
|
});
|
|
1729
1791
|
await Promise.race([executionPromise, timeoutPromise]);
|
|
1730
1792
|
const title = await ctx.page.title();
|
|
@@ -2398,11 +2460,11 @@ async function navigateForMap(page, url, timeout = 15e3) {
|
|
|
2398
2460
|
}
|
|
2399
2461
|
async function extractPageLinks(page, baseUrl) {
|
|
2400
2462
|
await navigateForMap(page, baseUrl);
|
|
2401
|
-
await new Promise((
|
|
2463
|
+
await new Promise((resolve10) => setTimeout(resolve10, 2e3));
|
|
2402
2464
|
await page.evaluate(() => {
|
|
2403
2465
|
window.scrollTo(0, document.body.scrollHeight);
|
|
2404
2466
|
});
|
|
2405
|
-
await new Promise((
|
|
2467
|
+
await new Promise((resolve10) => setTimeout(resolve10, 1e3));
|
|
2406
2468
|
const origin = new URL(baseUrl).origin;
|
|
2407
2469
|
const rawLinks = await page.evaluate((evalOrigin) => {
|
|
2408
2470
|
return Array.from(document.querySelectorAll("a[href]")).map((a) => {
|
|
@@ -3828,7 +3890,7 @@ var ENGINE_KEY_ENUM = z20.enum(ALL_ENGINE_KEYS);
|
|
|
3828
3890
|
|
|
3829
3891
|
// src/commands/snapshot.ts
|
|
3830
3892
|
import { z as z21 } from "zod";
|
|
3831
|
-
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";
|
|
3832
3894
|
|
|
3833
3895
|
// src/runtime/ref-store.ts
|
|
3834
3896
|
var sessions = /* @__PURE__ */ new Map();
|
|
@@ -4069,9 +4131,9 @@ async function resolveRefParams(page, params, selectorKeys, cache, sessionId) {
|
|
|
4069
4131
|
}
|
|
4070
4132
|
|
|
4071
4133
|
// src/utils/site-semantics.ts
|
|
4072
|
-
import { writeFileSync as
|
|
4073
|
-
import { join, dirname } from "path";
|
|
4074
|
-
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";
|
|
4075
4137
|
import { stringify, parse } from "yaml";
|
|
4076
4138
|
import { execFile } from "child_process";
|
|
4077
4139
|
var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
@@ -4148,10 +4210,10 @@ function inferAction(role, label) {
|
|
|
4148
4210
|
return {};
|
|
4149
4211
|
}
|
|
4150
4212
|
function getSemanticsDir() {
|
|
4151
|
-
return
|
|
4213
|
+
return join3(homedir3(), ".xbrowser", "site-semantics");
|
|
4152
4214
|
}
|
|
4153
4215
|
function getSemanticsPath(domain) {
|
|
4154
|
-
return
|
|
4216
|
+
return join3(getSemanticsDir(), `${domain}.yaml`);
|
|
4155
4217
|
}
|
|
4156
4218
|
function extractDomain2(url) {
|
|
4157
4219
|
try {
|
|
@@ -4188,9 +4250,9 @@ function saveSemantics(domain, pagePath, url, elements) {
|
|
|
4188
4250
|
}
|
|
4189
4251
|
site.updated_at = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
4190
4252
|
if (!existsSync(dir)) {
|
|
4191
|
-
|
|
4253
|
+
mkdirSync3(dir, { recursive: true });
|
|
4192
4254
|
}
|
|
4193
|
-
|
|
4255
|
+
writeFileSync3(filePath, stringify(site, { lineWidth: 0 }));
|
|
4194
4256
|
}
|
|
4195
4257
|
function loadSemantics(domain) {
|
|
4196
4258
|
const filePath = getSemanticsPath(domain);
|
|
@@ -4260,25 +4322,25 @@ aria snapshot\uFF1A
|
|
|
4260
4322
|
async function analyzeWithLLM(ariaSnapshot) {
|
|
4261
4323
|
const piBin = process.env.PI_CLI_PATH || "pi";
|
|
4262
4324
|
const prompt = LLM_PROMPT.replace("{snapshot}", ariaSnapshot.slice(0, 4e3));
|
|
4263
|
-
return new Promise((
|
|
4325
|
+
return new Promise((resolve10) => {
|
|
4264
4326
|
execFile(
|
|
4265
4327
|
piBin,
|
|
4266
4328
|
["--provider", LLM_PROVIDER, "--model", LLM_MODEL, prompt],
|
|
4267
4329
|
{ timeout: LLM_TIMEOUT_MS, maxBuffer: 1024 * 1024 },
|
|
4268
4330
|
(err, stdout, _stderr) => {
|
|
4269
4331
|
if (err) {
|
|
4270
|
-
|
|
4332
|
+
resolve10(null);
|
|
4271
4333
|
return;
|
|
4272
4334
|
}
|
|
4273
4335
|
const output = (stdout || "").trim();
|
|
4274
4336
|
if (!output) {
|
|
4275
|
-
|
|
4337
|
+
resolve10(null);
|
|
4276
4338
|
return;
|
|
4277
4339
|
}
|
|
4278
4340
|
try {
|
|
4279
4341
|
const parsed = parse(output);
|
|
4280
4342
|
if (!parsed || typeof parsed !== "object") {
|
|
4281
|
-
|
|
4343
|
+
resolve10(null);
|
|
4282
4344
|
return;
|
|
4283
4345
|
}
|
|
4284
4346
|
const elements = {};
|
|
@@ -4293,9 +4355,9 @@ async function analyzeWithLLM(ariaSnapshot) {
|
|
|
4293
4355
|
};
|
|
4294
4356
|
}
|
|
4295
4357
|
}
|
|
4296
|
-
|
|
4358
|
+
resolve10(Object.keys(elements).length > 0 ? elements : null);
|
|
4297
4359
|
} catch {
|
|
4298
|
-
|
|
4360
|
+
resolve10(null);
|
|
4299
4361
|
}
|
|
4300
4362
|
}
|
|
4301
4363
|
);
|
|
@@ -4650,7 +4712,7 @@ async function pollUntil(timeout, pollInterval, predicate) {
|
|
|
4650
4712
|
const startedAt = Date.now();
|
|
4651
4713
|
while (Date.now() - startedAt <= timeout) {
|
|
4652
4714
|
if (await predicate()) return true;
|
|
4653
|
-
await new Promise((
|
|
4715
|
+
await new Promise((resolve10) => setTimeout(resolve10, pollInterval));
|
|
4654
4716
|
}
|
|
4655
4717
|
return false;
|
|
4656
4718
|
}
|
|
@@ -4732,7 +4794,7 @@ var snapshotCommand = registerCommand({
|
|
|
4732
4794
|
interactive: z21.boolean().optional().default(false).describe("Return interactive agent refs only"),
|
|
4733
4795
|
interactiveOnly: z21.boolean().optional().default(false).describe("Alias for interactive"),
|
|
4734
4796
|
i: z21.boolean().optional().default(false).describe("Short alias for interactive"),
|
|
4735
|
-
compact: z21.boolean().optional().default(false).describe("Include compact
|
|
4797
|
+
compact: z21.boolean().optional().default(false).describe("Include compact xbrowser style snapshot text"),
|
|
4736
4798
|
c: z21.boolean().optional().default(false).describe("Short alias for compact"),
|
|
4737
4799
|
selectors: z21.boolean().optional().default(false).describe("Include ref to CSS selector map"),
|
|
4738
4800
|
all: z21.boolean().optional().default(false).describe("Include hidden interactive targets when using interactive snapshot")
|
|
@@ -4756,15 +4818,15 @@ var snapshotCommand = registerCommand({
|
|
|
4756
4818
|
if (p.compact || p.c || p.interactive || p.interactiveOnly || p.i) {
|
|
4757
4819
|
observation.compact = formatObservationCompact(observation, { selectors: p.selectors });
|
|
4758
4820
|
}
|
|
4759
|
-
return ok20(observation, [
|
|
4821
|
+
return ok20(observation, normalizeTips3([
|
|
4760
4822
|
`refs refreshed for ${observation.targets.length} targets; use click @e1 or fill @e2 "text"`
|
|
4761
|
-
]);
|
|
4823
|
+
]));
|
|
4762
4824
|
}
|
|
4763
4825
|
if (p.type === "aria") {
|
|
4764
4826
|
const aria = await captureAriaSnapshot(page, p.selector, p.depth);
|
|
4765
4827
|
const tips = await buildRefTips(page, aria);
|
|
4766
4828
|
persistSemantics(url, aria);
|
|
4767
|
-
return ok20({ url, title, aria }, tips);
|
|
4829
|
+
return ok20({ url, title, aria }, normalizeTips3(tips));
|
|
4768
4830
|
}
|
|
4769
4831
|
if (p.type === "text") {
|
|
4770
4832
|
const text = await captureTextSnapshot(page, p.selector);
|
|
@@ -4782,7 +4844,7 @@ var snapshotCommand = registerCommand({
|
|
|
4782
4844
|
]);
|
|
4783
4845
|
const tips = await buildRefTips(page, aria);
|
|
4784
4846
|
persistSemantics(url, aria);
|
|
4785
|
-
return ok20({ url, title, aria, text, dom }, tips);
|
|
4847
|
+
return ok20({ url, title, aria, text, dom }, normalizeTips3(tips));
|
|
4786
4848
|
}
|
|
4787
4849
|
return fail4(`Unknown snapshot type: ${p.type}`);
|
|
4788
4850
|
}
|
|
@@ -4862,7 +4924,7 @@ async function captureDomSnapshot(page, selector, maxDepth) {
|
|
|
4862
4924
|
|
|
4863
4925
|
// src/commands/agent.ts
|
|
4864
4926
|
import { z as z22 } from "zod";
|
|
4865
|
-
import { ok as ok21 } from "@dyyz1993/xcli-core";
|
|
4927
|
+
import { ok as ok21, normalizeTips as normalizeTips4 } from "@dyyz1993/xcli-core";
|
|
4866
4928
|
var observeCommand = registerCommand({
|
|
4867
4929
|
name: "observe",
|
|
4868
4930
|
description: "Observe the current page as structured agent targets with session refs",
|
|
@@ -4870,9 +4932,14 @@ var observeCommand = registerCommand({
|
|
|
4870
4932
|
parameters: z22.object({
|
|
4871
4933
|
includeHidden: z22.boolean().optional().default(false).describe("Include hidden elements in the target list"),
|
|
4872
4934
|
limit: z22.number().int().positive().max(300).optional().default(80).describe("Maximum number of targets to return"),
|
|
4873
|
-
compact: z22.boolean().optional().default(false).describe("Include compact
|
|
4935
|
+
compact: z22.boolean().optional().default(false).describe("Include compact xbrowser style snapshot text"),
|
|
4874
4936
|
selectors: z22.boolean().optional().default(false).describe("Include ref to stable CSS selector map")
|
|
4875
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(),
|
|
4876
4943
|
handler: async (p, ctx) => {
|
|
4877
4944
|
const observation = await observePage(ctx.page, ctx.sessionId, {
|
|
4878
4945
|
includeHidden: p.includeHidden,
|
|
@@ -4880,9 +4947,9 @@ var observeCommand = registerCommand({
|
|
|
4880
4947
|
});
|
|
4881
4948
|
if (p.selectors) observation.selectors = buildSelectorMap(observation);
|
|
4882
4949
|
if (p.compact) observation.compact = formatObservationCompact(observation, { selectors: p.selectors });
|
|
4883
|
-
return ok21(observation, [
|
|
4950
|
+
return ok21(observation, normalizeTips4([
|
|
4884
4951
|
`refs refreshed for ${observation.targets.length} targets; use act --ref @e1 --action click or click @e1`
|
|
4885
|
-
]);
|
|
4952
|
+
]));
|
|
4886
4953
|
}
|
|
4887
4954
|
});
|
|
4888
4955
|
var actCommand = registerCommand({
|
|
@@ -4908,10 +4975,10 @@ var actCommand = registerCommand({
|
|
|
4908
4975
|
success: false,
|
|
4909
4976
|
data: result,
|
|
4910
4977
|
message: result.message || result.reason || "Action failed",
|
|
4911
|
-
tips: result.stale ? ["run observe again to refresh refs"] : []
|
|
4978
|
+
tips: normalizeTips4(result.stale ? ["run observe again to refresh refs"] : [])
|
|
4912
4979
|
};
|
|
4913
4980
|
}
|
|
4914
|
-
return ok21(result, result.stale ? ["ref screen hash changed; run observe if the next action is uncertain"] : []);
|
|
4981
|
+
return ok21(result, normalizeTips4(result.stale ? ["ref screen hash changed; run observe if the next action is uncertain"] : []));
|
|
4915
4982
|
}
|
|
4916
4983
|
});
|
|
4917
4984
|
var waitForCommand = registerCommand({
|
|
@@ -5087,7 +5154,8 @@ import { ok as ok23 } from "@dyyz1993/xcli-core";
|
|
|
5087
5154
|
import { readFileSync as readFileSync2 } from "fs";
|
|
5088
5155
|
|
|
5089
5156
|
// src/chain-parser.ts
|
|
5090
|
-
import {
|
|
5157
|
+
import { splitCommand, parseCommandArgs } from "@dyyz1993/xcli-core";
|
|
5158
|
+
import { registerCommandDefinition } from "@dyyz1993/xcli-core";
|
|
5091
5159
|
function parseCommandChain(input, options) {
|
|
5092
5160
|
const result = [];
|
|
5093
5161
|
let currentPipeline = [];
|
|
@@ -5178,149 +5246,46 @@ function isSpaceAround(input, pos, tokenLen) {
|
|
|
5178
5246
|
const after = pos + tokenLen < input.length && input[pos + tokenLen] === " ";
|
|
5179
5247
|
return before && after;
|
|
5180
5248
|
}
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
if (v.startsWith("[") || v.startsWith("{")) {
|
|
5222
|
-
try {
|
|
5223
|
-
return JSON.parse(v);
|
|
5224
|
-
} catch {
|
|
5225
|
-
return v;
|
|
5226
|
-
}
|
|
5227
|
-
}
|
|
5228
|
-
return v;
|
|
5229
|
-
}
|
|
5230
|
-
function parseCommandArgs(name, args) {
|
|
5231
|
-
const definitions = getCommandDefinitions();
|
|
5232
|
-
const def = definitions[name];
|
|
5233
|
-
const positionalKeys = def ? def.positional : [];
|
|
5234
|
-
const params = {};
|
|
5235
|
-
let positionalIndex = 0;
|
|
5236
|
-
for (let i = 0; i < args.length; i++) {
|
|
5237
|
-
const raw = args[i];
|
|
5238
|
-
const arg = unquote2(raw);
|
|
5239
|
-
if (raw.startsWith("--")) {
|
|
5240
|
-
const key = raw.slice(2);
|
|
5241
|
-
const value = args[i + 1];
|
|
5242
|
-
if (value && !value.startsWith("-")) {
|
|
5243
|
-
params[key] = coerceValue2(value);
|
|
5244
|
-
i++;
|
|
5245
|
-
} else {
|
|
5246
|
-
params[key] = true;
|
|
5247
|
-
}
|
|
5248
|
-
} else if (raw.startsWith("-") && raw.length === 2) {
|
|
5249
|
-
const flag = raw[1];
|
|
5250
|
-
const mappedKey = SHORT_FLAG_MAP[flag];
|
|
5251
|
-
const value = args[i + 1];
|
|
5252
|
-
if (mappedKey && value && !value.startsWith("-")) {
|
|
5253
|
-
params[mappedKey] = coerceValue2(value);
|
|
5254
|
-
i++;
|
|
5255
|
-
} else if (value && !value.startsWith("-")) {
|
|
5256
|
-
params[flag] = coerceValue2(value);
|
|
5257
|
-
i++;
|
|
5258
|
-
} else {
|
|
5259
|
-
params[mappedKey || flag] = true;
|
|
5260
|
-
}
|
|
5261
|
-
} else {
|
|
5262
|
-
if (positionalIndex < positionalKeys.length) {
|
|
5263
|
-
const isLast = positionalIndex === positionalKeys.length - 1;
|
|
5264
|
-
if (isLast && (name === "eval" || name === "find")) {
|
|
5265
|
-
const remaining = args.slice(i).map(unquote2).join(" ");
|
|
5266
|
-
params[positionalKeys[positionalIndex]] = remaining;
|
|
5267
|
-
break;
|
|
5268
|
-
}
|
|
5269
|
-
params[positionalKeys[positionalIndex]] = arg;
|
|
5270
|
-
positionalIndex++;
|
|
5271
|
-
}
|
|
5272
|
-
}
|
|
5273
|
-
}
|
|
5274
|
-
return { command: name, params };
|
|
5275
|
-
}
|
|
5276
|
-
var commandDefCache = {
|
|
5277
|
-
goto: { positional: ["url"] },
|
|
5278
|
-
click: { positional: ["selector"] },
|
|
5279
|
-
fill: { positional: ["selector", "value"] },
|
|
5280
|
-
type: { positional: ["selector", "text"] },
|
|
5281
|
-
press: { positional: ["selector", "key"] },
|
|
5282
|
-
select: { positional: ["selector", "value"] },
|
|
5283
|
-
check: { positional: ["selector"] },
|
|
5284
|
-
uncheck: { positional: ["selector"] },
|
|
5285
|
-
hover: { positional: ["selector"] },
|
|
5286
|
-
dblclick: { positional: ["selector"] },
|
|
5287
|
-
wait: { positional: ["selector"] },
|
|
5288
|
-
screenshot: { positional: [] },
|
|
5289
|
-
eval: { positional: ["expression"] },
|
|
5290
|
-
scroll: { positional: ["direction"] },
|
|
5291
|
-
title: { positional: [] },
|
|
5292
|
-
url: { positional: [] },
|
|
5293
|
-
html: { positional: [] },
|
|
5294
|
-
text: { positional: [] },
|
|
5295
|
-
back: { positional: [] },
|
|
5296
|
-
forward: { positional: [] },
|
|
5297
|
-
refresh: { positional: [] },
|
|
5298
|
-
console: { positional: [] },
|
|
5299
|
-
network: { positional: [] },
|
|
5300
|
-
perf: { positional: [] },
|
|
5301
|
-
health: { positional: [] },
|
|
5302
|
-
scrape: { positional: ["url"] },
|
|
5303
|
-
structure: { positional: [] },
|
|
5304
|
-
"get-cookies": { positional: [] },
|
|
5305
|
-
"set-cookie": { positional: [] },
|
|
5306
|
-
"clear-cookies": { positional: [] },
|
|
5307
|
-
"get-local-storage": { positional: [] },
|
|
5308
|
-
"set-local-storage": { positional: [] },
|
|
5309
|
-
"clear-local-storage": { positional: [] },
|
|
5310
|
-
"set-viewport": { positional: [] },
|
|
5311
|
-
frames: { positional: [] },
|
|
5312
|
-
frame: { positional: ["selector"] },
|
|
5313
|
-
actions: { positional: ["url"] },
|
|
5314
|
-
find: { positional: ["strategy", "value", "operation"] },
|
|
5315
|
-
addinitscript: { positional: ["script"] },
|
|
5316
|
-
tab: { positional: ["subcommand"] }
|
|
5317
|
-
};
|
|
5318
|
-
function getCommandDefinitions() {
|
|
5319
|
-
return commandDefCache;
|
|
5320
|
-
}
|
|
5321
|
-
function registerCommandDefinition(name, positional) {
|
|
5322
|
-
commandDefCache[name] = { positional };
|
|
5323
|
-
}
|
|
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"]);
|
|
5324
5289
|
|
|
5325
5290
|
// src/commands/addinitscript.ts
|
|
5326
5291
|
var InitScriptParams = z24.object({
|
|
@@ -5350,12 +5315,12 @@ function resolveScriptContent(params) {
|
|
|
5350
5315
|
}
|
|
5351
5316
|
async function readStdin() {
|
|
5352
5317
|
const { createReadStream } = await import("fs");
|
|
5353
|
-
const { createInterface
|
|
5354
|
-
return new Promise((
|
|
5318
|
+
const { createInterface } = await import("readline");
|
|
5319
|
+
return new Promise((resolve10, reject) => {
|
|
5355
5320
|
const lines = [];
|
|
5356
|
-
const rl =
|
|
5321
|
+
const rl = createInterface({ input: createReadStream("/dev/stdin") });
|
|
5357
5322
|
rl.on("line", (line) => lines.push(line));
|
|
5358
|
-
rl.on("close", () =>
|
|
5323
|
+
rl.on("close", () => resolve10(lines.join("\n")));
|
|
5359
5324
|
rl.on("error", reject);
|
|
5360
5325
|
});
|
|
5361
5326
|
}
|
|
@@ -5364,6 +5329,15 @@ var addInitScriptCommand = registerCommand({
|
|
|
5364
5329
|
description: "Add an initialization script that runs on every page load",
|
|
5365
5330
|
scope: "page",
|
|
5366
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(),
|
|
5367
5341
|
handler: async (params, ctx) => {
|
|
5368
5342
|
if (params.list) {
|
|
5369
5343
|
const scripts = Array.from(registeredScripts.entries()).map(([n, content2]) => ({
|
|
@@ -5399,7 +5373,7 @@ registerCommandDefinition("addinitscript", ["script"]);
|
|
|
5399
5373
|
|
|
5400
5374
|
// src/commands/find.ts
|
|
5401
5375
|
import { z as z25 } from "zod";
|
|
5402
|
-
import { ok as ok24, fail as fail6 } from "@dyyz1993/xcli-core";
|
|
5376
|
+
import { ok as ok24, fail as fail6, normalizeTips as normalizeTips5 } from "@dyyz1993/xcli-core";
|
|
5403
5377
|
var actionSchema2 = z25.enum(["click", "fill", "type", "select", "hover", "check"]);
|
|
5404
5378
|
var findCommand = registerCommand({
|
|
5405
5379
|
name: "find",
|
|
@@ -5475,7 +5449,7 @@ var findCommand = registerCommand({
|
|
|
5475
5449
|
});
|
|
5476
5450
|
function okWithTips(data, tips) {
|
|
5477
5451
|
const result = ok24(data);
|
|
5478
|
-
if (tips.length > 0) result.tips = tips;
|
|
5452
|
+
if (tips.length > 0) result.tips = normalizeTips5(tips);
|
|
5479
5453
|
return result;
|
|
5480
5454
|
}
|
|
5481
5455
|
function parseOperation(operation) {
|
|
@@ -5730,16 +5704,11 @@ async function detectWebdriverExposure(page) {
|
|
|
5730
5704
|
try {
|
|
5731
5705
|
const webdriver = await page.evaluate(() => {
|
|
5732
5706
|
return {
|
|
5733
|
-
|
|
5734
|
-
webdriver: window.navigator?.webdriver,
|
|
5735
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5707
|
+
webdriver: window.navigator && window.navigator instanceof Object ? window.navigator.webdriver : void 0,
|
|
5736
5708
|
webdriverScriptFn: !!window.__webdriver_script_fn,
|
|
5737
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5738
5709
|
webdriverEvaluate: !!window.__webdriver_evaluate,
|
|
5739
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5740
5710
|
chrome: !!window.chrome,
|
|
5741
|
-
|
|
5742
|
-
permissions: window.navigator?.permissions
|
|
5711
|
+
permissions: window.navigator && window.navigator instanceof Object ? window.navigator.permissions : void 0
|
|
5743
5712
|
};
|
|
5744
5713
|
}).catch(() => null);
|
|
5745
5714
|
if (!webdriver) {
|
|
@@ -5819,377 +5788,23 @@ function formatDetectionMessage(result) {
|
|
|
5819
5788
|
Action: ${action}`;
|
|
5820
5789
|
}
|
|
5821
5790
|
|
|
5822
|
-
// src/commands/promo.ts
|
|
5823
|
-
import { z as z26 } from "zod";
|
|
5824
|
-
import { ok as ok25 } from "@dyyz1993/xcli-core";
|
|
5825
|
-
import { existsSync as existsSync2, readFileSync as readFileSync8 } from "fs";
|
|
5826
|
-
import { resolve as resolve6 } from "path";
|
|
5827
|
-
|
|
5828
|
-
// src/promo/devto.ts
|
|
5829
|
-
import { readFileSync as readFileSync3 } from "fs";
|
|
5830
|
-
import { resolve } from "path";
|
|
5831
|
-
import { execSync } from "child_process";
|
|
5832
|
-
var DEVTO_NEW_URL = "https://dev.to/new";
|
|
5833
|
-
function ab(config) {
|
|
5834
|
-
const parts = ["agent-browser"];
|
|
5835
|
-
if (config.cdpEndpoint) parts.push("--cdp", config.cdpEndpoint);
|
|
5836
|
-
if (config.session) parts.push("--session", config.session);
|
|
5837
|
-
return parts.join(" ");
|
|
5838
|
-
}
|
|
5839
|
-
function extractTitleFromMarkdown(content) {
|
|
5840
|
-
const match = content.match(/^#\s+(.+)$/m);
|
|
5841
|
-
if (match) return match[1].trim();
|
|
5842
|
-
const lines = content.split("\n").filter((l) => l.trim().length > 0);
|
|
5843
|
-
return lines[0] ? lines[0].replace(/^#+\s*/, "").trim() : "Untitled";
|
|
5844
|
-
}
|
|
5845
|
-
function stripTitleFromMarkdown(content) {
|
|
5846
|
-
return content.replace(/^#\s+.+$\n?/m, "").trim();
|
|
5847
|
-
}
|
|
5848
|
-
async function publishToDevto(config) {
|
|
5849
|
-
const cli = ab(config);
|
|
5850
|
-
try {
|
|
5851
|
-
const filePath = resolve(config.file);
|
|
5852
|
-
const raw = readFileSync3(filePath, "utf-8");
|
|
5853
|
-
const title = config.title ?? extractTitleFromMarkdown(raw);
|
|
5854
|
-
const body = stripTitleFromMarkdown(raw);
|
|
5855
|
-
execSync(`${cli} open ${DEVTO_NEW_URL}`, { encoding: "utf-8", timeout: 3e4 });
|
|
5856
|
-
const snapshot = execSync(`${cli} snapshot -i -s body`, { encoding: "utf-8", timeout: 15e3 });
|
|
5857
|
-
if (snapshot.includes("Log in") && !snapshot.includes("Notifications")) {
|
|
5858
|
-
const viewer = execSync(`${cli} viewer --json`, { encoding: "utf-8", timeout: 1e4 }).trim();
|
|
5859
|
-
return {
|
|
5860
|
-
success: false,
|
|
5861
|
-
error: `Not logged in to Dev.to. Please log in via viewer: ${viewer}`,
|
|
5862
|
-
platform: "devto"
|
|
5863
|
-
};
|
|
5864
|
-
}
|
|
5865
|
-
execSync(`${cli} fill @e_title ${JSON.stringify(title)}`, { encoding: "utf-8", timeout: 1e4 });
|
|
5866
|
-
const escapedBody = JSON.stringify(body);
|
|
5867
|
-
execSync(`${cli} fill @e_content ${escapedBody}`, { encoding: "utf-8", timeout: 15e3 });
|
|
5868
|
-
if (config.tags) {
|
|
5869
|
-
const tags = config.tags.split(",").map((t) => t.trim()).slice(0, 4).join(", ");
|
|
5870
|
-
execSync(`${cli} find text "tags" click`, { encoding: "utf-8", timeout: 1e4 });
|
|
5871
|
-
execSync(`${cli} type ${JSON.stringify(tags)}`, { encoding: "utf-8", timeout: 1e4 });
|
|
5872
|
-
}
|
|
5873
|
-
execSync(`${cli} find text "Publish" click`, { encoding: "utf-8", timeout: 15e3 });
|
|
5874
|
-
const postUrl = execSync(`${cli} get url`, { encoding: "utf-8", timeout: 15e3 }).trim();
|
|
5875
|
-
if (postUrl && postUrl !== DEVTO_NEW_URL && postUrl.includes("dev.to")) {
|
|
5876
|
-
return { success: true, url: postUrl, platform: "devto" };
|
|
5877
|
-
}
|
|
5878
|
-
return { success: true, url: postUrl || void 0, platform: "devto" };
|
|
5879
|
-
} catch (err) {
|
|
5880
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
5881
|
-
return { success: false, error: message, platform: "devto" };
|
|
5882
|
-
}
|
|
5883
|
-
}
|
|
5884
|
-
|
|
5885
|
-
// src/promo/medium.ts
|
|
5886
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
5887
|
-
import { resolve as resolve2 } from "path";
|
|
5888
|
-
import { execSync as execSync2 } from "child_process";
|
|
5889
|
-
var MEDIUM_NEW_URL = "https://medium.com/new-story";
|
|
5890
|
-
function ab2(config) {
|
|
5891
|
-
const parts = ["agent-browser"];
|
|
5892
|
-
if (config.cdpEndpoint) parts.push("--cdp", config.cdpEndpoint);
|
|
5893
|
-
if (config.session) parts.push("--session", config.session);
|
|
5894
|
-
return parts.join(" ");
|
|
5895
|
-
}
|
|
5896
|
-
function extractTitleFromMarkdown2(content) {
|
|
5897
|
-
const match = content.match(/^#\s+(.+)$/m);
|
|
5898
|
-
if (match) return match[1].trim();
|
|
5899
|
-
const lines = content.split("\n").filter((l) => l.trim().length > 0);
|
|
5900
|
-
return lines[0] ? lines[0].replace(/^#+\s*/, "").trim() : "Untitled";
|
|
5901
|
-
}
|
|
5902
|
-
function markdownToPlainText(content) {
|
|
5903
|
-
return content.replace(/^#+\s+.+$/gm, "").replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/`(.+?)`/g, "$1").replace(/\[(.+?)\]\(.+?\)/g, "$1").replace(/!\[.*?\]\(.+?\)/g, "").trim();
|
|
5904
|
-
}
|
|
5905
|
-
async function publishToMedium(config) {
|
|
5906
|
-
const cli = ab2(config);
|
|
5907
|
-
try {
|
|
5908
|
-
const filePath = resolve2(config.file);
|
|
5909
|
-
const raw = readFileSync4(filePath, "utf-8");
|
|
5910
|
-
const title = config.title ?? extractTitleFromMarkdown2(raw);
|
|
5911
|
-
const body = markdownToPlainText(raw);
|
|
5912
|
-
execSync2(`${cli} open ${MEDIUM_NEW_URL}`, { encoding: "utf-8", timeout: 3e4 });
|
|
5913
|
-
const snapshot = execSync2(`${cli} snapshot -i -s body`, { encoding: "utf-8", timeout: 15e3 });
|
|
5914
|
-
if (snapshot.includes("Sign in") && !snapshot.includes("Write")) {
|
|
5915
|
-
const viewer = execSync2(`${cli} viewer --json`, { encoding: "utf-8", timeout: 1e4 }).trim();
|
|
5916
|
-
return {
|
|
5917
|
-
success: false,
|
|
5918
|
-
error: `Not logged in to Medium. Please log in via viewer: ${viewer}`,
|
|
5919
|
-
platform: "medium"
|
|
5920
|
-
};
|
|
5921
|
-
}
|
|
5922
|
-
execSync2(`${cli} click @e_title`, { encoding: "utf-8", timeout: 1e4 });
|
|
5923
|
-
execSync2(`${cli} type ${JSON.stringify(title)}`, { encoding: "utf-8", timeout: 1e4 });
|
|
5924
|
-
execSync2(`${cli} click @e_content`, { encoding: "utf-8", timeout: 1e4 });
|
|
5925
|
-
const lines = body.split("\n");
|
|
5926
|
-
for (const line of lines) {
|
|
5927
|
-
execSync2(`${cli} type ${JSON.stringify(line)}`, { encoding: "utf-8", timeout: 1e4 });
|
|
5928
|
-
execSync2(`${cli} keyboard Enter`, { encoding: "utf-8", timeout: 5e3 });
|
|
5929
|
-
}
|
|
5930
|
-
if (config.tags) {
|
|
5931
|
-
execSync2(`${cli} find text "Tags" click`, { encoding: "utf-8", timeout: 1e4 });
|
|
5932
|
-
const tags = config.tags.split(",").map((t) => t.trim()).join(", ");
|
|
5933
|
-
execSync2(`${cli} type ${JSON.stringify(tags)}`, { encoding: "utf-8", timeout: 1e4 });
|
|
5934
|
-
}
|
|
5935
|
-
execSync2(`${cli} find text "Publish" click`, { encoding: "utf-8", timeout: 15e3 });
|
|
5936
|
-
const postUrl = execSync2(`${cli} get url`, { encoding: "utf-8", timeout: 15e3 }).trim();
|
|
5937
|
-
return { success: true, url: postUrl || void 0, platform: "medium" };
|
|
5938
|
-
} catch (err) {
|
|
5939
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
5940
|
-
return { success: false, error: message, platform: "medium" };
|
|
5941
|
-
}
|
|
5942
|
-
}
|
|
5943
|
-
|
|
5944
|
-
// src/promo/csdn.ts
|
|
5945
|
-
import { readFileSync as readFileSync5 } from "fs";
|
|
5946
|
-
import { resolve as resolve3 } from "path";
|
|
5947
|
-
import { execSync as execSync3 } from "child_process";
|
|
5948
|
-
var CSDN_EDITOR_URL = "https://mp.csdn.net/mp_blog/creation/editor";
|
|
5949
|
-
function ab3(config) {
|
|
5950
|
-
const parts = ["agent-browser"];
|
|
5951
|
-
if (config.cdpEndpoint) parts.push("--cdp", config.cdpEndpoint);
|
|
5952
|
-
if (config.session) parts.push("--session", config.session);
|
|
5953
|
-
return parts.join(" ");
|
|
5954
|
-
}
|
|
5955
|
-
function extractTitleFromMarkdown3(content) {
|
|
5956
|
-
const match = content.match(/^#\s+(.+)$/m);
|
|
5957
|
-
if (match) return match[1].trim();
|
|
5958
|
-
const lines = content.split("\n").filter((l) => l.trim().length > 0);
|
|
5959
|
-
return lines[0] ? lines[0].replace(/^#+\s*/, "").trim() : "Untitled";
|
|
5960
|
-
}
|
|
5961
|
-
async function publishToCsdn(config) {
|
|
5962
|
-
const cli = ab3(config);
|
|
5963
|
-
try {
|
|
5964
|
-
const filePath = resolve3(config.file);
|
|
5965
|
-
const raw = readFileSync5(filePath, "utf-8");
|
|
5966
|
-
const title = config.title ?? extractTitleFromMarkdown3(raw);
|
|
5967
|
-
const body = raw.replace(/^#\s+.+$\n?/m, "").trim();
|
|
5968
|
-
execSync3(`${cli} open ${CSDN_EDITOR_URL}`, { encoding: "utf-8", timeout: 3e4 });
|
|
5969
|
-
const snapshot = execSync3(`${cli} snapshot -i -s body`, { encoding: "utf-8", timeout: 15e3 });
|
|
5970
|
-
if (snapshot.includes("\u767B\u5F55") && !snapshot.includes("\u5199\u6587\u7AE0")) {
|
|
5971
|
-
const viewer = execSync3(`${cli} viewer --json`, { encoding: "utf-8", timeout: 1e4 }).trim();
|
|
5972
|
-
return {
|
|
5973
|
-
success: false,
|
|
5974
|
-
error: `Not logged in to CSDN. Please log in via viewer: ${viewer}`,
|
|
5975
|
-
platform: "csdn"
|
|
5976
|
-
};
|
|
5977
|
-
}
|
|
5978
|
-
execSync3(`${cli} fill @e_title ${JSON.stringify(title)}`, { encoding: "utf-8", timeout: 1e4 });
|
|
5979
|
-
const escapedBody = JSON.stringify(body);
|
|
5980
|
-
execSync3(`${cli} fill @e_content ${escapedBody}`, { encoding: "utf-8", timeout: 15e3 });
|
|
5981
|
-
if (config.tags) {
|
|
5982
|
-
const tags = config.tags.split(",").map((t) => t.trim());
|
|
5983
|
-
for (const tag of tags) {
|
|
5984
|
-
execSync3(`${cli} find text "\u6DFB\u52A0\u6807\u7B7E" click`, { encoding: "utf-8", timeout: 1e4 });
|
|
5985
|
-
execSync3(`${cli} type ${JSON.stringify(tag)}`, { encoding: "utf-8", timeout: 1e4 });
|
|
5986
|
-
execSync3(`${cli} keyboard Enter`, { encoding: "utf-8", timeout: 5e3 });
|
|
5987
|
-
}
|
|
5988
|
-
}
|
|
5989
|
-
execSync3(`${cli} find text "\u53D1\u5E03" click`, { encoding: "utf-8", timeout: 15e3 });
|
|
5990
|
-
const postUrl = execSync3(`${cli} get url`, { encoding: "utf-8", timeout: 15e3 }).trim();
|
|
5991
|
-
return { success: true, url: postUrl || void 0, platform: "csdn" };
|
|
5992
|
-
} catch (err) {
|
|
5993
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
5994
|
-
return { success: false, error: message, platform: "csdn" };
|
|
5995
|
-
}
|
|
5996
|
-
}
|
|
5997
|
-
|
|
5998
|
-
// src/promo/juejin.ts
|
|
5999
|
-
import { readFileSync as readFileSync6 } from "fs";
|
|
6000
|
-
import { resolve as resolve4 } from "path";
|
|
6001
|
-
import { execSync as execSync4 } from "child_process";
|
|
6002
|
-
var JUEJIN_EDITOR_URL = "https://juejin.cn/editor/draft/new";
|
|
6003
|
-
function ab4(config) {
|
|
6004
|
-
const parts = ["agent-browser"];
|
|
6005
|
-
if (config.cdpEndpoint) parts.push("--cdp", config.cdpEndpoint);
|
|
6006
|
-
if (config.session) parts.push("--session", config.session);
|
|
6007
|
-
return parts.join(" ");
|
|
6008
|
-
}
|
|
6009
|
-
function extractTitleFromMarkdown4(content) {
|
|
6010
|
-
const match = content.match(/^#\s+(.+)$/m);
|
|
6011
|
-
if (match) return match[1].trim();
|
|
6012
|
-
const lines = content.split("\n").filter((l) => l.trim().length > 0);
|
|
6013
|
-
return lines[0] ? lines[0].replace(/^#+\s*/, "").trim() : "Untitled";
|
|
6014
|
-
}
|
|
6015
|
-
async function publishToJuejin(config) {
|
|
6016
|
-
const cli = ab4(config);
|
|
6017
|
-
try {
|
|
6018
|
-
const filePath = resolve4(config.file);
|
|
6019
|
-
const raw = readFileSync6(filePath, "utf-8");
|
|
6020
|
-
const title = config.title ?? extractTitleFromMarkdown4(raw);
|
|
6021
|
-
const body = raw.replace(/^#\s+.+$\n?/m, "").trim();
|
|
6022
|
-
execSync4(`${cli} open ${JUEJIN_EDITOR_URL}`, { encoding: "utf-8", timeout: 3e4 });
|
|
6023
|
-
const snapshot = execSync4(`${cli} snapshot -i -s body`, { encoding: "utf-8", timeout: 15e3 });
|
|
6024
|
-
if (snapshot.includes("\u767B\u5F55") && snapshot.includes("\u6CE8\u518C") && !snapshot.includes("\u521B\u4F5C\u8005\u4E2D\u5FC3")) {
|
|
6025
|
-
const viewer = execSync4(`${cli} viewer --json`, { encoding: "utf-8", timeout: 1e4 }).trim();
|
|
6026
|
-
return {
|
|
6027
|
-
success: false,
|
|
6028
|
-
error: `Not logged in to Juejin. Please log in via viewer: ${viewer}`,
|
|
6029
|
-
platform: "juejin"
|
|
6030
|
-
};
|
|
6031
|
-
}
|
|
6032
|
-
execSync4(`${cli} fill @e_title ${JSON.stringify(title)}`, { encoding: "utf-8", timeout: 1e4 });
|
|
6033
|
-
const escapedBody = JSON.stringify(body);
|
|
6034
|
-
execSync4(`${cli} fill @e_content ${escapedBody}`, { encoding: "utf-8", timeout: 15e3 });
|
|
6035
|
-
if (config.tags) {
|
|
6036
|
-
const tags = config.tags.split(",").map((t) => t.trim());
|
|
6037
|
-
for (const tag of tags) {
|
|
6038
|
-
execSync4(`${cli} find text "\u6DFB\u52A0\u6807\u7B7E" click`, { encoding: "utf-8", timeout: 1e4 });
|
|
6039
|
-
execSync4(`${cli} type ${JSON.stringify(tag)}`, { encoding: "utf-8", timeout: 1e4 });
|
|
6040
|
-
execSync4(`${cli} keyboard Enter`, { encoding: "utf-8", timeout: 5e3 });
|
|
6041
|
-
}
|
|
6042
|
-
}
|
|
6043
|
-
execSync4(`${cli} find text "\u53D1\u5E03" click`, { encoding: "utf-8", timeout: 15e3 });
|
|
6044
|
-
const postUrl = execSync4(`${cli} get url`, { encoding: "utf-8", timeout: 15e3 }).trim();
|
|
6045
|
-
return { success: true, url: postUrl || void 0, platform: "juejin" };
|
|
6046
|
-
} catch (err) {
|
|
6047
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
6048
|
-
return { success: false, error: message, platform: "juejin" };
|
|
6049
|
-
}
|
|
6050
|
-
}
|
|
6051
|
-
|
|
6052
|
-
// src/promo/quora.ts
|
|
6053
|
-
import { readFileSync as readFileSync7 } from "fs";
|
|
6054
|
-
import { resolve as resolve5 } from "path";
|
|
6055
|
-
import { execSync as execSync5 } from "child_process";
|
|
6056
|
-
function ab5(config) {
|
|
6057
|
-
const parts = ["agent-browser"];
|
|
6058
|
-
if (config.cdpEndpoint) parts.push("--cdp", config.cdpEndpoint);
|
|
6059
|
-
if (config.session) parts.push("--session", config.session);
|
|
6060
|
-
return parts.join(" ");
|
|
6061
|
-
}
|
|
6062
|
-
async function publishToQuora(config) {
|
|
6063
|
-
const cli = ab5(config);
|
|
6064
|
-
try {
|
|
6065
|
-
if (!config.search) {
|
|
6066
|
-
return {
|
|
6067
|
-
success: false,
|
|
6068
|
-
error: "Quora requires --search parameter to find relevant questions",
|
|
6069
|
-
platform: "quora"
|
|
6070
|
-
};
|
|
6071
|
-
}
|
|
6072
|
-
const filePath = resolve5(config.file);
|
|
6073
|
-
const raw = readFileSync7(filePath, "utf-8");
|
|
6074
|
-
const answer = raw.trim();
|
|
6075
|
-
const searchUrl = `https://www.quora.com/search?q=${encodeURIComponent(config.search)}`;
|
|
6076
|
-
execSync5(`${cli} open ${searchUrl}`, { encoding: "utf-8", timeout: 3e4 });
|
|
6077
|
-
const snapshot = execSync5(`${cli} snapshot -i -s body`, { encoding: "utf-8", timeout: 15e3 });
|
|
6078
|
-
if (!snapshot.includes("Add question")) {
|
|
6079
|
-
const viewer = execSync5(`${cli} viewer --json`, { encoding: "utf-8", timeout: 1e4 }).trim();
|
|
6080
|
-
return {
|
|
6081
|
-
success: false,
|
|
6082
|
-
error: `Not logged in to Quora. Please log in via viewer: ${viewer}`,
|
|
6083
|
-
platform: "quora"
|
|
6084
|
-
};
|
|
6085
|
-
}
|
|
6086
|
-
const answerMatch = snapshot.match(/Answer\s*(?:button|link)/i);
|
|
6087
|
-
if (!answerMatch) {
|
|
6088
|
-
return {
|
|
6089
|
-
success: false,
|
|
6090
|
-
error: "No answerable questions found for the given search query. Try a different search term.",
|
|
6091
|
-
platform: "quora"
|
|
6092
|
-
};
|
|
6093
|
-
}
|
|
6094
|
-
execSync5(`${cli} find text "Answer" click`, { encoding: "utf-8", timeout: 1e4 });
|
|
6095
|
-
const escapedAnswer = JSON.stringify(answer);
|
|
6096
|
-
execSync5(`${cli} fill @e_answer ${escapedAnswer}`, { encoding: "utf-8", timeout: 15e3 });
|
|
6097
|
-
execSync5(`${cli} find text "Submit" click`, { encoding: "utf-8", timeout: 15e3 });
|
|
6098
|
-
const postUrl = execSync5(`${cli} get url`, { encoding: "utf-8", timeout: 15e3 }).trim();
|
|
6099
|
-
return { success: true, url: postUrl || void 0, platform: "quora" };
|
|
6100
|
-
} catch (err) {
|
|
6101
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
6102
|
-
return { success: false, error: message, platform: "quora" };
|
|
6103
|
-
}
|
|
6104
|
-
}
|
|
6105
|
-
|
|
6106
|
-
// src/promo/index.ts
|
|
6107
|
-
var PUBLISHERS = {
|
|
6108
|
-
devto: publishToDevto,
|
|
6109
|
-
medium: publishToMedium,
|
|
6110
|
-
csdn: publishToCsdn,
|
|
6111
|
-
juejin: publishToJuejin,
|
|
6112
|
-
quora: publishToQuora
|
|
6113
|
-
};
|
|
6114
|
-
async function dispatchPromo(config) {
|
|
6115
|
-
const publisher = PUBLISHERS[config.platform];
|
|
6116
|
-
if (!publisher) {
|
|
6117
|
-
return {
|
|
6118
|
-
success: false,
|
|
6119
|
-
error: `Unknown platform: ${config.platform}. Supported: ${Object.keys(PUBLISHERS).join(", ")}`,
|
|
6120
|
-
platform: config.platform
|
|
6121
|
-
};
|
|
6122
|
-
}
|
|
6123
|
-
return publisher(config);
|
|
6124
|
-
}
|
|
6125
|
-
|
|
6126
|
-
// src/commands/promo.ts
|
|
6127
|
-
var promoParams = z26.object({
|
|
6128
|
-
platform: z26.enum(["devto", "medium", "csdn", "juejin", "quora"]).describe("Target platform for promotion"),
|
|
6129
|
-
file: z26.string().describe("Path to Markdown file to publish"),
|
|
6130
|
-
tags: z26.string().optional().describe("Comma-separated tags"),
|
|
6131
|
-
title: z26.string().optional().describe("Custom title (default: extracted from file first heading)"),
|
|
6132
|
-
search: z26.string().optional().describe("Quora: search query to find questions"),
|
|
6133
|
-
cdpEndpoint: z26.string().optional().describe("CDP endpoint for agent-browser"),
|
|
6134
|
-
session: z26.string().optional().describe("agent-browser session name")
|
|
6135
|
-
}).refine(
|
|
6136
|
-
(data) => data.platform !== "quora" || !!data.search,
|
|
6137
|
-
{ message: "Quora platform requires --search parameter" }
|
|
6138
|
-
).refine(
|
|
6139
|
-
(data) => existsSync2(resolve6(data.file)),
|
|
6140
|
-
{ message: "File does not exist" }
|
|
6141
|
-
);
|
|
6142
|
-
var promoCommand = registerCommand({
|
|
6143
|
-
name: "promo",
|
|
6144
|
-
description: "Publish promotional articles to various platforms (devto, medium, csdn, juejin, quora)",
|
|
6145
|
-
scope: "project",
|
|
6146
|
-
parameters: promoParams,
|
|
6147
|
-
result: z26.object({
|
|
6148
|
-
success: z26.boolean(),
|
|
6149
|
-
url: z26.string().optional(),
|
|
6150
|
-
error: z26.string().optional(),
|
|
6151
|
-
platform: z26.string()
|
|
6152
|
-
}),
|
|
6153
|
-
handler: async (p, _ctx) => {
|
|
6154
|
-
const filePath = resolve6(p.file);
|
|
6155
|
-
const content = readFileSync8(filePath, "utf-8");
|
|
6156
|
-
if (content.trim().length === 0) {
|
|
6157
|
-
return ok25({
|
|
6158
|
-
success: false,
|
|
6159
|
-
error: `File is empty: ${filePath}`,
|
|
6160
|
-
platform: p.platform
|
|
6161
|
-
});
|
|
6162
|
-
}
|
|
6163
|
-
const result = await dispatchPromo({
|
|
6164
|
-
platform: p.platform,
|
|
6165
|
-
file: filePath,
|
|
6166
|
-
tags: p.tags,
|
|
6167
|
-
title: p.title,
|
|
6168
|
-
search: p.search,
|
|
6169
|
-
cdpEndpoint: p.cdpEndpoint ?? _ctx.cdpEndpoint,
|
|
6170
|
-
session: p.session ?? p.platform
|
|
6171
|
-
});
|
|
6172
|
-
return ok25(result);
|
|
6173
|
-
}
|
|
6174
|
-
});
|
|
6175
|
-
|
|
6176
5791
|
// src/plugin/loader.ts
|
|
6177
5792
|
import {
|
|
6178
5793
|
Core
|
|
6179
5794
|
} from "@dyyz1993/xcli-core";
|
|
6180
|
-
import { resolve as
|
|
6181
|
-
import { existsSync as
|
|
6182
|
-
import { homedir as
|
|
5795
|
+
import { resolve as resolve2 } from "path";
|
|
5796
|
+
import { existsSync as existsSync4, readdirSync } from "fs";
|
|
5797
|
+
import { homedir as homedir4 } from "os";
|
|
6183
5798
|
|
|
6184
5799
|
// src/plugin/metadata-parser.ts
|
|
6185
|
-
import { existsSync as
|
|
6186
|
-
import { resolve
|
|
5800
|
+
import { existsSync as existsSync2 } from "fs";
|
|
5801
|
+
import { resolve } from "path";
|
|
6187
5802
|
|
|
6188
5803
|
// src/utils/json-file.ts
|
|
6189
|
-
import { readFileSync as
|
|
5804
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
6190
5805
|
function readJsonFile(filePath, defaultValue) {
|
|
6191
5806
|
try {
|
|
6192
|
-
const content =
|
|
5807
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
6193
5808
|
return JSON.parse(content);
|
|
6194
5809
|
} catch {
|
|
6195
5810
|
return defaultValue;
|
|
@@ -6200,8 +5815,8 @@ function readJsonFile(filePath, defaultValue) {
|
|
|
6200
5815
|
var PluginMetadataParser = class {
|
|
6201
5816
|
static XBROWSER_KEYWORDS = ["xbrowser", "xbrowser-plugin"];
|
|
6202
5817
|
static parseFromPackageJson(pluginPath) {
|
|
6203
|
-
const packageJsonPath =
|
|
6204
|
-
if (!
|
|
5818
|
+
const packageJsonPath = resolve(pluginPath, "package.json");
|
|
5819
|
+
if (!existsSync2(packageJsonPath)) {
|
|
6205
5820
|
return null;
|
|
6206
5821
|
}
|
|
6207
5822
|
const packageJson = readJsonFile(packageJsonPath, null);
|
|
@@ -6265,20 +5880,20 @@ var PluginMetadataParser = class {
|
|
|
6265
5880
|
};
|
|
6266
5881
|
|
|
6267
5882
|
// src/plugin/ensure-deps.ts
|
|
6268
|
-
import { existsSync as
|
|
6269
|
-
import { join as
|
|
6270
|
-
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";
|
|
6271
5886
|
var SHARED_PLUGIN_DEPENDENCIES = {
|
|
6272
5887
|
"zod": "^3.24.0",
|
|
6273
5888
|
"@dyyz1993/xcli-core": "^0.12.1"
|
|
6274
5889
|
};
|
|
6275
5890
|
function ensurePluginDependencies(pluginsDir) {
|
|
6276
|
-
const zodPath =
|
|
6277
|
-
if (
|
|
6278
|
-
|
|
6279
|
-
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");
|
|
6280
5895
|
let pkg2 = {};
|
|
6281
|
-
if (
|
|
5896
|
+
if (existsSync3(pkgPath)) {
|
|
6282
5897
|
try {
|
|
6283
5898
|
pkg2 = readJsonFile(pkgPath, {});
|
|
6284
5899
|
} catch {
|
|
@@ -6292,13 +5907,13 @@ function ensurePluginDependencies(pluginsDir) {
|
|
|
6292
5907
|
needsInstall = true;
|
|
6293
5908
|
}
|
|
6294
5909
|
}
|
|
6295
|
-
if (!needsInstall &&
|
|
5910
|
+
if (!needsInstall && existsSync3(join4(pluginsDir, "node_modules"))) return;
|
|
6296
5911
|
pkg2.dependencies = existingDeps;
|
|
6297
5912
|
pkg2.private = true;
|
|
6298
5913
|
pkg2.description = pkg2.description || "xbrowser plugins \u2014 shared dependencies";
|
|
6299
|
-
|
|
5914
|
+
writeFileSync5(pkgPath, JSON.stringify(pkg2, null, 2) + "\n", "utf-8");
|
|
6300
5915
|
try {
|
|
6301
|
-
|
|
5916
|
+
execSync("npm install --production --no-package-lock --no-fund --no-audit", {
|
|
6302
5917
|
cwd: pluginsDir,
|
|
6303
5918
|
stdio: "pipe",
|
|
6304
5919
|
timeout: 6e4,
|
|
@@ -6310,8 +5925,15 @@ function ensurePluginDependencies(pluginsDir) {
|
|
|
6310
5925
|
}
|
|
6311
5926
|
|
|
6312
5927
|
// src/plugin/contract.ts
|
|
5928
|
+
import {
|
|
5929
|
+
unwrapZod,
|
|
5930
|
+
fieldsFromZodObjectReflected,
|
|
5931
|
+
zodTypeToContractType
|
|
5932
|
+
} from "@dyyz1993/xcli-core";
|
|
6313
5933
|
function buildPluginContract(site) {
|
|
6314
|
-
const commands = site.getAllCommands().map((command) => buildCommandContract(site.getCommand?.(command.name) || command
|
|
5934
|
+
const commands = site.getAllCommands().map((command) => buildCommandContract(site.getCommand?.(command.name) || command, {
|
|
5935
|
+
siteRequiresLogin: site.config?.requiresLogin
|
|
5936
|
+
}));
|
|
6315
5937
|
return {
|
|
6316
5938
|
version: 2,
|
|
6317
5939
|
plugin: {
|
|
@@ -6323,18 +5945,19 @@ function buildPluginContract(site) {
|
|
|
6323
5945
|
commands
|
|
6324
5946
|
};
|
|
6325
5947
|
}
|
|
6326
|
-
function buildCommandContract(command) {
|
|
5948
|
+
function buildCommandContract(command, options = {}) {
|
|
6327
5949
|
const extension = command.xbrowser || {};
|
|
6328
5950
|
const inferredFields = fieldsFromZodObject(command.parameters);
|
|
6329
5951
|
const fields = mergeFields(inferredFields, extension.form?.fields || []);
|
|
6330
5952
|
const positional = extension.positional || fields.filter((field) => field.positional).map((field) => field.name);
|
|
6331
|
-
const
|
|
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);
|
|
6332
5955
|
const outputSchema = command.result ? summarizeZod(command.result) : void 0;
|
|
6333
5956
|
return {
|
|
6334
5957
|
name: command.name,
|
|
6335
5958
|
description: command.description || "",
|
|
6336
5959
|
scope: command.scope || "project",
|
|
6337
|
-
requiresLogin
|
|
5960
|
+
requiresLogin,
|
|
6338
5961
|
category: extension.category,
|
|
6339
5962
|
capabilities,
|
|
6340
5963
|
positional,
|
|
@@ -6348,25 +5971,21 @@ function buildCommandContract(command) {
|
|
|
6348
5971
|
};
|
|
6349
5972
|
}
|
|
6350
5973
|
function fieldsFromZodObject(schema) {
|
|
6351
|
-
const
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
...unwrapped.defaultValue !== void 0 ? { default: unwrapped.defaultValue } : {},
|
|
6367
|
-
...enumValues ? { enum: enumValues } : {},
|
|
6368
|
-
...type === "array" ? { multiple: true } : {}
|
|
6369
|
-
};
|
|
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
|
+
});
|
|
6370
5989
|
}
|
|
6371
5990
|
function mergeFields(inferred, overrides) {
|
|
6372
5991
|
if (overrides.length === 0) return inferred;
|
|
@@ -6397,72 +6016,14 @@ function inferCapabilities(scope, requiresLogin) {
|
|
|
6397
6016
|
if (requiresLogin) caps.push("auth.login");
|
|
6398
6017
|
return caps;
|
|
6399
6018
|
}
|
|
6400
|
-
function
|
|
6401
|
-
const zod = schema;
|
|
6402
|
-
const shapeOrFn = zod?.shape ?? zod?._def?.shape;
|
|
6403
|
-
if (!shapeOrFn) return void 0;
|
|
6404
|
-
return typeof shapeOrFn === "function" ? shapeOrFn() : shapeOrFn;
|
|
6405
|
-
}
|
|
6406
|
-
function unwrapZod(schema) {
|
|
6407
|
-
let current = schema;
|
|
6408
|
-
let optional = typeof current?.isOptional === "function" ? current.isOptional() : false;
|
|
6409
|
-
let description = current?._def?.description;
|
|
6410
|
-
let defaultValue;
|
|
6411
|
-
for (let i = 0; i < 8; i++) {
|
|
6412
|
-
const def = current?._def;
|
|
6413
|
-
const typeName = def?.typeName || "unknown";
|
|
6414
|
-
if (def?.description) description = def.description;
|
|
6415
|
-
if (!def) return { schema: current, typeName, optional, description, defaultValue };
|
|
6416
|
-
if (typeName === "ZodDefault") {
|
|
6417
|
-
optional = true;
|
|
6418
|
-
defaultValue = typeof def.defaultValue === "function" ? def.defaultValue() : def.defaultValue;
|
|
6419
|
-
current = def.innerType || def.type;
|
|
6420
|
-
continue;
|
|
6421
|
-
}
|
|
6422
|
-
if (typeName === "ZodOptional" || typeName === "ZodNullable") {
|
|
6423
|
-
optional = true;
|
|
6424
|
-
current = def.innerType || def.type;
|
|
6425
|
-
continue;
|
|
6426
|
-
}
|
|
6427
|
-
return { schema: current, typeName, optional, description, defaultValue };
|
|
6428
|
-
}
|
|
6429
|
-
return { schema: current, typeName: current?._def?.typeName || "unknown", optional, description, defaultValue };
|
|
6430
|
-
}
|
|
6431
|
-
function zodTypeToContractType(typeName) {
|
|
6432
|
-
switch (typeName) {
|
|
6433
|
-
case "ZodString":
|
|
6434
|
-
return "string";
|
|
6435
|
-
case "ZodNumber":
|
|
6436
|
-
return "number";
|
|
6437
|
-
case "ZodBoolean":
|
|
6438
|
-
return "boolean";
|
|
6439
|
-
case "ZodEnum":
|
|
6440
|
-
case "ZodNativeEnum":
|
|
6441
|
-
return "enum";
|
|
6442
|
-
case "ZodArray":
|
|
6443
|
-
return "array";
|
|
6444
|
-
case "ZodObject":
|
|
6445
|
-
return "object";
|
|
6446
|
-
default:
|
|
6447
|
-
return typeName.replace(/^Zod/, "").toLowerCase() || "unknown";
|
|
6448
|
-
}
|
|
6449
|
-
}
|
|
6450
|
-
function widgetFor(type, enumValues, schema) {
|
|
6019
|
+
function widgetFor(type, enumValues) {
|
|
6451
6020
|
if (enumValues) return "select";
|
|
6452
6021
|
if (type === "boolean") return "checkbox";
|
|
6453
6022
|
if (type === "number") return "number";
|
|
6454
6023
|
if (type === "array") return "multi-select";
|
|
6455
6024
|
if (type === "object") return "json";
|
|
6456
|
-
const checks = schema?._def?.checks;
|
|
6457
|
-
if (checks?.some((check) => check.kind === "url")) return "url";
|
|
6458
6025
|
return "text";
|
|
6459
6026
|
}
|
|
6460
|
-
function extractEnumValues(schema) {
|
|
6461
|
-
const def = schema?._def;
|
|
6462
|
-
const values = def?.values;
|
|
6463
|
-
if (Array.isArray(values)) return values.map(String);
|
|
6464
|
-
return void 0;
|
|
6465
|
-
}
|
|
6466
6027
|
function summarizeZod(schema) {
|
|
6467
6028
|
const unwrapped = unwrapZod(schema);
|
|
6468
6029
|
if (unwrapped.typeName === "ZodArray") {
|
|
@@ -6472,7 +6033,7 @@ function summarizeZod(schema) {
|
|
|
6472
6033
|
items: summarizeZod(def?.type || def?.innerType)
|
|
6473
6034
|
};
|
|
6474
6035
|
}
|
|
6475
|
-
const shape =
|
|
6036
|
+
const shape = getObjectShape(schema);
|
|
6476
6037
|
if (!shape) {
|
|
6477
6038
|
return {
|
|
6478
6039
|
type: zodTypeToContractType(unwrapped.typeName),
|
|
@@ -6482,19 +6043,46 @@ function summarizeZod(schema) {
|
|
|
6482
6043
|
}
|
|
6483
6044
|
return Object.fromEntries(
|
|
6484
6045
|
Object.entries(shape).map(([name, field]) => {
|
|
6485
|
-
const
|
|
6046
|
+
const inner = unwrapZod(field);
|
|
6486
6047
|
return [name, {
|
|
6487
|
-
type: zodTypeToContractType(
|
|
6488
|
-
required: !
|
|
6489
|
-
...
|
|
6048
|
+
type: zodTypeToContractType(inner.typeName),
|
|
6049
|
+
required: !inner.optional,
|
|
6050
|
+
...inner.description ? { description: inner.description } : {}
|
|
6490
6051
|
}];
|
|
6491
6052
|
})
|
|
6492
6053
|
);
|
|
6493
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
|
+
}
|
|
6494
6061
|
function toLabel(name) {
|
|
6495
6062
|
return name.replace(/([A-Z])/g, " $1").replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim().replace(/^./, (char) => char.toUpperCase());
|
|
6496
6063
|
}
|
|
6497
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
|
+
|
|
6498
6086
|
// src/plugin/loader.ts
|
|
6499
6087
|
var DEFAULT_PLUGIN_DIRS = [".xcli/plugins", "../.xcli/plugins"];
|
|
6500
6088
|
var XBrowserPluginLoader = class {
|
|
@@ -6502,6 +6090,7 @@ var XBrowserPluginLoader = class {
|
|
|
6502
6090
|
loader;
|
|
6503
6091
|
options;
|
|
6504
6092
|
constructor(options) {
|
|
6093
|
+
patchLoginRequired();
|
|
6505
6094
|
this.options = options ?? {};
|
|
6506
6095
|
const cwd = this.options.cwd || process.cwd();
|
|
6507
6096
|
const coreConfig = {
|
|
@@ -6512,7 +6101,7 @@ var XBrowserPluginLoader = class {
|
|
|
6512
6101
|
envPrefix: "XBROWSER",
|
|
6513
6102
|
pluginDirs: [
|
|
6514
6103
|
...DEFAULT_PLUGIN_DIRS,
|
|
6515
|
-
|
|
6104
|
+
resolve2(cwd, ".xcli/plugins")
|
|
6516
6105
|
]
|
|
6517
6106
|
};
|
|
6518
6107
|
this.core = new Core(coreConfig);
|
|
@@ -6558,31 +6147,31 @@ var XBrowserPluginLoader = class {
|
|
|
6558
6147
|
}
|
|
6559
6148
|
async scanAndLoad() {
|
|
6560
6149
|
const cwd = this.options.cwd || process.cwd();
|
|
6561
|
-
const globalDir = this.options.globalDir ||
|
|
6150
|
+
const globalDir = this.options.globalDir || resolve2(homedir4(), ".xbrowser/plugins");
|
|
6562
6151
|
ensurePluginDependencies(globalDir);
|
|
6563
6152
|
const dirs = [
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
this.options.userDir ||
|
|
6153
|
+
resolve2(cwd, ".xcli/plugins"),
|
|
6154
|
+
resolve2(cwd, "../.xcli/plugins"),
|
|
6155
|
+
this.options.userDir || resolve2(homedir4(), ".xcli/plugins"),
|
|
6567
6156
|
globalDir
|
|
6568
6157
|
];
|
|
6569
6158
|
const loaded = [];
|
|
6570
6159
|
const seen = /* @__PURE__ */ new Set();
|
|
6571
6160
|
for (const dir of dirs) {
|
|
6572
|
-
if (!
|
|
6161
|
+
if (!existsSync4(dir)) continue;
|
|
6573
6162
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
6574
6163
|
for (const entry of entries) {
|
|
6575
6164
|
if (!entry.isDirectory()) continue;
|
|
6576
6165
|
if (seen.has(entry.name)) continue;
|
|
6577
6166
|
seen.add(entry.name);
|
|
6578
|
-
const pluginDir =
|
|
6579
|
-
let indexPath =
|
|
6580
|
-
if (!
|
|
6581
|
-
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");
|
|
6582
6171
|
}
|
|
6583
|
-
if (!
|
|
6172
|
+
if (!existsSync4(indexPath)) continue;
|
|
6584
6173
|
try {
|
|
6585
|
-
if (!
|
|
6174
|
+
if (!existsSync4(resolve2(pluginDir, "package.json"))) {
|
|
6586
6175
|
console.warn(`\u26A0\uFE0F Plugin "${entry.name}" has no package.json. Use "xbrowser create ${entry.name} --template static" for proper structure.`);
|
|
6587
6176
|
} else {
|
|
6588
6177
|
const metadata = PluginMetadataParser.parseFromPackageJson(pluginDir);
|
|
@@ -6620,6 +6209,110 @@ async function getPluginLoader() {
|
|
|
6620
6209
|
return pluginLoader;
|
|
6621
6210
|
}
|
|
6622
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
|
+
|
|
6623
6316
|
// src/tips/dom-watcher.ts
|
|
6624
6317
|
var DOM_WATCHER_SCRIPT = `
|
|
6625
6318
|
(function() {
|
|
@@ -6999,7 +6692,7 @@ var TipsManager = class {
|
|
|
6999
6692
|
}
|
|
7000
6693
|
}
|
|
7001
6694
|
debounce() {
|
|
7002
|
-
return new Promise((
|
|
6695
|
+
return new Promise((resolve10) => setTimeout(resolve10, DEBOUNCE_MS));
|
|
7003
6696
|
}
|
|
7004
6697
|
formatTips(tips) {
|
|
7005
6698
|
return tips.map((tip) => {
|
|
@@ -7027,33 +6720,105 @@ function getTipsManager() {
|
|
|
7027
6720
|
}
|
|
7028
6721
|
|
|
7029
6722
|
// src/hooks/loader.ts
|
|
7030
|
-
var
|
|
7031
|
-
|
|
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
|
+
}
|
|
7032
6789
|
};
|
|
6790
|
+
var customHooks = {};
|
|
7033
6791
|
async function loadHooks() {
|
|
7034
|
-
const
|
|
7035
|
-
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 [];
|
|
7036
6796
|
const hooks = [];
|
|
7037
|
-
for (const name of names
|
|
7038
|
-
const
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
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);
|
|
7042
6807
|
}
|
|
7043
6808
|
}
|
|
7044
6809
|
return hooks;
|
|
7045
6810
|
}
|
|
7046
6811
|
|
|
7047
6812
|
// src/executor.ts
|
|
7048
|
-
import { homedir as
|
|
7049
|
-
import { join as
|
|
6813
|
+
import { homedir as homedir5 } from "os";
|
|
6814
|
+
import { join as join5 } from "path";
|
|
7050
6815
|
var NAVIGATION_COMMANDS = /* @__PURE__ */ new Set(["goto", "back", "forward", "refresh"]);
|
|
7051
6816
|
var snapshotHintShown = /* @__PURE__ */ new WeakSet();
|
|
7052
|
-
var
|
|
6817
|
+
var CONFIG_DIR = join5(homedir5(), ".xbrowser");
|
|
7053
6818
|
var storageCache = /* @__PURE__ */ new Map();
|
|
7054
6819
|
function getPluginStorage(pluginName) {
|
|
7055
6820
|
if (!storageCache.has(pluginName)) {
|
|
7056
|
-
storageCache.set(pluginName, new
|
|
6821
|
+
storageCache.set(pluginName, new CompositeStorage(pluginName, CONFIG_DIR, "xbrowser"));
|
|
7057
6822
|
}
|
|
7058
6823
|
return storageCache.get(pluginName);
|
|
7059
6824
|
}
|
|
@@ -7061,7 +6826,7 @@ var archiveInitialized = false;
|
|
|
7061
6826
|
function ensureArchiveInit() {
|
|
7062
6827
|
if (!archiveInitialized) {
|
|
7063
6828
|
try {
|
|
7064
|
-
configureArchiveStore({ archiveDir:
|
|
6829
|
+
configureArchiveStore({ archiveDir: join5(homedir5(), ".xbrowser", "archives") });
|
|
7065
6830
|
} catch {
|
|
7066
6831
|
}
|
|
7067
6832
|
archiveInitialized = true;
|
|
@@ -7087,6 +6852,10 @@ async function guardCheck(commandName) {
|
|
|
7087
6852
|
function errorResult(message) {
|
|
7088
6853
|
return { ...fail7(message), duration: 0 };
|
|
7089
6854
|
}
|
|
6855
|
+
function tipsToMessages(tips) {
|
|
6856
|
+
if (!tips || tips.length === 0) return [];
|
|
6857
|
+
return tips.map((t) => typeof t === "string" ? t : t.message);
|
|
6858
|
+
}
|
|
7090
6859
|
var wsServer = null;
|
|
7091
6860
|
function streamCommandEvent(sessionId, message) {
|
|
7092
6861
|
if (!wsServer || !wsServer.getRunning()) return;
|
|
@@ -7114,7 +6883,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
7114
6883
|
}
|
|
7115
6884
|
let targetPageOverride = null;
|
|
7116
6885
|
if (_target && extraOpts?.cdpEndpoint) {
|
|
7117
|
-
const { findTargetPage } = await import("./browser-
|
|
6886
|
+
const { findTargetPage } = await import("./browser-R56O3CW6.js");
|
|
7118
6887
|
targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
|
|
7119
6888
|
if (!targetPageOverride) {
|
|
7120
6889
|
return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
|
|
@@ -7131,7 +6900,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
7131
6900
|
params = result.data;
|
|
7132
6901
|
}
|
|
7133
6902
|
if (command.scope !== "cli" && !process.env.XBROWSER_DAEMON_WORKER) {
|
|
7134
|
-
const { forwardExec } = await import("./daemon-client-
|
|
6903
|
+
const { forwardExec } = await import("./daemon-client-UZZEHHIV.js");
|
|
7135
6904
|
const result = await forwardExec(commandName, params, sessionName, extraOpts?.cdpEndpoint);
|
|
7136
6905
|
if (result) return result;
|
|
7137
6906
|
}
|
|
@@ -7157,18 +6926,10 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
7157
6926
|
});
|
|
7158
6927
|
}
|
|
7159
6928
|
}
|
|
7160
|
-
} else if (
|
|
6929
|
+
} else if (command.scope !== "project") {
|
|
7161
6930
|
session = await createSession(sessionName, params.url, {
|
|
7162
6931
|
cdpEndpoint: extraOpts?.cdpEndpoint
|
|
7163
6932
|
});
|
|
7164
|
-
} else if (command.scope === "browser") {
|
|
7165
|
-
session = await createSession(sessionName, void 0, {
|
|
7166
|
-
cdpEndpoint: extraOpts?.cdpEndpoint
|
|
7167
|
-
});
|
|
7168
|
-
} else if (command.scope !== "project") {
|
|
7169
|
-
return errorResult(
|
|
7170
|
-
`Session '${sessionName}' not found. Run "xbrowser session open <url>" first.`
|
|
7171
|
-
);
|
|
7172
6933
|
}
|
|
7173
6934
|
const ctx = {
|
|
7174
6935
|
page: session?.page,
|
|
@@ -7191,7 +6952,8 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
7191
6952
|
},
|
|
7192
6953
|
config: {},
|
|
7193
6954
|
site: {},
|
|
7194
|
-
cliName: "xbrowser"
|
|
6955
|
+
cliName: "xbrowser",
|
|
6956
|
+
tips: new TipCollector()
|
|
7195
6957
|
};
|
|
7196
6958
|
const start = Date.now();
|
|
7197
6959
|
if (session) {
|
|
@@ -7212,7 +6974,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
7212
6974
|
const cache = /* @__PURE__ */ new Map();
|
|
7213
6975
|
const resolved = await resolveRefParams(session.page, params, command.selectorParams, cache, session.id);
|
|
7214
6976
|
if (resolved.tips.length > 0) {
|
|
7215
|
-
refTips = resolved.tips;
|
|
6977
|
+
refTips = normalizeTips6(resolved.tips);
|
|
7216
6978
|
params = resolved.params;
|
|
7217
6979
|
}
|
|
7218
6980
|
}
|
|
@@ -7268,32 +7030,39 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
7268
7030
|
snapshotHintShown.add(session);
|
|
7269
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";
|
|
7270
7032
|
}
|
|
7271
|
-
const merged = [
|
|
7033
|
+
const merged = [
|
|
7034
|
+
...raw.tips || [],
|
|
7035
|
+
...normalizeTips6(smartTips),
|
|
7036
|
+
...snapshotHint ? normalizeTips6([snapshotHint]) : [],
|
|
7037
|
+
...refTips
|
|
7038
|
+
];
|
|
7272
7039
|
const isSuccess = raw.success !== false;
|
|
7040
|
+
const mergedOrRaw = merged.length > 0 ? merged : raw.tips || [];
|
|
7273
7041
|
recordArchive(session?.id, sessionName, {
|
|
7274
7042
|
step: 0,
|
|
7275
7043
|
command: commandName,
|
|
7276
7044
|
params,
|
|
7277
|
-
result: { success: isSuccess, data: raw.data, message: raw.message, tips:
|
|
7045
|
+
result: { success: isSuccess, data: raw.data, message: raw.message, tips: tipsToMessages(mergedOrRaw) },
|
|
7278
7046
|
toolCalls: [],
|
|
7279
7047
|
duration,
|
|
7280
7048
|
timestamp: start
|
|
7281
7049
|
});
|
|
7282
7050
|
if (isSuccess) {
|
|
7283
|
-
return { ...
|
|
7051
|
+
return { ...ok25(raw.data, merged.length > 0 ? merged : raw.tips), duration, ...hookOutputs ? { hookOutputs } : {} };
|
|
7284
7052
|
}
|
|
7285
|
-
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 } : {} };
|
|
7286
7054
|
}
|
|
7055
|
+
const smartTipNormalized = normalizeTips6(smartTips);
|
|
7287
7056
|
recordArchive(session?.id, sessionName, {
|
|
7288
7057
|
step: 0,
|
|
7289
7058
|
command: commandName,
|
|
7290
7059
|
params,
|
|
7291
|
-
result: { success: true, data: raw, tips:
|
|
7060
|
+
result: { success: true, data: raw, tips: tipsToMessages(smartTipNormalized) },
|
|
7292
7061
|
toolCalls: [],
|
|
7293
7062
|
duration,
|
|
7294
7063
|
timestamp: start
|
|
7295
7064
|
});
|
|
7296
|
-
return { ...
|
|
7065
|
+
return { ...ok25(raw, smartTipNormalized), duration, ...hookOutputs ? { hookOutputs } : {} };
|
|
7297
7066
|
} catch (err) {
|
|
7298
7067
|
const end = Date.now();
|
|
7299
7068
|
const duration = end - start;
|
|
@@ -7343,8 +7112,8 @@ async function executeChain(input, options) {
|
|
|
7343
7112
|
});
|
|
7344
7113
|
}
|
|
7345
7114
|
try {
|
|
7346
|
-
for (const
|
|
7347
|
-
const { type, pipeline: commands } =
|
|
7115
|
+
for (const pipeline of pipelines) {
|
|
7116
|
+
const { type, pipeline: commands } = pipeline;
|
|
7348
7117
|
for (const cmdStr of commands) {
|
|
7349
7118
|
const parts = splitCommand(cmdStr);
|
|
7350
7119
|
if (parts.length === 0) continue;
|
|
@@ -7409,11 +7178,52 @@ async function executeChain(input, options) {
|
|
|
7409
7178
|
},
|
|
7410
7179
|
config: {},
|
|
7411
7180
|
site,
|
|
7412
|
-
cliName: "xbrowser"
|
|
7181
|
+
cliName: "xbrowser",
|
|
7182
|
+
tips: new TipCollector()
|
|
7413
7183
|
};
|
|
7414
7184
|
const start2 = Date.now();
|
|
7415
7185
|
try {
|
|
7416
|
-
const
|
|
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
|
+
}
|
|
7226
|
+
const hooks = await loadHooks();
|
|
7417
7227
|
if (hooks.length > 0) {
|
|
7418
7228
|
await Promise.all(hooks.map((h) => h.onBeforeCommand?.({ page: session.page, command: `${cmdName} ${subCommand}`, params: pluginParams })));
|
|
7419
7229
|
}
|
|
@@ -7433,7 +7243,7 @@ async function executeChain(input, options) {
|
|
|
7433
7243
|
step: results.length,
|
|
7434
7244
|
command: `${cmdName} ${subCommand}`,
|
|
7435
7245
|
params: pluginParams,
|
|
7436
|
-
result: { success: true, data, tips: raw?.tips
|
|
7246
|
+
result: { success: true, data, tips: tipsToMessages(raw?.tips) },
|
|
7437
7247
|
toolCalls: [],
|
|
7438
7248
|
duration: duration2,
|
|
7439
7249
|
timestamp: start2
|
|
@@ -7441,7 +7251,7 @@ async function executeChain(input, options) {
|
|
|
7441
7251
|
results.push({
|
|
7442
7252
|
command: `${cmdName} ${subCommand}`,
|
|
7443
7253
|
raw: cmdStr,
|
|
7444
|
-
...
|
|
7254
|
+
...ok25(data),
|
|
7445
7255
|
duration: duration2,
|
|
7446
7256
|
...hookOutputs ? { hookOutputs } : {}
|
|
7447
7257
|
});
|
|
@@ -7484,7 +7294,7 @@ async function executeChain(input, options) {
|
|
|
7484
7294
|
}
|
|
7485
7295
|
continue;
|
|
7486
7296
|
}
|
|
7487
|
-
const { params } = parseCommandArgs(cmdName, cmdArgs);
|
|
7297
|
+
const { params } = parseCommandArgs(cmdName, cmdArgs, unquote2);
|
|
7488
7298
|
if (cmdName === "goto" && params.url) {
|
|
7489
7299
|
const existing2 = await findOrRestoreSession(sessionName, options?.cdpEndpoint);
|
|
7490
7300
|
if (!existing2) {
|
|
@@ -7545,15 +7355,6 @@ function isChainInput(input) {
|
|
|
7545
7355
|
}
|
|
7546
7356
|
|
|
7547
7357
|
// src/session/session-client.ts
|
|
7548
|
-
function sessionToInfo(s) {
|
|
7549
|
-
return { id: s.id, name: s.name, url: s.page.url(), createdAt: s.createdAt, cdpEndpoint: s.cdpEndpoint };
|
|
7550
|
-
}
|
|
7551
|
-
async function openSession(name, url, options) {
|
|
7552
|
-
const session = await createSession(name, url, { cdpEndpoint: options?.cdpEndpoint });
|
|
7553
|
-
const info = sessionToInfo(session);
|
|
7554
|
-
saveSessionDiskMeta(name, info);
|
|
7555
|
-
return info;
|
|
7556
|
-
}
|
|
7557
7358
|
async function closeSession(name) {
|
|
7558
7359
|
await closeSessionByName(name);
|
|
7559
7360
|
}
|
|
@@ -7567,53 +7368,22 @@ function handleSessionHelp() {
|
|
|
7567
7368
|
"Usage: xbrowser session <command> [options]",
|
|
7568
7369
|
"",
|
|
7569
7370
|
"Commands:",
|
|
7570
|
-
" open <url> [--name <name>] Open browser and create session",
|
|
7571
7371
|
" close [--name <name>] Close session",
|
|
7572
7372
|
" list, ls List active sessions",
|
|
7573
7373
|
" kill [--name <name>] Kill session forcefully",
|
|
7374
|
+
" kill-all Kill all sessions and daemon",
|
|
7574
7375
|
"",
|
|
7575
7376
|
"Options:",
|
|
7576
7377
|
' --name <name> Session name (default: "default")',
|
|
7577
7378
|
"",
|
|
7379
|
+
"Note: Sessions are auto-created via --session global option.",
|
|
7380
|
+
"",
|
|
7578
7381
|
"Examples:",
|
|
7579
|
-
" xbrowser
|
|
7580
|
-
" xbrowser session open https://example.com --name mypage",
|
|
7382
|
+
" xbrowser goto https://example.com --session mypage",
|
|
7581
7383
|
" xbrowser session close --name mypage",
|
|
7582
7384
|
" xbrowser session list"
|
|
7583
7385
|
].join("\n");
|
|
7584
7386
|
}
|
|
7585
|
-
var sessionOpenBuiltin = {
|
|
7586
|
-
name: "session open",
|
|
7587
|
-
description: "Open browser and create session",
|
|
7588
|
-
help: {
|
|
7589
|
-
usage: "xbrowser session open <url> [--name <name>]",
|
|
7590
|
-
description: "Open URL and create a browser session",
|
|
7591
|
-
options: [{ name: "--name <name>", description: 'Session name (default: "default")' }],
|
|
7592
|
-
examples: [
|
|
7593
|
-
{ cmd: "xbrowser session open https://example.com", description: "Open example.com" },
|
|
7594
|
-
{
|
|
7595
|
-
cmd: "xbrowser session open https://example.com --name test",
|
|
7596
|
-
description: "Open with custom name"
|
|
7597
|
-
}
|
|
7598
|
-
]
|
|
7599
|
-
},
|
|
7600
|
-
execute: async (args, options) => {
|
|
7601
|
-
const [url] = args;
|
|
7602
|
-
const name = options["name"] || "default";
|
|
7603
|
-
if (!url) {
|
|
7604
|
-
console.log("Usage: xbrowser session open <url> [--name <name>]");
|
|
7605
|
-
process.exit(1);
|
|
7606
|
-
}
|
|
7607
|
-
try {
|
|
7608
|
-
const info = await openSession(name, url);
|
|
7609
|
-
console.log(`Session "${info.name}" opened: ${info.url}`);
|
|
7610
|
-
console.log(`ID: ${info.id}`);
|
|
7611
|
-
} catch (e) {
|
|
7612
|
-
console.error("Error:", e instanceof Error ? e.message : String(e));
|
|
7613
|
-
process.exit(1);
|
|
7614
|
-
}
|
|
7615
|
-
}
|
|
7616
|
-
};
|
|
7617
7387
|
var sessionCloseBuiltin = {
|
|
7618
7388
|
name: "session close",
|
|
7619
7389
|
description: "Close browser session",
|
|
@@ -7695,22 +7465,17 @@ var sessionKillBuiltin = {
|
|
|
7695
7465
|
};
|
|
7696
7466
|
|
|
7697
7467
|
// src/config.ts
|
|
7698
|
-
import {
|
|
7699
|
-
import { join as
|
|
7700
|
-
import {
|
|
7701
|
-
function
|
|
7702
|
-
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") };
|
|
7703
7473
|
}
|
|
7704
7474
|
function loadConfig() {
|
|
7705
|
-
|
|
7706
|
-
if (!existsSync6(configFile)) return {};
|
|
7707
|
-
return readJsonFile(configFile, {});
|
|
7475
|
+
return coreLoadConfig(getConfigSource());
|
|
7708
7476
|
}
|
|
7709
7477
|
function saveConfig(config) {
|
|
7710
|
-
|
|
7711
|
-
const configFile = getConfigFile();
|
|
7712
|
-
if (!existsSync6(dir)) mkdirSync3(dir, { recursive: true });
|
|
7713
|
-
writeFileSync5(configFile, JSON.stringify(config, null, 2), "utf-8");
|
|
7478
|
+
coreSaveConfig(getConfigSource(), config);
|
|
7714
7479
|
}
|
|
7715
7480
|
function getConfigValue(key) {
|
|
7716
7481
|
return loadConfig()[key];
|
|
@@ -7818,38 +7583,69 @@ var configBuiltin = {
|
|
|
7818
7583
|
|
|
7819
7584
|
// src/plugin/installer.ts
|
|
7820
7585
|
import {
|
|
7821
|
-
existsSync as
|
|
7822
|
-
readdirSync as
|
|
7586
|
+
existsSync as existsSync10,
|
|
7587
|
+
readdirSync as readdirSync2,
|
|
7823
7588
|
mkdirSync as mkdirSync8,
|
|
7824
|
-
rmSync as
|
|
7589
|
+
rmSync as rmSync6
|
|
7825
7590
|
} from "fs";
|
|
7826
|
-
import { resolve as
|
|
7827
|
-
import { homedir as
|
|
7591
|
+
import { resolve as resolve8, basename as basename2 } from "path";
|
|
7592
|
+
import { homedir as homedir7 } from "os";
|
|
7828
7593
|
|
|
7829
7594
|
// src/plugin/install-sources/local.ts
|
|
7830
|
-
import { existsSync as
|
|
7831
|
-
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
|
+
}
|
|
7832
7631
|
|
|
7833
|
-
// 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";
|
|
7834
7636
|
import {
|
|
7835
|
-
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
createWriteStream
|
|
7842
|
-
} from "fs";
|
|
7843
|
-
import { resolve as resolve9 } from "path";
|
|
7844
|
-
import { execSync as execSync7 } from "child_process";
|
|
7845
|
-
import { pipeline } from "stream/promises";
|
|
7846
|
-
import { Readable } from "stream";
|
|
7637
|
+
downloadToFile,
|
|
7638
|
+
extractTarGz,
|
|
7639
|
+
flattenPackageRoot,
|
|
7640
|
+
verifyPlugin as verifyPlugin2,
|
|
7641
|
+
safeCleanup as safeCleanup2
|
|
7642
|
+
} from "@dyyz1993/xcli-core";
|
|
7847
7643
|
|
|
7848
7644
|
// src/utils/proxy-fetch.ts
|
|
7849
|
-
var
|
|
7645
|
+
var patched2 = false;
|
|
7850
7646
|
async function ensureProxyFetch() {
|
|
7851
|
-
if (
|
|
7852
|
-
|
|
7647
|
+
if (patched2) return;
|
|
7648
|
+
patched2 = true;
|
|
7853
7649
|
if (process.env.https_proxy && !process.env.HTTPS_PROXY) {
|
|
7854
7650
|
process.env.HTTPS_PROXY = process.env.https_proxy;
|
|
7855
7651
|
}
|
|
@@ -7889,113 +7685,7 @@ async function ensureProxyFetch() {
|
|
|
7889
7685
|
}
|
|
7890
7686
|
}
|
|
7891
7687
|
|
|
7892
|
-
// src/plugin/install-utils.ts
|
|
7893
|
-
async function downloadToFile(url, destPath) {
|
|
7894
|
-
await ensureProxyFetch();
|
|
7895
|
-
if (url.startsWith("file://")) {
|
|
7896
|
-
const filePath = decodeURIComponent(new URL(url).pathname);
|
|
7897
|
-
cpSync(filePath, destPath, { force: true });
|
|
7898
|
-
return;
|
|
7899
|
-
}
|
|
7900
|
-
const res = await fetch(url);
|
|
7901
|
-
if (!res.ok) {
|
|
7902
|
-
throw new Error(`Download failed: HTTP ${res.status} from ${url}`);
|
|
7903
|
-
}
|
|
7904
|
-
if (!res.body) {
|
|
7905
|
-
throw new Error(`No response body from ${url}`);
|
|
7906
|
-
}
|
|
7907
|
-
const nodeStream = Readable.fromWeb(res.body);
|
|
7908
|
-
await pipeline(nodeStream, createWriteStream(destPath));
|
|
7909
|
-
}
|
|
7910
|
-
function extractTarGz(tarballPath, targetDir) {
|
|
7911
|
-
mkdirSync4(targetDir, { recursive: true });
|
|
7912
|
-
execSync7(`tar -xzf "${tarballPath}" -C "${targetDir}"`, { stdio: "pipe" });
|
|
7913
|
-
}
|
|
7914
|
-
function flattenPackageRoot(targetDir) {
|
|
7915
|
-
const entries = readdirSync2(targetDir, { withFileTypes: true });
|
|
7916
|
-
const dirs = entries.filter((e) => e.isDirectory());
|
|
7917
|
-
const files = entries.filter((e) => !e.isDirectory());
|
|
7918
|
-
if (dirs.length === 1 && files.length === 0) {
|
|
7919
|
-
const pkgDir = resolve9(targetDir, dirs[0].name);
|
|
7920
|
-
const items = readdirSync2(pkgDir);
|
|
7921
|
-
for (const item of items) {
|
|
7922
|
-
const src = resolve9(pkgDir, item);
|
|
7923
|
-
const dst = resolve9(targetDir, item);
|
|
7924
|
-
cpSync(src, dst, { recursive: true, force: true });
|
|
7925
|
-
}
|
|
7926
|
-
rmSync(pkgDir, { recursive: true, force: true });
|
|
7927
|
-
}
|
|
7928
|
-
}
|
|
7929
|
-
async function verifyPlugin(dir) {
|
|
7930
|
-
const warnings = [];
|
|
7931
|
-
const indexPath = resolve9(dir, "index.ts");
|
|
7932
|
-
if (!existsSync7(indexPath)) {
|
|
7933
|
-
const indexJs = resolve9(dir, "index.js");
|
|
7934
|
-
if (!existsSync7(indexJs)) {
|
|
7935
|
-
return { valid: false, error: "No index.ts or index.js entry point found", warnings };
|
|
7936
|
-
}
|
|
7937
|
-
}
|
|
7938
|
-
const pkgPath = resolve9(dir, "package.json");
|
|
7939
|
-
if (!existsSync7(pkgPath)) {
|
|
7940
|
-
warnings.push("No package.json found");
|
|
7941
|
-
} else {
|
|
7942
|
-
try {
|
|
7943
|
-
const pkg2 = JSON.parse(readFileSync10(pkgPath, "utf-8"));
|
|
7944
|
-
if (!pkg2.xbrowser) {
|
|
7945
|
-
warnings.push("No xbrowser metadata in package.json");
|
|
7946
|
-
}
|
|
7947
|
-
} catch {
|
|
7948
|
-
warnings.push("Invalid package.json");
|
|
7949
|
-
}
|
|
7950
|
-
}
|
|
7951
|
-
return { valid: true, warnings };
|
|
7952
|
-
}
|
|
7953
|
-
function safeCleanup(dir) {
|
|
7954
|
-
try {
|
|
7955
|
-
rmSync(dir, { recursive: true, force: true });
|
|
7956
|
-
} catch {
|
|
7957
|
-
}
|
|
7958
|
-
}
|
|
7959
|
-
|
|
7960
|
-
// src/plugin/install-sources/local.ts
|
|
7961
|
-
async function installFromLocal(source, name, targetDir) {
|
|
7962
|
-
const srcPath = resolve10(source);
|
|
7963
|
-
if (!existsSync8(srcPath)) {
|
|
7964
|
-
throw new Error(`Local path does not exist: ${srcPath}`);
|
|
7965
|
-
}
|
|
7966
|
-
const tmpTarget = `${targetDir}-tmp-${Date.now()}`;
|
|
7967
|
-
let warnings = [];
|
|
7968
|
-
try {
|
|
7969
|
-
cpSync2(srcPath, tmpTarget, { recursive: true });
|
|
7970
|
-
const verify = await verifyPlugin(tmpTarget);
|
|
7971
|
-
warnings = verify.warnings ?? [];
|
|
7972
|
-
if (!verify.valid) {
|
|
7973
|
-
safeCleanup(tmpTarget);
|
|
7974
|
-
throw new Error(`Invalid plugin: ${verify.error}`);
|
|
7975
|
-
}
|
|
7976
|
-
if (existsSync8(targetDir)) {
|
|
7977
|
-
rmSync2(targetDir, { recursive: true, force: true });
|
|
7978
|
-
}
|
|
7979
|
-
cpSync2(tmpTarget, targetDir, { recursive: true, force: true });
|
|
7980
|
-
safeCleanup(tmpTarget);
|
|
7981
|
-
} catch (err) {
|
|
7982
|
-
safeCleanup(tmpTarget);
|
|
7983
|
-
throw err;
|
|
7984
|
-
}
|
|
7985
|
-
return {
|
|
7986
|
-
id: name,
|
|
7987
|
-
name,
|
|
7988
|
-
path: targetDir,
|
|
7989
|
-
source: "local",
|
|
7990
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7991
|
-
warnings
|
|
7992
|
-
};
|
|
7993
|
-
}
|
|
7994
|
-
|
|
7995
7688
|
// src/plugin/install-sources/npm.ts
|
|
7996
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync6, rmSync as rmSync3, cpSync as cpSync3 } from "fs";
|
|
7997
|
-
import { resolve as resolve11, join as join5 } from "path";
|
|
7998
|
-
import { tmpdir as tmpdir2 } from "os";
|
|
7999
7689
|
async function installFromNpm(packageName, name, targetDir) {
|
|
8000
7690
|
await ensureProxyFetch();
|
|
8001
7691
|
const encodedName = encodeURIComponent(packageName);
|
|
@@ -8013,34 +7703,34 @@ async function installFromNpm(packageName, name, targetDir) {
|
|
|
8013
7703
|
throw new Error(`No tarball URL for ${packageName}@${latestVersion}`);
|
|
8014
7704
|
}
|
|
8015
7705
|
const tarballUrl = versionMeta.dist.tarball;
|
|
8016
|
-
const tmpDir =
|
|
7706
|
+
const tmpDir = join7(tmpdir2(), `xbrowser-npm-${Date.now()}`);
|
|
8017
7707
|
mkdirSync5(tmpDir, { recursive: true });
|
|
8018
7708
|
let warnings = [];
|
|
8019
7709
|
try {
|
|
8020
|
-
const tarballPath =
|
|
7710
|
+
const tarballPath = join7(tmpDir, `${name}.tgz`);
|
|
8021
7711
|
await downloadToFile(tarballUrl, tarballPath);
|
|
8022
|
-
const extractDir =
|
|
7712
|
+
const extractDir = join7(tmpDir, "extracted");
|
|
8023
7713
|
extractTarGz(tarballPath, extractDir);
|
|
8024
7714
|
flattenPackageRoot(extractDir);
|
|
8025
|
-
const verify =
|
|
7715
|
+
const verify = verifyPlugin2(extractDir, { metadataField: "xbrowser" });
|
|
8026
7716
|
warnings = verify.warnings ?? [];
|
|
8027
7717
|
if (!verify.valid) {
|
|
8028
7718
|
throw new Error(`Invalid npm plugin: ${verify.error}`);
|
|
8029
7719
|
}
|
|
8030
|
-
if (
|
|
8031
|
-
|
|
7720
|
+
if (existsSync6(targetDir)) {
|
|
7721
|
+
rmSync2(targetDir, { recursive: true, force: true });
|
|
8032
7722
|
}
|
|
8033
|
-
|
|
8034
|
-
const pkgPath =
|
|
8035
|
-
if (
|
|
8036
|
-
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"));
|
|
8037
7727
|
if (!pkg2._npmSource) {
|
|
8038
7728
|
pkg2._npmSource = { name: packageName, version: latestVersion };
|
|
8039
7729
|
writeFileSync6(pkgPath, JSON.stringify(pkg2, null, 2));
|
|
8040
7730
|
}
|
|
8041
7731
|
}
|
|
8042
7732
|
} finally {
|
|
8043
|
-
|
|
7733
|
+
safeCleanup2(tmpDir);
|
|
8044
7734
|
}
|
|
8045
7735
|
return {
|
|
8046
7736
|
id: name,
|
|
@@ -8053,35 +7743,36 @@ async function installFromNpm(packageName, name, targetDir) {
|
|
|
8053
7743
|
}
|
|
8054
7744
|
|
|
8055
7745
|
// src/plugin/install-sources/git.ts
|
|
8056
|
-
import { existsSync as
|
|
8057
|
-
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";
|
|
8058
7748
|
import { tmpdir as tmpdir3 } from "os";
|
|
8059
|
-
import { execSync as
|
|
7749
|
+
import { execSync as execSync2 } from "child_process";
|
|
7750
|
+
import { verifyPlugin as verifyPlugin3, safeCleanup as safeCleanup3 } from "@dyyz1993/xcli-core";
|
|
8060
7751
|
async function installFromGit(gitUrl, name, targetDir) {
|
|
8061
|
-
const tmpDir =
|
|
7752
|
+
const tmpDir = join8(tmpdir3(), `xbrowser-git-${Date.now()}`);
|
|
8062
7753
|
let warnings = [];
|
|
8063
7754
|
try {
|
|
8064
|
-
|
|
8065
|
-
const verify =
|
|
7755
|
+
execSync2(`git clone --depth 1 "${gitUrl}" "${tmpDir}"`, { stdio: "pipe" });
|
|
7756
|
+
const verify = verifyPlugin3(tmpDir, { metadataField: "xbrowser" });
|
|
8066
7757
|
warnings = verify.warnings ?? [];
|
|
8067
7758
|
if (!verify.valid) {
|
|
8068
7759
|
throw new Error(`Invalid git plugin: ${verify.error}`);
|
|
8069
7760
|
}
|
|
8070
|
-
if (
|
|
8071
|
-
|
|
7761
|
+
if (existsSync7(targetDir)) {
|
|
7762
|
+
rmSync3(targetDir, { recursive: true, force: true });
|
|
8072
7763
|
}
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
const pkgPath =
|
|
8076
|
-
if (
|
|
8077
|
-
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"));
|
|
8078
7769
|
if (!pkg2._gitSource) {
|
|
8079
7770
|
pkg2._gitSource = { url: gitUrl };
|
|
8080
7771
|
writeFileSync7(pkgPath, JSON.stringify(pkg2, null, 2));
|
|
8081
7772
|
}
|
|
8082
7773
|
}
|
|
8083
7774
|
} finally {
|
|
8084
|
-
|
|
7775
|
+
safeCleanup3(tmpDir);
|
|
8085
7776
|
}
|
|
8086
7777
|
return {
|
|
8087
7778
|
id: name,
|
|
@@ -8094,39 +7785,46 @@ async function installFromGit(gitUrl, name, targetDir) {
|
|
|
8094
7785
|
}
|
|
8095
7786
|
|
|
8096
7787
|
// src/plugin/install-sources/url.ts
|
|
8097
|
-
import { existsSync as
|
|
8098
|
-
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";
|
|
8099
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";
|
|
8100
7798
|
async function installFromUrl(url, name, targetDir) {
|
|
8101
|
-
const tmpDir =
|
|
7799
|
+
const tmpDir = join9(tmpdir4(), `xbrowser-url-${Date.now()}`);
|
|
8102
7800
|
mkdirSync6(tmpDir, { recursive: true });
|
|
8103
7801
|
let warnings = [];
|
|
8104
7802
|
try {
|
|
8105
7803
|
const fileName = basename(new URL(url).pathname) || "plugin.tar.gz";
|
|
8106
|
-
const tarballPath =
|
|
8107
|
-
await
|
|
8108
|
-
const extractDir =
|
|
8109
|
-
|
|
8110
|
-
|
|
8111
|
-
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" });
|
|
8112
7810
|
warnings = verify.warnings ?? [];
|
|
8113
7811
|
if (!verify.valid) {
|
|
8114
7812
|
throw new Error(`Invalid plugin from URL: ${verify.error}`);
|
|
8115
7813
|
}
|
|
8116
|
-
if (
|
|
8117
|
-
|
|
7814
|
+
if (existsSync8(targetDir)) {
|
|
7815
|
+
rmSync4(targetDir, { recursive: true, force: true });
|
|
8118
7816
|
}
|
|
8119
|
-
|
|
8120
|
-
const pkgPath =
|
|
8121
|
-
if (
|
|
8122
|
-
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"));
|
|
8123
7821
|
if (!pkg2._urlSource) {
|
|
8124
7822
|
pkg2._urlSource = { url };
|
|
8125
7823
|
writeFileSync8(pkgPath, JSON.stringify(pkg2, null, 2));
|
|
8126
7824
|
}
|
|
8127
7825
|
}
|
|
8128
7826
|
} finally {
|
|
8129
|
-
|
|
7827
|
+
safeCleanup4(tmpDir);
|
|
8130
7828
|
}
|
|
8131
7829
|
return {
|
|
8132
7830
|
id: name,
|
|
@@ -8140,16 +7838,23 @@ async function installFromUrl(url, name, targetDir) {
|
|
|
8140
7838
|
|
|
8141
7839
|
// src/plugin/install-sources/marketplace.ts
|
|
8142
7840
|
import {
|
|
8143
|
-
existsSync as
|
|
7841
|
+
existsSync as existsSync9,
|
|
8144
7842
|
mkdirSync as mkdirSync7,
|
|
8145
7843
|
writeFileSync as writeFileSync9,
|
|
8146
|
-
readFileSync as
|
|
8147
|
-
rmSync as
|
|
8148
|
-
cpSync as
|
|
7844
|
+
readFileSync as readFileSync7,
|
|
7845
|
+
rmSync as rmSync5,
|
|
7846
|
+
cpSync as cpSync5
|
|
8149
7847
|
} from "fs";
|
|
8150
|
-
import { resolve as
|
|
7848
|
+
import { resolve as resolve7, join as join10, dirname as dirname2 } from "path";
|
|
8151
7849
|
import { tmpdir as tmpdir5 } from "os";
|
|
8152
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";
|
|
8153
7858
|
async function installFromMarketplace(pluginsDir, slug, options) {
|
|
8154
7859
|
await ensureProxyFetch();
|
|
8155
7860
|
const baseUrl = getMarketplaceUrl();
|
|
@@ -8164,24 +7869,24 @@ async function installFromMarketplace(pluginsDir, slug, options) {
|
|
|
8164
7869
|
}
|
|
8165
7870
|
const plugin = detailData.data;
|
|
8166
7871
|
const name = options?.name || String(plugin.slug || slug);
|
|
8167
|
-
const targetDir =
|
|
8168
|
-
if (
|
|
7872
|
+
const targetDir = resolve7(pluginsDir, name);
|
|
7873
|
+
if (existsSync9(targetDir) && !options?.force) {
|
|
8169
7874
|
throw new Error(`Plugin "${name}" already exists. Use --force to overwrite.`);
|
|
8170
7875
|
}
|
|
8171
7876
|
mkdirSync7(targetDir, { recursive: true });
|
|
8172
|
-
const tmpDir =
|
|
7877
|
+
const tmpDir = join10(tmpdir5(), `xbrowser-marketplace-${Date.now()}`);
|
|
8173
7878
|
mkdirSync7(tmpDir, { recursive: true });
|
|
8174
7879
|
const realSlug = String(plugin.slug || slug);
|
|
8175
7880
|
try {
|
|
8176
7881
|
await downloadAndExtractMarketplaceTarball(baseUrl, realSlug, tmpDir, targetDir);
|
|
8177
7882
|
} finally {
|
|
8178
|
-
|
|
7883
|
+
safeCleanup5(tmpDir);
|
|
8179
7884
|
}
|
|
8180
7885
|
writeMarketplacePackageJson(plugin, slug, name, baseUrl, targetDir);
|
|
8181
7886
|
ensureIndexFile(plugin, name, targetDir);
|
|
8182
|
-
const verify =
|
|
7887
|
+
const verify = verifyPlugin5(targetDir, { metadataField: "xbrowser" });
|
|
8183
7888
|
if (!verify.valid) {
|
|
8184
|
-
|
|
7889
|
+
safeCleanup5(targetDir);
|
|
8185
7890
|
throw new Error(`Invalid marketplace plugin: ${verify.error}`);
|
|
8186
7891
|
}
|
|
8187
7892
|
const trackUrl = `${baseUrl}/api/plugins/${realSlug}/install`;
|
|
@@ -8200,12 +7905,12 @@ function isManifestArray(data) {
|
|
|
8200
7905
|
return Array.isArray(data) && data.length > 0 && typeof data[0].path === "string" && typeof data[0].content === "string";
|
|
8201
7906
|
}
|
|
8202
7907
|
function extractManifestToDir(manifest, targetDir) {
|
|
8203
|
-
if (
|
|
8204
|
-
|
|
7908
|
+
if (existsSync9(targetDir)) {
|
|
7909
|
+
rmSync5(targetDir, { recursive: true, force: true });
|
|
8205
7910
|
}
|
|
8206
7911
|
mkdirSync7(targetDir, { recursive: true });
|
|
8207
7912
|
for (const file of manifest) {
|
|
8208
|
-
const filePath =
|
|
7913
|
+
const filePath = resolve7(targetDir, file.path);
|
|
8209
7914
|
mkdirSync7(dirname2(filePath), { recursive: true });
|
|
8210
7915
|
writeFileSync9(filePath, Buffer.from(file.content, "base64"));
|
|
8211
7916
|
}
|
|
@@ -8232,21 +7937,21 @@ async function downloadAndExtractMarketplaceTarball(baseUrl, slug, tmpDir, targe
|
|
|
8232
7937
|
}
|
|
8233
7938
|
if (tarballRes.status === 302 || tarballRes.headers.get("location")) {
|
|
8234
7939
|
const redirectUrl = tarballRes.headers.get("location");
|
|
8235
|
-
const tarballPath =
|
|
8236
|
-
await
|
|
8237
|
-
const buffer =
|
|
7940
|
+
const tarballPath = join10(tmpDir, `${slug}.tar.gz`);
|
|
7941
|
+
await downloadToFile3(redirectUrl, tarballPath);
|
|
7942
|
+
const buffer = readFileSync7(tarballPath);
|
|
8238
7943
|
const manifest = tryParseAsGzippedManifest(buffer);
|
|
8239
7944
|
if (manifest) {
|
|
8240
7945
|
extractManifestToDir(manifest, targetDir);
|
|
8241
7946
|
return;
|
|
8242
7947
|
}
|
|
8243
|
-
const extractDir =
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
if (
|
|
8247
|
-
|
|
7948
|
+
const extractDir = join10(tmpDir, "extracted");
|
|
7949
|
+
extractTarGz3(tarballPath, extractDir);
|
|
7950
|
+
flattenPackageRoot3(extractDir);
|
|
7951
|
+
if (existsSync9(targetDir)) {
|
|
7952
|
+
rmSync5(targetDir, { recursive: true, force: true });
|
|
8248
7953
|
}
|
|
8249
|
-
|
|
7954
|
+
cpSync5(extractDir, targetDir, { recursive: true, force: true });
|
|
8250
7955
|
} else {
|
|
8251
7956
|
const buffer = Buffer.from(await tarballRes.arrayBuffer());
|
|
8252
7957
|
const manifest = tryParseAsGzippedManifest(buffer);
|
|
@@ -8254,16 +7959,16 @@ async function downloadAndExtractMarketplaceTarball(baseUrl, slug, tmpDir, targe
|
|
|
8254
7959
|
extractManifestToDir(manifest, targetDir);
|
|
8255
7960
|
return;
|
|
8256
7961
|
}
|
|
8257
|
-
const tarballPath =
|
|
7962
|
+
const tarballPath = join10(tmpDir, `${slug}.tar.gz`);
|
|
8258
7963
|
writeFileSync9(tarballPath, buffer);
|
|
8259
7964
|
try {
|
|
8260
|
-
const extractDir =
|
|
8261
|
-
|
|
8262
|
-
|
|
8263
|
-
if (
|
|
8264
|
-
|
|
7965
|
+
const extractDir = join10(tmpDir, "extracted");
|
|
7966
|
+
extractTarGz3(tarballPath, extractDir);
|
|
7967
|
+
flattenPackageRoot3(extractDir);
|
|
7968
|
+
if (existsSync9(targetDir)) {
|
|
7969
|
+
rmSync5(targetDir, { recursive: true, force: true });
|
|
8265
7970
|
}
|
|
8266
|
-
|
|
7971
|
+
cpSync5(extractDir, targetDir, { recursive: true, force: true });
|
|
8267
7972
|
} catch {
|
|
8268
7973
|
throw new Error(
|
|
8269
7974
|
`Downloaded tarball for "${slug}" is neither a gzipped JSON manifest nor a valid tar.gz archive.`
|
|
@@ -8295,12 +8000,12 @@ function writeMarketplacePackageJson(plugin, slug, name, baseUrl, targetDir) {
|
|
|
8295
8000
|
url: baseUrl
|
|
8296
8001
|
}
|
|
8297
8002
|
};
|
|
8298
|
-
const pkgPath =
|
|
8299
|
-
if (!
|
|
8003
|
+
const pkgPath = resolve7(targetDir, "package.json");
|
|
8004
|
+
if (!existsSync9(pkgPath)) {
|
|
8300
8005
|
writeFileSync9(pkgPath, JSON.stringify(packageJson, null, 2));
|
|
8301
8006
|
} else {
|
|
8302
8007
|
try {
|
|
8303
|
-
const existing = JSON.parse(
|
|
8008
|
+
const existing = JSON.parse(readFileSync7(pkgPath, "utf-8"));
|
|
8304
8009
|
const merged = {
|
|
8305
8010
|
...existing,
|
|
8306
8011
|
xbrowser: { ...existing.xbrowser, ...packageJson.xbrowser },
|
|
@@ -8313,7 +8018,7 @@ function writeMarketplacePackageJson(plugin, slug, name, baseUrl, targetDir) {
|
|
|
8313
8018
|
}
|
|
8314
8019
|
}
|
|
8315
8020
|
function ensureIndexFile(plugin, name, targetDir) {
|
|
8316
|
-
if (!
|
|
8021
|
+
if (!existsSync9(resolve7(targetDir, "index.ts")) && !existsSync9(resolve7(targetDir, "index.js"))) {
|
|
8317
8022
|
const commands = plugin.commands || [];
|
|
8318
8023
|
const commandHandlers = commands.length > 0 ? commands.map((cmd) => {
|
|
8319
8024
|
return [
|
|
@@ -8329,7 +8034,7 @@ function ensureIndexFile(plugin, name, targetDir) {
|
|
|
8329
8034
|
` });`
|
|
8330
8035
|
].join("\n");
|
|
8331
8036
|
writeFileSync9(
|
|
8332
|
-
|
|
8037
|
+
resolve7(targetDir, "index.ts"),
|
|
8333
8038
|
[
|
|
8334
8039
|
`import type { XCLIAPI } from '@dyyz1993/xcli-core';`,
|
|
8335
8040
|
``,
|
|
@@ -8350,7 +8055,7 @@ function ensureIndexFile(plugin, name, targetDir) {
|
|
|
8350
8055
|
var PluginInstaller = class {
|
|
8351
8056
|
pluginsDir;
|
|
8352
8057
|
constructor(pluginsDir) {
|
|
8353
|
-
this.pluginsDir = pluginsDir ||
|
|
8058
|
+
this.pluginsDir = pluginsDir || resolve8(homedir7(), ".xbrowser/plugins");
|
|
8354
8059
|
}
|
|
8355
8060
|
getPluginsDir() {
|
|
8356
8061
|
return this.pluginsDir;
|
|
@@ -8366,8 +8071,8 @@ var PluginInstaller = class {
|
|
|
8366
8071
|
async install(source, options) {
|
|
8367
8072
|
const type = this.detectSourceType(source);
|
|
8368
8073
|
const name = options?.name || this.deriveName(source, type);
|
|
8369
|
-
const targetDir =
|
|
8370
|
-
if (
|
|
8074
|
+
const targetDir = resolve8(this.pluginsDir, name);
|
|
8075
|
+
if (existsSync10(targetDir) && !options?.force) {
|
|
8371
8076
|
throw new Error(`Plugin "${name}" already exists. Use --force to overwrite.`);
|
|
8372
8077
|
}
|
|
8373
8078
|
mkdirSync8(targetDir, { recursive: true });
|
|
@@ -8427,11 +8132,11 @@ var PluginInstaller = class {
|
|
|
8427
8132
|
* @throws If the plugin is not installed.
|
|
8428
8133
|
*/
|
|
8429
8134
|
async uninstall(name) {
|
|
8430
|
-
const targetDir =
|
|
8431
|
-
if (!
|
|
8135
|
+
const targetDir = resolve8(this.pluginsDir, name);
|
|
8136
|
+
if (!existsSync10(targetDir)) {
|
|
8432
8137
|
throw new Error(`Plugin "${name}" not found`);
|
|
8433
8138
|
}
|
|
8434
|
-
|
|
8139
|
+
rmSync6(targetDir, { recursive: true, force: true });
|
|
8435
8140
|
}
|
|
8436
8141
|
/**
|
|
8437
8142
|
* List all installed plugins with metadata.
|
|
@@ -8439,18 +8144,18 @@ var PluginInstaller = class {
|
|
|
8439
8144
|
* @returns Array of installed plugin information.
|
|
8440
8145
|
*/
|
|
8441
8146
|
async list(_options) {
|
|
8442
|
-
if (!
|
|
8443
|
-
const entries =
|
|
8147
|
+
if (!existsSync10(this.pluginsDir)) return [];
|
|
8148
|
+
const entries = readdirSync2(this.pluginsDir, { withFileTypes: true });
|
|
8444
8149
|
const plugins = [];
|
|
8445
8150
|
for (const entry of entries) {
|
|
8446
8151
|
if (!entry.isDirectory()) continue;
|
|
8447
|
-
const pluginPath =
|
|
8448
|
-
const indexPath =
|
|
8449
|
-
const indexJsPath =
|
|
8450
|
-
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;
|
|
8451
8156
|
const metadata = PluginMetadataParser.parseFromPackageJson(pluginPath);
|
|
8452
8157
|
let source = "local";
|
|
8453
|
-
const pkg2 = readJsonFile(
|
|
8158
|
+
const pkg2 = readJsonFile(resolve8(pluginPath, "package.json"), {});
|
|
8454
8159
|
if (pkg2._marketplace) source = "marketplace";
|
|
8455
8160
|
else if (pkg2._npmSource) source = "npm";
|
|
8456
8161
|
else if (pkg2._gitSource) source = "git";
|
|
@@ -8474,10 +8179,10 @@ var PluginInstaller = class {
|
|
|
8474
8179
|
}
|
|
8475
8180
|
if (source.startsWith("file://")) {
|
|
8476
8181
|
const filePath = decodeURIComponent(new URL(source).pathname);
|
|
8477
|
-
if (
|
|
8182
|
+
if (existsSync10(filePath)) return "url";
|
|
8478
8183
|
}
|
|
8479
8184
|
if (source.endsWith(".git") || source.includes("github.com/")) return "git";
|
|
8480
|
-
if (
|
|
8185
|
+
if (existsSync10(resolve8(source))) return "local";
|
|
8481
8186
|
return "npm";
|
|
8482
8187
|
}
|
|
8483
8188
|
deriveName(source, type) {
|
|
@@ -9311,7 +9016,7 @@ var previewBuiltin = {
|
|
|
9311
9016
|
if (options.json) {
|
|
9312
9017
|
outputResult({ running: false }, "json");
|
|
9313
9018
|
} else {
|
|
9314
|
-
console.log("Daemon is not running.
|
|
9019
|
+
console.log("Daemon is not running. It will start automatically when needed.");
|
|
9315
9020
|
console.log("");
|
|
9316
9021
|
console.log("Preview is automatically available when the daemon is running.");
|
|
9317
9022
|
}
|
|
@@ -9335,9 +9040,199 @@ var previewBuiltin = {
|
|
|
9335
9040
|
}
|
|
9336
9041
|
};
|
|
9337
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
|
+
|
|
9338
9234
|
// src/builtins/index.ts
|
|
9339
9235
|
var allBuiltins = [
|
|
9340
|
-
sessionOpenBuiltin,
|
|
9341
9236
|
sessionCloseBuiltin,
|
|
9342
9237
|
sessionListBuiltin,
|
|
9343
9238
|
sessionKillBuiltin,
|
|
@@ -9348,7 +9243,8 @@ var allBuiltins = [
|
|
|
9348
9243
|
pluginListBuiltin,
|
|
9349
9244
|
pluginReloadBuiltin,
|
|
9350
9245
|
createBuiltin,
|
|
9351
|
-
previewBuiltin
|
|
9246
|
+
previewBuiltin,
|
|
9247
|
+
knowledgeBuiltin
|
|
9352
9248
|
];
|
|
9353
9249
|
|
|
9354
9250
|
// src/utils/selector.ts
|
|
@@ -9571,7 +9467,8 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
|
|
|
9571
9467
|
} else {
|
|
9572
9468
|
switch (command) {
|
|
9573
9469
|
case "goto":
|
|
9574
|
-
|
|
9470
|
+
case "open":
|
|
9471
|
+
if (!args[0]) outputError(`Usage: xbrowser ${command} <url>`);
|
|
9575
9472
|
cmdName = "goto";
|
|
9576
9473
|
params = {
|
|
9577
9474
|
url: /^https?:\/\//i.test(args[0]) || /^wss?:\/\//i.test(args[0]) ? args[0] : "https://" + args[0],
|
|
@@ -9583,7 +9480,9 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
|
|
|
9583
9480
|
params = {
|
|
9584
9481
|
fullPage: !!(options["full-page"] || options.fullPage),
|
|
9585
9482
|
type: options.type,
|
|
9586
|
-
selector: options.selector || options.s
|
|
9483
|
+
selector: options.selector || options.s,
|
|
9484
|
+
base64: !!options.base64,
|
|
9485
|
+
output: options.output || options.o
|
|
9587
9486
|
};
|
|
9588
9487
|
break;
|
|
9589
9488
|
case "eval":
|
|
@@ -9660,7 +9559,7 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
|
|
|
9660
9559
|
let parsedActions;
|
|
9661
9560
|
if (options.action) {
|
|
9662
9561
|
const actionList = Array.isArray(options.action) ? options.action : [options.action];
|
|
9663
|
-
const { parseActionDsl } = await import("./parse-action-dsl-
|
|
9562
|
+
const { parseActionDsl } = await import("./parse-action-dsl-UM333TL2.js");
|
|
9664
9563
|
parsedActions = actionList.map((a) => parseActionDsl(a));
|
|
9665
9564
|
} else if (options["actions-file"]) {
|
|
9666
9565
|
const fs3 = await import("fs");
|
|
@@ -9788,36 +9687,27 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
|
|
|
9788
9687
|
}
|
|
9789
9688
|
|
|
9790
9689
|
// src/cli/session-routes.ts
|
|
9791
|
-
import { homedir as
|
|
9792
|
-
import { join as
|
|
9793
|
-
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";
|
|
9794
9693
|
function cleanSessionFiles() {
|
|
9795
|
-
const dir =
|
|
9694
|
+
const dir = join12(homedir8(), ".xbrowser", "sessions");
|
|
9796
9695
|
let count = 0;
|
|
9797
9696
|
try {
|
|
9798
|
-
for (const entry of
|
|
9799
|
-
const p =
|
|
9800
|
-
|
|
9697
|
+
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
9698
|
+
const p = join12(dir, entry.name);
|
|
9699
|
+
rmSync7(p, { recursive: true, force: true });
|
|
9801
9700
|
count++;
|
|
9802
9701
|
}
|
|
9803
9702
|
} catch {
|
|
9804
9703
|
}
|
|
9805
9704
|
return count;
|
|
9806
9705
|
}
|
|
9807
|
-
async function handleSession(args, options, mode,
|
|
9706
|
+
async function handleSession(args, options, mode, _cdpEndpoint) {
|
|
9808
9707
|
const sub = args[0];
|
|
9809
9708
|
switch (sub) {
|
|
9810
|
-
case "open": {
|
|
9811
|
-
const url = args[1];
|
|
9812
|
-
const name = options.name || process.env.XBROWSER_SESSION || "default";
|
|
9813
|
-
if (!url)
|
|
9814
|
-
outputError("Usage: xbrowser session open <url> [--name <name>] [--cdp <endpoint>]");
|
|
9815
|
-
const info = await forwardSessionCreate(name, url, cdpEndpoint);
|
|
9816
|
-
outputResult({ ok: true, ...info }, mode);
|
|
9817
|
-
break;
|
|
9818
|
-
}
|
|
9819
9709
|
case "close": {
|
|
9820
|
-
const name = options.name || process.env.XBROWSER_SESSION || "default";
|
|
9710
|
+
const name = options.session || options.name || process.env.XBROWSER_SESSION || "default";
|
|
9821
9711
|
try {
|
|
9822
9712
|
await forwardSessionClose(name);
|
|
9823
9713
|
} catch {
|
|
@@ -9838,7 +9728,7 @@ async function handleSession(args, options, mode, cdpEndpoint) {
|
|
|
9838
9728
|
break;
|
|
9839
9729
|
}
|
|
9840
9730
|
case "kill": {
|
|
9841
|
-
const name = options.name || process.env.XBROWSER_SESSION || "default";
|
|
9731
|
+
const name = options.session || options.name || process.env.XBROWSER_SESSION || "default";
|
|
9842
9732
|
try {
|
|
9843
9733
|
await forwardSessionClose(name);
|
|
9844
9734
|
} catch {
|
|
@@ -10256,7 +10146,7 @@ function handleDaemon(args, options, mode) {
|
|
|
10256
10146
|
break;
|
|
10257
10147
|
}
|
|
10258
10148
|
default:
|
|
10259
|
-
console.log("
|
|
10149
|
+
console.log("Daemon starts automatically. No manual action needed.");
|
|
10260
10150
|
}
|
|
10261
10151
|
}
|
|
10262
10152
|
|
|
@@ -10267,7 +10157,8 @@ async function handleRecord(args, options, mode) {
|
|
|
10267
10157
|
case "start": {
|
|
10268
10158
|
const url = options.url;
|
|
10269
10159
|
const sessionName = options.session || "default";
|
|
10270
|
-
const
|
|
10160
|
+
const cdpEndpoint = options.cdp;
|
|
10161
|
+
const result = await forwardRecordStart(sessionName, url, cdpEndpoint);
|
|
10271
10162
|
if (!result.ok) {
|
|
10272
10163
|
outputError(String(result.error || "Failed to start recording"));
|
|
10273
10164
|
return;
|
|
@@ -10351,6 +10242,13 @@ async function handleRecord(args, options, mode) {
|
|
|
10351
10242
|
outputResult(result, mode);
|
|
10352
10243
|
break;
|
|
10353
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
|
+
}
|
|
10354
10252
|
default:
|
|
10355
10253
|
console.log("Usage:");
|
|
10356
10254
|
console.log(" xbrowser record start [--url <url>] [--session <name>]");
|
|
@@ -10358,6 +10256,7 @@ async function handleRecord(args, options, mode) {
|
|
|
10358
10256
|
console.log(" xbrowser record status [--session <name>]");
|
|
10359
10257
|
console.log(" xbrowser record summary [--session <name>] [--json]");
|
|
10360
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>]");
|
|
10361
10260
|
console.log("");
|
|
10362
10261
|
console.log("Checkpoint types: dialog, captcha, login, iframe, slider, custom");
|
|
10363
10262
|
}
|
|
@@ -10546,7 +10445,7 @@ async function handleConvert(args, _mode) {
|
|
|
10546
10445
|
const fs3 = await import("fs");
|
|
10547
10446
|
const path3 = await import("path");
|
|
10548
10447
|
const { default: yaml } = await import("yaml");
|
|
10549
|
-
const { generateJSScript, generatePythonScript, generateBashScript } = await import("./convert-
|
|
10448
|
+
const { generateJSScript, generatePythonScript, generateBashScript } = await import("./convert-LB3GJTLR.js");
|
|
10550
10449
|
const content = fs3.readFileSync(filePath, "utf-8");
|
|
10551
10450
|
const recording = yaml.parse(content);
|
|
10552
10451
|
const ext = path3.extname(outputPath).toLowerCase();
|
|
@@ -10571,7 +10470,7 @@ async function handleExtract(args, _mode) {
|
|
|
10571
10470
|
console.error("Usage: xbrowser extract <recording.yaml>");
|
|
10572
10471
|
process.exit(1);
|
|
10573
10472
|
}
|
|
10574
|
-
const { extractAndSave, printExtractSummary } = await import("./extract-
|
|
10473
|
+
const { extractAndSave, printExtractSummary } = await import("./extract-BSYBM4MR.js");
|
|
10575
10474
|
const { summary, outputPath } = extractAndSave(filePath);
|
|
10576
10475
|
printExtractSummary(summary);
|
|
10577
10476
|
console.log(`
|
|
@@ -10584,33 +10483,196 @@ async function handleFilter(args, _mode) {
|
|
|
10584
10483
|
console.error("Usage: xbrowser filter <input.yaml> <output.yaml> [--exclude-types=type1,type2]");
|
|
10585
10484
|
process.exit(1);
|
|
10586
10485
|
}
|
|
10587
|
-
const { filterRecording, parseExcludeTypes } = await import("./filter-
|
|
10486
|
+
const { filterRecording, parseExcludeTypes } = await import("./filter-KCFO4RSV.js");
|
|
10588
10487
|
const excludeTypes = parseExcludeTypes(args.slice(2));
|
|
10589
10488
|
const result = filterRecording(filePath, outputPath, excludeTypes);
|
|
10590
10489
|
console.log(`Filtered ${filePath} -> ${outputPath}`);
|
|
10591
10490
|
console.log(` Original: ${result.originalCount}, After: ${result.filteredCount}, Removed: ${result.removed} (${result.percentage}%)`);
|
|
10592
10491
|
}
|
|
10593
|
-
|
|
10594
|
-
|
|
10595
|
-
|
|
10596
|
-
|
|
10597
|
-
|
|
10598
|
-
|
|
10599
|
-
|
|
10600
|
-
|
|
10601
|
-
|
|
10602
|
-
|
|
10603
|
-
|
|
10604
|
-
|
|
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);
|
|
10552
|
+
}
|
|
10553
|
+
if (action.type === "input" && !inputSelectors.some((s) => s.selector === sel)) {
|
|
10554
|
+
inputSelectors.push({ selector: sel, placeholder: el.placeholder });
|
|
10605
10555
|
}
|
|
10606
10556
|
}
|
|
10607
|
-
|
|
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
|
+
});`);
|
|
10624
|
+
}
|
|
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;
|
|
10608
10650
|
}
|
|
10609
|
-
|
|
10610
|
-
|
|
10611
|
-
|
|
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
|
+
`;
|
|
10612
10671
|
}
|
|
10613
10672
|
|
|
10673
|
+
// src/stdin.ts
|
|
10674
|
+
import { readStdin as readStdin2, readCommandFile, splitFileLine } from "@dyyz1993/xcli-core";
|
|
10675
|
+
|
|
10614
10676
|
// src/cli/run-routes.ts
|
|
10615
10677
|
async function handleRun(filePath, options) {
|
|
10616
10678
|
let commands;
|
|
@@ -10654,10 +10716,10 @@ async function handleRun(filePath, options) {
|
|
|
10654
10716
|
async function handleViewer(_args, options, mode, _cdpEndpoint) {
|
|
10655
10717
|
const name = options.name || process.env.XBROWSER_SESSION || "default";
|
|
10656
10718
|
const selector = options.selector;
|
|
10657
|
-
|
|
10719
|
+
let status = getDaemonProcessStatus();
|
|
10658
10720
|
if (!status.running) {
|
|
10659
|
-
|
|
10660
|
-
|
|
10721
|
+
await startDaemonProcess();
|
|
10722
|
+
status = getDaemonProcessStatus();
|
|
10661
10723
|
}
|
|
10662
10724
|
const port = status.port || getDaemonConfig().basePort;
|
|
10663
10725
|
let url = `http://localhost:${port}/preview/${name}`;
|
|
@@ -10977,6 +11039,235 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
10977
11039
|
}
|
|
10978
11040
|
}
|
|
10979
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
|
+
|
|
10980
11271
|
// src/cli/help.ts
|
|
10981
11272
|
function showMainHelp() {
|
|
10982
11273
|
console.log(`
|
|
@@ -10990,11 +11281,11 @@ Usage:
|
|
|
10990
11281
|
xbrowser -e cmd1 -e cmd2 Execute multiple -e commands
|
|
10991
11282
|
|
|
10992
11283
|
Commands:
|
|
10993
|
-
session
|
|
10994
|
-
session close [--name <n>] Close session
|
|
11284
|
+
session close [--session <name>] Close session
|
|
10995
11285
|
session list List sessions
|
|
10996
|
-
session kill [--
|
|
11286
|
+
session kill [--session <name>] Kill session
|
|
10997
11287
|
goto <url> Navigate to URL
|
|
11288
|
+
open <url> Navigate to URL (alias for goto)
|
|
10998
11289
|
click <selector> Click element (-s <sel>)
|
|
10999
11290
|
fill <selector> <value> Fill input (-s <sel> -v <val>)
|
|
11000
11291
|
type <selector> <text> Type text (-s <sel> -v <text>)
|
|
@@ -11004,7 +11295,7 @@ Commands:
|
|
|
11004
11295
|
dblclick <selector> Double click (-s <sel>)
|
|
11005
11296
|
check <selector> Check checkbox (-s <sel>)
|
|
11006
11297
|
uncheck <selector> Uncheck checkbox (-s <sel>)
|
|
11007
|
-
screenshot [--full-page]
|
|
11298
|
+
screenshot [--full-page] [--base64] Take screenshot (saves to ~/.xbrowser/screenshots/; use --base64 for inline data)
|
|
11008
11299
|
eval <expression> Evaluate JS
|
|
11009
11300
|
wait <selector> [--timeout <ms>] Wait for element (-s <sel>)
|
|
11010
11301
|
scroll <direction> [--distance N] Scroll page
|
|
@@ -11026,9 +11317,6 @@ Commands:
|
|
|
11026
11317
|
plugin list List plugins
|
|
11027
11318
|
plugin reload <name> Reload plugin
|
|
11028
11319
|
create <name> --template <type> Create plugin
|
|
11029
|
-
daemon start [--port <port>] Start daemon
|
|
11030
|
-
daemon stop Stop daemon
|
|
11031
|
-
daemon status Check status
|
|
11032
11320
|
serve [--port <port>] [--token <t>] Start HTTP server for remote access
|
|
11033
11321
|
remote <url> [command] [--token <t>] Execute command on remote server
|
|
11034
11322
|
record start --url <url> Start recording
|
|
@@ -11401,19 +11689,19 @@ function headersToObject(headers) {
|
|
|
11401
11689
|
return result;
|
|
11402
11690
|
}
|
|
11403
11691
|
function readBody(req) {
|
|
11404
|
-
return new Promise((
|
|
11692
|
+
return new Promise((resolve10, reject) => {
|
|
11405
11693
|
const chunks = [];
|
|
11406
11694
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
11407
11695
|
req.on("end", () => {
|
|
11408
11696
|
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
11409
11697
|
if (!raw) {
|
|
11410
|
-
|
|
11698
|
+
resolve10(null);
|
|
11411
11699
|
return;
|
|
11412
11700
|
}
|
|
11413
11701
|
try {
|
|
11414
|
-
|
|
11702
|
+
resolve10(JSON.parse(raw));
|
|
11415
11703
|
} catch {
|
|
11416
|
-
|
|
11704
|
+
resolve10(null);
|
|
11417
11705
|
}
|
|
11418
11706
|
});
|
|
11419
11707
|
req.on("error", reject);
|
|
@@ -11458,7 +11746,7 @@ var HTTPServer = class {
|
|
|
11458
11746
|
res.end(JSON.stringify({ error: "INTERNAL_ERROR", message, statusCode: 500 }));
|
|
11459
11747
|
});
|
|
11460
11748
|
});
|
|
11461
|
-
return new Promise((
|
|
11749
|
+
return new Promise((resolve10, reject) => {
|
|
11462
11750
|
const server = this.server;
|
|
11463
11751
|
server.on("error", (err) => {
|
|
11464
11752
|
this.server = null;
|
|
@@ -11470,7 +11758,7 @@ var HTTPServer = class {
|
|
|
11470
11758
|
this.port = addr.port;
|
|
11471
11759
|
}
|
|
11472
11760
|
console.log(`HTTP server listening on http://${this.host}:${this.port}`);
|
|
11473
|
-
|
|
11761
|
+
resolve10({ port: this.port, host: this.host });
|
|
11474
11762
|
});
|
|
11475
11763
|
});
|
|
11476
11764
|
}
|
|
@@ -11481,13 +11769,13 @@ var HTTPServer = class {
|
|
|
11481
11769
|
*/
|
|
11482
11770
|
async stop() {
|
|
11483
11771
|
if (!this.server) return;
|
|
11484
|
-
return new Promise((
|
|
11772
|
+
return new Promise((resolve10, reject) => {
|
|
11485
11773
|
this.server.close((err) => {
|
|
11486
11774
|
if (err) {
|
|
11487
11775
|
reject(err);
|
|
11488
11776
|
} else {
|
|
11489
11777
|
this.server = null;
|
|
11490
|
-
|
|
11778
|
+
resolve10();
|
|
11491
11779
|
}
|
|
11492
11780
|
});
|
|
11493
11781
|
});
|
|
@@ -11546,6 +11834,7 @@ function showCommandHelp(siteName, cmd, siteConfig, mode) {
|
|
|
11546
11834
|
command: c.name,
|
|
11547
11835
|
description: c.description,
|
|
11548
11836
|
scope: c.scope,
|
|
11837
|
+
...c.loginRequired ? { loginRequired: c.loginRequired } : {},
|
|
11549
11838
|
parameters: paramsList
|
|
11550
11839
|
}, mode);
|
|
11551
11840
|
} else {
|
|
@@ -11558,9 +11847,25 @@ function showCommandHelp(siteName, cmd, siteConfig, mode) {
|
|
|
11558
11847
|
examples: c.examples
|
|
11559
11848
|
}, { color: false, emoji: false });
|
|
11560
11849
|
console.log(text);
|
|
11850
|
+
if (c.loginRequired) {
|
|
11851
|
+
console.log(` Login: ${c.loginRequired}`);
|
|
11852
|
+
}
|
|
11561
11853
|
console.log("");
|
|
11562
11854
|
}
|
|
11563
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
|
+
}
|
|
11564
11869
|
function extractZodFieldInfo(value) {
|
|
11565
11870
|
const field = value;
|
|
11566
11871
|
const fieldDef = field._def;
|
|
@@ -11624,7 +11929,7 @@ function extractCdpFromArgv(argv) {
|
|
|
11624
11929
|
if (argv[i] === "--cdp" && argv[i + 1]) return argv[i + 1];
|
|
11625
11930
|
if (typeof argv[i] === "string" && argv[i].startsWith("--cdp=")) return argv[i].slice(6);
|
|
11626
11931
|
}
|
|
11627
|
-
return
|
|
11932
|
+
return process.env.XBROWSER_CDP;
|
|
11628
11933
|
}
|
|
11629
11934
|
async function handleStdinMode(stdinCommands, argv) {
|
|
11630
11935
|
const chain = stdinCommands.join(" && ");
|
|
@@ -11687,7 +11992,7 @@ async function routeCommand(argv, stdinCommands) {
|
|
|
11687
11992
|
const cmdArgs = positional.slice(1);
|
|
11688
11993
|
const mode = options.json ? "json" : options.yaml ? "yaml" : "text";
|
|
11689
11994
|
const sessionName = options.session || process.env.XBROWSER_SESSION || "default";
|
|
11690
|
-
const cdpEndpoint = options.cdp;
|
|
11995
|
+
const cdpEndpoint = options.cdp || process.env.XBROWSER_CDP;
|
|
11691
11996
|
if (options.version || options.v) {
|
|
11692
11997
|
console.log(`xbrowser v${version}`);
|
|
11693
11998
|
return;
|
|
@@ -11820,12 +12125,21 @@ async function routeCommand(argv, stdinCommands) {
|
|
|
11820
12125
|
if (builtin) await builtin.execute(cmdArgs, options, { cwd: process.cwd() });
|
|
11821
12126
|
break;
|
|
11822
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
|
+
}
|
|
11823
12134
|
case "viewer":
|
|
11824
12135
|
await handleViewer(cmdArgs, options, mode, cdpEndpoint);
|
|
11825
12136
|
break;
|
|
11826
12137
|
case "help":
|
|
11827
12138
|
showMainHelp();
|
|
11828
12139
|
break;
|
|
12140
|
+
case "test":
|
|
12141
|
+
await handleTest(cmdArgs, options, mode, cdpEndpoint);
|
|
12142
|
+
break;
|
|
11829
12143
|
case "net":
|
|
11830
12144
|
await handleNetCommand(cmdArgs, options, mode, sessionName);
|
|
11831
12145
|
break;
|
|
@@ -11935,9 +12249,14 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
11935
12249
|
}
|
|
11936
12250
|
const needsBrowser = cmdEntry.scope === "page" || cmdEntry.scope === "browser";
|
|
11937
12251
|
if (needsBrowser && !process.env.XBROWSER_DAEMON_WORKER) {
|
|
11938
|
-
const { forwardExec } = await import("./daemon-client-
|
|
12252
|
+
const { forwardExec } = await import("./daemon-client-UZZEHHIV.js");
|
|
11939
12253
|
const userTimeout = typeof params.timeout === "number" && params.timeout > 0 ? params.timeout * 1e3 + 3e4 : void 0;
|
|
11940
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
|
+
}
|
|
11941
12260
|
if (result && result.success !== false) {
|
|
11942
12261
|
if (isCommandResult2(result)) {
|
|
11943
12262
|
if (mode === "json" || mode === "yaml") {
|
|
@@ -11981,10 +12300,33 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
11981
12300
|
cliName: "xbrowser",
|
|
11982
12301
|
waitForHuman: async (_opts) => {
|
|
11983
12302
|
return { solved: false, timedOut: true };
|
|
11984
|
-
}
|
|
12303
|
+
},
|
|
12304
|
+
tips: new TipCollector2()
|
|
11985
12305
|
};
|
|
11986
12306
|
try {
|
|
11987
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
|
+
}
|
|
11988
12330
|
const cmdHooks = await loadHooks();
|
|
11989
12331
|
if (cmdHooks.length > 0 && session?.page) {
|
|
11990
12332
|
await Promise.all(cmdHooks.map((h) => h.onBeforeCommand?.({ page: session.page, command: `${command} ${subCommand}`, params })));
|
|
@@ -12003,23 +12345,39 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
12003
12345
|
saveSessionDiskMeta(sessionName, { conversationUrl: convUrl, cdpEndpoint });
|
|
12004
12346
|
}
|
|
12005
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
|
+
}
|
|
12006
12358
|
const outputData = isCommandResult2(result) ? result.data : result && typeof result === "object" ? result.data ?? result : result;
|
|
12007
12359
|
const tips = isCommandResult2(result) ? result.tips : result && typeof result === "object" ? result.tips : void 0;
|
|
12008
12360
|
if (mode === "json" || mode === "yaml") {
|
|
12009
12361
|
const finalOutput = {
|
|
12010
12362
|
data: outputData
|
|
12011
12363
|
};
|
|
12364
|
+
if (injectedViewerUrl) {
|
|
12365
|
+
finalOutput.viewerUrl = injectedViewerUrl;
|
|
12366
|
+
}
|
|
12367
|
+
if (tips?.length) {
|
|
12368
|
+
finalOutput.tips = tips;
|
|
12369
|
+
}
|
|
12012
12370
|
if (hookOutputs.length > 0) {
|
|
12013
12371
|
finalOutput.hooks = hookOutputs;
|
|
12014
12372
|
}
|
|
12015
12373
|
console.log(outputFormatter2.format(finalOutput, { mode, color: false, emoji: false }));
|
|
12016
12374
|
if (tips?.length) {
|
|
12017
|
-
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}`);
|
|
12018
12376
|
}
|
|
12019
12377
|
} else {
|
|
12020
12378
|
console.log(outputFormatter2.format(outputData, { mode: "text", color: true, emoji: true }));
|
|
12021
12379
|
if (tips?.length) {
|
|
12022
|
-
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}`);
|
|
12023
12381
|
}
|
|
12024
12382
|
if (hookOutputs.length > 0) {
|
|
12025
12383
|
for (const ho of hookOutputs) {
|
|
@@ -12197,7 +12555,7 @@ async function main() {
|
|
|
12197
12555
|
const command = process.argv[2];
|
|
12198
12556
|
const isLongRunning = command === "preview" || command === "serve";
|
|
12199
12557
|
if (!isLongRunning) {
|
|
12200
|
-
const { ensureProcessCanExit } = await import("./browser-
|
|
12558
|
+
const { ensureProcessCanExit } = await import("./browser-R56O3CW6.js");
|
|
12201
12559
|
await ensureProcessCanExit().catch(() => {
|
|
12202
12560
|
});
|
|
12203
12561
|
process.exit(exitCode);
|