Files
item-scraper/sidepanel.js
Julian Freeman 527046339a optimize
2026-03-11 13:51:10 -04:00

256 lines
9.6 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 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 rawFields = newPresetFields.value.trim();
if (!name || !rawFields) return alert('请输入名称和字段');
// 核心逻辑:支持中英文逗号,并清理多余空格
const normalizedFields = rawFields
.split(/[,]/) // 使用正则匹配中英文逗号
.map(f => f.trim()) // 清理每一项前后的空格
.filter(f => f !== "") // 过滤掉空项
.join(', '); // 统一用英文逗号+空格连接
const { userPresets = DEFAULT_PRESETS } = await chrome.storage.local.get('userPresets');
const newPreset = { id: Date.now().toString(), name, sheetName, fields: normalizedFields };
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);
});
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);
if (Array.isArray(cleanedJson)) {
cleanedJson = cleanedJson[0] || {};
}
resultsArea.textContent = JSON.stringify(cleanedJson, null, 2);
if (googleScriptUrl) {
updateStatus('保存至表格...', 'bg-purple-500 text-white');
const businessHeaders = presetObj.fields.split(',').map(f => f.trim()).filter(f => f !== "");
const metaHeaders = ["来源链接", "提取时间"];
const allHeaders = [...businessHeaders, ...metaHeaders];
const payload = {
sheetName: presetObj.sheetName || '',
headers: allHeaders
};
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}`;
}