bb-browser 0.8.1 → 0.8.2
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 +346 -48
- 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";
|
|
@@ -159,25 +160,31 @@ async function discoverCdpPort() {
|
|
|
159
160
|
if (Number.isInteger(explicitPort) && explicitPort > 0 && await canConnect("127.0.0.1", explicitPort)) {
|
|
160
161
|
return { host: "127.0.0.1", port: explicitPort };
|
|
161
162
|
}
|
|
163
|
+
try {
|
|
164
|
+
const rawPort = await readFile(MANAGED_PORT_FILE, "utf8");
|
|
165
|
+
const managedPort = Number.parseInt(rawPort.trim(), 10);
|
|
166
|
+
if (Number.isInteger(managedPort) && managedPort > 0 && await canConnect("127.0.0.1", managedPort)) {
|
|
167
|
+
return { host: "127.0.0.1", port: managedPort };
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
}
|
|
162
171
|
if (process.argv.includes("--openclaw")) {
|
|
163
172
|
const viaOpenClaw = await tryOpenClaw();
|
|
164
173
|
if (viaOpenClaw && await canConnect(viaOpenClaw.host, viaOpenClaw.port)) {
|
|
165
174
|
return viaOpenClaw;
|
|
166
175
|
}
|
|
167
176
|
}
|
|
168
|
-
const
|
|
169
|
-
if (
|
|
170
|
-
return
|
|
177
|
+
const launched = await launchManagedBrowser();
|
|
178
|
+
if (launched) {
|
|
179
|
+
return launched;
|
|
171
180
|
}
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return { host: "127.0.0.1", port: managedPort };
|
|
181
|
+
if (!process.argv.includes("--openclaw")) {
|
|
182
|
+
const detectedOpenClaw = await tryOpenClaw();
|
|
183
|
+
if (detectedOpenClaw && await canConnect(detectedOpenClaw.host, detectedOpenClaw.port)) {
|
|
184
|
+
return detectedOpenClaw;
|
|
177
185
|
}
|
|
178
|
-
} catch {
|
|
179
186
|
}
|
|
180
|
-
return
|
|
187
|
+
return null;
|
|
181
188
|
}
|
|
182
189
|
|
|
183
190
|
// packages/cli/src/cdp-client.ts
|
|
@@ -233,7 +240,10 @@ function connectWebSocket(url) {
|
|
|
233
240
|
return new Promise((resolve2, reject) => {
|
|
234
241
|
const ws = new WebSocket(url);
|
|
235
242
|
ws.once("open", () => {
|
|
236
|
-
ws._socket
|
|
243
|
+
const socket = ws._socket;
|
|
244
|
+
if (socket && typeof socket.unref === "function") {
|
|
245
|
+
socket.unref();
|
|
246
|
+
}
|
|
237
247
|
resolve2(ws);
|
|
238
248
|
});
|
|
239
249
|
ws.once("error", reject);
|
|
@@ -503,27 +513,118 @@ async function ensurePageTarget(targetId) {
|
|
|
503
513
|
target = targets[targetId] ?? targets.find((item) => Number(item.id) === targetId);
|
|
504
514
|
} else if (typeof targetId === "string") {
|
|
505
515
|
target = targets.find((item) => item.id === targetId);
|
|
516
|
+
if (!target) {
|
|
517
|
+
const numericTargetId = Number(targetId);
|
|
518
|
+
if (!Number.isNaN(numericTargetId)) {
|
|
519
|
+
target = targets[numericTargetId] ?? targets.find((item) => Number(item.id) === numericTargetId);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
506
522
|
}
|
|
507
523
|
target ??= targets[0];
|
|
508
524
|
connectionState.currentTargetId = target.id;
|
|
509
525
|
await attachTarget(target.id);
|
|
510
526
|
return target;
|
|
511
527
|
}
|
|
512
|
-
function
|
|
513
|
-
|
|
528
|
+
async function resolveBackendNodeIdByXPath(targetId, xpath) {
|
|
529
|
+
await sessionCommand(targetId, "DOM.getDocument", { depth: 0 });
|
|
530
|
+
const search = await sessionCommand(targetId, "DOM.performSearch", {
|
|
531
|
+
query: xpath,
|
|
532
|
+
includeUserAgentShadowDOM: true
|
|
533
|
+
});
|
|
534
|
+
try {
|
|
535
|
+
if (!search.resultCount) {
|
|
536
|
+
throw new Error(`Unknown ref xpath: ${xpath}`);
|
|
537
|
+
}
|
|
538
|
+
const { nodeIds } = await sessionCommand(targetId, "DOM.getSearchResults", {
|
|
539
|
+
searchId: search.searchId,
|
|
540
|
+
fromIndex: 0,
|
|
541
|
+
toIndex: search.resultCount
|
|
542
|
+
});
|
|
543
|
+
for (const nodeId of nodeIds) {
|
|
544
|
+
const described = await sessionCommand(targetId, "DOM.describeNode", {
|
|
545
|
+
nodeId
|
|
546
|
+
});
|
|
547
|
+
if (described.node.backendNodeId) {
|
|
548
|
+
return described.node.backendNodeId;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
throw new Error(`XPath resolved but no backend node id found: ${xpath}`);
|
|
552
|
+
} finally {
|
|
553
|
+
await sessionCommand(targetId, "DOM.discardSearchResults", { searchId: search.searchId }).catch(() => {
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
async function parseRef(ref) {
|
|
558
|
+
const targetId = connectionState?.currentTargetId ?? "";
|
|
559
|
+
let refs = connectionState?.refsByTarget.get(targetId) ?? {};
|
|
560
|
+
if (!refs[ref] && targetId) {
|
|
561
|
+
const persistedRefs = loadPersistedRefs(targetId);
|
|
562
|
+
if (persistedRefs) {
|
|
563
|
+
connectionState?.refsByTarget.set(targetId, persistedRefs);
|
|
564
|
+
refs = persistedRefs;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
514
567
|
const found = refs[ref];
|
|
515
|
-
if (!found
|
|
568
|
+
if (!found) {
|
|
516
569
|
throw new Error(`Unknown ref: ${ref}. Run snapshot first.`);
|
|
517
570
|
}
|
|
518
|
-
|
|
571
|
+
if (found.backendDOMNodeId) {
|
|
572
|
+
return found.backendDOMNodeId;
|
|
573
|
+
}
|
|
574
|
+
if (targetId && found.xpath) {
|
|
575
|
+
const backendDOMNodeId = await resolveBackendNodeIdByXPath(targetId, found.xpath);
|
|
576
|
+
found.backendDOMNodeId = backendDOMNodeId;
|
|
577
|
+
connectionState?.refsByTarget.set(targetId, refs);
|
|
578
|
+
const pageUrl = await evaluate(targetId, "location.href", true).catch(() => void 0);
|
|
579
|
+
if (pageUrl) {
|
|
580
|
+
persistRefs(targetId, pageUrl, refs);
|
|
581
|
+
}
|
|
582
|
+
return backendDOMNodeId;
|
|
583
|
+
}
|
|
584
|
+
throw new Error(`Unknown ref: ${ref}. Run snapshot first.`);
|
|
585
|
+
}
|
|
586
|
+
function getRefsFilePath(targetId) {
|
|
587
|
+
return path2.join(os2.tmpdir(), `bb-browser-refs-${targetId}.json`);
|
|
588
|
+
}
|
|
589
|
+
function loadPersistedRefs(targetId, expectedUrl) {
|
|
590
|
+
try {
|
|
591
|
+
const data = JSON.parse(readFileSync(getRefsFilePath(targetId), "utf-8"));
|
|
592
|
+
if (data.targetId !== targetId) return null;
|
|
593
|
+
if (expectedUrl !== void 0 && data.url !== expectedUrl) return null;
|
|
594
|
+
if (!data.refs || typeof data.refs !== "object") return null;
|
|
595
|
+
return data.refs;
|
|
596
|
+
} catch {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function persistRefs(targetId, url, refs) {
|
|
601
|
+
try {
|
|
602
|
+
writeFileSync(getRefsFilePath(targetId), JSON.stringify({ targetId, url, timestamp: Date.now(), refs }));
|
|
603
|
+
} catch {
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
function clearPersistedRefs(targetId) {
|
|
607
|
+
try {
|
|
608
|
+
unlinkSync(getRefsFilePath(targetId));
|
|
609
|
+
} catch {
|
|
610
|
+
}
|
|
519
611
|
}
|
|
520
612
|
function loadBuildDomTreeScript() {
|
|
521
613
|
const currentDir = path2.dirname(fileURLToPath(import.meta.url));
|
|
522
614
|
const candidates = [
|
|
615
|
+
path2.resolve(currentDir, "./extension/buildDomTree.js"),
|
|
616
|
+
// npm installed: dist/cli.js → ../extension/buildDomTree.js
|
|
617
|
+
path2.resolve(currentDir, "../extension/buildDomTree.js"),
|
|
618
|
+
path2.resolve(currentDir, "../extension/dist/buildDomTree.js"),
|
|
619
|
+
path2.resolve(currentDir, "../packages/extension/public/buildDomTree.js"),
|
|
620
|
+
path2.resolve(currentDir, "../packages/extension/dist/buildDomTree.js"),
|
|
621
|
+
// dev mode: packages/cli/dist/ → ../../../extension/
|
|
523
622
|
path2.resolve(currentDir, "../../../extension/buildDomTree.js"),
|
|
623
|
+
path2.resolve(currentDir, "../../../extension/dist/buildDomTree.js"),
|
|
624
|
+
// dev mode: packages/cli/src/ → ../../extension/
|
|
524
625
|
path2.resolve(currentDir, "../../extension/buildDomTree.js"),
|
|
525
|
-
path2.resolve(currentDir, "../../../packages/extension/buildDomTree.js"),
|
|
526
|
-
path2.resolve(currentDir, "../../../extension/
|
|
626
|
+
path2.resolve(currentDir, "../../../packages/extension/dist/buildDomTree.js"),
|
|
627
|
+
path2.resolve(currentDir, "../../../packages/extension/public/buildDomTree.js")
|
|
527
628
|
];
|
|
528
629
|
for (const candidate of candidates) {
|
|
529
630
|
try {
|
|
@@ -551,8 +652,34 @@ async function resolveNode(targetId, backendNodeId) {
|
|
|
551
652
|
return result.nodeId;
|
|
552
653
|
}
|
|
553
654
|
async function focusNode(targetId, backendNodeId) {
|
|
554
|
-
|
|
555
|
-
|
|
655
|
+
await sessionCommand(targetId, "DOM.focus", { backendNodeId });
|
|
656
|
+
}
|
|
657
|
+
async function insertTextIntoNode(targetId, backendNodeId, text, clearFirst) {
|
|
658
|
+
const resolved = await sessionCommand(targetId, "DOM.resolveNode", { backendNodeId });
|
|
659
|
+
await sessionCommand(targetId, "Runtime.callFunctionOn", {
|
|
660
|
+
objectId: resolved.object.objectId,
|
|
661
|
+
functionDeclaration: `function(value, clearFirst) {
|
|
662
|
+
if (typeof this.focus === 'function') this.focus();
|
|
663
|
+
if (clearFirst && ('value' in this)) {
|
|
664
|
+
this.value = '';
|
|
665
|
+
this.dispatchEvent(new Event('input', { bubbles: true }));
|
|
666
|
+
}
|
|
667
|
+
if ('value' in this) {
|
|
668
|
+
this.value = clearFirst ? value : String(this.value ?? '') + value;
|
|
669
|
+
this.dispatchEvent(new Event('input', { bubbles: true }));
|
|
670
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
671
|
+
return true;
|
|
672
|
+
}
|
|
673
|
+
return false;
|
|
674
|
+
}`,
|
|
675
|
+
arguments: [
|
|
676
|
+
{ value: text },
|
|
677
|
+
{ value: clearFirst }
|
|
678
|
+
],
|
|
679
|
+
returnByValue: true
|
|
680
|
+
});
|
|
681
|
+
await focusNode(targetId, backendNodeId);
|
|
682
|
+
await sessionCommand(targetId, "Input.insertText", { text });
|
|
556
683
|
}
|
|
557
684
|
async function getNodeBox(targetId, backendNodeId) {
|
|
558
685
|
const result = await sessionCommand(targetId, "DOM.getBoxModel", {
|
|
@@ -586,15 +713,181 @@ async function getAttributeValue(targetId, backendNodeId, attribute) {
|
|
|
586
713
|
}
|
|
587
714
|
async function buildSnapshot(targetId, request) {
|
|
588
715
|
const script = loadBuildDomTreeScript();
|
|
589
|
-
const
|
|
716
|
+
const buildArgs = {
|
|
717
|
+
showHighlightElements: true,
|
|
718
|
+
focusHighlightIndex: -1,
|
|
719
|
+
viewportExpansion: -1,
|
|
720
|
+
debugMode: false,
|
|
721
|
+
startId: 0,
|
|
722
|
+
startHighlightIndex: 0
|
|
723
|
+
};
|
|
724
|
+
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({
|
|
725
|
+
...buildArgs
|
|
726
|
+
})}); })()`;
|
|
727
|
+
const value = await evaluate(targetId, expression, true);
|
|
728
|
+
if (!value || !value.map || !value.rootId) {
|
|
729
|
+
const title = await evaluate(targetId, "document.title", true);
|
|
730
|
+
const pageUrl2 = await evaluate(targetId, "location.href", true);
|
|
731
|
+
const fallbackSnapshot = {
|
|
732
|
+
title,
|
|
733
|
+
url: pageUrl2,
|
|
734
|
+
lines: [title || pageUrl2],
|
|
735
|
+
refs: {}
|
|
736
|
+
};
|
|
737
|
+
connectionState?.refsByTarget.set(targetId, {});
|
|
738
|
+
persistRefs(targetId, pageUrl2, {});
|
|
739
|
+
return fallbackSnapshot;
|
|
740
|
+
}
|
|
741
|
+
const snapshot = convertBuildDomTreeResult(value, {
|
|
590
742
|
interactiveOnly: !!request.interactive,
|
|
591
743
|
compact: !!request.compact,
|
|
592
744
|
maxDepth: request.maxDepth,
|
|
593
745
|
selector: request.selector
|
|
594
|
-
})
|
|
595
|
-
const
|
|
596
|
-
connectionState?.refsByTarget.set(targetId,
|
|
597
|
-
|
|
746
|
+
});
|
|
747
|
+
const pageUrl = await evaluate(targetId, "location.href", true);
|
|
748
|
+
connectionState?.refsByTarget.set(targetId, snapshot.refs || {});
|
|
749
|
+
persistRefs(targetId, pageUrl, snapshot.refs || {});
|
|
750
|
+
return snapshot;
|
|
751
|
+
}
|
|
752
|
+
function convertBuildDomTreeResult(result, options) {
|
|
753
|
+
const { interactiveOnly, compact, maxDepth, selector } = options;
|
|
754
|
+
const { rootId, map } = result;
|
|
755
|
+
const refs = {};
|
|
756
|
+
const lines = [];
|
|
757
|
+
const getRole = (node) => {
|
|
758
|
+
const tagName = node.tagName.toLowerCase();
|
|
759
|
+
const role = node.attributes?.role;
|
|
760
|
+
if (role) return role;
|
|
761
|
+
const type = node.attributes?.type?.toLowerCase() || "text";
|
|
762
|
+
const inputRoleMap = {
|
|
763
|
+
text: "textbox",
|
|
764
|
+
password: "textbox",
|
|
765
|
+
email: "textbox",
|
|
766
|
+
url: "textbox",
|
|
767
|
+
tel: "textbox",
|
|
768
|
+
search: "searchbox",
|
|
769
|
+
number: "spinbutton",
|
|
770
|
+
range: "slider",
|
|
771
|
+
checkbox: "checkbox",
|
|
772
|
+
radio: "radio",
|
|
773
|
+
button: "button",
|
|
774
|
+
submit: "button",
|
|
775
|
+
reset: "button",
|
|
776
|
+
file: "button"
|
|
777
|
+
};
|
|
778
|
+
const roleMap = {
|
|
779
|
+
a: "link",
|
|
780
|
+
button: "button",
|
|
781
|
+
input: inputRoleMap[type] || "textbox",
|
|
782
|
+
select: "combobox",
|
|
783
|
+
textarea: "textbox",
|
|
784
|
+
img: "image",
|
|
785
|
+
nav: "navigation",
|
|
786
|
+
main: "main",
|
|
787
|
+
header: "banner",
|
|
788
|
+
footer: "contentinfo",
|
|
789
|
+
aside: "complementary",
|
|
790
|
+
form: "form",
|
|
791
|
+
table: "table",
|
|
792
|
+
ul: "list",
|
|
793
|
+
ol: "list",
|
|
794
|
+
li: "listitem",
|
|
795
|
+
h1: "heading",
|
|
796
|
+
h2: "heading",
|
|
797
|
+
h3: "heading",
|
|
798
|
+
h4: "heading",
|
|
799
|
+
h5: "heading",
|
|
800
|
+
h6: "heading",
|
|
801
|
+
dialog: "dialog",
|
|
802
|
+
article: "article",
|
|
803
|
+
section: "region",
|
|
804
|
+
label: "label",
|
|
805
|
+
details: "group",
|
|
806
|
+
summary: "button"
|
|
807
|
+
};
|
|
808
|
+
return roleMap[tagName] || tagName;
|
|
809
|
+
};
|
|
810
|
+
const collectTextContent = (node, nodeMap, depthLimit = 5) => {
|
|
811
|
+
const texts = [];
|
|
812
|
+
const visit = (nodeId, depth) => {
|
|
813
|
+
if (depth > depthLimit) return;
|
|
814
|
+
const currentNode = nodeMap[nodeId];
|
|
815
|
+
if (!currentNode) return;
|
|
816
|
+
if ("type" in currentNode && currentNode.type === "TEXT_NODE") {
|
|
817
|
+
const text = currentNode.text.trim();
|
|
818
|
+
if (text) texts.push(text);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
for (const childId of currentNode.children || []) visit(childId, depth + 1);
|
|
822
|
+
};
|
|
823
|
+
for (const childId of node.children || []) visit(childId, 0);
|
|
824
|
+
return texts.join(" ").trim();
|
|
825
|
+
};
|
|
826
|
+
const getName = (node) => {
|
|
827
|
+
const attrs = node.attributes || {};
|
|
828
|
+
return attrs["aria-label"] || attrs.title || attrs.placeholder || attrs.alt || attrs.value || collectTextContent(node, map) || attrs.name || void 0;
|
|
829
|
+
};
|
|
830
|
+
const truncateText = (text, length = 50) => text.length <= length ? text : `${text.slice(0, length - 3)}...`;
|
|
831
|
+
const selectorText = selector?.trim().toLowerCase();
|
|
832
|
+
const matchesSelector = (node, role, name) => {
|
|
833
|
+
if (!selectorText) return true;
|
|
834
|
+
const haystack = [node.tagName, role, name, node.xpath || "", ...Object.values(node.attributes || {})].join(" ").toLowerCase();
|
|
835
|
+
return haystack.includes(selectorText);
|
|
836
|
+
};
|
|
837
|
+
if (interactiveOnly) {
|
|
838
|
+
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));
|
|
839
|
+
for (const { node } of interactiveNodes) {
|
|
840
|
+
const refId = String(node.highlightIndex);
|
|
841
|
+
const role = getRole(node);
|
|
842
|
+
const name = getName(node);
|
|
843
|
+
if (!matchesSelector(node, role, name)) continue;
|
|
844
|
+
let line = `${role} [ref=${refId}]`;
|
|
845
|
+
if (name) line += ` ${JSON.stringify(truncateText(name))}`;
|
|
846
|
+
lines.push(line);
|
|
847
|
+
refs[refId] = {
|
|
848
|
+
xpath: node.xpath || "",
|
|
849
|
+
role,
|
|
850
|
+
name,
|
|
851
|
+
tagName: node.tagName.toLowerCase()
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
return { snapshot: lines.join("\n"), refs };
|
|
855
|
+
}
|
|
856
|
+
const walk = (nodeId, depth) => {
|
|
857
|
+
if (maxDepth !== void 0 && depth > maxDepth) return;
|
|
858
|
+
const node = map[nodeId];
|
|
859
|
+
if (!node) return;
|
|
860
|
+
if ("type" in node && node.type === "TEXT_NODE") {
|
|
861
|
+
const text = node.text.trim();
|
|
862
|
+
if (!text) return;
|
|
863
|
+
lines.push(`${" ".repeat(depth)}- text ${JSON.stringify(truncateText(text, compact ? 80 : 120))}`);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
const role = getRole(node);
|
|
867
|
+
const name = getName(node);
|
|
868
|
+
if (!matchesSelector(node, role, name)) {
|
|
869
|
+
for (const childId of node.children || []) walk(childId, depth + 1);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
const indent = " ".repeat(depth);
|
|
873
|
+
const refId = node.highlightIndex !== void 0 && node.highlightIndex !== null ? String(node.highlightIndex) : null;
|
|
874
|
+
let line = `${indent}- ${role}`;
|
|
875
|
+
if (refId) line += ` [ref=${refId}]`;
|
|
876
|
+
if (name) line += ` ${JSON.stringify(truncateText(name, compact ? 50 : 80))}`;
|
|
877
|
+
if (!compact) line += ` <${node.tagName.toLowerCase()}>`;
|
|
878
|
+
lines.push(line);
|
|
879
|
+
if (refId) {
|
|
880
|
+
refs[refId] = {
|
|
881
|
+
xpath: node.xpath || "",
|
|
882
|
+
role,
|
|
883
|
+
name,
|
|
884
|
+
tagName: node.tagName.toLowerCase()
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
for (const childId of node.children || []) walk(childId, depth + 1);
|
|
888
|
+
};
|
|
889
|
+
walk(rootId, 0);
|
|
890
|
+
return { snapshot: lines.join("\n"), refs };
|
|
598
891
|
}
|
|
599
892
|
function ok(id, data) {
|
|
600
893
|
return { id, success: true, data };
|
|
@@ -638,10 +931,12 @@ async function dispatchRequest(request) {
|
|
|
638
931
|
if (request.tabId === void 0) {
|
|
639
932
|
const created = await browserCommand("Target.createTarget", { url: request.url });
|
|
640
933
|
const newTarget = await ensurePageTarget(created.targetId);
|
|
641
|
-
return ok(request.id, { url: request.url, tabId:
|
|
934
|
+
return ok(request.id, { url: request.url, tabId: newTarget.id });
|
|
642
935
|
}
|
|
643
936
|
await pageCommand(target.id, "Page.navigate", { url: request.url });
|
|
644
|
-
|
|
937
|
+
connectionState?.refsByTarget.delete(target.id);
|
|
938
|
+
clearPersistedRefs(target.id);
|
|
939
|
+
return ok(request.id, { url: request.url, title: target.title, tabId: target.id });
|
|
645
940
|
}
|
|
646
941
|
case "snapshot": {
|
|
647
942
|
const snapshotData = await buildSnapshot(target.id, request);
|
|
@@ -650,7 +945,7 @@ async function dispatchRequest(request) {
|
|
|
650
945
|
case "click":
|
|
651
946
|
case "hover": {
|
|
652
947
|
if (!request.ref) return fail(request.id, "Missing ref parameter");
|
|
653
|
-
const backendNodeId = parseRef(request.ref);
|
|
948
|
+
const backendNodeId = await parseRef(request.ref);
|
|
654
949
|
const point = await getNodeBox(target.id, backendNodeId);
|
|
655
950
|
await sessionCommand(target.id, "Input.dispatchMouseEvent", { type: "mouseMoved", x: point.x, y: point.y, button: "none" });
|
|
656
951
|
if (request.action === "click") await mouseClick(target.id, point.x, point.y);
|
|
@@ -660,18 +955,14 @@ async function dispatchRequest(request) {
|
|
|
660
955
|
case "type": {
|
|
661
956
|
if (!request.ref) return fail(request.id, "Missing ref parameter");
|
|
662
957
|
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 });
|
|
958
|
+
const backendNodeId = await parseRef(request.ref);
|
|
959
|
+
await insertTextIntoNode(target.id, backendNodeId, request.text, request.action === "fill");
|
|
669
960
|
return ok(request.id, { value: request.text });
|
|
670
961
|
}
|
|
671
962
|
case "check":
|
|
672
963
|
case "uncheck": {
|
|
673
964
|
if (!request.ref) return fail(request.id, "Missing ref parameter");
|
|
674
|
-
const backendNodeId = parseRef(request.ref);
|
|
965
|
+
const backendNodeId = await parseRef(request.ref);
|
|
675
966
|
const desired = request.action === "check";
|
|
676
967
|
const resolved = await sessionCommand(target.id, "DOM.resolveNode", { backendNodeId });
|
|
677
968
|
await sessionCommand(target.id, "Runtime.callFunctionOn", {
|
|
@@ -682,7 +973,7 @@ async function dispatchRequest(request) {
|
|
|
682
973
|
}
|
|
683
974
|
case "select": {
|
|
684
975
|
if (!request.ref || request.value == null) return fail(request.id, "Missing ref or value parameter");
|
|
685
|
-
const backendNodeId = parseRef(request.ref);
|
|
976
|
+
const backendNodeId = await parseRef(request.ref);
|
|
686
977
|
const resolved = await sessionCommand(target.id, "DOM.resolveNode", { backendNodeId });
|
|
687
978
|
await sessionCommand(target.id, "Runtime.callFunctionOn", {
|
|
688
979
|
objectId: resolved.object.objectId,
|
|
@@ -692,7 +983,7 @@ async function dispatchRequest(request) {
|
|
|
692
983
|
}
|
|
693
984
|
case "get": {
|
|
694
985
|
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);
|
|
986
|
+
const value = await getAttributeValue(target.id, await parseRef(request.ref), request.attribute);
|
|
696
987
|
return ok(request.id, { value });
|
|
697
988
|
}
|
|
698
989
|
case "screenshot": {
|
|
@@ -701,6 +992,8 @@ async function dispatchRequest(request) {
|
|
|
701
992
|
}
|
|
702
993
|
case "close": {
|
|
703
994
|
await browserCommand("Target.closeTarget", { targetId: target.id });
|
|
995
|
+
connectionState?.refsByTarget.delete(target.id);
|
|
996
|
+
clearPersistedRefs(target.id);
|
|
704
997
|
return ok(request.id, {});
|
|
705
998
|
}
|
|
706
999
|
case "wait": {
|
|
@@ -739,12 +1032,12 @@ async function dispatchRequest(request) {
|
|
|
739
1032
|
return ok(request.id, { result });
|
|
740
1033
|
}
|
|
741
1034
|
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:
|
|
1035
|
+
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
1036
|
return ok(request.id, { tabs, activeIndex: tabs.findIndex((tab) => tab.active) });
|
|
744
1037
|
}
|
|
745
1038
|
case "tab_new": {
|
|
746
1039
|
const created = await browserCommand("Target.createTarget", { url: request.url ?? "about:blank" });
|
|
747
|
-
return ok(request.id, { tabId:
|
|
1040
|
+
return ok(request.id, { tabId: created.targetId, url: request.url ?? "about:blank" });
|
|
748
1041
|
}
|
|
749
1042
|
case "tab_select": {
|
|
750
1043
|
const tabs = (await getTargets()).filter((item) => item.type === "page");
|
|
@@ -752,14 +1045,16 @@ async function dispatchRequest(request) {
|
|
|
752
1045
|
if (!selected) return fail(request.id, "Tab not found");
|
|
753
1046
|
connectionState.currentTargetId = selected.id;
|
|
754
1047
|
await attachTarget(selected.id);
|
|
755
|
-
return ok(request.id, { tabId:
|
|
1048
|
+
return ok(request.id, { tabId: selected.id, url: selected.url, title: selected.title });
|
|
756
1049
|
}
|
|
757
1050
|
case "tab_close": {
|
|
758
1051
|
const tabs = (await getTargets()).filter((item) => item.type === "page");
|
|
759
1052
|
const selected = request.tabId !== void 0 ? tabs.find((item) => item.id === String(request.tabId) || Number(item.id) === request.tabId) : tabs[request.index ?? 0];
|
|
760
1053
|
if (!selected) return fail(request.id, "Tab not found");
|
|
761
1054
|
await browserCommand("Target.closeTarget", { targetId: selected.id });
|
|
762
|
-
|
|
1055
|
+
connectionState?.refsByTarget.delete(selected.id);
|
|
1056
|
+
clearPersistedRefs(selected.id);
|
|
1057
|
+
return ok(request.id, { tabId: selected.id });
|
|
763
1058
|
}
|
|
764
1059
|
case "frame": {
|
|
765
1060
|
if (!request.selector) return fail(request.id, "Missing selector parameter");
|
|
@@ -911,7 +1206,7 @@ async function ensureDaemonRunning() {
|
|
|
911
1206
|
}
|
|
912
1207
|
|
|
913
1208
|
// packages/cli/src/history-sqlite.ts
|
|
914
|
-
import { copyFileSync, existsSync as existsSync3, unlinkSync } from "fs";
|
|
1209
|
+
import { copyFileSync, existsSync as existsSync3, unlinkSync as unlinkSync2 } from "fs";
|
|
915
1210
|
import { execSync as execSync2 } from "child_process";
|
|
916
1211
|
import { homedir, tmpdir } from "os";
|
|
917
1212
|
import { join } from "path";
|
|
@@ -969,7 +1264,7 @@ function runHistoryQuery(sql, mapRow) {
|
|
|
969
1264
|
return [];
|
|
970
1265
|
} finally {
|
|
971
1266
|
try {
|
|
972
|
-
|
|
1267
|
+
unlinkSync2(tmpPath);
|
|
973
1268
|
} catch {
|
|
974
1269
|
}
|
|
975
1270
|
}
|
|
@@ -1960,11 +2255,11 @@ async function getCommand(attribute, ref, options = {}) {
|
|
|
1960
2255
|
// packages/cli/src/commands/screenshot.ts
|
|
1961
2256
|
import fs from "fs";
|
|
1962
2257
|
import path3 from "path";
|
|
1963
|
-
import
|
|
2258
|
+
import os3 from "os";
|
|
1964
2259
|
function getDefaultPath() {
|
|
1965
2260
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1966
2261
|
const filename = `bb-screenshot-${timestamp}.png`;
|
|
1967
|
-
return path3.join(
|
|
2262
|
+
return path3.join(os3.tmpdir(), filename);
|
|
1968
2263
|
}
|
|
1969
2264
|
function saveBase64Image(dataUrl, filePath) {
|
|
1970
2265
|
const base64Data = dataUrl.replace(/^data:image\/png;base64,/, "");
|
|
@@ -2510,6 +2805,9 @@ function parseTabSubcommand(args, rawArgv) {
|
|
|
2510
2805
|
return { action: "tab_list" };
|
|
2511
2806
|
}
|
|
2512
2807
|
const first = args[0];
|
|
2808
|
+
if (first === "list") {
|
|
2809
|
+
return { action: "tab_list" };
|
|
2810
|
+
}
|
|
2513
2811
|
if (first === "new") {
|
|
2514
2812
|
return { action: "tab_new", url: args[1] };
|
|
2515
2813
|
}
|
|
@@ -3038,9 +3336,9 @@ async function fetchCommand(url, options = {}) {
|
|
|
3038
3336
|
throw new Error(`Fetch error: ${result.error}`);
|
|
3039
3337
|
}
|
|
3040
3338
|
if (options.output) {
|
|
3041
|
-
const { writeFileSync } = await import("fs");
|
|
3339
|
+
const { writeFileSync: writeFileSync2 } = await import("fs");
|
|
3042
3340
|
const content = typeof result.body === "object" ? JSON.stringify(result.body, null, 2) : String(result.body);
|
|
3043
|
-
|
|
3341
|
+
writeFileSync2(options.output, content, "utf-8");
|
|
3044
3342
|
console.log(`\u5DF2\u5199\u5165 ${options.output} (${result.status}, ${content.length} bytes)`);
|
|
3045
3343
|
return;
|
|
3046
3344
|
}
|
|
@@ -3101,7 +3399,7 @@ async function historyCommand(subCommand, options = {}) {
|
|
|
3101
3399
|
}
|
|
3102
3400
|
|
|
3103
3401
|
// packages/cli/src/index.ts
|
|
3104
|
-
var VERSION = "0.8.
|
|
3402
|
+
var VERSION = "0.8.2";
|
|
3105
3403
|
var HELP_TEXT = `
|
|
3106
3404
|
bb-browser - AI Agent \u6D4F\u89C8\u5668\u81EA\u52A8\u5316\u5DE5\u5177
|
|
3107
3405
|
|