diff --git a/src/aura/asr/index.ts b/src/aura/asr/index.ts index 1b28399..9557369 100644 --- a/src/aura/asr/index.ts +++ b/src/aura/asr/index.ts @@ -2,7 +2,9 @@ import { app } from '@/app.ts' import { asr } from './modules/index.ts' app.route({ path: 'asr', - key: 'text' + key: 'text', + middleware: ['auth'], + description: '语音转文字,将base64的音频数据转换为文字, 参数: base64Audio, 为base64编码的音频数据', }).define(async (ctx) => { const base64Audio = ctx.query.base64Audio as string if (!base64Audio) { diff --git a/src/route.ts b/src/route.ts index d4488b4..ace6b67 100644 --- a/src/route.ts +++ b/src/route.ts @@ -152,7 +152,12 @@ app const tokenUser = ctx.state.tokenUser; let isUser = !!tokenUser; ctx.body = { - list: app.router.routes.map((item) => { + list: app.router.routes.filter(item => { + if (item.id === 'auth' || item.id === 'auth-can' || item.id === 'check-auth-admin' || item.id === 'auth-admin') { + return false; + } + return true; + }).map((item) => { return { id: item.id, path: item.path, diff --git a/src/routes/ai/cost.ts b/src/routes/ai/cost.ts new file mode 100644 index 0000000..8e1b444 --- /dev/null +++ b/src/routes/ai/cost.ts @@ -0,0 +1,51 @@ +import { redis } from '@/app.ts' + +export class AICost { + /** + * 获取余额 + * @param userId + * @returns + */ + static async getAmount(userId: string): Promise { + const cost = await redis.get(`ai:cost:amount:${userId}`); + if (!cost) { + return 2 * 100 * 10000;// 默认200万调用量 等于 2M 的额度,每一个月的初始额度,每一个月进行重置。 + } + return cost ? parseFloat(cost) : 0; + } + /** + * 扣除余额 + * @param userId + * @param amount + */ + static async decuctAmount(userId: string, amount: number): Promise { + const key = `ai:cost:amount:${userId}`; + const current = await this.getAmount(userId); + const newAmount = Math.max(0, current - amount); + await redis.set(key, newAmount.toString()); + } + /** + * 检查余额是否足够 + * @param userId + * @returns + */ + static async checkAmount(userId: string): Promise { + const current = await this.getAmount(userId); + return current >= 10; // 最低余额10 + } + /** + * 增加余额 + * @param userId + * @param amount + */ + static async addAmount(userId: string, amount: number): Promise { + const key = `ai:cost:amount:${userId}`; + const current = await this.getAmount(userId); + const newAmount = current + amount; + await redis.set(key, newAmount.toString()); + } + static async setAmount(userId: string, amount: number): Promise { + const key = `ai:cost:amount:${userId}`; + await redis.set(key, amount.toString()); + } +} \ No newline at end of file diff --git a/src/routes/ai/index.ts b/src/routes/ai/index.ts new file mode 100644 index 0000000..aa895dd --- /dev/null +++ b/src/routes/ai/index.ts @@ -0,0 +1,121 @@ +import { app, ai } from '@/app.ts'; +import { AICost } from './cost.ts'; + +const description = `ai调用工具,参数说明: +- model: 使用的模型,默认qwen-plus,可选qwen-plus、qwen-flash、qwen-max、qwen-coder-plus等 +- messages: 消息数组,格式为[{role: 'user'|'assistant'|'system', content: string}],与OpenAI的chat-completions接口一致 +- question: 兼容提问参数,如果传入messages则忽略question参数 +- isJson: 是否只返回json内容,默认false,如果为true则会尝试从AI返回的内容中提取json对象返回`; + +app.route({ + path: 'ai', + key: '', + description, + middleware: ['auth'] +}).define(async (ctx) => { + const query = ctx.query || {}; + const tokenUser = ctx.state.tokenUser; + const userId = tokenUser.id; + const isJson = query.isJson ?? false; + const model = query.model || 'qwen-plus'; + const hasEnough = await AICost.checkAmount(userId); + if (!hasEnough) { + ctx.throw(402, 'token 余额不足,请充值后重试'); + } + const messages = query.messages || []; + if (messages.length === 0 && query.question) { + messages.push({ role: 'user', content: query.question }); + } else if (messages.length === 0) { + ctx.throw(400, 'messages不能为空'); + } + await ai.chat(messages, { model, messages }); + const text = ai.responseText; + const cost = ai.total_tokens; + let payload: any = undefined; + await AICost.decuctAmount(userId, cost); + if (isJson) { + payload = ai.utils.extractJsonFromMarkdown(text); + } + + ctx.body = { + text, + cost: cost, + model, + payload + } +}).addTo(app) + +app.route({ + path: 'ai', + key: 'cost', + description: '获取AI调用余额', + middleware: ['auth'] +}).define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const userId = tokenUser.id; + const amount = await AICost.getAmount(userId); + ctx.body = { + amount + } +}).addTo(app) + +app.route({ + path: 'ai', + key: 'cost/get', + description: '检查AI调用余额是否足够', + middleware: ['auth-admin'] +}).define(async (ctx) => { + const userId = ctx.query.userId || ''; + if (!userId) { + ctx.throw(400, 'userId不能为空'); + } + const amount = await AICost.getAmount(userId); + ctx.body = { + amount + } +}).addTo(app) + + +app.route({ + path: 'ai', + key: 'cost/add', + description: '增加AI调用余额, 需要传入userId和amount两个参数', + middleware: ['auth-admin'] +}).define(async (ctx) => { + const userId = ctx.query.userId || ''; + if (!userId) { + ctx.throw(400, 'userId不能为空'); + } + const amount = ctx.query.amount || 0; + if (amount <= 0) { + ctx.throw(400, 'amount必须大于0'); + } + await AICost.addAmount(userId, amount); + const newAmount = await AICost.getAmount(userId); + ctx.body = { + amount: newAmount + } +}).addTo(app) + +app.route({ + path: 'ai', + key: 'cost/set', + description: '设置AI调用余额, 需要传入userId和amount两个参数', + middleware: ['auth-admin'] +}).define(async (ctx) => { + const userId = ctx.query.userId || ''; + if (!userId) { + ctx.throw(400, 'userId不能为空'); + } + + const amount = ctx.query.amount || 0; + if (amount < 0) { + ctx.throw(400, 'amount必须大于等于0'); + } + + await AICost.setAmount(userId, amount); + const newAmount = await AICost.getAmount(userId); + ctx.body = { + amount: newAmount + } +}).addTo(app) \ No newline at end of file diff --git a/src/routes/index.ts b/src/routes/index.ts index 1031de7..ce3522f 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,9 +1,5 @@ -import './container/index.ts'; - import './user/index.ts'; -import './github/index.ts'; - import './app-manager/index.ts'; import './file/index.ts'; @@ -12,6 +8,7 @@ import './micro-app/index.ts'; import './config/index.ts'; -// import './mark/index.ts'; +import './file-listener/index.ts'; -import './file-listener/index.ts'; \ No newline at end of file + +import './ai/index.ts'; \ No newline at end of file