Compare commits

..

No commits in common. "10534d64f24e3bc8fd6a4b089bd8b7004690b10a" and "8c413949ace0eaeb0c87935ad208a14eba07deff" have entirely different histories.

17 changed files with 692 additions and 1294 deletions

176
README.md
View File

@ -118,7 +118,7 @@
- [X] 获取引用消息 - [X] 获取引用消息
- [X] 适配多模态模型,查看图片等 - [X] 适配多模态模型,查看图片等
- [ ] 支持联网搜索 - [ ] 支持联网搜索
- [X] 支持生成图片 - [ ] 支持生成图片
- [ ] 支持渲染数学公式 - [ ] 支持渲染数学公式
- [ ] 违禁词检测 - [ ] 违禁词检测
- [ ] 使用toon代替json与模型交互 - [ ] 使用toon代替json与模型交互
@ -132,10 +132,180 @@
</details> </details>
## 插件配置 ## 插件配置
本插件暂未适配锅巴,请前往云崽根目录 `data/crystelf` 中修改配置文件
**配置文件已启用热更新,请不要修改插件目录下 `config` 文件夹中的文件**
## **本插件已适配锅巴,请务必使用锅巴进行插件配置** <details>
<summary>各模块配置文件解析</summary>
修改时请勿携带注释 `//`
只对需要注意的配置项进行解析,没有出现的配置项或配置文件可能是为以后的升级预留或不重要
<details>
<summary>config.json</summary>
**请不要修改插件目录下 `config` 文件夹中的文件** ```
{
"maxFeed": 10,//使用rss推送功能时,本地记录的最长长度,用于检测最新文章
//功能是否启用
"?autoUpdate": "是否自动更新插件",
"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
}
```
</details>
<details>
<summary>60s.json</summary>
```
{
"url": "https://60s.viki.moe" //60s基础api,用于调取每日新闻,可以自行部署
}
```
</details>
<details>
<summary>auth.json</summary>
```
{
"url": "https://carbon.crystelf.top",//手性碳api地址,可以自建,也可以用我们提供的api
//默认配置
"default": {
"enable": false,//在每个群是否启用
"carbon": {//手性碳配置
"enable": false,//是否启用手性碳验证,false则为数字验证
"hint": true,//是否提示手性碳位置
"hard-mode": false //是否启用困难模式,该模式下需要回答全部位置的手性碳
},
"timeout": 180,//超时时间(s)
"recall": true,//是否撤回错误答案
"frequency": 5 //最大尝试次数
}
}
```
</details>
<details>
<summary>coreConfig.json</summary>
```
{
"coreUrl": "https://core.crystelf.top",//晶灵核心地址,某些功能如早晚安问候,戳一戳,晶灵智能等需要使用到
"token": "" //验证api,可忽略
}
```
</details>
<details>
<summary>poke.json</summary>
```
{
"replyPoke": 0.4 //被戳回戳概率
}
```
</details>
<details>
<summary>profile.json</summary>
```
{
"nickName": "鸡气人" //你的bot的昵称
}
```
</details>
<details>
<summary>music.json</summary>
```
{
"?url": "api地址,不建议修改",
"url": "https://api.401658.xyz",
"?username&&password": "请勿修改",
"username": "crystelf",
"password": "1145141919810",
"?quality": "1为96kbpsAAC,2为320kbpsAAC,3为最高16-bit/44.1kHzflac",
"quality": "3"
}
```
</details>
<details>
<summary>ai.json</summary>
```
{
"?mode": "对话模式,mix为混合,ai为纯人工智能,keyword为纯关键词",
"mode": "mix", //mix模式下,会在消息长度小于maxMix时查找其中的关键词进行回复,以达到节省token的效果
"baseApi": "https://api.siliconflow.cn/v1", //baseapi地址,需支持openai协议
"apiKey": "", //你的api密钥
"?modelType": "模型名称,请根据baseApi填写的服务商的对应的模型",
"modelType": "deepseek-ai/DeepSeek-V3.2-Exp",
"?multimodalEnabled": "是否启用多模态模型模式,启用后将忽略文本模型",//开启多模态模式后,将默认使用多模态模型回复
"multimodalEnabled": false,//多模态模式可以处理视频,图片等
"?multimodalModel": "多模态模型名称",
"multimodalModel": "Qwen/Qwen2.5-VL-72B-Instruct",
"?temperature": "聊天温度,可选0-2.0,温度越高创造性越高",
"temperature": 1.2,
"?concurrency": "最大同时聊天群数,一个群最多一个人聊天",
"concurrency": 3,
"?maxMix": "mix模式下,如果用户消息长度大于这个值,那么使用ai回复",
"maxMix": 5,
"?timeout": "记忆默认超时时间(天)",
"timeout": 30,
"?maxSessions": "最大同时存在的sessions群聊数量",
"maxSessions": 10,
"?chatHistory": "聊天上下文最大长度",
"chatHistory": 10,
"?maxMessageLength": "最大消息长度,如果消息长度大于这个值,超出的部分将会被截断",
"maxMessageLength": 100,
"?getChatHistoryLength": "获取到的聊天上下文长度,ai可以看到多少条群聊的聊天记录",
"getChatHistoryLength":20,
"?blockGroup": "禁用的群聊(黑名单)",
"blockGroup": [],
"?whiteGroup": "白名单群聊,存在该部分时,黑名单将被禁用",
"whiteGroup": [],
"?character": "回复表情包时的角色",
"character": "zhenxun", //目前晶灵核心仅有zhenxun角色,后续可能会增加更多角色
"?botPersona": "机器人人设描述",
"botPersona": "你是一个名为晶灵的智能助手,性格温和友善,喜欢帮助用户解决问题.知识渊博,能够回答各种问题,偶尔会使用一些可爱的表情和语气.会记住与用户的对话内容,提供个性化的回复.",
"?codeRenderer": "代码渲染配置",
"codeRenderer": {
"theme": "github",
"fontSize": 14,
"lineNumbers": true,
"backgroundColor": "#f6f8fa"
},
"?markdownRenderer": "Markdown渲染配置",
"markdownRenderer": {
"theme": "dark",
"fontSize": 14,
"codeTheme": "github"
},
}
```
</details>
</details>
## 关于晶灵核心 ## 关于晶灵核心
晶灵核心是一个开源的api服务,使用nestjs框架编写,本插件部分功能依赖于晶灵核心,如戳一戳,早晚安,晶灵智能等. 晶灵核心是一个开源的api服务,使用nestjs框架编写,本插件部分功能依赖于晶灵核心,如戳一戳,早晚安,晶灵智能等.

