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.
481 lines
17 KiB
481 lines
17 KiB
import { dbGlobal } from "drizzle-pkg/lib/db";
|
|
import { agentTools } from "drizzle-pkg/lib/schema/agent-tool";
|
|
import type { UserRole } from "drizzle-pkg/lib/schema/auth";
|
|
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, fetchInputSchema } from "./executors/fetch/fetch";
|
|
import { parseCalculatorConfig, DEFAULT_CALCULATOR_CONFIG } from "./executors/calculator/config";
|
|
import { calculatorExecutor, calculatorInputSchema } from "./executors/calculator/calculator";
|
|
import { parseDatetimeConfig, DEFAULT_DATETIME_CONFIG } from "./executors/datetime/config";
|
|
import { datetimeExecutor, datetimeInputSchema } from "./executors/datetime/datetime";
|
|
import { parseUuidConfig, DEFAULT_UUID_CONFIG } from "./executors/uuid/config";
|
|
import { uuidExecutor, uuidInputSchema } from "./executors/uuid/uuid";
|
|
import { parseBase64Config, DEFAULT_BASE64_CONFIG } from "./executors/base64/config";
|
|
import { base64Executor, base64InputSchema } from "./executors/base64/base64";
|
|
import { parseJsonFormatterConfig, DEFAULT_JSON_FORMATTER_CONFIG } from "./executors/json-formatter/config";
|
|
import { jsonFormatterExecutor, jsonFormatterInputSchema } from "./executors/json-formatter/json-formatter";
|
|
import { parseRegexTesterConfig, DEFAULT_REGEX_TESTER_CONFIG } from "./executors/regex-tester/config";
|
|
import { regexTesterExecutor, regexTesterInputSchema } from "./executors/regex-tester/regex-tester";
|
|
import { parseUserInfoConfig, DEFAULT_USER_INFO_CONFIG } from "./executors/user-info/config";
|
|
import { userInfoExecutor, userInfoInputSchema } from "./executors/user-info/user-info";
|
|
|
|
// ============ 工具类型注册表 ============
|
|
interface ToolTypeRegistration {
|
|
executor: import("./registry").ToolExecutor<any>;
|
|
parseConfig: (raw: unknown) => Record<string, unknown>;
|
|
defaultConfig: Record<string, unknown>;
|
|
zodSchema: z.ZodType;
|
|
}
|
|
|
|
const TOOL_TYPE_REGISTRY: Record<string, ToolTypeRegistration> = {
|
|
fetch: {
|
|
executor: fetchExecutor,
|
|
parseConfig: parseFetchConfig,
|
|
defaultConfig: DEFAULT_FETCH_CONFIG,
|
|
zodSchema: fetchInputSchema,
|
|
},
|
|
calculator: {
|
|
executor: calculatorExecutor,
|
|
parseConfig: parseCalculatorConfig,
|
|
defaultConfig: DEFAULT_CALCULATOR_CONFIG,
|
|
zodSchema: calculatorInputSchema,
|
|
},
|
|
datetime: {
|
|
executor: datetimeExecutor,
|
|
parseConfig: parseDatetimeConfig,
|
|
defaultConfig: DEFAULT_DATETIME_CONFIG,
|
|
zodSchema: datetimeInputSchema,
|
|
},
|
|
uuid: {
|
|
executor: uuidExecutor,
|
|
parseConfig: parseUuidConfig,
|
|
defaultConfig: DEFAULT_UUID_CONFIG,
|
|
zodSchema: uuidInputSchema,
|
|
},
|
|
base64: {
|
|
executor: base64Executor,
|
|
parseConfig: parseBase64Config,
|
|
defaultConfig: DEFAULT_BASE64_CONFIG,
|
|
zodSchema: base64InputSchema,
|
|
},
|
|
"json-formatter": {
|
|
executor: jsonFormatterExecutor,
|
|
parseConfig: parseJsonFormatterConfig,
|
|
defaultConfig: DEFAULT_JSON_FORMATTER_CONFIG,
|
|
zodSchema: jsonFormatterInputSchema,
|
|
},
|
|
"regex-tester": {
|
|
executor: regexTesterExecutor,
|
|
parseConfig: parseRegexTesterConfig,
|
|
defaultConfig: DEFAULT_REGEX_TESTER_CONFIG,
|
|
zodSchema: regexTesterInputSchema,
|
|
},
|
|
"user-info": {
|
|
executor: userInfoExecutor,
|
|
parseConfig: parseUserInfoConfig,
|
|
defaultConfig: DEFAULT_USER_INFO_CONFIG,
|
|
zodSchema: userInfoInputSchema,
|
|
},
|
|
};
|
|
|
|
// 立即注册所有工具类型
|
|
for (const [type, reg] of Object.entries(TOOL_TYPE_REGISTRY)) {
|
|
registerToolType(type, reg.executor);
|
|
}
|
|
|
|
export type AgentToolRow = typeof agentTools.$inferSelect;
|
|
|
|
export interface CreateAgentToolInput {
|
|
name: string;
|
|
slug: string;
|
|
description: string;
|
|
type: string;
|
|
config: Record<string, unknown>;
|
|
enabled?: boolean;
|
|
needsApproval?: boolean;
|
|
adminOnly?: boolean;
|
|
sortOrder?: number;
|
|
}
|
|
|
|
export interface UpdateAgentToolInput {
|
|
name?: string;
|
|
slug?: string;
|
|
description?: string;
|
|
type?: string;
|
|
config?: Record<string, unknown>;
|
|
enabled?: boolean;
|
|
needsApproval?: boolean;
|
|
adminOnly?: 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> {
|
|
const reg = TOOL_TYPE_REGISTRY[type];
|
|
if (reg) {
|
|
return reg.parseConfig(config);
|
|
}
|
|
return config;
|
|
}
|
|
|
|
function getDefaultConfig(type: string): Record<string, unknown> {
|
|
return TOOL_TYPE_REGISTRY[type]?.defaultConfig ?? {};
|
|
}
|
|
|
|
function getZodSchema(type: string): z.ZodType {
|
|
return TOOL_TYPE_REGISTRY[type]?.zodSchema ?? z.object({});
|
|
}
|
|
|
|
export interface ToolTypeMeta {
|
|
type: string;
|
|
defaultConfig: Record<string, unknown>;
|
|
inputSchema: Record<string, unknown>;
|
|
}
|
|
|
|
export function getToolTypeMetaList(): ToolTypeMeta[] {
|
|
return Object.entries(TOOL_TYPE_REGISTRY).map(([type, reg]) => ({
|
|
type,
|
|
defaultConfig: reg.defaultConfig,
|
|
inputSchema: z.toJSONSchema(reg.zodSchema) as Record<string, unknown>,
|
|
}));
|
|
}
|
|
|
|
export function getToolTypeMeta(type: string): ToolTypeMeta | null {
|
|
const reg = TOOL_TYPE_REGISTRY[type];
|
|
if (!reg) return null;
|
|
return {
|
|
type,
|
|
defaultConfig: reg.defaultConfig,
|
|
inputSchema: z.toJSONSchema(reg.zodSchema) as Record<string, unknown>,
|
|
};
|
|
}
|
|
|
|
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,
|
|
needsApproval: input.needsApproval ? 1 : 0,
|
|
adminOnly: input.adminOnly ? 1 : 0,
|
|
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.needsApproval !== undefined) updates.needsApproval = input.needsApproval ? 1 : 0;
|
|
if (input.adminOnly !== undefined) updates.adminOnly = input.adminOnly ? 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 = getDefaultConfig(agentTool.type);
|
|
}
|
|
|
|
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 interface EnabledToolInfo {
|
|
slug: string;
|
|
name: string;
|
|
type: string;
|
|
description: string;
|
|
}
|
|
|
|
export async function getEnabledToolInfos(userRole: UserRole | null = null): Promise<EnabledToolInfo[]> {
|
|
const tools = await dbGlobal
|
|
.select()
|
|
.from(agentTools)
|
|
.where(eq(agentTools.enabled, 1))
|
|
.orderBy(asc(agentTools.sortOrder));
|
|
|
|
const isAdmin = userRole === "admin";
|
|
|
|
const result: EnabledToolInfo[] = [];
|
|
for (const agentTool of tools) {
|
|
if (!isAdmin && agentTool.adminOnly) continue;
|
|
const executor = getExecutor(agentTool.type);
|
|
if (!executor) continue;
|
|
let config: unknown;
|
|
try {
|
|
config = JSON.parse(agentTool.config);
|
|
} catch {
|
|
config = getDefaultConfig(agentTool.type);
|
|
}
|
|
result.push({
|
|
slug: agentTool.slug,
|
|
name: agentTool.name,
|
|
type: agentTool.type,
|
|
description: executor.buildDescription(config),
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export async function getEnabledToolsForLlm(
|
|
userId: number | null = null,
|
|
userRole: UserRole | null = null,
|
|
): Promise<{
|
|
tools: Record<string, ReturnType<typeof tool>>;
|
|
approvalConfig: Record<string, "user-approval" | "approved">;
|
|
}> {
|
|
const tools = await dbGlobal
|
|
.select()
|
|
.from(agentTools)
|
|
.where(eq(agentTools.enabled, 1))
|
|
.orderBy(asc(agentTools.sortOrder));
|
|
|
|
const isAdmin = userRole === "admin";
|
|
|
|
const result: Record<string, any> = {};
|
|
const approvalConfig: Record<string, "user-approval" | "approved"> = {};
|
|
for (const agentTool of tools) {
|
|
if (!isAdmin && agentTool.adminOnly) continue;
|
|
const executor = getExecutor(agentTool.type);
|
|
if (!executor) continue;
|
|
let config: unknown;
|
|
try {
|
|
config = JSON.parse(agentTool.config);
|
|
} catch {
|
|
config = getDefaultConfig(agentTool.type);
|
|
}
|
|
|
|
const zodSchema = getZodSchema(agentTool.type);
|
|
const jsonSch = z.toJSONSchema(zodSchema) as Record<string, unknown>;
|
|
|
|
result[agentTool.slug] = tool({
|
|
description: executor.buildDescription(config),
|
|
inputSchema: 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, userId);
|
|
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 `[${agentTool.type} 结果 ${statusInfo} ${sizeInfo}]\n${dataStr}`;
|
|
},
|
|
});
|
|
approvalConfig[agentTool.slug] = agentTool.needsApproval ? "user-approval" : "approved";
|
|
}
|
|
return { tools: result, approvalConfig };
|
|
}
|
|
|
|
export async function getAgentToolsForChat(params: {
|
|
userId: number | null;
|
|
userRole: UserRole | null;
|
|
enableTools: boolean;
|
|
publicToolSlugs?: string[];
|
|
}): Promise<{
|
|
tools: Record<string, ReturnType<typeof tool>>;
|
|
approvalConfig: Record<string, "user-approval" | "approved">;
|
|
}> {
|
|
const { userId, userRole, enableTools, publicToolSlugs } = params;
|
|
|
|
if (!userId) {
|
|
if (!publicToolSlugs || publicToolSlugs.length === 0) {
|
|
return { tools: {}, approvalConfig: {} };
|
|
}
|
|
const tools = await dbGlobal
|
|
.select()
|
|
.from(agentTools)
|
|
.where(eq(agentTools.enabled, 1))
|
|
.orderBy(asc(agentTools.sortOrder));
|
|
|
|
const result: Record<string, any> = {};
|
|
const approvalConfig: Record<string, "user-approval" | "approved"> = {};
|
|
for (const agentTool of tools) {
|
|
if (!publicToolSlugs.includes(agentTool.slug)) continue;
|
|
if (agentTool.adminOnly) continue;
|
|
const executor = getExecutor(agentTool.type);
|
|
if (!executor) continue;
|
|
let config: unknown;
|
|
try {
|
|
config = JSON.parse(agentTool.config);
|
|
} catch {
|
|
config = getDefaultConfig(agentTool.type);
|
|
}
|
|
|
|
const zodSchema = getZodSchema(agentTool.type);
|
|
const jsonSch = z.toJSONSchema(zodSchema) as Record<string, unknown>;
|
|
|
|
result[agentTool.slug] = tool({
|
|
description: executor.buildDescription(config),
|
|
inputSchema: 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, userId);
|
|
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 `[${agentTool.type} 结果 ${statusInfo} ${sizeInfo}]\n${dataStr}`;
|
|
},
|
|
});
|
|
approvalConfig[agentTool.slug] = "approved";
|
|
}
|
|
return { tools: result, approvalConfig };
|
|
}
|
|
|
|
if (!enableTools) {
|
|
return { tools: {}, approvalConfig: {} };
|
|
}
|
|
|
|
return getEnabledToolsForLlm(userId, userRole);
|
|
}
|
|
|
|
// 导出各工具默认配置和类型
|
|
export { DEFAULT_FETCH_CONFIG } from "./executors/fetch/config";
|
|
export type { FetchToolConfig } from "./executors/fetch/config";
|
|
export { DEFAULT_CALCULATOR_CONFIG } from "./executors/calculator/config";
|
|
export type { CalculatorToolConfig } from "./executors/calculator/config";
|
|
export { DEFAULT_DATETIME_CONFIG } from "./executors/datetime/config";
|
|
export type { DatetimeToolConfig } from "./executors/datetime/config";
|
|
export { DEFAULT_UUID_CONFIG } from "./executors/uuid/config";
|
|
export type { UuidToolConfig } from "./executors/uuid/config";
|
|
export { DEFAULT_BASE64_CONFIG } from "./executors/base64/config";
|
|
export type { Base64ToolConfig } from "./executors/base64/config";
|
|
export { DEFAULT_JSON_FORMATTER_CONFIG } from "./executors/json-formatter/config";
|
|
export type { JsonFormatterToolConfig } from "./executors/json-formatter/config";
|
|
export { DEFAULT_REGEX_TESTER_CONFIG } from "./executors/regex-tester/config";
|
|
export type { RegexTesterToolConfig } from "./executors/regex-tester/config";
|
|
export { DEFAULT_USER_INFO_CONFIG } from "./executors/user-info/config";
|
|
export type { UserInfoToolConfig } from "./executors/user-info/config";
|
|
export type { ToolExecutor, ToolContext, ToolResult } from "./registry";
|
|
|