e9939ef9dc
Add GitAtlas deploy assets, DATA_FILE support for isolated server data, and port documentation for sourcing.simosen.cn. Co-authored-by: Cursor <cursoragent@cursor.com>
273 lines
9.8 KiB
JavaScript
273 lines
9.8 KiB
JavaScript
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 = "<p>连接配置已保存,下次打开会自动带出。</p>";
|
|
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 = "<p>正在采集当前页面...</p>";
|
|
setButtonsDisabled(true);
|
|
|
|
try {
|
|
const items = await runExtractor(mode);
|
|
if (!items.length) {
|
|
resultBox.innerHTML = "<p>没有识别到可采集的商品/笔记。可以滚动页面后再试,或打开详情页采集主商品。</p>";
|
|
return;
|
|
}
|
|
|
|
const payload = await postCaptures(items);
|
|
resultBox.innerHTML = renderSavedResult(payload.items || [], payload.credits);
|
|
} catch (error) {
|
|
resultBox.innerHTML = `
|
|
<p>采集失败:${escapeHtml(error.message)}</p>
|
|
<p>排查:确认系统地址和插件 Token;当前页不是浏览器内部页;淘宝/小红书/1688 页面可刷新后重试。</p>
|
|
`;
|
|
} 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 = "<p>正在测试写入系统...</p>";
|
|
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 = `
|
|
<p>测试保存成功。采集链路正常。</p>
|
|
<p>已写入:${escapeHtml(payload.items?.[0]?.title || "测试款")}</p>
|
|
${payload.credits ? `<p>已扣 ${escapeHtml(payload.credits.deducted)} 积分,剩余 ${escapeHtml(payload.credits.remaining)}。</p>` : ""}
|
|
`;
|
|
} catch (error) {
|
|
resultBox.innerHTML = `
|
|
<p>测试保存失败:${escapeHtml(error.message)}</p>
|
|
<p>请确认系统地址可访问,SaaS 模式需要填写插件 Token。</p>
|
|
`;
|
|
} finally {
|
|
setButtonsDisabled(false);
|
|
}
|
|
}
|
|
|
|
function renderSavedResult(items, credits) {
|
|
return `
|
|
<p>已保存 <strong>${items.length}</strong> 条候选。</p>
|
|
${credits ? `<p>已扣 ${escapeHtml(credits.deducted)} 积分,剩余 ${escapeHtml(credits.remaining)}。</p>` : ""}
|
|
<ul>
|
|
${items
|
|
.slice(0, 5)
|
|
.map((item) => `<li>${escapeHtml(item.decision)} · ${escapeHtml(item.title || "未命名")}</li>`)
|
|
.join("")}
|
|
</ul>
|
|
`;
|
|
}
|
|
|
|
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 = `<p>${escapeHtml(guide)}</p><p>安装包:<code>${escapeHtml(latestExtensionInfo?.fileName || "product-sourcing-capture-extension.zip")}</code></p>`;
|
|
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}`;
|
|
}
|