import assert from "node:assert/strict"; import test from "node:test"; import { collectXhsMetricsFromMcp, resolveCollectionMcpConfig, resolveXhsPublicAccountDetails, resolveXhsPublicAccountId, resolveXhsAccountProfileFromMcp, resolveXhsProfileDetailsFromMcp, xhsNoteIdFromUrl, } from "../lib/mcp-collection-client.ts"; function sse(payload, options = {}) { return new Response( `event: message\ndata: ${JSON.stringify(payload)}\n\n`, { status: 200, ...options, headers: { "content-type": "text/event-stream", ...(options.headers ?? {}), }, }, ); } function toolEnvelope(payload, isError = false) { return { jsonrpc: "2.0", id: "tool-call", result: { isError, content: [ { type: "text", text: JSON.stringify(payload), }, ], }, }; } function createFakeMcp(toolResults) { let toolIndex = 0; const calls = []; const fetchImpl = async (url, init) => { const body = JSON.parse(init.body); calls.push({ url: String(url), body, headers: new Headers(init.headers) }); if (body.method === "initialize") { return sse( { jsonrpc: "2.0", id: body.id, result: { protocolVersion: "2025-03-26", capabilities: { tools: { listChanged: false } }, serverInfo: { name: "ai-tool-center", version: "test" }, }, }, { headers: { "mcp-session-id": "session-test" } }, ); } if (body.method === "notifications/initialized") { return new Response("", { status: 202 }); } if (body.method === "tools/call") { const result = toolResults[toolIndex]; toolIndex += 1; return sse(result); } throw new Error(`Unexpected MCP method: ${body.method}`); }; return { calls, fetchImpl }; } test("collects likes, comments and favorites from the verified MCP shape", async () => { const { calls, fetchImpl } = createFakeMcp([ toolEnvelope({ http_status: 200, response: { code: 200, success: true, msg: "获取内容详情成功", data: { likes: "483", comments: "41", collects: "519", }, }, }), ]); const result = await collectXhsMetricsFromMcp( "https://www.xiaohongshu.com/discovery/item/test?xsec_token=valid", { endpoint: "https://collector.example/mcp", key: "test-key", }, fetchImpl, ); assert.deepEqual(result, { likes: 483, comments: 41, collects: 519, }); assert.equal(calls.length, 3); assert.equal(calls[2].body.params.name, "fetch_content_detail"); assert.equal(calls[2].body.params.arguments.include_comments, false); assert.equal(calls[2].headers.get("mcp-session-id"), "session-test"); assert.equal(new URL(calls[0].url).searchParams.get("key"), "test-key"); }); test("resolves the real XHS account profile from a submitted note link", async () => { const { calls, fetchImpl } = createFakeMcp([ toolEnvelope({ response: { code: 200, success: true, data: { note_detail: { products: [ { content_tags: [ { notes: [ { ip_location: "重庆", note_id: "6a671108000000000f004bef", user: { nickname: "N我的麻辣烫好了吗", profile_url: "https://www.xiaohongshu.com/user/profile/6905cbca0000000037009f49", red_id: "94329495984", user_id: "6905cbca0000000037009f49", }, }, ], }, ], }, ], }, }, }, }), toolEnvelope({ response: { code: 200, success: true, data: { fans_count: "734", }, }, }), ]); const profile = await resolveXhsAccountProfileFromMcp( "https://www.xiaohongshu.com/discovery/item/6a671108000000000f004bef", "回填昵称", { endpoint: "https://collector.example/mcp", key: "test-key", }, fetchImpl, ); assert.deepEqual(profile, { platformUid: "6905cbca0000000037009f49", nickname: "N我的麻辣烫好了吗", profileUrl: "https://www.xiaohongshu.com/user/profile/6905cbca0000000037009f49", redId: "94329495984", ipLocation: "重庆", followers: 734, }); assert.equal(calls.length, 4); assert.equal( calls[2].body.params.name, "collect_xhs_wen_note_detail", ); assert.equal( calls[2].body.params.arguments.note_id, "6a671108000000000f004bef", ); assert.equal(calls[3].body.params.name, "parse_xhs_user_summary"); assert.equal( calls[3].body.params.arguments.url, "https://www.xiaohongshu.com/user/profile/6905cbca0000000037009f49", ); assert.equal( xhsNoteIdFromUrl( "https://www.xiaohongshu.com/explore/6a671108000000000f004bef", ), "6a671108000000000f004bef", ); }); test("resolves followers directly from the supported XHS user summary tool", async () => { const { calls, fetchImpl } = createFakeMcp([ toolEnvelope({ response: { code: 200, success: true, msg: "用户摘要解析成功", data: { user: { fansCount: 6, ipLocation: "福建", nickname: "555 五", userId: "1020668113", }, }, }, }), ]); const details = await resolveXhsProfileDetailsFromMcp( "https://www.xiaohongshu.com/user/profile/5fb21d32000000000101c23e", { endpoint: "https://collector.example/mcp", key: "test-key", }, fetchImpl, ); assert.deepEqual(details, { followers: 6, redId: "1020668113", ipLocation: "福建", }); assert.equal(calls[2].body.params.name, "parse_xhs_user_summary"); }); test("resolves an xhslink short URL before requesting the author profile", async () => { const fakeMcp = createFakeMcp([ toolEnvelope({ response: { code: 200, success: true, data: { note_detail: { note_id: "6a572da40000000021018bd2", ip_location: "上海", user: { nickname: "短链作者", user_id: "6905cbca0000000037009f49", }, }, }, }, }), toolEnvelope({ response: { code: 200, success: true, data: { red_id: "1020668113", followers: "1.2万", }, }, }), ]); const fetchImpl = async (url, init) => { if (String(url) === "http://xhslink.cn/o/AJFyP5dnj7O") { return new Response(null, { status: 302, headers: { location: "https://www.xiaohongshu.com/discovery/item/6a572da40000000021018bd2?xsec_token=valid", }, }); } return fakeMcp.fetchImpl(url, init); }; const profile = await resolveXhsAccountProfileFromMcp( "http://xhslink.cn/o/AJFyP5dnj7O", "回填昵称", { endpoint: "https://collector.example/mcp", key: "test-key", }, fetchImpl, ); assert.equal(profile.nickname, "短链作者"); assert.equal(profile.platformUid, "6905cbca0000000037009f49"); assert.equal(profile.redId, "1020668113"); assert.equal(profile.followers, 12_000); assert.equal( profile.profileUrl, "https://www.xiaohongshu.com/user/profile/6905cbca0000000037009f49", ); assert.equal( fakeMcp.calls[2].body.params.arguments.note_id, "6a572da40000000021018bd2", ); }); test("uses the public note author when MCP profile lookup fails", async () => { const fakeMcp = createFakeMcp([ toolEnvelope( { response: { code: 500, success: false, msg: "作者详情暂时不可用", }, }, true, ), ]); const fetchImpl = async (url, init) => { if (String(url) === "http://xhslink.cn/o/AJFyP5dnj7O") { return new Response( '', { status: 200, headers: { "content-type": "text/html; charset=utf-8", location: "https://www.xiaohongshu.com/discovery/item/6a572da40000000021018bd2?xsec_token=valid", }, }, ); } if ( String(url) === "https://www.xiaohongshu.com/user/profile/5fb21d32000000000101c23e" ) { return new Response( '', { status: 200 }, ); } return fakeMcp.fetchImpl(url, init); }; const profile = await resolveXhsAccountProfileFromMcp( "http://xhslink.cn/o/AJFyP5dnj7O", "回填昵称", { endpoint: "https://collector.example/mcp", key: "test-key", }, fetchImpl, ); assert.deepEqual(profile, { platformUid: "5fb21d32000000000101c23e", nickname: "555 五", profileUrl: "https://www.xiaohongshu.com/user/profile/5fb21d32000000000101c23e", redId: "1020668113", ipLocation: "待识别", followers: 6, }); assert.equal( fakeMcp.calls[2].body.params.name, "collect_xhs_wen_note_detail", ); }); test("reads the user-visible Xiaohongshu number from a public profile", async () => { const fetchImpl = async () => new Response( '
小红书号:1020668113
', { status: 200 }, ); const accountId = await resolveXhsPublicAccountId( "https://www.xiaohongshu.com/user/profile/5fb21d32000000000101c23e", fetchImpl, ); const details = await resolveXhsPublicAccountDetails( "https://www.xiaohongshu.com/user/profile/5fb21d32000000000101c23e", fetchImpl, ); assert.equal(accountId, "1020668113"); assert.deepEqual(details, { redId: "1020668113", followers: 10, ipLocation: "", }); }); test("falls back to parse_xhs_note when the primary tool fails", async () => { const { calls, fetchImpl } = createFakeMcp([ toolEnvelope( { response: { code: 400, success: false, msg: "获取内容详情失败", data: null, }, }, true, ), toolEnvelope({ response: { code: 200, success: true, data: { likes: "1.2万", comments: 32, collects: "2,345", }, }, }), ]); const result = await collectXhsMetricsFromMcp( "https://www.xiaohongshu.com/explore/test", { endpoint: "https://collector.example/mcp?key=test-key", }, fetchImpl, ); assert.deepEqual(result, { likes: 12_000, comments: 32, collects: 2_345, }); assert.equal(calls[3].body.params.name, "parse_xhs_note"); }); test("requires the MCP key without sending a network request", async () => { let requested = false; await assert.rejects( collectXhsMetricsFromMcp( "https://www.xiaohongshu.com/explore/test", resolveCollectionMcpConfig({}), async () => { requested = true; return new Response(); }, ), /MCP采集密钥未配置/, ); assert.equal(requested, false); }); test("rebuilds the MCP session after a gateway session miss", async () => { let initializeCount = 0; const fetchImpl = async (_url, init) => { const body = JSON.parse(init.body); if (body.method === "initialize") { initializeCount += 1; return sse( { jsonrpc: "2.0", id: body.id, result: { protocolVersion: "2025-03-26", capabilities: { tools: { listChanged: false } }, serverInfo: { name: "ai-tool-center", version: "test" }, }, }, { headers: { "mcp-session-id": `session-${initializeCount}`, }, }, ); } if ( body.method === "notifications/initialized" && initializeCount === 1 ) { return Response.json( { jsonrpc: "2.0", id: "server-error", error: { code: -32600, message: "Session not found" }, }, { status: 404 }, ); } if (body.method === "notifications/initialized") { return new Response("", { status: 202 }); } if (body.method === "tools/call") { return sse( toolEnvelope({ response: { code: 200, success: true, data: { likes: 8, comments: 2, collects: 5 }, }, }), ); } throw new Error(`Unexpected MCP method: ${body.method}`); }; const result = await collectXhsMetricsFromMcp( "https://www.xiaohongshu.com/explore/test", { endpoint: "https://collector.example/mcp", key: "test-key", }, fetchImpl, ); assert.deepEqual(result, { likes: 8, comments: 2, collects: 5 }); assert.equal(initializeCount, 2); });