diff --git a/README.md b/README.md index 2448661..6428919 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ - [X] 获取引用消息 - [X] 适配多模态模型,查看图片等 - [ ] 支持联网搜索 -- [ ] 支持生成图片 +- [X] 支持生成图片 - [ ] 支持渲染数学公式 - [ ] 违禁词检测 - [ ] 使用toon代替json与模型交互 @@ -301,6 +301,34 @@ "fontSize": 14, "codeTheme": "github" }, + + "?imageConfig": "图像生成配置", + "imageConfig": { + "?enabled": "是否启用图像生成功能", + "enabled": true, + "?imageMode": "图像生成模式: 'openai'使用/v1/images/generations接口, 'chat'使用对话式生图模型(如gemini-3-pro-image-preview)", + "imageMode": "openai", + "?model": "图像生成模型名称(支持gemini-3-pro-image-preview等)", + "model": "gemini-3-pro-image-preview", + "?baseApi": "图像生成API基础地址(不加v1后面的)", + "baseApi": "https://api.uniapi.io", + "?apiKey": "图像生成API密钥", + "apiKey": "", + "?timeout": "图像生成超时时间(豪秒)", + "timeout": 60000, + "?maxRetries": "最大重试次数", + "maxRetries": 3, + "?quality": "生成图像质量(standard/high)", + "quality": "standard", + "?style": "图像风格(natural/vivid)", + "style": "natural", + "?size": "生成图像尺寸(1024x1024/1792x1024/...)", + "size": "1024x1024", + "?responseFormat": "响应格式(url/b64_json)", + "responseFormat": "url", + "?modalities": "模态类型(text/image)", + "modalities": ["text", "image"] + } } ``` diff --git a/apps/fanqie.js b/apps/fanqie.js deleted file mode 100644 index 8a213b3..0000000 --- a/apps/fanqie.js +++ /dev/null @@ -1,225 +0,0 @@ -import fs from 'node:fs'; -import path from 'path'; -import chokidar from 'chokidar'; -import ConfigControl from '../lib/config/configControl.js'; -import Fanqie from '../modules/apps/fanqie/fanqie.js'; - -/** - * 本功能由 y68(github@yeqiu6080) 提供技术支持 - */ -export default class FanqiePlugin extends plugin { - constructor() { - super({ - name: 'crystelf-fanqie', - dsc: '番茄小说下载器', - event: 'message', - priority: -114, - rule: [ - { - reg: '(changdunovel.com/wap/share-v2.html|fanqienovel.com/page)', - fnc: 'handleFanqieLink', - }, - { - reg: '#?fq下载(.*)', - fnc: 'downloadByBookId', - }, - { - reg: '^fq清(理|除|空)缓存$', - fnc: 'clearFanqieCache', - }, - ], - }); - - this.initPromise = this.initFanqieConfig(); - this.fanqieClient = null; - - // 注册计划任务 - this.task = { - cron: '0 0 16 * * ?', - name: '定时清理番茄缓存', - fnc: () => this.clearFanqieCache(false, true), - }; - } - - async initFanqieConfig() { - this.outDir = await ConfigControl.get('fanqieConfig')?.outDir; - this.apiUrl = await ConfigControl.get('fanqieConfig')?.url; - this.fanqieClient = new Fanqie(this.apiUrl); - } - - /** - * 监听下载输出目录 - */ - async waitForOutputFile(dir, timeout = 30000) { - if (!dir) return false; - - return new Promise((resolve) => { - const watcher = chokidar.watch(dir, { - persistent: true, - ignoreInitial: true, - }); - - const timer = setTimeout(() => { - watcher.close(); - resolve(false); - }, timeout); - - watcher.on('add', (filePath) => { - clearTimeout(timer); - watcher.close(); - resolve(filePath); - }); - }); - } - - /** - * 清理缓存 - */ - async clearFanqieCache(e, isScheduled = false, specificId = false) { - if (!isScheduled && e && !e.isMaster) { - e.reply('你没有权限使用此功能', true); - return false; - } - - if (!this.outDir) { - await this.initPromise; - if (!this.outDir) { - if (e) e.reply('缓存目录未初始化,无法清理', true); - return false; - } - } - - if (specificId) { - const specificPath = path.join(this.outDir, 'files', specificId); - if (fs.existsSync(specificPath)) { - fs.rmSync(specificPath, { recursive: true, force: true }); - } - } - - const mainCachePath = path.join(this.outDir, 'fanqie'); - if (fs.existsSync(mainCachePath)) { - fs.readdirSync(mainCachePath).forEach((file) => { - const fullPath = path.join(mainCachePath, file); - const stat = fs.statSync(fullPath); - if (stat.isDirectory()) { - fs.rmSync(fullPath, { recursive: true, force: true }); - } else { - fs.unlinkSync(fullPath); - } - }); - } - - if (!isScheduled && e) e.reply('缓存清理完成', true); - return true; - } - - /** - * 解析网页链接中的 book_id - */ - async handleFanqieLink(e) { - const message = e.msg.trim(); - let bookId = null; - - try { - if (message.includes('changdunovel.com')) { - bookId = message.match(/book_id=(\d+)/)[1]; - } else { - bookId = message.match(/page\/(\d+)/)[1]; - } - } catch { - return e.reply('解析失败,请检查链接是否正确', true); - } - - return this.downloadFanqieBook(e, bookId); - } - - /** - * 使用 #fq下载 命令下载 - */ - async downloadByBookId(e) { - const bookId = e.msg.replace(/^#?fq下载/, '').trim(); - return this.downloadFanqieBook(e, bookId); - } - - /** - * 执行下载并上传文件 - */ - async downloadFanqieBook(e, bookId) { - await this.initPromise; - - let bookInfo; - try { - bookInfo = await this.fanqieClient.get_info(bookId); - } catch (err) { - logger.error(err); - return e.reply('获取小说信息失败', true); - } - - if (!bookInfo) return e.reply('获取失败,请稍后再试', true); - - e.reply( - `识别小说:[番茄小说]《${bookInfo.book_name}》\n作者:${bookInfo.author}\n原名:${bookInfo.original_book_name}`, - true - ); - - e.reply('开始下载,请稍等片刻...', true); - const startTime = Date.now(); - - try { - await this.fanqieClient.down(bookId, e.message_id); - } catch (err) { - logger.error(err); - return e.reply('下载失败,请稍后重试', true); - } - - const outPath = path.join(this.outDir, 'files', String(e.message_id)); - let finalFilePath = await this.waitForOutputFile(outPath); - if (!finalFilePath) return e.reply('下载超时', true); - - // 文件重命名防止空格 - const safePath = finalFilePath.replace(/ /g, '_'); - if (finalFilePath !== safePath) { - try { - fs.renameSync(finalFilePath, safePath); - finalFilePath = safePath; - } catch (err) { - logger.error(`重命名失败:${err.stack}`); - return e.reply('重命名失败', true); - } - } - - const uploaded = await this.sendFileToUser(e, finalFilePath); - await this.clearFanqieCache(false, true, String(e.message_id)); - - if (!uploaded) return e.reply('上传失败', true); - - e.reply(`《${bookInfo.book_name}》上传成功,耗时 ${(Date.now() - startTime) / 1000}s`); - return true; - } - - /** - * 上传文件至群或私聊 - */ - async sendFileToUser(e, filePath) { - try { - const fileName = path.basename(filePath); - if (e.isGroup) { - return await e.bot.sendApi('upload_group_file', { - group_id: e.group_id, - file: filePath, - name: fileName, - }); - } else if (e.friend) { - return await e.bot.sendApi('upload_private_file', { - user_id: e.user_id, - file: filePath, - name: fileName, - }); - } - } catch (err) { - logger.error(`文件上传失败:${logger.red(err.stack)}`); - e.reply(`上传失败:${err.message}`, true); - return null; - } - } -} diff --git a/components/date.js b/components/date.js deleted file mode 100644 index 2a4b254..0000000 --- a/components/date.js +++ /dev/null @@ -1,3 +0,0 @@ -let date = {}; //咕咕咕 - -export default date; diff --git a/components/tool.js b/components/tool.js index 98233d0..0687703 100644 --- a/components/tool.js +++ b/components/tool.js @@ -7,15 +7,6 @@ let tools = { sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }, - - /** - * 生成指定范围内的随机整数 - * @param {number} min - 最小值 - * @param {number} max - 最大值 - */ - randomInt(min, max) { - return Math.floor(Math.random() * (max - min + 1)) + min; - }, }; export default tools; diff --git a/config/ai.json b/config/ai.json index 8e9de02..b7c9880 100644 --- a/config/ai.json +++ b/config/ai.json @@ -32,8 +32,6 @@ "getChatHistoryLength":20, "?keywordCache": "是否缓存关键词到本地", "keywordCache": true, - "?pinyinMatch": "是否启用拼音匹配", - "pinyinMatch": true, "?blockGroup": "禁用的群聊(黑名单)", "blockGroup": [], "?whiteGroup": "白名单群聊,存在该部分时,黑名单将被禁用", @@ -82,7 +80,7 @@ "?model": "图像生成模型名称(支持gemini-3-pro-image-preview等)", "model": "gemini-3-pro-image-preview", "?baseApi": "图像生成API基础地址(不加v1后面的)", - "baseApi": "https://api.uniapi.io", + "baseApi": "https://api.siliconflow.cn", "?apiKey": "图像生成API密钥", "apiKey": "", "?timeout": "图像生成超时时间(豪秒)", diff --git a/config/blackwords.json b/config/blackwords.json deleted file mode 100644 index 2a4d6f2..0000000 --- a/config/blackwords.json +++ /dev/null @@ -1,135 +0,0 @@ -{ - "?check": "是否在模糊匹配时使用人工智能二次检查", - "check": true, - "hours": 2, - "min": 30, - "day": 5, - "?level": "不同等级对应惩罚", - "level": { - "1": "ban", - "2": "ban", - "3": "day", - "4": "hours", - "5": "min" - }, - "?words": "不同等级对应的违禁词", - "words": { - "1": [], - "2": [], - "3": [ - "byd", - "qs", - "sb", - "2b", - "cnm", - "rnm", - "fw", - "还不死", - "没父母", - "没母亲", - "没家人", - "畜生", - "赶紧死", - "举报", - "举办", - "杀你", - "死一死", - "死了算了", - "傻子", - "傻X", - "神经病", - "废材", - "傻", - "逼", - "phuck", - "fuck", - "nigger", - "niger", - "mom" - ], - "4": [ - "nmsl", - "mdzz", - "jb", - "憨憨", - "rnm", - "低能", - "撅你", - "找死", - "混蛋", - "蠢", - "混账", - "傻瓜", - "屁", - "屎", - "白痴", - "小丑", - "贱", - "臭", - "骚", - "尿", - "猪", - "粪", - "称冯", - "柠檬", - "缲称犸", - "亻尔女马", - "mother", - "bitch", - "你冯" - ], - "5": [ - "入机", - "低智", - "无用", - "无能", - "闭嘴", - "别说话", - "禁言你", - "烦" - ] - }, - "?pinyin": "模糊匹配,可能出现误判", - "pinyin":{ - "1": [], - "2": [], - "3": [ - "qusi", - "cao", - "gun", - "jubao", - "juban" - ], - "4": [ - "shabi", - "wocaonima", - "sima", - "sabi", - "zhizhang", - "naocan", - "naotan", - "shadiao", - "nima", - "simadongxi", - "simawanyi", - "hanbi", - "siquanjia", - "hanpi", - "laji", - "feiwu", - "meima", - "simu", - "rini", - "chaonima", - "renji", - "youbing", - "bendan", - "ben", - "youbin", - "chengma", - "chenma" - ], - "5": [] - } - -} diff --git a/config/config.json b/config/config.json index 0c3c6d2..d6b236b 100644 --- a/config/config.json +++ b/config/config.json @@ -8,14 +8,12 @@ "autoUpdate": true, "poke": true, "60s": true, - "fanqie": true, "zwa": true, "rss": true, "help": true, "welcome": true, "faceReply": true, "ai": true, - "blackWords": true, "music": true, "auth": true } diff --git a/config/fanqieConfig.json b/config/fanqieConfig.json deleted file mode 100644 index ef4fcf7..0000000 --- a/config/fanqieConfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "url": "http://127.0.0.1:6868", - "outDir": "/home/user/debian/cache/Downloads" -} diff --git a/guoba.support.js b/guoba.support.js new file mode 100644 index 0000000..6fb237e --- /dev/null +++ b/guoba.support.js @@ -0,0 +1,30 @@ +import path from 'path'; +import { getConfigData, setConfigData } from './guoba/configHandler.js'; +import guobaSchema from './guoba/configSchema.js'; + +export function supportGuoba() { + return { + pluginInfo: { + name: 'crystelf-plugin', + title: '晶灵插件', + description: '多功能娱乐插件,支持AI对话、图像生成、音乐点播、60s新闻、验证管理等功能', + author: 'Jerry', + authorLink: 'https://github.com/jerryplusy', + link: 'https://github.com/jerryplusy/crystelf-plugin', + isV3: true, + isV2: false, + showInMenu: 'auto', + icon: 'mdi:crystal', + iconColor: '#7c4dff', + iconPath: path.join(process.cwd(), '/plugins/crystelf-plugin/resources/img/logo.png'), + }, + configInfo: { + schemas: guobaSchema, + // 获取配置数据方法(用于前端填充显示数据) + getConfigData, + + // 设置配置的方法(前端点确定后调用的方法) + setConfigData, + }, + }; +} diff --git a/guoba/configHandler.js b/guoba/configHandler.js new file mode 100644 index 0000000..7fcecfa --- /dev/null +++ b/guoba/configHandler.js @@ -0,0 +1,351 @@ +import ConfigControl from '../lib/config/configControl.js'; +import UserConfigManager from '../lib/ai/userConfigManager.js'; +import lodash from 'lodash'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +/** + * 配置处理逻辑 + * 处理锅巴WebUI的配置读取、设置和保存 + */ + +/** + * 将嵌套对象转换为扁平化的点分隔路径 + * @param {Object} obj - 要扁平化的对象 + * @param {string} prefix - 前缀 + * @returns {Object} 扁平化的对象 + */ +function flattenObject(obj, prefix = '') { + const result = {}; + + for (const [key, value] of Object.entries(obj)) { + // 跳过以?开头的注释字段 + if (key.startsWith('?')) continue; + + const newKey = prefix ? `${prefix}.${key}` : key; + + if (value !== null && typeof value === 'object' && !Array.isArray(value)) { + // 递归处理嵌套对象 + Object.assign(result, flattenObject(value, newKey)); + } else { + result[newKey] = value; + } + } + + return result; +} + +/** + * 获取当前配置 + * @returns {Promise} 当前配置对象 + */ +export function getConfigData() { + // 获取所有配置文件 + const allConfigs = ConfigControl.get(); + const result = {}; + + // 将各个配置文件的内容扁平化到结果对象中 + for (const [configName, configData] of Object.entries(allConfigs)) { + if (configName === 'feeds' || configName === 'newcomer') continue; + // 将配置数据扁平化 + const flattened = flattenObject(configData, configName); + Object.assign(result, flattened); + } + + return result; +} + +/** + * 设置配置数据 + * @param {Object} data - 新的配置数据 + * @param {Object} options - 选项对象,包含Result等 + * @returns {Promise} 操作结果 + */ +export async function setConfigData(data, { Result }) { + try { + // 将扁平化的数据重新组织成配置文件结构 + const configUpdates = {}; + + for (const [fieldPath, value] of Object.entries(data)) { + const parts = fieldPath.split('.'); + const configName = parts[0]; + + // 跳过feeds和newcomer配置 + if (configName === 'feeds' || configName === 'newcomer') continue; + + if (!configUpdates[configName]) { + configUpdates[configName] = {}; + } + + // 使用lodash.set设置嵌套属性 + const keyPath = parts.slice(1).join('.'); + lodash.set(configUpdates[configName], keyPath, value); + } + + // 只更新实际有变化的配置文件 + for (const [configName, newConfigData] of Object.entries(configUpdates)) { + // 获取现有配置 + const existingConfig = ConfigControl.get(configName) || {}; + + // 检查配置是否真的发生了变化 + const isChanged = !lodash.isEqual( + newConfigData, + lodash.pick(existingConfig, Object.keys(newConfigData)) + ); + + if (isChanged) { + // 合并配置(保留注释字段) + const updatedConfig = lodash.merge({}, existingConfig, newConfigData); + + // 保存配置 + await ConfigControl.set(configName, updatedConfig); + } + } + + return Result.ok({}, '保存成功~'); + } catch (error) { + logger.error('[crystelf-plugin] 保存配置失败:', error); + return Result.error('保存配置失败: ' + error.message); + } +} + +/** + * 重置配置为默认值 + * @param {Object} options - 选项对象,包含Result等 + * @returns {Promise} 操作结果 + */ +export async function resetConfig({ Result }) { + try { + // 获取插件目录路径 + const __filename = fileURLToPath(import.meta.url); + const pluginDir = path.dirname(__filename); + const configDir = path.join(pluginDir, '..', '..', 'config'); + + // 获取数据目录路径 + const dataConfigPath = path.join(process.cwd(), 'data', 'crystelf'); + + // 确保数据目录存在 + if (!fs.existsSync(dataConfigPath)) { + fs.mkdirSync(dataConfigPath, { recursive: true }); + } + + // 读取所有配置文件 + const configFiles = fs.readdirSync(configDir).filter((file) => file.endsWith('.json')); + const defaultConfigs = {}; + + // 复制每个配置文件 + for (const file of configFiles) { + const configName = path.basename(file, '.json'); + const sourcePath = path.join(configDir, file); + const targetPath = path.join(dataConfigPath, file); + + try { + // 读取源配置文件 + const configContent = fs.readFileSync(sourcePath, 'utf8'); + const configData = JSON.parse(configContent); + + // 写入目标配置文件 + fs.writeFileSync(targetPath, configContent, 'utf8'); + + // 添加到默认配置对象 + defaultConfigs[configName] = configData; + } catch (error) { + logger.error(`[crystelf-ai] 复制配置文件失败 ${file}: ${error.message}`); + return Result.error({}, `复制配置文件失败 ${file}: ${error.message}`); + } + } + + // 使用 ConfigControl.setMultiple 重置所有配置 + await ConfigControl.setMultiple(defaultConfigs); + + // 清除用户配置缓存 + UserConfigManager.clearCache(); + + return Result.ok({}, '重置成功~'); + } catch (error) { + logger.error(`[crystelf-ai] 重置配置失败: ${error.message}`); + return Result.error({}, `重置失败: ${error.message}`); + } +} + +/** + * 导出配置 + * @param {Object} options - 选项对象,包含Result等 + * @returns {Promise} 操作结果,包含配置数据 + */ +export async function exportConfig({ Result }) { + try { + const config = await getConfigData(); + return Result.ok({ config }, '导出成功~'); + } catch (error) { + logger.error(`[crystelf-ai] 导出配置失败: ${error.message}`); + return Result.error({}, `导出失败: ${error.message}`); + } +} + +/** + * 导入配置 + * @param {Object} data - 包含配置数据的对象 + * @param {Object} options - 选项对象,包含Result等 + * @returns {Promise} 操作结果 + */ +export async function importConfig(data, { Result }) { + try { + if (!data.config) { + return Result.error({}, '导入数据格式错误'); + } + + // 验证配置 + const validationResult = validateConfig(data.config); + if (!validationResult.valid) { + return Result.error({}, `配置验证失败: ${validationResult.errors.join(', ')}`); + } + + // 使用 ConfigControl.setMultiple 保存配置 + await ConfigControl.setMultiple(data.config); + + // 清除用户配置缓存 + UserConfigManager.clearCache(); + + return Result.ok({}, '导入成功~'); + } catch (error) { + logger.error(`[crystelf-ai] 导入配置失败: ${error.message}`); + return Result.error({}, `导入失败: ${error.message}`); + } +} + +/** + * 验证配置 + * @param {string|Object} configType - 配置类型或配置对象 + * @param {Object} config - 要验证的配置对象(当第一个参数是配置类型时) + * @returns {Object} 验证结果,包含valid和errors + */ +function validateConfig(configType, config = null) { + // 如果只有一个参数,则认为是配置对象,进行通用验证 + if (config === null) { + config = configType; + configType = 'general'; + } + + const errors = []; + + // 根据配置类型进行特定验证 + switch (configType) { + case 'ai': + // 验证AI配置 + if (!config.baseApi) { + errors.push('API基础地址不能为空'); + } + + if (!config.mode) { + errors.push('对话模式不能为空'); + } + + if (!config.apiKey) { + errors.push('API密钥不能为空'); + } + + if (!config.modelType) { + errors.push('模型名称不能为空'); + } + + if (!config.multimodalModel) { + errors.push('多模态模型名称不能为空'); + } + + if (!config.character) { + errors.push('表情包角色不能为空'); + } + + if (!config.botPersona) { + errors.push('机器人人设不能为空'); + } + + // 验证数值范围 + if (config.temperature !== undefined && (config.temperature < 0 || config.temperature > 2)) { + errors.push('温度值必须在0-2之间'); + } + + if (config.concurrency !== undefined && (config.concurrency < 1 || config.concurrency > 10)) { + errors.push('并发数必须在1-10之间'); + } + + if ( + config.chatHistory !== undefined && + (config.chatHistory < 1 || config.chatHistory > 100) + ) { + errors.push('聊天历史长度必须在1-100之间'); + } + + // 验证数组字段 + if (config.blockGroup && !Array.isArray(config.blockGroup)) { + errors.push('禁用群聊必须是数组'); + } + + if (config.whiteGroup && !Array.isArray(config.whiteGroup)) { + errors.push('白名单群聊必须是数组'); + } + break; + + case '60s': + // 验证60s新闻配置 + if (!config.url) { + errors.push('60s新闻API地址不能为空'); + } + break; + + case 'auth': + // 验证验证配置 + if (!config.url) { + errors.push('验证API地址不能为空'); + } + break; + + case 'music': + // 验证音乐配置 + if (!config.url) { + errors.push('音乐服务器url不能为空'); + } + if (!config.username) { + errors.push('音乐服务器用户名不能为空'); + } + if (!config.password) { + errors.push('音乐服务器密码不能为空'); + } + if (!config.quality) { + errors.push('音乐质量不能为空'); + } + break; + + case 'poke': + // 验证戳一戳配置 + if (config.replyPoke !== undefined && (config.replyPoke < 0 || config.replyPoke > 1)) { + errors.push('戳一戳概率必须在0-1之间'); + } + break; + + case 'profile': + // 验证个人资料配置 + if (!config.nickName) { + errors.push('机器人昵称不能为空'); + } + break; + + case 'coreConfig': + // 验证核心配置 + if (!config.coreUrl) { + errors.push('核心url不能为空'); + } + break; + + default: + // 通用验证 + break; + } + + return { + valid: errors.length === 0, + errors, + }; +} diff --git a/guoba/configSchema.js b/guoba/configSchema.js new file mode 100644 index 0000000..cc5cd3e --- /dev/null +++ b/guoba/configSchema.js @@ -0,0 +1,789 @@ +const guobaSchema = [ + // config.json - 主配置 + { + label: '主配置', + component: 'SOFT_GROUP_BEGIN', + }, + { + field: 'config.debug', + label: '调试模式', + component: 'Switch', + bottomHelpMessage: '是否启用调试模式', + componentProps: { + checkedValue: true, + unCheckedValue: false, + }, + }, + { + field: 'config.core', + label: '晶灵核心', + component: 'Switch', + bottomHelpMessage: '是否启用晶灵核心相关功能', + componentProps: { + checkedValue: true, + unCheckedValue: false, + }, + }, + { + field: 'config.maxFeed', + label: '最长订阅', + component: 'InputNumber', + bottomHelpMessage: '最长订阅数量', + componentProps: { + min: 1, + max: 50, + step: 1, + placeholder: '请输入最长订阅数量', + }, + }, + { + field: 'config.autoUpdate', + label: '自动更新', + component: 'Switch', + bottomHelpMessage: '是否自动更新插件', + componentProps: { + checkedValue: true, + unCheckedValue: false, + }, + }, + { + field: 'config.poke', + label: '戳一戳功能', + component: 'Switch', + bottomHelpMessage: '是否启用戳一戳功能', + componentProps: { + checkedValue: true, + unCheckedValue: false, + }, + }, + { + field: 'config.60s', + label: '60s新闻', + component: 'Switch', + bottomHelpMessage: '是否启用60s新闻功能', + componentProps: { + checkedValue: true, + unCheckedValue: false, + }, + }, + { + field: 'config.zwa', + label: '早晚安', + component: 'Switch', + bottomHelpMessage: '是否启用早晚安功能', + componentProps: { + checkedValue: true, + unCheckedValue: false, + }, + }, + { + field: 'config.rss', + label: 'RSS订阅', + component: 'Switch', + bottomHelpMessage: '是否启用RSS订阅功能', + componentProps: { + checkedValue: true, + unCheckedValue: false, + }, + }, + { + field: 'config.help', + label: '帮助功能', + component: 'Switch', + bottomHelpMessage: '是否启用帮助功能', + componentProps: { + checkedValue: true, + unCheckedValue: false, + }, + }, + { + field: 'config.welcome', + label: '入群欢迎功能', + component: 'Switch', + bottomHelpMessage: '是否启用欢迎功能', + componentProps: { + checkedValue: true, + unCheckedValue: false, + }, + }, + { + field: 'config.faceReply', + label: '表情回复(贴表情)', + component: 'Switch', + bottomHelpMessage: '是否启用表情回复功能', + componentProps: { + checkedValue: true, + unCheckedValue: false, + }, + }, + { + field: 'config.ai', + label: '晶灵智能', + component: 'Switch', + bottomHelpMessage: '是否启用AI功能', + componentProps: { + checkedValue: true, + unCheckedValue: false, + }, + }, + { + field: 'config.music', + label: '点歌', + component: 'Switch', + bottomHelpMessage: '是否启用点歌功能', + componentProps: { + checkedValue: true, + unCheckedValue: false, + }, + }, + { + field: 'config.auth', + label: '入群验证功能', + component: 'Switch', + bottomHelpMessage: '是否启用入群验证', + componentProps: { + checkedValue: true, + unCheckedValue: false, + }, + }, + + // coreConfig.json - 核心配置 + { + label: '晶灵核心配置', + component: 'SOFT_GROUP_BEGIN', + }, + { + field: 'coreConfig.coreUrl', + label: '核心API地址', + component: 'Input', + bottomHelpMessage: '晶灵核心API地址', + componentProps: { + placeholder: '请输入核心API地址', + }, + }, + { + field: 'coreConfig.token', + label: '核心Token', + component: 'InputPassword', + required: false, + bottomHelpMessage: '晶灵核心可选访问Token', + componentProps: { + placeholder: '请输入核心Token', + }, + }, + + // auth.json - 认证配置 + { + label: '入群验证', + component: 'SOFT_GROUP_BEGIN', + }, + { + field: 'auth.url', + label: '手性碳验证API地址', + component: 'Input', + bottomHelpMessage: '验证基础api,有需求可自建', + componentProps: { + placeholder: '请输入验证API地址', + }, + }, + { + field: 'auth.default.enable', + label: '全局启用验证', + component: 'Switch', + bottomHelpMessage: '是否在全部群聊启用验证', + componentProps: { + checkedValue: true, + unCheckedValue: false, + }, + }, + { + field: 'auth.default.carbon.enable', + label: '手性碳验证', + component: 'Switch', + bottomHelpMessage: '是否默认启用手性碳验证,关闭则为数字验证', + componentProps: { + checkedValue: true, + unCheckedValue: false, + }, + }, + { + field: 'auth.default.carbon.hint', + label: '手性碳验证提示', + component: 'Switch', + bottomHelpMessage: '是否显示手性碳验证提示(使用星号标注手性碳位置)', + componentProps: { + checkedValue: true, + unCheckedValue: false, + }, + }, + { + field: 'auth.default.carbon.hard-mode', + label: '手性碳验证困难模式', + component: 'Switch', + bottomHelpMessage: '是否启用手性碳验证困难模式(困难模式下需要找出全部手性碳)', + componentProps: { + checkedValue: true, + unCheckedValue: false, + }, + }, + { + field: 'auth.default.timeout', + label: '验证超时时间', + component: 'InputNumber', + bottomHelpMessage: '验证超时时间(秒)', + componentProps: { + min: 30, + max: 600, + step: 10, + placeholder: '请输入验证超时时间(秒)', + }, + }, + { + field: 'auth.default.recall', + label: '撤回未认证消息', + component: 'Switch', + bottomHelpMessage: '是否撤回验证通过前用户发送的消息', + componentProps: { + checkedValue: true, + unCheckedValue: false, + }, + }, + { + field: 'auth.default.frequency', + label: '最大验证次数', + component: 'InputNumber', + bottomHelpMessage: '验证的最大次数,超过视为失败', + componentProps: { + min: 1, + max: 24, + step: 1, + placeholder: '请输入最大验证次数', + }, + }, + + // ai.json - AI配置 + { + label: '晶灵智能', + component: 'SOFT_GROUP_BEGIN', + }, + { + field: 'ai.mode', + label: '对话模式', + component: 'Select', + bottomHelpMessage: '推荐使用混合模式,如果你不喜欢词库或不想消耗token可以修改', + componentProps: { + options: [ + { label: '混合模式', value: 'mix' }, + { label: 'AI模式', value: 'ai' }, + { label: '词库模式', value: 'keyword' }, + ], + placeholder: '请选择对话模式', + }, + }, + { + field: 'ai.baseApi', + label: 'API基础地址', + component: 'Input', + bottomHelpMessage: '请求基础api地址(仅支持openai),其余可自行部署newapi代理', + required: true, + componentProps: { + placeholder: '请输入API基础地址,如: https://api.siliconflow.cn/v1', + }, + }, + { + field: 'ai.apiKey', + label: 'API密钥', + component: 'InputPassword', + bottomHelpMessage: '用于请求API的密钥', + required: true, + componentProps: { + placeholder: '请输入API密钥', + }, + }, + { + field: 'ai.modelType', + label: '文本模型', + component: 'Input', + bottomHelpMessage: '用于文本生成的模型名称', + required: true, + componentProps: { + placeholder: '请输入模型名称,如: deepseek-ai/DeepSeek-V3.2-Exp', + }, + }, + { + field: 'ai.temperature', + label: '聊天温度', + component: 'InputNumber', + bottomHelpMessage: '温度越高聊天的发散性越高,可选0-2.0', + componentProps: { + min: 0, + max: 2, + step: 0.1, + precision: 1, + placeholder: '请输入温度值,如: 1.2', + }, + }, + { + field: 'ai.concurrency', + label: '最大并发数', + component: 'InputNumber', + bottomHelpMessage: '最大同时聊天群数,一个群最多一个人聊天', + componentProps: { + min: 1, + max: 10, + step: 1, + placeholder: '请输入最大并发数', + }, + }, + { + field: 'ai.maxMix', + label: '混合模式阈值', + component: 'InputNumber', + bottomHelpMessage: '混合模式下,如果用户消息长度大于这个值,那么使用ai回复', + componentProps: { + min: 1, + step: 1, + placeholder: '请输入消息长度阈值', + }, + }, + { + field: 'ai.timeout', + label: '记忆超时时间', + component: 'InputNumber', + bottomHelpMessage: '记忆默认超时时间(天)', + componentProps: { + min: 1, + max: 365, + step: 1, + placeholder: '请输入超时天数', + }, + }, + { + field: 'ai.maxSessions', + label: '最大会话数', + component: 'InputNumber', + bottomHelpMessage: '最大同时存在的活跃群聊数量', + componentProps: { + min: 1, + max: 50, + step: 1, + placeholder: '请输入最大会话数', + }, + }, + { + field: 'ai.chatHistory', + label: '聊天历史长度', + component: 'InputNumber', + bottomHelpMessage: '聊天上下文最大长度', + componentProps: { + min: 1, + max: 50, + step: 1, + placeholder: '请输入聊天历史长度', + }, + }, + { + field: 'ai.maxMessageLength', + label: '最大消息长度', + component: 'InputNumber', + bottomHelpMessage: '处理群消息的最大长度', + componentProps: { + min: 50, + max: 100, + step: 10, + placeholder: '请输入最大消息长度', + }, + }, + { + field: 'ai.getChatHistoryLength', + label: '获取上下文长度', + component: 'InputNumber', + bottomHelpMessage: '获取到的聊天上下文长度', + componentProps: { + min: 1, + max: 100, + step: 1, + placeholder: '请输入获取上下文长度', + }, + }, + { + field: 'ai.keywordCache', + label: '词库缓存', + component: 'Switch', + bottomHelpMessage: '是否缓存词库到本地', + componentProps: { + checkedValue: true, + unCheckedValue: false, + }, + }, + { + field: 'ai.botPersona', + label: '机器人人设', + component: 'InputTextArea', + bottomHelpMessage: '机器人的性格和行为描述', + componentProps: { + rows: 4, + placeholder: '请输入机器人人设描述', + }, + }, + { + field: 'ai.character', + label: '表情包角色', + component: 'Select', + bottomHelpMessage: '回复表情包时的角色(能力有限,目前仅支持一种角色qwq)', + componentProps: { + options: [{ label: '真寻', value: 'zhenxun' }], + placeholder: '请选择表情包角色', + }, + }, + { + field: 'ai.multimodalEnabled', + label: '多模态模式', + component: 'Switch', + bottomHelpMessage: '启用后将使用多模态模型', + componentProps: { + checkedValue: true, + unCheckedValue: false, + }, + }, + { + field: 'ai.smartMultimodal', + label: '智能多模态', + component: 'Switch', + bottomHelpMessage: '开启时只有有图片才用多模态模型,其他情况使用默认模型', + componentProps: { + checkedValue: true, + unCheckedValue: false, + }, + }, + { + field: 'ai.multimodalModel', + label: '多模态模型', + component: 'Input', + bottomHelpMessage: '用于多模态处理的模型名称', + required: true, + componentProps: { + placeholder: '请输入多模态模型名称,例如Qwen/Qwen2.5-VL-72B-Instruct', + }, + }, + { + field: 'ai.imageConfig.enabled', + label: '图像生成功能', + component: 'Switch', + bottomHelpMessage: '是否允许ai生成图像', + componentProps: { + checkedValue: true, + unCheckedValue: false, + }, + }, + { + field: 'ai.imageConfig.imageMode', + label: '图像生成模式', + component: 'Select', + bottomHelpMessage: + 'openai使用/v1/images/generations接口(如Qwen-Image), chat使用对话式生图模型(如gemini-3-pro-image-preview)', + componentProps: { + options: [ + { label: 'OpenAI接口', value: 'openai' }, + { label: '对话式生成', value: 'chat' }, + ], + placeholder: '请选择图像生成模式', + }, + }, + { + field: 'ai.imageConfig.model', + label: '图像生成模型', + component: 'Input', + bottomHelpMessage: '用于图像生成的模型名称', + required: true, + componentProps: { + placeholder: '请输入图像生成模型名称,例如如gemini-3-pro-image-preview', + }, + }, + { + field: 'ai.imageConfig.baseApi', + label: '图像API地址', + component: 'Input', + bottomHelpMessage: '图像生成API基础地址,不加v1', + required: true, + componentProps: { + placeholder: '请输入图像API地址,例如https://api.siliconflow.cn', + }, + }, + { + field: 'ai.imageConfig.apiKey', + label: '图像API密钥', + component: 'InputPassword', + bottomHelpMessage: '用于图像生成的API密钥', + required: false, + componentProps: { + placeholder: '请输入图像API密钥', + }, + }, + { + field: 'ai.imageConfig.timeout', + label: '图像生成超时', + component: 'InputNumber', + bottomHelpMessage: '图像生成超时时间(毫秒)', + componentProps: { + min: 1000, + max: 300000, + step: 1000, + placeholder: '请输入超时时间(毫秒)', + }, + }, + { + field: 'ai.imageConfig.maxRetries', + label: '最大重试次数', + component: 'InputNumber', + bottomHelpMessage: '图像生成失败时的最大重试次数', + componentProps: { + min: 0, + max: 10, + step: 1, + placeholder: '请输入最大重试次数', + }, + }, + { + field: 'ai.imageConfig.quality', + label: '图像质量', + component: 'Select', + bottomHelpMessage: '生成图像的质量', + componentProps: { + options: [ + { label: '标准', value: 'standard' }, + { label: '高质量', value: 'high' }, + ], + placeholder: '请选择图像质量', + }, + }, + { + field: 'ai.imageConfig.style', + label: '图像风格', + component: 'Select', + bottomHelpMessage: '生成图像的风格', + componentProps: { + options: [ + { label: '自然', value: 'natural' }, + { label: '生动', value: 'vivid' }, + ], + placeholder: '请选择图像风格', + }, + }, + { + field: 'ai.imageConfig.size', + label: '图像尺寸', + component: 'Select', + bottomHelpMessage: '生成图像的尺寸', + componentProps: { + options: [ + { label: '1024x1024', value: '1024x1024' }, + { label: '1792x1024', value: '1792x1024' }, + { label: '1024x1792', value: '1024x1792' }, + ], + placeholder: '请选择图像尺寸', + }, + }, + { + field: 'ai.imageConfig.responseFormat', + label: '响应格式', + component: 'Select', + bottomHelpMessage: '图像响应的格式,建议url', + componentProps: { + options: [ + { label: 'URL', value: 'url' }, + { label: 'Base64', value: 'b64_json' }, + ], + placeholder: '请选择响应格式', + }, + }, + { + field: 'ai.blockGroup', + label: '禁用群聊', + component: 'InputArray', + bottomHelpMessage: '黑名单群聊,AI不会在这些群聊中工作', + componentProps: { + placeholder: '请输入群号,按回车添加', + }, + }, + { + field: 'ai.whiteGroup', + label: '白名单群聊', + component: 'InputArray', + bottomHelpMessage: '白名单群聊,存在时黑名单将被禁用', + componentProps: { + placeholder: '请输入群号,按回车添加', + }, + }, + { + field: 'ai.codeRenderer.theme', + label: '代码主题', + component: 'Select', + bottomHelpMessage: '代码渲染的主题', + componentProps: { + options: [ + { label: 'GitHub', value: 'github' }, + { label: 'Monokai', value: 'monokai' }, + { label: 'Dark', value: 'dark' }, + { label: 'Light', value: 'light' }, + ], + placeholder: '请选择代码主题', + }, + }, + { + field: 'ai.codeRenderer.fontSize', + label: '代码字体大小', + component: 'InputNumber', + bottomHelpMessage: '代码渲染的字体大小', + componentProps: { + min: 10, + max: 24, + step: 1, + placeholder: '请输入字体大小', + }, + }, + { + field: 'ai.codeRenderer.lineNumbers', + label: '显示行号', + component: 'Switch', + bottomHelpMessage: '是否显示代码行号', + componentProps: { + checkedValue: true, + unCheckedValue: false, + }, + }, + { + field: 'ai.codeRenderer.backgroundColor', + label: '背景颜色', + component: 'Input', + bottomHelpMessage: '代码渲染的背景颜色', + componentProps: { + placeholder: '请输入背景颜色,如: #f6f8fa', + }, + }, + { + field: 'ai.markdownRenderer.theme', + label: 'Markdown主题', + component: 'Select', + bottomHelpMessage: 'Markdown渲染的主题', + componentProps: { + options: [ + { label: '深色', value: 'dark' }, + { label: '浅色', value: 'light' }, + ], + placeholder: '请选择Markdown主题', + }, + }, + { + field: 'ai.markdownRenderer.fontSize', + label: 'Markdown字体大小', + component: 'InputNumber', + bottomHelpMessage: 'Markdown渲染的字体大小', + componentProps: { + min: 10, + max: 24, + step: 1, + placeholder: '请输入字体大小', + }, + }, + { + field: 'ai.markdownRenderer.codeTheme', + label: '代码主题', + component: 'Select', + bottomHelpMessage: 'Markdown中代码块的主题', + componentProps: { + options: [ + { label: 'GitHub', value: 'github' }, + { label: 'Monokai', value: 'monokai' }, + { label: 'Dark', value: 'dark' }, + { label: 'Light', value: 'light' }, + ], + placeholder: '请选择代码主题', + }, + }, + + // 60s.json - 60s新闻配置 + { + label: '60s新闻', + component: 'SOFT_GROUP_BEGIN', + }, + { + field: '60s.url', + label: '60s新闻API', + component: 'Input', + bottomHelpMessage: '60s新闻的API地址', + required: true, + componentProps: { + placeholder: '请输入60s新闻API地址', + }, + }, + + // music.json - 音乐配置 + { + label: '点歌配置', + component: 'SOFT_GROUP_BEGIN', + }, + { + field: 'music.url', + label: '音乐API地址', + component: 'Input', + bottomHelpMessage: '音乐API地址', + required: true, + componentProps: { + placeholder: '请输入音乐API地址', + }, + }, + { + field: 'music.username', + label: '音乐API用户名', + component: 'Input', + bottomHelpMessage: '音乐API用户名', + componentProps: { + placeholder: '请输入音乐API用户名', + }, + }, + { + field: 'music.password', + label: '音乐API密码', + component: 'InputPassword', + bottomHelpMessage: '音乐API密码', + componentProps: { + placeholder: '请输入音乐API密码', + }, + }, + + // poke.json - 戳一戳配置 + { + label: '戳一戳', + component: 'SOFT_GROUP_BEGIN', + }, + { + field: 'poke.replyPoke', + label: '戳一戳回戳概率', + component: 'InputNumber', + bottomHelpMessage: '戳一戳回戳概率', + componentProps: { + min: 0, + max: 1, + step: 0.1, + placeholder: '请输入回戳概率', + }, + }, + + // profile.json - 用户资料配置 + { + label: '机器人资料', + component: 'SOFT_GROUP_BEGIN', + }, + { + field: 'profile.nickName', + label: '机器人昵称', + component: 'Input', + bottomHelpMessage: '机器人的昵称', + componentProps: { + placeholder: '请输入机器人昵称', + }, + }, +]; + +export default guobaSchema; diff --git a/lib/ai/userConfigManager.js b/lib/ai/userConfigManager.js index 3a4476e..0ecee83 100644 --- a/lib/ai/userConfigManager.js +++ b/lib/ai/userConfigManager.js @@ -141,7 +141,7 @@ class UserConfigManager { /** * 清除用户配置缓存 - * @param {string} userId - 用户QQ号,如果不传则清除所有缓存 + * @param {string|null} userId - 用户QQ号,如果不传则清除所有缓存 */ clearCache(userId) { if (userId) { diff --git a/lib/config/configControl.js b/lib/config/configControl.js index ff7a234..5c8970e 100644 --- a/lib/config/configControl.js +++ b/lib/config/configControl.js @@ -15,32 +15,61 @@ let watchers = []; */ async function init() { try { + // 确保数据配置目录存在 try { await fsp.access(dataConfigPath); } catch { await fsp.mkdir(dataConfigPath, { recursive: true }); logger.mark(`[crystelf-plugin] 配置目录创建成功: ${dataConfigPath}`); } + + // 确保默认配置目录存在 + try { + await fsp.access(pluginConfigPath); + } catch { + logger.warn(`[crystelf-plugin] 默认配置目录不存在: ${pluginConfigPath}`); + } + + // 处理主配置文件 const pluginDefaultFile = path.join(pluginConfigPath, 'config.json'); try { await fsp.access(configFile); } catch { - await fsp.copyFile(pluginDefaultFile, configFile); - logger.mark(`[crystelf-plugin] 默认配置复制成功: ${configFile}`); + try { + await fsp.copyFile(pluginDefaultFile, configFile); + logger.mark(`[crystelf-plugin] 默认配置复制成功: ${configFile}`); + } catch (copyError) { + logger.warn(`[crystelf-plugin] 复制默认配置失败,创建空配置: ${copyError}`); + await fc.writeJSON(configFile, {}); + } } - const pluginFiles = (await fsp.readdir(pluginConfigPath)).filter((f) => f.endsWith('.json')); + let pluginFiles = []; + try { + pluginFiles = (await fsp.readdir(pluginConfigPath)).filter((f) => f.endsWith('.json')); + } catch (error) { + logger.warn(`[crystelf-plugin] 读取默认配置目录失败: ${error}`); + } + + // 复制缺失的配置文件 for (const file of pluginFiles) { const pluginFilePath = path.join(pluginConfigPath, file); const dataFilePath = path.join(dataConfigPath, file); try { await fsp.access(dataFilePath); } catch { - await fsp.copyFile(pluginFilePath, dataFilePath); - logger.mark(`[crystelf-plugin] 配置文件缺失,已复制: ${file}`); + try { + await fsp.copyFile(pluginFilePath, dataFilePath); + logger.mark(`[crystelf-plugin] 配置文件缺失,已复制: ${file}`); + } catch (copyError) { + logger.warn(`[crystelf-plugin] 复制配置文件失败 ${file}: ${copyError}`); + } } } + + // 读取所有配置文件 const files = (await fsp.readdir(dataConfigPath)).filter((f) => f.endsWith('.json')); configCache = {}; + for (const file of files) { const filePath = path.join(dataConfigPath, file); const name = path.basename(file, '.json'); @@ -50,7 +79,9 @@ async function init() { try { await fsp.access(pluginFilePath); const pluginData = await fc.readJSON(pluginFilePath); + if (Array.isArray(data) && Array.isArray(pluginData)) { + // 合并数组类型配置 const strSet = new Set(data.map((x) => JSON.stringify(x))); for (const item of pluginData) { const str = JSON.stringify(item); @@ -60,20 +91,29 @@ async function init() { } } } else if (!Array.isArray(data) && !Array.isArray(pluginData)) { + // 合并对象类型配置 data = fc.mergeConfig(data, pluginData); } + + // 保存合并后的配置 await fc.writeJSON(filePath, data); - } catch {} + } catch (mergeError) { + logger.error('[crystelf-plugin]合并配置失败..'); + logger.error(mergeError); + // 忽略合并错误,使用现有数据 + } + configCache[name] = data; } catch (e) { logger.warn(`[crystelf-plugin] 读取配置文件 ${file} 失败:`, e); } } + if (configCache.debug) { logger.info('[crystelf-plugin] 配置模块初始化成功..'); } } catch (err) { - logger.warn('[crystelf-plugin] 配置初始化失败,使用空配置..', err); + logger.warn('[crystelf-plugin] 配置初始化失败,使用空配置..', err); configCache = {}; } } @@ -98,7 +138,7 @@ function watchConfigs() { const data = await fc.readJSON(filePath); const name = path.basename(file, '.json'); configCache[name] = data; - logger.info(`[crystelf-plugin] 配置热更新: ${file}`); + logger.info(`[crystelf-plugin] 配置热更新: ${file}`); } catch (e) { logger.warn(`[crystelf-plugin] 热更新读取失败 ${file}:`, e); } @@ -120,36 +160,65 @@ const configControl = { }, async set(key, value) { + // 更新内存中的配置 configCache[key] = value; const filePath = path.join(dataConfigPath, `${key}.json`); + try { + // 尝试访问文件,如果存在则直接写入 await fsp.access(filePath); await fc.writeJSON(filePath, value); - } catch { - let cfg = await fc.readJSON(configFile); - if (Array.isArray(cfg)) { - cfg.push(value); - } else { - cfg[key] = value; + } catch (error) { + // 文件不存在,创建新文件 + try { + // 确保目录存在 + await fsp.mkdir(dataConfigPath, { recursive: true }); + // 直接写入新文件 + await fc.writeJSON(filePath, value); + logger.mark(`[crystelf-plugin] 创建新配置文件: ${filePath}`); + } catch (writeError) { + logger.error(`[crystelf-plugin] 创建配置文件失败: ${writeError}`); + throw writeError; + } + } + }, + + /** + * 批量设置配置 + * @param {Object} configs - 配置对象,键为配置名,值为配置数据 + */ + async setMultiple(configs) { + // 确保目录存在 + await fsp.mkdir(dataConfigPath, { recursive: true }); + + for (const [key, value] of Object.entries(configs)) { + try { + // 更新内存中的配置 + configCache[key] = value; + const filePath = path.join(dataConfigPath, `${key}.json`); + + // 写入配置文件 + await fc.writeJSON(filePath, value); + } catch (error) { + logger.error(`[crystelf-plugin] 设置配置失败 ${key}: ${error}`); + throw error; } - await fc.writeJSON(configFile, cfg); } }, async save() { + // 确保目录存在 + await fsp.mkdir(dataConfigPath, { recursive: true }); + for (const [key, value] of Object.entries(configCache)) { const filePath = path.join(dataConfigPath, `${key}.json`); + try { - await fsp.access(filePath); + // 直接写入配置文件 await fc.writeJSON(filePath, value); - } catch { - let cfg = await fc.readJSON(configFile); - if (Array.isArray(cfg)) { - cfg = value; - } else { - cfg[key] = value; - } - await fc.writeJSON(configFile, cfg); + } catch (error) { + logger.error(`[crystelf-plugin] 保存配置文件失败 ${filePath}: ${error}`); + throw error; } } }, diff --git a/modules/apps/fanqie/fanqie.js b/modules/apps/fanqie/fanqie.js deleted file mode 100644 index 8127677..0000000 --- a/modules/apps/fanqie/fanqie.js +++ /dev/null @@ -1,38 +0,0 @@ -import axios from 'axios'; - -class Fanqie { - constructor(apiurl) { - this.apiurl = apiurl; - } - - async get_info(book_id) { - try { - let url = `${this.apiurl}/api/info?book_id=${book_id}&source=fanqie`; - let res = await axios.get(url); - if (res.status !== 200 || !res.data) throw new Error('请求失败或无数据'); - let result = res.data['data']; - if (!result) throw new Error('data 字段不存在'); - return { - author: result.author, - book_name: result.book_name, - original_book_name: result.original_book_name, - }; - } catch (e) { - logger.error(e); - return false; - } - } - async down(book_id, msg_id) { - try { - let url = `${this.apiurl}/api/down?book_id=${book_id}&source=fanqie&type=txt&user_id=${msg_id}`; - // 发送get请求 - await axios.get(url); - return true; - } catch (e) { - logger.error(e); - return false; - } - } -} - -export default Fanqie; diff --git a/modules/openai/openaiChat.js b/modules/openai/openaiChat.js index e59f8f6..bc0a920 100644 --- a/modules/openai/openaiChat.js +++ b/modules/openai/openaiChat.js @@ -62,9 +62,32 @@ class OpenaiChat { presence_penalty: 0.2, stream:false }); - - const aiResponse = completion.choices[0].message.content; - //logger.info(aiResponse); + let parsedCompletion = completion; + if (typeof completion === 'string') { + try { + parsedCompletion = JSON.parse(completion); + } catch (parseError) { + logger.error('[crystelf-ai] 响应JSON解析失败:', parseError); + return { success: false }; + } + } + + //logger.info("[DEBUG] 解析后的响应:", JSON.stringify(parsedCompletion)); + let aiResponse = null; + + if (parsedCompletion && parsedCompletion.choices && Array.isArray(parsedCompletion.choices) && parsedCompletion.choices.length > 0) { + const choice = parsedCompletion.choices[0]; + if (choice && choice.message && choice.message.content) { + aiResponse = choice.message.content; + } + } + + if (!aiResponse) { + logger.error('[crystelf-ai] 无法从响应中提取AI回复内容:', parsedCompletion); + return { success: false }; + } + + logger.info("[DEBUG] AI响应内容:", aiResponse); return { success: true, aiResponse: aiResponse, diff --git a/resources/img/logo.png b/resources/img/logo.png new file mode 100644 index 0000000..e5f0dfe Binary files /dev/null and b/resources/img/logo.png differ diff --git a/utils/pinyin.js b/utils/pinyin.js deleted file mode 100644 index 9957cf1..0000000 --- a/utils/pinyin.js +++ /dev/null @@ -1,72 +0,0 @@ -import pinyin from 'pinyin-pro'; - -class PinyinUtils { - /** - * 将中文转化为拼音 - * @param text 文本 - * @param toneType none - * @returns {*|string} - */ - static toPinyin(text, toneType = 'none') { - try { - return pinyin.pinyin(text, { - toneType, - type: 'string', - nonZh: 'consecutive' - }); - } catch (error) { - logger.error(`[crystelf-ai] 拼音转换失败: ${error.message}`); - return text; - } - } - - /** - * 检查文本是否包含拼音关键词 - * @param text - * @param pinyinKeywords - * @returns {{keyword: *, matched: boolean, type: string}|null} - */ - static matchPinyin(text, pinyinKeywords) { - if (!text || !pinyinKeywords || pinyinKeywords.length === 0) { - return null; - } - const textPinyin = this.toPinyin(text.toLowerCase()); - for (const keyword of pinyinKeywords) { - if (textPinyin.includes(keyword.toLowerCase())) { - return { - keyword, - matched: true, - type: 'pinyin' - }; - } - } - return null; - } - - /** - * 检查文本是否包含关键词 - * @param text 文本 - * @param chineseKeywords 中文关键词数组 - * @param pinyinKeywords 拼音关键词数组 - * @returns {{keyword: *, matched: boolean, type: string}|null|{keyword: *, matched: boolean, type: string}} - */ - static matchKeywords(text, chineseKeywords = [], pinyinKeywords = []) { - if (!text) return null; - const lowerText = text.toLowerCase(); - for (const keyword of chineseKeywords) { - if (lowerText.includes(keyword.toLowerCase())) { - return { - keyword, - matched: true, - type: 'chinese' - }; - } - } - if (pinyinKeywords.length > 0) { - return this.matchPinyin(text, pinyinKeywords); - } - return null; - } -} - -export default PinyinUtils;