mirror of
https://github.com/Jerryplusy/crystelf-plugin.git
synced 2026-01-29 09:17:27 +00:00
Compare commits
4 Commits
a57c9b2c55
...
a9843beed7
| Author | SHA1 | Date | |
|---|---|---|---|
| a9843beed7 | |||
| b0c45f413a | |||
| afd448bf40 | |||
| 13281e96fe |
30
guoba.support.js
Normal file
30
guoba.support.js
Normal file
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
351
guoba/configHandler.js
Normal file
351
guoba/configHandler.js
Normal file
@ -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<Object>} 当前配置对象
|
||||
*/
|
||||
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<Object>} 操作结果
|
||||
*/
|
||||
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<Object>} 操作结果
|
||||
*/
|
||||
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<Object>} 操作结果,包含配置数据
|
||||
*/
|
||||
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<Object>} 操作结果
|
||||
*/
|
||||
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,
|
||||
};
|
||||
}
|
||||
@ -430,7 +430,7 @@ const guobaSchema = [
|
||||
field: 'ai.character',
|
||||
label: '表情包角色',
|
||||
component: 'Select',
|
||||
bottomHelpMessage: '回复表情包时的角色(能力有限,目前仅支持一种角色)',
|
||||
bottomHelpMessage: '回复表情包时的角色(能力有限,目前仅支持一种角色qwq)',
|
||||
componentProps: {
|
||||
options: [{ label: '真寻', value: 'zhenxun' }],
|
||||
placeholder: '请选择表情包角色',
|
||||
@ -601,7 +601,7 @@ const guobaSchema = [
|
||||
field: 'ai.blockGroup',
|
||||
label: '禁用群聊',
|
||||
component: 'InputArray',
|
||||
bottomHelpMessage: '黑名单群聊,插件不会在这些群聊中工作',
|
||||
bottomHelpMessage: '黑名单群聊,AI不会在这些群聊中工作',
|
||||
componentProps: {
|
||||
placeholder: '请输入群号,按回车添加',
|
||||
},
|
||||
@ -776,7 +776,7 @@ const guobaSchema = [
|
||||
component: 'SOFT_GROUP_BEGIN',
|
||||
},
|
||||
{
|
||||
field: 'profile.nickname',
|
||||
field: 'profile.nickName',
|
||||
label: '机器人昵称',
|
||||
component: 'Input',
|
||||
bottomHelpMessage: '机器人的昵称',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user