bb-browser 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1107 -312
- 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,858 @@ 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", () => resolve2(ws));
|
|
236
|
+
ws.once("error", reject);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
function createState(host, port, browserWsUrl, browserSocket) {
|
|
240
|
+
const state = {
|
|
241
|
+
host,
|
|
242
|
+
port,
|
|
243
|
+
browserWsUrl,
|
|
244
|
+
browserSocket,
|
|
245
|
+
browserPending: /* @__PURE__ */ new Map(),
|
|
246
|
+
nextMessageId: 1,
|
|
247
|
+
sessions: /* @__PURE__ */ new Map(),
|
|
248
|
+
attachedTargets: /* @__PURE__ */ new Map(),
|
|
249
|
+
refsByTarget: /* @__PURE__ */ new Map(),
|
|
250
|
+
activeFrameIdByTarget: /* @__PURE__ */ new Map(),
|
|
251
|
+
dialogHandlers: /* @__PURE__ */ new Map()
|
|
252
|
+
};
|
|
253
|
+
browserSocket.on("message", (raw) => {
|
|
254
|
+
const message = JSON.parse(raw.toString());
|
|
255
|
+
if (typeof message.id === "number") {
|
|
256
|
+
const pending = state.browserPending.get(message.id);
|
|
257
|
+
if (!pending) return;
|
|
258
|
+
state.browserPending.delete(message.id);
|
|
259
|
+
if (message.error) {
|
|
260
|
+
pending.reject(new Error(`${pending.method}: ${message.error.message ?? "Unknown CDP error"}`));
|
|
261
|
+
} else {
|
|
262
|
+
pending.resolve(message.result);
|
|
263
|
+
}
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (message.method === "Target.attachedToTarget") {
|
|
267
|
+
const params = message.params;
|
|
268
|
+
const sessionId = params.sessionId;
|
|
269
|
+
const targetInfo = params.targetInfo;
|
|
270
|
+
if (typeof sessionId === "string" && typeof targetInfo?.targetId === "string") {
|
|
271
|
+
state.sessions.set(targetInfo.targetId, sessionId);
|
|
272
|
+
state.attachedTargets.set(sessionId, targetInfo.targetId);
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (message.method === "Target.detachedFromTarget") {
|
|
277
|
+
const params = message.params;
|
|
278
|
+
const sessionId = params.sessionId;
|
|
279
|
+
if (typeof sessionId === "string") {
|
|
280
|
+
const targetId = state.attachedTargets.get(sessionId);
|
|
281
|
+
if (targetId) {
|
|
282
|
+
state.sessions.delete(targetId);
|
|
283
|
+
state.attachedTargets.delete(sessionId);
|
|
284
|
+
state.activeFrameIdByTarget.delete(targetId);
|
|
285
|
+
state.dialogHandlers.delete(targetId);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (message.method === "Target.receivedMessageFromTarget") {
|
|
291
|
+
const params = message.params;
|
|
292
|
+
const sessionId = params.sessionId;
|
|
293
|
+
const messageText = params.message;
|
|
294
|
+
if (typeof sessionId === "string" && typeof messageText === "string") {
|
|
295
|
+
const targetId = state.attachedTargets.get(sessionId);
|
|
296
|
+
if (targetId) {
|
|
297
|
+
handleSessionEvent(targetId, JSON.parse(messageText)).catch(() => {
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (typeof message.sessionId === "string" && typeof message.method === "string") {
|
|
304
|
+
const targetId = state.attachedTargets.get(message.sessionId);
|
|
305
|
+
if (targetId) {
|
|
306
|
+
handleSessionEvent(targetId, message).catch(() => {
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
browserSocket.on("close", () => {
|
|
312
|
+
if (connectionState === state) {
|
|
313
|
+
connectionState = null;
|
|
314
|
+
}
|
|
315
|
+
for (const pending of state.browserPending.values()) {
|
|
316
|
+
pending.reject(new Error("CDP connection closed"));
|
|
317
|
+
}
|
|
318
|
+
state.browserPending.clear();
|
|
319
|
+
});
|
|
320
|
+
browserSocket.on("error", () => {
|
|
321
|
+
});
|
|
322
|
+
return state;
|
|
323
|
+
}
|
|
324
|
+
async function browserCommand(method, params = {}) {
|
|
325
|
+
const state = connectionState;
|
|
326
|
+
if (!state) throw new Error("CDP connection not initialized");
|
|
327
|
+
const id = state.nextMessageId++;
|
|
328
|
+
const payload = JSON.stringify({ id, method, params });
|
|
329
|
+
const promise = new Promise((resolve2, reject) => {
|
|
330
|
+
state.browserPending.set(id, { resolve: resolve2, reject, method });
|
|
331
|
+
});
|
|
332
|
+
state.browserSocket.send(payload);
|
|
333
|
+
return promise;
|
|
334
|
+
}
|
|
335
|
+
async function sessionCommand(targetId, method, params = {}) {
|
|
336
|
+
const state = connectionState;
|
|
337
|
+
if (!state) throw new Error("CDP connection not initialized");
|
|
338
|
+
const sessionId = state.sessions.get(targetId) ?? await attachTarget(targetId);
|
|
339
|
+
const id = state.nextMessageId++;
|
|
340
|
+
const payload = JSON.stringify({ id, method, params, sessionId });
|
|
341
|
+
return new Promise((resolve2, reject) => {
|
|
342
|
+
const check = (raw) => {
|
|
343
|
+
const msg = JSON.parse(raw.toString());
|
|
344
|
+
if (msg.id === id && msg.sessionId === sessionId) {
|
|
345
|
+
state.browserSocket.off("message", check);
|
|
346
|
+
if (msg.error) reject(new Error(`${method}: ${msg.error.message ?? "Unknown CDP error"}`));
|
|
347
|
+
else resolve2(msg.result);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
state.browserSocket.on("message", check);
|
|
351
|
+
state.browserSocket.send(payload);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
function getActiveFrameId(targetId) {
|
|
355
|
+
const frameId = connectionState?.activeFrameIdByTarget.get(targetId);
|
|
356
|
+
return frameId ?? void 0;
|
|
357
|
+
}
|
|
358
|
+
async function pageCommand(targetId, method, params = {}) {
|
|
359
|
+
const frameId = getActiveFrameId(targetId);
|
|
360
|
+
return sessionCommand(targetId, method, frameId ? { ...params, frameId } : params);
|
|
361
|
+
}
|
|
362
|
+
function normalizeHeaders(headers) {
|
|
363
|
+
if (!headers || typeof headers !== "object") return void 0;
|
|
364
|
+
return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, String(value)]));
|
|
365
|
+
}
|
|
366
|
+
async function handleSessionEvent(targetId, event) {
|
|
367
|
+
const method = event.method;
|
|
368
|
+
const params = event.params ?? {};
|
|
369
|
+
if (typeof method !== "string") return;
|
|
370
|
+
if (method === "Page.javascriptDialogOpening") {
|
|
371
|
+
const handler = connectionState?.dialogHandlers.get(targetId);
|
|
372
|
+
if (handler) {
|
|
373
|
+
await sessionCommand(targetId, "Page.handleJavaScriptDialog", {
|
|
374
|
+
accept: handler.accept,
|
|
375
|
+
...handler.promptText !== void 0 ? { promptText: handler.promptText } : {}
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (method === "Network.requestWillBeSent") {
|
|
381
|
+
const requestId = typeof params.requestId === "string" ? params.requestId : void 0;
|
|
382
|
+
const request = params.request;
|
|
383
|
+
if (!requestId || !request) return;
|
|
384
|
+
networkRequests.set(requestId, {
|
|
385
|
+
requestId,
|
|
386
|
+
url: String(request.url ?? ""),
|
|
387
|
+
method: String(request.method ?? "GET"),
|
|
388
|
+
type: String(params.type ?? "Other"),
|
|
389
|
+
timestamp: Math.round(Number(params.timestamp ?? Date.now()) * 1e3),
|
|
390
|
+
requestHeaders: normalizeHeaders(request.headers),
|
|
391
|
+
requestBody: typeof request.postData === "string" ? request.postData : void 0
|
|
392
|
+
});
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (method === "Network.responseReceived") {
|
|
396
|
+
const requestId = typeof params.requestId === "string" ? params.requestId : void 0;
|
|
397
|
+
const response = params.response;
|
|
398
|
+
if (!requestId || !response) return;
|
|
399
|
+
const existing = networkRequests.get(requestId);
|
|
400
|
+
if (!existing) return;
|
|
401
|
+
existing.status = typeof response.status === "number" ? response.status : void 0;
|
|
402
|
+
existing.statusText = typeof response.statusText === "string" ? response.statusText : void 0;
|
|
403
|
+
existing.responseHeaders = normalizeHeaders(response.headers);
|
|
404
|
+
existing.mimeType = typeof response.mimeType === "string" ? response.mimeType : void 0;
|
|
405
|
+
networkRequests.set(requestId, existing);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (method === "Network.loadingFailed") {
|
|
409
|
+
const requestId = typeof params.requestId === "string" ? params.requestId : void 0;
|
|
410
|
+
if (!requestId) return;
|
|
411
|
+
const existing = networkRequests.get(requestId);
|
|
412
|
+
if (!existing) return;
|
|
413
|
+
existing.failed = true;
|
|
414
|
+
existing.failureReason = typeof params.errorText === "string" ? params.errorText : "Unknown error";
|
|
415
|
+
networkRequests.set(requestId, existing);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (method === "Runtime.consoleAPICalled") {
|
|
419
|
+
const type = String(params.type ?? "log");
|
|
420
|
+
const args = Array.isArray(params.args) ? params.args : [];
|
|
421
|
+
const text = args.map((arg) => {
|
|
422
|
+
if (typeof arg.value === "string") return arg.value;
|
|
423
|
+
if (arg.value !== void 0) return String(arg.value);
|
|
424
|
+
if (typeof arg.description === "string") return arg.description;
|
|
425
|
+
return "";
|
|
426
|
+
}).filter(Boolean).join(" ");
|
|
427
|
+
const stack = params.stackTrace;
|
|
428
|
+
const firstCallFrame = Array.isArray(stack?.callFrames) ? stack?.callFrames[0] : void 0;
|
|
429
|
+
consoleMessages.push({
|
|
430
|
+
type: ["log", "info", "warn", "error", "debug"].includes(type) ? type : "log",
|
|
431
|
+
text,
|
|
432
|
+
timestamp: Math.round(Number(params.timestamp ?? Date.now())),
|
|
433
|
+
url: typeof firstCallFrame?.url === "string" ? firstCallFrame.url : void 0,
|
|
434
|
+
lineNumber: typeof firstCallFrame?.lineNumber === "number" ? firstCallFrame.lineNumber : void 0
|
|
435
|
+
});
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (method === "Runtime.exceptionThrown") {
|
|
439
|
+
const details = params.exceptionDetails;
|
|
440
|
+
if (!details) return;
|
|
441
|
+
const exception = details.exception;
|
|
442
|
+
const stackTrace = details.stackTrace;
|
|
443
|
+
const callFrames = Array.isArray(stackTrace?.callFrames) ? stackTrace.callFrames : [];
|
|
444
|
+
jsErrors.push({
|
|
445
|
+
message: typeof exception?.description === "string" ? exception.description : String(details.text ?? "JavaScript exception"),
|
|
446
|
+
url: typeof details.url === "string" ? details.url : typeof callFrames[0]?.url === "string" ? String(callFrames[0].url) : void 0,
|
|
447
|
+
lineNumber: typeof details.lineNumber === "number" ? details.lineNumber : void 0,
|
|
448
|
+
columnNumber: typeof details.columnNumber === "number" ? details.columnNumber : void 0,
|
|
449
|
+
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,
|
|
450
|
+
timestamp: Date.now()
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async function ensureNetworkMonitoring(targetId) {
|
|
455
|
+
if (networkEnabled) return;
|
|
456
|
+
await sessionCommand(targetId, "Network.enable");
|
|
457
|
+
networkEnabled = true;
|
|
458
|
+
}
|
|
459
|
+
async function ensureConsoleMonitoring(targetId) {
|
|
460
|
+
if (consoleEnabled && errorsEnabled) return;
|
|
461
|
+
await sessionCommand(targetId, "Runtime.enable");
|
|
462
|
+
consoleEnabled = true;
|
|
463
|
+
errorsEnabled = true;
|
|
464
|
+
}
|
|
465
|
+
async function attachTarget(targetId) {
|
|
466
|
+
const result = await browserCommand("Target.attachToTarget", {
|
|
467
|
+
targetId,
|
|
468
|
+
flatten: true
|
|
469
|
+
});
|
|
470
|
+
connectionState?.sessions.set(targetId, result.sessionId);
|
|
471
|
+
connectionState?.attachedTargets.set(result.sessionId, targetId);
|
|
472
|
+
connectionState?.activeFrameIdByTarget.set(targetId, connectionState?.activeFrameIdByTarget.get(targetId) ?? null);
|
|
473
|
+
await sessionCommand(targetId, "Page.enable");
|
|
474
|
+
await sessionCommand(targetId, "Runtime.enable");
|
|
475
|
+
await sessionCommand(targetId, "DOM.enable");
|
|
476
|
+
await sessionCommand(targetId, "Accessibility.enable");
|
|
477
|
+
return result.sessionId;
|
|
478
|
+
}
|
|
479
|
+
async function getTargets() {
|
|
480
|
+
const state = connectionState;
|
|
481
|
+
if (!state) throw new Error("CDP connection not initialized");
|
|
482
|
+
try {
|
|
483
|
+
const result = await browserCommand("Target.getTargets");
|
|
484
|
+
return (result.targetInfos || []).map((target) => ({
|
|
485
|
+
id: target.targetId,
|
|
486
|
+
type: target.type,
|
|
487
|
+
title: target.title,
|
|
488
|
+
url: target.url,
|
|
489
|
+
webSocketDebuggerUrl: ""
|
|
490
|
+
}));
|
|
491
|
+
} catch {
|
|
492
|
+
return getJsonList(state.host, state.port);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
async function ensurePageTarget(targetId) {
|
|
496
|
+
const targets = (await getTargets()).filter((target2) => target2.type === "page");
|
|
497
|
+
if (targets.length === 0) throw new Error("No page target found");
|
|
498
|
+
let target;
|
|
499
|
+
if (typeof targetId === "number") {
|
|
500
|
+
target = targets[targetId] ?? targets.find((item) => Number(item.id) === targetId);
|
|
501
|
+
} else if (typeof targetId === "string") {
|
|
502
|
+
target = targets.find((item) => item.id === targetId);
|
|
503
|
+
}
|
|
504
|
+
target ??= targets[0];
|
|
505
|
+
connectionState.currentTargetId = target.id;
|
|
506
|
+
await attachTarget(target.id);
|
|
507
|
+
return target;
|
|
508
|
+
}
|
|
509
|
+
function parseRef(ref) {
|
|
510
|
+
const refs = connectionState?.refsByTarget.get(connectionState.currentTargetId ?? "") ?? {};
|
|
511
|
+
const found = refs[ref];
|
|
512
|
+
if (!found?.backendDOMNodeId) {
|
|
513
|
+
throw new Error(`Unknown ref: ${ref}. Run snapshot first.`);
|
|
514
|
+
}
|
|
515
|
+
return found.backendDOMNodeId;
|
|
516
|
+
}
|
|
517
|
+
function loadBuildDomTreeScript() {
|
|
518
|
+
const currentDir = path2.dirname(fileURLToPath(import.meta.url));
|
|
519
|
+
const candidates = [
|
|
520
|
+
path2.resolve(currentDir, "../../../extension/buildDomTree.js"),
|
|
521
|
+
path2.resolve(currentDir, "../../extension/buildDomTree.js"),
|
|
522
|
+
path2.resolve(currentDir, "../../../packages/extension/buildDomTree.js"),
|
|
523
|
+
path2.resolve(currentDir, "../../../extension/dist/buildDomTree.js")
|
|
524
|
+
];
|
|
525
|
+
for (const candidate of candidates) {
|
|
526
|
+
try {
|
|
527
|
+
return readFileSync(candidate, "utf8");
|
|
528
|
+
} catch {
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
throw new Error("Cannot find buildDomTree.js");
|
|
532
|
+
}
|
|
533
|
+
async function evaluate(targetId, expression, returnByValue = true) {
|
|
534
|
+
const result = await sessionCommand(targetId, "Runtime.evaluate", {
|
|
535
|
+
expression,
|
|
536
|
+
awaitPromise: true,
|
|
537
|
+
returnByValue
|
|
538
|
+
});
|
|
539
|
+
if (result.exceptionDetails) {
|
|
540
|
+
throw new Error(result.exceptionDetails.text || "Runtime.evaluate failed");
|
|
541
|
+
}
|
|
542
|
+
return result.result.value ?? result.result;
|
|
543
|
+
}
|
|
544
|
+
async function resolveNode(targetId, backendNodeId) {
|
|
545
|
+
const result = await sessionCommand(targetId, "DOM.pushNodesByBackendIdsToFrontend", {
|
|
546
|
+
backendNodeIds: [backendNodeId]
|
|
547
|
+
});
|
|
548
|
+
return result.nodeId;
|
|
549
|
+
}
|
|
550
|
+
async function focusNode(targetId, backendNodeId) {
|
|
551
|
+
const nodeId = await resolveNode(targetId, backendNodeId);
|
|
552
|
+
await sessionCommand(targetId, "DOM.focus", { nodeId });
|
|
553
|
+
}
|
|
554
|
+
async function getNodeBox(targetId, backendNodeId) {
|
|
555
|
+
const result = await sessionCommand(targetId, "DOM.getBoxModel", {
|
|
556
|
+
backendNodeId
|
|
557
|
+
});
|
|
558
|
+
const quad = result.model.content.length >= 8 ? result.model.content : result.model.border;
|
|
559
|
+
const xs = [quad[0], quad[2], quad[4], quad[6]];
|
|
560
|
+
const ys = [quad[1], quad[3], quad[5], quad[7]];
|
|
561
|
+
return {
|
|
562
|
+
x: xs.reduce((a, b) => a + b, 0) / xs.length,
|
|
563
|
+
y: ys.reduce((a, b) => a + b, 0) / ys.length
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
async function mouseClick(targetId, x, y) {
|
|
567
|
+
await sessionCommand(targetId, "Input.dispatchMouseEvent", { type: "mouseMoved", x, y, button: "none" });
|
|
568
|
+
await sessionCommand(targetId, "Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 });
|
|
569
|
+
await sessionCommand(targetId, "Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 });
|
|
570
|
+
}
|
|
571
|
+
async function getAttributeValue(targetId, backendNodeId, attribute) {
|
|
572
|
+
const nodeId = await resolveNode(targetId, backendNodeId);
|
|
573
|
+
if (attribute === "text") {
|
|
574
|
+
return evaluate(targetId, `(() => { const n = this; return n.innerText ?? n.textContent ?? ''; }).call(document.querySelector('[data-bb-node-id="${nodeId}"]'))`);
|
|
575
|
+
}
|
|
576
|
+
const result = await sessionCommand(targetId, "DOM.resolveNode", { backendNodeId });
|
|
577
|
+
const call = await sessionCommand(targetId, "Runtime.callFunctionOn", {
|
|
578
|
+
objectId: result.object.objectId,
|
|
579
|
+
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)}) || ''; }`,
|
|
580
|
+
returnByValue: true
|
|
581
|
+
});
|
|
582
|
+
return String(call.result.value ?? "");
|
|
583
|
+
}
|
|
584
|
+
async function buildSnapshot(targetId, request) {
|
|
585
|
+
const script = loadBuildDomTreeScript();
|
|
586
|
+
const expression = `(() => { ${script}; return (typeof buildDomTree === 'function' ? buildDomTree : globalThis.buildDomTree)(${JSON.stringify({
|
|
587
|
+
interactiveOnly: !!request.interactive,
|
|
588
|
+
compact: !!request.compact,
|
|
589
|
+
maxDepth: request.maxDepth,
|
|
590
|
+
selector: request.selector
|
|
591
|
+
})}); })()`;
|
|
592
|
+
const value = await evaluate(targetId, expression, true);
|
|
593
|
+
connectionState?.refsByTarget.set(targetId, value.refs || {});
|
|
594
|
+
return value;
|
|
595
|
+
}
|
|
596
|
+
function ok(id, data) {
|
|
597
|
+
return { id, success: true, data };
|
|
598
|
+
}
|
|
599
|
+
function fail(id, error) {
|
|
600
|
+
return { id, success: false, error: buildRequestError(error).message };
|
|
601
|
+
}
|
|
602
|
+
async function ensureCdpConnection() {
|
|
603
|
+
if (connectionState) return;
|
|
604
|
+
if (reconnecting) return reconnecting;
|
|
605
|
+
reconnecting = (async () => {
|
|
606
|
+
const discovered = await discoverCdpPort();
|
|
607
|
+
if (!discovered) {
|
|
608
|
+
throw new Error("No browser connection found");
|
|
609
|
+
}
|
|
610
|
+
const version = await getJsonVersion(discovered.host, discovered.port);
|
|
611
|
+
const wsUrl = version.webSocketDebuggerUrl;
|
|
612
|
+
const socket = await connectWebSocket(wsUrl);
|
|
613
|
+
connectionState = createState(discovered.host, discovered.port, wsUrl, socket);
|
|
614
|
+
})();
|
|
615
|
+
try {
|
|
616
|
+
await reconnecting;
|
|
617
|
+
} finally {
|
|
618
|
+
reconnecting = null;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
async function sendCommand(request) {
|
|
622
|
+
try {
|
|
623
|
+
await ensureCdpConnection();
|
|
624
|
+
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("\u8BF7\u6C42\u8D85\u65F6")), COMMAND_TIMEOUT));
|
|
625
|
+
return await Promise.race([dispatchRequest(request), timeout]);
|
|
626
|
+
} catch (error) {
|
|
627
|
+
return fail(request.id, error);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
async function dispatchRequest(request) {
|
|
631
|
+
const target = await ensurePageTarget(request.tabId);
|
|
632
|
+
switch (request.action) {
|
|
633
|
+
case "open": {
|
|
634
|
+
if (!request.url) return fail(request.id, "Missing url parameter");
|
|
635
|
+
if (request.tabId === void 0) {
|
|
636
|
+
const created = await browserCommand("Target.createTarget", { url: request.url });
|
|
637
|
+
const newTarget = await ensurePageTarget(created.targetId);
|
|
638
|
+
return ok(request.id, { url: request.url, tabId: Number(newTarget.id) || void 0 });
|
|
639
|
+
}
|
|
640
|
+
await pageCommand(target.id, "Page.navigate", { url: request.url });
|
|
641
|
+
return ok(request.id, { url: request.url, title: target.title, tabId: Number(target.id) || void 0 });
|
|
642
|
+
}
|
|
643
|
+
case "snapshot": {
|
|
644
|
+
const snapshotData = await buildSnapshot(target.id, request);
|
|
645
|
+
return ok(request.id, { title: target.title, url: target.url, snapshotData });
|
|
646
|
+
}
|
|
647
|
+
case "click":
|
|
648
|
+
case "hover": {
|
|
649
|
+
if (!request.ref) return fail(request.id, "Missing ref parameter");
|
|
650
|
+
const backendNodeId = parseRef(request.ref);
|
|
651
|
+
const point = await getNodeBox(target.id, backendNodeId);
|
|
652
|
+
await sessionCommand(target.id, "Input.dispatchMouseEvent", { type: "mouseMoved", x: point.x, y: point.y, button: "none" });
|
|
653
|
+
if (request.action === "click") await mouseClick(target.id, point.x, point.y);
|
|
654
|
+
return ok(request.id, {});
|
|
655
|
+
}
|
|
656
|
+
case "fill":
|
|
657
|
+
case "type": {
|
|
658
|
+
if (!request.ref) return fail(request.id, "Missing ref parameter");
|
|
659
|
+
if (request.text == null) return fail(request.id, "Missing text parameter");
|
|
660
|
+
const backendNodeId = parseRef(request.ref);
|
|
661
|
+
await focusNode(target.id, backendNodeId);
|
|
662
|
+
if (request.action === "fill") {
|
|
663
|
+
await evaluate(target.id, `document.activeElement && ((document.activeElement.value = ''), document.activeElement.dispatchEvent(new Event('input', { bubbles: true })))`);
|
|
664
|
+
}
|
|
665
|
+
await sessionCommand(target.id, "Input.insertText", { text: request.text });
|
|
666
|
+
return ok(request.id, { value: request.text });
|
|
667
|
+
}
|
|
668
|
+
case "check":
|
|
669
|
+
case "uncheck": {
|
|
670
|
+
if (!request.ref) return fail(request.id, "Missing ref parameter");
|
|
671
|
+
const backendNodeId = parseRef(request.ref);
|
|
672
|
+
const desired = request.action === "check";
|
|
673
|
+
const resolved = await sessionCommand(target.id, "DOM.resolveNode", { backendNodeId });
|
|
674
|
+
await sessionCommand(target.id, "Runtime.callFunctionOn", {
|
|
675
|
+
objectId: resolved.object.objectId,
|
|
676
|
+
functionDeclaration: `function() { this.checked = ${desired}; this.dispatchEvent(new Event('input', { bubbles: true })); this.dispatchEvent(new Event('change', { bubbles: true })); }`
|
|
677
|
+
});
|
|
678
|
+
return ok(request.id, {});
|
|
679
|
+
}
|
|
680
|
+
case "select": {
|
|
681
|
+
if (!request.ref || request.value == null) return fail(request.id, "Missing ref or value parameter");
|
|
682
|
+
const backendNodeId = parseRef(request.ref);
|
|
683
|
+
const resolved = await sessionCommand(target.id, "DOM.resolveNode", { backendNodeId });
|
|
684
|
+
await sessionCommand(target.id, "Runtime.callFunctionOn", {
|
|
685
|
+
objectId: resolved.object.objectId,
|
|
686
|
+
functionDeclaration: `function() { this.value = ${JSON.stringify(request.value)}; this.dispatchEvent(new Event('input', { bubbles: true })); this.dispatchEvent(new Event('change', { bubbles: true })); }`
|
|
687
|
+
});
|
|
688
|
+
return ok(request.id, { value: request.value });
|
|
689
|
+
}
|
|
690
|
+
case "get": {
|
|
691
|
+
if (!request.ref || !request.attribute) return fail(request.id, "Missing ref or attribute parameter");
|
|
692
|
+
const value = await getAttributeValue(target.id, parseRef(request.ref), request.attribute);
|
|
693
|
+
return ok(request.id, { value });
|
|
694
|
+
}
|
|
695
|
+
case "screenshot": {
|
|
696
|
+
const result = await sessionCommand(target.id, "Page.captureScreenshot", { format: "png", fromSurface: true });
|
|
697
|
+
return ok(request.id, { dataUrl: `data:image/png;base64,${result.data}` });
|
|
698
|
+
}
|
|
699
|
+
case "close": {
|
|
700
|
+
await browserCommand("Target.closeTarget", { targetId: target.id });
|
|
701
|
+
return ok(request.id, {});
|
|
702
|
+
}
|
|
703
|
+
case "wait": {
|
|
704
|
+
await new Promise((resolve2) => setTimeout(resolve2, request.ms ?? 1e3));
|
|
705
|
+
return ok(request.id, {});
|
|
706
|
+
}
|
|
707
|
+
case "press": {
|
|
708
|
+
if (!request.key) return fail(request.id, "Missing key parameter");
|
|
709
|
+
await sessionCommand(target.id, "Input.dispatchKeyEvent", { type: "keyDown", key: request.key });
|
|
710
|
+
if (request.key.length === 1) {
|
|
711
|
+
await sessionCommand(target.id, "Input.dispatchKeyEvent", { type: "char", text: request.key, key: request.key });
|
|
712
|
+
}
|
|
713
|
+
await sessionCommand(target.id, "Input.dispatchKeyEvent", { type: "keyUp", key: request.key });
|
|
714
|
+
return ok(request.id, {});
|
|
715
|
+
}
|
|
716
|
+
case "scroll": {
|
|
717
|
+
const deltaY = request.direction === "up" ? -(request.pixels ?? 300) : request.pixels ?? 300;
|
|
718
|
+
await sessionCommand(target.id, "Input.dispatchMouseEvent", { type: "mouseWheel", x: 0, y: 0, deltaX: 0, deltaY });
|
|
719
|
+
return ok(request.id, {});
|
|
720
|
+
}
|
|
721
|
+
case "back": {
|
|
722
|
+
await evaluate(target.id, "history.back(); undefined");
|
|
723
|
+
return ok(request.id, {});
|
|
724
|
+
}
|
|
725
|
+
case "forward": {
|
|
726
|
+
await evaluate(target.id, "history.forward(); undefined");
|
|
727
|
+
return ok(request.id, {});
|
|
728
|
+
}
|
|
729
|
+
case "refresh": {
|
|
730
|
+
await sessionCommand(target.id, "Page.reload", { ignoreCache: false });
|
|
731
|
+
return ok(request.id, {});
|
|
732
|
+
}
|
|
733
|
+
case "eval": {
|
|
734
|
+
if (!request.script) return fail(request.id, "Missing script parameter");
|
|
735
|
+
const result = await evaluate(target.id, request.script, true);
|
|
736
|
+
return ok(request.id, { result });
|
|
737
|
+
}
|
|
738
|
+
case "tab_list": {
|
|
739
|
+
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 }));
|
|
740
|
+
return ok(request.id, { tabs, activeIndex: tabs.findIndex((tab) => tab.active) });
|
|
741
|
+
}
|
|
742
|
+
case "tab_new": {
|
|
743
|
+
const created = await browserCommand("Target.createTarget", { url: request.url ?? "about:blank" });
|
|
744
|
+
return ok(request.id, { tabId: Number(created.targetId) || void 0, url: request.url ?? "about:blank" });
|
|
745
|
+
}
|
|
746
|
+
case "tab_select": {
|
|
747
|
+
const tabs = (await getTargets()).filter((item) => item.type === "page");
|
|
748
|
+
const selected = request.tabId !== void 0 ? tabs.find((item) => item.id === String(request.tabId) || Number(item.id) === request.tabId) : tabs[request.index ?? 0];
|
|
749
|
+
if (!selected) return fail(request.id, "Tab not found");
|
|
750
|
+
connectionState.currentTargetId = selected.id;
|
|
751
|
+
await attachTarget(selected.id);
|
|
752
|
+
return ok(request.id, { tabId: Number(selected.id) || void 0, url: selected.url, title: selected.title });
|
|
753
|
+
}
|
|
754
|
+
case "tab_close": {
|
|
755
|
+
const tabs = (await getTargets()).filter((item) => item.type === "page");
|
|
756
|
+
const selected = request.tabId !== void 0 ? tabs.find((item) => item.id === String(request.tabId) || Number(item.id) === request.tabId) : tabs[request.index ?? 0];
|
|
757
|
+
if (!selected) return fail(request.id, "Tab not found");
|
|
758
|
+
await browserCommand("Target.closeTarget", { targetId: selected.id });
|
|
759
|
+
return ok(request.id, { tabId: Number(selected.id) || void 0 });
|
|
760
|
+
}
|
|
761
|
+
case "frame": {
|
|
762
|
+
if (!request.selector) return fail(request.id, "Missing selector parameter");
|
|
763
|
+
const document = await pageCommand(target.id, "DOM.getDocument", {});
|
|
764
|
+
const node = await pageCommand(target.id, "DOM.querySelector", { nodeId: document.root.nodeId, selector: request.selector });
|
|
765
|
+
if (!node.nodeId) return fail(request.id, `\u627E\u4E0D\u5230 iframe: ${request.selector}`);
|
|
766
|
+
const described = await pageCommand(target.id, "DOM.describeNode", { nodeId: node.nodeId });
|
|
767
|
+
const frameId = described.node.frameId;
|
|
768
|
+
const nodeName = String(described.node.nodeName ?? "").toLowerCase();
|
|
769
|
+
if (!frameId) return fail(request.id, `\u65E0\u6CD5\u83B7\u53D6 iframe frameId: ${request.selector}`);
|
|
770
|
+
if (nodeName && nodeName !== "iframe" && nodeName !== "frame") return fail(request.id, `\u5143\u7D20\u4E0D\u662F iframe: ${nodeName}`);
|
|
771
|
+
connectionState?.activeFrameIdByTarget.set(target.id, frameId);
|
|
772
|
+
const attributes = described.node.attributes ?? [];
|
|
773
|
+
const attrMap = {};
|
|
774
|
+
for (let i = 0; i < attributes.length; i += 2) attrMap[String(attributes[i])] = String(attributes[i + 1] ?? "");
|
|
775
|
+
return ok(request.id, { frameInfo: { selector: request.selector, name: attrMap.name ?? "", url: attrMap.src ?? "", frameId } });
|
|
776
|
+
}
|
|
777
|
+
case "frame_main": {
|
|
778
|
+
connectionState?.activeFrameIdByTarget.set(target.id, null);
|
|
779
|
+
return ok(request.id, { frameInfo: { frameId: 0 } });
|
|
780
|
+
}
|
|
781
|
+
case "dialog": {
|
|
782
|
+
connectionState?.dialogHandlers.set(target.id, { accept: request.dialogResponse !== "dismiss", ...request.promptText !== void 0 ? { promptText: request.promptText } : {} });
|
|
783
|
+
await sessionCommand(target.id, "Page.enable");
|
|
784
|
+
return ok(request.id, { dialog: { armed: true, response: request.dialogResponse ?? "accept" } });
|
|
785
|
+
}
|
|
786
|
+
case "network": {
|
|
787
|
+
const subCommand = request.networkCommand ?? "requests";
|
|
788
|
+
switch (subCommand) {
|
|
789
|
+
case "requests": {
|
|
790
|
+
await ensureNetworkMonitoring(target.id);
|
|
791
|
+
const requests = Array.from(networkRequests.values()).filter((item) => !request.filter || item.url.includes(request.filter));
|
|
792
|
+
if (request.withBody) {
|
|
793
|
+
await Promise.all(requests.map(async (item) => {
|
|
794
|
+
if (item.failed || item.responseBody !== void 0 || item.bodyError !== void 0) return;
|
|
795
|
+
try {
|
|
796
|
+
const body = await sessionCommand(target.id, "Network.getResponseBody", { requestId: item.requestId });
|
|
797
|
+
item.responseBody = body.body;
|
|
798
|
+
item.responseBodyBase64 = body.base64Encoded;
|
|
799
|
+
} catch (error) {
|
|
800
|
+
item.bodyError = error instanceof Error ? error.message : String(error);
|
|
801
|
+
}
|
|
802
|
+
}));
|
|
803
|
+
}
|
|
804
|
+
return ok(request.id, { networkRequests: requests });
|
|
805
|
+
}
|
|
806
|
+
case "route":
|
|
807
|
+
return ok(request.id, { routeCount: 0 });
|
|
808
|
+
case "unroute":
|
|
809
|
+
return ok(request.id, { routeCount: 0 });
|
|
810
|
+
case "clear":
|
|
811
|
+
networkRequests.clear();
|
|
812
|
+
return ok(request.id, {});
|
|
813
|
+
default:
|
|
814
|
+
return fail(request.id, `Unknown network subcommand: ${subCommand}`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
case "console": {
|
|
818
|
+
const subCommand = request.consoleCommand ?? "get";
|
|
819
|
+
await ensureConsoleMonitoring(target.id);
|
|
820
|
+
switch (subCommand) {
|
|
821
|
+
case "get":
|
|
822
|
+
return ok(request.id, { consoleMessages: consoleMessages.filter((item) => !request.filter || item.text.includes(request.filter)) });
|
|
823
|
+
case "clear":
|
|
824
|
+
consoleMessages.length = 0;
|
|
825
|
+
return ok(request.id, {});
|
|
826
|
+
default:
|
|
827
|
+
return fail(request.id, `Unknown console subcommand: ${subCommand}`);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
case "errors": {
|
|
831
|
+
const subCommand = request.errorsCommand ?? "get";
|
|
832
|
+
await ensureConsoleMonitoring(target.id);
|
|
833
|
+
switch (subCommand) {
|
|
834
|
+
case "get":
|
|
835
|
+
return ok(request.id, { jsErrors: jsErrors.filter((item) => !request.filter || item.message.includes(request.filter) || item.url?.includes(request.filter)) });
|
|
836
|
+
case "clear":
|
|
837
|
+
jsErrors.length = 0;
|
|
838
|
+
return ok(request.id, {});
|
|
839
|
+
default:
|
|
840
|
+
return fail(request.id, `Unknown errors subcommand: ${subCommand}`);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
case "trace": {
|
|
844
|
+
const subCommand = request.traceCommand ?? "status";
|
|
845
|
+
switch (subCommand) {
|
|
846
|
+
case "start":
|
|
847
|
+
traceRecording = true;
|
|
848
|
+
traceEvents.length = 0;
|
|
849
|
+
return ok(request.id, { traceStatus: { recording: true, eventCount: 0 } });
|
|
850
|
+
case "stop": {
|
|
851
|
+
traceRecording = false;
|
|
852
|
+
return ok(request.id, { traceEvents: [...traceEvents], traceStatus: { recording: false, eventCount: traceEvents.length } });
|
|
853
|
+
}
|
|
854
|
+
case "status":
|
|
855
|
+
return ok(request.id, { traceStatus: { recording: traceRecording, eventCount: traceEvents.length } });
|
|
856
|
+
default:
|
|
857
|
+
return fail(request.id, `Unknown trace subcommand: ${subCommand}`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
default:
|
|
861
|
+
return fail(request.id, `Action not yet supported in direct CDP mode: ${request.action}`);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
14
864
|
|
|
15
865
|
// packages/cli/src/client.ts
|
|
16
866
|
var jqExpression;
|
|
@@ -30,184 +880,190 @@ function handleJqResponse(response) {
|
|
|
30
880
|
printJqResults(response);
|
|
31
881
|
}
|
|
32
882
|
}
|
|
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
|
-
}
|
|
883
|
+
async function sendCommand2(request) {
|
|
884
|
+
return sendCommand(request);
|
|
101
885
|
}
|
|
102
886
|
|
|
103
887
|
// packages/cli/src/daemon-manager.ts
|
|
104
|
-
import {
|
|
105
|
-
import { existsSync } from "fs";
|
|
106
|
-
import { fileURLToPath } from "url";
|
|
888
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
107
889
|
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;
|
|
890
|
+
import { existsSync as existsSync2 } from "fs";
|
|
119
891
|
async function isDaemonRunning() {
|
|
892
|
+
return await discoverCdpPort() !== null;
|
|
893
|
+
}
|
|
894
|
+
async function ensureDaemonRunning() {
|
|
120
895
|
try {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
896
|
+
await ensureCdpConnection();
|
|
897
|
+
} catch (error) {
|
|
898
|
+
if (error instanceof Error && error.message.includes("No browser connection found")) {
|
|
899
|
+
throw new Error([
|
|
900
|
+
"bb-browser: Could not start browser.",
|
|
901
|
+
"",
|
|
902
|
+
"Make sure Chrome is installed, then try again.",
|
|
903
|
+
"Or specify a CDP port manually: bb-browser --port 9222"
|
|
904
|
+
].join("\n"));
|
|
905
|
+
}
|
|
906
|
+
throw error;
|
|
130
907
|
}
|
|
131
908
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
909
|
+
|
|
910
|
+
// packages/cli/src/history-sqlite.ts
|
|
911
|
+
import { copyFileSync, existsSync as existsSync3, unlinkSync } from "fs";
|
|
912
|
+
import { execSync as execSync2 } from "child_process";
|
|
913
|
+
import { homedir, tmpdir } from "os";
|
|
914
|
+
import { join } from "path";
|
|
915
|
+
function getHistoryPathCandidates() {
|
|
916
|
+
const home = homedir();
|
|
917
|
+
const localAppData = process.env.LOCALAPPDATA || "";
|
|
918
|
+
const candidates = [
|
|
919
|
+
join(home, "Library/Application Support/Google/Chrome/Default/History"),
|
|
920
|
+
join(home, "Library/Application Support/Microsoft Edge/Default/History"),
|
|
921
|
+
join(home, "Library/Application Support/BraveSoftware/Brave-Browser/Default/History"),
|
|
922
|
+
join(home, "Library/Application Support/Arc/User Data/Default/History"),
|
|
923
|
+
join(home, ".config/google-chrome/Default/History")
|
|
924
|
+
];
|
|
925
|
+
if (localAppData) {
|
|
926
|
+
candidates.push(
|
|
927
|
+
join(localAppData, "Google/Chrome/User Data/Default/History"),
|
|
928
|
+
join(localAppData, "Microsoft/Edge/User Data/Default/History")
|
|
929
|
+
);
|
|
145
930
|
}
|
|
931
|
+
return candidates;
|
|
146
932
|
}
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return true;
|
|
933
|
+
function findHistoryPath() {
|
|
934
|
+
for (const historyPath of getHistoryPathCandidates()) {
|
|
935
|
+
if (existsSync3(historyPath)) {
|
|
936
|
+
return historyPath;
|
|
152
937
|
}
|
|
153
|
-
await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL));
|
|
154
938
|
}
|
|
155
|
-
return
|
|
939
|
+
return null;
|
|
156
940
|
}
|
|
157
|
-
function
|
|
158
|
-
|
|
159
|
-
const daemonProcess = spawn(process.execPath, [daemonPath], {
|
|
160
|
-
detached: true,
|
|
161
|
-
stdio: "ignore",
|
|
162
|
-
env: { ...process.env }
|
|
163
|
-
});
|
|
164
|
-
daemonProcess.unref();
|
|
941
|
+
function sqlEscape(value) {
|
|
942
|
+
return value.replace(/'/g, "''");
|
|
165
943
|
}
|
|
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));
|
|
944
|
+
function buildTimeWhere(days) {
|
|
945
|
+
if (!days || days <= 0) {
|
|
946
|
+
return "";
|
|
181
947
|
}
|
|
948
|
+
return `last_visit_time > (strftime('%s', 'now') - ${Math.floor(days)}*86400) * 1000000 + 11644473600000000`;
|
|
182
949
|
}
|
|
183
|
-
|
|
950
|
+
function runHistoryQuery(sql, mapRow) {
|
|
951
|
+
const historyPath = findHistoryPath();
|
|
952
|
+
if (!historyPath) {
|
|
953
|
+
return [];
|
|
954
|
+
}
|
|
955
|
+
const tmpPath = join(tmpdir(), `bb-history-${Date.now()}-${Math.random().toString(36).slice(2)}.db`);
|
|
184
956
|
try {
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
957
|
+
copyFileSync(historyPath, tmpPath);
|
|
958
|
+
const escapedTmpPath = tmpPath.replace(/"/g, '\\"');
|
|
959
|
+
const escapedSql = sql.replace(/"/g, '\\"');
|
|
960
|
+
const output = execSync2(`sqlite3 -separator $'\\t' "${escapedTmpPath}" "${escapedSql}"`, {
|
|
961
|
+
encoding: "utf-8",
|
|
962
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
190
963
|
});
|
|
191
|
-
|
|
192
|
-
return response.ok;
|
|
964
|
+
return output.split("\n").filter(Boolean).map((line) => mapRow(line.split(" "))).filter((item) => item !== null);
|
|
193
965
|
} catch {
|
|
194
|
-
return
|
|
966
|
+
return [];
|
|
967
|
+
} finally {
|
|
968
|
+
try {
|
|
969
|
+
unlinkSync(tmpPath);
|
|
970
|
+
} catch {
|
|
971
|
+
}
|
|
195
972
|
}
|
|
196
973
|
}
|
|
974
|
+
function searchHistory(query, days) {
|
|
975
|
+
const conditions = [];
|
|
976
|
+
const trimmedQuery = query?.trim();
|
|
977
|
+
if (trimmedQuery) {
|
|
978
|
+
const escapedQuery = sqlEscape(trimmedQuery);
|
|
979
|
+
conditions.push(`(url LIKE '%${escapedQuery}%' OR title LIKE '%${escapedQuery}%')`);
|
|
980
|
+
}
|
|
981
|
+
const timeWhere = buildTimeWhere(days);
|
|
982
|
+
if (timeWhere) {
|
|
983
|
+
conditions.push(timeWhere);
|
|
984
|
+
}
|
|
985
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
986
|
+
const sql = `
|
|
987
|
+
SELECT
|
|
988
|
+
url,
|
|
989
|
+
REPLACE(IFNULL(title, ''), char(9), ' '),
|
|
990
|
+
IFNULL(visit_count, 0),
|
|
991
|
+
IFNULL(last_visit_time, 0)
|
|
992
|
+
FROM urls
|
|
993
|
+
${whereClause}
|
|
994
|
+
ORDER BY last_visit_time DESC
|
|
995
|
+
LIMIT 100;
|
|
996
|
+
`.trim();
|
|
997
|
+
return runHistoryQuery(sql, (row) => {
|
|
998
|
+
if (row.length < 4) {
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
1001
|
+
const chromeTimestamp = Number(row[3]) || 0;
|
|
1002
|
+
return {
|
|
1003
|
+
url: row[0] || "",
|
|
1004
|
+
title: row[1] || "",
|
|
1005
|
+
visitCount: Number(row[2]) || 0,
|
|
1006
|
+
lastVisitTime: chromeTimestamp > 0 ? chromeTimestamp / 1e6 - 11644473600 : 0
|
|
1007
|
+
};
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
function getHistoryDomains(days) {
|
|
1011
|
+
const timeWhere = buildTimeWhere(days);
|
|
1012
|
+
const whereClause = timeWhere ? `WHERE ${timeWhere}` : "";
|
|
1013
|
+
const sql = `
|
|
1014
|
+
SELECT
|
|
1015
|
+
domain,
|
|
1016
|
+
SUM(visit_count) AS visits,
|
|
1017
|
+
GROUP_CONCAT(title, char(31)) AS titles
|
|
1018
|
+
FROM (
|
|
1019
|
+
SELECT
|
|
1020
|
+
CASE
|
|
1021
|
+
WHEN instr(url, '//') > 0 AND instr(substr(url, instr(url, '//') + 2), '/') > 0
|
|
1022
|
+
THEN substr(
|
|
1023
|
+
substr(url, instr(url, '//') + 2),
|
|
1024
|
+
1,
|
|
1025
|
+
instr(substr(url, instr(url, '//') + 2), '/') - 1
|
|
1026
|
+
)
|
|
1027
|
+
WHEN instr(url, '//') > 0 THEN substr(url, instr(url, '//') + 2)
|
|
1028
|
+
WHEN instr(url, '/') > 0 THEN substr(url, 1, instr(url, '/') - 1)
|
|
1029
|
+
ELSE url
|
|
1030
|
+
END AS domain,
|
|
1031
|
+
IFNULL(visit_count, 0) AS visit_count,
|
|
1032
|
+
REPLACE(IFNULL(title, ''), char(31), ' ') AS title
|
|
1033
|
+
FROM urls
|
|
1034
|
+
${whereClause}
|
|
1035
|
+
)
|
|
1036
|
+
WHERE domain != ''
|
|
1037
|
+
GROUP BY domain
|
|
1038
|
+
ORDER BY visits DESC
|
|
1039
|
+
LIMIT 50;
|
|
1040
|
+
`.trim();
|
|
1041
|
+
return runHistoryQuery(sql, (row) => {
|
|
1042
|
+
if (row.length < 3) {
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
const titles = row[2] ? Array.from(new Set(row[2].split(String.fromCharCode(31)).map((title) => title.trim()).filter(Boolean))).slice(0, 10) : [];
|
|
1046
|
+
return {
|
|
1047
|
+
domain: row[0] || "",
|
|
1048
|
+
visits: Number(row[1]) || 0,
|
|
1049
|
+
titles
|
|
1050
|
+
};
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
197
1053
|
|
|
198
1054
|
// 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 =
|
|
1055
|
+
import { readFileSync as readFileSync2, readdirSync, existsSync as existsSync4, mkdirSync } from "fs";
|
|
1056
|
+
import { join as join2, relative } from "path";
|
|
1057
|
+
import { homedir as homedir2 } from "os";
|
|
1058
|
+
import { execSync as execSync3 } from "child_process";
|
|
1059
|
+
var BB_DIR = join2(homedir2(), ".bb-browser");
|
|
1060
|
+
var LOCAL_SITES_DIR = join2(BB_DIR, "sites");
|
|
1061
|
+
var COMMUNITY_SITES_DIR = join2(BB_DIR, "bb-sites");
|
|
206
1062
|
var COMMUNITY_REPO = "https://github.com/epiral/bb-sites.git";
|
|
207
1063
|
function checkCliUpdate() {
|
|
208
1064
|
try {
|
|
209
|
-
const current =
|
|
210
|
-
const latest =
|
|
1065
|
+
const current = execSync3("bb-browser --version", { timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
|
|
1066
|
+
const latest = execSync3("npm view bb-browser version", { timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
|
|
211
1067
|
if (latest && current && latest !== current && latest.localeCompare(current, void 0, { numeric: true }) > 0) {
|
|
212
1068
|
console.log(`
|
|
213
1069
|
\u{1F4E6} bb-browser ${latest} available (current: ${current}). Run: npm install -g bb-browser`);
|
|
@@ -218,7 +1074,7 @@ function checkCliUpdate() {
|
|
|
218
1074
|
function parseSiteMeta(filePath, source) {
|
|
219
1075
|
let content;
|
|
220
1076
|
try {
|
|
221
|
-
content =
|
|
1077
|
+
content = readFileSync2(filePath, "utf-8");
|
|
222
1078
|
} catch {
|
|
223
1079
|
return null;
|
|
224
1080
|
}
|
|
@@ -278,7 +1134,7 @@ function parseSiteMeta(filePath, source) {
|
|
|
278
1134
|
return meta;
|
|
279
1135
|
}
|
|
280
1136
|
function scanSites(dir, source) {
|
|
281
|
-
if (!
|
|
1137
|
+
if (!existsSync4(dir)) return [];
|
|
282
1138
|
const sites = [];
|
|
283
1139
|
function walk(currentDir) {
|
|
284
1140
|
let entries;
|
|
@@ -288,7 +1144,7 @@ function scanSites(dir, source) {
|
|
|
288
1144
|
return;
|
|
289
1145
|
}
|
|
290
1146
|
for (const entry of entries) {
|
|
291
|
-
const fullPath =
|
|
1147
|
+
const fullPath = join2(currentDir, entry.name);
|
|
292
1148
|
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
293
1149
|
walk(fullPath);
|
|
294
1150
|
} else if (entry.isFile() && entry.name.endsWith(".js")) {
|
|
@@ -392,10 +1248,10 @@ function siteSearch(query, options) {
|
|
|
392
1248
|
}
|
|
393
1249
|
function siteUpdate() {
|
|
394
1250
|
mkdirSync(BB_DIR, { recursive: true });
|
|
395
|
-
if (
|
|
1251
|
+
if (existsSync4(join2(COMMUNITY_SITES_DIR, ".git"))) {
|
|
396
1252
|
console.log("\u66F4\u65B0\u793E\u533A site adapter \u5E93...");
|
|
397
1253
|
try {
|
|
398
|
-
|
|
1254
|
+
execSync3("git pull --ff-only", { cwd: COMMUNITY_SITES_DIR, stdio: "pipe" });
|
|
399
1255
|
console.log("\u66F4\u65B0\u5B8C\u6210\u3002");
|
|
400
1256
|
console.log("");
|
|
401
1257
|
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 +1263,7 @@ function siteUpdate() {
|
|
|
407
1263
|
} else {
|
|
408
1264
|
console.log(`\u514B\u9686\u793E\u533A adapter \u5E93: ${COMMUNITY_REPO}`);
|
|
409
1265
|
try {
|
|
410
|
-
|
|
1266
|
+
execSync3(`git clone ${COMMUNITY_REPO} ${COMMUNITY_SITES_DIR}`, { stdio: "pipe" });
|
|
411
1267
|
console.log("\u514B\u9686\u5B8C\u6210\u3002");
|
|
412
1268
|
console.log("");
|
|
413
1269
|
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 +1322,7 @@ function siteInfo(name, options) {
|
|
|
466
1322
|
}
|
|
467
1323
|
async function siteRecommend(options) {
|
|
468
1324
|
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 || [];
|
|
1325
|
+
const historyDomains = getHistoryDomains(days);
|
|
479
1326
|
const sites = getAllSites();
|
|
480
1327
|
const sitesByDomain = /* @__PURE__ */ new Map();
|
|
481
1328
|
for (const site of sites) {
|
|
@@ -593,7 +1440,7 @@ async function siteRun(name, args, options) {
|
|
|
593
1440
|
process.exit(1);
|
|
594
1441
|
}
|
|
595
1442
|
}
|
|
596
|
-
const jsContent =
|
|
1443
|
+
const jsContent = readFileSync2(site.filePath, "utf-8");
|
|
597
1444
|
const jsBody = jsContent.replace(/\/\*\s*@meta[\s\S]*?\*\//, "").trim();
|
|
598
1445
|
const argsJson = JSON.stringify(argMap);
|
|
599
1446
|
const script = `(${jsBody})(${argsJson})`;
|
|
@@ -653,7 +1500,7 @@ async function siteRun(name, args, options) {
|
|
|
653
1500
|
let targetTabId = options.tabId;
|
|
654
1501
|
if (!targetTabId && site.domain) {
|
|
655
1502
|
const listReq = { id: generateId(), action: "tab_list" };
|
|
656
|
-
const listResp = await
|
|
1503
|
+
const listResp = await sendCommand2(listReq);
|
|
657
1504
|
if (listResp.success && listResp.data?.tabs) {
|
|
658
1505
|
const matchingTab = listResp.data.tabs.find(
|
|
659
1506
|
(tab) => matchTabOrigin(tab.url, site.domain)
|
|
@@ -663,7 +1510,7 @@ async function siteRun(name, args, options) {
|
|
|
663
1510
|
}
|
|
664
1511
|
}
|
|
665
1512
|
if (!targetTabId) {
|
|
666
|
-
const newResp = await
|
|
1513
|
+
const newResp = await sendCommand2({
|
|
667
1514
|
id: generateId(),
|
|
668
1515
|
action: "tab_new",
|
|
669
1516
|
url: `https://${site.domain}`
|
|
@@ -673,7 +1520,7 @@ async function siteRun(name, args, options) {
|
|
|
673
1520
|
}
|
|
674
1521
|
}
|
|
675
1522
|
const evalReq = { id: generateId(), action: "eval", script, tabId: targetTabId };
|
|
676
|
-
const evalResp = await
|
|
1523
|
+
const evalResp = await sendCommand2(evalReq);
|
|
677
1524
|
if (!evalResp.success) {
|
|
678
1525
|
const hint = site.domain ? `Open https://${site.domain} in your browser, make sure you are logged in, then retry.` : void 0;
|
|
679
1526
|
if (options.json) {
|
|
@@ -808,10 +1655,10 @@ async function siteCommand(args, options = {}) {
|
|
|
808
1655
|
silentUpdate();
|
|
809
1656
|
}
|
|
810
1657
|
function silentUpdate() {
|
|
811
|
-
const gitDir =
|
|
812
|
-
if (!
|
|
813
|
-
import("child_process").then(({ spawn:
|
|
814
|
-
const child =
|
|
1658
|
+
const gitDir = join2(COMMUNITY_SITES_DIR, ".git");
|
|
1659
|
+
if (!existsSync4(gitDir)) return;
|
|
1660
|
+
import("child_process").then(({ spawn: spawn2 }) => {
|
|
1661
|
+
const child = spawn2("git", ["pull", "--ff-only"], {
|
|
815
1662
|
cwd: COMMUNITY_SITES_DIR,
|
|
816
1663
|
stdio: "ignore",
|
|
817
1664
|
detached: true
|
|
@@ -847,7 +1694,7 @@ async function openCommand(url, options = {}) {
|
|
|
847
1694
|
request.tabId = tabId;
|
|
848
1695
|
}
|
|
849
1696
|
}
|
|
850
|
-
const response = await
|
|
1697
|
+
const response = await sendCommand2(request);
|
|
851
1698
|
if (options.json) {
|
|
852
1699
|
console.log(JSON.stringify(response, null, 2));
|
|
853
1700
|
} else {
|
|
@@ -883,7 +1730,7 @@ async function snapshotCommand(options = {}) {
|
|
|
883
1730
|
selector: options.selector,
|
|
884
1731
|
tabId: options.tabId
|
|
885
1732
|
};
|
|
886
|
-
const response = await
|
|
1733
|
+
const response = await sendCommand2(request);
|
|
887
1734
|
if (options.json) {
|
|
888
1735
|
console.log(JSON.stringify(response, null, 2));
|
|
889
1736
|
} else {
|
|
@@ -902,7 +1749,7 @@ async function snapshotCommand(options = {}) {
|
|
|
902
1749
|
}
|
|
903
1750
|
|
|
904
1751
|
// packages/cli/src/commands/click.ts
|
|
905
|
-
function
|
|
1752
|
+
function parseRef2(ref) {
|
|
906
1753
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
907
1754
|
}
|
|
908
1755
|
async function clickCommand(ref, options = {}) {
|
|
@@ -910,14 +1757,14 @@ async function clickCommand(ref, options = {}) {
|
|
|
910
1757
|
throw new Error("\u7F3A\u5C11 ref \u53C2\u6570");
|
|
911
1758
|
}
|
|
912
1759
|
await ensureDaemonRunning();
|
|
913
|
-
const parsedRef =
|
|
1760
|
+
const parsedRef = parseRef2(ref);
|
|
914
1761
|
const request = {
|
|
915
1762
|
id: generateId(),
|
|
916
1763
|
action: "click",
|
|
917
1764
|
ref: parsedRef,
|
|
918
1765
|
tabId: options.tabId
|
|
919
1766
|
};
|
|
920
|
-
const response = await
|
|
1767
|
+
const response = await sendCommand2(request);
|
|
921
1768
|
if (options.json) {
|
|
922
1769
|
console.log(JSON.stringify(response, null, 2));
|
|
923
1770
|
} else {
|
|
@@ -937,7 +1784,7 @@ async function clickCommand(ref, options = {}) {
|
|
|
937
1784
|
}
|
|
938
1785
|
|
|
939
1786
|
// packages/cli/src/commands/hover.ts
|
|
940
|
-
function
|
|
1787
|
+
function parseRef3(ref) {
|
|
941
1788
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
942
1789
|
}
|
|
943
1790
|
async function hoverCommand(ref, options = {}) {
|
|
@@ -945,14 +1792,14 @@ async function hoverCommand(ref, options = {}) {
|
|
|
945
1792
|
throw new Error("\u7F3A\u5C11 ref \u53C2\u6570");
|
|
946
1793
|
}
|
|
947
1794
|
await ensureDaemonRunning();
|
|
948
|
-
const parsedRef =
|
|
1795
|
+
const parsedRef = parseRef3(ref);
|
|
949
1796
|
const request = {
|
|
950
1797
|
id: generateId(),
|
|
951
1798
|
action: "hover",
|
|
952
1799
|
ref: parsedRef,
|
|
953
1800
|
tabId: options.tabId
|
|
954
1801
|
};
|
|
955
|
-
const response = await
|
|
1802
|
+
const response = await sendCommand2(request);
|
|
956
1803
|
if (options.json) {
|
|
957
1804
|
console.log(JSON.stringify(response, null, 2));
|
|
958
1805
|
} else {
|
|
@@ -972,7 +1819,7 @@ async function hoverCommand(ref, options = {}) {
|
|
|
972
1819
|
}
|
|
973
1820
|
|
|
974
1821
|
// packages/cli/src/commands/fill.ts
|
|
975
|
-
function
|
|
1822
|
+
function parseRef4(ref) {
|
|
976
1823
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
977
1824
|
}
|
|
978
1825
|
async function fillCommand(ref, text, options = {}) {
|
|
@@ -983,7 +1830,7 @@ async function fillCommand(ref, text, options = {}) {
|
|
|
983
1830
|
throw new Error("\u7F3A\u5C11 text \u53C2\u6570");
|
|
984
1831
|
}
|
|
985
1832
|
await ensureDaemonRunning();
|
|
986
|
-
const parsedRef =
|
|
1833
|
+
const parsedRef = parseRef4(ref);
|
|
987
1834
|
const request = {
|
|
988
1835
|
id: generateId(),
|
|
989
1836
|
action: "fill",
|
|
@@ -991,7 +1838,7 @@ async function fillCommand(ref, text, options = {}) {
|
|
|
991
1838
|
text,
|
|
992
1839
|
tabId: options.tabId
|
|
993
1840
|
};
|
|
994
|
-
const response = await
|
|
1841
|
+
const response = await sendCommand2(request);
|
|
995
1842
|
if (options.json) {
|
|
996
1843
|
console.log(JSON.stringify(response, null, 2));
|
|
997
1844
|
} else {
|
|
@@ -1012,7 +1859,7 @@ async function fillCommand(ref, text, options = {}) {
|
|
|
1012
1859
|
}
|
|
1013
1860
|
|
|
1014
1861
|
// packages/cli/src/commands/type.ts
|
|
1015
|
-
function
|
|
1862
|
+
function parseRef5(ref) {
|
|
1016
1863
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
1017
1864
|
}
|
|
1018
1865
|
async function typeCommand(ref, text, options = {}) {
|
|
@@ -1023,7 +1870,7 @@ async function typeCommand(ref, text, options = {}) {
|
|
|
1023
1870
|
throw new Error("\u7F3A\u5C11 text \u53C2\u6570");
|
|
1024
1871
|
}
|
|
1025
1872
|
await ensureDaemonRunning();
|
|
1026
|
-
const parsedRef =
|
|
1873
|
+
const parsedRef = parseRef5(ref);
|
|
1027
1874
|
const request = {
|
|
1028
1875
|
id: generateId(),
|
|
1029
1876
|
action: "type",
|
|
@@ -1031,7 +1878,7 @@ async function typeCommand(ref, text, options = {}) {
|
|
|
1031
1878
|
text,
|
|
1032
1879
|
tabId: options.tabId
|
|
1033
1880
|
};
|
|
1034
|
-
const response = await
|
|
1881
|
+
const response = await sendCommand2(request);
|
|
1035
1882
|
if (options.json) {
|
|
1036
1883
|
console.log(JSON.stringify(response, null, 2));
|
|
1037
1884
|
} else {
|
|
@@ -1059,7 +1906,7 @@ async function closeCommand(options = {}) {
|
|
|
1059
1906
|
action: "close",
|
|
1060
1907
|
tabId: options.tabId
|
|
1061
1908
|
};
|
|
1062
|
-
const response = await
|
|
1909
|
+
const response = await sendCommand2(request);
|
|
1063
1910
|
if (options.json) {
|
|
1064
1911
|
console.log(JSON.stringify(response, null, 2));
|
|
1065
1912
|
} else {
|
|
@@ -1078,7 +1925,7 @@ async function closeCommand(options = {}) {
|
|
|
1078
1925
|
}
|
|
1079
1926
|
|
|
1080
1927
|
// packages/cli/src/commands/get.ts
|
|
1081
|
-
function
|
|
1928
|
+
function parseRef6(ref) {
|
|
1082
1929
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
1083
1930
|
}
|
|
1084
1931
|
async function getCommand(attribute, ref, options = {}) {
|
|
@@ -1090,10 +1937,10 @@ async function getCommand(attribute, ref, options = {}) {
|
|
|
1090
1937
|
id: generateId(),
|
|
1091
1938
|
action: "get",
|
|
1092
1939
|
attribute,
|
|
1093
|
-
ref: ref ?
|
|
1940
|
+
ref: ref ? parseRef6(ref) : void 0,
|
|
1094
1941
|
tabId: options.tabId
|
|
1095
1942
|
};
|
|
1096
|
-
const response = await
|
|
1943
|
+
const response = await sendCommand2(request);
|
|
1097
1944
|
if (options.json) {
|
|
1098
1945
|
console.log(JSON.stringify(response, null, 2));
|
|
1099
1946
|
} else {
|
|
@@ -1109,17 +1956,17 @@ async function getCommand(attribute, ref, options = {}) {
|
|
|
1109
1956
|
|
|
1110
1957
|
// packages/cli/src/commands/screenshot.ts
|
|
1111
1958
|
import fs from "fs";
|
|
1112
|
-
import
|
|
1113
|
-
import
|
|
1959
|
+
import path3 from "path";
|
|
1960
|
+
import os2 from "os";
|
|
1114
1961
|
function getDefaultPath() {
|
|
1115
1962
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1116
1963
|
const filename = `bb-screenshot-${timestamp}.png`;
|
|
1117
|
-
return
|
|
1964
|
+
return path3.join(os2.tmpdir(), filename);
|
|
1118
1965
|
}
|
|
1119
1966
|
function saveBase64Image(dataUrl, filePath) {
|
|
1120
1967
|
const base64Data = dataUrl.replace(/^data:image\/png;base64,/, "");
|
|
1121
1968
|
const buffer = Buffer.from(base64Data, "base64");
|
|
1122
|
-
const dir =
|
|
1969
|
+
const dir = path3.dirname(filePath);
|
|
1123
1970
|
if (!fs.existsSync(dir)) {
|
|
1124
1971
|
fs.mkdirSync(dir, { recursive: true });
|
|
1125
1972
|
}
|
|
@@ -1127,13 +1974,13 @@ function saveBase64Image(dataUrl, filePath) {
|
|
|
1127
1974
|
}
|
|
1128
1975
|
async function screenshotCommand(outputPath, options = {}) {
|
|
1129
1976
|
await ensureDaemonRunning();
|
|
1130
|
-
const filePath = outputPath ?
|
|
1977
|
+
const filePath = outputPath ? path3.resolve(outputPath) : getDefaultPath();
|
|
1131
1978
|
const request = {
|
|
1132
1979
|
id: generateId(),
|
|
1133
1980
|
action: "screenshot",
|
|
1134
1981
|
tabId: options.tabId
|
|
1135
1982
|
};
|
|
1136
|
-
const response = await
|
|
1983
|
+
const response = await sendCommand2(request);
|
|
1137
1984
|
if (response.success && response.data?.dataUrl) {
|
|
1138
1985
|
const dataUrl = response.data.dataUrl;
|
|
1139
1986
|
saveBase64Image(dataUrl, filePath);
|
|
@@ -1160,7 +2007,7 @@ async function screenshotCommand(outputPath, options = {}) {
|
|
|
1160
2007
|
function isTimeWait(target) {
|
|
1161
2008
|
return /^\d+$/.test(target);
|
|
1162
2009
|
}
|
|
1163
|
-
function
|
|
2010
|
+
function parseRef7(ref) {
|
|
1164
2011
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
1165
2012
|
}
|
|
1166
2013
|
async function waitCommand(target, options = {}) {
|
|
@@ -1179,7 +2026,7 @@ async function waitCommand(target, options = {}) {
|
|
|
1179
2026
|
tabId: options.tabId
|
|
1180
2027
|
};
|
|
1181
2028
|
} else {
|
|
1182
|
-
const ref =
|
|
2029
|
+
const ref = parseRef7(target);
|
|
1183
2030
|
request = {
|
|
1184
2031
|
id: generateId(),
|
|
1185
2032
|
action: "wait",
|
|
@@ -1188,7 +2035,7 @@ async function waitCommand(target, options = {}) {
|
|
|
1188
2035
|
tabId: options.tabId
|
|
1189
2036
|
};
|
|
1190
2037
|
}
|
|
1191
|
-
const response = await
|
|
2038
|
+
const response = await sendCommand2(request);
|
|
1192
2039
|
if (options.json) {
|
|
1193
2040
|
console.log(JSON.stringify(response, null, 2));
|
|
1194
2041
|
} else {
|
|
@@ -1196,7 +2043,7 @@ async function waitCommand(target, options = {}) {
|
|
|
1196
2043
|
if (isTimeWait(target)) {
|
|
1197
2044
|
console.log(`\u5DF2\u7B49\u5F85 ${target}ms`);
|
|
1198
2045
|
} else {
|
|
1199
|
-
console.log(`\u5143\u7D20 @${
|
|
2046
|
+
console.log(`\u5143\u7D20 @${parseRef7(target)} \u5DF2\u51FA\u73B0`);
|
|
1200
2047
|
}
|
|
1201
2048
|
} else {
|
|
1202
2049
|
console.error(`\u9519\u8BEF: ${response.error}`);
|
|
@@ -1236,7 +2083,7 @@ async function pressCommand(keyString, options = {}) {
|
|
|
1236
2083
|
modifiers,
|
|
1237
2084
|
tabId: options.tabId
|
|
1238
2085
|
};
|
|
1239
|
-
const response = await
|
|
2086
|
+
const response = await sendCommand2(request);
|
|
1240
2087
|
if (options.json) {
|
|
1241
2088
|
console.log(JSON.stringify(response, null, 2));
|
|
1242
2089
|
} else {
|
|
@@ -1277,7 +2124,7 @@ async function scrollCommand(direction, pixels, options = {}) {
|
|
|
1277
2124
|
pixels: pixelValue,
|
|
1278
2125
|
tabId: options.tabId
|
|
1279
2126
|
};
|
|
1280
|
-
const response = await
|
|
2127
|
+
const response = await sendCommand2(request);
|
|
1281
2128
|
if (options.json) {
|
|
1282
2129
|
console.log(JSON.stringify(response, null, 2));
|
|
1283
2130
|
} else {
|
|
@@ -1291,76 +2138,17 @@ async function scrollCommand(direction, pixels, options = {}) {
|
|
|
1291
2138
|
}
|
|
1292
2139
|
|
|
1293
2140
|
// 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
2141
|
async function statusCommand(options = {}) {
|
|
1354
2142
|
const running = await isDaemonRunning();
|
|
1355
2143
|
if (options.json) {
|
|
1356
2144
|
console.log(JSON.stringify({ running }));
|
|
1357
2145
|
} else {
|
|
1358
|
-
console.log(running ? "
|
|
2146
|
+
console.log(running ? "\u6D4F\u89C8\u5668\u8FD0\u884C\u4E2D" : "\u6D4F\u89C8\u5668\u672A\u8FD0\u884C");
|
|
1359
2147
|
}
|
|
1360
2148
|
}
|
|
1361
2149
|
|
|
1362
2150
|
// packages/cli/src/commands/reload.ts
|
|
1363
|
-
import
|
|
2151
|
+
import WebSocket2 from "ws";
|
|
1364
2152
|
var EXTENSION_NAME = "bb-browser";
|
|
1365
2153
|
async function reloadCommand(options = {}) {
|
|
1366
2154
|
const port = options.port || 9222;
|
|
@@ -1377,7 +2165,7 @@ async function reloadCommand(options = {}) {
|
|
|
1377
2165
|
throw new Error("\u8BF7\u5148\u6253\u5F00 chrome://extensions \u9875\u9762");
|
|
1378
2166
|
}
|
|
1379
2167
|
const result = await new Promise((resolve2, reject) => {
|
|
1380
|
-
const ws = new
|
|
2168
|
+
const ws = new WebSocket2(extPage.webSocketDebuggerUrl);
|
|
1381
2169
|
let resolved = false;
|
|
1382
2170
|
const timeout = setTimeout(() => {
|
|
1383
2171
|
if (!resolved) {
|
|
@@ -1474,7 +2262,7 @@ async function backCommand(options = {}) {
|
|
|
1474
2262
|
action: "back",
|
|
1475
2263
|
tabId: options.tabId
|
|
1476
2264
|
};
|
|
1477
|
-
const response = await
|
|
2265
|
+
const response = await sendCommand2(request);
|
|
1478
2266
|
if (options.json) {
|
|
1479
2267
|
console.log(JSON.stringify(response, null, 2));
|
|
1480
2268
|
} else {
|
|
@@ -1498,7 +2286,7 @@ async function forwardCommand(options = {}) {
|
|
|
1498
2286
|
action: "forward",
|
|
1499
2287
|
tabId: options.tabId
|
|
1500
2288
|
};
|
|
1501
|
-
const response = await
|
|
2289
|
+
const response = await sendCommand2(request);
|
|
1502
2290
|
if (options.json) {
|
|
1503
2291
|
console.log(JSON.stringify(response, null, 2));
|
|
1504
2292
|
} else {
|
|
@@ -1522,7 +2310,7 @@ async function refreshCommand(options = {}) {
|
|
|
1522
2310
|
action: "refresh",
|
|
1523
2311
|
tabId: options.tabId
|
|
1524
2312
|
};
|
|
1525
|
-
const response = await
|
|
2313
|
+
const response = await sendCommand2(request);
|
|
1526
2314
|
if (options.json) {
|
|
1527
2315
|
console.log(JSON.stringify(response, null, 2));
|
|
1528
2316
|
} else {
|
|
@@ -1541,7 +2329,7 @@ async function refreshCommand(options = {}) {
|
|
|
1541
2329
|
}
|
|
1542
2330
|
|
|
1543
2331
|
// packages/cli/src/commands/check.ts
|
|
1544
|
-
function
|
|
2332
|
+
function parseRef8(ref) {
|
|
1545
2333
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
1546
2334
|
}
|
|
1547
2335
|
async function checkCommand(ref, options = {}) {
|
|
@@ -1549,14 +2337,14 @@ async function checkCommand(ref, options = {}) {
|
|
|
1549
2337
|
throw new Error("\u7F3A\u5C11 ref \u53C2\u6570");
|
|
1550
2338
|
}
|
|
1551
2339
|
await ensureDaemonRunning();
|
|
1552
|
-
const parsedRef =
|
|
2340
|
+
const parsedRef = parseRef8(ref);
|
|
1553
2341
|
const request = {
|
|
1554
2342
|
id: generateId(),
|
|
1555
2343
|
action: "check",
|
|
1556
2344
|
ref: parsedRef,
|
|
1557
2345
|
tabId: options.tabId
|
|
1558
2346
|
};
|
|
1559
|
-
const response = await
|
|
2347
|
+
const response = await sendCommand2(request);
|
|
1560
2348
|
if (options.json) {
|
|
1561
2349
|
console.log(JSON.stringify(response, null, 2));
|
|
1562
2350
|
} else {
|
|
@@ -1588,14 +2376,14 @@ async function uncheckCommand(ref, options = {}) {
|
|
|
1588
2376
|
throw new Error("\u7F3A\u5C11 ref \u53C2\u6570");
|
|
1589
2377
|
}
|
|
1590
2378
|
await ensureDaemonRunning();
|
|
1591
|
-
const parsedRef =
|
|
2379
|
+
const parsedRef = parseRef8(ref);
|
|
1592
2380
|
const request = {
|
|
1593
2381
|
id: generateId(),
|
|
1594
2382
|
action: "uncheck",
|
|
1595
2383
|
ref: parsedRef,
|
|
1596
2384
|
tabId: options.tabId
|
|
1597
2385
|
};
|
|
1598
|
-
const response = await
|
|
2386
|
+
const response = await sendCommand2(request);
|
|
1599
2387
|
if (options.json) {
|
|
1600
2388
|
console.log(JSON.stringify(response, null, 2));
|
|
1601
2389
|
} else {
|
|
@@ -1624,7 +2412,7 @@ async function uncheckCommand(ref, options = {}) {
|
|
|
1624
2412
|
}
|
|
1625
2413
|
|
|
1626
2414
|
// packages/cli/src/commands/select.ts
|
|
1627
|
-
function
|
|
2415
|
+
function parseRef9(ref) {
|
|
1628
2416
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
1629
2417
|
}
|
|
1630
2418
|
async function selectCommand(ref, value, options = {}) {
|
|
@@ -1635,7 +2423,7 @@ async function selectCommand(ref, value, options = {}) {
|
|
|
1635
2423
|
throw new Error("\u7F3A\u5C11 value \u53C2\u6570");
|
|
1636
2424
|
}
|
|
1637
2425
|
await ensureDaemonRunning();
|
|
1638
|
-
const parsedRef =
|
|
2426
|
+
const parsedRef = parseRef9(ref);
|
|
1639
2427
|
const request = {
|
|
1640
2428
|
id: generateId(),
|
|
1641
2429
|
action: "select",
|
|
@@ -1643,7 +2431,7 @@ async function selectCommand(ref, value, options = {}) {
|
|
|
1643
2431
|
value,
|
|
1644
2432
|
tabId: options.tabId
|
|
1645
2433
|
};
|
|
1646
|
-
const response = await
|
|
2434
|
+
const response = await sendCommand2(request);
|
|
1647
2435
|
if (options.json) {
|
|
1648
2436
|
console.log(JSON.stringify(response, null, 2));
|
|
1649
2437
|
} else {
|
|
@@ -1681,7 +2469,7 @@ async function evalCommand(script, options = {}) {
|
|
|
1681
2469
|
script,
|
|
1682
2470
|
tabId: options.tabId
|
|
1683
2471
|
};
|
|
1684
|
-
const response = await
|
|
2472
|
+
const response = await sendCommand2(request);
|
|
1685
2473
|
if (options.json) {
|
|
1686
2474
|
console.log(JSON.stringify(response, null, 2));
|
|
1687
2475
|
} else {
|
|
@@ -1768,7 +2556,7 @@ async function tabCommand(args, options = {}) {
|
|
|
1768
2556
|
index: parsed.index,
|
|
1769
2557
|
tabId: parsed.tabId
|
|
1770
2558
|
};
|
|
1771
|
-
const response = await
|
|
2559
|
+
const response = await sendCommand2(request);
|
|
1772
2560
|
if (options.json) {
|
|
1773
2561
|
console.log(JSON.stringify(response, null, 2));
|
|
1774
2562
|
} else {
|
|
@@ -1817,7 +2605,7 @@ async function frameCommand(selector, options = {}) {
|
|
|
1817
2605
|
selector,
|
|
1818
2606
|
tabId: options.tabId
|
|
1819
2607
|
};
|
|
1820
|
-
const response = await
|
|
2608
|
+
const response = await sendCommand2(request);
|
|
1821
2609
|
if (options.json) {
|
|
1822
2610
|
console.log(JSON.stringify(response, null, 2));
|
|
1823
2611
|
} else {
|
|
@@ -1841,7 +2629,7 @@ async function frameMainCommand(options = {}) {
|
|
|
1841
2629
|
action: "frame_main",
|
|
1842
2630
|
tabId: options.tabId
|
|
1843
2631
|
};
|
|
1844
|
-
const response = await
|
|
2632
|
+
const response = await sendCommand2(request);
|
|
1845
2633
|
if (options.json) {
|
|
1846
2634
|
console.log(JSON.stringify(response, null, 2));
|
|
1847
2635
|
} else {
|
|
@@ -1867,7 +2655,7 @@ async function dialogCommand(subCommand, promptText, options = {}) {
|
|
|
1867
2655
|
promptText: subCommand === "accept" ? promptText : void 0,
|
|
1868
2656
|
tabId: options.tabId
|
|
1869
2657
|
};
|
|
1870
|
-
const response = await
|
|
2658
|
+
const response = await sendCommand2(request);
|
|
1871
2659
|
if (options.json) {
|
|
1872
2660
|
console.log(JSON.stringify(response, null, 2));
|
|
1873
2661
|
} else {
|
|
@@ -1888,7 +2676,7 @@ async function dialogCommand(subCommand, promptText, options = {}) {
|
|
|
1888
2676
|
|
|
1889
2677
|
// packages/cli/src/commands/network.ts
|
|
1890
2678
|
async function networkCommand(subCommand, urlOrFilter, options = {}) {
|
|
1891
|
-
const response = await
|
|
2679
|
+
const response = await sendCommand2({
|
|
1892
2680
|
id: crypto.randomUUID(),
|
|
1893
2681
|
action: "network",
|
|
1894
2682
|
networkCommand: subCommand,
|
|
@@ -1975,7 +2763,7 @@ async function networkCommand(subCommand, urlOrFilter, options = {}) {
|
|
|
1975
2763
|
|
|
1976
2764
|
// packages/cli/src/commands/console.ts
|
|
1977
2765
|
async function consoleCommand(options = {}) {
|
|
1978
|
-
const response = await
|
|
2766
|
+
const response = await sendCommand2({
|
|
1979
2767
|
id: crypto.randomUUID(),
|
|
1980
2768
|
action: "console",
|
|
1981
2769
|
consoleCommand: options.clear ? "clear" : "get",
|
|
@@ -2020,7 +2808,7 @@ async function consoleCommand(options = {}) {
|
|
|
2020
2808
|
|
|
2021
2809
|
// packages/cli/src/commands/errors.ts
|
|
2022
2810
|
async function errorsCommand(options = {}) {
|
|
2023
|
-
const response = await
|
|
2811
|
+
const response = await sendCommand2({
|
|
2024
2812
|
id: crypto.randomUUID(),
|
|
2025
2813
|
action: "errors",
|
|
2026
2814
|
errorsCommand: options.clear ? "clear" : "get",
|
|
@@ -2060,7 +2848,7 @@ async function errorsCommand(options = {}) {
|
|
|
2060
2848
|
|
|
2061
2849
|
// packages/cli/src/commands/trace.ts
|
|
2062
2850
|
async function traceCommand(subCommand, options = {}) {
|
|
2063
|
-
const response = await
|
|
2851
|
+
const response = await sendCommand2({
|
|
2064
2852
|
id: crypto.randomUUID(),
|
|
2065
2853
|
action: "trace",
|
|
2066
2854
|
traceCommand: subCommand,
|
|
@@ -2150,7 +2938,7 @@ function matchTabOrigin2(tabUrl, targetHostname) {
|
|
|
2150
2938
|
}
|
|
2151
2939
|
async function ensureTabForOrigin(origin, hostname) {
|
|
2152
2940
|
const listReq = { id: generateId(), action: "tab_list" };
|
|
2153
|
-
const listResp = await
|
|
2941
|
+
const listResp = await sendCommand2(listReq);
|
|
2154
2942
|
if (listResp.success && listResp.data?.tabs) {
|
|
2155
2943
|
const matchingTab = listResp.data.tabs.find(
|
|
2156
2944
|
(tab) => matchTabOrigin2(tab.url, hostname)
|
|
@@ -2159,7 +2947,7 @@ async function ensureTabForOrigin(origin, hostname) {
|
|
|
2159
2947
|
return matchingTab.tabId;
|
|
2160
2948
|
}
|
|
2161
2949
|
}
|
|
2162
|
-
const newResp = await
|
|
2950
|
+
const newResp = await sendCommand2({ id: generateId(), action: "tab_new", url: origin });
|
|
2163
2951
|
if (!newResp.success) {
|
|
2164
2952
|
throw new Error(`\u65E0\u6CD5\u6253\u5F00 ${origin}: ${newResp.error}`);
|
|
2165
2953
|
}
|
|
@@ -2228,7 +3016,7 @@ async function fetchCommand(url, options = {}) {
|
|
|
2228
3016
|
}
|
|
2229
3017
|
const script = buildFetchScript(url, options);
|
|
2230
3018
|
const evalReq = { id: generateId(), action: "eval", script, tabId: targetTabId };
|
|
2231
|
-
const evalResp = await
|
|
3019
|
+
const evalResp = await sendCommand2(evalReq);
|
|
2232
3020
|
if (!evalResp.success) {
|
|
2233
3021
|
throw new Error(`Fetch \u5931\u8D25: ${evalResp.error}`);
|
|
2234
3022
|
}
|
|
@@ -2262,21 +3050,16 @@ async function fetchCommand(url, options = {}) {
|
|
|
2262
3050
|
|
|
2263
3051
|
// packages/cli/src/commands/history.ts
|
|
2264
3052
|
async function historyCommand(subCommand, options = {}) {
|
|
2265
|
-
const
|
|
2266
|
-
|
|
2267
|
-
action: "history",
|
|
2268
|
-
historyCommand: subCommand,
|
|
2269
|
-
text: options.query,
|
|
2270
|
-
ms: options.days
|
|
2271
|
-
});
|
|
3053
|
+
const days = options.days || 30;
|
|
3054
|
+
const data = subCommand === "search" ? { historyItems: searchHistory(options.query, days) } : { historyDomains: getHistoryDomains(days) };
|
|
2272
3055
|
if (options.json) {
|
|
2273
|
-
console.log(JSON.stringify(
|
|
3056
|
+
console.log(JSON.stringify({
|
|
3057
|
+
id: crypto.randomUUID(),
|
|
3058
|
+
success: true,
|
|
3059
|
+
data
|
|
3060
|
+
}));
|
|
2274
3061
|
return;
|
|
2275
3062
|
}
|
|
2276
|
-
if (!response.success) {
|
|
2277
|
-
throw new Error(response.error || "History command failed");
|
|
2278
|
-
}
|
|
2279
|
-
const data = response.data;
|
|
2280
3063
|
switch (subCommand) {
|
|
2281
3064
|
case "search": {
|
|
2282
3065
|
const items = data?.historyItems || [];
|
|
@@ -2315,10 +3098,13 @@ async function historyCommand(subCommand, options = {}) {
|
|
|
2315
3098
|
}
|
|
2316
3099
|
|
|
2317
3100
|
// packages/cli/src/index.ts
|
|
2318
|
-
var VERSION = "0.
|
|
3101
|
+
var VERSION = "0.8.0";
|
|
2319
3102
|
var HELP_TEXT = `
|
|
2320
3103
|
bb-browser - AI Agent \u6D4F\u89C8\u5668\u81EA\u52A8\u5316\u5DE5\u5177
|
|
2321
3104
|
|
|
3105
|
+
\u5B89\u88C5\uFF1A
|
|
3106
|
+
npm install -g bb-browser
|
|
3107
|
+
|
|
2322
3108
|
\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
3109
|
bb-browser site list \u67E5\u770B\u6240\u6709\u53EF\u7528\u547D\u4EE4
|
|
2324
3110
|
bb-browser site twitter/search "AI" \u793A\u4F8B\uFF1A\u641C\u7D22\u63A8\u6587
|
|
@@ -2356,6 +3142,7 @@ bb-browser - AI Agent \u6D4F\u89C8\u5668\u81EA\u52A8\u5316\u5DE5\u5177
|
|
|
2356
3142
|
|
|
2357
3143
|
\u6807\u7B7E\u9875\uFF1A
|
|
2358
3144
|
tab [list|new|close|<n>] \u7BA1\u7406\u6807\u7B7E\u9875
|
|
3145
|
+
status \u67E5\u770B\u53D7\u7BA1\u6D4F\u89C8\u5668\u72B6\u6001
|
|
2359
3146
|
|
|
2360
3147
|
\u5BFC\u822A\uFF1A
|
|
2361
3148
|
back / forward / refresh \u540E\u9000 / \u524D\u8FDB / \u5237\u65B0
|
|
@@ -2369,6 +3156,8 @@ bb-browser - AI Agent \u6D4F\u89C8\u5668\u81EA\u52A8\u5316\u5DE5\u5177
|
|
|
2369
3156
|
|
|
2370
3157
|
\u9009\u9879\uFF1A
|
|
2371
3158
|
--json \u4EE5 JSON \u683C\u5F0F\u8F93\u51FA
|
|
3159
|
+
--port <n> \u6307\u5B9A Chrome CDP \u7AEF\u53E3
|
|
3160
|
+
--openclaw \u4F18\u5148\u590D\u7528 OpenClaw \u6D4F\u89C8\u5668\u5B9E\u4F8B
|
|
2372
3161
|
--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
3162
|
-i, --interactive \u53EA\u8F93\u51FA\u53EF\u4EA4\u4E92\u5143\u7D20\uFF08snapshot \u547D\u4EE4\uFF09
|
|
2374
3163
|
-c, --compact \u79FB\u9664\u7A7A\u7ED3\u6784\u8282\u70B9\uFF08snapshot \u547D\u4EE4\uFF09
|
|
@@ -2409,6 +3198,12 @@ function parseArgs(argv) {
|
|
|
2409
3198
|
}
|
|
2410
3199
|
} else if (arg === "--openclaw") {
|
|
2411
3200
|
result.flags.openclaw = true;
|
|
3201
|
+
} else if (arg === "--port") {
|
|
3202
|
+
skipNext = true;
|
|
3203
|
+
const nextIdx = args.indexOf(arg) + 1;
|
|
3204
|
+
if (nextIdx < args.length) {
|
|
3205
|
+
result.flags.port = parseInt(args[nextIdx], 10);
|
|
3206
|
+
}
|
|
2412
3207
|
} else if (arg === "--help" || arg === "-h") {
|
|
2413
3208
|
result.flags.help = true;
|
|
2414
3209
|
} else if (arg === "--version" || arg === "-v") {
|
|
@@ -2458,9 +3253,9 @@ async function main() {
|
|
|
2458
3253
|
return;
|
|
2459
3254
|
}
|
|
2460
3255
|
if (process.argv.includes("--mcp")) {
|
|
2461
|
-
const mcpPath =
|
|
2462
|
-
const { spawn:
|
|
2463
|
-
const child =
|
|
3256
|
+
const mcpPath = fileURLToPath3(new URL("./mcp.js", import.meta.url));
|
|
3257
|
+
const { spawn: spawn2 } = await import("child_process");
|
|
3258
|
+
const child = spawn2(process.execPath, [mcpPath], { stdio: "inherit" });
|
|
2464
3259
|
child.on("exit", (code) => process.exit(code ?? 0));
|
|
2465
3260
|
return;
|
|
2466
3261
|
}
|
|
@@ -2818,9 +3613,9 @@ async function main() {
|
|
|
2818
3613
|
break;
|
|
2819
3614
|
}
|
|
2820
3615
|
case "star": {
|
|
2821
|
-
const { execSync:
|
|
3616
|
+
const { execSync: execSync4 } = await import("child_process");
|
|
2822
3617
|
try {
|
|
2823
|
-
|
|
3618
|
+
execSync4("gh auth status", { stdio: "pipe" });
|
|
2824
3619
|
} catch {
|
|
2825
3620
|
console.error("\u9700\u8981\u5148\u5B89\u88C5\u5E76\u767B\u5F55 GitHub CLI: https://cli.github.com");
|
|
2826
3621
|
console.error(" brew install gh && gh auth login");
|
|
@@ -2829,7 +3624,7 @@ async function main() {
|
|
|
2829
3624
|
const repos = ["epiral/bb-browser", "epiral/bb-sites"];
|
|
2830
3625
|
for (const repo of repos) {
|
|
2831
3626
|
try {
|
|
2832
|
-
|
|
3627
|
+
execSync4(`gh api user/starred/${repo} -X PUT`, { stdio: "pipe" });
|
|
2833
3628
|
console.log(`\u2B50 Starred ${repo}`);
|
|
2834
3629
|
} catch {
|
|
2835
3630
|
console.log(`Already starred or failed: ${repo}`);
|