Files
cv/background.js
2026-05-18 00:18:20 +03:00

174 lines
6.4 KiB
JavaScript

// 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, action = 'generateText', jobDescription = '', jobTitle = '' }) {
// Rate limit check
const canProceed = await checkRateLimit();
if (!canProceed) {
throw new Error('Daily limit reached (1,000 requests). Resets at midnight PT.');
}
// Truncate text — 12k chars to fit list_analysis prompts
const maxChars = 12000;
const trimmedPrompt = prompt && prompt.length > maxChars
? prompt.substring(0, maxChars) + '\n\n[Truncated]'
: prompt;
const MAX_RETRIES = 3;
const RETRY_DELAYS = [0, 15000, 30000]; // No delay on first attempt
let lastError = '';
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
if (RETRY_DELAYS[attempt] > 0) await delay(RETRY_DELAYS[attempt]);
try {
const response = await fetch('https://cv.intaleqapp.com/cv/server/generate_cv.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: action,
apiKey: apiKey,
prompt: trimmedPrompt,
jobDescription: jobDescription,
jobTitle: jobTitle
})
});
if (response.ok) {
const data = await response.json();
if (action === 'generatePdf') {
if (!data.pdf) throw new Error('Empty PDF response from server.');
await incrementUsage();
return { pdf: data.pdf, filename: data.filename, fromCache: false };
} else {
const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
if (!text) {
console.error('[LJA-BG] Empty text. Full response:', JSON.stringify(data).substring(0, 500));
lastError = 'Empty response from API. Server returned: ' + JSON.stringify(data).substring(0, 200);
continue;
}
await incrementUsage();
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));
}