Files
koc-loop/tests/mcp-collection-client.test.mjs
2026-08-15 03:53:09 +08:00

841 lines
22 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import test from "node:test";
import {
collectMetricsFromMcp,
collectXhsMetricsFromMcp,
resolveAccountProfileFromMcp,
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, redirects = {}) {
let toolIndex = 0;
const calls = [];
const fetchImpl = async (url, init) => {
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 } });
}
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,
shares: 0,
});
assert.equal(calls.length, 3);
assert.equal(calls[2].body.params.name, "fetch_content_detail");
assert.equal(calls[2].body.params.arguments.request.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("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,
);
assert.deepEqual(result, { likes: 12, comments: 3, collects: 6, shares: 0 });
assert.deepEqual(
calls.map((call) => call.body.method),
["initialize", "tools/call"],
);
assert.equal(calls[1].headers.get("mcp-session-id"), null);
});
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,
gender: "",
bio: "",
recentNoteTitles: [],
providerTags: [],
});
const mcpCalls = calls.filter((call) => call.body?.method);
assert.equal(mcpCalls.length, 4);
assert.equal(
mcpCalls[2].body.params.name,
"fetch_content_detail",
);
assert.equal(
mcpCalls[2].body.params.arguments.request.link,
"https://www.xiaohongshu.com/discovery/item/6a671108000000000f004bef",
);
assert.equal(mcpCalls[3].body.params.name, "parse_xhs_user_summary");
assert.equal(
mcpCalls[3].body.params.arguments.request.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",
gender: "女",
desc: "分享城市周末与美食",
tags: ["本地生活"],
},
notes: [
{
note_id: "note-1",
title: "长沙湘菜探店",
url: "https://www.xiaohongshu.com/explore/note-1",
},
],
},
},
}),
]);
const details = await resolveXhsProfileDetailsFromMcp(
"https://www.xiaohongshu.com/user/profile/5fb21d32000000000101c23e",
{
endpoint: "https://collector.example/mcp",
key: "test-key",
},
fetchImpl,
);
assert.deepEqual(details, {
nickname: "555 五",
followers: 6,
redId: "1020668113",
ipLocation: "福建",
gender: "女",
bio: "分享城市周末与美食",
recentNoteTitles: ["长沙湘菜探店"],
providerTags: ["本地生活"],
});
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.request.link,
"http://xhslink.cn/o/AJFyP5dnj7O",
);
});
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__={&quot;noteData&quot;:{&quot;desc&quot;:&quot;测试&quot;,&quot;user&quot;:{&quot;userId&quot;:&quot;5fb21d32000000000101c23e&quot;,&quot;nickName&quot;:&quot;555 五&quot;}}}</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,
gender: "",
bio: "",
recentNoteTitles: [],
providerTags: [],
});
assert.equal(
fakeMcp.calls[2].body.params.name,
"fetch_content_detail",
);
});
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, {
nickname: "",
redId: "1020668113",
followers: 10,
ipLocation: "",
});
});
test("treats empty interaction counters from the current detail tool as zero", async () => {
const { calls, fetchImpl } = createFakeMcp([
toolEnvelope({
response: {
code: 200,
success: true,
data: {
likes: "2",
comments: "",
collects: "",
},
},
}),
]);
const result = await collectXhsMetricsFromMcp(
"https://www.xiaohongshu.com/explore/test",
{
endpoint: "https://collector.example/mcp?key=test-key",
},
fetchImpl,
);
assert.deepEqual(result, {
likes: 2,
comments: 0,
collects: 0,
shares: 0,
});
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,
),
/获取内容详情失败/,
);
assert.equal(calls.length, 3);
assert.equal(calls[2].body.params.name, "fetch_content_detail");
});
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, shares: 0 });
assert.equal(initializeCount, 2);
});
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}`,
);
});