const test = require("node:test"); const assert = require("node:assert/strict"); global.document = { cookie: "x=y" }; global.GM_xmlhttpRequest = undefined; const api = require("./xhs-pgy-export.user.js"); function okJson(payload) { return { ok: true, status: 200, async json() { return payload; }, }; } test("data_summary request includes business=1", async () => { const urls = []; async function fetchImpl(url) { urls.push(url); if (String(url).startsWith(api.API_BASE)) { return okJson({ data: { userId: "u-123" } }); } return okJson({ data: {} }); } await api.fetchMergedBloggerRecord("any-id", fetchImpl); const target = urls.find((u) => String(u).includes("/v1/pugongying/data_summary")); assert.ok(target, "expected a call to /v1/pugongying/data_summary"); const parsed = new URL(target); assert.equal(parsed.searchParams.get("business"), "1"); assert.equal(parsed.searchParams.get("userId"), "u-123"); }); test("supplemental proxy requests include cookie query parameter", async () => { const urls = []; async function fetchImpl(url) { urls.push(String(url)); if (String(url).startsWith(api.API_BASE)) { return okJson({ data: { userId: "u-123" } }); } return okJson({ data: {} }); } await api.fetchMergedBloggerRecord("any-id", fetchImpl); const proxyUrls = urls.filter((url) => url.includes("/v1/pugongying/")); assert.equal(proxyUrls.length, 2); for (const url of proxyUrls) { const parsed = new URL(url); assert.equal(parsed.searchParams.get("cookie"), "x=y"); } }); test("extractBloggerId resolves xhs short link from SSR profile HTML", async () => { const previousGm = global.GM_xmlhttpRequest; global.GM_xmlhttpRequest = (options) => { assert.equal(options.url, "https://xhslink.com/m/example"); options.onload({ status: 200, finalUrl: "https://www.xiaohongshu.com/user/profile/2022919XN?xsec_source=app_share", responseText: '', }); }; try { await assert.doesNotReject(async () => { assert.equal( await api.extractBloggerId("https://xhslink.com/m/example"), "60379f3c000000000101e53f", ); }); } finally { global.GM_xmlhttpRequest = previousGm; } }); test("buildFeishuSheetValues uses labels as header row", () => { const records = [ { flattened: { userId: "u-1", name: "达人 A", fansCount: 123, }, }, ]; const values = api.buildFeishuSheetValues(records, ["userId", "name", "fansCount"]); assert.deepEqual(values, [ ["达人ID", "达人昵称", "粉丝数"], ["u-1", "达人 A", 123], ]); }); test("buildFeishuRange expands columns past Z", () => { assert.equal(api.buildFeishuRange("abc123", 1, 1), "abc123!A1:A1"); assert.equal(api.buildFeishuRange("abc123", 2, 28), "abc123!A1:AB2"); }); test("exportRecordsToFeishuSpreadsheet creates spreadsheet then writes values", async () => { const calls = []; async function fetchImpl(url, options) { const body = options && options.body ? JSON.parse(options.body) : null; calls.push({ url: String(url), method: options && options.method, headers: options && options.headers, body, }); if (String(url).includes("/auth/v3/tenant_access_token/internal")) { return okJson({ code: 0, tenant_access_token: "tenant-token" }); } if (String(url).includes("/sheets/v3/spreadsheets")) { return okJson({ code: 0, data: { spreadsheet: { spreadsheet_token: "spreadsheet-token", url: "https://feishu.example/spreadsheet-token", }, }, }); } if (String(url).includes("/metainfo")) { return okJson({ code: 0, data: { sheets: [ { sheetId: "sheet-a", title: "Sheet1", }, ], }, }); } if (String(url).includes("/values_batch_update")) { return okJson({ code: 0, data: {} }); } throw new Error(`unexpected url: ${url}`); } const result = await api.exportRecordsToFeishuSpreadsheet({ appId: "cli_xxx", appSecret: "secret", title: "测试导出", records: [ { flattened: { userId: "u-1", name: "达人 A", }, }, ], fields: ["userId", "name"], fetchImpl, }); assert.equal(result.spreadsheetToken, "spreadsheet-token"); assert.equal(result.url, "https://feishu.example/spreadsheet-token"); assert.equal(result.sheetId, "sheet-a"); assert.equal(calls.length, 4); assert.deepEqual(calls[0].body, { app_id: "cli_xxx", app_secret: "secret", }); assert.equal(calls[1].body.title, "测试导出"); assert.equal(calls[2].method, "GET"); assert.equal(calls[3].headers.Authorization, "Bearer tenant-token"); assert.deepEqual(calls[3].body, { valueRanges: [ { range: "sheet-a!A1:B2", values: [ ["达人ID", "达人昵称"], ["u-1", "达人 A"], ], }, ], }); }); test("exportRecordsToFeishuBitable creates app fields and records", async () => { const calls = []; async function fetchImpl(url, options) { const body = options && options.body ? JSON.parse(options.body) : null; calls.push({ url: String(url), method: options && options.method, headers: options && options.headers, body, }); if (String(url).includes("/auth/v3/tenant_access_token/internal")) { return okJson({ code: 0, tenant_access_token: "tenant-token" }); } if (String(url).endsWith("/bitable/v1/apps")) { return okJson({ code: 0, data: { app: { app_token: "base-token", default_table_id: "tbl-default", url: "https://feishu.example/base-token", }, }, }); } if (String(url).includes("/fields")) { return okJson({ code: 0, data: { field: { field_id: "fld-created" } } }); } if (String(url).includes("/records/batch_create")) { return okJson({ code: 0, data: { records: [{ record_id: "rec-1" }] } }); } throw new Error(`unexpected url: ${url}`); } const result = await api.exportRecordsToFeishuBitable({ appId: "cli_xxx", appSecret: "secret", title: "测试多维表格", records: [ { flattened: { userId: "u-1", name: "达人 A", }, }, ], fields: ["userId", "name"], fetchImpl, }); assert.equal(result.appToken, "base-token"); assert.equal(result.tableId, "tbl-default"); assert.equal(result.url, "https://feishu.example/base-token"); assert.equal(result.rowCount, 1); assert.equal(calls.length, 5); assert.deepEqual(calls[0].body, { app_id: "cli_xxx", app_secret: "secret", }); assert.deepEqual(calls[1].body, { name: "测试多维表格", }); assert.equal(calls[2].url.endsWith("/bitable/v1/apps/base-token/tables/tbl-default/fields"), true); assert.deepEqual(calls[2].body, { field_name: "达人ID", type: 1, }); assert.deepEqual(calls[3].body, { field_name: "达人昵称", type: 1, }); assert.equal( calls[4].url.endsWith( "/bitable/v1/apps/base-token/tables/tbl-default/records/batch_create", ), true, ); assert.deepEqual(calls[4].body, { records: [ { fields: { "达人ID": "u-1", "达人昵称": "达人 A", }, }, ], }); }); test("writeFeishuBitableRecords writes records in 500-row batches", async () => { const batches = []; async function fetchImpl(url, options) { assert.equal( String(url).endsWith("/bitable/v1/apps/base-token/tables/tbl-default/records/batch_create"), true, ); batches.push(JSON.parse(options.body).records.length); return okJson({ code: 0, data: {} }); } const records = Array.from({ length: 501 }, (_, index) => ({ fields: { "达人ID": `u-${index + 1}`, }, })); const result = await api.writeFeishuBitableRecords({ tenantAccessToken: "tenant-token", appToken: "base-token", tableId: "tbl-default", records, fetchImpl, }); assert.deepEqual(batches, [500, 1]); assert.equal(result.writtenCount, 501); }); test("controller reads Feishu credentials from localStorage instead of bundled secrets", async () => { const previousLocalStorage = global.localStorage; const previousGm = global.GM_xmlhttpRequest; const sentAuthBodies = []; global.GM_xmlhttpRequest = undefined; global.localStorage = { getItem(key) { if (key === "xhs-pgy-export:feishu-app-id") { return JSON.stringify("cli_from_storage"); } if (key === "xhs-pgy-export:feishu-app-secret") { return JSON.stringify("secret_from_storage"); } return null; }, setItem() {}, }; async function fetchImpl(url, options) { const body = options && options.body ? JSON.parse(options.body) : null; if (String(url).startsWith(api.API_BASE)) { return okJson({ code: 0, data: { id: "60379f3c000000000101e53f", userId: "60379f3c000000000101e53f", name: "达人 A", }, }); } if (String(url).includes("/auth/v3/tenant_access_token/internal")) { sentAuthBodies.push(body); return okJson({ code: 0, tenant_access_token: "tenant-token" }); } if (String(url).includes("/sheets/v3/spreadsheets")) { return okJson({ code: 0, data: { spreadsheet: { spreadsheet_token: "spreadsheet-token", }, }, }); } if (String(url).includes("/metainfo")) { return okJson({ code: 0, data: { sheets: [{ sheetId: "sheet-a" }] } }); } if (String(url).includes("/values_batch_update")) { return okJson({ code: 0, data: {} }); } throw new Error(`unexpected url: ${url}`); } try { const controller = api.createExportController({ fetchImpl }); await controller.preview("60379f3c000000000101e53f"); await controller.exportFeishuSpreadsheet(["userId", "name"]); assert.deepEqual(sentAuthBodies, [ { app_id: "cli_from_storage", app_secret: "secret_from_storage", }, ]); } finally { global.localStorage = previousLocalStorage; global.GM_xmlhttpRequest = previousGm; } }); test("controller saves Feishu credentials for later exports", async () => { const previousLocalStorage = global.localStorage; const previousGm = global.GM_xmlhttpRequest; const storage = new Map(); const sentAuthBodies = []; global.GM_xmlhttpRequest = undefined; global.localStorage = { getItem(key) { return storage.has(key) ? storage.get(key) : null; }, setItem(key, value) { storage.set(key, value); }, }; async function fetchImpl(url, options) { const body = options && options.body ? JSON.parse(options.body) : null; if (String(url).startsWith(api.API_BASE)) { return okJson({ code: 0, data: { id: "60379f3c000000000101e53f", userId: "60379f3c000000000101e53f", name: "达人 A", }, }); } if (String(url).includes("/auth/v3/tenant_access_token/internal")) { sentAuthBodies.push(body); return okJson({ code: 0, tenant_access_token: "tenant-token" }); } if (String(url).includes("/sheets/v3/spreadsheets")) { return okJson({ code: 0, data: { spreadsheet: { spreadsheet_token: "spreadsheet-token", }, }, }); } if (String(url).includes("/metainfo")) { return okJson({ code: 0, data: { sheets: [{ sheetId: "sheet-a" }] } }); } if (String(url).includes("/values_batch_update")) { return okJson({ code: 0, data: {} }); } throw new Error(`unexpected url: ${url}`); } try { const controller = api.createExportController({ fetchImpl }); controller.saveFeishuCredentials({ appId: " cli_saved ", appSecret: " secret_saved ", }); await controller.preview("60379f3c000000000101e53f"); await controller.exportFeishuSpreadsheet(["userId", "name"]); assert.equal(storage.get("xhs-pgy-export:feishu-app-id"), JSON.stringify("cli_saved")); assert.equal( storage.get("xhs-pgy-export:feishu-app-secret"), JSON.stringify("secret_saved"), ); assert.deepEqual(sentAuthBodies, [ { app_id: "cli_saved", app_secret: "secret_saved", }, ]); } finally { global.localStorage = previousLocalStorage; global.GM_xmlhttpRequest = previousGm; } }); test("controller uses GM_xmlhttpRequest for Feishu even when page fetch is available", async () => { const gmUrls = []; const pageFetchUrls = []; const previousGm = global.GM_xmlhttpRequest; const previousLocalStorage = global.localStorage; global.localStorage = { getItem(key) { if (key === "xhs-pgy-export:feishu-app-id") { return JSON.stringify("cli_from_storage"); } if (key === "xhs-pgy-export:feishu-app-secret") { return JSON.stringify("secret_from_storage"); } return null; }, setItem() {}, }; global.GM_xmlhttpRequest = (options) => { gmUrls.push(options.url); let payload; if (String(options.url).includes("/auth/v3/tenant_access_token/internal")) { payload = { code: 0, tenant_access_token: "tenant-token" }; } else if (String(options.url).includes("/sheets/v3/spreadsheets")) { payload = { code: 0, data: { spreadsheet: { spreadsheet_token: "spreadsheet-token", url: "https://feishu.example/spreadsheet-token", }, }, }; } else if (String(options.url).includes("/metainfo")) { payload = { code: 0, data: { sheets: [{ sheetId: "sheet-a" }] }, }; } else if (String(options.url).includes("/values_batch_update")) { payload = { code: 0, data: {} }; } else { throw new Error(`unexpected GM url: ${options.url}`); } options.onload({ status: 200, responseText: JSON.stringify(payload), }); }; try { async function pageFetch(url) { pageFetchUrls.push(String(url)); if (String(url).startsWith(api.API_BASE)) { return okJson({ code: 0, data: { id: "60379f3c000000000101e53f", userId: "60379f3c000000000101e53f", name: "达人 A", }, }); } return okJson({ code: 0, data: {} }); } const controller = api.createExportController({ fetchImpl: pageFetch }); await controller.preview("60379f3c000000000101e53f"); const result = await controller.exportFeishuSpreadsheet(["userId", "name"]); assert.equal(result.spreadsheetToken, "spreadsheet-token"); assert.equal(gmUrls.filter((url) => String(url).includes("open.feishu.cn")).length, 4); assert.equal( pageFetchUrls.some((url) => url.includes("open.feishu.cn")), false, "Feishu requests should not use page fetch because browsers block cross-origin OpenAPI calls", ); } finally { global.GM_xmlhttpRequest = previousGm; global.localStorage = previousLocalStorage; } });