feat: KOC 资源导入后自动爬取主页资料并调用 LLM 打标签

- 新增 structured_text_process 打标客户端(文本构造、标签解析清洗、MCP 调用)
- 写库后后台打标(tagUntaggedAccounts),单条新增 / 批量导入 / 每日补全三个入口统一
- 标签只补空,UPDATE 带 tags 为空守卫,不覆盖已有标签
- 新增 AI_TAGGING_TOOL 配置项(留空关闭),更新配置示例与文档
- 新增 16 个打标测试用例,全量 131 个测试通过
This commit is contained in:
ABAPPLO
2026-08-26 15:21:28 +08:00
parent 9697b5890d
commit 4a64feee02
13 changed files with 557 additions and 11 deletions

View File

@@ -1,9 +1,12 @@
import {
generateAccountTagsFromMcp,
resolveXhsPublicAccountDetails,
resolveAccountProfileFromMcp,
resolveProfileDetailsFromMcp,
type AccountTaggingEnv,
type CollectionMcpConfig,
} from "./mcp-collection-client";
import { getRuntimeEnv } from "./runtime-env";
import { hashText } from "./mvp-db";
import type { DatabaseClient } from "./database";
@@ -260,6 +263,7 @@ export async function backfillAccountProfiles(
mcpConfig: CollectionMcpConfig,
limit = 10,
) {
const taggingEnv: AccountTaggingEnv = getRuntimeEnv();
const rows = await db
.prepare(
`SELECT
@@ -334,6 +338,7 @@ export async function backfillAccountProfiles(
const isDemoAccount = row.platform_uid?.startsWith("xhs-");
let attemptedThisRow = false;
let publicProfileUpdated = false;
let tagsUpdated = false;
if (
row.platform === "小红书" &&
!isDemoAccount &&
@@ -341,7 +346,8 @@ export async function backfillAccountProfiles(
(!row.public_account_id ||
Number(row.followers ?? 0) === 0 ||
!row.gender ||
!row.bio)
!row.bio ||
!row.tags?.trim())
) {
attempted += 1;
attemptedThisRow = true;
@@ -402,6 +408,39 @@ export async function backfillAccountProfiles(
.run();
publicProfileUpdated = true;
}
if (
row.account_id &&
!row.tags?.trim() &&
(details.bio ||
details.providerTags.length > 0 ||
details.recentNoteTitles.length > 0)
) {
const tags = await generateAccountTagsFromMcp(
{
platform: "小红书",
nickname: row.nickname || details.nickname || "",
bio: details.bio,
gender: details.gender,
ipLocation: details.ipLocation ?? "",
followers: details.followers,
providerTags: details.providerTags,
recentNoteTitles: details.recentNoteTitles,
},
mcpConfig,
taggingEnv,
).catch(() => []);
if (tags.length > 0) {
await db
.prepare(
`UPDATE accounts
SET tags = CASE WHEN ? != '' THEN ? ELSE tags END
WHERE id = ?`,
)
.bind(tags.join(","), tags.join(","), row.account_id)
.run();
tagsUpdated = true;
}
}
if (
(details.redId || row.public_account_id) &&
Number(details.followers ?? row.followers ?? 0) > 0
@@ -410,7 +449,7 @@ export async function backfillAccountProfiles(
continue;
}
if (attempted >= Math.max(1, Math.min(25, limit))) {
if (publicProfileUpdated) updated += 1;
if (publicProfileUpdated || tagsUpdated) updated += 1;
continue;
}
}
@@ -430,7 +469,7 @@ export async function backfillAccountProfiles(
row.platform_uid?.startsWith("pending-") ||
Boolean(noteId && row.platform_uid === noteId));
if (!needsEnrichment || !row.publish_url) {
if (publicProfileUpdated) updated += 1;
if (publicProfileUpdated || tagsUpdated) updated += 1;
continue;
}
@@ -443,9 +482,9 @@ export async function backfillAccountProfiles(
row.nickname || "待识别账号",
mcpConfig,
);
if (result.updated || publicProfileUpdated) updated += 1;
if (result.updated || publicProfileUpdated || tagsUpdated) updated += 1;
} catch {
if (publicProfileUpdated) updated += 1;
if (publicProfileUpdated || tagsUpdated) updated += 1;
else failed += 1;
}
}