bb-browser 0.6.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/README.md +18 -5
- package/README.zh-CN.md +18 -5
- package/dist/cli.js +1139 -306
- 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 {
|
|
@@ -9,6 +8,860 @@ import {
|
|
|
9
8
|
} from "./chunk-AHGAQEFO.js";
|
|
10
9
|
import "./chunk-D4HDZEJT.js";
|
|
11
10
|
|
|
11
|
+
// packages/cli/src/index.ts
|
|
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
|
+
}
|
|
864
|
+
|
|
12
865
|
// packages/cli/src/client.ts
|
|
13
866
|
var jqExpression;
|
|
14
867
|
function setJqExpression(expression) {
|
|
@@ -27,184 +880,201 @@ function handleJqResponse(response) {
|
|
|
27
880
|
printJqResults(response);
|
|
28
881
|
}
|
|
29
882
|
}
|
|
30
|
-
async function
|
|
31
|
-
|
|
32
|
-
const timeoutId = setTimeout(() => controller.abort(), COMMAND_TIMEOUT);
|
|
33
|
-
try {
|
|
34
|
-
const res = await fetch(`${DAEMON_BASE_URL}/command`, {
|
|
35
|
-
method: "POST",
|
|
36
|
-
headers: {
|
|
37
|
-
"Content-Type": "application/json"
|
|
38
|
-
},
|
|
39
|
-
body: JSON.stringify(request),
|
|
40
|
-
signal: controller.signal
|
|
41
|
-
});
|
|
42
|
-
clearTimeout(timeoutId);
|
|
43
|
-
if (!res.ok) {
|
|
44
|
-
if (res.status === 408) {
|
|
45
|
-
return {
|
|
46
|
-
id: request.id,
|
|
47
|
-
success: false,
|
|
48
|
-
error: "\u547D\u4EE4\u6267\u884C\u8D85\u65F6"
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
if (res.status === 503) {
|
|
52
|
-
return {
|
|
53
|
-
id: request.id,
|
|
54
|
-
success: false,
|
|
55
|
-
error: [
|
|
56
|
-
"Chrome extension not connected.",
|
|
57
|
-
"",
|
|
58
|
-
"1. Download extension: https://github.com/epiral/bb-browser/releases/latest",
|
|
59
|
-
"2. Unzip the downloaded file",
|
|
60
|
-
"3. Open chrome://extensions/ \u2192 Enable Developer Mode",
|
|
61
|
-
'4. Click "Load unpacked" \u2192 select the unzipped folder'
|
|
62
|
-
].join("\n")
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
return {
|
|
66
|
-
id: request.id,
|
|
67
|
-
success: false,
|
|
68
|
-
error: `HTTP \u9519\u8BEF: ${res.status} ${res.statusText}`
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
const response = await res.json();
|
|
72
|
-
return response;
|
|
73
|
-
} catch (error) {
|
|
74
|
-
clearTimeout(timeoutId);
|
|
75
|
-
if (error instanceof Error) {
|
|
76
|
-
if (error.name === "AbortError") {
|
|
77
|
-
return {
|
|
78
|
-
id: request.id,
|
|
79
|
-
success: false,
|
|
80
|
-
error: "\u8BF7\u6C42\u8D85\u65F6"
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
if (error.message.includes("fetch failed") || error.message.includes("ECONNREFUSED")) {
|
|
84
|
-
throw new Error([
|
|
85
|
-
"Cannot connect to daemon.",
|
|
86
|
-
"",
|
|
87
|
-
"Start the daemon first:",
|
|
88
|
-
" bb-browser daemon",
|
|
89
|
-
"",
|
|
90
|
-
"Then load the Chrome extension:",
|
|
91
|
-
" chrome://extensions/ \u2192 Developer Mode \u2192 Load unpacked \u2192 node_modules/bb-browser/extension/"
|
|
92
|
-
].join("\n"));
|
|
93
|
-
}
|
|
94
|
-
throw error;
|
|
95
|
-
}
|
|
96
|
-
throw error;
|
|
97
|
-
}
|
|
883
|
+
async function sendCommand2(request) {
|
|
884
|
+
return sendCommand(request);
|
|
98
885
|
}
|
|
99
886
|
|
|
100
887
|
// packages/cli/src/daemon-manager.ts
|
|
101
|
-
import {
|
|
102
|
-
import { existsSync } from "fs";
|
|
103
|
-
import { fileURLToPath } from "url";
|
|
888
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
104
889
|
import { dirname, resolve } from "path";
|
|
105
|
-
|
|
106
|
-
const currentFile = fileURLToPath(import.meta.url);
|
|
107
|
-
const currentDir = dirname(currentFile);
|
|
108
|
-
const sameDirPath = resolve(currentDir, "daemon.js");
|
|
109
|
-
if (existsSync(sameDirPath)) {
|
|
110
|
-
return sameDirPath;
|
|
111
|
-
}
|
|
112
|
-
return resolve(currentDir, "../../daemon/dist/index.js");
|
|
113
|
-
}
|
|
114
|
-
var DAEMON_START_TIMEOUT = 5e3;
|
|
115
|
-
var POLL_INTERVAL = 200;
|
|
890
|
+
import { existsSync as existsSync2 } from "fs";
|
|
116
891
|
async function isDaemonRunning() {
|
|
892
|
+
return await discoverCdpPort() !== null;
|
|
893
|
+
}
|
|
894
|
+
async function ensureDaemonRunning() {
|
|
117
895
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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;
|
|
127
907
|
}
|
|
128
908
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
+
);
|
|
142
930
|
}
|
|
931
|
+
return candidates;
|
|
143
932
|
}
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return true;
|
|
933
|
+
function findHistoryPath() {
|
|
934
|
+
for (const historyPath of getHistoryPathCandidates()) {
|
|
935
|
+
if (existsSync3(historyPath)) {
|
|
936
|
+
return historyPath;
|
|
149
937
|
}
|
|
150
|
-
await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL));
|
|
151
938
|
}
|
|
152
|
-
return
|
|
939
|
+
return null;
|
|
153
940
|
}
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
const daemonProcess = spawn(process.execPath, [daemonPath], {
|
|
157
|
-
detached: true,
|
|
158
|
-
stdio: "ignore",
|
|
159
|
-
env: { ...process.env }
|
|
160
|
-
});
|
|
161
|
-
daemonProcess.unref();
|
|
941
|
+
function sqlEscape(value) {
|
|
942
|
+
return value.replace(/'/g, "''");
|
|
162
943
|
}
|
|
163
|
-
|
|
164
|
-
if (
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
spawnDaemon();
|
|
168
|
-
const ready = await waitForDaemon(DAEMON_START_TIMEOUT);
|
|
169
|
-
if (!ready) {
|
|
170
|
-
throw new Error(
|
|
171
|
-
"\u65E0\u6CD5\u542F\u52A8 Daemon\u3002\u8BF7\u624B\u52A8\u8FD0\u884C bb-browser daemon \u6216 bb-daemon \u542F\u52A8\u670D\u52A1"
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
const extStart = Date.now();
|
|
175
|
-
while (Date.now() - extStart < 1e4) {
|
|
176
|
-
if (await isExtensionConnected()) return;
|
|
177
|
-
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
944
|
+
function buildTimeWhere(days) {
|
|
945
|
+
if (!days || days <= 0) {
|
|
946
|
+
return "";
|
|
178
947
|
}
|
|
948
|
+
return `last_visit_time > (strftime('%s', 'now') - ${Math.floor(days)}*86400) * 1000000 + 11644473600000000`;
|
|
179
949
|
}
|
|
180
|
-
|
|
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`);
|
|
181
956
|
try {
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
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"]
|
|
187
963
|
});
|
|
188
|
-
|
|
189
|
-
return response.ok;
|
|
964
|
+
return output.split("\n").filter(Boolean).map((line) => mapRow(line.split(" "))).filter((item) => item !== null);
|
|
190
965
|
} catch {
|
|
191
|
-
return
|
|
966
|
+
return [];
|
|
967
|
+
} finally {
|
|
968
|
+
try {
|
|
969
|
+
unlinkSync(tmpPath);
|
|
970
|
+
} catch {
|
|
971
|
+
}
|
|
192
972
|
}
|
|
193
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
|
+
}
|
|
194
1053
|
|
|
195
1054
|
// packages/cli/src/commands/site.ts
|
|
196
|
-
import { readFileSync, readdirSync, existsSync as
|
|
197
|
-
import { join, relative } from "path";
|
|
198
|
-
import { homedir } from "os";
|
|
199
|
-
import { execSync } from "child_process";
|
|
200
|
-
var BB_DIR =
|
|
201
|
-
var LOCAL_SITES_DIR =
|
|
202
|
-
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");
|
|
203
1062
|
var COMMUNITY_REPO = "https://github.com/epiral/bb-sites.git";
|
|
1063
|
+
function checkCliUpdate() {
|
|
1064
|
+
try {
|
|
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();
|
|
1067
|
+
if (latest && current && latest !== current && latest.localeCompare(current, void 0, { numeric: true }) > 0) {
|
|
1068
|
+
console.log(`
|
|
1069
|
+
\u{1F4E6} bb-browser ${latest} available (current: ${current}). Run: npm install -g bb-browser`);
|
|
1070
|
+
}
|
|
1071
|
+
} catch {
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
204
1074
|
function parseSiteMeta(filePath, source) {
|
|
205
1075
|
let content;
|
|
206
1076
|
try {
|
|
207
|
-
content =
|
|
1077
|
+
content = readFileSync2(filePath, "utf-8");
|
|
208
1078
|
} catch {
|
|
209
1079
|
return null;
|
|
210
1080
|
}
|
|
@@ -264,7 +1134,7 @@ function parseSiteMeta(filePath, source) {
|
|
|
264
1134
|
return meta;
|
|
265
1135
|
}
|
|
266
1136
|
function scanSites(dir, source) {
|
|
267
|
-
if (!
|
|
1137
|
+
if (!existsSync4(dir)) return [];
|
|
268
1138
|
const sites = [];
|
|
269
1139
|
function walk(currentDir) {
|
|
270
1140
|
let entries;
|
|
@@ -274,7 +1144,7 @@ function scanSites(dir, source) {
|
|
|
274
1144
|
return;
|
|
275
1145
|
}
|
|
276
1146
|
for (const entry of entries) {
|
|
277
|
-
const fullPath =
|
|
1147
|
+
const fullPath = join2(currentDir, entry.name);
|
|
278
1148
|
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
279
1149
|
walk(fullPath);
|
|
280
1150
|
} else if (entry.isFile() && entry.name.endsWith(".js")) {
|
|
@@ -378,10 +1248,10 @@ function siteSearch(query, options) {
|
|
|
378
1248
|
}
|
|
379
1249
|
function siteUpdate() {
|
|
380
1250
|
mkdirSync(BB_DIR, { recursive: true });
|
|
381
|
-
if (
|
|
1251
|
+
if (existsSync4(join2(COMMUNITY_SITES_DIR, ".git"))) {
|
|
382
1252
|
console.log("\u66F4\u65B0\u793E\u533A site adapter \u5E93...");
|
|
383
1253
|
try {
|
|
384
|
-
|
|
1254
|
+
execSync3("git pull --ff-only", { cwd: COMMUNITY_SITES_DIR, stdio: "pipe" });
|
|
385
1255
|
console.log("\u66F4\u65B0\u5B8C\u6210\u3002");
|
|
386
1256
|
console.log("");
|
|
387
1257
|
console.log("\u{1F4A1} \u8FD0\u884C bb-browser site recommend \u770B\u770B\u54EA\u4E9B\u548C\u4F60\u7684\u6D4F\u89C8\u4E60\u60EF\u5339\u914D");
|
|
@@ -393,7 +1263,7 @@ function siteUpdate() {
|
|
|
393
1263
|
} else {
|
|
394
1264
|
console.log(`\u514B\u9686\u793E\u533A adapter \u5E93: ${COMMUNITY_REPO}`);
|
|
395
1265
|
try {
|
|
396
|
-
|
|
1266
|
+
execSync3(`git clone ${COMMUNITY_REPO} ${COMMUNITY_SITES_DIR}`, { stdio: "pipe" });
|
|
397
1267
|
console.log("\u514B\u9686\u5B8C\u6210\u3002");
|
|
398
1268
|
console.log("");
|
|
399
1269
|
console.log("\u{1F4A1} \u8FD0\u884C bb-browser site recommend \u770B\u770B\u54EA\u4E9B\u548C\u4F60\u7684\u6D4F\u89C8\u4E60\u60EF\u5339\u914D");
|
|
@@ -405,6 +1275,8 @@ function siteUpdate() {
|
|
|
405
1275
|
}
|
|
406
1276
|
const sites = scanSites(COMMUNITY_SITES_DIR, "community");
|
|
407
1277
|
console.log(`\u5DF2\u5B89\u88C5 ${sites.length} \u4E2A\u793E\u533A adapter\u3002`);
|
|
1278
|
+
console.log(`\u2B50 Like bb-browser? \u2192 bb-browser star`);
|
|
1279
|
+
checkCliUpdate();
|
|
408
1280
|
}
|
|
409
1281
|
function findSiteByName(name) {
|
|
410
1282
|
return getAllSites().find((site) => site.name === name);
|
|
@@ -450,16 +1322,7 @@ function siteInfo(name, options) {
|
|
|
450
1322
|
}
|
|
451
1323
|
async function siteRecommend(options) {
|
|
452
1324
|
const days = options.days ?? 30;
|
|
453
|
-
const
|
|
454
|
-
id: generateId(),
|
|
455
|
-
action: "history",
|
|
456
|
-
historyCommand: "domains",
|
|
457
|
-
ms: days
|
|
458
|
-
});
|
|
459
|
-
if (!response.success) {
|
|
460
|
-
throw new Error(response.error || "History command failed");
|
|
461
|
-
}
|
|
462
|
-
const historyDomains = response.data?.historyDomains || [];
|
|
1325
|
+
const historyDomains = getHistoryDomains(days);
|
|
463
1326
|
const sites = getAllSites();
|
|
464
1327
|
const sitesByDomain = /* @__PURE__ */ new Map();
|
|
465
1328
|
for (const site of sites) {
|
|
@@ -577,7 +1440,7 @@ async function siteRun(name, args, options) {
|
|
|
577
1440
|
process.exit(1);
|
|
578
1441
|
}
|
|
579
1442
|
}
|
|
580
|
-
const jsContent =
|
|
1443
|
+
const jsContent = readFileSync2(site.filePath, "utf-8");
|
|
581
1444
|
const jsBody = jsContent.replace(/\/\*\s*@meta[\s\S]*?\*\//, "").trim();
|
|
582
1445
|
const argsJson = JSON.stringify(argMap);
|
|
583
1446
|
const script = `(${jsBody})(${argsJson})`;
|
|
@@ -637,7 +1500,7 @@ async function siteRun(name, args, options) {
|
|
|
637
1500
|
let targetTabId = options.tabId;
|
|
638
1501
|
if (!targetTabId && site.domain) {
|
|
639
1502
|
const listReq = { id: generateId(), action: "tab_list" };
|
|
640
|
-
const listResp = await
|
|
1503
|
+
const listResp = await sendCommand2(listReq);
|
|
641
1504
|
if (listResp.success && listResp.data?.tabs) {
|
|
642
1505
|
const matchingTab = listResp.data.tabs.find(
|
|
643
1506
|
(tab) => matchTabOrigin(tab.url, site.domain)
|
|
@@ -647,7 +1510,7 @@ async function siteRun(name, args, options) {
|
|
|
647
1510
|
}
|
|
648
1511
|
}
|
|
649
1512
|
if (!targetTabId) {
|
|
650
|
-
const newResp = await
|
|
1513
|
+
const newResp = await sendCommand2({
|
|
651
1514
|
id: generateId(),
|
|
652
1515
|
action: "tab_new",
|
|
653
1516
|
url: `https://${site.domain}`
|
|
@@ -657,7 +1520,7 @@ async function siteRun(name, args, options) {
|
|
|
657
1520
|
}
|
|
658
1521
|
}
|
|
659
1522
|
const evalReq = { id: generateId(), action: "eval", script, tabId: targetTabId };
|
|
660
|
-
const evalResp = await
|
|
1523
|
+
const evalResp = await sendCommand2(evalReq);
|
|
661
1524
|
if (!evalResp.success) {
|
|
662
1525
|
const hint = site.domain ? `Open https://${site.domain} in your browser, make sure you are logged in, then retry.` : void 0;
|
|
663
1526
|
if (options.json) {
|
|
@@ -792,10 +1655,10 @@ async function siteCommand(args, options = {}) {
|
|
|
792
1655
|
silentUpdate();
|
|
793
1656
|
}
|
|
794
1657
|
function silentUpdate() {
|
|
795
|
-
const gitDir =
|
|
796
|
-
if (!
|
|
797
|
-
import("child_process").then(({ spawn:
|
|
798
|
-
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"], {
|
|
799
1662
|
cwd: COMMUNITY_SITES_DIR,
|
|
800
1663
|
stdio: "ignore",
|
|
801
1664
|
detached: true
|
|
@@ -831,7 +1694,7 @@ async function openCommand(url, options = {}) {
|
|
|
831
1694
|
request.tabId = tabId;
|
|
832
1695
|
}
|
|
833
1696
|
}
|
|
834
|
-
const response = await
|
|
1697
|
+
const response = await sendCommand2(request);
|
|
835
1698
|
if (options.json) {
|
|
836
1699
|
console.log(JSON.stringify(response, null, 2));
|
|
837
1700
|
} else {
|
|
@@ -867,7 +1730,7 @@ async function snapshotCommand(options = {}) {
|
|
|
867
1730
|
selector: options.selector,
|
|
868
1731
|
tabId: options.tabId
|
|
869
1732
|
};
|
|
870
|
-
const response = await
|
|
1733
|
+
const response = await sendCommand2(request);
|
|
871
1734
|
if (options.json) {
|
|
872
1735
|
console.log(JSON.stringify(response, null, 2));
|
|
873
1736
|
} else {
|
|
@@ -886,7 +1749,7 @@ async function snapshotCommand(options = {}) {
|
|
|
886
1749
|
}
|
|
887
1750
|
|
|
888
1751
|
// packages/cli/src/commands/click.ts
|
|
889
|
-
function
|
|
1752
|
+
function parseRef2(ref) {
|
|
890
1753
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
891
1754
|
}
|
|
892
1755
|
async function clickCommand(ref, options = {}) {
|
|
@@ -894,14 +1757,14 @@ async function clickCommand(ref, options = {}) {
|
|
|
894
1757
|
throw new Error("\u7F3A\u5C11 ref \u53C2\u6570");
|
|
895
1758
|
}
|
|
896
1759
|
await ensureDaemonRunning();
|
|
897
|
-
const parsedRef =
|
|
1760
|
+
const parsedRef = parseRef2(ref);
|
|
898
1761
|
const request = {
|
|
899
1762
|
id: generateId(),
|
|
900
1763
|
action: "click",
|
|
901
1764
|
ref: parsedRef,
|
|
902
1765
|
tabId: options.tabId
|
|
903
1766
|
};
|
|
904
|
-
const response = await
|
|
1767
|
+
const response = await sendCommand2(request);
|
|
905
1768
|
if (options.json) {
|
|
906
1769
|
console.log(JSON.stringify(response, null, 2));
|
|
907
1770
|
} else {
|
|
@@ -921,7 +1784,7 @@ async function clickCommand(ref, options = {}) {
|
|
|
921
1784
|
}
|
|
922
1785
|
|
|
923
1786
|
// packages/cli/src/commands/hover.ts
|
|
924
|
-
function
|
|
1787
|
+
function parseRef3(ref) {
|
|
925
1788
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
926
1789
|
}
|
|
927
1790
|
async function hoverCommand(ref, options = {}) {
|
|
@@ -929,14 +1792,14 @@ async function hoverCommand(ref, options = {}) {
|
|
|
929
1792
|
throw new Error("\u7F3A\u5C11 ref \u53C2\u6570");
|
|
930
1793
|
}
|
|
931
1794
|
await ensureDaemonRunning();
|
|
932
|
-
const parsedRef =
|
|
1795
|
+
const parsedRef = parseRef3(ref);
|
|
933
1796
|
const request = {
|
|
934
1797
|
id: generateId(),
|
|
935
1798
|
action: "hover",
|
|
936
1799
|
ref: parsedRef,
|
|
937
1800
|
tabId: options.tabId
|
|
938
1801
|
};
|
|
939
|
-
const response = await
|
|
1802
|
+
const response = await sendCommand2(request);
|
|
940
1803
|
if (options.json) {
|
|
941
1804
|
console.log(JSON.stringify(response, null, 2));
|
|
942
1805
|
} else {
|
|
@@ -956,7 +1819,7 @@ async function hoverCommand(ref, options = {}) {
|
|
|
956
1819
|
}
|
|
957
1820
|
|
|
958
1821
|
// packages/cli/src/commands/fill.ts
|
|
959
|
-
function
|
|
1822
|
+
function parseRef4(ref) {
|
|
960
1823
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
961
1824
|
}
|
|
962
1825
|
async function fillCommand(ref, text, options = {}) {
|
|
@@ -967,7 +1830,7 @@ async function fillCommand(ref, text, options = {}) {
|
|
|
967
1830
|
throw new Error("\u7F3A\u5C11 text \u53C2\u6570");
|
|
968
1831
|
}
|
|
969
1832
|
await ensureDaemonRunning();
|
|
970
|
-
const parsedRef =
|
|
1833
|
+
const parsedRef = parseRef4(ref);
|
|
971
1834
|
const request = {
|
|
972
1835
|
id: generateId(),
|
|
973
1836
|
action: "fill",
|
|
@@ -975,7 +1838,7 @@ async function fillCommand(ref, text, options = {}) {
|
|
|
975
1838
|
text,
|
|
976
1839
|
tabId: options.tabId
|
|
977
1840
|
};
|
|
978
|
-
const response = await
|
|
1841
|
+
const response = await sendCommand2(request);
|
|
979
1842
|
if (options.json) {
|
|
980
1843
|
console.log(JSON.stringify(response, null, 2));
|
|
981
1844
|
} else {
|
|
@@ -996,7 +1859,7 @@ async function fillCommand(ref, text, options = {}) {
|
|
|
996
1859
|
}
|
|
997
1860
|
|
|
998
1861
|
// packages/cli/src/commands/type.ts
|
|
999
|
-
function
|
|
1862
|
+
function parseRef5(ref) {
|
|
1000
1863
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
1001
1864
|
}
|
|
1002
1865
|
async function typeCommand(ref, text, options = {}) {
|
|
@@ -1007,7 +1870,7 @@ async function typeCommand(ref, text, options = {}) {
|
|
|
1007
1870
|
throw new Error("\u7F3A\u5C11 text \u53C2\u6570");
|
|
1008
1871
|
}
|
|
1009
1872
|
await ensureDaemonRunning();
|
|
1010
|
-
const parsedRef =
|
|
1873
|
+
const parsedRef = parseRef5(ref);
|
|
1011
1874
|
const request = {
|
|
1012
1875
|
id: generateId(),
|
|
1013
1876
|
action: "type",
|
|
@@ -1015,7 +1878,7 @@ async function typeCommand(ref, text, options = {}) {
|
|
|
1015
1878
|
text,
|
|
1016
1879
|
tabId: options.tabId
|
|
1017
1880
|
};
|
|
1018
|
-
const response = await
|
|
1881
|
+
const response = await sendCommand2(request);
|
|
1019
1882
|
if (options.json) {
|
|
1020
1883
|
console.log(JSON.stringify(response, null, 2));
|
|
1021
1884
|
} else {
|
|
@@ -1043,7 +1906,7 @@ async function closeCommand(options = {}) {
|
|
|
1043
1906
|
action: "close",
|
|
1044
1907
|
tabId: options.tabId
|
|
1045
1908
|
};
|
|
1046
|
-
const response = await
|
|
1909
|
+
const response = await sendCommand2(request);
|
|
1047
1910
|
if (options.json) {
|
|
1048
1911
|
console.log(JSON.stringify(response, null, 2));
|
|
1049
1912
|
} else {
|
|
@@ -1062,7 +1925,7 @@ async function closeCommand(options = {}) {
|
|
|
1062
1925
|
}
|
|
1063
1926
|
|
|
1064
1927
|
// packages/cli/src/commands/get.ts
|
|
1065
|
-
function
|
|
1928
|
+
function parseRef6(ref) {
|
|
1066
1929
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
1067
1930
|
}
|
|
1068
1931
|
async function getCommand(attribute, ref, options = {}) {
|
|
@@ -1074,10 +1937,10 @@ async function getCommand(attribute, ref, options = {}) {
|
|
|
1074
1937
|
id: generateId(),
|
|
1075
1938
|
action: "get",
|
|
1076
1939
|
attribute,
|
|
1077
|
-
ref: ref ?
|
|
1940
|
+
ref: ref ? parseRef6(ref) : void 0,
|
|
1078
1941
|
tabId: options.tabId
|
|
1079
1942
|
};
|
|
1080
|
-
const response = await
|
|
1943
|
+
const response = await sendCommand2(request);
|
|
1081
1944
|
if (options.json) {
|
|
1082
1945
|
console.log(JSON.stringify(response, null, 2));
|
|
1083
1946
|
} else {
|
|
@@ -1093,17 +1956,17 @@ async function getCommand(attribute, ref, options = {}) {
|
|
|
1093
1956
|
|
|
1094
1957
|
// packages/cli/src/commands/screenshot.ts
|
|
1095
1958
|
import fs from "fs";
|
|
1096
|
-
import
|
|
1097
|
-
import
|
|
1959
|
+
import path3 from "path";
|
|
1960
|
+
import os2 from "os";
|
|
1098
1961
|
function getDefaultPath() {
|
|
1099
1962
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1100
1963
|
const filename = `bb-screenshot-${timestamp}.png`;
|
|
1101
|
-
return
|
|
1964
|
+
return path3.join(os2.tmpdir(), filename);
|
|
1102
1965
|
}
|
|
1103
1966
|
function saveBase64Image(dataUrl, filePath) {
|
|
1104
1967
|
const base64Data = dataUrl.replace(/^data:image\/png;base64,/, "");
|
|
1105
1968
|
const buffer = Buffer.from(base64Data, "base64");
|
|
1106
|
-
const dir =
|
|
1969
|
+
const dir = path3.dirname(filePath);
|
|
1107
1970
|
if (!fs.existsSync(dir)) {
|
|
1108
1971
|
fs.mkdirSync(dir, { recursive: true });
|
|
1109
1972
|
}
|
|
@@ -1111,13 +1974,13 @@ function saveBase64Image(dataUrl, filePath) {
|
|
|
1111
1974
|
}
|
|
1112
1975
|
async function screenshotCommand(outputPath, options = {}) {
|
|
1113
1976
|
await ensureDaemonRunning();
|
|
1114
|
-
const filePath = outputPath ?
|
|
1977
|
+
const filePath = outputPath ? path3.resolve(outputPath) : getDefaultPath();
|
|
1115
1978
|
const request = {
|
|
1116
1979
|
id: generateId(),
|
|
1117
1980
|
action: "screenshot",
|
|
1118
1981
|
tabId: options.tabId
|
|
1119
1982
|
};
|
|
1120
|
-
const response = await
|
|
1983
|
+
const response = await sendCommand2(request);
|
|
1121
1984
|
if (response.success && response.data?.dataUrl) {
|
|
1122
1985
|
const dataUrl = response.data.dataUrl;
|
|
1123
1986
|
saveBase64Image(dataUrl, filePath);
|
|
@@ -1144,7 +2007,7 @@ async function screenshotCommand(outputPath, options = {}) {
|
|
|
1144
2007
|
function isTimeWait(target) {
|
|
1145
2008
|
return /^\d+$/.test(target);
|
|
1146
2009
|
}
|
|
1147
|
-
function
|
|
2010
|
+
function parseRef7(ref) {
|
|
1148
2011
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
1149
2012
|
}
|
|
1150
2013
|
async function waitCommand(target, options = {}) {
|
|
@@ -1163,7 +2026,7 @@ async function waitCommand(target, options = {}) {
|
|
|
1163
2026
|
tabId: options.tabId
|
|
1164
2027
|
};
|
|
1165
2028
|
} else {
|
|
1166
|
-
const ref =
|
|
2029
|
+
const ref = parseRef7(target);
|
|
1167
2030
|
request = {
|
|
1168
2031
|
id: generateId(),
|
|
1169
2032
|
action: "wait",
|
|
@@ -1172,7 +2035,7 @@ async function waitCommand(target, options = {}) {
|
|
|
1172
2035
|
tabId: options.tabId
|
|
1173
2036
|
};
|
|
1174
2037
|
}
|
|
1175
|
-
const response = await
|
|
2038
|
+
const response = await sendCommand2(request);
|
|
1176
2039
|
if (options.json) {
|
|
1177
2040
|
console.log(JSON.stringify(response, null, 2));
|
|
1178
2041
|
} else {
|
|
@@ -1180,7 +2043,7 @@ async function waitCommand(target, options = {}) {
|
|
|
1180
2043
|
if (isTimeWait(target)) {
|
|
1181
2044
|
console.log(`\u5DF2\u7B49\u5F85 ${target}ms`);
|
|
1182
2045
|
} else {
|
|
1183
|
-
console.log(`\u5143\u7D20 @${
|
|
2046
|
+
console.log(`\u5143\u7D20 @${parseRef7(target)} \u5DF2\u51FA\u73B0`);
|
|
1184
2047
|
}
|
|
1185
2048
|
} else {
|
|
1186
2049
|
console.error(`\u9519\u8BEF: ${response.error}`);
|
|
@@ -1220,7 +2083,7 @@ async function pressCommand(keyString, options = {}) {
|
|
|
1220
2083
|
modifiers,
|
|
1221
2084
|
tabId: options.tabId
|
|
1222
2085
|
};
|
|
1223
|
-
const response = await
|
|
2086
|
+
const response = await sendCommand2(request);
|
|
1224
2087
|
if (options.json) {
|
|
1225
2088
|
console.log(JSON.stringify(response, null, 2));
|
|
1226
2089
|
} else {
|
|
@@ -1261,7 +2124,7 @@ async function scrollCommand(direction, pixels, options = {}) {
|
|
|
1261
2124
|
pixels: pixelValue,
|
|
1262
2125
|
tabId: options.tabId
|
|
1263
2126
|
};
|
|
1264
|
-
const response = await
|
|
2127
|
+
const response = await sendCommand2(request);
|
|
1265
2128
|
if (options.json) {
|
|
1266
2129
|
console.log(JSON.stringify(response, null, 2));
|
|
1267
2130
|
} else {
|
|
@@ -1275,76 +2138,17 @@ async function scrollCommand(direction, pixels, options = {}) {
|
|
|
1275
2138
|
}
|
|
1276
2139
|
|
|
1277
2140
|
// packages/cli/src/commands/daemon.ts
|
|
1278
|
-
import { spawn as spawn2 } from "child_process";
|
|
1279
|
-
async function daemonCommand(options = {}) {
|
|
1280
|
-
if (await isDaemonRunning()) {
|
|
1281
|
-
if (options.json) {
|
|
1282
|
-
console.log(JSON.stringify({ success: false, error: "Daemon \u5DF2\u5728\u8FD0\u884C" }));
|
|
1283
|
-
} else {
|
|
1284
|
-
console.log("Daemon \u5DF2\u5728\u8FD0\u884C");
|
|
1285
|
-
}
|
|
1286
|
-
return;
|
|
1287
|
-
}
|
|
1288
|
-
const daemonPath = getDaemonPath();
|
|
1289
|
-
const args = [daemonPath];
|
|
1290
|
-
if (options.host) {
|
|
1291
|
-
args.push("--host", options.host);
|
|
1292
|
-
}
|
|
1293
|
-
if (options.json) {
|
|
1294
|
-
console.log(JSON.stringify({ success: true, message: "Daemon \u542F\u52A8\u4E2D..." }));
|
|
1295
|
-
} else {
|
|
1296
|
-
console.log("Daemon \u542F\u52A8\u4E2D...");
|
|
1297
|
-
}
|
|
1298
|
-
await new Promise((resolve2, reject) => {
|
|
1299
|
-
const child = spawn2(process.execPath, args, {
|
|
1300
|
-
stdio: "inherit"
|
|
1301
|
-
});
|
|
1302
|
-
child.on("exit", (code) => {
|
|
1303
|
-
if (code && code !== 0) {
|
|
1304
|
-
reject(new Error(`Daemon exited with code ${code}`));
|
|
1305
|
-
} else {
|
|
1306
|
-
resolve2();
|
|
1307
|
-
}
|
|
1308
|
-
});
|
|
1309
|
-
child.on("error", reject);
|
|
1310
|
-
});
|
|
1311
|
-
}
|
|
1312
|
-
async function stopCommand(options = {}) {
|
|
1313
|
-
if (!await isDaemonRunning()) {
|
|
1314
|
-
if (options.json) {
|
|
1315
|
-
console.log(JSON.stringify({ success: false, error: "Daemon \u672A\u8FD0\u884C" }));
|
|
1316
|
-
} else {
|
|
1317
|
-
console.log("Daemon \u672A\u8FD0\u884C");
|
|
1318
|
-
}
|
|
1319
|
-
return;
|
|
1320
|
-
}
|
|
1321
|
-
const stopped = await stopDaemon();
|
|
1322
|
-
if (stopped) {
|
|
1323
|
-
if (options.json) {
|
|
1324
|
-
console.log(JSON.stringify({ success: true, message: "Daemon \u5DF2\u505C\u6B62" }));
|
|
1325
|
-
} else {
|
|
1326
|
-
console.log("Daemon \u5DF2\u505C\u6B62");
|
|
1327
|
-
}
|
|
1328
|
-
} else {
|
|
1329
|
-
if (options.json) {
|
|
1330
|
-
console.log(JSON.stringify({ success: false, error: "\u65E0\u6CD5\u505C\u6B62 Daemon" }));
|
|
1331
|
-
} else {
|
|
1332
|
-
console.error("\u65E0\u6CD5\u505C\u6B62 Daemon");
|
|
1333
|
-
}
|
|
1334
|
-
process.exit(1);
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
2141
|
async function statusCommand(options = {}) {
|
|
1338
2142
|
const running = await isDaemonRunning();
|
|
1339
2143
|
if (options.json) {
|
|
1340
2144
|
console.log(JSON.stringify({ running }));
|
|
1341
2145
|
} else {
|
|
1342
|
-
console.log(running ? "
|
|
2146
|
+
console.log(running ? "\u6D4F\u89C8\u5668\u8FD0\u884C\u4E2D" : "\u6D4F\u89C8\u5668\u672A\u8FD0\u884C");
|
|
1343
2147
|
}
|
|
1344
2148
|
}
|
|
1345
2149
|
|
|
1346
2150
|
// packages/cli/src/commands/reload.ts
|
|
1347
|
-
import
|
|
2151
|
+
import WebSocket2 from "ws";
|
|
1348
2152
|
var EXTENSION_NAME = "bb-browser";
|
|
1349
2153
|
async function reloadCommand(options = {}) {
|
|
1350
2154
|
const port = options.port || 9222;
|
|
@@ -1361,7 +2165,7 @@ async function reloadCommand(options = {}) {
|
|
|
1361
2165
|
throw new Error("\u8BF7\u5148\u6253\u5F00 chrome://extensions \u9875\u9762");
|
|
1362
2166
|
}
|
|
1363
2167
|
const result = await new Promise((resolve2, reject) => {
|
|
1364
|
-
const ws = new
|
|
2168
|
+
const ws = new WebSocket2(extPage.webSocketDebuggerUrl);
|
|
1365
2169
|
let resolved = false;
|
|
1366
2170
|
const timeout = setTimeout(() => {
|
|
1367
2171
|
if (!resolved) {
|
|
@@ -1458,7 +2262,7 @@ async function backCommand(options = {}) {
|
|
|
1458
2262
|
action: "back",
|
|
1459
2263
|
tabId: options.tabId
|
|
1460
2264
|
};
|
|
1461
|
-
const response = await
|
|
2265
|
+
const response = await sendCommand2(request);
|
|
1462
2266
|
if (options.json) {
|
|
1463
2267
|
console.log(JSON.stringify(response, null, 2));
|
|
1464
2268
|
} else {
|
|
@@ -1482,7 +2286,7 @@ async function forwardCommand(options = {}) {
|
|
|
1482
2286
|
action: "forward",
|
|
1483
2287
|
tabId: options.tabId
|
|
1484
2288
|
};
|
|
1485
|
-
const response = await
|
|
2289
|
+
const response = await sendCommand2(request);
|
|
1486
2290
|
if (options.json) {
|
|
1487
2291
|
console.log(JSON.stringify(response, null, 2));
|
|
1488
2292
|
} else {
|
|
@@ -1506,7 +2310,7 @@ async function refreshCommand(options = {}) {
|
|
|
1506
2310
|
action: "refresh",
|
|
1507
2311
|
tabId: options.tabId
|
|
1508
2312
|
};
|
|
1509
|
-
const response = await
|
|
2313
|
+
const response = await sendCommand2(request);
|
|
1510
2314
|
if (options.json) {
|
|
1511
2315
|
console.log(JSON.stringify(response, null, 2));
|
|
1512
2316
|
} else {
|
|
@@ -1525,7 +2329,7 @@ async function refreshCommand(options = {}) {
|
|
|
1525
2329
|
}
|
|
1526
2330
|
|
|
1527
2331
|
// packages/cli/src/commands/check.ts
|
|
1528
|
-
function
|
|
2332
|
+
function parseRef8(ref) {
|
|
1529
2333
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
1530
2334
|
}
|
|
1531
2335
|
async function checkCommand(ref, options = {}) {
|
|
@@ -1533,14 +2337,14 @@ async function checkCommand(ref, options = {}) {
|
|
|
1533
2337
|
throw new Error("\u7F3A\u5C11 ref \u53C2\u6570");
|
|
1534
2338
|
}
|
|
1535
2339
|
await ensureDaemonRunning();
|
|
1536
|
-
const parsedRef =
|
|
2340
|
+
const parsedRef = parseRef8(ref);
|
|
1537
2341
|
const request = {
|
|
1538
2342
|
id: generateId(),
|
|
1539
2343
|
action: "check",
|
|
1540
2344
|
ref: parsedRef,
|
|
1541
2345
|
tabId: options.tabId
|
|
1542
2346
|
};
|
|
1543
|
-
const response = await
|
|
2347
|
+
const response = await sendCommand2(request);
|
|
1544
2348
|
if (options.json) {
|
|
1545
2349
|
console.log(JSON.stringify(response, null, 2));
|
|
1546
2350
|
} else {
|
|
@@ -1572,14 +2376,14 @@ async function uncheckCommand(ref, options = {}) {
|
|
|
1572
2376
|
throw new Error("\u7F3A\u5C11 ref \u53C2\u6570");
|
|
1573
2377
|
}
|
|
1574
2378
|
await ensureDaemonRunning();
|
|
1575
|
-
const parsedRef =
|
|
2379
|
+
const parsedRef = parseRef8(ref);
|
|
1576
2380
|
const request = {
|
|
1577
2381
|
id: generateId(),
|
|
1578
2382
|
action: "uncheck",
|
|
1579
2383
|
ref: parsedRef,
|
|
1580
2384
|
tabId: options.tabId
|
|
1581
2385
|
};
|
|
1582
|
-
const response = await
|
|
2386
|
+
const response = await sendCommand2(request);
|
|
1583
2387
|
if (options.json) {
|
|
1584
2388
|
console.log(JSON.stringify(response, null, 2));
|
|
1585
2389
|
} else {
|
|
@@ -1608,7 +2412,7 @@ async function uncheckCommand(ref, options = {}) {
|
|
|
1608
2412
|
}
|
|
1609
2413
|
|
|
1610
2414
|
// packages/cli/src/commands/select.ts
|
|
1611
|
-
function
|
|
2415
|
+
function parseRef9(ref) {
|
|
1612
2416
|
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
1613
2417
|
}
|
|
1614
2418
|
async function selectCommand(ref, value, options = {}) {
|
|
@@ -1619,7 +2423,7 @@ async function selectCommand(ref, value, options = {}) {
|
|
|
1619
2423
|
throw new Error("\u7F3A\u5C11 value \u53C2\u6570");
|
|
1620
2424
|
}
|
|
1621
2425
|
await ensureDaemonRunning();
|
|
1622
|
-
const parsedRef =
|
|
2426
|
+
const parsedRef = parseRef9(ref);
|
|
1623
2427
|
const request = {
|
|
1624
2428
|
id: generateId(),
|
|
1625
2429
|
action: "select",
|
|
@@ -1627,7 +2431,7 @@ async function selectCommand(ref, value, options = {}) {
|
|
|
1627
2431
|
value,
|
|
1628
2432
|
tabId: options.tabId
|
|
1629
2433
|
};
|
|
1630
|
-
const response = await
|
|
2434
|
+
const response = await sendCommand2(request);
|
|
1631
2435
|
if (options.json) {
|
|
1632
2436
|
console.log(JSON.stringify(response, null, 2));
|
|
1633
2437
|
} else {
|
|
@@ -1665,7 +2469,7 @@ async function evalCommand(script, options = {}) {
|
|
|
1665
2469
|
script,
|
|
1666
2470
|
tabId: options.tabId
|
|
1667
2471
|
};
|
|
1668
|
-
const response = await
|
|
2472
|
+
const response = await sendCommand2(request);
|
|
1669
2473
|
if (options.json) {
|
|
1670
2474
|
console.log(JSON.stringify(response, null, 2));
|
|
1671
2475
|
} else {
|
|
@@ -1752,7 +2556,7 @@ async function tabCommand(args, options = {}) {
|
|
|
1752
2556
|
index: parsed.index,
|
|
1753
2557
|
tabId: parsed.tabId
|
|
1754
2558
|
};
|
|
1755
|
-
const response = await
|
|
2559
|
+
const response = await sendCommand2(request);
|
|
1756
2560
|
if (options.json) {
|
|
1757
2561
|
console.log(JSON.stringify(response, null, 2));
|
|
1758
2562
|
} else {
|
|
@@ -1801,7 +2605,7 @@ async function frameCommand(selector, options = {}) {
|
|
|
1801
2605
|
selector,
|
|
1802
2606
|
tabId: options.tabId
|
|
1803
2607
|
};
|
|
1804
|
-
const response = await
|
|
2608
|
+
const response = await sendCommand2(request);
|
|
1805
2609
|
if (options.json) {
|
|
1806
2610
|
console.log(JSON.stringify(response, null, 2));
|
|
1807
2611
|
} else {
|
|
@@ -1825,7 +2629,7 @@ async function frameMainCommand(options = {}) {
|
|
|
1825
2629
|
action: "frame_main",
|
|
1826
2630
|
tabId: options.tabId
|
|
1827
2631
|
};
|
|
1828
|
-
const response = await
|
|
2632
|
+
const response = await sendCommand2(request);
|
|
1829
2633
|
if (options.json) {
|
|
1830
2634
|
console.log(JSON.stringify(response, null, 2));
|
|
1831
2635
|
} else {
|
|
@@ -1851,7 +2655,7 @@ async function dialogCommand(subCommand, promptText, options = {}) {
|
|
|
1851
2655
|
promptText: subCommand === "accept" ? promptText : void 0,
|
|
1852
2656
|
tabId: options.tabId
|
|
1853
2657
|
};
|
|
1854
|
-
const response = await
|
|
2658
|
+
const response = await sendCommand2(request);
|
|
1855
2659
|
if (options.json) {
|
|
1856
2660
|
console.log(JSON.stringify(response, null, 2));
|
|
1857
2661
|
} else {
|
|
@@ -1872,7 +2676,7 @@ async function dialogCommand(subCommand, promptText, options = {}) {
|
|
|
1872
2676
|
|
|
1873
2677
|
// packages/cli/src/commands/network.ts
|
|
1874
2678
|
async function networkCommand(subCommand, urlOrFilter, options = {}) {
|
|
1875
|
-
const response = await
|
|
2679
|
+
const response = await sendCommand2({
|
|
1876
2680
|
id: crypto.randomUUID(),
|
|
1877
2681
|
action: "network",
|
|
1878
2682
|
networkCommand: subCommand,
|
|
@@ -1959,7 +2763,7 @@ async function networkCommand(subCommand, urlOrFilter, options = {}) {
|
|
|
1959
2763
|
|
|
1960
2764
|
// packages/cli/src/commands/console.ts
|
|
1961
2765
|
async function consoleCommand(options = {}) {
|
|
1962
|
-
const response = await
|
|
2766
|
+
const response = await sendCommand2({
|
|
1963
2767
|
id: crypto.randomUUID(),
|
|
1964
2768
|
action: "console",
|
|
1965
2769
|
consoleCommand: options.clear ? "clear" : "get",
|
|
@@ -2004,7 +2808,7 @@ async function consoleCommand(options = {}) {
|
|
|
2004
2808
|
|
|
2005
2809
|
// packages/cli/src/commands/errors.ts
|
|
2006
2810
|
async function errorsCommand(options = {}) {
|
|
2007
|
-
const response = await
|
|
2811
|
+
const response = await sendCommand2({
|
|
2008
2812
|
id: crypto.randomUUID(),
|
|
2009
2813
|
action: "errors",
|
|
2010
2814
|
errorsCommand: options.clear ? "clear" : "get",
|
|
@@ -2044,7 +2848,7 @@ async function errorsCommand(options = {}) {
|
|
|
2044
2848
|
|
|
2045
2849
|
// packages/cli/src/commands/trace.ts
|
|
2046
2850
|
async function traceCommand(subCommand, options = {}) {
|
|
2047
|
-
const response = await
|
|
2851
|
+
const response = await sendCommand2({
|
|
2048
2852
|
id: crypto.randomUUID(),
|
|
2049
2853
|
action: "trace",
|
|
2050
2854
|
traceCommand: subCommand,
|
|
@@ -2134,7 +2938,7 @@ function matchTabOrigin2(tabUrl, targetHostname) {
|
|
|
2134
2938
|
}
|
|
2135
2939
|
async function ensureTabForOrigin(origin, hostname) {
|
|
2136
2940
|
const listReq = { id: generateId(), action: "tab_list" };
|
|
2137
|
-
const listResp = await
|
|
2941
|
+
const listResp = await sendCommand2(listReq);
|
|
2138
2942
|
if (listResp.success && listResp.data?.tabs) {
|
|
2139
2943
|
const matchingTab = listResp.data.tabs.find(
|
|
2140
2944
|
(tab) => matchTabOrigin2(tab.url, hostname)
|
|
@@ -2143,7 +2947,7 @@ async function ensureTabForOrigin(origin, hostname) {
|
|
|
2143
2947
|
return matchingTab.tabId;
|
|
2144
2948
|
}
|
|
2145
2949
|
}
|
|
2146
|
-
const newResp = await
|
|
2950
|
+
const newResp = await sendCommand2({ id: generateId(), action: "tab_new", url: origin });
|
|
2147
2951
|
if (!newResp.success) {
|
|
2148
2952
|
throw new Error(`\u65E0\u6CD5\u6253\u5F00 ${origin}: ${newResp.error}`);
|
|
2149
2953
|
}
|
|
@@ -2212,7 +3016,7 @@ async function fetchCommand(url, options = {}) {
|
|
|
2212
3016
|
}
|
|
2213
3017
|
const script = buildFetchScript(url, options);
|
|
2214
3018
|
const evalReq = { id: generateId(), action: "eval", script, tabId: targetTabId };
|
|
2215
|
-
const evalResp = await
|
|
3019
|
+
const evalResp = await sendCommand2(evalReq);
|
|
2216
3020
|
if (!evalResp.success) {
|
|
2217
3021
|
throw new Error(`Fetch \u5931\u8D25: ${evalResp.error}`);
|
|
2218
3022
|
}
|
|
@@ -2246,21 +3050,16 @@ async function fetchCommand(url, options = {}) {
|
|
|
2246
3050
|
|
|
2247
3051
|
// packages/cli/src/commands/history.ts
|
|
2248
3052
|
async function historyCommand(subCommand, options = {}) {
|
|
2249
|
-
const
|
|
2250
|
-
|
|
2251
|
-
action: "history",
|
|
2252
|
-
historyCommand: subCommand,
|
|
2253
|
-
text: options.query,
|
|
2254
|
-
ms: options.days
|
|
2255
|
-
});
|
|
3053
|
+
const days = options.days || 30;
|
|
3054
|
+
const data = subCommand === "search" ? { historyItems: searchHistory(options.query, days) } : { historyDomains: getHistoryDomains(days) };
|
|
2256
3055
|
if (options.json) {
|
|
2257
|
-
console.log(JSON.stringify(
|
|
3056
|
+
console.log(JSON.stringify({
|
|
3057
|
+
id: crypto.randomUUID(),
|
|
3058
|
+
success: true,
|
|
3059
|
+
data
|
|
3060
|
+
}));
|
|
2258
3061
|
return;
|
|
2259
3062
|
}
|
|
2260
|
-
if (!response.success) {
|
|
2261
|
-
throw new Error(response.error || "History command failed");
|
|
2262
|
-
}
|
|
2263
|
-
const data = response.data;
|
|
2264
3063
|
switch (subCommand) {
|
|
2265
3064
|
case "search": {
|
|
2266
3065
|
const items = data?.historyItems || [];
|
|
@@ -2299,10 +3098,13 @@ async function historyCommand(subCommand, options = {}) {
|
|
|
2299
3098
|
}
|
|
2300
3099
|
|
|
2301
3100
|
// packages/cli/src/index.ts
|
|
2302
|
-
var VERSION = "0.
|
|
3101
|
+
var VERSION = "0.8.0";
|
|
2303
3102
|
var HELP_TEXT = `
|
|
2304
3103
|
bb-browser - AI Agent \u6D4F\u89C8\u5668\u81EA\u52A8\u5316\u5DE5\u5177
|
|
2305
3104
|
|
|
3105
|
+
\u5B89\u88C5\uFF1A
|
|
3106
|
+
npm install -g bb-browser
|
|
3107
|
+
|
|
2306
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
|
|
2307
3109
|
bb-browser site list \u67E5\u770B\u6240\u6709\u53EF\u7528\u547D\u4EE4
|
|
2308
3110
|
bb-browser site twitter/search "AI" \u793A\u4F8B\uFF1A\u641C\u7D22\u63A8\u6587
|
|
@@ -2318,6 +3120,7 @@ bb-browser - AI Agent \u6D4F\u89C8\u5668\u81EA\u52A8\u5316\u5DE5\u5177
|
|
|
2318
3120
|
site <name> [args] \u8FD0\u884C adapter
|
|
2319
3121
|
site update \u66F4\u65B0\u793E\u533A adapter \u5E93
|
|
2320
3122
|
guide \u5982\u4F55\u628A\u4EFB\u4F55\u7F51\u7AD9\u53D8\u6210 adapter
|
|
3123
|
+
star \u2B50 Star bb-browser on GitHub
|
|
2321
3124
|
|
|
2322
3125
|
\u6D4F\u89C8\u5668\u64CD\u4F5C\uFF1A
|
|
2323
3126
|
open <url> [--tab] \u6253\u5F00 URL
|
|
@@ -2339,6 +3142,7 @@ bb-browser - AI Agent \u6D4F\u89C8\u5668\u81EA\u52A8\u5316\u5DE5\u5177
|
|
|
2339
3142
|
|
|
2340
3143
|
\u6807\u7B7E\u9875\uFF1A
|
|
2341
3144
|
tab [list|new|close|<n>] \u7BA1\u7406\u6807\u7B7E\u9875
|
|
3145
|
+
status \u67E5\u770B\u53D7\u7BA1\u6D4F\u89C8\u5668\u72B6\u6001
|
|
2342
3146
|
|
|
2343
3147
|
\u5BFC\u822A\uFF1A
|
|
2344
3148
|
back / forward / refresh \u540E\u9000 / \u524D\u8FDB / \u5237\u65B0
|
|
@@ -2352,6 +3156,8 @@ bb-browser - AI Agent \u6D4F\u89C8\u5668\u81EA\u52A8\u5316\u5DE5\u5177
|
|
|
2352
3156
|
|
|
2353
3157
|
\u9009\u9879\uFF1A
|
|
2354
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
|
|
2355
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
|
|
2356
3162
|
-i, --interactive \u53EA\u8F93\u51FA\u53EF\u4EA4\u4E92\u5143\u7D20\uFF08snapshot \u547D\u4EE4\uFF09
|
|
2357
3163
|
-c, --compact \u79FB\u9664\u7A7A\u7ED3\u6784\u8282\u70B9\uFF08snapshot \u547D\u4EE4\uFF09
|
|
@@ -2392,6 +3198,12 @@ function parseArgs(argv) {
|
|
|
2392
3198
|
}
|
|
2393
3199
|
} else if (arg === "--openclaw") {
|
|
2394
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
|
+
}
|
|
2395
3207
|
} else if (arg === "--help" || arg === "-h") {
|
|
2396
3208
|
result.flags.help = true;
|
|
2397
3209
|
} else if (arg === "--version" || arg === "-v") {
|
|
@@ -2441,9 +3253,9 @@ async function main() {
|
|
|
2441
3253
|
return;
|
|
2442
3254
|
}
|
|
2443
3255
|
if (process.argv.includes("--mcp")) {
|
|
2444
|
-
const mcpPath = new URL("./mcp.js", import.meta.url)
|
|
2445
|
-
const { spawn:
|
|
2446
|
-
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" });
|
|
2447
3259
|
child.on("exit", (code) => process.exit(code ?? 0));
|
|
2448
3260
|
return;
|
|
2449
3261
|
}
|
|
@@ -2800,6 +3612,27 @@ async function main() {
|
|
|
2800
3612
|
});
|
|
2801
3613
|
break;
|
|
2802
3614
|
}
|
|
3615
|
+
case "star": {
|
|
3616
|
+
const { execSync: execSync4 } = await import("child_process");
|
|
3617
|
+
try {
|
|
3618
|
+
execSync4("gh auth status", { stdio: "pipe" });
|
|
3619
|
+
} catch {
|
|
3620
|
+
console.error("\u9700\u8981\u5148\u5B89\u88C5\u5E76\u767B\u5F55 GitHub CLI: https://cli.github.com");
|
|
3621
|
+
console.error(" brew install gh && gh auth login");
|
|
3622
|
+
process.exit(1);
|
|
3623
|
+
}
|
|
3624
|
+
const repos = ["epiral/bb-browser", "epiral/bb-sites"];
|
|
3625
|
+
for (const repo of repos) {
|
|
3626
|
+
try {
|
|
3627
|
+
execSync4(`gh api user/starred/${repo} -X PUT`, { stdio: "pipe" });
|
|
3628
|
+
console.log(`\u2B50 Starred ${repo}`);
|
|
3629
|
+
} catch {
|
|
3630
|
+
console.log(`Already starred or failed: ${repo}`);
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
console.log("\nThanks for your support! \u{1F64F}");
|
|
3634
|
+
break;
|
|
3635
|
+
}
|
|
2803
3636
|
case "guide": {
|
|
2804
3637
|
console.log(`How to turn any website into a bb-browser site adapter
|
|
2805
3638
|
=======================================================
|