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'),
|
||||
uploads: path.join(this.baseDir, 'public/files/uploads'),
|
||||
words: path.join(this.baseDir, 'private/data/word'),
|
||||
plugins: path.join(this.baseDir, 'plugins'),
|
||||
};
|
||||
|
||||
return type ? mappings[type] : this.baseDir;
|
||||
@ -69,6 +70,7 @@ class PathManager {
|
||||
this.get('temp'),
|
||||
this.get('uploads'),
|
||||
this.get('words'),
|
||||
this.get('plugins'),
|
||||
];
|
||||
|
||||
pathsToInit.forEach((dirPath) => {
|
||||
@ -92,6 +94,7 @@ type PathType =
|
||||
| 'media'
|
||||
| 'modules'
|
||||
| 'words'
|
||||
| 'plugins'
|
||||
| 'uploads';
|
||||
|
||||
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 config from './core/utils/system/config';
|
||||
import redis from './core/services/redis/redis';
|
||||
import autoUpdater from './core/utils/system/autoUpdater';
|
||||
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']);
|
||||
const PORT = config.get('PORT') || 3000;
|
||||
@ -11,21 +11,33 @@ const PORT = config.get('PORT') || 3000;
|
||||
apps
|
||||
.createApp()
|
||||
.then(async (app) => {
|
||||
app.listen(PORT, () => {
|
||||
const server = app.listen(PORT, () => {
|
||||
logger.info(`Crystelf-core listening on ${PORT}`);
|
||||
});
|
||||
|
||||
const pluginLoader = new PluginLoader(app, server);
|
||||
await pluginLoader.loadPlugins();
|
||||
const isUpdated = await autoUpdater.checkForUpdates();
|
||||
if (isUpdated) {
|
||||
logger.warn(`检测到更新,正在重启..`);
|
||||
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) => {
|
||||
logger.error('Crystelf-core启动失败:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await redis.disconnect();
|
||||
process.exit(0);
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user