diff options
Diffstat (limited to 'extensions')
| -rw-r--r-- | extensions/chrome-profile-bridge/browser-extension/service_worker.js | 89 | ||||
| -rw-r--r-- | extensions/chrome-profile-bridge/index.ts | 11 |
2 files changed, 61 insertions, 39 deletions
diff --git a/extensions/chrome-profile-bridge/browser-extension/service_worker.js b/extensions/chrome-profile-bridge/browser-extension/service_worker.js index d47b385..9ac247c 100644 --- a/extensions/chrome-profile-bridge/browser-extension/service_worker.js +++ b/extensions/chrome-profile-bridge/browser-extension/service_worker.js @@ -614,27 +614,36 @@ async function executeScriptWithFallback(options, label) { if (!isScriptingAccessDenied(error) || !chrome.debugger) throw error; const tabId = options?.target?.tabId; recordAttachEvent({ kind: "scripting-denied-cdp-fallback", tabId, files: options?.files || null, message: String(error?.message || error).slice(0, 300) }); - if (Array.isArray(options?.files) && options.files.length) { - for (const file of options.files) { - if (!packagedFileSources.has(file)) { - const response = await fetch(chrome.runtime.getURL(file)); - if (!response.ok) throw new Error(`Could not read ${file} for CDP injection (HTTP ${response.status})`); - packagedFileSources.set(file, await response.text()); - } - const injected = await cdpEval(tabId, packagedFileSources.get(file)); - if (injected.exceptionDetails) { - throw new Error(`${label || file}: ${cdpExceptionText(injected.exceptionDetails) || "CDP injection failed"}`); - } + return executeScriptViaCdp(options, label); + } +} + +// The CDP half of executeScriptWithFallback, also used directly when scripting silently runs +// nothing: a document served with CSP `sandbox` (no allow-scripts), such as +// raw.githubusercontent.com, makes chrome.scripting.executeScript resolve with a null result +// instead of throwing, while Runtime.evaluate still works there. +async function executeScriptViaCdp(options, label) { + const tabId = options?.target?.tabId; + if (Array.isArray(options?.files) && options.files.length) { + for (const file of options.files) { + if (!packagedFileSources.has(file)) { + const response = await fetch(chrome.runtime.getURL(file)); + if (!response.ok) throw new Error(`Could not read ${file} for CDP injection (HTTP ${response.status})`); + packagedFileSources.set(file, await response.text()); + } + const injected = await cdpEval(tabId, packagedFileSources.get(file)); + if (injected.exceptionDetails) { + throw new Error(`${label || file}: ${cdpExceptionText(injected.exceptionDetails) || "CDP injection failed"}`); } - return [{ result: undefined }]; - } - const expression = `(${options.func.toString()})(...${JSON.stringify(Array.isArray(options.args) ? options.args : [])})`; - const result = await cdpEval(tabId, expression); - if (result.exceptionDetails) { - throw new Error(`${label || "page script"}: ${cdpExceptionText(result.exceptionDetails) || "evaluation failed"}`); } - return [{ result: result.result?.value }]; + return [{ result: undefined }]; + } + const expression = `(${options.func.toString()})(...${JSON.stringify(Array.isArray(options.args) ? options.args : [])})`; + const result = await cdpEval(tabId, expression); + if (result.exceptionDetails) { + throw new Error(`${label || "page script"}: ${cdpExceptionText(result.exceptionDetails) || "evaluation failed"}`); } + return [{ result: result.result?.value }]; } // Wraps cdpRaw with one auto-recover on detached/closed sessions: @@ -2412,6 +2421,28 @@ async function withOptionalSnapshot(params, actionFn) { // chrome.scripting.executeScript({ files }). That file is free of eval/new Function, so it works // on strict-CSP pages, and it installs globalThis.__piChromeSnapshotPage / __piChromeInspectTarget. // It shares window.__PI_CHROME_STATE__ (same el- uid scheme) with the CDP-injected input helpers. +// Injects snapshot_injected.js and runs `scriptOptions.func` (which must return an +// { ok, value | error } envelope) in the tab's top frame. chrome.scripting is primary; if it +// resolves without an envelope, the page ran no extension script (CSP `sandbox` without +// allow-scripts, e.g. raw.githubusercontent.com), so both steps are redone over CDP. +async function runSnapshotInjected(tab, kind, scriptOptions) { + const inject = { target: { tabId: tab.id, frameIds: [0] }, world: "MAIN", files: ["snapshot_injected.js"] }; + const run = { target: { tabId: tab.id, frameIds: [0] }, world: "MAIN", ...scriptOptions }; + const injectLabel = `inject ${kind} script in tab ${tab.id}`; + const runLabel = `run ${kind} script in tab ${tab.id}`; + await executeScriptWithFallback(inject, injectLabel); + const results = await executeScriptWithFallback(run, runLabel); + const first = results?.[0]; + if (first?.error || first?.result != null || !chrome.debugger) return results; + recordAttachEvent({ kind: "scripting-no-result-cdp-fallback", tabId: tab.id, files: inject.files }); + await executeScriptViaCdp(inject, injectLabel); + const viaCdp = await executeScriptViaCdp(run, runLabel); + if (viaCdp?.[0]?.result == null) { + throw new Error(`Chrome ${kind} script returned no result in tab ${tab.id} (${tab.url || "unknown url"}); the page may block script execution.`); + } + return viaCdp; +} + async function snapshotInTab(params) { const tab = await getTabByParams(params); await bringToFront(tab, params); @@ -2424,14 +2455,7 @@ async function snapshotInTab(params) { params.query ?? null, params.maxTextChars ?? null, ]; - await executeScriptWithFallback({ - target: { tabId: tab.id, frameIds: [0] }, - world: "MAIN", - files: ["snapshot_injected.js"], - }, `inject snapshot script in tab ${tab.id}`); - const results = await executeScriptWithFallback({ - target: { tabId: tab.id, frameIds: [0] }, - world: "MAIN", + const results = await runSnapshotInjected(tab, "snapshot", { func: async (invocationArgs) => { try { const snapshotPage = globalThis.__piChromeSnapshotPage; @@ -2442,7 +2466,7 @@ async function snapshotInTab(params) { } }, args: [args], - }, `run snapshot script in tab ${tab.id}`); + }); const first = results?.[0]; if (first?.error) { const message = typeof first.error === "string" ? first.error : (first.error.message || JSON.stringify(first.error)); @@ -2460,14 +2484,7 @@ async function inspectInTab(params) { const tab = await getTabByParams(params); await bringToFront(tab, params); const args = [params.uid ?? null, params.selector ?? null, params.scrollIntoView === true]; - await executeScriptWithFallback({ - target: { tabId: tab.id, frameIds: [0] }, - world: "MAIN", - files: ["snapshot_injected.js"], - }, `inject inspect script in tab ${tab.id}`); - const results = await executeScriptWithFallback({ - target: { tabId: tab.id, frameIds: [0] }, - world: "MAIN", + const results = await runSnapshotInjected(tab, "inspect", { func: async (invocationArgs) => { try { const inspectTarget = globalThis.__piChromeInspectTarget; @@ -2478,7 +2495,7 @@ async function inspectInTab(params) { } }, args: [args], - }, `run inspect script in tab ${tab.id}`); + }); const first = results?.[0]; if (first?.error) { const message = typeof first.error === "string" ? first.error : (first.error.message || JSON.stringify(first.error)); diff --git a/extensions/chrome-profile-bridge/index.ts b/extensions/chrome-profile-bridge/index.ts index 525de6e..0338c5c 100644 --- a/extensions/chrome-profile-bridge/index.ts +++ b/extensions/chrome-profile-bridge/index.ts @@ -133,8 +133,11 @@ function truncateText(text: string, maxChars = MAX_TEXT_CHARS): string { return `${text.slice(0, maxChars)}\n\n[truncated ${text.length - maxChars} characters]`; } +// JSON.stringify returns undefined (not a string) for undefined, functions, and symbols. A tool +// result block with `text: undefined` is persisted to the session and makes every later turn +// fail with "Cannot read properties of undefined (reading 'length')", so always return a string. function safeJson(value: unknown): string { - return JSON.stringify(value, null, 2); + return JSON.stringify(value, null, 2) ?? String(value); } const snapshotModeValues = ["auto", "interactive", "forms", "pageMap", "text", "changes", "full"] as const; @@ -150,7 +153,8 @@ function rectText(rect: any): string { } function formatChromeSnapshot(snapshot: any): string { - if (!snapshot || typeof snapshot !== "object") return safeJson(snapshot); + if (snapshot == null) throw new Error("Chrome returned no snapshot for this page; it may block extension scripts. Try chrome_evaluate instead."); + if (typeof snapshot !== "object") return safeJson(snapshot); if (snapshot.mode === "full") return truncateText(safeJson(snapshot)); const lines: string[] = []; lines.push(`# Chrome snapshot${snapshot.mode ? ` (${snapshot.mode})` : ""}`); @@ -351,7 +355,8 @@ async function pruneScreenshotDir(dir: string, retentionDays?: number): Promise< // End screenshot pruning. function formatChromeInspect(inspect: any): string { - if (!inspect || typeof inspect !== "object") return safeJson(inspect); + if (inspect == null) throw new Error("Chrome returned no inspect result for this page; it may block extension scripts. Try chrome_evaluate instead."); + if (typeof inspect !== "object") return safeJson(inspect); const t = inspect.target || {}; const lines: string[] = []; lines.push(`# Chrome inspect ${t.uid || ""}`.trim()); |
