2026-07-30 12:06:41 +08:00
|
|
|
|
import assert from "node:assert/strict";
|
|
|
|
|
|
import test from "node:test";
|
|
|
|
|
|
import {
|
2026-08-15 03:53:09 +08:00
|
|
|
|
collectMetricsFromMcp,
|
2026-07-30 12:06:41 +08:00
|
|
|
|
collectXhsMetricsFromMcp,
|
2026-08-15 03:53:09 +08:00
|
|
|
|
resolveAccountProfileFromMcp,
|
2026-07-30 12:06:41 +08:00
|
|
|
|
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),
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-15 03:53:09 +08:00
|
|
|
|
function createFakeMcp(toolResults, redirects = {}) {
|
2026-07-30 12:06:41 +08:00
|
|
|
|
let toolIndex = 0;
|
|
|
|
|
|
const calls = [];
|
|
|
|
|
|
const fetchImpl = async (url, init) => {
|
2026-08-15 03:53:09 +08:00
|
|
|
|
if (!init?.body) {
|
|
|
|
|
|
const requestUrl = String(url);
|
|
|
|
|
|
calls.push({ url: requestUrl, body: null, headers: new Headers(init?.headers) });
|
|
|
|
|
|
const location = redirects[requestUrl];
|
|
|
|
|
|
if (!location) throw new Error(`Unexpected public request: ${requestUrl}`);
|
|
|
|
|
|
return new Response("", { status: 302, headers: { location } });
|
|
|
|
|
|
}
|
2026-07-30 12:06:41 +08:00
|
|
|
|
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,
|
2026-08-15 03:53:09 +08:00
|
|
|
|
shares: 0,
|
2026-07-30 12:06:41 +08:00
|
|
|
|
});
|
|
|
|
|
|
assert.equal(calls.length, 3);
|
|
|
|
|
|
assert.equal(calls[2].body.params.name, "fetch_content_detail");
|
2026-08-11 23:07:26 +08:00
|
|
|
|
assert.equal(calls[2].body.params.arguments.request.include_comments, false);
|
2026-07-30 12:06:41 +08:00
|
|
|
|
assert.equal(calls[2].headers.get("mcp-session-id"), "session-test");
|
|
|
|
|
|
assert.equal(new URL(calls[0].url).searchParams.get("key"), "test-key");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-11 23:07:26 +08:00
|
|
|
|
test("collects through a stateless MCP server without a session header", async () => {
|
|
|
|
|
|
const calls = [];
|
|
|
|
|
|
const fetchImpl = async (_url, init) => {
|
|
|
|
|
|
const body = JSON.parse(init.body);
|
|
|
|
|
|
calls.push({ 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: "production" },
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
if (body.method === "tools/call") {
|
|
|
|
|
|
return sse(
|
|
|
|
|
|
toolEnvelope({
|
|
|
|
|
|
response: {
|
|
|
|
|
|
code: 200,
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: { likes: 12, comments: 3, collects: 6 },
|
|
|
|
|
|
},
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
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,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-08-15 03:53:09 +08:00
|
|
|
|
assert.deepEqual(result, { likes: 12, comments: 3, collects: 6, shares: 0 });
|
2026-08-11 23:07:26 +08:00
|
|
|
|
assert.deepEqual(
|
|
|
|
|
|
calls.map((call) => call.body.method),
|
|
|
|
|
|
["initialize", "tools/call"],
|
|
|
|
|
|
);
|
|
|
|
|
|
assert.equal(calls[1].headers.get("mcp-session-id"), null);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-30 12:06:41 +08:00
|
|
|
|
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,
|
2026-08-15 03:53:09 +08:00
|
|
|
|
gender: "",
|
|
|
|
|
|
bio: "",
|
|
|
|
|
|
recentNoteTitles: [],
|
|
|
|
|
|
providerTags: [],
|
2026-07-30 12:06:41 +08:00
|
|
|
|
});
|
2026-08-15 03:53:09 +08:00
|
|
|
|
const mcpCalls = calls.filter((call) => call.body?.method);
|
|
|
|
|
|
assert.equal(mcpCalls.length, 4);
|
2026-07-30 12:06:41 +08:00
|
|
|
|
assert.equal(
|
2026-08-15 03:53:09 +08:00
|
|
|
|
mcpCalls[2].body.params.name,
|
|
|
|
|
|
"fetch_content_detail",
|
2026-07-30 12:06:41 +08:00
|
|
|
|
);
|
|
|
|
|
|
assert.equal(
|
2026-08-15 03:53:09 +08:00
|
|
|
|
mcpCalls[2].body.params.arguments.request.link,
|
|
|
|
|
|
"https://www.xiaohongshu.com/discovery/item/6a671108000000000f004bef",
|
2026-07-30 12:06:41 +08:00
|
|
|
|
);
|
2026-08-15 03:53:09 +08:00
|
|
|
|
assert.equal(mcpCalls[3].body.params.name, "parse_xhs_user_summary");
|
2026-07-30 12:06:41 +08:00
|
|
|
|
assert.equal(
|
2026-08-15 03:53:09 +08:00
|
|
|
|
mcpCalls[3].body.params.arguments.request.url,
|
2026-07-30 12:06:41 +08:00
|
|
|
|
"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",
|
2026-08-15 03:53:09 +08:00
|
|
|
|
gender: "女",
|
|
|
|
|
|
desc: "分享城市周末与美食",
|
|
|
|
|
|
tags: ["本地生活"],
|
2026-07-30 12:06:41 +08:00
|
|
|
|
},
|
2026-08-15 03:53:09 +08:00
|
|
|
|
notes: [
|
|
|
|
|
|
{
|
|
|
|
|
|
note_id: "note-1",
|
|
|
|
|
|
title: "长沙湘菜探店",
|
|
|
|
|
|
url: "https://www.xiaohongshu.com/explore/note-1",
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
2026-07-30 12:06:41 +08:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
const details = await resolveXhsProfileDetailsFromMcp(
|
|
|
|
|
|
"https://www.xiaohongshu.com/user/profile/5fb21d32000000000101c23e",
|
|
|
|
|
|
{
|
|
|
|
|
|
endpoint: "https://collector.example/mcp",
|
|
|
|
|
|
key: "test-key",
|
|
|
|
|
|
},
|
|
|
|
|
|
fetchImpl,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
assert.deepEqual(details, {
|
2026-08-11 23:07:26 +08:00
|
|
|
|
nickname: "555 五",
|
2026-07-30 12:06:41 +08:00
|
|
|
|
followers: 6,
|
|
|
|
|
|
redId: "1020668113",
|
|
|
|
|
|
ipLocation: "福建",
|
2026-08-15 03:53:09 +08:00
|
|
|
|
gender: "女",
|
|
|
|
|
|
bio: "分享城市周末与美食",
|
|
|
|
|
|
recentNoteTitles: ["长沙湘菜探店"],
|
|
|
|
|
|
providerTags: ["本地生活"],
|
2026-07-30 12:06:41 +08:00
|
|
|
|
});
|
|
|
|
|
|
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(
|
2026-08-15 03:53:09 +08:00
|
|
|
|
fakeMcp.calls[2].body.params.arguments.request.link,
|
|
|
|
|
|
"http://xhslink.cn/o/AJFyP5dnj7O",
|
2026-07-30 12:06:41 +08:00
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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(
|
|
|
|
|
|
'<script>window.__STATE__={"noteData":{"desc":"测试","user":{"userId":"5fb21d32000000000101c23e","nickName":"555 五"}}}</script>',
|
|
|
|
|
|
{
|
|
|
|
|
|
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(
|
|
|
|
|
|
'<script>{"redId":"1020668113","interactions":[{"name":"粉丝","count":"6","i18nCount":"6"}]}</script>',
|
|
|
|
|
|
{ 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,
|
2026-08-15 03:53:09 +08:00
|
|
|
|
gender: "",
|
|
|
|
|
|
bio: "",
|
|
|
|
|
|
recentNoteTitles: [],
|
|
|
|
|
|
providerTags: [],
|
2026-07-30 12:06:41 +08:00
|
|
|
|
});
|
|
|
|
|
|
assert.equal(
|
|
|
|
|
|
fakeMcp.calls[2].body.params.name,
|
2026-08-15 03:53:09 +08:00
|
|
|
|
"fetch_content_detail",
|
2026-07-30 12:06:41 +08:00
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("reads the user-visible Xiaohongshu number from a public profile", async () => {
|
|
|
|
|
|
const fetchImpl = async () =>
|
|
|
|
|
|
new Response(
|
|
|
|
|
|
'<div class="redId">小红书号:1020668113</div><script>{"redId":"1020668113","interactions":[{"name":"粉丝","count":"10+","i18nCount":"10+"}]}</script>',
|
|
|
|
|
|
{ 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, {
|
2026-08-11 23:07:26 +08:00
|
|
|
|
nickname: "",
|
2026-07-30 12:06:41 +08:00
|
|
|
|
redId: "1020668113",
|
|
|
|
|
|
followers: 10,
|
|
|
|
|
|
ipLocation: "",
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-15 03:53:09 +08:00
|
|
|
|
test("treats empty interaction counters from the current detail tool as zero", async () => {
|
2026-07-30 12:06:41 +08:00
|
|
|
|
const { calls, fetchImpl } = createFakeMcp([
|
|
|
|
|
|
toolEnvelope({
|
|
|
|
|
|
response: {
|
|
|
|
|
|
code: 200,
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: {
|
2026-08-15 03:53:09 +08:00
|
|
|
|
likes: "2",
|
|
|
|
|
|
comments: "",
|
|
|
|
|
|
collects: "",
|
2026-07-30 12:06:41 +08:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
const result = await collectXhsMetricsFromMcp(
|
|
|
|
|
|
"https://www.xiaohongshu.com/explore/test",
|
|
|
|
|
|
{
|
|
|
|
|
|
endpoint: "https://collector.example/mcp?key=test-key",
|
|
|
|
|
|
},
|
|
|
|
|
|
fetchImpl,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
assert.deepEqual(result, {
|
2026-08-15 03:53:09 +08:00
|
|
|
|
likes: 2,
|
|
|
|
|
|
comments: 0,
|
|
|
|
|
|
collects: 0,
|
|
|
|
|
|
shares: 0,
|
2026-07-30 12:06:41 +08:00
|
|
|
|
});
|
2026-08-15 03:53:09 +08:00
|
|
|
|
assert.equal(calls.length, 3);
|
|
|
|
|
|
assert.equal(calls[2].body.params.name, "fetch_content_detail");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("surfaces current detail tool failures without calling removed tools", async () => {
|
|
|
|
|
|
const { calls, fetchImpl } = createFakeMcp([
|
|
|
|
|
|
toolEnvelope(
|
|
|
|
|
|
{
|
|
|
|
|
|
response: {
|
|
|
|
|
|
code: 400,
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
msg: "获取内容详情失败",
|
|
|
|
|
|
data: null,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
true,
|
|
|
|
|
|
),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
await assert.rejects(
|
|
|
|
|
|
collectXhsMetricsFromMcp(
|
|
|
|
|
|
"https://www.xiaohongshu.com/explore/test",
|
|
|
|
|
|
{
|
|
|
|
|
|
endpoint: "https://collector.example/mcp?key=test-key",
|
|
|
|
|
|
},
|
|
|
|
|
|
fetchImpl,
|
|
|
|
|
|
),
|
2026-08-19 15:08:17 +08:00
|
|
|
|
/MCP工具 fetch_content_detail 返回失败(code 400):获取内容详情失败/,
|
2026-08-15 03:53:09 +08:00
|
|
|
|
);
|
|
|
|
|
|
assert.equal(calls.length, 3);
|
|
|
|
|
|
assert.equal(calls[2].body.params.name, "fetch_content_detail");
|
2026-07-30 12:06:41 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-08-15 03:53:09 +08:00
|
|
|
|
assert.deepEqual(result, { likes: 8, comments: 2, collects: 5, shares: 0 });
|
2026-07-30 12:06:41 +08:00
|
|
|
|
assert.equal(initializeCount, 2);
|
|
|
|
|
|
});
|
2026-08-15 03:53:09 +08:00
|
|
|
|
|
|
|
|
|
|
test("collects Douyin likes, favorites, shares and comments", async () => {
|
|
|
|
|
|
const { calls, fetchImpl } = createFakeMcp([
|
|
|
|
|
|
toolEnvelope({
|
|
|
|
|
|
response: {
|
|
|
|
|
|
code: 200,
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: {
|
|
|
|
|
|
digg_count: "120",
|
|
|
|
|
|
collect_count: "30",
|
|
|
|
|
|
share_count: "8",
|
|
|
|
|
|
comment_count: "12",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
const result = await collectMetricsFromMcp(
|
|
|
|
|
|
"https://www.douyin.com/video/7520000000000000000",
|
|
|
|
|
|
"抖音",
|
|
|
|
|
|
{ endpoint: "https://collector.example/mcp", key: "test-key" },
|
|
|
|
|
|
fetchImpl,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
assert.deepEqual(result, {
|
|
|
|
|
|
likes: 120,
|
|
|
|
|
|
collects: 30,
|
|
|
|
|
|
shares: 8,
|
|
|
|
|
|
comments: 12,
|
|
|
|
|
|
});
|
|
|
|
|
|
assert.equal(calls[2].body.params.name, "fetch_content_detail");
|
|
|
|
|
|
assert.equal(calls[2].body.params.arguments.request.plant, "dy");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("maps the current Douyin MCP metric field names", async () => {
|
|
|
|
|
|
const { fetchImpl } = createFakeMcp([
|
|
|
|
|
|
toolEnvelope({
|
|
|
|
|
|
response: {
|
|
|
|
|
|
code: 200,
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: {
|
|
|
|
|
|
liked_count: "5682",
|
|
|
|
|
|
collected_count: "565",
|
|
|
|
|
|
share_count: "6878",
|
|
|
|
|
|
comment_count: "332",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
const result = await collectMetricsFromMcp(
|
|
|
|
|
|
"https://v.douyin.com/5O5VpgomO2U/",
|
|
|
|
|
|
"抖音",
|
|
|
|
|
|
{ endpoint: "https://collector.example/mcp", key: "test-key" },
|
|
|
|
|
|
fetchImpl,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
assert.deepEqual(result, {
|
|
|
|
|
|
likes: 5682,
|
|
|
|
|
|
collects: 565,
|
|
|
|
|
|
shares: 6878,
|
|
|
|
|
|
comments: 332,
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("resolves a Douyin account from a submitted work link", async () => {
|
|
|
|
|
|
const secUid = "MS4wLjABAAAA-test-profile-123456";
|
|
|
|
|
|
const { calls, fetchImpl } = createFakeMcp([
|
|
|
|
|
|
toolEnvelope({
|
|
|
|
|
|
response: {
|
|
|
|
|
|
code: 200,
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: {
|
|
|
|
|
|
author: {
|
|
|
|
|
|
nickname: "抖音作者",
|
|
|
|
|
|
sec_uid: secUid,
|
|
|
|
|
|
unique_id: "douyin-123",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}),
|
|
|
|
|
|
toolEnvelope({
|
|
|
|
|
|
response: {
|
|
|
|
|
|
code: 200,
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: {
|
|
|
|
|
|
user: {
|
|
|
|
|
|
nickname: "抖音作者",
|
|
|
|
|
|
unique_id: "douyin-123",
|
|
|
|
|
|
sec_uid: secUid,
|
|
|
|
|
|
follower_count: "1.5万",
|
|
|
|
|
|
ip_location: "上海",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
const profile = await resolveAccountProfileFromMcp(
|
|
|
|
|
|
"https://www.douyin.com/video/7520000000000000000",
|
|
|
|
|
|
"抖音",
|
|
|
|
|
|
"回填昵称",
|
|
|
|
|
|
{ endpoint: "https://collector.example/mcp", key: "test-key" },
|
|
|
|
|
|
fetchImpl,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
assert.deepEqual(profile, {
|
|
|
|
|
|
platformUid: secUid,
|
|
|
|
|
|
nickname: "抖音作者",
|
|
|
|
|
|
profileUrl: `https://www.douyin.com/user/${secUid}`,
|
|
|
|
|
|
redId: "douyin-123",
|
|
|
|
|
|
ipLocation: "上海",
|
|
|
|
|
|
followers: 15_000,
|
|
|
|
|
|
gender: "",
|
|
|
|
|
|
bio: "",
|
|
|
|
|
|
recentNoteTitles: [],
|
|
|
|
|
|
providerTags: [],
|
|
|
|
|
|
});
|
|
|
|
|
|
assert.equal(calls[2].body.params.arguments.request.plant, "dy");
|
|
|
|
|
|
assert.equal(calls[3].body.params.name, "parse_dy_user_summary");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("does not treat a Douyin short-link device id as the author sec_uid", async () => {
|
|
|
|
|
|
const shortLink = "https://v.douyin.com/5O5VpgomO2U/";
|
|
|
|
|
|
const { calls, fetchImpl } = createFakeMcp(
|
|
|
|
|
|
[
|
|
|
|
|
|
toolEnvelope({
|
|
|
|
|
|
response: {
|
|
|
|
|
|
code: 200,
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: {
|
|
|
|
|
|
nickname: "搞怪噜噜😜",
|
|
|
|
|
|
user_id: "4065311277723529",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}),
|
|
|
|
|
|
],
|
|
|
|
|
|
{
|
|
|
|
|
|
[shortLink]: "https://www.iesdouyin.com/share/video/7671270545631842038/?did=MS4wLjABAAAA-device-token&with_sec_did=1",
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const profile = await resolveAccountProfileFromMcp(
|
|
|
|
|
|
shortLink,
|
|
|
|
|
|
"抖音",
|
|
|
|
|
|
"回填昵称",
|
|
|
|
|
|
{ endpoint: "https://collector.example/mcp", key: "test-key" },
|
|
|
|
|
|
fetchImpl,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
assert.deepEqual(profile, {
|
|
|
|
|
|
platformUid: "4065311277723529",
|
|
|
|
|
|
nickname: "搞怪噜噜😜",
|
|
|
|
|
|
profileUrl: "",
|
|
|
|
|
|
redId: "",
|
|
|
|
|
|
ipLocation: "待识别",
|
|
|
|
|
|
followers: null,
|
|
|
|
|
|
gender: "",
|
|
|
|
|
|
bio: "",
|
|
|
|
|
|
recentNoteTitles: [],
|
|
|
|
|
|
providerTags: [],
|
|
|
|
|
|
});
|
|
|
|
|
|
assert.equal(calls.find((call) => call.body === null)?.url, shortLink);
|
|
|
|
|
|
assert.equal(
|
|
|
|
|
|
calls.find((call) => call.body?.params?.name === "parse_dy_user_summary"),
|
|
|
|
|
|
undefined,
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("resolves a Douyin profile only when the public redirect exposes sec_uid", async () => {
|
|
|
|
|
|
const shortLink = "https://v.douyin.com/author-sec-uid/";
|
|
|
|
|
|
const secUid = "MS4wLjABAAAAOqL4Jdu8htr7EWCDAyIr5z_7uvCAxhj-GOzCWg5zn8bDiKOp3WPw7lWkTyHvZpMY";
|
|
|
|
|
|
const { calls, fetchImpl } = createFakeMcp(
|
|
|
|
|
|
[
|
|
|
|
|
|
toolEnvelope({
|
|
|
|
|
|
response: {
|
|
|
|
|
|
code: 200,
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: {
|
|
|
|
|
|
nickname: "抖音作者",
|
|
|
|
|
|
user_id: "4065311277723529",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}),
|
|
|
|
|
|
toolEnvelope({
|
|
|
|
|
|
response: {
|
|
|
|
|
|
code: 200,
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: {
|
|
|
|
|
|
user: {
|
|
|
|
|
|
nickname: "抖音作者",
|
|
|
|
|
|
unique_id: "douyin-987",
|
|
|
|
|
|
sec_uid: secUid,
|
|
|
|
|
|
follower_count: "2.3万",
|
|
|
|
|
|
ip_location: "广东",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}),
|
|
|
|
|
|
],
|
|
|
|
|
|
{
|
|
|
|
|
|
[shortLink]: `https://www.iesdouyin.com/share/video/7671270545631842038/?sec_uid=${secUid}`,
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const profile = await resolveAccountProfileFromMcp(
|
|
|
|
|
|
shortLink,
|
|
|
|
|
|
"抖音",
|
|
|
|
|
|
"回填昵称",
|
|
|
|
|
|
{ endpoint: "https://collector.example/mcp", key: "test-key" },
|
|
|
|
|
|
fetchImpl,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
assert.deepEqual(profile, {
|
|
|
|
|
|
platformUid: secUid,
|
|
|
|
|
|
nickname: "抖音作者",
|
|
|
|
|
|
profileUrl: `https://www.douyin.com/user/${secUid}`,
|
|
|
|
|
|
redId: "douyin-987",
|
|
|
|
|
|
ipLocation: "广东",
|
|
|
|
|
|
followers: 23_000,
|
|
|
|
|
|
gender: "",
|
|
|
|
|
|
bio: "",
|
|
|
|
|
|
recentNoteTitles: [],
|
|
|
|
|
|
providerTags: [],
|
|
|
|
|
|
});
|
|
|
|
|
|
assert.equal(
|
|
|
|
|
|
calls.find((call) => call.body?.params?.name === "parse_dy_user_summary")
|
|
|
|
|
|
?.body.params.arguments.request.url,
|
|
|
|
|
|
`https://www.douyin.com/user/${secUid}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|