From b40bfa40e551089a4587cdcf2cb3e916ca4c38ea Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Sun, 9 Aug 2026 12:59:59 +0800 Subject: [PATCH] feat: add document tool executor with config, registration, and approved flag in ToolContext - Create document executor config (approvalRequiredActions, defaultType, maxContentLength, listPageSize) - Implement document executor with create/read/update/delete/list actions - Register document executor in TOOL_TYPE_REGISTRY - Extend ToolContext with approved?: boolean field - Update executeAgentTool signature to accept approved parameter - Pass approved flag from chat-engine to executeAgentTool - Remove stale guess-who references (executor never existed) Co-authored-by: CodeFree --- packages/drizzle-pkg/db.sqlite | Bin 655360 -> 659456 bytes .../agent-tool/executors/document/config.ts | 25 ++ .../agent-tool/executors/document/document.ts | 292 +++++++++++++++++++++ server/service/agent-tool/index.ts | 20 +- server/service/agent-tool/registry.ts | 1 + server/service/agent/chat-engine.ts | 2 +- 6 files changed, 330 insertions(+), 10 deletions(-) create mode 100644 server/service/agent-tool/executors/document/config.ts create mode 100644 server/service/agent-tool/executors/document/document.ts diff --git a/packages/drizzle-pkg/db.sqlite b/packages/drizzle-pkg/db.sqlite index 31520ef0da40bd1d371b1e61d63b3db667c5927d..d4c4e2e5725a7f156da9d9c15ff05ebf316d80f3 100644 GIT binary patch delta 3005 zcmb_eZERCj7{0e3>&n{uwu7;crdY3#F)32Efl!zP(fCIk(V+nXu4ymO(C(J@RzcF- zwnH|cd`NX0LuJE7&_u>kWpjhI-D8U$m@4dITFv?^CP0sCm z-+Rt^&v~Brea;Do=ZAadA2q2n1VQw^XIVvN2Aca8=@f7i9Yinu4L1`8TN>uzTf71v z$4d10EP@yTf50#BEu4p5h$W2Yc0t+9BIN?ov5jfr-gL3fpcjpRBHr6Li!{a0H@;F~ zqKpJ=1`jk+A!-M;fqIErLOn$p6V$pt^}JD8O>!*j$&WP^c&Z9Kl?9%P0#A8?XLf<7 ztTb_S3r8wV)aAIQxrTx&08mkXQP-(4>T<$p=%&`rP^wAtWHR}5ttoFVsLGq>%Dic) z$eWsS*{r!eJ8#}9i_c@PC=Er;agEPHHe-NgMSQg00M%x5)NGD8wAx~YT0IF58B_T` z)qWy1GCUTJWcx=_M>^0*pZGG}aSn}Cd(ZgsKGEn*wGF2`+S0wBOk8O9GG5lddyzbY zB{|fWIejQIFp~QC-~@kR|5!MhdH+mu_;7OQKyv6Hj?11smm1xdjzyARznhA6q>jas z!+pt-uFU?=lEbHQZX6;{w}*E4e17&Vhv!vhXVC3uT&ta&n{5fu_D!^l@h}`iZ?>Yf z)e-b?O}l*zZKvhIv~@Y{b?jQhws4p?FEcIMxb3uki4A{N+T#vz>oBukbH9UQYHcB+ zLtui6NY6y)+uq{22I?vSt&o5+Oi2xO6|TW|@GH!P9n=k254G?-#1ZI0sGUTJ`VR!vWv zria9)z^25e!luSngiSN3=_&q)pQ9&E5&A8<#*)yCecB_%7d2;!zENLN*_CU^tHi0e z(GcdND=KtEwOQ5Jy2(V4_s;^N>)3GwTm%?l6cD1VG$(;7@l1#I&~A?L3izYtLgeo4 zf$wh4Bk=IzF2)(;Sihx-;RBrI1=fkv1%h6$!@rxhw*);N?CaPja>+4bFyD4ZfT^1Neye@s9=|(jgTQl93%fp6==&4|iur+boPsv7{WC?!D=+4rcp5 zkmVsJ)&4mDc`Vx>v8)s5wbWTs2To>NFJhh3GRz{%ng};rM7bgAco>wYMwGu#PCg=} z{|iyRAa(EFc+KrrDANn&8D)i?Pp>|u3K1%_u-@`}%kjoPEOAruhEXT!!jzRPal%hi zC-Uol%ra3wJ)Owa{0t!*Yiql*= zK>=6L{}UKMrK%J^NNZJePE2S`3b=+}e}gBrDit9OD@IEzOdwpyl++Uhb%Q!YJ%Q(w zk>Wk35+Oqyi1Ih4*1zt%&X2qFZ*hq6`&qxF3dx~EsV}-xAD&6KcceRe(jT>E&yJ3F zp0&((yBZePh#RYQR*BJ8uaK*O&tKx?GxDC}Dt8;m3GN*!ko+jLqUPopR*O*C*#%Jp`=H*u-;$i+5J&P?= delta 592 zcmX|8L1@!p6i%9^NjjS^tDPtl8$v59Za8)v&fzZVL8}M7xh}L>TWlI>6K0js&Vhn# zR+mHdv<^hXlQr0YATmT0RuDbt!GRZdoYUei4#B@0^zc32``&xs|9}5qsSi{t1NZyc zb&8_u59D!ry)pIpxSz%jAt)O=t|$qcI7Eu2o&bqHEK>M`uXu+hR}LGDKO|NuTtN0tEE#g8MRc>UIK}A=uUnD1( zStDOmMn{GRl~i_mI(sB(<;)Tp(zR69(v?|ifnNTxddAv|PHjf#BM{pZ=HZ}^yb^~^ zyvH|0a8+#M5=Jlq8#eLiioqZgqfNueC3bI;Aa0!B_x95fV2r{OvUeG;a1L#J!3L(t z-WYBn3lj}AhysT|fCXueqnbw`_~ZzF^%}R5*E;{O>AiYj1?{mwxsa+`k_MNh0J1YFk@Ezw00N O`~>gyQUdjG>Cu1Fi@?YL diff --git a/server/service/agent-tool/executors/document/config.ts b/server/service/agent-tool/executors/document/config.ts new file mode 100644 index 0000000..4f4fddd --- /dev/null +++ b/server/service/agent-tool/executors/document/config.ts @@ -0,0 +1,25 @@ +import { z } from "zod"; + +export const documentConfigSchema = z.object({ + approvalRequiredActions: z.array(z.enum(["create", "update", "delete"])).default(["delete"]), + defaultType: z.enum(["memory", "document"]).default("document"), + maxContentLength: z.number().int().positive().max(100000).default(50000), + listPageSize: z.number().int().positive().max(100).default(20), +}); + +export type DocumentToolConfig = z.infer; + +export const DEFAULT_DOCUMENT_CONFIG: DocumentToolConfig = { + approvalRequiredActions: ["delete"], + defaultType: "document", + maxContentLength: 50000, + listPageSize: 20, +}; + +export function parseDocumentConfig(raw: unknown): DocumentToolConfig { + const parsed = documentConfigSchema.safeParse(raw); + if (!parsed.success) { + throw new Error(`Invalid document config: ${parsed.error.message}`); + } + return parsed.data; +} diff --git a/server/service/agent-tool/executors/document/document.ts b/server/service/agent-tool/executors/document/document.ts new file mode 100644 index 0000000..874fd5a --- /dev/null +++ b/server/service/agent-tool/executors/document/document.ts @@ -0,0 +1,292 @@ +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; + +export const documentExecutor: ToolExecutor = { + 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 { + 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, + 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: doc.createdAt, + updatedAt: 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 = { + id: doc.id, + type: doc.type, + title: doc.title, + summary: doc.summary, + tags: deserializeTags(doc.tags), + createdAt: doc.createdAt, + updatedAt: 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: updated.createdAt, + updatedAt: 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, + 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: item.createdAt, + updatedAt: 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 }, + }; + } + }, +}; diff --git a/server/service/agent-tool/index.ts b/server/service/agent-tool/index.ts index b7ac3fa..d7837f4 100644 --- a/server/service/agent-tool/index.ts +++ b/server/service/agent-tool/index.ts @@ -35,8 +35,8 @@ import { parseDiceConfig, DEFAULT_DICE_CONFIG } from "./executors/dice/config"; import { diceExecutor, diceInputSchema } from "./executors/dice/dice"; import { parseMagic8ballConfig, DEFAULT_MAGIC8BALL_CONFIG } from "./executors/magic-8ball/config"; import { magic8ballExecutor, magic8ballInputSchema } from "./executors/magic-8ball/magic-8ball"; -import { parseGuessWhoConfig, DEFAULT_GUESS_WHO_CONFIG } from "./executors/guess-who/config"; -import { guessWhoExecutor, guessWhoInputSchema } from "./executors/guess-who/guess-who"; +import { parseDocumentConfig, DEFAULT_DOCUMENT_CONFIG } from "./executors/document/config"; +import { documentExecutor, documentInputSchema } from "./executors/document/document"; // ============ 工具类型注册表 ============ interface ToolTypeRegistration { @@ -125,11 +125,11 @@ const TOOL_TYPE_REGISTRY: Record = { defaultConfig: DEFAULT_MAGIC8BALL_CONFIG, zodSchema: magic8ballInputSchema, }, - "guess-who": { - executor: guessWhoExecutor, - parseConfig: parseGuessWhoConfig, - defaultConfig: DEFAULT_GUESS_WHO_CONFIG, - zodSchema: guessWhoInputSchema, + document: { + executor: documentExecutor, + parseConfig: parseDocumentConfig, + defaultConfig: DEFAULT_DOCUMENT_CONFIG, + zodSchema: documentInputSchema, }, }; @@ -294,6 +294,7 @@ export async function executeAgentTool( id: string, input: unknown, userId: number | null, + approved?: boolean, ): Promise { const agentTool = await getAgentToolById(id); if (!agentTool) { @@ -332,6 +333,7 @@ export async function executeAgentTool( toolId: agentTool.id, toolSlug: agentTool.slug, userId, + approved, }; const result = await executor.execute(input, config, ctx); @@ -622,6 +624,6 @@ export { DEFAULT_DICE_CONFIG } from "./executors/dice/config"; export type { DiceToolConfig } from "./executors/dice/config"; export { DEFAULT_MAGIC8BALL_CONFIG } from "./executors/magic-8ball/config"; export type { Magic8ballToolConfig } from "./executors/magic-8ball/config"; -export { DEFAULT_GUESS_WHO_CONFIG } from "./executors/guess-who/config"; -export type { GuessWhoToolConfig } from "./executors/guess-who/config"; +export { DEFAULT_DOCUMENT_CONFIG } from "./executors/document/config"; +export type { DocumentToolConfig } from "./executors/document/config"; export type { ToolExecutor, ToolContext, ToolResult } from "./registry"; diff --git a/server/service/agent-tool/registry.ts b/server/service/agent-tool/registry.ts index ee4f2d4..e4a4c98 100644 --- a/server/service/agent-tool/registry.ts +++ b/server/service/agent-tool/registry.ts @@ -10,6 +10,7 @@ export interface ToolContext { toolId: string; toolSlug: string; userId: number | null; + approved?: boolean; } export interface ToolResult { diff --git a/server/service/agent/chat-engine.ts b/server/service/agent/chat-engine.ts index ddc7d82..e750631 100644 --- a/server/service/agent/chat-engine.ts +++ b/server/service/agent/chat-engine.ts @@ -352,7 +352,7 @@ export async function executeChat(params: ChatEngineParams): Promise