Spaces:
Sleeping
Sleeping
| /** | |
| * Flow Agent β Chrome Extension Background Service Worker | |
| * | |
| * Connects to local Python agent via WebSocket (agent runs WS server). | |
| * Captures bearer token, solves reCAPTCHA, proxies API calls through browser. | |
| */ | |
| const AGENT_WS_URL = 'ws://127.0.0.1:9227'; | |
| // NOTE: This is a browser-restricted public API key β safe to ship in extension bundles. | |
| const API_KEY = 'AIzaSyBtrm0o5ab1c-Ec8ZuLcGt3oJAA5VWt3pY'; | |
| let ws = null; | |
| let flowKey = null; | |
| let callbackSecret = null; // Auth secret for HTTP callback, received from server on WS connect | |
| let state = 'off'; // off | idle | running | |
| let manualDisconnect = false; | |
| let metrics = { | |
| tokenCapturedAt: null, | |
| requestCount: 0, // captcha-consuming requests only (gen image/video/upscale) | |
| successCount: 0, | |
| failedCount: 0, | |
| lastError: null, | |
| }; | |
| // βββ URL β Log Type Classifier βββββββββββββββββββββββββββββ | |
| // Visible log types β only these appear in the request log | |
| const _VISIBLE_TYPES = new Set(['GEN_IMG', 'GEN_VID', 'GEN_VID_REF', 'UPSCALE', 'TRACKING', 'URL_REFRESH']); | |
| function _classifyApiUrl(url) { | |
| if (url.includes('uploadImage')) return 'UPLOAD'; | |
| if (url.includes('batchGenerateImages')) return 'GEN_IMG'; | |
| if (url.includes('UpsampleVideo')) return 'UPSCALE'; | |
| if (url.includes('ReferenceImages')) return 'GEN_VID_REF'; | |
| if (url.includes('batchAsyncGenerateVideo')) return 'GEN_VID'; | |
| if (url.includes('batchCheckAsync')) return 'POLL'; | |
| if (url.includes('upsampleImage')) return 'UPS_IMG'; | |
| if (url.includes('/media/')) return 'MEDIA'; | |
| if (url.includes('/credits')) return 'CREDITS'; | |
| return 'API'; | |
| } | |
| // βββ Request Log ββββββββββββββββββββββββββββββββββββββββββββ | |
| let requestLog = []; | |
| function addRequestLog(entry) { | |
| requestLog.unshift(entry); | |
| if (requestLog.length > 100) requestLog.pop(); | |
| broadcastRequestLog(); | |
| } | |
| function updateRequestLog(id, updates) { | |
| const entry = requestLog.find((e) => e.id === id); | |
| if (entry) Object.assign(entry, updates); | |
| broadcastRequestLog(); | |
| } | |
| function broadcastRequestLog() { | |
| chrome.runtime.sendMessage({ type: 'REQUEST_LOG_UPDATE', log: requestLog }).catch(() => {}); | |
| } | |
| // βββ Startup ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| chrome.runtime.onInstalled.addListener(init); | |
| chrome.runtime.onStartup.addListener(init); | |
| chrome.alarms.onAlarm.addListener(async (alarm) => { | |
| if (alarm.name === 'reconnect') connectToAgent(); | |
| if (alarm.name === 'keepAlive') keepAlive(); | |
| if (alarm.name === 'token-refresh') { | |
| await captureTokenFromFlowTab(); | |
| } | |
| }); | |
| async function init() { | |
| const data = await chrome.storage.local.get(['flowKey', 'metrics', 'callbackSecret']); | |
| if (data.flowKey) flowKey = data.flowKey; | |
| if (data.metrics) Object.assign(metrics, data.metrics); | |
| if (data.callbackSecret) callbackSecret = data.callbackSecret; | |
| connectToAgent(); | |
| chrome.alarms.create('keepAlive', { periodInMinutes: 0.4 }); | |
| } | |
| // βββ Token Capture ββββββββββββββββββββββββββββββββββββββββββ | |
| chrome.webRequest.onBeforeSendHeaders.addListener( | |
| (details) => { | |
| if (!details?.requestHeaders?.length) return; | |
| const authHeader = details.requestHeaders.find( | |
| (h) => h.name?.toLowerCase() === 'authorization', | |
| ); | |
| const value = authHeader?.value || ''; | |
| if (!value.startsWith('Bearer ya29.')) return; | |
| const token = value.replace(/^Bearer\s+/i, '').trim(); | |
| if (!token) return; | |
| // Always update β even if same token string, refresh the timestamp | |
| flowKey = token; | |
| metrics.tokenCapturedAt = Date.now(); | |
| chrome.storage.local.set({ flowKey, metrics }); | |
| console.log('[Flow Agent] Bearer token captured'); | |
| // Notify agent | |
| if (ws?.readyState === WebSocket.OPEN) { | |
| ws.send(JSON.stringify({ type: 'token_captured', flowKey })); | |
| } | |
| }, | |
| { urls: ['https://aisandbox-pa.googleapis.com/*', 'https://labs.google/*'] }, | |
| ['requestHeaders', 'extraHeaders'], | |
| ); | |
| let _openingFlowTab = false; | |
| async function captureTokenFromFlowTab() { | |
| const tabs = await chrome.tabs.query({ | |
| url: ['https://labs.google/fx/tools/flow*', 'https://labs.google/fx/*/tools/flow*'], | |
| }); | |
| if (!tabs.length) { | |
| if (_openingFlowTab) { | |
| console.log('[Flow Agent] Flow tab already opening, skipping'); | |
| return; | |
| } | |
| _openingFlowTab = true; | |
| try { | |
| console.log('[Flow Agent] No Flow tab found β opening one in background'); | |
| await chrome.tabs.create({ url: 'https://labs.google/fx/tools/flow', active: false }); | |
| await sleep(3000); | |
| const retryTabs = await chrome.tabs.query({ | |
| url: ['https://labs.google/fx/tools/flow*', 'https://labs.google/fx/*/tools/flow*'], | |
| }); | |
| if (!retryTabs.length) { | |
| console.log('[Flow Agent] Flow tab not ready yet after open'); | |
| return; | |
| } | |
| await chrome.scripting.executeScript({ | |
| target: { tabId: retryTabs[0].id }, | |
| files: ['content.js'], | |
| }); | |
| console.log('[Flow Agent] Token refresh triggered on newly opened Flow tab'); | |
| } catch (e) { | |
| console.error('[Flow Agent] Token refresh failed after opening tab:', e); | |
| } finally { | |
| _openingFlowTab = false; | |
| } | |
| return; | |
| } | |
| try { | |
| await chrome.scripting.executeScript({ | |
| target: { tabId: tabs[0].id }, | |
| files: ['content.js'], | |
| }); | |
| console.log('[Flow Agent] Token refresh triggered on Flow tab'); | |
| } catch (e) { | |
| console.error('[Flow Agent] Token refresh failed:', e); | |
| } | |
| } | |
| // βββ WebSocket to Agent βββββββββββββββββββββββββββββββββββββ | |
| function connectToAgent() { | |
| if (manualDisconnect) return; | |
| if (ws?.readyState === WebSocket.CONNECTING) return; | |
| if (ws?.readyState === WebSocket.OPEN) return; | |
| try { | |
| ws = new WebSocket(AGENT_WS_URL); | |
| } catch (e) { | |
| console.error('[Flow Agent] WS connect error:', e); | |
| scheduleReconnect(); | |
| return; | |
| } | |
| ws.onopen = () => { | |
| console.log('[Flow Agent] Connected to agent'); | |
| chrome.alarms.clear('reconnect'); | |
| setState('idle'); | |
| // Token refresh alarm β 45 min gives buffer before ~60 min expiry | |
| chrome.alarms.create('token-refresh', { periodInMinutes: 45 }); | |
| // Send current state + resend token if we have one | |
| ws.send(JSON.stringify({ | |
| type: 'extension_ready', | |
| flowKeyPresent: !!flowKey, | |
| tokenAge: flowKey && metrics.tokenCapturedAt ? Date.now() - metrics.tokenCapturedAt : null, | |
| })); | |
| if (flowKey) { | |
| ws.send(JSON.stringify({ type: 'token_captured', flowKey })); | |
| } | |
| }; | |
| ws.onmessage = async ({ data }) => { | |
| try { | |
| const msg = JSON.parse(data); | |
| if (msg.method === 'api_request') { | |
| await handleApiRequest(msg); | |
| } else if (msg.method === 'trpc_request') { | |
| await handleTrpcRequest(msg); | |
| } else if (msg.method === 'upload_video') { | |
| await handleUploadVideo(msg); | |
| } else if (msg.method === 'solve_captcha') { | |
| await handleSolveCaptcha(msg); | |
| } else if (msg.method === 'get_status') { | |
| sendToAgent({ | |
| id: msg.id, | |
| result: { | |
| state, | |
| flowKeyPresent: !!flowKey, | |
| manualDisconnect, | |
| tokenAge: metrics.tokenCapturedAt ? Date.now() - metrics.tokenCapturedAt : null, | |
| metrics, | |
| }, | |
| }); | |
| } else if (msg.method === 'open_flow_tab') { | |
| // Python bridge asks us to open/focus a Flow tab | |
| console.log('[Flow Agent] Agent requested: open Flow tab'); | |
| const tabs = await chrome.tabs.query({ | |
| url: ['https://labs.google/fx/tools/flow*', 'https://labs.google/fx/*/tools/flow*'], | |
| }); | |
| if (tabs.length) { | |
| // Tab exists β refresh it to trigger fresh API calls β token capture | |
| await chrome.tabs.reload(tabs[0].id); | |
| console.log('[Flow Agent] Refreshed existing Flow tab'); | |
| } else { | |
| // No tab β open one (active so it loads properly) | |
| await chrome.tabs.create({ url: 'https://labs.google/fx/tools/flow', active: true }); | |
| console.log('[Flow Agent] Opened new Flow tab'); | |
| } | |
| // Wait for page to load and make API calls that trigger token capture | |
| await sleep(5000); | |
| // If token was captured by webRequest during page load, send it | |
| if (flowKey && ws?.readyState === WebSocket.OPEN) { | |
| ws.send(JSON.stringify({ type: 'token_captured', flowKey })); | |
| console.log('[Flow Agent] Sent stored token after tab open'); | |
| } else { | |
| // Try reading from storage as fallback | |
| const data = await chrome.storage.local.get(['flowKey']); | |
| if (data.flowKey) { | |
| flowKey = data.flowKey; | |
| if (ws?.readyState === WebSocket.OPEN) { | |
| ws.send(JSON.stringify({ type: 'token_captured', flowKey })); | |
| console.log('[Flow Agent] Sent token from storage after tab open'); | |
| } | |
| } | |
| } | |
| } else if (msg.method === 'refresh_flow_tab') { | |
| // Python bridge asks us to refresh token | |
| console.log('[Flow Agent] Agent requested: refresh token'); | |
| await captureTokenFromFlowTab(); | |
| await sleep(3000); | |
| // Actively send token if we have one | |
| if (flowKey && ws?.readyState === WebSocket.OPEN) { | |
| ws.send(JSON.stringify({ type: 'token_captured', flowKey })); | |
| console.log('[Flow Agent] Sent token after refresh'); | |
| } else { | |
| const data = await chrome.storage.local.get(['flowKey']); | |
| if (data.flowKey) { | |
| flowKey = data.flowKey; | |
| if (ws?.readyState === WebSocket.OPEN) { | |
| ws.send(JSON.stringify({ type: 'token_captured', flowKey })); | |
| console.log('[Flow Agent] Sent token from storage after refresh'); | |
| } | |
| } | |
| } | |
| } else if (msg.type === 'callback_secret') { | |
| callbackSecret = msg.secret; | |
| chrome.storage.local.set({ callbackSecret: msg.secret }); | |
| console.log('[Flow Agent] Received callback secret'); | |
| } else if (msg.type === 'pong') { | |
| // keepalive response | |
| } | |
| } catch (e) { | |
| console.error('[Flow Agent] Message error:', e); | |
| } | |
| }; | |
| ws.onclose = () => { | |
| setState('off'); | |
| chrome.alarms.clear('token-refresh'); | |
| if (!manualDisconnect) scheduleReconnect(); | |
| }; | |
| ws.onerror = (e) => { | |
| console.error('[Flow Agent] WS error:', e); | |
| metrics.lastError = 'WS_ERROR'; | |
| chrome.storage.local.set({ metrics }); | |
| }; | |
| } | |
| function scheduleReconnect() { | |
| chrome.alarms.create('reconnect', { delayInMinutes: 0.083 }); // ~5s | |
| } | |
| function keepAlive() { | |
| if (ws?.readyState === WebSocket.OPEN) { | |
| ws.send(JSON.stringify({ type: 'ping' })); | |
| } else { | |
| connectToAgent(); | |
| } | |
| } | |
| function sendToAgent(msg) { | |
| // API responses (with msg.id) go via HTTP β immune to WS disconnect | |
| if (msg.id) { | |
| fetch('http://127.0.0.1:8100/api/ext/callback', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(msg), | |
| }).catch(() => { | |
| // HTTP failed β fallback to WS | |
| if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg)); | |
| }); | |
| return; | |
| } | |
| // Non-response messages (ping, status) or no secret yet β use WS | |
| if (ws?.readyState === WebSocket.OPEN) { | |
| ws.send(JSON.stringify(msg)); | |
| } | |
| } | |
| // βββ reCAPTCHA Solving ββββββββββββββββββββββββββββββββββββββ | |
| async function requestCaptchaFromTab(tabId, requestId, pageAction) { | |
| try { | |
| return await chrome.tabs.sendMessage(tabId, { | |
| type: 'GET_CAPTCHA', | |
| requestId, | |
| pageAction, | |
| }); | |
| } catch (error) { | |
| const msg = error?.message || ''; | |
| const shouldInject = | |
| msg.includes('Receiving end does not exist') || | |
| msg.includes('Could not establish connection'); | |
| if (!shouldInject) throw error; | |
| // Inject content script and retry | |
| await chrome.scripting.executeScript({ | |
| target: { tabId }, | |
| files: ['content.js'], | |
| }); | |
| await sleep(200); | |
| return await chrome.tabs.sendMessage(tabId, { | |
| type: 'GET_CAPTCHA', | |
| requestId, | |
| pageAction, | |
| }); | |
| } | |
| } | |
| async function solveCaptcha(requestId, captchaAction) { | |
| const tabs = await chrome.tabs.query({ | |
| url: ['https://labs.google/fx/tools/flow*', 'https://labs.google/fx/*/tools/flow*'], | |
| }); | |
| if (!tabs.length) { | |
| // Auto-open Flow tab and wait briefly before returning error | |
| try { | |
| await chrome.tabs.create({ url: 'https://labs.google/fx/tools/flow', active: false }); | |
| await sleep(3000); | |
| // Retry tab query after opening | |
| const retryTabs = await chrome.tabs.query({ | |
| url: ['https://labs.google/fx/tools/flow*', 'https://labs.google/fx/*/tools/flow*'], | |
| }); | |
| if (!retryTabs.length) return { error: 'NO_FLOW_TAB' }; | |
| const resp = await Promise.race([ | |
| requestCaptchaFromTab(retryTabs[0].id, requestId, captchaAction), | |
| new Promise((_, rej) => setTimeout(() => rej(new Error('CAPTCHA_TIMEOUT')), 30000)), | |
| ]); | |
| return resp; | |
| } catch (e) { | |
| return { error: e.message || 'NO_FLOW_TAB' }; | |
| } | |
| } | |
| try { | |
| const resp = await Promise.race([ | |
| requestCaptchaFromTab(tabs[0].id, requestId, captchaAction), | |
| new Promise((_, rej) => setTimeout(() => rej(new Error('CAPTCHA_TIMEOUT')), 30000)), | |
| ]); | |
| return resp; | |
| } catch (e) { | |
| return { error: e.message }; | |
| } | |
| } | |
| async function handleSolveCaptcha(msg) { | |
| const { id, params } = msg; | |
| const result = await solveCaptcha(id, params?.captchaAction || 'VIDEO_GENERATION'); | |
| // Standalone captcha solve counts as captcha-consuming | |
| metrics.requestCount++; | |
| if (result?.token) { | |
| metrics.successCount++; | |
| } else { | |
| metrics.failedCount++; | |
| metrics.lastError = result?.error || 'NO_TOKEN'; | |
| } | |
| chrome.storage.local.set({ metrics }); | |
| sendToAgent({ id, result }); | |
| } | |
| // βββ API Request Proxy ββββββββββββββββββββββββββββββββββββββ | |
| async function handleTrpcRequest(msg) { | |
| const { id, params } = msg; | |
| const { url, method = 'POST', headers = {}, body } = params; | |
| if (!url || !url.startsWith('https://labs.google/')) { | |
| sendToAgent({ id, error: 'INVALID_TRPC_URL' }); | |
| return; | |
| } | |
| setState('running'); | |
| // TRPC calls don't consume captcha β don't count in metrics | |
| const logId = id; | |
| const logType = url.includes('createProject') ? 'CREATE_PROJECT' : 'TRPC'; | |
| // TRPC calls are silent β don't show in request log | |
| const fetchHeaders = { 'Content-Type': 'application/json', ...headers }; | |
| if (flowKey) { | |
| fetchHeaders['authorization'] = `Bearer ${flowKey}`; | |
| } | |
| try { | |
| const resp = await fetch(url, { | |
| method, | |
| headers: fetchHeaders, | |
| body: body ? JSON.stringify(body) : undefined, | |
| credentials: 'include', | |
| }); | |
| const data = await resp.json(); | |
| chrome.storage.local.set({ metrics }); | |
| updateRequestLog(logId, { status: 'success' }); | |
| sendToAgent({ id, status: resp.status, data }); | |
| } catch (e) { | |
| console.error('[Flow Agent] tRPC request failed:', e); | |
| chrome.storage.local.set({ metrics }); | |
| updateRequestLog(logId, { status: 'failed', error: e.message || 'TRPC_FETCH_FAILED' }); | |
| sendToAgent({ id, error: e.message || 'TRPC_FETCH_FAILED' }); | |
| } finally { | |
| setState('idle'); | |
| } | |
| } | |
| async function handleUploadVideo(msg) { | |
| const { id, params } = msg; | |
| const { videoBase64, projectId, videoSize } = params; | |
| try { | |
| const tabs = await chrome.tabs.query({ url: '*://labs.google/*' }); | |
| if (!tabs.length) { | |
| sendToAgent({ id, error: 'NO_FLOW_TAB' }); | |
| return; | |
| } | |
| const size = videoSize || (videoBase64 ? Math.floor(videoBase64.length * 3 / 4) : 0); | |
| // Get session URL via page context XHR (needs session cookies) | |
| const startResults = await chrome.scripting.executeScript({ | |
| target: { tabId: tabs[0].id }, | |
| world: 'MAIN', | |
| func: (projId, sz) => { | |
| return new Promise((resolve) => { | |
| const xhr = new XMLHttpRequest(); | |
| xhr.open('POST', '/fx/api/upload-video?action=start'); | |
| xhr.setRequestHeader('X-Upload-Project-Id', projId); | |
| xhr.setRequestHeader('X-Upload-Content-Type', 'video/mp4'); | |
| xhr.setRequestHeader('X-Upload-Content-Length', sz.toString()); | |
| xhr.withCredentials = true; | |
| xhr.onload = () => { | |
| let data; | |
| try { data = JSON.parse(xhr.responseText); } catch { data = {}; } | |
| resolve({ | |
| sessionUrl: data.sessionUrl || xhr.getResponseHeader('X-Upload-Session-Url') || '', | |
| status: xhr.status, | |
| }); | |
| }; | |
| xhr.onerror = () => resolve({ error: 'POST_FAILED' }); | |
| xhr.send(); | |
| }); | |
| }, | |
| args: [projectId, size], | |
| }); | |
| const step1 = startResults?.[0]?.result; | |
| if (!step1 || step1.error || !step1.sessionUrl) { | |
| sendToAgent({ id, error: step1?.error || 'NO_SESSION_URL' }); | |
| return; | |
| } | |
| // Return sessionUrl + token β caller handles PUT | |
| sendToAgent({ | |
| id, | |
| result: { | |
| sessionUrl: step1.sessionUrl, | |
| token: flowKey || '', | |
| }, | |
| }); | |
| } catch (e) { | |
| sendToAgent({ id, error: `UPLOAD_ERROR: ${e.message}` }); | |
| } | |
| } | |
| async function handleApiRequest(msg) { | |
| const { id, params } = msg; | |
| const { url, method, headers, body, captchaAction } = params; | |
| if (!url) { | |
| sendToAgent({ id, error: 'MISSING_URL' }); | |
| return; | |
| } | |
| if (!url.startsWith('https://aisandbox-pa.googleapis.com/')) { | |
| sendToAgent({ id, error: 'INVALID_URL' }); | |
| return; | |
| } | |
| setState('running'); | |
| const hasCaptcha = !!captchaAction; | |
| if (hasCaptcha) metrics.requestCount++; | |
| const logId = id; | |
| const logType = _classifyApiUrl(url); | |
| if (_VISIBLE_TYPES.has(logType)) { | |
| const payloadSummary = body ? JSON.stringify(body).slice(0, 200) : null; | |
| addRequestLog({ id: logId, type: logType, time: new Date().toISOString(), status: 'processing', error: null, outputUrl: null, url, payloadSummary }); | |
| } | |
| try { | |
| // Step 1: Solve captcha if needed | |
| let captchaToken = null; | |
| if (captchaAction) { | |
| const captchaResult = await solveCaptcha(id, captchaAction); | |
| captchaToken = captchaResult?.token || null; | |
| if (!captchaToken) { | |
| // Cannot proceed without captcha β API will 403 | |
| const err = captchaResult?.error || 'CAPTCHA_FAILED'; | |
| console.error(`[Flow Agent] Captcha failed for ${captchaAction}: ${err}`); | |
| sendToAgent({ id, status: 403, error: `CAPTCHA_FAILED: ${err}` }); | |
| if (hasCaptcha) { metrics.failedCount++; metrics.lastError = `CAPTCHA_FAILED: ${err}`; } | |
| chrome.storage.local.set({ metrics }); | |
| updateRequestLog(logId, { status: 'failed', error: `CAPTCHA_FAILED: ${err}` }); | |
| setState('idle'); | |
| return; | |
| } | |
| } | |
| // Step 2: Inject captcha token into body | |
| let finalBody = body; | |
| if (captchaToken && finalBody) { | |
| finalBody = JSON.parse(JSON.stringify(finalBody)); // deep clone | |
| if (finalBody.clientContext?.recaptchaContext) { | |
| finalBody.clientContext.recaptchaContext.token = captchaToken; | |
| } | |
| if (finalBody.requests && Array.isArray(finalBody.requests)) { | |
| for (const req of finalBody.requests) { | |
| if (req.clientContext?.recaptchaContext) { | |
| req.clientContext.recaptchaContext.token = captchaToken; | |
| } | |
| } | |
| } | |
| } | |
| // Step 3: Use flowKey for auth | |
| const activeFlowKey = flowKey; | |
| if (!activeFlowKey) { | |
| sendToAgent({ id, status: 503, error: 'NO_FLOW_KEY' }); | |
| if (hasCaptcha) { metrics.failedCount++; metrics.lastError = 'NO_FLOW_KEY'; } | |
| chrome.storage.local.set({ metrics }); | |
| updateRequestLog(logId, { status: 'failed', error: 'NO_FLOW_KEY' }); | |
| setState('idle'); | |
| return; | |
| } | |
| const fetchHeaders = { ...(headers || {}) }; | |
| fetchHeaders['authorization'] = `Bearer ${activeFlowKey}`; | |
| // Step 4: Make the API call from browser context | |
| const response = await fetch(url, { | |
| method: method || 'POST', | |
| headers: fetchHeaders, | |
| credentials: 'include', | |
| body: method === 'GET' ? undefined : JSON.stringify(finalBody), | |
| }); | |
| let responseData; | |
| const responseText = await response.text(); | |
| try { | |
| responseData = JSON.parse(responseText); | |
| } catch { | |
| responseData = responseText; | |
| } | |
| sendToAgent({ | |
| id, | |
| status: response.status, | |
| data: responseData, | |
| }); | |
| const responseSummary = responseText ? responseText.slice(0, 300) : null; | |
| if (response.ok) { | |
| if (hasCaptcha) { metrics.successCount++; metrics.lastError = null; } | |
| updateRequestLog(logId, { status: 'success', httpStatus: response.status, responseSummary }); | |
| } else { | |
| if (hasCaptcha) { metrics.failedCount++; metrics.lastError = `API_${response.status}`; } | |
| updateRequestLog(logId, { status: 'failed', error: `API_${response.status}`, httpStatus: response.status, responseSummary }); | |
| } | |
| } catch (e) { | |
| sendToAgent({ | |
| id, | |
| status: 500, | |
| error: e.message || 'API_REQUEST_FAILED', | |
| }); | |
| if (hasCaptcha) { metrics.failedCount++; metrics.lastError = e.message; } | |
| updateRequestLog(logId, { status: 'failed', error: e.message || 'API_REQUEST_FAILED' }); | |
| } | |
| chrome.storage.local.set({ metrics }); | |
| setState('idle'); | |
| } | |
| // βββ State & Popup ββββββββββββββββββββββββββββββββββββββββββ | |
| function setState(newState) { | |
| state = newState; | |
| const badges = { idle: 'β', running: 'βΆ', off: 'β' }; | |
| const colors = { idle: '#22c55e', running: '#f59e0b', off: '#6b7280' }; | |
| chrome.action.setBadgeText({ text: badges[state] || '' }); | |
| chrome.action.setBadgeBackgroundColor({ color: colors[state] || '#000' }); | |
| broadcastStatus(); | |
| } | |
| function broadcastStatus() { | |
| chrome.runtime.sendMessage({ type: 'STATUS_PUSH' }).catch(() => {}); | |
| } | |
| chrome.runtime.onMessage.addListener((msg, _, reply) => { | |
| if (msg.type === 'STATUS') { | |
| reply({ | |
| connected: ws?.readyState === WebSocket.OPEN, | |
| agentConnected: ws?.readyState === WebSocket.OPEN, | |
| flowKeyPresent: !!flowKey, | |
| manualDisconnect, | |
| tokenAge: metrics.tokenCapturedAt ? Date.now() - metrics.tokenCapturedAt : null, | |
| metrics: { | |
| requestCount: metrics.requestCount, | |
| successCount: metrics.successCount, | |
| failedCount: metrics.failedCount, | |
| lastError: metrics.lastError, | |
| }, | |
| state, | |
| }); | |
| } | |
| if (msg.type === 'DISCONNECT') { | |
| manualDisconnect = true; | |
| if (ws) ws.close(); | |
| reply({ ok: true }); | |
| return true; | |
| } | |
| if (msg.type === 'RECONNECT') { | |
| manualDisconnect = false; | |
| connectToAgent(); | |
| reply({ ok: true }); | |
| return true; | |
| } | |
| if (msg.type === 'REQUEST_LOG') { | |
| reply({ log: requestLog }); | |
| return true; | |
| } | |
| if (msg.type === 'OPEN_FLOW_TAB') { | |
| chrome.tabs.query({ | |
| url: ['https://labs.google/fx/tools/flow*', 'https://labs.google/fx/*/tools/flow*'], | |
| }).then((tabs) => { | |
| if (tabs.length) { | |
| chrome.tabs.update(tabs[0].id, { active: true }); | |
| reply({ ok: true, tabId: tabs[0].id }); | |
| } else { | |
| chrome.tabs.create({ url: 'https://labs.google/fx/tools/flow' }) | |
| .then((tab) => reply({ ok: true, tabId: tab.id })) | |
| .catch((e) => reply({ error: e.message })); | |
| } | |
| }).catch((e) => reply({ error: e.message })); | |
| return true; | |
| } | |
| if (msg.type === 'REFRESH_TOKEN') { | |
| captureTokenFromFlowTab() | |
| .then(() => reply({ ok: true })) | |
| .catch((e) => reply({ error: e.message })); | |
| return true; | |
| } | |
| if (msg.type === 'TEST_CAPTCHA') { | |
| solveCaptcha(`test-${Date.now()}`, msg.pageAction || 'IMAGE_GENERATION') | |
| .then((r) => reply(r)) | |
| .catch((e) => reply({ error: e.message })); | |
| return true; | |
| } | |
| if (msg.type === 'TRPC_MEDIA_URLS') { | |
| handleTrpcMediaUrls(msg.trpcUrl, msg.body); | |
| reply({ ok: true }); | |
| return true; | |
| } | |
| if (msg.type === 'SNIFFED_AISANDBOX_REQUEST') { | |
| console.log('[Flow Agent] SNIFFED aisandbox request:', msg.url); | |
| fetch('http://127.0.0.1:8100/api/ext/callback', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| type: 'sniffed_video_request', | |
| url: msg.url, | |
| method: msg.method, | |
| payload: msg.payload, | |
| timestamp: msg.timestamp, | |
| }), | |
| }).catch((e) => console.error('[Flow Agent] Failed to forward sniffed request:', e)); | |
| reply({ ok: true }); | |
| return true; | |
| } | |
| return true; | |
| }); | |
| // βββ TRPC Media URL Extractor ββββββββββββββββββββββββββββββ | |
| function handleTrpcMediaUrls(trpcUrl, bodyText) { | |
| try { | |
| // Extract all fresh GCS signed URLs | |
| const urlRegex = /https:\/\/storage\.googleapis\.com\/ai-sandbox-videofx\/(?:image|video)\/[0-9a-f-]{36}\?[^"'\s]+/g; | |
| const matches = bodyText.match(urlRegex) || []; | |
| if (!matches.length) return; | |
| // Deduplicate and parse | |
| const urlMap = {}; | |
| for (const rawUrl of matches) { | |
| // Unescape JSON-escaped URLs | |
| const url = rawUrl.replace(/\\u0026/g, '&').replace(/\\/g, ''); | |
| const mediaMatch = url.match(/\/(image|video)\/([0-9a-f-]{36})\?/); | |
| if (mediaMatch) { | |
| const [, mediaType, mediaId] = mediaMatch; | |
| // Keep last occurrence (freshest) | |
| urlMap[mediaId] = { mediaType, url, mediaId }; | |
| } | |
| } | |
| const entries = Object.values(urlMap); | |
| if (!entries.length) return; | |
| console.log(`[Flow Agent] Captured ${entries.length} fresh media URLs from TRPC`); | |
| // URL refresh is silent β don't show in request log | |
| // Forward to agent for DB update | |
| if (ws?.readyState === WebSocket.OPEN) { | |
| ws.send(JSON.stringify({ | |
| type: 'media_urls_refresh', | |
| urls: entries, | |
| })); | |
| } | |
| } catch (e) { | |
| console.error('[Flow Agent] Failed to extract TRPC media URLs:', e); | |
| } | |
| } | |
| function sleep(ms) { | |
| return new Promise((r) => setTimeout(r, ms)); | |
| } | |
| // βββ Human-like Telemetry ββββββββββββββββββββββββββββββββββ | |
| // Periodically send tracking events to Google's analytics endpoints | |
| // to mimic normal browser behavior. | |
| const _UA = navigator.userAgent; | |
| let _telemetrySessionId = `;${Date.now()}`; | |
| function _rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } | |
| function _buildBatchLogPayload() { | |
| const events = []; | |
| const types = ['FLOW_IMAGE_LATENCY', 'FLOW_VIDEO_LATENCY']; | |
| const count = _rand(1, 3); | |
| for (let i = 0; i < count; i++) { | |
| events.push({ | |
| event: types[_rand(0, types.length - 1)], | |
| eventProperties: [ | |
| { key: 'CURRENT_TIME_MS', doubleValue: Date.now() }, | |
| { key: 'DURATION_MS', doubleValue: _rand(150, 800) }, | |
| { key: 'USER_AGENT', stringValue: _UA }, | |
| { key: 'IS_DESKTOP', booleanValue: true }, | |
| ], | |
| eventMetadata: { sessionId: _telemetrySessionId }, | |
| eventTime: new Date().toISOString(), | |
| }); | |
| } | |
| return { appEvents: events }; | |
| } | |
| function _buildFrontendEventsPayload() { | |
| const eventTypes = [ | |
| 'FLOW_IMAGE_LATENCY', 'FLOW_VIDEO_LATENCY', 'GRID_SCROLL_DEPTH', | |
| 'FLOW_PROJECT_OPEN', 'FLOW_SCENE_VIEW', | |
| ]; | |
| const count = _rand(1, 4); | |
| const events = []; | |
| for (let i = 0; i < count; i++) { | |
| const et = eventTypes[_rand(0, eventTypes.length - 1)]; | |
| const params = { | |
| USER_AGENT: { '@type': 'type.googleapis.com/google.protobuf.StringValue', value: _UA }, | |
| IS_DESKTOP: { '@type': 'type.googleapis.com/google.protobuf.StringValue', value: 'true' }, | |
| }; | |
| if (et.includes('LATENCY')) { | |
| params.CURRENT_TIME_MS = { '@type': 'type.googleapis.com/google.protobuf.StringValue', value: String(Date.now()) }; | |
| params.DURATION_MS = { '@type': 'type.googleapis.com/google.protobuf.StringValue', value: String(_rand(100, 600)) }; | |
| } | |
| if (et === 'GRID_SCROLL_DEPTH') { | |
| params.MEDIA_GENERATION_PAYGATE_TIER = { '@type': 'type.googleapis.com/google.protobuf.StringValue', value: 'PAYGATE_TIER_TWO' }; | |
| } | |
| events.push({ | |
| eventType: et, | |
| metadata: { | |
| sessionId: _telemetrySessionId, | |
| createTime: new Date().toISOString(), | |
| additionalParams: params, | |
| }, | |
| }); | |
| } | |
| return { events }; | |
| } | |
| async function sendTelemetry() { | |
| if (!flowKey || state === 'off') return; | |
| const headers = { | |
| 'Content-Type': 'text/plain;charset=UTF-8', | |
| 'authorization': `Bearer ${flowKey}`, | |
| }; | |
| // Telemetry is silent β don't show in request log | |
| try { | |
| if (Math.random() < 0.5) { | |
| await fetch(`https://aisandbox-pa.googleapis.com/v1:batchLog`, { | |
| method: 'POST', headers, credentials: 'include', | |
| body: JSON.stringify(_buildBatchLogPayload()), | |
| }); | |
| } else { | |
| await fetch(`https://aisandbox-pa.googleapis.com/v1/flow:batchLogFrontendEvents`, { | |
| method: 'POST', headers, credentials: 'include', | |
| body: JSON.stringify(_buildFrontendEventsPayload()), | |
| }); | |
| } | |
| } catch {} | |
| } | |
| // Send telemetry at random intervals (45-120s) to look organic | |
| function scheduleTelemetry() { | |
| const delay = _rand(45, 120) * 1000; | |
| setTimeout(async () => { | |
| await sendTelemetry(); | |
| scheduleTelemetry(); // reschedule with new random interval | |
| }, delay); | |
| } | |
| // Refresh session ID every ~30min like a real user | |
| setInterval(() => { _telemetrySessionId = `;${Date.now()}`; }, _rand(25, 35) * 60 * 1000); | |
| scheduleTelemetry(); | |
| console.log('[Flow Agent] Extension loaded'); | |