import { dbGlobal } from "drizzle-pkg/lib/db"; import { agentTools } from "drizzle-pkg/lib/schema/agent-tool"; import { eq, asc } from "drizzle-orm"; import { tool, jsonSchema } from "ai"; import { z } from "zod"; import { registerToolType, getExecutor, type ToolContext, type ToolResult } from "./registry"; import { writeToolLog } from "./log"; import { parseFetchConfig, DEFAULT_FETCH_CONFIG } from "./executors/fetch/config"; import { fetchExecutor } from "./executors/fetch/fetch"; // 立即注册 fetch 工具类型,确保在任何 execute 调用前完成 registerToolType("fetch", fetchExecutor); // fetch 工具的固定 input schema const FETCH_INPUT_SCHEMA = z.object({ url: z.string().describe("要抓取的 URL"), }); export type AgentToolRow = typeof agentTools.$inferSelect; export interface CreateAgentToolInput { name: string; slug: string; description: string; type: string; config: Record; enabled?: boolean; sortOrder?: number; } export interface UpdateAgentToolInput { name?: string; slug?: string; description?: string; type?: string; config?: Record; enabled?: boolean; sortOrder?: number; } export async function listAgentTools(): Promise { return dbGlobal.select().from(agentTools).orderBy(asc(agentTools.sortOrder), asc(agentTools.createdAt)); } export async function getAgentToolById(id: string): Promise { const rows = await dbGlobal.select().from(agentTools).where(eq(agentTools.id, id)).limit(1); return rows[0] ?? null; } export async function getAgentToolBySlug(slug: string): Promise { const rows = await dbGlobal.select().from(agentTools).where(eq(agentTools.slug, slug)).limit(1); return rows[0] ?? null; } function generateId(): string { return `at_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`; } function validateConfig(type: string, config: Record): Record { if (type === "fetch") { return parseFetchConfig(config); } return config; } export async function createAgentTool(input: CreateAgentToolInput): Promise { const existing = await getAgentToolBySlug(input.slug); if (existing) { throw new Error(`slug 已存在: ${input.slug}`); } const validatedConfig = validateConfig(input.type, input.config); const id = generateId(); await dbGlobal.insert(agentTools).values({ id, name: input.name, slug: input.slug, description: input.description, type: input.type, config: JSON.stringify(validatedConfig), enabled: input.enabled === false ? 0 : 1, sortOrder: input.sortOrder ?? 0, }); const row = await getAgentToolById(id); if (!row) throw new Error("创建后查询失败"); return row; } export async function updateAgentTool(id: string, input: UpdateAgentToolInput): Promise { const existing = await getAgentToolById(id); if (!existing) return null; if (input.slug && input.slug !== existing.slug) { const slugConflict = await getAgentToolBySlug(input.slug); if (slugConflict) { throw new Error(`slug 已存在: ${input.slug}`); } } const updates: Partial = {}; if (input.name !== undefined) updates.name = input.name; if (input.slug !== undefined) updates.slug = input.slug; if (input.description !== undefined) updates.description = input.description; if (input.type !== undefined) updates.type = input.type; if (input.config !== undefined) { const typeToValidate = input.type ?? existing.type; updates.config = JSON.stringify(validateConfig(typeToValidate, input.config)); } if (input.enabled !== undefined) updates.enabled = input.enabled ? 1 : 0; if (input.sortOrder !== undefined) updates.sortOrder = input.sortOrder; if (Object.keys(updates).length === 0) return existing; await dbGlobal.update(agentTools).set(updates).where(eq(agentTools.id, id)); return getAgentToolById(id); } export async function deleteAgentTool(id: string): Promise { await dbGlobal.delete(agentTools).where(eq(agentTools.id, id)); } export async function executeAgentTool( id: string, input: unknown, userId: number | null, ): Promise { const agentTool = await getAgentToolById(id); if (!agentTool) { return { success: false, data: null, error: `工具不存在: ${id}`, metadata: { durationMs: 0 }, }; } if (!agentTool.enabled) { return { success: false, data: null, error: `工具已禁用: ${agentTool.slug}`, metadata: { durationMs: 0 }, }; } const executor = getExecutor(agentTool.type); if (!executor) { return { success: false, error: `工具类型未注册: ${agentTool.type}`, metadata: { durationMs: 0 }, }; } let config: unknown; try { config = JSON.parse(agentTool.config); } catch { config = agentTool.type === "fetch" ? DEFAULT_FETCH_CONFIG : {}; } const ctx: ToolContext = { toolId: agentTool.id, toolSlug: agentTool.slug, userId, }; const result = await executor.execute(input, config, ctx); // 写日志 const status = result.success ? "success" : result.error?.includes("超时") ? "timeout" : "error"; await writeToolLog({ toolId: agentTool.id, toolSlug: agentTool.slug, userId, input: JSON.stringify(input), output: result.success ? JSON.stringify(result.data)?.slice(0, 10000) ?? null : null, status: status as "success" | "error" | "timeout", errorMessage: result.error ?? null, durationMs: result.metadata?.durationMs ?? 0, }); return result; } export async function getEnabledToolsForLlm(): Promise>> { const tools = await dbGlobal .select() .from(agentTools) .where(eq(agentTools.enabled, 1)) .orderBy(asc(agentTools.sortOrder)); const result: Record = {}; for (const agentTool of tools) { const executor = getExecutor(agentTool.type); if (!executor) continue; let config: unknown; try { config = JSON.parse(agentTool.config); } catch { config = agentTool.type === "fetch" ? DEFAULT_FETCH_CONFIG : {}; } const zodSchema = agentTool.type === "fetch" ? FETCH_INPUT_SCHEMA : z.object({}); const jsonSch = z.toJSONSchema(zodSchema) as Record; result[agentTool.slug] = tool({ description: executor.buildDescription(config), parameters: jsonSchema(jsonSch, { validate: (v: unknown) => { const r = zodSchema.safeParse(v); return r.success ? { success: true as const, value: r.data } : { success: false as const, error: r.error }; }, }), execute: async (input: unknown) => { const execResult = await executeAgentTool(agentTool.id, input, null); if (!execResult.success) { return `工具执行失败: ${execResult.error ?? "未知错误"}。请停止调用此工具,基于已有信息回答用户或告知用户此工具不可用。`; } // 成功时返回内容,附带元信息帮助模型判断结果是否有效 const meta = execResult.metadata; const sizeInfo = meta?.responseSize ? `${meta.responseSize} bytes` : `未知大小`; const statusInfo = meta?.statusCode ? `HTTP ${meta.statusCode}` : ""; const dataStr = typeof execResult.data === "string" ? execResult.data : JSON.stringify(execResult.data); return `[fetch 结果 ${statusInfo} ${sizeInfo}]\n${dataStr}`; }, }); } return result; } export { DEFAULT_FETCH_CONFIG } from "./executors/fetch/config"; export type { FetchToolConfig } from "./executors/fetch/config"; export type { ToolExecutor, ToolContext, ToolResult } from "./registry";