// 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 newPresetSheet = document.getElementById('newPresetSheet'); const newPresetFields = document.getElementById('newPresetFields'); const addPresetBtn = document.getElementById('addPreset'); const presetList = document.getElementById('presetList'); const presetSelect = document.getElementById('presetSelect'); const runPresetBtn = document.getElementById('runPreset'); // Collapsible logic const toggleConfig = document.getElementById('toggleConfig'); const configContent = document.getElementById('configContent'); const configChevron = document.getElementById('configChevron'); const DEFAULT_PRESETS = [ { id: 'p1', name: '提取笔记本', sheetName: '笔记本', fields: '品牌, CPU, 内存, 存储, 价格, URL' }, { id: 'p2', name: '提取外设', sheetName: '外设', fields: '品牌, 型号, 连接方式, 电池寿命, 价格, URL' } ]; let currentPresets = []; 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; currentPresets = data.userPresets || DEFAULT_PRESETS; renderPresets(currentPresets); }); // Save settings 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 sheetName = newPresetSheet.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, sheetName, fields }; currentPresets = [...userPresets, newPreset]; await chrome.storage.local.set({ userPresets: currentPresets }); newPresetName.value = ''; newPresetSheet.value = ''; newPresetFields.value = ''; renderPresets(currentPresets); }); async function deletePreset(id) { const { userPresets = DEFAULT_PRESETS } = await chrome.storage.local.get('userPresets'); currentPresets = userPresets.filter(p => p.id !== id); await chrome.storage.local.set({ userPresets: currentPresets }); renderPresets(currentPresets); } function renderPresets(presets) { // Clear containers presetList.innerHTML = ''; presetSelect.innerHTML = ''; presets.forEach(preset => { // 1. Render in Settings List const item = document.createElement('div'); item.className = 'flex items-center justify-between bg-white p-3 mb-2 rounded-lg border border-gray-200 shadow-sm transition-all hover:border-indigo-200'; item.innerHTML = `
${preset.name} ${preset.sheetName ? `#${preset.sheetName}` : ''}
${preset.fields}
`; item.querySelector('button').onclick = (e) => { e.stopPropagation(); deletePreset(preset.id); }; presetList.appendChild(item); // 2. Render as Dropdown Option const opt = document.createElement('option'); opt.value = preset.id; opt.textContent = preset.name; presetSelect.appendChild(opt); }); } // Action Handlers runPresetBtn.addEventListener('click', () => { const selectedId = presetSelect.value; const preset = currentPresets.find(p => p.id === selectedId); if (preset) handleExtraction('preset', preset); }); sendCustomBtn.addEventListener('click', () => handleExtraction('custom', { fields: customInput.value, sheetName: '' })); 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}。 【重要约束】: 1. 必须使用我提供的字段名称作为 JSON 的键名(Key),不要翻译成英文。 2. 将提取到的具体内容翻译成中文。 3. 仅返回 JSON 格式,不要包含任何多余的解释文字。`; const geminiResult = await callGemini(geminiApiKey, selectedModel, systemPrompt, pageData.text, userPrompt); let cleanedJson = parseGeminiJson(geminiResult); // 核心修复:处理 AI 返回数组的情况 if (Array.isArray(cleanedJson)) { cleanedJson = cleanedJson[0] || {}; } resultsArea.textContent = JSON.stringify(cleanedJson, null, 2); if (googleScriptUrl) { updateStatus('保存至表格...', 'bg-purple-500 text-white'); // 1. 获取用户预设的业务表头 const businessHeaders = presetObj.fields.split(',').map(f => f.trim()).filter(f => f !== ""); // 2. 定义元数据表头(可选,方便您追踪来源) const metaHeaders = ["来源链接", "提取时间"]; // 3. 合并总表头顺序 const allHeaders = [...businessHeaders, ...metaHeaders]; // 4. 构建发送给后端的最终数据 const payload = { sheetName: presetObj.sheetName || '', headers: allHeaders }; // 将提取的数据填入 payload businessHeaders.forEach(h => { payload[h] = cleanedJson[h] || "N/A"; }); // 填入元数据 payload["来源链接"] = pageData.url; payload["提取时间"] = new Date().toLocaleString(); await sendToGoogleScript(googleScriptUrl, payload); 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}`; }