@different-ai/opencode-browser 4.0.6 → 4.1.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 +4 -1
- package/dist/plugin.js +222 -85
- package/extension/background.js +645 -49
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/plugin.js
CHANGED
|
@@ -12354,6 +12354,7 @@ function getPackageVersion() {
|
|
|
12354
12354
|
cachedVersion = "unknown";
|
|
12355
12355
|
return cachedVersion;
|
|
12356
12356
|
}
|
|
12357
|
+
var { schema } = tool;
|
|
12357
12358
|
var BASE_DIR = join(homedir(), ".opencode-browser");
|
|
12358
12359
|
var SOCKET_PATH = join(BASE_DIR, "broker.sock");
|
|
12359
12360
|
mkdirSync(BASE_DIR, { recursive: true });
|
|
@@ -12467,91 +12468,227 @@ function toolResultText(data, fallback) {
|
|
|
12467
12468
|
return JSON.stringify(data.content);
|
|
12468
12469
|
return fallback;
|
|
12469
12470
|
}
|
|
12470
|
-
var plugin = {
|
|
12471
|
-
|
|
12472
|
-
|
|
12473
|
-
tool
|
|
12474
|
-
|
|
12475
|
-
|
|
12476
|
-
|
|
12477
|
-
|
|
12478
|
-
|
|
12479
|
-
|
|
12480
|
-
|
|
12481
|
-
|
|
12482
|
-
|
|
12483
|
-
|
|
12484
|
-
|
|
12485
|
-
|
|
12486
|
-
|
|
12487
|
-
|
|
12488
|
-
|
|
12489
|
-
|
|
12490
|
-
|
|
12491
|
-
|
|
12492
|
-
|
|
12493
|
-
|
|
12494
|
-
|
|
12495
|
-
|
|
12496
|
-
|
|
12497
|
-
|
|
12498
|
-
|
|
12499
|
-
|
|
12500
|
-
|
|
12501
|
-
|
|
12502
|
-
|
|
12503
|
-
|
|
12504
|
-
|
|
12505
|
-
|
|
12506
|
-
|
|
12507
|
-
|
|
12508
|
-
|
|
12509
|
-
|
|
12510
|
-
|
|
12511
|
-
|
|
12512
|
-
|
|
12513
|
-
|
|
12514
|
-
|
|
12515
|
-
|
|
12516
|
-
|
|
12517
|
-
|
|
12518
|
-
|
|
12519
|
-
|
|
12520
|
-
|
|
12521
|
-
|
|
12522
|
-
|
|
12523
|
-
|
|
12524
|
-
|
|
12525
|
-
|
|
12526
|
-
|
|
12527
|
-
|
|
12528
|
-
|
|
12529
|
-
|
|
12530
|
-
|
|
12531
|
-
|
|
12532
|
-
|
|
12533
|
-
|
|
12534
|
-
|
|
12535
|
-
|
|
12536
|
-
|
|
12537
|
-
|
|
12538
|
-
|
|
12539
|
-
|
|
12540
|
-
|
|
12541
|
-
|
|
12542
|
-
|
|
12543
|
-
|
|
12544
|
-
|
|
12545
|
-
|
|
12546
|
-
|
|
12547
|
-
|
|
12548
|
-
|
|
12549
|
-
|
|
12550
|
-
|
|
12551
|
-
|
|
12552
|
-
|
|
12553
|
-
|
|
12554
|
-
|
|
12471
|
+
var plugin = async (ctx) => {
|
|
12472
|
+
console.log("[opencode-browser] Plugin loading...", { pid: process.pid });
|
|
12473
|
+
return {
|
|
12474
|
+
tool: {
|
|
12475
|
+
browser_debug: tool({
|
|
12476
|
+
description: "Debug plugin loading and connection status.",
|
|
12477
|
+
args: {},
|
|
12478
|
+
async execute(args, ctx2) {
|
|
12479
|
+
console.log("[opencode-browser] browser_debug called", { sessionId, pid: process.pid });
|
|
12480
|
+
return JSON.stringify({
|
|
12481
|
+
loaded: true,
|
|
12482
|
+
sessionId,
|
|
12483
|
+
pid: process.pid,
|
|
12484
|
+
pluginVersion: getPackageVersion(),
|
|
12485
|
+
timestamp: new Date().toISOString()
|
|
12486
|
+
});
|
|
12487
|
+
}
|
|
12488
|
+
}),
|
|
12489
|
+
browser_version: tool({
|
|
12490
|
+
description: "Return the installed @different-ai/opencode-browser plugin version.",
|
|
12491
|
+
args: {},
|
|
12492
|
+
async execute(args, ctx2) {
|
|
12493
|
+
return JSON.stringify({
|
|
12494
|
+
name: "@different-ai/opencode-browser",
|
|
12495
|
+
version: getPackageVersion(),
|
|
12496
|
+
sessionId,
|
|
12497
|
+
pid: process.pid
|
|
12498
|
+
});
|
|
12499
|
+
}
|
|
12500
|
+
}),
|
|
12501
|
+
browser_status: tool({
|
|
12502
|
+
description: "Check broker/native-host connection status and current tab claims.",
|
|
12503
|
+
args: {},
|
|
12504
|
+
async execute(args, ctx2) {
|
|
12505
|
+
const data = await brokerRequest("status", {});
|
|
12506
|
+
return JSON.stringify(data);
|
|
12507
|
+
}
|
|
12508
|
+
}),
|
|
12509
|
+
browser_get_tabs: tool({
|
|
12510
|
+
description: "List all open browser tabs",
|
|
12511
|
+
args: {},
|
|
12512
|
+
async execute(args, ctx2) {
|
|
12513
|
+
const data = await brokerRequest("tool", { tool: "get_tabs", args: {} });
|
|
12514
|
+
return toolResultText(data, "ok");
|
|
12515
|
+
}
|
|
12516
|
+
}),
|
|
12517
|
+
browser_navigate: tool({
|
|
12518
|
+
description: "Navigate to a URL in the browser",
|
|
12519
|
+
args: {
|
|
12520
|
+
url: schema.string(),
|
|
12521
|
+
tabId: schema.number().optional()
|
|
12522
|
+
},
|
|
12523
|
+
async execute({ url: url2, tabId }, ctx2) {
|
|
12524
|
+
const data = await brokerRequest("tool", { tool: "navigate", args: { url: url2, tabId } });
|
|
12525
|
+
return toolResultText(data, `Navigated to ${url2}`);
|
|
12526
|
+
}
|
|
12527
|
+
}),
|
|
12528
|
+
browser_click: tool({
|
|
12529
|
+
description: "Click an element on the page using a CSS selector",
|
|
12530
|
+
args: {
|
|
12531
|
+
selector: schema.string(),
|
|
12532
|
+
index: schema.number().optional(),
|
|
12533
|
+
tabId: schema.number().optional()
|
|
12534
|
+
},
|
|
12535
|
+
async execute({ selector, index, tabId }, ctx2) {
|
|
12536
|
+
const data = await brokerRequest("tool", { tool: "click", args: { selector, index, tabId } });
|
|
12537
|
+
return toolResultText(data, `Clicked ${selector}`);
|
|
12538
|
+
}
|
|
12539
|
+
}),
|
|
12540
|
+
browser_type: tool({
|
|
12541
|
+
description: "Type text into an input element",
|
|
12542
|
+
args: {
|
|
12543
|
+
selector: schema.string(),
|
|
12544
|
+
text: schema.string(),
|
|
12545
|
+
clear: schema.boolean().optional(),
|
|
12546
|
+
index: schema.number().optional(),
|
|
12547
|
+
tabId: schema.number().optional()
|
|
12548
|
+
},
|
|
12549
|
+
async execute({ selector, text, clear, index, tabId }, ctx2) {
|
|
12550
|
+
const data = await brokerRequest("tool", { tool: "type", args: { selector, text, clear, index, tabId } });
|
|
12551
|
+
return toolResultText(data, `Typed "${text}" into ${selector}`);
|
|
12552
|
+
}
|
|
12553
|
+
}),
|
|
12554
|
+
browser_screenshot: tool({
|
|
12555
|
+
description: "Take a screenshot of the current page. Returns base64 image data URL.",
|
|
12556
|
+
args: {
|
|
12557
|
+
tabId: schema.number().optional()
|
|
12558
|
+
},
|
|
12559
|
+
async execute({ tabId }, ctx2) {
|
|
12560
|
+
const data = await brokerRequest("tool", { tool: "screenshot", args: { tabId } });
|
|
12561
|
+
return toolResultText(data, "Screenshot failed");
|
|
12562
|
+
}
|
|
12563
|
+
}),
|
|
12564
|
+
browser_snapshot: tool({
|
|
12565
|
+
description: "Get an accessibility tree snapshot of the page.",
|
|
12566
|
+
args: {
|
|
12567
|
+
tabId: schema.number().optional()
|
|
12568
|
+
},
|
|
12569
|
+
async execute({ tabId }, ctx2) {
|
|
12570
|
+
const data = await brokerRequest("tool", { tool: "snapshot", args: { tabId } });
|
|
12571
|
+
return toolResultText(data, "Snapshot failed");
|
|
12572
|
+
}
|
|
12573
|
+
}),
|
|
12574
|
+
browser_scroll: tool({
|
|
12575
|
+
description: "Scroll the page or scroll an element into view",
|
|
12576
|
+
args: {
|
|
12577
|
+
selector: schema.string().optional(),
|
|
12578
|
+
x: schema.number().optional(),
|
|
12579
|
+
y: schema.number().optional(),
|
|
12580
|
+
tabId: schema.number().optional()
|
|
12581
|
+
},
|
|
12582
|
+
async execute({ selector, x, y, tabId }, ctx2) {
|
|
12583
|
+
const data = await brokerRequest("tool", { tool: "scroll", args: { selector, x, y, tabId } });
|
|
12584
|
+
return toolResultText(data, "Scrolled");
|
|
12585
|
+
}
|
|
12586
|
+
}),
|
|
12587
|
+
browser_wait: tool({
|
|
12588
|
+
description: "Wait for a specified duration",
|
|
12589
|
+
args: {
|
|
12590
|
+
ms: schema.number().optional(),
|
|
12591
|
+
tabId: schema.number().optional()
|
|
12592
|
+
},
|
|
12593
|
+
async execute({ ms, tabId }, ctx2) {
|
|
12594
|
+
const data = await brokerRequest("tool", { tool: "wait", args: { ms, tabId } });
|
|
12595
|
+
return toolResultText(data, "Waited");
|
|
12596
|
+
}
|
|
12597
|
+
}),
|
|
12598
|
+
browser_execute: tool({
|
|
12599
|
+
description: "(Deprecated) Execute arbitrary JavaScript in-page. Blocked on many sites by MV3 CSP/unsafe-eval. This tool now accepts JSON commands; prefer browser_query/browser_extract/browser_wait_for.",
|
|
12600
|
+
args: {
|
|
12601
|
+
code: schema.string(),
|
|
12602
|
+
tabId: schema.number().optional()
|
|
12603
|
+
},
|
|
12604
|
+
async execute({ code, tabId }, ctx2) {
|
|
12605
|
+
const data = await brokerRequest("tool", { tool: "execute_script", args: { code, tabId } });
|
|
12606
|
+
return toolResultText(data, "Execute failed");
|
|
12607
|
+
}
|
|
12608
|
+
}),
|
|
12609
|
+
browser_query: tool({
|
|
12610
|
+
description: "Read data from the page using selectors (supports shadow DOM + same-origin iframes).",
|
|
12611
|
+
args: {
|
|
12612
|
+
selector: schema.string(),
|
|
12613
|
+
mode: schema.string().optional(),
|
|
12614
|
+
attribute: schema.string().optional(),
|
|
12615
|
+
property: schema.string().optional(),
|
|
12616
|
+
index: schema.number().optional(),
|
|
12617
|
+
limit: schema.number().optional(),
|
|
12618
|
+
tabId: schema.number().optional()
|
|
12619
|
+
},
|
|
12620
|
+
async execute({ selector, mode, attribute, property, index, limit, tabId }, ctx2) {
|
|
12621
|
+
const data = await brokerRequest("tool", {
|
|
12622
|
+
tool: "query",
|
|
12623
|
+
args: { selector, mode, attribute, property, index, limit, tabId }
|
|
12624
|
+
});
|
|
12625
|
+
return toolResultText(data, "Query failed");
|
|
12626
|
+
}
|
|
12627
|
+
}),
|
|
12628
|
+
browser_wait_for: tool({
|
|
12629
|
+
description: "Wait until a selector appears (supports shadow DOM + same-origin iframes).",
|
|
12630
|
+
args: {
|
|
12631
|
+
selector: schema.string(),
|
|
12632
|
+
timeoutMs: schema.number().optional(),
|
|
12633
|
+
pollMs: schema.number().optional(),
|
|
12634
|
+
tabId: schema.number().optional()
|
|
12635
|
+
},
|
|
12636
|
+
async execute({ selector, timeoutMs, pollMs, tabId }, ctx2) {
|
|
12637
|
+
const data = await brokerRequest("tool", {
|
|
12638
|
+
tool: "wait_for",
|
|
12639
|
+
args: { selector, timeoutMs, pollMs, tabId }
|
|
12640
|
+
});
|
|
12641
|
+
return toolResultText(data, "Wait-for failed");
|
|
12642
|
+
}
|
|
12643
|
+
}),
|
|
12644
|
+
browser_extract: tool({
|
|
12645
|
+
description: "Extract readable text from the page (optionally regex match). Useful when content isn't in the accessibility tree.",
|
|
12646
|
+
args: {
|
|
12647
|
+
mode: schema.string().optional(),
|
|
12648
|
+
pattern: schema.string().optional(),
|
|
12649
|
+
flags: schema.string().optional(),
|
|
12650
|
+
limit: schema.number().optional(),
|
|
12651
|
+
tabId: schema.number().optional()
|
|
12652
|
+
},
|
|
12653
|
+
async execute({ mode, pattern, flags, limit, tabId }, ctx2) {
|
|
12654
|
+
const data = await brokerRequest("tool", {
|
|
12655
|
+
tool: "extract",
|
|
12656
|
+
args: { mode, pattern, flags, limit, tabId }
|
|
12657
|
+
});
|
|
12658
|
+
return toolResultText(data, "Extract failed");
|
|
12659
|
+
}
|
|
12660
|
+
}),
|
|
12661
|
+
browser_claim_tab: tool({
|
|
12662
|
+
description: "Claim a tab for this OpenCode session (per-tab ownership).",
|
|
12663
|
+
args: {
|
|
12664
|
+
tabId: schema.number(),
|
|
12665
|
+
force: schema.boolean().optional()
|
|
12666
|
+
},
|
|
12667
|
+
async execute({ tabId, force }, ctx2) {
|
|
12668
|
+
const data = await brokerRequest("claim_tab", { tabId, force });
|
|
12669
|
+
return JSON.stringify(data);
|
|
12670
|
+
}
|
|
12671
|
+
}),
|
|
12672
|
+
browser_release_tab: tool({
|
|
12673
|
+
description: "Release a previously claimed tab.",
|
|
12674
|
+
args: {
|
|
12675
|
+
tabId: schema.number()
|
|
12676
|
+
},
|
|
12677
|
+
async execute({ tabId }, ctx2) {
|
|
12678
|
+
const data = await brokerRequest("release_tab", { tabId });
|
|
12679
|
+
return JSON.stringify(data);
|
|
12680
|
+
}
|
|
12681
|
+
}),
|
|
12682
|
+
browser_list_claims: tool({
|
|
12683
|
+
description: "List current tab ownership claims.",
|
|
12684
|
+
args: {},
|
|
12685
|
+
async execute(args, ctx2) {
|
|
12686
|
+
const data = await brokerRequest("list_claims", {});
|
|
12687
|
+
return JSON.stringify(data);
|
|
12688
|
+
}
|
|
12689
|
+
})
|
|
12690
|
+
}
|
|
12691
|
+
};
|
|
12555
12692
|
};
|
|
12556
12693
|
var plugin_default = plugin;
|
|
12557
12694
|
export {
|
package/extension/background.js
CHANGED
|
@@ -15,7 +15,9 @@ chrome.alarms.onAlarm.addListener((alarm) => {
|
|
|
15
15
|
|
|
16
16
|
function connect() {
|
|
17
17
|
if (port) {
|
|
18
|
-
try {
|
|
18
|
+
try {
|
|
19
|
+
port.disconnect()
|
|
20
|
+
} catch {}
|
|
19
21
|
port = null
|
|
20
22
|
}
|
|
21
23
|
|
|
@@ -35,7 +37,6 @@ function connect() {
|
|
|
35
37
|
|
|
36
38
|
const err = chrome.runtime.lastError
|
|
37
39
|
if (err?.message) {
|
|
38
|
-
// Usually means native host not installed or crashed
|
|
39
40
|
connectionAttempts++
|
|
40
41
|
if (connectionAttempts === 1) {
|
|
41
42
|
console.log("[OpenCode] Native host not available. Run: npx @different-ai/opencode-browser install")
|
|
@@ -104,6 +105,9 @@ async function executeTool(toolName, args) {
|
|
|
104
105
|
type: toolType,
|
|
105
106
|
screenshot: toolScreenshot,
|
|
106
107
|
snapshot: toolSnapshot,
|
|
108
|
+
extract: toolExtract,
|
|
109
|
+
query: toolQuery,
|
|
110
|
+
wait_for: toolWaitFor,
|
|
107
111
|
execute_script: toolExecuteScript,
|
|
108
112
|
scroll: toolScroll,
|
|
109
113
|
wait: toolWait,
|
|
@@ -151,52 +155,250 @@ async function toolNavigate({ url, tabId }) {
|
|
|
151
155
|
return { tabId: tab.id, content: `Navigated to ${url}` }
|
|
152
156
|
}
|
|
153
157
|
|
|
154
|
-
|
|
158
|
+
function normalizeSelectorList(selector) {
|
|
159
|
+
if (typeof selector !== "string") return []
|
|
160
|
+
const parts = selector
|
|
161
|
+
.split(",")
|
|
162
|
+
.map((s) => s.trim())
|
|
163
|
+
.filter(Boolean)
|
|
164
|
+
return parts.length ? parts : [selector.trim()].filter(Boolean)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function toolClick({ selector, tabId, index = 0 }) {
|
|
155
168
|
if (!selector) throw new Error("Selector is required")
|
|
156
169
|
const tab = await getTabById(tabId)
|
|
157
170
|
|
|
171
|
+
const selectorList = normalizeSelectorList(selector)
|
|
172
|
+
|
|
158
173
|
const result = await chrome.scripting.executeScript({
|
|
159
174
|
target: { tabId: tab.id },
|
|
160
|
-
func: (
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
175
|
+
func: (selectors, index) => {
|
|
176
|
+
function safeString(v) {
|
|
177
|
+
return typeof v === "string" ? v : ""
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isVisible(el) {
|
|
181
|
+
if (!el) return false
|
|
182
|
+
const rect = el.getBoundingClientRect()
|
|
183
|
+
if (rect.width <= 0 || rect.height <= 0) return false
|
|
184
|
+
const style = window.getComputedStyle(el)
|
|
185
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false
|
|
186
|
+
return true
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function deepQuerySelectorAll(sel, rootDoc) {
|
|
190
|
+
const out = []
|
|
191
|
+
const seen = new Set()
|
|
192
|
+
|
|
193
|
+
function addAll(nodeList) {
|
|
194
|
+
for (const el of nodeList) {
|
|
195
|
+
if (!el || seen.has(el)) continue
|
|
196
|
+
seen.add(el)
|
|
197
|
+
out.push(el)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function walkRoot(root, depth) {
|
|
202
|
+
if (!root || depth > 6) return
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
addAll(root.querySelectorAll(sel))
|
|
206
|
+
} catch {
|
|
207
|
+
// Invalid selector
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const tree = root.querySelectorAll ? root.querySelectorAll("*") : []
|
|
212
|
+
for (const el of tree) {
|
|
213
|
+
if (el.shadowRoot) {
|
|
214
|
+
walkRoot(el.shadowRoot, depth + 1)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Same-origin iframes only
|
|
219
|
+
const frames = root.querySelectorAll ? root.querySelectorAll("iframe") : []
|
|
220
|
+
for (const frame of frames) {
|
|
221
|
+
try {
|
|
222
|
+
const doc = frame.contentDocument
|
|
223
|
+
if (doc) walkRoot(doc, depth + 1)
|
|
224
|
+
} catch {
|
|
225
|
+
// cross-origin
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
walkRoot(rootDoc || document, 0)
|
|
231
|
+
return out
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function tryClick(el) {
|
|
235
|
+
try {
|
|
236
|
+
el.scrollIntoView({ block: "center", inline: "center" })
|
|
237
|
+
} catch {}
|
|
238
|
+
|
|
239
|
+
const rect = el.getBoundingClientRect()
|
|
240
|
+
const x = Math.min(Math.max(rect.left + rect.width / 2, 0), window.innerWidth - 1)
|
|
241
|
+
const y = Math.min(Math.max(rect.top + rect.height / 2, 0), window.innerHeight - 1)
|
|
242
|
+
|
|
243
|
+
const opts = { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y }
|
|
244
|
+
try {
|
|
245
|
+
el.dispatchEvent(new MouseEvent("mouseover", opts))
|
|
246
|
+
el.dispatchEvent(new MouseEvent("mousemove", opts))
|
|
247
|
+
el.dispatchEvent(new MouseEvent("mousedown", opts))
|
|
248
|
+
el.dispatchEvent(new MouseEvent("mouseup", opts))
|
|
249
|
+
el.dispatchEvent(new MouseEvent("click", opts))
|
|
250
|
+
} catch {}
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
el.click()
|
|
254
|
+
} catch {}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
for (const sel of selectors) {
|
|
258
|
+
const s = safeString(sel)
|
|
259
|
+
if (!s) continue
|
|
260
|
+
|
|
261
|
+
const matches = deepQuerySelectorAll(s, document)
|
|
262
|
+
const visible = matches.filter(isVisible)
|
|
263
|
+
const chosen = visible[index] || matches[index]
|
|
264
|
+
if (chosen) {
|
|
265
|
+
tryClick(chosen)
|
|
266
|
+
return { success: true, selectorUsed: s }
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return { success: false, error: `Element not found for selectors: ${selectors.join(", ")}` }
|
|
165
271
|
},
|
|
166
|
-
args: [
|
|
272
|
+
args: [selectorList, index],
|
|
273
|
+
world: "ISOLATED",
|
|
167
274
|
})
|
|
168
275
|
|
|
169
276
|
if (!result[0]?.result?.success) throw new Error(result[0]?.result?.error || "Click failed")
|
|
170
|
-
|
|
277
|
+
const used = result[0]?.result?.selectorUsed || selector
|
|
278
|
+
return { tabId: tab.id, content: `Clicked ${used}` }
|
|
171
279
|
}
|
|
172
280
|
|
|
173
|
-
async function toolType({ selector, text, tabId, clear = false }) {
|
|
281
|
+
async function toolType({ selector, text, tabId, clear = false, index = 0 }) {
|
|
174
282
|
if (!selector) throw new Error("Selector is required")
|
|
175
283
|
if (text === undefined) throw new Error("Text is required")
|
|
176
284
|
const tab = await getTabById(tabId)
|
|
177
285
|
|
|
286
|
+
const selectorList = normalizeSelectorList(selector)
|
|
287
|
+
|
|
178
288
|
const result = await chrome.scripting.executeScript({
|
|
179
289
|
target: { tabId: tab.id },
|
|
180
|
-
func: (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
290
|
+
func: (selectors, txt, shouldClear, index) => {
|
|
291
|
+
function isVisible(el) {
|
|
292
|
+
if (!el) return false
|
|
293
|
+
const rect = el.getBoundingClientRect()
|
|
294
|
+
if (rect.width <= 0 || rect.height <= 0) return false
|
|
295
|
+
const style = window.getComputedStyle(el)
|
|
296
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false
|
|
297
|
+
return true
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function deepQuerySelectorAll(sel, rootDoc) {
|
|
301
|
+
const out = []
|
|
302
|
+
const seen = new Set()
|
|
303
|
+
|
|
304
|
+
function addAll(nodeList) {
|
|
305
|
+
for (const el of nodeList) {
|
|
306
|
+
if (!el || seen.has(el)) continue
|
|
307
|
+
seen.add(el)
|
|
308
|
+
out.push(el)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function walkRoot(root, depth) {
|
|
313
|
+
if (!root || depth > 6) return
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
addAll(root.querySelectorAll(sel))
|
|
317
|
+
} catch {
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const tree = root.querySelectorAll ? root.querySelectorAll("*") : []
|
|
322
|
+
for (const el of tree) {
|
|
323
|
+
if (el.shadowRoot) {
|
|
324
|
+
walkRoot(el.shadowRoot, depth + 1)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const frames = root.querySelectorAll ? root.querySelectorAll("iframe") : []
|
|
329
|
+
for (const frame of frames) {
|
|
330
|
+
try {
|
|
331
|
+
const doc = frame.contentDocument
|
|
332
|
+
if (doc) walkRoot(doc, depth + 1)
|
|
333
|
+
} catch {}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
walkRoot(rootDoc || document, 0)
|
|
338
|
+
return out
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function setNativeValue(el, value) {
|
|
342
|
+
const tag = el.tagName
|
|
343
|
+
if (tag === "INPUT" || tag === "TEXTAREA") {
|
|
344
|
+
const proto = tag === "INPUT" ? window.HTMLInputElement.prototype : window.HTMLTextAreaElement.prototype
|
|
345
|
+
const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set
|
|
346
|
+
if (setter) setter.call(el, value)
|
|
347
|
+
else el.value = value
|
|
348
|
+
return true
|
|
349
|
+
}
|
|
350
|
+
return false
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
for (const sel of selectors) {
|
|
354
|
+
if (!sel) continue
|
|
355
|
+
const matches = deepQuerySelectorAll(sel, document)
|
|
356
|
+
const visible = matches.filter(isVisible)
|
|
357
|
+
const el = visible[index] || matches[index]
|
|
358
|
+
if (!el) continue
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
el.scrollIntoView({ block: "center", inline: "center" })
|
|
362
|
+
} catch {}
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
el.focus()
|
|
366
|
+
} catch {}
|
|
367
|
+
|
|
368
|
+
const tag = el.tagName
|
|
369
|
+
const isTextInput = tag === "INPUT" || tag === "TEXTAREA"
|
|
370
|
+
|
|
371
|
+
if (isTextInput) {
|
|
372
|
+
if (shouldClear) setNativeValue(el, "")
|
|
373
|
+
setNativeValue(el, (el.value || "") + txt)
|
|
374
|
+
el.dispatchEvent(new Event("input", { bubbles: true }))
|
|
375
|
+
el.dispatchEvent(new Event("change", { bubbles: true }))
|
|
376
|
+
return { success: true, selectorUsed: sel }
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (el.isContentEditable) {
|
|
380
|
+
if (shouldClear) el.textContent = ""
|
|
381
|
+
try {
|
|
382
|
+
document.execCommand("insertText", false, txt)
|
|
383
|
+
} catch {
|
|
384
|
+
el.textContent = (el.textContent || "") + txt
|
|
385
|
+
}
|
|
386
|
+
el.dispatchEvent(new Event("input", { bubbles: true }))
|
|
387
|
+
return { success: true, selectorUsed: sel }
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return { success: false, error: `Element is not typable: ${sel} (${tag.toLowerCase()})` }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return { success: false, error: `Element not found for selectors: ${selectors.join(", ")}` }
|
|
194
394
|
},
|
|
195
|
-
args: [
|
|
395
|
+
args: [selectorList, text, !!clear, index],
|
|
396
|
+
world: "ISOLATED",
|
|
196
397
|
})
|
|
197
398
|
|
|
198
399
|
if (!result[0]?.result?.success) throw new Error(result[0]?.result?.error || "Type failed")
|
|
199
|
-
|
|
400
|
+
const used = result[0]?.result?.selectorUsed || selector
|
|
401
|
+
return { tabId: tab.id, content: `Typed "${text}" into ${used}` }
|
|
200
402
|
}
|
|
201
403
|
|
|
202
404
|
async function toolScreenshot({ tabId }) {
|
|
@@ -211,56 +413,109 @@ async function toolSnapshot({ tabId }) {
|
|
|
211
413
|
const result = await chrome.scripting.executeScript({
|
|
212
414
|
target: { tabId: tab.id },
|
|
213
415
|
func: () => {
|
|
416
|
+
function safeText(s) {
|
|
417
|
+
return typeof s === "string" ? s : ""
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function isVisible(el) {
|
|
421
|
+
if (!el) return false
|
|
422
|
+
const rect = el.getBoundingClientRect()
|
|
423
|
+
if (rect.width <= 0 || rect.height <= 0) return false
|
|
424
|
+
const style = window.getComputedStyle(el)
|
|
425
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false
|
|
426
|
+
return true
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function pseudoText(el) {
|
|
430
|
+
try {
|
|
431
|
+
const before = window.getComputedStyle(el, "::before").content
|
|
432
|
+
const after = window.getComputedStyle(el, "::after").content
|
|
433
|
+
const norm = (v) => {
|
|
434
|
+
const s = safeText(v)
|
|
435
|
+
if (!s || s === "none") return ""
|
|
436
|
+
return s.replace(/^"|"$/g, "")
|
|
437
|
+
}
|
|
438
|
+
return { before: norm(before), after: norm(after) }
|
|
439
|
+
} catch {
|
|
440
|
+
return { before: "", after: "" }
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
214
444
|
function getName(el) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
)
|
|
445
|
+
const aria = el.getAttribute("aria-label")
|
|
446
|
+
if (aria) return aria
|
|
447
|
+
const alt = el.getAttribute("alt")
|
|
448
|
+
if (alt) return alt
|
|
449
|
+
const title = el.getAttribute("title")
|
|
450
|
+
if (title) return title
|
|
451
|
+
const placeholder = el.getAttribute("placeholder")
|
|
452
|
+
if (placeholder) return placeholder
|
|
453
|
+
const txt = safeText(el.innerText)
|
|
454
|
+
if (txt.trim()) return txt.slice(0, 200)
|
|
455
|
+
const pt = pseudoText(el)
|
|
456
|
+
const combo = `${pt.before} ${pt.after}`.trim()
|
|
457
|
+
if (combo) return combo.slice(0, 200)
|
|
458
|
+
return ""
|
|
223
459
|
}
|
|
224
460
|
|
|
225
461
|
function build(el, depth = 0, uid = 0) {
|
|
226
|
-
if (depth >
|
|
462
|
+
if (!el || depth > 12) return { nodes: [], nextUid: uid }
|
|
227
463
|
const nodes = []
|
|
228
|
-
|
|
229
|
-
if (
|
|
464
|
+
|
|
465
|
+
if (!isVisible(el)) return { nodes: [], nextUid: uid }
|
|
230
466
|
|
|
231
467
|
const isInteractive =
|
|
232
468
|
["A", "BUTTON", "INPUT", "TEXTAREA", "SELECT"].includes(el.tagName) ||
|
|
233
469
|
el.getAttribute("onclick") ||
|
|
234
470
|
el.getAttribute("role") === "button" ||
|
|
235
471
|
el.isContentEditable
|
|
236
|
-
const rect = el.getBoundingClientRect()
|
|
237
472
|
|
|
238
|
-
|
|
473
|
+
const name = getName(el)
|
|
474
|
+
const pt = pseudoText(el)
|
|
475
|
+
|
|
476
|
+
const shouldInclude = isInteractive || name.trim() || pt.before || pt.after
|
|
477
|
+
|
|
478
|
+
if (shouldInclude) {
|
|
239
479
|
const node = {
|
|
240
480
|
uid: `e${uid}`,
|
|
241
481
|
role: el.getAttribute("role") || el.tagName.toLowerCase(),
|
|
242
|
-
name:
|
|
482
|
+
name: name,
|
|
243
483
|
tag: el.tagName.toLowerCase(),
|
|
244
484
|
}
|
|
485
|
+
|
|
486
|
+
if (pt.before) node.before = pt.before
|
|
487
|
+
if (pt.after) node.after = pt.after
|
|
488
|
+
|
|
245
489
|
if (el.href) node.href = el.href
|
|
246
|
-
|
|
490
|
+
|
|
491
|
+
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
|
247
492
|
node.type = el.type
|
|
248
493
|
node.value = el.value
|
|
494
|
+
if (el.readOnly) node.readOnly = true
|
|
495
|
+
if (el.disabled) node.disabled = true
|
|
249
496
|
}
|
|
497
|
+
|
|
250
498
|
if (el.id) node.selector = `#${el.id}`
|
|
251
499
|
else if (el.className && typeof el.className === "string") {
|
|
252
500
|
const cls = el.className.trim().split(/\s+/).slice(0, 2).join(".")
|
|
253
501
|
if (cls) node.selector = `${el.tagName.toLowerCase()}.${cls}`
|
|
254
502
|
}
|
|
503
|
+
|
|
255
504
|
nodes.push(node)
|
|
256
505
|
uid++
|
|
257
506
|
}
|
|
258
507
|
|
|
508
|
+
if (el.shadowRoot) {
|
|
509
|
+
const r = build(el.shadowRoot.host, depth + 1, uid)
|
|
510
|
+
uid = r.nextUid
|
|
511
|
+
}
|
|
512
|
+
|
|
259
513
|
for (const child of el.children) {
|
|
260
514
|
const r = build(child, depth + 1, uid)
|
|
261
515
|
nodes.push(...r.nodes)
|
|
262
516
|
uid = r.nextUid
|
|
263
517
|
}
|
|
518
|
+
|
|
264
519
|
return { nodes, nextUid: uid }
|
|
265
520
|
}
|
|
266
521
|
|
|
@@ -275,16 +530,114 @@ async function toolSnapshot({ tabId }) {
|
|
|
275
530
|
links.push({ href, text })
|
|
276
531
|
}
|
|
277
532
|
})
|
|
278
|
-
return links.slice(0,
|
|
533
|
+
return links.slice(0, 200)
|
|
279
534
|
}
|
|
280
535
|
|
|
536
|
+
let pageText = ""
|
|
537
|
+
try {
|
|
538
|
+
pageText = safeText(document.body?.innerText || "").slice(0, 20000)
|
|
539
|
+
} catch {}
|
|
540
|
+
|
|
541
|
+
const built = build(document.body).nodes.slice(0, 800)
|
|
542
|
+
|
|
281
543
|
return {
|
|
282
544
|
url: location.href,
|
|
283
545
|
title: document.title,
|
|
284
|
-
|
|
546
|
+
text: pageText,
|
|
547
|
+
nodes: built,
|
|
285
548
|
links: getAllLinks(),
|
|
286
549
|
}
|
|
287
550
|
},
|
|
551
|
+
world: "ISOLATED",
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
return { tabId: tab.id, content: JSON.stringify(result[0]?.result, null, 2) }
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async function toolExtract({ tabId, mode = "combined", pattern, flags = "i", limit = 20000 }) {
|
|
558
|
+
const tab = await getTabById(tabId)
|
|
559
|
+
|
|
560
|
+
const result = await chrome.scripting.executeScript({
|
|
561
|
+
target: { tabId: tab.id },
|
|
562
|
+
func: (mode, pattern, flags, limit) => {
|
|
563
|
+
const cap = (s) => String(s ?? "").slice(0, Math.max(0, limit || 0))
|
|
564
|
+
|
|
565
|
+
const getPseudoText = () => {
|
|
566
|
+
const out = []
|
|
567
|
+
const pushContent = (content) => {
|
|
568
|
+
if (!content) return
|
|
569
|
+
const c = String(content)
|
|
570
|
+
if (!c || c === "none" || c === "normal") return
|
|
571
|
+
const unquoted = c.replace(/^"|"$/g, "").replace(/^'|'$/g, "")
|
|
572
|
+
if (unquoted && unquoted !== "none" && unquoted !== "normal") out.push(unquoted)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const elements = Array.from(document.querySelectorAll("*"))
|
|
576
|
+
for (let i = 0; i < elements.length && out.length < 2000; i++) {
|
|
577
|
+
const el = elements[i]
|
|
578
|
+
try {
|
|
579
|
+
const style = window.getComputedStyle(el)
|
|
580
|
+
if (style.display === "none" || style.visibility === "hidden") continue
|
|
581
|
+
const before = window.getComputedStyle(el, "::before").content
|
|
582
|
+
const after = window.getComputedStyle(el, "::after").content
|
|
583
|
+
pushContent(before)
|
|
584
|
+
pushContent(after)
|
|
585
|
+
} catch {
|
|
586
|
+
// ignore
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return out.join("\n")
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const getInputValues = () => {
|
|
593
|
+
const out = []
|
|
594
|
+
const nodes = document.querySelectorAll("input, textarea")
|
|
595
|
+
nodes.forEach((el) => {
|
|
596
|
+
try {
|
|
597
|
+
const name = el.getAttribute("aria-label") || el.getAttribute("name") || el.id || el.className || el.tagName
|
|
598
|
+
const value = el.value
|
|
599
|
+
if (value != null && String(value).trim()) out.push(`${name}: ${value}`)
|
|
600
|
+
} catch {
|
|
601
|
+
// ignore
|
|
602
|
+
}
|
|
603
|
+
})
|
|
604
|
+
return out.join("\n")
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const getText = () => {
|
|
608
|
+
try {
|
|
609
|
+
return document.body ? document.body.innerText || "" : ""
|
|
610
|
+
} catch {
|
|
611
|
+
return ""
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const parts = []
|
|
616
|
+
if (mode === "text" || mode === "combined") parts.push(getText())
|
|
617
|
+
if (mode === "pseudo" || mode === "combined") parts.push(getPseudoText())
|
|
618
|
+
if (mode === "inputs" || mode === "combined") parts.push(getInputValues())
|
|
619
|
+
|
|
620
|
+
const text = cap(parts.filter(Boolean).join("\n\n"))
|
|
621
|
+
|
|
622
|
+
let matches = []
|
|
623
|
+
if (pattern) {
|
|
624
|
+
try {
|
|
625
|
+
const re = new RegExp(pattern, flags || "")
|
|
626
|
+
const found = []
|
|
627
|
+
let m
|
|
628
|
+
while ((m = re.exec(text)) && found.length < 50) {
|
|
629
|
+
found.push(m[0])
|
|
630
|
+
if (!re.global) break
|
|
631
|
+
}
|
|
632
|
+
matches = found
|
|
633
|
+
} catch (e) {
|
|
634
|
+
matches = []
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return { url: location.href, title: document.title, mode, text, matches }
|
|
639
|
+
},
|
|
640
|
+
args: [mode, pattern, flags, limit],
|
|
288
641
|
})
|
|
289
642
|
|
|
290
643
|
return { tabId: tab.id, content: JSON.stringify(result[0]?.result, null, 2) }
|
|
@@ -296,13 +649,258 @@ async function toolGetTabs() {
|
|
|
296
649
|
return { content: JSON.stringify(out, null, 2) }
|
|
297
650
|
}
|
|
298
651
|
|
|
652
|
+
async function toolQuery({ tabId, selector, mode = "text", attribute, property, limit = 50, index = 0 }) {
|
|
653
|
+
if (!selector) throw new Error("selector is required")
|
|
654
|
+
const tab = await getTabById(tabId)
|
|
655
|
+
|
|
656
|
+
const selectorList = normalizeSelectorList(selector)
|
|
657
|
+
|
|
658
|
+
const result = await chrome.scripting.executeScript({
|
|
659
|
+
target: { tabId: tab.id },
|
|
660
|
+
func: (selectors, mode, attribute, property, limit, index) => {
|
|
661
|
+
function isVisible(el) {
|
|
662
|
+
if (!el) return false
|
|
663
|
+
const rect = el.getBoundingClientRect()
|
|
664
|
+
if (rect.width <= 0 || rect.height <= 0) return false
|
|
665
|
+
const style = window.getComputedStyle(el)
|
|
666
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false
|
|
667
|
+
return true
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function deepQuerySelectorAll(sel, rootDoc) {
|
|
671
|
+
const out = []
|
|
672
|
+
const seen = new Set()
|
|
673
|
+
|
|
674
|
+
function addAll(nodeList) {
|
|
675
|
+
for (const el of nodeList) {
|
|
676
|
+
if (!el || seen.has(el)) continue
|
|
677
|
+
seen.add(el)
|
|
678
|
+
out.push(el)
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function walkRoot(root, depth) {
|
|
683
|
+
if (!root || depth > 6) return
|
|
684
|
+
|
|
685
|
+
try {
|
|
686
|
+
addAll(root.querySelectorAll(sel))
|
|
687
|
+
} catch {
|
|
688
|
+
return
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const tree = root.querySelectorAll ? root.querySelectorAll("*") : []
|
|
692
|
+
for (const el of tree) {
|
|
693
|
+
if (el.shadowRoot) {
|
|
694
|
+
walkRoot(el.shadowRoot, depth + 1)
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const frames = root.querySelectorAll ? root.querySelectorAll("iframe") : []
|
|
699
|
+
for (const frame of frames) {
|
|
700
|
+
try {
|
|
701
|
+
const doc = frame.contentDocument
|
|
702
|
+
if (doc) walkRoot(doc, depth + 1)
|
|
703
|
+
} catch {}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
walkRoot(rootDoc || document, 0)
|
|
708
|
+
return out
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
for (const sel of selectors) {
|
|
712
|
+
const matches = deepQuerySelectorAll(sel, document)
|
|
713
|
+
if (!matches.length) continue
|
|
714
|
+
|
|
715
|
+
const visible = matches.filter(isVisible)
|
|
716
|
+
const chosen = visible[index] || matches[index]
|
|
717
|
+
|
|
718
|
+
if (mode === "exists") {
|
|
719
|
+
return { ok: true, selectorUsed: sel, exists: true, count: matches.length }
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (!chosen) return { ok: false, error: `No element at index ${index} for ${sel}`, selectorUsed: sel }
|
|
723
|
+
|
|
724
|
+
if (mode === "text") {
|
|
725
|
+
const text = (chosen.innerText || chosen.textContent || "").trim()
|
|
726
|
+
return { ok: true, selectorUsed: sel, value: text }
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (mode === "value") {
|
|
730
|
+
const v = chosen.value
|
|
731
|
+
return { ok: true, selectorUsed: sel, value: typeof v === "string" ? v : String(v ?? "") }
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (mode === "attribute") {
|
|
735
|
+
const a = attribute ? chosen.getAttribute(attribute) : null
|
|
736
|
+
return { ok: true, selectorUsed: sel, value: a }
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (mode === "property") {
|
|
740
|
+
if (!property) return { ok: false, error: "property is required", selectorUsed: sel }
|
|
741
|
+
const v = chosen[property]
|
|
742
|
+
return { ok: true, selectorUsed: sel, value: v }
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (mode === "html") {
|
|
746
|
+
return { ok: true, selectorUsed: sel, value: chosen.outerHTML }
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (mode === "list") {
|
|
750
|
+
const items = matches
|
|
751
|
+
.slice(0, Math.max(1, Math.min(200, limit)))
|
|
752
|
+
.map((el) => ({
|
|
753
|
+
text: (el.innerText || el.textContent || "").trim().slice(0, 200),
|
|
754
|
+
tag: (el.tagName || "").toLowerCase(),
|
|
755
|
+
ariaLabel: el.getAttribute ? el.getAttribute("aria-label") : null,
|
|
756
|
+
}))
|
|
757
|
+
return { ok: true, selectorUsed: sel, items, count: matches.length }
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return { ok: false, error: `Unknown mode: ${mode}`, selectorUsed: sel }
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
return { ok: false, error: `No matches for selectors: ${selectors.join(", ")}` }
|
|
764
|
+
},
|
|
765
|
+
args: [selectorList, mode, attribute || null, property || null, limit, index],
|
|
766
|
+
world: "ISOLATED",
|
|
767
|
+
})
|
|
768
|
+
|
|
769
|
+
const r = result[0]?.result
|
|
770
|
+
if (!r?.ok) throw new Error(r?.error || "Query failed")
|
|
771
|
+
|
|
772
|
+
// Keep output predictable: JSON for list/property, string otherwise
|
|
773
|
+
if (mode === "list" || mode === "property") {
|
|
774
|
+
return { tabId: tab.id, content: JSON.stringify(r, null, 2) }
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
return { tabId: tab.id, content: typeof r.value === "string" ? r.value : JSON.stringify(r.value) }
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
async function toolWaitFor({ tabId, selector, timeoutMs = 10000, pollMs = 200 }) {
|
|
781
|
+
if (!selector) throw new Error("selector is required")
|
|
782
|
+
const tab = await getTabById(tabId)
|
|
783
|
+
|
|
784
|
+
const selectorList = normalizeSelectorList(selector)
|
|
785
|
+
|
|
786
|
+
const result = await chrome.scripting.executeScript({
|
|
787
|
+
target: { tabId: tab.id },
|
|
788
|
+
func: async (selectors, timeoutMs, pollMs) => {
|
|
789
|
+
function isVisible(el) {
|
|
790
|
+
if (!el) return false
|
|
791
|
+
const rect = el.getBoundingClientRect()
|
|
792
|
+
if (rect.width <= 0 || rect.height <= 0) return false
|
|
793
|
+
const style = window.getComputedStyle(el)
|
|
794
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false
|
|
795
|
+
return true
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function deepQuerySelector(sel, rootDoc) {
|
|
799
|
+
function findInRoot(root, depth) {
|
|
800
|
+
if (!root || depth > 6) return null
|
|
801
|
+
try {
|
|
802
|
+
const found = root.querySelector(sel)
|
|
803
|
+
if (found) return found
|
|
804
|
+
} catch {
|
|
805
|
+
return null
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const tree = root.querySelectorAll ? root.querySelectorAll("*") : []
|
|
809
|
+
for (const el of tree) {
|
|
810
|
+
if (el.shadowRoot) {
|
|
811
|
+
const f = findInRoot(el.shadowRoot, depth + 1)
|
|
812
|
+
if (f) return f
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const frames = root.querySelectorAll ? root.querySelectorAll("iframe") : []
|
|
817
|
+
for (const frame of frames) {
|
|
818
|
+
try {
|
|
819
|
+
const doc = frame.contentDocument
|
|
820
|
+
if (doc) {
|
|
821
|
+
const f = findInRoot(doc, depth + 1)
|
|
822
|
+
if (f) return f
|
|
823
|
+
}
|
|
824
|
+
} catch {}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return null
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
return findInRoot(rootDoc || document, 0)
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const start = Date.now()
|
|
834
|
+
while (Date.now() - start < timeoutMs) {
|
|
835
|
+
for (const sel of selectors) {
|
|
836
|
+
if (!sel) continue
|
|
837
|
+
const el = deepQuerySelector(sel, document)
|
|
838
|
+
if (el && isVisible(el)) return { ok: true, selectorUsed: sel }
|
|
839
|
+
}
|
|
840
|
+
await new Promise((r) => setTimeout(r, pollMs))
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return { ok: false, error: `Timed out waiting for selectors: ${selectors.join(", ")}` }
|
|
844
|
+
},
|
|
845
|
+
args: [selectorList, timeoutMs, pollMs],
|
|
846
|
+
world: "ISOLATED",
|
|
847
|
+
})
|
|
848
|
+
|
|
849
|
+
const r = result[0]?.result
|
|
850
|
+
if (!r?.ok) throw new Error(r?.error || "wait_for failed")
|
|
851
|
+
return { tabId: tab.id, content: `Found ${r.selectorUsed}` }
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Legacy tool kept for compatibility.
|
|
855
|
+
// We intentionally do NOT evaluate arbitrary JS strings (unpredictable + CSP/unsafe-eval issues).
|
|
856
|
+
// Instead, accept a JSON payload string describing a query.
|
|
299
857
|
async function toolExecuteScript({ code, tabId }) {
|
|
300
858
|
if (!code) throw new Error("Code is required")
|
|
859
|
+
|
|
860
|
+
let command
|
|
861
|
+
try {
|
|
862
|
+
command = JSON.parse(code)
|
|
863
|
+
} catch {
|
|
864
|
+
throw new Error(
|
|
865
|
+
"browser_execute expects JSON (not raw JS) due to MV3 CSP. Try: {\"op\":\"query\",\"selector\":\"...\",\"return\":\"text\" } or use browser_extract."
|
|
866
|
+
)
|
|
867
|
+
}
|
|
868
|
+
|
|
301
869
|
const tab = await getTabById(tabId)
|
|
302
870
|
const result = await chrome.scripting.executeScript({
|
|
303
871
|
target: { tabId: tab.id },
|
|
304
|
-
func:
|
|
872
|
+
func: (cmd) => {
|
|
873
|
+
const getBySelector = (selector) => {
|
|
874
|
+
if (!selector) return null
|
|
875
|
+
try {
|
|
876
|
+
return document.querySelector(selector)
|
|
877
|
+
} catch {
|
|
878
|
+
return null
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const op = cmd?.op
|
|
883
|
+
if (op === "query") {
|
|
884
|
+
const el = getBySelector(cmd.selector)
|
|
885
|
+
if (!el) return { ok: false, error: "not_found" }
|
|
886
|
+
const ret = cmd.return || "text"
|
|
887
|
+
if (ret === "text") return { ok: true, value: el.innerText ?? el.textContent ?? "" }
|
|
888
|
+
if (ret === "value") return { ok: true, value: el.value }
|
|
889
|
+
if (ret === "html") return { ok: true, value: el.innerHTML }
|
|
890
|
+
if (ret === "attr") return { ok: true, value: el.getAttribute(cmd.name) }
|
|
891
|
+
if (ret === "href") return { ok: true, value: el.href }
|
|
892
|
+
return { ok: false, error: `unknown_return:${ret}` }
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (op === "location") {
|
|
896
|
+
return { ok: true, value: { url: location.href, title: document.title } }
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
return { ok: false, error: `unknown_op:${String(op)}` }
|
|
900
|
+
},
|
|
901
|
+
args: [command],
|
|
305
902
|
})
|
|
903
|
+
|
|
306
904
|
return { tabId: tab.id, content: JSON.stringify(result[0]?.result) }
|
|
307
905
|
}
|
|
308
906
|
|
|
@@ -323,15 +921,13 @@ async function toolScroll({ x = 0, y = 0, selector, tabId }) {
|
|
|
323
921
|
window.scrollBy(scrollX, scrollY)
|
|
324
922
|
},
|
|
325
923
|
args: [x, y, sel],
|
|
924
|
+
world: "ISOLATED",
|
|
326
925
|
})
|
|
327
926
|
|
|
328
927
|
return { tabId: tab.id, content: `Scrolled ${sel ? `to ${sel}` : `by (${x}, ${y})`}` }
|
|
329
928
|
}
|
|
330
929
|
|
|
331
930
|
async function toolWait({ ms = 1000, tabId }) {
|
|
332
|
-
if (typeof tabId === "number") {
|
|
333
|
-
// keep tabId in response for ownership purposes
|
|
334
|
-
}
|
|
335
931
|
await new Promise((resolve) => setTimeout(resolve, ms))
|
|
336
932
|
return { tabId, content: `Waited ${ms}ms` }
|
|
337
933
|
}
|
|
@@ -348,4 +944,4 @@ chrome.action.onClicked.addListener(() => {
|
|
|
348
944
|
})
|
|
349
945
|
})
|
|
350
946
|
|
|
351
|
-
connect()
|
|
947
|
+
connect()
|
package/extension/manifest.json
CHANGED