Files
koc-loop/tests/account-tagging.test.mjs
ABAPPLO 4a64feee02 feat: KOC 资源导入后自动爬取主页资料并调用 LLM 打标签
- 新增 structured_text_process 打标客户端(文本构造、标签解析清洗、MCP 调用)
- 写库后后台打标(tagUntaggedAccounts),单条新增 / 批量导入 / 每日补全三个入口统一
- 标签只补空,UPDATE 带 tags 为空守卫,不覆盖已有标签
- 新增 AI_TAGGING_TOOL 配置项(留空关闭),更新配置示例与文档
- 新增 16 个打标测试用例,全量 131 个测试通过
2026-08-26 15:21:28 +08:00

222 lines
6.6 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 {
buildAccountTagText,
generateAccountTagsFromMcp,
parseAccountTags,
} from "../lib/mcp-collection-client.ts";
import { needsResourceEnrichment } from "../lib/resource-import.ts";
function sse(payload, headers = {}) {
return new Response(`event: message\ndata: ${JSON.stringify(payload)}\n\n`, {
status: 200,
headers: {
"content-type": "text/event-stream",
...headers,
},
});
}
function toolEnvelope(payload, isError = false) {
return {
jsonrpc: "2.0",
id: "tool-call",
result: {
isError,
content: [{ type: "text", text: JSON.stringify(payload) }],
},
};
}
function createFakeTaggingMcp(toolResult, isError = false) {
const calls = [];
const fetchImpl = async (url, init) => {
const body = JSON.parse(init.body);
calls.push({ url: String(url), body });
if (body.method === "initialize") {
return sse(
{
jsonrpc: "2.0",
id: body.id,
result: {
protocolVersion: "2025-03-26",
capabilities: {},
serverInfo: { name: "ai-tool-center", version: "test" },
},
},
{ "mcp-session-id": "session-test" },
);
}
if (body.method === "notifications/initialized") {
return new Response("", { status: 202 });
}
if (body.method === "tools/call") {
return sse(toolEnvelope(toolResult, isError));
}
throw new Error(`Unexpected MCP method: ${body.method}`);
};
return { fetchImpl, calls };
}
const config = { endpoint: "https://mcp.test/mcp", key: "test-key" };
const taggingEnv = { AI_TAGGING_TOOL: "generate_tags" };
const input = {
platform: "小红书",
nickname: "小美的日常",
bio: "分享护肤和生活",
gender: "女",
ipLocation: "上海",
followers: 1200,
providerTags: ["美妆"],
recentNoteTitles: ["平价护肤分享"],
};
test("parseAccountTags 解析纯 JSON 数组", () => {
assert.deepEqual(parseAccountTags('["美妆","学生党"]'), ["美妆", "学生党"]);
});
test("parseAccountTags 剥离代码块围栏", () => {
assert.deepEqual(parseAccountTags('```json\n["美食"]\n```'), ["美食"]);
assert.deepEqual(parseAccountTags('```\n["旅行"]\n```'), ["旅行"]);
});
test("parseAccountTags 从夹杂文字中提取数组", () => {
const text = '好的,以下是标签:["旅行","摄影"] 希望有帮助';
assert.deepEqual(parseAccountTags(text), ["旅行", "摄影"]);
});
test("parseAccountTags 清洗与过滤", () => {
assert.deepEqual(parseAccountTags('["#美食","探店#"]'), ["美食", "探店"]);
assert.deepEqual(parseAccountTags('["美妆","美妆","穿搭"]'), [
"美妆",
"穿搭",
]);
assert.deepEqual(
parseAccountTags('["这个标签实在太长了超过十二个字","护肤"]'),
["护肤"],
);
assert.deepEqual(
parseAccountTags('[1, null, "母婴", true]'),
["母婴"],
);
});
test("parseAccountTags 超过 5 个截断", () => {
const text = '["一","二","三","四","五","六"]';
assert.deepEqual(parseAccountTags(text), ["一", "二", "三", "四", "五"]);
});
test("parseAccountTags 无法解析时返回空数组", () => {
assert.deepEqual(parseAccountTags("无法生成标签"), []);
assert.deepEqual(parseAccountTags(""), []);
assert.deepEqual(parseAccountTags('{"tags":"美妆"}'), []);
});
test("buildAccountTagText 包含关键字段", () => {
const text = buildAccountTagText(input);
assert.ok(text.includes("小美的日常"));
assert.ok(text.includes("分享护肤和生活"));
assert.ok(text.includes("粉丝数:1200"));
assert.ok(text.includes("美妆"));
assert.ok(text.includes("平价护肤分享"));
});
test("buildAccountTagText 粉丝数为 0 或空时省略", () => {
assert.ok(!buildAccountTagText({ ...input, followers: 0 }).includes("粉丝数"));
assert.ok(
!buildAccountTagText({ ...input, followers: null }).includes("粉丝数"),
);
});
test("generateAccountTagsFromMcp 工具名为空时不发起请求", async () => {
const fetchImpl = async () => {
throw new Error("should not be called");
};
const tags = await generateAccountTagsFromMcp(
input,
config,
{},
fetchImpl,
);
assert.deepEqual(tags, []);
});
test("generateAccountTagsFromMcp 正常返回标签数组", async () => {
const { fetchImpl, calls } = createFakeTaggingMcp(["美妆", "探店", "好物分享"]);
const tags = await generateAccountTagsFromMcp(
input,
config,
taggingEnv,
fetchImpl,
);
assert.deepEqual(tags, ["美妆", "探店", "好物分享"]);
const toolCall = calls.find((call) => call.body.method === "tools/call");
assert.equal(toolCall.body.params.name, "generate_tags");
const args = toolCall.body.params.arguments;
assert.equal(args.objective, "为KOC账号生成运营标签");
assert.ok(args.text.includes("小美的日常"));
assert.ok(Array.isArray(args.rules) && args.rules.length > 0);
assert.equal(args.output_schema.properties.tags.type, "array");
assert.ok(String(toolCall.url).includes("key=test-key"));
});
test("generateAccountTagsFromMcp 支持对象包裹的返回", async () => {
const { fetchImpl } = createFakeTaggingMcp({ tags: ["母婴", "育儿"] });
const tags = await generateAccountTagsFromMcp(
input,
config,
taggingEnv,
fetchImpl,
);
assert.deepEqual(tags, ["母婴", "育儿"]);
});
test("generateAccountTagsFromMcp 工具报错时返回空数组", async () => {
const { fetchImpl } = createFakeTaggingMcp({ message: "boom" }, true);
const tags = await generateAccountTagsFromMcp(
input,
config,
taggingEnv,
fetchImpl,
);
assert.deepEqual(tags, []);
});
test("generateAccountTagsFromMcp 网络异常时返回空数组", async () => {
const fetchImpl = async () => {
throw new Error("network down");
};
const tags = await generateAccountTagsFromMcp(
input,
config,
taggingEnv,
fetchImpl,
);
assert.deepEqual(tags, []);
});
const completeRow = {
nickname: "小美",
publicAccountId: "xhs-123",
ipLocation: "上海",
followersResolved: true,
gender: "女",
bio: "分享生活",
tags: ["美妆"],
};
test("needsResourceEnrichment 资料齐全且有标签时为 false", () => {
assert.equal(needsResourceEnrichment(completeRow), false);
});
test("needsResourceEnrichment 资料齐全但无标签时为 true", () => {
assert.equal(needsResourceEnrichment({ ...completeRow, tags: [] }), true);
});
test("needsResourceEnrichment 缺资料且有标签时为 true", () => {
assert.equal(
needsResourceEnrichment({ ...completeRow, bio: "" }),
true,
);
});