mirror of
https://github.com/crystelf/crystelf-admin.git
synced 2025-12-05 13:41:57 +00:00
init
This commit is contained in:
parent
ff7679eaf2
commit
fbd78dd7ad
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/node_modules/
|
||||||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
12
.idea/crystelf-admin.iml
generated
Normal file
12
.idea/crystelf-admin.iml
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/crystelf-admin.iml" filepath="$PROJECT_DIR$/.idea/crystelf-admin.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
||||||
@ -1,2 +1,4 @@
|
|||||||
# crystelf-admin
|
# crystelf-admin
|
||||||
TRSS-Yunzai的后🚪插件
|
TRSS-Yunzai的管理插件
|
||||||
|
|
||||||
|
用于晶灵管理群聊等,含有大量后门逻辑,请千万~~不~~要安装
|
||||||
|
|||||||
38
apps/coreRestart.js
Normal file
38
apps/coreRestart.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import systemControl from '../lib/core/systemControl.js';
|
||||||
|
import tools from '../components/tool.js';
|
||||||
|
import configControl from '../lib/config/configControl.js';
|
||||||
|
|
||||||
|
export default class CoreRestart extends plugin {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
name: 'crystelf重启核心',
|
||||||
|
dsc: '实现核心的重启功能',
|
||||||
|
rule: [
|
||||||
|
{
|
||||||
|
reg: '^#core重启$',
|
||||||
|
fnc: 'restart',
|
||||||
|
permission: 'master',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async restart(e) {
|
||||||
|
if (!configControl.get('core')) {
|
||||||
|
return e.reply(`晶灵核心未启用..`, true);
|
||||||
|
}
|
||||||
|
const returnData = await systemControl.systemRestart();
|
||||||
|
if (returnData?.data?.success) {
|
||||||
|
await e.reply(`操作成功:${returnData?.data?.data}..`, true);
|
||||||
|
} else {
|
||||||
|
await e.reply(`操作失败:${returnData?.data?.data}..`, true);
|
||||||
|
}
|
||||||
|
await tools.sleep(8000);
|
||||||
|
const restartTime = await systemControl.getRestartTime();
|
||||||
|
if (restartTime) {
|
||||||
|
await e.reply(`晶灵核心重启成功!耗时${restartTime?.data?.data}秒..`, true);
|
||||||
|
} else {
|
||||||
|
await e.reply(`核心重启花的时间有点久了呢..${restartTime?.data?.data}`, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
69
apps/reportBots.js
Normal file
69
apps/reportBots.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import botControl from '../lib/core/botControl.js';
|
||||||
|
import configControl from '../lib/config/configControl.js';
|
||||||
|
import schedule from 'node-schedule';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export default class ReportBots extends plugin {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
name: 'crystelf Bot状态上报',
|
||||||
|
dsc: '一些操作bot的功能',
|
||||||
|
rule: [
|
||||||
|
{
|
||||||
|
reg: '^#crystelf同步$',
|
||||||
|
fnc: 'manualReport',
|
||||||
|
permission: 'master',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reg: '^#crystelf广播(.+)$',
|
||||||
|
fnc: 'broadcast',
|
||||||
|
permission: 'master',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
schedule.scheduleJob('*/30 * * * *', () => this.autoReport());
|
||||||
|
}
|
||||||
|
|
||||||
|
async autoReport() {
|
||||||
|
logger.mark(`正在自动同步bot数据到晶灵核心..`);
|
||||||
|
if (configControl.get('core')) {
|
||||||
|
await botControl.reportBots();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async manualReport(e) {
|
||||||
|
if (!configControl.get('core')) {
|
||||||
|
return e.reply(`晶灵核心未启用..`, true);
|
||||||
|
}
|
||||||
|
let success = await botControl.reportBots();
|
||||||
|
if (success) {
|
||||||
|
await e.reply('crystelf Bot信息已同步到核心..', true);
|
||||||
|
} else {
|
||||||
|
await e.reply('crystelf Bot同步失败:核心未连接..', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async broadcast(e) {
|
||||||
|
const msg = e?.msg?.match(/^#crystelf广播(.+)$/)?.[1]?.trim();
|
||||||
|
if (!msg) {
|
||||||
|
return e.reply('广播内容不能为空');
|
||||||
|
}
|
||||||
|
await e.reply(`开始广播消息到所有群..`);
|
||||||
|
try {
|
||||||
|
const sendData = {
|
||||||
|
token: configControl.get('coreConfig')?.token,
|
||||||
|
message: msg.toString(),
|
||||||
|
};
|
||||||
|
const url = configControl.get('coreConfig')?.coreUrl;
|
||||||
|
const returnData = await axios.post(`${url}/api/bot/broadcast`, sendData);
|
||||||
|
if (returnData?.data?.success) {
|
||||||
|
return await e.reply(`操作成功:${returnData?.data.data?.toString()}`);
|
||||||
|
} else {
|
||||||
|
return await e.reply(`广播出现错误,请检查日志..`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`广播执行异常: ${err.message}`);
|
||||||
|
return await e.reply('广播过程中发生错误,请检查日志..');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
components/date.js
Normal file
44
components/date.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
let date = {
|
||||||
|
/**
|
||||||
|
* 格式化日期时间
|
||||||
|
* @param {Date|number|string} [date=new Date()] - 可接收Date对象、时间戳或日期字符串
|
||||||
|
* @param {string} [format='YYYY-MM-DD HH:mm:ss'] - 格式模板,支持:
|
||||||
|
* YYYY-年, MM-月, DD-日,
|
||||||
|
* HH-时, mm-分, ss-秒
|
||||||
|
* @returns {string} 格式化后的日期字符串
|
||||||
|
* @example
|
||||||
|
* fc.formatDate(new Date(), 'YYYY年MM月DD日') // "2023年08月15日"
|
||||||
|
*/
|
||||||
|
formatDate(date = new Date(), format = 'YYYY-MM-DD HH:mm:ss') {
|
||||||
|
const d = new Date(date);
|
||||||
|
const pad = (n) => n.toString().padStart(2, '0');
|
||||||
|
|
||||||
|
return format
|
||||||
|
.replace(/YYYY/g, pad(d.getFullYear()))
|
||||||
|
.replace(/MM/g, pad(d.getMonth() + 1))
|
||||||
|
.replace(/DD/g, pad(d.getDate()))
|
||||||
|
.replace(/HH/g, pad(d.getHours()))
|
||||||
|
.replace(/mm/g, pad(d.getMinutes()))
|
||||||
|
.replace(/ss/g, pad(d.getSeconds()));
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDuration(seconds) {
|
||||||
|
const days = Math.floor(seconds / 86400);
|
||||||
|
const hours = Math.floor((seconds % 86400) / 3600);
|
||||||
|
const mins = Math.floor((seconds % 3600) / 60);
|
||||||
|
const secs = seconds % 60;
|
||||||
|
|
||||||
|
return (
|
||||||
|
[
|
||||||
|
days > 0 ? `${days}天` : '',
|
||||||
|
hours > 0 ? `${hours}小时` : '',
|
||||||
|
mins > 0 ? `${mins}分钟` : '',
|
||||||
|
secs > 0 ? `${secs}秒` : '',
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ') || '0秒'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default date;
|
||||||
247
components/json.js
Normal file
247
components/json.js
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import Version from '../lib/system/version.js';
|
||||||
|
|
||||||
|
const Plugin_Name = Version.name;
|
||||||
|
|
||||||
|
const _path = process.cwd();
|
||||||
|
const getRoot = (root = '') => {
|
||||||
|
if (root === 'root' || root === 'yunzai') {
|
||||||
|
root = `${_path}/`;
|
||||||
|
} else if (!root) {
|
||||||
|
root = `${_path}/plugins/${Plugin_Name}/`;
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
};
|
||||||
|
|
||||||
|
let fc = {
|
||||||
|
/**
|
||||||
|
* 递归创建目录结构
|
||||||
|
* @param {string} [path=""] - 要创建的相对路径,支持多级目录(如 "dir1/dir2")
|
||||||
|
* @param {string} [root=""] - 基础根目录,可选值:
|
||||||
|
* - "root" 或 "yunzai": 使用 Yunzai 根目录
|
||||||
|
* - 空值: 使用插件目录
|
||||||
|
* @param {boolean} [includeFile=false] - 是否包含最后一级作为文件名
|
||||||
|
* @example
|
||||||
|
* fc.createDir("config/deepseek", "root") // 在 Yunzai 根目录创建 config/deepseek 目录
|
||||||
|
*/
|
||||||
|
createDir(path = '', root = '', includeFile = false) {
|
||||||
|
root = getRoot(root);
|
||||||
|
let pathList = path.split('/');
|
||||||
|
let nowPath = root;
|
||||||
|
pathList.forEach((name, idx) => {
|
||||||
|
name = name.trim();
|
||||||
|
if (!includeFile && idx <= pathList.length - 1) {
|
||||||
|
nowPath += name + '/';
|
||||||
|
if (name) {
|
||||||
|
if (!fs.existsSync(nowPath)) {
|
||||||
|
fs.mkdirSync(nowPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取JSON文件
|
||||||
|
* @param {string} [file=""] - JSON文件路径(相对路径)
|
||||||
|
* @param {string} [root=""] - 基础根目录(同 createDir)
|
||||||
|
* @returns {object} 解析后的JSON对象,如文件不存在或解析失败返回空对象
|
||||||
|
* @example
|
||||||
|
* const config = fc.readJSON("config.json", "root")
|
||||||
|
*/
|
||||||
|
readJSON(file = '', root = '') {
|
||||||
|
root = getRoot(root);
|
||||||
|
if (fs.existsSync(`${root}/${file}`)) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(`${root}/${file}`, 'utf8'));
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
|
statSync(file = '', root = '') {
|
||||||
|
root = getRoot(root);
|
||||||
|
try {
|
||||||
|
return fs.statSync(`${root}/${file}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入JSON文件(完全覆盖)
|
||||||
|
* @param {string} file - 目标文件路径
|
||||||
|
* @param {object} data - 要写入的JSON数据
|
||||||
|
* @param {string} [root=""] - 基础根目录(同 createDir)
|
||||||
|
* @param {number} [space=4] - JSON格式化缩进空格数
|
||||||
|
* @returns {boolean} 是否写入成功
|
||||||
|
* @warning 此方法会完全覆盖目标文件原有内容
|
||||||
|
* @example
|
||||||
|
* fc.writeJSON("config.json", {key: "value"}, "root", 4)
|
||||||
|
*/
|
||||||
|
writeJSON(file, data, root = '', space = 4) {
|
||||||
|
fc.createDir(file, root, true);
|
||||||
|
root = getRoot(root);
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(`${root}/${file}`, JSON.stringify(data, null, space));
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全写入JSON文件(合并模式)
|
||||||
|
* @param {string} file - 目标文件路径
|
||||||
|
* @param {object} data - 要合并的数据
|
||||||
|
* @param {string} [root=""] - 基础根目录(同 createDir)
|
||||||
|
* @param {number} [space=4] - JSON格式化缩进空格数
|
||||||
|
* @returns {boolean} 是否写入成功
|
||||||
|
* @description
|
||||||
|
* - 如果目标文件不存在,创建新文件
|
||||||
|
* - 如果目标文件存在,深度合并新旧数据
|
||||||
|
* - 如果目标文件损坏,会创建新文件并记录警告
|
||||||
|
* @example
|
||||||
|
* fc.safewriteJSON("config.json", {newKey: "value"})
|
||||||
|
*/
|
||||||
|
safeWriteJSON(file, data, root = '', space = 4) {
|
||||||
|
fc.createDir(file, root, true);
|
||||||
|
root = getRoot(root);
|
||||||
|
const filePath = `${root}/${file}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let existingData = {};
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
try {
|
||||||
|
existingData = JSON.parse(fs.readFileSync(filePath, 'utf8')) || {};
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`无法解析现有JSON文件 ${filePath},将创建新文件`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedData = this.deepMerge(existingData, data);
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(mergedData, null, space));
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`写入JSON文件失败 ${filePath}:`, err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 深度合并两个对象
|
||||||
|
* @param {object} target - 目标对象(将被修改)
|
||||||
|
* @param {object} source - 源对象
|
||||||
|
* @returns {object} 合并后的目标对象
|
||||||
|
* @description
|
||||||
|
* - 递归合并嵌套对象
|
||||||
|
* - 对于非对象属性直接覆盖
|
||||||
|
* - 不会合并数组(数组会被直接覆盖)
|
||||||
|
* @example
|
||||||
|
* const merged = fc.deepMerge({a: 1}, {b: {c: 2}})
|
||||||
|
* // 返回 {a: 1, b: {c: 2}}
|
||||||
|
*/
|
||||||
|
deepMerge(target, source) {
|
||||||
|
for (const key in source) {
|
||||||
|
if (source.hasOwnProperty(key)) {
|
||||||
|
if (
|
||||||
|
source[key] &&
|
||||||
|
typeof source[key] === 'object' &&
|
||||||
|
target[key] &&
|
||||||
|
typeof target[key] === 'object'
|
||||||
|
) {
|
||||||
|
this.deepMerge(target[key], source[key]);
|
||||||
|
} else {
|
||||||
|
target[key] = source[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归读取目录中的特定扩展名文件
|
||||||
|
* @param {string} directory - 要搜索的目录路径
|
||||||
|
* @param {string} extension - 文件扩展名(不带点)
|
||||||
|
* @param {string} [excludeDir] - 要排除的目录名
|
||||||
|
* @returns {string[]} 匹配的文件相对路径数组
|
||||||
|
* @description
|
||||||
|
* - 自动跳过以下划线开头的文件
|
||||||
|
* - 结果包含子目录中的文件
|
||||||
|
* @example
|
||||||
|
* const jsFiles = fc.readDirRecursive("./plugins", "js", "node_modules")
|
||||||
|
*/
|
||||||
|
readDirRecursive(directory, extension, excludeDir) {
|
||||||
|
let files = fs.readdirSync(directory);
|
||||||
|
|
||||||
|
let jsFiles = files.filter(
|
||||||
|
(file) => path.extname(file) === `.${extension}` && !file.startsWith('_')
|
||||||
|
);
|
||||||
|
|
||||||
|
files
|
||||||
|
.filter((file) => fs.statSync(path.join(directory, file)).isDirectory())
|
||||||
|
.forEach((subdirectory) => {
|
||||||
|
if (subdirectory === excludeDir) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subdirectoryPath = path.join(directory, subdirectory);
|
||||||
|
jsFiles.push(
|
||||||
|
...fc
|
||||||
|
.readDirRecursive(subdirectoryPath, extension, excludeDir)
|
||||||
|
.map((fileName) => path.join(subdirectory, fileName))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return jsFiles;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 深度克隆对象(支持基本类型/数组/对象/Date/RegExp)
|
||||||
|
* @param {*} source - 要克隆的数据
|
||||||
|
* @returns {*} 深度克隆后的副本
|
||||||
|
* @description
|
||||||
|
* - 处理循环引用
|
||||||
|
* - 保持原型链
|
||||||
|
* - 支持特殊对象类型(Date/RegExp等)
|
||||||
|
* @example
|
||||||
|
* const obj = { a: 1, b: [2, 3] };
|
||||||
|
* const cloned = fc.deepClone(obj);
|
||||||
|
*/
|
||||||
|
deepClone(source) {
|
||||||
|
const cache = new WeakMap();
|
||||||
|
|
||||||
|
const clone = (value) => {
|
||||||
|
if (value === null || typeof value !== 'object') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cache.has(value)) {
|
||||||
|
return cache.get(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof Date) return new Date(value);
|
||||||
|
if (value instanceof RegExp) return new RegExp(value);
|
||||||
|
|
||||||
|
const target = new value.constructor();
|
||||||
|
cache.set(value, target);
|
||||||
|
|
||||||
|
for (const key in value) {
|
||||||
|
if (value.hasOwnProperty(key)) {
|
||||||
|
target[key] = clone(value[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return target;
|
||||||
|
};
|
||||||
|
|
||||||
|
return clone(source);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default fc;
|
||||||
57
components/module.js
Normal file
57
components/module.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import Version from '../lib/system/version.js';
|
||||||
|
|
||||||
|
const Plugin_Name = Version.name;
|
||||||
|
|
||||||
|
const _path = process.cwd();
|
||||||
|
const getRoot = (root = '') => {
|
||||||
|
if (root === 'root' || root === 'yunzai') {
|
||||||
|
root = `${_path}/`;
|
||||||
|
} else if (!root) {
|
||||||
|
root = `${_path}/plugins/${Plugin_Name}/`;
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mc = {
|
||||||
|
/**
|
||||||
|
* 动态导入JS模块
|
||||||
|
* @param {string} file - 模块文件路径(可省略.js后缀)
|
||||||
|
* @param {string} [root=""] - 基础根目录(同 createDir)
|
||||||
|
* @returns {Promise<object>} 模块导出对象,如导入失败返回空对象
|
||||||
|
* @description
|
||||||
|
* - 自动添加时间戳参数防止缓存
|
||||||
|
* - 自动补全.js后缀
|
||||||
|
* @example
|
||||||
|
* const module = await fc.importModule("utils/helper")
|
||||||
|
*/
|
||||||
|
async importModule(file, root = '') {
|
||||||
|
root = getRoot(root);
|
||||||
|
if (!/\.js$/.test(file)) {
|
||||||
|
file = file + '.js';
|
||||||
|
}
|
||||||
|
if (fs.existsSync(`${root}/${file}`)) {
|
||||||
|
try {
|
||||||
|
let data = await import(`file://${root}/${file}?t=${new Date() * 1}`);
|
||||||
|
return data || {};
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态导入JS模块的默认导出
|
||||||
|
* @param {string} file - 模块文件路径
|
||||||
|
* @param {string} [root=""] - 基础根目录(同 createDir)
|
||||||
|
* @returns {Promise<object>} 模块的默认导出,如失败返回空对象
|
||||||
|
* @example
|
||||||
|
* const defaultExport = await fc.importDefault("components/Header")
|
||||||
|
*/
|
||||||
|
async importDefault(file, root) {
|
||||||
|
let ret = await fc.importModule(file, root);
|
||||||
|
return ret.default || {};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default mc;
|
||||||
125
components/tool.js
Normal file
125
components/tool.js
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
let tools = {
|
||||||
|
/**
|
||||||
|
* 异步延时函数
|
||||||
|
* @param {number} ms - 等待的毫秒数
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
* @example
|
||||||
|
* await fc.sleep(1000) // 等待1秒
|
||||||
|
*/
|
||||||
|
sleep(ms) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成指定范围内的随机整数
|
||||||
|
* @param {number} min - 最小值(包含)
|
||||||
|
* @param {number} max - 最大值(包含)
|
||||||
|
* @returns {number} 范围内的随机整数
|
||||||
|
* @example
|
||||||
|
* const randomNum = fc.randomInt(1, 10) // 可能返回 5
|
||||||
|
*/
|
||||||
|
randomInt(min, max) {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 防抖函数
|
||||||
|
* @param {Function} fn - 要执行的函数
|
||||||
|
* @param {number} [delay=300] - 延迟时间(毫秒)
|
||||||
|
* @param {boolean} [immediate=false] - 是否立即执行
|
||||||
|
* @returns {Function} 防抖处理后的函数
|
||||||
|
* @description
|
||||||
|
* 1. immediate=true时:先立即执行,后续调用在delay时间内被忽略
|
||||||
|
* 2. immediate=false时:延迟执行,重复调用会重置计时器
|
||||||
|
* @example
|
||||||
|
* window.addEventListener('resize', fc.debounce(() => {
|
||||||
|
* console.log('resize end');
|
||||||
|
* }, 500));
|
||||||
|
*/
|
||||||
|
debounce(fn, delay = 300, immediate = false) {
|
||||||
|
let timer = null;
|
||||||
|
return function (...args) {
|
||||||
|
if (immediate && !timer) {
|
||||||
|
fn.apply(this, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
if (!immediate) {
|
||||||
|
fn.apply(this, args);
|
||||||
|
}
|
||||||
|
timer = null;
|
||||||
|
}, delay);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步重试机制
|
||||||
|
* @param {Function} asyncFn - 返回Promise的异步函数
|
||||||
|
* @param {number} [maxRetries=3] - 最大重试次数
|
||||||
|
* @param {number} [delay=1000] - 重试间隔(毫秒)
|
||||||
|
* @param {Function} [retryCondition] - 重试条件函数(err => boolean)
|
||||||
|
* @returns {Promise} 最终成功或失败的结果
|
||||||
|
* @example
|
||||||
|
* await fc.retry(fetchData, 5, 2000, err => err.status !== 404);
|
||||||
|
*/
|
||||||
|
async retry(asyncFn, maxRetries = 3, delay = 1000, retryCondition = () => true) {
|
||||||
|
let attempt = 0;
|
||||||
|
let lastError;
|
||||||
|
|
||||||
|
while (attempt <= maxRetries) {
|
||||||
|
try {
|
||||||
|
return await asyncFn();
|
||||||
|
} catch (err) {
|
||||||
|
lastError = err;
|
||||||
|
if (attempt === maxRetries || !retryCondition(err)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
attempt++;
|
||||||
|
await this.sleep(delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将对象转换为URL查询字符串
|
||||||
|
* @param {object} params - 参数对象
|
||||||
|
* @param {boolean} [encode=true] - 是否进行URL编码
|
||||||
|
* @returns {string} 查询字符串(不带问号)
|
||||||
|
* @example
|
||||||
|
* fc.objectToQuery({a: 1, b: 'test'}) // "a=1&b=test"
|
||||||
|
*/
|
||||||
|
objectToQuery(params, encode = true) {
|
||||||
|
return Object.entries(params)
|
||||||
|
.map(([key, val]) => {
|
||||||
|
const value = val === null || val === undefined ? '' : val;
|
||||||
|
return `${key}=${encode ? encodeURIComponent(value) : value}`;
|
||||||
|
})
|
||||||
|
.join('&');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从错误堆栈中提取简洁的错误信息
|
||||||
|
* @param {Error} error - 错误对象
|
||||||
|
* @param {number} [depth=3] - 保留的堆栈深度
|
||||||
|
* @returns {string} 格式化后的错误信息
|
||||||
|
* @example
|
||||||
|
* try { ... } catch(err) {
|
||||||
|
* logger.error(fc.formatError(err));
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
formatError(error, depth = 3) {
|
||||||
|
if (!(error instanceof Error)) return String(error);
|
||||||
|
|
||||||
|
const stack = error.stack?.split('\n') || [];
|
||||||
|
const message = `${error.name}: ${error.message}`;
|
||||||
|
|
||||||
|
if (stack.length <= 1) return message;
|
||||||
|
|
||||||
|
return [message, ...stack.slice(1, depth + 1).map((line) => line.trim())].join('\n at ');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default tools;
|
||||||
61
config/config.md
Normal file
61
config/config.md
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
### 参考以下注释进行配置,不要带注释粘贴,也不要改动`default.json`
|
||||||
|
|
||||||
|
配置文件位于`yunzai`根目录`/data/crystelf下`
|
||||||
|
|
||||||
|
参考注释:
|
||||||
|
|
||||||
|
``` yaml
|
||||||
|
{
|
||||||
|
"debug": true,\\是否启用调试模式
|
||||||
|
"core": true,\\是否启用晶灵核心相关功能
|
||||||
|
"coreConfig": { //晶灵核心配置
|
||||||
|
"coreUrl": "", //核心网址,需要加https://前缀
|
||||||
|
"wsUrl": "", //ws连接地址如ws://
|
||||||
|
"wsClientId": "",//端id
|
||||||
|
"wsSecret": "", wsmiy
|
||||||
|
"wsReConnectInterval": "5000",
|
||||||
|
"token": ""//postAPI调用密钥
|
||||||
|
},
|
||||||
|
"maxFeed": 10,//最大缓存rss流
|
||||||
|
"feeds": [//rss相关配置,无需手动更改
|
||||||
|
{
|
||||||
|
"url": "",
|
||||||
|
"targetGroups": [114,154],
|
||||||
|
"screenshot": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fanqieConfig": {//番茄小说功能
|
||||||
|
"url": "http://127.0.0.1:6868",
|
||||||
|
"outDir": "/home/user/debian/cache/Downloads"
|
||||||
|
},
|
||||||
|
"poke": {//戳一戳概率,加起来不超过1,余下的概率为反击概率
|
||||||
|
"replyText": 0.4,
|
||||||
|
"replyVoice": 0.2,
|
||||||
|
"mutePick": 0.1,
|
||||||
|
""muteTime": 2"
|
||||||
|
},
|
||||||
|
"mode": "deepseek",//deepseekORopenai
|
||||||
|
"modelType": "deepseek-ai/DeepSeek-V3",//无需更改
|
||||||
|
"historyLength": 3,
|
||||||
|
"maxLength": 3,
|
||||||
|
"chatTemperature": 1,
|
||||||
|
"pluginTemperature": 0.5,
|
||||||
|
"nickName": "寄气人",//昵称
|
||||||
|
"checkChat": {
|
||||||
|
"rdNum": 2,//随机数,0-100
|
||||||
|
"masterReply": true,//主人回复
|
||||||
|
"userId": [ //一定回复的人
|
||||||
|
114514
|
||||||
|
],
|
||||||
|
"blackGroups": [//不许使用的群聊
|
||||||
|
114,
|
||||||
|
514
|
||||||
|
],
|
||||||
|
"enableGroups": [//一定回复的群聊
|
||||||
|
11115
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"maxMessageLength": 100//最大上下文
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
11
config/default.json
Normal file
11
config/default.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"debug": true,
|
||||||
|
"core": true,
|
||||||
|
"coreConfig": {
|
||||||
|
"coreUrl": "",
|
||||||
|
"wsClientId": "",
|
||||||
|
"wsSecret": "",
|
||||||
|
"wsReConnectInterval": "5000",
|
||||||
|
"token": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
29
constants/path.js
Normal file
29
constants/path.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import url from 'url';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const __filename = url.fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const rootDir = path.join(__dirname, '..');
|
||||||
|
|
||||||
|
const Path = {
|
||||||
|
root: rootDir,
|
||||||
|
apps: path.join(rootDir, 'apps'),
|
||||||
|
components: path.join(rootDir, 'components'),
|
||||||
|
defaultConfig: path.join(rootDir, 'config/default.json'),
|
||||||
|
config: path.resolve(rootDir, '../../data/crystelf-admin'),
|
||||||
|
constants: path.join(rootDir, 'constants'),
|
||||||
|
lib: path.join(rootDir, 'lib'),
|
||||||
|
models: path.join(rootDir, 'models'),
|
||||||
|
index: path.join(rootDir, 'index.js'),
|
||||||
|
pkg: path.join(rootDir, 'package.json'),
|
||||||
|
yunzai: path.join(rootDir, '../../'),
|
||||||
|
data: path.join(rootDir, '../../data/crystelf-admin/data'),
|
||||||
|
rssHTML: path.join(rootDir, 'constants/rss/rss_template.html'),
|
||||||
|
rssCache: path.join(rootDir, '../../data/crystelf-admin'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const configFile = fs.readFileSync(Path.defaultConfig, 'utf8');
|
||||||
|
export const defaultConfig = JSON.parse(configFile);
|
||||||
|
|
||||||
|
export default Path;
|
||||||
6
constants/relativelyPath.js
Normal file
6
constants/relativelyPath.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
const relativelyPath = {
|
||||||
|
config: '/data/crystelf/config.json',
|
||||||
|
data: '/data/crystelf/data/',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default relativelyPath;
|
||||||
37
index.js
Normal file
37
index.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import chalk from 'chalk';
|
||||||
|
import Version from './lib/system/version.js';
|
||||||
|
import fc from './components/json.js';
|
||||||
|
import Path from './constants/path.js';
|
||||||
|
import { crystelfInit } from './lib/system/init.js';
|
||||||
|
import updater from './lib/system/updater.js';
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
chalk.rgb(134, 142, 204)(`crystelf-admin ${Version.ver} 初始化 ~ by ${Version.author}`)
|
||||||
|
);
|
||||||
|
|
||||||
|
updater.checkAndUpdate().catch((err) => {
|
||||||
|
logger.err(err);
|
||||||
|
});
|
||||||
|
await crystelfInit.CSH();
|
||||||
|
|
||||||
|
const appPath = Path.apps;
|
||||||
|
const jsFiles = fc.readDirRecursive(appPath, 'js');
|
||||||
|
|
||||||
|
let ret = jsFiles.map((file) => {
|
||||||
|
return import(`./apps/${file}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
ret = await Promise.allSettled(ret);
|
||||||
|
|
||||||
|
let apps = {};
|
||||||
|
for (let i in jsFiles) {
|
||||||
|
let name = jsFiles[i].replace('.js', '');
|
||||||
|
|
||||||
|
if (ret[i].status !== 'fulfilled') {
|
||||||
|
logger.error(name, ret[i].reason);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
apps[name] = ret[i].value[Object.keys(ret[i].value)[0]];
|
||||||
|
}
|
||||||
|
|
||||||
|
export { apps };
|
||||||
74
lib/config/configControl.js
Normal file
74
lib/config/configControl.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import Path, { defaultConfig } from '../../constants/path.js';
|
||||||
|
import fc from '../../components/json.js';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import relativelyPath from '../../constants/relativelyPath.js';
|
||||||
|
|
||||||
|
const configPath = Path.config;
|
||||||
|
const dataPath = Path.data;
|
||||||
|
const configFile = path.join(configPath, 'config.json');
|
||||||
|
const configDir = relativelyPath.config;
|
||||||
|
|
||||||
|
let configCache = {};
|
||||||
|
let lastModified = 0;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(configPath)) {
|
||||||
|
fs.mkdirSync(configPath, { recursive: true });
|
||||||
|
fs.mkdirSync(dataPath, { recursive: true });
|
||||||
|
logger.mark(`crystelf 配置文件夹创建成功,位于 ${configPath}..`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(configFile)) {
|
||||||
|
fs.writeFileSync(configFile, JSON.stringify(defaultConfig, null, 4), 'utf8');
|
||||||
|
logger.mark('crystelf 配置文件创建成功..');
|
||||||
|
} else {
|
||||||
|
const cfgFile = fs.readFileSync(configFile, 'utf8');
|
||||||
|
const loadedConfig = JSON.parse(cfgFile);
|
||||||
|
const cfg = { ...defaultConfig, ...loadedConfig };
|
||||||
|
|
||||||
|
if (JSON.stringify(cfg) !== JSON.stringify(loadedConfig)) {
|
||||||
|
fs.writeFileSync(configFile, JSON.stringify(cfg, null, 4), 'utf8');
|
||||||
|
logger.mark('crystelf 配置文件已更新,补充配置项..');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = fc.statSync(configDir, 'root');
|
||||||
|
configCache = fc.readJSON(configDir, 'root');
|
||||||
|
lastModified = stats.mtimeMs;
|
||||||
|
|
||||||
|
if (configCache.debug) {
|
||||||
|
logger.info('crystelf-plugin 配置模块初始化成功..');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn('crystelf-plugin 初始化配置失败,使用空配置..', err);
|
||||||
|
configCache = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const configControl = {
|
||||||
|
async init() {
|
||||||
|
init();
|
||||||
|
},
|
||||||
|
|
||||||
|
get(key) {
|
||||||
|
return key ? configCache[key] : configCache;
|
||||||
|
},
|
||||||
|
|
||||||
|
async set(key, value) {
|
||||||
|
configCache[key] = value;
|
||||||
|
return fc.safeWriteJSON(configDir, configCache, 'root', 4);
|
||||||
|
},
|
||||||
|
|
||||||
|
async save() {
|
||||||
|
return fc.safeWriteJSON(configDir, configCache, 'root', 4);
|
||||||
|
},
|
||||||
|
|
||||||
|
async reload() {
|
||||||
|
await init();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default configControl;
|
||||||
98
lib/core/botControl.js
Normal file
98
lib/core/botControl.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import wsClient from '../../modules/ws/wsClient.js';
|
||||||
|
import configControl from '../config/configControl.js';
|
||||||
|
|
||||||
|
const botControl = {
|
||||||
|
/**
|
||||||
|
* 获取全部bot信息并同步到core
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async reportBots() {
|
||||||
|
const bots = [{ client: configControl.get('coreConfig').wsClientId }];
|
||||||
|
|
||||||
|
for (const bot of Object.values(Bot)) {
|
||||||
|
if (!bot || !bot.uin) continue;
|
||||||
|
|
||||||
|
const botInfo = {
|
||||||
|
uin: bot.uin,
|
||||||
|
nickName: bot.nickname.replace(/[\u200E-\u200F\u202A-\u202E\u2066-\u2069]/g, ''),
|
||||||
|
groups: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
let groupsMap = bot.gl;
|
||||||
|
if (groupsMap) {
|
||||||
|
for (const [groupId, groupInfo] of groupsMap) {
|
||||||
|
botInfo.groups.push({
|
||||||
|
group_id: groupId,
|
||||||
|
group_name: groupInfo.group_name || '未知',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bots.push(botInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
type: 'reportBots',
|
||||||
|
data: bots,
|
||||||
|
};
|
||||||
|
|
||||||
|
return await wsClient.sendMessage(message);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取群聊信息
|
||||||
|
* @param botId
|
||||||
|
* @param groupId
|
||||||
|
* @returns {Promise<*|null>}
|
||||||
|
*/
|
||||||
|
async getGroupInfo(botId, groupId) {
|
||||||
|
const bot = Bot[botId];
|
||||||
|
if (!bot) {
|
||||||
|
logger.warn(`未找到bot: ${botId}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = bot.pickGroup(groupId);
|
||||||
|
if (!group) {
|
||||||
|
logger.warn(`Bot ${botId}中未找到群${groupId}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await group.getInfo();
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`获取群聊信息失败:${groupId}..`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送信息到群
|
||||||
|
* @param botId bot账号
|
||||||
|
* @param message 发送的信息
|
||||||
|
* @param groupId 群号
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async sendMessage(botId, message, groupId) {
|
||||||
|
const bot = Bot[botId];
|
||||||
|
if (!bot) {
|
||||||
|
logger.warn(`未找到bot: ${botId}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = bot.pickGroup(groupId);
|
||||||
|
if (!group) {
|
||||||
|
logger.warn(`Bot ${botId}中未找到群${groupId}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return !!(await group.send(message));
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`发送群信息失败:${groupId}..`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default botControl;
|
||||||
20
lib/core/systemControl.js
Normal file
20
lib/core/systemControl.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import configControl from '../config/configControl.js';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
let systemControl = {
|
||||||
|
async systemRestart() {
|
||||||
|
const token = configControl.get('coreConfig')?.token;
|
||||||
|
const coreUrl = configControl.get('coreConfig')?.coreUrl;
|
||||||
|
const postUrl = coreUrl + '/api/system/restart';
|
||||||
|
//logger.info(returnData);
|
||||||
|
return await axios.post(postUrl, { token: token });
|
||||||
|
},
|
||||||
|
|
||||||
|
async getRestartTime() {
|
||||||
|
const token = configControl.get('coreConfig')?.token;
|
||||||
|
const coreUrl = configControl.get('coreConfig')?.coreUrl;
|
||||||
|
const postUrl = coreUrl + '/api/system/getRestartTime';
|
||||||
|
return axios.post(postUrl, { token: token });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default systemControl;
|
||||||
12
lib/system/init.js
Normal file
12
lib/system/init.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import configControl from '../config/configControl.js';
|
||||||
|
import wsClient from '../../modules/ws/wsClient.js';
|
||||||
|
|
||||||
|
export const crystelfInit = {
|
||||||
|
async CSH() {
|
||||||
|
await configControl.init();
|
||||||
|
if (configControl.get('core')) {
|
||||||
|
await wsClient.initialize();
|
||||||
|
}
|
||||||
|
logger.mark('crystelf-admin 完成初始化');
|
||||||
|
},
|
||||||
|
};
|
||||||
79
lib/system/updater.js
Normal file
79
lib/system/updater.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import child_process from 'child_process';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import Path from '../../constants/path.js';
|
||||||
|
|
||||||
|
const GIT_DIR = path.join(Path.root, '.git');
|
||||||
|
|
||||||
|
const execStr = (cmd) => child_process.execSync(cmd, { cwd: Path.root }).toString().trim();
|
||||||
|
|
||||||
|
const Updater = {
|
||||||
|
isGitRepo() {
|
||||||
|
return fs.existsSync(GIT_DIR);
|
||||||
|
},
|
||||||
|
|
||||||
|
getBranch() {
|
||||||
|
return execStr('git symbolic-ref --short HEAD');
|
||||||
|
},
|
||||||
|
|
||||||
|
getLocalHash() {
|
||||||
|
return execStr('git rev-parse HEAD');
|
||||||
|
},
|
||||||
|
|
||||||
|
getRemoteHash(branch = 'main') {
|
||||||
|
return execStr(`git rev-parse origin/${branch}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async hasUpdate() {
|
||||||
|
try {
|
||||||
|
const branch = this.getBranch();
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
child_process.exec('git fetch', { cwd: Path.root }, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const local = this.getLocalHash();
|
||||||
|
const remote = this.getRemoteHash(branch);
|
||||||
|
|
||||||
|
return local !== remote;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('[crystelf-plugin] 检查更新失败:', err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async update() {
|
||||||
|
logger.mark(chalk.cyan('[crystelf-plugin] 检测到插件有更新,自动执行 git pull'));
|
||||||
|
child_process.execSync('git pull', {
|
||||||
|
cwd: Path.root,
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
logger.mark(chalk.green('[crystelf-plugin] 插件已自动更新完成'));
|
||||||
|
},
|
||||||
|
|
||||||
|
async checkAndUpdate() {
|
||||||
|
if (!this.isGitRepo()) {
|
||||||
|
logger.warn('[crystelf-plugin] 当前目录不是 Git 仓库,自动更新功能已禁用');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (await this.hasUpdate()) {
|
||||||
|
await this.update();
|
||||||
|
} else {
|
||||||
|
logger.info('[crystelf-plugin] 当前已是最新版本,无需更新');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('[crystelf-plugin] 自动更新失败:', err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Updater;
|
||||||
26
lib/system/version.js
Normal file
26
lib/system/version.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import url from 'url';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const __filename = url.fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const pkgPath = path.join(__dirname, '../..', 'package.json');
|
||||||
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
||||||
|
|
||||||
|
const Version = {
|
||||||
|
get ver() {
|
||||||
|
return pkg.version;
|
||||||
|
},
|
||||||
|
get author() {
|
||||||
|
return pkg.author;
|
||||||
|
},
|
||||||
|
get name() {
|
||||||
|
return pkg.name;
|
||||||
|
},
|
||||||
|
get description() {
|
||||||
|
return pkg.description;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Version;
|
||||||
95
modules/ws/handler.js
Normal file
95
modules/ws/handler.js
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import botControl from '../../lib/core/botControl.js';
|
||||||
|
import wsClient from './wsClient.js';
|
||||||
|
|
||||||
|
class Handler {
|
||||||
|
constructor() {
|
||||||
|
this.handlers = new Map([
|
||||||
|
['auth', this.handleAuth.bind(this)],
|
||||||
|
['ping', this.handlePing.bind(this)],
|
||||||
|
['message', this.handleMessageFromServer.bind(this)],
|
||||||
|
['error', this.handleError.bind(this)],
|
||||||
|
['getGroupInfo', this.handleGetGroupInfo.bind(this)],
|
||||||
|
['sendMessage', this.handleSendMessage.bind(this)],
|
||||||
|
['reportBots', this.reportBots.bind(this)],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(client, msg) {
|
||||||
|
const handler = this.handlers.get(msg.type);
|
||||||
|
if (handler) {
|
||||||
|
await handler(client, msg);
|
||||||
|
} else {
|
||||||
|
logger.warn(`未知消息类型: ${msg.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleAuth(client, msg) {
|
||||||
|
if (msg.success) {
|
||||||
|
logger.mark('crystelf WS 认证成功..');
|
||||||
|
} else {
|
||||||
|
logger.error('crystelf WS 认证失败,关闭连接..');
|
||||||
|
client.ws.close(4001, '认证失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handlePing(client, msg) {
|
||||||
|
await client.sendMessage({ type: 'pong' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleMessageFromServer(client, msg) {
|
||||||
|
logger.mark(`crystelf 服务端消息: ${msg.data}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleError(client, msg) {
|
||||||
|
logger.warn(`crystelf WS 错误:${msg.data}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
获取群聊信息,自动回调
|
||||||
|
@examples 请求示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
requestId: 114514,
|
||||||
|
type: 'getGroupInfo',
|
||||||
|
data: {
|
||||||
|
botId: 114514,
|
||||||
|
groupId: 114514,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**/
|
||||||
|
async handleGetGroupInfo(client, msg) {
|
||||||
|
const requestId = msg?.requestId;
|
||||||
|
const botId = msg.data?.botId;
|
||||||
|
const groupId = msg.data?.groupId;
|
||||||
|
const type = msg.type + 'Return';
|
||||||
|
const groupData = await botControl.getGroupInfo(botId, groupId);
|
||||||
|
const returnData = {
|
||||||
|
type: type,
|
||||||
|
requestId: requestId,
|
||||||
|
data: groupData,
|
||||||
|
};
|
||||||
|
await wsClient.sendMessage(returnData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送信息到群聊
|
||||||
|
* @param client
|
||||||
|
* @param msg
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
// TODO 测试可用性
|
||||||
|
async handleSendMessage(client, msg) {
|
||||||
|
const botId = Number(msg.data?.botId);
|
||||||
|
const groupId = Number(msg.data?.groupId);
|
||||||
|
const message = msg.data?.message?.toString();
|
||||||
|
await botControl.sendMessage(botId, message, groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async reportBots(client, msg) {
|
||||||
|
await botControl.reportBots();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = new Handler();
|
||||||
|
export default handler;
|
||||||
95
modules/ws/wsClient.js
Normal file
95
modules/ws/wsClient.js
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import WebSocket from 'ws';
|
||||||
|
import configControl from '../../lib/config/configControl.js';
|
||||||
|
import handler from './handler.js';
|
||||||
|
|
||||||
|
class WsClient {
|
||||||
|
constructor() {
|
||||||
|
this.ws = null;
|
||||||
|
this.wsURL = null;
|
||||||
|
this.secret = null;
|
||||||
|
this.clientId = null;
|
||||||
|
this.reconnectInterval = null;
|
||||||
|
this.isReconnecting = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
try {
|
||||||
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
logger.mark('crystelf WS 客户端已连接..');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.wsURL = configControl.get('coreConfig')?.wsUrl;
|
||||||
|
this.secret = configControl.get('coreConfig')?.wsSecret;
|
||||||
|
this.clientId = configControl.get('coreConfig')?.wsClientId;
|
||||||
|
this.reconnectInterval = configControl.get('coreConfig')?.wsReConnectInterval;
|
||||||
|
|
||||||
|
//logger.info(this.wsURL);
|
||||||
|
this.ws = new WebSocket(this.wsURL);
|
||||||
|
|
||||||
|
this.ws.on('open', () => {
|
||||||
|
logger.mark('crystelf WS 客户端连接成功..');
|
||||||
|
this.authenticate();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.on('message', (raw) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(raw);
|
||||||
|
handler.handle(this, data);
|
||||||
|
} catch (err) {
|
||||||
|
logger.err(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.on('error', (err) => {
|
||||||
|
logger.error('WS 连接错误:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.on('close', (code, reason) => {
|
||||||
|
logger.warn(`crystelf WS 客户端连接断开:${code} - ${reason}`);
|
||||||
|
this.reconnect();
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async authenticate() {
|
||||||
|
const authMsg = {
|
||||||
|
type: 'auth',
|
||||||
|
secret: this.secret,
|
||||||
|
clientId: this.clientId,
|
||||||
|
};
|
||||||
|
await this.sendMessage(authMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送信息到ws服务端,自动格式化
|
||||||
|
* @param msg
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async sendMessage(msg) {
|
||||||
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||||
|
this.ws.send(JSON.stringify(msg));
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
logger.warn('crystelf WS 服务器未连接,无法发送消息..');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async reconnect() {
|
||||||
|
if (this.isReconnecting) return;
|
||||||
|
this.isReconnecting = true;
|
||||||
|
|
||||||
|
logger.mark('crystelf WS 客户端尝试重连..');
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isReconnecting = false;
|
||||||
|
this.initialize();
|
||||||
|
}, this.reconnectInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wsClient = new WsClient();
|
||||||
|
|
||||||
|
export default wsClient;
|
||||||
30
package.json
Normal file
30
package.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "crystelf-admin",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "晶灵管理插件",
|
||||||
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": ""
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"TRSS-Yunzai",
|
||||||
|
"crystelf-admin"
|
||||||
|
],
|
||||||
|
"author": "Jerry",
|
||||||
|
"License": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.8.4",
|
||||||
|
"chalk": "^5.4.1",
|
||||||
|
"ws": "^8.18.1"
|
||||||
|
},
|
||||||
|
"imports": {},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.23.0",
|
||||||
|
"eslint": "^8.57.1",
|
||||||
|
"globals": "^16.0.0",
|
||||||
|
"prettier": "^3.5.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user