修结构

This commit is contained in:
Jerry 2025-07-07 21:48:42 +08:00
parent d821003899
commit d43b05bae5
18 changed files with 325 additions and 103 deletions

View File

@ -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
View 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
View 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
View 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;

View File

@ -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();

View File

@ -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);
});