diff --git a/lib/ai/aiCaller.js b/lib/ai/aiCaller.js index 3c1e019..7ff5812 100644 --- a/lib/ai/aiCaller.js +++ b/lib/ai/aiCaller.js @@ -2,6 +2,7 @@ import ConfigControl from '../config/configControl.js'; import OpenaiChat from '../../modules/openai/openaiChat.js'; import { getSystemPrompt } from '../../constants/ai/prompts.js'; import SessionManager from "./sessionManager.js"; +import UserConfigManager from './userConfigManager.js'; //ai调用器 class AiCaller { @@ -9,6 +10,7 @@ class AiCaller { this.openaiChat = new OpenaiChat(); this.isInitialized = false; this.config = null; + this.userOpenaiInstances = new Map(); } /** @@ -22,6 +24,7 @@ class AiCaller { return; } this.openaiChat.init(this.config.apiKey, this.config.baseApi); + await UserConfigManager.init(); this.isInitialized = true; logger.info('[crystelf-ai] 初始化完成'); @@ -44,20 +47,26 @@ class AiCaller { logger.error('[crystelf-ai] 未初始化或配置无效'); return { success: false, error: 'AI调用器未初始化' }; } + try { - if (this.config.smartMultimodal && this.config.multimodalEnabled) { + const userId = e.user_id; + const userConfig = await UserConfigManager.getUserConfig(userId); + logger.info(`[crystelf-ai] 用户 ${userId} 使用配置 - 智能多模态: ${userConfig.smartMultimodal}, 多模态启用: ${userConfig.multimodalEnabled}`); + + if (userConfig.smartMultimodal && userConfig.multimodalEnabled) { const hasImage = originalMessages.some(msg => msg.type === 'image_url'); + logger.info(`[crystelf-ai] 智能多模态模式 - 检测到图片: ${hasImage}, 消息类型统计: ${JSON.stringify(originalMessages.map(msg => msg.type))}`); if (hasImage) { logger.info('[crystelf-ai] 检测到图片,使用多模态模型'); - return await this.callMultimodalAi(originalMessages, chatHistory, memories, e); + return await this.callMultimodalAi(originalMessages, chatHistory, memories, e, userConfig); } else { logger.info('[crystelf-ai] 纯文本消息,使用文本模型'); - return await this.callTextAi(prompt, chatHistory, memories, e); + return await this.callTextAi(prompt, chatHistory, memories, e, userConfig); } - } else if (this.config.multimodalEnabled) { - return await this.callMultimodalAi(originalMessages, chatHistory, memories, e); + } else if (userConfig.multimodalEnabled) { + return await this.callMultimodalAi(originalMessages, chatHistory, memories, e, userConfig); } else { - return await this.callTextAi(prompt, chatHistory, memories, e); + return await this.callTextAi(prompt, chatHistory, memories, e, userConfig); } } catch (error) { logger.error(`[crystelf-ai] 调用失败: ${error.message}`); @@ -75,12 +84,14 @@ class AiCaller { * @param chatHistory 聊天历史 * @param memories 记忆 * @param e + * @param userConfig 用户特定配置 * @returns {Promise<{success: boolean, response: (*|string), rawResponse: (*|string)}|{success: boolean, error: string}>} */ - async callTextAi(prompt, chatHistory = [], memories = [], e) { + async callTextAi(prompt, chatHistory = [], memories = [], e, userConfig = null) { try { + const config = userConfig || this.config; const fullPrompt = this.buildPrompt(prompt); - const apiCaller = this.openaiChat; + const apiCaller = await this.getUserOpenaiInstance(e.user_id, config); const formattedChatHistory = chatHistory.map(msg => ({ role: msg.role, @@ -90,8 +101,8 @@ class AiCaller { const result = await apiCaller.callAi({ prompt: fullPrompt, chatHistory: formattedChatHistory, - model: this.config.modelType, - temperature: this.config.temperature, + model: config.modelType, + temperature: config.temperature, customPrompt: await this.getSystemPrompt(e, memories), }); @@ -118,16 +129,18 @@ class AiCaller { * @param chatHistory 聊天历史 * @param memories 记忆 * @param e + * @param userConfig 用户特定配置 * @returns {Promise<{success: boolean, response: (*|string), rawResponse: (*|string)}|{success: boolean, error: string}>} */ - async callMultimodalAi(originalMessages, chatHistory = [], memories = [], e) { + async callMultimodalAi(originalMessages, chatHistory = [], memories = [], e, userConfig = null) { try { + const config = userConfig || this.config; const messages = await this.formatMultimodalMessages(originalMessages, chatHistory, memories, e); - const apiCaller = this.openaiChat; + const apiCaller = await this.getUserOpenaiInstance(e.user_id, config); const result = await apiCaller.callAi({ messages: messages, - model: this.config.multimodalModel, - temperature: this.config.temperature, + model: config.multimodalModel, + temperature: config.temperature, }); if (result.success) { @@ -244,6 +257,27 @@ class AiCaller { return result || '刚刚'; } + /** + * 获取用户的OpenAI实例 + * @param {string} userId - 用户QQ号 + * @param {Object} config - 用户配置 + * @returns {OpenaiChat} OpenAI实例 + */ + async getUserOpenaiInstance(userId, config) { + if (config.apiKey === this.config.apiKey && config.baseApi === this.config.baseApi) { + return this.openaiChat; + } + const cacheKey = `${userId}_${config.apiKey}_${config.baseApi}`; + if (this.userOpenaiInstances.has(cacheKey)) { + return this.userOpenaiInstances.get(cacheKey); + } + const userOpenaiChat = new OpenaiChat(); + userOpenaiChat.init(config.apiKey, config.baseApi); + this.userOpenaiInstances.set(cacheKey, userOpenaiChat); + logger.info(`[crystelf-ai] 为用户 ${userId} 创建新的OpenAI实例`); + return userOpenaiChat; + } + /** * 获取系统提示词 * @param {object} e 上下文事件对象 diff --git a/lib/ai/userConfigManager.js b/lib/ai/userConfigManager.js new file mode 100644 index 0000000..87cdde9 --- /dev/null +++ b/lib/ai/userConfigManager.js @@ -0,0 +1,173 @@ +import fs from 'fs/promises'; +import path from 'path'; +import { logger } from '../../utils/log.js'; +import ConfigControl from '../config/configControl.js'; + +/** + * 用户AI配置管理器 + * 处理每个用户的独立AI配置,支持用户自定义API密钥、模型等设置 + */ +class UserConfigManager { + constructor() { + this.basePath = path.join(process.cwd(), 'data', 'crystelf', 'ai'); + this.userConfigs = new Map(); + this.globalConfig = null; + } + + async init() { + try { + await fs.mkdir(this.basePath, { recursive: true }); + this.globalConfig = await ConfigControl.get('ai'); + } catch (error) { + logger.error(`[crystelf-ai] 用户配置管理器初始化失败: ${error.message}`); + } + } + + /** + * 获取用户的AI配置 + * @param {string} userId - 用户QQ号 + * @returns {Promise} 合并后的用户配置 + */ + async getUserConfig(userId) { + try { + if (this.userConfigs.has(userId)) { + return this.userConfigs.get(userId); + } + + const userConfigPath = path.join(this.basePath, `${userId}.json`); + let userConfig = {}; + + try { + const configData = await fs.readFile(userConfigPath, 'utf-8'); + userConfig = JSON.parse(configData); + } catch (error) { + if (error.code !== 'ENOENT') { + logger.warn(`[crystelf-ai] 用户 ${userId} 的配置文件解析失败,使用默认配置: ${error.message}`); + } + } + const mergedConfig = this.mergeConfigs(this.globalConfig, userConfig); + this.userConfigs.set(userId, mergedConfig); + + return mergedConfig; + } catch (error) { + logger.error(`[crystelf-ai] 获取用户 ${userId} 配置失败: ${error.message}`); + return this.globalConfig; + } + } + + /** + * 保存用户配置 + * @param {string} userId - 用户QQ号 + * @param {Object} config - 用户配置 + */ + async saveUserConfig(userId, config) { + try { + const userConfigPath = path.join(this.basePath, `${userId}.json`); + const filteredConfig = this.filterUserConfig(config); + + await fs.writeFile(userConfigPath, JSON.stringify(filteredConfig, null, 2)); + const mergedConfig = this.mergeConfigs(this.globalConfig, filteredConfig); + this.userConfigs.set(userId, mergedConfig); + + logger.info(`[crystelf-ai] 保存用户 ${userId} 的AI配置`); + } catch (error) { + logger.error(`[crystelf-ai] 保存用户 ${userId} 配置失败: ${error.message}`); + throw error; + } + } + + /** + * 合并全局配置和用户配置 + * @param {Object} globalConfig - 全局配置 + * @param {Object} userConfig - 用户配置 + * @returns {Object} 合并后的配置 + */ + mergeConfigs(globalConfig, userConfig) { + if (!globalConfig) return userConfig; + if (!userConfig || Object.keys(userConfig).length === 0) return globalConfig; + const mergedConfig = JSON.parse(JSON.stringify(globalConfig)); + for (const [key, value] of Object.entries(userConfig)) { + if (this.isUserConfigurable(key)) { + mergedConfig[key] = value; + } + } + + return mergedConfig; + } + + /** + * 判断配置项是否允许用户自定义 + * @param {string} key - 配置项键名 + * @returns {boolean} 是否允许用户配置 + */ + isUserConfigurable(key) { + const forbiddenKeys = [ + 'blacklist', 'whitelist', 'blackWords', + 'enableGroups', 'disableGroups' + ]; + + return !forbiddenKeys.includes(key); + } + + /** + * 过滤用户配置,移除不允许的配置项 + * @param {Object} config - 原始配置 + * @returns {Object} 过滤后的配置 + */ + filterUserConfig(config) { + const filtered = {}; + + for (const [key, value] of Object.entries(config)) { + if (this.isUserConfigurable(key)) { + filtered[key] = value; + } + } + + return filtered; + } + + /** + * 清除用户配置缓存 + * @param {string} userId - 用户QQ号,如果不传则清除所有缓存 + */ + clearCache(userId) { + if (userId) { + this.userConfigs.delete(userId); + } else { + this.userConfigs.clear(); + } + } + + /** + * 重新加载全局配置 + */ + async reloadGlobalConfig() { + this.globalConfig = await ConfigControl.get('ai'); + this.clearCache(); // 清除缓存,下次获取时会重新合并配置 + } + + /** + * 获取用户配置目录路径 + * @returns {string} 用户配置目录路径 + */ + getUserConfigPath() { + return this.basePath; + } + + /** + * 检查用户是否存在自定义配置 + * @param {string} userId - 用户QQ号 + * @returns {Promise} 是否存在自定义配置 + */ + async hasUserConfig(userId) { + try { + const userConfigPath = path.join(this.basePath, `${userId}.json`); + await fs.access(userConfigPath); + return true; + } catch { + return false; + } + } +} + +export default new UserConfigManager(); \ No newline at end of file