const DEFAULT_API_BASE = "http://127.0.0.1:4777"; const statusText = document.querySelector("#statusText"); const versionText = document.querySelector("#versionText"); const updatePanel = document.querySelector("#updatePanel"); const updateText = document.querySelector("#updateText"); const updateGuideBtn = document.querySelector("#updateGuideBtn"); const resultBox = document.querySelector("#resultBox"); const settingsPanel = document.querySelector("#settingsPanel"); const capturePageBtn = document.querySelector("#capturePageBtn"); const captureMainBtn = document.querySelector("#captureMainBtn"); const testSaveBtn = document.querySelector("#testSaveBtn"); const openDashboardBtn = document.querySelector("#openDashboardBtn"); const apiBaseInput = document.querySelector("#apiBaseInput"); const pluginTokenInput = document.querySelector("#pluginTokenInput"); const saveSettingsBtn = document.querySelector("#saveSettingsBtn"); let settings = { apiBase: DEFAULT_API_BASE, pluginToken: "", }; let latestExtensionInfo = null; async function init() { versionText.textContent = `插件版本 ${currentVersion()}`; settings = await loadSettings(); apiBaseInput.value = settings.apiBase; pluginTokenInput.value = settings.pluginToken; await checkHealth(); } async function loadSettings() { const stored = await chromeStorageGet(["apiBase", "pluginToken"]); return { apiBase: normalizeApiBase(stored.apiBase || DEFAULT_API_BASE), pluginToken: stored.pluginToken || "", }; } async function saveSettings() { settings = { apiBase: normalizeApiBase(apiBaseInput.value || DEFAULT_API_BASE), pluginToken: pluginTokenInput.value.trim(), }; await chromeStorageSet(settings); settings = await loadSettings(); apiBaseInput.value = settings.apiBase; pluginTokenInput.value = settings.pluginToken; resultBox.innerHTML = "

连接配置已保存,下次打开会自动带出。

"; await checkHealth(); settingsPanel.open = false; } async function checkHealth() { try { const response = await fetch(`${settings.apiBase}/api/health`); if (!response.ok) throw new Error("服务未响应"); const payload = await response.json(); latestExtensionInfo = payload.extension || null; statusText.textContent = payload.authRequired && !settings.pluginToken ? "系统已连接,待填写插件 Token" : "系统已连接"; renderUpdatePrompt(payload.extension); setButtonsDisabled(payload.authRequired && !settings.pluginToken); openDashboardBtn.disabled = false; } catch { statusText.textContent = "系统未连接"; latestExtensionInfo = null; renderUpdatePrompt(null); setButtonsDisabled(true); openDashboardBtn.disabled = false; } } function renderUpdatePrompt(extensionInfo) { if (!extensionInfo?.recommendedVersion || !isVersionGreater(extensionInfo.recommendedVersion, currentVersion())) { updatePanel.hidden = true; settingsPanel.classList.remove("has-update"); return; } updatePanel.hidden = false; settingsPanel.classList.add("has-update"); updateText.textContent = `当前 ${currentVersion()},系统推荐 ${extensionInfo.recommendedVersion}。${extensionInfo.updateNote || "建议更新插件后再采集。"}`; } function setButtonsDisabled(disabled) { capturePageBtn.disabled = disabled; captureMainBtn.disabled = disabled; testSaveBtn.disabled = disabled; openDashboardBtn.disabled = disabled; } async function getActiveTab() { const activeTabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true }); let tab = activeTabs.find(isInjectableTab); if (!tab) { const allTabs = await chrome.tabs.query({}); tab = allTabs.reverse().find(isInjectableTab); } if (!tab?.id) throw new Error("没有找到可采集的普通网页标签页,请先打开淘宝、小红书或 1688 页面"); return tab; } async function runExtractor(mode) { const tab = await getActiveTab(); try { const response = await chrome.tabs.sendMessage(tab.id, { type: "EXTRACT_PRODUCTS", mode }); return response?.items || []; } catch (firstError) { try { await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ["content.js"], }); const response = await chrome.tabs.sendMessage(tab.id, { type: "EXTRACT_PRODUCTS", mode }); return response?.items || []; } catch (secondError) { throw new Error(`页面采集脚本无法运行:${secondError.message || firstError.message}`); } } } async function capture(mode) { resultBox.innerHTML = "

正在采集当前页面...

"; setButtonsDisabled(true); try { const items = await runExtractor(mode); if (!items.length) { resultBox.innerHTML = "

没有识别到可采集的商品/笔记。可以滚动页面后再试,或打开详情页采集主商品。

"; return; } const payload = await postCaptures(items); resultBox.innerHTML = renderSavedResult(payload.items || [], payload.credits); } catch (error) { resultBox.innerHTML = `

采集失败:${escapeHtml(error.message)}

排查:确认系统地址和插件 Token;当前页不是浏览器内部页;淘宝/小红书/1688 页面可刷新后重试。

`; } finally { setButtonsDisabled(false); } } async function postCaptures(items) { const headers = { "Content-Type": "application/json" }; if (settings.pluginToken) headers.Authorization = `Bearer ${settings.pluginToken}`; const response = await fetch(`${settings.apiBase}/api/captures`, { method: "POST", headers, body: JSON.stringify({ items }), }); const payload = await response.json(); if (!response.ok) throw new Error(payload.error || "保存失败"); return payload; } async function testSave() { resultBox.innerHTML = "

正在测试写入系统...

"; setButtonsDisabled(true); try { const payload = await postCaptures([ { platform: "1688", title: "插件测试款 藏式编绳手链 民族风", url: `https://detail.1688.com/offer/plugin-test-${Date.now()}.html`, image: "", priceText: "¥6.80", supplier: "插件诊断测试", metrics: { visibleText: "现货 混批 支持拿样 1000+人付款 求链接" }, notes: "来自浏览器插件测试保存按钮", }, ]); resultBox.innerHTML = `

测试保存成功。采集链路正常。

已写入:${escapeHtml(payload.items?.[0]?.title || "测试款")}

${payload.credits ? `

已扣 ${escapeHtml(payload.credits.deducted)} 积分,剩余 ${escapeHtml(payload.credits.remaining)}。

` : ""} `; } catch (error) { resultBox.innerHTML = `

测试保存失败:${escapeHtml(error.message)}

请确认系统地址可访问,SaaS 模式需要填写插件 Token。

`; } finally { setButtonsDisabled(false); } } function renderSavedResult(items, credits) { return `

已保存 ${items.length} 条候选。

${credits ? `

已扣 ${escapeHtml(credits.deducted)} 积分,剩余 ${escapeHtml(credits.remaining)}。

` : ""} `; } function chromeStorageGet(keys) { return new Promise((resolve, reject) => { chrome.storage.sync.get(keys, (value) => { const error = chrome.runtime.lastError; if (error) reject(new Error(error.message || "读取配置失败")); else resolve(value || {}); }); }); } function chromeStorageSet(value) { return new Promise((resolve, reject) => { chrome.storage.sync.set(value, () => { const error = chrome.runtime.lastError; if (error) reject(new Error(error.message || "保存配置失败,请检查插件 storage 权限")); else resolve(); }); }); } function isInjectableTab(tab) { return Boolean(tab?.id && /^https?:\/\//.test(tab.url || "")); } function normalizeApiBase(value = "") { return String(value).trim().replace(/\/+$/, "") || DEFAULT_API_BASE; } function escapeHtml(value = "") { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } capturePageBtn.addEventListener("click", () => capture("page")); captureMainBtn.addEventListener("click", () => capture("main")); testSaveBtn.addEventListener("click", testSave); saveSettingsBtn.addEventListener("click", saveSettings); openDashboardBtn.addEventListener("click", () => chrome.tabs.create({ url: settings.apiBase })); updateGuideBtn.addEventListener("click", () => { const guide = latestExtensionInfo?.reloadRequiredForUnpacked ? "请下载系统里的新版插件安装包,解压后到 chrome://extensions 重新加载解压后的文件夹。" : "请按系统提示更新插件。"; resultBox.innerHTML = `

${escapeHtml(guide)}

安装包:${escapeHtml(latestExtensionInfo?.fileName || "product-sourcing-capture-extension.zip")}

`; const installUrl = latestExtensionInfo?.installUrl || "/"; chrome.tabs.create({ url: absoluteSystemUrl(installUrl) }); }); init(); function currentVersion() { return chrome.runtime.getManifest().version || "0.0.0"; } function isVersionGreater(a, b) { const left = String(a).split(".").map((part) => Number(part) || 0); const right = String(b).split(".").map((part) => Number(part) || 0); for (let i = 0; i < Math.max(left.length, right.length); i += 1) { const diff = (left[i] || 0) - (right[i] || 0); if (diff > 0) return true; if (diff < 0) return false; } return false; } function absoluteSystemUrl(pathOrUrl = "/") { if (/^https?:\/\//.test(pathOrUrl)) return pathOrUrl; return `${settings.apiBase}${String(pathOrUrl).startsWith("/") ? "" : "/"}${pathOrUrl}`; }