// 宿主工具执行体的接口与装载机制。 // // 装载完全镜像 soma-core 的 api/service 自动挂载(core/server/api/load-host-service.ts): // jiti + @soma-host/@core 别名、default export 类、接口形状探测、 // dev 不缓存实例 / prod 按文件路径缓存。那套机制已在生产验证,这里不另造第二种, // 接入方写 ai-tools/ 执行体的心智与写 api/service/ 完全一致。 import path from 'node:path'; import createJiti from 'jiti'; import { AiApiError, AiErrorCodes } from '../../ai-shared/AiError'; export interface AssistantToolHandlerContext { hostDir: string; /** 当前请求的 origin,执行体若要回调宿主自身接口可用。 */ requestOrigin: string; signal?: AbortSignal; } /** * 宿主自定义工具的执行体。default export 一个实现本接口的类,与 IService 同心智。 * input 已按同名 tool.yaml 的 in 声明经 zod 校验;返回值须 JSON 可序列化并受字节帽约束。 */ export interface IAssistantToolHandler { execute( input: Record, context: AssistantToolHandlerContext, ): Promise | unknown; } let jitiLoader: ReturnType | undefined; let jitiHostDir: string | undefined; const instanceCache = new Map(); export function loadAssistantToolHandler( handlerFile: string, hostDir: string, ): IAssistantToolHandler { const isDev = process.env.NODE_ENV !== 'production'; const cached = instanceCache.get(handlerFile); if (!isDev && cached) { return cached; } let instance: IAssistantToolHandler; try { const module = getJiti(hostDir)(handlerFile) as { default?: new () => IAssistantToolHandler }; const HandlerClass = module?.default; if (typeof HandlerClass !== 'function') { throw new Error('缺少 default export 的执行体类'); } instance = new HandlerClass(); } catch (error) { throw handlerError(handlerFile, error instanceof Error ? error.message : String(error)); } if (typeof instance?.execute !== 'function') { throw handlerError(handlerFile, '执行体类未实现 IAssistantToolHandler.execute'); } if (!isDev) { instanceCache.set(handlerFile, instance); } return instance; } function getJiti(hostDir: string): ReturnType { if (!jitiLoader || jitiHostDir !== hostDir) { jitiLoader = createJiti(import.meta.url, { interopDefault: true, alias: { '@core': path.resolve(process.cwd(), 'core'), '@soma-host': hostDir, }, }); jitiHostDir = hostDir; } return jitiLoader; } /** 声明指向了装不起来的执行体,属开发期错误:编译期直接报,不留到对话中途才炸。 */ function handlerError(handlerFile: string, message: string): AiApiError { return new AiApiError( AiErrorCodes.AI_TOOL_DECL_INVALID, `${path.basename(handlerFile)} 装载失败:${message}`, ); }