Files
koc-loop/lib/mcp-tools.ts

248 lines
10 KiB
TypeScript
Raw Normal View History

2026-08-07 14:12:52 +08:00
import { type McpServer } from "@modelcontextprotocol/server";
import { z } from "zod/v4";
import {
backfillResourceProfile,
collectNow,
recoveryExport,
recoveryList,
resourceExport,
resourceGet,
resourceSearch,
retryFailed,
runDueCollections,
setCollectionPlan,
taskGet,
taskList,
type McpOperationBindings,
} from "./mcp-operations";
type Options = {
bindings: McpOperationBindings;
origin: string;
};
function result(label: string, data: unknown) {
return {
content: [{ type: "text" as const, text: label }],
structuredContent: data as Record<string, unknown>,
};
}
function errorResult(error: unknown) {
const message = error instanceof Error ? error.message : "操作失败,请稍后重试";
return {
isError: true as const,
content: [{ type: "text" as const, text: message.slice(0, 240) }],
};
}
function withError<T extends unknown[]>(handler: (...args: T) => Promise<ReturnType<typeof result>>) {
return async (...args: T) => {
try {
return await handler(...args);
} catch (error) {
return errorResult(error);
}
};
}
const pagination = {
limit: z.number().int().min(1).max(200).optional().describe("返回数量默认50最大200"),
offset: z.number().int().min(0).optional().describe("分页偏移量默认0"),
};
const resourceFilters = {
query: z.string().max(100).optional().describe("账号名称、账号号、简介或标签,支持模糊搜索"),
2026-08-07 14:12:52 +08:00
ip_location: z.string().max(100).optional().describe("IP地区关键词支持模糊搜索"),
cooperation_source: z.string().max(100).optional().describe("历史合作来源关键词,支持模糊搜索"),
platform: z.string().max(30).optional().describe("平台,例如小红书;不传表示全部"),
};
export function registerMcpOperationTools(server: McpServer, options: Options) {
server.registerTool(
"task_list",
{
title: "查询任务及进度",
description: "查询 KOC 分发任务列表、领取数、发布数、第7天回收数及领取链接。",
inputSchema: z.object({
query: z.string().max(100).optional().describe("任务名或品牌/项目关键词"),
status: z.string().max(30).optional().describe("任务状态;不传或 all 表示全部"),
...pagination,
}),
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
},
withError(async (input) => {
const data = await taskList(input, options.bindings.KOC_PORTAL_URL?.trim() || "");
return result(`共找到 ${data.total} 个任务。`, data);
}),
);
server.registerTool(
"task_get",
{
title: "查看任务完整情况",
description: "查看指定任务、笔记、领取记录、发布回填和采集执行记录。",
inputSchema: z.object({ task_id: z.string().min(1).describe("任务ID") }),
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
},
withError(async ({ task_id }) => {
const data = await taskGet(task_id, options.bindings.KOC_PORTAL_URL?.trim() || "");
return result(`已读取任务“${String(data.task.name)}”的完整情况。`, data);
}),
);
server.registerTool(
"recovery_list",
{
title: "查询数据回收队列",
description: "查询已发布、未回填、待第7天数据或已到第7天仍未回填的笔记。",
inputSchema: z.object({
task_id: z.string().optional().describe("任务ID不传则跨任务查询"),
stage: z.enum(["all", "published", "unfilled", "waiting_day7", "day7_due"]).optional(),
...pagination,
}),
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
},
withError(async ({ task_id, stage, limit, offset }) => {
const data = await recoveryList({ taskId: task_id, stage, limit, offset });
return result(`数据回收队列共 ${data.total} 条记录。`, data);
}),
);
server.registerTool(
"recovery_export",
{
title: "导出任务完整数据",
description: "生成任务完整 Excel包含笔记原图、发布截图、创作者截图和回收数据。",
inputSchema: z.object({ task_id: z.string().min(1).describe("任务ID") }),
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false },
},
withError(async ({ task_id }) => {
const data = await recoveryExport(task_id, options.origin);
return result(`导出文件已生成,下载链接将在 ${data.expires_at} 失效。`, data);
}),
);
server.registerTool(
"collection_plan_set",
{
title: "设置自动采集计划",
description: "为任务设置开始日期及第1至第7天的自动采集日系统在北京时间09:00执行。",
2026-08-07 14:12:52 +08:00
inputSchema: z.object({
task_id: z.string().min(1),
start_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("北京时间开始日期 YYYY-MM-DD"),
days: z.array(z.number().int().min(1).max(7)).min(1).max(7).describe("需要采集的相对天数,例如 [2,5,7]"),
}),
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
},
withError(async ({ task_id, start_date, days }) => {
const data = await setCollectionPlan(task_id, start_date, days, options.bindings);
return result(`已为任务设置 ${data.days.length} 个自动采集日。`, data);
}),
);
server.registerTool(
"collection_run_due",
{
title: "执行到期采集",
description: "立即执行今天或此前已到期但尚未成功的自动采集任务。",
inputSchema: z.object({ task_id: z.string().optional().describe("任务ID不传则执行所有到期任务") }),
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
},
withError(async ({ task_id }) => {
const data = await runDueCollections(task_id, options.bindings);
return result(`到期采集完成:成功 ${data.succeeded},失败 ${data.failed}`, data);
}),
);
server.registerTool(
"collection_collect_now",
{
title: "立即采集指定笔记",
description: "对指定分发记录立即采集互动数据;小红书为点赞/收藏/评论,抖音另含转发。",
2026-08-07 14:12:52 +08:00
inputSchema: z.object({
distribution_id: z.string().min(1).describe("分发记录ID可从 task_get 或 recovery_list 获取"),
schedule_day: z.number().int().min(1).max(7).optional().describe("标记为第几天采集,可不传"),
}),
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
},
withError(async ({ distribution_id, schedule_day }) => {
const data = await collectNow(distribution_id, schedule_day, options.bindings);
return result("指定笔记采集完成。", data);
}),
);
server.registerTool(
"collection_retry_failed",
{
title: "补采异常数据",
description: "重试指定任务或全部任务中采集状态异常的已发布笔记。",
inputSchema: z.object({ task_id: z.string().optional().describe("任务ID不传则补采最近异常任务") }),
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
},
withError(async ({ task_id }) => {
const data = await retryFailed(task_id, options.bindings);
return result(`补采完成:成功 ${data.succeeded},失败 ${data.failed}`, data);
}),
);
server.registerTool(
"resource_search",
{
title: "搜索 KOC 账号资源",
description: "按账号名称/账号号/标签、IP地区、合作来源或平台搜索 KOC 资源。",
2026-08-07 14:12:52 +08:00
inputSchema: z.object({ ...resourceFilters, ...pagination }),
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
},
withError(async ({ query, ip_location, cooperation_source, platform, limit, offset }) => {
const data = await resourceSearch({ query, ipLocation: ip_location, cooperationSource: cooperation_source, platform, limit, offset });
return result(`共找到 ${data.total} 个账号。`, data);
}),
);
server.registerTool(
"resource_get",
{
title: "查看 KOC 账号详情",
description: "查看账号主页、账号号、粉丝数、性别、简介、标签、IP地区以及全部合作记录。",
2026-08-07 14:12:52 +08:00
inputSchema: z.object({ account_id: z.string().min(1).describe("KOC LOOP 账号ID") }),
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
},
withError(async ({ account_id }) => {
const data = await resourceGet(account_id);
return result(`已读取账号“${String(data.account.nickname)}”的详情。`, data);
}),
);
server.registerTool(
"resource_backfill_profile",
{
title: "补全公开账号信息",
description: "根据已回填的小红书或抖音作品链接补全账号主页、昵称、账号号、IP地区和粉丝数。",
2026-08-07 14:12:52 +08:00
inputSchema: z.object({
distribution_id: z.string().optional().describe("分发记录ID和发布链接二选一"),
publish_url: z.string().optional().describe("小红书或抖音作品链接,也可传包含链接的分享文案"),
2026-08-07 14:12:52 +08:00
}).refine((value) => Boolean(value.distribution_id || value.publish_url), "请提供分发记录ID或发布链接"),
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
},
withError(async ({ distribution_id, publish_url }) => {
const data = await backfillResourceProfile({ distributionId: distribution_id, publishUrl: publish_url }, options.bindings);
return result(data.updated ? "账号公开信息已补全。" : "账号信息未发生变化。", data);
}),
);
server.registerTool(
"resource_export",
{
title: "导出 KOC 资源",
description: "按账号、IP地区、合作来源或平台筛选并导出 KOC 资源 Excel。",
inputSchema: z.object(resourceFilters),
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false },
},
withError(async ({ query, ip_location, cooperation_source, platform }) => {
const data = await resourceExport({ query, ipLocation: ip_location, cooperationSource: cooperation_source, platform }, options.origin);
return result(`已生成 ${data.account_count} 个账号的导出文件。`, data);
}),
);
}