feat(lib/ai/aiCaller.js): integrate user configuration for OpenAI instance management and API calls

This commit is contained in:
Jerry 2025-12-06 01:02:51 +08:00
parent 1283db5d1e
commit bb6cfa8689
2 changed files with 221 additions and 14 deletions

View File

@ -2,6 +2,7 @@ import ConfigControl from '../config/configControl.js';
import OpenaiChat from '../../modules/openai/openaiChat.js'; import OpenaiChat from '../../modules/openai/openaiChat.js';
import { getSystemPrompt } from '../../constants/ai/prompts.js'; import { getSystemPrompt } from '../../constants/ai/prompts.js';
import SessionManager from "./sessionManager.js"; import SessionManager from "./sessionManager.js";
import UserConfigManager from './userConfigManager.js';
//ai调用器 //ai调用器
class AiCaller { class AiCaller {
@ -9,6 +10,7 @@ class AiCaller {
this.openaiChat = new OpenaiChat(); this.openaiChat = new OpenaiChat();
this.isInitialized = false; this.isInitialized = false;
this.config = null; this.config = null;
this.userOpenaiInstances = new Map();
} }
/** /**
@ -22,6 +24,7 @@ class AiCaller {
return; return;
} }
this.openaiChat.init(this.config.apiKey, this.config.baseApi); this.openaiChat.init(this.config.apiKey, this.config.baseApi);
await UserConfigManager.init();
this.isInitialized = true; this.isInitialized = true;
logger.info('[crystelf-ai] 初始化完成'); logger.info('[crystelf-ai] 初始化完成');
@ -44,20 +47,26 @@ class AiCaller {
logger.error('[crystelf-ai] 未初始化或配置无效'); logger.error('[crystelf-ai] 未初始化或配置无效');
return { success: false, error: 'AI调用器未初始化' }; return { success: false, error: 'AI调用器未初始化' };
} }
try { 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'); const hasImage = originalMessages.some(msg => msg.type === 'image_url');
logger.info(`[crystelf-ai] 智能多模态模式 - 检测到图片: ${hasImage}, 消息类型统计: ${JSON.stringify(originalMessages.map(msg => msg.type))}`);
if (hasImage) { if (hasImage) {
logger.info('[crystelf-ai] 检测到图片,使用多模态模型'); logger.info('[crystelf-ai] 检测到图片,使用多模态模型');
return await this.callMultimodalAi(originalMessages, chatHistory, memories, e); return await this.callMultimodalAi(originalMessages, chatHistory, memories, e, userConfig);
} else { } else {
logger.info('[crystelf-ai] 纯文本消息,使用文本模型'); 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) { } else if (userConfig.multimodalEnabled) {
return await this.callMultimodalAi(originalMessages, chatHistory, memories, e); return await this.callMultimodalAi(originalMessages, chatHistory, memories, e, userConfig);
} else { } else {
return await this.callTextAi(prompt, chatHistory, memories, e); return await this.callTextAi(prompt, chatHistory, memories, e, userConfig);
} }
} catch (error) { } catch (error) {
logger.error(`[crystelf-ai] 调用失败: ${error.message}`); logger.error(`[crystelf-ai] 调用失败: ${error.message}`);
@ -75,12 +84,14 @@ class AiCaller {
* @param chatHistory 聊天历史 * @param chatHistory 聊天历史
* @param memories 记忆 * @param memories 记忆
* @param e * @param e
* @param userConfig 用户特定配置
* @returns {Promise<{success: boolean, response: (*|string), rawResponse: (*|string)}|{success: boolean, error: string}>} * @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 { try {
const config = userConfig || this.config;
const fullPrompt = this.buildPrompt(prompt); const fullPrompt = this.buildPrompt(prompt);
const apiCaller = this.openaiChat; const apiCaller = await this.getUserOpenaiInstance(e.user_id, config);
const formattedChatHistory = chatHistory.map(msg => ({ const formattedChatHistory = chatHistory.map(msg => ({
role: msg.role, role: msg.role,
@ -90,8 +101,8 @@ class AiCaller {
const result = await apiCaller.callAi({ const result = await apiCaller.callAi({
prompt: fullPrompt, prompt: fullPrompt,
chatHistory: formattedChatHistory, chatHistory: formattedChatHistory,
model: this.config.modelType, model: config.modelType,
temperature: this.config.temperature, temperature: config.temperature,
customPrompt: await this.getSystemPrompt(e, memories), customPrompt: await this.getSystemPrompt(e, memories),
}); });
@ -118,16 +129,18 @@ class AiCaller {
* @param chatHistory 聊天历史 * @param chatHistory 聊天历史
* @param memories 记忆 * @param memories 记忆
* @param e * @param e
* @param userConfig 用户特定配置
* @returns {Promise<{success: boolean, response: (*|string), rawResponse: (*|string)}|{success: boolean, error: string}>} * @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 { try {
const config = userConfig || this.config;
const messages = await this.formatMultimodalMessages(originalMessages, chatHistory, memories, e); 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({ const result = await apiCaller.callAi({
messages: messages, messages: messages,
model: this.config.multimodalModel, model: config.multimodalModel,
temperature: this.config.temperature, temperature: config.temperature,
}); });
if (result.success) { if (result.success) {
@ -244,6 +257,27 @@ class AiCaller {
return result || '刚刚'; 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 上下文事件对象 * @param {object} e 上下文事件对象

173
lib/ai/userConfigManager.js Normal file
View File

@ -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<Object>} 合并后的用户配置
*/
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<boolean>} 是否存在自定义配置
*/
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();