174 lines
6.4 KiB
JavaScript
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-flash-lite-latest';
|
|
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));
|
|
}
|