Files
koc-loop/lib/mcp-tools.ts
2026-08-15 03:53:09 +08:00

248 lines
10 KiB
TypeScript
Raw 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 { 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("账号名称、账号号、简介或标签,支持模糊搜索"),
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执行。",
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: "对指定分发记录立即采集互动数据;小红书为点赞/收藏/评论,抖音另含转发。",
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 资源。",
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地区以及全部合作记录。",
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地区和粉丝数。",
inputSchema: z.object({
distribution_id: z.string().optional().describe("分发记录ID和发布链接二选一"),
publish_url: z.string().optional().describe("小红书或抖音作品链接,也可传包含链接的分享文案"),
}).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);
}),
);
}