Browse Source

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 <codefree@chinatelcom.cn>
feat/agent-independence
npmrun 2 months ago
parent
commit
b40bfa40e5
  1. BIN
      packages/drizzle-pkg/db.sqlite
  2. 25
      server/service/agent-tool/executors/document/config.ts
  3. 292
      server/service/agent-tool/executors/document/document.ts
  4. 20
      server/service/agent-tool/index.ts
  5. 1
      server/service/agent-tool/registry.ts
  6. 2
      server/service/agent/chat-engine.ts

BIN
packages/drizzle-pkg/db.sqlite

Binary file not shown.

25
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<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;
}

292
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<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 },
};
}
},
};

20
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<string, ToolTypeRegistration> = {
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<ToolResult> {
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";

1
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 {

2
server/service/agent/chat-engine.ts

@ -352,7 +352,7 @@ export async function executeChat(params: ChatEngineParams): Promise<ChatEngineR
continue;
}
try {
const result = await executeAgentTool(agentTool.id, p.args, user?.id ?? null);
const result = await executeAgentTool(agentTool.id, p.args, user?.id ?? null, p.approved);
approvalToolResults[p.toolCallId] = result;
logger.info("[%s] [APPROVAL-EXEC] tool=%s result=%s", requestId, p.toolName, JSON.stringify(result).slice(0, 200));
} catch (err) {

Loading…
Cancel
Save