| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { defineBackground } from 'wxt/sandbox'; |
|
|
| export default defineBackground({ |
| type: 'module', |
| persistent: true, |
|
|
| main() { |
| const WS_URL = (import.meta.env.VITE_WS_URL || 'ws://localhost:7860'); |
| const CLIENT_ID = getOrCreateClientId(); |
|
|
| let ws: WebSocket | null = null; |
| let reconnectDelay = 1000; |
| let reconnectTimer: ReturnType<typeof setTimeout> | null = null; |
| let isConnected = false; |
|
|
| |
| const pendingTabs = new Map<string, number>(); |
|
|
| |
| function setBadge(status: 'connected' | 'reconnecting' | 'offline') { |
| const colors = { |
| connected: '#22c55e', |
| reconnecting: '#eab308', |
| offline: '#6b7280', |
| }; |
| const labels = { connected: 'β', reconnecting: 'β»', offline: 'β' }; |
| chrome.action.setBadgeText({ text: labels[status] }); |
| chrome.action.setBadgeBackgroundColor({ color: colors[status] }); |
| |
| chrome.tabs.query({}, (tabs) => { |
| tabs.forEach((tab) => { |
| if (tab.id) { |
| chrome.tabs.sendMessage(tab.id, { type: 'ws_status', status }).catch(() => {}); |
| } |
| }); |
| }); |
| } |
|
|
| |
| function connect() { |
| if (ws && ws.readyState === WebSocket.OPEN) return; |
|
|
| const url = `${WS_URL}/ws/${CLIENT_ID}`; |
| console.log('[FactEngine] Connecting to', url); |
| setBadge('reconnecting'); |
|
|
| try { |
| ws = new WebSocket(url); |
| } catch (err) { |
| console.error('[FactEngine] WebSocket construction failed', err); |
| scheduleReconnect(); |
| return; |
| } |
|
|
| ws.onopen = () => { |
| console.log('[FactEngine] WebSocket connected'); |
| isConnected = true; |
| reconnectDelay = 1000; |
| setBadge('connected'); |
| }; |
|
|
| ws.onmessage = (event: MessageEvent) => { |
| try { |
| const msg = JSON.parse(event.data as string); |
| handleBackendMessage(msg); |
| } catch (err) { |
| console.error('[FactEngine] Failed to parse backend message', err); |
| } |
| }; |
|
|
| ws.onerror = (err) => { |
| console.error('[FactEngine] WebSocket error', err); |
| }; |
|
|
| ws.onclose = () => { |
| console.log('[FactEngine] WebSocket closed'); |
| isConnected = false; |
| ws = null; |
| setBadge('offline'); |
| scheduleReconnect(); |
| }; |
| } |
|
|
| function scheduleReconnect() { |
| if (reconnectTimer) clearTimeout(reconnectTimer); |
| const jitter = Math.random() * 500; |
| reconnectTimer = setTimeout(() => { |
| connect(); |
| reconnectDelay = Math.min(reconnectDelay * 2, 30_000); |
| }, reconnectDelay + jitter); |
| } |
|
|
| function sendToBackend(payload: object) { |
| if (ws && ws.readyState === WebSocket.OPEN) { |
| ws.send(JSON.stringify(payload)); |
| return true; |
| } |
| return false; |
| } |
|
|
| |
| function handleBackendMessage(msg: Record<string, unknown>) { |
| if (msg.type === 'analysis_batch') { |
| const results = msg.results as Array<Record<string, unknown>>; |
| |
| chrome.tabs.query({ active: true }, (tabs) => { |
| tabs.forEach((tab) => { |
| if (tab.id) { |
| chrome.tabs.sendMessage(tab.id, { |
| type: 'analysis_results', |
| results, |
| }).catch(() => {}); |
| } |
| }); |
| }); |
| |
| chrome.tabs.query({}, (tabs) => { |
| tabs.forEach((tab) => { |
| if (tab.id) { |
| chrome.tabs.sendMessage(tab.id, { |
| type: 'analysis_results', |
| results, |
| }).catch(() => {}); |
| } |
| }); |
| }); |
| } |
| } |
|
|
| |
| chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { |
| if (msg.type === 'analyze_claims') { |
| const { claims, platform } = msg; |
| const sent = sendToBackend({ |
| client_id: CLIENT_ID, |
| claims, |
| platform, |
| timestamp: Date.now() / 1000, |
| }); |
| sendResponse({ queued: sent }); |
| return true; |
| } |
|
|
| if (msg.type === 'get_ws_status') { |
| sendResponse({ status: isConnected ? 'connected' : 'offline' }); |
| return true; |
| } |
|
|
| if (msg.type === 'get_client_id') { |
| sendResponse({ clientId: CLIENT_ID }); |
| return true; |
| } |
|
|
| if (msg.type === 'reconnect') { |
| if (!isConnected) connect(); |
| sendResponse({ ok: true }); |
| return true; |
| } |
| }); |
|
|
| |
| connect(); |
|
|
| |
| setInterval(() => { |
| if (ws && ws.readyState === WebSocket.OPEN) { |
| try { |
| ws.send(JSON.stringify({ type: 'ping', client_id: CLIENT_ID })); |
| } catch (_) { |
| connect(); |
| } |
| } else if (!ws || ws.readyState === WebSocket.CLOSED) { |
| connect(); |
| } |
| }, 20_000); |
| }, |
| }); |
|
|
| function getOrCreateClientId(): string { |
| |
| |
| const stored = sessionStorage.getItem('fact_engine_client_id'); |
| if (stored) return stored; |
| const arr = new Uint8Array(8); |
| crypto.getRandomValues(arr); |
| const id = 'ext-' + Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join(''); |
| sessionStorage.setItem('fact_engine_client_id', id); |
| return id; |
| } |
|
|