Browse Source
- 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 <codefree@chinatelcom.cn>feat/agent-independence
6 changed files with 330 additions and 10 deletions
Binary file not shown.
@ -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<typeof documentConfigSchema>; |
||||
|
|
||||
|
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; |
||||
|
} |
||||
@ -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<typeof documentInputSchema>; |
||||
|
|
||||
|
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, |
||||
|
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<string, unknown> = { |
||||
|
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 }, |
||||
|
}; |
||||
|
} |
||||
|
}, |
||||
|
}; |
||||
Loading…
Reference in new issue