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.
 
 
 
 

234 lines
7.7 KiB

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<string, unknown>;
enabled?: boolean;
sortOrder?: number;
}
export interface UpdateAgentToolInput {
name?: string;
slug?: string;
description?: string;
type?: string;
config?: Record<string, unknown>;
enabled?: boolean;
sortOrder?: number;
}
export async function listAgentTools(): Promise<AgentToolRow[]> {
return dbGlobal.select().from(agentTools).orderBy(asc(agentTools.sortOrder), asc(agentTools.createdAt));
}
export async function getAgentToolById(id: string): Promise<AgentToolRow | null> {
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<AgentToolRow | null> {
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<string, unknown>): Record<string, unknown> {
if (type === "fetch") {
return parseFetchConfig(config);
}
return config;
}
export async function createAgentTool(input: CreateAgentToolInput): Promise<AgentToolRow> {
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<AgentToolRow | null> {
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<AgentToolRow> = {};
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<void> {
await dbGlobal.delete(agentTools).where(eq(agentTools.id, id));
}
export async function executeAgentTool(
id: string,
input: unknown,
userId: number | null,
): Promise<ToolResult> {
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<Record<string, ReturnType<typeof tool>>> {
const tools = await dbGlobal
.select()
.from(agentTools)
.where(eq(agentTools.enabled, 1))
.orderBy(asc(agentTools.sortOrder));
const result: Record<string, any> = {};
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<string, unknown>;
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";