225
apps/fanqie.js Normal file
View File

@ -0,0 +1,225 @@
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;
}
}
}

3
components/date.js Normal file
View File

@ -0,0 +1,3 @@
let date = {}; //咕咕咕
export default date;

View File

@ -7,6 +7,15 @@ let tools = {
sleep(ms) { sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, 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; export default tools;

View File

@ -32,6 +32,8 @@
"getChatHistoryLength":20, "getChatHistoryLength":20,
"?keywordCache": "是否缓存关键词到本地", "?keywordCache": "是否缓存关键词到本地",
"keywordCache": true, "keywordCache": true,
"?pinyinMatch": "是否启用拼音匹配",
"pinyinMatch": true,
"?blockGroup": "禁用的群聊(黑名单)", "?blockGroup": "禁用的群聊(黑名单)",
"blockGroup": [], "blockGroup": [],
"?whiteGroup": "白名单群聊,存在该部分时,黑名单将被禁用", "?whiteGroup": "白名单群聊,存在该部分时,黑名单将被禁用",
@ -80,7 +82,7 @@
"?model": "图像生成模型名称(支持gemini-3-pro-image-preview等)", "?model": "图像生成模型名称(支持gemini-3-pro-image-preview等)",
"model": "gemini-3-pro-image-preview", "model": "gemini-3-pro-image-preview",
"?baseApi": "图像生成API基础地址(不加v1后面的)", "?baseApi": "图像生成API基础地址(不加v1后面的)",
"baseApi": "https://api.siliconflow.cn", "baseApi": "https://api.uniapi.io",
"?apiKey": "图像生成API密钥", "?apiKey": "图像生成API密钥",
"apiKey": "", "apiKey": "",
"?timeout": "图像生成超时时间(豪秒)", "?timeout": "图像生成超时时间(豪秒)",

135
config/blackwords.json Normal file
View File

@ -0,0 +1,135 @@
{
"?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": []
}
}

View File

@ -8,12 +8,14 @@
"autoUpdate": true, "autoUpdate": true,
"poke": true, "poke": true,
"60s": true, "60s": true,
"fanqie": true,
"zwa": true, "zwa": true,
"rss": true, "rss": true,
"help": true, "help": true,
"welcome": true, "welcome": true,
"faceReply": true, "faceReply": true,
"ai": true, "ai": true,
"blackWords": true,
"music": true, "music": true,
"auth": true "auth": true
} }

