import assert from "node:assert/strict"; import { mkdir, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { test } from "node:test"; import { fileURLToPath } from "node:url"; import { hashToken } from "../src/auth.js"; import { createServer } from "../src/server.js"; import { JsonStore } from "../src/store.js"; const ROOT_DIR = dirname(dirname(fileURLToPath(import.meta.url))); async function startServer(options = {}) { const dir = join(tmpdir(), `sourcing-api-${Date.now()}-${Math.random().toString(36).slice(2)}`); await mkdir(dir, { recursive: true }); const store = new JsonStore(join(dir, "items.json")); const server = createServer({ store, publicDir: join(dir, "public"), ...options }); await new Promise((resolve) => server.listen(0, resolve)); const baseUrl = `http://127.0.0.1:${server.address().port}`; return { baseUrl, async close() { await new Promise((resolve) => server.close(resolve)); await rm(dir, { recursive: true, force: true }); }, }; } async function jsonRequest(baseUrl, path, { method = "GET", token, body } = {}) { const headers = {}; if (body !== undefined) headers["Content-Type"] = "application/json"; if (token) headers.Authorization = `Bearer ${token}`; const response = await fetch(`${baseUrl}${path}`, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), }); const contentType = response.headers.get("content-type") || ""; const payload = contentType.includes("application/json") ? await response.json() : await response.text(); return { response, payload }; } test("local API accepts extension captures and exports CSV", async () => { const dir = join(tmpdir(), `sourcing-api-${Date.now()}`); await mkdir(dir, { recursive: true }); const store = new JsonStore(join(dir, "items.json")); const server = createServer({ store, publicDir: join(dir, "public") }); await new Promise((resolve) => server.listen(0, resolve)); const baseUrl = `http://127.0.0.1:${server.address().port}`; try { const captureResponse = await fetch(`${baseUrl}/api/captures`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: [ { platform: "1688", title: "藏式编绳手链 直播民族风", url: "https://detail.1688.com/offer/100.html", image: "https://img.example/100.jpg", priceText: "¥6.80", supplier: "义乌源头饰品厂", metrics: { visibleText: "现货 混批 1000+人付款" }, }, ], }), }); assert.equal(captureResponse.status, 201); const capturePayload = await captureResponse.json(); assert.equal(capturePayload.items.length, 1); assert.equal(capturePayload.items[0].decision, "拿样"); const listResponse = await fetch(`${baseUrl}/api/items?platform=1688`); assert.equal(listResponse.status, 200); const listPayload = await listResponse.json(); assert.equal(listPayload.items.length, 1); const id = listPayload.items[0].id; const updateResponse = await fetch(`${baseUrl}/api/items/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: "ordered_sample", notes: "先拿样 3 件" }), }); assert.equal(updateResponse.status, 200); const updated = await updateResponse.json(); assert.equal(updated.item.status, "ordered_sample"); const csvResponse = await fetch(`${baseUrl}/api/export.csv`); assert.equal(csvResponse.status, 200); assert.match(await csvResponse.text(), /藏式编绳手链/); } finally { await new Promise((resolve) => server.close(resolve)); await rm(dir, { recursive: true, force: true }); } }); test("health API exposes recommended extension version for update prompts", async () => { const app = await startServer(); try { const health = await jsonRequest(app.baseUrl, "/api/health"); assert.equal(health.response.status, 200); assert.equal(health.payload.ok, true); assert.equal(health.payload.extension.name, "选品采购采集器"); assert.match(health.payload.extension.recommendedVersion, /^\d+\.\d+\.\d+$/); assert.equal(health.payload.extension.installUrl, "/downloads/product-sourcing-capture-extension.zip"); assert.match(health.payload.extension.fileName, /^product-sourcing-capture-extension-\d+\.\d+\.\d+\.zip$/); assert.equal(health.payload.extension.reloadRequiredForUnpacked, true); assert.ok(health.payload.extension.updateNote.includes("storage")); } finally { await app.close(); } }); test("server packages browser extension as a downloadable zip", async () => { const app = await startServer(); try { const headResponse = await fetch(`${app.baseUrl}/downloads/product-sourcing-capture-extension.zip`, { method: "HEAD" }); assert.equal(headResponse.status, 200); assert.equal(headResponse.headers.get("content-type"), "application/zip"); assert.match( headResponse.headers.get("content-disposition") || "", /attachment; filename="product-sourcing-capture-extension-\d+\.\d+\.\d+\.zip"/, ); const response = await fetch(`${app.baseUrl}/downloads/product-sourcing-capture-extension.zip`); assert.equal(response.status, 200); assert.equal(response.headers.get("content-type"), "application/zip"); assert.match( response.headers.get("content-disposition") || "", /attachment; filename="product-sourcing-capture-extension-\d+\.\d+\.\d+\.zip"/, ); const zip = Buffer.from(await response.arrayBuffer()); assert.ok(zip.length > 1000); assert.equal(zip.slice(0, 4).toString("binary"), "PK\u0003\u0004"); assert.ok(zip.includes(Buffer.from("manifest.json"))); assert.ok(zip.includes(Buffer.from("popup.html"))); assert.ok(zip.includes(Buffer.from("content.js"))); } finally { await app.close(); } }); test("download routes do not fall back to the dashboard HTML", async () => { const app = await startServer(); try { const response = await fetch(`${app.baseUrl}/downloads/missing-extension.zip`); assert.equal(response.status, 404); assert.match(response.headers.get("content-type") || "", /application\/json/); assert.deepEqual(await response.json(), { error: "Download not found" }); } finally { await app.close(); } }); test("dashboard exposes browser extension package download in plugin install areas", async () => { const dir = join(tmpdir(), `sourcing-api-ui-${Date.now()}-${Math.random().toString(36).slice(2)}`); const server = createServer({ store: new JsonStore(join(dir, "items.json")), publicDir: join(ROOT_DIR, "public") }); await new Promise((resolve) => server.listen(0, resolve)); const baseUrl = `http://127.0.0.1:${server.address().port}`; try { const homepage = await fetch(`${baseUrl}/`); const html = await homepage.text(); assert.equal(homepage.status, 200); assert.match(html, /\/downloads\/product-sourcing-capture-extension\.zip/); assert.match(html, /下载插件安装包/); const appJs = await fetch(`${baseUrl}/app.js`); const js = await appJs.text(); assert.equal(appJs.status, 200); assert.match(js, /\/downloads\/product-sourcing-capture-extension\.zip/); assert.match(js, /下载插件安装包/); } finally { await new Promise((resolve) => server.close(resolve)); await rm(dir, { recursive: true, force: true }); } }); test("homepage exposes absolute social sharing metadata and brand assets", async () => { const dir = join(tmpdir(), `sourcing-api-brand-${Date.now()}-${Math.random().toString(36).slice(2)}`); const publicDir = join(dir, "public"); await mkdir(join(publicDir, "assets"), { recursive: true }); await writeFile( join(publicDir, "index.html"), [ "", '选品采购台|竞品采集、货源匹配与采购决策', '', '', '', '', '', "", ].join(""), "utf8", ); await writeFile(join(publicDir, "favicon.ico"), "icon", "utf8"); await writeFile(join(publicDir, "assets", "share-card.png"), "share", "utf8"); const server = createServer({ store: new JsonStore(join(dir, "items.json")), publicDir }); await new Promise((resolve) => server.listen(0, resolve)); const baseUrl = `http://127.0.0.1:${server.address().port}`; try { const homepage = await fetch(`${baseUrl}/`); const html = await homepage.text(); assert.equal(homepage.status, 200); assert.match(html, /选品采购台|竞品采集、货源匹配与采购决策/); assert.match(html, /rel="icon" href="\/favicon\.ico"/); assert.match(html, /property="og:image"/); assert.ok(html.includes(`content="${baseUrl}/assets/share-card.png"`)); assert.ok(html.includes(`content="${baseUrl}/"`)); assert.doesNotMatch(html, /__SHARE_ORIGIN__/); assert.equal((await fetch(`${baseUrl}/favicon.ico`)).status, 200); assert.equal((await fetch(`${baseUrl}/assets/share-card.png`)).status, 200); } finally { await new Promise((resolve) => server.close(resolve)); await rm(dir, { recursive: true, force: true }); } }); test("SaaS auth isolates captures and rules by user", async () => { const app = await startServer(); try { const aliceRegister = await jsonRequest(app.baseUrl, "/api/auth/register", { method: "POST", body: { email: "alice@example.com", password: "secret123", name: "Alice 买手" }, }); assert.equal(aliceRegister.response.status, 201); assert.ok(aliceRegister.payload.token); assert.equal(aliceRegister.payload.user.email, "alice@example.com"); assert.equal(aliceRegister.payload.user.passwordHash, undefined); const bobRegister = await jsonRequest(app.baseUrl, "/api/auth/register", { method: "POST", body: { email: "bob@example.com", password: "secret123", name: "Bob 买手" }, }); assert.equal(bobRegister.response.status, 201); const unauthenticated = await jsonRequest(app.baseUrl, "/api/items"); assert.equal(unauthenticated.response.status, 401); const aliceToken = aliceRegister.payload.token; const bobToken = bobRegister.payload.token; const capture = await jsonRequest(app.baseUrl, "/api/captures", { method: "POST", token: aliceToken, body: { items: [ { platform: "1688", title: "Alice 藏式编绳手链", url: "https://detail.1688.com/offer/alice-auth.html", priceText: "¥6.80", }, ], }, }); assert.equal(capture.response.status, 201); assert.equal(capture.payload.items[0].title, "Alice 藏式编绳手链"); const aliceItems = await jsonRequest(app.baseUrl, "/api/items", { token: aliceToken }); assert.equal(aliceItems.response.status, 200); assert.deepEqual( aliceItems.payload.items.map((item) => item.title), ["Alice 藏式编绳手链"], ); const bobItems = await jsonRequest(app.baseUrl, "/api/items", { token: bobToken }); assert.equal(bobItems.response.status, 200); assert.deepEqual(bobItems.payload.items, []); const aliceRules = await jsonRequest(app.baseUrl, "/api/rules", { token: aliceToken }); assert.equal(aliceRules.response.status, 200); assert.ok(aliceRules.payload.rules.tracks.some((track) => track.name === "藏式/民族风")); const updateRules = await jsonRequest(app.baseUrl, "/api/rules", { method: "PUT", token: aliceToken, body: { rules: { riskKeywords: ["过敏包治"], profitModel: { shippingFee: 5, packagingFee: 2, platformFeeRate: 0.06, promoFee: 4, maxRunningPrice: 99, }, }, }, }); assert.equal(updateRules.response.status, 200); assert.deepEqual(updateRules.payload.rules.riskKeywords, ["过敏包治"]); assert.equal(updateRules.payload.rules.profitModel.shippingFee, 5); const bobRules = await jsonRequest(app.baseUrl, "/api/rules", { token: bobToken }); assert.equal(bobRules.response.status, 200); assert.notDeepEqual(bobRules.payload.rules.riskKeywords, ["过敏包治"]); const login = await jsonRequest(app.baseUrl, "/api/auth/login", { method: "POST", body: { email: "alice@example.com", password: "secret123" }, }); assert.equal(login.response.status, 200); assert.ok(login.payload.token); assert.equal(login.payload.user.email, "alice@example.com"); const badLogin = await jsonRequest(app.baseUrl, "/api/auth/login", { method: "POST", body: { email: "alice@example.com", password: "wrong-password" }, }); assert.equal(badLogin.response.status, 401); } finally { await app.close(); } }); test("admin can manage users and disabled users cannot sign in", async () => { const app = await startServer(); try { const adminRegister = await jsonRequest(app.baseUrl, "/api/auth/register", { method: "POST", body: { email: "admin@example.com", password: "secret123", name: "平台管理员" }, }); assert.equal(adminRegister.response.status, 201); assert.equal(adminRegister.payload.user.role, "admin"); assert.equal(adminRegister.payload.user.status, "active"); const buyerRegister = await jsonRequest(app.baseUrl, "/api/auth/register", { method: "POST", body: { email: "buyer@example.com", password: "secret123", name: "买手用户" }, }); assert.equal(buyerRegister.response.status, 201); assert.equal(buyerRegister.payload.user.role, "buyer"); const buyerAdminRead = await jsonRequest(app.baseUrl, "/api/admin/users", { token: buyerRegister.payload.token }); assert.equal(buyerAdminRead.response.status, 403); const adminUsers = await jsonRequest(app.baseUrl, "/api/admin/users", { token: adminRegister.payload.token }); assert.equal(adminUsers.response.status, 200); assert.equal(adminUsers.payload.users.length, 2); assert.deepEqual( adminUsers.payload.users.map((user) => [user.email, user.role, user.status, user.itemCount, user.pluginTokenCount]), [ ["admin@example.com", "admin", "active", 0, 0], ["buyer@example.com", "buyer", "active", 0, 0], ], ); const disableBuyer = await jsonRequest(app.baseUrl, `/api/admin/users/${buyerRegister.payload.user.id}/status`, { method: "PATCH", token: adminRegister.payload.token, body: { status: "disabled" }, }); assert.equal(disableBuyer.response.status, 200); assert.equal(disableBuyer.payload.user.status, "disabled"); const disabledLogin = await jsonRequest(app.baseUrl, "/api/auth/login", { method: "POST", body: { email: "buyer@example.com", password: "secret123" }, }); assert.equal(disabledLogin.response.status, 403); const disabledSessionRead = await jsonRequest(app.baseUrl, "/api/items", { token: buyerRegister.payload.token }); assert.equal(disabledSessionRead.response.status, 403); const disableSelf = await jsonRequest(app.baseUrl, `/api/admin/users/${adminRegister.payload.user.id}/status`, { method: "PATCH", token: adminRegister.payload.token, body: { status: "disabled" }, }); assert.equal(disableSelf.response.status, 400); } finally { await app.close(); } }); test("plugin tokens can capture to one user but cannot read private library", async () => { const app = await startServer(); try { const register = await jsonRequest(app.baseUrl, "/api/auth/register", { method: "POST", body: { email: "plugin-owner@example.com", password: "secret123", name: "插件用户" }, }); const sessionToken = register.payload.token; const createdToken = await jsonRequest(app.baseUrl, "/api/plugin-tokens", { method: "POST", token: sessionToken, body: { name: "Chrome 云采集插件" }, }); assert.equal(createdToken.response.status, 201); assert.ok(createdToken.payload.token.startsWith("pst_")); assert.equal(createdToken.payload.pluginToken.name, "Chrome 云采集插件"); assert.equal(createdToken.payload.pluginToken.tokenHash, undefined); const createdCode = await jsonRequest(app.baseUrl, "/api/admin/credit-codes", { method: "POST", token: sessionToken, body: { credits: 3 }, }); const redeemed = await jsonRequest(app.baseUrl, "/api/credits/redeem", { method: "POST", token: sessionToken, body: { code: createdCode.payload.code }, }); assert.equal(redeemed.response.status, 200); const pluginCapture = await jsonRequest(app.baseUrl, "/api/captures", { method: "POST", token: createdToken.payload.token, body: { items: [ { platform: "xiaohongshu", title: "插件云端采集珍珠耳饰", url: "https://www.xiaohongshu.com/explore/plugin-token", priceText: "¥59", }, ], }, }); assert.equal(pluginCapture.response.status, 201); assert.equal(pluginCapture.payload.items[0].title, "插件云端采集珍珠耳饰"); const pluginRead = await jsonRequest(app.baseUrl, "/api/items", { token: createdToken.payload.token }); assert.equal(pluginRead.response.status, 403); const ownerRead = await jsonRequest(app.baseUrl, "/api/items", { token: sessionToken }); assert.equal(ownerRead.response.status, 200); assert.deepEqual( ownerRead.payload.items.map((item) => item.title), ["插件云端采集珍珠耳饰"], ); } finally { await app.close(); } }); test("plugin tokens track usage and can be disabled or deleted by owner", async () => { const app = await startServer(); try { const register = await jsonRequest(app.baseUrl, "/api/auth/register", { method: "POST", body: { email: "token-owner@example.com", password: "secret123", name: "Token 用户" }, }); const sessionToken = register.payload.token; const createdToken = await jsonRequest(app.baseUrl, "/api/plugin-tokens", { method: "POST", token: sessionToken, body: { name: "直播间采集插件" }, }); assert.equal(createdToken.response.status, 201); assert.equal(createdToken.payload.pluginToken.status, "active"); assert.equal(createdToken.payload.pluginToken.usageCount, 0); assert.equal(createdToken.payload.pluginToken.lastUsedAt, null); const createdCode = await jsonRequest(app.baseUrl, "/api/admin/credit-codes", { method: "POST", token: sessionToken, body: { credits: 2 }, }); const redeemed = await jsonRequest(app.baseUrl, "/api/credits/redeem", { method: "POST", token: sessionToken, body: { code: createdCode.payload.code }, }); assert.equal(redeemed.response.status, 200); const pluginCapture = await jsonRequest(app.baseUrl, "/api/captures", { method: "POST", token: createdToken.payload.token, body: { items: [ { platform: "1688", title: "Token 统计藏式手链", url: "https://detail.1688.com/offer/token-usage.html", priceText: "¥6.80", }, ], }, }); assert.equal(pluginCapture.response.status, 201); const tokenListAfterUse = await jsonRequest(app.baseUrl, "/api/plugin-tokens", { token: sessionToken }); assert.equal(tokenListAfterUse.response.status, 200); assert.equal(tokenListAfterUse.payload.pluginTokens[0].usageCount, 1); assert.ok(tokenListAfterUse.payload.pluginTokens[0].lastUsedAt); const disabledToken = await jsonRequest(app.baseUrl, `/api/plugin-tokens/${createdToken.payload.pluginToken.id}`, { method: "PATCH", token: sessionToken, body: { status: "disabled" }, }); assert.equal(disabledToken.response.status, 200); assert.equal(disabledToken.payload.pluginToken.status, "disabled"); const disabledCapture = await jsonRequest(app.baseUrl, "/api/captures", { method: "POST", token: createdToken.payload.token, body: { items: [ { platform: "1688", title: "禁用 token 不应采集", url: "https://detail.1688.com/offer/token-disabled.html", priceText: "¥7.80", }, ], }, }); assert.equal(disabledCapture.response.status, 403); const deleteToken = await jsonRequest(app.baseUrl, `/api/plugin-tokens/${createdToken.payload.pluginToken.id}`, { method: "DELETE", token: sessionToken, }); assert.equal(deleteToken.response.status, 200); assert.equal(deleteToken.payload.deleted, 1); const tokenListAfterDelete = await jsonRequest(app.baseUrl, "/api/plugin-tokens", { token: sessionToken }); assert.equal(tokenListAfterDelete.response.status, 200); assert.deepEqual(tokenListAfterDelete.payload.pluginTokens, []); } finally { await app.close(); } }); test("plugin token owner can copy token again from token list", async () => { const app = await startServer(); try { const ownerRegister = await jsonRequest(app.baseUrl, "/api/auth/register", { method: "POST", body: { email: "copy-token-owner@example.com", password: "secret123", name: "可复制 Token 用户" }, }); const otherRegister = await jsonRequest(app.baseUrl, "/api/auth/register", { method: "POST", body: { email: "copy-token-other@example.com", password: "secret123", name: "其他用户" }, }); const createdToken = await jsonRequest(app.baseUrl, "/api/plugin-tokens", { method: "POST", token: ownerRegister.payload.token, body: { name: "可再次复制的插件" }, }); assert.equal(createdToken.response.status, 201); assert.ok(createdToken.payload.token.startsWith("pst_")); const ownerList = await jsonRequest(app.baseUrl, "/api/plugin-tokens", { token: ownerRegister.payload.token }); assert.equal(ownerList.response.status, 200); assert.equal(ownerList.payload.pluginTokens.length, 1); assert.equal(ownerList.payload.pluginTokens[0].token, createdToken.payload.token); assert.equal(ownerList.payload.pluginTokens[0].tokenHash, undefined); const otherList = await jsonRequest(app.baseUrl, "/api/plugin-tokens", { token: otherRegister.payload.token }); assert.equal(otherList.response.status, 200); assert.deepEqual(otherList.payload.pluginTokens, []); } finally { await app.close(); } }); test("admin can issue credit codes and buyers redeem them into account credits", async () => { const app = await startServer(); try { const adminRegister = await jsonRequest(app.baseUrl, "/api/auth/register", { method: "POST", body: { email: "credit-admin@example.com", password: "secret123", name: "积分管理员" }, }); const buyerRegister = await jsonRequest(app.baseUrl, "/api/auth/register", { method: "POST", body: { email: "credit-buyer@example.com", password: "secret123", name: "积分买手" }, }); assert.equal(adminRegister.payload.user.credits, 0); assert.equal(buyerRegister.payload.user.credits, 0); const buyerCannotIssue = await jsonRequest(app.baseUrl, "/api/admin/credit-codes", { method: "POST", token: buyerRegister.payload.token, body: { credits: 30 }, }); assert.equal(buyerCannotIssue.response.status, 403); const createdCode = await jsonRequest(app.baseUrl, "/api/admin/credit-codes", { method: "POST", token: adminRegister.payload.token, body: { credits: 30, note: "首批采集额度" }, }); assert.equal(createdCode.response.status, 201); assert.equal(createdCode.payload.creditCode.credits, 30); assert.equal(createdCode.payload.creditCode.status, "active"); assert.match(createdCode.payload.code, /^CDX-[A-Z0-9-]+$/); const codeList = await jsonRequest(app.baseUrl, "/api/admin/credit-codes", { token: adminRegister.payload.token }); assert.equal(codeList.response.status, 200); assert.equal(codeList.payload.creditCodes.length, 1); assert.equal(codeList.payload.creditCodes[0].code, createdCode.payload.code); assert.equal(codeList.payload.creditCodes[0].redeemedBy, null); const redeemed = await jsonRequest(app.baseUrl, "/api/credits/redeem", { method: "POST", token: buyerRegister.payload.token, body: { code: createdCode.payload.code.toLowerCase() }, }); assert.equal(redeemed.response.status, 200); assert.equal(redeemed.payload.user.credits, 30); assert.equal(redeemed.payload.creditCode.status, "redeemed"); const me = await jsonRequest(app.baseUrl, "/api/me", { token: buyerRegister.payload.token }); assert.equal(me.response.status, 200); assert.equal(me.payload.user.credits, 30); const redeemAgain = await jsonRequest(app.baseUrl, "/api/credits/redeem", { method: "POST", token: buyerRegister.payload.token, body: { code: createdCode.payload.code }, }); assert.equal(redeemAgain.response.status, 409); } finally { await app.close(); } }); test("admin manages AI settings without exposing API keys to buyers", async () => { const app = await startServer(); try { const adminRegister = await jsonRequest(app.baseUrl, "/api/auth/register", { method: "POST", body: { email: "ai-admin@example.com", password: "secret123", name: "AI 管理员" }, }); const buyerRegister = await jsonRequest(app.baseUrl, "/api/auth/register", { method: "POST", body: { email: "ai-buyer@example.com", password: "secret123", name: "AI 买手" }, }); const buyerRead = await jsonRequest(app.baseUrl, "/api/admin/ai-settings", { token: buyerRegister.payload.token }); assert.equal(buyerRead.response.status, 403); const saved = await jsonRequest(app.baseUrl, "/api/admin/ai-settings", { method: "PUT", token: adminRegister.payload.token, body: { enabled: true, provider: "deepseek", baseUrl: "https://api.deepseek.com/v1", apiKey: "sk-secret-value", model: "deepseek-chat", temperature: 0.2, maxOutputTokens: 900, timeoutMs: 15000, tokenUnitCost: 2, prompts: { itemAnalysis: "请分析这个商品", }, }, }); assert.equal(saved.response.status, 200); assert.equal(saved.payload.aiSettings.enabled, true); assert.equal(saved.payload.aiSettings.provider, "deepseek"); assert.equal(saved.payload.aiSettings.model, "deepseek-chat"); assert.equal(saved.payload.aiSettings.apiKeyMasked, "sk-...alue"); assert.equal(saved.payload.aiSettings.apiKey, undefined); const readBack = await jsonRequest(app.baseUrl, "/api/admin/ai-settings", { token: adminRegister.payload.token }); assert.equal(readBack.response.status, 200); assert.equal(readBack.payload.aiSettings.apiKeyMasked, "sk-...alue"); assert.equal(readBack.payload.aiSettings.apiKey, undefined); assert.equal(readBack.payload.aiSettings.prompts.itemAnalysis, "请分析这个商品"); } finally { await app.close(); } }); test("admin can issue AI token codes and buyers redeem them into AI token balance", async () => { const app = await startServer(); try { const adminRegister = await jsonRequest(app.baseUrl, "/api/auth/register", { method: "POST", body: { email: "ai-token-admin@example.com", password: "secret123", name: "AI Token 管理员" }, }); const buyerRegister = await jsonRequest(app.baseUrl, "/api/auth/register", { method: "POST", body: { email: "ai-token-buyer@example.com", password: "secret123", name: "AI Token 买手" }, }); assert.equal(buyerRegister.payload.user.aiTokens, 0); const createdCode = await jsonRequest(app.baseUrl, "/api/admin/credit-codes", { method: "POST", token: adminRegister.payload.token, body: { credits: 5, aiTokens: 12000, note: "AI 分析包" }, }); assert.equal(createdCode.response.status, 201); assert.equal(createdCode.payload.creditCode.credits, 5); assert.equal(createdCode.payload.creditCode.aiTokens, 12000); const redeemed = await jsonRequest(app.baseUrl, "/api/credits/redeem", { method: "POST", token: buyerRegister.payload.token, body: { code: createdCode.payload.code }, }); assert.equal(redeemed.response.status, 200); assert.equal(redeemed.payload.user.credits, 5); assert.equal(redeemed.payload.user.aiTokens, 12000); const me = await jsonRequest(app.baseUrl, "/api/me", { token: buyerRegister.payload.token }); assert.equal(me.payload.user.aiTokens, 12000); } finally { await app.close(); } }); test("AI item analysis uses admin model settings and deducts user AI tokens", async () => { const calls = []; const fakeAiClient = { async chat({ settings, messages }) { calls.push({ settings, messages }); return { text: JSON.stringify({ summary: "适合直播测试的低客单商品", purchaseAdvice: "建议先拿样 3 件", sellingPoints: ["视觉记忆点强", "适合组合购"], risks: ["确认材质和售后"], supplierQuestions: ["是否现货?"], }), usage: { inputTokens: 320, outputTokens: 180, totalTokens: 500 }, raw: { id: "chatcmpl_fake" }, }; }, }; const app = await startServer({ aiClient: fakeAiClient }); try { const adminRegister = await jsonRequest(app.baseUrl, "/api/auth/register", { method: "POST", body: { email: "ai-use-admin@example.com", password: "secret123", name: "AI 使用管理员" }, }); const buyerRegister = await jsonRequest(app.baseUrl, "/api/auth/register", { method: "POST", body: { email: "ai-use-buyer@example.com", password: "secret123", name: "AI 使用买手" }, }); await jsonRequest(app.baseUrl, "/api/admin/ai-settings", { method: "PUT", token: adminRegister.payload.token, body: { enabled: true, provider: "openai-compatible", baseUrl: "https://proxy.example.com/v1", apiKey: "sk-proxy-secret", model: "gpt-4o-mini", tokenUnitCost: 1, }, }); const createdCode = await jsonRequest(app.baseUrl, "/api/admin/credit-codes", { method: "POST", token: adminRegister.payload.token, body: { aiTokens: 1000, note: "AI 测试额度" }, }); await jsonRequest(app.baseUrl, "/api/credits/redeem", { method: "POST", token: buyerRegister.payload.token, body: { code: createdCode.payload.code }, }); const capture = await jsonRequest(app.baseUrl, "/api/captures", { method: "POST", token: buyerRegister.payload.token, body: { items: [ { platform: "1688", title: "藏式编绳手机链 低价跑量", url: "https://detail.1688.com/offer/ai-analysis.html", priceText: "¥5.80", supplier: "义乌源头饰品厂", }, ], }, }); const analysis = await jsonRequest(app.baseUrl, "/api/ai/analyze", { method: "POST", token: buyerRegister.payload.token, body: { type: "item_analysis", itemId: capture.payload.items[0].id }, }); assert.equal(analysis.response.status, 200); assert.equal(analysis.payload.analysis.summary, "适合直播测试的低客单商品"); assert.deepEqual(analysis.payload.analysis.sellingPoints, ["视觉记忆点强", "适合组合购"]); assert.equal(analysis.payload.usage.totalTokens, 500); assert.equal(analysis.payload.billing.deductedAiTokens, 500); assert.equal(analysis.payload.billing.remainingAiTokens, 500); assert.equal(calls.length, 1); assert.equal(calls[0].settings.apiKey, "sk-proxy-secret"); assert.equal(calls[0].settings.model, "gpt-4o-mini"); assert.match(calls[0].messages.at(-1).content, /藏式编绳手机链/); const me = await jsonRequest(app.baseUrl, "/api/me", { token: buyerRegister.payload.token }); assert.equal(me.payload.user.aiTokens, 500); } finally { await app.close(); } }); test("AI analysis requires enabled settings and enough AI tokens", async () => { const app = await startServer({ aiClient: { async chat() { throw new Error("AI client should not be called without enough tokens"); }, }, }); try { const adminRegister = await jsonRequest(app.baseUrl, "/api/auth/register", { method: "POST", body: { email: "ai-limit-admin@example.com", password: "secret123", name: "AI 限额管理员" }, }); const buyerRegister = await jsonRequest(app.baseUrl, "/api/auth/register", { method: "POST", body: { email: "ai-limit-buyer@example.com", password: "secret123", name: "AI 限额买手" }, }); const disabled = await jsonRequest(app.baseUrl, "/api/ai/analyze", { method: "POST", token: buyerRegister.payload.token, body: { type: "item_analysis", item: { title: "未启用 AI" } }, }); assert.equal(disabled.response.status, 409); await jsonRequest(app.baseUrl, "/api/admin/ai-settings", { method: "PUT", token: adminRegister.payload.token, body: { enabled: true, baseUrl: "https://proxy.example.com/v1", apiKey: "sk-secret", model: "gpt-4o-mini", }, }); const noBalance = await jsonRequest(app.baseUrl, "/api/ai/analyze", { method: "POST", token: buyerRegister.payload.token, body: { type: "item_analysis", item: { title: "无 AI Token" } }, }); assert.equal(noBalance.response.status, 402); assert.equal(noBalance.payload.aiTokens.remaining, 0); } finally { await app.close(); } }); test("plugin capture requires shared account credits and deducts per saved item", async () => { const app = await startServer(); try { const adminRegister = await jsonRequest(app.baseUrl, "/api/auth/register", { method: "POST", body: { email: "quota-admin@example.com", password: "secret123", name: "额度管理员" }, }); const buyerRegister = await jsonRequest(app.baseUrl, "/api/auth/register", { method: "POST", body: { email: "quota-buyer@example.com", password: "secret123", name: "额度买手" }, }); const buyerToken = buyerRegister.payload.token; const firstPluginToken = await jsonRequest(app.baseUrl, "/api/plugin-tokens", { method: "POST", token: buyerToken, body: { name: "淘宝采集插件" }, }); const secondPluginToken = await jsonRequest(app.baseUrl, "/api/plugin-tokens", { method: "POST", token: buyerToken, body: { name: "1688 采集插件" }, }); assert.ok(firstPluginToken.payload.token.startsWith("pst_")); assert.ok(secondPluginToken.payload.token.startsWith("pst_")); const noCreditCapture = await jsonRequest(app.baseUrl, "/api/captures", { method: "POST", token: firstPluginToken.payload.token, body: { items: [ { platform: "taobao", title: "无积分不应采集", url: "https://item.taobao.com/item.htm?id=no-credit", priceText: "¥39", }, ], }, }); assert.equal(noCreditCapture.response.status, 402); const createdCode = await jsonRequest(app.baseUrl, "/api/admin/credit-codes", { method: "POST", token: adminRegister.payload.token, body: { credits: 3 }, }); const redeemed = await jsonRequest(app.baseUrl, "/api/credits/redeem", { method: "POST", token: buyerToken, body: { code: createdCode.payload.code }, }); assert.equal(redeemed.payload.user.credits, 3); const firstCapture = await jsonRequest(app.baseUrl, "/api/captures", { method: "POST", token: firstPluginToken.payload.token, body: { items: [ { platform: "taobao", title: "积分采集淘宝耳饰", url: "https://item.taobao.com/item.htm?id=credit-a", priceText: "¥39", }, { platform: "xiaohongshu", title: "积分采集小红书手链", url: "https://www.xiaohongshu.com/explore/credit-b", priceText: "¥49", }, ], }, }); assert.equal(firstCapture.response.status, 201); assert.equal(firstCapture.payload.credits.remaining, 1); assert.equal(firstCapture.payload.credits.deducted, 2); const secondCapture = await jsonRequest(app.baseUrl, "/api/captures", { method: "POST", token: secondPluginToken.payload.token, body: { items: [ { platform: "1688", title: "第二个 Token 共用积分", url: "https://detail.1688.com/offer/shared-credit.html", priceText: "¥6.80", }, ], }, }); assert.equal(secondCapture.response.status, 201); assert.equal(secondCapture.payload.credits.remaining, 0); const meAfterCapture = await jsonRequest(app.baseUrl, "/api/me", { token: buyerToken }); assert.equal(meAfterCapture.payload.user.credits, 0); const insufficientCapture = await jsonRequest(app.baseUrl, "/api/captures", { method: "POST", token: firstPluginToken.payload.token, body: { items: [ { platform: "1688", title: "积分耗尽不应采集", url: "https://detail.1688.com/offer/no-more-credit.html", priceText: "¥8.80", }, ], }, }); assert.equal(insufficientCapture.response.status, 402); } finally { await app.close(); } }); test("plugin capture charges duplicate urls in one batch as one saved lead", async () => { const app = await startServer(); try { const adminRegister = await jsonRequest(app.baseUrl, "/api/auth/register", { method: "POST", body: { email: "dedupe-admin@example.com", password: "secret123", name: "去重管理员" }, }); const buyerRegister = await jsonRequest(app.baseUrl, "/api/auth/register", { method: "POST", body: { email: "dedupe-buyer@example.com", password: "secret123", name: "去重买手" }, }); const buyerToken = buyerRegister.payload.token; const pluginToken = await jsonRequest(app.baseUrl, "/api/plugin-tokens", { method: "POST", token: buyerToken, body: { name: "重复链接采集插件" }, }); const createdCode = await jsonRequest(app.baseUrl, "/api/admin/credit-codes", { method: "POST", token: adminRegister.payload.token, body: { credits: 1 }, }); await jsonRequest(app.baseUrl, "/api/credits/redeem", { method: "POST", token: buyerToken, body: { code: createdCode.payload.code }, }); const duplicateCapture = await jsonRequest(app.baseUrl, "/api/captures", { method: "POST", token: pluginToken.payload.token, body: { items: [ { platform: "taobao", title: "重复采集的藏式手链 A", url: "https://item.taobao.com/item.htm?id=duplicate-credit", priceText: "¥49", }, { platform: "taobao", title: "重复采集的藏式手链 B", url: "https://item.taobao.com/item.htm?id=duplicate-credit", priceText: "¥45", }, ], }, }); assert.equal(duplicateCapture.response.status, 201); assert.equal(duplicateCapture.payload.items.length, 1); assert.equal(duplicateCapture.payload.items[0].title, "重复采集的藏式手链 B"); assert.equal(duplicateCapture.payload.credits.deducted, 1); assert.equal(duplicateCapture.payload.credits.remaining, 0); const items = await jsonRequest(app.baseUrl, "/api/items", { token: buyerToken }); assert.equal(items.payload.items.length, 1); } finally { await app.close(); } }); test("plugin capture keeps credits unchanged when saving fails", async () => { const pluginTokenValue = "pst_failure_test_token"; const fakeStore = { user: { id: "usr_failure", email: "failure-buyer@example.com", name: "失败买手", role: "buyer", status: "active", credits: 1, }, pluginToken: { id: "ptk_failure", userId: "usr_failure", tokenHash: hashToken(pluginTokenValue), status: "active", usageCount: 0, }, async userCount() { return 1; }, async findPluginTokenByHash(tokenHash) { if (tokenHash !== this.pluginToken.tokenHash) return null; return { user: this.user, pluginToken: this.pluginToken }; }, async deductCredits(userId, amount) { this.user.credits -= amount; return { deducted: amount, remaining: this.user.credits }; }, async insertMany() { const error = new Error("Simulated save failure"); error.status = 503; throw error; }, async insertManyWithCreditDeduction() { const error = new Error("Simulated save failure"); error.status = 503; throw error; }, }; const dir = join(tmpdir(), `sourcing-api-failed-save-${Date.now()}-${Math.random().toString(36).slice(2)}`); await mkdir(dir, { recursive: true }); const server = createServer({ store: fakeStore, publicDir: join(dir, "public") }); await new Promise((resolve) => server.listen(0, resolve)); const baseUrl = `http://127.0.0.1:${server.address().port}`; try { const failedCapture = await jsonRequest(baseUrl, "/api/captures", { method: "POST", token: pluginTokenValue, body: { items: [ { platform: "1688", title: "保存失败不应扣积分", url: "https://detail.1688.com/offer/failure-credit.html", priceText: "¥6.80", }, ], }, }); assert.equal(failedCapture.response.status, 503); assert.equal(fakeStore.user.credits, 1); assert.equal(fakeStore.pluginToken.usageCount, 0); } finally { await new Promise((resolve) => server.close(resolve)); await rm(dir, { recursive: true, force: true }); } }); test("legacy users without role or status are normalized for admin access", async () => { const dir = join(tmpdir(), `sourcing-api-legacy-users-${Date.now()}-${Math.random().toString(36).slice(2)}`); await mkdir(dir, { recursive: true }); const filePath = join(dir, "items.json"); const store = new JsonStore(filePath); const server = createServer({ store, publicDir: join(dir, "public") }); const admin = await store.createUser({ email: "legacy-admin@example.com", name: "旧管理员", passwordHash: "legacy-admin-hash", }); const buyer = await store.createUser({ email: "legacy-buyer@example.com", name: "旧买手", passwordHash: "legacy-buyer-hash", }); await writeFile( filePath, JSON.stringify( { users: [ { id: admin.id, email: admin.email, name: admin.name, passwordHash: admin.passwordHash, createdAt: admin.createdAt }, { id: buyer.id, email: buyer.email, name: buyer.name, passwordHash: buyer.passwordHash, createdAt: buyer.createdAt }, ], items: [{ id: "legacy-item", userId: buyer.id, title: "旧数据饰品", platform: "1688", createdAt: "2026-01-01T00:00:00.000Z" }], pluginTokens: [{ id: "legacy-token", userId: buyer.id, name: "旧插件", tokenHash: "hash", createdAt: "2026-01-01T00:00:00.000Z" }], sessions: [], rules: {}, }, null, 2, ), "utf8", ); await new Promise((resolve) => server.listen(0, resolve)); const baseUrl = `http://127.0.0.1:${server.address().port}`; try { const adminToken = "sess_legacy_admin_access"; const adminSession = await store.createSession({ userId: admin.id, tokenHash: hashToken(adminToken) }); assert.ok(adminSession.id); const adminUsers = await jsonRequest(baseUrl, "/api/admin/users", { token: adminToken }); assert.equal(adminUsers.response.status, 200); assert.deepEqual( adminUsers.payload.users.map((user) => [user.email, user.role, user.status, user.itemCount, user.pluginTokenCount]), [ ["legacy-admin@example.com", "admin", "active", 0, 0], ["legacy-buyer@example.com", "buyer", "active", 1, 1], ], ); } finally { await new Promise((resolve) => server.close(resolve)); await rm(dir, { recursive: true, force: true }); } }); test("first SaaS user claims existing local captures during migration", async () => { const app = await startServer(); try { const localCapture = await jsonRequest(app.baseUrl, "/api/captures", { method: "POST", body: { items: [ { platform: "1688", title: "注册前本地采集藏式手链", url: "https://detail.1688.com/offer/local-before-register.html", priceText: "¥5.80", }, ], }, }); assert.equal(localCapture.response.status, 201); const register = await jsonRequest(app.baseUrl, "/api/auth/register", { method: "POST", body: { email: "first-user@example.com", password: "secret123", name: "首个用户" }, }); assert.equal(register.response.status, 201); const ownedItems = await jsonRequest(app.baseUrl, "/api/items", { token: register.payload.token }); assert.equal(ownedItems.response.status, 200); assert.deepEqual( ownedItems.payload.items.map((item) => item.title), ["注册前本地采集藏式手链"], ); } finally { await app.close(); } }); test("local API deletes selected captures in one bulk request", async () => { const dir = join(tmpdir(), `sourcing-api-bulk-${Date.now()}`); await mkdir(dir, { recursive: true }); const store = new JsonStore(join(dir, "items.json")); const server = createServer({ store, publicDir: join(dir, "public") }); await new Promise((resolve) => server.listen(0, resolve)); const baseUrl = `http://127.0.0.1:${server.address().port}`; try { const captureResponse = await fetch(`${baseUrl}/api/captures`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: [ { platform: "1688", title: "误采集 A", url: "https://detail.1688.com/offer/api-bulk-a.html", priceText: "¥3.80", }, { platform: "1688", title: "误采集 B", url: "https://detail.1688.com/offer/api-bulk-b.html", priceText: "¥4.80", }, { platform: "1688", title: "保留 C", url: "https://detail.1688.com/offer/api-bulk-c.html", priceText: "¥5.80", }, ], }), }); const { items } = await captureResponse.json(); const deleteResponse = await fetch(`${baseUrl}/api/items/bulk-delete`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ids: [items[0].id, items[1].id, "missing-id"] }), }); assert.equal(deleteResponse.status, 200); const deleted = await deleteResponse.json(); assert.equal(deleted.deleted, 2); assert.deepEqual(deleted.missing, ["missing-id"]); const listResponse = await fetch(`${baseUrl}/api/items`); const listPayload = await listResponse.json(); assert.equal(listPayload.items.length, 1); assert.equal(listPayload.items[0].title, "保留 C"); } finally { await new Promise((resolve) => server.close(resolve)); await rm(dir, { recursive: true, force: true }); } }); test("local API exposes dashboard, groups, sample queue, and bulk updates", async () => { const dir = join(tmpdir(), `sourcing-api-workbench-${Date.now()}`); await mkdir(dir, { recursive: true }); const store = new JsonStore(join(dir, "items.json")); const server = createServer({ store, publicDir: join(dir, "public") }); await new Promise((resolve) => server.listen(0, resolve)); const baseUrl = `http://127.0.0.1:${server.address().port}`; try { const captureResponse = await fetch(`${baseUrl}/api/captures`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: [ { platform: "taobao", title: "藏式绿松石编绳手链 民族风", url: "https://item.taobao.com/item.htm?id=api-style-a", priceText: "¥39.90", }, { platform: "1688", title: "民族风绿松石色编织手链 藏式", url: "https://detail.1688.com/offer/api-style-b.html", priceText: "¥6.80", supplier: "义乌源头厂", }, ], }), }); const { items } = await captureResponse.json(); const bulkResponse = await fetch(`${baseUrl}/api/items/bulk-update`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ids: [items[1].id], patch: { status: "asking_supplier" }, addTags: ["低价跑量"], }), }); assert.equal(bulkResponse.status, 200); assert.equal((await bulkResponse.json()).updated, 1); const dashboardResponse = await fetch(`${baseUrl}/api/dashboard`); assert.equal(dashboardResponse.status, 200); const dashboard = await dashboardResponse.json(); assert.equal(dashboard.dashboard.totals.items, 2); const groupsResponse = await fetch(`${baseUrl}/api/style-groups`); assert.equal(groupsResponse.status, 200); const groups = await groupsResponse.json(); assert.equal(groups.groups.length, 1); assert.equal(groups.groups[0].items.length, 2); const sampleResponse = await fetch(`${baseUrl}/api/sample-queue`); assert.equal(sampleResponse.status, 200); const sampleQueue = await sampleResponse.json(); assert.equal(sampleQueue.items.length, 1); assert.equal(sampleQueue.items[0].status, "asking_supplier"); } finally { await new Promise((resolve) => server.close(resolve)); await rm(dir, { recursive: true, force: true }); } });