mirror of
https://github.com/Jerryplusy/crystelf-plugin.git
synced 2026-01-29 09:17:27 +00:00
Compare commits
13 Commits
8c413949ac
...
10534d64f2
| Author | SHA1 | Date | |
|---|---|---|---|
| 10534d64f2 | |||
|
|
2e1d113298 | ||
| a9843beed7 | |||
| b0c45f413a | |||
| afd448bf40 | |||
| 13281e96fe | |||
| a57c9b2c55 | |||
| 27609a7c80 | |||
| 85bd436b1f | |||
| 58c4f19dd6 | |||
| 9a642aa560 | |||
| e0b0b5bacf | |||
| 943a51be65 |
176
README.md
176
README.md
@ -118,7 +118,7 @@
|
||||
- [X] 获取引用消息
|
||||
- [X] 适配多模态模型,查看图片等
|
||||
- [ ] 支持联网搜索
|
||||
- [ ] 支持生成图片
|
||||
- [X] 支持生成图片
|
||||
- [ ] 支持渲染数学公式
|
||||
- [ ] 违禁词检测
|
||||
- [ ] 使用toon代替json与模型交互
|
||||
@ -132,180 +132,10 @@
|
||||
</details>
|
||||
|
||||
## 插件配置
|
||||
本插件暂未适配锅巴,请前往云崽根目录 `data/crystelf` 中修改配置文件
|
||||
**配置文件已启用热更新,请不要修改插件目录下 `config` 文件夹中的文件**
|
||||
|
||||
<details>
|
||||
<summary>各模块配置文件解析</summary>
|
||||
修改时请勿携带注释 `//`
|
||||
只对需要注意的配置项进行解析,没有出现的配置项或配置文件可能是为以后的升级预留或不重要
|
||||
<details>
|
||||
<summary>config.json</summary>
|
||||
## **本插件已适配锅巴,请务必使用锅巴进行插件配置**
|
||||
|
||||
```
|
||||
{
|
||||
"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>
|
||||
**请不要修改插件目录下 `config` 文件夹中的文件**
|
||||
|
||||
## 关于晶灵核心
|
||||
晶灵核心是一个开源的api服务,使用nestjs框架编写,本插件部分功能依赖于晶灵核心,如戳一戳,早晚安,晶灵智能等.
|
||||
|
||||
225
apps/fanqie.js
225
apps/fanqie.js
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
let date = {}; //咕咕咕
|
||||
|
||||
export default date;
|
||||
@ -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;
|
||||
|
||||
@ -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": "图像生成超时时间(豪秒)",
|
||||
|
||||
@ -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": []
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
{
|
||||
"url": "http://127.0.0.1:6868",
|
||||
"outDir": "/home/user/debian/cache/Downloads"
|
||||
}
|
||||
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,
|
||||
};
|
||||
}
|
||||
789
guoba/configSchema.js
Normal file
789
guoba/configSchema.js
Normal file
@ -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;
|
||||
@ -141,7 +141,7 @@ class UserConfigManager {
|
||||
|
||||
/**
|
||||
* 清除用户配置缓存
|
||||
* @param {string} userId - 用户QQ号,如果不传则清除所有缓存
|
||||
* @param {string|null} userId - 用户QQ号,如果不传则清除所有缓存
|
||||
*/
|
||||
clearCache(userId) {
|
||||
if (userId) {
|
||||
|
||||
@ -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 {
|
||||
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 {
|
||||
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 = {};
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -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;
|
||||
@ -62,9 +62,32 @@ class OpenaiChat {
|
||||
presence_penalty: 0.2,
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
const aiResponse = completion.choices[0].message.content;
|
||||
//logger.info(aiResponse);
|
||||
//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,
|
||||
|
||||
BIN
resources/img/logo.png
Normal file
BIN
resources/img/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
@ -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;
|
||||
Loading…
x
Reference in New Issue
Block a user