1 line
14 KiB
JSON
1 line
14 KiB
JSON
{"repo": "allwillcome/scriptsofshuai", "files": {"Notes/Project/ICUAIProject/intervals-icu-ai-coach/background.js": {"1b11bda174bdd12b1013a499f05d3af5c7880857": {"t": 1785768946.8820317, "n": 13398, "h": "9c365b52b61c5106", "c": "/**\n * Operation Logger Instance\n * Logs all background operations for debugging\n */\nclass OperationLogger {\n constructor() {\n this.logs = [];\n this.maxLogs = 100;\n }\n\n log(level, operation, details = {}) {\n const entry = {\n timestamp: new Date().toISOString(),\n level,\n operation,\n ...details\n };\n\n this.logs.push(entry);\n if (this.logs.length > this.maxLogs) {\n this.logs.shift();\n }\n\n const consoleMethod = level === 'error' ? 'error' : level === 'warn' ? 'warn' : 'log';\n console[consoleMethod](`[${level.toUpperCase()}] ${operation}:`, details);\n\n return entry;\n }\n\n info(operation, details) { return this.log('info', operation, details); }\n warn(operation, details) { return this.log('warn', operation, details); }\n error(operation, details) { return this.log('error', operation, details); }\n success(operation, details) { return this.log('success', operation, details); }\n}\n\nconst logger = new OperationLogger();\n\n// Listen to messages from content script\nchrome.runtime.onMessage.addListener((request, sender, sendResponse) => {\n if (request.action === \"OPEN_OPTIONS\") {\n chrome.runtime.openOptionsPage();\n }\n\n if (request.action === \"ANALYZE_ACTIVITY\") {\n logger.info('ANALYZE_REQUEST_RECEIVED', { tabId: sender.tab?.id });\n\n // Start async agent process\n handleAgentAnalysis(request.data, request.lang, sender.tab?.id).then(result => {\n sendResponse({ success: true, result: result });\n }).catch(err => {\n logger.error('ANALYSIS_FAILED', { error: err.message, stack: err.stack });\n sendResponse({ success: false, error: err.message });\n });\n return true;\n }\n});\n\n// Logic when clicking extension icon (emergency injection)\nchrome.action.onClicked.addListener(async (tab) => {\n if (tab.url && tab.url.includes(\"intervals.icu\")) {\n try {\n await chrome.scripting.insertCSS({\n target: { tabId: tab.id },\n files: [\"styles.css\"]\n });\n await chrome.scripting.executeScript({\n target: { tabId: tab.id },\n files: [\"validator.js\"]\n });\n await chrome.scripting.executeScript({\n target: { tabId: tab.id },\n files: [\"content.js\"]\n });\n logger.info('MANUAL_INJECTION_SUCCESS', { tabId: tab.id });\n } catch (e) {\n logger.error('INJECTION_FAILED', { error: e.message });\n chrome.runtime.openOptionsPage();\n }\n } else {\n chrome.runtime.openOptionsPage();\n }\n});\n\n/**\n * Send progress update to content script\n */\nfunction sendProgress(tabId, stage, data = {}) {\n if (!tabId) return;\n\n chrome.tabs.sendMessage(tabId, {\n action: 'ANALYSIS_PROGRESS',\n stage: stage,\n data: data\n }).catch(err => {\n logger.warn('PROGRESS_SEND_FAILED', { stage, error: err.message });\n });\n}\n\n/**\n * Extract code blocks from markdown\n */\nfunction extractCodeBlocks(markdown) {\n const codeBlocks = [];\n const pattern = /```(intervals)?\\\\n([\\\\s\\\\S]*?)```/g;\n let match;\n\n while ((match = pattern.exec(markdown)) !== null) {\n codeBlocks.push({\n lang: match[1] || 'text',\n code: match[2].trim()\n });\n }\n\n return codeBlocks;\n}\n\n/**\n * Validate Intervals.icu workout format\n * Simplified version - full validation happens in content script with validator.js\n */\nfunction validateWorkout(workoutText) {\n const errors = [];\n const warnings = [];\n\n if (!workoutText || !workoutText.trim()) {\n errors.push('Workout is empty');\n return { valid: false, errors, warnings };\n }\n\n const lines = workoutText.trim().split('\\\\n');\n const stepPattern = /^\\\\s*-\\\\s+/;\n let hasSteps = false;\n\n for (const line of lines) {\n if (stepPattern.test(line)) {\n hasSteps = true;\n break;\n }\n }\n\n if (!hasSteps) {\n errors.push('No workout steps found (lines must start with \"-\")');\n }\n\n return {\n valid: errors.length === 0,\n errors,\n warnings\n };\n}\n\n/**\n * Agent-style analysis with self-correction\n * Workflow:\n * 1. Generate initial analysis + workout plan\n * 2. Validate workout format\n * 3. If invalid, ask AI to correct (max 3 attempts)\n * 4. Return validated result\n */\nasync function handleAgentAnalysis(activityData, lang, tabId) {\n const maxAttempts = 3;\n let attempt = 0;\n let finalResult = '';\n let validationErrors = [];\n\n try {\n logger.info('AGENT_ANALYSIS_START', { attempt: 1, maxAttempts });\n sendProgress(tabId, 'generating', { attempt: 1 });\n\n // Step 1: Generate initial analysis\n const initialResult = await callAI(activityData, lang, null);\n finalResult = initialResult;\n\n logger.info('INITIAL_ANALYSIS_COMPLETE', { length: initialResult.length });\n sendProgress(tabId, 'validating', { attempt: 1 });\n\n // Step 2: Extract and validate workout plans\n const codeBlocks = extractCodeBlocks(initialResult);\n const intervalsBlocks = codeBlocks.filter(block => block.lang === 'intervals');\n\n logger.info('CODE_BLOCKS_EXTRACTED', { total: codeBlocks.length, intervals: intervalsBlocks.length });\n\n if (intervalsBlocks.length === 0) {\n logger.warn('NO_INTERVALS_BLOCKS_FOUND', {});\n // No workout plans to validate, return as-is\n return finalResult;\n }\n\n // Validate each intervals block\n let allValid = true;\n const validationResults = [];\n\n for (const block of intervalsBlocks) {\n const validation = validateWorkout(block.code);\n validationResults.push(validation);\n\n if (!validation.valid) {\n allValid = false;\n validationErrors.push(...validation.errors);\n }\n }\n\n logger.info('VALIDATION_COMPLETE', {\n allValid,\n totalBlocks: intervalsBlocks.length,\n errors: validationErrors.length\n });\n\n // Step 3: Self-correction loop if validation failed\n attempt = 1;\n while (!allValid && attempt < maxAttempts) {\n attempt++;\n logger.info('SELF_CORRECTION_ATTEMPT', { attempt, errors: validationErrors });\n sendProgress(tabId, 'correcting', { attempt, errors: validationErrors });\n\n // Ask AI to fix the errors\n const correctionPrompt = `The workout plan you generated has formatting errors:\\n\\n${validationErrors.join('\\\\n')}\\n\\nPlease fix these errors and regenerate the workout plan in correct Intervals.icu format. Remember:\\n- Each step must start with \"-\"\\n- Include duration (e.g., 10m, 30s) or distance (e.g., 5km)\\n- Include intensity (e.g., 75%, Z2, 200w)\\n\\nPrevious output:\\n${finalResult}`;\n\n const correctedResult = await callAI(activityData, lang, correctionPrompt);\n finalResult = correctedResult;\n\n // Re-validate\n const newCodeBlocks = extractCodeBlocks(correctedResult);\n const newIntervalsBlocks = newCodeBlocks.filter(block => block.lang === 'intervals');\n\n validationErrors = [];\n allValid = true;\n\n for (const block of newIntervalsBlocks) {\n const validation = validateWorkout(block.code);\n if (!validation.valid) {\n allValid = false;\n validationErrors.push(...validation.errors);\n }\n }\n\n logger.info('CORRECTION_VALIDATION', {\n attempt,\n allValid,\n errorsRemaining: validationErrors.length\n });\n }\n\n if (allValid) {\n logger.success('ANALYSIS_COMPLETE_VALID', { attempts: attempt });\n sendProgress(tabId, 'complete', { valid: true, attempts: attempt });\n } else {\n logger.warn('ANALYSIS_COMPLETE_WITH_ERRORS', {\n attempts: attempt,\n errors: validationErrors\n });\n sendProgress(tabId, 'complete', {\n valid: false,\n attempts: attempt,\n errors: validationErrors\n });\n }\n\n return finalResult;\n\n } catch (error) {\n logger.error('AGENT_ANALYSIS_ERROR', { error: error.message, stack: error.stack });\n sendProgress(tabId, 'error', { error: error.message });\n throw error;\n }\n}\n\n/**\n * Call AI service (extracted from handleAnalysis)\n * @param {Object} activityData - Activity data to analyze\n * @param {string} lang - Language for response\n * @param {string|null} customUserPrompt - Optional custom prompt for correction\n */\nasync function callAI(activityData, lang, customUserPrompt = null) {\n try {\n // 1. Read configuration\n const config = await chrome.storage.sync.get(['provider', 'apiBaseUrl', 'apiKey', 'modelName', 'customPrompt']);\n\n if (!config.apiKey) {\n throw new Error(\"API Key missing. Please configure in settings.\");\n }\n\n const provider = config.provider || 'openai';\n const apiUrl = config.apiBaseUrl || \"https://api.openai.com/v1/chat/completions\";\n const model = config.modelName || \"gpt-4o-mini\";\n\n // 2. Prepare prompt\n let systemPrompt = config.customPrompt;\n if (!systemPrompt) {\n try {\n const url = chrome.runtime.getURL('prompt.txt');\n const response = await fetch(url);\n if (!response.ok) throw new Error(\"Prompt file not found\");\n const text = await response.text();\n systemPrompt = text.replace(/{{LANG}}/g, lang);\n } catch (e) {\n console.warn(\"Failed to load prompt.txt, using fallback:\", e);\n systemPrompt = `You are a professional sports coach. Please analyze the following data in ${lang} and provide brief feedback.`;\n }\n }\n\n // Build final prompt\n let finalPrompt;\n if (customUserPrompt) {\n // For correction requests, use custom prompt\n finalPrompt = customUserPrompt;\n } else {\n // For initial analysis, use system prompt + data\n finalPrompt = `${systemPrompt}\\n\\nData: ${JSON.stringify(activityData)}`;\n }\n\n // 3. Call API\n let fetchOptions = {};\n let responseText = \"\";\n\n if (provider === 'claude') {\n // Anthropic / Claude\n fetchOptions = {\n method: 'POST',\n headers: {\n 'x-api-key': config.apiKey,\n 'anthropic-version': '2023-06-01',\n 'content-type': 'application/json'\n },\n body: JSON.stringify({\n model: model,\n max_tokens: 1024,\n messages: [{ role: \"user\", content: finalPrompt }]\n })\n };\n } else if (provider === 'gemini') {\n // Google Gemini\n // URL usually needs ?key=API_KEY\n const urlWithKey = `${apiUrl}?key=${config.apiKey}`;\n fetchOptions = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\n contents: [{ parts: [{ text: finalPrompt }] }]\n })\n };\n // Override URL for fetch\n var fetchUrl = urlWithKey; \n } else {\n // OpenAI / DeepSeek / API2D / Custom (OpenAI Compatible)\n \n // Auto-fix URL if missing endpoint\n let url = apiUrl;\n if (!url.endsWith('/chat/completions') && !url.endsWith('/v1/messages')) {\n // Simple heuristic: if it looks like a base URL, append standard endpoint\n if (url.includes('deepseek') || url.includes('openai') || url.includes('api2d')) {\n if (url.endsWith('/')) {\n url += 'chat/completions';\n } else {\n url += '/chat/completions';\n }\n }\n }\n\n fetchOptions = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${config.apiKey}`\n },\n body: JSON.stringify({\n model: model,\n messages: [{ role: \"user\", content: finalPrompt }],\n temperature: 0.7\n })\n };\n \n // Update targetUrl to the normalized one\n if (provider !== 'gemini') {\n // We need to pass this new url to fetch\n // Since targetUrl is const defined later, we need to handle this.\n // Let's refactor slightly below.\n }\n }\n\n let targetUrl = apiUrl;\n if (provider === 'gemini') {\n targetUrl = fetchUrl;\n } else if (provider === 'claude') {\n targetUrl = apiUrl;\n } else {\n // Apply the fix logic again or use the one we calculated\n let url = apiUrl;\n if (!url.endsWith('/chat/completions') && !url.endsWith('/v1/messages')) {\n if (url.includes('deepseek') || url.includes('openai') || url.includes('api2d')) {\n url = url.endsWith('/') ? url + 'chat/completions' : url + '/chat/completions';\n }\n }\n targetUrl = url;\n }\n\n const res = await fetch(targetUrl, fetchOptions);\n\n if (!res.ok) {\n const errText = await res.text();\n let errMsg = errText;\n try {\n const errJson = JSON.parse(errText);\n if (errJson.error && errJson.error.message) errMsg = errJson.error.message;\n } catch(e) {}\n throw new Error(`API Request Failed (${res.status}): ${errMsg}`);\n }\n\n const json = await res.json();\n\n // 4. Parse response\n if (provider === 'claude') {\n if (json.content && json.content.length > 0) {\n return json.content[0].text;\n }\n } else if (provider === 'gemini') {\n if (json.candidates && json.candidates.length > 0 && json.candidates[0].content) {\n return json.candidates[0].content.parts[0].text;\n }\n } else {\n // OpenAI Compatible\n if (json.choices && json.choices.length > 0) {\n return json.choices[0].message.content;\n }\n }\n\n throw new Error(\"API returned empty content. Check model name or permissions.\");\n\n } catch (error) {\n logger.error(\"CALL_AI_ERROR\", { error: error.message, stack: error.stack });\n throw error;\n }\n}\n"}}}} |