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
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";
|
|
|