@xbrowser/cli 0.16.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{browser-R7B255ML.js → browser-53KUFEEM.js} +5 -1
- package/dist/{browser-I2HJZ7IP.js → browser-DSVV4GHS.js} +2 -2
- package/dist/{browser-GWBH6OJK.js → browser-GURRY444.js} +3 -1
- package/dist/cdp-driver-MNPR3HZH.js +2537 -0
- package/dist/cdp-driver-SSXUGXP6.js +47 -0
- package/dist/{chunk-2ONMTDLK.js → chunk-2BQZIT3S.js} +2535 -50
- package/dist/{chunk-KDYXFLAC.js → chunk-2MFXKN32.js} +2 -2
- package/dist/chunk-42RPMJ76.js +2530 -0
- package/dist/{chunk-F3ZWFCJJ.js → chunk-E4O5ZU3H.js} +2535 -50
- package/dist/{chunk-ATFTAKMN.js → chunk-IDVD44ED.js} +20 -0
- package/dist/chunk-T4J4C2NZ.js +250 -0
- package/dist/{chunk-RS6YYWTK.js → chunk-YKOHDEFV.js} +73 -38
- package/dist/cli.js +1054 -140
- package/dist/{convert-4DUWZIKH.js → convert-EGFYNICZ.js} +2 -0
- package/dist/{daemon-client-GX2UYIW4.js → daemon-client-3VM7VU7O.js} +22 -0
- package/dist/{daemon-client-3IJD6X4B.js → daemon-client-YAVQ343A.js} +7 -1
- package/dist/daemon-main.js +984 -114
- package/dist/{extract-EGRXZSSK.js → extract-L2IW3IUB.js} +2 -0
- package/dist/{filter-OLAE26HN.js → filter-HC4RA7JY.js} +2 -0
- package/dist/index.d.ts +581 -41
- package/dist/index.js +1100 -182
- package/dist/launcher-KA7J32K5.js +19 -0
- package/dist/{network-store-YAF5OIBH.js → network-store-66A2RATI.js} +1 -0
- package/dist/{session-recorder-XET3DNML.js → session-recorder-MA75PKTQ.js} +1 -1
- package/package.json +2 -3
- package/dist/daemon-client-XWSSQBEA.js +0 -58
- package/dist/network-store-2S5HATEV.js +0 -194
- package/dist/parse-action-dsl-DRSPBALP.js +0 -72
- package/dist/screenshot-MB6R7RSS.js +0 -26
- package/dist/session-recorder-ILSSV2UC.js +0 -6
package/dist/index.js
CHANGED
|
@@ -1,3 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardCommandLog,
|
|
3
|
+
forwardNetworkAnalyze,
|
|
4
|
+
forwardNetworkAround,
|
|
5
|
+
forwardNetworkClear,
|
|
6
|
+
forwardNetworkCurl,
|
|
7
|
+
forwardNetworkDislike,
|
|
8
|
+
forwardNetworkExport,
|
|
9
|
+
forwardNetworkInspect,
|
|
10
|
+
forwardNetworkLike,
|
|
11
|
+
forwardNetworkList,
|
|
12
|
+
forwardNetworkReplay,
|
|
13
|
+
forwardNetworkTop,
|
|
14
|
+
forwardRecordCheckpoint,
|
|
15
|
+
forwardRecordStart,
|
|
16
|
+
forwardRecordStatus,
|
|
17
|
+
forwardRecordStop,
|
|
18
|
+
forwardRecordSummary,
|
|
19
|
+
forwardReplay,
|
|
20
|
+
forwardSessionClose,
|
|
21
|
+
forwardSessionCreate,
|
|
22
|
+
forwardSessionList,
|
|
23
|
+
forwardViewerCheckSelector,
|
|
24
|
+
getDaemonConfig,
|
|
25
|
+
getDaemonProcessStatus,
|
|
26
|
+
killAllDaemonProcesses,
|
|
27
|
+
startDaemonProcess,
|
|
28
|
+
stopDaemonProcess
|
|
29
|
+
} from "./chunk-IDVD44ED.js";
|
|
30
|
+
import {
|
|
31
|
+
CaptchaDetector,
|
|
32
|
+
HumanInteractionManager,
|
|
33
|
+
NPM_REGISTRY_URL,
|
|
34
|
+
NPM_SCOPE,
|
|
35
|
+
ScreencastCapturer,
|
|
36
|
+
WebhookNotifier,
|
|
37
|
+
getCaptchaConfig,
|
|
38
|
+
getConfigValue,
|
|
39
|
+
getMarketplaceUrl,
|
|
40
|
+
loadConfig,
|
|
41
|
+
readJsonFile,
|
|
42
|
+
resolveNpmPackageWithFallback,
|
|
43
|
+
setConfigValue
|
|
44
|
+
} from "./chunk-ITKPSIP7.js";
|
|
1
45
|
import {
|
|
2
46
|
generateBashScript,
|
|
3
47
|
generateJSScript,
|
|
@@ -14,7 +58,7 @@ import {
|
|
|
14
58
|
} from "./chunk-OPRXFZVE.js";
|
|
15
59
|
import {
|
|
16
60
|
SessionRecorder
|
|
17
|
-
} from "./chunk-
|
|
61
|
+
} from "./chunk-2MFXKN32.js";
|
|
18
62
|
import {
|
|
19
63
|
closeAllSessions,
|
|
20
64
|
closeEphemeralContext,
|
|
@@ -31,7 +75,9 @@ import {
|
|
|
31
75
|
resolveLaunchOpts,
|
|
32
76
|
saveSessionDiskMeta,
|
|
33
77
|
setActivePage
|
|
34
|
-
} from "./chunk-
|
|
78
|
+
} from "./chunk-YKOHDEFV.js";
|
|
79
|
+
import "./chunk-42RPMJ76.js";
|
|
80
|
+
import "./chunk-T4J4C2NZ.js";
|
|
35
81
|
import {
|
|
36
82
|
CDPInterceptorProxy,
|
|
37
83
|
advise,
|
|
@@ -46,50 +92,6 @@ import {
|
|
|
46
92
|
networkAnomalyRule,
|
|
47
93
|
pageLifecycleRule
|
|
48
94
|
} from "./chunk-ZZ2TFWIV.js";
|
|
49
|
-
import {
|
|
50
|
-
forwardCommandLog,
|
|
51
|
-
forwardNetworkAnalyze,
|
|
52
|
-
forwardNetworkAround,
|
|
53
|
-
forwardNetworkClear,
|
|
54
|
-
forwardNetworkCurl,
|
|
55
|
-
forwardNetworkDislike,
|
|
56
|
-
forwardNetworkExport,
|
|
57
|
-
forwardNetworkInspect,
|
|
58
|
-
forwardNetworkLike,
|
|
59
|
-
forwardNetworkList,
|
|
60
|
-
forwardNetworkReplay,
|
|
61
|
-
forwardNetworkTop,
|
|
62
|
-
forwardRecordCheckpoint,
|
|
63
|
-
forwardRecordStart,
|
|
64
|
-
forwardRecordStatus,
|
|
65
|
-
forwardRecordStop,
|
|
66
|
-
forwardRecordSummary,
|
|
67
|
-
forwardReplay,
|
|
68
|
-
forwardSessionClose,
|
|
69
|
-
forwardSessionCreate,
|
|
70
|
-
forwardSessionList,
|
|
71
|
-
forwardViewerCheckSelector,
|
|
72
|
-
getDaemonConfig,
|
|
73
|
-
getDaemonProcessStatus,
|
|
74
|
-
killAllDaemonProcesses,
|
|
75
|
-
startDaemonProcess,
|
|
76
|
-
stopDaemonProcess
|
|
77
|
-
} from "./chunk-ATFTAKMN.js";
|
|
78
|
-
import {
|
|
79
|
-
CaptchaDetector,
|
|
80
|
-
HumanInteractionManager,
|
|
81
|
-
NPM_REGISTRY_URL,
|
|
82
|
-
NPM_SCOPE,
|
|
83
|
-
ScreencastCapturer,
|
|
84
|
-
WebhookNotifier,
|
|
85
|
-
getCaptchaConfig,
|
|
86
|
-
getConfigValue,
|
|
87
|
-
getMarketplaceUrl,
|
|
88
|
-
loadConfig,
|
|
89
|
-
readJsonFile,
|
|
90
|
-
resolveNpmPackageWithFallback,
|
|
91
|
-
setConfigValue
|
|
92
|
-
} from "./chunk-ITKPSIP7.js";
|
|
93
95
|
import {
|
|
94
96
|
__require
|
|
95
97
|
} from "./chunk-3RG5ZIWI.js";
|
|
@@ -102,7 +104,7 @@ var version = pkg.version;
|
|
|
102
104
|
|
|
103
105
|
// src/executor.ts
|
|
104
106
|
import {
|
|
105
|
-
ok as
|
|
107
|
+
ok as ok26,
|
|
106
108
|
fail as fail7,
|
|
107
109
|
isCommandResult,
|
|
108
110
|
configureArchiveStore,
|
|
@@ -581,7 +583,7 @@ var pressCommand = registerCommand({
|
|
|
581
583
|
key: z2.string()
|
|
582
584
|
}),
|
|
583
585
|
handler: async (p, ctx) => {
|
|
584
|
-
await ctx.page.press(p.selector || "body", p.key, {
|
|
586
|
+
await ctx.page.press(p.selector || "body", p.key, { timeout: 1e4 });
|
|
585
587
|
return ok2({ key: p.key });
|
|
586
588
|
}
|
|
587
589
|
});
|
|
@@ -600,7 +602,7 @@ var selectCommand = registerCommand({
|
|
|
600
602
|
}),
|
|
601
603
|
handler: async (p, ctx) => {
|
|
602
604
|
const values = typeof p.value === "string" ? [p.value] : p.value;
|
|
603
|
-
await ctx.page.selectOption(p.selector, values
|
|
605
|
+
await ctx.page.selectOption(p.selector, values);
|
|
604
606
|
return ok2({ selector: p.selector, value: p.value });
|
|
605
607
|
}
|
|
606
608
|
});
|
|
@@ -616,7 +618,7 @@ var checkCommand = registerCommand({
|
|
|
616
618
|
selector: z2.string()
|
|
617
619
|
}),
|
|
618
620
|
handler: async (p, ctx) => {
|
|
619
|
-
await ctx.page.check(p.selector, {
|
|
621
|
+
await ctx.page.check(p.selector, { timeout: 1e4 });
|
|
620
622
|
return ok2({ selector: p.selector });
|
|
621
623
|
}
|
|
622
624
|
});
|
|
@@ -633,7 +635,7 @@ var hoverCommand = registerCommand({
|
|
|
633
635
|
selector: z2.string()
|
|
634
636
|
}),
|
|
635
637
|
handler: async (p, ctx) => {
|
|
636
|
-
await ctx.page.hover(p.selector, {
|
|
638
|
+
await ctx.page.hover(p.selector, { timeout: 1e4 });
|
|
637
639
|
return ok2({ selector: p.selector });
|
|
638
640
|
}
|
|
639
641
|
});
|
|
@@ -2162,7 +2164,7 @@ var scrapeCommand = registerCommand({
|
|
|
2162
2164
|
await page.goto(p.url, { waitUntil: "commit", timeout: p.timeout });
|
|
2163
2165
|
await page.waitForSelector("body", { timeout: p.timeout }).catch(() => {
|
|
2164
2166
|
});
|
|
2165
|
-
await page.waitForLoadState("networkidle",
|
|
2167
|
+
await page.waitForLoadState("networkidle", Math.min(p.timeout, 8e3)).catch(() => {
|
|
2166
2168
|
});
|
|
2167
2169
|
await page.waitForTimeout(p.waitAfterLoad > 0 ? p.waitAfterLoad : 2e3);
|
|
2168
2170
|
if (p.selector) {
|
|
@@ -3167,7 +3169,7 @@ async function fetchFullContent(urls, timeout, cdpEndpoint) {
|
|
|
3167
3169
|
const pg = await context.newPage();
|
|
3168
3170
|
try {
|
|
3169
3171
|
await pg.goto(url, { waitUntil: "domcontentloaded", timeout });
|
|
3170
|
-
await pg.waitForLoadState("networkidle",
|
|
3172
|
+
await pg.waitForLoadState("networkidle", timeout).catch(() => {
|
|
3171
3173
|
});
|
|
3172
3174
|
const html = await pg.content();
|
|
3173
3175
|
contentMap.set(url, htmlToMarkdown(html, { onlyMainContent: true }));
|
|
@@ -3563,7 +3565,7 @@ var networkCommand = registerCommand({
|
|
|
3563
3565
|
waitUntil: "domcontentloaded",
|
|
3564
3566
|
timeout: p.timeout
|
|
3565
3567
|
});
|
|
3566
|
-
await page.waitForLoadState("networkidle",
|
|
3568
|
+
await page.waitForLoadState("networkidle", p.timeout).catch(() => {
|
|
3567
3569
|
});
|
|
3568
3570
|
await page.waitForTimeout(p.wait);
|
|
3569
3571
|
const totalCount = captures.length;
|
|
@@ -4151,6 +4153,24 @@ function parseMarkdownResults(rawText) {
|
|
|
4151
4153
|
import { z as z21 } from "zod";
|
|
4152
4154
|
import { ok as ok20, fail as fail4 } from "@dyyz1993/xcli-core";
|
|
4153
4155
|
|
|
4156
|
+
// src/runtime/ref-store.ts
|
|
4157
|
+
var sessions = /* @__PURE__ */ new Map();
|
|
4158
|
+
function normalizeAgentRef(ref) {
|
|
4159
|
+
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
4160
|
+
}
|
|
4161
|
+
function replaceRefs(sessionKey2, screenHash, targets) {
|
|
4162
|
+
sessions.set(sessionKey2, {
|
|
4163
|
+
screenHash,
|
|
4164
|
+
targets: new Map(targets.map((target) => [target.ref, target]))
|
|
4165
|
+
});
|
|
4166
|
+
}
|
|
4167
|
+
function getRefTarget(sessionKey2, ref) {
|
|
4168
|
+
const session = sessions.get(sessionKey2);
|
|
4169
|
+
const target = session?.targets.get(normalizeAgentRef(ref));
|
|
4170
|
+
if (!session || !target) return null;
|
|
4171
|
+
return { screenHash: session.screenHash, target };
|
|
4172
|
+
}
|
|
4173
|
+
|
|
4154
4174
|
// src/utils/resolve-selector.ts
|
|
4155
4175
|
function buildElementSelector(el) {
|
|
4156
4176
|
function isUnique(sel) {
|
|
@@ -4322,9 +4342,9 @@ async function resolveSelectors(page, ariaSnapshot) {
|
|
|
4322
4342
|
}
|
|
4323
4343
|
return results;
|
|
4324
4344
|
}
|
|
4325
|
-
var REF_ONLY =
|
|
4345
|
+
var REF_ONLY = /^@?(e\d+)$/;
|
|
4326
4346
|
var refCache = /* @__PURE__ */ new Map();
|
|
4327
|
-
async function resolveRefParams(page, params, selectorKeys, cache) {
|
|
4347
|
+
async function resolveRefParams(page, params, selectorKeys, cache, sessionId) {
|
|
4328
4348
|
const tips = [];
|
|
4329
4349
|
const newParams = { ...params };
|
|
4330
4350
|
if (!selectorKeys || selectorKeys.length === 0) {
|
|
@@ -4333,7 +4353,17 @@ async function resolveRefParams(page, params, selectorKeys, cache) {
|
|
|
4333
4353
|
for (const key of selectorKeys) {
|
|
4334
4354
|
const val = params[key];
|
|
4335
4355
|
if (typeof val !== "string" || !REF_ONLY.test(val)) continue;
|
|
4336
|
-
const
|
|
4356
|
+
const match = val.match(REF_ONLY);
|
|
4357
|
+
if (!match) continue;
|
|
4358
|
+
const ref = normalizeAgentRef(match[1]);
|
|
4359
|
+
if (sessionId) {
|
|
4360
|
+
const runtimeTarget = getRefTarget(sessionId, ref);
|
|
4361
|
+
if (runtimeTarget) {
|
|
4362
|
+
tips.push(`ref=@${ref} (${key}) => ${runtimeTarget.target.selector} (observe)`);
|
|
4363
|
+
newParams[key] = runtimeTarget.target.selector;
|
|
4364
|
+
continue;
|
|
4365
|
+
}
|
|
4366
|
+
}
|
|
4337
4367
|
const activeCache = cache ?? refCache;
|
|
4338
4368
|
const cached = activeCache.get(ref);
|
|
4339
4369
|
if (cached) {
|
|
@@ -4614,6 +4644,404 @@ async function enhanceSemanticsWithLLM(url, ariaSnapshot, ruleBasedElements) {
|
|
|
4614
4644
|
saveSemantics(domain, pathKey, url, llmElements);
|
|
4615
4645
|
}
|
|
4616
4646
|
|
|
4647
|
+
// src/runtime/agent-runtime.ts
|
|
4648
|
+
function sessionKey(sessionId) {
|
|
4649
|
+
return sessionId || "default";
|
|
4650
|
+
}
|
|
4651
|
+
async function observePage(page, sessionId, options = {}) {
|
|
4652
|
+
const [title, raw] = await Promise.all([
|
|
4653
|
+
page.title().catch(() => ""),
|
|
4654
|
+
page.evaluate(
|
|
4655
|
+
({ includeHidden, limit }) => {
|
|
4656
|
+
function hash(input) {
|
|
4657
|
+
let h = 2166136261;
|
|
4658
|
+
for (let i = 0; i < input.length; i++) {
|
|
4659
|
+
h ^= input.charCodeAt(i);
|
|
4660
|
+
h = Math.imul(h, 16777619);
|
|
4661
|
+
}
|
|
4662
|
+
return (h >>> 0).toString(16);
|
|
4663
|
+
}
|
|
4664
|
+
function cssEscape(value) {
|
|
4665
|
+
const css = globalThis.CSS;
|
|
4666
|
+
return css?.escape ? css.escape(value) : value.replace(/["\\#.:,[\]>+~*]/g, "\\$&");
|
|
4667
|
+
}
|
|
4668
|
+
function isUnique(selector2) {
|
|
4669
|
+
try {
|
|
4670
|
+
return document.querySelectorAll(selector2).length === 1;
|
|
4671
|
+
} catch {
|
|
4672
|
+
return false;
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
function nthOfType(el) {
|
|
4676
|
+
const tag = el.tagName.toLowerCase();
|
|
4677
|
+
const parent = el.parentElement;
|
|
4678
|
+
if (!parent) return tag;
|
|
4679
|
+
const same = Array.from(parent.children).filter((child) => child.tagName === el.tagName);
|
|
4680
|
+
if (same.length === 1) return tag;
|
|
4681
|
+
return `${tag}:nth-of-type(${same.indexOf(el) + 1})`;
|
|
4682
|
+
}
|
|
4683
|
+
function selectorFor(el) {
|
|
4684
|
+
const tag = el.tagName.toLowerCase();
|
|
4685
|
+
const id = el.getAttribute("id");
|
|
4686
|
+
if (id) {
|
|
4687
|
+
const selector2 = `#${cssEscape(id)}`;
|
|
4688
|
+
if (isUnique(selector2)) return selector2;
|
|
4689
|
+
}
|
|
4690
|
+
for (const attr of ["data-testid", "data-test", "data-qa", "name", "aria-label"]) {
|
|
4691
|
+
const value = el.getAttribute(attr);
|
|
4692
|
+
if (!value) continue;
|
|
4693
|
+
const selector2 = `[${attr}="${cssEscape(value)}"]`;
|
|
4694
|
+
if (isUnique(selector2)) return selector2;
|
|
4695
|
+
const tagged = `${tag}${selector2}`;
|
|
4696
|
+
if (isUnique(tagged)) return tagged;
|
|
4697
|
+
}
|
|
4698
|
+
const classes = Array.from(el.classList).slice(0, 3);
|
|
4699
|
+
for (const cls of classes) {
|
|
4700
|
+
const selector2 = `${tag}.${cssEscape(cls)}`;
|
|
4701
|
+
if (isUnique(selector2)) return selector2;
|
|
4702
|
+
}
|
|
4703
|
+
const parts = [];
|
|
4704
|
+
let cur = el;
|
|
4705
|
+
while (cur && cur !== document.body && cur !== document.documentElement && parts.length < 6) {
|
|
4706
|
+
parts.unshift(nthOfType(cur));
|
|
4707
|
+
const selector2 = parts.join(" > ");
|
|
4708
|
+
if (isUnique(selector2)) return selector2;
|
|
4709
|
+
cur = cur.parentElement;
|
|
4710
|
+
}
|
|
4711
|
+
return parts.join(" > ") || tag;
|
|
4712
|
+
}
|
|
4713
|
+
function roleFor(el) {
|
|
4714
|
+
const explicit = el.getAttribute("role");
|
|
4715
|
+
if (explicit) return explicit;
|
|
4716
|
+
const tag = el.tagName.toLowerCase();
|
|
4717
|
+
if (tag === "a") return "link";
|
|
4718
|
+
if (tag === "button") return "button";
|
|
4719
|
+
if (tag === "select") return "combobox";
|
|
4720
|
+
if (tag === "textarea") return "textbox";
|
|
4721
|
+
if (tag === "input") {
|
|
4722
|
+
const type = (el.getAttribute("type") || "text").toLowerCase();
|
|
4723
|
+
if (type === "checkbox") return "checkbox";
|
|
4724
|
+
if (type === "radio") return "radio";
|
|
4725
|
+
if (type === "button" || type === "submit" || type === "reset") return "button";
|
|
4726
|
+
return "textbox";
|
|
4727
|
+
}
|
|
4728
|
+
return tag;
|
|
4729
|
+
}
|
|
4730
|
+
function textName(el) {
|
|
4731
|
+
const input = el;
|
|
4732
|
+
const direct = el.getAttribute("aria-label") || el.getAttribute("placeholder") || el.getAttribute("title") || input.value || el.textContent || "";
|
|
4733
|
+
return direct.replace(/\s+/g, " ").trim().slice(0, 120);
|
|
4734
|
+
}
|
|
4735
|
+
function actionsFor(role, editable) {
|
|
4736
|
+
const actions = [];
|
|
4737
|
+
if (editable) actions.push("fill", "type");
|
|
4738
|
+
if (role === "combobox") actions.push("select");
|
|
4739
|
+
if (role === "checkbox" || role === "radio") actions.push("check");
|
|
4740
|
+
actions.push("click", "hover");
|
|
4741
|
+
return Array.from(new Set(actions));
|
|
4742
|
+
}
|
|
4743
|
+
const selector = [
|
|
4744
|
+
"a[href]",
|
|
4745
|
+
"button",
|
|
4746
|
+
"input",
|
|
4747
|
+
"textarea",
|
|
4748
|
+
"select",
|
|
4749
|
+
"summary",
|
|
4750
|
+
"label",
|
|
4751
|
+
"[role]",
|
|
4752
|
+
"[tabindex]",
|
|
4753
|
+
'[contenteditable="true"]'
|
|
4754
|
+
].join(",");
|
|
4755
|
+
const candidates = Array.from(document.querySelectorAll(selector));
|
|
4756
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4757
|
+
const targets2 = [];
|
|
4758
|
+
for (const el of candidates) {
|
|
4759
|
+
const rect = el.getBoundingClientRect();
|
|
4760
|
+
const style = window.getComputedStyle(el);
|
|
4761
|
+
const visible = rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
|
|
4762
|
+
if (!includeHidden && !visible) continue;
|
|
4763
|
+
const selectorValue = selectorFor(el);
|
|
4764
|
+
if (!selectorValue || seen.has(selectorValue)) continue;
|
|
4765
|
+
seen.add(selectorValue);
|
|
4766
|
+
const input = el;
|
|
4767
|
+
const tag = el.tagName.toLowerCase();
|
|
4768
|
+
const role = roleFor(el);
|
|
4769
|
+
const editable = tag === "textarea" || tag === "select" || tag === "input" && !["button", "submit", "reset", "checkbox", "radio"].includes((input.type || "").toLowerCase()) || el.isContentEditable;
|
|
4770
|
+
const enabled = !input.disabled && el.getAttribute("aria-disabled") !== "true";
|
|
4771
|
+
const checked = typeof input.checked === "boolean" && ["checkbox", "radio"].includes((input.type || "").toLowerCase()) ? input.checked : void 0;
|
|
4772
|
+
targets2.push({
|
|
4773
|
+
selector: selectorValue,
|
|
4774
|
+
role,
|
|
4775
|
+
name: textName(el),
|
|
4776
|
+
tag,
|
|
4777
|
+
visible,
|
|
4778
|
+
enabled,
|
|
4779
|
+
editable,
|
|
4780
|
+
...checked !== void 0 ? { checked } : {},
|
|
4781
|
+
...editable && input.value ? { value: input.value.slice(0, 120) } : {},
|
|
4782
|
+
...visible ? { box: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) } } : {},
|
|
4783
|
+
actions: actionsFor(role, editable)
|
|
4784
|
+
});
|
|
4785
|
+
if (targets2.length >= limit) break;
|
|
4786
|
+
}
|
|
4787
|
+
const stateText = [
|
|
4788
|
+
location.href,
|
|
4789
|
+
document.title,
|
|
4790
|
+
document.body?.innerText?.slice(0, 5e3) || ""
|
|
4791
|
+
].join("\n");
|
|
4792
|
+
return { screenHash: hash(stateText), targets: targets2 };
|
|
4793
|
+
},
|
|
4794
|
+
{ includeHidden: !!options.includeHidden, limit: options.limit ?? 80 }
|
|
4795
|
+
)
|
|
4796
|
+
]);
|
|
4797
|
+
const targets = raw.targets.map((target, index) => ({
|
|
4798
|
+
ref: `e${index + 1}`,
|
|
4799
|
+
...target
|
|
4800
|
+
}));
|
|
4801
|
+
replaceRefs(sessionKey(sessionId), raw.screenHash, targets);
|
|
4802
|
+
return {
|
|
4803
|
+
url: page.url(),
|
|
4804
|
+
title,
|
|
4805
|
+
screenHash: raw.screenHash,
|
|
4806
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4807
|
+
targets
|
|
4808
|
+
};
|
|
4809
|
+
}
|
|
4810
|
+
function quoteName(name) {
|
|
4811
|
+
const cleaned = name.replace(/\s+/g, " ").trim();
|
|
4812
|
+
if (!cleaned) return "";
|
|
4813
|
+
return ` "${cleaned.replace(/"/g, '\\"')}"`;
|
|
4814
|
+
}
|
|
4815
|
+
function targetFlags(target) {
|
|
4816
|
+
const flags = [target.role || target.tag];
|
|
4817
|
+
if (!target.enabled) flags.push("disabled");
|
|
4818
|
+
if (target.editable) flags.push("editable");
|
|
4819
|
+
if (target.checked !== void 0) flags.push(target.checked ? "checked" : "unchecked");
|
|
4820
|
+
return flags.join(" ");
|
|
4821
|
+
}
|
|
4822
|
+
function buildSelectorMap(observation) {
|
|
4823
|
+
return Object.fromEntries(observation.targets.map((target) => [target.ref, target.selector]));
|
|
4824
|
+
}
|
|
4825
|
+
function formatObservationCompact(observation, options = {}) {
|
|
4826
|
+
const lines = [
|
|
4827
|
+
`Page: ${observation.title || "(untitled)"}`,
|
|
4828
|
+
`URL: ${observation.url}`,
|
|
4829
|
+
`Screen: ${observation.screenHash}`,
|
|
4830
|
+
""
|
|
4831
|
+
];
|
|
4832
|
+
if (observation.targets.length === 0) {
|
|
4833
|
+
lines.push("(no interactive targets)");
|
|
4834
|
+
} else {
|
|
4835
|
+
for (const target of observation.targets) {
|
|
4836
|
+
lines.push(`@${target.ref} [${targetFlags(target)}]${quoteName(target.name)}`);
|
|
4837
|
+
}
|
|
4838
|
+
}
|
|
4839
|
+
if (options.selectors && observation.targets.length > 0) {
|
|
4840
|
+
lines.push("", "## Selectors");
|
|
4841
|
+
lines.push(observation.targets.map((target) => `${target.ref}: ${target.selector}`).join(" | "));
|
|
4842
|
+
}
|
|
4843
|
+
return lines.join("\n");
|
|
4844
|
+
}
|
|
4845
|
+
async function getPageScreenHash(page) {
|
|
4846
|
+
const observation = await page.evaluate(() => {
|
|
4847
|
+
let h = 2166136261;
|
|
4848
|
+
const input = [location.href, document.title, document.body?.innerText?.slice(0, 5e3) || ""].join("\n");
|
|
4849
|
+
for (let i = 0; i < input.length; i++) {
|
|
4850
|
+
h ^= input.charCodeAt(i);
|
|
4851
|
+
h = Math.imul(h, 16777619);
|
|
4852
|
+
}
|
|
4853
|
+
return (h >>> 0).toString(16);
|
|
4854
|
+
});
|
|
4855
|
+
return observation;
|
|
4856
|
+
}
|
|
4857
|
+
async function actionability(page, selector) {
|
|
4858
|
+
return await page.evaluate((sel) => {
|
|
4859
|
+
const el = document.querySelector(sel);
|
|
4860
|
+
if (!el) return { ok: false, reason: "not_found" };
|
|
4861
|
+
const rect = el.getBoundingClientRect();
|
|
4862
|
+
const style = window.getComputedStyle(el);
|
|
4863
|
+
const visible = rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
|
|
4864
|
+
if (!visible) return { ok: false, reason: "not_visible" };
|
|
4865
|
+
const input = el;
|
|
4866
|
+
const enabled = !input.disabled && el.getAttribute("aria-disabled") !== "true";
|
|
4867
|
+
if (!enabled) return { ok: false, reason: "disabled" };
|
|
4868
|
+
const cx = rect.left + rect.width / 2;
|
|
4869
|
+
const cy = rect.top + rect.height / 2;
|
|
4870
|
+
const hit = document.elementFromPoint(cx, cy);
|
|
4871
|
+
if (hit && hit !== el && !el.contains(hit) && !hit.contains(el)) {
|
|
4872
|
+
return { ok: false, reason: "covered" };
|
|
4873
|
+
}
|
|
4874
|
+
return { ok: true };
|
|
4875
|
+
}, selector);
|
|
4876
|
+
}
|
|
4877
|
+
async function actOnPage(page, sessionId, input) {
|
|
4878
|
+
const normalizedRef = input.ref ? normalizeAgentRef(input.ref) : void 0;
|
|
4879
|
+
const refMatch = normalizedRef ? getRefTarget(sessionKey(sessionId), normalizedRef) : null;
|
|
4880
|
+
const selector = input.selector || refMatch?.target.selector;
|
|
4881
|
+
if (!selector) {
|
|
4882
|
+
return {
|
|
4883
|
+
action: input.action,
|
|
4884
|
+
selector: "",
|
|
4885
|
+
ref: normalizedRef,
|
|
4886
|
+
success: false,
|
|
4887
|
+
reason: input.ref ? "unknown_ref" : "missing_target",
|
|
4888
|
+
message: normalizedRef ? `Ref "${input.ref}" not found. Run observe again.` : "Provide ref or selector."
|
|
4889
|
+
};
|
|
4890
|
+
}
|
|
4891
|
+
const hash = await getPageScreenHash(page).catch(() => void 0);
|
|
4892
|
+
const stale = !!(refMatch && hash && hash !== refMatch.screenHash);
|
|
4893
|
+
if (!input.force) {
|
|
4894
|
+
const check = await actionability(page, selector);
|
|
4895
|
+
if (!check.ok) {
|
|
4896
|
+
return {
|
|
4897
|
+
action: input.action,
|
|
4898
|
+
selector,
|
|
4899
|
+
ref: normalizedRef,
|
|
4900
|
+
success: false,
|
|
4901
|
+
reason: stale ? "stale_ref" : check.reason,
|
|
4902
|
+
message: stale ? `Ref "${input.ref}" may be stale. Run observe again.` : `Target is not actionable: ${check.reason}`,
|
|
4903
|
+
stale,
|
|
4904
|
+
screenHash: hash,
|
|
4905
|
+
target: refMatch?.target
|
|
4906
|
+
};
|
|
4907
|
+
}
|
|
4908
|
+
}
|
|
4909
|
+
const timeout = input.timeout ?? 1e4;
|
|
4910
|
+
try {
|
|
4911
|
+
switch (input.action) {
|
|
4912
|
+
case "click":
|
|
4913
|
+
await page.locator(selector).first().click({ timeout, force: !!input.force });
|
|
4914
|
+
break;
|
|
4915
|
+
case "fill":
|
|
4916
|
+
if (input.value === void 0) throw new Error("fill requires value");
|
|
4917
|
+
await page.locator(selector).first().fill(input.value, { timeout, force: !!input.force });
|
|
4918
|
+
break;
|
|
4919
|
+
case "type":
|
|
4920
|
+
if (input.value === void 0) throw new Error("type requires value");
|
|
4921
|
+
await page.locator(selector).first().pressSequentially(input.value, { timeout });
|
|
4922
|
+
break;
|
|
4923
|
+
case "press":
|
|
4924
|
+
if (!input.key) throw new Error("press requires key");
|
|
4925
|
+
await page.locator(selector).first().press(input.key, { timeout });
|
|
4926
|
+
break;
|
|
4927
|
+
case "select":
|
|
4928
|
+
if (input.value === void 0) throw new Error("select requires value");
|
|
4929
|
+
await page.locator(selector).first().selectOption(input.value);
|
|
4930
|
+
break;
|
|
4931
|
+
case "check":
|
|
4932
|
+
await page.locator(selector).first().check({ timeout });
|
|
4933
|
+
break;
|
|
4934
|
+
case "hover":
|
|
4935
|
+
await page.locator(selector).first().hover({ timeout });
|
|
4936
|
+
break;
|
|
4937
|
+
default: {
|
|
4938
|
+
const neverAction = input.action;
|
|
4939
|
+
throw new Error(`Unsupported action: ${neverAction}`);
|
|
4940
|
+
}
|
|
4941
|
+
}
|
|
4942
|
+
} catch (error) {
|
|
4943
|
+
return {
|
|
4944
|
+
action: input.action,
|
|
4945
|
+
selector,
|
|
4946
|
+
ref: normalizedRef,
|
|
4947
|
+
success: false,
|
|
4948
|
+
reason: "browser_error",
|
|
4949
|
+
message: error.message,
|
|
4950
|
+
stale,
|
|
4951
|
+
screenHash: hash,
|
|
4952
|
+
target: refMatch?.target
|
|
4953
|
+
};
|
|
4954
|
+
}
|
|
4955
|
+
return {
|
|
4956
|
+
action: input.action,
|
|
4957
|
+
selector,
|
|
4958
|
+
ref: normalizedRef,
|
|
4959
|
+
success: true,
|
|
4960
|
+
stale,
|
|
4961
|
+
screenHash: hash,
|
|
4962
|
+
target: refMatch?.target
|
|
4963
|
+
};
|
|
4964
|
+
}
|
|
4965
|
+
function matchUrlPattern(url, pattern) {
|
|
4966
|
+
if (pattern.includes("*")) {
|
|
4967
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
4968
|
+
return new RegExp(`^${escaped}$`).test(url);
|
|
4969
|
+
}
|
|
4970
|
+
return url.includes(pattern);
|
|
4971
|
+
}
|
|
4972
|
+
async function pollUntil(timeout, pollInterval, predicate) {
|
|
4973
|
+
const startedAt = Date.now();
|
|
4974
|
+
while (Date.now() - startedAt <= timeout) {
|
|
4975
|
+
if (await predicate()) return true;
|
|
4976
|
+
await new Promise((resolve16) => setTimeout(resolve16, pollInterval));
|
|
4977
|
+
}
|
|
4978
|
+
return false;
|
|
4979
|
+
}
|
|
4980
|
+
async function waitForPage(page, input) {
|
|
4981
|
+
const timeout = input.timeout ?? 3e4;
|
|
4982
|
+
const pollInterval = input.pollInterval ?? 200;
|
|
4983
|
+
const startedAt = Date.now();
|
|
4984
|
+
try {
|
|
4985
|
+
if (input.selector) {
|
|
4986
|
+
const state = input.state ?? "visible";
|
|
4987
|
+
await page.locator(input.selector).first().waitFor({ state, timeout });
|
|
4988
|
+
return { success: true, matched: "selector", timeout, elapsed: Date.now() - startedAt };
|
|
4989
|
+
}
|
|
4990
|
+
if (input.text) {
|
|
4991
|
+
await page.getByText(input.text).first().waitFor({ state: "visible", timeout });
|
|
4992
|
+
return { success: true, matched: "text", timeout, elapsed: Date.now() - startedAt };
|
|
4993
|
+
}
|
|
4994
|
+
if (input.url) {
|
|
4995
|
+
const matched = await pollUntil(timeout, pollInterval, async () => matchUrlPattern(page.url(), input.url));
|
|
4996
|
+
return {
|
|
4997
|
+
success: matched,
|
|
4998
|
+
matched: "url",
|
|
4999
|
+
timeout,
|
|
5000
|
+
elapsed: Date.now() - startedAt,
|
|
5001
|
+
...matched ? {} : { message: `Timed out waiting for URL pattern: ${input.url}` }
|
|
5002
|
+
};
|
|
5003
|
+
}
|
|
5004
|
+
if (input.load) {
|
|
5005
|
+
await page.waitForLoadState(input.load, timeout);
|
|
5006
|
+
return { success: true, matched: "load", timeout, elapsed: Date.now() - startedAt };
|
|
5007
|
+
}
|
|
5008
|
+
if (input.fn) {
|
|
5009
|
+
await page.waitForFunction(input.fn, void 0, { timeout });
|
|
5010
|
+
return { success: true, matched: "fn", timeout, elapsed: Date.now() - startedAt };
|
|
5011
|
+
}
|
|
5012
|
+
if (input.screenHashChanged) {
|
|
5013
|
+
let screenHash = await getPageScreenHash(page);
|
|
5014
|
+
const matched = await pollUntil(timeout, pollInterval, async () => {
|
|
5015
|
+
screenHash = await getPageScreenHash(page);
|
|
5016
|
+
return screenHash !== input.screenHashChanged;
|
|
5017
|
+
});
|
|
5018
|
+
return {
|
|
5019
|
+
success: matched,
|
|
5020
|
+
matched: "screenHashChanged",
|
|
5021
|
+
timeout,
|
|
5022
|
+
elapsed: Date.now() - startedAt,
|
|
5023
|
+
screenHash,
|
|
5024
|
+
...matched ? {} : { message: `Timed out waiting for screen hash to change: ${input.screenHashChanged}` }
|
|
5025
|
+
};
|
|
5026
|
+
}
|
|
5027
|
+
} catch (error) {
|
|
5028
|
+
return {
|
|
5029
|
+
success: false,
|
|
5030
|
+
matched: input.selector ? "selector" : input.text ? "text" : input.url ? "url" : input.load ? "load" : input.fn ? "fn" : "screenHashChanged",
|
|
5031
|
+
timeout,
|
|
5032
|
+
elapsed: Date.now() - startedAt,
|
|
5033
|
+
message: error.message
|
|
5034
|
+
};
|
|
5035
|
+
}
|
|
5036
|
+
return {
|
|
5037
|
+
success: false,
|
|
5038
|
+
matched: "selector",
|
|
5039
|
+
timeout,
|
|
5040
|
+
elapsed: Date.now() - startedAt,
|
|
5041
|
+
message: "Provide one wait predicate: selector, text, url, load, fn, or screenHashChanged."
|
|
5042
|
+
};
|
|
5043
|
+
}
|
|
5044
|
+
|
|
4617
5045
|
// src/commands/snapshot.ts
|
|
4618
5046
|
var snapshotCommand = registerCommand({
|
|
4619
5047
|
name: "snapshot",
|
|
@@ -4623,7 +5051,14 @@ var snapshotCommand = registerCommand({
|
|
|
4623
5051
|
parameters: z21.object({
|
|
4624
5052
|
type: z21.enum(["aria", "text", "dom", "all"]).default("aria").describe("Snapshot type: aria (accessibility tree), text (visible text), dom (element summary), all (combined)"),
|
|
4625
5053
|
selector: z21.string().optional().describe("Scope to a specific element"),
|
|
4626
|
-
depth: z21.number().optional().default(6).describe("Max depth for DOM/aria tree")
|
|
5054
|
+
depth: z21.number().optional().default(6).describe("Max depth for DOM/aria tree"),
|
|
5055
|
+
interactive: z21.boolean().optional().default(false).describe("Return interactive agent refs only"),
|
|
5056
|
+
interactiveOnly: z21.boolean().optional().default(false).describe("Alias for interactive"),
|
|
5057
|
+
i: z21.boolean().optional().default(false).describe("Short alias for interactive"),
|
|
5058
|
+
compact: z21.boolean().optional().default(false).describe("Include compact agent-browser style snapshot text"),
|
|
5059
|
+
c: z21.boolean().optional().default(false).describe("Short alias for compact"),
|
|
5060
|
+
selectors: z21.boolean().optional().default(false).describe("Include ref to CSS selector map"),
|
|
5061
|
+
all: z21.boolean().optional().default(false).describe("Include hidden interactive targets when using interactive snapshot")
|
|
4627
5062
|
}),
|
|
4628
5063
|
result: z21.object({
|
|
4629
5064
|
url: z21.string(),
|
|
@@ -4636,6 +5071,18 @@ var snapshotCommand = registerCommand({
|
|
|
4636
5071
|
const page = ctx.page;
|
|
4637
5072
|
const url = page.url();
|
|
4638
5073
|
const title = await page.title().catch(() => "");
|
|
5074
|
+
if (p.interactive || p.interactiveOnly || p.i || p.compact || p.c || p.selectors) {
|
|
5075
|
+
const observation = await observePage(page, ctx.sessionId, {
|
|
5076
|
+
includeHidden: p.all
|
|
5077
|
+
});
|
|
5078
|
+
if (p.selectors) observation.selectors = buildSelectorMap(observation);
|
|
5079
|
+
if (p.compact || p.c || p.interactive || p.interactiveOnly || p.i) {
|
|
5080
|
+
observation.compact = formatObservationCompact(observation, { selectors: p.selectors });
|
|
5081
|
+
}
|
|
5082
|
+
return ok20(observation, [
|
|
5083
|
+
`refs refreshed for ${observation.targets.length} targets; use click @e1 or fill @e2 "text"`
|
|
5084
|
+
]);
|
|
5085
|
+
}
|
|
4639
5086
|
if (p.type === "aria") {
|
|
4640
5087
|
const aria = await captureAriaSnapshot(page, p.selector, p.depth);
|
|
4641
5088
|
const tips = await buildRefTips(page, aria);
|
|
@@ -4686,10 +5133,10 @@ async function buildRefTips(page, aria) {
|
|
|
4686
5133
|
return [];
|
|
4687
5134
|
}
|
|
4688
5135
|
}
|
|
4689
|
-
async function captureAriaSnapshot(page, selector,
|
|
5136
|
+
async function captureAriaSnapshot(page, selector, _depth) {
|
|
4690
5137
|
try {
|
|
4691
5138
|
const locator = selector ? page.locator(selector).first() : page.locator("body");
|
|
4692
|
-
return await locator.ariaSnapshot(
|
|
5139
|
+
return await locator.ariaSnapshot();
|
|
4693
5140
|
} catch {
|
|
4694
5141
|
try {
|
|
4695
5142
|
return await page.locator("body").ariaSnapshot();
|
|
@@ -4700,7 +5147,7 @@ async function captureAriaSnapshot(page, selector, depth) {
|
|
|
4700
5147
|
}
|
|
4701
5148
|
async function captureTextSnapshot(page, selector) {
|
|
4702
5149
|
if (selector) {
|
|
4703
|
-
return await page.locator(selector).first().innerText(
|
|
5150
|
+
return await page.locator(selector).first().innerText().catch(() => "");
|
|
4704
5151
|
}
|
|
4705
5152
|
return await page.evaluate(() => document.body?.innerText || "").catch(() => "");
|
|
4706
5153
|
}
|
|
@@ -4736,22 +5183,108 @@ async function captureDomSnapshot(page, selector, maxDepth) {
|
|
|
4736
5183
|
).catch(() => ({ tag: "error" }));
|
|
4737
5184
|
}
|
|
4738
5185
|
|
|
4739
|
-
// src/commands/
|
|
5186
|
+
// src/commands/agent.ts
|
|
4740
5187
|
import { z as z22 } from "zod";
|
|
4741
|
-
import { ok as ok21
|
|
4742
|
-
var
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
5188
|
+
import { ok as ok21 } from "@dyyz1993/xcli-core";
|
|
5189
|
+
var observeCommand = registerCommand({
|
|
5190
|
+
name: "observe",
|
|
5191
|
+
description: "Observe the current page as structured agent targets with session refs",
|
|
5192
|
+
scope: "page",
|
|
5193
|
+
parameters: z22.object({
|
|
5194
|
+
includeHidden: z22.boolean().optional().default(false).describe("Include hidden elements in the target list"),
|
|
5195
|
+
limit: z22.number().int().positive().max(300).optional().default(80).describe("Maximum number of targets to return"),
|
|
5196
|
+
compact: z22.boolean().optional().default(false).describe("Include compact agent-browser style snapshot text"),
|
|
5197
|
+
selectors: z22.boolean().optional().default(false).describe("Include ref to stable CSS selector map")
|
|
5198
|
+
}),
|
|
5199
|
+
handler: async (p, ctx) => {
|
|
5200
|
+
const observation = await observePage(ctx.page, ctx.sessionId, {
|
|
5201
|
+
includeHidden: p.includeHidden,
|
|
5202
|
+
limit: p.limit
|
|
5203
|
+
});
|
|
5204
|
+
if (p.selectors) observation.selectors = buildSelectorMap(observation);
|
|
5205
|
+
if (p.compact) observation.compact = formatObservationCompact(observation, { selectors: p.selectors });
|
|
5206
|
+
return ok21(observation, [
|
|
5207
|
+
`refs refreshed for ${observation.targets.length} targets; use act --ref @e1 --action click or click @e1`
|
|
5208
|
+
]);
|
|
5209
|
+
}
|
|
5210
|
+
});
|
|
5211
|
+
var actCommand = registerCommand({
|
|
5212
|
+
name: "act",
|
|
5213
|
+
description: "Perform an agent action using an observe ref or explicit selector",
|
|
5214
|
+
scope: "element",
|
|
5215
|
+
selectorParams: ["selector"],
|
|
5216
|
+
parameters: z22.object({
|
|
5217
|
+
action: z22.enum(["click", "fill", "type", "press", "select", "check", "hover"]).default("click"),
|
|
5218
|
+
ref: z22.string().optional().describe("Session-scoped ref returned by observe, such as e1"),
|
|
5219
|
+
selector: z22.string().optional().describe("CSS selector fallback when no ref is available"),
|
|
5220
|
+
value: z22.string().optional().describe("Value for fill/type/select"),
|
|
5221
|
+
key: z22.string().optional().describe("Key for press"),
|
|
5222
|
+
force: z22.boolean().optional().default(false).describe("Bypass actionability checks"),
|
|
5223
|
+
timeout: z22.number().optional().default(1e4).describe("Playwright action timeout in milliseconds")
|
|
5224
|
+
}).refine((p) => !!p.ref || !!p.selector, {
|
|
5225
|
+
message: "Either ref or selector is required"
|
|
5226
|
+
}),
|
|
5227
|
+
handler: async (p, ctx) => {
|
|
5228
|
+
const result = await actOnPage(ctx.page, ctx.sessionId, { ...p });
|
|
5229
|
+
if (!result.success) {
|
|
5230
|
+
return {
|
|
5231
|
+
success: false,
|
|
5232
|
+
data: result,
|
|
5233
|
+
message: result.message || result.reason || "Action failed",
|
|
5234
|
+
tips: result.stale ? ["run observe again to refresh refs"] : []
|
|
5235
|
+
};
|
|
5236
|
+
}
|
|
5237
|
+
return ok21(result, result.stale ? ["ref screen hash changed; run observe if the next action is uncertain"] : []);
|
|
5238
|
+
}
|
|
5239
|
+
});
|
|
5240
|
+
var waitForCommand = registerCommand({
|
|
5241
|
+
name: "waitFor",
|
|
5242
|
+
description: "Wait for agent predicates such as text, URL, load state, selector state, or screen hash changes",
|
|
5243
|
+
scope: "page",
|
|
5244
|
+
selectorParams: ["selector"],
|
|
5245
|
+
parameters: z22.object({
|
|
5246
|
+
selector: z22.string().optional().describe("CSS selector or observe ref to wait for"),
|
|
5247
|
+
state: z22.enum(["attached", "detached", "visible", "hidden"]).optional().default("visible"),
|
|
5248
|
+
text: z22.string().optional().describe("Visible text to wait for"),
|
|
5249
|
+
url: z22.string().optional().describe("URL substring or glob pattern to wait for"),
|
|
5250
|
+
load: z22.enum(["load", "domcontentloaded", "networkidle"]).optional().describe("Load state to wait for"),
|
|
5251
|
+
fn: z22.string().optional().describe("JavaScript predicate to wait for"),
|
|
5252
|
+
screenHashChanged: z22.string().optional().describe("Previous screenHash from observe"),
|
|
5253
|
+
timeout: z22.number().optional().default(3e4),
|
|
5254
|
+
pollInterval: z22.number().optional().default(200)
|
|
5255
|
+
}).refine((p) => [p.selector, p.text, p.url, p.load, p.fn, p.screenHashChanged].filter(Boolean).length === 1, {
|
|
5256
|
+
message: "Provide exactly one wait predicate: selector, text, url, load, fn, or screenHashChanged"
|
|
5257
|
+
}),
|
|
5258
|
+
handler: async (p, ctx) => {
|
|
5259
|
+
const result = await waitForPage(ctx.page, { ...p });
|
|
5260
|
+
if (!result.success) {
|
|
5261
|
+
return {
|
|
5262
|
+
success: false,
|
|
5263
|
+
data: result,
|
|
5264
|
+
message: result.message || `Timed out waiting for ${result.matched}`,
|
|
5265
|
+
tips: []
|
|
5266
|
+
};
|
|
5267
|
+
}
|
|
5268
|
+
return ok21(result);
|
|
5269
|
+
}
|
|
5270
|
+
});
|
|
5271
|
+
|
|
5272
|
+
// src/commands/tab.ts
|
|
5273
|
+
import { z as z23 } from "zod";
|
|
5274
|
+
import { ok as ok22, fail as fail5 } from "@dyyz1993/xcli-core";
|
|
5275
|
+
var TabParams = z23.object({
|
|
5276
|
+
subcommand: z23.enum(["list", "new", "close", "switch"]),
|
|
5277
|
+
url: z23.string().optional(),
|
|
5278
|
+
index: z23.number().int().min(0).optional()
|
|
4746
5279
|
});
|
|
4747
5280
|
var tabCommand = registerCommand({
|
|
4748
5281
|
name: "tab",
|
|
4749
5282
|
description: "Manage browser tabs: list, new, close, switch",
|
|
4750
5283
|
scope: "page",
|
|
4751
5284
|
parameters: TabParams,
|
|
4752
|
-
result:
|
|
4753
|
-
success:
|
|
4754
|
-
data:
|
|
5285
|
+
result: z23.object({
|
|
5286
|
+
success: z23.boolean(),
|
|
5287
|
+
data: z23.unknown()
|
|
4755
5288
|
}),
|
|
4756
5289
|
handler: async (p, ctx) => {
|
|
4757
5290
|
const pages = ctx.browserContext.pages();
|
|
@@ -4791,7 +5324,7 @@ function handleList(pages, ctx) {
|
|
|
4791
5324
|
active: i === currentIndex
|
|
4792
5325
|
};
|
|
4793
5326
|
});
|
|
4794
|
-
return
|
|
5327
|
+
return ok22({ tabs, total: tabs.length, activeIndex: currentIndex });
|
|
4795
5328
|
}
|
|
4796
5329
|
async function handleNew(p, _pages, ctx) {
|
|
4797
5330
|
const newPage = await ctx.browserContext.newPage();
|
|
@@ -4813,7 +5346,7 @@ async function handleNew(p, _pages, ctx) {
|
|
|
4813
5346
|
const title = await newPage.title().catch(() => "");
|
|
4814
5347
|
const allPages = ctx.browserContext.pages();
|
|
4815
5348
|
const newIndex = allPages.indexOf(newPage);
|
|
4816
|
-
return
|
|
5349
|
+
return ok22({
|
|
4817
5350
|
index: newIndex >= 0 ? newIndex : allPages.length - 1,
|
|
4818
5351
|
url: newPage.url(),
|
|
4819
5352
|
title,
|
|
@@ -4841,7 +5374,7 @@ async function handleClose(p, pages, ctx) {
|
|
|
4841
5374
|
}
|
|
4842
5375
|
ctx.page = newActivePage;
|
|
4843
5376
|
}
|
|
4844
|
-
return
|
|
5377
|
+
return ok22({
|
|
4845
5378
|
closedIndex: closeIndex,
|
|
4846
5379
|
total: remainingPages.length,
|
|
4847
5380
|
activeIndex: isActivePage ? closeIndex < remainingPages.length ? closeIndex : remainingPages.length - 1 : pages.indexOf(ctx.page)
|
|
@@ -4863,7 +5396,7 @@ async function handleSwitch(p, pages, ctx) {
|
|
|
4863
5396
|
}
|
|
4864
5397
|
ctx.page = targetPage;
|
|
4865
5398
|
const title = await targetPage.title().catch(() => "");
|
|
4866
|
-
return
|
|
5399
|
+
return ok22({
|
|
4867
5400
|
index: p.index,
|
|
4868
5401
|
url: targetPage.url(),
|
|
4869
5402
|
title,
|
|
@@ -4872,8 +5405,8 @@ async function handleSwitch(p, pages, ctx) {
|
|
|
4872
5405
|
}
|
|
4873
5406
|
|
|
4874
5407
|
// src/commands/addinitscript.ts
|
|
4875
|
-
import { z as
|
|
4876
|
-
import { ok as
|
|
5408
|
+
import { z as z24 } from "zod";
|
|
5409
|
+
import { ok as ok23 } from "@dyyz1993/xcli-core";
|
|
4877
5410
|
import { readFileSync as readFileSync2 } from "fs";
|
|
4878
5411
|
|
|
4879
5412
|
// src/chain-parser.ts
|
|
@@ -5051,7 +5584,7 @@ function parseCommandArgs(name, args) {
|
|
|
5051
5584
|
} else {
|
|
5052
5585
|
if (positionalIndex < positionalKeys.length) {
|
|
5053
5586
|
const isLast = positionalIndex === positionalKeys.length - 1;
|
|
5054
|
-
if (isLast && name === "eval") {
|
|
5587
|
+
if (isLast && (name === "eval" || name === "find")) {
|
|
5055
5588
|
const remaining = args.slice(i).map(unquote2).join(" ");
|
|
5056
5589
|
params[positionalKeys[positionalIndex]] = remaining;
|
|
5057
5590
|
break;
|
|
@@ -5101,7 +5634,7 @@ var commandDefCache = {
|
|
|
5101
5634
|
frames: { positional: [] },
|
|
5102
5635
|
frame: { positional: ["selector"] },
|
|
5103
5636
|
actions: { positional: ["url"] },
|
|
5104
|
-
find: { positional: ["strategy", "value"] },
|
|
5637
|
+
find: { positional: ["strategy", "value", "operation"] },
|
|
5105
5638
|
addinitscript: { positional: ["script"] },
|
|
5106
5639
|
tab: { positional: ["subcommand"] }
|
|
5107
5640
|
};
|
|
@@ -5113,14 +5646,14 @@ function registerCommandDefinition(name, positional) {
|
|
|
5113
5646
|
}
|
|
5114
5647
|
|
|
5115
5648
|
// src/commands/addinitscript.ts
|
|
5116
|
-
var InitScriptParams =
|
|
5117
|
-
script:
|
|
5118
|
-
file:
|
|
5119
|
-
stdin:
|
|
5120
|
-
name:
|
|
5121
|
-
list:
|
|
5122
|
-
remove:
|
|
5123
|
-
base64:
|
|
5649
|
+
var InitScriptParams = z24.object({
|
|
5650
|
+
script: z24.string().optional(),
|
|
5651
|
+
file: z24.string().optional(),
|
|
5652
|
+
stdin: z24.boolean().optional(),
|
|
5653
|
+
name: z24.string().optional(),
|
|
5654
|
+
list: z24.boolean().optional(),
|
|
5655
|
+
remove: z24.string().optional(),
|
|
5656
|
+
base64: z24.string().optional()
|
|
5124
5657
|
});
|
|
5125
5658
|
var registeredScripts = /* @__PURE__ */ new Map();
|
|
5126
5659
|
function resolveScriptContent(params) {
|
|
@@ -5161,15 +5694,15 @@ var addInitScriptCommand = registerCommand({
|
|
|
5161
5694
|
size: content2.length,
|
|
5162
5695
|
preview: content2.slice(0, 80)
|
|
5163
5696
|
}));
|
|
5164
|
-
return
|
|
5697
|
+
return ok23({ scripts });
|
|
5165
5698
|
}
|
|
5166
5699
|
if (params.remove) {
|
|
5167
5700
|
const existed = registeredScripts.delete(params.remove);
|
|
5168
|
-
return
|
|
5701
|
+
return ok23({ removed: params.remove, existed });
|
|
5169
5702
|
}
|
|
5170
5703
|
let content = params.stdin ? await readStdin() : resolveScriptContent(params);
|
|
5171
5704
|
if (!content) {
|
|
5172
|
-
return
|
|
5705
|
+
return ok23({ error: "No script content provided. Use --script, --file, --stdin, or --base64" });
|
|
5173
5706
|
}
|
|
5174
5707
|
const scriptName = params.name ?? `script-${Date.now()}`;
|
|
5175
5708
|
registeredScripts.set(scriptName, content);
|
|
@@ -5177,74 +5710,134 @@ var addInitScriptCommand = registerCommand({
|
|
|
5177
5710
|
try {
|
|
5178
5711
|
await ctx.page.evaluate(content);
|
|
5179
5712
|
} catch {
|
|
5180
|
-
return
|
|
5713
|
+
return ok23({
|
|
5181
5714
|
registered: scriptName,
|
|
5182
5715
|
hint: "Script registered for future page loads; immediate execution skipped (page may not be ready)"
|
|
5183
5716
|
});
|
|
5184
5717
|
}
|
|
5185
|
-
return
|
|
5718
|
+
return ok23({ registered: scriptName, executedImmediately: true });
|
|
5186
5719
|
}
|
|
5187
5720
|
});
|
|
5188
5721
|
registerCommandDefinition("addinitscript", ["script"]);
|
|
5189
5722
|
|
|
5190
5723
|
// src/commands/find.ts
|
|
5191
|
-
import { z as
|
|
5192
|
-
import { ok as
|
|
5724
|
+
import { z as z25 } from "zod";
|
|
5725
|
+
import { ok as ok24, fail as fail6 } from "@dyyz1993/xcli-core";
|
|
5726
|
+
var actionSchema2 = z25.enum(["click", "fill", "type", "select", "hover", "check"]);
|
|
5193
5727
|
var findCommand = registerCommand({
|
|
5194
5728
|
name: "find",
|
|
5195
5729
|
description: "Find elements by semantic strategy (text/role/label/placeholder/testid) and optionally perform an action",
|
|
5196
5730
|
scope: "page",
|
|
5197
|
-
parameters:
|
|
5198
|
-
strategy:
|
|
5199
|
-
value:
|
|
5200
|
-
name:
|
|
5201
|
-
exact:
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5731
|
+
parameters: z25.object({
|
|
5732
|
+
strategy: z25.enum(["text", "role", "label", "placeholder", "testid", "alt", "title", "first", "last", "nth"]),
|
|
5733
|
+
value: z25.string(),
|
|
5734
|
+
name: z25.string().optional(),
|
|
5735
|
+
exact: z25.boolean().optional().default(false),
|
|
5736
|
+
operation: z25.string().optional().describe('Trailing operation syntax, e.g. click, fill "text", type "text"'),
|
|
5737
|
+
action: actionSchema2.optional().describe("Action to perform when not using trailing operation syntax"),
|
|
5738
|
+
actionValue: z25.string().optional().describe("Value for fill/type/select when using action"),
|
|
5739
|
+
index: z25.number().int().optional().describe("Index for nth strategy"),
|
|
5740
|
+
click: z25.boolean().optional().default(false),
|
|
5741
|
+
fill: z25.string().optional(),
|
|
5742
|
+
type: z25.string().optional(),
|
|
5743
|
+
select: z25.string().optional(),
|
|
5744
|
+
hover: z25.boolean().optional().default(false),
|
|
5745
|
+
check: z25.boolean().optional().default(false),
|
|
5746
|
+
timeout: z25.number().optional().default(1e4)
|
|
5207
5747
|
}),
|
|
5208
|
-
result:
|
|
5209
|
-
matched:
|
|
5210
|
-
selector:
|
|
5211
|
-
action:
|
|
5748
|
+
result: z25.object({
|
|
5749
|
+
matched: z25.number(),
|
|
5750
|
+
selector: z25.string(),
|
|
5751
|
+
action: z25.string().optional()
|
|
5212
5752
|
}),
|
|
5213
5753
|
handler: async (p, ctx) => {
|
|
5214
5754
|
const page = ctx.page;
|
|
5215
|
-
const
|
|
5755
|
+
const normalized = normalizeFindParams({ ...p });
|
|
5756
|
+
const parsedOperation = parseOperation(normalized.operation);
|
|
5757
|
+
const actionName = parsedOperation.action || p.action || inferLegacyAction(p);
|
|
5758
|
+
const actionValue = parsedOperation.value ?? p.actionValue ?? p.fill ?? p.type ?? p.select;
|
|
5759
|
+
const locator = buildLocator(page, normalized.strategy, normalized.value, {
|
|
5216
5760
|
name: p.name,
|
|
5217
|
-
exact: p.exact
|
|
5761
|
+
exact: p.exact,
|
|
5762
|
+
index: normalized.index
|
|
5218
5763
|
});
|
|
5219
5764
|
const count = await locator.count();
|
|
5220
5765
|
if (count === 0) {
|
|
5221
5766
|
return fail6(`No element found with ${p.strategy}="${p.value}"`);
|
|
5222
5767
|
}
|
|
5223
5768
|
const tips = [];
|
|
5224
|
-
const target = locator.
|
|
5769
|
+
const target = selectTarget(locator, p.strategy);
|
|
5225
5770
|
if (count > 1) {
|
|
5226
|
-
tips.push(`\u26A0\uFE0F Matched ${count} elements,
|
|
5771
|
+
tips.push(`\u26A0\uFE0F Matched ${count} elements, used first match. Use 'find nth <index> ${normalized.strategy} "${normalized.value}" ${actionName || "click"}' for a specific match.`);
|
|
5227
5772
|
}
|
|
5228
|
-
const selector = describeSelector(
|
|
5229
|
-
|
|
5230
|
-
if (p.click) {
|
|
5773
|
+
const selector = describeSelector(normalized.strategy, normalized.value, p.name);
|
|
5774
|
+
if (actionName === "click") {
|
|
5231
5775
|
await target.click({ timeout: p.timeout, force: true });
|
|
5232
|
-
action
|
|
5233
|
-
} else if (
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5776
|
+
return okWithTips({ matched: count, selector, action: "click" }, tips);
|
|
5777
|
+
} else if (actionName === "fill") {
|
|
5778
|
+
if (actionValue === void 0) return fail6("find fill requires a value");
|
|
5779
|
+
await target.fill(actionValue, { timeout: p.timeout, force: true });
|
|
5780
|
+
return okWithTips({ matched: count, selector, action: `fill("${actionValue}")` }, tips);
|
|
5781
|
+
} else if (actionName === "type") {
|
|
5782
|
+
if (actionValue === void 0) return fail6("find type requires a value");
|
|
5783
|
+
await target.type(actionValue, { delay: 10, timeout: p.timeout });
|
|
5784
|
+
return okWithTips({ matched: count, selector, action: `type("${actionValue}")` }, tips);
|
|
5785
|
+
} else if (actionName === "select") {
|
|
5786
|
+
if (actionValue === void 0) return fail6("find select requires a value");
|
|
5787
|
+
await target.selectOption(actionValue);
|
|
5788
|
+
return okWithTips({ matched: count, selector, action: `select("${actionValue}")` }, tips);
|
|
5789
|
+
} else if (actionName === "hover") {
|
|
5790
|
+
await target.hover({ timeout: p.timeout, force: true });
|
|
5791
|
+
return okWithTips({ matched: count, selector, action: "hover" }, tips);
|
|
5792
|
+
} else if (actionName === "check") {
|
|
5793
|
+
await target.check({ timeout: p.timeout });
|
|
5794
|
+
return okWithTips({ matched: count, selector, action: "check" }, tips);
|
|
5795
|
+
}
|
|
5796
|
+
return okWithTips({ matched: count, selector }, tips);
|
|
5246
5797
|
}
|
|
5247
5798
|
});
|
|
5799
|
+
function okWithTips(data, tips) {
|
|
5800
|
+
const result = ok24(data);
|
|
5801
|
+
if (tips.length > 0) result.tips = tips;
|
|
5802
|
+
return result;
|
|
5803
|
+
}
|
|
5804
|
+
function parseOperation(operation) {
|
|
5805
|
+
if (!operation) return {};
|
|
5806
|
+
const match = operation.trim().match(/^(\S+)(?:\s+([\s\S]+))?$/);
|
|
5807
|
+
if (!match) return {};
|
|
5808
|
+
const maybeAction = match[1];
|
|
5809
|
+
const parsed = actionSchema2.safeParse(maybeAction);
|
|
5810
|
+
if (!parsed.success) return {};
|
|
5811
|
+
const rawValue = match[2];
|
|
5812
|
+
const value = rawValue?.replace(/^["']|["']$/g, "");
|
|
5813
|
+
return { action: parsed.data, ...value !== void 0 ? { value } : {} };
|
|
5814
|
+
}
|
|
5815
|
+
function normalizeFindParams(p) {
|
|
5816
|
+
if (p.strategy !== "nth") return { strategy: p.strategy, value: p.value, operation: p.operation, index: p.index };
|
|
5817
|
+
const parsedIndex = Number(p.value);
|
|
5818
|
+
if (!Number.isInteger(parsedIndex) || !p.operation) {
|
|
5819
|
+
return { strategy: p.strategy, value: p.value, operation: p.operation, index: p.index };
|
|
5820
|
+
}
|
|
5821
|
+
const match = p.operation.trim().match(/^(\S+)(?:\s+([\s\S]+))?$/);
|
|
5822
|
+
if (!match) {
|
|
5823
|
+
return { strategy: p.strategy, value: p.value, operation: p.operation, index: parsedIndex };
|
|
5824
|
+
}
|
|
5825
|
+
return {
|
|
5826
|
+
strategy: p.strategy,
|
|
5827
|
+
value: match[1].replace(/^["']|["']$/g, ""),
|
|
5828
|
+
...match[2] ? { operation: match[2] } : {},
|
|
5829
|
+
index: parsedIndex
|
|
5830
|
+
};
|
|
5831
|
+
}
|
|
5832
|
+
function inferLegacyAction(p) {
|
|
5833
|
+
if (p.click) return "click";
|
|
5834
|
+
if (p.fill !== void 0) return "fill";
|
|
5835
|
+
if (p.type !== void 0) return "type";
|
|
5836
|
+
if (p.select !== void 0) return "select";
|
|
5837
|
+
if (p.hover) return "hover";
|
|
5838
|
+
if (p.check) return "check";
|
|
5839
|
+
return void 0;
|
|
5840
|
+
}
|
|
5248
5841
|
function buildLocator(page, strategy, value, opts) {
|
|
5249
5842
|
switch (strategy) {
|
|
5250
5843
|
case "text":
|
|
@@ -5257,10 +5850,24 @@ function buildLocator(page, strategy, value, opts) {
|
|
|
5257
5850
|
return page.getByPlaceholder(value, { exact: opts.exact });
|
|
5258
5851
|
case "testid":
|
|
5259
5852
|
return page.getByTestId(value);
|
|
5853
|
+
case "alt":
|
|
5854
|
+
return page.getByAltText(value, { exact: opts.exact });
|
|
5855
|
+
case "title":
|
|
5856
|
+
return page.getByTitle(value, { exact: opts.exact });
|
|
5857
|
+
case "first":
|
|
5858
|
+
return page.locator(value).first();
|
|
5859
|
+
case "last":
|
|
5860
|
+
return page.locator(value).last();
|
|
5861
|
+
case "nth":
|
|
5862
|
+
return page.locator(value).nth(opts.index ?? 0);
|
|
5260
5863
|
default:
|
|
5261
5864
|
return page.getByText(value, { exact: opts.exact });
|
|
5262
5865
|
}
|
|
5263
5866
|
}
|
|
5867
|
+
function selectTarget(locator, strategy) {
|
|
5868
|
+
if (strategy === "first" || strategy === "last" || strategy === "nth") return locator;
|
|
5869
|
+
return locator.first();
|
|
5870
|
+
}
|
|
5264
5871
|
function describeSelector(strategy, value, name) {
|
|
5265
5872
|
switch (strategy) {
|
|
5266
5873
|
case "role":
|
|
@@ -5273,6 +5880,16 @@ function describeSelector(strategy, value, name) {
|
|
|
5273
5880
|
return `getByPlaceholder("${value}")`;
|
|
5274
5881
|
case "testid":
|
|
5275
5882
|
return `getByTestId("${value}")`;
|
|
5883
|
+
case "alt":
|
|
5884
|
+
return `getByAltText("${value}")`;
|
|
5885
|
+
case "title":
|
|
5886
|
+
return `getByTitle("${value}")`;
|
|
5887
|
+
case "first":
|
|
5888
|
+
return `first("${value}")`;
|
|
5889
|
+
case "last":
|
|
5890
|
+
return `last("${value}")`;
|
|
5891
|
+
case "nth":
|
|
5892
|
+
return `nth("${value}")`;
|
|
5276
5893
|
default:
|
|
5277
5894
|
return `${strategy}("${value}")`;
|
|
5278
5895
|
}
|
|
@@ -5395,7 +6012,7 @@ async function detectCaptcha2(page) {
|
|
|
5395
6012
|
}
|
|
5396
6013
|
async function detectWarningText(page) {
|
|
5397
6014
|
try {
|
|
5398
|
-
const pageText = await page.textContent("body"
|
|
6015
|
+
const pageText = await page.textContent("body").catch(() => "") || "";
|
|
5399
6016
|
const lowerText = pageText.toLowerCase();
|
|
5400
6017
|
for (const { text, severity } of WARNING_TEXTS) {
|
|
5401
6018
|
if (lowerText.includes(text.toLowerCase())) {
|
|
@@ -5526,8 +6143,8 @@ function formatDetectionMessage(result) {
|
|
|
5526
6143
|
}
|
|
5527
6144
|
|
|
5528
6145
|
// src/commands/promo.ts
|
|
5529
|
-
import { z as
|
|
5530
|
-
import { ok as
|
|
6146
|
+
import { z as z26 } from "zod";
|
|
6147
|
+
import { ok as ok25 } from "@dyyz1993/xcli-core";
|
|
5531
6148
|
import { existsSync as existsSync2, readFileSync as readFileSync8 } from "fs";
|
|
5532
6149
|
import { resolve as resolve6 } from "path";
|
|
5533
6150
|
|
|
@@ -5830,14 +6447,14 @@ async function dispatchPromo(config) {
|
|
|
5830
6447
|
}
|
|
5831
6448
|
|
|
5832
6449
|
// src/commands/promo.ts
|
|
5833
|
-
var promoParams =
|
|
5834
|
-
platform:
|
|
5835
|
-
file:
|
|
5836
|
-
tags:
|
|
5837
|
-
title:
|
|
5838
|
-
search:
|
|
5839
|
-
cdpEndpoint:
|
|
5840
|
-
session:
|
|
6450
|
+
var promoParams = z26.object({
|
|
6451
|
+
platform: z26.enum(["devto", "medium", "csdn", "juejin", "quora"]).describe("Target platform for promotion"),
|
|
6452
|
+
file: z26.string().describe("Path to Markdown file to publish"),
|
|
6453
|
+
tags: z26.string().optional().describe("Comma-separated tags"),
|
|
6454
|
+
title: z26.string().optional().describe("Custom title (default: extracted from file first heading)"),
|
|
6455
|
+
search: z26.string().optional().describe("Quora: search query to find questions"),
|
|
6456
|
+
cdpEndpoint: z26.string().optional().describe("CDP endpoint for agent-browser"),
|
|
6457
|
+
session: z26.string().optional().describe("agent-browser session name")
|
|
5841
6458
|
}).refine(
|
|
5842
6459
|
(data) => data.platform !== "quora" || !!data.search,
|
|
5843
6460
|
{ message: "Quora platform requires --search parameter" }
|
|
@@ -5850,17 +6467,17 @@ var promoCommand = registerCommand({
|
|
|
5850
6467
|
description: "Publish promotional articles to various platforms (devto, medium, csdn, juejin, quora)",
|
|
5851
6468
|
scope: "project",
|
|
5852
6469
|
parameters: promoParams,
|
|
5853
|
-
result:
|
|
5854
|
-
success:
|
|
5855
|
-
url:
|
|
5856
|
-
error:
|
|
5857
|
-
platform:
|
|
6470
|
+
result: z26.object({
|
|
6471
|
+
success: z26.boolean(),
|
|
6472
|
+
url: z26.string().optional(),
|
|
6473
|
+
error: z26.string().optional(),
|
|
6474
|
+
platform: z26.string()
|
|
5858
6475
|
}),
|
|
5859
6476
|
handler: async (p, _ctx) => {
|
|
5860
6477
|
const filePath = resolve6(p.file);
|
|
5861
6478
|
const content = readFileSync8(filePath, "utf-8");
|
|
5862
6479
|
if (content.trim().length === 0) {
|
|
5863
|
-
return
|
|
6480
|
+
return ok25({
|
|
5864
6481
|
success: false,
|
|
5865
6482
|
error: `File is empty: ${filePath}`,
|
|
5866
6483
|
platform: p.platform
|
|
@@ -5875,7 +6492,7 @@ var promoCommand = registerCommand({
|
|
|
5875
6492
|
cdpEndpoint: p.cdpEndpoint ?? _ctx.cdpEndpoint,
|
|
5876
6493
|
session: p.session ?? p.platform
|
|
5877
6494
|
});
|
|
5878
|
-
return
|
|
6495
|
+
return ok25(result);
|
|
5879
6496
|
}
|
|
5880
6497
|
});
|
|
5881
6498
|
|
|
@@ -6002,6 +6619,192 @@ function ensurePluginDependencies(pluginsDir) {
|
|
|
6002
6619
|
}
|
|
6003
6620
|
}
|
|
6004
6621
|
|
|
6622
|
+
// src/plugin/contract.ts
|
|
6623
|
+
function buildPluginContract(site) {
|
|
6624
|
+
const commands = site.getAllCommands().map((command) => buildCommandContract(site.getCommand?.(command.name) || command));
|
|
6625
|
+
return {
|
|
6626
|
+
version: 2,
|
|
6627
|
+
plugin: {
|
|
6628
|
+
name: site.name,
|
|
6629
|
+
url: site.url,
|
|
6630
|
+
description: site.config?.description,
|
|
6631
|
+
requiresLogin: site.config?.requiresLogin
|
|
6632
|
+
},
|
|
6633
|
+
commands
|
|
6634
|
+
};
|
|
6635
|
+
}
|
|
6636
|
+
function buildCommandContract(command) {
|
|
6637
|
+
const extension = command.xbrowser || {};
|
|
6638
|
+
const inferredFields = fieldsFromZodObject(command.parameters);
|
|
6639
|
+
const fields = mergeFields(inferredFields, extension.form?.fields || []);
|
|
6640
|
+
const positional = extension.positional || fields.filter((field) => field.positional).map((field) => field.name);
|
|
6641
|
+
const capabilities = extension.capabilities || inferCapabilities(command.scope || "project", command.requiresLogin);
|
|
6642
|
+
const outputSchema = command.result ? summarizeZod(command.result) : void 0;
|
|
6643
|
+
return {
|
|
6644
|
+
name: command.name,
|
|
6645
|
+
description: command.description || "",
|
|
6646
|
+
scope: command.scope || "project",
|
|
6647
|
+
requiresLogin: command.requiresLogin === true,
|
|
6648
|
+
category: extension.category,
|
|
6649
|
+
capabilities,
|
|
6650
|
+
positional,
|
|
6651
|
+
form: {
|
|
6652
|
+
title: extension.form?.title || command.description || command.name,
|
|
6653
|
+
description: extension.form?.description,
|
|
6654
|
+
submitLabel: extension.form?.submitLabel || "Run",
|
|
6655
|
+
fields
|
|
6656
|
+
},
|
|
6657
|
+
output: extension.output || (outputSchema ? { schema: outputSchema } : void 0)
|
|
6658
|
+
};
|
|
6659
|
+
}
|
|
6660
|
+
function fieldsFromZodObject(schema) {
|
|
6661
|
+
const shape = getShape(schema);
|
|
6662
|
+
if (!shape) return [];
|
|
6663
|
+
return Object.entries(shape).map(([name, field]) => fieldFromZod(name, field));
|
|
6664
|
+
}
|
|
6665
|
+
function fieldFromZod(name, schema) {
|
|
6666
|
+
const unwrapped = unwrapZod(schema);
|
|
6667
|
+
const type = zodTypeToContractType(unwrapped.typeName);
|
|
6668
|
+
const enumValues = extractEnumValues(unwrapped.schema);
|
|
6669
|
+
return {
|
|
6670
|
+
name,
|
|
6671
|
+
label: toLabel(name),
|
|
6672
|
+
type,
|
|
6673
|
+
widget: widgetFor(type, enumValues, unwrapped.schema),
|
|
6674
|
+
required: !unwrapped.optional,
|
|
6675
|
+
...unwrapped.description ? { description: unwrapped.description } : {},
|
|
6676
|
+
...unwrapped.defaultValue !== void 0 ? { default: unwrapped.defaultValue } : {},
|
|
6677
|
+
...enumValues ? { enum: enumValues } : {},
|
|
6678
|
+
...type === "array" ? { multiple: true } : {}
|
|
6679
|
+
};
|
|
6680
|
+
}
|
|
6681
|
+
function mergeFields(inferred, overrides) {
|
|
6682
|
+
if (overrides.length === 0) return inferred;
|
|
6683
|
+
const byName = new Map(inferred.map((field) => [field.name, field]));
|
|
6684
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6685
|
+
const merged = [];
|
|
6686
|
+
for (const override of overrides) {
|
|
6687
|
+
if (!override.name) continue;
|
|
6688
|
+
const base = byName.get(override.name) || {
|
|
6689
|
+
name: override.name,
|
|
6690
|
+
label: toLabel(override.name),
|
|
6691
|
+
type: "string",
|
|
6692
|
+
widget: "text",
|
|
6693
|
+
required: false
|
|
6694
|
+
};
|
|
6695
|
+
merged.push({ ...base, ...override, name: override.name });
|
|
6696
|
+
seen.add(override.name);
|
|
6697
|
+
}
|
|
6698
|
+
for (const field of inferred) {
|
|
6699
|
+
if (!seen.has(field.name)) merged.push(field);
|
|
6700
|
+
}
|
|
6701
|
+
return merged;
|
|
6702
|
+
}
|
|
6703
|
+
function inferCapabilities(scope, requiresLogin) {
|
|
6704
|
+
const caps = [];
|
|
6705
|
+
if (scope === "page") caps.push("browser.page");
|
|
6706
|
+
if (scope === "browser") caps.push("browser.context");
|
|
6707
|
+
if (requiresLogin) caps.push("auth.login");
|
|
6708
|
+
return caps;
|
|
6709
|
+
}
|
|
6710
|
+
function getShape(schema) {
|
|
6711
|
+
const zod = schema;
|
|
6712
|
+
const shapeOrFn = zod?.shape ?? zod?._def?.shape;
|
|
6713
|
+
if (!shapeOrFn) return void 0;
|
|
6714
|
+
return typeof shapeOrFn === "function" ? shapeOrFn() : shapeOrFn;
|
|
6715
|
+
}
|
|
6716
|
+
function unwrapZod(schema) {
|
|
6717
|
+
let current = schema;
|
|
6718
|
+
let optional = typeof current?.isOptional === "function" ? current.isOptional() : false;
|
|
6719
|
+
let description = current?._def?.description;
|
|
6720
|
+
let defaultValue;
|
|
6721
|
+
for (let i = 0; i < 8; i++) {
|
|
6722
|
+
const def = current?._def;
|
|
6723
|
+
const typeName = def?.typeName || "unknown";
|
|
6724
|
+
if (def?.description) description = def.description;
|
|
6725
|
+
if (!def) return { schema: current, typeName, optional, description, defaultValue };
|
|
6726
|
+
if (typeName === "ZodDefault") {
|
|
6727
|
+
optional = true;
|
|
6728
|
+
defaultValue = typeof def.defaultValue === "function" ? def.defaultValue() : def.defaultValue;
|
|
6729
|
+
current = def.innerType || def.type;
|
|
6730
|
+
continue;
|
|
6731
|
+
}
|
|
6732
|
+
if (typeName === "ZodOptional" || typeName === "ZodNullable") {
|
|
6733
|
+
optional = true;
|
|
6734
|
+
current = def.innerType || def.type;
|
|
6735
|
+
continue;
|
|
6736
|
+
}
|
|
6737
|
+
return { schema: current, typeName, optional, description, defaultValue };
|
|
6738
|
+
}
|
|
6739
|
+
return { schema: current, typeName: current?._def?.typeName || "unknown", optional, description, defaultValue };
|
|
6740
|
+
}
|
|
6741
|
+
function zodTypeToContractType(typeName) {
|
|
6742
|
+
switch (typeName) {
|
|
6743
|
+
case "ZodString":
|
|
6744
|
+
return "string";
|
|
6745
|
+
case "ZodNumber":
|
|
6746
|
+
return "number";
|
|
6747
|
+
case "ZodBoolean":
|
|
6748
|
+
return "boolean";
|
|
6749
|
+
case "ZodEnum":
|
|
6750
|
+
case "ZodNativeEnum":
|
|
6751
|
+
return "enum";
|
|
6752
|
+
case "ZodArray":
|
|
6753
|
+
return "array";
|
|
6754
|
+
case "ZodObject":
|
|
6755
|
+
return "object";
|
|
6756
|
+
default:
|
|
6757
|
+
return typeName.replace(/^Zod/, "").toLowerCase() || "unknown";
|
|
6758
|
+
}
|
|
6759
|
+
}
|
|
6760
|
+
function widgetFor(type, enumValues, schema) {
|
|
6761
|
+
if (enumValues) return "select";
|
|
6762
|
+
if (type === "boolean") return "checkbox";
|
|
6763
|
+
if (type === "number") return "number";
|
|
6764
|
+
if (type === "array") return "multi-select";
|
|
6765
|
+
if (type === "object") return "json";
|
|
6766
|
+
const checks = schema?._def?.checks;
|
|
6767
|
+
if (checks?.some((check) => check.kind === "url")) return "url";
|
|
6768
|
+
return "text";
|
|
6769
|
+
}
|
|
6770
|
+
function extractEnumValues(schema) {
|
|
6771
|
+
const def = schema?._def;
|
|
6772
|
+
const values = def?.values;
|
|
6773
|
+
if (Array.isArray(values)) return values.map(String);
|
|
6774
|
+
return void 0;
|
|
6775
|
+
}
|
|
6776
|
+
function summarizeZod(schema) {
|
|
6777
|
+
const unwrapped = unwrapZod(schema);
|
|
6778
|
+
if (unwrapped.typeName === "ZodArray") {
|
|
6779
|
+
const def = unwrapped.schema?._def;
|
|
6780
|
+
return {
|
|
6781
|
+
type: "array",
|
|
6782
|
+
items: summarizeZod(def?.type || def?.innerType)
|
|
6783
|
+
};
|
|
6784
|
+
}
|
|
6785
|
+
const shape = getShape(schema);
|
|
6786
|
+
if (!shape) {
|
|
6787
|
+
return {
|
|
6788
|
+
type: zodTypeToContractType(unwrapped.typeName),
|
|
6789
|
+
required: !unwrapped.optional,
|
|
6790
|
+
...unwrapped.description ? { description: unwrapped.description } : {}
|
|
6791
|
+
};
|
|
6792
|
+
}
|
|
6793
|
+
return Object.fromEntries(
|
|
6794
|
+
Object.entries(shape).map(([name, field]) => {
|
|
6795
|
+
const unwrapped2 = unwrapZod(field);
|
|
6796
|
+
return [name, {
|
|
6797
|
+
type: zodTypeToContractType(unwrapped2.typeName),
|
|
6798
|
+
required: !unwrapped2.optional,
|
|
6799
|
+
...unwrapped2.description ? { description: unwrapped2.description } : {}
|
|
6800
|
+
}];
|
|
6801
|
+
})
|
|
6802
|
+
);
|
|
6803
|
+
}
|
|
6804
|
+
function toLabel(name) {
|
|
6805
|
+
return name.replace(/([A-Z])/g, " $1").replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim().replace(/^./, (char) => char.toUpperCase());
|
|
6806
|
+
}
|
|
6807
|
+
|
|
6005
6808
|
// src/plugin/loader.ts
|
|
6006
6809
|
var DEFAULT_PLUGIN_DIRS = [".xcli/plugins", "../.xcli/plugins"];
|
|
6007
6810
|
var XBrowserPluginLoader = class {
|
|
@@ -6044,6 +6847,13 @@ var XBrowserPluginLoader = class {
|
|
|
6044
6847
|
getLoadedPlugins() {
|
|
6045
6848
|
return this.loader.getLoadedPlugins();
|
|
6046
6849
|
}
|
|
6850
|
+
getPluginContract(siteName, commandName) {
|
|
6851
|
+
const site = this.core.loader.getSite(siteName);
|
|
6852
|
+
if (!site) return void 0;
|
|
6853
|
+
const contract = buildPluginContract(site);
|
|
6854
|
+
if (!commandName) return contract;
|
|
6855
|
+
return contract.commands.find((command) => command.name === commandName);
|
|
6856
|
+
}
|
|
6047
6857
|
async loadPlugin(pluginPath, id) {
|
|
6048
6858
|
return this.loader.loadPlugin(pluginPath, id);
|
|
6049
6859
|
}
|
|
@@ -6617,7 +7427,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6617
7427
|
}
|
|
6618
7428
|
let targetPageOverride = null;
|
|
6619
7429
|
if (_target && extraOpts?.cdpEndpoint) {
|
|
6620
|
-
const { findTargetPage } = await import("./browser-
|
|
7430
|
+
const { findTargetPage } = await import("./browser-53KUFEEM.js");
|
|
6621
7431
|
targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
|
|
6622
7432
|
if (!targetPageOverride) {
|
|
6623
7433
|
return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
|
|
@@ -6634,7 +7444,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6634
7444
|
params = result.data;
|
|
6635
7445
|
}
|
|
6636
7446
|
if (command.scope !== "cli" && !process.env.XBROWSER_DAEMON_WORKER) {
|
|
6637
|
-
const { forwardExec } = await import("./daemon-client-
|
|
7447
|
+
const { forwardExec } = await import("./daemon-client-YAVQ343A.js");
|
|
6638
7448
|
const result = await forwardExec(commandName, params, sessionName, extraOpts?.cdpEndpoint);
|
|
6639
7449
|
if (result) return result;
|
|
6640
7450
|
}
|
|
@@ -6642,7 +7452,18 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6642
7452
|
const existing = await findOrRestoreSession(sessionName, extraOpts?.cdpEndpoint);
|
|
6643
7453
|
if (existing) {
|
|
6644
7454
|
session = existing;
|
|
6645
|
-
if (
|
|
7455
|
+
if (session.page) {
|
|
7456
|
+
try {
|
|
7457
|
+
await Promise.race([
|
|
7458
|
+
session.page.evaluate(() => true),
|
|
7459
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 3e3))
|
|
7460
|
+
]);
|
|
7461
|
+
} catch {
|
|
7462
|
+
await closeSessionByName(session.name);
|
|
7463
|
+
session = void 0;
|
|
7464
|
+
}
|
|
7465
|
+
}
|
|
7466
|
+
if (session && targetPageOverride && session.page) {
|
|
6646
7467
|
const currentUrl = session.page.url();
|
|
6647
7468
|
if (currentUrl !== targetPageOverride.url) {
|
|
6648
7469
|
await session.page.goto(targetPageOverride.url, { waitUntil: "domcontentloaded", timeout: 15e3 }).catch(() => {
|
|
@@ -6667,7 +7488,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6667
7488
|
browser: session?.context.browser(),
|
|
6668
7489
|
browserContext: session?.context,
|
|
6669
7490
|
sessionId: session?.id,
|
|
6670
|
-
cdpEndpoint:
|
|
7491
|
+
cdpEndpoint: session?.cdpEndpoint || extraOpts?.cdpEndpoint,
|
|
6671
7492
|
args: [],
|
|
6672
7493
|
options: {},
|
|
6673
7494
|
cwd: process.cwd(),
|
|
@@ -6702,7 +7523,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6702
7523
|
let refTips = [];
|
|
6703
7524
|
if (session?.page && command.selectorParams && command.selectorParams.length > 0) {
|
|
6704
7525
|
const cache = /* @__PURE__ */ new Map();
|
|
6705
|
-
const resolved = await resolveRefParams(session.page, params, command.selectorParams, cache);
|
|
7526
|
+
const resolved = await resolveRefParams(session.page, params, command.selectorParams, cache, session.id);
|
|
6706
7527
|
if (resolved.tips.length > 0) {
|
|
6707
7528
|
refTips = resolved.tips;
|
|
6708
7529
|
params = resolved.params;
|
|
@@ -6772,7 +7593,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6772
7593
|
timestamp: start
|
|
6773
7594
|
});
|
|
6774
7595
|
if (isSuccess) {
|
|
6775
|
-
return { ...
|
|
7596
|
+
return { ...ok26(raw.data, merged.length > 0 ? merged : raw.tips), duration, ...hookOutputs ? { hookOutputs } : {} };
|
|
6776
7597
|
}
|
|
6777
7598
|
return { success: false, data: raw.data, message: raw.message, tips: merged.length > 0 ? merged : raw.tips || [], duration, ...hookOutputs ? { hookOutputs } : {} };
|
|
6778
7599
|
}
|
|
@@ -6785,7 +7606,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6785
7606
|
duration,
|
|
6786
7607
|
timestamp: start
|
|
6787
7608
|
});
|
|
6788
|
-
return { ...
|
|
7609
|
+
return { ...ok26(raw, smartTips), duration, ...hookOutputs ? { hookOutputs } : {} };
|
|
6789
7610
|
} catch (err) {
|
|
6790
7611
|
const end = Date.now();
|
|
6791
7612
|
const duration = end - start;
|
|
@@ -6933,7 +7754,7 @@ async function executeChain(input, options) {
|
|
|
6933
7754
|
results.push({
|
|
6934
7755
|
command: `${cmdName} ${subCommand}`,
|
|
6935
7756
|
raw: cmdStr,
|
|
6936
|
-
...
|
|
7757
|
+
...ok26(data),
|
|
6937
7758
|
duration: duration2,
|
|
6938
7759
|
...hookOutputs ? { hookOutputs } : {}
|
|
6939
7760
|
});
|
|
@@ -7208,13 +8029,13 @@ var sessionListBuiltin = {
|
|
|
7208
8029
|
},
|
|
7209
8030
|
execute: async () => {
|
|
7210
8031
|
try {
|
|
7211
|
-
const
|
|
7212
|
-
if (
|
|
8032
|
+
const sessions2 = await listSessions();
|
|
8033
|
+
if (sessions2.length === 0) {
|
|
7213
8034
|
console.log("No active sessions");
|
|
7214
8035
|
return;
|
|
7215
8036
|
}
|
|
7216
8037
|
console.log("Active sessions:");
|
|
7217
|
-
for (const s of
|
|
8038
|
+
for (const s of sessions2) {
|
|
7218
8039
|
console.log(` ${s.name} (${s.id})`);
|
|
7219
8040
|
}
|
|
7220
8041
|
} catch (e) {
|
|
@@ -8040,6 +8861,7 @@ function handlePluginHelp() {
|
|
|
8040
8861
|
" install <slug> --from-marketplace Install from marketplace",
|
|
8041
8862
|
" uninstall <name> Uninstall a plugin",
|
|
8042
8863
|
" list [--json] List installed plugins",
|
|
8864
|
+
" schema <name> [command] [--json] Show plugin contract and command forms",
|
|
8043
8865
|
" reload <name> Reload a plugin",
|
|
8044
8866
|
"",
|
|
8045
8867
|
"Examples:",
|
|
@@ -8048,6 +8870,7 @@ function handlePluginHelp() {
|
|
|
8048
8870
|
" xbrowser plugin install ./my-plugin",
|
|
8049
8871
|
" xbrowser plugin uninstall my-plugin",
|
|
8050
8872
|
" xbrowser plugin list",
|
|
8873
|
+
" xbrowser plugin schema my-plugin --json",
|
|
8051
8874
|
" xbrowser plugin reload my-plugin"
|
|
8052
8875
|
].join("\n");
|
|
8053
8876
|
}
|
|
@@ -9110,6 +9933,25 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
|
|
|
9110
9933
|
cmdName = "text";
|
|
9111
9934
|
params = { selector: options.selector || options.s };
|
|
9112
9935
|
break;
|
|
9936
|
+
case "find": {
|
|
9937
|
+
if (!args[0] || !args[1]) {
|
|
9938
|
+
outputError("Usage: xbrowser find <text|role|label|placeholder|testid|alt|title|first|last|nth> <value> [action] [--name <name>]");
|
|
9939
|
+
}
|
|
9940
|
+
const strategy = args[0];
|
|
9941
|
+
const value = args[1];
|
|
9942
|
+
const operation = args.slice(2).join(" ") || void 0;
|
|
9943
|
+
cmdName = "find";
|
|
9944
|
+
params = {
|
|
9945
|
+
strategy,
|
|
9946
|
+
value,
|
|
9947
|
+
...operation ? { operation } : {},
|
|
9948
|
+
name: options.name,
|
|
9949
|
+
exact: !!options.exact,
|
|
9950
|
+
timeout: options.timeout ? Number(options.timeout) : void 0,
|
|
9951
|
+
index: options.index ? Number(options.index) : void 0
|
|
9952
|
+
};
|
|
9953
|
+
break;
|
|
9954
|
+
}
|
|
9113
9955
|
case "back":
|
|
9114
9956
|
cmdName = "back";
|
|
9115
9957
|
params = {};
|
|
@@ -9301,11 +10143,11 @@ async function handleSession(args, options, mode, cdpEndpoint) {
|
|
|
9301
10143
|
case "list":
|
|
9302
10144
|
case "ls": {
|
|
9303
10145
|
try {
|
|
9304
|
-
const
|
|
9305
|
-
outputResult({ sessions }, mode);
|
|
10146
|
+
const sessions2 = await forwardSessionList();
|
|
10147
|
+
outputResult({ sessions: sessions2 }, mode);
|
|
9306
10148
|
} catch {
|
|
9307
|
-
const
|
|
9308
|
-
outputResult({ sessions }, mode);
|
|
10149
|
+
const sessions2 = await listSessions();
|
|
10150
|
+
outputResult({ sessions: sessions2 }, mode);
|
|
9309
10151
|
}
|
|
9310
10152
|
break;
|
|
9311
10153
|
}
|
|
@@ -9325,8 +10167,8 @@ async function handleSession(args, options, mode, cdpEndpoint) {
|
|
|
9325
10167
|
}
|
|
9326
10168
|
case "kill-all": {
|
|
9327
10169
|
try {
|
|
9328
|
-
const
|
|
9329
|
-
for (const s of
|
|
10170
|
+
const sessions2 = await forwardSessionList();
|
|
10171
|
+
for (const s of sessions2) {
|
|
9330
10172
|
try {
|
|
9331
10173
|
await forwardSessionClose(s.name);
|
|
9332
10174
|
} catch {
|
|
@@ -9353,15 +10195,28 @@ function getPluginLoader2() {
|
|
|
9353
10195
|
if (!pluginLoader3) pluginLoader3 = new XBrowserPluginLoader();
|
|
9354
10196
|
return pluginLoader3;
|
|
9355
10197
|
}
|
|
9356
|
-
async function
|
|
10198
|
+
async function buildRuntimePluginInfo() {
|
|
9357
10199
|
const loader = await getPluginLoader();
|
|
9358
10200
|
const sites = loader.getCore().loader.getSites();
|
|
9359
10201
|
const map = /* @__PURE__ */ new Map();
|
|
9360
10202
|
for (const site of sites) {
|
|
9361
|
-
const cmds = site.getAllCommands()
|
|
9362
|
-
|
|
9363
|
-
|
|
10203
|
+
const cmds = site.getAllCommands();
|
|
10204
|
+
const commandNames = cmds.map((c) => c.name);
|
|
10205
|
+
if (commandNames.length === 0) continue;
|
|
10206
|
+
const anySite = site;
|
|
10207
|
+
const hasLoginHandler = typeof anySite.hasLoginCommand === "function" && anySite.hasLoginCommand();
|
|
10208
|
+
const configRequiresLogin = !!site.config.requiresLogin;
|
|
10209
|
+
const hasLogin = hasLoginHandler || configRequiresLogin;
|
|
10210
|
+
let loggedIn = null;
|
|
10211
|
+
if (hasLogin) {
|
|
10212
|
+
try {
|
|
10213
|
+
loggedIn = await site.isLoggedIn();
|
|
10214
|
+
} catch {
|
|
10215
|
+
loggedIn = null;
|
|
10216
|
+
}
|
|
9364
10217
|
}
|
|
10218
|
+
const requiresLoginCommands = cmds.filter((c) => c.requiresLogin === true).map((c) => c.name);
|
|
10219
|
+
map.set(site.name, { commands: commandNames, hasLogin, loggedIn, requiresLoginCommands });
|
|
9365
10220
|
}
|
|
9366
10221
|
return map;
|
|
9367
10222
|
}
|
|
@@ -9525,6 +10380,54 @@ async function handlePluginInfo(args, options, mode) {
|
|
|
9525
10380
|
console.error("\u67E5\u8BE2\u5931\u8D25:", err.message);
|
|
9526
10381
|
}
|
|
9527
10382
|
}
|
|
10383
|
+
async function handlePluginSchema(args, mode) {
|
|
10384
|
+
const pluginName = args[0];
|
|
10385
|
+
const commandName = args[1];
|
|
10386
|
+
if (!pluginName) outputError("Usage: xbrowser plugin schema <name> [command] [--json]");
|
|
10387
|
+
const loader = await getPluginLoader();
|
|
10388
|
+
const contract = loader.getPluginContract(pluginName, commandName);
|
|
10389
|
+
if (!contract) {
|
|
10390
|
+
outputError(commandName ? `Command "${commandName}" not found in plugin "${pluginName}"` : `Plugin "${pluginName}" not found`);
|
|
10391
|
+
return;
|
|
10392
|
+
}
|
|
10393
|
+
if (mode === "json") {
|
|
10394
|
+
outputResult(contract, mode);
|
|
10395
|
+
return;
|
|
10396
|
+
}
|
|
10397
|
+
if ("commands" in contract) {
|
|
10398
|
+
printPluginContract(contract);
|
|
10399
|
+
} else {
|
|
10400
|
+
printCommandContract(pluginName, contract);
|
|
10401
|
+
}
|
|
10402
|
+
}
|
|
10403
|
+
function printPluginContract(contract) {
|
|
10404
|
+
console.log(`${contract.plugin.name} contract v${contract.version}`);
|
|
10405
|
+
if (contract.plugin.description) console.log(contract.plugin.description);
|
|
10406
|
+
console.log("");
|
|
10407
|
+
for (const command of contract.commands) {
|
|
10408
|
+
printCommandContract(contract.plugin.name, command);
|
|
10409
|
+
}
|
|
10410
|
+
}
|
|
10411
|
+
function printCommandContract(pluginName, command) {
|
|
10412
|
+
console.log(`${pluginName} ${command.name}`);
|
|
10413
|
+
if (command.description) console.log(` ${command.description}`);
|
|
10414
|
+
console.log(` scope: ${command.scope}`);
|
|
10415
|
+
if (command.capabilities.length > 0) {
|
|
10416
|
+
console.log(` capabilities: ${command.capabilities.join(", ")}`);
|
|
10417
|
+
}
|
|
10418
|
+
if (command.positional.length > 0) {
|
|
10419
|
+
console.log(` positional: ${command.positional.join(", ")}`);
|
|
10420
|
+
}
|
|
10421
|
+
if (command.form.fields.length > 0) {
|
|
10422
|
+
console.log(" fields:");
|
|
10423
|
+
for (const field of command.form.fields) {
|
|
10424
|
+
const required = field.required ? "required" : "optional";
|
|
10425
|
+
const choices = field.enum ? ` [${field.enum.join("|")}]` : "";
|
|
10426
|
+
console.log(` --${field.name}: ${field.type}/${field.widget} ${required}${choices}`);
|
|
10427
|
+
}
|
|
10428
|
+
}
|
|
10429
|
+
console.log("");
|
|
10430
|
+
}
|
|
9528
10431
|
async function handlePlugin(args, options, mode) {
|
|
9529
10432
|
const sub = args[0];
|
|
9530
10433
|
const subArgs = args.slice(1);
|
|
@@ -9565,17 +10468,20 @@ async function handlePlugin(args, options, mode) {
|
|
|
9565
10468
|
}
|
|
9566
10469
|
case "list": {
|
|
9567
10470
|
const plugins = await installer.list();
|
|
9568
|
-
const
|
|
10471
|
+
const runtimeInfo = await buildRuntimePluginInfo();
|
|
9569
10472
|
const enrichedPlugins = plugins.map((p) => {
|
|
9570
10473
|
const metadata = p.metadata;
|
|
9571
10474
|
const staticCommands = metadata?.commands;
|
|
9572
|
-
const
|
|
9573
|
-
const commands =
|
|
10475
|
+
const rt = runtimeInfo.get(p.name);
|
|
10476
|
+
const commands = rt?.commands || staticCommands;
|
|
9574
10477
|
return {
|
|
9575
10478
|
...p,
|
|
9576
10479
|
commands,
|
|
9577
10480
|
version: metadata?.version,
|
|
9578
|
-
description: metadata?.description
|
|
10481
|
+
description: metadata?.description,
|
|
10482
|
+
hasLogin: rt?.hasLogin ?? false,
|
|
10483
|
+
loggedIn: rt?.loggedIn ?? null,
|
|
10484
|
+
requiresLoginCommands: rt?.requiresLoginCommands ?? []
|
|
9579
10485
|
};
|
|
9580
10486
|
});
|
|
9581
10487
|
if (mode === "json") {
|
|
@@ -9586,20 +10492,27 @@ async function handlePlugin(args, options, mode) {
|
|
|
9586
10492
|
return;
|
|
9587
10493
|
}
|
|
9588
10494
|
for (const p of enrichedPlugins) {
|
|
10495
|
+
const loginTag = p.hasLogin ? p.loggedIn ? " [logged in]" : " [need login]" : "";
|
|
9589
10496
|
if (p.version && p.description) {
|
|
9590
|
-
console.log(`${p.name} (${p.version}) - ${p.description}`);
|
|
10497
|
+
console.log(`${p.name} (${p.version}) - ${p.description}${loginTag}`);
|
|
9591
10498
|
} else {
|
|
9592
|
-
console.log(p.name);
|
|
10499
|
+
console.log(`${p.name}${loginTag}`);
|
|
9593
10500
|
}
|
|
9594
10501
|
if (p.commands && p.commands.length > 0) {
|
|
9595
10502
|
console.log(` ${p.commands.join(", ")}`);
|
|
9596
10503
|
}
|
|
10504
|
+
if (p.requiresLoginCommands.length > 0) {
|
|
10505
|
+
console.log(` requires login: ${p.requiresLoginCommands.join(", ")}`);
|
|
10506
|
+
}
|
|
9597
10507
|
}
|
|
9598
10508
|
console.log(`
|
|
9599
10509
|
Total: ${enrichedPlugins.length} plugins`);
|
|
9600
10510
|
}
|
|
9601
10511
|
break;
|
|
9602
10512
|
}
|
|
10513
|
+
case "schema":
|
|
10514
|
+
await handlePluginSchema(subArgs, mode);
|
|
10515
|
+
break;
|
|
9603
10516
|
case "reload": {
|
|
9604
10517
|
const name = subArgs[0];
|
|
9605
10518
|
if (!name) outputError("Usage: xbrowser plugin reload <name>");
|
|
@@ -10640,14 +11553,14 @@ async function listCommands() {
|
|
|
10640
11553
|
return jsonResponse(200, { commands });
|
|
10641
11554
|
}
|
|
10642
11555
|
async function listSessions2() {
|
|
10643
|
-
const
|
|
11556
|
+
const sessions2 = getAllSessions().map((s) => ({
|
|
10644
11557
|
id: s.id,
|
|
10645
11558
|
name: s.name,
|
|
10646
11559
|
url: s.page?.url() ?? null,
|
|
10647
11560
|
createdAt: s.createdAt,
|
|
10648
11561
|
isCDP: s.isCDP ?? false
|
|
10649
11562
|
}));
|
|
10650
|
-
return jsonResponse(200, { sessions });
|
|
11563
|
+
return jsonResponse(200, { sessions: sessions2 });
|
|
10651
11564
|
}
|
|
10652
11565
|
async function createSessionHandler(req) {
|
|
10653
11566
|
const body = req.body;
|
|
@@ -11336,9 +12249,9 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
11336
12249
|
}
|
|
11337
12250
|
const needsBrowser = cmdEntry.scope === "page" || cmdEntry.scope === "browser";
|
|
11338
12251
|
if (needsBrowser && !process.env.XBROWSER_DAEMON_WORKER) {
|
|
11339
|
-
const { forwardExec } = await import("./daemon-client-
|
|
12252
|
+
const { forwardExec } = await import("./daemon-client-YAVQ343A.js");
|
|
11340
12253
|
const userTimeout = typeof params.timeout === "number" && params.timeout > 0 ? params.timeout * 1e3 + 3e4 : void 0;
|
|
11341
|
-
const result = await forwardExec(command
|
|
12254
|
+
const result = await forwardExec(`${command}.${subCommand}`, params, sessionName, cdpEndpoint, userTimeout);
|
|
11342
12255
|
if (result && result.success !== false) {
|
|
11343
12256
|
if (isCommandResult2(result)) {
|
|
11344
12257
|
if (mode === "json" || mode === "yaml") {
|
|
@@ -11371,6 +12284,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
11371
12284
|
browser: needsBrowser ? session.context.browser() : null,
|
|
11372
12285
|
browserContext: needsBrowser ? session.context : null,
|
|
11373
12286
|
sessionId: needsBrowser ? session.id : "",
|
|
12287
|
+
cdpEndpoint: cdpEndpoint || (needsBrowser ? session?.cdpEndpoint : void 0),
|
|
11374
12288
|
storage: getPluginStorage(command),
|
|
11375
12289
|
output: { mode, showTips: true, color: true, emoji: true },
|
|
11376
12290
|
error: (msg) => {
|
|
@@ -12369,7 +13283,7 @@ var WSServer = class extends EventEmitter {
|
|
|
12369
13283
|
const vp = this.lastFrameViewport || initSc.page.viewportSize() || await initSc.page.evaluate(() => ({ width: window.innerWidth, height: window.innerHeight }));
|
|
12370
13284
|
this.sendToClient(clientId, {
|
|
12371
13285
|
type: "status",
|
|
12372
|
-
data: { status: "connected", sessionId: client.sessionId || void 0, viewport: vp }
|
|
13286
|
+
data: { status: "connected", sessionId: client.sessionId || void 0, viewport: vp ?? void 0 }
|
|
12373
13287
|
});
|
|
12374
13288
|
} catch {
|
|
12375
13289
|
}
|
|
@@ -12787,7 +13701,7 @@ var WSServer = class extends EventEmitter {
|
|
|
12787
13701
|
const vp = this.lastFrameViewport || sc.page.viewportSize() || await sc.page.evaluate(() => ({ width: window.innerWidth, height: window.innerHeight }));
|
|
12788
13702
|
this.sendToClient(clientId, {
|
|
12789
13703
|
type: "status",
|
|
12790
|
-
data: { status: "connected", sessionId, viewport: vp }
|
|
13704
|
+
data: { status: "connected", sessionId, viewport: vp ?? void 0 }
|
|
12791
13705
|
});
|
|
12792
13706
|
} catch {
|
|
12793
13707
|
}
|
|
@@ -13918,8 +14832,8 @@ var DataCollector = class {
|
|
|
13918
14832
|
return results;
|
|
13919
14833
|
}
|
|
13920
14834
|
async createBrowserContext() {
|
|
13921
|
-
const {
|
|
13922
|
-
const browser = await
|
|
14835
|
+
const { launch } = await import("./cdp-driver-SSXUGXP6.js");
|
|
14836
|
+
const { browser } = await launch({
|
|
13923
14837
|
headless: true,
|
|
13924
14838
|
args: ["--no-sandbox", "--disable-setuid-sandbox"]
|
|
13925
14839
|
});
|
|
@@ -13980,6 +14894,7 @@ export {
|
|
|
13980
14894
|
WSServer,
|
|
13981
14895
|
WebhookNotifier,
|
|
13982
14896
|
XBrowserPluginLoader,
|
|
14897
|
+
actOnPage,
|
|
13983
14898
|
advise,
|
|
13984
14899
|
allBuiltins,
|
|
13985
14900
|
assertPageScope,
|
|
@@ -14019,6 +14934,7 @@ export {
|
|
|
14019
14934
|
getCommandNames,
|
|
14020
14935
|
getCompanyType,
|
|
14021
14936
|
getDaemonProcessStatus,
|
|
14937
|
+
getPageScreenHash,
|
|
14022
14938
|
getPlatformName,
|
|
14023
14939
|
getSessionPage,
|
|
14024
14940
|
getWSServerFromCache,
|
|
@@ -14029,6 +14945,7 @@ export {
|
|
|
14029
14945
|
mouseTrajectoryRule,
|
|
14030
14946
|
networkAnomalyRule,
|
|
14031
14947
|
normalizeSelector,
|
|
14948
|
+
observePage,
|
|
14032
14949
|
openSession,
|
|
14033
14950
|
pageLifecycleRule,
|
|
14034
14951
|
parseCommandArgs,
|
|
@@ -14045,5 +14962,6 @@ export {
|
|
|
14045
14962
|
splitCommand,
|
|
14046
14963
|
startDaemonProcess,
|
|
14047
14964
|
stopDaemonProcess,
|
|
14048
|
-
version
|
|
14965
|
+
version,
|
|
14966
|
+
waitForPage
|
|
14049
14967
|
};
|