You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
298 lines
10 KiB
298 lines
10 KiB
import { z } from "zod";
|
|
import type { JSONSchema7 } from "json-schema";
|
|
import type { ToolExecutor, ToolContext, ToolResult } from "../../registry";
|
|
import type { DocumentToolConfig } from "./config";
|
|
import {
|
|
createDocument,
|
|
getDocumentById,
|
|
listDocuments,
|
|
updateDocument,
|
|
deleteDocument,
|
|
deserializeTags,
|
|
type AgentDocumentType,
|
|
} from "../../../agent-document";
|
|
|
|
const documentActionSchema = z.enum(["create", "read", "update", "delete", "list"]);
|
|
|
|
export const documentInputSchema = z.object({
|
|
action: documentActionSchema.describe("文档操作类型"),
|
|
title: z.string().min(1).max(200).optional().describe("文档标题(create/update 必填)"),
|
|
content: z.string().max(50000).optional().describe("文档正文(create/update 必填)"),
|
|
type: z.enum(["memory", "document"]).optional().describe("文档类型,默认 document"),
|
|
tags: z.array(z.string()).optional().describe("标签列表"),
|
|
id: z.number().int().positive().optional().describe("文档 ID(read/update/delete 必填)"),
|
|
keyword: z.string().optional().describe("搜索关键词(list 时可选)"),
|
|
page: z.number().int().positive().optional().describe("页码(list 时可选,默认 1)"),
|
|
pageSize: z.number().int().positive().max(100).optional().describe("每页数量(list 时可选,默认 20)"),
|
|
fullContent: z.boolean().optional().describe("read 时是否返回完整 content,默认 false(仅返回 summary)"),
|
|
});
|
|
|
|
type DocumentInput = z.infer<typeof documentInputSchema>;
|
|
|
|
function toISO(date: Date | null | undefined): string | null {
|
|
return date ? date.toISOString() : null;
|
|
}
|
|
|
|
export const documentExecutor: ToolExecutor<DocumentToolConfig> = {
|
|
buildInputSchema(_config: DocumentToolConfig): JSONSchema7 {
|
|
return {
|
|
type: "object",
|
|
properties: {
|
|
action: {
|
|
type: "string",
|
|
enum: ["create", "read", "update", "delete", "list"],
|
|
description: "文档操作类型",
|
|
},
|
|
title: { type: "string", description: "文档标题(create/update 必填)" },
|
|
content: { type: "string", description: "文档正文(create/update 必填)" },
|
|
type: {
|
|
type: "string",
|
|
enum: ["memory", "document"],
|
|
description: "文档类型,默认 document",
|
|
},
|
|
tags: { type: "array", items: { type: "string" }, description: "标签列表" },
|
|
id: { type: "number", description: "文档 ID(read/update/delete 必填)" },
|
|
keyword: { type: "string", description: "搜索关键词(list 时可选)" },
|
|
page: { type: "number", description: "页码(list 时可选,默认 1)" },
|
|
pageSize: { type: "number", description: "每页数量(list 时可选,默认 20)" },
|
|
fullContent: { type: "boolean", description: "read 时是否返回完整 content,默认 false" },
|
|
},
|
|
required: ["action"],
|
|
};
|
|
},
|
|
|
|
buildDescription(config: DocumentToolConfig): string {
|
|
const approvalDesc =
|
|
config.approvalRequiredActions.length > 0
|
|
? `需要审批的操作: ${config.approvalRequiredActions.join(", ")}。`
|
|
: "无需审批。";
|
|
return `创建、读取、更新、删除、列出文档(memory/document 两种类型)。${approvalDesc}用于上下文记忆持久化和交付物产出。`;
|
|
},
|
|
|
|
async execute(
|
|
input: unknown,
|
|
config: DocumentToolConfig,
|
|
ctx: ToolContext,
|
|
): Promise<ToolResult> {
|
|
const start = Date.now();
|
|
|
|
const parsed = documentInputSchema.safeParse(input);
|
|
if (!parsed.success) {
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: `输入参数校验失败: ${parsed.error.message}`,
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
const data = parsed.data;
|
|
|
|
if (ctx.userId === null || ctx.userId === undefined) {
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: "无法识别当前用户:会话上下文中缺少 userId。该工具需要在登录会话中调用。",
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
const userId = ctx.userId;
|
|
|
|
try {
|
|
switch (data.action) {
|
|
case "create": {
|
|
if (!data.title || !data.content) {
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: "create 操作需要 title 和 content 参数",
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
if (data.content.length > config.maxContentLength) {
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: `content 长度超过上限 ${config.maxContentLength}`,
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
const doc = await createDocument({
|
|
userId,
|
|
agentId: ctx.agentId ?? null,
|
|
type: data.type ?? config.defaultType,
|
|
title: data.title,
|
|
content: data.content,
|
|
tags: data.tags ?? null,
|
|
});
|
|
return {
|
|
success: true,
|
|
data: {
|
|
id: doc.id,
|
|
type: doc.type,
|
|
title: doc.title,
|
|
summary: doc.summary,
|
|
tags: deserializeTags(doc.tags),
|
|
createdAt: toISO(doc.createdAt),
|
|
updatedAt: toISO(doc.updatedAt),
|
|
},
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
case "read": {
|
|
if (!data.id) {
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: "read 操作需要 id 参数",
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
const doc = await getDocumentById(data.id, userId);
|
|
if (!doc) {
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: `文档不存在或无权访问 (id=${data.id})`,
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
const result: Record<string, unknown> = {
|
|
id: doc.id,
|
|
type: doc.type,
|
|
title: doc.title,
|
|
summary: doc.summary,
|
|
tags: deserializeTags(doc.tags),
|
|
createdAt: toISO(doc.createdAt),
|
|
updatedAt: toISO(doc.updatedAt),
|
|
};
|
|
if (data.fullContent) {
|
|
result.content = doc.content;
|
|
}
|
|
return {
|
|
success: true,
|
|
data: result,
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
case "update": {
|
|
if (!data.id) {
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: "update 操作需要 id 参数",
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
if (data.content && data.content.length > config.maxContentLength) {
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: `content 长度超过上限 ${config.maxContentLength}`,
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
const updated = await updateDocument(data.id, userId, {
|
|
title: data.title,
|
|
content: data.content,
|
|
tags: data.tags,
|
|
});
|
|
if (!updated) {
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: `文档不存在或无权访问 (id=${data.id})`,
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
return {
|
|
success: true,
|
|
data: {
|
|
id: updated.id,
|
|
type: updated.type,
|
|
title: updated.title,
|
|
summary: updated.summary,
|
|
tags: deserializeTags(updated.tags),
|
|
createdAt: toISO(updated.createdAt),
|
|
updatedAt: toISO(updated.updatedAt),
|
|
},
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
case "delete": {
|
|
if (!data.id) {
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: "delete 操作需要 id 参数",
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
const deleted = await deleteDocument(data.id, userId);
|
|
if (!deleted) {
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: `文档不存在或无权访问 (id=${data.id})`,
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
return {
|
|
success: true,
|
|
data: { id: data.id, deleted: true },
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
case "list": {
|
|
const result = await listDocuments({
|
|
userId,
|
|
agentId: ctx.agentId ?? undefined,
|
|
type: data.type as AgentDocumentType | undefined,
|
|
keyword: data.keyword,
|
|
page: data.page,
|
|
pageSize: data.pageSize ?? config.listPageSize,
|
|
});
|
|
return {
|
|
success: true,
|
|
data: {
|
|
items: result.items.map((item) => ({
|
|
id: item.id,
|
|
type: item.type,
|
|
title: item.title,
|
|
summary: item.summary,
|
|
tags: deserializeTags(item.tags),
|
|
createdAt: toISO(item.createdAt),
|
|
updatedAt: toISO(item.updatedAt),
|
|
})),
|
|
total: result.total,
|
|
page: result.page,
|
|
pageSize: result.pageSize,
|
|
hasMore: result.hasMore,
|
|
},
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
|
|
default:
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: `未知的 action: ${data.action}`,
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
} catch (e) {
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: e instanceof Error ? e.message : String(e),
|
|
metadata: { durationMs: Date.now() - start },
|
|
};
|
|
}
|
|
},
|
|
};
|
|
|