- 新增 structured_text_process 打标客户端(文本构造、标签解析清洗、MCP 调用) - 写库后后台打标(tagUntaggedAccounts),单条新增 / 批量导入 / 每日补全三个入口统一 - 标签只补空,UPDATE 带 tags 为空守卫,不覆盖已有标签 - 新增 AI_TAGGING_TOOL 配置项(留空关闭),更新配置示例与文档 - 新增 16 个打标测试用例,全量 131 个测试通过
222 lines
6.6 KiB
JavaScript
222 lines
6.6 KiB
JavaScript
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,
|
||
);
|
||
});
|