bb-browser 0.8.1 → 0.8.3
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 +350 -186
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -12,9 +12,10 @@ import "./chunk-D4HDZEJT.js";
|
|
|
12
12
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
13
13
|
|
|
14
14
|
// packages/cli/src/cdp-client.ts
|
|
15
|
-
import { readFileSync } from "fs";
|
|
15
|
+
import { readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
16
16
|
import { request as httpRequest } from "http";
|
|
17
17
|
import { request as httpsRequest } from "https";
|
|
18
|
+
import os2 from "os";
|
|
18
19
|
import path2 from "path";
|
|
19
20
|
import { fileURLToPath } from "url";
|
|
20
21
|
import WebSocket from "ws";
|
|
@@ -72,6 +73,10 @@ function findBrowserExecutable() {
|
|
|
72
73
|
if (process.platform === "darwin") {
|
|
73
74
|
const candidates = [
|
|
74
75
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
76
|
+
"/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev",
|
|
77
|
+
"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
|
|
78
|
+
"/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta",
|
|
79
|
+
"/Applications/Arc.app/Contents/MacOS/Arc",
|
|
75
80
|
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
76
81
|
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
|
|
77
82
|
];
|
|
@@ -159,25 +164,31 @@ async function discoverCdpPort() {
|
|
|
159
164
|
if (Number.isInteger(explicitPort) && explicitPort > 0 && await canConnect("127.0.0.1", explicitPort)) {
|
|
160
165
|
return { host: "127.0.0.1", port: explicitPort };
|
|
161
166
|
}
|
|
167
|
+
try {
|
|
168
|
+
const rawPort = await readFile(MANAGED_PORT_FILE, "utf8");
|
|
169
|
+
const managedPort = Number.parseInt(rawPort.trim(), 10);
|
|
170
|
+
if (Number.isInteger(managedPort) && managedPort > 0 && await canConnect("127.0.0.1", managedPort)) {
|
|
171
|
+
return { host: "127.0.0.1", port: managedPort };
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
}
|
|
162
175
|
if (process.argv.includes("--openclaw")) {
|
|
163
176
|
const viaOpenClaw = await tryOpenClaw();
|
|
164
177
|
if (viaOpenClaw && await canConnect(viaOpenClaw.host, viaOpenClaw.port)) {
|
|
165
178
|
return viaOpenClaw;
|
|
166
179
|
}
|
|
167
180
|
}
|
|
168
|
-
const
|
|
169
|
-
if (
|
|
170
|
-
return
|
|
181
|
+
const launched = await launchManagedBrowser();
|
|
182
|
+
if (launched) {
|
|
183
|
+
return launched;
|
|
171
184
|
}
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return { host: "127.0.0.1", port: managedPort };
|
|
185
|
+
if (!process.argv.includes("--openclaw")) {
|
|
186
|
+
const detectedOpenClaw = await tryOpenClaw();
|
|
187
|
+
if (detectedOpenClaw && await canConnect(detectedOpenClaw.host, detectedOpenClaw.port)) {
|
|
188
|
+
return detectedOpenClaw;
|
|
177
189
|
}
|
|
178
|
-
} catch {
|
|
179
190
|
}
|
|
180
|
-
return
|
|
191
|
+
return null;
|
|
181
192
|
}
|
|
182
193
|
|
|
183
194
|
// packages/cli/src/cdp-client.ts
|
|
@@ -233,7 +244,10 @@ function connectWebSocket(url) {
|
|
|
233
244
|
return new Promise((resolve2, reject) => {
|
|
234
245
|
const ws = new WebSocket(url);
|
|
235
246
|
ws.once("open", () => {
|
|
236
|
-
ws._socket
|
|
247
|
+
const socket = ws._socket;
|
|
248
|
+
if (socket && typeof socket.unref === "function") {
|
|
249
|
+
socket.unref();
|
|
250
|
+
}
|
|
237
251
|
resolve2(ws);
|
|
238
252
|
});
|
|
239
253
|
ws.once("error", reject);
|
|
@@ -503,27 +517,118 @@ async function ensurePageTarget(targetId) {
|
|
|
503
517
|
target = targets[targetId] ?? targets.find((item) => Number(item.id) === targetId);
|
|
504
518
|
} else if (typeof targetId === "string") {
|
|
505
519
|
target = targets.find((item) => item.id === targetId);
|
|
520
|
+
if (!target) {
|
|
521
|
+
const numericTargetId = Number(targetId);
|
|
522
|
+
if (!Number.isNaN(numericTargetId)) {
|
|
523
|
+
target = targets[numericTargetId] ?? targets.find((item) => Number(item.id) === numericTargetId);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
506
526
|
}
|
|
507
527
|
target ??= targets[0];
|
|
508
528
|
connectionState.currentTargetId = target.id;
|
|
509
529
|
await attachTarget(target.id);
|
|
510
530
|
return target;
|
|
511
531
|
}
|
|
512
|
-
function
|
|
513
|
-
|
|
532
|
+
async function resolveBackendNodeIdByXPath(targetId, xpath) {
|
|
533
|
+
await sessionCommand(targetId, "DOM.getDocument", { depth: 0 });
|
|
534
|
+
const search = await sessionCommand(targetId, "DOM.performSearch", {
|
|
535
|
+
query: xpath,
|
|
536
|
+
includeUserAgentShadowDOM: true
|
|
537
|
+
});
|
|
538
|
+
try {
|
|
539
|
+
if (!search.resultCount) {
|
|
540
|
+
throw new Error(`Unknown ref xpath: ${xpath}`);
|
|
541
|
+
}
|
|
542
|
+
const { nodeIds } = await sessionCommand(targetId, "DOM.getSearchResults", {
|
|
543
|
+
searchId: search.searchId,
|
|
544
|
+
fromIndex: 0,
|
|
545
|
+
toIndex: search.resultCount
|
|
546
|
+
});
|
|
547
|
+
for (const nodeId of nodeIds) {
|
|
548
|
+
const described = await sessionCommand(targetId, "DOM.describeNode", {
|
|
549
|
+
nodeId
|
|
550
|
+
});
|
|
551
|
+
if (described.node.backendNodeId) {
|
|
552
|
+
return described.node.backendNodeId;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
throw new Error(`XPath resolved but no backend node id found: ${xpath}`);
|
|
556
|
+
} finally {
|
|
557
|
+
await sessionCommand(targetId, "DOM.discardSearchResults", { searchId: search.searchId }).catch(() => {
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
async function parseRef(ref) {
|
|
562
|
+
const targetId = connectionState?.currentTargetId ?? "";
|
|
563
|
+
let refs = connectionState?.refsByTarget.get(targetId) ?? {};
|
|
564
|
+
if (!refs[ref] && targetId) {
|
|
565
|
+
const persistedRefs = loadPersistedRefs(targetId);
|
|
566
|
+
if (persistedRefs) {
|
|
567
|
+
connectionState?.refsByTarget.set(targetId, persistedRefs);
|
|
568
|
+
refs = persistedRefs;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
514
571
|
const found = refs[ref];
|
|
515
|
-
if (!found
|
|
572
|
+
if (!found) {
|
|
516
573
|
throw new Error(`Unknown ref: ${ref}. Run snapshot first.`);
|
|
517
574
|
}
|
|
518
|
-
|
|
575
|
+
if (found.backendDOMNodeId) {
|
|
576
|
+
return found.backendDOMNodeId;
|
|
577
|
+
}
|
|
578
|
+
if (targetId && found.xpath) {
|
|
579
|
+
const backendDOMNodeId = await resolveBackendNodeIdByXPath(targetId, found.xpath);
|
|
580
|
+
found.backendDOMNodeId = backendDOMNodeId;
|
|
581
|
+
connectionState?.refsByTarget.set(targetId, refs);
|
|
582
|
+
const pageUrl = await evaluate(targetId, "location.href", true).catch(() => void 0);
|
|
583
|
+
if (pageUrl) {
|
|
584
|
+
persistRefs(targetId, pageUrl, refs);
|
|
585
|
+
}
|
|
586
|
+
return backendDOMNodeId;
|
|
587
|
+
}
|
|
588
|
+
throw new Error(`Unknown ref: ${ref}. Run snapshot first.`);
|
|
589
|
+
}
|
|
590
|
+
function getRefsFilePath(targetId) {
|
|
591
|
+
return path2.join(os2.tmpdir(), `bb-browser-refs-${targetId}.json`);
|
|
592
|
+
}
|
|
593
|
+
function loadPersistedRefs(targetId, expectedUrl) {
|
|
594
|
+
try {
|
|
595
|
+
const data = JSON.parse(readFileSync(getRefsFilePath(targetId), "utf-8"));
|
|
596
|
+
if (data.targetId !== targetId) return null;
|
|
597
|
+
if (expectedUrl !== void 0 && data.url !== expectedUrl) return null;
|
|
598
|
+
if (!data.refs || typeof data.refs !== "object") return null;
|
|
599
|
+
return data.refs;
|
|
600
|
+
} catch {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function persistRefs(targetId, url, refs) {
|
|
605
|
+
try {
|
|
606
|
+
writeFileSync(getRefsFilePath(targetId), JSON.stringify({ targetId, url, timestamp: Date.now(), refs }));
|
|
607
|
+
} catch {
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
function clearPersistedRefs(targetId) {
|
|
611
|
+
try {
|
|
612
|
+
unlinkSync(getRefsFilePath(targetId));
|
|
613
|
+
} catch {
|
|
614
|
+
}
|
|
519
615
|
}
|
|
520
616
|
function loadBuildDomTreeScript() {
|
|
521
617
|
const currentDir = path2.dirname(fileURLToPath(import.meta.url));
|
|
522
618
|
const candidates = [
|
|
619
|
+
path2.resolve(currentDir, "./extension/buildDomTree.js"),
|
|
620
|
+
// npm installed: dist/cli.js → ../extension/buildDomTree.js
|
|
621
|
+
path2.resolve(currentDir, "../extension/buildDomTree.js"),
|
|
622
|
+
path2.resolve(currentDir, "../extension/dist/buildDomTree.js"),
|
|
623
|
+
path2.resolve(currentDir, "../packages/extension/public/buildDomTree.js"),
|
|
624
|
+
path2.resolve(currentDir, "../packages/extension/dist/buildDomTree.js"),
|
|
625
|
+
// dev mode: packages/cli/dist/ → ../../../extension/
|
|
523
626
|
path2.resolve(currentDir, "../../../extension/buildDomTree.js"),
|
|
627
|
+
path2.resolve(currentDir, "../../../extension/dist/buildDomTree.js"),
|
|
628
|
+
// dev mode: packages/cli/src/ → ../../extension/
|
|
524
629
|
path2.resolve(currentDir, "../../extension/buildDomTree.js"),
|
|
525
|
-
path2.resolve(currentDir, "../../../packages/extension/buildDomTree.js"),
|
|
526
|
-
path2.resolve(currentDir, "../../../extension/
|
|
630
|
+
path2.resolve(currentDir, "../../../packages/extension/dist/buildDomTree.js"),
|
|
631
|
+
path2.resolve(currentDir, "../../../packages/extension/public/buildDomTree.js")
|
|
527
632
|
];
|
|
528
633
|
for (const candidate of candidates) {
|
|
529
634
|
try {
|
|
@@ -551,8 +656,34 @@ async function resolveNode(targetId, backendNodeId) {
|
|
|
551
656
|
return result.nodeId;
|
|
552
657
|
}
|
|
553
658
|
async function focusNode(targetId, backendNodeId) {
|
|
554
|
-
|
|
555
|
-
|
|
659
|
+
await sessionCommand(targetId, "DOM.focus", { backendNodeId });
|
|
660
|
+
}
|
|
661
|
+
async function insertTextIntoNode(targetId, backendNodeId, text, clearFirst) {
|
|
662
|
+
const resolved = await sessionCommand(targetId, "DOM.resolveNode", { backendNodeId });
|
|
663
|
+
await sessionCommand(targetId, "Runtime.callFunctionOn", {
|
|
664
|
+
objectId: resolved.object.objectId,
|
|
665
|
+
functionDeclaration: `function(value, clearFirst) {
|
|
666
|
+
if (typeof this.focus === 'function') this.focus();
|
|
667
|
+
if (clearFirst && ('value' in this)) {
|
|
668
|
+
this.value = '';
|
|
669
|
+
this.dispatchEvent(new Event('input', { bubbles: true }));
|
|
670
|
+
}
|
|
671
|
+
if ('value' in this) {
|
|
672
|
+
this.value = clearFirst ? value : String(this.value ?? '') + value;
|
|
673
|
+
this.dispatchEvent(new Event('input', { bubbles: true }));
|
|
674
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
677
|
+
return false;
|
|
678
|
+
}`,
|
|
679
|
+
arguments: [
|
|
680
|
+
{ value: text },
|
|
681
|
+
{ value: clearFirst }
|
|
682
|
+
],
|
|
683
|
+
returnByValue: true
|
|
684
|
+
});
|
|
685
|
+
await focusNode(targetId, backendNodeId);
|
|
686
|
+
await sessionCommand(targetId, "Input.insertText", { text });
|
|
556
687
|
}
|
|
557
688
|
async function getNodeBox(targetId, backendNodeId) {
|
|
558
689
|
const result = await sessionCommand(targetId, "DOM.getBoxModel", {
|
|
@@ -586,15 +717,181 @@ async function getAttributeValue(targetId, backendNodeId, attribute) {
|
|
|
586
717
|
}
|
|
587
718
|
async function buildSnapshot(targetId, request) {
|
|
588
719
|
const script = loadBuildDomTreeScript();
|
|
589
|
-
const
|
|
720
|
+
const buildArgs = {
|
|
721
|
+
showHighlightElements: true,
|
|
722
|
+
focusHighlightIndex: -1,
|
|
723
|
+
viewportExpansion: -1,
|
|
724
|
+
debugMode: false,
|
|
725
|
+
startId: 0,
|
|
726
|
+
startHighlightIndex: 0
|
|
727
|
+
};
|
|
728
|
+
const expression = `(() => { ${script}; const fn = globalThis.buildDomTree ?? (typeof window !== 'undefined' ? window.buildDomTree : undefined); if (typeof fn !== 'function') { throw new Error('buildDomTree is not available after script injection'); } return fn(${JSON.stringify({
|
|
729
|
+
...buildArgs
|
|
730
|
+
})}); })()`;
|
|
731
|
+
const value = await evaluate(targetId, expression, true);
|
|
732
|
+
if (!value || !value.map || !value.rootId) {
|
|
733
|
+
const title = await evaluate(targetId, "document.title", true);
|
|
734
|
+
const pageUrl2 = await evaluate(targetId, "location.href", true);
|
|
735
|
+
const fallbackSnapshot = {
|
|
736
|
+
title,
|
|
737
|
+
url: pageUrl2,
|
|
738
|
+
lines: [title || pageUrl2],
|
|
739
|
+
refs: {}
|
|
740
|
+
};
|
|
741
|
+
connectionState?.refsByTarget.set(targetId, {});
|
|
742
|
+
persistRefs(targetId, pageUrl2, {});
|
|
743
|
+
return fallbackSnapshot;
|
|
744
|
+
}
|
|
745
|
+
const snapshot = convertBuildDomTreeResult(value, {
|
|
590
746
|
interactiveOnly: !!request.interactive,
|
|
591
747
|
compact: !!request.compact,
|
|
592
748
|
maxDepth: request.maxDepth,
|
|
593
749
|
selector: request.selector
|
|
594
|
-
})
|
|
595
|
-
const
|
|
596
|
-
connectionState?.refsByTarget.set(targetId,
|
|
597
|
-
|
|
750
|
+
});
|
|
751
|
+
const pageUrl = await evaluate(targetId, "location.href", true);
|
|
752
|
+
connectionState?.refsByTarget.set(targetId, snapshot.refs || {});
|
|
753
|
+
persistRefs(targetId, pageUrl, snapshot.refs || {});
|
|
754
|
+
return snapshot;
|
|
755
|
+
}
|
|
756
|
+
function convertBuildDomTreeResult(result, options) {
|
|
757
|
+
const { interactiveOnly, compact, maxDepth, selector } = options;
|
|
758
|
+
const { rootId, map } = result;
|
|
759
|
+
const refs = {};
|
|
760
|
+
const lines = [];
|
|
761
|
+
const getRole = (node) => {
|
|
762
|
+
const tagName = node.tagName.toLowerCase();
|
|
763
|
+
const role = node.attributes?.role;
|
|
764
|
+
if (role) return role;
|
|
765
|
+
const type = node.attributes?.type?.toLowerCase() || "text";
|
|
766
|
+
const inputRoleMap = {
|
|
767
|
+
text: "textbox",
|
|
768
|
+
password: "textbox",
|
|
769
|
+
email: "textbox",
|
|
770
|
+
url: "textbox",
|
|
771
|
+
tel: "textbox",
|
|
772
|
+
search: "searchbox",
|
|
773
|
+
number: "spinbutton",
|
|
774
|
+
range: "slider",
|
|
775
|
+
checkbox: "checkbox",
|
|
776
|
+
radio: "radio",
|
|
777
|
+
button: "button",
|
|
778
|
+
submit: "button",
|
|
779
|
+
reset: "button",
|
|
780
|
+
file: "button"
|
|
781
|
+
};
|
|
782
|
+
const roleMap = {
|
|
783
|
+
a: "link",
|
|
784
|
+
button: "button",
|
|
785
|
+
input: inputRoleMap[type] || "textbox",
|
|
786
|
+
select: "combobox",
|
|
787
|
+
textarea: "textbox",
|
|
788
|
+
img: "image",
|
|
789
|
+
nav: "navigation",
|
|
790
|
+
main: "main",
|
|
791
|
+
header: "banner",
|
|
792
|
+
footer: "contentinfo",
|
|
793
|
+
aside: "complementary",
|
|
794
|
+
form: "form",
|
|
795
|
+
table: "table",
|
|
796
|
+
ul: "list",
|
|
797
|
+
ol: "list",
|
|
798
|
+
li: "listitem",
|
|
799
|
+
h1: "heading",
|
|
800
|
+
h2: "heading",
|
|
801
|
+
h3: "heading",
|
|
802
|
+
h4: "heading",
|
|
803
|
+
h5: "heading",
|
|
804
|
+
h6: "heading",
|
|
805
|
+
dialog: "dialog",
|
|
806
|
+
article: "article",
|
|
807
|
+
section: "region",
|
|
808
|
+
label: "label",
|
|
809
|
+
details: "group",
|
|
810
|
+
summary: "button"
|
|
811
|
+
};
|
|
812
|
+
return roleMap[tagName] || tagName;
|
|
813
|
+
};
|
|
814
|
+
const collectTextContent = (node, nodeMap, depthLimit = 5) => {
|
|
815
|
+
const texts = [];
|
|
816
|
+
const visit = (nodeId, depth) => {
|
|
817
|
+
if (depth > depthLimit) return;
|
|
818
|
+
const currentNode = nodeMap[nodeId];
|
|
819
|
+
if (!currentNode) return;
|
|
820
|
+
if ("type" in currentNode && currentNode.type === "TEXT_NODE") {
|
|
821
|
+
const text = currentNode.text.trim();
|
|
822
|
+
if (text) texts.push(text);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
for (const childId of currentNode.children || []) visit(childId, depth + 1);
|
|
826
|
+
};
|
|
827
|
+
for (const childId of node.children || []) visit(childId, 0);
|
|
828
|
+
return texts.join(" ").trim();
|
|
829
|
+
};
|
|
830
|
+
const getName = (node) => {
|
|
831
|
+
const attrs = node.attributes || {};
|
|
832
|
+
return attrs["aria-label"] || attrs.title || attrs.placeholder || attrs.alt || attrs.value || collectTextContent(node, map) || attrs.name || void 0;
|
|
833
|
+
};
|
|
834
|
+
const truncateText = (text, length = 50) => text.length <= length ? text : `${text.slice(0, length - 3)}...`;
|
|
835
|
+
const selectorText = selector?.trim().toLowerCase();
|
|
836
|
+
const matchesSelector = (node, role, name) => {
|
|
837
|
+
if (!selectorText) return true;
|
|
838
|
+
const haystack = [node.tagName, role, name, node.xpath || "", ...Object.values(node.attributes || {})].join(" ").toLowerCase();
|
|
839
|
+
return haystack.includes(selectorText);
|
|
840
|
+
};
|
|
841
|
+
if (interactiveOnly) {
|
|
842
|
+
const interactiveNodes = Object.entries(map).filter(([, node]) => !("type" in node) && node.highlightIndex !== void 0 && node.highlightIndex !== null).map(([id, node]) => ({ id, node })).sort((a, b) => (a.node.highlightIndex ?? 0) - (b.node.highlightIndex ?? 0));
|
|
843
|
+
for (const { node } of interactiveNodes) {
|
|
844
|
+
const refId = String(node.highlightIndex);
|
|
845
|
+
const role = getRole(node);
|
|
846
|
+
const name = getName(node);
|
|
847
|
+
if (!matchesSelector(node, role, name)) continue;
|
|
848
|
+
let line = `${role} [ref=${refId}]`;
|
|
849
|
+
if (name) line += ` ${JSON.stringify(truncateText(name))}`;
|
|
850
|
+
lines.push(line);
|
|
851
|
+
refs[refId] = {
|
|
852
|
+
xpath: node.xpath || "",
|
|
853
|
+
role,
|
|
854
|
+
name,
|
|
855
|
+
tagName: node.tagName.toLowerCase()
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
return { snapshot: lines.join("\n"), refs };
|
|
859
|
+
}
|
|
860
|
+
const walk = (nodeId, depth) => {
|
|
861
|
+
if (maxDepth !== void 0 && depth > maxDepth) return;
|
|
862
|
+
const node = map[nodeId];
|
|
863
|
+
if (!node) return;
|
|
864
|
+
if ("type" in node && node.type === "TEXT_NODE") {
|
|
865
|
+
const text = node.text.trim();
|
|
866
|
+
if (!text) return;
|
|
867
|
+
lines.push(`${" ".repeat(depth)}- text ${JSON.stringify(truncateText(text, compact ? 80 : 120))}`);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
const role = getRole(node);
|
|
871
|
+
const name = getName(node);
|
|
872
|
+
if (!matchesSelector(node, role, name)) {
|
|
873
|
+
for (const childId of node.children || []) walk(childId, depth + 1);
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
const indent = " ".repeat(depth);
|
|
877
|
+
const refId = node.highlightIndex !== void 0 && node.highlightIndex !== null ? String(node.highlightIndex) : null;
|
|
878
|
+
let line = `${indent}- ${role}`;
|
|
879
|
+
if (refId) line += ` [ref=${refId}]`;
|
|
880
|
+
if (name) line += ` ${JSON.stringify(truncateText(name, compact ? 50 : 80))}`;
|
|
881
|
+
if (!compact) line += ` <${node.tagName.toLowerCase()}>`;
|
|
882
|
+
lines.push(line);
|
|
883
|
+
if (refId) {
|
|
884
|
+
refs[refId] = {
|
|
885
|
+
xpath: node.xpath || "",
|
|
886
|
+
role,
|
|
887
|
+
name,
|
|
888
|
+
tagName: node.tagName.toLowerCase()
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
for (const childId of node.children || []) walk(childId, depth + 1);
|
|
892
|
+
};
|
|
893
|
+
walk(rootId, 0);
|
|
894
|
+
return { snapshot: lines.join("\n"), refs };
|
|
598
895
|
}
|
|
599
896
|
function ok(id, data) {
|
|
600
897
|
return { id, success: true, data };
|
|
@@ -638,10 +935,12 @@ async function dispatchRequest(request) {
|
|
|
638
935
|
if (request.tabId === void 0) {
|
|
639
936
|
const created = await browserCommand("Target.createTarget", { url: request.url });
|
|
640
937
|
const newTarget = await ensurePageTarget(created.targetId);
|
|
641
|
-
return ok(request.id, { url: request.url, tabId:
|
|
938
|
+
return ok(request.id, { url: request.url, tabId: newTarget.id });
|
|
642
939
|
}
|
|
643
940
|
await pageCommand(target.id, "Page.navigate", { url: request.url });
|
|
644
|
-
|
|
941
|
+
connectionState?.refsByTarget.delete(target.id);
|
|
942
|
+
clearPersistedRefs(target.id);
|
|
943
|
+
return ok(request.id, { url: request.url, title: target.title, tabId: target.id });
|
|
645
944
|
}
|
|
646
945
|
case "snapshot": {
|
|
647
946
|
const snapshotData = await buildSnapshot(target.id, request);
|
|
@@ -650,7 +949,7 @@ async function dispatchRequest(request) {
|
|
|
650
949
|
case "click":
|
|
651
950
|
case "hover": {
|
|
652
951
|
if (!request.ref) return fail(request.id, "Missing ref parameter");
|
|
653
|
-
const backendNodeId = parseRef(request.ref);
|
|
952
|
+
const backendNodeId = await parseRef(request.ref);
|
|
654
953
|
const point = await getNodeBox(target.id, backendNodeId);
|
|
655
954
|
await sessionCommand(target.id, "Input.dispatchMouseEvent", { type: "mouseMoved", x: point.x, y: point.y, button: "none" });
|
|
656
955
|
if (request.action === "click") await mouseClick(target.id, point.x, point.y);
|
|
@@ -660,18 +959,14 @@ async function dispatchRequest(request) {
|
|
|
660
959
|
case "type": {
|
|
661
960
|
if (!request.ref) return fail(request.id, "Missing ref parameter");
|
|
662
961
|
if (request.text == null) return fail(request.id, "Missing text parameter");
|
|
663
|
-
const backendNodeId = parseRef(request.ref);
|
|
664
|
-
await
|
|
665
|
-
if (request.action === "fill") {
|
|
666
|
-
await evaluate(target.id, `document.activeElement && ((document.activeElement.value = ''), document.activeElement.dispatchEvent(new Event('input', { bubbles: true })))`);
|
|
667
|
-
}
|
|
668
|
-
await sessionCommand(target.id, "Input.insertText", { text: request.text });
|
|
962
|
+
const backendNodeId = await parseRef(request.ref);
|
|
963
|
+
await insertTextIntoNode(target.id, backendNodeId, request.text, request.action === "fill");
|
|
669
964
|
return ok(request.id, { value: request.text });
|
|
670
965
|
}
|
|
671
966
|
case "check":
|
|
672
967
|
case "uncheck": {
|
|
673
968
|
if (!request.ref) return fail(request.id, "Missing ref parameter");
|
|
674
|
-
const backendNodeId = parseRef(request.ref);
|
|
969
|
+
const backendNodeId = await parseRef(request.ref);
|
|
675
970
|
const desired = request.action === "check";
|
|
676
971
|
const resolved = await sessionCommand(target.id, "DOM.resolveNode", { backendNodeId });
|
|
677
972
|
await sessionCommand(target.id, "Runtime.callFunctionOn", {
|
|
@@ -682,7 +977,7 @@ async function dispatchRequest(request) {
|
|
|
682
977
|
}
|
|
683
978
|
case "select": {
|
|
684
979
|
if (!request.ref || request.value == null) return fail(request.id, "Missing ref or value parameter");
|
|
685
|
-
const backendNodeId = parseRef(request.ref);
|
|
980
|
+
const backendNodeId = await parseRef(request.ref);
|
|
686
981
|
const resolved = await sessionCommand(target.id, "DOM.resolveNode", { backendNodeId });
|
|
687
982
|
await sessionCommand(target.id, "Runtime.callFunctionOn", {
|
|
688
983
|
objectId: resolved.object.objectId,
|
|
@@ -692,7 +987,7 @@ async function dispatchRequest(request) {
|
|
|
692
987
|
}
|
|
693
988
|
case "get": {
|
|
694
989
|
if (!request.ref || !request.attribute) return fail(request.id, "Missing ref or attribute parameter");
|
|
695
|
-
const value = await getAttributeValue(target.id, parseRef(request.ref), request.attribute);
|
|
990
|
+
const value = await getAttributeValue(target.id, await parseRef(request.ref), request.attribute);
|
|
696
991
|
return ok(request.id, { value });
|
|
697
992
|
}
|
|
698
993
|
case "screenshot": {
|
|
@@ -701,6 +996,8 @@ async function dispatchRequest(request) {
|
|
|
701
996
|
}
|
|
702
997
|
case "close": {
|
|
703
998
|
await browserCommand("Target.closeTarget", { targetId: target.id });
|
|
999
|
+
connectionState?.refsByTarget.delete(target.id);
|
|
1000
|
+
clearPersistedRefs(target.id);
|
|
704
1001
|
return ok(request.id, {});
|
|
705
1002
|
}
|
|
706
1003
|
case "wait": {
|
|
@@ -739,12 +1036,12 @@ async function dispatchRequest(request) {
|
|
|
739
1036
|
return ok(request.id, { result });
|
|
740
1037
|
}
|
|
741
1038
|
case "tab_list": {
|
|
742
|
-
const tabs = (await getTargets()).filter((item) => item.type === "page").map((item, index) => ({ index, url: item.url, title: item.title, active: item.id === connectionState?.currentTargetId || !connectionState?.currentTargetId && index === 0, tabId:
|
|
1039
|
+
const tabs = (await getTargets()).filter((item) => item.type === "page").map((item, index) => ({ index, url: item.url, title: item.title, active: item.id === connectionState?.currentTargetId || !connectionState?.currentTargetId && index === 0, tabId: item.id }));
|
|
743
1040
|
return ok(request.id, { tabs, activeIndex: tabs.findIndex((tab) => tab.active) });
|
|
744
1041
|
}
|
|
745
1042
|
case "tab_new": {
|
|
746
1043
|
const created = await browserCommand("Target.createTarget", { url: request.url ?? "about:blank" });
|
|
747
|
-
return ok(request.id, { tabId:
|
|
1044
|
+
return ok(request.id, { tabId: created.targetId, url: request.url ?? "about:blank" });
|
|
748
1045
|
}
|
|
749
1046
|
case "tab_select": {
|
|
750
1047
|
const tabs = (await getTargets()).filter((item) => item.type === "page");
|
|
@@ -752,14 +1049,16 @@ async function dispatchRequest(request) {
|
|
|
752
1049
|
if (!selected) return fail(request.id, "Tab not found");
|
|
753
1050
|
connectionState.currentTargetId = selected.id;
|
|
754
1051
|
await attachTarget(selected.id);
|
|
755
|
-
return ok(request.id, { tabId:
|
|
1052
|
+
return ok(request.id, { tabId: selected.id, url: selected.url, title: selected.title });
|
|
756
1053
|
}
|
|
757
1054
|
case "tab_close": {
|
|
758
1055
|
const tabs = (await getTargets()).filter((item) => item.type === "page");
|
|
759
1056
|
const selected = request.tabId !== void 0 ? tabs.find((item) => item.id === String(request.tabId) || Number(item.id) === request.tabId) : tabs[request.index ?? 0];
|
|
760
1057
|
if (!selected) return fail(request.id, "Tab not found");
|
|
761
1058
|
await browserCommand("Target.closeTarget", { targetId: selected.id });
|
|
762
|
-
|
|
1059
|
+
connectionState?.refsByTarget.delete(selected.id);
|
|
1060
|
+
clearPersistedRefs(selected.id);
|
|
1061
|
+
return ok(request.id, { tabId: selected.id });
|
|
763
1062
|
}
|
|
764
1063
|
case "frame": {
|
|
765
1064
|
if (!request.selector) return fail(request.id, "Missing selector parameter");
|
|
@@ -891,9 +1190,6 @@ async function sendCommand2(request) {
|
|
|
891
1190
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
892
1191
|
import { dirname, resolve } from "path";
|
|
893
1192
|
import { existsSync as existsSync2 } from "fs";
|
|
894
|
-
async function isDaemonRunning() {
|
|
895
|
-
return await discoverCdpPort() !== null;
|
|
896
|
-
}
|
|
897
1193
|
async function ensureDaemonRunning() {
|
|
898
1194
|
try {
|
|
899
1195
|
await ensureCdpConnection();
|
|
@@ -911,7 +1207,7 @@ async function ensureDaemonRunning() {
|
|
|
911
1207
|
}
|
|
912
1208
|
|
|
913
1209
|
// packages/cli/src/history-sqlite.ts
|
|
914
|
-
import { copyFileSync, existsSync as existsSync3, unlinkSync } from "fs";
|
|
1210
|
+
import { copyFileSync, existsSync as existsSync3, unlinkSync as unlinkSync2 } from "fs";
|
|
915
1211
|
import { execSync as execSync2 } from "child_process";
|
|
916
1212
|
import { homedir, tmpdir } from "os";
|
|
917
1213
|
import { join } from "path";
|
|
@@ -969,7 +1265,7 @@ function runHistoryQuery(sql, mapRow) {
|
|
|
969
1265
|
return [];
|
|
970
1266
|
} finally {
|
|
971
1267
|
try {
|
|
972
|
-
|
|
1268
|
+
unlinkSync2(tmpPath);
|
|
973
1269
|
} catch {
|
|
974
1270
|
}
|
|
975
1271
|
}
|
|
@@ -1960,11 +2256,11 @@ async function getCommand(attribute, ref, options = {}) {
|
|
|
1960
2256
|
// packages/cli/src/commands/screenshot.ts
|
|
1961
2257
|
import fs from "fs";
|
|
1962
2258
|
import path3 from "path";
|
|
1963
|
-
import
|
|
2259
|
+
import os3 from "os";
|
|
1964
2260
|
function getDefaultPath() {
|
|
1965
2261
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1966
2262
|
const filename = `bb-screenshot-${timestamp}.png`;
|
|
1967
|
-
return path3.join(
|
|
2263
|
+
return path3.join(os3.tmpdir(), filename);
|
|
1968
2264
|
}
|
|
1969
2265
|
function saveBase64Image(dataUrl, filePath) {
|
|
1970
2266
|
const base64Data = dataUrl.replace(/^data:image\/png;base64,/, "");
|
|
@@ -2140,123 +2436,6 @@ async function scrollCommand(direction, pixels, options = {}) {
|
|
|
2140
2436
|
}
|
|
2141
2437
|
}
|
|
2142
2438
|
|
|
2143
|
-
// packages/cli/src/commands/daemon.ts
|
|
2144
|
-
async function statusCommand(options = {}) {
|
|
2145
|
-
const running = await isDaemonRunning();
|
|
2146
|
-
if (options.json) {
|
|
2147
|
-
console.log(JSON.stringify({ running }));
|
|
2148
|
-
} else {
|
|
2149
|
-
console.log(running ? "\u6D4F\u89C8\u5668\u8FD0\u884C\u4E2D" : "\u6D4F\u89C8\u5668\u672A\u8FD0\u884C");
|
|
2150
|
-
}
|
|
2151
|
-
}
|
|
2152
|
-
|
|
2153
|
-
// packages/cli/src/commands/reload.ts
|
|
2154
|
-
import WebSocket2 from "ws";
|
|
2155
|
-
var EXTENSION_NAME = "bb-browser";
|
|
2156
|
-
async function reloadCommand(options = {}) {
|
|
2157
|
-
const port = options.port || 9222;
|
|
2158
|
-
try {
|
|
2159
|
-
const listRes = await fetch(`http://127.0.0.1:${port}/json/list`);
|
|
2160
|
-
if (!listRes.ok) {
|
|
2161
|
-
throw new Error(`CDP \u672A\u542F\u7528\u3002\u8BF7\u7528 --remote-debugging-port=${port} \u542F\u52A8 Chrome`);
|
|
2162
|
-
}
|
|
2163
|
-
const list = await listRes.json();
|
|
2164
|
-
const extPage = list.find(
|
|
2165
|
-
(t) => t.type === "page" && t.url.includes("chrome://extensions")
|
|
2166
|
-
);
|
|
2167
|
-
if (!extPage) {
|
|
2168
|
-
throw new Error("\u8BF7\u5148\u6253\u5F00 chrome://extensions \u9875\u9762");
|
|
2169
|
-
}
|
|
2170
|
-
const result = await new Promise((resolve2, reject) => {
|
|
2171
|
-
const ws = new WebSocket2(extPage.webSocketDebuggerUrl);
|
|
2172
|
-
let resolved = false;
|
|
2173
|
-
const timeout = setTimeout(() => {
|
|
2174
|
-
if (!resolved) {
|
|
2175
|
-
resolved = true;
|
|
2176
|
-
ws.close();
|
|
2177
|
-
reject(new Error("CDP \u8FDE\u63A5\u8D85\u65F6"));
|
|
2178
|
-
}
|
|
2179
|
-
}, 1e4);
|
|
2180
|
-
ws.on("open", () => {
|
|
2181
|
-
const script = `
|
|
2182
|
-
(async function() {
|
|
2183
|
-
if (!chrome || !chrome.developerPrivate) {
|
|
2184
|
-
return { error: 'developerPrivate API not available' };
|
|
2185
|
-
}
|
|
2186
|
-
|
|
2187
|
-
try {
|
|
2188
|
-
const exts = await chrome.developerPrivate.getExtensionsInfo();
|
|
2189
|
-
const bbExt = exts.find(e => e.name === '${EXTENSION_NAME}');
|
|
2190
|
-
|
|
2191
|
-
if (!bbExt) {
|
|
2192
|
-
return { error: '${EXTENSION_NAME} \u6269\u5C55\u672A\u5B89\u88C5' };
|
|
2193
|
-
}
|
|
2194
|
-
|
|
2195
|
-
if (bbExt.state !== 'ENABLED') {
|
|
2196
|
-
return { error: '${EXTENSION_NAME} \u6269\u5C55\u5DF2\u7981\u7528' };
|
|
2197
|
-
}
|
|
2198
|
-
|
|
2199
|
-
await chrome.developerPrivate.reload(bbExt.id, {failQuietly: true});
|
|
2200
|
-
return { success: true, extensionId: bbExt.id };
|
|
2201
|
-
} catch (e) {
|
|
2202
|
-
return { error: e.message };
|
|
2203
|
-
}
|
|
2204
|
-
})()
|
|
2205
|
-
`;
|
|
2206
|
-
ws.send(JSON.stringify({
|
|
2207
|
-
id: 1,
|
|
2208
|
-
method: "Runtime.evaluate",
|
|
2209
|
-
params: {
|
|
2210
|
-
expression: script,
|
|
2211
|
-
awaitPromise: true,
|
|
2212
|
-
returnByValue: true
|
|
2213
|
-
}
|
|
2214
|
-
}));
|
|
2215
|
-
});
|
|
2216
|
-
ws.on("message", (data) => {
|
|
2217
|
-
const msg = JSON.parse(data.toString());
|
|
2218
|
-
if (msg.id === 1) {
|
|
2219
|
-
clearTimeout(timeout);
|
|
2220
|
-
resolved = true;
|
|
2221
|
-
ws.close();
|
|
2222
|
-
const value = msg.result?.result?.value;
|
|
2223
|
-
if (value?.success) {
|
|
2224
|
-
resolve2({
|
|
2225
|
-
success: true,
|
|
2226
|
-
message: "\u6269\u5C55\u5DF2\u91CD\u8F7D",
|
|
2227
|
-
extensionId: value.extensionId
|
|
2228
|
-
});
|
|
2229
|
-
} else if (value?.error) {
|
|
2230
|
-
reject(new Error(value.error));
|
|
2231
|
-
} else {
|
|
2232
|
-
reject(new Error(`\u91CD\u8F7D\u5931\u8D25: ${JSON.stringify(value)}`));
|
|
2233
|
-
}
|
|
2234
|
-
}
|
|
2235
|
-
});
|
|
2236
|
-
ws.on("error", (err) => {
|
|
2237
|
-
clearTimeout(timeout);
|
|
2238
|
-
if (!resolved) {
|
|
2239
|
-
resolved = true;
|
|
2240
|
-
reject(new Error(`CDP \u8FDE\u63A5\u5931\u8D25: ${err.message}`));
|
|
2241
|
-
}
|
|
2242
|
-
});
|
|
2243
|
-
});
|
|
2244
|
-
if (options.json) {
|
|
2245
|
-
console.log(JSON.stringify(result));
|
|
2246
|
-
} else {
|
|
2247
|
-
console.log(`${result.message} (${result.extensionId})`);
|
|
2248
|
-
}
|
|
2249
|
-
} catch (error) {
|
|
2250
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2251
|
-
if (options.json) {
|
|
2252
|
-
console.log(JSON.stringify({ success: false, error: message }));
|
|
2253
|
-
} else {
|
|
2254
|
-
console.error(`\u9519\u8BEF: ${message}`);
|
|
2255
|
-
}
|
|
2256
|
-
process.exit(1);
|
|
2257
|
-
}
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
2439
|
// packages/cli/src/commands/nav.ts
|
|
2261
2440
|
async function backCommand(options = {}) {
|
|
2262
2441
|
await ensureDaemonRunning();
|
|
@@ -2510,6 +2689,9 @@ function parseTabSubcommand(args, rawArgv) {
|
|
|
2510
2689
|
return { action: "tab_list" };
|
|
2511
2690
|
}
|
|
2512
2691
|
const first = args[0];
|
|
2692
|
+
if (first === "list") {
|
|
2693
|
+
return { action: "tab_list" };
|
|
2694
|
+
}
|
|
2513
2695
|
if (first === "new") {
|
|
2514
2696
|
return { action: "tab_new", url: args[1] };
|
|
2515
2697
|
}
|
|
@@ -3038,9 +3220,9 @@ async function fetchCommand(url, options = {}) {
|
|
|
3038
3220
|
throw new Error(`Fetch error: ${result.error}`);
|
|
3039
3221
|
}
|
|
3040
3222
|
if (options.output) {
|
|
3041
|
-
const { writeFileSync } = await import("fs");
|
|
3223
|
+
const { writeFileSync: writeFileSync2 } = await import("fs");
|
|
3042
3224
|
const content = typeof result.body === "object" ? JSON.stringify(result.body, null, 2) : String(result.body);
|
|
3043
|
-
|
|
3225
|
+
writeFileSync2(options.output, content, "utf-8");
|
|
3044
3226
|
console.log(`\u5DF2\u5199\u5165 ${options.output} (${result.status}, ${content.length} bytes)`);
|
|
3045
3227
|
return;
|
|
3046
3228
|
}
|
|
@@ -3101,7 +3283,7 @@ async function historyCommand(subCommand, options = {}) {
|
|
|
3101
3283
|
}
|
|
3102
3284
|
|
|
3103
3285
|
// packages/cli/src/index.ts
|
|
3104
|
-
var VERSION = "0.8.
|
|
3286
|
+
var VERSION = "0.8.2";
|
|
3105
3287
|
var HELP_TEXT = `
|
|
3106
3288
|
bb-browser - AI Agent \u6D4F\u89C8\u5668\u81EA\u52A8\u5316\u5DE5\u5177
|
|
3107
3289
|
|
|
@@ -3419,24 +3601,6 @@ async function main() {
|
|
|
3419
3601
|
break;
|
|
3420
3602
|
}
|
|
3421
3603
|
case "daemon":
|
|
3422
|
-
case "start": {
|
|
3423
|
-
const hostIdx = process.argv.findIndex((a) => a === "--host");
|
|
3424
|
-
const host = hostIdx >= 0 ? process.argv[hostIdx + 1] : void 0;
|
|
3425
|
-
await daemonCommand({ json: parsed.flags.json, host });
|
|
3426
|
-
break;
|
|
3427
|
-
}
|
|
3428
|
-
case "stop": {
|
|
3429
|
-
await stopCommand({ json: parsed.flags.json });
|
|
3430
|
-
break;
|
|
3431
|
-
}
|
|
3432
|
-
case "status": {
|
|
3433
|
-
await statusCommand({ json: parsed.flags.json });
|
|
3434
|
-
break;
|
|
3435
|
-
}
|
|
3436
|
-
case "reload": {
|
|
3437
|
-
await reloadCommand({ json: parsed.flags.json });
|
|
3438
|
-
break;
|
|
3439
|
-
}
|
|
3440
3604
|
case "close": {
|
|
3441
3605
|
await closeCommand({ json: parsed.flags.json, tabId: globalTabId });
|
|
3442
3606
|
break;
|