import { AsyncLocalStorage } from "node:async_hooks" // 请求级的 AI 供应商凭证。 // // 为什么用 AsyncLocalStorage,而不是把 credentials 一路当参数传下去: // key 的消费点埋得很深(LangGraph 图节点内部、后台异步生成任务),沿途每一层都加一个 // 参数会污染十来个签名,而且漏传一处就会静默降级(拿不到 key → 报"未配置"),很难查。 // ALS 让「谁付费」成为请求上下文的一部分:路由入口取一次**操作者本人**的凭证并 run 起来, // 里面无论多深、包括 fire-and-forget 的后台任务(只要在 run 内启动)都能拿到同一份。 // // 铁律:凭证只从这里来。lib/ai 内部不再读 process.env.EVOLINK_*—— // 全站一把平台 key(见 lib/credentials/platform.ts + /admin/ai),由管理员配; // 用户侧花的是积分,不是自己的服务额度。 export type AiCredentials = { /// 供应商 API key(明文,只在服务端内存里) apiKey: string /// 图像/视频/任务管理基址,不填用供应商默认 baseUrl?: string /// 文本模型基址,不填用供应商默认 chatBaseUrl?: string /// 默认文本模型,不填用注册表默认 chatModel?: string } const storage = new AsyncLocalStorage() /// 在这份凭证下执行(路由入口用)。里面所有 AI 调用都用它,包括 run 内启动的后台任务。 export function runWithAiCredentials(credentials: AiCredentials, fn: () => T): T { return storage.run(credentials, fn) } /// 进入凭证上下文,覆盖「当前同步执行的剩余部分 + 之后所有异步续接」。 /// 给路由 handler 用:守卫通过后调一次,后面整段(含 fire-and-forget 的后台任务)都拿得到, /// 不必把 handler 主体缩进到回调里——也就不会出现「后来加的调用忘了包」这种漏洞。 /// 只在每个请求自己的上下文里调用(Next 每个请求独立),别在模块顶层调。 export function enterAiCredentials(credentials: AiCredentials): void { storage.enterWith(credentials) } /// 当前上下文的凭证;不在凭证上下文里则为 undefined export function currentAiCredentials(): AiCredentials | undefined { return storage.getStore() } /// 取凭证,没有就抛——错误话术直接面向用户,告诉他去哪儿填 export function requireAiCredentials(): AiCredentials { const credentials = storage.getStore() if (!credentials?.apiKey) { throw new Error("尚未配置 AI 服务密钥,请到「设置 → AI 服务」填写你的 API Key") } return credentials }