bb-browser 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1111 -313
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
COMMAND_TIMEOUT,
|
|
4
|
-
DAEMON_BASE_URL,
|
|
5
4
|
generateId
|
|
6
5
|
} from "./chunk-DBJBHYC7.js";
|
|
7
6
|
import {
|
|
@@ -10,7 +9,861 @@ import {
|
|
|
10
9
|
import "./chunk-D4HDZEJT.js";
|
|
11
10
|
|
|
12
11
|
// packages/cli/src/index.ts
|
|
13
|
-
import { fileURLToPath as
|
|
12
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
13
|
+
|
|
14
|
+
// packages/cli/src/cdp-client.ts
|
|
15
|
+
import { readFileSync } from "fs";
|
|
16
|
+
import { request as httpRequest } from "http";
|
|
17
|
+
import { request as httpsRequest } from "https";
|
|
18
|
+
import path2 from "path";
|
|
19
|
+
import { fileURLToPath } from "url";
|
|
20
|
+
import WebSocket from "ws";
|
|
21
|
+
|
|
22
|
+
// packages/cli/src/cdp-discovery.ts
|
|
23
|
+
import { execFile, execSync, spawn } from "child_process";
|
|
24
|
+
import { existsSync } from "fs";
|
|
25
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
26
|
+
import os from "os";
|
|
27
|
+
import path from "path";
|
|
28
|
+
var DEFAULT_CDP_PORT = 19825;
|
|
29
|
+
var MANAGED_BROWSER_DIR = path.join(os.homedir(), ".bb-browser", "browser");
|
|
30
|
+
var MANAGED_USER_DATA_DIR = path.join(MANAGED_BROWSER_DIR, "user-data");
|
|
31
|
+
var MANAGED_PORT_FILE = path.join(MANAGED_BROWSER_DIR, "cdp-port");
|
|
32
|
+
function execFileAsync(command, args, timeout) {
|
|
33
|
+
return new Promise((resolve2, reject) => {
|
|
34
|
+
execFile(command, args, { encoding: "utf8", timeout }, (error, stdout) => {
|
|
35
|
+
if (error) {
|
|
36
|
+
reject(error);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
resolve2(stdout.trim());
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function getArgValue(flag) {
|
|
44
|
+
const index = process.argv.indexOf(flag);
|
|
45
|
+
if (index < 0) return void 0;
|
|
46
|
+
return process.argv[index + 1];
|
|
47
|
+
}
|
|
48
|
+
async function tryOpenClaw() {
|
|
49
|
+
try {
|
|
50
|
+
const raw = await execFileAsync("npx", ["openclaw", "browser", "status", "--json"], 5e3);
|
|
51
|
+
const parsed = JSON.parse(raw);
|
|
52
|
+
const port = Number(parsed?.cdpPort);
|
|
53
|
+
if (Number.isInteger(port) && port > 0) {
|
|
54
|
+
return { host: "127.0.0.1", port };
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
async function canConnect(host, port) {
|
|
61
|
+
try {
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
const timeout = setTimeout(() => controller.abort(), 1200);
|
|
64
|
+
const response = await fetch(`http://${host}:${port}/json/version`, { signal: controller.signal });
|
|
65
|
+
clearTimeout(timeout);
|
|
66
|
+
return response.ok;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function findBrowserExecutable() {
|
|
72
|
+
if (process.platform === "darwin") {
|
|
73
|
+
const candidates = [
|
|
74
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
75
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
76
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
|
|
77
|
+
];
|
|
78
|
+
return candidates.find((candidate) => existsSync(candidate)) ?? null;
|
|
79
|
+
}
|
|
80
|
+
if (process.platform === "linux") {
|
|
81
|
+
const candidates = ["google-chrome", "google-chrome-stable", "chromium-browser", "chromium"];
|
|
82
|
+
for (const candidate of candidates) {
|
|
83
|
+
try {
|
|
84
|
+
const resolved = execSync(`which ${candidate}`, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
85
|
+
if (resolved) {
|
|
86
|
+
return resolved;
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
if (process.platform === "win32") {
|
|
94
|
+
const candidates = [
|
|
95
|
+
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
96
|
+
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
|
97
|
+
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"
|
|
98
|
+
];
|
|
99
|
+
return candidates.find((candidate) => existsSync(candidate)) ?? null;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
async function launchManagedBrowser(port = DEFAULT_CDP_PORT) {
|
|
104
|
+
const executable = findBrowserExecutable();
|
|
105
|
+
if (!executable) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
await mkdir(MANAGED_USER_DATA_DIR, { recursive: true });
|
|
109
|
+
const defaultProfileDir = path.join(MANAGED_USER_DATA_DIR, "Default");
|
|
110
|
+
const prefsPath = path.join(defaultProfileDir, "Preferences");
|
|
111
|
+
await mkdir(defaultProfileDir, { recursive: true });
|
|
112
|
+
try {
|
|
113
|
+
let prefs = {};
|
|
114
|
+
try {
|
|
115
|
+
prefs = JSON.parse(await readFile(prefsPath, "utf8"));
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
118
|
+
if (!prefs.profile?.name || prefs.profile.name !== "bb-browser") {
|
|
119
|
+
prefs.profile = { ...prefs.profile || {}, name: "bb-browser" };
|
|
120
|
+
await writeFile(prefsPath, JSON.stringify(prefs), "utf8");
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
const args = [
|
|
125
|
+
`--remote-debugging-port=${port}`,
|
|
126
|
+
`--user-data-dir=${MANAGED_USER_DATA_DIR}`,
|
|
127
|
+
"--no-first-run",
|
|
128
|
+
"--no-default-browser-check",
|
|
129
|
+
"--disable-sync",
|
|
130
|
+
"--disable-background-networking",
|
|
131
|
+
"--disable-component-update",
|
|
132
|
+
"--disable-features=Translate,MediaRouter",
|
|
133
|
+
"--disable-session-crashed-bubble",
|
|
134
|
+
"--hide-crash-restore-bubble",
|
|
135
|
+
"about:blank"
|
|
136
|
+
];
|
|
137
|
+
try {
|
|
138
|
+
const child = spawn(executable, args, {
|
|
139
|
+
detached: true,
|
|
140
|
+
stdio: "ignore"
|
|
141
|
+
});
|
|
142
|
+
child.unref();
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
await mkdir(MANAGED_BROWSER_DIR, { recursive: true });
|
|
147
|
+
await writeFile(MANAGED_PORT_FILE, String(port), "utf8");
|
|
148
|
+
const deadline = Date.now() + 8e3;
|
|
149
|
+
while (Date.now() < deadline) {
|
|
150
|
+
if (await canConnect("127.0.0.1", port)) {
|
|
151
|
+
return { host: "127.0.0.1", port };
|
|
152
|
+
}
|
|
153
|
+
await new Promise((resolve2) => setTimeout(resolve2, 250));
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
async function discoverCdpPort() {
|
|
158
|
+
const explicitPort = Number.parseInt(getArgValue("--port") ?? "", 10);
|
|
159
|
+
if (Number.isInteger(explicitPort) && explicitPort > 0 && await canConnect("127.0.0.1", explicitPort)) {
|
|
160
|
+
return { host: "127.0.0.1", port: explicitPort };
|
|
161
|
+
}
|
|
162
|
+
if (process.argv.includes("--openclaw")) {
|
|
163
|
+
const viaOpenClaw = await tryOpenClaw();
|
|
164
|
+
if (viaOpenClaw && await canConnect(viaOpenClaw.host, viaOpenClaw.port)) {
|
|
165
|
+
return viaOpenClaw;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const detectedOpenClaw = await tryOpenClaw();
|
|
169
|
+
if (detectedOpenClaw && await canConnect(detectedOpenClaw.host, detectedOpenClaw.port)) {
|
|
170
|
+
return detectedOpenClaw;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
const rawPort = await readFile(MANAGED_PORT_FILE, "utf8");
|
|
174
|
+
const managedPort = Number.parseInt(rawPort.trim(), 10);
|
|
175
|
+
if (Number.isInteger(managedPort) && managedPort > 0 && await canConnect("127.0.0.1", managedPort)) {
|
|
176
|
+
return { host: "127.0.0.1", port: managedPort };
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
}
|
|
180
|
+
return launchManagedBrowser();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// packages/cli/src/cdp-client.ts
|
|
184
|
+
var connectionState = null;
|
|
185
|
+
var reconnecting = null;
|
|
186
|
+
var networkRequests = /* @__PURE__ */ new Map();
|
|
187
|
+
var networkEnabled = false;
|
|
188
|
+
var consoleMessages = [];
|
|
189
|
+
var consoleEnabled = false;
|
|
190
|
+
var jsErrors = [];
|
|
191
|
+
var errorsEnabled = false;
|
|
192
|
+
var traceRecording = false;
|
|
193
|
+
var traceEvents = [];
|
|
194
|
+
function buildRequestError(error) {
|
|
195
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
196
|
+
}
|
|
197
|
+
function fetchJson(url) {
|
|
198
|
+
return new Promise((resolve2, reject) => {
|
|
199
|
+
const requester = url.startsWith("https:") ? httpsRequest : httpRequest;
|
|
200
|
+
const req = requester(url, { method: "GET" }, (res) => {
|
|
201
|
+
const chunks = [];
|
|
202
|
+
res.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
203
|
+
res.on("end", () => {
|
|
204
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
205
|
+
if ((res.statusCode ?? 500) >= 400) {
|
|
206
|
+
reject(new Error(`HTTP ${res.statusCode ?? 500}: ${raw}`));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
resolve2(JSON.parse(raw));
|
|
211
|
+
} catch (error) {
|
|
212
|
+
reject(error);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
req.on("error", reject);
|
|
217
|
+
req.end();
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
async function getJsonList(host, port) {
|
|
221
|
+
const data = await fetchJson(`http://${host}:${port}/json/list`);
|
|
222
|
+
return Array.isArray(data) ? data : [];
|
|
223
|
+
}
|
|
224
|
+
async function getJsonVersion(host, port) {
|
|
225
|
+
const data = await fetchJson(`http://${host}:${port}/json/version`);
|
|
226
|
+
const url = data.webSocketDebuggerUrl;
|
|
227
|
+
if (typeof url !== "string" || !url) {
|
|
228
|
+
throw new Error("CDP endpoint missing webSocketDebuggerUrl");
|
|
229
|
+
}
|
|
230
|
+
return { webSocketDebuggerUrl: url };
|
|
231
|
+
}
|
|
232
|
+
function connectWebSocket(url) {
|
|
233
|
+
return new Promise((resolve2, reject) => {
|
|
234
|
+
const ws = new WebSocket(url);
|
|
235
|
+
ws.once("open", () => {
|
|
236
|
+
ws._socket?.unref?.();
|
|
237
|
+
resolve2(ws);
|
|
238
|
+
});
|
|
239
|
+
ws.once("error", reject);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
function createState(host, port, browserWsUrl, browserSocket) {
|
|
243
|
+
const state = {
|
|
244
|
+
host,
|
|
245
|
+
port,
|
|
246
|
+
browserWsUrl,
|
|
247
|
+
browserSocket,
|
|
248
|
+
browserPending: /* @__PURE__ */ new Map(),
|
|
249
|
+
nextMessageId: 1,
|
|
250
|
+
sessions: /* @__PURE__ */ new Map(),
|
|
251
|
+
attachedTargets: /* @__PURE__ */ new Map(),
|
|
252
|
+
refsByTarget: /* @__PURE__ */ new Map(),
|
|
253
|
+
activeFrameIdByTarget: /* @__PURE__ */ new Map(),
|
|
254
|
+
dialogHandlers: /* @__PURE__ */ new Map()
|
|
255
|
+
};
|
|
256
|
+
browserSocket.on("message", (raw) => {
|
|
257
|
+
const message = JSON.parse(raw.toString());
|
|
258
|
+
if (typeof message.id === "number") {
|
|
259
|
+
const pending = state.browserPending.get(message.id);
|
|
260
|
+
if (!pending) return;
|
|
261
|
+
state.browserPending.delete(message.id);
|
|
262
|
+
if (message.error) {
|
|
263
|
+
pending.reject(new Error(`${pending.method}: ${message.error.message ?? "Unknown CDP error"}`));
|
|
264
|
+
} else {
|
|
265
|
+
pending.resolve(message.result);
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (message.method === "Target.attachedToTarget") {
|
|
270
|
+
const params = message.params;
|
|
271
|
+
const sessionId = params.sessionId;
|
|
272
|
+
const targetInfo = params.targetInfo;
|
|
273
|
+
if (typeof sessionId === "string" && typeof targetInfo?.targetId === "string") {
|
|
274
|
+
state.sessions.set(targetInfo.targetId, sessionId);
|
|
275
|
+
state.attachedTargets.set(sessionId, targetInfo.targetId);
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (message.method === "Target.detachedFromTarget") {
|
|
280
|
+
const params = message.params;
|
|
281
|
+
const sessionId = params.sessionId;
|
|
282
|
+
if (typeof sessionId === "string") {
|
|
283
|
+
const targetId = state.attachedTargets.get(sessionId);
|
|
284
|
+
if (targetId) {
|
|
285
|
+
state.sessions.delete(targetId);
|
|
286
|
+
state.attachedTargets.delete(sessionId);
|
|
287
|
+
state.activeFrameIdByTarget.delete(targetId);
|
|
288
|
+
state.dialogHandlers.delete(targetId);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (message.method === "Target.receivedMessageFromTarget") {
|
|
294
|
+
const params = message.params;
|
|
295
|
+
const sessionId = params.sessionId;
|
|
296
|
+
const messageText = params.message;
|
|
297
|
+
if (typeof sessionId === "string" && typeof messageText === "string") {
|
|
298
|
+
const targetId = state.attachedTargets.get(sessionId);
|
|
299
|
+
if (targetId) {
|
|
300
|
+
handleSessionEvent(targetId, JSON.parse(messageText)).catch(() => {
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (typeof message.sessionId === "string" && typeof message.method === "string") {
|
|
307
|
+
const targetId = state.attachedTargets.get(message.sessionId);
|
|
308
|
+
if (targetId) {
|
|
309
|
+
handleSessionEvent(targetId, message).catch(() => {
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
browserSocket.on("close", () => {
|
|
315
|
+
if (connectionState === state) {
|
|
316
|
+
connectionState = null;
|
|
317
|
+
}
|
|
318
|
+
for (const pending of state.browserPending.values()) {
|
|
319
|
+
pending.reject(new Error("CDP connection closed"));
|
|
320
|
+
}
|
|
321
|
+
state.browserPending.clear();
|
|
322
|
+
});
|
|
323
|
+
browserSocket.on("error", () => {
|
|
324
|
+
});
|
|
325
|
+
return state;
|
|
326
|
+
}
|
|
327
|
+
async function browserCommand(method, params = {}) {
|
|
328
|
+
const state = connectionState;
|
|
329
|
+
if (!state) throw new Error("CDP connection not initialized");
|
|
330
|
+
const id = state.nextMessageId++;
|
|
331
|
+
const payload = JSON.stringify({ id, method, params });
|
|
332
|
+
const promise = new Promise((resolve2, reject) => {
|
|
333
|
+
state.browserPending.set(id, { resolve: resolve2, reject, method });
|
|
334
|
+
});
|
|
335
|
+
state.browserSocket.send(payload);
|
|
336
|
+
return promise;
|
|
337
|
+
}
|
|
338
|
+
async function sessionCommand(targetId, method, params = {}) {
|
|
339
|
+
const state = connectionState;
|
|
340
|
+
if (!state) throw new Error("CDP connection not initialized");
|
|
341
|
+
const sessionId = state.sessions.get(targetId) ?? await attachTarget(targetId);
|
|
342
|
+
const id = state.nextMessageId++;
|
|
343
|
+
const payload = JSON.stringify({ id, method, params, sessionId });
|
|
344
|
+
return new Promise((resolve2, reject) => {
|
|
345
|
+
const check = (raw) => {
|
|
346
|
+
const msg = JSON.parse(raw.toString());
|
|
347
|
+
if (msg.id === id && msg.sessionId === sessionId) {
|
|
348
|
+
state.browserSocket.off("message", check);
|
|
349
|
+
if (msg.error) reject(new Error(`${method}: ${msg.error.message ?? "Unknown CDP error"}`));
|
|
350
|
+
else resolve2(msg.result);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
state.browserSocket.on("message", check);
|
|
354
|
+
state.browserSocket.send(payload);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
function getActiveFrameId(targetId) {
|
|
358
|
+
const frameId = connectionState?.activeFrameIdByTarget.get(targetId);
|
|
359
|
+
return frameId ?? void 0;
|
|
360
|
+
}
|
|
361
|
+
async function pageCommand(targetId, method, params = {}) {
|
|
362
|
+
const frameId = getActiveFrameId(targetId);
|
|
363
|
+
return sessionCommand(targetId, method, frameId ? { ...params, frameId } : params);
|
|
364
|
+
}
|
|
365
|
+
function normalizeHeaders(headers) {
|
|
366
|
+
if (!headers || typeof headers !== "object") return void 0;
|
|
367
|
+
return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, String(value)]));
|
|
368
|
+
}
|
|
369
|
+
async function handleSessionEvent(targetId, event) {
|
|
370
|
+
const method = event.method;
|
|
371
|
+
const params = event.params ?? {};
|
|
372
|
+
if (typeof method !== "string") return;
|
|
373
|
+
if (method === "Page.javascriptDialogOpening") {
|
|
374
|
+
const handler = connectionState?.dialogHandlers.get(targetId);
|
|
375
|
+
if (handler) {
|
|
376
|
+
await sessionCommand(targetId, "Page.handleJavaScriptDialog", {
|
|
377
|
+
accept: handler.accept,
|
|
378
|
+
...handler.promptText !== void 0 ? { promptText: handler.promptText } : {}
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (method === "Network.requestWillBeSent") {
|
|
384
|
+
const requestId = typeof params.requestId === "string" ? params.requestId : void 0;
|
|
385
|
+
const request = params.request;
|
|
386
|
+
if (!requestId || !request) return;
|
|
387
|
+
networkRequests.set(requestId, {
|
|
388
|
+
requestId,
|
|
389
|
+
url: String(request.url ?? ""),
|
|
390
|
+
method: String(request.method ?? "GET"),
|
|
391
|
+
type: String(params.type ?? "Other"),
|
|
392
|
+
timestamp: Math.round(Number(params.timestamp ?? Date.now()) * 1e3),
|
|
393
|
+
requestHeaders: normalizeHeaders(request.headers),
|
|
394
|
+
requestBody: typeof request.postData === "string" ? request.postData : void 0
|
|
395
|
+
});
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (method === "Network.responseReceived") {
|
|
399
|
+
const requestId = typeof params.requestId === "string" ? params.requestId : void 0;
|
|
400
|
+
const response = params.response;
|
|
401
|
+
if (!requestId || !response) return;
|
|
402
|
+
const existing = networkRequests.get(requestId);
|
|
403
|
+
if (!existing) return;
|
|
404
|
+
existing.status = typeof response.status === "number" ? response.status : void 0;
|
|
405
|
+
existing.statusText = typeof response.statusText === "string" ? response.statusText : void 0;
|
|
406
|
+
existing.responseHeaders = normalizeHeaders(response.headers);
|
|
407
|
+
existing.mimeType = typeof response.mimeType === "string" ? response.mimeType : void 0;
|
|
408
|
+
networkRequests.set(requestId, existing);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (method === "Network.loadingFailed") {
|
|
412
|
+
const requestId = typeof params.requestId === "string" ? params.requestId : void 0;
|
|
413
|
+
if (!requestId) return;
|
|
414
|
+
const existing = networkRequests.get(requestId);
|
|
415
|
+
if (!existing) return;
|
|
416
|
+
existing.failed = true;
|
|
417
|
+
existing.failureReason = typeof params.errorText === "string" ? params.errorText : "Unknown error";
|
|
418
|
+
networkRequests.set(requestId, existing);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (method === "Runtime.consoleAPICalled") {
|
|
422
|
+
const type = String(params.type ?? "log");
|
|
423
|
+
const args = Array.isArray(params.args) ? params.args : [];
|
|
424
|
+
const text = args.map((arg) => {
|
|
425
|
+
if (typeof arg.value === "string") return arg.value;
|
|
426
|
+
if (arg.value !== void 0) return String(arg.value);
|
|
427
|
+
if (typeof arg.description === "string") return arg.description;
|
|
428
|
+
return "";
|
|
429
|
+
}).filter(Boolean).join(" ");
|
|
430
|
+
const stack = params.stackTrace;
|
|
431
|
+
const firstCallFrame = Array.isArray(stack?.callFrames) ? stack?.callFrames[0] : void 0;
|
|
432
|
+
consoleMessages.push({
|
|
433
|
+
type: ["log", "info", "warn", "error", "debug"].includes(type) ? type : "log",
|
|
434
|
+
text,
|
|
435
|
+
timestamp: Math.round(Number(params.timestamp ?? Date.now())),
|
|
436
|
+
url: typeof firstCallFrame?.url === "string" ? firstCallFrame.url : void 0,
|
|
437
|
+
lineNumber: typeof firstCallFrame?.lineNumber === "number" ? firstCallFrame.lineNumber : void 0
|
|
438
|
+
});
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (method === "Runtime.exceptionThrown") {
|
|
442
|
+
const details = params.exceptionDetails;
|
|
443
|
+
if (!details) return;
|
|
444
|
+
const exception = details.exception;
|
|
445
|
+
const stackTrace = details.stackTrace;
|
|
446
|
+
const callFrames = Array.isArray(stackTrace?.callFrames) ? stackTrace.callFrames : [];
|
|
447
|
+
jsErrors.push({
|
|
448
|
+
message: typeof exception?.description === "string" ? exception.description : String(details.text ?? "JavaScript exception"),
|
|
449
|
+
url: typeof details.url === "string" ? details.url : typeof callFrames[0]?.url === "string" ? String(callFrames[0].url) : void 0,
|
|
450
|
+
lineNumber: typeof details.lineNumber === "number" ? details.lineNumber : void 0,
|
|
451
|
+
columnNumber: typeof details.columnNumber === "number" ? details.columnNumber : void 0,
|
|
452
|
+
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,
|
|
453
|
+
timestamp: Date.now()
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
async function ensureNetworkMonitoring(targetId) {
|
|
458
|
+
if (networkEnabled) return;
|
|
459
|
+
await sessionCommand(targetId, "Network.enable");
|
|
460
|
+
networkEnabled = true;
|
|
461
|
+
}
|
|
462
|
+
async function ensureConsoleMonitoring(targetId) {
|
|
463
|
+
if (consoleEnabled && errorsEnabled) return;
|
|
464
|
+
await sessionCommand(targetId, "Runtime.enable");
|
|
465
|
+
consoleEnabled = true;
|
|
466
|
+
errorsEnabled = true;
|
|
467
|
+
}
|
|
468
|
+
async function attachTarget(targetId) {
|
|
469
|
+
const result = await browserCommand("Target.attachToTarget", {
|
|
470
|
+
targetId,
|
|
471
|
+
flatten: true
|
|
472
|
+
});
|
|
473
|
+
connectionState?.sessions.set(targetId, result.sessionId);
|
|
474
|
+
connectionState?.attachedTargets.set(result.sessionId, targetId);
|
|
475
|
+
connectionState?.activeFrameIdByTarget.set(targetId, connectionState?.activeFrameIdByTarget.get(targetId) ?? null);
|
|
476
|
+
await sessionCommand(targetId, "Page.enable");
|
|
477
|
+
await sessionCommand(targetId, "Runtime.enable");
|
|
478
|
+
await sessionCommand(targetId, "DOM.enable");
|
|
479
|
+
await sessionCommand(targetId, "Accessibility.enable");
|
|
480
|
+
return result.sessionId;
|
|
481
|
+
}
|
|
482
|
+
async function getTargets() {
|
|
483
|
+
const state = connectionState;
|
|
484
|
+
if (!state) throw new Error("CDP connection not initialized");
|
|
485
|
+
try {
|
|
486
|
+
const result = await browserCommand("Target.getTargets");
|
|
487
|
+
return (result.targetInfos || []).map((target) => ({
|
|
488
|
+
id: target.targetId,
|
|
489
|
+
type: target.type,
|
|
490
|
+
title: target.title,
|
|
491
|
+
url: target.url,
|
|
492
|
+
webSocketDebuggerUrl: ""
|
|
493
|
+
}));
|
|
494
|
+
} catch {
|
|
495
|
+
return getJsonList(state.host, state.port);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
async function ensurePageTarget(targetId) {
|
|
499
|
+
const targets = (await getTargets()).filter((target2) => target2.type === "page");
|
|
500
|
+
if (targets.length === 0) throw new Error("No page target found");
|
|
501
|
+
let target;
|
|
502
|
+
if (typeof targetId === "number") {
|
|
503
|
+
target = targets[targetId] ?? targets.find((item) => Number(item.id) === targetId);
|
|
504
|
+
} else if (typeof targetId === "string") {
|
|
505
|
+
target = targets.find((item) => item.id === targetId);
|
|
506
|
+
}
|
|
507
|
+
target ??= targets[0];
|
|
508
|
+
connectionState.currentTargetId = target.id;
|
|
509
|
+
await attachTarget(target.id);
|
|
510
|
+
return target;
|
|
511
|
+
}
|
|
512
|
+
function parseRef(ref) {
|
|
513
|
+
const refs = connectionState?.refsByTarget.get(connectionState.currentTargetId ?? "") ?? {};
|
|
514
|
+
const found = refs[ref];
|
|
515
|
+
if (!found?.backendDOMNodeId) {
|
|
516
|
+
throw new Error(`Unknown ref: ${ref}. Run snapshot first.`);
|
|
517
|
+
}
|
|
518
|
+
return found.backendDOMNodeId;
|
|
519
|
+
}
|
|
520
|
+
function loadBuildDomTreeScript() {
|
|
521
|
+
const currentDir = path2.dirname(fileURLToPath(import.meta.url));
|
|
522
|
+
const candidates = [
|
|
523
|
+
path2.resolve(currentDir, "../../../extension/buildDomTree.js"),
|
|
524
|
+
path2.resolve(currentDir, "../../extension/buildDomTree.js"),
|
|
525
|
+
path2.resolve(currentDir, "../../../packages/extension/buildDomTree.js"),
|
|
526
|
+
path2.resolve(currentDir, "../../../extension/dist/buildDomTree.js")
|
|
527
|
+
];
|
|
528
|
+
for (const candidate of candidates) {
|
|
529
|
+
try {
|
|
530
|
+
return readFileSync(candidate, "utf8");
|
|
531
|
+
} catch {
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
throw new Error("Cannot find buildDomTree.js");
|
|
535
|
+
}
|
|
536
|
+
async function evaluate(targetId, expression, returnByValue = true) {
|
|
537
|
+
const result = await sessionCommand(targetId, "Runtime.evaluate", {
|
|
538
|
+
expression,
|
|
539
|
+
awaitPromise: true,
|
|
540
|
+
returnByValue
|
|
541
|
+
});
|
|
542
|
+
if (result.exceptionDetails) {
|
|
543
|
+
throw new Error(result.exceptionDetails.text || "Runtime.evaluate failed");
|
|
544
|
+
}
|
|
545
|
+
return result.result.value ?? result.result;
|
|
546
|
+
}
|
|
547
|
+
async function resolveNode(targetId, backendNodeId) {
|
|
548
|
+
const result = await sessionCommand(targetId, "DOM.pushNodesByBackendIdsToFrontend", {
|
|
549
|
+
backendNodeIds: [backendNodeId]
|
|
550
|
+
});
|
|
551
|
+
return result.nodeId;
|
|
552
|
+
}
|
|
553
|
+
async function focusNode(targetId, backendNodeId) {
|
|
554
|
+
const nodeId = await resolveNode(targetId, backendNodeId);
|
|
555
|
+
await sessionCommand(targetId, "DOM.focus", { nodeId });
|
|
556
|
+
}
|
|
557
|
+
async function getNodeBox(targetId, backendNodeId) {
|
|
558
|
+
const result = await sessionCommand(targetId, "DOM.getBoxModel", {
|
|
559
|
+
backendNodeId
|
|
560
|
+
});
|
|
561
|
+
const quad = result.model.content.length >= 8 ? result.model.content : result.model.border;
|
|
562
|
+
const xs = [quad[0], quad[2], quad[4], quad[6]];
|
|
563
|
+
const ys = [quad[1], quad[3], quad[5], quad[7]];
|
|
564
|
+
return {
|
|
565
|
+
x: xs.reduce((a, b) => a + b, 0) / xs.length,
|
|
566
|
+
y: ys.reduce((a, b) => a + b, 0) / ys.length
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
async function mouseClick(targetId, x, y) {
|
|
570
|
+
await sessionCommand(targetId, "Input.dispatchMouseEvent", { type: "mouseMoved", x, y, button: "none" });
|
|
571
|
+
await sessionCommand(targetId, "Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 });
|
|
572
|
+
await sessionCommand(targetId, "Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 });
|
|
573
|
+
}
|
|
574
|
+
async function getAttributeValue(targetId, backendNodeId, attribute) {
|
|
575
|
+
const nodeId = await resolveNode(targetId, backendNodeId);
|
|
576
|
+
if (attribute === "text") {
|
|
577
|
+
return evaluate(targetId, `(() => { const n = this; return n.innerText ?? n.textContent ?? ''; }).call(document.querySelector('[data-bb-node-id="${nodeId}"]'))`);
|
|
578
|
+
}
|
|
579
|
+
const result = await sessionCommand(targetId, "DOM.resolveNode", { backendNodeId });
|
|
580
|
+
const call = await sessionCommand(targetId, "Runtime.callFunctionOn", {
|
|
581
|
+
objectId: result.object.objectId,
|
|
582
|
+
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)}) || ''; }`,
|
|
583
|
+
returnByValue: true
|
|
584
|
+
});
|
|
585
|
+
return String(call.result.value ?? "");
|
|
586
|
+
}
|
|
587
|
+
async function buildSnapshot(targetId, request) {
|
|
588
|
+
const script = loadBuildDomTreeScript();
|
|
589
|
+
const expression = `(() => { ${script}; return (typeof buildDomTree === 'function' ? buildDomTree : globalThis.buildDomTree)(${JSON.stringify({
|
|
590
|
+
interactiveOnly: !!request.interactive,
|
|
591
|
+
compact: !!request.compact,
|
|
592
|
+
maxDepth: request.maxDepth,
|
|
593
|
+
selector: request.selector
|
|
594
|
+
})}); })()`;
|
|
595
|
+
const value = await evaluate(targetId, expression, true);
|
|
596
|
+
connectionState?.refsByTarget.set(targetId, value.refs || {});
|
|
597
|
+
return value;
|
|
598
|
+
}
|
|
599
|
+
function ok(id, data) {
|
|
600
|
+
return { id, success: true, data };
|
|
601
|
+
}
|
|
602
|
+
function fail(id, error) {
|
|
603
|
+
return { id, success: false, error: buildRequestError(error).message };
|
|
604
|
+
}
|
|
605
|
+
async function ensureCdpConnection() {
|
|
606
|
+
if (connectionState) return;
|
|
607
|
+
if (reconnecting) return reconnecting;
|
|
608
|
+
reconnecting = (async () => {
|
|
609
|
+
const discovered = await discoverCdpPort();
|
|
610
|
+
if (!discovered) {
|
|
611
|
+
throw new Error("No browser connection found");
|
|
612
|
+
}
|
|
613
|
+
const version = await getJsonVersion(discovered.host, discovered.port);
|
|
614
|
+
const wsUrl = version.webSocketDebuggerUrl;
|
|
615
|
+
const socket = await connectWebSocket(wsUrl);
|
|
616
|
+
connectionState = createState(discovered.host, discovered.port, wsUrl, socket);
|
|
617
|
+
})();
|
|
618
|
+
try {
|
|
619
|
+
await reconnecting;
|
|
620
|
+
} finally {
|
|
621
|
+
reconnecting = null;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
async function sendCommand(request) {
|
|
625
|
+
try {
|
|
626
|
+
await ensureCdpConnection();
|
|
627
|
+
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("\u8BF7\u6C42\u8D85\u65F6")), COMMAND_TIMEOUT));
|
|
628
|
+
return await Promise.race([dispatchRequest(request), timeout]);
|
|
629
|
+
} catch (error) {
|
|
630
|
+
return fail(request.id, error);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
async function dispatchRequest(request) {
|
|
634
|
+
const target = await ensurePageTarget(request.tabId);
|
|
635
|
+
switch (request.action) {
|
|
636
|
+
case "open": {
|
|
637
|
+
if (!request.url) return fail(request.id, "Missing url parameter");
|
|
638
|
+
if (request.tabId === void 0) {
|
|
639
|
+
const created = await browserCommand("Target.createTarget", { url: request.url });
|
|
640
|
+
const newTarget = await ensurePageTarget(created.targetId);
|
|
641
|
+
return ok(request.id, { url: request.url, tabId: Number(newTarget.id) || void 0 });
|
|
642
|
+
}
|
|
643
|
+
await pageCommand(target.id, "Page.navigate", { url: request.url });
|
|
644
|
+
return ok(request.id, { url: request.url, title: target.title, tabId: Number(target.id) || void 0 });
|
|
645
|
+
}
|
|
646
|
+
case "snapshot": {
|
|
647
|
+
const snapshotData = await buildSnapshot(target.id, request);
|
|
648
|
+
return ok(request.id, { title: target.title, url: target.url, snapshotData });
|
|
649
|
+
}
|
|
650
|
+
case "click":
|
|
651
|
+
case "hover": {
|
|
652
|
+
if (!request.ref) return fail(request.id, "Missing ref parameter");
|
|
653
|
+
const backendNodeId = parseRef(request.ref);
|
|
654
|
+
const point = await getNodeBox(target.id, backendNodeId);
|
|
655
|
+
await sessionCommand(target.id, "Input.dispatchMouseEvent", { type: "mouseMoved", x: point.x, y: point.y, button: "none" });
|
|
656
|
+
if (request.action === "click") await mouseClick(target.id, point.x, point.y);
|
|
657
|
+
return ok(request.id, {});
|
|
658
|
+
}
|
|
659
|
+
case "fill":
|
|
660
|
+
case "type": {
|
|
661
|
+
if (!request.ref) return fail(request.id, "Missing ref parameter");
|
|
662
|
+
if (request.text == null) return fail(request.id, "Missing text parameter");
|
|
663
|
+
const backendNodeId = parseRef(request.ref);
|
|
664
|
+
await focusNode(target.id, backendNodeId);
|
|
665
|
+
if (request.action === "fill") {
|
|
666
|
+
await evaluate(target.id, `document.activeElement && ((document.activeElement.value = ''), document.activeElement.dispatchEvent(new Event('input', { bubbles: true })))`);
|
|
667
|
+
}
|
|
668
|
+
await sessionCommand(target.id, "Input.insertText", { text: request.text });
|
|
669
|
+
return ok(request.id, { value: request.text });
|
|
670
|
+
}
|
|
671
|
+
case "check":
|
|
672
|
+
case "uncheck": {
|
|
673
|
+
if (!request.ref) return fail(request.id, "Missing ref parameter");
|
|
674
|
+
const backendNodeId = parseRef(request.ref);
|
|
675
|
+
const desired = request.action === "check";
|
|
676
|
+
const resolved = await sessionCommand(target.id, "DOM.resolveNode", { backendNodeId });
|
|
677
|
+
await sessionCommand(target.id, "Runtime.callFunctionOn", {
|
|
678
|
+
objectId: resolved.object.objectId,
|
|
679
|
+
functionDeclaration: `function() { this.checked = ${desired}; this.dispatchEvent(new Event('input', { bubbles: true })); this.dispatchEvent(new Event('change', { bubbles: true })); }`
|
|
680
|
+
});
|
|
681
|
+
return ok(request.id, {});
|
|
682
|
+
}
|
|
683
|
+
case "select": {
|
|
684
|
+
if (!request.ref || request.value == null) return fail(request.id, "Missing ref or value parameter");
|
|
685
|
+
const backendNodeId = parseRef(request.ref);
|
|
686
|
+
const resolved = await sessionCommand(target.id, "DOM.resolveNode", { backendNodeId });
|
|
687
|
+
await sessionCommand(target.id, "Runtime.callFunctionOn", {
|
|
688
|
+
objectId: resolved.object.objectId,
|
|
689
|
+
functionDeclaration: `function() { this.value = ${JSON.stringify(request.value)}; this.dispatchEvent(new Event('input', { bubbles: true })); this.dispatchEvent(new Event('change', { bubbles: true })); }`
|
|
690
|
+
});
|
|
691
|
+
return ok(request.id, { value: request.value });
|
|
692
|
+
}
|
|
693
|
+
case "get": {
|
|
694
|
+
if (!request.ref || !request.attribute) return fail(request.id, "Missing ref or attribute parameter");
|
|
695
|
+
const value = await getAttributeValue(target.id, parseRef(request.ref), request.attribute);
|
|
696
|
+
return ok(request.id, { value });
|
|
697
|
+
}
|
|
698
|
+
case "screenshot": {
|
|
699
|
+
const result = await sessionCommand(target.id, "Page.captureScreenshot", { format: "png", fromSurface: true });
|
|
700
|
+
return ok(request.id, { dataUrl: `data:image/png;base64,${result.data}` });
|
|
701
|
+
}
|
|
702
|
+
case "close": {
|
|
703
|
+
await browserCommand("Target.closeTarget", { targetId: target.id });
|
|
704
|
+
return ok(request.id, {});
|
|
705
|
+
}
|
|
706
|
+
case "wait": {
|
|
707
|
+
await new Promise((resolve2) => setTimeout(resolve2, request.ms ?? 1e3));
|
|
708
|
+
return ok(request.id, {});
|
|
709
|
+
}
|
|
710
|
+
case "press": {
|
|
711
|
+
if (!request.key) return fail(request.id, "Missing key parameter");
|
|
712
|
+
await sessionCommand(target.id, "Input.dispatchKeyEvent", { type: "keyDown", key: request.key });
|
|
713
|
+
if (request.key.length === 1) {
|
|
714
|
+
await sessionCommand(target.id, "Input.dispatchKeyEvent", { type: "char", text: request.key, key: request.key });
|
|
715
|
+
}
|
|
716
|
+
await sessionCommand(target.id, "Input.dispatchKeyEvent", { type: "keyUp", key: request.key });
|
|
717
|
+
return ok(request.id, {});
|
|
718
|
+
}
|
|
719
|
+
case "scroll": {
|
|
720
|
+
const deltaY = request.direction === "up" ? -(request.pixels ?? 300) : request.pixels ?? 300;
|
|
721
|
+
await sessionCommand(target.id, "Input.dispatchMouseEvent", { type: "mouseWheel", x: 0, y: 0, deltaX: 0, deltaY });
|
|
722
|
+
return ok(request.id, {});
|
|
723
|
+
}
|
|
724
|
+
case "back": {
|
|
725
|
+
await evaluate(target.id, "history.back(); undefined");
|
|
726
|
+
return ok(request.id, {});
|
|
727
|
+
}
|
|
728
|
+
case "forward": {
|
|
729
|
+
await evaluate(target.id, "history.forward(); undefined");
|
|
730
|
+
return ok(request.id, {});
|
|
731
|
+
}
|
|
732
|
+
case "refresh": {
|
|
733
|
+
await sessionCommand(target.id, "Page.reload", { ignoreCache: false });
|
|
734
|
+
return ok(request.id, {});
|
|
735
|
+
}
|
|
736
|
+
case "eval": {
|
|
737
|
+
if (!request.script) return fail(request.id, "Missing script parameter");
|
|
738
|
+
const result = await evaluate(target.id, request.script, true);
|
|
739
|
+
return ok(request.id, { result });
|
|
740
|
+
}
|
|
741
|
+
case "tab_list": {
|
|
742
|
+
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: Number(item.id) || index }));
|
|
743
|
+
return ok(request.id, { tabs, activeIndex: tabs.findIndex((tab) => tab.active) });
|
|
744
|
+
}
|
|
745
|
+
case "tab_new": {
|
|
746
|
+
const created = await browserCommand("Target.createTarget", { url: request.url ?? "about:blank" });
|
|
747
|
+
return ok(request.id, { tabId: Number(created.targetId) || void 0, url: request.url ?? "about:blank" });
|
|
748
|
+
}
|
|
749
|
+
case "tab_select": {
|
|
750
|
+
const tabs = (await getTargets()).filter((item) => item.type === "page");
|
|
751
|
+
const selected = request.tabId !== void 0 ? tabs.find((item) => item.id === String(request.tabId) || Number(item.id) === request.tabId) : tabs[request.index ?? 0];
|
|
752
|
+
if (!selected) return fail(request.id, "Tab not found");
|
|
753
|
+
connectionState.currentTargetId = selected.id;
|
|
754
|
+
await attachTarget(selected.id);
|
|
755
|
+
return ok(request.id, { tabId: Number(selected.id) || void 0, url: selected.url, title: selected.title });
|
|
756
|
+
}
|
|
757
|
+
case "tab_close": {
|
|
758
|
+
const tabs = (await getTargets()).filter((item) => item.type === "page");
|
|
759
|
+
const selected = request.tabId !== void 0 ? tabs.find((item) => item.id === String(request.tabId) || Number(item.id) === request.tabId) : tabs[request.index ?? 0];
|
|
760
|
+
if (!selected) return fail(request.id, "Tab not found");
|
|
761
|
+
await browserCommand("Target.closeTarget", { targetId: selected.id });
|
|
762
|
+
return ok(request.id, { tabId: Number(selected.id) || void 0 });
|
|
763
|
+
}
|
|
764
|
+
case "frame": {
|
|
765
|
+
if (!request.selector) return fail(request.id, "Missing selector parameter");
|
|
766
|
+
const document = await pageCommand(target.id, "DOM.getDocument", {});
|
|
767
|
+
const node = await pageCommand(target.id, "DOM.querySelector", { nodeId: document.root.nodeId, selector: request.selector });
|
|
768
|
+
if (!node.nodeId) return fail(request.id, `\u627E\u4E0D\u5230 iframe: ${request.selector}`);
|
|
769
|
+
const described = await pageCommand(target.id, "DOM.describeNode", { nodeId: node.nodeId });
|
|
770
|
+
const frameId = described.node.frameId;
|
|
771
|
+
const nodeName = String(described.node.nodeName ?? "").toLowerCase();
|
|
772
|
+
if (!frameId) return fail(request.id, `\u65E0\u6CD5\u83B7\u53D6 iframe frameId: ${request.selector}`);
|
|
773
|
+
if (nodeName && nodeName !== "iframe" && nodeName !== "frame") return fail(request.id, `\u5143\u7D20\u4E0D\u662F iframe: ${nodeName}`);
|
|
774
|
+
connectionState?.activeFrameIdByTarget.set(target.id, frameId);
|
|
775
|
+
const attributes = described.node.attributes ?? [];
|
|
776
|
+
const attrMap = {};
|
|
777
|
+
for (let i = 0; i < attributes.length; i += 2) attrMap[String(attributes[i])] = String(attributes[i + 1] ?? "");
|
|
778
|
+
return ok(request.id, { frameInfo: { selector: request.selector, name: attrMap.name ?? "", url: attrMap.src ?? "", frameId } });
|
|
779
|
+
}
|
|
780
|
+
case "frame_main": {
|
|
781
|
+
connectionState?.activeFrameIdByTarget.set(target.id, null);
|
|
782
|
+
return ok(request.id, { frameInfo: { frameId: 0 } });
|
|
783
|
+
}
|
|
784
|
+
case "dialog": {
|
|
785
|
+
connectionState?.dialogHandlers.set(target.id, { accept: request.dialogResponse !== "dismiss", ...request.promptText !== void 0 ? { promptText: request.promptText } : {} });
|
|
786
|
+
await sessionCommand(target.id, "Page.enable");
|
|
787
|
+
return ok(request.id, { dialog: { armed: true, response: request.dialogResponse ?? "accept" } });
|
|
788
|
+
}
|
|
789
|
+
case "network": {
|
|
790
|
+
const subCommand = request.networkCommand ?? "requests";
|
|
791
|
+
switch (subCommand) {
|
|
792
|
+
case "requests": {
|
|
793
|
+
await ensureNetworkMonitoring(target.id);
|
|
794
|
+
const requests = Array.from(networkRequests.values()).filter((item) => !request.filter || item.url.includes(request.filter));
|
|
795
|
+
if (request.withBody) {
|
|
796
|
+
await Promise.all(requests.map(async (item) => {
|
|
797
|
+
if (item.failed || item.responseBody !== void 0 || item.bodyError !== void 0) return;
|
|
798
|
+
try {
|
|
799
|
+
const body = await sessionCommand(target.id, "Network.getResponseBody", { requestId: item.requestId });
|
|
800
|
+
item.responseBody = body.body;
|
|
801
|
+
item.responseBodyBase64 = body.base64Encoded;
|
|
802
|
+
} catch (error) {
|
|
803
|
+
item.bodyError = error instanceof Error ? error.message : String(error);
|
|
804
|
+
}
|
|
805
|
+
}));
|
|
806
|
+
}
|
|
807
|
+
return ok(request.id, { networkRequests: requests });
|
|
808
|
+
}
|
|
809
|
+
case "route":
|
|
810
|
+
return ok(request.id, { routeCount: 0 });
|
|
811
|
+
case "unroute":
|
|
812
|
+
return ok(request.id, { routeCount: 0 });
|
|
813
|
+
case "clear":
|
|
814
|
+
networkRequests.clear();
|
|
815
|
+
return ok(request.id, {});
|
|
816
|
+
default:
|
|
817
|
+
return fail(request.id, `Unknown network subcommand: ${subCommand}`);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
case "console": {
|
|
821
|
+
const subCommand = request.consoleCommand ?? "get";
|
|
822
|
+
await ensureConsoleMonitoring(target.id);
|
|
823
|
+
switch (subCommand) {
|
|
824
|
+
case "get":
|
|
825
|
+
return ok(request.id, { consoleMessages: consoleMessages.filter((item) => !request.filter || item.text.includes(request.filter)) });
|
|
826
|
+
case "clear":
|
|
827
|
+
consoleMessages.length = 0;
|
|
828
|
+
return ok(request.id, {});
|
|
829
|
+
default:
|
|
830
|
+
return fail(request.id, `Unknown console subcommand: ${subCommand}`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
case "errors": {
|
|
834
|
+
const subCommand = request.errorsCommand ?? "get";
|
|
835
|
+
await ensureConsoleMonitoring(target.id);
|
|
836
|
+
switch (subCommand) {
|
|
837
|
+
case "get":
|
|
838
|
+
return ok(request.id, { jsErrors: jsErrors.filter((item) => !request.filter || item.message.includes(request.filter) || item.url?.includes(request.filter)) });
|
|
839
|
+
case "clear":
|
|
840
|
+
jsErrors.length = 0;
|
|
841
|
+
return ok(request.id, {});
|
|
842
|
+
default:
|
|
843
|
+
return fail(request.id, `Unknown errors subcommand: ${subCommand}`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
case "trace": {
|
|
847
|
+
const subCommand = request.traceCommand ?? "status";
|
|
848
|
+
switch (subCommand) {
|
|
849
|
+
case "start":
|
|
850
|
+
traceRecording = true;
|
|
851
|
+
traceEvents.length = 0;
|
|
852
|
+
return ok(request.id, { traceStatus: { recording: true, eventCount: 0 } });
|
|
853
|
+
case "stop": {
|
|
854
|
+
traceRecording = false;
|
|
855
|
+
return ok(request.id, { traceEvents: [...traceEvents], traceStatus: { recording: false, eventCount: traceEvents.length } });
|
|
856
|
+
}
|
|
857
|
+
case "status":
|
|
858
|
+
return ok(request.id, { traceStatus: { recording: traceRecording, eventCount: traceEvents.length } });
|
|
859
|
+
default:
|
|
860
|
+
return fail(request.id, `Unknown trace subcommand: ${subCommand}`);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
default:
|
|
864
|
+
return fail(request.id, `Action not yet supported in direct CDP mode: ${request.action}`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
14
867
|
|
|
15
868
|
// packages/cli/src/client.ts
|
|
16
869
|
var jqExpression;
|
|
@@ -30,184 +883,190 @@ function handleJqResponse(response) {
|
|
|
30
883
|
printJqResults(response);
|
|
31
884
|
}
|
|
32
885
|
}
|
|
33
|
-
async function
|
|
34
|
-
|
|
35
|
-
const timeoutId = setTimeout(() => controller.abort(), COMMAND_TIMEOUT);
|
|
36
|
-
try {
|
|
37
|
-
const res = await fetch(`${DAEMON_BASE_URL}/command`, {
|
|
38
|
-
method: "POST",
|
|
39
|
-
headers: {
|
|
40
|
-
"Content-Type": "application/json"
|
|
41
|
-
},
|
|
42
|
-
body: JSON.stringify(request),
|
|
43
|
-
signal: controller.signal
|
|
44
|
-
});
|
|
45
|
-
clearTimeout(timeoutId);
|
|
46
|
-
if (!res.ok) {
|
|
47
|
-
if (res.status === 408) {
|
|
48
|
-
return {
|
|
49
|
-
id: request.id,
|
|
50
|
-
success: false,
|
|
51
|
-
error: "\u547D\u4EE4\u6267\u884C\u8D85\u65F6"
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
if (res.status === 503) {
|
|
55
|
-
return {
|
|
56
|
-
id: request.id,
|
|
57
|
-
success: false,
|
|
58
|
-
error: [
|
|
59
|
-
"Chrome extension not connected.",
|
|
60
|
-
"",
|
|
61
|
-
"1. Download extension: https://github.com/epiral/bb-browser/releases/latest",
|
|
62
|
-
"2. Unzip the downloaded file",
|
|
63
|
-
"3. Open chrome://extensions/ \u2192 Enable Developer Mode",
|
|
64
|
-
'4. Click "Load unpacked" \u2192 select the unzipped folder'
|
|
65
|
-
].join("\n")
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
return {
|
|
69
|
-
id: request.id,
|
|
70
|
-
success: false,
|
|
71
|
-
error: `HTTP \u9519\u8BEF: ${res.status} ${res.statusText}`
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
const response = await res.json();
|
|
75
|
-
return response;
|
|
76
|
-
} catch (error) {
|
|
77
|
-
clearTimeout(timeoutId);
|
|
78
|
-
if (error instanceof Error) {
|
|
79
|
-
if (error.name === "AbortError") {
|
|
80
|
-
return {
|
|
81
|
-
id: request.id,
|
|
82
|
-
success: false,
|
|
83
|
-
error: "\u8BF7\u6C42\u8D85\u65F6"
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
if (error.message.includes("fetch failed") || error.message.includes("ECONNREFUSED")) {
|
|
87
|
-
throw new Error([
|
|
88
|
-
"Cannot connect to daemon.",
|
|
89
|
-
"",
|
|
90
|
-
"Start the daemon first:",
|
|
91
|
-
" bb-browser daemon",
|
|
92
|
-
"",
|
|
93
|
-
"Then load the Chrome extension:",
|
|
94
|
-
" chrome://extensions/ \u2192 Developer Mode \u2192 Load unpacked \u2192 node_modules/bb-browser/extension/"
|
|
95
|
-
].join("\n"));
|
|
96
|
-
}
|
|
97
|
-
throw error;
|
|
98
|
-
}
|
|
99
|
-
throw error;
|
|
100
|
-
}
|
|
886
|
+
async function sendCommand2(request) {
|
|
887
|
+
return sendCommand(request);
|
|
101
888
|
}
|
|
102
889
|
|
|
103
890
|
// packages/cli/src/daemon-manager.ts
|
|
104
|
-
import {
|
|
105
|
-
import { existsSync } from "fs";
|
|
106
|
-
import { fileURLToPath } from "url";
|
|
891
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
107
892
|
import { dirname, resolve } from "path";
|
|
108
|
-
|
|
109
|
-
const currentFile = fileURLToPath(import.meta.url);
|
|
110
|
-
const currentDir = dirname(currentFile);
|
|
111
|
-
const sameDirPath = resolve(currentDir, "daemon.js");
|
|
112
|
-
if (existsSync(sameDirPath)) {
|
|
113
|
-
return sameDirPath;
|
|
114
|
-
}
|
|
115
|
-
return resolve(currentDir, "../../daemon/dist/index.js");
|
|
116
|
-
}
|
|
117
|
-
var DAEMON_START_TIMEOUT = 5e3;
|
|
118
|
-
var POLL_INTERVAL = 200;
|
|
893
|
+
import { existsSync as existsSync2 } from "fs";
|
|
119
894
|
async function isDaemonRunning() {
|
|
895
|
+
return await discoverCdpPort() !== null;
|
|
896
|
+
}
|
|
897
|
+
async function ensureDaemonRunning() {
|
|
120
898
|
try {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
899
|
+
await ensureCdpConnection();
|
|
900
|
+
} catch (error) {
|
|
901
|
+
if (error instanceof Error && error.message.includes("No browser connection found")) {
|
|
902
|
+
throw new Error([
|
|
903
|
+
"bb-browser: Could not start browser.",
|
|
904
|
+
"",
|
|
905
|
+
"Make sure Chrome is installed, then try again.",
|
|
906
|
+
"Or specify a CDP port manually: bb-browser --port 9222"
|
|
907
|
+
].join("\n"));
|
|
908
|
+
}
|
|
909
|
+
throw error;
|
|
130
910
|
}
|
|
131
911
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
912
|
+
|
|
913
|
+
// packages/cli/src/history-sqlite.ts
|
|
914
|
+
import { copyFileSync, existsSync as existsSync3, unlinkSync } from "fs";
|
|
915
|
+
import { execSync as execSync2 } from "child_process";
|
|
916
|
+
import { homedir, tmpdir } from "os";
|
|
917
|
+
import { join } from "path";
|
|
918
|
+
function getHistoryPathCandidates() {
|
|
919
|
+
const home = homedir();
|
|
920
|
+
const localAppData = process.env.LOCALAPPDATA || "";
|
|
921
|
+
const candidates = [
|
|
922
|
+
join(home, "Library/Application Support/Google/Chrome/Default/History"),
|
|
923
|
+
join(home, "Library/Application Support/Microsoft Edge/Default/History"),
|
|
924
|
+
join(home, "Library/Application Support/BraveSoftware/Brave-Browser/Default/History"),
|
|
925
|
+
join(home, "Library/Application Support/Arc/User Data/Default/History"),
|
|
926
|
+
join(home, ".config/google-chrome/Default/History")
|
|
927
|
+
];
|
|
928
|
+
if (localAppData) {
|
|
929
|
+
candidates.push(
|
|
930
|
+
join(localAppData, "Google/Chrome/User Data/Default/History"),
|
|
931
|
+
join(localAppData, "Microsoft/Edge/User Data/Default/History")
|
|
932
|
+
);
|
|
145
933
|
}
|
|
934
|
+
return candidates;
|
|
146
935
|
}
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return true;
|
|
936
|
+
function findHistoryPath() {
|
|
937
|
+
for (const historyPath of getHistoryPathCandidates()) {
|
|
938
|
+
if (existsSync3(historyPath)) {
|
|
939
|
+
return historyPath;
|
|
152
940
|
}
|
|
153
|
-
await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL));
|
|
154
941
|
}
|
|
155
|
-
return
|
|
942
|
+
return null;
|
|
156
943
|
}
|
|
157
|
-
function
|
|
158
|
-
|
|
159
|
-
const daemonProcess = spawn(process.execPath, [daemonPath], {
|
|
160
|
-
detached: true,
|
|
161
|
-
stdio: "ignore",
|
|
162
|
-
env: { ...process.env }
|
|
163
|
-
});
|
|
164
|
-
daemonProcess.unref();
|
|
944
|
+
function sqlEscape(value) {
|
|
945
|
+
return value.replace(/'/g, "''");
|
|
165
946
|
}
|
|
166
|
-
|
|
167
|
-
if (
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
spawnDaemon();
|
|
171
|
-
const ready = await waitForDaemon(DAEMON_START_TIMEOUT);
|
|
172
|
-
if (!ready) {
|
|
173
|
-
throw new Error(
|
|
174
|
-
"\u65E0\u6CD5\u542F\u52A8 Daemon\u3002\u8BF7\u624B\u52A8\u8FD0\u884C bb-browser daemon \u6216 bb-daemon \u542F\u52A8\u670D\u52A1"
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
const extStart = Date.now();
|
|
178
|
-
while (Date.now() - extStart < 1e4) {
|
|
179
|
-
if (await isExtensionConnected()) return;
|
|
180
|
-
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
947
|
+
function buildTimeWhere(days) {
|
|
948
|
+
if (!days || days <= 0) {
|
|
949
|
+
return "";
|
|
181
950
|
}
|
|
951
|
+
return `last_visit_time > (strftime('%s', 'now') - ${Math.floor(days)}*86400) * 1000000 + 11644473600000000`;
|
|
182
952
|
}
|
|
183
|
-
|
|
953
|
+
function runHistoryQuery(sql, mapRow) {
|
|
954
|
+
const historyPath = findHistoryPath();
|
|
955
|
+
if (!historyPath) {
|
|
956
|
+
return [];
|
|
957
|
+
}
|
|
958
|
+
const tmpPath = join(tmpdir(), `bb-history-${Date.now()}-${Math.random().toString(36).slice(2)}.db`);
|
|
184
959
|
try {
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
960
|
+
copyFileSync(historyPath, tmpPath);
|
|
961
|
+
const escapedTmpPath = tmpPath.replace(/"/g, '\\"');
|
|
962
|
+
const escapedSql = sql.replace(/"/g, '\\"');
|
|
963
|
+
const output = execSync2(`sqlite3 -separator $'\\t' "${escapedTmpPath}" "${escapedSql}"`, {
|
|
964
|
+
encoding: "utf-8",
|
|
965
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
190
966
|
});
|
|
191
|
-
|
|
192
|
-
return response.ok;
|
|
967
|
+
return output.split("\n").filter(Boolean).map((line) => mapRow(line.split(" "))).filter((item) => item !== null);
|
|
193
968
|
} catch {
|
|
194
|
-
return
|
|
969
|
+
return [];
|
|
970
|
+
} finally {
|
|
971
|
+
try {
|
|
972
|
+
unlinkSync(tmpPath);
|
|
973
|
+
} catch {
|
|
974
|
+
}
|
|
195
975
|
}
|
|
196
976
|
}
|
|
977
|
+
function searchHistory(query, days) {
|
|
978
|
+
const conditions = [];
|
|
979
|
+
const trimmedQuery = query?.trim();
|
|
980
|
+
if (trimmedQuery) {
|
|
981
|
+
const escapedQuery = sqlEscape(trimmedQuery);
|
|
982
|
+
conditions.push(`(url LIKE '%${escapedQuery}%' OR title LIKE '%${escapedQuery}%')`);
|
|
983
|
+
}
|
|
984
|
+
const timeWhere = buildTimeWhere(days);
|
|
985
|
+
if (timeWhere) {
|
|
986
|
+
conditions.push(timeWhere);
|
|
987
|
+
}
|
|
988
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
989
|
+
const sql = `
|
|
990
|
+
SELECT
|
|
991
|
+
url,
|
|
992
|
+
REPLACE(IFNULL(title, ''), char(9), ' '),
|
|
993
|
+
IFNULL(visit_count, 0),
|
|
994
|
+
IFNULL(last_visit_time, 0)
|
|
995
|
+
FROM urls
|
|
996
|
+
${whereClause}
|
|
997
|
+
ORDER BY last_visit_time DESC
|
|
998
|
+
LIMIT 100;
|
|
999
|
+
`.trim();
|
|
1000
|
+
return runHistoryQuery(sql, (row) => {
|
|
1001
|
+
if (row.length < 4) {
|
|
1002
|
+
return null;
|
|
1003
|
+
}
|
|
1004
|
+
const chromeTimestamp = Number(row[3]) || 0;
|
|
1005
|
+
return {
|
|
1006
|
+
url: row[0] || "",
|
|
1007
|
+
title: row[1] || "",
|
|
1008
|
+
visitCount: Number(row[2]) || 0,
|
|
1009
|
+
lastVisitTime: chromeTimestamp > 0 ? chromeTimestamp / 1e6 - 11644473600 : 0
|
|
1010
|
+
};
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
function getHistoryDomains(days) {
|
|
1014
|
+
const timeWhere = buildTimeWhere(days);
|
|
1015
|
+
const whereClause = timeWhere ? `WHERE ${timeWhere}` : "";
|
|
1016
|
+
const sql = `
|
|
1017
|
+
SELECT
|
|
1018
|
+
domain,
|
|
1019
|
+
SUM(visit_count) AS visits,
|
|
1020
|
+
GROUP_CONCAT(title, char(31)) AS titles
|
|
1021
|
+
FROM (
|
|
1022
|
+
SELECT
|
|
1023
|
+
CASE
|
|
1024
|
+
WHEN instr(url, '//') > 0 AND instr(substr(url, instr(url, '//') + 2), '/') > 0
|
|
1025
|
+
THEN substr(
|
|
1026
|
+
substr(url, instr(url, '//') + 2),
|
|
1027
|
+
1,
|
|
1028
|
+
instr(substr(url, instr(url, '//') + 2), '/') - 1
|
|
1029
|
+
)
|
|
1030
|
+
WHEN instr(url, '//') > 0 THEN substr(url, instr(url, '//') + 2)
|
|
1031
|
+
WHEN instr(url, '/') > 0 THEN substr(url, 1, instr(url, '/') - 1)
|
|
1032
|
+
ELSE url
|
|
1033
|
+
END AS domain,
|
|
1034
|
+
IFNULL(visit_count, 0) AS visit_count,
|
|
1035
|
+
REPLACE(IFNULL(title, ''), char(31), ' ') AS title
|
|
1036
|
+
FROM urls
|
|
1037
|
+
${whereClause}
|
|
1038
|
+
)
|
|
1039
|
+
WHERE domain != ''
|
|
1040
|
+
GROUP BY domain
|
|
1041
|
+
ORDER BY visits DESC
|
|
1042
|
+
LIMIT 50;
|
|
1043
|
+
`.trim();
|
|
1044
|
+
return runHistoryQuery(sql, (row) => {
|
|
1045
|
+
if (row.length < 3) {
|
|
1046
|
+
return null;
|
|
1047
|
+
}
|
|
1048
|
+
const titles = row[2] ? Array.from(new Set(row[2].split(String.fromCharCode(31)).map((title) => title.trim()).filter(Boolean))).slice(0, 10) : [];
|
|
1049
|
+
return {
|
|
1050
|
+
domain: row[0] || "",
|
|
1051
|
+
visits: Number(row[1]) || 0,
|
|
1052
|
+
titles
|
|
1053
|
+
};
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
197
1056
|
|
|
198
1057
|
// packages/cli/src/commands/site.ts
|
|
199
|
-
import { readFileSync, readdirSync, existsSync as
|
|
200
|
-
import { join, relative } from "path";
|
|
201
|
-
import { homedir } from "os";
|
|
202
|
-
import { execSync } from "child_process";
|
|
203
|
-
var BB_DIR =
|
|
204
|
-
var LOCAL_SITES_DIR =
|
|
205
|
-
var COMMUNITY_SITES_DIR =
|
|
1058
|
+
import { readFileSync as readFileSync2, readdirSync, existsSync as existsSync4, mkdirSync } from "fs";
|
|
1059
|
+
import { join as join2, relative } from "path";
|
|
1060
|
+
import { homedir as homedir2 } from "os";
|
|
1061
|
+
import { execSync as execSync3 } from "child_process";
|
|
1062
|
+
var BB_DIR = join2(homedir2(), ".bb-browser");
|
|
1063
|
+
var LOCAL_SITES_DIR = join2(BB_DIR, "sites");
|
|
1064
|
+
var COMMUNITY_SITES_DIR = join2(BB_DIR, "bb-sites");
|
|
206
1065
|
var COMMUNITY_REPO = "https://github.com/epiral/bb-sites.git";
|
|
207
1066
|
function checkCliUpdate() {
|
|
208
1067
|
try {
|
|
209
|
-
const current =
|
|
210
|
-
const latest =
|
|
1068
|
+
const current = execSync3("bb-browser --version", { timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
|
|
1069
|
+
const latest = execSync3("npm view bb-browser version", { timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
|
|
211
1070
|
if (latest && current && latest !== current && latest.localeCompare(current, void 0, { numeric: true }) > 0) {
|
|
212
1071
|
console.log(`
|
|
213
1072
|
\u{1F4E6} bb-browser ${latest} available (current: ${current}). Run: npm install -g bb-browser`);
|
|
@@ -218,7 +1077,7 @@ function checkCliUpdate() {
|
|
|
218
1077
|
function parseSiteMeta(filePath, source) {
|
|
219
1078
|
let content;
|
|
220
1079
|
try {
|
|
221
|
-
content =
|
|
1080
|
+
content = readFileSync2(filePath, "utf-8");
|
|
222
1081
|
} catch {
|
|
223
1082
|
return null;
|
|
224
1083
|
}
|
|
@@ -278,7 +1137,7 @@ function parseSiteMeta(filePath, source) {
|
|
|
278
1137
|
return meta;
|
|
279
1138
|
}
|
|
280
1139
|
function scanSites(dir, source) {
|
|
281
|
-
if (!
|
|
1140
|
+
if (!existsSync4(dir)) return [];
|
|
282
1141
|
const sites = [];
|
|
283
1142
|
function walk(currentDir) {
|
|
284
1143
|
let entries;
|
|
@@ -288,7 +1147,7 @@ function scanSites(dir, source) {
|
|
|
288
1147
|
return;
|
|
289
1148
|
}
|
|
290
1149
|
for (const entry of entries) {
|
|
291
|
-
const fullPath =
|
|
1150
|
+
const fullPath = join2(currentDir, entry.name);
|
|
292
1151
|
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
293
1152
|
walk(fullPath);
|
|
294
1153
|
} else if (entry.isFile() && entry.name.endsWith(".js")) {
|
|
@@ -392,10 +1251,10 @@ function siteSearch(query, options) {
|
|
|
392
1251
|
}
|
|
393
1252
|
function siteUpdate() {
|
|
394
1253
|
mkdirSync(BB_DIR, { recursive: true });
|
|
395
|
-
if (
|
|
1254
|
+
if (existsSync4(join2(COMMUNITY_SITES_DIR, ".git"))) {
|
|
396
1255
|
console.log("\u66F4\u65B0\u793E\u533A site adapter \u5E93...");
|
|
397
1256
|
try {
|
|
398
|
-
|
|
1257
|
+
execSync3("git pull --ff-only", { cwd: COMMUNITY_SITES_DIR, stdio: "pipe" });
|
|
399
1258
|
console.log("\u66F4\u65B0\u5B8C\u6210\u3002");
|
|
400
1259
|
console.log("");
|
|
401
1260
|
console.log("\u{1F4A1} \u8FD0\u884C bb-browser site recommend \u770B\u770B\u54EA\u4E9B\u548C\u4F60\u7684\u6D4F\u89C8\u4E60\u60EF\u5339\u914D");
|
|
@@ -407,7 +1266,7 @@ function siteUpdate() {
|
|
|
407
1266
|
} else {
|
|
408
1267
|
console.log(`\u514B\u9686\u793E\u533A adapter \u5E93: ${COMMUNITY_REPO}`);
|
|
409
1268
|
try {
|
|
410
|
-
|
|
1269
|
+
execSync3(`git clone ${COMMUNITY_REPO} ${COMMUNITY_SITES_DIR}`, { stdio: "pipe" });
|
|
411
1270
|
console.log("\u514B\u9686\u5B8C\u6210\u3002");
|
|
412
1271
|
console.log("");
|
|
413
1272
|
console.log("\u{1F4A1} \u8FD0\u884C bb-browser site recommend \u770B\u770B\u54EA\u4E9B\u548C\u4F60\u7684\u6D4F\u89C8\u4E60\u60EF\u5339\u914D");
|
|
@@ -466,16 +1325,7 @@ function siteInfo(name, options) {
|
|
|
466
1325
|
}
|
|
467
1326
|
async function siteRecommend(options) {
|
|
468
1327
|
const days = options.days ?? 30;
|
|
469
|
-
const
|
|
470
|
-
id: generateId(),
|
|
471
|
-
action: "history",
|
|
472
|
-
historyCommand: "domains",
|
|
473
|
-
ms: days
|
|
474
|
-
});
|
|
475
|
-
if (!response.success) {
|
|
476
|
-
throw new Error(response.error || "History command failed");
|
|
477
|
-
}
|
|
478
|
-
const historyDomains = response.data?.historyDomains || [];
|
|
1328
|
+
const historyDomains = getHistoryDomains(days);
|
|
479
1329
|
const sites = getAllSites();
|
|
480
1330
|
const sitesByDomain = /* @__PURE__ */ new Map();
|
|
481
1331
|
for (const site of sites) {
|
|
@@ -593,7 +1443,7 @@ async function siteRun(name, args, options) {
|
|
|
593
1443
|
process.exit(1);
|
|
594
1444
|
}
|
|
595
1445
|
}
|
|
596
|
-
const jsContent =
|
|
1446
|
+
const jsContent = readFileSync2(site.filePath, "utf-8");
|
|
597
1447
|
const jsBody = jsContent.replace(/\/\*\s*@meta[\s\S]*?\*\//, "").trim();
|
|
598
1448
|
const argsJson = JSON.stringify(argMap);
|
|
599
1449
|
const script = `(${jsBody})(${argsJson})`;
|
|
@@ -653,7 +1503,7 @@ async function siteRun(name, args, options) {
|
|
|
653
1503
|
let targetTabId = options.tabId;
|
|
654
1504
|
if (!targetTabId && site.domain) {
|
|
655
1505
|
const listReq = { id: generateId(), action: "tab_list" };
|
|
656
|
-
const listResp = await
|
|
1506
|
+
const listResp = await sendCommand2(listReq);
|
|
657
1507
|
if (listResp.success && listResp.data?.tabs) {
|
|
658
1508
|
const matchingTab = listResp.data.tabs.find(
|
|
659
1509
|
(tab) => matchTabOrigin(tab.url, site.domain)
|
|
@@ -663,7 +1513,7 @@ async function siteRun(name, args, options) {
|
|
|
663
1513
|
}
|
|
664
1514
|
}
|
|
665
1515
|
if (!targetTabId) {
|
|
666
|
-
const newResp = await
|
|
1516
|
+
const newResp = await sendCommand2({
|
|
667
1517
|
id: generateId(),
|
|
668
1518
|
action: "tab_new",
|
|
669
1519
|
url: `https://${site.domain}`
|
|
@@ -673,7 +1523,7 @@ async function siteRun(name, args, options) {
|
|
|
673
1523
|
}
|
|
674
1524
|
}
|
|
675
1525
|
const evalReq = { id: generateId(), action: "eval", script, tabId: targetTabId };
|
|
676
|
-
const evalResp = await
|
|
1526
|
+
const evalResp = await sendCommand2(evalReq);
|
|
677
1527
|
if (!evalResp.success) {
|
|
678
1528
|
const hint = site.domain ? `Open https://${site.domain} in your browser, make sure you are logged in, then retry.` : void 0;
|
|
679
1529
|
if (options.json) {
|
|
@@ -808,10 +1658,10 @@ async function siteCommand(args, options = {}) {
|
|
|
808
1658
|
silentUpdate();
|
|
809
1659
|
}
|
|
810
1660
|
function silentUpdate() {
|
|
811
|
-
const gitDir =
|
|
812
|
-
if (!
|
|
813
|
-
import("child_process").then(({ spawn:
|
|
814
|
-
const child =
|
|
1661
|
+
const gitDir = join2(COMMUNITY_SITES_DIR, ".git");
|
|
1662
|
+
if (!existsSync4(gitDir)) return;
|
|
1663
|
+
import("child_process").then(({ spawn: spawn2 }) => {
|
|
1664
|
+
const child = spawn2("git", ["pull", "--ff-only"], {
|
|
815
1665
|
cwd: COMMUNITY_SITES_DIR,
|
|
816
1666
|
stdio: "ignore",
|
|
817
1667
|
detached: true
|
|
@@ -847,7 +1697,7 @@ async function openCommand(url, options = {}) {
|
|
|
847
1697
|
request.tabId = tabId;
|
|
848
1698
|
}
|
|
849
1699
|
}
|
|
850
|
-
const response = await
|
|
1700
|
+
const response = await sendCommand2(request);
|
|
851
1701
|
if (options.json) {
|
|
852
1702
|
console.log(JSON.stringify(response, null, 2));
|
|
853
1703
|
} else {
|
|
@@ -883,7 +1733,7 @@ async function snapshotCommand(options = {}) {
|
|
|
883
1733
|
selector: options.selector,
|
|
884
1734
|
tabId: options.tabId
|
|
885
1735
|
};
|
|
886
|
-
const response = await
|
|
1736
|
+
const response = await sendCommand2(request);
|
|
887
1737
|
if (options.json) {
|
|
888
1738
|
console.log(JSON.stringify(response, null, 2));
|
|
889
1739
|
} else {
|
|
@@ -902,7 +1752,7 @@ async function snapshotCommand(options = {}) {
|
|
|
902
1752
|
}
|
|
903
1753
|
|
|
904
1754
|
// packages/cli/src/commands/click.ts
|
|
905
|
-
function
|
|
1755
|
+
function parseRef2(ref) {
|
|
906
1756
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
907
1757
|
}
|
|
908
1758
|
async function clickCommand(ref, options = {}) {
|
|
@@ -910,14 +1760,14 @@ async function clickCommand(ref, options = {}) {
|
|
|
910
1760
|
throw new Error("\u7F3A\u5C11 ref \u53C2\u6570");
|
|
911
1761
|
}
|
|
912
1762
|
await ensureDaemonRunning();
|
|
913
|
-
const parsedRef =
|
|
1763
|
+
const parsedRef = parseRef2(ref);
|
|
914
1764
|
const request = {
|
|
915
1765
|
id: generateId(),
|
|
916
1766
|
action: "click",
|
|
917
1767
|
ref: parsedRef,
|
|
918
1768
|
tabId: options.tabId
|
|
919
1769
|
};
|
|
920
|
-
const response = await
|
|
1770
|
+
const response = await sendCommand2(request);
|
|
921
1771
|
if (options.json) {
|
|
922
1772
|
console.log(JSON.stringify(response, null, 2));
|
|
923
1773
|
} else {
|
|
@@ -937,7 +1787,7 @@ async function clickCommand(ref, options = {}) {
|
|
|
937
1787
|
}
|
|
938
1788
|
|
|
939
1789
|
// packages/cli/src/commands/hover.ts
|
|
940
|
-
function
|
|
1790
|
+
function parseRef3(ref) {
|
|
941
1791
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
942
1792
|
}
|
|
943
1793
|
async function hoverCommand(ref, options = {}) {
|
|
@@ -945,14 +1795,14 @@ async function hoverCommand(ref, options = {}) {
|
|
|
945
1795
|
throw new Error("\u7F3A\u5C11 ref \u53C2\u6570");
|
|
946
1796
|
}
|
|
947
1797
|
await ensureDaemonRunning();
|
|
948
|
-
const parsedRef =
|
|
1798
|
+
const parsedRef = parseRef3(ref);
|
|
949
1799
|
const request = {
|
|
950
1800
|
id: generateId(),
|
|
951
1801
|
action: "hover",
|
|
952
1802
|
ref: parsedRef,
|
|
953
1803
|
tabId: options.tabId
|
|
954
1804
|
};
|
|
955
|
-
const response = await
|
|
1805
|
+
const response = await sendCommand2(request);
|
|
956
1806
|
if (options.json) {
|
|
957
1807
|
console.log(JSON.stringify(response, null, 2));
|
|
958
1808
|
} else {
|
|
@@ -972,7 +1822,7 @@ async function hoverCommand(ref, options = {}) {
|
|
|
972
1822
|
}
|
|
973
1823
|
|
|
974
1824
|
// packages/cli/src/commands/fill.ts
|
|
975
|
-
function
|
|
1825
|
+
function parseRef4(ref) {
|
|
976
1826
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
977
1827
|
}
|
|
978
1828
|
async function fillCommand(ref, text, options = {}) {
|
|
@@ -983,7 +1833,7 @@ async function fillCommand(ref, text, options = {}) {
|
|
|
983
1833
|
throw new Error("\u7F3A\u5C11 text \u53C2\u6570");
|
|
984
1834
|
}
|
|
985
1835
|
await ensureDaemonRunning();
|
|
986
|
-
const parsedRef =
|
|
1836
|
+
const parsedRef = parseRef4(ref);
|
|
987
1837
|
const request = {
|
|
988
1838
|
id: generateId(),
|
|
989
1839
|
action: "fill",
|
|
@@ -991,7 +1841,7 @@ async function fillCommand(ref, text, options = {}) {
|
|
|
991
1841
|
text,
|
|
992
1842
|
tabId: options.tabId
|
|
993
1843
|
};
|
|
994
|
-
const response = await
|
|
1844
|
+
const response = await sendCommand2(request);
|
|
995
1845
|
if (options.json) {
|
|
996
1846
|
console.log(JSON.stringify(response, null, 2));
|
|
997
1847
|
} else {
|
|
@@ -1012,7 +1862,7 @@ async function fillCommand(ref, text, options = {}) {
|
|
|
1012
1862
|
}
|
|
1013
1863
|
|
|
1014
1864
|
// packages/cli/src/commands/type.ts
|
|
1015
|
-
function
|
|
1865
|
+
function parseRef5(ref) {
|
|
1016
1866
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
1017
1867
|
}
|
|
1018
1868
|
async function typeCommand(ref, text, options = {}) {
|
|
@@ -1023,7 +1873,7 @@ async function typeCommand(ref, text, options = {}) {
|
|
|
1023
1873
|
throw new Error("\u7F3A\u5C11 text \u53C2\u6570");
|
|
1024
1874
|
}
|
|
1025
1875
|
await ensureDaemonRunning();
|
|
1026
|
-
const parsedRef =
|
|
1876
|
+
const parsedRef = parseRef5(ref);
|
|
1027
1877
|
const request = {
|
|
1028
1878
|
id: generateId(),
|
|
1029
1879
|
action: "type",
|
|
@@ -1031,7 +1881,7 @@ async function typeCommand(ref, text, options = {}) {
|
|
|
1031
1881
|
text,
|
|
1032
1882
|
tabId: options.tabId
|
|
1033
1883
|
};
|
|
1034
|
-
const response = await
|
|
1884
|
+
const response = await sendCommand2(request);
|
|
1035
1885
|
if (options.json) {
|
|
1036
1886
|
console.log(JSON.stringify(response, null, 2));
|
|
1037
1887
|
} else {
|
|
@@ -1059,7 +1909,7 @@ async function closeCommand(options = {}) {
|
|
|
1059
1909
|
action: "close",
|
|
1060
1910
|
tabId: options.tabId
|
|
1061
1911
|
};
|
|
1062
|
-
const response = await
|
|
1912
|
+
const response = await sendCommand2(request);
|
|
1063
1913
|
if (options.json) {
|
|
1064
1914
|
console.log(JSON.stringify(response, null, 2));
|
|
1065
1915
|
} else {
|
|
@@ -1078,7 +1928,7 @@ async function closeCommand(options = {}) {
|
|
|
1078
1928
|
}
|
|
1079
1929
|
|
|
1080
1930
|
// packages/cli/src/commands/get.ts
|
|
1081
|
-
function
|
|
1931
|
+
function parseRef6(ref) {
|
|
1082
1932
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
1083
1933
|
}
|
|
1084
1934
|
async function getCommand(attribute, ref, options = {}) {
|
|
@@ -1090,10 +1940,10 @@ async function getCommand(attribute, ref, options = {}) {
|
|
|
1090
1940
|
id: generateId(),
|
|
1091
1941
|
action: "get",
|
|
1092
1942
|
attribute,
|
|
1093
|
-
ref: ref ?
|
|
1943
|
+
ref: ref ? parseRef6(ref) : void 0,
|
|
1094
1944
|
tabId: options.tabId
|
|
1095
1945
|
};
|
|
1096
|
-
const response = await
|
|
1946
|
+
const response = await sendCommand2(request);
|
|
1097
1947
|
if (options.json) {
|
|
1098
1948
|
console.log(JSON.stringify(response, null, 2));
|
|
1099
1949
|
} else {
|
|
@@ -1109,17 +1959,17 @@ async function getCommand(attribute, ref, options = {}) {
|
|
|
1109
1959
|
|
|
1110
1960
|
// packages/cli/src/commands/screenshot.ts
|
|
1111
1961
|
import fs from "fs";
|
|
1112
|
-
import
|
|
1113
|
-
import
|
|
1962
|
+
import path3 from "path";
|
|
1963
|
+
import os2 from "os";
|
|
1114
1964
|
function getDefaultPath() {
|
|
1115
1965
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1116
1966
|
const filename = `bb-screenshot-${timestamp}.png`;
|
|
1117
|
-
return
|
|
1967
|
+
return path3.join(os2.tmpdir(), filename);
|
|
1118
1968
|
}
|
|
1119
1969
|
function saveBase64Image(dataUrl, filePath) {
|
|
1120
1970
|
const base64Data = dataUrl.replace(/^data:image\/png;base64,/, "");
|
|
1121
1971
|
const buffer = Buffer.from(base64Data, "base64");
|
|
1122
|
-
const dir =
|
|
1972
|
+
const dir = path3.dirname(filePath);
|
|
1123
1973
|
if (!fs.existsSync(dir)) {
|
|
1124
1974
|
fs.mkdirSync(dir, { recursive: true });
|
|
1125
1975
|
}
|
|
@@ -1127,13 +1977,13 @@ function saveBase64Image(dataUrl, filePath) {
|
|
|
1127
1977
|
}
|
|
1128
1978
|
async function screenshotCommand(outputPath, options = {}) {
|
|
1129
1979
|
await ensureDaemonRunning();
|
|
1130
|
-
const filePath = outputPath ?
|
|
1980
|
+
const filePath = outputPath ? path3.resolve(outputPath) : getDefaultPath();
|
|
1131
1981
|
const request = {
|
|
1132
1982
|
id: generateId(),
|
|
1133
1983
|
action: "screenshot",
|
|
1134
1984
|
tabId: options.tabId
|
|
1135
1985
|
};
|
|
1136
|
-
const response = await
|
|
1986
|
+
const response = await sendCommand2(request);
|
|
1137
1987
|
if (response.success && response.data?.dataUrl) {
|
|
1138
1988
|
const dataUrl = response.data.dataUrl;
|
|
1139
1989
|
saveBase64Image(dataUrl, filePath);
|
|
@@ -1160,7 +2010,7 @@ async function screenshotCommand(outputPath, options = {}) {
|
|
|
1160
2010
|
function isTimeWait(target) {
|
|
1161
2011
|
return /^\d+$/.test(target);
|
|
1162
2012
|
}
|
|
1163
|
-
function
|
|
2013
|
+
function parseRef7(ref) {
|
|
1164
2014
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
1165
2015
|
}
|
|
1166
2016
|
async function waitCommand(target, options = {}) {
|
|
@@ -1179,7 +2029,7 @@ async function waitCommand(target, options = {}) {
|
|
|
1179
2029
|
tabId: options.tabId
|
|
1180
2030
|
};
|
|
1181
2031
|
} else {
|
|
1182
|
-
const ref =
|
|
2032
|
+
const ref = parseRef7(target);
|
|
1183
2033
|
request = {
|
|
1184
2034
|
id: generateId(),
|
|
1185
2035
|
action: "wait",
|
|
@@ -1188,7 +2038,7 @@ async function waitCommand(target, options = {}) {
|
|
|
1188
2038
|
tabId: options.tabId
|
|
1189
2039
|
};
|
|
1190
2040
|
}
|
|
1191
|
-
const response = await
|
|
2041
|
+
const response = await sendCommand2(request);
|
|
1192
2042
|
if (options.json) {
|
|
1193
2043
|
console.log(JSON.stringify(response, null, 2));
|
|
1194
2044
|
} else {
|
|
@@ -1196,7 +2046,7 @@ async function waitCommand(target, options = {}) {
|
|
|
1196
2046
|
if (isTimeWait(target)) {
|
|
1197
2047
|
console.log(`\u5DF2\u7B49\u5F85 ${target}ms`);
|
|
1198
2048
|
} else {
|
|
1199
|
-
console.log(`\u5143\u7D20 @${
|
|
2049
|
+
console.log(`\u5143\u7D20 @${parseRef7(target)} \u5DF2\u51FA\u73B0`);
|
|
1200
2050
|
}
|
|
1201
2051
|
} else {
|
|
1202
2052
|
console.error(`\u9519\u8BEF: ${response.error}`);
|
|
@@ -1236,7 +2086,7 @@ async function pressCommand(keyString, options = {}) {
|
|
|
1236
2086
|
modifiers,
|
|
1237
2087
|
tabId: options.tabId
|
|
1238
2088
|
};
|
|
1239
|
-
const response = await
|
|
2089
|
+
const response = await sendCommand2(request);
|
|
1240
2090
|
if (options.json) {
|
|
1241
2091
|
console.log(JSON.stringify(response, null, 2));
|
|
1242
2092
|
} else {
|
|
@@ -1277,7 +2127,7 @@ async function scrollCommand(direction, pixels, options = {}) {
|
|
|
1277
2127
|
pixels: pixelValue,
|
|
1278
2128
|
tabId: options.tabId
|
|
1279
2129
|
};
|
|
1280
|
-
const response = await
|
|
2130
|
+
const response = await sendCommand2(request);
|
|
1281
2131
|
if (options.json) {
|
|
1282
2132
|
console.log(JSON.stringify(response, null, 2));
|
|
1283
2133
|
} else {
|
|
@@ -1291,76 +2141,17 @@ async function scrollCommand(direction, pixels, options = {}) {
|
|
|
1291
2141
|
}
|
|
1292
2142
|
|
|
1293
2143
|
// packages/cli/src/commands/daemon.ts
|
|
1294
|
-
import { spawn as spawn2 } from "child_process";
|
|
1295
|
-
async function daemonCommand(options = {}) {
|
|
1296
|
-
if (await isDaemonRunning()) {
|
|
1297
|
-
if (options.json) {
|
|
1298
|
-
console.log(JSON.stringify({ success: false, error: "Daemon \u5DF2\u5728\u8FD0\u884C" }));
|
|
1299
|
-
} else {
|
|
1300
|
-
console.log("Daemon \u5DF2\u5728\u8FD0\u884C");
|
|
1301
|
-
}
|
|
1302
|
-
return;
|
|
1303
|
-
}
|
|
1304
|
-
const daemonPath = getDaemonPath();
|
|
1305
|
-
const args = [daemonPath];
|
|
1306
|
-
if (options.host) {
|
|
1307
|
-
args.push("--host", options.host);
|
|
1308
|
-
}
|
|
1309
|
-
if (options.json) {
|
|
1310
|
-
console.log(JSON.stringify({ success: true, message: "Daemon \u542F\u52A8\u4E2D..." }));
|
|
1311
|
-
} else {
|
|
1312
|
-
console.log("Daemon \u542F\u52A8\u4E2D...");
|
|
1313
|
-
}
|
|
1314
|
-
await new Promise((resolve2, reject) => {
|
|
1315
|
-
const child = spawn2(process.execPath, args, {
|
|
1316
|
-
stdio: "inherit"
|
|
1317
|
-
});
|
|
1318
|
-
child.on("exit", (code) => {
|
|
1319
|
-
if (code && code !== 0) {
|
|
1320
|
-
reject(new Error(`Daemon exited with code ${code}`));
|
|
1321
|
-
} else {
|
|
1322
|
-
resolve2();
|
|
1323
|
-
}
|
|
1324
|
-
});
|
|
1325
|
-
child.on("error", reject);
|
|
1326
|
-
});
|
|
1327
|
-
}
|
|
1328
|
-
async function stopCommand(options = {}) {
|
|
1329
|
-
if (!await isDaemonRunning()) {
|
|
1330
|
-
if (options.json) {
|
|
1331
|
-
console.log(JSON.stringify({ success: false, error: "Daemon \u672A\u8FD0\u884C" }));
|
|
1332
|
-
} else {
|
|
1333
|
-
console.log("Daemon \u672A\u8FD0\u884C");
|
|
1334
|
-
}
|
|
1335
|
-
return;
|
|
1336
|
-
}
|
|
1337
|
-
const stopped = await stopDaemon();
|
|
1338
|
-
if (stopped) {
|
|
1339
|
-
if (options.json) {
|
|
1340
|
-
console.log(JSON.stringify({ success: true, message: "Daemon \u5DF2\u505C\u6B62" }));
|
|
1341
|
-
} else {
|
|
1342
|
-
console.log("Daemon \u5DF2\u505C\u6B62");
|
|
1343
|
-
}
|
|
1344
|
-
} else {
|
|
1345
|
-
if (options.json) {
|
|
1346
|
-
console.log(JSON.stringify({ success: false, error: "\u65E0\u6CD5\u505C\u6B62 Daemon" }));
|
|
1347
|
-
} else {
|
|
1348
|
-
console.error("\u65E0\u6CD5\u505C\u6B62 Daemon");
|
|
1349
|
-
}
|
|
1350
|
-
process.exit(1);
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
2144
|
async function statusCommand(options = {}) {
|
|
1354
2145
|
const running = await isDaemonRunning();
|
|
1355
2146
|
if (options.json) {
|
|
1356
2147
|
console.log(JSON.stringify({ running }));
|
|
1357
2148
|
} else {
|
|
1358
|
-
console.log(running ? "
|
|
2149
|
+
console.log(running ? "\u6D4F\u89C8\u5668\u8FD0\u884C\u4E2D" : "\u6D4F\u89C8\u5668\u672A\u8FD0\u884C");
|
|
1359
2150
|
}
|
|
1360
2151
|
}
|
|
1361
2152
|
|
|
1362
2153
|
// packages/cli/src/commands/reload.ts
|
|
1363
|
-
import
|
|
2154
|
+
import WebSocket2 from "ws";
|
|
1364
2155
|
var EXTENSION_NAME = "bb-browser";
|
|
1365
2156
|
async function reloadCommand(options = {}) {
|
|
1366
2157
|
const port = options.port || 9222;
|
|
@@ -1377,7 +2168,7 @@ async function reloadCommand(options = {}) {
|
|
|
1377
2168
|
throw new Error("\u8BF7\u5148\u6253\u5F00 chrome://extensions \u9875\u9762");
|
|
1378
2169
|
}
|
|
1379
2170
|
const result = await new Promise((resolve2, reject) => {
|
|
1380
|
-
const ws = new
|
|
2171
|
+
const ws = new WebSocket2(extPage.webSocketDebuggerUrl);
|
|
1381
2172
|
let resolved = false;
|
|
1382
2173
|
const timeout = setTimeout(() => {
|
|
1383
2174
|
if (!resolved) {
|
|
@@ -1474,7 +2265,7 @@ async function backCommand(options = {}) {
|
|
|
1474
2265
|
action: "back",
|
|
1475
2266
|
tabId: options.tabId
|
|
1476
2267
|
};
|
|
1477
|
-
const response = await
|
|
2268
|
+
const response = await sendCommand2(request);
|
|
1478
2269
|
if (options.json) {
|
|
1479
2270
|
console.log(JSON.stringify(response, null, 2));
|
|
1480
2271
|
} else {
|
|
@@ -1498,7 +2289,7 @@ async function forwardCommand(options = {}) {
|
|
|
1498
2289
|
action: "forward",
|
|
1499
2290
|
tabId: options.tabId
|
|
1500
2291
|
};
|
|
1501
|
-
const response = await
|
|
2292
|
+
const response = await sendCommand2(request);
|
|
1502
2293
|
if (options.json) {
|
|
1503
2294
|
console.log(JSON.stringify(response, null, 2));
|
|
1504
2295
|
} else {
|
|
@@ -1522,7 +2313,7 @@ async function refreshCommand(options = {}) {
|
|
|
1522
2313
|
action: "refresh",
|
|
1523
2314
|
tabId: options.tabId
|
|
1524
2315
|
};
|
|
1525
|
-
const response = await
|
|
2316
|
+
const response = await sendCommand2(request);
|
|
1526
2317
|
if (options.json) {
|
|
1527
2318
|
console.log(JSON.stringify(response, null, 2));
|
|
1528
2319
|
} else {
|
|
@@ -1541,7 +2332,7 @@ async function refreshCommand(options = {}) {
|
|
|
1541
2332
|
}
|
|
1542
2333
|
|
|
1543
2334
|
// packages/cli/src/commands/check.ts
|
|
1544
|
-
function
|
|
2335
|
+
function parseRef8(ref) {
|
|
1545
2336
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
1546
2337
|
}
|
|
1547
2338
|
async function checkCommand(ref, options = {}) {
|
|
@@ -1549,14 +2340,14 @@ async function checkCommand(ref, options = {}) {
|
|
|
1549
2340
|
throw new Error("\u7F3A\u5C11 ref \u53C2\u6570");
|
|
1550
2341
|
}
|
|
1551
2342
|
await ensureDaemonRunning();
|
|
1552
|
-
const parsedRef =
|
|
2343
|
+
const parsedRef = parseRef8(ref);
|
|
1553
2344
|
const request = {
|
|
1554
2345
|
id: generateId(),
|
|
1555
2346
|
action: "check",
|
|
1556
2347
|
ref: parsedRef,
|
|
1557
2348
|
tabId: options.tabId
|
|
1558
2349
|
};
|
|
1559
|
-
const response = await
|
|
2350
|
+
const response = await sendCommand2(request);
|
|
1560
2351
|
if (options.json) {
|
|
1561
2352
|
console.log(JSON.stringify(response, null, 2));
|
|
1562
2353
|
} else {
|
|
@@ -1588,14 +2379,14 @@ async function uncheckCommand(ref, options = {}) {
|
|
|
1588
2379
|
throw new Error("\u7F3A\u5C11 ref \u53C2\u6570");
|
|
1589
2380
|
}
|
|
1590
2381
|
await ensureDaemonRunning();
|
|
1591
|
-
const parsedRef =
|
|
2382
|
+
const parsedRef = parseRef8(ref);
|
|
1592
2383
|
const request = {
|
|
1593
2384
|
id: generateId(),
|
|
1594
2385
|
action: "uncheck",
|
|
1595
2386
|
ref: parsedRef,
|
|
1596
2387
|
tabId: options.tabId
|
|
1597
2388
|
};
|
|
1598
|
-
const response = await
|
|
2389
|
+
const response = await sendCommand2(request);
|
|
1599
2390
|
if (options.json) {
|
|
1600
2391
|
console.log(JSON.stringify(response, null, 2));
|
|
1601
2392
|
} else {
|
|
@@ -1624,7 +2415,7 @@ async function uncheckCommand(ref, options = {}) {
|
|
|
1624
2415
|
}
|
|
1625
2416
|
|
|
1626
2417
|
// packages/cli/src/commands/select.ts
|
|
1627
|
-
function
|
|
2418
|
+
function parseRef9(ref) {
|
|
1628
2419
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
1629
2420
|
}
|
|
1630
2421
|
async function selectCommand(ref, value, options = {}) {
|
|
@@ -1635,7 +2426,7 @@ async function selectCommand(ref, value, options = {}) {
|
|
|
1635
2426
|
throw new Error("\u7F3A\u5C11 value \u53C2\u6570");
|
|
1636
2427
|
}
|
|
1637
2428
|
await ensureDaemonRunning();
|
|
1638
|
-
const parsedRef =
|
|
2429
|
+
const parsedRef = parseRef9(ref);
|
|
1639
2430
|
const request = {
|
|
1640
2431
|
id: generateId(),
|
|
1641
2432
|
action: "select",
|
|
@@ -1643,7 +2434,7 @@ async function selectCommand(ref, value, options = {}) {
|
|
|
1643
2434
|
value,
|
|
1644
2435
|
tabId: options.tabId
|
|
1645
2436
|
};
|
|
1646
|
-
const response = await
|
|
2437
|
+
const response = await sendCommand2(request);
|
|
1647
2438
|
if (options.json) {
|
|
1648
2439
|
console.log(JSON.stringify(response, null, 2));
|
|
1649
2440
|
} else {
|
|
@@ -1681,7 +2472,7 @@ async function evalCommand(script, options = {}) {
|
|
|
1681
2472
|
script,
|
|
1682
2473
|
tabId: options.tabId
|
|
1683
2474
|
};
|
|
1684
|
-
const response = await
|
|
2475
|
+
const response = await sendCommand2(request);
|
|
1685
2476
|
if (options.json) {
|
|
1686
2477
|
console.log(JSON.stringify(response, null, 2));
|
|
1687
2478
|
} else {
|
|
@@ -1768,7 +2559,7 @@ async function tabCommand(args, options = {}) {
|
|
|
1768
2559
|
index: parsed.index,
|
|
1769
2560
|
tabId: parsed.tabId
|
|
1770
2561
|
};
|
|
1771
|
-
const response = await
|
|
2562
|
+
const response = await sendCommand2(request);
|
|
1772
2563
|
if (options.json) {
|
|
1773
2564
|
console.log(JSON.stringify(response, null, 2));
|
|
1774
2565
|
} else {
|
|
@@ -1817,7 +2608,7 @@ async function frameCommand(selector, options = {}) {
|
|
|
1817
2608
|
selector,
|
|
1818
2609
|
tabId: options.tabId
|
|
1819
2610
|
};
|
|
1820
|
-
const response = await
|
|
2611
|
+
const response = await sendCommand2(request);
|
|
1821
2612
|
if (options.json) {
|
|
1822
2613
|
console.log(JSON.stringify(response, null, 2));
|
|
1823
2614
|
} else {
|
|
@@ -1841,7 +2632,7 @@ async function frameMainCommand(options = {}) {
|
|
|
1841
2632
|
action: "frame_main",
|
|
1842
2633
|
tabId: options.tabId
|
|
1843
2634
|
};
|
|
1844
|
-
const response = await
|
|
2635
|
+
const response = await sendCommand2(request);
|
|
1845
2636
|
if (options.json) {
|
|
1846
2637
|
console.log(JSON.stringify(response, null, 2));
|
|
1847
2638
|
} else {
|
|
@@ -1867,7 +2658,7 @@ async function dialogCommand(subCommand, promptText, options = {}) {
|
|
|
1867
2658
|
promptText: subCommand === "accept" ? promptText : void 0,
|
|
1868
2659
|
tabId: options.tabId
|
|
1869
2660
|
};
|
|
1870
|
-
const response = await
|
|
2661
|
+
const response = await sendCommand2(request);
|
|
1871
2662
|
if (options.json) {
|
|
1872
2663
|
console.log(JSON.stringify(response, null, 2));
|
|
1873
2664
|
} else {
|
|
@@ -1888,7 +2679,7 @@ async function dialogCommand(subCommand, promptText, options = {}) {
|
|
|
1888
2679
|
|
|
1889
2680
|
// packages/cli/src/commands/network.ts
|
|
1890
2681
|
async function networkCommand(subCommand, urlOrFilter, options = {}) {
|
|
1891
|
-
const response = await
|
|
2682
|
+
const response = await sendCommand2({
|
|
1892
2683
|
id: crypto.randomUUID(),
|
|
1893
2684
|
action: "network",
|
|
1894
2685
|
networkCommand: subCommand,
|
|
@@ -1975,7 +2766,7 @@ async function networkCommand(subCommand, urlOrFilter, options = {}) {
|
|
|
1975
2766
|
|
|
1976
2767
|
// packages/cli/src/commands/console.ts
|
|
1977
2768
|
async function consoleCommand(options = {}) {
|
|
1978
|
-
const response = await
|
|
2769
|
+
const response = await sendCommand2({
|
|
1979
2770
|
id: crypto.randomUUID(),
|
|
1980
2771
|
action: "console",
|
|
1981
2772
|
consoleCommand: options.clear ? "clear" : "get",
|
|
@@ -2020,7 +2811,7 @@ async function consoleCommand(options = {}) {
|
|
|
2020
2811
|
|
|
2021
2812
|
// packages/cli/src/commands/errors.ts
|
|
2022
2813
|
async function errorsCommand(options = {}) {
|
|
2023
|
-
const response = await
|
|
2814
|
+
const response = await sendCommand2({
|
|
2024
2815
|
id: crypto.randomUUID(),
|
|
2025
2816
|
action: "errors",
|
|
2026
2817
|
errorsCommand: options.clear ? "clear" : "get",
|
|
@@ -2060,7 +2851,7 @@ async function errorsCommand(options = {}) {
|
|
|
2060
2851
|
|
|
2061
2852
|
// packages/cli/src/commands/trace.ts
|
|
2062
2853
|
async function traceCommand(subCommand, options = {}) {
|
|
2063
|
-
const response = await
|
|
2854
|
+
const response = await sendCommand2({
|
|
2064
2855
|
id: crypto.randomUUID(),
|
|
2065
2856
|
action: "trace",
|
|
2066
2857
|
traceCommand: subCommand,
|
|
@@ -2150,7 +2941,7 @@ function matchTabOrigin2(tabUrl, targetHostname) {
|
|
|
2150
2941
|
}
|
|
2151
2942
|
async function ensureTabForOrigin(origin, hostname) {
|
|
2152
2943
|
const listReq = { id: generateId(), action: "tab_list" };
|
|
2153
|
-
const listResp = await
|
|
2944
|
+
const listResp = await sendCommand2(listReq);
|
|
2154
2945
|
if (listResp.success && listResp.data?.tabs) {
|
|
2155
2946
|
const matchingTab = listResp.data.tabs.find(
|
|
2156
2947
|
(tab) => matchTabOrigin2(tab.url, hostname)
|
|
@@ -2159,7 +2950,7 @@ async function ensureTabForOrigin(origin, hostname) {
|
|
|
2159
2950
|
return matchingTab.tabId;
|
|
2160
2951
|
}
|
|
2161
2952
|
}
|
|
2162
|
-
const newResp = await
|
|
2953
|
+
const newResp = await sendCommand2({ id: generateId(), action: "tab_new", url: origin });
|
|
2163
2954
|
if (!newResp.success) {
|
|
2164
2955
|
throw new Error(`\u65E0\u6CD5\u6253\u5F00 ${origin}: ${newResp.error}`);
|
|
2165
2956
|
}
|
|
@@ -2228,7 +3019,7 @@ async function fetchCommand(url, options = {}) {
|
|
|
2228
3019
|
}
|
|
2229
3020
|
const script = buildFetchScript(url, options);
|
|
2230
3021
|
const evalReq = { id: generateId(), action: "eval", script, tabId: targetTabId };
|
|
2231
|
-
const evalResp = await
|
|
3022
|
+
const evalResp = await sendCommand2(evalReq);
|
|
2232
3023
|
if (!evalResp.success) {
|
|
2233
3024
|
throw new Error(`Fetch \u5931\u8D25: ${evalResp.error}`);
|
|
2234
3025
|
}
|
|
@@ -2262,21 +3053,16 @@ async function fetchCommand(url, options = {}) {
|
|
|
2262
3053
|
|
|
2263
3054
|
// packages/cli/src/commands/history.ts
|
|
2264
3055
|
async function historyCommand(subCommand, options = {}) {
|
|
2265
|
-
const
|
|
2266
|
-
|
|
2267
|
-
action: "history",
|
|
2268
|
-
historyCommand: subCommand,
|
|
2269
|
-
text: options.query,
|
|
2270
|
-
ms: options.days
|
|
2271
|
-
});
|
|
3056
|
+
const days = options.days || 30;
|
|
3057
|
+
const data = subCommand === "search" ? { historyItems: searchHistory(options.query, days) } : { historyDomains: getHistoryDomains(days) };
|
|
2272
3058
|
if (options.json) {
|
|
2273
|
-
console.log(JSON.stringify(
|
|
3059
|
+
console.log(JSON.stringify({
|
|
3060
|
+
id: crypto.randomUUID(),
|
|
3061
|
+
success: true,
|
|
3062
|
+
data
|
|
3063
|
+
}));
|
|
2274
3064
|
return;
|
|
2275
3065
|
}
|
|
2276
|
-
if (!response.success) {
|
|
2277
|
-
throw new Error(response.error || "History command failed");
|
|
2278
|
-
}
|
|
2279
|
-
const data = response.data;
|
|
2280
3066
|
switch (subCommand) {
|
|
2281
3067
|
case "search": {
|
|
2282
3068
|
const items = data?.historyItems || [];
|
|
@@ -2315,10 +3101,13 @@ async function historyCommand(subCommand, options = {}) {
|
|
|
2315
3101
|
}
|
|
2316
3102
|
|
|
2317
3103
|
// packages/cli/src/index.ts
|
|
2318
|
-
var VERSION = "0.
|
|
3104
|
+
var VERSION = "0.8.0";
|
|
2319
3105
|
var HELP_TEXT = `
|
|
2320
3106
|
bb-browser - AI Agent \u6D4F\u89C8\u5668\u81EA\u52A8\u5316\u5DE5\u5177
|
|
2321
3107
|
|
|
3108
|
+
\u5B89\u88C5\uFF1A
|
|
3109
|
+
npm install -g bb-browser
|
|
3110
|
+
|
|
2322
3111
|
\u63D0\u793A\uFF1A\u5927\u591A\u6570\u6570\u636E\u83B7\u53D6\u4EFB\u52A1\u8BF7\u76F4\u63A5\u4F7F\u7528 site \u547D\u4EE4\uFF0C\u65E0\u9700\u624B\u52A8\u64CD\u4F5C\u6D4F\u89C8\u5668\uFF1A
|
|
2323
3112
|
bb-browser site list \u67E5\u770B\u6240\u6709\u53EF\u7528\u547D\u4EE4
|
|
2324
3113
|
bb-browser site twitter/search "AI" \u793A\u4F8B\uFF1A\u641C\u7D22\u63A8\u6587
|
|
@@ -2356,6 +3145,7 @@ bb-browser - AI Agent \u6D4F\u89C8\u5668\u81EA\u52A8\u5316\u5DE5\u5177
|
|
|
2356
3145
|
|
|
2357
3146
|
\u6807\u7B7E\u9875\uFF1A
|
|
2358
3147
|
tab [list|new|close|<n>] \u7BA1\u7406\u6807\u7B7E\u9875
|
|
3148
|
+
status \u67E5\u770B\u53D7\u7BA1\u6D4F\u89C8\u5668\u72B6\u6001
|
|
2359
3149
|
|
|
2360
3150
|
\u5BFC\u822A\uFF1A
|
|
2361
3151
|
back / forward / refresh \u540E\u9000 / \u524D\u8FDB / \u5237\u65B0
|
|
@@ -2369,6 +3159,8 @@ bb-browser - AI Agent \u6D4F\u89C8\u5668\u81EA\u52A8\u5316\u5DE5\u5177
|
|
|
2369
3159
|
|
|
2370
3160
|
\u9009\u9879\uFF1A
|
|
2371
3161
|
--json \u4EE5 JSON \u683C\u5F0F\u8F93\u51FA
|
|
3162
|
+
--port <n> \u6307\u5B9A Chrome CDP \u7AEF\u53E3
|
|
3163
|
+
--openclaw \u4F18\u5148\u590D\u7528 OpenClaw \u6D4F\u89C8\u5668\u5B9E\u4F8B
|
|
2372
3164
|
--jq <expr> \u5BF9 JSON \u8F93\u51FA\u5E94\u7528 jq \u8FC7\u6EE4\uFF08\u76F4\u63A5\u4F5C\u7528\u4E8E\u6570\u636E\uFF0C\u8DF3\u8FC7 id/success \u4FE1\u5C01\uFF09
|
|
2373
3165
|
-i, --interactive \u53EA\u8F93\u51FA\u53EF\u4EA4\u4E92\u5143\u7D20\uFF08snapshot \u547D\u4EE4\uFF09
|
|
2374
3166
|
-c, --compact \u79FB\u9664\u7A7A\u7ED3\u6784\u8282\u70B9\uFF08snapshot \u547D\u4EE4\uFF09
|
|
@@ -2409,6 +3201,12 @@ function parseArgs(argv) {
|
|
|
2409
3201
|
}
|
|
2410
3202
|
} else if (arg === "--openclaw") {
|
|
2411
3203
|
result.flags.openclaw = true;
|
|
3204
|
+
} else if (arg === "--port") {
|
|
3205
|
+
skipNext = true;
|
|
3206
|
+
const nextIdx = args.indexOf(arg) + 1;
|
|
3207
|
+
if (nextIdx < args.length) {
|
|
3208
|
+
result.flags.port = parseInt(args[nextIdx], 10);
|
|
3209
|
+
}
|
|
2412
3210
|
} else if (arg === "--help" || arg === "-h") {
|
|
2413
3211
|
result.flags.help = true;
|
|
2414
3212
|
} else if (arg === "--version" || arg === "-v") {
|
|
@@ -2458,9 +3256,9 @@ async function main() {
|
|
|
2458
3256
|
return;
|
|
2459
3257
|
}
|
|
2460
3258
|
if (process.argv.includes("--mcp")) {
|
|
2461
|
-
const mcpPath =
|
|
2462
|
-
const { spawn:
|
|
2463
|
-
const child =
|
|
3259
|
+
const mcpPath = fileURLToPath3(new URL("./mcp.js", import.meta.url));
|
|
3260
|
+
const { spawn: spawn2 } = await import("child_process");
|
|
3261
|
+
const child = spawn2(process.execPath, [mcpPath], { stdio: "inherit" });
|
|
2464
3262
|
child.on("exit", (code) => process.exit(code ?? 0));
|
|
2465
3263
|
return;
|
|
2466
3264
|
}
|
|
@@ -2818,9 +3616,9 @@ async function main() {
|
|
|
2818
3616
|
break;
|
|
2819
3617
|
}
|
|
2820
3618
|
case "star": {
|
|
2821
|
-
const { execSync:
|
|
3619
|
+
const { execSync: execSync4 } = await import("child_process");
|
|
2822
3620
|
try {
|
|
2823
|
-
|
|
3621
|
+
execSync4("gh auth status", { stdio: "pipe" });
|
|
2824
3622
|
} catch {
|
|
2825
3623
|
console.error("\u9700\u8981\u5148\u5B89\u88C5\u5E76\u767B\u5F55 GitHub CLI: https://cli.github.com");
|
|
2826
3624
|
console.error(" brew install gh && gh auth login");
|
|
@@ -2829,7 +3627,7 @@ async function main() {
|
|
|
2829
3627
|
const repos = ["epiral/bb-browser", "epiral/bb-sites"];
|
|
2830
3628
|
for (const repo of repos) {
|
|
2831
3629
|
try {
|
|
2832
|
-
|
|
3630
|
+
execSync4(`gh api user/starred/${repo} -X PUT`, { stdio: "pipe" });
|
|
2833
3631
|
console.log(`\u2B50 Starred ${repo}`);
|
|
2834
3632
|
} catch {
|
|
2835
3633
|
console.log(`Already starred or failed: ${repo}`);
|
|
@@ -2919,5 +3717,5 @@ Full guide: https://github.com/epiral/bb-sites/blob/main/SKILL.md`);
|
|
|
2919
3717
|
process.exit(1);
|
|
2920
3718
|
}
|
|
2921
3719
|
}
|
|
2922
|
-
main();
|
|
3720
|
+
main().then(() => process.exit(0));
|
|
2923
3721
|
//# sourceMappingURL=cli.js.map
|