4
config/fanqieConfig.json Normal file
View File

@ -0,0 +1,4 @@
{
"url": "http://127.0.0.1:6868",
"outDir": "/home/user/debian/cache/Downloads"
}

View File

@ -1,30 +0,0 @@
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,
},
};
}

View File

@ -1,351 +0,0 @@
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,
};
}

View File

@ -1,789 +0,0 @@
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;

View File

@ -141,7 +141,7 @@ class UserConfigManager {
/** /**
* 清除用户配置缓存 * 清除用户配置缓存
* @param {string|null} userId - 用户QQ号如果不传则清除所有缓存 * @param {string} userId - 用户QQ号如果不传则清除所有缓存
*/ */
clearCache(userId) { clearCache(userId) {
if (userId) { if (userId) {

View File

@ -15,61 +15,32 @@ let watchers = [];
*/ */
async function init() { async function init() {
try { try {
// 确保数据配置目录存在
try { try {
await fsp.access(dataConfigPath); await fsp.access(dataConfigPath);
} catch { } catch {
await fsp.mkdir(dataConfigPath, { recursive: true }); await fsp.mkdir(dataConfigPath, { recursive: true });
logger.mark(`[crystelf-plugin] 配置目录创建成功: ${dataConfigPath}`); logger.mark(`[crystelf-plugin] 配置目录创建成功: ${dataConfigPath}`);
} }
// 确保默认配置目录存在
try {
await fsp.access(pluginConfigPath);
} catch {
logger.warn(`[crystelf-plugin] 默认配置目录不存在: ${pluginConfigPath}`);
}
// 处理主配置文件
const pluginDefaultFile = path.join(pluginConfigPath, 'config.json'); const pluginDefaultFile = path.join(pluginConfigPath, 'config.json');
try { try {
await fsp.access(configFile); await fsp.access(configFile);
} catch { } catch {
try { await fsp.copyFile(pluginDefaultFile, configFile);
await fsp.copyFile(pluginDefaultFile, configFile); logger.mark(`[crystelf-plugin] 默认配置复制成功: ${configFile}`);
logger.mark(`[crystelf-plugin] 默认配置复制成功: ${configFile}`);
} catch (copyError) {
logger.warn(`[crystelf-plugin] 复制默认配置失败,创建空配置: ${copyError}`);
await fc.writeJSON(configFile, {});
}
} }
let pluginFiles = []; const pluginFiles = (await fsp.readdir(pluginConfigPath)).filter((f) => f.endsWith('.json'));
try {
pluginFiles = (await fsp.readdir(pluginConfigPath)).filter((f) => f.endsWith('.json'));
} catch (error) {
logger.warn(`[crystelf-plugin] 读取默认配置目录失败: ${error}`);
}
// 复制缺失的配置文件
for (const file of pluginFiles) { for (const file of pluginFiles) {
const pluginFilePath = path.join(pluginConfigPath, file); const pluginFilePath = path.join(pluginConfigPath, file);
const dataFilePath = path.join(dataConfigPath, file); const dataFilePath = path.join(dataConfigPath, file);
try { try {
await fsp.access(dataFilePath); await fsp.access(dataFilePath);
} catch { } catch {
try { await fsp.copyFile(pluginFilePath, dataFilePath);
await fsp.copyFile(pluginFilePath, dataFilePath); logger.mark(`[crystelf-plugin] 配置文件缺失,已复制: ${file}`);
logger.mark(`[crystelf-plugin] 配置文件缺失,已复制: ${file}`);
} catch (copyError) {
logger.warn(`[crystelf-plugin] 复制配置文件失败 ${file}: ${copyError}`);
}
} }
} }
// 读取所有配置文件
const files = (await fsp.readdir(dataConfigPath)).filter((f) => f.endsWith('.json')); const files = (await fsp.readdir(dataConfigPath)).filter((f) => f.endsWith('.json'));
configCache = {}; configCache = {};
for (const file of files) { for (const file of files) {
const filePath = path.join(dataConfigPath, file); const filePath = path.join(dataConfigPath, file);
const name = path.basename(file, '.json'); const name = path.basename(file, '.json');
@ -79,9 +50,7 @@ async function init() {
try { try {
await fsp.access(pluginFilePath); await fsp.access(pluginFilePath);
const pluginData = await fc.readJSON(pluginFilePath); const pluginData = await fc.readJSON(pluginFilePath);
if (Array.isArray(data) && Array.isArray(pluginData)) { if (Array.isArray(data) && Array.isArray(pluginData)) {
// 合并数组类型配置
const strSet = new Set(data.map((x) => JSON.stringify(x))); const strSet = new Set(data.map((x) => JSON.stringify(x)));
for (const item of pluginData) { for (const item of pluginData) {
const str = JSON.stringify(item); const str = JSON.stringify(item);
@ -91,29 +60,20 @@ async function init() {
} }
} }
} else if (!Array.isArray(data) && !Array.isArray(pluginData)) { } else if (!Array.isArray(data) && !Array.isArray(pluginData)) {
// 合并对象类型配置
data = fc.mergeConfig(data, pluginData); data = fc.mergeConfig(data, pluginData);
} }
// 保存合并后的配置
await fc.writeJSON(filePath, data); await fc.writeJSON(filePath, data);
} catch (mergeError) { } catch {}
logger.error('[crystelf-plugin]合并配置失败..');
logger.error(mergeError);
// 忽略合并错误,使用现有数据
}
configCache[name] = data; configCache[name] = data;
} catch (e) { } catch (e) {
logger.warn(`[crystelf-plugin] 读取配置文件 ${file} 失败:`, e); logger.warn(`[crystelf-plugin] 读取配置文件 ${file} 失败:`, e);
} }
} }
if (configCache.debug) { if (configCache.debug) {
logger.info('[crystelf-plugin] 配置模块初始化成功..'); logger.info('[crystelf-plugin] 配置模块初始化成功..');
} }
} catch (err) { } catch (err) {
logger.warn('[crystelf-plugin] 配置初始化失败,使用空配置..', err); logger.warn('[crystelf-plugin] 配置初始化失败使用空配置..', err);
configCache = {}; configCache = {};
} }
} }
@ -138,7 +98,7 @@ function watchConfigs() {
const data = await fc.readJSON(filePath); const data = await fc.readJSON(filePath);
const name = path.basename(file, '.json'); const name = path.basename(file, '.json');
configCache[name] = data; configCache[name] = data;
logger.info(`[crystelf-plugin] 配置热更新: ${file}`); logger.info(`[crystelf-plugin] 配置热更新: ${file}`);
} catch (e) { } catch (e) {
logger.warn(`[crystelf-plugin] 热更新读取失败 ${file}:`, e); logger.warn(`[crystelf-plugin] 热更新读取失败 ${file}:`, e);
} }
@ -160,65 +120,36 @@ const configControl = {
}, },
async set(key, value) { async set(key, value) {
// 更新内存中的配置
configCache[key] = value; configCache[key] = value;
const filePath = path.join(dataConfigPath, `${key}.json`); const filePath = path.join(dataConfigPath, `${key}.json`);
try { try {
// 尝试访问文件,如果存在则直接写入
await fsp.access(filePath); await fsp.access(filePath);
await fc.writeJSON(filePath, value); await fc.writeJSON(filePath, value);
} catch (error) { } catch {
// 文件不存在,创建新文件 let cfg = await fc.readJSON(configFile);
try { if (Array.isArray(cfg)) {
// 确保目录存在 cfg.push(value);
await fsp.mkdir(dataConfigPath, { recursive: true }); } else {
// 直接写入新文件 cfg[key] = value;
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() { async save() {
// 确保目录存在
await fsp.mkdir(dataConfigPath, { recursive: true });
for (const [key, value] of Object.entries(configCache)) { for (const [key, value] of Object.entries(configCache)) {
const filePath = path.join(dataConfigPath, `${key}.json`); const filePath = path.join(dataConfigPath, `${key}.json`);
try { try {
// 直接写入配置文件 await fsp.access(filePath);
await fc.writeJSON(filePath, value); await fc.writeJSON(filePath, value);
} catch (error) { } catch {
logger.error(`[crystelf-plugin] 保存配置文件失败 ${filePath}: ${error}`); let cfg = await fc.readJSON(configFile);
throw error; if (Array.isArray(cfg)) {
cfg = value;
} else {
cfg[key] = value;
}
await fc.writeJSON(configFile, cfg);
} }
} }
}, },

View File

@ -0,0 +1,38 @@
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;

View File

@ -62,32 +62,9 @@ class OpenaiChat {
presence_penalty: 0.2, presence_penalty: 0.2,
stream:false stream:false
}); });
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)); const aiResponse = completion.choices[0].message.content;
let aiResponse = null; //logger.info(aiResponse);
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 { return {
success: true, success: true,
aiResponse: aiResponse, aiResponse: aiResponse,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

72
utils/pinyin.js Normal file
View File

@ -0,0 +1,72 @@
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;