Files
item-scraper/sidepanel.js
Julian Freeman d50a9a1b86 fix bug
2026-03-11 13:21:34 -04:00

262 lines
9.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 = `
<div class="flex-1 overflow-hidden mr-3">
<div class="text-xs font-bold text-gray-800 truncate">${preset.name} ${preset.sheetName ? `<span class="text-indigo-500 ml-1">#${preset.sheetName}</span>` : ''}</div>
<div class="text-[11px] text-gray-400 truncate mt-1 leading-tight">${preset.fields}</div>
</div>
<button class="flex items-center justify-center w-7 h-7 rounded-full text-gray-300 hover:text-red-500 hover:bg-red-50 transition-all focus:outline-none border-none bg-transparent cursor-pointer" title="删除预设">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
`;
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}`;
}