// background.js — Service Worker // Handles all Gemini API calls (avoids CORS issues from content scripts) const GEMINI_MODEL = 'gemini-2.5-flash'; const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent`; // ─── Message listener ──────────────────────────────────────────────────────── chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === 'GEMINI_REQUEST') { handleGeminiRequest(message.payload) .then(result => sendResponse({ success: true, data: result })) .catch(err => sendResponse({ success: false, error: err.message })); return true; // Keep message channel open for async } }); // ─── Core API call ─────────────────────────────────────────────────────────── async function handleGeminiRequest({ apiKey, prompt, tab }) { // Rate limit check const canProceed = await checkRateLimit(); if (!canProceed) { throw new Error('Daily limit reached (1,000 requests). Resets at midnight PT.'); } // Check cache (keyed by tab + prompt hash — different jobs produce different hashes) const cacheKey = `cache_${tab}_${hashString(prompt)}`; const cached = await getCached(cacheKey); if (cached) { return { text: cached, fromCache: true }; } // Truncate prompt if too long (free tier has strict TPM limits) const maxPromptChars = 6000; const trimmedPrompt = prompt.length > maxPromptChars ? prompt.substring(0, maxPromptChars) + '\n\n[Description truncated for length]' : prompt; // Retry logic (up to 3 attempts with LONG backoff for free tier) const MAX_RETRIES = 3; const RETRY_DELAYS = [2000, 20000, 30000]; // 2s, 20s, 30s let lastError = ''; for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { await delay(RETRY_DELAYS[attempt]); try { const response = await fetch(`${GEMINI_URL}?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: trimmedPrompt }] }], generationConfig: { temperature: 0.7, maxOutputTokens: 2048, topP: 0.9 } }) }); if (response.ok) { const data = await response.json(); const text = data.candidates?.[0]?.content?.parts?.[0]?.text; if (!text) { lastError = 'Empty response from Gemini.'; continue; } await incrementUsage(); await setCached(cacheKey, text); return { text, fromCache: false }; } const status = response.status; const errData = await response.json().catch(() => ({})); const errMsg = errData.error?.message || ''; if (status === 429 || status === 503) { lastError = `API quota limit (${status}): ${errMsg || 'Too many tokens'}. Waiting before retry...`; continue; } else if (status === 400) { throw new Error('Request error: ' + (errMsg || 'Bad request')); } else if (status === 403) { throw new Error('Access denied. Check your API key in settings.'); } else { throw new Error(`API Error (${status}): ${errMsg || 'Unknown'}`); } } catch (e) { if (e.message.startsWith('Request error') || e.message.startsWith('Access denied') || e.message.startsWith('API Error')) { throw e; } lastError = e.message || 'Network error'; if (attempt === MAX_RETRIES - 1) throw new Error(`Failed after ${MAX_RETRIES} attempts: ${lastError}`); } } throw new Error(`API busy. ${lastError}. Please wait 1-2 minutes and try again.`); } // ─── Rate Limiting ─────────────────────────────────────────────────────────── async function checkRateLimit() { const today = new Date().toDateString(); const data = await storageGet(['usageDate', 'usageCount']); if (data.usageDate !== today) return true; // New day, reset return (data.usageCount || 0) < 1000; } async function incrementUsage() { const today = new Date().toDateString(); const data = await storageGet(['usageDate', 'usageCount']); if (data.usageDate !== today) { await storageSet({ usageDate: today, usageCount: 1 }); } else { await storageSet({ usageCount: (data.usageCount || 0) + 1 }); } } // ─── Cache ─────────────────────────────────────────────────────────────────── async function getCached(key) { const data = await storageGet(['analysisCache']); const cache = data.analysisCache || {}; const entry = cache[key]; if (!entry) return null; // Expire after 24 hours if (Date.now() - entry.timestamp > 24 * 60 * 60 * 1000) { return null; } return entry.value; } async function setCached(key, value) { const data = await storageGet(['analysisCache']); const cache = data.analysisCache || {}; // Keep max 50 entries const keys = Object.keys(cache); if (keys.length >= 50) { // Remove oldest const oldest = keys.sort((a, b) => cache[a].timestamp - cache[b].timestamp)[0]; delete cache[oldest]; } cache[key] = { value, timestamp: Date.now() }; await storageSet({ analysisCache: cache }); } // ─── Utilities ─────────────────────────────────────────────────────────────── function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function hashString(str) { let hash = 0; for (let i = 0; i < Math.min(str.length, 200); i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return Math.abs(hash).toString(36); } function storageGet(keys) { return new Promise(resolve => chrome.storage.local.get(keys, resolve)); } function storageSet(data) { return new Promise(resolve => chrome.storage.local.set(data, resolve)); }