mirror of
https://github.com/crystelf/crystelf-core.git
synced 2025-10-14 05:19:19 +00:00
修结构
This commit is contained in:
parent
d821003899
commit
d43b05bae5
@ -1,95 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import compression from 'compression';
|
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import logger from './utils/system/logger';
|
|
||||||
import paths from './utils/system/path';
|
|
||||||
import config from './utils/system/config';
|
|
||||||
import './services/ws/wsServer';
|
|
||||||
import System from './utils/system/system';
|
|
||||||
|
|
||||||
const apps = {
|
|
||||||
async createApp() {
|
|
||||||
const app = express();
|
|
||||||
paths.init();
|
|
||||||
logger.info('晶灵核心初始化..');
|
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
const contentType = req.headers['content-type'] || '';
|
|
||||||
if (contentType.includes('multipart/form-data')) {
|
|
||||||
logger.debug('检测到form-data数据流,跳过加载 express.json() 中间件..');
|
|
||||||
next();
|
|
||||||
} else {
|
|
||||||
express.json()(req, res, next);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
app.use(compression());
|
|
||||||
logger.debug('成功加载 express.json() 中间件..');
|
|
||||||
|
|
||||||
const publicPath = paths.get('public');
|
|
||||||
app.use('/public', express.static(publicPath));
|
|
||||||
logger.debug(`静态资源路由挂载: /public => ${publicPath}`);
|
|
||||||
|
|
||||||
const modulesDir = path.resolve(__dirname, './modules');
|
|
||||||
const controllerPattern = /\.controller\.[jt]s$/;
|
|
||||||
|
|
||||||
if (!fs.existsSync(modulesDir)) {
|
|
||||||
logger.warn(`未找到模块目录: ${modulesDir}`);
|
|
||||||
} else {
|
|
||||||
const moduleFolders = fs.readdirSync(modulesDir).filter((folder) => {
|
|
||||||
const fullPath = path.join(modulesDir, folder);
|
|
||||||
return fs.statSync(fullPath).isDirectory();
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const folder of moduleFolders) {
|
|
||||||
const folderPath = path.join(modulesDir, folder);
|
|
||||||
const files = fs.readdirSync(folderPath).filter((f) => controllerPattern.test(f));
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const filePath = path.join(folderPath, file);
|
|
||||||
|
|
||||||
try {
|
|
||||||
//logger.debug(`尝试加载模块: ${filePath}`);
|
|
||||||
const controllerModule = require(filePath);
|
|
||||||
const controller = controllerModule.default;
|
|
||||||
|
|
||||||
if (controller?.getRouter) {
|
|
||||||
const isPublic = folder === 'public';
|
|
||||||
const routePath = isPublic ? `/${folder}` : `/api/${folder}`;
|
|
||||||
app.use(routePath, controller.getRouter());
|
|
||||||
logger.debug(`模块路由挂载: ${routePath.padEnd(12)} => ${file}`);
|
|
||||||
|
|
||||||
if (config.get('DEBUG', false)) {
|
|
||||||
controller.getRouter().stack.forEach((layer: any) => {
|
|
||||||
if (layer.route) {
|
|
||||||
const methods = Object.keys(layer.route.methods || {})
|
|
||||||
.map((m) => m.toUpperCase())
|
|
||||||
.join(',');
|
|
||||||
logger.debug(` ↳ ${methods.padEnd(6)} ${routePath}${layer.route.path}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.warn(`模块 ${file} 没有导出 getRouter 方法,跳过..`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`模块 ${file} 加载失败:`, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const duration = System.checkRestartTime();
|
|
||||||
//logger.info(duration);
|
|
||||||
if (duration) {
|
|
||||||
logger.warn(`重启完成!耗时 ${duration} 秒..`);
|
|
||||||
const restartTimePath = path.join(paths.get('temp'), 'restart_time');
|
|
||||||
fs.writeFileSync(restartTimePath, duration.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('晶灵核心初始化完毕!');
|
|
||||||
return app;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default apps;
|
|
39
src/core/app/app.ts
Normal file
39
src/core/app/app.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import compression from 'compression';
|
||||||
|
import logger from '../utils/system/logger';
|
||||||
|
import paths from '../utils/system/path';
|
||||||
|
import System from '../utils/system/system';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const apps = {
|
||||||
|
async createApp() {
|
||||||
|
const app = express();
|
||||||
|
paths.init();
|
||||||
|
logger.info('晶灵核心初始化..');
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
const contentType = req.headers['content-type'] || '';
|
||||||
|
if (contentType.includes('multipart/form-data')) {
|
||||||
|
logger.debug('检测到form-data数据流,跳过加载 express.json() 中间件..');
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
express.json()(req, res, next);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
app.use(compression());
|
||||||
|
logger.debug('成功加载 express.json() 中间件..');
|
||||||
|
const publicPath = paths.get('public');
|
||||||
|
app.use('/public', express.static(publicPath));
|
||||||
|
logger.debug(`静态资源路由挂载: /public => ${publicPath}`);
|
||||||
|
const duration = System.checkRestartTime();
|
||||||
|
if (duration) {
|
||||||
|
logger.warn(`重启完成!耗时 ${duration} 秒..`);
|
||||||
|
const restartTimePath = path.join(paths.get('temp'), 'restart_time');
|
||||||
|
require('fs').writeFileSync(restartTimePath, duration.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('晶灵核心初始化完毕!');
|
||||||
|
return app;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default apps;
|
241
src/core/app/loader.ts
Normal file
241
src/core/app/loader.ts
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import simpleGit, { SimpleGit } from 'simple-git';
|
||||||
|
import { Application } from 'express';
|
||||||
|
import { Server } from 'http';
|
||||||
|
import Plugin from '../types/plugin';
|
||||||
|
import logger from '../utils/system/logger';
|
||||||
|
import paths from '../utils/system/path';
|
||||||
|
|
||||||
|
class PluginLoader {
|
||||||
|
private readonly pluginsDir: string;
|
||||||
|
private loadedPlugins: Map<string, Plugin> = new Map();
|
||||||
|
private gitInstances: Map<string, SimpleGit> = new Map();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private app: Application,
|
||||||
|
private server?: Server
|
||||||
|
) {
|
||||||
|
this.pluginsDir = paths.get('plugins');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async loadPlugins(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const pluginFolders = await fs.readdir(this.pluginsDir);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
pluginFolders.map(async (folder) => {
|
||||||
|
const pluginPath = path.join(this.pluginsDir, folder);
|
||||||
|
const stat = await fs.stat(pluginPath);
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
await this.loadPlugin(pluginPath);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.invokePluginHooks('onReady');
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('加载插件时出错:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadPlugin(pluginPath: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const pluginName = path.basename(pluginPath);
|
||||||
|
const indexPath = path.join(pluginPath, 'index.js');
|
||||||
|
|
||||||
|
if (!(await this.fileExists(indexPath))) {
|
||||||
|
logger.warn(`插件 ${pluginName} 缺少 index.js 入口文件`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await this.checkPluginUpdates(pluginPath)) {
|
||||||
|
logger.info(`插件 ${pluginName} 有可用更新,正在更新..`);
|
||||||
|
await this.updatePlugin(pluginPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginModule = await import(indexPath);
|
||||||
|
const plugin: Plugin = pluginModule.default || pluginModule;
|
||||||
|
|
||||||
|
if (!plugin.name || !plugin.version) {
|
||||||
|
logger.warn(`插件 ${pluginName} 缺少必要信息 (name/version)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin.initialize) {
|
||||||
|
await plugin.initialize(this.app, this.server);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin.routes) {
|
||||||
|
plugin.routes(this.app);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadedPlugins.set(plugin.name, plugin);
|
||||||
|
logger.info(`插件加载成功: ${plugin.name}@${plugin.version}`);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`加载插件 ${path.basename(pluginPath)} 失败:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkPluginUpdates(pluginPath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const git = simpleGit(pluginPath);
|
||||||
|
this.gitInstances.set(pluginPath, git);
|
||||||
|
|
||||||
|
const isRepo = await git.checkIsRepo();
|
||||||
|
if (!isRepo) return false;
|
||||||
|
|
||||||
|
const pluginConfig = await this.getPluginConfig(pluginPath);
|
||||||
|
if (!pluginConfig.autoUpdateEnabled) return false;
|
||||||
|
|
||||||
|
await git.fetch();
|
||||||
|
const status = await git.status();
|
||||||
|
|
||||||
|
return status.behind > 0;
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(`检查插件更新失败: ${path.basename(pluginPath)}`, err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updatePlugin(pluginPath: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const git = this.gitInstances.get(pluginPath);
|
||||||
|
if (!git) return;
|
||||||
|
|
||||||
|
const status = await git.status();
|
||||||
|
if (status.current) {
|
||||||
|
await git.pull('origin', status.current);
|
||||||
|
logger.info(`插件 ${path.basename(pluginPath)} 更新成功`);
|
||||||
|
|
||||||
|
await this.loadPlugin(pluginPath);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`更新插件 ${path.basename(pluginPath)} 失败:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async closePlugins(): Promise<void> {
|
||||||
|
await this.invokePluginHooks('onClose');
|
||||||
|
this.loadedPlugins.clear();
|
||||||
|
this.gitInstances.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async invokePluginHooks(hookName: 'onReady' | 'onClose'): Promise<void> {
|
||||||
|
for (const [name, plugin] of this.loadedPlugins) {
|
||||||
|
try {
|
||||||
|
if (plugin[hookName]) {
|
||||||
|
await plugin[hookName]!();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`执行插件 ${name} 的 ${hookName} 钩子失败:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查目录是否存在
|
||||||
|
* @param path 目录路径
|
||||||
|
*/
|
||||||
|
private async dirExists(path: string): Promise<boolean | undefined> {
|
||||||
|
try {
|
||||||
|
const stat = await fs.stat(path);
|
||||||
|
return stat.isDirectory();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查文件是否存在
|
||||||
|
* @param path 文件路径
|
||||||
|
*/
|
||||||
|
private async fileExists(path: string): Promise<boolean | undefined> {
|
||||||
|
try {
|
||||||
|
const stat = await fs.stat(path);
|
||||||
|
return stat.isFile();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取插件配置
|
||||||
|
* @param pluginPath 插件路径
|
||||||
|
*/
|
||||||
|
private async getPluginConfig(pluginPath: string): Promise<{
|
||||||
|
autoUpdateEnabled: boolean;
|
||||||
|
[key: string]: any;
|
||||||
|
}> {
|
||||||
|
const packagePath = path.join(pluginPath, 'package.json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!(await this.fileExists(packagePath))) {
|
||||||
|
return { autoUpdateEnabled: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const pkg = JSON.parse(await fs.readFile(packagePath, 'utf-8'));
|
||||||
|
return {
|
||||||
|
autoUpdateEnabled: pkg?.crystelf?.autoUpdate ?? false,
|
||||||
|
...pkg,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(`读取插件配置失败: ${path.basename(pluginPath)}`, err);
|
||||||
|
return { autoUpdateEnabled: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取插件元数据
|
||||||
|
* @param pluginPath 插件路径
|
||||||
|
*/
|
||||||
|
private async getPluginMetadata(pluginPath: string): Promise<{
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
description?: string;
|
||||||
|
} | null> {
|
||||||
|
const packagePath = path.join(pluginPath, 'package.json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!(await this.fileExists(packagePath))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pkg = JSON.parse(await fs.readFile(packagePath, 'utf-8'));
|
||||||
|
return {
|
||||||
|
name: pkg.name,
|
||||||
|
version: pkg.version,
|
||||||
|
description: pkg.description,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(`读取插件元数据失败: ${path.basename(pluginPath)}`, err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证插件名称是否符合规范
|
||||||
|
* @param name 插件名称
|
||||||
|
*/
|
||||||
|
private isValidPluginName(name: string): boolean {
|
||||||
|
return /^[a-z][a-z0-9-]*$/.test(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有已加载插件信息
|
||||||
|
*/
|
||||||
|
public getLoadedPlugins(): Array<{
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
path: string;
|
||||||
|
}> {
|
||||||
|
return Array.from(this.loadedPlugins.entries()).map(([name, plugin]) => ({
|
||||||
|
name,
|
||||||
|
version: plugin.version,
|
||||||
|
path: path.join(this.pluginsDir, name),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PluginLoader;
|
22
src/core/types/plugin.ts
Normal file
22
src/core/types/plugin.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Application } from 'express';
|
||||||
|
import { Server } from 'http';
|
||||||
|
|
||||||
|
interface Plugin {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
description?: string;
|
||||||
|
// 初始化插件
|
||||||
|
initialize?: (app: Application, server?: Server) => void | Promise<void>;
|
||||||
|
// 路由挂载点
|
||||||
|
routes?: (app: Application) => void;
|
||||||
|
// 生命周期钩子
|
||||||
|
onReady?: () => void | Promise<void>;
|
||||||
|
onClose?: () => void | Promise<void>;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
// 自动更新相关
|
||||||
|
autoUpdateEnabled?: boolean;
|
||||||
|
checkForUpdates?: () => Promise<boolean>;
|
||||||
|
applyUpdate?: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Plugin;
|
@ -39,6 +39,7 @@ class PathManager {
|
|||||||
modules: path.join(this.baseDir, 'src/modules'),
|
modules: path.join(this.baseDir, 'src/modules'),
|
||||||
uploads: path.join(this.baseDir, 'public/files/uploads'),
|
uploads: path.join(this.baseDir, 'public/files/uploads'),
|
||||||
words: path.join(this.baseDir, 'private/data/word'),
|
words: path.join(this.baseDir, 'private/data/word'),
|
||||||
|
plugins: path.join(this.baseDir, 'plugins'),
|
||||||
};
|
};
|
||||||
|
|
||||||
return type ? mappings[type] : this.baseDir;
|
return type ? mappings[type] : this.baseDir;
|
||||||
@ -69,6 +70,7 @@ class PathManager {
|
|||||||
this.get('temp'),
|
this.get('temp'),
|
||||||
this.get('uploads'),
|
this.get('uploads'),
|
||||||
this.get('words'),
|
this.get('words'),
|
||||||
|
this.get('plugins'),
|
||||||
];
|
];
|
||||||
|
|
||||||
pathsToInit.forEach((dirPath) => {
|
pathsToInit.forEach((dirPath) => {
|
||||||
@ -92,6 +94,7 @@ type PathType =
|
|||||||
| 'media'
|
| 'media'
|
||||||
| 'modules'
|
| 'modules'
|
||||||
| 'words'
|
| 'words'
|
||||||
|
| 'plugins'
|
||||||
| 'uploads';
|
| 'uploads';
|
||||||
|
|
||||||
const paths = PathManager.getInstance();
|
const paths = PathManager.getInstance();
|
||||||
|
28
src/main.ts
28
src/main.ts
@ -1,9 +1,9 @@
|
|||||||
import apps from './core/app';
|
import apps from './core/app/app';
|
||||||
import logger from './core/utils/system/logger';
|
import logger from './core/utils/system/logger';
|
||||||
import config from './core/utils/system/config';
|
import config from './core/utils/system/config';
|
||||||
import redis from './core/services/redis/redis';
|
|
||||||
import autoUpdater from './core/utils/system/autoUpdater';
|
import autoUpdater from './core/utils/system/autoUpdater';
|
||||||
import System from './core/utils/system/system';
|
import System from './core/utils/system/system';
|
||||||
|
import PluginLoader from './core/app/loader';
|
||||||
|
|
||||||
config.check(['PORT', 'DEBUG', 'RD_PORT', 'RD_ADD', 'WS_SECRET', 'WS_PORT']);
|
config.check(['PORT', 'DEBUG', 'RD_PORT', 'RD_ADD', 'WS_SECRET', 'WS_PORT']);
|
||||||
const PORT = config.get('PORT') || 3000;
|
const PORT = config.get('PORT') || 3000;
|
||||||
@ -11,21 +11,33 @@ const PORT = config.get('PORT') || 3000;
|
|||||||
apps
|
apps
|
||||||
.createApp()
|
.createApp()
|
||||||
.then(async (app) => {
|
.then(async (app) => {
|
||||||
app.listen(PORT, () => {
|
const server = app.listen(PORT, () => {
|
||||||
logger.info(`Crystelf-core listening on ${PORT}`);
|
logger.info(`Crystelf-core listening on ${PORT}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pluginLoader = new PluginLoader(app, server);
|
||||||
|
await pluginLoader.loadPlugins();
|
||||||
const isUpdated = await autoUpdater.checkForUpdates();
|
const isUpdated = await autoUpdater.checkForUpdates();
|
||||||
if (isUpdated) {
|
if (isUpdated) {
|
||||||
logger.warn(`检测到更新,正在重启..`);
|
logger.warn(`检测到更新,正在重启..`);
|
||||||
await System.restart();
|
await System.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
process.on('SIGTERM', async () => {
|
||||||
|
logger.info('收到终止信号,正在关闭插件和服务..');
|
||||||
|
await pluginLoader.closePlugins();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('uncaughtException', (err) => {
|
||||||
|
logger.error('未捕获的异常:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
logger.error('未处理的Promise拒绝:', reason);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
logger.error('Crystelf-core启动失败:', err);
|
logger.error('Crystelf-core启动失败:', err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
|
||||||
await redis.disconnect();
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user