Initial release with production deploy config.

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>
This commit is contained in:
2026-06-15 00:52:31 +08:00
commit e9939ef9dc
45 changed files with 14376 additions and 0 deletions
+272
View File
@@ -0,0 +1,272 @@
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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
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}`;
}