feat: 完善视频任务与 KOC 资源库
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
collectMetricsFromMcp,
|
||||
collectXhsMetricsFromMcp,
|
||||
resolveAccountProfileFromMcp,
|
||||
resolveCollectionMcpConfig,
|
||||
resolveXhsPublicAccountDetails,
|
||||
resolveXhsPublicAccountId,
|
||||
@@ -40,10 +42,17 @@ function toolEnvelope(payload, isError = false) {
|
||||
};
|
||||
}
|
||||
|
||||
function createFakeMcp(toolResults) {
|
||||
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") {
|
||||
@@ -103,6 +112,7 @@ test("collects likes, comments and favorites from the verified MCP shape", async
|
||||
likes: 483,
|
||||
comments: 41,
|
||||
collects: 519,
|
||||
shares: 0,
|
||||
});
|
||||
assert.equal(calls.length, 3);
|
||||
assert.equal(calls[2].body.params.name, "fetch_content_detail");
|
||||
@@ -147,7 +157,7 @@ test("collects through a stateless MCP server without a session header", async (
|
||||
fetchImpl,
|
||||
);
|
||||
|
||||
assert.deepEqual(result, { likes: 12, comments: 3, collects: 6 });
|
||||
assert.deepEqual(result, { likes: 12, comments: 3, collects: 6, shares: 0 });
|
||||
assert.deepEqual(
|
||||
calls.map((call) => call.body.method),
|
||||
["initialize", "tools/call"],
|
||||
@@ -217,19 +227,24 @@ test("resolves the real XHS account profile from a submitted note link", async (
|
||||
redId: "94329495984",
|
||||
ipLocation: "重庆",
|
||||
followers: 734,
|
||||
gender: "",
|
||||
bio: "",
|
||||
recentNoteTitles: [],
|
||||
providerTags: [],
|
||||
});
|
||||
assert.equal(calls.length, 4);
|
||||
const mcpCalls = calls.filter((call) => call.body?.method);
|
||||
assert.equal(mcpCalls.length, 4);
|
||||
assert.equal(
|
||||
calls[2].body.params.name,
|
||||
"collect_xhs_wen_note_detail",
|
||||
mcpCalls[2].body.params.name,
|
||||
"fetch_content_detail",
|
||||
);
|
||||
assert.equal(
|
||||
calls[2].body.params.arguments.request.note_id,
|
||||
"6a671108000000000f004bef",
|
||||
mcpCalls[2].body.params.arguments.request.link,
|
||||
"https://www.xiaohongshu.com/discovery/item/6a671108000000000f004bef",
|
||||
);
|
||||
assert.equal(calls[3].body.params.name, "parse_xhs_user_summary");
|
||||
assert.equal(mcpCalls[3].body.params.name, "parse_xhs_user_summary");
|
||||
assert.equal(
|
||||
calls[3].body.params.arguments.request.url,
|
||||
mcpCalls[3].body.params.arguments.request.url,
|
||||
"https://www.xiaohongshu.com/user/profile/6905cbca0000000037009f49",
|
||||
);
|
||||
assert.equal(
|
||||
@@ -253,7 +268,17 @@ test("resolves followers directly from the supported XHS user summary tool", asy
|
||||
ipLocation: "福建",
|
||||
nickname: "555 五",
|
||||
userId: "1020668113",
|
||||
gender: "女",
|
||||
desc: "分享城市周末与美食",
|
||||
tags: ["本地生活"],
|
||||
},
|
||||
notes: [
|
||||
{
|
||||
note_id: "note-1",
|
||||
title: "长沙湘菜探店",
|
||||
url: "https://www.xiaohongshu.com/explore/note-1",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -273,6 +298,10 @@ test("resolves followers directly from the supported XHS user summary tool", asy
|
||||
followers: 6,
|
||||
redId: "1020668113",
|
||||
ipLocation: "福建",
|
||||
gender: "女",
|
||||
bio: "分享城市周末与美食",
|
||||
recentNoteTitles: ["长沙湘菜探店"],
|
||||
providerTags: ["本地生活"],
|
||||
});
|
||||
assert.equal(calls[2].body.params.name, "parse_xhs_user_summary");
|
||||
});
|
||||
@@ -338,8 +367,8 @@ test("resolves an xhslink short URL before requesting the author profile", async
|
||||
"https://www.xiaohongshu.com/user/profile/6905cbca0000000037009f49",
|
||||
);
|
||||
assert.equal(
|
||||
fakeMcp.calls[2].body.params.arguments.request.note_id,
|
||||
"6a572da40000000021018bd2",
|
||||
fakeMcp.calls[2].body.params.arguments.request.link,
|
||||
"http://xhslink.cn/o/AJFyP5dnj7O",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -400,10 +429,14 @@ test("uses the public note author when MCP profile lookup fails", async () => {
|
||||
redId: "1020668113",
|
||||
ipLocation: "待识别",
|
||||
followers: 6,
|
||||
gender: "",
|
||||
bio: "",
|
||||
recentNoteTitles: [],
|
||||
providerTags: [],
|
||||
});
|
||||
assert.equal(
|
||||
fakeMcp.calls[2].body.params.name,
|
||||
"collect_xhs_wen_note_detail",
|
||||
"fetch_content_detail",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -431,27 +464,16 @@ test("reads the user-visible Xiaohongshu number from a public profile", async ()
|
||||
});
|
||||
});
|
||||
|
||||
test("falls back to parse_xhs_note when the primary tool fails", async () => {
|
||||
test("treats empty interaction counters from the current detail tool as zero", 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",
|
||||
likes: "2",
|
||||
comments: "",
|
||||
collects: "",
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -466,11 +488,42 @@ test("falls back to parse_xhs_note when the primary tool fails", async () => {
|
||||
);
|
||||
|
||||
assert.deepEqual(result, {
|
||||
likes: 12_000,
|
||||
comments: 32,
|
||||
collects: 2_345,
|
||||
likes: 2,
|
||||
comments: 0,
|
||||
collects: 0,
|
||||
shares: 0,
|
||||
});
|
||||
assert.equal(calls[3].body.params.name, "parse_xhs_note");
|
||||
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 () => {
|
||||
@@ -551,6 +604,237 @@ test("rebuilds the MCP session after a gateway session miss", async () => {
|
||||
fetchImpl,
|
||||
);
|
||||
|
||||
assert.deepEqual(result, { likes: 8, comments: 2, collects: 5 });
|
||||
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}`,
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user