mirror of
https://github.com/crystelf/crystelf-core.git
synced 2025-10-14 05:19:19 +00:00
nest初始化
feat:配置模块 feat:路径模块
This commit is contained in:
parent
4846e27e6a
commit
4100b4f0aa
59
.gitignore
vendored
59
.gitignore
vendored
@ -1,15 +1,56 @@
|
||||
/tmp
|
||||
/out-tsc
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/node_modules
|
||||
/build
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
/.pnp
|
||||
.pnp.js
|
||||
lerna-debug.log*
|
||||
|
||||
.env
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Tests
|
||||
/coverage
|
||||
/.nyc_output
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
/dist/
|
||||
/logs/
|
||||
/private/
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# temp directory
|
||||
.temp
|
||||
.tmp
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
8
.idea/.gitignore
generated
vendored
8
.idea/.gitignore
generated
vendored
@ -1,8 +0,0 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
8
.idea/codeStyles/Project.xml
generated
8
.idea/codeStyles/Project.xml
generated
@ -26,7 +26,7 @@
|
||||
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
|
||||
</VueCodeStyleSettings>
|
||||
<codeStyleSettings language="HTML">
|
||||
<option name="SOFT_MARGINS" value="100" />
|
||||
<option name="SOFT_MARGINS" value="80" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
@ -34,7 +34,7 @@
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JavaScript">
|
||||
<option name="SOFT_MARGINS" value="100" />
|
||||
<option name="SOFT_MARGINS" value="80" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
@ -42,7 +42,7 @@
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="TypeScript">
|
||||
<option name="SOFT_MARGINS" value="100" />
|
||||
<option name="SOFT_MARGINS" value="80" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
@ -50,7 +50,7 @@
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="Vue">
|
||||
<option name="SOFT_MARGINS" value="100" />
|
||||
<option name="SOFT_MARGINS" value="80" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
|
14
.idea/crystelf-core.iml
generated
14
.idea/crystelf-core.iml
generated
@ -1,14 +0,0 @@
|
||||
<?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" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/logs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/private" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
6
.idea/inspectionProfiles/Project_Default.xml
generated
6
.idea/inspectionProfiles/Project_Default.xml
generated
@ -1,6 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
8
.idea/modules.xml
generated
8
.idea/modules.xml
generated
@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/crystelf-core.iml" filepath="$PROJECT_DIR$/.idea/crystelf-core.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
3
.idea/prettier.xml
generated
3
.idea/prettier.xml
generated
@ -1,8 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="PrettierConfiguration">
|
||||
<option name="myConfigurationMode" value="MANUAL" />
|
||||
<option name="myConfigurationMode" value="AUTOMATIC" />
|
||||
<option name="myRunOnSave" value="true" />
|
||||
<option name="myRunOnReformat" value="true" />
|
||||
</component>
|
||||
</project>
|
2
.idea/vcs.xml
generated
2
.idea/vcs.xml
generated
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
14
.idea/webResources.xml
generated
14
.idea/webResources.xml
generated
@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="WebResourcesPaths">
|
||||
<contentEntries>
|
||||
<entry url="file://$PROJECT_DIR$">
|
||||
<entryData>
|
||||
<resourceRoots>
|
||||
<path value="file://$PROJECT_DIR$/src" />
|
||||
</resourceRoots>
|
||||
</entryData>
|
||||
</entry>
|
||||
</contentEntries>
|
||||
</component>
|
||||
</project>
|
@ -1,7 +1,4 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
"trailingComma": "all"
|
||||
}
|
21
LICENSE
21
LICENSE
@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Crystelf
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
34
eslint.config.mjs
Normal file
34
eslint.config.mjs
Normal file
@ -0,0 +1,34 @@
|
||||
// @ts-check
|
||||
import eslint from '@eslint/js';
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ['eslint.config.mjs'],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
eslintPluginPrettierRecommended,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
sourceType: 'commonjs',
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'warn',
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn'
|
||||
},
|
||||
},
|
||||
);
|
8
nest-cli.json
Normal file
8
nest-cli.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
90
package.json
90
package.json
@ -1,33 +1,73 @@
|
||||
{
|
||||
"name": "crystelf-core",
|
||||
"version": "1.0.0",
|
||||
"name": "nest-backend",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"dev": "ts-node-dev src/main.ts",
|
||||
"start": "node dist/main.js",
|
||||
"build": "tsc"
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.8.4",
|
||||
"chalk": "4",
|
||||
"compression": "^1.8.0",
|
||||
"dotenv": "^16.0.0",
|
||||
"express": "^4.18.0",
|
||||
"ioredis": "^5.6.0",
|
||||
"mkdirp": "^3.0.1",
|
||||
"multer": "1.4.5-lts.2",
|
||||
"simple-git": "^3.27.0",
|
||||
"uuid": "^11.1.0",
|
||||
"ws": "^8.18.1"
|
||||
"@nestjs/axios": "^4.0.1",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/swagger": "^11.2.0",
|
||||
"axios": "^1.10.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"ssh2": "^1.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/compression": "^1.7.5",
|
||||
"@types/express": "^4.17.0",
|
||||
"@types/mkdirp": "^2.0.0",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/node": "^18.0.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"prettier": "^3.5.3",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@nestjs/cli": "^11.0.0",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.1",
|
||||
"@swc/cli": "^0.6.0",
|
||||
"@swc/core": "^1.10.7",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.16.4",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.2",
|
||||
"globals": "^16.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.20.0"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
|
7239
pnpm-lock.yaml
generated
7239
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
9
src/app.module.ts
Normal file
9
src/app.module.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { RootModule } from './root/root.module';
|
||||
import { AppConfigModule } from './config/config.module';
|
||||
import { PathModule } from './core/path/path.module';
|
||||
|
||||
@Module({
|
||||
imports: [RootModule, AppConfigModule, PathModule],
|
||||
})
|
||||
export class AppModule {}
|
95
src/app.ts
95
src/app.ts
@ -1,95 +0,0 @@
|
||||
import express from 'express';
|
||||
import compression from 'compression';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import logger from './utils/core/logger';
|
||||
import paths from './utils/core/path';
|
||||
import config from './utils/core/config';
|
||||
import './services/ws/wsServer';
|
||||
import System from './utils/core/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;
|
28
src/common/filters/all-exception.filter.ts
Normal file
28
src/common/filters/all-exception.filter.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import {
|
||||
ArgumentsHost,
|
||||
Catch,
|
||||
ExceptionFilter,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
|
||||
@Catch()
|
||||
export class AllExceptionsFilter implements ExceptionFilter {
|
||||
catch(exception: unknown, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse();
|
||||
const status =
|
||||
exception instanceof HttpException
|
||||
? exception.getStatus()
|
||||
: HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
const message =
|
||||
exception instanceof HttpException ? exception.message : '服务器内部错误';
|
||||
|
||||
response.status(status).json({
|
||||
success: false,
|
||||
data: null,
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
26
src/common/interceptors/response.interceptor.ts
Normal file
26
src/common/interceptors/response.interceptor.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import {
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
} from '@nestjs/common';
|
||||
import { Observable, map } from 'rxjs';
|
||||
import { ApiResponse } from '../response-format';
|
||||
|
||||
@Injectable()
|
||||
export class ResponseInterceptor<T>
|
||||
implements NestInterceptor<T, ApiResponse<T>>
|
||||
{
|
||||
intercept(
|
||||
context: ExecutionContext,
|
||||
next: CallHandler,
|
||||
): Observable<ApiResponse<T>> {
|
||||
return next.handle().pipe(
|
||||
map((data) => ({
|
||||
success: true,
|
||||
data,
|
||||
message: '操作成功',
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
5
src/common/response-format.ts
Normal file
5
src/common/response-format.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean;
|
||||
data: T;
|
||||
message: string;
|
||||
}
|
16
src/config/config.module.ts
Normal file
16
src/config/config.module.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule as NestConfigModule } from '@nestjs/config';
|
||||
import { AppConfigService } from './config.service';
|
||||
import * as path from 'node:path';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
NestConfigModule.forRoot({
|
||||
envFilePath: path.resolve(__dirname, '../../.env'),
|
||||
isGlobal: true,
|
||||
}),
|
||||
],
|
||||
providers: [AppConfigService],
|
||||
exports: [AppConfigService],
|
||||
})
|
||||
export class AppConfigModule {}
|
47
src/config/config.service.ts
Normal file
47
src/config/config.service.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { ConfigService as NestConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class AppConfigService implements OnModuleInit {
|
||||
private readonly logger = new Logger(AppConfigService.name);
|
||||
|
||||
constructor(
|
||||
@Inject(NestConfigService)
|
||||
private readonly nestConfigService: NestConfigService,
|
||||
) {}
|
||||
|
||||
onModuleInit() {
|
||||
this.checkRequiredVariables();
|
||||
}
|
||||
|
||||
get<T = string>(key: string, defaultValue?: T): T | undefined {
|
||||
const value = this.nestConfigService.get<T>(key);
|
||||
if (value === undefined || value === null) {
|
||||
if (defaultValue !== undefined) {
|
||||
return defaultValue;
|
||||
}
|
||||
this.logger.error(`环境变量 ${key} 未定义!`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private checkRequiredVariables(): void {
|
||||
this.logger.log('检查必要环境变量..');
|
||||
const requiredVariables = [
|
||||
'PORT',
|
||||
'RD_PORT',
|
||||
'RD_ADD',
|
||||
'WS_SECRET',
|
||||
'WS_PORT',
|
||||
];
|
||||
|
||||
requiredVariables.forEach((key) => {
|
||||
const value = this.nestConfigService.get(key);
|
||||
if (value === undefined || value === null) {
|
||||
this.logger.fatal(`必需环境变量缺失: ${key}`);
|
||||
} else {
|
||||
this.logger.debug(`检测到环境变量: ${key}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
keep
|
8
src/core/path/path.module.ts
Normal file
8
src/core/path/path.module.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PathService } from './path.service';
|
||||
|
||||
@Module({
|
||||
providers: [PathService],
|
||||
exports: [PathService],
|
||||
})
|
||||
export class PathModule {}
|
119
src/core/path/path.service.ts
Normal file
119
src/core/path/path.service.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { ConfigService } from '../../config/config.service';
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class PathService {
|
||||
private readonly baseDir: string;
|
||||
private readonly logger = new Logger(PathService.name);
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
this.baseDir = path.join(__dirname, '../../..');
|
||||
this.initializePaths();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预定义路径
|
||||
* @param type 路径类型
|
||||
*/
|
||||
get(type?: PathType): string {
|
||||
const mappings: Record<PathType, string> = {
|
||||
root: this.baseDir,
|
||||
public: path.join(this.baseDir, 'public'),
|
||||
images: path.join(this.baseDir, 'public/files/image'),
|
||||
log: path.join(this.baseDir, 'logs'),
|
||||
config: path.join(this.baseDir, 'config'),
|
||||
temp: path.join(this.baseDir, 'temp'),
|
||||
userData: path.join(this.baseDir, 'private/data'),
|
||||
files: path.join(this.baseDir, 'public/files'),
|
||||
media: path.join(this.baseDir, 'public/files/media'),
|
||||
package: path.join(this.baseDir, 'package.json'),
|
||||
modules: path.join(this.baseDir, 'src/modules'),
|
||||
uploads: path.join(this.baseDir, 'public/files/uploads'),
|
||||
words: path.join(this.baseDir, 'private/data/word'),
|
||||
};
|
||||
|
||||
return type ? mappings[type] : this.baseDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化必要的目录
|
||||
*/
|
||||
private initializePaths(): void {
|
||||
this.logger.log('path初始化..');
|
||||
const pathsToInit = [
|
||||
this.get('log'),
|
||||
this.get('config'),
|
||||
this.get('userData'),
|
||||
this.get('media'),
|
||||
this.get('temp'),
|
||||
this.get('uploads'),
|
||||
this.get('words'),
|
||||
];
|
||||
|
||||
pathsToInit.forEach((dirPath) => {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
this.createDir(dirPath);
|
||||
this.logger.debug(`创建目录:${dirPath}..`);
|
||||
}
|
||||
});
|
||||
this.logger.log('path初始化完毕!');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建目录
|
||||
* @param targetPath 目标路径
|
||||
* @param includeFile 是否包含文件路径
|
||||
*/
|
||||
createDir(targetPath: string, includeFile: boolean = false): void {
|
||||
try {
|
||||
const dirToCreate = includeFile ? path.dirname(targetPath) : targetPath;
|
||||
fs.mkdirSync(dirToCreate, { recursive: true });
|
||||
this.logger.debug(`成功创建目录: ${dirToCreate}`);
|
||||
} catch (err) {
|
||||
this.logger.error(`创建目录失败: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接路径
|
||||
* @param paths 路径片段
|
||||
*/
|
||||
join(...paths: string[]): string {
|
||||
return path.join(...paths);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件扩展名
|
||||
* @param filePath 文件路径
|
||||
*/
|
||||
getExtension(filePath: string): string {
|
||||
return path.extname(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件名(不含扩展名)
|
||||
* @param filePath 文件路径
|
||||
*/
|
||||
getBasename(filePath: string): string {
|
||||
return path.basename(filePath, path.extname(filePath));
|
||||
}
|
||||
}
|
||||
|
||||
export type PathType =
|
||||
| 'root'
|
||||
| 'public'
|
||||
| 'images'
|
||||
| 'log'
|
||||
| 'config'
|
||||
| 'temp'
|
||||
| 'userData'
|
||||
| 'files'
|
||||
| 'package'
|
||||
| 'media'
|
||||
| 'modules'
|
||||
| 'words'
|
||||
| 'uploads';
|
53
src/main.ts
53
src/main.ts
@ -1,31 +1,26 @@
|
||||
import apps from './app';
|
||||
import logger from './utils/core/logger';
|
||||
import config from './utils/core/config';
|
||||
import redis from './services/redis/redis';
|
||||
import autoUpdater from './utils/core/autoUpdater';
|
||||
import System from './utils/core/system';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
import { ResponseInterceptor } from './common/interceptors/response.interceptor';
|
||||
import { AllExceptionsFilter } from './common/filters/all-exception.filter';
|
||||
|
||||
config.check(['PORT', 'DEBUG', 'RD_PORT', 'RD_ADD', 'WS_SECRET', 'WS_PORT']);
|
||||
const PORT = config.get('PORT') || 3000;
|
||||
|
||||
apps
|
||||
.createApp()
|
||||
.then(async (app) => {
|
||||
app.listen(PORT, () => {
|
||||
logger.info(`Crystelf-core listening on ${PORT}`);
|
||||
});
|
||||
const isUpdated = await autoUpdater.checkForUpdates();
|
||||
if (isUpdated) {
|
||||
logger.warn(`检测到更新,正在重启..`);
|
||||
await System.restart();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error('Crystelf-core启动失败:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await redis.disconnect();
|
||||
process.exit(0);
|
||||
async function bootstrap() {
|
||||
Logger.log('晶灵核心初始化..');
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.setGlobalPrefix('api');
|
||||
app.useGlobalInterceptors(new ResponseInterceptor());
|
||||
app.useGlobalFilters(new AllExceptionsFilter());
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('晶灵核心')
|
||||
.setDescription('为晶灵提供API服务')
|
||||
.setVersion('1.0')
|
||||
.build();
|
||||
const document = () => SwaggerModule.createDocument(app, config);
|
||||
SwaggerModule.setup('', app, document);
|
||||
await app.listen(7000);
|
||||
}
|
||||
bootstrap().then(() => {
|
||||
Logger.log(`API服务已启动:http://localhost:7000`);
|
||||
Logger.log(`API文档: http://localhost:7000/api`);
|
||||
});
|
||||
|
@ -1,156 +0,0 @@
|
||||
import express from 'express';
|
||||
import response from '../../utils/core/response';
|
||||
import BotService from './bot.service';
|
||||
import tools from '../../utils/modules/tools';
|
||||
import logger from '../../utils/core/logger';
|
||||
import wsClientManager from '../../services/ws/wsClientManager';
|
||||
|
||||
class BotController {
|
||||
private readonly router: express.Router;
|
||||
|
||||
constructor() {
|
||||
this.router = express.Router();
|
||||
this.init();
|
||||
}
|
||||
|
||||
public getRouter(): express.Router {
|
||||
return this.router;
|
||||
}
|
||||
|
||||
private init(): void {
|
||||
this.router.post(`/getBotId`, this.postBotsId);
|
||||
this.router.post('/getGroupInfo', this.postGroupInfo);
|
||||
this.router.post('/sendMessage', this.sendMessage);
|
||||
this.router.post('/reportBots', this.reportBots);
|
||||
this.router.post('/broadcast', this.smartBroadcast);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前连接到核心的全部botId数组
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
private postBotsId = async (req: express.Request, res: express.Response): Promise<void> => {
|
||||
try {
|
||||
const token = req.body.token;
|
||||
if (tools.checkToken(token.toString())) {
|
||||
const result = await BotService.getBotId();
|
||||
await response.success(res, result);
|
||||
} else {
|
||||
await tools.tokenCheckFailed(res, token);
|
||||
}
|
||||
} catch (err) {
|
||||
await response.error(res, `请求失败..`, 500, err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取群聊信息
|
||||
* @example req示例
|
||||
* ```json
|
||||
* {
|
||||
* token: ‘114514’,
|
||||
* groupId: 114514
|
||||
* }
|
||||
* ```
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
private postGroupInfo = async (req: express.Request, res: express.Response): Promise<void> => {
|
||||
try {
|
||||
const token = req.body.token;
|
||||
if (tools.checkToken(token.toString())) {
|
||||
const groupId: number = req.body.groupId;
|
||||
let returnData = await BotService.getGroupInfo({ groupId: groupId });
|
||||
if (returnData) {
|
||||
await response.success(res, returnData);
|
||||
logger.debug(returnData);
|
||||
} else {
|
||||
await response.error(res);
|
||||
}
|
||||
} else {
|
||||
await tools.tokenCheckFailed(res, token);
|
||||
}
|
||||
} catch (e) {
|
||||
await response.error(res);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 广播要求同步群聊信息和bot连接情况
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
// TODO 测试接口可用性
|
||||
private reportBots = async (req: express.Request, res: express.Response): Promise<void> => {
|
||||
try {
|
||||
const token = req.body.token;
|
||||
if (tools.checkToken(token.toString())) {
|
||||
const sendMessage = {
|
||||
type: 'reportBots',
|
||||
data: {},
|
||||
};
|
||||
logger.info(`正在请求同步bot数据..`);
|
||||
await response.success(res, {});
|
||||
await wsClientManager.broadcast(sendMessage);
|
||||
} else {
|
||||
await tools.tokenCheckFailed(res, token);
|
||||
}
|
||||
} catch (e) {
|
||||
await response.error(res);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 发送消息到群聊,自动获取client
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
// TODO 测试接口可用性
|
||||
private sendMessage = async (req: express.Request, res: express.Response): Promise<void> => {
|
||||
try {
|
||||
const token = req.body.token;
|
||||
if (tools.checkToken(token.toString())) {
|
||||
const groupId: number = Number(req.body.groupId);
|
||||
const message: string = req.body.message.toString();
|
||||
const flag: boolean = await BotService.sendMessage(groupId, message);
|
||||
if (flag) {
|
||||
await response.success(res, { message: '消息发送成功..' });
|
||||
} else {
|
||||
await response.error(res);
|
||||
}
|
||||
} else {
|
||||
await tools.tokenCheckFailed(res, token);
|
||||
}
|
||||
} catch (e) {
|
||||
await response.error(res);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 智能广播消息到全部群聊
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
// TODO 测试接口可用性
|
||||
private smartBroadcast = async (req: express.Request, res: express.Response): Promise<void> => {
|
||||
try {
|
||||
const token = req.body.token;
|
||||
const message = req.body.message;
|
||||
if (!message || typeof message !== 'string') {
|
||||
return await response.error(res, '缺少 message 字段', 400);
|
||||
}
|
||||
if (tools.checkToken(token.toString())) {
|
||||
logger.info(`广播任务已开始,正在后台执行..`);
|
||||
await response.success(res, '广播任务已开始,正在后台执行..');
|
||||
await BotService.broadcastToAllGroups(message);
|
||||
} else {
|
||||
await tools.tokenCheckFailed(res, token);
|
||||
}
|
||||
} catch (e) {
|
||||
await response.error(res);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default new BotController();
|
@ -1,241 +0,0 @@
|
||||
import logger from '../../utils/core/logger';
|
||||
import paths from '../../utils/core/path';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import redisService from '../../services/redis/redis';
|
||||
import wsClientManager from '../../services/ws/wsClientManager';
|
||||
import tools from '../../utils/core/tool';
|
||||
|
||||
class BotService {
|
||||
/**
|
||||
* 获取botId数组
|
||||
*/
|
||||
public async getBotId(): Promise<{ uin: number; nickName: string }[]> {
|
||||
logger.debug('GetBotId..');
|
||||
const userPath = paths.get('userData');
|
||||
const botsPath = path.join(userPath, '/crystelfBots');
|
||||
const dirData = await fs.readdir(botsPath);
|
||||
const uins: { uin: number; nickName: string }[] = [];
|
||||
|
||||
for (const fileName of dirData) {
|
||||
if (!fileName.endsWith('.json')) continue;
|
||||
|
||||
try {
|
||||
const raw = await redisService.fetch('crystelfBots', fileName);
|
||||
if (!raw || !Array.isArray(raw)) continue;
|
||||
|
||||
for (const bot of raw) {
|
||||
const uin = Number(bot.uin);
|
||||
const nickName = bot.nickName || '';
|
||||
if (!isNaN(uin)) {
|
||||
uins.push({ uin, nickName });
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`读取或解析 ${fileName} 出错: ${err}`);
|
||||
}
|
||||
}
|
||||
logger.debug(uins);
|
||||
return uins;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取群聊信息
|
||||
* @param data
|
||||
*/
|
||||
public async getGroupInfo(data: {
|
||||
botId?: number;
|
||||
groupId: number;
|
||||
clientId?: string;
|
||||
}): Promise<any> {
|
||||
logger.debug('GetGroupInfo..');
|
||||
const sendBot: number | undefined = data.botId ?? (await this.getGroupBot(data.groupId));
|
||||
if (!sendBot) {
|
||||
logger.warn(`不存在能向群聊${data.groupId}发送消息的Bot!`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sendData = {
|
||||
type: 'getGroupInfo',
|
||||
data: {
|
||||
botId: sendBot,
|
||||
groupId: data.groupId,
|
||||
clientID: data.clientId ?? (await this.getBotClient(sendBot)),
|
||||
},
|
||||
};
|
||||
|
||||
if (sendData.data.clientID) {
|
||||
const returnData = await wsClientManager.sendAndWait(sendData.data.clientID, sendData);
|
||||
return returnData ?? undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息到群聊
|
||||
* @param groupId 群号
|
||||
* @param message 消息
|
||||
*/
|
||||
public async sendMessage(groupId: number, message: string): Promise<boolean> {
|
||||
logger.info(`发送${message}到${groupId}..`);
|
||||
const sendBot = await this.getGroupBot(groupId);
|
||||
if (!sendBot) {
|
||||
logger.warn(`不存在能向群聊${groupId}发送消息的Bot!`);
|
||||
return false;
|
||||
}
|
||||
const client = await this.getBotClient(sendBot);
|
||||
if (!client) {
|
||||
logger.warn(`不存在${sendBot}对应的client!`);
|
||||
return false;
|
||||
}
|
||||
const sendData = {
|
||||
type: 'sendMessage',
|
||||
data: {
|
||||
botId: sendBot,
|
||||
groupId: groupId,
|
||||
clientId: client,
|
||||
message: message,
|
||||
},
|
||||
};
|
||||
await wsClientManager.send(client, sendData);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能投放广播消息实现
|
||||
* @param message 要广播的消息
|
||||
*/
|
||||
// TODO 添加群聊信誉分机制,低于30分的群聊不播报等..
|
||||
public async broadcastToAllGroups(message: string): Promise<void> {
|
||||
const userPath = paths.get('userData');
|
||||
const botsPath = path.join(userPath, '/crystelfBots');
|
||||
const dirData = await fs.readdir(botsPath);
|
||||
const groupMap: Map<number, { botId: number; clientId: string }[]> = new Map();
|
||||
|
||||
for (const fileName of dirData) {
|
||||
if (!fileName.endsWith('.json')) continue;
|
||||
|
||||
const clientId = path.basename(fileName, '.json');
|
||||
const botList = await redisService.fetch('crystelfBots', fileName);
|
||||
if (!Array.isArray(botList)) continue;
|
||||
|
||||
for (const bot of botList) {
|
||||
const botId = Number(bot.uin);
|
||||
const groups = bot.groups;
|
||||
|
||||
if (!botId || !Array.isArray(groups)) continue;
|
||||
|
||||
for (const group of groups) {
|
||||
if (group.group_id === '未知') continue;
|
||||
const groupId = Number(group.group_id);
|
||||
if (isNaN(groupId)) continue;
|
||||
|
||||
if (!groupMap.has(groupId)) {
|
||||
groupMap.set(groupId, []);
|
||||
}
|
||||
groupMap.get(groupId)!.push({ botId, clientId });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [groupId, botEntries] of groupMap.entries()) {
|
||||
logger.debug(`[群 ${groupId}] 候选Bot列表: ${JSON.stringify(botEntries)}`);
|
||||
|
||||
const clientGroups = new Map<string, number[]>();
|
||||
botEntries.forEach(({ botId, clientId }) => {
|
||||
if (!clientGroups.has(clientId)) clientGroups.set(clientId, []);
|
||||
clientGroups.get(clientId)!.push(botId);
|
||||
});
|
||||
|
||||
const selectedClientId = tools.getRandomItem([...clientGroups.keys()]);
|
||||
const botCandidates = clientGroups.get(selectedClientId)!;
|
||||
const selectedBotId = tools.getRandomItem(botCandidates);
|
||||
const delay = tools.getRandomDelay(10_000, 150_000);
|
||||
|
||||
((groupId, selectedClientId, selectedBotId, delay) => {
|
||||
setTimeout(() => {
|
||||
const sendData = {
|
||||
type: 'sendMessage',
|
||||
data: {
|
||||
botId: selectedBotId,
|
||||
groupId: groupId,
|
||||
clientId: selectedClientId,
|
||||
message: message,
|
||||
},
|
||||
};
|
||||
logger.info(
|
||||
`[广播] 向群 ${groupId} 使用Bot ${selectedBotId}(客户端 ${selectedClientId})发送消息${message},延迟 ${delay / 1000} 秒`
|
||||
);
|
||||
wsClientManager.send(selectedClientId, sendData).catch((e) => {
|
||||
logger.error(`发送到群${groupId}失败:`, e);
|
||||
});
|
||||
}, delay);
|
||||
})(groupId, selectedClientId, selectedBotId, delay);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取botId对应的client
|
||||
* @param botId
|
||||
* @private
|
||||
*/
|
||||
private async getBotClient(botId: number): Promise<string | undefined> {
|
||||
const userPath = paths.get('userData');
|
||||
const botsPath = path.join(userPath, '/crystelfBots');
|
||||
const dirData = await fs.readdir(botsPath);
|
||||
|
||||
for (const clientId of dirData) {
|
||||
if (!clientId.endsWith('.json')) continue;
|
||||
|
||||
try {
|
||||
const raw = await redisService.fetch('crystelfBots', clientId);
|
||||
if (!Array.isArray(raw)) continue;
|
||||
|
||||
for (const bot of raw) {
|
||||
const uin = Number(bot.uin);
|
||||
if (!isNaN(uin) && uin === botId) {
|
||||
return path.basename(clientId, '.json');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`读取${clientId}出错..`);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取groupId对应的botId
|
||||
* @param groupId
|
||||
* @private
|
||||
*/
|
||||
private async getGroupBot(groupId: number): Promise<number | undefined> {
|
||||
const userPath = paths.get('userData');
|
||||
const botsPath = path.join(userPath, '/crystelfBots');
|
||||
const dirData = await fs.readdir(botsPath);
|
||||
|
||||
for (const clientId of dirData) {
|
||||
if (!clientId.endsWith('.json')) continue;
|
||||
|
||||
try {
|
||||
const raw = await redisService.fetch('crystelfBots', clientId);
|
||||
if (!Array.isArray(raw)) continue;
|
||||
|
||||
for (const bot of raw) {
|
||||
const uin = Number(bot.uin);
|
||||
const groups = bot.groups;
|
||||
if (!uin || !Array.isArray(groups)) continue;
|
||||
|
||||
if (groups.find((g) => Number(g.group_id) === groupId)) {
|
||||
return uin;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`读取${clientId}出错..`);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export default new BotService();
|
@ -1,112 +0,0 @@
|
||||
import express from 'express';
|
||||
import FileService from './file.service';
|
||||
import logger from '../../utils/core/logger';
|
||||
import response from '../../utils/core/response';
|
||||
import paths from '../../utils/core/path';
|
||||
import multer from 'multer';
|
||||
import tools from '../../utils/modules/tools';
|
||||
|
||||
const uploadDir = paths.get('uploads');
|
||||
const upload = multer({
|
||||
dest: uploadDir,
|
||||
});
|
||||
|
||||
class FileController {
|
||||
private readonly router: express.Router;
|
||||
private readonly FileService: FileService;
|
||||
|
||||
constructor() {
|
||||
this.router = express.Router();
|
||||
this.FileService = new FileService();
|
||||
this.initializeRoutes();
|
||||
}
|
||||
|
||||
public getRouter(): express.Router {
|
||||
return this.router;
|
||||
}
|
||||
|
||||
private initializeRoutes(): void {
|
||||
this.router.get('*', this.handleGetFile);
|
||||
this.router.post('/upload', upload.single('file'), this.handleUploadFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* get文件
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
private handleGetFile = async (req: express.Request, res: express.Response): Promise<void> => {
|
||||
try {
|
||||
const fullPath = req.params[0];
|
||||
logger.debug(`有个小可爱正在请求${fullPath}噢..`);
|
||||
const filePath = await this.FileService.getFile(fullPath);
|
||||
if (!filePath) {
|
||||
logger.warn(`${fullPath}:文件不存在..`);
|
||||
await response.error(res, '文件不存在啦!', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
res.sendFile(filePath);
|
||||
logger.info(`成功投递文件: ${filePath}`);
|
||||
} catch (error) {
|
||||
await response.error(res, '晶灵服务处理文件请求时出错..', 500);
|
||||
logger.error('晶灵数据请求处理失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理文件上传请求
|
||||
* 客户端应以 `multipart/form-data` 格式上传文件,字段名为 `file`
|
||||
* @example 示例请求(使用 axios 和 form-data)
|
||||
* ```js
|
||||
* const form = new FormData();
|
||||
* const fileStream = fs.createReadStream(filePath);
|
||||
* form.append('file', fileStream);
|
||||
* const uploadUrl = `http://localhost:4000/upload?dir=example&expire=600`;
|
||||
* const response = await axios.post(uploadUrl, form, {
|
||||
* headers: {
|
||||
* ...form.getHeaders(),
|
||||
* },
|
||||
* maxContentLength: Infinity,
|
||||
* maxBodyLength: Infinity,
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @queryParam dir 上传到的相对目录,默认根目录
|
||||
* @queryParam expire 文件保留时间,默认 600 秒
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
private handleUploadFile = async (req: express.Request, res: express.Response): Promise<void> => {
|
||||
try {
|
||||
const token = req.body.token;
|
||||
if (tools.checkToken(token.toString())) {
|
||||
if (!req.file) {
|
||||
await response.error(res, `未检测到上传文件`, 400);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`检测到上传文件..`);
|
||||
const uploadDir = req.query.dir?.toString() || '';
|
||||
const deleteAfter = parseInt(req.query.expire as string) || 10 * 60;
|
||||
const { fullPath, relativePath } = await this.FileService.saveUploadedFile(
|
||||
req.file,
|
||||
uploadDir
|
||||
);
|
||||
await this.FileService.scheduleDelete(fullPath, deleteAfter * 1000);
|
||||
await response.success(res, {
|
||||
message: '文件上传成功..',
|
||||
filePath: fullPath,
|
||||
url: relativePath,
|
||||
});
|
||||
} else {
|
||||
await tools.tokenCheckFailed(res, token);
|
||||
}
|
||||
} catch (e) {
|
||||
await response.error(res, `文件上传失败..`, 500);
|
||||
logger.error(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default new FileController();
|
@ -1,93 +0,0 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
import paths from '../../utils/core/path';
|
||||
import logger from '../../utils/core/logger';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
class FileService {
|
||||
private readonly filePath: string;
|
||||
|
||||
constructor() {
|
||||
this.filePath = paths.get('files');
|
||||
logger.info(`晶灵云图数据中心初始化..数据存储在: ${this.filePath}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件
|
||||
* @param relativePath 文件绝对路径
|
||||
*/
|
||||
public async getFile(relativePath: string): Promise<string | null> {
|
||||
if (!this.isValidPath(relativePath) && !this.isValidFilename(path.basename(relativePath))) {
|
||||
throw new Error('非法路径请求');
|
||||
}
|
||||
|
||||
const filePath = path.join(this.filePath, relativePath);
|
||||
logger.debug(`尝试访问文件路径: ${filePath}`);
|
||||
|
||||
return existsSync(filePath) ? filePath : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存文件到目标目录
|
||||
* @param file multer的file
|
||||
* @param dir 可选上传目录,相对目录
|
||||
*/
|
||||
public async saveUploadedFile(
|
||||
file: Express.Multer.File,
|
||||
dir: string = ''
|
||||
): Promise<{ fullPath: string; relativePath: string }> {
|
||||
const baseDir = paths.get('uploads');
|
||||
const targetDir = path.join(baseDir, dir);
|
||||
if (!existsSync(targetDir)) {
|
||||
await fs.mkdir(targetDir, { recursive: true });
|
||||
logger.debug(`已创建上传目录: ${targetDir}`);
|
||||
}
|
||||
const fileName = `${Date.now()}-${file.originalname.replace(/\s+/g, '_')}`;
|
||||
const finalPath = path.join(targetDir, fileName);
|
||||
await fs.rename(file.path, finalPath);
|
||||
logger.info(`保存上传文件: ${finalPath}`);
|
||||
return {
|
||||
fullPath: finalPath,
|
||||
relativePath: `uploads/${dir}/${fileName}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时删除文件
|
||||
* @param filePath 文件绝对路径
|
||||
* @param timeoutMs 毫秒,默认10分钟
|
||||
*/
|
||||
public async scheduleDelete(filePath: string, timeoutMs: number = 10 * 60 * 1000): Promise<void> {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await fs.unlink(filePath);
|
||||
logger.info(`已自动删除文件: ${filePath}`);
|
||||
} catch (err) {
|
||||
logger.warn(`删除文件失败: ${filePath}`, err);
|
||||
}
|
||||
}, timeoutMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路径合法性
|
||||
* @param relativePath
|
||||
* @private
|
||||
*/
|
||||
private isValidPath(relativePath: string): boolean {
|
||||
try {
|
||||
const normalized = path.normalize(relativePath);
|
||||
let flag = true;
|
||||
if (normalized.startsWith('../') && path.isAbsolute(normalized)) flag = false;
|
||||
return flag;
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private isValidFilename(filename: string): boolean {
|
||||
return /^[a-zA-Z0-9_\-.]+$/.test(filename);
|
||||
}
|
||||
}
|
||||
|
||||
export default FileService;
|
@ -1,45 +0,0 @@
|
||||
import express from 'express';
|
||||
import sampleService from './sample.service';
|
||||
import response from '../../utils/core/response';
|
||||
|
||||
class SampleController {
|
||||
private readonly router: express.Router;
|
||||
|
||||
constructor() {
|
||||
this.router = express.Router();
|
||||
this.initializeRoutes();
|
||||
}
|
||||
|
||||
public getRouter(): express.Router {
|
||||
return this.router;
|
||||
}
|
||||
|
||||
private initializeRoutes(): void {
|
||||
this.router.get('/hello', this.getHello);
|
||||
this.router.post('/greet', this.postGreet);
|
||||
}
|
||||
|
||||
private getHello = async (req: express.Request, res: express.Response): Promise<void> => {
|
||||
try {
|
||||
const result = await sampleService.getHello();
|
||||
await response.success(res, result);
|
||||
} catch (error) {
|
||||
await response.error(res, '请求失败了..', 500, error);
|
||||
}
|
||||
};
|
||||
|
||||
private postGreet = async (req: express.Request, res: express.Response): Promise<void> => {
|
||||
try {
|
||||
const { name } = req.body;
|
||||
if (!name) {
|
||||
return response.error(res, '姓名不能为空!', 400);
|
||||
}
|
||||
const result = await sampleService.generateGreeting(name);
|
||||
await response.success(res, result);
|
||||
} catch (error) {
|
||||
await response.error(res, '请求失败了..', 500, error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default new SampleController();
|
@ -1,19 +0,0 @@
|
||||
import logger from '../../utils/core/logger';
|
||||
|
||||
class SampleService {
|
||||
public async getHello() {
|
||||
logger.debug(`有个小可爱正在请求GetHello方法..`);
|
||||
return { message: 'Hello World!' };
|
||||
}
|
||||
|
||||
public async generateGreeting(name: string): Promise<object> {
|
||||
logger.debug(`有个小可爱正在请求generateGreeting方法..`);
|
||||
if (!name) {
|
||||
logger.warn('Name is required');
|
||||
throw new Error('Name is required');
|
||||
}
|
||||
return { message: `Hello, ${name}!` };
|
||||
}
|
||||
}
|
||||
|
||||
export default new SampleService();
|
@ -1,62 +0,0 @@
|
||||
import express from 'express';
|
||||
import tools from '../../utils/modules/tools';
|
||||
import response from '../../utils/core/response';
|
||||
import SystemService from './system.service';
|
||||
|
||||
class SystemController {
|
||||
private readonly router: express.Router;
|
||||
|
||||
constructor() {
|
||||
this.router = express.Router();
|
||||
this.init();
|
||||
}
|
||||
|
||||
public getRouter(): express.Router {
|
||||
return this.router;
|
||||
}
|
||||
|
||||
private init(): void {
|
||||
this.router.post('/restart', this.systemRestart);
|
||||
this.router.post('/getRestartTime', this.getRestartTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统重启路由
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
private systemRestart = async (req: express.Request, res: express.Response): Promise<void> => {
|
||||
try {
|
||||
const token = req.body.token;
|
||||
if (tools.checkToken(token.toString())) {
|
||||
await response.success(res, '核心正在重启..');
|
||||
await SystemService.systemRestart();
|
||||
} else {
|
||||
await tools.tokenCheckFailed(res, token);
|
||||
}
|
||||
} catch (e) {
|
||||
await response.error(res);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取重启所需时间
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
private getRestartTime = async (req: express.Request, res: express.Response): Promise<void> => {
|
||||
try {
|
||||
const token = req.body.token;
|
||||
if (tools.checkToken(token.toString())) {
|
||||
const time = await SystemService.getRestartTime();
|
||||
await response.success(res, time);
|
||||
} else {
|
||||
await tools.tokenCheckFailed(res, token);
|
||||
}
|
||||
} catch (e) {
|
||||
await response.error(res);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default new SystemController();
|
@ -1,20 +0,0 @@
|
||||
import System from '../../utils/core/system';
|
||||
import fs from 'fs/promises';
|
||||
import logger from '../../utils/core/logger';
|
||||
import path from 'path';
|
||||
import paths from '../../utils/core/path';
|
||||
|
||||
class SystemService {
|
||||
public async systemRestart() {
|
||||
logger.debug(`有个小可爱正在请求重启核心..`);
|
||||
await System.restart();
|
||||
}
|
||||
|
||||
public async getRestartTime() {
|
||||
logger.debug(`有个小可爱想知道核心重启花了多久..`);
|
||||
const restartTimePath = path.join(paths.get('temp'), 'restart_time');
|
||||
return await fs.readFile(restartTimePath, 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
export default new SystemService();
|
@ -1,31 +0,0 @@
|
||||
import express from 'express';
|
||||
import TestService from './test.service';
|
||||
import response from '../../utils/core/response';
|
||||
import logger from '../../utils/core/logger';
|
||||
|
||||
class TestController {
|
||||
private readonly router: express.Router;
|
||||
|
||||
constructor() {
|
||||
this.router = express.Router();
|
||||
this.initRouter();
|
||||
}
|
||||
|
||||
public getRouter(): express.Router {
|
||||
return this.router;
|
||||
}
|
||||
|
||||
public initRouter(): void {
|
||||
this.router.get('/test', this.test);
|
||||
}
|
||||
private test = async (req: express.Request, res: express.Response): Promise<void> => {
|
||||
try {
|
||||
const result = await TestService.test();
|
||||
await response.success(res, result);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default new TestController();
|
@ -1,21 +0,0 @@
|
||||
import wsClientManager from '../../services/ws/wsClientManager';
|
||||
import logger from '../../utils/core/logger';
|
||||
|
||||
class TestService {
|
||||
public async test() {
|
||||
try {
|
||||
const testData = {
|
||||
type: 'getGroupInfo',
|
||||
data: {
|
||||
botId: 'stdin',
|
||||
groupId: 'stdin',
|
||||
},
|
||||
};
|
||||
return await wsClientManager.sendAndWait('test', testData);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new TestService();
|
@ -1,66 +0,0 @@
|
||||
import express from 'express';
|
||||
import WordsService from './words.service';
|
||||
import response from '../../utils/core/response';
|
||||
import tools from '../../utils/modules/tools';
|
||||
|
||||
class WordsController {
|
||||
private readonly router: express.Router;
|
||||
|
||||
constructor() {
|
||||
this.router = express.Router();
|
||||
this.init();
|
||||
}
|
||||
|
||||
public getRouter(): express.Router {
|
||||
return this.router;
|
||||
}
|
||||
|
||||
private init(): void {
|
||||
this.router.get('/getText/:id', this.getText);
|
||||
this.router.post('/reloadText', this.reloadWord);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取随机文案
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
private getText = async (req: express.Request, res: express.Response): Promise<void> => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const texts = await WordsService.loadWordById(id.toString());
|
||||
if (!texts || texts.length === 0) {
|
||||
return await response.error(res, `文案${id}不存在或为空..`, 404);
|
||||
}
|
||||
const randomIndex = Math.floor(Math.random() * texts.length);
|
||||
const result = texts[randomIndex];
|
||||
await response.success(res, result);
|
||||
} catch (e) {
|
||||
await response.error(res);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 重载文案
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
private reloadWord = async (req: express.Request, res: express.Response): Promise<void> => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const token = req.params.token;
|
||||
if (tools.checkToken(token)) {
|
||||
if (await WordsService.reloadWord(id.toString())) {
|
||||
await response.success(res, '成功重载..');
|
||||
} else {
|
||||
await response.error(res, '重载失败..');
|
||||
}
|
||||
} else {
|
||||
await tools.tokenCheckFailed(res, token);
|
||||
}
|
||||
} catch (e) {
|
||||
await response.error(res);
|
||||
}
|
||||
};
|
||||
}
|
||||
export default new WordsController();
|
@ -1,66 +0,0 @@
|
||||
import path from 'path';
|
||||
import paths from '../../utils/core/path';
|
||||
import fs from 'fs/promises';
|
||||
import logger from '../../utils/core/logger';
|
||||
|
||||
class WordsService {
|
||||
private wordCache: Record<string, string[]> = {}; //缓存
|
||||
private readonly clearIntervalMs = 30 * 60 * 1000; //30min
|
||||
|
||||
constructor() {
|
||||
this.startAutoClear();
|
||||
}
|
||||
private startAutoClear() {
|
||||
setInterval(() => {
|
||||
logger.info('[WordsService] Clearing wordCache..');
|
||||
this.wordCache = {};
|
||||
}, this.clearIntervalMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地加载json到内存&返回
|
||||
* @param id 文件名
|
||||
*/
|
||||
public async loadWordById(id: string): Promise<string[] | null> {
|
||||
logger.info(`Loading words ${id}..`);
|
||||
if (this.wordCache[id]) return this.wordCache[id];
|
||||
const filePath = path.join(paths.get('words'), `${id}.json`);
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
const parsed = JSON.parse(content);
|
||||
if (Array.isArray(parsed)) {
|
||||
const texts = parsed.filter((item) => typeof item === 'string');
|
||||
this.wordCache[id] = texts;
|
||||
return texts;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to loadWordById: ${id}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重载json到内存
|
||||
* @param id 文件名
|
||||
*/
|
||||
public async reloadWord(id: string): Promise<boolean> {
|
||||
logger.info(`Reloading word: ${id}..`);
|
||||
const filePath = path.join(paths.get('words'), `${id}.json`);
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
const parsed = JSON.parse(content);
|
||||
if (Array.isArray(parsed)) {
|
||||
this.wordCache[id] = parsed.filter((item) => typeof item === 'string');
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(`Failed to reloadWordById: ${id}..`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
export default new WordsService();
|
11
src/root/root.controller.ts
Normal file
11
src/root/root.controller.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
|
||||
@Controller()
|
||||
export class RootController {
|
||||
@Get()
|
||||
getWelcome() {
|
||||
return {
|
||||
message: '欢迎使用晶灵核心',
|
||||
};
|
||||
}
|
||||
}
|
7
src/root/root.module.ts
Normal file
7
src/root/root.module.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { RootController } from './root.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [RootController],
|
||||
})
|
||||
export class RootModule {}
|
@ -1,191 +0,0 @@
|
||||
import Redis from 'ioredis';
|
||||
import logger from '../../utils/core/logger';
|
||||
import tools from '../../utils/core/tool';
|
||||
import config from '../../utils/core/config';
|
||||
import redisTools from '../../utils/redis/redisTools';
|
||||
import Persistence from '../../utils/redis/persistence';
|
||||
import IUser from '../../types/user';
|
||||
|
||||
class RedisService {
|
||||
private client!: Redis;
|
||||
private isConnected = false;
|
||||
|
||||
constructor() {
|
||||
this.initialize().then();
|
||||
}
|
||||
|
||||
private async initialize() {
|
||||
await this.connectWithRetry();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
private async connectWithRetry(): Promise<void> {
|
||||
try {
|
||||
await tools.retry(
|
||||
async () => {
|
||||
this.client = new Redis({
|
||||
host: config.get('RD_ADD'),
|
||||
port: Number(config.get('RD_PORT')),
|
||||
retryStrategy: (times) => {
|
||||
return Math.min(times * 1000, 5000);
|
||||
},
|
||||
});
|
||||
|
||||
await this.client.ping();
|
||||
this.isConnected = true;
|
||||
logger.info(`Redis连接成功!位于${config.get('RD_ADD')}:${config.get('RD_PORT')}`);
|
||||
},
|
||||
{
|
||||
maxAttempts: 5,
|
||||
initialDelay: 1000,
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('Redis连接失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置Redis客户端事件监听器
|
||||
*/
|
||||
private setupEventListeners(): void {
|
||||
this.client.on('error', (err) => {
|
||||
if (!err.message.includes('ECONNREFUSED')) {
|
||||
logger.error('Redis错误:', err);
|
||||
}
|
||||
this.isConnected = false;
|
||||
});
|
||||
|
||||
this.client.on('ready', () => {
|
||||
this.isConnected = true;
|
||||
logger.debug('Redis连接就绪!');
|
||||
});
|
||||
|
||||
this.client.on('reconnecting', () => {
|
||||
logger.warn('Redis重新连接中...');
|
||||
});
|
||||
}
|
||||
|
||||
public async waitUntilReady(): Promise<void> {
|
||||
if (this.isConnected) return;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const check = () => {
|
||||
if (this.isConnected) {
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(check, 100);
|
||||
}
|
||||
};
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Redis客户端实例
|
||||
* @returns {Redis} Redis客户端
|
||||
* @throws 如果未连接,则记录fatal日志
|
||||
*/
|
||||
public getClient(): Redis {
|
||||
if (!this.isConnected) {
|
||||
logger.fatal(1, 'Redis未连接..');
|
||||
}
|
||||
return this.client;
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开Redis连接
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async disconnect(): Promise<void> {
|
||||
await this.client.quit();
|
||||
this.isConnected = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储对象到Redis
|
||||
* @template T
|
||||
* @param {string} key Redis键
|
||||
* @param {T} value 要存储的对象
|
||||
* @param {number} [ttl] 过期时间(秒)
|
||||
*/
|
||||
public async setObject<T>(key: string, value: T, ttl?: number): Promise<void> {
|
||||
const serialized = redisTools.serialize(value);
|
||||
await this.getClient().set(key, serialized);
|
||||
|
||||
if (ttl) {
|
||||
await this.getClient().expire(key, ttl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Redis获取对象
|
||||
* @template T
|
||||
* @param {string} key Redis键
|
||||
* @returns {Promise<T | undefined>} 获取到的对象或undefined
|
||||
*/
|
||||
public async getObject<T>(key: string): Promise<T | undefined> {
|
||||
const serialized = await this.getClient().get(key);
|
||||
if (!serialized) return undefined;
|
||||
|
||||
const deserialized = redisTools.deserialize<T>(serialized);
|
||||
return redisTools.reviveDates(deserialized);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新Redis中已存在的对象
|
||||
* @template T
|
||||
* @param {string} key Redis键
|
||||
* @param {T} updates 更新内容
|
||||
* @returns {Promise<T>} 更新后的对象
|
||||
*/
|
||||
public async update<T>(key: string, updates: T): Promise<T> {
|
||||
const existing = await this.getObject<T>(key);
|
||||
if (!existing) {
|
||||
logger.error(`数据${key}不存在..`);
|
||||
}
|
||||
const updated = { ...existing, ...updates };
|
||||
await this.setObject(key, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Redis或本地文件获取数据
|
||||
* @template T
|
||||
* @param {string} key Redis键
|
||||
* @param {string} fileName 本地文件名
|
||||
* @returns {Promise<T | undefined>} 获取到的数据或undefined
|
||||
*/
|
||||
public async fetch<T>(key: string, fileName: string): Promise<T | undefined> {
|
||||
const data = await this.getObject<T>(key);
|
||||
if (data) return data;
|
||||
const fromLocal = await Persistence.readDataLocal<T>(key, fileName);
|
||||
if (fromLocal) {
|
||||
await this.setObject(key, fromLocal);
|
||||
return fromLocal;
|
||||
}
|
||||
logger.error(`数据${key}不存在..`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数据持久化到Redis和本地文件
|
||||
* @template T
|
||||
* @param {string} key Redis键
|
||||
* @param {T} data 要持久化的数据
|
||||
* @param {string} fileName 本地文件名
|
||||
*/
|
||||
public async persistData<T>(key: string, data: T, fileName: string): Promise<void> {
|
||||
await this.setObject(key, data);
|
||||
await Persistence.writeDataLocal(key, data, fileName);
|
||||
return;
|
||||
}
|
||||
|
||||
public async test(): Promise<void> {
|
||||
const user = await this.fetch<IUser>('Jerry', 'IUser');
|
||||
logger.debug('User:', user);
|
||||
}
|
||||
}
|
||||
|
||||
const redisService = new RedisService();
|
||||
export default redisService;
|
@ -1,82 +0,0 @@
|
||||
import { AuthenticatedSocket } from '../../types/ws';
|
||||
import wsTools from '../../utils/ws/wsTools';
|
||||
import { WebSocket } from 'ws';
|
||||
import logger from '../../utils/core/logger';
|
||||
import redisService from '../redis/redis';
|
||||
import wsClientManager from './wsClientManager';
|
||||
|
||||
type MessageHandler = (socket: WebSocket, msg: any) => Promise<void>;
|
||||
|
||||
class WSMessageHandler {
|
||||
private handlers: Map<string, MessageHandler>;
|
||||
|
||||
constructor() {
|
||||
this.handlers = new Map([
|
||||
['test', this.handleTest],
|
||||
['ping', this.handlePing],
|
||||
['pong', this.handlePong],
|
||||
['reportBots', this.handleReportBots],
|
||||
]);
|
||||
}
|
||||
|
||||
async handle(socket: AuthenticatedSocket, clientId: string, msg: any) {
|
||||
try {
|
||||
//如果是 pendingRequests 的回包
|
||||
if (msg.requestId && wsClientManager.resolvePendingRequest(msg.requestId, msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = this.handlers.get(msg.type);
|
||||
|
||||
if (handler) {
|
||||
await handler(socket, msg);
|
||||
} else {
|
||||
await this.handleUnknown(socket, msg);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`ws消息处理时出错: ${err}`);
|
||||
await wsTools.send(socket, {
|
||||
type: 'error',
|
||||
message: 'error message',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async handleTest(socket: WebSocket, msg: any) {
|
||||
await wsTools.send(socket, {
|
||||
type: 'test',
|
||||
data: { status: 'ok' },
|
||||
});
|
||||
}
|
||||
|
||||
private async handlePing(socket: WebSocket, msg: any) {
|
||||
await wsTools.send(socket, { type: 'pong' });
|
||||
}
|
||||
|
||||
private async handlePong(socket: WebSocket, msg: any) {
|
||||
//logger.debug(`received pong`);
|
||||
}
|
||||
|
||||
private async handleUnknown(socket: WebSocket, msg: any) {
|
||||
logger.warn(`收到未知消息类型: ${msg.type}`);
|
||||
await wsTools.send(socket, {
|
||||
type: 'error',
|
||||
message: `未知消息类型: ${msg.type}`,
|
||||
});
|
||||
}
|
||||
|
||||
private async handleReportBots(socket: WebSocket, msg: any) {
|
||||
logger.debug(`received reportBots: ${msg.data}`);
|
||||
const clientId = msg.data[0].client;
|
||||
const botsData = msg.data.slice(1);
|
||||
await redisService.persistData('crystelfBots', botsData, clientId);
|
||||
logger.debug(`保存了 ${botsData.length} 个 bot(client: ${clientId})`);
|
||||
}
|
||||
|
||||
public registerHandler(type: string, handler: MessageHandler): void {
|
||||
this.handlers.set(type, handler);
|
||||
}
|
||||
}
|
||||
|
||||
const wsHandler = new WSMessageHandler();
|
||||
export default wsHandler;
|
@ -1,127 +0,0 @@
|
||||
import WebSocket from 'ws';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { clearTimeout } from 'node:timers';
|
||||
|
||||
type ClientID = string;
|
||||
const pendingRequests = new Map<string, (data: any) => void>();
|
||||
|
||||
class WSClientManager {
|
||||
private clients = new Map<ClientID, WebSocket>();
|
||||
|
||||
/**
|
||||
* 添加ws客户端实例
|
||||
* @param id 标识符
|
||||
* @param socket
|
||||
*/
|
||||
public add(id: ClientID, socket: WebSocket) {
|
||||
this.clients.set(id, socket);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除ws客户端实例
|
||||
* @param id
|
||||
*/
|
||||
public remove(id: ClientID) {
|
||||
this.clients.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取ws客户端实例
|
||||
* @param id
|
||||
*/
|
||||
public get(id: ClientID): WebSocket | undefined {
|
||||
return this.clients.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息到ws客户端
|
||||
* @param id ws客户端标识符
|
||||
* @param data 要发送的内容
|
||||
*/
|
||||
public async send(id: ClientID, data: any): Promise<boolean> {
|
||||
const socket = this.clients.get(id);
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) return false;
|
||||
return this.safeSend(socket, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* ws发送请求&等待回调
|
||||
* @param id ws客户端标识符-id
|
||||
* @param data 发送的信息
|
||||
* @param timeout 超时时间 默认5秒
|
||||
*/
|
||||
public async sendAndWait(id: ClientID, data: any, timeout = 5000): Promise<any> {
|
||||
const socket = this.clients.get(id);
|
||||
if (!socket) return;
|
||||
|
||||
data.requestId = uuidv4();
|
||||
const requestId = data.requestId;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
pendingRequests.delete(requestId);
|
||||
reject(new Error(`${requestId}: 请求超时`));
|
||||
}, timeout);
|
||||
|
||||
pendingRequests.set(requestId, (response) => {
|
||||
clearTimeout(timer);
|
||||
pendingRequests.delete(requestId);
|
||||
resolve(response);
|
||||
});
|
||||
|
||||
this.safeSend(socket, data).catch((err) => {
|
||||
clearTimeout(timer);
|
||||
pendingRequests.delete(requestId);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理回调
|
||||
* @param requestId
|
||||
* @param data
|
||||
*/
|
||||
public resolvePendingRequest(requestId: string, data: any): boolean {
|
||||
const callback = pendingRequests.get(requestId);
|
||||
if (callback) {
|
||||
pendingRequests.delete(requestId);
|
||||
callback(data);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播消息到全部ws客户端
|
||||
* @param data 消息
|
||||
*/
|
||||
public async broadcast(data: any): Promise<void> {
|
||||
const tasks = Array.from(this.clients.values()).map((socket) => {
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
return this.safeSend(socket, data);
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
await Promise.all(tasks);
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全发送消息到ws客户端
|
||||
* @param socket ws客户端
|
||||
* @param data 发送的内容,会自动格式化
|
||||
* @private
|
||||
*/
|
||||
private async safeSend(socket: WebSocket, data: any): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
socket.send(JSON.stringify(data), (err) => {
|
||||
if (err) reject(new Error('发送失败'));
|
||||
else resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const wsClientManager = new WSClientManager();
|
||||
export default wsClientManager;
|
@ -1,100 +0,0 @@
|
||||
import WebSocket, { WebSocketServer } from 'ws';
|
||||
import config from '../../utils/core/config';
|
||||
import logger from '../../utils/core/logger';
|
||||
import { AuthenticatedSocket, AuthMessage, WSMessage } from '../../types/ws';
|
||||
import WsTools from '../../utils/ws/wsTools';
|
||||
import wsHandler from './handler';
|
||||
import { clearInterval } from 'node:timers';
|
||||
import wsClientManager from './wsClientManager';
|
||||
|
||||
class WSServer {
|
||||
private readonly wss: WebSocketServer;
|
||||
private readonly port = Number(config.get('WS_PORT'));
|
||||
private readonly secret = config.get('WS_SECRET');
|
||||
|
||||
constructor() {
|
||||
this.wss = new WebSocketServer({ port: this.port });
|
||||
this.init();
|
||||
logger.info(`WS Server listening on ws://localhost:${this.port}`);
|
||||
}
|
||||
|
||||
private init(): void {
|
||||
this.wss.on('connection', (socket: AuthenticatedSocket, req) => {
|
||||
const ip = req.socket.remoteAddress || 'unknown';
|
||||
logger.info(`收到来自 ${ip} 的 WebSocket 连接请求..`);
|
||||
|
||||
socket.heartbeat = WsTools.setUpHeartbeat(socket);
|
||||
|
||||
socket.on('message', async (raw) => {
|
||||
logger.debug(`Received raw message from ${ip}: ${raw.toString()}`);
|
||||
|
||||
const msg = WsTools.parseMessage<WSMessage>(raw);
|
||||
if (!msg) return this.handleInvalidMessage(socket, ip);
|
||||
|
||||
await this.routeMessage(socket, msg, ip);
|
||||
});
|
||||
|
||||
socket.on('close', () => {
|
||||
logger.info(`ws断开连接 ${ip} (${socket.clientId || 'unauthenticated'})`);
|
||||
this.handleDisconnect(socket);
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
logger.error(`WS error from ${ip}: ${err.message}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async handleInvalidMessage(socket: WebSocket, ip: string) {
|
||||
logger.warn(`Invalid message received from ${ip}`);
|
||||
await WsTools.send(socket, {
|
||||
type: 'error',
|
||||
message: 'Invalid message format',
|
||||
});
|
||||
}
|
||||
|
||||
private async routeMessage(socket: AuthenticatedSocket, msg: WSMessage, ip: string) {
|
||||
if (!socket.isAuthed) {
|
||||
if (this.isAuthMessage(msg)) {
|
||||
logger.info(`Attempting auth from ${ip} as ${msg.clientId}`);
|
||||
await this.handleAuth(socket, msg, ip);
|
||||
} else {
|
||||
logger.warn(`Received message before auth from ${ip}: ${JSON.stringify(msg)}`);
|
||||
await this.handleInvalidMessage(socket, ip);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`Routing message from ${socket.clientId}: ${JSON.stringify(msg)}`);
|
||||
await wsHandler.handle(socket, socket.clientId!, msg);
|
||||
}
|
||||
|
||||
private isAuthMessage(msg: WSMessage): msg is AuthMessage {
|
||||
return msg.type === 'auth';
|
||||
}
|
||||
|
||||
private async handleAuth(socket: AuthenticatedSocket, msg: AuthMessage, ip: string) {
|
||||
if (msg.secret === this.secret) {
|
||||
socket.isAuthed = true;
|
||||
socket.clientId = msg.clientId;
|
||||
wsClientManager.add(msg.clientId, socket);
|
||||
logger.info(`Auth success from ${ip}, clientId: ${msg.clientId}`);
|
||||
await WsTools.send(socket, { type: 'auth', success: true });
|
||||
} else {
|
||||
logger.warn(`Auth failed from ${ip} (invalid secret), clientId: ${msg.clientId}`);
|
||||
await WsTools.send(socket, { type: 'auth', success: false });
|
||||
socket.close(4001, 'Authentication failed');
|
||||
}
|
||||
}
|
||||
|
||||
private handleDisconnect(socket: AuthenticatedSocket) {
|
||||
if (socket.heartbeat) clearInterval(socket.heartbeat);
|
||||
if (socket.clientId) {
|
||||
wsClientManager.remove(socket.clientId);
|
||||
logger.info(`Removed client ${socket.clientId} from manager`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wsServer = new WSServer();
|
||||
export default wsServer;
|
@ -1,70 +0,0 @@
|
||||
import WebSocket from 'ws';
|
||||
import axios from 'axios';
|
||||
import logger from '../utils/core/logger';
|
||||
|
||||
const WS_URL = 'ws://127.0.0.1:4001';
|
||||
const WS_SECRET = '114514';
|
||||
const CLIENT_ID = 'test';
|
||||
|
||||
function createWebSocketClient() {
|
||||
const socket = new WebSocket(WS_URL);
|
||||
|
||||
socket.on('open', () => {
|
||||
console.log('[WS] Connected to server');
|
||||
|
||||
const authPayload = {
|
||||
type: 'auth',
|
||||
secret: WS_SECRET,
|
||||
clientId: CLIENT_ID,
|
||||
};
|
||||
socket.send(JSON.stringify(authPayload));
|
||||
});
|
||||
|
||||
socket.on('message', (raw) => {
|
||||
const msg = JSON.parse(raw.toString());
|
||||
console.log('[WS] Message from server:', msg);
|
||||
|
||||
if (msg.type === 'auth' && msg.success === true) {
|
||||
socket.send(JSON.stringify({ type: 'test' }));
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('close', () => {
|
||||
console.log('[WS] Connection closed');
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
console.error('[WS] Error:', err);
|
||||
});
|
||||
}
|
||||
|
||||
async function testGetAPI() {
|
||||
try {
|
||||
const response = await axios.get('http://localhost:4000/api/sample/hello');
|
||||
console.log('[HTTP][GET] Response:', response.data);
|
||||
} catch (err) {
|
||||
console.error('[HTTP][GET] Error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function testPostAPI() {
|
||||
try {
|
||||
const response = await axios.post('https://core.crystelf.top/api/bot/getGroupInfo', {
|
||||
token: '114113',
|
||||
groupId: 796070855,
|
||||
});
|
||||
logger.info('[HTTP][POST] Response:', response.data);
|
||||
} catch (err) {
|
||||
console.error('[HTTP][POST] Error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
createWebSocketClient();
|
||||
|
||||
setTimeout(() => {
|
||||
testPostAPI();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
main();
|
@ -1,5 +0,0 @@
|
||||
export default interface GroupInfo {
|
||||
name: string;
|
||||
groupId: number;
|
||||
memberCount: number; //群人数
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
export default interface UserInfo {
|
||||
qq: number;
|
||||
email?: string;
|
||||
labAccount?: string;
|
||||
username: string;
|
||||
nickname?: string;
|
||||
|
||||
/**
|
||||
* 管理的群
|
||||
* 第一个number为群号,第二个number为在群内的botId
|
||||
*/
|
||||
manageGroups: Record<number, number[]>;
|
||||
role: 'super' | 'admin' | 'user';
|
||||
balance: number;
|
||||
bots: number[];
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
interface RetryOptions {
|
||||
maxAttempts: number;
|
||||
initialDelay: number;
|
||||
}
|
||||
|
||||
export default RetryOptions;
|
@ -1,10 +0,0 @@
|
||||
interface IUser {
|
||||
name: string;
|
||||
qq: string;
|
||||
password: string;
|
||||
isAdmin: boolean;
|
||||
lastLogin?: Date;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export default IUser;
|
@ -1,18 +0,0 @@
|
||||
import WebSocket from 'ws';
|
||||
|
||||
export interface AuthenticatedSocket extends WebSocket {
|
||||
isAuthed?: boolean;
|
||||
clientId?: string;
|
||||
heartbeat?: NodeJS.Timeout;
|
||||
}
|
||||
|
||||
export interface WSMessage {
|
||||
type: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface AuthMessage extends WSMessage {
|
||||
type: 'auth';
|
||||
secret: string;
|
||||
clientId: string;
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
import simpleGit, { SimpleGit } from 'simple-git';
|
||||
import paths from './path';
|
||||
import logger from './logger';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import fs from 'fs';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
class AutoUpdater {
|
||||
private git: SimpleGit;
|
||||
private readonly repoPath: string;
|
||||
|
||||
constructor() {
|
||||
this.git = simpleGit();
|
||||
this.repoPath = paths.get('root');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查远程更新
|
||||
*/
|
||||
public async checkForUpdates(): Promise<boolean> {
|
||||
try {
|
||||
logger.info('检查仓库更新中..');
|
||||
|
||||
const status = await this.git.status();
|
||||
if (status.ahead > 0) {
|
||||
logger.info('检测到当地仓库有未提交的更改,跳过更新..');
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.info('正在获取远程仓库信息..');
|
||||
await this.git.fetch();
|
||||
|
||||
const localBranch = status.current;
|
||||
const diffSummary = await this.git.diffSummary([`${localBranch}..origin/${localBranch}`]);
|
||||
|
||||
if (diffSummary.files.length > 0) {
|
||||
logger.info('检测到远程仓库有更新!');
|
||||
|
||||
logger.info('正在拉取更新..');
|
||||
if (localBranch) {
|
||||
await this.git.pull('origin', localBranch);
|
||||
} else {
|
||||
logger.error('当前分支名称未知,无法执行拉取操作..');
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.info('代码更新成功,开始更新依赖..');
|
||||
await this.updateDependencies();
|
||||
|
||||
logger.info('自动更新流程完成。');
|
||||
return true;
|
||||
} else {
|
||||
logger.info('远程仓库没有新变化..');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('检查仓库更新失败: ', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动更新依赖和构建
|
||||
*/
|
||||
private async updateDependencies(): Promise<void> {
|
||||
try {
|
||||
logger.info('执行 pnpm install...');
|
||||
await execAsync('pnpm install', { cwd: this.repoPath });
|
||||
logger.info('依赖安装完成。');
|
||||
|
||||
const pkgPath = paths.get('package');
|
||||
const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
||||
|
||||
if (pkgJson.scripts?.build) {
|
||||
logger.info('检测到 build 脚本,执行 pnpm build...');
|
||||
await execAsync('pnpm build', { cwd: this.repoPath });
|
||||
logger.info('构建完成。');
|
||||
} else {
|
||||
logger.info('未检测到 build 脚本,跳过构建。');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('更新依赖或构建失败: ', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const autoUpdater = new AutoUpdater();
|
||||
export default autoUpdater;
|
@ -1,72 +0,0 @@
|
||||
import dotenv from 'dotenv';
|
||||
import logger from './logger';
|
||||
|
||||
class ConfigManger {
|
||||
private static instance: ConfigManger;
|
||||
private readonly env: NodeJS.ProcessEnv;
|
||||
|
||||
private constructor() {
|
||||
dotenv.config();
|
||||
this.env = process.env;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*/
|
||||
public static getInstance(): ConfigManger {
|
||||
if (!ConfigManger.instance) {
|
||||
ConfigManger.instance = new ConfigManger();
|
||||
}
|
||||
return ConfigManger.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取环境变量(带类型推断)
|
||||
* @param key 环境变量键名
|
||||
* @param defaultValue 默认值(决定返回类型)
|
||||
*/
|
||||
public get<T = string>(key: string, defaultValue?: T): T {
|
||||
const value = this.env[key];
|
||||
if (value === undefined) {
|
||||
if (defaultValue !== undefined) return defaultValue;
|
||||
logger.fatal(1, `环境变量${key}未定义!`);
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
switch (typeof defaultValue) {
|
||||
case 'number':
|
||||
return Number(value) as T;
|
||||
case 'boolean':
|
||||
return (value === 'true') as T;
|
||||
default:
|
||||
return value as T;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置环境变量(运行时有效)
|
||||
* @param key 键名
|
||||
* @param value 值
|
||||
*/
|
||||
public set(key: string, value: string | number | boolean): void {
|
||||
this.env[key] = String(value);
|
||||
logger.debug(`成功更改环境变量${key}为${value}!`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查环境变量是否已加载
|
||||
*/
|
||||
public check(keys: string[]): void {
|
||||
keys.forEach((key) => {
|
||||
if (!(key in this.env)) {
|
||||
logger.fatal(1, `必须环境变量缺失:${key}`);
|
||||
} else {
|
||||
logger.debug(`检测到环境变量${key}!`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const config = ConfigManger.getInstance();
|
||||
|
||||
export default config;
|
@ -1,74 +0,0 @@
|
||||
class date {
|
||||
/**
|
||||
* 获取当前日期 (格式: YYYYMMDD)
|
||||
*/
|
||||
public static getCurrentDate(): string {
|
||||
const now = new Date();
|
||||
return [
|
||||
now.getFullYear(),
|
||||
(now.getMonth() + 1).toString().padStart(2, '0'),
|
||||
now.getDate().toString().padStart(2, '0'),
|
||||
].join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前时间 (格式: HH:mm:ss)
|
||||
*/
|
||||
public static getCurrentTime(): string {
|
||||
return new Date().toLocaleTimeString('en-US', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取格式化日期时间
|
||||
* @param formatStr 格式字符串 (YYYY-年, MM-月, DD-日, HH-时, mm-分, ss-秒)
|
||||
* @example format('YYYY-MM-DD HH:mm:ss') => '2023-10-15 14:30:45'
|
||||
*/
|
||||
public static format(formatStr: string = 'YYYY-MM-DD HH:mm:ss'): string {
|
||||
const now = new Date();
|
||||
|
||||
const replacements: Record<string, string> = {
|
||||
YYYY: now.getFullYear().toString(),
|
||||
MM: (now.getMonth() + 1).toString().padStart(2, '0'),
|
||||
DD: now.getDate().toString().padStart(2, '0'),
|
||||
HH: now.getHours().toString().padStart(2, '0'),
|
||||
mm: now.getMinutes().toString().padStart(2, '0'),
|
||||
ss: now.getSeconds().toString().padStart(2, '0'),
|
||||
};
|
||||
|
||||
return formatStr.replace(/YYYY|MM|DD|HH|mm|ss/g, (match) => replacements[match]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算日期差值
|
||||
* @param start 开始日期
|
||||
* @param end 结束日期 (默认当前时间)
|
||||
* @param unit 返回单位 ('days' | 'hours' | 'minutes' | 'seconds')
|
||||
*/
|
||||
public static diff(
|
||||
start: Date,
|
||||
end: Date = new Date(),
|
||||
unit: 'days' | 'hours' | 'minutes' | 'seconds' = 'days'
|
||||
): number {
|
||||
const msDiff = end.getTime() - start.getTime();
|
||||
|
||||
switch (unit) {
|
||||
case 'seconds':
|
||||
return Math.floor(msDiff / 1000);
|
||||
case 'minutes':
|
||||
return Math.floor(msDiff / (1000 * 60));
|
||||
case 'hours':
|
||||
return Math.floor(msDiff / (1000 * 60 * 60));
|
||||
case 'days':
|
||||
return Math.floor(msDiff / (1000 * 60 * 60 * 24));
|
||||
default:
|
||||
return msDiff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default date;
|
@ -1,57 +0,0 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
import paths from './path';
|
||||
import date from './date';
|
||||
import logger from './logger';
|
||||
import chalk from 'chalk';
|
||||
|
||||
class fc {
|
||||
/**
|
||||
* 创建目录
|
||||
* @param targetPath 目标路径
|
||||
* @param includeFile 是否包含文件路径
|
||||
*/
|
||||
public static async createDir(
|
||||
targetPath: string = '',
|
||||
includeFile: boolean = false
|
||||
): Promise<void> {
|
||||
const root = paths.get('root');
|
||||
|
||||
try {
|
||||
if (path.isAbsolute(targetPath)) {
|
||||
const dirToCreate = includeFile ? path.dirname(targetPath) : targetPath;
|
||||
await fs.mkdir(dirToCreate, { recursive: true });
|
||||
//logger.debug(`成功创建绝对目录: ${dirToCreate}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const fullPath = includeFile
|
||||
? path.join(root, path.dirname(targetPath))
|
||||
: path.join(root, targetPath);
|
||||
|
||||
await fs.mkdir(fullPath, { recursive: true });
|
||||
//logger.debug(`成功创建相对目录: ${fullPath}`);
|
||||
} catch (err) {
|
||||
logger.error(`创建目录失败: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出日志到文件
|
||||
* @param message
|
||||
*/
|
||||
public static async logToFile(message: string): Promise<void> {
|
||||
const logFile = path.join(paths.get('log'), `${date.getCurrentDate()}.log`);
|
||||
const logMessage = `${message}\n`;
|
||||
|
||||
try {
|
||||
//await this.createDir(paths.get('log'));
|
||||
|
||||
await fs.appendFile(logFile, logMessage);
|
||||
} catch (err) {
|
||||
console.error(chalk.red('[LOGGER] 写入日志失败:'), err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default fc;
|
@ -1,69 +0,0 @@
|
||||
import chalk from 'chalk';
|
||||
import config from './config';
|
||||
import fc from './file';
|
||||
import date from './date';
|
||||
|
||||
class Logger {
|
||||
private static instance: Logger;
|
||||
private readonly isDebug: boolean;
|
||||
|
||||
private constructor() {
|
||||
this.isDebug = config.get('DEBUG', false);
|
||||
}
|
||||
|
||||
public static getInstance(): Logger {
|
||||
if (!Logger.instance) {
|
||||
Logger.instance = new Logger();
|
||||
}
|
||||
return Logger.instance;
|
||||
}
|
||||
|
||||
public debug(...args: any[]): void {
|
||||
if (this.isDebug) {
|
||||
const message = this.formatMessage('DEBUG', args);
|
||||
console.log(chalk.cyan(message));
|
||||
}
|
||||
}
|
||||
|
||||
public info(...args: any[]): void {
|
||||
const message = this.formatMessage('INFO', args);
|
||||
console.log(chalk.green(message));
|
||||
this.logToFile(message).then();
|
||||
}
|
||||
|
||||
public warn(...args: any[]): void {
|
||||
const message = this.formatMessage('WARN', args);
|
||||
console.log(chalk.yellow(message));
|
||||
this.logToFile(message).then();
|
||||
}
|
||||
|
||||
public error(...args: any[]): void {
|
||||
const message = this.formatMessage('ERROR', args);
|
||||
console.error(chalk.red(message));
|
||||
this.logToFile(message).then();
|
||||
}
|
||||
|
||||
public fatal(exitCode: number = 1, ...args: any[]): never {
|
||||
const message = this.formatMessage('FATAL', args);
|
||||
console.error(chalk.red.bold(message));
|
||||
this.logToFile(message).then();
|
||||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
private formatMessage(level: string, args: any[]): string {
|
||||
return `[${date.getCurrentTime()}][${level}] ${args
|
||||
.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : arg))
|
||||
.join(' ')}`;
|
||||
}
|
||||
|
||||
private async logToFile(message: string): Promise<void> {
|
||||
try {
|
||||
await fc.logToFile(`${message}`);
|
||||
} catch (err: any) {
|
||||
console.error(chalk.red(`[LOGGER] 写入日志失败: ${err.message}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const logger = Logger.getInstance();
|
||||
export default logger;
|
@ -1,98 +0,0 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import fc from './file';
|
||||
|
||||
class PathManager {
|
||||
private static instance: PathManager;
|
||||
private readonly baseDir: string;
|
||||
|
||||
private constructor() {
|
||||
this.baseDir = path.join(__dirname, '../../..');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*/
|
||||
public static getInstance(): PathManager {
|
||||
if (!PathManager.instance) {
|
||||
PathManager.instance = new PathManager();
|
||||
}
|
||||
return PathManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预定义路径
|
||||
* @param type 路径类型
|
||||
*/
|
||||
public get(type?: PathType): string {
|
||||
const mappings: Record<PathType, string> = {
|
||||
root: this.baseDir,
|
||||
public: path.join(this.baseDir, 'public'),
|
||||
images: path.join(this.baseDir, 'public/files/image'),
|
||||
log: path.join(this.baseDir, 'logs'),
|
||||
config: path.join(this.baseDir, 'config'),
|
||||
temp: path.join(this.baseDir, 'temp'),
|
||||
userData: path.join(this.baseDir, 'private/data'),
|
||||
files: path.join(this.baseDir, 'public/files'),
|
||||
media: path.join(this.baseDir, 'public/files/media'),
|
||||
package: path.join(this.baseDir, 'package.json'),
|
||||
modules: path.join(this.baseDir, 'src/modules'),
|
||||
uploads: path.join(this.baseDir, 'public/files/uploads'),
|
||||
words: path.join(this.baseDir, 'private/data/word'),
|
||||
};
|
||||
|
||||
return type ? mappings[type] : this.baseDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
public init(): void {
|
||||
/*
|
||||
const logPath = this.get('log');
|
||||
const imagePath = this.get('images');
|
||||
const dataPath = this.get('userData');
|
||||
const mediaPath = this.get('media');
|
||||
fc.createDir(logPath, false);
|
||||
fc.createDir(imagePath, false);
|
||||
fc.createDir(mediaPath, false);
|
||||
fc.createDir(dataPath, false);
|
||||
logger.debug(`日志目录初始化: ${logPath}`);
|
||||
logger.debug(`图像目录初始化: ${imagePath};${mediaPath}`);
|
||||
logger.debug(`用户数据目录初始化: ${dataPath}`);
|
||||
*/
|
||||
const pathsToInit = [
|
||||
this.get('log'),
|
||||
this.get('config'),
|
||||
this.get('userData'),
|
||||
this.get('media'),
|
||||
this.get('temp'),
|
||||
this.get('uploads'),
|
||||
this.get('words'),
|
||||
];
|
||||
|
||||
pathsToInit.forEach((dirPath) => {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fc.createDir(dirPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
type PathType =
|
||||
| 'root'
|
||||
| 'public'
|
||||
| 'images'
|
||||
| 'log'
|
||||
| 'config'
|
||||
| 'temp'
|
||||
| 'userData'
|
||||
| 'files'
|
||||
| 'package'
|
||||
| 'media'
|
||||
| 'modules'
|
||||
| 'words'
|
||||
| 'uploads';
|
||||
|
||||
const paths = PathManager.getInstance();
|
||||
export default paths;
|
@ -1,72 +0,0 @@
|
||||
import { Response } from 'express';
|
||||
import logger from './logger';
|
||||
|
||||
class response {
|
||||
/**
|
||||
* 成功响应
|
||||
* @param res Express响应对象
|
||||
* @param data 返回数据
|
||||
* @param statusCode HTTP状态码,默认200
|
||||
*/
|
||||
public static async success(res: Response, data: any, statusCode = 200) {
|
||||
res.status(statusCode).json({
|
||||
success: true,
|
||||
data: data,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误响应
|
||||
* @param res Express响应对象
|
||||
* @param message 错误信息
|
||||
* @param statusCode HTTP状态码,默认500
|
||||
* @param error 原始错误对象(开发环境显示)
|
||||
*/
|
||||
public static async error(
|
||||
res: Response,
|
||||
message: string = '请求失败..',
|
||||
statusCode = 500,
|
||||
error?: any
|
||||
) {
|
||||
const response: Record<string, any> = {
|
||||
success: false,
|
||||
data: message,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
logger.debug(error instanceof Error ? error.stack : error);
|
||||
|
||||
res.status(statusCode).json(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页数据响应
|
||||
* @param res Express响应对象
|
||||
* @param data 数据数组
|
||||
* @param total 总条数
|
||||
* @param page 当前页码
|
||||
* @param pageSize 每页条数
|
||||
*/
|
||||
public static async pagination(
|
||||
res: Response,
|
||||
data: any[],
|
||||
total: number,
|
||||
page: number,
|
||||
pageSize: number
|
||||
) {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data,
|
||||
pagination: {
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.ceil(total / pageSize),
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default response;
|
@ -1,40 +0,0 @@
|
||||
import path from 'path';
|
||||
import paths from './path';
|
||||
import fs from 'fs';
|
||||
import logger from './logger';
|
||||
|
||||
const restartFile = path.join(paths.get('temp'), 'restart.timestamp');
|
||||
class System {
|
||||
/**
|
||||
* 重启前保存时间戳
|
||||
*/
|
||||
private static markRestartTime() {
|
||||
const now = Date.now();
|
||||
fs.writeFileSync(restartFile, now.toString(), 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查重启时间戳
|
||||
*/
|
||||
public static checkRestartTime() {
|
||||
if (fs.existsSync(restartFile)) {
|
||||
const prev = Number(fs.readFileSync(restartFile, 'utf-8'));
|
||||
const duration = ((Date.now() - prev) / 1000 - 5).toFixed(2);
|
||||
fs.unlinkSync(restartFile);
|
||||
return Number(duration);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重启服务
|
||||
*/
|
||||
public static async restart() {
|
||||
this.markRestartTime();
|
||||
logger.warn('服务即将重启..');
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
export default System;
|
@ -1,48 +0,0 @@
|
||||
import RetryOptions from '../../types/retry';
|
||||
import logger from './logger';
|
||||
|
||||
let tools = {
|
||||
/**
|
||||
* 异步重试机制
|
||||
* @param operation
|
||||
* @param options
|
||||
*/
|
||||
async retry(operation: () => Promise<any>, options: RetryOptions): Promise<any> {
|
||||
let attempt = 0;
|
||||
let lastError: any;
|
||||
|
||||
while (attempt < options.maxAttempts) {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
attempt++;
|
||||
|
||||
if (attempt < options.maxAttempts) {
|
||||
const delay = options.initialDelay * Math.pow(2, attempt - 1);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.error(lastError);
|
||||
},
|
||||
|
||||
/**
|
||||
* 从一个可迭代列表中随机选择一个对象
|
||||
* @param list 可迭代数据
|
||||
*/
|
||||
getRandomItem<T>(list: T[]): T {
|
||||
return list[Math.floor(Math.random() * list.length)];
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取随机数
|
||||
* @param min 最小值
|
||||
* @param max 最大值
|
||||
*/
|
||||
getRandomDelay(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
},
|
||||
};
|
||||
|
||||
export default tools;
|
@ -1,29 +0,0 @@
|
||||
import express from 'express';
|
||||
import response from '../core/response';
|
||||
import Config from '../core/config';
|
||||
|
||||
let tools = {
|
||||
/**
|
||||
* token验证错误处理逻辑
|
||||
* @param res
|
||||
* @param token
|
||||
*/
|
||||
async tokenCheckFailed(res: express.Response, token: string): Promise<void> {
|
||||
await response.error(
|
||||
res,
|
||||
'token验证失败..',
|
||||
404,
|
||||
`有个小可爱使用了错误的token:${JSON.stringify(token)}`
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查token是否正确
|
||||
* @param token
|
||||
*/
|
||||
checkToken(token: string): boolean {
|
||||
return token.toString() === Config.get('TOKEN').toString();
|
||||
},
|
||||
};
|
||||
|
||||
export default tools;
|
@ -1,66 +0,0 @@
|
||||
import path from 'path';
|
||||
import paths from '../core/path';
|
||||
import fc from '../core/file';
|
||||
import logger from '../core/logger';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
class Persistence {
|
||||
private static getDataPath(dataName: string, fileName: string): string {
|
||||
return path.join(paths.get('userData'), dataName, `${fileName}.json`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保数据目录存在
|
||||
* @param dataName
|
||||
* @private
|
||||
*/
|
||||
private static async ensureDataPath(dataName: string): Promise<void> {
|
||||
const dataPath = path.join(paths.get('userData'), dataName);
|
||||
try {
|
||||
await fc.createDir(dataPath, false);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数据写入本地,以json格式存储
|
||||
* @param dataName 目录名
|
||||
* @param data 文件内容
|
||||
* @param fileName 文件名
|
||||
*/
|
||||
public static async writeDataLocal<T>(
|
||||
dataName: string,
|
||||
data: T,
|
||||
fileName: string
|
||||
): Promise<void> {
|
||||
await this.ensureDataPath(dataName);
|
||||
const filePath = this.getDataPath(dataName, fileName);
|
||||
|
||||
try {
|
||||
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
||||
logger.debug(`用户数据已持久化到本地${filePath}`);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地读取文件
|
||||
* @param dataName 目录名
|
||||
* @param fileName 文件名
|
||||
*/
|
||||
public static async readDataLocal<T>(dataName: string, fileName: string): Promise<T | undefined> {
|
||||
const filePath = this.getDataPath(dataName, fileName);
|
||||
|
||||
try {
|
||||
const data = await fs.readFile(filePath, 'utf-8');
|
||||
return JSON.parse(data) as T;
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Persistence;
|
@ -1,31 +0,0 @@
|
||||
import logger from '../core/logger';
|
||||
|
||||
class redisTools {
|
||||
public static serialize<T>(data: T): string {
|
||||
return JSON.stringify(data);
|
||||
}
|
||||
|
||||
public static deserialize<T>(jsonString: string): T | undefined {
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch (err) {
|
||||
logger.error(`redis反序列化失败:${err}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public static reviveDates<T>(obj: T): T {
|
||||
const dateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/;
|
||||
|
||||
const reviver = (_: string, value: any) => {
|
||||
if (typeof value === 'string' && dateRegex.test(value)) {
|
||||
return new Date(value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
return JSON.parse(JSON.stringify(obj), reviver);
|
||||
}
|
||||
}
|
||||
|
||||
const redisTool = redisTools;
|
||||
export default redisTool;
|
@ -1,49 +0,0 @@
|
||||
import WebSocket from 'ws';
|
||||
import logger from '../core/logger';
|
||||
import { setInterval } from 'node:timers';
|
||||
|
||||
class WsTools {
|
||||
/**
|
||||
* 发送消息
|
||||
* @param socket
|
||||
* @param data
|
||||
*/
|
||||
static async send(socket: WebSocket, data: unknown): Promise<boolean> {
|
||||
if (socket.readyState !== WebSocket.OPEN) return false;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
socket.send(JSON.stringify(data), (err) => {
|
||||
resolve(!err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析消息
|
||||
* @param data
|
||||
*/
|
||||
static parseMessage<T>(data: WebSocket.RawData): T | null {
|
||||
try {
|
||||
return JSON.parse(data.toString()) as T;
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 心跳检测
|
||||
* @param socket
|
||||
* @param interval
|
||||
*/
|
||||
static setUpHeartbeat(socket: WebSocket, interval = 30000): NodeJS.Timeout {
|
||||
const heartbeat = () => {
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
WsTools.send(socket, { type: 'ping' });
|
||||
}
|
||||
};
|
||||
return setInterval(heartbeat, interval);
|
||||
}
|
||||
}
|
||||
|
||||
export default WsTools;
|
4
tsconfig.build.json
Normal file
4
tsconfig.build.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
@ -1,12 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2023",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"]
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"noFallthroughCasesInSwitch": false
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user