// DOM Elements const apiKeyInput = document.getElementById('apiKey'); const scriptUrlInput = document.getElementById('scriptUrl'); const modelInput = document.getElementById('modelInput'); const saveConfigBtn = document.getElementById('saveConfig'); const customInput = document.getElementById('customInput'); const sendCustomBtn = document.getElementById('sendCustom'); const statusBadge = document.getElementById('statusBadge'); const resultsArea = document.getElementById('results'); // Preset Management Elements const newPresetName = document.getElementById('newPresetName'); const newPresetFields = document.getElementById('newPresetFields'); const addPresetBtn = document.getElementById('addPreset'); const presetList = document.getElementById('presetList'); const presetButtonsContainer = document.getElementById('presetButtons'); // Collapsible logic const toggleConfig = document.getElementById('toggleConfig'); const configContent = document.getElementById('configContent'); const configChevron = document.getElementById('configChevron'); const DEFAULT_PRESETS = [ { id: 'p1', name: '提取笔记本', fields: '品牌, CPU, 内存, 存储, 价格, URL' }, { id: 'p2', name: '提取外设', fields: '品牌, 型号, 连接方式, 电池寿命, 价格, URL' } ]; toggleConfig.addEventListener('click', () => { const isHidden = configContent.classList.toggle('hidden'); configChevron.classList.toggle('rotate-180', !isHidden); }); // Load settings and presets on startup chrome.storage.local.get(['geminiApiKey', 'googleScriptUrl', 'geminiModel', 'userPresets'], (data) => { if (data.geminiApiKey) { apiKeyInput.value = data.geminiApiKey; configContent.classList.add('hidden'); configChevron.classList.remove('rotate-180'); } else { configContent.classList.remove('hidden'); configChevron.classList.add('rotate-180'); } if (data.googleScriptUrl) scriptUrlInput.value = data.googleScriptUrl; if (data.geminiModel) modelInput.value = data.geminiModel; const presets = data.userPresets || DEFAULT_PRESETS; renderPresets(presets); }); // Save settings (including presets is handled separately via add/delete) saveConfigBtn.addEventListener('click', () => { const apiKey = apiKeyInput.value.trim(); const scriptUrl = scriptUrlInput.value.trim(); const model = modelInput.value.trim(); chrome.storage.local.set({ geminiApiKey: apiKey, googleScriptUrl: scriptUrl, geminiModel: model }, () => { updateStatus('已保存', 'bg-green-500 text-white'); setTimeout(() => updateStatus('待机', 'bg-gray-200 text-gray-600'), 2000); }); }); // Preset Management Logic addPresetBtn.addEventListener('click', async () => { const name = newPresetName.value.trim(); const fields = newPresetFields.value.trim(); if (!name || !fields) return alert('请输入名称和字段'); const { userPresets = DEFAULT_PRESETS } = await chrome.storage.local.get('userPresets'); const newPreset = { id: Date.now().toString(), name, fields }; const updatedPresets = [...userPresets, newPreset]; await chrome.storage.local.set({ userPresets: updatedPresets }); newPresetName.value = ''; newPresetFields.value = ''; renderPresets(updatedPresets); }); async function deletePreset(id) { const { userPresets = DEFAULT_PRESETS } = await chrome.storage.local.get('userPresets'); const updatedPresets = userPresets.filter(p => p.id !== id); await chrome.storage.local.set({ userPresets: updatedPresets }); renderPresets(updatedPresets); } function renderPresets(presets) { // Clear containers presetList.innerHTML = ''; presetButtonsContainer.innerHTML = ''; presets.forEach(preset => { // 1. Render in Settings List const item = document.createElement('div'); item.className = 'flex items-center justify-between bg-gray-50 p-2 rounded border border-gray-200 text-xs'; item.innerHTML = `
${preset.name}
${preset.fields}
`; item.querySelector('button').onclick = () => deletePreset(preset.id); presetList.appendChild(item); // 2. Render as Action Button const btn = document.createElement('button'); btn.className = 'flex flex-col items-center justify-center p-4 bg-white border border-gray-200 rounded-lg hover:border-indigo-400 hover:bg-indigo-50 group transition-all'; btn.innerHTML = ` ⚙️ ${preset.name} `; btn.onclick = () => handleExtraction('preset', preset); presetButtonsContainer.appendChild(btn); }); } // Action Handlers sendCustomBtn.addEventListener('click', () => handleExtraction('custom', { fields: customInput.value })); async function handleExtraction(type, presetObj) { const { geminiApiKey, googleScriptUrl, geminiModel } = await chrome.storage.local.get(['geminiApiKey', 'googleScriptUrl', 'geminiModel']); if (!geminiApiKey) { alert('请先输入 Gemini API 密钥。'); return; } const selectedModel = geminiModel || 'gemini-1.5-flash'; updateStatus('提取中...', 'bg-blue-500 text-white'); resultsArea.textContent = `正在使用 ${selectedModel} 提取 ${presetObj.name || '自定义项'}...`; try { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); const [{ result: pageData }] = await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['content.js'] }); resultsArea.textContent = '正在发送至 Gemini...'; const systemPrompt = `你是一个专业的采购助手。 请从以下文本中提取产品数据。 将技术术语翻译成中文。 仅返回有效的 JSON 格式。 如果可能,将本地货币转换为美元,或者保留原始符号。 URL: ${pageData.url} Title: ${pageData.title}`; const userPrompt = `提取以下字段:${presetObj.fields}。仅返回 JSON 格式。`; const geminiResult = await callGemini(geminiApiKey, selectedModel, systemPrompt, pageData.text, userPrompt); const cleanedJson = parseGeminiJson(geminiResult); resultsArea.textContent = JSON.stringify(cleanedJson, null, 2); if (googleScriptUrl) { updateStatus('保存至表格...', 'bg-purple-500 text-white'); await sendToGoogleScript(googleScriptUrl, { ...cleanedJson, preset_type: presetObj.name || 'custom', source_url: pageData.url, timestamp: new Date().toISOString() }); updateStatus('成功', 'bg-green-500 text-white'); } else { updateStatus('完成 (未配置脚本链接)', 'bg-yellow-500 text-white'); } } catch (error) { console.error(error); updateStatus('错误', 'bg-red-500 text-white'); resultsArea.textContent = `错误: ${error.message}`; } } async function callGemini(apiKey, model, systemPrompt, contextText, userPrompt) { const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: `${systemPrompt}\n\n上下文:\n${contextText}\n\n任务: ${userPrompt}` }] }] }) }); const data = await response.json(); if (data.error) throw new Error(data.error.message); return data.candidates[0].content.parts[0].text; } function parseGeminiJson(text) { try { const jsonStr = text.replace(/```json/g, '').replace(/```/g, '').trim(); return JSON.parse(jsonStr); } catch (e) { throw new Error('AI 返回了无效的 JSON: ' + text); } } async function sendToGoogleScript(url, payload) { return await fetch(url, { method: 'POST', mode: 'no-cors', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); } function updateStatus(text, classes) { statusBadge.textContent = text; statusBadge.className = `status-badge ${classes}`; }