Files
cnb/agent/modules/cnb-manager.ts

141 lines
4.2 KiB
TypeScript

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<typeof createOpenAICompatible>
}
// 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<string, CNBItem> = 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<CNBItem | null> {
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<CNBItem>) {
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)
}
}