bb-browser 0.10.1 → 0.11.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/README.md +9 -11
- package/README.zh-CN.md +9 -11
- package/dist/cdp-monitor.js +0 -0
- package/dist/{chunk-XYKHDJST.js → chunk-5WGFUZLM.js} +1 -3
- package/dist/chunk-5WGFUZLM.js.map +1 -0
- package/dist/chunk-AHGAQEFO.js +0 -0
- package/dist/chunk-D4HDZEJT.js +0 -0
- package/dist/cli.js +255 -1485
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +1835 -262
- package/dist/daemon.js.map +1 -1
- package/dist/jq-HHMLHEPA.js +0 -0
- package/dist/mcp.js +109 -31
- package/dist/mcp.js.map +1 -1
- package/dist/{openclaw-bridge-7BW5M4YX.js → openclaw-bridge-Q6EFUQCH.js} +50 -4
- package/dist/openclaw-bridge-Q6EFUQCH.js.map +1 -0
- package/extension/manifest.json +1 -1
- package/package.json +13 -13
- package/dist/chunk-FSL4RNI6.js +0 -53
- package/dist/chunk-FSL4RNI6.js.map +0 -1
- package/dist/chunk-XYKHDJST.js.map +0 -1
- package/dist/openclaw-bridge-7BW5M4YX.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,1302 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
COMMAND_TIMEOUT,
|
|
4
|
+
DAEMON_PORT,
|
|
4
5
|
generateId
|
|
5
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-5WGFUZLM.js";
|
|
6
7
|
import {
|
|
7
8
|
applyJq
|
|
8
9
|
} from "./chunk-AHGAQEFO.js";
|
|
9
|
-
import {
|
|
10
|
-
parseOpenClawJson
|
|
11
|
-
} from "./chunk-FSL4RNI6.js";
|
|
12
10
|
import "./chunk-D4HDZEJT.js";
|
|
13
11
|
|
|
14
12
|
// packages/cli/src/index.ts
|
|
15
|
-
import { fileURLToPath as
|
|
13
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
16
14
|
|
|
17
|
-
// packages/cli/src/
|
|
18
|
-
import {
|
|
15
|
+
// packages/cli/src/daemon-manager.ts
|
|
16
|
+
import { spawn } from "child_process";
|
|
17
|
+
import { readFile } from "fs/promises";
|
|
19
18
|
import { request as httpRequest } from "http";
|
|
20
|
-
import { request as httpsRequest } from "https";
|
|
21
|
-
import os2 from "os";
|
|
22
|
-
import path2 from "path";
|
|
23
19
|
import { fileURLToPath } from "url";
|
|
24
|
-
import
|
|
25
|
-
|
|
26
|
-
// packages/cli/src/cdp-discovery.ts
|
|
27
|
-
import { execFile, execSync, spawn } from "child_process";
|
|
20
|
+
import { dirname, resolve } from "path";
|
|
28
21
|
import { existsSync } from "fs";
|
|
29
|
-
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
30
22
|
import os from "os";
|
|
31
23
|
import path from "path";
|
|
32
|
-
var
|
|
33
|
-
var
|
|
34
|
-
var
|
|
35
|
-
var
|
|
36
|
-
function
|
|
37
|
-
return new Promise((
|
|
38
|
-
execFile(command, args, { encoding: "utf8", timeout }, (error, stdout) => {
|
|
39
|
-
if (error) {
|
|
40
|
-
reject(error);
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
resolve3(stdout.trim());
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
function getArgValue(flag) {
|
|
48
|
-
const index = process.argv.indexOf(flag);
|
|
49
|
-
if (index < 0) return void 0;
|
|
50
|
-
return process.argv[index + 1];
|
|
51
|
-
}
|
|
52
|
-
async function tryOpenClaw() {
|
|
53
|
-
try {
|
|
54
|
-
const raw = await execFileAsync("npx", ["openclaw", "browser", "status", "--json"], 5e3);
|
|
55
|
-
const parsed = parseOpenClawJson(raw);
|
|
56
|
-
const port = Number(parsed?.cdpPort);
|
|
57
|
-
if (Number.isInteger(port) && port > 0) {
|
|
58
|
-
return { host: "127.0.0.1", port };
|
|
59
|
-
}
|
|
60
|
-
} catch {
|
|
61
|
-
}
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
async function canConnect(host, port) {
|
|
65
|
-
try {
|
|
66
|
-
const controller = new AbortController();
|
|
67
|
-
const timeout = setTimeout(() => controller.abort(), 1200);
|
|
68
|
-
const response = await fetch(`http://${host}:${port}/json/version`, { signal: controller.signal });
|
|
69
|
-
clearTimeout(timeout);
|
|
70
|
-
return response.ok;
|
|
71
|
-
} catch {
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
function findBrowserExecutable() {
|
|
76
|
-
if (process.platform === "darwin") {
|
|
77
|
-
const candidates = [
|
|
78
|
-
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
79
|
-
"/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev",
|
|
80
|
-
"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
|
|
81
|
-
"/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta",
|
|
82
|
-
"/Applications/Arc.app/Contents/MacOS/Arc",
|
|
83
|
-
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
84
|
-
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
|
|
85
|
-
];
|
|
86
|
-
return candidates.find((candidate) => existsSync(candidate)) ?? null;
|
|
87
|
-
}
|
|
88
|
-
if (process.platform === "linux") {
|
|
89
|
-
const candidates = ["google-chrome", "google-chrome-stable", "chromium-browser", "chromium"];
|
|
90
|
-
for (const candidate of candidates) {
|
|
91
|
-
try {
|
|
92
|
-
const resolved = execSync(`which ${candidate}`, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
93
|
-
if (resolved) {
|
|
94
|
-
return resolved;
|
|
95
|
-
}
|
|
96
|
-
} catch {
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
if (process.platform === "win32") {
|
|
102
|
-
const localAppData = process.env.LOCALAPPDATA ?? "";
|
|
103
|
-
const candidates = [
|
|
104
|
-
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
105
|
-
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
|
106
|
-
...localAppData ? [
|
|
107
|
-
`${localAppData}\\Google\\Chrome Dev\\Application\\chrome.exe`,
|
|
108
|
-
`${localAppData}\\Google\\Chrome SxS\\Application\\chrome.exe`,
|
|
109
|
-
`${localAppData}\\Google\\Chrome Beta\\Application\\chrome.exe`
|
|
110
|
-
] : [],
|
|
111
|
-
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
112
|
-
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
113
|
-
"C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe"
|
|
114
|
-
];
|
|
115
|
-
return candidates.find((candidate) => existsSync(candidate)) ?? null;
|
|
116
|
-
}
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
async function isManagedBrowserRunning() {
|
|
120
|
-
try {
|
|
121
|
-
const rawPort = await readFile(MANAGED_PORT_FILE, "utf8");
|
|
122
|
-
const port = Number.parseInt(rawPort.trim(), 10);
|
|
123
|
-
if (!Number.isInteger(port) || port <= 0) {
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
|
-
return await canConnect("127.0.0.1", port);
|
|
127
|
-
} catch {
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
async function launchManagedBrowser(port = DEFAULT_CDP_PORT) {
|
|
132
|
-
const executable = findBrowserExecutable();
|
|
133
|
-
if (!executable) {
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
await mkdir(MANAGED_USER_DATA_DIR, { recursive: true });
|
|
137
|
-
const defaultProfileDir = path.join(MANAGED_USER_DATA_DIR, "Default");
|
|
138
|
-
const prefsPath = path.join(defaultProfileDir, "Preferences");
|
|
139
|
-
await mkdir(defaultProfileDir, { recursive: true });
|
|
140
|
-
try {
|
|
141
|
-
let prefs = {};
|
|
142
|
-
try {
|
|
143
|
-
prefs = JSON.parse(await readFile(prefsPath, "utf8"));
|
|
144
|
-
} catch {
|
|
145
|
-
}
|
|
146
|
-
if (!prefs.profile?.name || prefs.profile.name !== "bb-browser") {
|
|
147
|
-
prefs.profile = { ...prefs.profile || {}, name: "bb-browser" };
|
|
148
|
-
await writeFile(prefsPath, JSON.stringify(prefs), "utf8");
|
|
149
|
-
}
|
|
150
|
-
} catch {
|
|
151
|
-
}
|
|
152
|
-
const args = [
|
|
153
|
-
`--remote-debugging-port=${port}`,
|
|
154
|
-
`--user-data-dir=${MANAGED_USER_DATA_DIR}`,
|
|
155
|
-
"--no-first-run",
|
|
156
|
-
"--no-default-browser-check",
|
|
157
|
-
"--disable-sync",
|
|
158
|
-
"--disable-background-networking",
|
|
159
|
-
"--disable-component-update",
|
|
160
|
-
"--disable-features=Translate,MediaRouter",
|
|
161
|
-
"--disable-session-crashed-bubble",
|
|
162
|
-
"--hide-crash-restore-bubble",
|
|
163
|
-
"about:blank"
|
|
164
|
-
];
|
|
165
|
-
try {
|
|
166
|
-
const child = spawn(executable, args, {
|
|
167
|
-
detached: true,
|
|
168
|
-
stdio: "ignore"
|
|
169
|
-
});
|
|
170
|
-
child.unref();
|
|
171
|
-
} catch {
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
await mkdir(MANAGED_BROWSER_DIR, { recursive: true });
|
|
175
|
-
await writeFile(MANAGED_PORT_FILE, String(port), "utf8");
|
|
176
|
-
const deadline = Date.now() + 8e3;
|
|
177
|
-
while (Date.now() < deadline) {
|
|
178
|
-
if (await canConnect("127.0.0.1", port)) {
|
|
179
|
-
return { host: "127.0.0.1", port };
|
|
180
|
-
}
|
|
181
|
-
await new Promise((resolve3) => setTimeout(resolve3, 250));
|
|
182
|
-
}
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
async function discoverCdpPort() {
|
|
186
|
-
const explicitPort = Number.parseInt(getArgValue("--port") ?? "", 10);
|
|
187
|
-
if (Number.isInteger(explicitPort) && explicitPort > 0 && await canConnect("127.0.0.1", explicitPort)) {
|
|
188
|
-
return { host: "127.0.0.1", port: explicitPort };
|
|
189
|
-
}
|
|
190
|
-
try {
|
|
191
|
-
const rawPort = await readFile(MANAGED_PORT_FILE, "utf8");
|
|
192
|
-
const managedPort = Number.parseInt(rawPort.trim(), 10);
|
|
193
|
-
if (Number.isInteger(managedPort) && managedPort > 0 && await canConnect("127.0.0.1", managedPort)) {
|
|
194
|
-
return { host: "127.0.0.1", port: managedPort };
|
|
195
|
-
}
|
|
196
|
-
} catch {
|
|
197
|
-
}
|
|
198
|
-
if (process.argv.includes("--openclaw")) {
|
|
199
|
-
const viaOpenClaw = await tryOpenClaw();
|
|
200
|
-
if (viaOpenClaw && await canConnect(viaOpenClaw.host, viaOpenClaw.port)) {
|
|
201
|
-
return viaOpenClaw;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
const launched = await launchManagedBrowser();
|
|
205
|
-
if (launched) {
|
|
206
|
-
return launched;
|
|
207
|
-
}
|
|
208
|
-
if (!process.argv.includes("--openclaw")) {
|
|
209
|
-
const detectedOpenClaw = await tryOpenClaw();
|
|
210
|
-
if (detectedOpenClaw && await canConnect(detectedOpenClaw.host, detectedOpenClaw.port)) {
|
|
211
|
-
return detectedOpenClaw;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
return null;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// packages/cli/src/cdp-client.ts
|
|
218
|
-
var connectionState = null;
|
|
219
|
-
var reconnecting = null;
|
|
220
|
-
var networkRequests = /* @__PURE__ */ new Map();
|
|
221
|
-
var networkEnabled = false;
|
|
222
|
-
var consoleMessages = [];
|
|
223
|
-
var consoleEnabled = false;
|
|
224
|
-
var jsErrors = [];
|
|
225
|
-
var errorsEnabled = false;
|
|
226
|
-
var traceRecording = false;
|
|
227
|
-
var traceEvents = [];
|
|
228
|
-
function getContextFilePath(host, port) {
|
|
229
|
-
const safeHost = host.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
230
|
-
return path2.join(os2.tmpdir(), `bb-browser-cdp-context-${safeHost}-${port}.json`);
|
|
231
|
-
}
|
|
232
|
-
function loadPersistedCurrentTargetId(host, port) {
|
|
233
|
-
try {
|
|
234
|
-
const data = JSON.parse(readFileSync(getContextFilePath(host, port), "utf-8"));
|
|
235
|
-
return typeof data.currentTargetId === "string" && data.currentTargetId ? data.currentTargetId : void 0;
|
|
236
|
-
} catch {
|
|
237
|
-
return void 0;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
function persistCurrentTargetId(host, port, currentTargetId) {
|
|
241
|
-
try {
|
|
242
|
-
writeFileSync(getContextFilePath(host, port), JSON.stringify({ currentTargetId }));
|
|
243
|
-
} catch {
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
function setCurrentTargetId(targetId) {
|
|
247
|
-
const state = connectionState;
|
|
248
|
-
if (!state) return;
|
|
249
|
-
state.currentTargetId = targetId;
|
|
250
|
-
persistCurrentTargetId(state.host, state.port, targetId);
|
|
251
|
-
}
|
|
252
|
-
function buildRequestError(error) {
|
|
253
|
-
return error instanceof Error ? error : new Error(String(error));
|
|
254
|
-
}
|
|
255
|
-
function fetchJson(url) {
|
|
256
|
-
return new Promise((resolve3, reject) => {
|
|
257
|
-
const requester = url.startsWith("https:") ? httpsRequest : httpRequest;
|
|
258
|
-
const req = requester(url, { method: "GET" }, (res) => {
|
|
259
|
-
const chunks = [];
|
|
260
|
-
res.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
261
|
-
res.on("end", () => {
|
|
262
|
-
const raw = Buffer.concat(chunks).toString("utf8");
|
|
263
|
-
if ((res.statusCode ?? 500) >= 400) {
|
|
264
|
-
reject(new Error(`HTTP ${res.statusCode ?? 500}: ${raw}`));
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
try {
|
|
268
|
-
resolve3(JSON.parse(raw));
|
|
269
|
-
} catch (error) {
|
|
270
|
-
reject(error);
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
});
|
|
274
|
-
req.on("error", reject);
|
|
275
|
-
req.end();
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
async function getJsonList(host, port) {
|
|
279
|
-
const data = await fetchJson(`http://${host}:${port}/json/list`);
|
|
280
|
-
return Array.isArray(data) ? data : [];
|
|
281
|
-
}
|
|
282
|
-
async function getJsonVersion(host, port) {
|
|
283
|
-
const data = await fetchJson(`http://${host}:${port}/json/version`);
|
|
284
|
-
const url = data.webSocketDebuggerUrl;
|
|
285
|
-
if (typeof url !== "string" || !url) {
|
|
286
|
-
throw new Error("CDP endpoint missing webSocketDebuggerUrl");
|
|
287
|
-
}
|
|
288
|
-
return { webSocketDebuggerUrl: url };
|
|
289
|
-
}
|
|
290
|
-
function connectWebSocket(url) {
|
|
291
|
-
return new Promise((resolve3, reject) => {
|
|
292
|
-
const ws = new WebSocket(url);
|
|
293
|
-
ws.once("open", () => {
|
|
294
|
-
const socket = ws._socket;
|
|
295
|
-
if (socket && typeof socket.unref === "function") {
|
|
296
|
-
socket.unref();
|
|
297
|
-
}
|
|
298
|
-
resolve3(ws);
|
|
299
|
-
});
|
|
300
|
-
ws.once("error", reject);
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
function createState(host, port, browserWsUrl, browserSocket) {
|
|
304
|
-
const state = {
|
|
305
|
-
host,
|
|
306
|
-
port,
|
|
307
|
-
browserWsUrl,
|
|
308
|
-
browserSocket,
|
|
309
|
-
browserPending: /* @__PURE__ */ new Map(),
|
|
310
|
-
nextMessageId: 1,
|
|
311
|
-
sessions: /* @__PURE__ */ new Map(),
|
|
312
|
-
attachedTargets: /* @__PURE__ */ new Map(),
|
|
313
|
-
refsByTarget: /* @__PURE__ */ new Map(),
|
|
314
|
-
currentTargetId: loadPersistedCurrentTargetId(host, port),
|
|
315
|
-
activeFrameIdByTarget: /* @__PURE__ */ new Map(),
|
|
316
|
-
dialogHandlers: /* @__PURE__ */ new Map()
|
|
317
|
-
};
|
|
318
|
-
browserSocket.on("message", (raw) => {
|
|
319
|
-
const message = JSON.parse(raw.toString());
|
|
320
|
-
if (typeof message.id === "number") {
|
|
321
|
-
const pending = state.browserPending.get(message.id);
|
|
322
|
-
if (!pending) return;
|
|
323
|
-
state.browserPending.delete(message.id);
|
|
324
|
-
if (message.error) {
|
|
325
|
-
pending.reject(new Error(`${pending.method}: ${message.error.message ?? "Unknown CDP error"}`));
|
|
326
|
-
} else {
|
|
327
|
-
pending.resolve(message.result);
|
|
328
|
-
}
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
if (message.method === "Target.attachedToTarget") {
|
|
332
|
-
const params = message.params;
|
|
333
|
-
const sessionId = params.sessionId;
|
|
334
|
-
const targetInfo = params.targetInfo;
|
|
335
|
-
if (typeof sessionId === "string" && typeof targetInfo?.targetId === "string") {
|
|
336
|
-
state.sessions.set(targetInfo.targetId, sessionId);
|
|
337
|
-
state.attachedTargets.set(sessionId, targetInfo.targetId);
|
|
338
|
-
}
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
if (message.method === "Target.detachedFromTarget") {
|
|
342
|
-
const params = message.params;
|
|
343
|
-
const sessionId = params.sessionId;
|
|
344
|
-
if (typeof sessionId === "string") {
|
|
345
|
-
const targetId = state.attachedTargets.get(sessionId);
|
|
346
|
-
if (targetId) {
|
|
347
|
-
state.sessions.delete(targetId);
|
|
348
|
-
state.attachedTargets.delete(sessionId);
|
|
349
|
-
state.activeFrameIdByTarget.delete(targetId);
|
|
350
|
-
state.dialogHandlers.delete(targetId);
|
|
351
|
-
if (state.currentTargetId === targetId) {
|
|
352
|
-
state.currentTargetId = void 0;
|
|
353
|
-
persistCurrentTargetId(state.host, state.port, void 0);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
-
if (message.method === "Target.receivedMessageFromTarget") {
|
|
360
|
-
const params = message.params;
|
|
361
|
-
const sessionId = params.sessionId;
|
|
362
|
-
const messageText = params.message;
|
|
363
|
-
if (typeof sessionId === "string" && typeof messageText === "string") {
|
|
364
|
-
const targetId = state.attachedTargets.get(sessionId);
|
|
365
|
-
if (targetId) {
|
|
366
|
-
handleSessionEvent(targetId, JSON.parse(messageText)).catch(() => {
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
if (typeof message.sessionId === "string" && typeof message.method === "string") {
|
|
373
|
-
const targetId = state.attachedTargets.get(message.sessionId);
|
|
374
|
-
if (targetId) {
|
|
375
|
-
handleSessionEvent(targetId, message).catch(() => {
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
});
|
|
380
|
-
browserSocket.on("close", () => {
|
|
381
|
-
if (connectionState === state) {
|
|
382
|
-
connectionState = null;
|
|
383
|
-
}
|
|
384
|
-
for (const pending of state.browserPending.values()) {
|
|
385
|
-
pending.reject(new Error("CDP connection closed"));
|
|
386
|
-
}
|
|
387
|
-
state.browserPending.clear();
|
|
388
|
-
});
|
|
389
|
-
browserSocket.on("error", () => {
|
|
390
|
-
});
|
|
391
|
-
return state;
|
|
392
|
-
}
|
|
393
|
-
async function browserCommand(method, params = {}) {
|
|
394
|
-
const state = connectionState;
|
|
395
|
-
if (!state) throw new Error("CDP connection not initialized");
|
|
396
|
-
const id = state.nextMessageId++;
|
|
397
|
-
const payload = JSON.stringify({ id, method, params });
|
|
398
|
-
const promise = new Promise((resolve3, reject) => {
|
|
399
|
-
state.browserPending.set(id, { resolve: resolve3, reject, method });
|
|
400
|
-
});
|
|
401
|
-
state.browserSocket.send(payload);
|
|
402
|
-
return promise;
|
|
403
|
-
}
|
|
404
|
-
async function sessionCommand(targetId, method, params = {}) {
|
|
405
|
-
const state = connectionState;
|
|
406
|
-
if (!state) throw new Error("CDP connection not initialized");
|
|
407
|
-
const sessionId = state.sessions.get(targetId) ?? await attachTarget(targetId);
|
|
408
|
-
const id = state.nextMessageId++;
|
|
409
|
-
const payload = JSON.stringify({ id, method, params, sessionId });
|
|
410
|
-
return new Promise((resolve3, reject) => {
|
|
411
|
-
const check = (raw) => {
|
|
412
|
-
const msg = JSON.parse(raw.toString());
|
|
413
|
-
if (msg.id === id && msg.sessionId === sessionId) {
|
|
414
|
-
state.browserSocket.off("message", check);
|
|
415
|
-
if (msg.error) reject(new Error(`${method}: ${msg.error.message ?? "Unknown CDP error"}`));
|
|
416
|
-
else resolve3(msg.result);
|
|
417
|
-
}
|
|
418
|
-
};
|
|
419
|
-
state.browserSocket.on("message", check);
|
|
420
|
-
state.browserSocket.send(payload);
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
function getActiveFrameId(targetId) {
|
|
424
|
-
const frameId = connectionState?.activeFrameIdByTarget.get(targetId);
|
|
425
|
-
return frameId ?? void 0;
|
|
426
|
-
}
|
|
427
|
-
async function pageCommand(targetId, method, params = {}) {
|
|
428
|
-
const frameId = getActiveFrameId(targetId);
|
|
429
|
-
return sessionCommand(targetId, method, frameId ? { ...params, frameId } : params);
|
|
430
|
-
}
|
|
431
|
-
function normalizeHeaders(headers) {
|
|
432
|
-
if (!headers || typeof headers !== "object") return void 0;
|
|
433
|
-
return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, String(value)]));
|
|
434
|
-
}
|
|
435
|
-
async function handleSessionEvent(targetId, event) {
|
|
436
|
-
const method = event.method;
|
|
437
|
-
const params = event.params ?? {};
|
|
438
|
-
if (typeof method !== "string") return;
|
|
439
|
-
if (method === "Page.javascriptDialogOpening") {
|
|
440
|
-
const handler = connectionState?.dialogHandlers.get(targetId);
|
|
441
|
-
if (handler) {
|
|
442
|
-
await sessionCommand(targetId, "Page.handleJavaScriptDialog", {
|
|
443
|
-
accept: handler.accept,
|
|
444
|
-
...handler.promptText !== void 0 ? { promptText: handler.promptText } : {}
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
if (method === "Network.requestWillBeSent") {
|
|
450
|
-
const requestId = typeof params.requestId === "string" ? params.requestId : void 0;
|
|
451
|
-
const request = params.request;
|
|
452
|
-
if (!requestId || !request) return;
|
|
453
|
-
networkRequests.set(requestId, {
|
|
454
|
-
requestId,
|
|
455
|
-
url: String(request.url ?? ""),
|
|
456
|
-
method: String(request.method ?? "GET"),
|
|
457
|
-
type: String(params.type ?? "Other"),
|
|
458
|
-
timestamp: Math.round(Number(params.timestamp ?? Date.now()) * 1e3),
|
|
459
|
-
requestHeaders: normalizeHeaders(request.headers),
|
|
460
|
-
requestBody: typeof request.postData === "string" ? request.postData : void 0
|
|
461
|
-
});
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
if (method === "Network.responseReceived") {
|
|
465
|
-
const requestId = typeof params.requestId === "string" ? params.requestId : void 0;
|
|
466
|
-
const response = params.response;
|
|
467
|
-
if (!requestId || !response) return;
|
|
468
|
-
const existing = networkRequests.get(requestId);
|
|
469
|
-
if (!existing) return;
|
|
470
|
-
existing.status = typeof response.status === "number" ? response.status : void 0;
|
|
471
|
-
existing.statusText = typeof response.statusText === "string" ? response.statusText : void 0;
|
|
472
|
-
existing.responseHeaders = normalizeHeaders(response.headers);
|
|
473
|
-
existing.mimeType = typeof response.mimeType === "string" ? response.mimeType : void 0;
|
|
474
|
-
networkRequests.set(requestId, existing);
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
if (method === "Network.loadingFailed") {
|
|
478
|
-
const requestId = typeof params.requestId === "string" ? params.requestId : void 0;
|
|
479
|
-
if (!requestId) return;
|
|
480
|
-
const existing = networkRequests.get(requestId);
|
|
481
|
-
if (!existing) return;
|
|
482
|
-
existing.failed = true;
|
|
483
|
-
existing.failureReason = typeof params.errorText === "string" ? params.errorText : "Unknown error";
|
|
484
|
-
networkRequests.set(requestId, existing);
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
|
-
if (method === "Runtime.consoleAPICalled") {
|
|
488
|
-
const type = String(params.type ?? "log");
|
|
489
|
-
const args = Array.isArray(params.args) ? params.args : [];
|
|
490
|
-
const text = args.map((arg) => {
|
|
491
|
-
if (typeof arg.value === "string") return arg.value;
|
|
492
|
-
if (arg.value !== void 0) return String(arg.value);
|
|
493
|
-
if (typeof arg.description === "string") return arg.description;
|
|
494
|
-
return "";
|
|
495
|
-
}).filter(Boolean).join(" ");
|
|
496
|
-
const stack = params.stackTrace;
|
|
497
|
-
const firstCallFrame = Array.isArray(stack?.callFrames) ? stack?.callFrames[0] : void 0;
|
|
498
|
-
consoleMessages.push({
|
|
499
|
-
type: ["log", "info", "warn", "error", "debug"].includes(type) ? type : "log",
|
|
500
|
-
text,
|
|
501
|
-
timestamp: Math.round(Number(params.timestamp ?? Date.now())),
|
|
502
|
-
url: typeof firstCallFrame?.url === "string" ? firstCallFrame.url : void 0,
|
|
503
|
-
lineNumber: typeof firstCallFrame?.lineNumber === "number" ? firstCallFrame.lineNumber : void 0
|
|
504
|
-
});
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
if (method === "Runtime.exceptionThrown") {
|
|
508
|
-
const details = params.exceptionDetails;
|
|
509
|
-
if (!details) return;
|
|
510
|
-
const exception = details.exception;
|
|
511
|
-
const stackTrace = details.stackTrace;
|
|
512
|
-
const callFrames = Array.isArray(stackTrace?.callFrames) ? stackTrace.callFrames : [];
|
|
513
|
-
jsErrors.push({
|
|
514
|
-
message: typeof exception?.description === "string" ? exception.description : String(details.text ?? "JavaScript exception"),
|
|
515
|
-
url: typeof details.url === "string" ? details.url : typeof callFrames[0]?.url === "string" ? String(callFrames[0].url) : void 0,
|
|
516
|
-
lineNumber: typeof details.lineNumber === "number" ? details.lineNumber : void 0,
|
|
517
|
-
columnNumber: typeof details.columnNumber === "number" ? details.columnNumber : void 0,
|
|
518
|
-
stackTrace: callFrames.length > 0 ? callFrames.map((frame) => `${String(frame.functionName ?? "<anonymous>")} (${String(frame.url ?? "")}:${String(frame.lineNumber ?? 0)}:${String(frame.columnNumber ?? 0)})`).join("\n") : void 0,
|
|
519
|
-
timestamp: Date.now()
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
async function ensureNetworkMonitoring(targetId) {
|
|
524
|
-
if (networkEnabled) return;
|
|
525
|
-
await sessionCommand(targetId, "Network.enable");
|
|
526
|
-
networkEnabled = true;
|
|
527
|
-
}
|
|
528
|
-
async function ensureConsoleMonitoring(targetId) {
|
|
529
|
-
if (consoleEnabled && errorsEnabled) return;
|
|
530
|
-
await sessionCommand(targetId, "Runtime.enable");
|
|
531
|
-
consoleEnabled = true;
|
|
532
|
-
errorsEnabled = true;
|
|
533
|
-
}
|
|
534
|
-
async function attachTarget(targetId) {
|
|
535
|
-
const result = await browserCommand("Target.attachToTarget", {
|
|
536
|
-
targetId,
|
|
537
|
-
flatten: true
|
|
538
|
-
});
|
|
539
|
-
connectionState?.sessions.set(targetId, result.sessionId);
|
|
540
|
-
connectionState?.attachedTargets.set(result.sessionId, targetId);
|
|
541
|
-
connectionState?.activeFrameIdByTarget.set(targetId, connectionState?.activeFrameIdByTarget.get(targetId) ?? null);
|
|
542
|
-
await sessionCommand(targetId, "Page.enable");
|
|
543
|
-
await sessionCommand(targetId, "Runtime.enable");
|
|
544
|
-
await sessionCommand(targetId, "DOM.enable");
|
|
545
|
-
await sessionCommand(targetId, "Accessibility.enable");
|
|
546
|
-
return result.sessionId;
|
|
547
|
-
}
|
|
548
|
-
async function getTargets() {
|
|
549
|
-
const state = connectionState;
|
|
550
|
-
if (!state) throw new Error("CDP connection not initialized");
|
|
551
|
-
try {
|
|
552
|
-
const result = await browserCommand("Target.getTargets");
|
|
553
|
-
return (result.targetInfos || []).map((target) => ({
|
|
554
|
-
id: target.targetId,
|
|
555
|
-
type: target.type,
|
|
556
|
-
title: target.title,
|
|
557
|
-
url: target.url,
|
|
558
|
-
webSocketDebuggerUrl: ""
|
|
559
|
-
}));
|
|
560
|
-
} catch {
|
|
561
|
-
return getJsonList(state.host, state.port);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
async function ensurePageTarget(targetId) {
|
|
565
|
-
const targets = (await getTargets()).filter((target2) => target2.type === "page");
|
|
566
|
-
if (targets.length === 0) throw new Error("No page target found");
|
|
567
|
-
const persistedTargetId = targetId === void 0 ? connectionState?.currentTargetId : void 0;
|
|
568
|
-
let target;
|
|
569
|
-
if (typeof targetId === "number") {
|
|
570
|
-
target = targets[targetId] ?? targets.find((item) => Number(item.id) === targetId);
|
|
571
|
-
} else if (typeof targetId === "string") {
|
|
572
|
-
target = targets.find((item) => item.id === targetId);
|
|
573
|
-
if (!target) {
|
|
574
|
-
const numericTargetId = Number(targetId);
|
|
575
|
-
if (!Number.isNaN(numericTargetId)) {
|
|
576
|
-
target = targets[numericTargetId] ?? targets.find((item) => Number(item.id) === numericTargetId);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
} else if (persistedTargetId) {
|
|
580
|
-
target = targets.find((item) => item.id === persistedTargetId);
|
|
581
|
-
}
|
|
582
|
-
target ??= targets[0];
|
|
583
|
-
setCurrentTargetId(target.id);
|
|
584
|
-
await attachTarget(target.id);
|
|
585
|
-
return target;
|
|
586
|
-
}
|
|
587
|
-
async function resolveBackendNodeIdByXPath(targetId, xpath) {
|
|
588
|
-
await sessionCommand(targetId, "DOM.getDocument", { depth: 0 });
|
|
589
|
-
const search = await sessionCommand(targetId, "DOM.performSearch", {
|
|
590
|
-
query: xpath,
|
|
591
|
-
includeUserAgentShadowDOM: true
|
|
592
|
-
});
|
|
593
|
-
try {
|
|
594
|
-
if (!search.resultCount) {
|
|
595
|
-
throw new Error(`Unknown ref xpath: ${xpath}`);
|
|
596
|
-
}
|
|
597
|
-
const { nodeIds } = await sessionCommand(targetId, "DOM.getSearchResults", {
|
|
598
|
-
searchId: search.searchId,
|
|
599
|
-
fromIndex: 0,
|
|
600
|
-
toIndex: search.resultCount
|
|
601
|
-
});
|
|
602
|
-
for (const nodeId of nodeIds) {
|
|
603
|
-
const described = await sessionCommand(targetId, "DOM.describeNode", {
|
|
604
|
-
nodeId
|
|
605
|
-
});
|
|
606
|
-
if (described.node.backendNodeId) {
|
|
607
|
-
return described.node.backendNodeId;
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
throw new Error(`XPath resolved but no backend node id found: ${xpath}`);
|
|
611
|
-
} finally {
|
|
612
|
-
await sessionCommand(targetId, "DOM.discardSearchResults", { searchId: search.searchId }).catch(() => {
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
async function parseRef(ref) {
|
|
617
|
-
const targetId = connectionState?.currentTargetId ?? "";
|
|
618
|
-
let refs = connectionState?.refsByTarget.get(targetId) ?? {};
|
|
619
|
-
if (!refs[ref] && targetId) {
|
|
620
|
-
const persistedRefs = loadPersistedRefs(targetId);
|
|
621
|
-
if (persistedRefs) {
|
|
622
|
-
connectionState?.refsByTarget.set(targetId, persistedRefs);
|
|
623
|
-
refs = persistedRefs;
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
const found = refs[ref];
|
|
627
|
-
if (!found) {
|
|
628
|
-
throw new Error(`Unknown ref: ${ref}. Run snapshot first.`);
|
|
629
|
-
}
|
|
630
|
-
if (found.backendDOMNodeId) {
|
|
631
|
-
return found.backendDOMNodeId;
|
|
632
|
-
}
|
|
633
|
-
if (targetId && found.xpath) {
|
|
634
|
-
const backendDOMNodeId = await resolveBackendNodeIdByXPath(targetId, found.xpath);
|
|
635
|
-
found.backendDOMNodeId = backendDOMNodeId;
|
|
636
|
-
connectionState?.refsByTarget.set(targetId, refs);
|
|
637
|
-
const pageUrl = await evaluate(targetId, "location.href", true).catch(() => void 0);
|
|
638
|
-
if (pageUrl) {
|
|
639
|
-
persistRefs(targetId, pageUrl, refs);
|
|
640
|
-
}
|
|
641
|
-
return backendDOMNodeId;
|
|
642
|
-
}
|
|
643
|
-
throw new Error(`Unknown ref: ${ref}. Run snapshot first.`);
|
|
644
|
-
}
|
|
645
|
-
function getRefsFilePath(targetId) {
|
|
646
|
-
return path2.join(os2.tmpdir(), `bb-browser-refs-${targetId}.json`);
|
|
647
|
-
}
|
|
648
|
-
function loadPersistedRefs(targetId, expectedUrl) {
|
|
649
|
-
try {
|
|
650
|
-
const data = JSON.parse(readFileSync(getRefsFilePath(targetId), "utf-8"));
|
|
651
|
-
if (data.targetId !== targetId) return null;
|
|
652
|
-
if (expectedUrl !== void 0 && data.url !== expectedUrl) return null;
|
|
653
|
-
if (!data.refs || typeof data.refs !== "object") return null;
|
|
654
|
-
return data.refs;
|
|
655
|
-
} catch {
|
|
656
|
-
return null;
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
function persistRefs(targetId, url, refs) {
|
|
660
|
-
try {
|
|
661
|
-
writeFileSync(getRefsFilePath(targetId), JSON.stringify({ targetId, url, timestamp: Date.now(), refs }));
|
|
662
|
-
} catch {
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
function clearPersistedRefs(targetId) {
|
|
666
|
-
try {
|
|
667
|
-
unlinkSync(getRefsFilePath(targetId));
|
|
668
|
-
} catch {
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
function loadBuildDomTreeScript() {
|
|
672
|
-
const currentDir = path2.dirname(fileURLToPath(import.meta.url));
|
|
673
|
-
const candidates = [
|
|
674
|
-
path2.resolve(currentDir, "./extension/buildDomTree.js"),
|
|
675
|
-
// npm installed: dist/cli.js → ../extension/buildDomTree.js
|
|
676
|
-
path2.resolve(currentDir, "../extension/buildDomTree.js"),
|
|
677
|
-
path2.resolve(currentDir, "../extension/dist/buildDomTree.js"),
|
|
678
|
-
path2.resolve(currentDir, "../packages/extension/public/buildDomTree.js"),
|
|
679
|
-
path2.resolve(currentDir, "../packages/extension/dist/buildDomTree.js"),
|
|
680
|
-
// dev mode: packages/cli/dist/ → ../../../extension/
|
|
681
|
-
path2.resolve(currentDir, "../../../extension/buildDomTree.js"),
|
|
682
|
-
path2.resolve(currentDir, "../../../extension/dist/buildDomTree.js"),
|
|
683
|
-
// dev mode: packages/cli/src/ → ../../extension/
|
|
684
|
-
path2.resolve(currentDir, "../../extension/buildDomTree.js"),
|
|
685
|
-
path2.resolve(currentDir, "../../../packages/extension/dist/buildDomTree.js"),
|
|
686
|
-
path2.resolve(currentDir, "../../../packages/extension/public/buildDomTree.js")
|
|
687
|
-
];
|
|
688
|
-
for (const candidate of candidates) {
|
|
689
|
-
try {
|
|
690
|
-
return readFileSync(candidate, "utf8");
|
|
691
|
-
} catch {
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
throw new Error("Cannot find buildDomTree.js");
|
|
695
|
-
}
|
|
696
|
-
async function evaluate(targetId, expression, returnByValue = true) {
|
|
697
|
-
const result = await sessionCommand(targetId, "Runtime.evaluate", {
|
|
698
|
-
expression,
|
|
699
|
-
awaitPromise: true,
|
|
700
|
-
returnByValue
|
|
701
|
-
});
|
|
702
|
-
if (result.exceptionDetails) {
|
|
703
|
-
throw new Error(
|
|
704
|
-
result.exceptionDetails.exception?.description || result.exceptionDetails.text || "Runtime.evaluate failed"
|
|
705
|
-
);
|
|
706
|
-
}
|
|
707
|
-
return result.result.value ?? result.result;
|
|
708
|
-
}
|
|
709
|
-
async function focusNode(targetId, backendNodeId) {
|
|
710
|
-
await sessionCommand(targetId, "DOM.focus", { backendNodeId });
|
|
711
|
-
}
|
|
712
|
-
async function insertTextIntoNode(targetId, backendNodeId, text, clearFirst) {
|
|
713
|
-
const resolved = await sessionCommand(targetId, "DOM.resolveNode", { backendNodeId });
|
|
714
|
-
await sessionCommand(targetId, "Runtime.callFunctionOn", {
|
|
715
|
-
objectId: resolved.object.objectId,
|
|
716
|
-
functionDeclaration: `function(clearFirst) {
|
|
717
|
-
if (typeof this.scrollIntoView === 'function') {
|
|
718
|
-
this.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
|
|
719
|
-
}
|
|
720
|
-
if (typeof this.focus === 'function') this.focus();
|
|
721
|
-
if (this instanceof HTMLInputElement || this instanceof HTMLTextAreaElement) {
|
|
722
|
-
if (clearFirst) {
|
|
723
|
-
this.value = '';
|
|
724
|
-
this.dispatchEvent(new Event('input', { bubbles: true }));
|
|
725
|
-
}
|
|
726
|
-
if (typeof this.setSelectionRange === 'function') {
|
|
727
|
-
const end = this.value.length;
|
|
728
|
-
this.setSelectionRange(end, end);
|
|
729
|
-
}
|
|
730
|
-
return true;
|
|
731
|
-
}
|
|
732
|
-
if (this instanceof HTMLElement && this.isContentEditable) {
|
|
733
|
-
if (clearFirst) {
|
|
734
|
-
this.textContent = '';
|
|
735
|
-
this.dispatchEvent(new Event('input', { bubbles: true }));
|
|
736
|
-
}
|
|
737
|
-
const selection = window.getSelection();
|
|
738
|
-
if (selection) {
|
|
739
|
-
const range = document.createRange();
|
|
740
|
-
range.selectNodeContents(this);
|
|
741
|
-
range.collapse(false);
|
|
742
|
-
selection.removeAllRanges();
|
|
743
|
-
selection.addRange(range);
|
|
744
|
-
}
|
|
745
|
-
return true;
|
|
746
|
-
}
|
|
747
|
-
return false;
|
|
748
|
-
}`,
|
|
749
|
-
arguments: [
|
|
750
|
-
{ value: clearFirst }
|
|
751
|
-
],
|
|
752
|
-
returnByValue: true
|
|
753
|
-
});
|
|
754
|
-
if (text) {
|
|
755
|
-
await focusNode(targetId, backendNodeId);
|
|
756
|
-
await sessionCommand(targetId, "Input.insertText", { text });
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
async function getInteractablePoint(targetId, backendNodeId) {
|
|
760
|
-
const resolved = await sessionCommand(targetId, "DOM.resolveNode", { backendNodeId });
|
|
761
|
-
const call = await sessionCommand(targetId, "Runtime.callFunctionOn", {
|
|
762
|
-
objectId: resolved.object.objectId,
|
|
763
|
-
functionDeclaration: `function() {
|
|
764
|
-
if (!(this instanceof Element)) {
|
|
765
|
-
throw new Error('Ref does not resolve to an element');
|
|
766
|
-
}
|
|
767
|
-
this.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
|
|
768
|
-
const rect = this.getBoundingClientRect();
|
|
769
|
-
if (!rect || rect.width <= 0 || rect.height <= 0) {
|
|
770
|
-
throw new Error('Element is not visible');
|
|
771
|
-
}
|
|
772
|
-
return {
|
|
773
|
-
x: rect.left + rect.width / 2,
|
|
774
|
-
y: rect.top + rect.height / 2,
|
|
775
|
-
};
|
|
776
|
-
}`,
|
|
777
|
-
returnByValue: true
|
|
778
|
-
});
|
|
779
|
-
if (call.exceptionDetails) {
|
|
780
|
-
throw new Error(call.exceptionDetails.text || "Failed to resolve element point");
|
|
781
|
-
}
|
|
782
|
-
const point = call.result.value;
|
|
783
|
-
if (!point || typeof point.x !== "number" || typeof point.y !== "number" || !Number.isFinite(point.x) || !Number.isFinite(point.y)) {
|
|
784
|
-
throw new Error("Failed to resolve element point");
|
|
785
|
-
}
|
|
786
|
-
return point;
|
|
787
|
-
}
|
|
788
|
-
async function mouseClick(targetId, x, y) {
|
|
789
|
-
await sessionCommand(targetId, "Input.dispatchMouseEvent", { type: "mouseMoved", x, y, button: "none" });
|
|
790
|
-
await sessionCommand(targetId, "Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 });
|
|
791
|
-
await sessionCommand(targetId, "Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 });
|
|
792
|
-
}
|
|
793
|
-
async function getAttributeValue(targetId, backendNodeId, attribute) {
|
|
794
|
-
if (attribute === "text") {
|
|
795
|
-
const resolved = await sessionCommand(targetId, "DOM.resolveNode", { backendNodeId });
|
|
796
|
-
const call2 = await sessionCommand(targetId, "Runtime.callFunctionOn", {
|
|
797
|
-
objectId: resolved.object.objectId,
|
|
798
|
-
functionDeclaration: `function() { return (this instanceof HTMLElement ? this.innerText : this.textContent || '').trim(); }`,
|
|
799
|
-
returnByValue: true
|
|
800
|
-
});
|
|
801
|
-
return String(call2.result.value ?? "");
|
|
802
|
-
}
|
|
803
|
-
const result = await sessionCommand(targetId, "DOM.resolveNode", { backendNodeId });
|
|
804
|
-
const call = await sessionCommand(targetId, "Runtime.callFunctionOn", {
|
|
805
|
-
objectId: result.object.objectId,
|
|
806
|
-
functionDeclaration: `function() { if (${JSON.stringify(attribute)} === 'url') return this.href || this.src || location.href; if (${JSON.stringify(attribute)} === 'title') return document.title; return this.getAttribute(${JSON.stringify(attribute)}) || ''; }`,
|
|
807
|
-
returnByValue: true
|
|
808
|
-
});
|
|
809
|
-
return String(call.result.value ?? "");
|
|
810
|
-
}
|
|
811
|
-
async function buildSnapshot(targetId, request) {
|
|
812
|
-
const script = loadBuildDomTreeScript();
|
|
813
|
-
const buildArgs = {
|
|
814
|
-
showHighlightElements: true,
|
|
815
|
-
focusHighlightIndex: -1,
|
|
816
|
-
viewportExpansion: -1,
|
|
817
|
-
debugMode: false,
|
|
818
|
-
startId: 0,
|
|
819
|
-
startHighlightIndex: 0
|
|
820
|
-
};
|
|
821
|
-
const expression = `(() => { ${script}; const fn = globalThis.buildDomTree ?? (typeof window !== 'undefined' ? window.buildDomTree : undefined); if (typeof fn !== 'function') { throw new Error('buildDomTree is not available after script injection'); } return fn(${JSON.stringify({
|
|
822
|
-
...buildArgs
|
|
823
|
-
})}); })()`;
|
|
824
|
-
const value = await evaluate(targetId, expression, true);
|
|
825
|
-
if (!value || !value.map || !value.rootId) {
|
|
826
|
-
const title = await evaluate(targetId, "document.title", true);
|
|
827
|
-
const pageUrl2 = await evaluate(targetId, "location.href", true);
|
|
828
|
-
const fallbackSnapshot = {
|
|
829
|
-
title,
|
|
830
|
-
url: pageUrl2,
|
|
831
|
-
lines: [title || pageUrl2],
|
|
832
|
-
refs: {}
|
|
833
|
-
};
|
|
834
|
-
connectionState?.refsByTarget.set(targetId, {});
|
|
835
|
-
persistRefs(targetId, pageUrl2, {});
|
|
836
|
-
return fallbackSnapshot;
|
|
837
|
-
}
|
|
838
|
-
const snapshot = convertBuildDomTreeResult(value, {
|
|
839
|
-
interactiveOnly: !!request.interactive,
|
|
840
|
-
compact: !!request.compact,
|
|
841
|
-
maxDepth: request.maxDepth,
|
|
842
|
-
selector: request.selector
|
|
843
|
-
});
|
|
844
|
-
const pageUrl = await evaluate(targetId, "location.href", true);
|
|
845
|
-
connectionState?.refsByTarget.set(targetId, snapshot.refs || {});
|
|
846
|
-
persistRefs(targetId, pageUrl, snapshot.refs || {});
|
|
847
|
-
return snapshot;
|
|
848
|
-
}
|
|
849
|
-
function convertBuildDomTreeResult(result, options) {
|
|
850
|
-
const { interactiveOnly, compact, maxDepth, selector } = options;
|
|
851
|
-
const { rootId, map } = result;
|
|
852
|
-
const refs = {};
|
|
853
|
-
const lines = [];
|
|
854
|
-
const getRole = (node) => {
|
|
855
|
-
const tagName = node.tagName.toLowerCase();
|
|
856
|
-
const role = node.attributes?.role;
|
|
857
|
-
if (role) return role;
|
|
858
|
-
const type = node.attributes?.type?.toLowerCase() || "text";
|
|
859
|
-
const inputRoleMap = {
|
|
860
|
-
text: "textbox",
|
|
861
|
-
password: "textbox",
|
|
862
|
-
email: "textbox",
|
|
863
|
-
url: "textbox",
|
|
864
|
-
tel: "textbox",
|
|
865
|
-
search: "searchbox",
|
|
866
|
-
number: "spinbutton",
|
|
867
|
-
range: "slider",
|
|
868
|
-
checkbox: "checkbox",
|
|
869
|
-
radio: "radio",
|
|
870
|
-
button: "button",
|
|
871
|
-
submit: "button",
|
|
872
|
-
reset: "button",
|
|
873
|
-
file: "button"
|
|
874
|
-
};
|
|
875
|
-
const roleMap = {
|
|
876
|
-
a: "link",
|
|
877
|
-
button: "button",
|
|
878
|
-
input: inputRoleMap[type] || "textbox",
|
|
879
|
-
select: "combobox",
|
|
880
|
-
textarea: "textbox",
|
|
881
|
-
img: "image",
|
|
882
|
-
nav: "navigation",
|
|
883
|
-
main: "main",
|
|
884
|
-
header: "banner",
|
|
885
|
-
footer: "contentinfo",
|
|
886
|
-
aside: "complementary",
|
|
887
|
-
form: "form",
|
|
888
|
-
table: "table",
|
|
889
|
-
ul: "list",
|
|
890
|
-
ol: "list",
|
|
891
|
-
li: "listitem",
|
|
892
|
-
h1: "heading",
|
|
893
|
-
h2: "heading",
|
|
894
|
-
h3: "heading",
|
|
895
|
-
h4: "heading",
|
|
896
|
-
h5: "heading",
|
|
897
|
-
h6: "heading",
|
|
898
|
-
dialog: "dialog",
|
|
899
|
-
article: "article",
|
|
900
|
-
section: "region",
|
|
901
|
-
label: "label",
|
|
902
|
-
details: "group",
|
|
903
|
-
summary: "button"
|
|
904
|
-
};
|
|
905
|
-
return roleMap[tagName] || tagName;
|
|
906
|
-
};
|
|
907
|
-
const collectTextContent = (node, nodeMap, depthLimit = 5) => {
|
|
908
|
-
const texts = [];
|
|
909
|
-
const visit = (nodeId, depth) => {
|
|
910
|
-
if (depth > depthLimit) return;
|
|
911
|
-
const currentNode = nodeMap[nodeId];
|
|
912
|
-
if (!currentNode) return;
|
|
913
|
-
if ("type" in currentNode && currentNode.type === "TEXT_NODE") {
|
|
914
|
-
const text = currentNode.text.trim();
|
|
915
|
-
if (text) texts.push(text);
|
|
916
|
-
return;
|
|
917
|
-
}
|
|
918
|
-
for (const childId of currentNode.children || []) visit(childId, depth + 1);
|
|
919
|
-
};
|
|
920
|
-
for (const childId of node.children || []) visit(childId, 0);
|
|
921
|
-
return texts.join(" ").trim();
|
|
922
|
-
};
|
|
923
|
-
const getName = (node) => {
|
|
924
|
-
const attrs = node.attributes || {};
|
|
925
|
-
return attrs["aria-label"] || attrs.title || attrs.placeholder || attrs.alt || attrs.value || collectTextContent(node, map) || attrs.name || void 0;
|
|
926
|
-
};
|
|
927
|
-
const truncateText = (text, length = 50) => text.length <= length ? text : `${text.slice(0, length - 3)}...`;
|
|
928
|
-
const selectorText = selector?.trim().toLowerCase();
|
|
929
|
-
const matchesSelector = (node, role, name) => {
|
|
930
|
-
if (!selectorText) return true;
|
|
931
|
-
const haystack = [node.tagName, role, name, node.xpath || "", ...Object.values(node.attributes || {})].join(" ").toLowerCase();
|
|
932
|
-
return haystack.includes(selectorText);
|
|
933
|
-
};
|
|
934
|
-
if (interactiveOnly) {
|
|
935
|
-
const interactiveNodes = Object.entries(map).filter(([, node]) => !("type" in node) && node.highlightIndex !== void 0 && node.highlightIndex !== null).map(([id, node]) => ({ id, node })).sort((a, b) => (a.node.highlightIndex ?? 0) - (b.node.highlightIndex ?? 0));
|
|
936
|
-
for (const { node } of interactiveNodes) {
|
|
937
|
-
const refId = String(node.highlightIndex);
|
|
938
|
-
const role = getRole(node);
|
|
939
|
-
const name = getName(node);
|
|
940
|
-
if (!matchesSelector(node, role, name)) continue;
|
|
941
|
-
let line = `${role} [ref=${refId}]`;
|
|
942
|
-
if (name) line += ` ${JSON.stringify(truncateText(name))}`;
|
|
943
|
-
lines.push(line);
|
|
944
|
-
refs[refId] = {
|
|
945
|
-
xpath: node.xpath || "",
|
|
946
|
-
role,
|
|
947
|
-
name,
|
|
948
|
-
tagName: node.tagName.toLowerCase()
|
|
949
|
-
};
|
|
950
|
-
}
|
|
951
|
-
return { snapshot: lines.join("\n"), refs };
|
|
952
|
-
}
|
|
953
|
-
const walk = (nodeId, depth) => {
|
|
954
|
-
if (maxDepth !== void 0 && depth > maxDepth) return;
|
|
955
|
-
const node = map[nodeId];
|
|
956
|
-
if (!node) return;
|
|
957
|
-
if ("type" in node && node.type === "TEXT_NODE") {
|
|
958
|
-
const text = node.text.trim();
|
|
959
|
-
if (!text) return;
|
|
960
|
-
lines.push(`${" ".repeat(depth)}- text ${JSON.stringify(truncateText(text, compact ? 80 : 120))}`);
|
|
961
|
-
return;
|
|
962
|
-
}
|
|
963
|
-
const role = getRole(node);
|
|
964
|
-
const name = getName(node);
|
|
965
|
-
if (!matchesSelector(node, role, name)) {
|
|
966
|
-
for (const childId of node.children || []) walk(childId, depth + 1);
|
|
967
|
-
return;
|
|
968
|
-
}
|
|
969
|
-
const indent = " ".repeat(depth);
|
|
970
|
-
const refId = node.highlightIndex !== void 0 && node.highlightIndex !== null ? String(node.highlightIndex) : null;
|
|
971
|
-
let line = `${indent}- ${role}`;
|
|
972
|
-
if (refId) line += ` [ref=${refId}]`;
|
|
973
|
-
if (name) line += ` ${JSON.stringify(truncateText(name, compact ? 50 : 80))}`;
|
|
974
|
-
if (!compact) line += ` <${node.tagName.toLowerCase()}>`;
|
|
975
|
-
lines.push(line);
|
|
976
|
-
if (refId) {
|
|
977
|
-
refs[refId] = {
|
|
978
|
-
xpath: node.xpath || "",
|
|
979
|
-
role,
|
|
980
|
-
name,
|
|
981
|
-
tagName: node.tagName.toLowerCase()
|
|
982
|
-
};
|
|
983
|
-
}
|
|
984
|
-
for (const childId of node.children || []) walk(childId, depth + 1);
|
|
985
|
-
};
|
|
986
|
-
walk(rootId, 0);
|
|
987
|
-
return { snapshot: lines.join("\n"), refs };
|
|
988
|
-
}
|
|
989
|
-
function ok(id, data) {
|
|
990
|
-
return { id, success: true, data };
|
|
991
|
-
}
|
|
992
|
-
function fail(id, error) {
|
|
993
|
-
return { id, success: false, error: buildRequestError(error).message };
|
|
994
|
-
}
|
|
995
|
-
async function ensureCdpConnection() {
|
|
996
|
-
if (connectionState) return;
|
|
997
|
-
if (reconnecting) return reconnecting;
|
|
998
|
-
reconnecting = (async () => {
|
|
999
|
-
const discovered = await discoverCdpPort();
|
|
1000
|
-
if (!discovered) {
|
|
1001
|
-
throw new Error("No browser connection found");
|
|
1002
|
-
}
|
|
1003
|
-
const version = await getJsonVersion(discovered.host, discovered.port);
|
|
1004
|
-
const wsUrl = version.webSocketDebuggerUrl;
|
|
1005
|
-
const socket = await connectWebSocket(wsUrl);
|
|
1006
|
-
connectionState = createState(discovered.host, discovered.port, wsUrl, socket);
|
|
1007
|
-
})();
|
|
1008
|
-
try {
|
|
1009
|
-
await reconnecting;
|
|
1010
|
-
} finally {
|
|
1011
|
-
reconnecting = null;
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
async function sendCommand(request) {
|
|
1015
|
-
try {
|
|
1016
|
-
await ensureCdpConnection();
|
|
1017
|
-
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("\u8BF7\u6C42\u8D85\u65F6")), COMMAND_TIMEOUT));
|
|
1018
|
-
return await Promise.race([dispatchRequest(request), timeout]);
|
|
1019
|
-
} catch (error) {
|
|
1020
|
-
return fail(request.id, error);
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
async function dispatchRequest(request) {
|
|
1024
|
-
const target = await ensurePageTarget(request.tabId);
|
|
1025
|
-
switch (request.action) {
|
|
1026
|
-
case "open": {
|
|
1027
|
-
if (!request.url) return fail(request.id, "Missing url parameter");
|
|
1028
|
-
if (request.tabId === void 0) {
|
|
1029
|
-
const created = await browserCommand("Target.createTarget", { url: request.url, background: true });
|
|
1030
|
-
const newTarget = await ensurePageTarget(created.targetId);
|
|
1031
|
-
return ok(request.id, { url: request.url, tabId: newTarget.id });
|
|
1032
|
-
}
|
|
1033
|
-
await pageCommand(target.id, "Page.navigate", { url: request.url });
|
|
1034
|
-
connectionState?.refsByTarget.delete(target.id);
|
|
1035
|
-
clearPersistedRefs(target.id);
|
|
1036
|
-
return ok(request.id, { url: request.url, title: target.title, tabId: target.id });
|
|
1037
|
-
}
|
|
1038
|
-
case "snapshot": {
|
|
1039
|
-
const snapshotData = await buildSnapshot(target.id, request);
|
|
1040
|
-
return ok(request.id, { title: target.title, url: target.url, snapshotData });
|
|
1041
|
-
}
|
|
1042
|
-
case "click":
|
|
1043
|
-
case "hover": {
|
|
1044
|
-
if (!request.ref) return fail(request.id, "Missing ref parameter");
|
|
1045
|
-
const backendNodeId = await parseRef(request.ref);
|
|
1046
|
-
const point = await getInteractablePoint(target.id, backendNodeId);
|
|
1047
|
-
await sessionCommand(target.id, "Input.dispatchMouseEvent", { type: "mouseMoved", x: point.x, y: point.y, button: "none" });
|
|
1048
|
-
if (request.action === "click") await mouseClick(target.id, point.x, point.y);
|
|
1049
|
-
return ok(request.id, {});
|
|
1050
|
-
}
|
|
1051
|
-
case "fill":
|
|
1052
|
-
case "type": {
|
|
1053
|
-
if (!request.ref) return fail(request.id, "Missing ref parameter");
|
|
1054
|
-
if (request.text == null) return fail(request.id, "Missing text parameter");
|
|
1055
|
-
const backendNodeId = await parseRef(request.ref);
|
|
1056
|
-
await insertTextIntoNode(target.id, backendNodeId, request.text, request.action === "fill");
|
|
1057
|
-
return ok(request.id, { value: request.text });
|
|
1058
|
-
}
|
|
1059
|
-
case "check":
|
|
1060
|
-
case "uncheck": {
|
|
1061
|
-
if (!request.ref) return fail(request.id, "Missing ref parameter");
|
|
1062
|
-
const backendNodeId = await parseRef(request.ref);
|
|
1063
|
-
const desired = request.action === "check";
|
|
1064
|
-
const resolved = await sessionCommand(target.id, "DOM.resolveNode", { backendNodeId });
|
|
1065
|
-
await sessionCommand(target.id, "Runtime.callFunctionOn", {
|
|
1066
|
-
objectId: resolved.object.objectId,
|
|
1067
|
-
functionDeclaration: `function() { this.checked = ${desired}; this.dispatchEvent(new Event('input', { bubbles: true })); this.dispatchEvent(new Event('change', { bubbles: true })); }`
|
|
1068
|
-
});
|
|
1069
|
-
return ok(request.id, {});
|
|
1070
|
-
}
|
|
1071
|
-
case "select": {
|
|
1072
|
-
if (!request.ref || request.value == null) return fail(request.id, "Missing ref or value parameter");
|
|
1073
|
-
const backendNodeId = await parseRef(request.ref);
|
|
1074
|
-
const resolved = await sessionCommand(target.id, "DOM.resolveNode", { backendNodeId });
|
|
1075
|
-
await sessionCommand(target.id, "Runtime.callFunctionOn", {
|
|
1076
|
-
objectId: resolved.object.objectId,
|
|
1077
|
-
functionDeclaration: `function() { this.value = ${JSON.stringify(request.value)}; this.dispatchEvent(new Event('input', { bubbles: true })); this.dispatchEvent(new Event('change', { bubbles: true })); }`
|
|
1078
|
-
});
|
|
1079
|
-
return ok(request.id, { value: request.value });
|
|
1080
|
-
}
|
|
1081
|
-
case "get": {
|
|
1082
|
-
if (!request.attribute) return fail(request.id, "Missing attribute parameter");
|
|
1083
|
-
if (request.attribute === "url" && !request.ref) {
|
|
1084
|
-
return ok(request.id, { value: await evaluate(target.id, "location.href", true) });
|
|
1085
|
-
}
|
|
1086
|
-
if (request.attribute === "title" && !request.ref) {
|
|
1087
|
-
return ok(request.id, { value: await evaluate(target.id, "document.title", true) });
|
|
1088
|
-
}
|
|
1089
|
-
if (!request.ref) return fail(request.id, "Missing ref parameter");
|
|
1090
|
-
const value = await getAttributeValue(target.id, await parseRef(request.ref), request.attribute);
|
|
1091
|
-
return ok(request.id, { value });
|
|
1092
|
-
}
|
|
1093
|
-
case "screenshot": {
|
|
1094
|
-
const result = await sessionCommand(target.id, "Page.captureScreenshot", { format: "png", fromSurface: true });
|
|
1095
|
-
return ok(request.id, { dataUrl: `data:image/png;base64,${result.data}` });
|
|
1096
|
-
}
|
|
1097
|
-
case "close": {
|
|
1098
|
-
await browserCommand("Target.closeTarget", { targetId: target.id });
|
|
1099
|
-
connectionState?.refsByTarget.delete(target.id);
|
|
1100
|
-
clearPersistedRefs(target.id);
|
|
1101
|
-
return ok(request.id, {});
|
|
1102
|
-
}
|
|
1103
|
-
case "wait": {
|
|
1104
|
-
await new Promise((resolve3) => setTimeout(resolve3, request.ms ?? 1e3));
|
|
1105
|
-
return ok(request.id, {});
|
|
1106
|
-
}
|
|
1107
|
-
case "press": {
|
|
1108
|
-
if (!request.key) return fail(request.id, "Missing key parameter");
|
|
1109
|
-
await sessionCommand(target.id, "Input.dispatchKeyEvent", { type: "keyDown", key: request.key });
|
|
1110
|
-
if (request.key.length === 1) {
|
|
1111
|
-
await sessionCommand(target.id, "Input.dispatchKeyEvent", { type: "char", text: request.key, key: request.key });
|
|
1112
|
-
}
|
|
1113
|
-
await sessionCommand(target.id, "Input.dispatchKeyEvent", { type: "keyUp", key: request.key });
|
|
1114
|
-
return ok(request.id, {});
|
|
1115
|
-
}
|
|
1116
|
-
case "scroll": {
|
|
1117
|
-
const deltaY = request.direction === "up" ? -(request.pixels ?? 300) : request.pixels ?? 300;
|
|
1118
|
-
await sessionCommand(target.id, "Input.dispatchMouseEvent", { type: "mouseWheel", x: 0, y: 0, deltaX: 0, deltaY });
|
|
1119
|
-
return ok(request.id, {});
|
|
1120
|
-
}
|
|
1121
|
-
case "back": {
|
|
1122
|
-
await evaluate(target.id, "history.back(); undefined");
|
|
1123
|
-
return ok(request.id, {});
|
|
1124
|
-
}
|
|
1125
|
-
case "forward": {
|
|
1126
|
-
await evaluate(target.id, "history.forward(); undefined");
|
|
1127
|
-
return ok(request.id, {});
|
|
1128
|
-
}
|
|
1129
|
-
case "refresh": {
|
|
1130
|
-
await sessionCommand(target.id, "Page.reload", { ignoreCache: false });
|
|
1131
|
-
return ok(request.id, {});
|
|
1132
|
-
}
|
|
1133
|
-
case "eval": {
|
|
1134
|
-
if (!request.script) return fail(request.id, "Missing script parameter");
|
|
1135
|
-
const result = await evaluate(target.id, request.script, true);
|
|
1136
|
-
return ok(request.id, { result });
|
|
1137
|
-
}
|
|
1138
|
-
case "tab_list": {
|
|
1139
|
-
const tabs = (await getTargets()).filter((item) => item.type === "page").map((item, index) => ({ index, url: item.url, title: item.title, active: item.id === connectionState?.currentTargetId || !connectionState?.currentTargetId && index === 0, tabId: item.id }));
|
|
1140
|
-
return ok(request.id, { tabs, activeIndex: tabs.findIndex((tab) => tab.active) });
|
|
1141
|
-
}
|
|
1142
|
-
case "tab_new": {
|
|
1143
|
-
const created = await browserCommand("Target.createTarget", { url: request.url ?? "about:blank", background: true });
|
|
1144
|
-
return ok(request.id, { tabId: created.targetId, url: request.url ?? "about:blank" });
|
|
1145
|
-
}
|
|
1146
|
-
case "tab_select": {
|
|
1147
|
-
const tabs = (await getTargets()).filter((item) => item.type === "page");
|
|
1148
|
-
const selected = request.tabId !== void 0 ? tabs.find((item) => item.id === String(request.tabId) || Number(item.id) === request.tabId) : tabs[request.index ?? 0];
|
|
1149
|
-
if (!selected) return fail(request.id, "Tab not found");
|
|
1150
|
-
setCurrentTargetId(selected.id);
|
|
1151
|
-
await attachTarget(selected.id);
|
|
1152
|
-
return ok(request.id, { tabId: selected.id, url: selected.url, title: selected.title });
|
|
1153
|
-
}
|
|
1154
|
-
case "tab_close": {
|
|
1155
|
-
const tabs = (await getTargets()).filter((item) => item.type === "page");
|
|
1156
|
-
const selected = request.tabId !== void 0 ? tabs.find((item) => item.id === String(request.tabId) || Number(item.id) === request.tabId) : tabs[request.index ?? 0];
|
|
1157
|
-
if (!selected) return fail(request.id, "Tab not found");
|
|
1158
|
-
await browserCommand("Target.closeTarget", { targetId: selected.id });
|
|
1159
|
-
connectionState?.refsByTarget.delete(selected.id);
|
|
1160
|
-
if (connectionState?.currentTargetId === selected.id) {
|
|
1161
|
-
setCurrentTargetId(void 0);
|
|
1162
|
-
}
|
|
1163
|
-
clearPersistedRefs(selected.id);
|
|
1164
|
-
return ok(request.id, { tabId: selected.id });
|
|
1165
|
-
}
|
|
1166
|
-
case "frame": {
|
|
1167
|
-
if (!request.selector) return fail(request.id, "Missing selector parameter");
|
|
1168
|
-
const document = await pageCommand(target.id, "DOM.getDocument", {});
|
|
1169
|
-
const node = await pageCommand(target.id, "DOM.querySelector", { nodeId: document.root.nodeId, selector: request.selector });
|
|
1170
|
-
if (!node.nodeId) return fail(request.id, `\u627E\u4E0D\u5230 iframe: ${request.selector}`);
|
|
1171
|
-
const described = await pageCommand(target.id, "DOM.describeNode", { nodeId: node.nodeId });
|
|
1172
|
-
const frameId = described.node.frameId;
|
|
1173
|
-
const nodeName = String(described.node.nodeName ?? "").toLowerCase();
|
|
1174
|
-
if (!frameId) return fail(request.id, `\u65E0\u6CD5\u83B7\u53D6 iframe frameId: ${request.selector}`);
|
|
1175
|
-
if (nodeName && nodeName !== "iframe" && nodeName !== "frame") return fail(request.id, `\u5143\u7D20\u4E0D\u662F iframe: ${nodeName}`);
|
|
1176
|
-
connectionState?.activeFrameIdByTarget.set(target.id, frameId);
|
|
1177
|
-
const attributes = described.node.attributes ?? [];
|
|
1178
|
-
const attrMap = {};
|
|
1179
|
-
for (let i = 0; i < attributes.length; i += 2) attrMap[String(attributes[i])] = String(attributes[i + 1] ?? "");
|
|
1180
|
-
return ok(request.id, { frameInfo: { selector: request.selector, name: attrMap.name ?? "", url: attrMap.src ?? "", frameId } });
|
|
1181
|
-
}
|
|
1182
|
-
case "frame_main": {
|
|
1183
|
-
connectionState?.activeFrameIdByTarget.set(target.id, null);
|
|
1184
|
-
return ok(request.id, { frameInfo: { frameId: 0 } });
|
|
1185
|
-
}
|
|
1186
|
-
case "dialog": {
|
|
1187
|
-
connectionState?.dialogHandlers.set(target.id, { accept: request.dialogResponse !== "dismiss", ...request.promptText !== void 0 ? { promptText: request.promptText } : {} });
|
|
1188
|
-
await sessionCommand(target.id, "Page.enable");
|
|
1189
|
-
return ok(request.id, { dialog: { armed: true, response: request.dialogResponse ?? "accept" } });
|
|
1190
|
-
}
|
|
1191
|
-
case "network": {
|
|
1192
|
-
const subCommand = request.networkCommand ?? "requests";
|
|
1193
|
-
switch (subCommand) {
|
|
1194
|
-
case "requests": {
|
|
1195
|
-
await ensureNetworkMonitoring(target.id);
|
|
1196
|
-
const requests = Array.from(networkRequests.values()).filter((item) => !request.filter || item.url.includes(request.filter));
|
|
1197
|
-
if (request.withBody) {
|
|
1198
|
-
await Promise.all(requests.map(async (item) => {
|
|
1199
|
-
if (item.failed || item.responseBody !== void 0 || item.bodyError !== void 0) return;
|
|
1200
|
-
try {
|
|
1201
|
-
const body = await sessionCommand(target.id, "Network.getResponseBody", { requestId: item.requestId });
|
|
1202
|
-
item.responseBody = body.body;
|
|
1203
|
-
item.responseBodyBase64 = body.base64Encoded;
|
|
1204
|
-
} catch (error) {
|
|
1205
|
-
item.bodyError = error instanceof Error ? error.message : String(error);
|
|
1206
|
-
}
|
|
1207
|
-
}));
|
|
1208
|
-
}
|
|
1209
|
-
return ok(request.id, { networkRequests: requests });
|
|
1210
|
-
}
|
|
1211
|
-
case "route":
|
|
1212
|
-
return ok(request.id, { routeCount: 0 });
|
|
1213
|
-
case "unroute":
|
|
1214
|
-
return ok(request.id, { routeCount: 0 });
|
|
1215
|
-
case "clear":
|
|
1216
|
-
networkRequests.clear();
|
|
1217
|
-
return ok(request.id, {});
|
|
1218
|
-
default:
|
|
1219
|
-
return fail(request.id, `Unknown network subcommand: ${subCommand}`);
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
case "console": {
|
|
1223
|
-
const subCommand = request.consoleCommand ?? "get";
|
|
1224
|
-
await ensureConsoleMonitoring(target.id);
|
|
1225
|
-
switch (subCommand) {
|
|
1226
|
-
case "get":
|
|
1227
|
-
return ok(request.id, { consoleMessages: consoleMessages.filter((item) => !request.filter || item.text.includes(request.filter)) });
|
|
1228
|
-
case "clear":
|
|
1229
|
-
consoleMessages.length = 0;
|
|
1230
|
-
return ok(request.id, {});
|
|
1231
|
-
default:
|
|
1232
|
-
return fail(request.id, `Unknown console subcommand: ${subCommand}`);
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
case "errors": {
|
|
1236
|
-
const subCommand = request.errorsCommand ?? "get";
|
|
1237
|
-
await ensureConsoleMonitoring(target.id);
|
|
1238
|
-
switch (subCommand) {
|
|
1239
|
-
case "get":
|
|
1240
|
-
return ok(request.id, { jsErrors: jsErrors.filter((item) => !request.filter || item.message.includes(request.filter) || item.url?.includes(request.filter)) });
|
|
1241
|
-
case "clear":
|
|
1242
|
-
jsErrors.length = 0;
|
|
1243
|
-
return ok(request.id, {});
|
|
1244
|
-
default:
|
|
1245
|
-
return fail(request.id, `Unknown errors subcommand: ${subCommand}`);
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
case "trace": {
|
|
1249
|
-
const subCommand = request.traceCommand ?? "status";
|
|
1250
|
-
switch (subCommand) {
|
|
1251
|
-
case "start":
|
|
1252
|
-
traceRecording = true;
|
|
1253
|
-
traceEvents.length = 0;
|
|
1254
|
-
return ok(request.id, { traceStatus: { recording: true, eventCount: 0 } });
|
|
1255
|
-
case "stop": {
|
|
1256
|
-
traceRecording = false;
|
|
1257
|
-
return ok(request.id, { traceEvents: [...traceEvents], traceStatus: { recording: false, eventCount: traceEvents.length } });
|
|
1258
|
-
}
|
|
1259
|
-
case "status":
|
|
1260
|
-
return ok(request.id, { traceStatus: { recording: traceRecording, eventCount: traceEvents.length } });
|
|
1261
|
-
default:
|
|
1262
|
-
return fail(request.id, `Unknown trace subcommand: ${subCommand}`);
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
default:
|
|
1266
|
-
return fail(request.id, `Action not yet supported in direct CDP mode: ${request.action}`);
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
// packages/cli/src/monitor-manager.ts
|
|
1271
|
-
import { spawn as spawn2 } from "child_process";
|
|
1272
|
-
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, unlink } from "fs/promises";
|
|
1273
|
-
import { request as httpRequest2 } from "http";
|
|
1274
|
-
import { randomBytes } from "crypto";
|
|
1275
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1276
|
-
import { dirname, resolve } from "path";
|
|
1277
|
-
import { existsSync as existsSync2 } from "fs";
|
|
1278
|
-
import os3 from "os";
|
|
1279
|
-
import path3 from "path";
|
|
1280
|
-
var MONITOR_DIR = path3.join(os3.homedir(), ".bb-browser");
|
|
1281
|
-
var PID_FILE = path3.join(MONITOR_DIR, "monitor.pid");
|
|
1282
|
-
var PORT_FILE = path3.join(MONITOR_DIR, "monitor.port");
|
|
1283
|
-
var TOKEN_FILE = path3.join(MONITOR_DIR, "monitor.token");
|
|
1284
|
-
var DEFAULT_MONITOR_PORT = 19826;
|
|
1285
|
-
function httpJson(method, url, token, body) {
|
|
1286
|
-
return new Promise((resolve3, reject) => {
|
|
1287
|
-
const parsed = new URL(url);
|
|
24
|
+
var DAEMON_DIR = path.join(os.homedir(), ".bb-browser");
|
|
25
|
+
var TOKEN_FILE = path.join(DAEMON_DIR, "daemon.token");
|
|
26
|
+
var cachedToken = null;
|
|
27
|
+
var daemonReady = false;
|
|
28
|
+
function httpJson(method, urlPath, token, body, timeout = 5e3) {
|
|
29
|
+
return new Promise((resolve2, reject) => {
|
|
1288
30
|
const payload = body !== void 0 ? JSON.stringify(body) : void 0;
|
|
1289
|
-
const req =
|
|
31
|
+
const req = httpRequest(
|
|
1290
32
|
{
|
|
1291
|
-
hostname:
|
|
1292
|
-
port:
|
|
1293
|
-
path:
|
|
33
|
+
hostname: "127.0.0.1",
|
|
34
|
+
port: DAEMON_PORT,
|
|
35
|
+
path: urlPath,
|
|
1294
36
|
method,
|
|
1295
37
|
headers: {
|
|
1296
38
|
Authorization: `Bearer ${token}`,
|
|
1297
|
-
...payload ? {
|
|
39
|
+
...payload ? {
|
|
40
|
+
"Content-Type": "application/json",
|
|
41
|
+
"Content-Length": Buffer.byteLength(payload)
|
|
42
|
+
} : {}
|
|
1298
43
|
},
|
|
1299
|
-
timeout
|
|
44
|
+
timeout
|
|
1300
45
|
},
|
|
1301
46
|
(res) => {
|
|
1302
47
|
const chunks = [];
|
|
@@ -1304,13 +49,13 @@ function httpJson(method, url, token, body) {
|
|
|
1304
49
|
res.on("end", () => {
|
|
1305
50
|
const raw = Buffer.concat(chunks).toString("utf8");
|
|
1306
51
|
if ((res.statusCode ?? 500) >= 400) {
|
|
1307
|
-
reject(new Error(`
|
|
52
|
+
reject(new Error(`Daemon HTTP ${res.statusCode}: ${raw}`));
|
|
1308
53
|
return;
|
|
1309
54
|
}
|
|
1310
55
|
try {
|
|
1311
|
-
|
|
56
|
+
resolve2(JSON.parse(raw));
|
|
1312
57
|
} catch {
|
|
1313
|
-
reject(new Error(`Invalid JSON from
|
|
58
|
+
reject(new Error(`Invalid JSON from daemon: ${raw}`));
|
|
1314
59
|
}
|
|
1315
60
|
});
|
|
1316
61
|
}
|
|
@@ -1318,64 +63,52 @@ function httpJson(method, url, token, body) {
|
|
|
1318
63
|
req.on("error", reject);
|
|
1319
64
|
req.on("timeout", () => {
|
|
1320
65
|
req.destroy();
|
|
1321
|
-
reject(new Error("
|
|
66
|
+
reject(new Error("Daemon request timed out"));
|
|
1322
67
|
});
|
|
1323
68
|
if (payload) req.write(payload);
|
|
1324
69
|
req.end();
|
|
1325
70
|
});
|
|
1326
71
|
}
|
|
1327
|
-
async function
|
|
72
|
+
async function readToken() {
|
|
1328
73
|
try {
|
|
1329
|
-
|
|
1330
|
-
const port = Number.parseInt(raw.trim(), 10);
|
|
1331
|
-
return Number.isInteger(port) && port > 0 ? port : null;
|
|
74
|
+
return (await readFile(TOKEN_FILE, "utf8")).trim();
|
|
1332
75
|
} catch {
|
|
1333
76
|
return null;
|
|
1334
77
|
}
|
|
1335
78
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
79
|
+
function getDaemonPath() {
|
|
80
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
81
|
+
const currentDir = dirname(currentFile);
|
|
82
|
+
const sameDirPath = resolve(currentDir, "daemon.js");
|
|
83
|
+
if (existsSync(sameDirPath)) {
|
|
84
|
+
return sameDirPath;
|
|
1341
85
|
}
|
|
86
|
+
return resolve(currentDir, "../../daemon/dist/index.js");
|
|
1342
87
|
}
|
|
1343
|
-
async function
|
|
1344
|
-
|
|
1345
|
-
const existingToken = await readTokenFile();
|
|
1346
|
-
if (existingPort && existingToken) {
|
|
88
|
+
async function ensureDaemon() {
|
|
89
|
+
if (daemonReady && cachedToken) {
|
|
1347
90
|
try {
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
91
|
+
await httpJson("GET", "/status", cachedToken, void 0, 2e3);
|
|
92
|
+
return;
|
|
93
|
+
} catch {
|
|
94
|
+
daemonReady = false;
|
|
95
|
+
cachedToken = null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
let token = await readToken();
|
|
99
|
+
if (token) {
|
|
100
|
+
try {
|
|
101
|
+
const status = await httpJson("GET", "/status", token, void 0, 2e3);
|
|
1353
102
|
if (status.running) {
|
|
1354
|
-
|
|
103
|
+
cachedToken = token;
|
|
104
|
+
daemonReady = true;
|
|
105
|
+
return;
|
|
1355
106
|
}
|
|
1356
107
|
} catch {
|
|
1357
108
|
}
|
|
1358
109
|
}
|
|
1359
|
-
const
|
|
1360
|
-
|
|
1361
|
-
throw new Error("Cannot start monitor: no browser connection found");
|
|
1362
|
-
}
|
|
1363
|
-
const token = randomBytes(32).toString("hex");
|
|
1364
|
-
const monitorPort = DEFAULT_MONITOR_PORT;
|
|
1365
|
-
const monitorScript = findMonitorScript();
|
|
1366
|
-
await mkdir2(MONITOR_DIR, { recursive: true });
|
|
1367
|
-
await writeFile2(TOKEN_FILE, token, { mode: 384 });
|
|
1368
|
-
const child = spawn2(process.execPath, [
|
|
1369
|
-
monitorScript,
|
|
1370
|
-
"--cdp-host",
|
|
1371
|
-
cdp.host,
|
|
1372
|
-
"--cdp-port",
|
|
1373
|
-
String(cdp.port),
|
|
1374
|
-
"--monitor-port",
|
|
1375
|
-
String(monitorPort),
|
|
1376
|
-
"--token",
|
|
1377
|
-
token
|
|
1378
|
-
], {
|
|
110
|
+
const daemonPath = getDaemonPath();
|
|
111
|
+
const child = spawn(process.execPath, [daemonPath], {
|
|
1379
112
|
detached: true,
|
|
1380
113
|
stdio: "ignore"
|
|
1381
114
|
});
|
|
@@ -1383,51 +116,43 @@ async function ensureMonitorRunning() {
|
|
|
1383
116
|
const deadline = Date.now() + 5e3;
|
|
1384
117
|
while (Date.now() < deadline) {
|
|
1385
118
|
await new Promise((r) => setTimeout(r, 200));
|
|
119
|
+
token = await readToken();
|
|
120
|
+
if (!token) continue;
|
|
1386
121
|
try {
|
|
1387
|
-
const status = await httpJson(
|
|
1388
|
-
"GET",
|
|
1389
|
-
`http://127.0.0.1:${monitorPort}/status`,
|
|
1390
|
-
token
|
|
1391
|
-
);
|
|
122
|
+
const status = await httpJson("GET", "/status", token, void 0, 2e3);
|
|
1392
123
|
if (status.running) {
|
|
1393
|
-
|
|
124
|
+
cachedToken = token;
|
|
125
|
+
daemonReady = true;
|
|
126
|
+
return;
|
|
1394
127
|
}
|
|
1395
128
|
} catch {
|
|
1396
129
|
}
|
|
1397
130
|
}
|
|
1398
|
-
throw new Error(
|
|
1399
|
-
|
|
1400
|
-
async function monitorCommand(request) {
|
|
1401
|
-
const { port, token } = await ensureMonitorRunning();
|
|
1402
|
-
return httpJson(
|
|
1403
|
-
"POST",
|
|
1404
|
-
`http://127.0.0.1:${port}/command`,
|
|
1405
|
-
token,
|
|
1406
|
-
request
|
|
131
|
+
throw new Error(
|
|
132
|
+
"bb-browser: Daemon did not start in time.\n\nMake sure Chrome is installed, then try again."
|
|
1407
133
|
);
|
|
1408
134
|
}
|
|
1409
|
-
function
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
// Development: packages/cli/src -> packages/cli/dist
|
|
1416
|
-
resolve(currentDir, "../dist/cdp-monitor.js"),
|
|
1417
|
-
// Monorepo root dist
|
|
1418
|
-
resolve(currentDir, "../../dist/cdp-monitor.js"),
|
|
1419
|
-
resolve(currentDir, "../../../dist/cdp-monitor.js")
|
|
1420
|
-
];
|
|
1421
|
-
for (const candidate of candidates) {
|
|
1422
|
-
if (existsSync2(candidate)) {
|
|
1423
|
-
return candidate;
|
|
1424
|
-
}
|
|
135
|
+
async function daemonCommand(request) {
|
|
136
|
+
if (!cachedToken) {
|
|
137
|
+
cachedToken = await readToken();
|
|
138
|
+
}
|
|
139
|
+
if (!cachedToken) {
|
|
140
|
+
throw new Error("No daemon token found. Is the daemon running?");
|
|
1425
141
|
}
|
|
1426
|
-
return
|
|
142
|
+
return httpJson("POST", "/command", cachedToken, request, COMMAND_TIMEOUT);
|
|
1427
143
|
}
|
|
144
|
+
async function getDaemonStatus() {
|
|
145
|
+
const token = cachedToken ?? await readToken();
|
|
146
|
+
if (!token) return null;
|
|
147
|
+
try {
|
|
148
|
+
return await httpJson("GET", "/status", token, void 0, 2e3);
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
var ensureDaemonRunning = ensureDaemon;
|
|
1428
154
|
|
|
1429
155
|
// packages/cli/src/client.ts
|
|
1430
|
-
var MONITOR_ACTIONS = /* @__PURE__ */ new Set(["network", "console", "errors", "trace"]);
|
|
1431
156
|
var jqExpression;
|
|
1432
157
|
function setJqExpression(expression) {
|
|
1433
158
|
jqExpression = expression;
|
|
@@ -1445,43 +170,14 @@ function handleJqResponse(response) {
|
|
|
1445
170
|
printJqResults(response);
|
|
1446
171
|
}
|
|
1447
172
|
}
|
|
1448
|
-
async function
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
return await monitorCommand(request);
|
|
1452
|
-
} catch {
|
|
1453
|
-
return sendCommand(request);
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
return sendCommand(request);
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
// packages/cli/src/daemon-manager.ts
|
|
1460
|
-
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1461
|
-
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
1462
|
-
import { existsSync as existsSync3 } from "fs";
|
|
1463
|
-
async function isDaemonRunning() {
|
|
1464
|
-
return await isManagedBrowserRunning();
|
|
1465
|
-
}
|
|
1466
|
-
async function ensureDaemonRunning() {
|
|
1467
|
-
try {
|
|
1468
|
-
await ensureCdpConnection();
|
|
1469
|
-
} catch (error) {
|
|
1470
|
-
if (error instanceof Error && error.message.includes("No browser connection found")) {
|
|
1471
|
-
throw new Error([
|
|
1472
|
-
"bb-browser: Could not start browser.",
|
|
1473
|
-
"",
|
|
1474
|
-
"Make sure Chrome is installed, then try again.",
|
|
1475
|
-
"Or specify a CDP port manually: bb-browser --port 9222"
|
|
1476
|
-
].join("\n"));
|
|
1477
|
-
}
|
|
1478
|
-
throw error;
|
|
1479
|
-
}
|
|
173
|
+
async function sendCommand(request) {
|
|
174
|
+
await ensureDaemon();
|
|
175
|
+
return daemonCommand(request);
|
|
1480
176
|
}
|
|
1481
177
|
|
|
1482
178
|
// packages/cli/src/history-sqlite.ts
|
|
1483
|
-
import { copyFileSync, existsSync as
|
|
1484
|
-
import { execSync
|
|
179
|
+
import { copyFileSync, existsSync as existsSync2, unlinkSync } from "fs";
|
|
180
|
+
import { execSync } from "child_process";
|
|
1485
181
|
import { homedir, tmpdir } from "os";
|
|
1486
182
|
import { join } from "path";
|
|
1487
183
|
function getHistoryPathCandidates() {
|
|
@@ -1504,7 +200,7 @@ function getHistoryPathCandidates() {
|
|
|
1504
200
|
}
|
|
1505
201
|
function findHistoryPath() {
|
|
1506
202
|
for (const historyPath of getHistoryPathCandidates()) {
|
|
1507
|
-
if (
|
|
203
|
+
if (existsSync2(historyPath)) {
|
|
1508
204
|
return historyPath;
|
|
1509
205
|
}
|
|
1510
206
|
}
|
|
@@ -1529,7 +225,7 @@ function runHistoryQuery(sql, mapRow) {
|
|
|
1529
225
|
copyFileSync(historyPath, tmpPath);
|
|
1530
226
|
const escapedTmpPath = tmpPath.replace(/"/g, '\\"');
|
|
1531
227
|
const escapedSql = sql.replace(/"/g, '\\"');
|
|
1532
|
-
const output =
|
|
228
|
+
const output = execSync(`sqlite3 -separator $'\\t' "${escapedTmpPath}" "${escapedSql}"`, {
|
|
1533
229
|
encoding: "utf-8",
|
|
1534
230
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1535
231
|
});
|
|
@@ -1538,7 +234,7 @@ function runHistoryQuery(sql, mapRow) {
|
|
|
1538
234
|
return [];
|
|
1539
235
|
} finally {
|
|
1540
236
|
try {
|
|
1541
|
-
|
|
237
|
+
unlinkSync(tmpPath);
|
|
1542
238
|
} catch {
|
|
1543
239
|
}
|
|
1544
240
|
}
|
|
@@ -1624,18 +320,18 @@ function getHistoryDomains(days) {
|
|
|
1624
320
|
}
|
|
1625
321
|
|
|
1626
322
|
// packages/cli/src/commands/site.ts
|
|
1627
|
-
import { readFileSync
|
|
323
|
+
import { readFileSync, readdirSync, existsSync as existsSync3, mkdirSync } from "fs";
|
|
1628
324
|
import { join as join2, relative } from "path";
|
|
1629
325
|
import { homedir as homedir2 } from "os";
|
|
1630
|
-
import { execSync as
|
|
326
|
+
import { execSync as execSync2 } from "child_process";
|
|
1631
327
|
var BB_DIR = join2(homedir2(), ".bb-browser");
|
|
1632
328
|
var LOCAL_SITES_DIR = join2(BB_DIR, "sites");
|
|
1633
329
|
var COMMUNITY_SITES_DIR = join2(BB_DIR, "bb-sites");
|
|
1634
330
|
var COMMUNITY_REPO = "https://github.com/epiral/bb-sites.git";
|
|
1635
331
|
function checkCliUpdate() {
|
|
1636
332
|
try {
|
|
1637
|
-
const current =
|
|
1638
|
-
const latest =
|
|
333
|
+
const current = execSync2("bb-browser --version", { timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
|
|
334
|
+
const latest = execSync2("npm view bb-browser version", { timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
|
|
1639
335
|
if (latest && current && latest !== current && latest.localeCompare(current, void 0, { numeric: true }) > 0) {
|
|
1640
336
|
console.log(`
|
|
1641
337
|
\u{1F4E6} bb-browser ${latest} available (current: ${current}). Run: npm install -g bb-browser`);
|
|
@@ -1650,7 +346,7 @@ function exitJsonError(error, extra = {}) {
|
|
|
1650
346
|
function parseSiteMeta(filePath, source) {
|
|
1651
347
|
let content;
|
|
1652
348
|
try {
|
|
1653
|
-
content =
|
|
349
|
+
content = readFileSync(filePath, "utf-8");
|
|
1654
350
|
} catch {
|
|
1655
351
|
return null;
|
|
1656
352
|
}
|
|
@@ -1710,7 +406,7 @@ function parseSiteMeta(filePath, source) {
|
|
|
1710
406
|
return meta;
|
|
1711
407
|
}
|
|
1712
408
|
function scanSites(dir, source) {
|
|
1713
|
-
if (!
|
|
409
|
+
if (!existsSync3(dir)) return [];
|
|
1714
410
|
const sites = [];
|
|
1715
411
|
function walk(currentDir) {
|
|
1716
412
|
let entries;
|
|
@@ -1832,13 +528,13 @@ function siteSearch(query, options) {
|
|
|
1832
528
|
}
|
|
1833
529
|
function siteUpdate(options = {}) {
|
|
1834
530
|
mkdirSync(BB_DIR, { recursive: true });
|
|
1835
|
-
const updateMode =
|
|
531
|
+
const updateMode = existsSync3(join2(COMMUNITY_SITES_DIR, ".git")) ? "pull" : "clone";
|
|
1836
532
|
if (updateMode === "pull") {
|
|
1837
533
|
if (!options.json) {
|
|
1838
534
|
console.log("\u66F4\u65B0\u793E\u533A site adapter \u5E93...");
|
|
1839
535
|
}
|
|
1840
536
|
try {
|
|
1841
|
-
|
|
537
|
+
execSync2("git pull --ff-only", { cwd: COMMUNITY_SITES_DIR, stdio: "pipe" });
|
|
1842
538
|
if (!options.json) {
|
|
1843
539
|
console.log("\u66F4\u65B0\u5B8C\u6210\u3002");
|
|
1844
540
|
console.log("");
|
|
@@ -1859,7 +555,7 @@ function siteUpdate(options = {}) {
|
|
|
1859
555
|
console.log(`\u514B\u9686\u793E\u533A adapter \u5E93: ${COMMUNITY_REPO}`);
|
|
1860
556
|
}
|
|
1861
557
|
try {
|
|
1862
|
-
|
|
558
|
+
execSync2(`git clone ${COMMUNITY_REPO} ${COMMUNITY_SITES_DIR}`, { stdio: "pipe" });
|
|
1863
559
|
if (!options.json) {
|
|
1864
560
|
console.log("\u514B\u9686\u5B8C\u6210\u3002");
|
|
1865
561
|
console.log("");
|
|
@@ -2068,12 +764,12 @@ async function siteRun(name, args, options) {
|
|
|
2068
764
|
process.exit(1);
|
|
2069
765
|
}
|
|
2070
766
|
}
|
|
2071
|
-
const jsContent =
|
|
767
|
+
const jsContent = readFileSync(site.filePath, "utf-8");
|
|
2072
768
|
const jsBody = jsContent.replace(/\/\*\s*@meta[\s\S]*?\*\//, "").trim();
|
|
2073
769
|
const argsJson = JSON.stringify(argMap);
|
|
2074
770
|
const script = `(${jsBody})(${argsJson})`;
|
|
2075
771
|
if (options.openclaw) {
|
|
2076
|
-
const { ocGetTabs, ocFindTabByDomain, ocOpenTab, ocEvaluate } = await import("./openclaw-bridge-
|
|
772
|
+
const { ocGetTabs, ocFindTabByDomain, ocOpenTab, ocEvaluate } = await import("./openclaw-bridge-Q6EFUQCH.js");
|
|
2077
773
|
let targetId;
|
|
2078
774
|
if (site.domain) {
|
|
2079
775
|
const tabs = ocGetTabs();
|
|
@@ -2082,7 +778,7 @@ async function siteRun(name, args, options) {
|
|
|
2082
778
|
targetId = existing.targetId;
|
|
2083
779
|
} else {
|
|
2084
780
|
targetId = ocOpenTab(`https://${site.domain}`);
|
|
2085
|
-
await new Promise((
|
|
781
|
+
await new Promise((resolve2) => setTimeout(resolve2, 3e3));
|
|
2086
782
|
}
|
|
2087
783
|
} else {
|
|
2088
784
|
const tabs = ocGetTabs();
|
|
@@ -2128,7 +824,7 @@ async function siteRun(name, args, options) {
|
|
|
2128
824
|
let targetTabId = options.tabId;
|
|
2129
825
|
if (!targetTabId && site.domain) {
|
|
2130
826
|
const listReq = { id: generateId(), action: "tab_list" };
|
|
2131
|
-
const listResp = await
|
|
827
|
+
const listResp = await sendCommand(listReq);
|
|
2132
828
|
if (listResp.success && listResp.data?.tabs) {
|
|
2133
829
|
const matchingTab = listResp.data.tabs.find(
|
|
2134
830
|
(tab) => matchTabOrigin(tab.url, site.domain)
|
|
@@ -2138,17 +834,17 @@ async function siteRun(name, args, options) {
|
|
|
2138
834
|
}
|
|
2139
835
|
}
|
|
2140
836
|
if (!targetTabId) {
|
|
2141
|
-
const newResp = await
|
|
837
|
+
const newResp = await sendCommand({
|
|
2142
838
|
id: generateId(),
|
|
2143
839
|
action: "tab_new",
|
|
2144
840
|
url: `https://${site.domain}`
|
|
2145
841
|
});
|
|
2146
842
|
targetTabId = newResp.data?.tabId;
|
|
2147
|
-
await new Promise((
|
|
843
|
+
await new Promise((resolve2) => setTimeout(resolve2, 3e3));
|
|
2148
844
|
}
|
|
2149
845
|
}
|
|
2150
846
|
const evalReq = { id: generateId(), action: "eval", script, tabId: targetTabId };
|
|
2151
|
-
const evalResp = await
|
|
847
|
+
const evalResp = await sendCommand(evalReq);
|
|
2152
848
|
if (!evalResp.success) {
|
|
2153
849
|
const hint = site.domain ? `Open https://${site.domain} in your browser, make sure you are logged in, then retry.` : void 0;
|
|
2154
850
|
if (options.json) {
|
|
@@ -2284,9 +980,9 @@ async function siteCommand(args, options = {}) {
|
|
|
2284
980
|
}
|
|
2285
981
|
function silentUpdate() {
|
|
2286
982
|
const gitDir = join2(COMMUNITY_SITES_DIR, ".git");
|
|
2287
|
-
if (!
|
|
2288
|
-
import("child_process").then(({ spawn:
|
|
2289
|
-
const child =
|
|
983
|
+
if (!existsSync3(gitDir)) return;
|
|
984
|
+
import("child_process").then(({ spawn: spawn2 }) => {
|
|
985
|
+
const child = spawn2("git", ["pull", "--ff-only"], {
|
|
2290
986
|
cwd: COMMUNITY_SITES_DIR,
|
|
2291
987
|
stdio: "ignore",
|
|
2292
988
|
detached: true
|
|
@@ -2322,7 +1018,7 @@ async function openCommand(url, options = {}) {
|
|
|
2322
1018
|
request.tabId = tabId;
|
|
2323
1019
|
}
|
|
2324
1020
|
}
|
|
2325
|
-
const response = await
|
|
1021
|
+
const response = await sendCommand(request);
|
|
2326
1022
|
if (options.json) {
|
|
2327
1023
|
console.log(JSON.stringify(response, null, 2));
|
|
2328
1024
|
} else {
|
|
@@ -2358,7 +1054,7 @@ async function snapshotCommand(options = {}) {
|
|
|
2358
1054
|
selector: options.selector,
|
|
2359
1055
|
tabId: options.tabId
|
|
2360
1056
|
};
|
|
2361
|
-
const response = await
|
|
1057
|
+
const response = await sendCommand(request);
|
|
2362
1058
|
if (options.json) {
|
|
2363
1059
|
console.log(JSON.stringify(response, null, 2));
|
|
2364
1060
|
} else {
|
|
@@ -2377,7 +1073,7 @@ async function snapshotCommand(options = {}) {
|
|
|
2377
1073
|
}
|
|
2378
1074
|
|
|
2379
1075
|
// packages/cli/src/commands/click.ts
|
|
2380
|
-
function
|
|
1076
|
+
function parseRef(ref) {
|
|
2381
1077
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
2382
1078
|
}
|
|
2383
1079
|
async function clickCommand(ref, options = {}) {
|
|
@@ -2385,14 +1081,14 @@ async function clickCommand(ref, options = {}) {
|
|
|
2385
1081
|
throw new Error("\u7F3A\u5C11 ref \u53C2\u6570");
|
|
2386
1082
|
}
|
|
2387
1083
|
await ensureDaemonRunning();
|
|
2388
|
-
const parsedRef =
|
|
1084
|
+
const parsedRef = parseRef(ref);
|
|
2389
1085
|
const request = {
|
|
2390
1086
|
id: generateId(),
|
|
2391
1087
|
action: "click",
|
|
2392
1088
|
ref: parsedRef,
|
|
2393
1089
|
tabId: options.tabId
|
|
2394
1090
|
};
|
|
2395
|
-
const response = await
|
|
1091
|
+
const response = await sendCommand(request);
|
|
2396
1092
|
if (options.json) {
|
|
2397
1093
|
console.log(JSON.stringify(response, null, 2));
|
|
2398
1094
|
} else {
|
|
@@ -2412,7 +1108,7 @@ async function clickCommand(ref, options = {}) {
|
|
|
2412
1108
|
}
|
|
2413
1109
|
|
|
2414
1110
|
// packages/cli/src/commands/hover.ts
|
|
2415
|
-
function
|
|
1111
|
+
function parseRef2(ref) {
|
|
2416
1112
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
2417
1113
|
}
|
|
2418
1114
|
async function hoverCommand(ref, options = {}) {
|
|
@@ -2420,14 +1116,14 @@ async function hoverCommand(ref, options = {}) {
|
|
|
2420
1116
|
throw new Error("\u7F3A\u5C11 ref \u53C2\u6570");
|
|
2421
1117
|
}
|
|
2422
1118
|
await ensureDaemonRunning();
|
|
2423
|
-
const parsedRef =
|
|
1119
|
+
const parsedRef = parseRef2(ref);
|
|
2424
1120
|
const request = {
|
|
2425
1121
|
id: generateId(),
|
|
2426
1122
|
action: "hover",
|
|
2427
1123
|
ref: parsedRef,
|
|
2428
1124
|
tabId: options.tabId
|
|
2429
1125
|
};
|
|
2430
|
-
const response = await
|
|
1126
|
+
const response = await sendCommand(request);
|
|
2431
1127
|
if (options.json) {
|
|
2432
1128
|
console.log(JSON.stringify(response, null, 2));
|
|
2433
1129
|
} else {
|
|
@@ -2447,7 +1143,7 @@ async function hoverCommand(ref, options = {}) {
|
|
|
2447
1143
|
}
|
|
2448
1144
|
|
|
2449
1145
|
// packages/cli/src/commands/fill.ts
|
|
2450
|
-
function
|
|
1146
|
+
function parseRef3(ref) {
|
|
2451
1147
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
2452
1148
|
}
|
|
2453
1149
|
async function fillCommand(ref, text, options = {}) {
|
|
@@ -2458,7 +1154,7 @@ async function fillCommand(ref, text, options = {}) {
|
|
|
2458
1154
|
throw new Error("\u7F3A\u5C11 text \u53C2\u6570");
|
|
2459
1155
|
}
|
|
2460
1156
|
await ensureDaemonRunning();
|
|
2461
|
-
const parsedRef =
|
|
1157
|
+
const parsedRef = parseRef3(ref);
|
|
2462
1158
|
const request = {
|
|
2463
1159
|
id: generateId(),
|
|
2464
1160
|
action: "fill",
|
|
@@ -2466,7 +1162,7 @@ async function fillCommand(ref, text, options = {}) {
|
|
|
2466
1162
|
text,
|
|
2467
1163
|
tabId: options.tabId
|
|
2468
1164
|
};
|
|
2469
|
-
const response = await
|
|
1165
|
+
const response = await sendCommand(request);
|
|
2470
1166
|
if (options.json) {
|
|
2471
1167
|
console.log(JSON.stringify(response, null, 2));
|
|
2472
1168
|
} else {
|
|
@@ -2487,7 +1183,7 @@ async function fillCommand(ref, text, options = {}) {
|
|
|
2487
1183
|
}
|
|
2488
1184
|
|
|
2489
1185
|
// packages/cli/src/commands/type.ts
|
|
2490
|
-
function
|
|
1186
|
+
function parseRef4(ref) {
|
|
2491
1187
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
2492
1188
|
}
|
|
2493
1189
|
async function typeCommand(ref, text, options = {}) {
|
|
@@ -2498,7 +1194,7 @@ async function typeCommand(ref, text, options = {}) {
|
|
|
2498
1194
|
throw new Error("\u7F3A\u5C11 text \u53C2\u6570");
|
|
2499
1195
|
}
|
|
2500
1196
|
await ensureDaemonRunning();
|
|
2501
|
-
const parsedRef =
|
|
1197
|
+
const parsedRef = parseRef4(ref);
|
|
2502
1198
|
const request = {
|
|
2503
1199
|
id: generateId(),
|
|
2504
1200
|
action: "type",
|
|
@@ -2506,7 +1202,7 @@ async function typeCommand(ref, text, options = {}) {
|
|
|
2506
1202
|
text,
|
|
2507
1203
|
tabId: options.tabId
|
|
2508
1204
|
};
|
|
2509
|
-
const response = await
|
|
1205
|
+
const response = await sendCommand(request);
|
|
2510
1206
|
if (options.json) {
|
|
2511
1207
|
console.log(JSON.stringify(response, null, 2));
|
|
2512
1208
|
} else {
|
|
@@ -2534,7 +1230,7 @@ async function closeCommand(options = {}) {
|
|
|
2534
1230
|
action: "close",
|
|
2535
1231
|
tabId: options.tabId
|
|
2536
1232
|
};
|
|
2537
|
-
const response = await
|
|
1233
|
+
const response = await sendCommand(request);
|
|
2538
1234
|
if (options.json) {
|
|
2539
1235
|
console.log(JSON.stringify(response, null, 2));
|
|
2540
1236
|
} else {
|
|
@@ -2553,7 +1249,7 @@ async function closeCommand(options = {}) {
|
|
|
2553
1249
|
}
|
|
2554
1250
|
|
|
2555
1251
|
// packages/cli/src/commands/get.ts
|
|
2556
|
-
function
|
|
1252
|
+
function parseRef5(ref) {
|
|
2557
1253
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
2558
1254
|
}
|
|
2559
1255
|
async function getCommand(attribute, ref, options = {}) {
|
|
@@ -2565,10 +1261,10 @@ async function getCommand(attribute, ref, options = {}) {
|
|
|
2565
1261
|
id: generateId(),
|
|
2566
1262
|
action: "get",
|
|
2567
1263
|
attribute,
|
|
2568
|
-
ref: ref ?
|
|
1264
|
+
ref: ref ? parseRef5(ref) : void 0,
|
|
2569
1265
|
tabId: options.tabId
|
|
2570
1266
|
};
|
|
2571
|
-
const response = await
|
|
1267
|
+
const response = await sendCommand(request);
|
|
2572
1268
|
if (options.json) {
|
|
2573
1269
|
console.log(JSON.stringify(response, null, 2));
|
|
2574
1270
|
} else {
|
|
@@ -2584,17 +1280,17 @@ async function getCommand(attribute, ref, options = {}) {
|
|
|
2584
1280
|
|
|
2585
1281
|
// packages/cli/src/commands/screenshot.ts
|
|
2586
1282
|
import fs from "fs";
|
|
2587
|
-
import
|
|
2588
|
-
import
|
|
1283
|
+
import path2 from "path";
|
|
1284
|
+
import os2 from "os";
|
|
2589
1285
|
function getDefaultPath() {
|
|
2590
1286
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2591
1287
|
const filename = `bb-screenshot-${timestamp}.png`;
|
|
2592
|
-
return
|
|
1288
|
+
return path2.join(os2.tmpdir(), filename);
|
|
2593
1289
|
}
|
|
2594
1290
|
function saveBase64Image(dataUrl, filePath) {
|
|
2595
1291
|
const base64Data = dataUrl.replace(/^data:image\/png;base64,/, "");
|
|
2596
1292
|
const buffer = Buffer.from(base64Data, "base64");
|
|
2597
|
-
const dir =
|
|
1293
|
+
const dir = path2.dirname(filePath);
|
|
2598
1294
|
if (!fs.existsSync(dir)) {
|
|
2599
1295
|
fs.mkdirSync(dir, { recursive: true });
|
|
2600
1296
|
}
|
|
@@ -2602,13 +1298,13 @@ function saveBase64Image(dataUrl, filePath) {
|
|
|
2602
1298
|
}
|
|
2603
1299
|
async function screenshotCommand(outputPath, options = {}) {
|
|
2604
1300
|
await ensureDaemonRunning();
|
|
2605
|
-
const filePath = outputPath ?
|
|
1301
|
+
const filePath = outputPath ? path2.resolve(outputPath) : getDefaultPath();
|
|
2606
1302
|
const request = {
|
|
2607
1303
|
id: generateId(),
|
|
2608
1304
|
action: "screenshot",
|
|
2609
1305
|
tabId: options.tabId
|
|
2610
1306
|
};
|
|
2611
|
-
const response = await
|
|
1307
|
+
const response = await sendCommand(request);
|
|
2612
1308
|
if (response.success && response.data?.dataUrl) {
|
|
2613
1309
|
const dataUrl = response.data.dataUrl;
|
|
2614
1310
|
saveBase64Image(dataUrl, filePath);
|
|
@@ -2635,7 +1331,7 @@ async function screenshotCommand(outputPath, options = {}) {
|
|
|
2635
1331
|
function isTimeWait(target) {
|
|
2636
1332
|
return /^\d+$/.test(target);
|
|
2637
1333
|
}
|
|
2638
|
-
function
|
|
1334
|
+
function parseRef6(ref) {
|
|
2639
1335
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
2640
1336
|
}
|
|
2641
1337
|
async function waitCommand(target, options = {}) {
|
|
@@ -2654,7 +1350,7 @@ async function waitCommand(target, options = {}) {
|
|
|
2654
1350
|
tabId: options.tabId
|
|
2655
1351
|
};
|
|
2656
1352
|
} else {
|
|
2657
|
-
const ref =
|
|
1353
|
+
const ref = parseRef6(target);
|
|
2658
1354
|
request = {
|
|
2659
1355
|
id: generateId(),
|
|
2660
1356
|
action: "wait",
|
|
@@ -2663,7 +1359,7 @@ async function waitCommand(target, options = {}) {
|
|
|
2663
1359
|
tabId: options.tabId
|
|
2664
1360
|
};
|
|
2665
1361
|
}
|
|
2666
|
-
const response = await
|
|
1362
|
+
const response = await sendCommand(request);
|
|
2667
1363
|
if (options.json) {
|
|
2668
1364
|
console.log(JSON.stringify(response, null, 2));
|
|
2669
1365
|
} else {
|
|
@@ -2671,7 +1367,7 @@ async function waitCommand(target, options = {}) {
|
|
|
2671
1367
|
if (isTimeWait(target)) {
|
|
2672
1368
|
console.log(`\u5DF2\u7B49\u5F85 ${target}ms`);
|
|
2673
1369
|
} else {
|
|
2674
|
-
console.log(`\u5143\u7D20 @${
|
|
1370
|
+
console.log(`\u5143\u7D20 @${parseRef6(target)} \u5DF2\u51FA\u73B0`);
|
|
2675
1371
|
}
|
|
2676
1372
|
} else {
|
|
2677
1373
|
console.error(`\u9519\u8BEF: ${response.error}`);
|
|
@@ -2711,7 +1407,7 @@ async function pressCommand(keyString, options = {}) {
|
|
|
2711
1407
|
modifiers,
|
|
2712
1408
|
tabId: options.tabId
|
|
2713
1409
|
};
|
|
2714
|
-
const response = await
|
|
1410
|
+
const response = await sendCommand(request);
|
|
2715
1411
|
if (options.json) {
|
|
2716
1412
|
console.log(JSON.stringify(response, null, 2));
|
|
2717
1413
|
} else {
|
|
@@ -2752,7 +1448,7 @@ async function scrollCommand(direction, pixels, options = {}) {
|
|
|
2752
1448
|
pixels: pixelValue,
|
|
2753
1449
|
tabId: options.tabId
|
|
2754
1450
|
};
|
|
2755
|
-
const response = await
|
|
1451
|
+
const response = await sendCommand(request);
|
|
2756
1452
|
if (options.json) {
|
|
2757
1453
|
console.log(JSON.stringify(response, null, 2));
|
|
2758
1454
|
} else {
|
|
@@ -2773,7 +1469,7 @@ async function backCommand(options = {}) {
|
|
|
2773
1469
|
action: "back",
|
|
2774
1470
|
tabId: options.tabId
|
|
2775
1471
|
};
|
|
2776
|
-
const response = await
|
|
1472
|
+
const response = await sendCommand(request);
|
|
2777
1473
|
if (options.json) {
|
|
2778
1474
|
console.log(JSON.stringify(response, null, 2));
|
|
2779
1475
|
} else {
|
|
@@ -2797,7 +1493,7 @@ async function forwardCommand(options = {}) {
|
|
|
2797
1493
|
action: "forward",
|
|
2798
1494
|
tabId: options.tabId
|
|
2799
1495
|
};
|
|
2800
|
-
const response = await
|
|
1496
|
+
const response = await sendCommand(request);
|
|
2801
1497
|
if (options.json) {
|
|
2802
1498
|
console.log(JSON.stringify(response, null, 2));
|
|
2803
1499
|
} else {
|
|
@@ -2821,7 +1517,7 @@ async function refreshCommand(options = {}) {
|
|
|
2821
1517
|
action: "refresh",
|
|
2822
1518
|
tabId: options.tabId
|
|
2823
1519
|
};
|
|
2824
|
-
const response = await
|
|
1520
|
+
const response = await sendCommand(request);
|
|
2825
1521
|
if (options.json) {
|
|
2826
1522
|
console.log(JSON.stringify(response, null, 2));
|
|
2827
1523
|
} else {
|
|
@@ -2840,7 +1536,7 @@ async function refreshCommand(options = {}) {
|
|
|
2840
1536
|
}
|
|
2841
1537
|
|
|
2842
1538
|
// packages/cli/src/commands/check.ts
|
|
2843
|
-
function
|
|
1539
|
+
function parseRef7(ref) {
|
|
2844
1540
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
2845
1541
|
}
|
|
2846
1542
|
async function checkCommand(ref, options = {}) {
|
|
@@ -2848,14 +1544,14 @@ async function checkCommand(ref, options = {}) {
|
|
|
2848
1544
|
throw new Error("\u7F3A\u5C11 ref \u53C2\u6570");
|
|
2849
1545
|
}
|
|
2850
1546
|
await ensureDaemonRunning();
|
|
2851
|
-
const parsedRef =
|
|
1547
|
+
const parsedRef = parseRef7(ref);
|
|
2852
1548
|
const request = {
|
|
2853
1549
|
id: generateId(),
|
|
2854
1550
|
action: "check",
|
|
2855
1551
|
ref: parsedRef,
|
|
2856
1552
|
tabId: options.tabId
|
|
2857
1553
|
};
|
|
2858
|
-
const response = await
|
|
1554
|
+
const response = await sendCommand(request);
|
|
2859
1555
|
if (options.json) {
|
|
2860
1556
|
console.log(JSON.stringify(response, null, 2));
|
|
2861
1557
|
} else {
|
|
@@ -2887,14 +1583,14 @@ async function uncheckCommand(ref, options = {}) {
|
|
|
2887
1583
|
throw new Error("\u7F3A\u5C11 ref \u53C2\u6570");
|
|
2888
1584
|
}
|
|
2889
1585
|
await ensureDaemonRunning();
|
|
2890
|
-
const parsedRef =
|
|
1586
|
+
const parsedRef = parseRef7(ref);
|
|
2891
1587
|
const request = {
|
|
2892
1588
|
id: generateId(),
|
|
2893
1589
|
action: "uncheck",
|
|
2894
1590
|
ref: parsedRef,
|
|
2895
1591
|
tabId: options.tabId
|
|
2896
1592
|
};
|
|
2897
|
-
const response = await
|
|
1593
|
+
const response = await sendCommand(request);
|
|
2898
1594
|
if (options.json) {
|
|
2899
1595
|
console.log(JSON.stringify(response, null, 2));
|
|
2900
1596
|
} else {
|
|
@@ -2923,7 +1619,7 @@ async function uncheckCommand(ref, options = {}) {
|
|
|
2923
1619
|
}
|
|
2924
1620
|
|
|
2925
1621
|
// packages/cli/src/commands/select.ts
|
|
2926
|
-
function
|
|
1622
|
+
function parseRef8(ref) {
|
|
2927
1623
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
2928
1624
|
}
|
|
2929
1625
|
async function selectCommand(ref, value, options = {}) {
|
|
@@ -2934,7 +1630,7 @@ async function selectCommand(ref, value, options = {}) {
|
|
|
2934
1630
|
throw new Error("\u7F3A\u5C11 value \u53C2\u6570");
|
|
2935
1631
|
}
|
|
2936
1632
|
await ensureDaemonRunning();
|
|
2937
|
-
const parsedRef =
|
|
1633
|
+
const parsedRef = parseRef8(ref);
|
|
2938
1634
|
const request = {
|
|
2939
1635
|
id: generateId(),
|
|
2940
1636
|
action: "select",
|
|
@@ -2942,7 +1638,7 @@ async function selectCommand(ref, value, options = {}) {
|
|
|
2942
1638
|
value,
|
|
2943
1639
|
tabId: options.tabId
|
|
2944
1640
|
};
|
|
2945
|
-
const response = await
|
|
1641
|
+
const response = await sendCommand(request);
|
|
2946
1642
|
if (options.json) {
|
|
2947
1643
|
console.log(JSON.stringify(response, null, 2));
|
|
2948
1644
|
} else {
|
|
@@ -2980,7 +1676,7 @@ async function evalCommand(script, options = {}) {
|
|
|
2980
1676
|
script,
|
|
2981
1677
|
tabId: options.tabId
|
|
2982
1678
|
};
|
|
2983
|
-
const response = await
|
|
1679
|
+
const response = await sendCommand(request);
|
|
2984
1680
|
if (options.json) {
|
|
2985
1681
|
console.log(JSON.stringify(response, null, 2));
|
|
2986
1682
|
} else {
|
|
@@ -3063,6 +1759,12 @@ function formatTabList(tabs, activeIndex) {
|
|
|
3063
1759
|
async function tabCommand(args, options = {}) {
|
|
3064
1760
|
await ensureDaemonRunning();
|
|
3065
1761
|
const parsed = parseTabSubcommand(args, process.argv);
|
|
1762
|
+
if (options.globalTabId && parsed.tabId === void 0 && parsed.index === void 0) {
|
|
1763
|
+
if (parsed.action === "tab_close" || parsed.action === "tab_select") {
|
|
1764
|
+
const numId = parseInt(options.globalTabId, 10);
|
|
1765
|
+
parsed.tabId = isNaN(numId) ? options.globalTabId : numId;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
3066
1768
|
const request = {
|
|
3067
1769
|
id: generateId(),
|
|
3068
1770
|
action: parsed.action,
|
|
@@ -3070,7 +1772,7 @@ async function tabCommand(args, options = {}) {
|
|
|
3070
1772
|
index: parsed.index,
|
|
3071
1773
|
tabId: parsed.tabId
|
|
3072
1774
|
};
|
|
3073
|
-
const response = await
|
|
1775
|
+
const response = await sendCommand(request);
|
|
3074
1776
|
if (options.json) {
|
|
3075
1777
|
console.log(JSON.stringify(response, null, 2));
|
|
3076
1778
|
} else {
|
|
@@ -3119,7 +1821,7 @@ async function frameCommand(selector, options = {}) {
|
|
|
3119
1821
|
selector,
|
|
3120
1822
|
tabId: options.tabId
|
|
3121
1823
|
};
|
|
3122
|
-
const response = await
|
|
1824
|
+
const response = await sendCommand(request);
|
|
3123
1825
|
if (options.json) {
|
|
3124
1826
|
console.log(JSON.stringify(response, null, 2));
|
|
3125
1827
|
} else {
|
|
@@ -3143,7 +1845,7 @@ async function frameMainCommand(options = {}) {
|
|
|
3143
1845
|
action: "frame_main",
|
|
3144
1846
|
tabId: options.tabId
|
|
3145
1847
|
};
|
|
3146
|
-
const response = await
|
|
1848
|
+
const response = await sendCommand(request);
|
|
3147
1849
|
if (options.json) {
|
|
3148
1850
|
console.log(JSON.stringify(response, null, 2));
|
|
3149
1851
|
} else {
|
|
@@ -3169,7 +1871,7 @@ async function dialogCommand(subCommand, promptText, options = {}) {
|
|
|
3169
1871
|
promptText: subCommand === "accept" ? promptText : void 0,
|
|
3170
1872
|
tabId: options.tabId
|
|
3171
1873
|
};
|
|
3172
|
-
const response = await
|
|
1874
|
+
const response = await sendCommand(request);
|
|
3173
1875
|
if (options.json) {
|
|
3174
1876
|
console.log(JSON.stringify(response, null, 2));
|
|
3175
1877
|
} else {
|
|
@@ -3190,7 +1892,12 @@ async function dialogCommand(subCommand, promptText, options = {}) {
|
|
|
3190
1892
|
|
|
3191
1893
|
// packages/cli/src/commands/network.ts
|
|
3192
1894
|
async function networkCommand(subCommand, urlOrFilter, options = {}) {
|
|
3193
|
-
|
|
1895
|
+
let since;
|
|
1896
|
+
if (subCommand === "requests" && options.since) {
|
|
1897
|
+
const num = parseInt(options.since, 10);
|
|
1898
|
+
since = !isNaN(num) && String(num) === options.since ? num : options.since;
|
|
1899
|
+
}
|
|
1900
|
+
const request = {
|
|
3194
1901
|
id: generateId(),
|
|
3195
1902
|
action: "network",
|
|
3196
1903
|
networkCommand: subCommand,
|
|
@@ -3201,8 +1908,12 @@ async function networkCommand(subCommand, urlOrFilter, options = {}) {
|
|
|
3201
1908
|
body: options.body
|
|
3202
1909
|
} : void 0,
|
|
3203
1910
|
withBody: subCommand === "requests" ? options.withBody : void 0,
|
|
1911
|
+
since,
|
|
1912
|
+
method: subCommand === "requests" ? options.method : void 0,
|
|
1913
|
+
status: subCommand === "requests" ? options.status : void 0,
|
|
3204
1914
|
tabId: options.tabId
|
|
3205
|
-
}
|
|
1915
|
+
};
|
|
1916
|
+
const response = await sendCommand(request);
|
|
3206
1917
|
if (options.json) {
|
|
3207
1918
|
console.log(JSON.stringify(response));
|
|
3208
1919
|
return;
|
|
@@ -3277,12 +1988,19 @@ async function networkCommand(subCommand, urlOrFilter, options = {}) {
|
|
|
3277
1988
|
|
|
3278
1989
|
// packages/cli/src/commands/console.ts
|
|
3279
1990
|
async function consoleCommand(options = {}) {
|
|
3280
|
-
|
|
1991
|
+
let since;
|
|
1992
|
+
if (options.since) {
|
|
1993
|
+
const num = parseInt(options.since, 10);
|
|
1994
|
+
since = !isNaN(num) && String(num) === options.since ? num : options.since;
|
|
1995
|
+
}
|
|
1996
|
+
const request = {
|
|
3281
1997
|
id: generateId(),
|
|
3282
1998
|
action: "console",
|
|
3283
1999
|
consoleCommand: options.clear ? "clear" : "get",
|
|
3284
|
-
tabId: options.tabId
|
|
3285
|
-
|
|
2000
|
+
tabId: options.tabId,
|
|
2001
|
+
since
|
|
2002
|
+
};
|
|
2003
|
+
const response = await sendCommand(request);
|
|
3286
2004
|
if (options.json) {
|
|
3287
2005
|
console.log(JSON.stringify(response));
|
|
3288
2006
|
return;
|
|
@@ -3322,12 +2040,19 @@ async function consoleCommand(options = {}) {
|
|
|
3322
2040
|
|
|
3323
2041
|
// packages/cli/src/commands/errors.ts
|
|
3324
2042
|
async function errorsCommand(options = {}) {
|
|
3325
|
-
|
|
2043
|
+
let since;
|
|
2044
|
+
if (options.since) {
|
|
2045
|
+
const num = parseInt(options.since, 10);
|
|
2046
|
+
since = !isNaN(num) && String(num) === options.since ? num : options.since;
|
|
2047
|
+
}
|
|
2048
|
+
const request = {
|
|
3326
2049
|
id: generateId(),
|
|
3327
2050
|
action: "errors",
|
|
3328
2051
|
errorsCommand: options.clear ? "clear" : "get",
|
|
3329
|
-
tabId: options.tabId
|
|
3330
|
-
|
|
2052
|
+
tabId: options.tabId,
|
|
2053
|
+
since
|
|
2054
|
+
};
|
|
2055
|
+
const response = await sendCommand(request);
|
|
3331
2056
|
if (options.json) {
|
|
3332
2057
|
console.log(JSON.stringify(response));
|
|
3333
2058
|
return;
|
|
@@ -3362,7 +2087,7 @@ async function errorsCommand(options = {}) {
|
|
|
3362
2087
|
|
|
3363
2088
|
// packages/cli/src/commands/trace.ts
|
|
3364
2089
|
async function traceCommand(subCommand, options = {}) {
|
|
3365
|
-
const response = await
|
|
2090
|
+
const response = await sendCommand({
|
|
3366
2091
|
id: generateId(),
|
|
3367
2092
|
action: "trace",
|
|
3368
2093
|
traceCommand: subCommand,
|
|
@@ -3452,7 +2177,7 @@ function matchTabOrigin2(tabUrl, targetHostname) {
|
|
|
3452
2177
|
}
|
|
3453
2178
|
async function ensureTabForOrigin(origin, hostname) {
|
|
3454
2179
|
const listReq = { id: generateId(), action: "tab_list" };
|
|
3455
|
-
const listResp = await
|
|
2180
|
+
const listResp = await sendCommand(listReq);
|
|
3456
2181
|
if (listResp.success && listResp.data?.tabs) {
|
|
3457
2182
|
const matchingTab = listResp.data.tabs.find(
|
|
3458
2183
|
(tab) => matchTabOrigin2(tab.url, hostname)
|
|
@@ -3461,11 +2186,11 @@ async function ensureTabForOrigin(origin, hostname) {
|
|
|
3461
2186
|
return matchingTab.tabId;
|
|
3462
2187
|
}
|
|
3463
2188
|
}
|
|
3464
|
-
const newResp = await
|
|
2189
|
+
const newResp = await sendCommand({ id: generateId(), action: "tab_new", url: origin });
|
|
3465
2190
|
if (!newResp.success) {
|
|
3466
2191
|
throw new Error(`\u65E0\u6CD5\u6253\u5F00 ${origin}: ${newResp.error}`);
|
|
3467
2192
|
}
|
|
3468
|
-
await new Promise((
|
|
2193
|
+
await new Promise((resolve2) => setTimeout(resolve2, 3e3));
|
|
3469
2194
|
return newResp.data?.tabId;
|
|
3470
2195
|
}
|
|
3471
2196
|
function buildFetchScript(url, options) {
|
|
@@ -3530,7 +2255,7 @@ async function fetchCommand(url, options = {}) {
|
|
|
3530
2255
|
}
|
|
3531
2256
|
const script = buildFetchScript(url, options);
|
|
3532
2257
|
const evalReq = { id: generateId(), action: "eval", script, tabId: targetTabId };
|
|
3533
|
-
const evalResp = await
|
|
2258
|
+
const evalResp = await sendCommand(evalReq);
|
|
3534
2259
|
if (!evalResp.success) {
|
|
3535
2260
|
throw new Error(`Fetch \u5931\u8D25: ${evalResp.error}`);
|
|
3536
2261
|
}
|
|
@@ -3549,9 +2274,9 @@ async function fetchCommand(url, options = {}) {
|
|
|
3549
2274
|
throw new Error(`Fetch error: ${result.error}`);
|
|
3550
2275
|
}
|
|
3551
2276
|
if (options.output) {
|
|
3552
|
-
const { writeFileSync
|
|
2277
|
+
const { writeFileSync } = await import("fs");
|
|
3553
2278
|
const content = typeof result.body === "object" ? JSON.stringify(result.body, null, 2) : String(result.body);
|
|
3554
|
-
|
|
2279
|
+
writeFileSync(options.output, content, "utf-8");
|
|
3555
2280
|
console.log(`\u5DF2\u5199\u5165 ${options.output} (${result.status}, ${content.length} bytes)`);
|
|
3556
2281
|
return;
|
|
3557
2282
|
}
|
|
@@ -3613,16 +2338,49 @@ async function historyCommand(subCommand, options = {}) {
|
|
|
3613
2338
|
|
|
3614
2339
|
// packages/cli/src/commands/daemon.ts
|
|
3615
2340
|
async function statusCommand(options = {}) {
|
|
3616
|
-
const
|
|
2341
|
+
const status = await getDaemonStatus();
|
|
2342
|
+
if (!status) {
|
|
2343
|
+
if (options.json) {
|
|
2344
|
+
console.log(JSON.stringify({ running: false }));
|
|
2345
|
+
} else {
|
|
2346
|
+
console.log("Daemon not running");
|
|
2347
|
+
}
|
|
2348
|
+
return;
|
|
2349
|
+
}
|
|
3617
2350
|
if (options.json) {
|
|
3618
|
-
console.log(JSON.stringify(
|
|
2351
|
+
console.log(JSON.stringify(status, null, 2));
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
console.log(`Daemon running: ${status.running ? "yes" : "no"}`);
|
|
2355
|
+
console.log(`CDP connected: ${status.cdpConnected ? "yes" : "no"}`);
|
|
2356
|
+
console.log(`Uptime: ${formatUptime(status.uptime)}`);
|
|
2357
|
+
console.log(`Global seq: ${status.currentSeq ?? "N/A"}`);
|
|
2358
|
+
const tabs = status.tabs;
|
|
2359
|
+
if (tabs && tabs.length > 0) {
|
|
2360
|
+
console.log(`
|
|
2361
|
+
Tabs (${tabs.length}):`);
|
|
2362
|
+
for (const tab of tabs) {
|
|
2363
|
+
const active = tab.targetId === status.currentTargetId ? " *" : "";
|
|
2364
|
+
console.log(
|
|
2365
|
+
` ${tab.shortId}${active} net:${tab.networkRequests} console:${tab.consoleMessages} err:${tab.jsErrors} seq:${tab.lastActionSeq}`
|
|
2366
|
+
);
|
|
2367
|
+
}
|
|
3619
2368
|
} else {
|
|
3620
|
-
console.log(
|
|
2369
|
+
console.log("\nNo tabs");
|
|
3621
2370
|
}
|
|
3622
2371
|
}
|
|
2372
|
+
function formatUptime(ms) {
|
|
2373
|
+
if (!ms || ms <= 0) return "0s";
|
|
2374
|
+
const s = Math.floor(ms / 1e3);
|
|
2375
|
+
if (s < 60) return `${s}s`;
|
|
2376
|
+
const m = Math.floor(s / 60);
|
|
2377
|
+
if (m < 60) return `${m}m ${s % 60}s`;
|
|
2378
|
+
const h = Math.floor(m / 60);
|
|
2379
|
+
return `${h}h ${m % 60}m`;
|
|
2380
|
+
}
|
|
3623
2381
|
|
|
3624
2382
|
// packages/cli/src/index.ts
|
|
3625
|
-
var VERSION = "0.
|
|
2383
|
+
var VERSION = "0.11.0";
|
|
3626
2384
|
var HELP_TEXT = `
|
|
3627
2385
|
bb-browser - AI Agent \u6D4F\u89C8\u5668\u81EA\u52A8\u5316\u5DE5\u5177
|
|
3628
2386
|
|
|
@@ -3758,6 +2516,12 @@ function parseArgs(argv) {
|
|
|
3758
2516
|
skipNext = true;
|
|
3759
2517
|
} else if (arg === "--tab") {
|
|
3760
2518
|
skipNext = true;
|
|
2519
|
+
} else if (arg === "--since") {
|
|
2520
|
+
skipNext = true;
|
|
2521
|
+
} else if (arg === "--method") {
|
|
2522
|
+
skipNext = true;
|
|
2523
|
+
} else if (arg === "--status") {
|
|
2524
|
+
skipNext = true;
|
|
3761
2525
|
} else if (arg.startsWith("-")) {
|
|
3762
2526
|
} else if (result.command === null) {
|
|
3763
2527
|
result.command = arg;
|
|
@@ -3771,15 +2535,17 @@ async function main() {
|
|
|
3771
2535
|
const parsed = parseArgs(process.argv);
|
|
3772
2536
|
setJqExpression(parsed.flags.jq);
|
|
3773
2537
|
const tabArgIdx = process.argv.indexOf("--tab");
|
|
3774
|
-
const globalTabId = tabArgIdx >= 0 && process.argv[tabArgIdx + 1] ?
|
|
2538
|
+
const globalTabId = tabArgIdx >= 0 && process.argv[tabArgIdx + 1] ? process.argv[tabArgIdx + 1] : void 0;
|
|
2539
|
+
const sinceArgIdx = process.argv.indexOf("--since");
|
|
2540
|
+
const globalSince = sinceArgIdx >= 0 && process.argv[sinceArgIdx + 1] ? process.argv[sinceArgIdx + 1] : void 0;
|
|
3775
2541
|
if (parsed.flags.version) {
|
|
3776
2542
|
console.log(VERSION);
|
|
3777
2543
|
return;
|
|
3778
2544
|
}
|
|
3779
2545
|
if (process.argv.includes("--mcp")) {
|
|
3780
|
-
const mcpPath =
|
|
3781
|
-
const { spawn:
|
|
3782
|
-
const child =
|
|
2546
|
+
const mcpPath = fileURLToPath2(new URL("./mcp.js", import.meta.url));
|
|
2547
|
+
const { spawn: spawn2 } = await import("child_process");
|
|
2548
|
+
const child = spawn2(process.execPath, [mcpPath], { stdio: "inherit" });
|
|
3783
2549
|
child.on("exit", (code) => process.exit(code ?? 0));
|
|
3784
2550
|
return;
|
|
3785
2551
|
}
|
|
@@ -3999,7 +2765,7 @@ async function main() {
|
|
|
3999
2765
|
break;
|
|
4000
2766
|
}
|
|
4001
2767
|
case "tab": {
|
|
4002
|
-
await tabCommand(parsed.args, { json: parsed.flags.json });
|
|
2768
|
+
await tabCommand(parsed.args, { json: parsed.flags.json, globalTabId });
|
|
4003
2769
|
break;
|
|
4004
2770
|
}
|
|
4005
2771
|
case "status": {
|
|
@@ -4043,17 +2809,21 @@ async function main() {
|
|
|
4043
2809
|
const withBody = process.argv.includes("--with-body");
|
|
4044
2810
|
const bodyIndex = process.argv.findIndex((a) => a === "--body");
|
|
4045
2811
|
const body = bodyIndex >= 0 ? process.argv[bodyIndex + 1] : void 0;
|
|
4046
|
-
|
|
2812
|
+
const methodIndex = process.argv.findIndex((a) => a === "--method");
|
|
2813
|
+
const method = methodIndex >= 0 ? process.argv[methodIndex + 1] : void 0;
|
|
2814
|
+
const statusIndex = process.argv.findIndex((a) => a === "--status");
|
|
2815
|
+
const statusFilter = statusIndex >= 0 ? process.argv[statusIndex + 1] : void 0;
|
|
2816
|
+
await networkCommand(subCommand, urlOrFilter, { json: parsed.flags.json, abort, body, withBody, tabId: globalTabId, since: globalSince, method, status: statusFilter });
|
|
4047
2817
|
break;
|
|
4048
2818
|
}
|
|
4049
2819
|
case "console": {
|
|
4050
2820
|
const clear = process.argv.includes("--clear");
|
|
4051
|
-
await consoleCommand({ json: parsed.flags.json, clear, tabId: globalTabId });
|
|
2821
|
+
await consoleCommand({ json: parsed.flags.json, clear, tabId: globalTabId, since: globalSince });
|
|
4052
2822
|
break;
|
|
4053
2823
|
}
|
|
4054
2824
|
case "errors": {
|
|
4055
2825
|
const clear = process.argv.includes("--clear");
|
|
4056
|
-
await errorsCommand({ json: parsed.flags.json, clear, tabId: globalTabId });
|
|
2826
|
+
await errorsCommand({ json: parsed.flags.json, clear, tabId: globalTabId, since: globalSince });
|
|
4057
2827
|
break;
|
|
4058
2828
|
}
|
|
4059
2829
|
case "trace": {
|
|
@@ -4123,9 +2893,9 @@ async function main() {
|
|
|
4123
2893
|
break;
|
|
4124
2894
|
}
|
|
4125
2895
|
case "star": {
|
|
4126
|
-
const { execSync:
|
|
2896
|
+
const { execSync: execSync3 } = await import("child_process");
|
|
4127
2897
|
try {
|
|
4128
|
-
|
|
2898
|
+
execSync3("gh auth status", { stdio: "pipe" });
|
|
4129
2899
|
} catch {
|
|
4130
2900
|
console.error("\u9700\u8981\u5148\u5B89\u88C5\u5E76\u767B\u5F55 GitHub CLI: https://cli.github.com");
|
|
4131
2901
|
console.error(" brew install gh && gh auth login");
|
|
@@ -4134,7 +2904,7 @@ async function main() {
|
|
|
4134
2904
|
const repos = ["epiral/bb-browser", "epiral/bb-sites"];
|
|
4135
2905
|
for (const repo of repos) {
|
|
4136
2906
|
try {
|
|
4137
|
-
|
|
2907
|
+
execSync3(`gh api user/starred/${repo} -X PUT`, { stdio: "pipe" });
|
|
4138
2908
|
console.log(`\u2B50 Starred ${repo}`);
|
|
4139
2909
|
} catch {
|
|
4140
2910
|
console.log(`Already starred or failed: ${repo}`);
|