/);
-});
-
-test("buildCsvContent adds BOM and escapes commas, quotes and newlines", () => {
- const csv = buildCsvContent({
- columns: ["id", "name"],
- headers: ["达人ID", "达人昵称"],
- rows: [{ id: "1", name: 'A,"B"\n达人' }],
- });
-
- assert.equal(csv.charCodeAt(0), 0xfeff);
- assert.match(csv, /达人ID,达人昵称/);
- assert.match(csv, /1,"A,""B""\n达人"/);
-});
-
-test("createExportController previews and exports creator data", async () => {
- const seenRequests = [];
- const controller = createExportController({
- fetchImpl: async (url, options) => {
- seenRequests.push({ url, options });
- if (!url.includes("/api/solar/cooperator/user/blogger/")) {
- return {
- ok: false,
- status: 404,
- json: async () => ({}),
- };
- }
-
- const id = url.split("/").pop();
- return {
- ok: true,
- json: async () => ({
- success: true,
- data: {
- id,
- name: `达人-${id.slice(-4)}`,
- metrics: {
- fans: Number(id.slice(-2)),
- },
- },
- }),
- };
- },
- now: () => new Date("2026-03-12T08:09:10Z"),
- });
-
- const preview = await controller.preview(`
- 5776652682ec3912d6f508d5
- 5f1234567890abcdef123456
- `);
-
- assert.equal(seenRequests.length, 8);
- assert.equal(preview.records.length, 2);
- assert.deepEqual(preview.selectedFields, ["id", "metrics.fans", "name"]);
- assert.equal(preview.fields.find((field) => field.path === "name").label, "达人昵称");
- assert.deepEqual(
- preview.fields.map((field) => Object.keys(field).sort()),
- preview.fields.map(() => ["label", "path"]),
- );
-
- const exported = controller.exportSheet(["id", "name", "metrics.fans"]);
- assert.equal(exported.filename, "xhs-bloggers-20260312-160910.xlsx");
- assert.equal(exported.rows.length, 2);
- assert.deepEqual(exported.headers, ["ID", "达人昵称", "粉丝数"]);
- assert.ok(Buffer.isBuffer(exported.content));
- assert.equal(exported.content[0], 0x50); // P
- assert.equal(exported.content[1], 0x4b); // K
-});
-
-test("createExportController merges supplemental endpoint payloads into namespaced fields", async () => {
- const seenUrls = [];
- const controller = createExportController({
- fetchImpl: async (url) => {
- seenUrls.push(url);
- if (url.includes("/api/solar/cooperator/user/blogger/")) {
- return {
- ok: true,
- json: async () => ({
- data: {
- id: "61f27a60000000001000cb5f",
- userId: "61f27a60000000001000cb5f",
- name: "测试达人",
- },
- }),
- };
- }
-
- if (url.includes("/api/pgy/kol/data/data_summary")) {
- return {
- ok: true,
- json: async () => ({
- data: {
- avgRead: 1200,
- avgInteract: 98,
- },
- }),
- };
- }
-
- if (url.includes("/api/solar/kol/data_v3/fans_summary")) {
- return {
- ok: true,
- json: async () => ({
- data: {
- cityDistribution: ["上海", "杭州"],
- maleRate: 0.22,
- },
- }),
- };
- }
-
- if (url.includes("/api/solar/kol/data/61f27a60000000001000cb5f/fans_profile")) {
- return {
- ok: true,
- json: async () => ({
- data: {
- age18_24: 0.31,
- age25_34: 0.44,
- },
- }),
- };
- }
-
- throw new Error(`unexpected url: ${url}`);
- },
- });
-
- const preview = await controller.preview("61f27a60000000001000cb5f");
-
- assert.equal(seenUrls.length, 4);
- assert.ok(
- seenUrls.some((url) =>
- url.includes("/api/pgy/kol/data/data_summary?userId=61f27a60000000001000cb5f&business=1"),
- ),
- );
- assert.ok(
- seenUrls.some((url) =>
- url.includes("/api/solar/kol/data_v3/fans_summary?userId=61f27a60000000001000cb5f"),
- ),
- );
- assert.ok(
- seenUrls.some((url) =>
- url.includes("/api/solar/kol/data/61f27a60000000001000cb5f/fans_profile"),
- ),
- );
- assert.equal(preview.records[0].raw.dataSummary.avgRead, 1200);
- assert.equal(preview.records[0].raw.fansSummary.maleRate, 0.22);
- assert.equal(preview.records[0].raw.fansProfile.age18_24, 0.31);
- assert.equal(preview.records[0].flattened["dataSummary.avgRead"], "1200");
- assert.equal(preview.records[0].flattened["fansSummary.cityDistribution"], "上海 | 杭州");
- assert.equal(preview.fields.find((field) => field.path === "dataSummary.avgRead").label, "平均阅读量");
- assert.equal(
- preview.fields.find((field) => field.path === "fansProfile.age18_24"),
- undefined,
- );
-});
-
-test("createExportController applies mapped Chinese headers for provided supplemental sample fields", async () => {
- const controller = createExportController({
- fetchImpl: async (url) => {
- if (url.includes("/api/solar/cooperator/user/blogger/")) {
- return {
- ok: true,
- json: async () => ({
- data: {
- id: "61f27a60000000001000cb5f",
- userId: "61f27a60000000001000cb5f",
- name: "李欢喜",
- },
- }),
- };
- }
-
- if (url.includes("/api/pgy/kol/data/data_summary")) {
- return {
- ok: true,
- json: async () => ({
- data: {
- fans30GrowthRate: "4.7",
- activeDayInLast7: 7,
- noteType: [{ contentTag: "母婴", percent: "100.0" }],
- responseRate: "95.6",
- },
- }),
- };
- }
-
- if (url.includes("/api/solar/kol/data_v3/fans_summary")) {
- return {
- ok: true,
- json: async () => ({
- data: {},
- }),
- };
- }
-
- if (url.includes("/fans_profile")) {
- return {
- ok: true,
- json: async () => ({
- data: {
- ages: [{ group: "25-34", percent: 0.67 }],
- gender: { male: 0.12, female: 0.88 },
- interests: [{ name: "母婴", percent: 0.17 }],
- provinces: [{ name: "广东", percent: 0.14 }],
- cities: [{ name: "广州", percent: 0.03 }],
- devices: [{ name: "apple inc.", desc: "苹果", percent: 0.28 }],
- dateKey: "2026-03-11",
- },
- }),
- };
- }
-
- throw new Error(`unexpected url: ${url}`);
- },
- });
-
- await controller.preview("61f27a60000000001000cb5f");
- const exported = controller.exportSheet([
- "name",
- "dataSummary.fans30GrowthRate",
- "dataSummary.activeDayInLast7",
- "dataSummary.noteType",
- "dataSummary.responseRate",
- "fansProfile.ages",
- "fansProfile.gender.male",
- "fansProfile.gender.female",
- "fansProfile.interests",
- "fansProfile.provinces",
- "fansProfile.cities",
- "fansProfile.devices",
- "fansProfile.dateKey",
- ]);
-
- assert.deepEqual(exported.headers, [
- "达人昵称",
- "近30天涨粉率",
- "近7天活跃天数",
- "笔记内容类型",
- "响应率",
- "粉丝年龄分布",
- "粉丝男性占比",
- "粉丝女性占比",
- "粉丝兴趣分布",
- "粉丝省份分布",
- "粉丝城市分布",
- "粉丝设备分布",
- "画像日期",
- ]);
-});
-
-test("createExportController applies mapped Chinese headers for provided fansSummary fields", async () => {
- const controller = createExportController({
- fetchImpl: async (url) => {
- if (url.includes("/api/solar/cooperator/user/blogger/")) {
- return {
- ok: true,
- json: async () => ({
- data: {
- id: "61f27a60000000001000cb5f",
- userId: "61f27a60000000001000cb5f",
- name: "李欢喜",
- },
- }),
- };
- }
-
- if (url.includes("/api/pgy/kol/data/data_summary")) {
- return { ok: true, json: async () => ({ data: {} }) };
- }
-
- if (url.includes("/api/solar/kol/data_v3/fans_summary")) {
- return {
- ok: true,
- json: async () => ({
- data: {
- fansNum: 11824,
- fansIncreaseNum: 534,
- fansGrowthRate: "4.7",
- fansGrowthBeyondRate: "90.3",
- activeFansL28: 4329,
- activeFansRate: "36.6",
- activeFansBeyondRate: "31.4",
- engageFansRate: "8.0",
- engageFansL30: 946,
- engageFansBeyondRate: "97.6",
- readFansIn30: 1343,
- readFansRate: "11.4",
- readFansBeyondRate: "89.0",
- payFansUserRate30d: "2.7",
- payFansUserNum30d: 320,
- },
- }),
- };
- }
-
- if (url.includes("/fans_profile")) {
- return { ok: true, json: async () => ({ data: {} }) };
- }
-
- throw new Error(`unexpected url: ${url}`);
- },
- });
-
- await controller.preview("61f27a60000000001000cb5f");
- const exported = controller.exportSheet([
- "fansSummary.fansNum",
- "fansSummary.fansIncreaseNum",
- "fansSummary.fansGrowthRate",
- "fansSummary.fansGrowthBeyondRate",
- "fansSummary.activeFansL28",
- "fansSummary.activeFansRate",
- "fansSummary.activeFansBeyondRate",
- "fansSummary.engageFansRate",
- "fansSummary.engageFansL30",
- "fansSummary.engageFansBeyondRate",
- "fansSummary.readFansIn30",
- "fansSummary.readFansRate",
- "fansSummary.readFansBeyondRate",
- "fansSummary.payFansUserRate30d",
- "fansSummary.payFansUserNum30d",
- ]);
-
- assert.deepEqual(exported.headers, [
- "粉丝总数",
- "涨粉数",
- "粉丝增长率",
- "粉丝增长超越率",
- "近28天活跃粉丝数",
- "活跃粉丝占比",
- "活跃粉丝超越率",
- "互动粉丝占比",
- "近30天互动粉丝数",
- "互动粉丝超越率",
- "近30天阅读粉丝数",
- "阅读粉丝占比",
- "阅读粉丝超越率",
- "近30天支付粉丝占比",
- "近30天支付粉丝数",
- ]);
-});
-
-test("createExportController tolerates supplemental endpoint failures", async () => {
- const controller = createExportController({
- fetchImpl: async (url) => {
- if (url.includes("/api/solar/cooperator/user/blogger/")) {
- return {
- ok: true,
- json: async () => ({
- data: {
- id: "61f27a60000000001000cb5f",
- userId: "61f27a60000000001000cb5f",
- name: "测试达人",
- },
- }),
- };
- }
-
- return {
- ok: false,
- status: 500,
- json: async () => ({}),
- };
- },
- });
-
- const preview = await controller.preview("61f27a60000000001000cb5f");
- assert.equal(preview.records[0].raw.name, "测试达人");
- assert.equal(preview.records[0].raw.dataSummary, undefined);
- assert.equal(preview.records[0].flattened.name, "测试达人");
-});
-
-test("createExportController rejects empty input and request failures", async () => {
- const controller = createExportController({
- fetchImpl: async () => ({
- ok: false,
- status: 403,
- json: async () => ({}),
- }),
- });
-
- await assert.rejects(
- controller.preview(""),
- /请输入至少一个有效的达人主页链接或达人 ID/,
- );
-
- await assert.rejects(
- controller.preview("5776652682ec3912d6f508d5"),
- /请求达人 5776652682ec3912d6f508d5 失败,状态码:403/,
- );
-
- assert.throws(
- () => controller.exportSheet(["id"]),
- /请先读取字段并确认达人数据/,
- );
-});
diff --git a/pugongying/xhs-pgy-export.user.js b/pugongying/xhs-pgy-export.user.js
index 4885b48..14ed0d7 100644
--- a/pugongying/xhs-pgy-export.user.js
+++ b/pugongying/xhs-pgy-export.user.js
@@ -17,20 +17,6 @@
const API_BASE =
"https://pgy.xiaohongshu.com/api/solar/cooperator/user/blogger/";
const SUPPLEMENTAL_ENDPOINTS = [
- {
- namespace: "dataSummary",
- buildUrl: (userId) =>
- `https://pgy.xiaohongshu.com/api/pgy/kol/data/data_summary?userId=${encodeURIComponent(
- userId,
- )}&business=1`,
- },
- {
- namespace: "fansSummary",
- buildUrl: (userId) =>
- `https://pgy.xiaohongshu.com/api/solar/kol/data_v3/fans_summary?userId=${encodeURIComponent(
- userId,
- )}`,
- },
{
namespace: "fansProfile",
buildUrl: (userId) =>
@@ -40,28 +26,14 @@
},
];
const NAMESPACE_LABEL_MAP = {
- dataSummary: "数据概览",
- fansSummary: "粉丝概览",
fansProfile: "粉丝画像",
};
const FIELD_LABEL_MAP = {
id: "ID",
- "metrics.fans": "粉丝数",
- dataSummary: "数据概览",
- fansSummary: "粉丝概览",
fansProfile: "粉丝画像",
- "dataSummary.fans30GrowthRate": "近30天粉丝量变化幅度",
- "dataSummary.estimateVideoCpm": "预估视频CPM",
- "dataSummary.estimatePictureCpm": "预估图文CPM",
- "dataSummary.videoReadCost": "预估阅读单价(视频)",
- "dataSummary.picReadCost": "预估阅读单价(图文)",
- "dataSummary.mCpuvNum": "外溢进店中位数",
"fansProfile.ages": "粉丝年龄分布",
"fansProfile.gender.male": "粉丝男性占比",
"fansProfile.gender.female": "粉丝女性占比",
- "fansSummary.activeFansRate": "活跃粉丝占比",
- "fansSummary.engageFansRate": "互动粉丝占比",
- "fansSummary.readFansRate": "阅读粉丝占比",
userId: "达人ID",
name: "达人昵称",
redId: "小红书号",
@@ -71,7 +43,6 @@
fansCount: "粉丝数",
likeCollectCountInfo: "获赞与收藏",
businessNoteCount: "商业笔记数",
- totalNoteCount: "总笔记数",
picturePrice: "图文报价",
videoPrice: "视频报价",
lowerPrice: "最低报价",
@@ -1241,7 +1212,6 @@
${escapeXml(labelText)}
- 映射字段:${escapeXml(field.path)}
`;
list.appendChild(item);
}