import { Result } from '@kevisual/query'; import { CNB } from '../../src/index.ts'; import { useKey } from '@kevisual/context'; import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; export const getConfig = async (opts: { token?: string }) => { const kevisualEnv = useKey('KEVISUAL_ENV') const isCNB = useKey('CNB'); let isProduction = kevisualEnv !== 'development' || (isCNB && !kevisualEnv); const baseUrl = isProduction ? 'https://kevisual.cn/api/router' : 'https://kevisual.xiongxiao.me/api/router'; const res = await fetch(baseUrl, { method: 'POST', body: JSON.stringify({ path: 'config', key: 'get', data: { key: "cnb_center_config.json" } }), headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${opts.token!}` }, }).then(res => res.json()); return res as Result<{ id: string, key: 'cnb_center_config.json', data: { CNB_API_KEY: string, CNB_COOKIE: string } }>; } type CNBItem = { username: string, token: string, cookie?: string runAt?: number owner?: boolean cnb: CNB cnbAi: ReturnType } // const repo = useKey('CNB_REPO_SLUG_LOWERCASE') as string || 'kevision/kevision'; // export const cnbAi = createOpenAICompatible({ // baseURL: `https://api.cnb.cool/${repo}/-/ai/`, // name: 'custom-cnb', // apiKey: token, // }); export class CNBManager { cnbMap: Map = new Map() constructor() { setInterval(() => { this.clearExpiredCNB() }, 1000 * 60 * 30) // 每30分钟清理一次过期的 CNB 实例 } getDefaultCNB() { const cnbItem = this.cnbMap.get('default') if (!cnbItem) { throw new Error('Default CNB not found') } return cnbItem } async getCNB(opts?: { username?: string, kevisualToken?: string }): Promise { const username = opts?.username const cnbItem = this.cnbMap.get(username) if (cnbItem) { cnbItem.runAt = Date.now() return cnbItem } const res = await getConfig({ token: opts?.kevisualToken }) if (res.code === 200) { const cookie = res.data?.data?.CNB_COOKIE const token = res.data?.data?.CNB_API_KEY if (token) { return this.addCNB({ username, token, cookie }) } } else { console.error('获取 CNB 配置失败', username, res) } return null } /** * 通过上下文获取 CNB 实例(直接返回 cnb 对象) * @param ctx * @returns CNB 实例 */ async getContext(ctx: any) { const item = await this.getCNBItem(ctx) return item.cnb } async getCNBItem(ctx: any) { const tokenUser = ctx?.state?.tokenUser const username = tokenUser?.username if (!username) { ctx.throw(403, 'Unauthorized') } if (username === 'default') { return this.getDefaultCNB() } const kevisualToken = ctx.query?.token; const item = await this.getCNB({ username, kevisualToken }); if (!item) { ctx.throw(400, '不存在的 CNB 配置项,请检查 登录 Token 是否正确,或添加 CNB 配置') } return item; } addCNB(opts: Partial) { if (!opts.username || !opts.token) { throw new Error('username and token are required') } const exist = this.cnbMap.get(opts.username) if (exist) { exist.runAt = Date.now() return exist } const cnb = opts?.cnb || new CNB({ token: opts.token, cookie: opts.cookie }); opts.cnb = cnb; opts.runAt = Date.now() const repoSlug = useKey('CNB_REPO_SLUG_LOWERCASE') as string || 'kevision/kevision'; opts.cnbAi = createOpenAICompatible({ baseURL: `https://api.cnb.cool/${repoSlug}/-/ai/`, name: `custom-cnb-${opts.username}`, apiKey: opts.token, }) this.cnbMap.set(opts.username, opts as CNBItem) return opts as CNBItem } // 定期清理过期的 CNB 实例,默认过期时间为 1 小时 clearExpiredCNB(expireTime = 1000 * 60 * 60) { const now = Date.now() for (const [username, item] of this.cnbMap.entries()) { if (username === 'default') { continue } if (item.runAt && now - item.runAt > expireTime) { this.cnbMap.delete(username) } } } clearUsername(username: string) { this.cnbMap.delete(username) } }