mirror of
https://github.com/yaklang/yaklang-chrome-extension.git
synced 2026-09-22 03:10:43 +08:00
feat: advance browser agent integration workflows
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import {mkdtemp, readFile, rm} from 'node:fs/promises'
|
||||
import {tmpdir} from 'node:os'
|
||||
import {join, resolve} from 'node:path'
|
||||
import {chromium} from 'playwright-core'
|
||||
import {resolveChromiumPath} from './resolve-chromium.mjs'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
|
||||
export async function extensionRequest(page, action, payload = {}) {
|
||||
return page.evaluate(async ({requestAction, requestPayload}) => {
|
||||
const response = await chrome.runtime.sendMessage({action: requestAction, payload: requestPayload})
|
||||
if (!response?.ok) throw new Error(response?.error?.message || response?.error || requestAction)
|
||||
return response.data
|
||||
}, {requestAction: action, requestPayload: payload})
|
||||
}
|
||||
|
||||
export async function waitFor(page, action, payload, predicate, timeoutMs = 15_000) {
|
||||
const deadline = Date.now() + timeoutMs
|
||||
let value
|
||||
while (Date.now() < deadline) {
|
||||
value = await extensionRequest(page, action, payload)
|
||||
if (predicate(value)) return value
|
||||
await page.waitForTimeout(150)
|
||||
}
|
||||
throw new Error(`Timed out waiting for ${action}: ${JSON.stringify(value)}`)
|
||||
}
|
||||
|
||||
export async function launchBrowserAgentContractHarness({
|
||||
profilePrefix,
|
||||
targetURL,
|
||||
extensionPath = resolve(root, process.env.EXTENSION_PATH || '.output/chrome-mv3-enterprise'),
|
||||
}) {
|
||||
const manifest = JSON.parse(await readFile(resolve(extensionPath, 'manifest.json'), 'utf8'))
|
||||
const executablePath = await resolveChromiumPath()
|
||||
const userDataDir = await mkdtemp(join(tmpdir(), profilePrefix))
|
||||
const context = await chromium.launchPersistentContext(userDataDir, {
|
||||
executablePath,
|
||||
headless: true,
|
||||
viewport: {width: 1280, height: 760},
|
||||
args: [
|
||||
`--disable-extensions-except=${extensionPath}`,
|
||||
`--load-extension=${extensionPath}`,
|
||||
'--no-first-run',
|
||||
'--no-default-browser-check',
|
||||
],
|
||||
})
|
||||
try {
|
||||
let serviceWorker = context.serviceWorkers()[0]
|
||||
if (!serviceWorker) serviceWorker = await context.waitForEvent('serviceworker', {timeout: 15_000})
|
||||
const extensionId = new URL(serviceWorker.url()).host
|
||||
|
||||
if (manifest.permissions?.includes('userScripts')) {
|
||||
const extensionsPage = await context.newPage()
|
||||
await extensionsPage.goto(`chrome://extensions/?id=${extensionId}`)
|
||||
const toggle = extensionsPage.locator('#allow-user-scripts cr-toggle')
|
||||
await toggle.waitFor({state: 'visible', timeout: 10_000})
|
||||
if (!await toggle.evaluate((element) => Boolean(element.checked))) await toggle.click()
|
||||
await extensionsPage.close()
|
||||
}
|
||||
|
||||
const targetPage = await context.newPage()
|
||||
targetPage.on('dialog', (dialog) => void dialog.dismiss())
|
||||
await targetPage.goto(targetURL)
|
||||
|
||||
const controlPage = await context.newPage()
|
||||
await controlPage.goto(`chrome-extension://${extensionId}/options.html`)
|
||||
const tabId = await controlPage.evaluate(async (url) => {
|
||||
const tabs = await chrome.tabs.query({})
|
||||
return tabs.find((tab) => tab.url === url)?.id
|
||||
}, targetPage.url())
|
||||
if (!tabId) throw new Error(`Could not resolve target tab ${targetPage.url()}`)
|
||||
|
||||
return {
|
||||
context,
|
||||
controlPage,
|
||||
extensionId,
|
||||
tabId,
|
||||
targetPage,
|
||||
async close() {
|
||||
await context.close().catch(() => undefined)
|
||||
await rm(userDataDir, {recursive: true, force: true})
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
await context.close().catch(() => undefined)
|
||||
await rm(userDataDir, {recursive: true, force: true})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export function transformedFetchOptions(execution, originalHeaders) {
|
||||
const headers = new Map(originalHeaders.map((header) => [header.name.toLowerCase(), {
|
||||
name: header.name,
|
||||
value: header.value,
|
||||
}]))
|
||||
for (const name of execution.removeHeaders || []) headers.delete(name.toLowerCase())
|
||||
for (const header of execution.setHeaders || []) {
|
||||
headers.set(header.name.toLowerCase(), {name: header.name, value: header.value})
|
||||
}
|
||||
return {
|
||||
method: 'POST',
|
||||
headers: Object.fromEntries([...headers.values()].map((header) => [header.name, header.value])),
|
||||
body: Buffer.from(execution.bodyBase64, 'base64'),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user