// 内置工具包 C:宿主自定义工具(ai-tools/ 固定目录自动扫描)。 // // 业务只管开发能力:放一份 *.tool.yaml 就注册,删文件就卸载,永远没有注册代码。 // 执行体二选一——绑既有接口(零代码)或同目录 TS 实现类,两条路都由系统统一治理。 // // 绑既有接口时走"服务端 fetch 当前请求 origin 下的该路径":它天然经过宿主的 // somaRequestHook 入口网关与该接口自身的限流,与人直接调这个接口同权,不开后门。 import { tool, type ToolSet } from 'ai'; import { z, type ZodTypeAny } from 'zod'; import { AiApiError, AiErrorCodes } from '../../ai-shared/AiError'; import { createAiTimeoutBudget } from '../../ai-shared/AiTimeout'; import { compileAssistantToolDeclarations, type AssistantToolDeclaration, type AssistantToolInputField, } from './AssistantToolYamlCompiler'; import { loadAssistantToolHandler, type AssistantToolHandlerContext, type IAssistantToolHandler, } from './AssistantToolHandler'; import type { AssistantToolBundle, AssistantToolDisplay, AssistantToolkit, AssistantToolkitContext, } from './AssistantToolkit'; export const HOST_DECLARED_TOOLKIT_NAME = 'host-declared'; export type { AssistantToolHandlerContext, IAssistantToolHandler }; const DEFAULT_DISPLAY: AssistantToolDisplay = { pending: '正在处理…', done: '已完成处理', error: '处理失败', }; const declarationCache = new Map(); export const hostDeclaredToolkit: AssistantToolkit = { name: HOST_DECLARED_TOOLKIT_NAME, create(context: AssistantToolkitContext): AssistantToolBundle { const declarations = readDeclarations(context.hostDir); if (declarations.length === 0) { return { tools: {} }; } const tools: ToolSet = {}; const display: Record = {}; for (const declaration of declarations) { tools[declaration.tool] = tool({ description: declaration.desc, inputSchema: buildInputSchema(declaration.input), execute: (input, { abortSignal }) => executeDeclaredTool( declaration, input as Record, context, abortSignal ?? context.signal, ), }); display[declaration.tool] = declaration.display ?? DEFAULT_DISPLAY; } return { tools, display, systemPrompt: [ '- 调用业务工具完成下列事项:', ...declarations.map((declaration) => ` · ${declaration.desc}`), ].join('\n'), }; }, }; /** * dev 每次重扫,让"放文件即注册、删文件即卸载"成立;prod 扫一次即可, * 生产环境的 ai-tools/ 不会在进程存活期间变化。 */ function readDeclarations(hostDir: string): AssistantToolDeclaration[] { if (process.env.NODE_ENV !== 'production') { return compileAssistantToolDeclarations(hostDir); } const cached = declarationCache.get(hostDir); if (cached) { return cached; } const declarations = compileAssistantToolDeclarations(hostDir); declarationCache.set(hostDir, declarations); return declarations; } async function executeDeclaredTool( declaration: AssistantToolDeclaration, input: Record, context: AssistantToolkitContext, parentSignal?: AbortSignal, ): Promise { const timeoutMs = declaration.call.timeoutMs ?? context.config.timeoutMs; const budget = createAiTimeoutBudget(timeoutMs, parentSignal); try { if (declaration.call.kind === 'handler') { const handler: IAssistantToolHandler = loadAssistantToolHandler( declaration.call.handlerFile, context.hostDir, ); return await handler.execute(input, { hostDir: context.hostDir, requestOrigin: context.requestOrigin, signal: budget.signal, }); } return await callHostService(declaration, input, context, budget.signal); } finally { budget.dispose(); } } async function callHostService( declaration: AssistantToolDeclaration, input: Record, context: AssistantToolkitContext, signal: AbortSignal, ): Promise { const call = declaration.call; if (call.kind !== 'service') { throw new AiApiError(AiErrorCodes.AI_UPSTREAM_ERROR, '工具执行体声明不合法'); } if (!context.requestOrigin) { throw new AiApiError(AiErrorCodes.AI_UPSTREAM_ERROR, '取不到当前服务地址,工具暂不可用'); } const url = new URL(call.service, context.requestOrigin); if (call.method === 'GET') { for (const [name, value] of Object.entries(input)) { if (value !== undefined && value !== null) { url.searchParams.set(name, String(value)); } } } const response = await fetch(url, { method: call.method, headers: { Accept: 'application/json', ...(call.method === 'POST' ? { 'Content-Type': 'application/json' } : {}), }, ...(call.method === 'POST' ? { body: JSON.stringify(input) } : {}), signal, }); if (!response.ok) { throw new AiApiError( AiErrorCodes.AI_UPSTREAM_ERROR, `业务接口返回 ${response.status},暂时无法获取结果`, ); } try { return await response.json(); } catch { throw new AiApiError(AiErrorCodes.AI_UPSTREAM_ERROR, '业务接口返回的不是 JSON'); } } function buildInputSchema(input: Record) { return z.object(Object.fromEntries( Object.entries(input).map(([name, field]) => [name, buildFieldSchema(field)]), )); } function buildFieldSchema(field: AssistantToolInputField): ZodTypeAny { const base = field.type === 'number' ? z.number() : field.type === 'boolean' ? z.boolean() : field.type === 'enum' ? z.enum(field.values as [string, ...string[]]) : z.string(); const described = base.describe(field.desc); return field.required ? described : described.optional(); }