diff --git a/package.json b/package.json index 99ba59c..b36f3ab 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "packageManager": "pnpm@10.7.1", "type": "module", "dependencies": { + "@kevisual/cache": "^0.0.2", + "@kevisual/permission": "^0.0.1", "@kevisual/router": "0.0.10" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b6355d..d3f2c45 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: dependencies: + '@kevisual/cache': + specifier: ^0.0.2 + version: 0.0.2(rollup@4.39.0)(tslib@2.8.1)(typescript@5.8.2) + '@kevisual/permission': + specifier: ^0.0.1 + version: 0.0.1 '@kevisual/router': specifier: 0.0.10 version: 0.0.10 @@ -398,6 +404,9 @@ packages: '@kevisual/auth@1.0.5': resolution: {integrity: sha512-GwsLj7unKXi7lmMiIIgdig4LwwLiDJnOy15HHZR5gMbyK6s5/uJiMY5RXPB2+onGzTNDqFo/hXjsD2wkerHPVg==} + '@kevisual/cache@0.0.2': + resolution: {integrity: sha512-2Cl5KF2Gi27uLfhO6CdTMFnRzx9vYnqevAo7d9ab3rOaqTgF8tLeAXglXyRbaWW3WUbHU2XaOb4r98uUsqIQQw==} + '@kevisual/code-center-module@0.0.18': resolution: {integrity: sha512-BfANmxLEO1AwVmqpa6VDgxk//YN8asf1r5jIPpyKDQm12kyyrYgHND9AgGCDRH8lvq6rYVe0svCZXD5b06UPWQ==} peerDependencies: @@ -414,6 +423,9 @@ packages: '@kevisual/mark@0.0.7': resolution: {integrity: sha512-PiEEy4yvWEpixw76PzgrIWeNelzm+FrhtzFmqJU92o5GkgawaFwighcvIxqcVZRKeEFF4uvlTjFrGeQvXw6F4A==} + '@kevisual/permission@0.0.1': + resolution: {integrity: sha512-nSX2LzbPkU3YAMegbUFGU8tfmtFb7dcF5edqzm+gI6crcyCL1JzIB9HAYNEeEVIljLxuREwM/vVg9aFmF4cz9Q==} + '@kevisual/query@0.0.13': resolution: {integrity: sha512-gSEIDiCvwSaLLAFZv4vam4wSrMsaCuQ3VGjE3kwRwZ8urlVH1TOA+NUO908A22p9m1Iij7Y1Q/JlfSJi2QzuKQ==} @@ -1747,6 +1759,9 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + idb-keyval@6.2.1: + resolution: {integrity: sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==} + ignore-by-default@1.0.1: resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} @@ -3347,6 +3362,18 @@ snapshots: '@kevisual/auth@1.0.5': {} + '@kevisual/cache@0.0.2(rollup@4.39.0)(tslib@2.8.1)(typescript@5.8.2)': + dependencies: + '@rollup/plugin-commonjs': 28.0.3(rollup@4.39.0) + '@rollup/plugin-node-resolve': 16.0.1(rollup@4.39.0) + '@rollup/plugin-typescript': 12.1.2(rollup@4.39.0)(tslib@2.8.1)(typescript@5.8.2) + idb-keyval: 6.2.1 + rollup-plugin-dts: 6.2.1(rollup@4.39.0)(typescript@5.8.2) + transitivePeerDependencies: + - rollup + - tslib + - typescript + '@kevisual/code-center-module@0.0.18(@kevisual/auth@1.0.5)(@kevisual/router@0.0.10)(@kevisual/use-config@1.0.10(dotenv@16.4.7))(ioredis@5.6.0)(pg@8.14.1)(sequelize@6.37.7(pg-hstore@2.3.4)(pg@8.14.1))': dependencies: '@kevisual/auth': 1.0.5 @@ -3393,6 +3420,8 @@ snapshots: - tedious - utf-8-validate + '@kevisual/permission@0.0.1': {} + '@kevisual/query@0.0.13(ws@8.18.1)(zod@3.24.2)': dependencies: openai: 4.91.1(ws@8.18.1)(zod@3.24.2) @@ -4882,6 +4911,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + idb-keyval@6.2.1: {} + ignore-by-default@1.0.1: {} ignore@5.3.2: {} diff --git a/src/provider/utils/parse-config.ts b/src/provider/utils/parse-config.ts index e8d78df..62a7b05 100644 --- a/src/provider/utils/parse-config.ts +++ b/src/provider/utils/parse-config.ts @@ -1,3 +1,4 @@ +import { Permission } from '@kevisual/permission'; import CryptoJS from 'crypto-js'; // 加密函数 @@ -73,6 +74,7 @@ export type AIConfig = { description?: string; models: AIModel[]; secretKeys: SecretKey[]; + permission?: Permission; filter?: { objectKey: string; type: 'array' | 'object'; diff --git a/src/routes/ai-chat/index.ts b/src/routes/ai-chat/index.ts index 082e08b..695f3de 100644 --- a/src/routes/ai-chat/index.ts +++ b/src/routes/ai-chat/index.ts @@ -2,7 +2,7 @@ import { app } from '@/app.ts'; import { ChatServices } from './services/chat-services.ts'; import { ChatConfigServices } from './services/chat-config-srevices.ts'; import { AiChatHistoryModel } from './models/ai-chat-history.ts'; - +import { UserPermission } from '@kevisual/permission'; app .route({ path: 'ai', @@ -21,19 +21,20 @@ app if (!aiChatHistory) { ctx.throw(400, 'aiChatHistory not found'); } - if (aiChatHistory.uid !== tokenUser.uid) { + if (aiChatHistory.uid !== tokenUser.id) { ctx.throw(403, 'not permission'); } - username = username || aiChatHistory.username; + username = username || aiChatHistory.username || tokenUsername; model = model || aiChatHistory.model; group = group || aiChatHistory.group; } else { username = username || tokenUsername; } + const isSelf = username === tokenUsername; + if (!Array.isArray(messages)) { ctx.throw(400, 'chat messages is not array'); } - // 初始化服务 const chatServices = await ChatServices.createServices({ owner: username, @@ -41,6 +42,15 @@ app group, username: tokenUsername, }); + if (!isSelf && username !== 'root') { + const aiConfig = chatServices.aiConfig; + const permission = new UserPermission({ permission: aiConfig.permission, owner: username }); + const checkPermission = permission.checkPermissionSuccess({ username: tokenUsername, password: options.password }); + if (!checkPermission.success) { + ctx.throw(403, checkPermission.message); + } + } + const chatConfigServices = new ChatConfigServices(username, tokenUsername); await chatConfigServices.checkUserCanChat(tokenUsername); await chatServices.checkCanChat(); @@ -72,6 +82,10 @@ app prompt_tokens: aiChatHistory.prompt_tokens + usage.prompt_tokens, completion_tokens: aiChatHistory.completion_tokens + usage.completion_tokens, total_tokens: aiChatHistory.total_tokens + usage.total_tokens, + version: aiChatHistory.version + 1, + model: model, + group: group, + username: username, }; if (hook) { needUpdateData.data = { @@ -126,19 +140,46 @@ app .define(async (ctx) => { const username = ctx.query.username || 'root'; const tokenUser = ctx.state.tokenUser; + const usernames = ctx.query.data?.usernames || []; + const tokenUsername = tokenUser.username; const isSameUser = username === tokenUser.username; - const configObject: Record = {}; + const configArray: any[] = []; const services = new ChatConfigServices(username, tokenUser.username); const res = await services.getChatConfig(true, ctx.query.token); - configObject[username] = res; + configArray.push({ + username, + config: res, + }); if (!isSameUser) { const selfServices = new ChatConfigServices(tokenUser.username, tokenUser.username); const selfRes = await selfServices.getChatConfig(true, ctx.query.token); - configObject['self'] = selfRes; - } else { - configObject['self'] = res; + configArray.push({ + username: tokenUser.username, + self: true, + config: selfRes, + }); } - ctx.body = configObject; + for (const username of usernames) { + const services = new ChatConfigServices(username, tokenUser.username); + const res = await services.getChatConfig(true, ctx.query.token); + const aiConfig = services.aiConfig; + const permission = new UserPermission({ permission: aiConfig.permission, owner: username }); + const checkPermission = permission.checkPermissionSuccess({ username: tokenUsername, password: '-----------------' }); + if (!checkPermission.success) { + // ctx.throw(403, `[${username}] ${checkPermission.message}`); + configArray.push({ + username, + config: null, + error: checkPermission.message, + }); + } else { + configArray.push({ + username, + config: res, + }); + } + } + ctx.body = configArray; }) .addTo(app); @@ -146,7 +187,7 @@ app .route({ path: 'ai', key: 'get-chat-usage', - description: '获取chat使用情况', + description: '获取chat使用情况, 只获取root的使用情况', middleware: ['auth'], }) .define(async (ctx) => { diff --git a/src/routes/ai-chat/list.ts b/src/routes/ai-chat/list.ts index b270d2f..cca9dd2 100644 --- a/src/routes/ai-chat/list.ts +++ b/src/routes/ai-chat/list.ts @@ -12,12 +12,121 @@ app const tokenUser = ctx.state.tokenUser; const aiChatList = await AiChatHistoryModel.findAll({ where: { - uid: tokenUser.uid, + uid: tokenUser.id, }, - order: [['createdAt', 'DESC']], + order: [['updatedAt', 'DESC']], }); ctx.body = { list: aiChatList, }; }) .addTo(app); + +app + .route({ + path: 'ai', + key: 'update-chat', + description: '更新chat', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const uid = tokenUser.id; + const { id, data, prompt_tokens, total_tokens, completion_tokens, createdAt, updatedAt, ...rest } = ctx.query.data || {}; + let aiChat: AiChatHistoryModel | null = null; + if (id) { + aiChat = await AiChatHistoryModel.findByPk(id); + if (!aiChat) { + ctx.throw(404, 'chat not found'); + } + if (aiChat.uid !== uid) { + ctx.throw(403, 'no permission'); + } + await aiChat.update({ data: { ...aiChat.data, ...data }, ...rest, version: aiChat.version + 1 }); + } else { + aiChat = await AiChatHistoryModel.create({ + ...rest, + uid: uid, + }); + } + ctx.body = aiChat; + }) + .addTo(app); + +app + .route({ + path: 'ai', + key: 'get-chat', + description: '获取chat', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const { id } = ctx.query.data || {}; + if (!id) { + ctx.throw(400, 'id is required'); + } + const aiChat = await AiChatHistoryModel.findByPk(id); + if (!aiChat) { + ctx.throw(404, 'chat not found'); + } + if (aiChat.uid !== tokenUser.id) { + ctx.throw(403, 'no permission'); + } + ctx.body = aiChat; + }) + .addTo(app); +app + .route({ + path: 'ai', + key: 'get-chat-version', + description: '获取chat版本', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const { id } = ctx.query.data || {}; + if (!id) { + ctx.throw(400, 'id is required'); + } + const aiChat = await AiChatHistoryModel.findByPk(id); + if (!aiChat) { + ctx.throw(404, 'chat not found'); + } + if (aiChat.uid !== tokenUser.id) { + ctx.throw(403, 'no permission'); + } + ctx.body = { + id: aiChat.id, + version: aiChat.version, + }; + }) + .addTo(app); + +app + .route({ + path: 'ai', + key: 'delete-chat', + description: '删除chat', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const { id } = ctx.query.data || {}; + if (!id) { + ctx.throw(400, 'id is required'); + } + const aiChat = await AiChatHistoryModel.findByPk(id); + if (!aiChat) { + ctx.throw(404, 'chat not found'); + } + if (aiChat.uid !== tokenUser.id) { + ctx.throw(403, 'no permission'); + } + await aiChat.destroy(); + + ctx.body = { + success: true, + }; + }) + .addTo(app); diff --git a/src/routes/ai-chat/models/ai-chat-history.ts b/src/routes/ai-chat/models/ai-chat-history.ts index ce7b915..f0a4f80 100644 --- a/src/routes/ai-chat/models/ai-chat-history.ts +++ b/src/routes/ai-chat/models/ai-chat-history.ts @@ -33,6 +33,8 @@ export class AiChatHistoryModel extends Model { declare total_tokens: number; declare completion_tokens: number; + declare version: number; + declare createdAt: Date; declare updatedAt: Date; } @@ -47,14 +49,17 @@ AiChatHistoryModel.init( username: { type: DataTypes.STRING, allowNull: false, + defaultValue: '', }, model: { type: DataTypes.STRING, allowNull: false, + defaultValue: '', }, group: { type: DataTypes.STRING, allowNull: false, + defaultValue: '', }, title: { type: DataTypes.STRING, @@ -82,6 +87,10 @@ AiChatHistoryModel.init( type: DataTypes.JSONB, defaultValue: {}, }, + version: { + type: DataTypes.INTEGER, + defaultValue: 0, + }, uid: { type: DataTypes.UUID, allowNull: true, diff --git a/src/routes/ai-chat/services/chat-config-srevices.ts b/src/routes/ai-chat/services/chat-config-srevices.ts index 9994f83..746e618 100644 --- a/src/routes/ai-chat/services/chat-config-srevices.ts +++ b/src/routes/ai-chat/services/chat-config-srevices.ts @@ -8,6 +8,8 @@ export class ChatConfigServices { owner: string; // 使用者 username: string; + aiConfig?: AIConfig; + /** * username 是使用的模型的用户名,使用谁的模型 * @param username @@ -50,6 +52,7 @@ export class ChatConfigServices { const cacheTime = 60 * 60 * 24 * 40; // 1天 await redis.set(key, JSON.stringify(modelConfig), 'EX', cacheTime); } + this.aiConfig = modelConfig; if (needClearSecret) { modelConfig = this.filterApiKey(modelConfig); } diff --git a/src/routes/ai-chat/services/chat-services.ts b/src/routes/ai-chat/services/chat-services.ts index e4969b4..774bc21 100644 --- a/src/routes/ai-chat/services/chat-services.ts +++ b/src/routes/ai-chat/services/chat-services.ts @@ -1,4 +1,4 @@ -import { AIConfigParser, ProviderResult } from '@/provider/utils/parse-config.ts'; +import { AIConfig, AIConfigParser, ProviderResult } from '@/provider/utils/parse-config.ts'; import { ProviderManager, ChatMessage, BaseChat, ChatMessageOptions } from '@/provider/index.ts'; import { redis } from '@/modules/db.ts'; import { CustomError } from '@kevisual/router'; @@ -36,6 +36,7 @@ export class ChatServices { * 模型配置 */ modelConfig?: ProviderResult; + aiConfig?: AIConfig; chatProvider?: BaseChat; constructor(opts: ChatServicesConfig) { this.owner = opts.owner; @@ -65,6 +66,7 @@ export class ChatServices { }, }); that.modelConfig = { ...providerResult, apiKey }; + that.aiConfig = config; return that.modelConfig; } /** @@ -108,7 +110,7 @@ export class ChatServices { async createNewMessage(messages: ChastHistoryMessage[]) { return messages.map((item) => { if (!item.id) { - item.id = 'chat' + nanoid(); + item.id = 'chat-' + nanoid(); item.createdAt = Date.now(); item.updatedAt = Date.now(); }