Initial commit: LinkedIn Analyzer with Gemini 2.5 Flash and PHP Backend
This commit is contained in:
176
background.js
Normal file
176
background.js
Normal file
@@ -0,0 +1,176 @@
|
||||
// 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));
|
||||
}
|
||||
Reference in New Issue
Block a user