mirror of
https://github.com/crystelf/crystelf-core.git
synced 2025-10-14 05:19:19 +00:00
Compare commits
14 Commits
4846e27e6a
...
0f303dfc5c
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0f303dfc5c | ||
839df5170a | |||
de264b1244 | |||
e775bcdf77 | |||
6809d07bcf | |||
35eea17a8f | |||
ccda3ec271 | |||
120bf912f1 | |||
3b546e50a1 | |||
08f74445da | |||
7c4b933e9d | |||
d91f900f49 | |||
3e3f8029e6 | |||
4100b4f0aa |
59
.gitignore
vendored
59
.gitignore
vendored
@ -1,15 +1,56 @@
|
|||||||
/tmp
|
# compiled output
|
||||||
/out-tsc
|
/dist
|
||||||
|
|
||||||
/node_modules
|
/node_modules
|
||||||
|
/build
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
/.pnp
|
lerna-debug.log*
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
.env
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
/coverage
|
||||||
|
/.nyc_output
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
.vscode/*
|
.vscode/*
|
||||||
/dist/
|
!.vscode/settings.json
|
||||||
/logs/
|
!.vscode/tasks.json
|
||||||
/private/
|
!.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" />
|
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
|
||||||
</VueCodeStyleSettings>
|
</VueCodeStyleSettings>
|
||||||
<codeStyleSettings language="HTML">
|
<codeStyleSettings language="HTML">
|
||||||
<option name="SOFT_MARGINS" value="100" />
|
<option name="SOFT_MARGINS" value="80" />
|
||||||
<indentOptions>
|
<indentOptions>
|
||||||
<option name="INDENT_SIZE" value="2" />
|
<option name="INDENT_SIZE" value="2" />
|
||||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
@ -34,7 +34,7 @@
|
|||||||
</indentOptions>
|
</indentOptions>
|
||||||
</codeStyleSettings>
|
</codeStyleSettings>
|
||||||
<codeStyleSettings language="JavaScript">
|
<codeStyleSettings language="JavaScript">
|
||||||
<option name="SOFT_MARGINS" value="100" />
|
<option name="SOFT_MARGINS" value="80" />
|
||||||
<indentOptions>
|
<indentOptions>
|
||||||
<option name="INDENT_SIZE" value="2" />
|
<option name="INDENT_SIZE" value="2" />
|
||||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
@ -42,7 +42,7 @@
|
|||||||
</indentOptions>
|
</indentOptions>
|
||||||
</codeStyleSettings>
|
</codeStyleSettings>
|
||||||
<codeStyleSettings language="TypeScript">
|
<codeStyleSettings language="TypeScript">
|
||||||
<option name="SOFT_MARGINS" value="100" />
|
<option name="SOFT_MARGINS" value="80" />
|
||||||
<indentOptions>
|
<indentOptions>
|
||||||
<option name="INDENT_SIZE" value="2" />
|
<option name="INDENT_SIZE" value="2" />
|
||||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
@ -50,7 +50,7 @@
|
|||||||
</indentOptions>
|
</indentOptions>
|
||||||
</codeStyleSettings>
|
</codeStyleSettings>
|
||||||
<codeStyleSettings language="Vue">
|
<codeStyleSettings language="Vue">
|
||||||
<option name="SOFT_MARGINS" value="100" />
|
<option name="SOFT_MARGINS" value="80" />
|
||||||
<indentOptions>
|
<indentOptions>
|
||||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
</indentOptions>
|
</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"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="PrettierConfiguration">
|
<component name="PrettierConfiguration">
|
||||||
<option name="myConfigurationMode" value="MANUAL" />
|
<option name="myConfigurationMode" value="AUTOMATIC" />
|
||||||
<option name="myRunOnSave" value="true" />
|
<option name="myRunOnSave" value="true" />
|
||||||
<option name="myRunOnReformat" value="true" />
|
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
2
.idea/vcs.xml
generated
2
.idea/vcs.xml
generated
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</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,
|
"singleQuote": true,
|
||||||
"printWidth": 100,
|
"trailingComma": "all"
|
||||||
"tabWidth": 2,
|
|
||||||
"trailingComma": "es5"
|
|
||||||
}
|
}
|
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
|
||||||
|
}
|
||||||
|
}
|
94
package.json
94
package.json
@ -1,33 +1,81 @@
|
|||||||
{
|
{
|
||||||
"name": "crystelf-core",
|
"name": "nest-backend",
|
||||||
"version": "1.0.0",
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"author": "",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "ts-node-dev src/main.ts",
|
"build": "nest build",
|
||||||
"start": "node dist/main.js",
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
"build": "tsc"
|
"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": {
|
"dependencies": {
|
||||||
"axios": "^1.8.4",
|
"@nestjs/axios": "^4.0.1",
|
||||||
"chalk": "4",
|
"@nestjs/common": "^11.0.1",
|
||||||
"compression": "^1.8.0",
|
"@nestjs/config": "^4.0.2",
|
||||||
"dotenv": "^16.0.0",
|
"@nestjs/core": "^11.0.1",
|
||||||
"express": "^4.18.0",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"ioredis": "^5.6.0",
|
"@nestjs/platform-socket.io": "^11.1.6",
|
||||||
"mkdirp": "^3.0.1",
|
"@nestjs/platform-ws": "^11.1.6",
|
||||||
"multer": "1.4.5-lts.2",
|
"@nestjs/swagger": "^11.2.0",
|
||||||
"simple-git": "^3.27.0",
|
"@nestjs/websockets": "^11.1.6",
|
||||||
|
"axios": "1.11.0",
|
||||||
|
"ioredis": "^5.6.1",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"simple-git": "^3.28.0",
|
||||||
|
"ssh2": "^1.16.0",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"ws": "^8.18.1"
|
"ws": "^8.18.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/compression": "^1.7.5",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
"@types/express": "^4.17.0",
|
"@eslint/js": "^9.18.0",
|
||||||
"@types/mkdirp": "^2.0.0",
|
"@nestjs/cli": "^11.0.0",
|
||||||
"@types/multer": "^1.4.12",
|
"@nestjs/schematics": "^11.0.0",
|
||||||
"@types/node": "^18.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",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"prettier": "^3.5.3",
|
"eslint": "^9.18.0",
|
||||||
"ts-node-dev": "^2.0.0",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"typescript": "^5.0.0"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
6856
pnpm-lock.yaml
generated
6856
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
33
src/app.module.ts
Normal file
33
src/app.module.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { RootModule } from './root/root.module';
|
||||||
|
import { AppConfigModule } from './config/config.module';
|
||||||
|
import { PathModule } from './core/path/path.module';
|
||||||
|
import { SystemModule } from './core/system/system.module';
|
||||||
|
import { ToolsModule } from './core/tools/tools.module';
|
||||||
|
import { AutoUpdateModule } from './core/auto-update/auto-update.module';
|
||||||
|
import { PersistenceModule } from './core/persistence/persistence.module';
|
||||||
|
import { RedisModule } from './core/redis/redis.module';
|
||||||
|
import { WsModule } from './core/ws/ws.module';
|
||||||
|
import { SystemWebModule } from './modules/system/systemWeb.module';
|
||||||
|
import { BotModule } from './modules/bot/bot.module';
|
||||||
|
import { CdnModule } from './modules/cdn/cdn.module';
|
||||||
|
import { WordsModule } from './modules/words/words.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
RootModule,
|
||||||
|
AppConfigModule,
|
||||||
|
PathModule,
|
||||||
|
SystemModule,
|
||||||
|
ToolsModule,
|
||||||
|
PersistenceModule,
|
||||||
|
AutoUpdateModule,
|
||||||
|
RedisModule,
|
||||||
|
WsModule,
|
||||||
|
SystemWebModule,
|
||||||
|
BotModule,
|
||||||
|
CdnModule,
|
||||||
|
WordsModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
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;
|
|
31
src/common/filters/all-exception.filter.ts
Normal file
31
src/common/filters/all-exception.filter.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
29
src/common/interceptors/response.interceptor.ts
Normal file
29
src/common/interceptors/response.interceptor.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
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: '欢迎使用晶灵核心 | Welcome to use crystelf-core',
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
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 {}
|
52
src/config/config.service.ts
Normal file
52
src/config/config.service.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取环境变量
|
||||||
|
* @param key 键值
|
||||||
|
* @param defaultValue 默认
|
||||||
|
*/
|
||||||
|
public 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
|
|
10
src/core/auto-update/auto-update.module.ts
Normal file
10
src/core/auto-update/auto-update.module.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AutoUpdateService } from './auto-update.service';
|
||||||
|
import { PathModule } from '../path/path.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PathModule],
|
||||||
|
providers: [AutoUpdateService],
|
||||||
|
exports: [AutoUpdateService],
|
||||||
|
})
|
||||||
|
export class AutoUpdateModule {}
|
112
src/core/auto-update/auto-update.service.ts
Normal file
112
src/core/auto-update/auto-update.service.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import simpleGit, { SimpleGit } from 'simple-git';
|
||||||
|
import { PathService } from '../path/path.service';
|
||||||
|
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AutoUpdateService {
|
||||||
|
private readonly logger = new Logger(AutoUpdateService.name);
|
||||||
|
private readonly git: SimpleGit;
|
||||||
|
private readonly repoPath: string;
|
||||||
|
|
||||||
|
constructor(@Inject(PathService) private readonly pathService: PathService) {
|
||||||
|
this.git = simpleGit();
|
||||||
|
this.repoPath = this.pathService.get('root');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查主仓库远程更新
|
||||||
|
*/
|
||||||
|
async checkForUpdates(): Promise<boolean> {
|
||||||
|
return this.checkRepoForUpdates(this.repoPath, 'crystelf-core');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查指定文件夹的更新
|
||||||
|
*/
|
||||||
|
async checkRepoForUpdates(
|
||||||
|
folderPath: string,
|
||||||
|
label = '子仓库',
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
this.logger.log(`[${label}] 检查仓库更新中...`);
|
||||||
|
|
||||||
|
const repoGit = simpleGit(folderPath);
|
||||||
|
const status = await repoGit.status();
|
||||||
|
|
||||||
|
if (status.ahead > 0) {
|
||||||
|
this.logger.warn(`[${label}] 检测到本地仓库有未提交的更改,跳过更新`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`[${label}] 正在获取远程仓库信息...`);
|
||||||
|
await repoGit.fetch();
|
||||||
|
|
||||||
|
const localBranch = status.current;
|
||||||
|
const diffSummary = await repoGit.diffSummary([
|
||||||
|
`${localBranch}..origin/${localBranch}`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (diffSummary.files.length > 0) {
|
||||||
|
this.logger.log(`[${label}] 检测到远程仓库有更新!`);
|
||||||
|
if (localBranch) {
|
||||||
|
this.logger.log(`[${label}] 正在拉取远程代码...`);
|
||||||
|
await repoGit.pull('origin', localBranch);
|
||||||
|
} else {
|
||||||
|
this.logger.error(`[${label}] 当前分支名称未知,无法执行拉取操作。`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`[${label}] 代码更新成功,开始更新依赖...`);
|
||||||
|
await this.updateDependencies(folderPath, label);
|
||||||
|
|
||||||
|
this.logger.log(`[${label}] 自动更新流程完成`);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
this.logger.log(`[${label}] 远程仓库没有新变化`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[${label}] 检查仓库更新失败:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动安装依赖和构建
|
||||||
|
*/
|
||||||
|
private async updateDependencies(
|
||||||
|
folderPath: string,
|
||||||
|
label = '仓库',
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.logger.log(`[${label}] 执行 pnpm install...`);
|
||||||
|
await execAsync('pnpm install', { cwd: folderPath });
|
||||||
|
this.logger.log(`[${label}] 依赖安装完成`);
|
||||||
|
|
||||||
|
const pkgPath = `${folderPath}/package.json`;
|
||||||
|
let pkgJson: any;
|
||||||
|
|
||||||
|
try {
|
||||||
|
pkgJson = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
||||||
|
} catch {
|
||||||
|
this.logger.warn(`[${label}] 未找到 package.json,跳过依赖构建`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pkgJson.scripts?.build) {
|
||||||
|
this.logger.log(`[${label}] 检测到 build 脚本,执行 pnpm build...`);
|
||||||
|
await execAsync('pnpm build', { cwd: folderPath });
|
||||||
|
this.logger.log(`[${label}] 构建完成`);
|
||||||
|
} else {
|
||||||
|
this.logger.log(`[${label}] 未检测到 build 脚本,跳过构建`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[${label}] 更新依赖或构建失败:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
src/core/files/files.module.ts
Normal file
10
src/core/files/files.module.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { PathModule } from '../path/path.module';
|
||||||
|
import { FilesService } from './files.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PathModule],
|
||||||
|
providers: [FilesService],
|
||||||
|
exports: [FilesService],
|
||||||
|
})
|
||||||
|
export class FilesModule {}
|
33
src/core/files/files.service.ts
Normal file
33
src/core/files/files.service.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import { PathService } from '../path/path.service';
|
||||||
|
@Injectable()
|
||||||
|
export class FilesService {
|
||||||
|
private readonly logger = new Logger(FilesService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(PathService)
|
||||||
|
private readonly paths: PathService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建目录
|
||||||
|
* @param targetPath 目标路径
|
||||||
|
* @param includeFile 是否包含文件路径
|
||||||
|
*/
|
||||||
|
async createDir(targetPath = '', includeFile = false): Promise<void> {
|
||||||
|
const root = this.paths.get('root');
|
||||||
|
try {
|
||||||
|
const dirToCreate = path.isAbsolute(targetPath)
|
||||||
|
? includeFile
|
||||||
|
? path.dirname(targetPath)
|
||||||
|
: targetPath
|
||||||
|
: path.join(root, includeFile ? path.dirname(targetPath) : targetPath);
|
||||||
|
|
||||||
|
await fs.mkdir(dirToCreate, { recursive: true });
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(`创建目录失败: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
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 {}
|
109
src/core/path/path.service.ts
Normal file
109
src/core/path/path.service.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PathService {
|
||||||
|
private readonly baseDir: string;
|
||||||
|
private readonly logger = new Logger(PathService.name);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
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'),
|
||||||
|
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'),
|
||||||
|
package: path.join(this.baseDir, 'package.json'),
|
||||||
|
modules: path.join(this.baseDir, 'src/modules'),
|
||||||
|
words: path.join(this.baseDir, 'private/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('temp'),
|
||||||
|
this.get('public'),
|
||||||
|
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'
|
||||||
|
| 'log'
|
||||||
|
| 'config'
|
||||||
|
| 'temp'
|
||||||
|
| 'userData'
|
||||||
|
| 'package'
|
||||||
|
| 'modules'
|
||||||
|
| 'words';
|
11
src/core/persistence/persistence.module.ts
Normal file
11
src/core/persistence/persistence.module.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { PersistenceService } from './persistence.service';
|
||||||
|
import { PathModule } from '../path/path.module';
|
||||||
|
import { FilesModule } from '../files/files.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PathModule, FilesModule],
|
||||||
|
providers: [PersistenceService],
|
||||||
|
exports: [PersistenceService],
|
||||||
|
})
|
||||||
|
export class PersistenceModule {}
|
77
src/core/persistence/persistence.service.ts
Normal file
77
src/core/persistence/persistence.service.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs/promises';
|
||||||
|
import { PathService } from '../path/path.service';
|
||||||
|
import { FilesService } from '../files/files.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PersistenceService {
|
||||||
|
private readonly logger = new Logger(PersistenceService.name);
|
||||||
|
|
||||||
|
private getDataPath(dataName: string, fileName: string): string {
|
||||||
|
return path.join(this.paths.get('userData'), dataName, `${fileName}.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(PathService)
|
||||||
|
private readonly paths: PathService,
|
||||||
|
@Inject(FilesService)
|
||||||
|
private readonly fileService: FilesService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保有某个目录
|
||||||
|
* @param dataName 目录名/资源名
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async ensureDataPath(dataName: string): Promise<void> {
|
||||||
|
const dataPath = path.join(this.paths.get('userData'), dataName);
|
||||||
|
try {
|
||||||
|
await this.fileService.createDir(dataPath, false);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error('目录创建失败:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将数据保存到本地 带格式化
|
||||||
|
* @param dataName 数据名 -> 文件夹名
|
||||||
|
* @param data 内容
|
||||||
|
* @param fileName 文件名
|
||||||
|
*/
|
||||||
|
public 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');
|
||||||
|
this.logger.debug(`用户数据已持久化到本地: ${filePath}`);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error('写入失败:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从本地读取数据 带解析
|
||||||
|
* @param dataName 数据名 -> 文件夹名
|
||||||
|
* @param fileName 文件名
|
||||||
|
*/
|
||||||
|
public 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) {
|
||||||
|
this.logger.error('读取失败:', err);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
src/core/redis/redis.module.ts
Normal file
12
src/core/redis/redis.module.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { RedisService } from './redis.service';
|
||||||
|
import { AppConfigModule } from '../../config/config.module';
|
||||||
|
import { ToolsModule } from '../tools/tools.module';
|
||||||
|
import { PersistenceModule } from '../persistence/persistence.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [AppConfigModule, ToolsModule, PersistenceModule],
|
||||||
|
providers: [RedisService],
|
||||||
|
exports: [RedisService],
|
||||||
|
})
|
||||||
|
export class RedisModule {}
|
185
src/core/redis/redis.service.ts
Normal file
185
src/core/redis/redis.service.ts
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
|
import Redis from 'ioredis';
|
||||||
|
import { RedisUtils } from './redis.utils';
|
||||||
|
import { AppConfigService } from '../../config/config.service';
|
||||||
|
import { ToolsService } from '../tools/tools.service';
|
||||||
|
import IUser from '../../types/user';
|
||||||
|
import { PersistenceService } from '../persistence/persistence.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RedisService implements OnModuleInit {
|
||||||
|
private readonly logger = new Logger(RedisService.name);
|
||||||
|
private client!: Redis;
|
||||||
|
private isConnected = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(AppConfigService)
|
||||||
|
private readonly config: AppConfigService,
|
||||||
|
@Inject(ToolsService)
|
||||||
|
private readonly tools: ToolsService,
|
||||||
|
@Inject(PersistenceService)
|
||||||
|
private readonly Persistence: PersistenceService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
await this.connectWithRetry();
|
||||||
|
this.setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async connectWithRetry(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.tools.retry(
|
||||||
|
async () => {
|
||||||
|
this.client = new Redis({
|
||||||
|
host: this.config.get('RD_ADD'),
|
||||||
|
port: Number(this.config.get('RD_PORT')),
|
||||||
|
retryStrategy: (times: number) => Math.min(times * 1000, 5000),
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.client.ping();
|
||||||
|
this.isConnected = true;
|
||||||
|
this.logger.log(
|
||||||
|
`Redis连接成功! 位于 ${this.config.get('RD_ADD')}:${this.config.get('RD_PORT')}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
maxAttempts: 5,
|
||||||
|
initialDelay: 1000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Redis连接失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupEventListeners(): void {
|
||||||
|
this.client.on('error', (err) => {
|
||||||
|
if (!err.message.includes('ECONNREFUSED')) {
|
||||||
|
this.logger.error('Redis错误:', err);
|
||||||
|
}
|
||||||
|
this.isConnected = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.on('ready', () => {
|
||||||
|
this.isConnected = true;
|
||||||
|
this.logger.log('Redis连接就绪!');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.on('reconnecting', () => {
|
||||||
|
this.logger.warn('Redis重新连接中...');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待redis就绪
|
||||||
|
*/
|
||||||
|
public async waitUntilReady(): Promise<void> {
|
||||||
|
if (this.isConnected) return;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const check = () =>
|
||||||
|
this.isConnected ? resolve() : setTimeout(check, 100);
|
||||||
|
check();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单一redis实例
|
||||||
|
*/
|
||||||
|
public getClient(): Redis {
|
||||||
|
if (!this.isConnected) {
|
||||||
|
this.logger.error('Redis未连接');
|
||||||
|
}
|
||||||
|
return this.client;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 断开连接
|
||||||
|
*/
|
||||||
|
public async disconnect(): Promise<void> {
|
||||||
|
await this.client.quit();
|
||||||
|
this.isConnected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 储存对象
|
||||||
|
* @param key 键
|
||||||
|
* @param value 键值
|
||||||
|
* @param ttl 缓存时间
|
||||||
|
*/
|
||||||
|
public async setObject<T>(
|
||||||
|
key: string,
|
||||||
|
value: T,
|
||||||
|
ttl?: number,
|
||||||
|
): Promise<void> {
|
||||||
|
const serialized = RedisUtils.serialize(value);
|
||||||
|
await this.client.set(key, serialized);
|
||||||
|
if (ttl) {
|
||||||
|
await this.client.expire(key, ttl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从redis中获取对象
|
||||||
|
* @param key 键
|
||||||
|
*/
|
||||||
|
public async getObject<T>(key: string): Promise<T | undefined> {
|
||||||
|
const serialized = await this.client.get(key);
|
||||||
|
if (!serialized) return undefined;
|
||||||
|
const deserialized = RedisUtils.deserialize<T>(serialized);
|
||||||
|
return RedisUtils.reviveDates(deserialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新redis中的呃对象
|
||||||
|
* @param key
|
||||||
|
* @param updates
|
||||||
|
*/
|
||||||
|
public async update<T>(key: string, updates: T): Promise<T> {
|
||||||
|
const existing = await this.getObject<T>(key);
|
||||||
|
if (!existing) {
|
||||||
|
this.logger.error(`数据${key}不存在`);
|
||||||
|
}
|
||||||
|
const updated = { ...existing, ...updates };
|
||||||
|
await this.setObject(key, updated);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从本地或redis获取对象
|
||||||
|
* @param key 键 / 文件夹名
|
||||||
|
* @param fileName 文件名
|
||||||
|
*/
|
||||||
|
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 this.Persistence.readDataLocal<T>(key, fileName);
|
||||||
|
if (fromLocal) {
|
||||||
|
await this.setObject(key, fromLocal);
|
||||||
|
return fromLocal;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.error(`数据${key}不存在`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存对象
|
||||||
|
* @param key 键
|
||||||
|
* @param data 内容
|
||||||
|
* @param fileName 文件名
|
||||||
|
*/
|
||||||
|
public async persistData<T>(
|
||||||
|
key: string,
|
||||||
|
data: T,
|
||||||
|
fileName: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.setObject(key, data);
|
||||||
|
await this.Persistence.writeDataLocal(key, data, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async test(): Promise<void> {
|
||||||
|
const user = await this.fetch<IUser>('Jerry', 'IUser');
|
||||||
|
this.logger.debug('User:', user);
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,13 @@
|
|||||||
import logger from '../core/logger';
|
import { Logger } from '@nestjs/common';
|
||||||
|
|
||||||
class redisTools {
|
const logger = new Logger('RedisUtils');
|
||||||
public static serialize<T>(data: T): string {
|
|
||||||
|
export class RedisUtils {
|
||||||
|
static serialize<T>(data: T): string {
|
||||||
return JSON.stringify(data);
|
return JSON.stringify(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static deserialize<T>(jsonString: string): T | undefined {
|
static deserialize<T>(jsonString: string): T | undefined {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(jsonString);
|
return JSON.parse(jsonString);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -14,7 +16,7 @@ class redisTools {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static reviveDates<T>(obj: T): T {
|
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 dateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/;
|
||||||
|
|
||||||
const reviver = (_: string, value: any) => {
|
const reviver = (_: string, value: any) => {
|
||||||
@ -26,6 +28,3 @@ class redisTools {
|
|||||||
return JSON.parse(JSON.stringify(obj), reviver);
|
return JSON.parse(JSON.stringify(obj), reviver);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const redisTool = redisTools;
|
|
||||||
export default redisTool;
|
|
11
src/core/system/system.module.ts
Normal file
11
src/core/system/system.module.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { SystemService } from './system.service';
|
||||||
|
import { PathModule } from '../path/path.module';
|
||||||
|
import { AutoUpdateModule } from '../auto-update/auto-update.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PathModule, AutoUpdateModule],
|
||||||
|
providers: [SystemService],
|
||||||
|
exports: [SystemService],
|
||||||
|
})
|
||||||
|
export class SystemModule {}
|
68
src/core/system/system.service.ts
Normal file
68
src/core/system/system.service.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { PathService } from '../path/path.service';
|
||||||
|
import { AutoUpdateService } from '../auto-update/auto-update.service';
|
||||||
|
import * as process from 'node:process';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SystemService {
|
||||||
|
private readonly logger = new Logger(SystemService.name);
|
||||||
|
private readonly restartFile: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(PathService)
|
||||||
|
private readonly pathService: PathService,
|
||||||
|
@Inject(AutoUpdateService)
|
||||||
|
private readonly autoUpdateService: AutoUpdateService,
|
||||||
|
) {
|
||||||
|
this.restartFile = path.join(
|
||||||
|
this.pathService.get('temp'),
|
||||||
|
'restart.timestamp',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重启前保存时间戳
|
||||||
|
*/
|
||||||
|
private markRestartTime(): void {
|
||||||
|
const now = Date.now();
|
||||||
|
fs.writeFileSync(this.restartFile, now.toString(), 'utf-8');
|
||||||
|
this.logger.debug(`记录重启时间戳: ${now}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查重启时间戳
|
||||||
|
*/
|
||||||
|
checkRestartTime(): number | null {
|
||||||
|
if (fs.existsSync(this.restartFile)) {
|
||||||
|
const prev = Number(fs.readFileSync(this.restartFile, 'utf-8'));
|
||||||
|
const duration = ((Date.now() - prev) / 1000 - 5).toFixed(2);
|
||||||
|
fs.unlinkSync(this.restartFile);
|
||||||
|
this.logger.debug(`检测到重启,耗时: ${duration}秒`);
|
||||||
|
return Number(duration);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重启服务
|
||||||
|
*/
|
||||||
|
async restart(): Promise<void> {
|
||||||
|
this.markRestartTime();
|
||||||
|
this.logger.warn('服务即将重启..');
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查更新
|
||||||
|
*/
|
||||||
|
async checkUpdate(): Promise<void> {
|
||||||
|
const updated = await this.autoUpdateService.checkForUpdates();
|
||||||
|
if (updated) {
|
||||||
|
this.logger.warn('系统代码已更新,正在重启..');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
src/core/tools/retry-options.interface.ts
Normal file
4
src/core/tools/retry-options.interface.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export interface RetryOptions {
|
||||||
|
maxAttempts: number;
|
||||||
|
initialDelay: number;
|
||||||
|
}
|
37
src/core/tools/token-auth.guard.ts
Normal file
37
src/core/tools/token-auth.guard.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
UnauthorizedException,
|
||||||
|
Logger,
|
||||||
|
Inject,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ToolsService } from './tools.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* token验证守卫
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class TokenAuthGuard implements CanActivate {
|
||||||
|
private readonly logger = new Logger(TokenAuthGuard.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(ToolsService) private readonly toolsService: ToolsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
canActivate(context: ExecutionContext): boolean {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const token = request.body?.token || request.headers['x-token']; //两种传入方式
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
this.logger.warn('请求缺少 token');
|
||||||
|
throw new UnauthorizedException('缺少 token');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.toolsService.checkToken(token)) {
|
||||||
|
this.toolsService.tokenCheckFailed(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
10
src/core/tools/tools.module.ts
Normal file
10
src/core/tools/tools.module.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ToolsService } from './tools.service';
|
||||||
|
import { AppConfigModule } from '../../config/config.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [AppConfigModule],
|
||||||
|
providers: [ToolsService],
|
||||||
|
exports: [ToolsService],
|
||||||
|
})
|
||||||
|
export class ToolsModule {}
|
82
src/core/tools/tools.service.ts
Normal file
82
src/core/tools/tools.service.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import {
|
||||||
|
Inject,
|
||||||
|
Injectable,
|
||||||
|
Logger,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { RetryOptions } from './retry-options.interface';
|
||||||
|
import { AppConfigService } from '../../config/config.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ToolsService {
|
||||||
|
private readonly logger = new Logger(ToolsService.name);
|
||||||
|
constructor(
|
||||||
|
@Inject(AppConfigService)
|
||||||
|
private readonly config: AppConfigService,
|
||||||
|
) {}
|
||||||
|
/**
|
||||||
|
* 异步重试
|
||||||
|
* @param operation
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
async retry<T>(
|
||||||
|
operation: () => Promise<T>,
|
||||||
|
options: RetryOptions,
|
||||||
|
): Promise<T> {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.error('重试失败', lastError);
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从一个可迭代列表中随机选择一个对象
|
||||||
|
*/
|
||||||
|
getRandomItem<T>(list: T[]): T {
|
||||||
|
return list[Math.floor(Math.random() * list.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取随机数
|
||||||
|
*/
|
||||||
|
getRandomDelay(min: number, max: number): number {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查 token 是否有效
|
||||||
|
* @param token 待验证的 token
|
||||||
|
*/
|
||||||
|
checkToken(token: string): boolean {
|
||||||
|
const expected = this.config.get<string>('TOKEN');
|
||||||
|
if (!expected) {
|
||||||
|
this.logger.error('环境变量 TOKEN 未配置,无法进行验证!');
|
||||||
|
throw new UnauthorizedException('系统配置错误,缺少 TOKEN');
|
||||||
|
}
|
||||||
|
return token === expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* token 验证失败时的逻辑
|
||||||
|
* @param token 无效的 token
|
||||||
|
*/
|
||||||
|
tokenCheckFailed(token: string): never {
|
||||||
|
this.logger.warn(`有个小可爱使用了错误的 token: ${JSON.stringify(token)}`);
|
||||||
|
throw new UnauthorizedException('token 验证失败..');
|
||||||
|
}
|
||||||
|
}
|
12
src/core/ws/handlers/ping.handler.ts
Normal file
12
src/core/ws/handlers/ping.handler.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { WsTools } from '../ws.tools';
|
||||||
|
import { IMessageHandler } from 'src/types/ws/ws.handlers.interface';
|
||||||
|
import { AuthenticatedSocket } from '../../../types/ws/ws.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PingHandler implements IMessageHandler {
|
||||||
|
type = 'ping';
|
||||||
|
async handle(socket: AuthenticatedSocket, msg: any) {
|
||||||
|
await WsTools.send(socket, { type: 'pong' });
|
||||||
|
}
|
||||||
|
}
|
12
src/core/ws/handlers/pong.handler.ts
Normal file
12
src/core/ws/handlers/pong.handler.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AuthenticatedSocket } from '../../../types/ws/ws.interface';
|
||||||
|
import { IMessageHandler } from '../../../types/ws/ws.handlers.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PongHandler implements IMessageHandler {
|
||||||
|
type = 'pong';
|
||||||
|
|
||||||
|
async handle(socket: AuthenticatedSocket, msg: any) {
|
||||||
|
//this.logger.debug(`收到 pong 消息: ${JSON.stringify(msg)}`);
|
||||||
|
}
|
||||||
|
}
|
26
src/core/ws/handlers/report-bots.handler.ts
Normal file
26
src/core/ws/handlers/report-bots.handler.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Logger } from '@nestjs/common';
|
||||||
|
import { IMessageHandler } from '../../../types/ws/ws.handlers.interface';
|
||||||
|
import { RedisService } from '../../redis/redis.service';
|
||||||
|
import { AuthenticatedSocket } from '../../../types/ws/ws.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ReportBotsHandler implements IMessageHandler {
|
||||||
|
type = 'reportBots';
|
||||||
|
private readonly logger = new Logger(ReportBotsHandler.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(RedisService)
|
||||||
|
private readonly redisService: RedisService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async handle(socket: AuthenticatedSocket, msg: any) {
|
||||||
|
this.logger.debug(`received reportBots: ${msg.data}`);
|
||||||
|
const clientId = msg.data[0].client;
|
||||||
|
const botsData = msg.data.slice(1);
|
||||||
|
await this.redisService.persistData('crystelfBots', botsData, clientId);
|
||||||
|
this.logger.debug(
|
||||||
|
`保存了 ${botsData.length} 个 bot(client: ${clientId})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
16
src/core/ws/handlers/test.handler.ts
Normal file
16
src/core/ws/handlers/test.handler.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { WsTools } from '../ws.tools';
|
||||||
|
import { IMessageHandler } from '../../../types/ws/ws.handlers.interface';
|
||||||
|
import { AuthenticatedSocket } from '../../../types/ws/ws.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TestHandler implements IMessageHandler {
|
||||||
|
type = 'test';
|
||||||
|
|
||||||
|
async handle(socket: AuthenticatedSocket, msg: any) {
|
||||||
|
await WsTools.send(socket, {
|
||||||
|
type: 'test',
|
||||||
|
data: { status: 'ok' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
18
src/core/ws/handlers/unknown.handler.ts
Normal file
18
src/core/ws/handlers/unknown.handler.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { WsTools } from '../ws.tools';
|
||||||
|
import { IMessageHandler } from '../../../types/ws/ws.handlers.interface';
|
||||||
|
import { AuthenticatedSocket } from '../../../types/ws/ws.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UnknownHandler implements IMessageHandler {
|
||||||
|
type = 'unknown';
|
||||||
|
private readonly logger = new Logger(UnknownHandler.name);
|
||||||
|
|
||||||
|
async handle(socket: AuthenticatedSocket, msg: any) {
|
||||||
|
this.logger.warn(`收到未知消息类型: ${msg.type}`);
|
||||||
|
await WsTools.send(socket, {
|
||||||
|
type: 'error',
|
||||||
|
message: `未知消息类型: ${msg.type}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,56 +1,60 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
import WebSocket from 'ws';
|
import WebSocket from 'ws';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { clearTimeout } from 'node:timers';
|
|
||||||
|
|
||||||
type ClientID = string;
|
type ClientID = string;
|
||||||
const pendingRequests = new Map<string, (data: any) => void>();
|
const pendingRequests = new Map<string, (data: any) => void>();
|
||||||
|
|
||||||
class WSClientManager {
|
/**
|
||||||
|
* 客户端管理
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class WsClientManager {
|
||||||
private clients = new Map<ClientID, WebSocket>();
|
private clients = new Map<ClientID, WebSocket>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加ws客户端实例
|
* 增添新的客户端
|
||||||
* @param id 标识符
|
* @param id 编号
|
||||||
* @param socket
|
* @param socket 客户端
|
||||||
*/
|
*/
|
||||||
public add(id: ClientID, socket: WebSocket) {
|
add(id: ClientID, socket: WebSocket) {
|
||||||
this.clients.set(id, socket);
|
this.clients.set(id, socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 移除ws客户端实例
|
* 移除客户端
|
||||||
* @param id
|
* @param id 编号
|
||||||
*/
|
*/
|
||||||
public remove(id: ClientID) {
|
remove(id: ClientID) {
|
||||||
this.clients.delete(id);
|
this.clients.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取ws客户端实例
|
* 获取客户端单例
|
||||||
* @param id
|
* @param id 编号
|
||||||
*/
|
*/
|
||||||
public get(id: ClientID): WebSocket | undefined {
|
get(id: ClientID): WebSocket | undefined {
|
||||||
return this.clients.get(id);
|
return this.clients.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送消息到ws客户端
|
* 发送消息到客户端
|
||||||
* @param id ws客户端标识符
|
* @param id 编号
|
||||||
* @param data 要发送的内容
|
* @param data 要发送的信息
|
||||||
*/
|
*/
|
||||||
public async send(id: ClientID, data: any): Promise<boolean> {
|
async send(id: ClientID, data: any): Promise<boolean> {
|
||||||
const socket = this.clients.get(id);
|
const socket = this.clients.get(id);
|
||||||
if (!socket || socket.readyState !== WebSocket.OPEN) return false;
|
if (!socket || socket.readyState !== WebSocket.OPEN) return false;
|
||||||
return this.safeSend(socket, data);
|
return this.safeSend(socket, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ws发送请求&等待回调
|
* 发送消息并等待返回
|
||||||
* @param id ws客户端标识符-id
|
* @param id 编号
|
||||||
* @param data 发送的信息
|
* @param data 消息
|
||||||
* @param timeout 超时时间 默认5秒
|
* @param timeout
|
||||||
*/
|
*/
|
||||||
public async sendAndWait(id: ClientID, data: any, timeout = 5000): Promise<any> {
|
async sendAndWait(id: ClientID, data: any, timeout = 5000): Promise<any> {
|
||||||
const socket = this.clients.get(id);
|
const socket = this.clients.get(id);
|
||||||
if (!socket) return;
|
if (!socket) return;
|
||||||
|
|
||||||
@ -79,10 +83,10 @@ class WSClientManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理回调
|
* 处理回调
|
||||||
* @param requestId
|
* @param requestId 请求id
|
||||||
* @param data
|
* @param data 内容
|
||||||
*/
|
*/
|
||||||
public resolvePendingRequest(requestId: string, data: any): boolean {
|
resolvePendingRequest(requestId: string, data: any): boolean {
|
||||||
const callback = pendingRequests.get(requestId);
|
const callback = pendingRequests.get(requestId);
|
||||||
if (callback) {
|
if (callback) {
|
||||||
pendingRequests.delete(requestId);
|
pendingRequests.delete(requestId);
|
||||||
@ -93,10 +97,10 @@ class WSClientManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 广播消息到全部ws客户端
|
* 广播消息
|
||||||
* @param data 消息
|
* @param data 内容
|
||||||
*/
|
*/
|
||||||
public async broadcast(data: any): Promise<void> {
|
async broadcast(data: any): Promise<void> {
|
||||||
const tasks = Array.from(this.clients.values()).map((socket) => {
|
const tasks = Array.from(this.clients.values()).map((socket) => {
|
||||||
if (socket.readyState === WebSocket.OPEN) {
|
if (socket.readyState === WebSocket.OPEN) {
|
||||||
return this.safeSend(socket, data);
|
return this.safeSend(socket, data);
|
||||||
@ -108,9 +112,9 @@ class WSClientManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 安全发送消息到ws客户端
|
* 安全发送
|
||||||
* @param socket ws客户端
|
* @param socket
|
||||||
* @param data 发送的内容,会自动格式化
|
* @param data
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private async safeSend(socket: WebSocket, data: any): Promise<boolean> {
|
private async safeSend(socket: WebSocket, data: any): Promise<boolean> {
|
||||||
@ -122,6 +126,3 @@ class WSClientManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const wsClientManager = new WSClientManager();
|
|
||||||
export default wsClientManager;
|
|
56
src/core/ws/ws-message.handler.ts
Normal file
56
src/core/ws/ws-message.handler.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { WsTools } from './ws.tools';
|
||||||
|
import { WsClientManager } from './ws-client.manager';
|
||||||
|
import { IMessageHandler } from '../../types/ws/ws.handlers.interface';
|
||||||
|
import { AuthenticatedSocket } from '../../types/ws/ws.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WsMessageHandler {
|
||||||
|
private readonly logger = new Logger(WsMessageHandler.name);
|
||||||
|
private handlers = new Map<string, IMessageHandler>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly wsClientManager: WsClientManager,
|
||||||
|
@Inject('WS_HANDLERS') handlers: IMessageHandler[],
|
||||||
|
) {
|
||||||
|
handlers.forEach((h) => this.handlers.set(h.type, h));
|
||||||
|
this.logger.log(`已注册 ${handlers.length} 个 WS handler`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(socket: AuthenticatedSocket, clientId: string, msg: any) {
|
||||||
|
try {
|
||||||
|
// 如果是 pendingRequests 的回包
|
||||||
|
if (
|
||||||
|
msg.requestId &&
|
||||||
|
this.wsClientManager.resolvePendingRequest(msg.requestId, msg)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const handler =
|
||||||
|
this.handlers.get(msg.type) || this.handlers.get('unknown');
|
||||||
|
if (handler) {
|
||||||
|
await handler.handle(socket, msg);
|
||||||
|
} else {
|
||||||
|
await this.handleUnknown(socket, msg);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(`ws消息处理时出错: ${err}`);
|
||||||
|
await WsTools.send(socket, {
|
||||||
|
type: 'error',
|
||||||
|
message: 'error message',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleUnknown(socket: AuthenticatedSocket, msg: any) {
|
||||||
|
this.logger.warn(`收到未知消息类型: ${msg.type}`);
|
||||||
|
await WsTools.send(socket, {
|
||||||
|
type: 'error',
|
||||||
|
message: `未知消息类型: ${msg.type}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public registerHandler(handler: IMessageHandler): void {
|
||||||
|
this.handlers.set(handler.type, handler);
|
||||||
|
}
|
||||||
|
}
|
153
src/core/ws/ws.gateway.ts
Normal file
153
src/core/ws/ws.gateway.ts
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import {
|
||||||
|
OnGatewayConnection,
|
||||||
|
OnGatewayDisconnect,
|
||||||
|
WebSocketGateway,
|
||||||
|
WebSocketServer,
|
||||||
|
} from '@nestjs/websockets';
|
||||||
|
import { Inject, Logger } from '@nestjs/common';
|
||||||
|
import { Server, WebSocket } from 'ws';
|
||||||
|
import { WsTools } from './ws.tools';
|
||||||
|
import { WsClientManager } from './ws-client.manager';
|
||||||
|
import {
|
||||||
|
AuthenticatedSocket,
|
||||||
|
AuthMessage,
|
||||||
|
WSMessage,
|
||||||
|
} from '../../types/ws/ws.interface';
|
||||||
|
import { AppConfigService } from '../../config/config.service';
|
||||||
|
import { WsMessageHandler } from './ws-message.handler';
|
||||||
|
|
||||||
|
@WebSocketGateway(7001, {
|
||||||
|
cors: { origin: '*' },
|
||||||
|
driver: 'ws',
|
||||||
|
})
|
||||||
|
export class WsGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
||||||
|
private readonly logger = new Logger(WsGateway.name);
|
||||||
|
private readonly secret: string | undefined;
|
||||||
|
|
||||||
|
@WebSocketServer()
|
||||||
|
server: Server;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(AppConfigService)
|
||||||
|
private readonly configService: AppConfigService,
|
||||||
|
@Inject(WsClientManager)
|
||||||
|
private readonly wsClientManager: WsClientManager,
|
||||||
|
@Inject(WsMessageHandler)
|
||||||
|
private readonly wsMessageHandler: WsMessageHandler,
|
||||||
|
) {
|
||||||
|
this.secret = this.configService.get<string>('WS_SECRET');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新的连接请求
|
||||||
|
* @param client 客户端
|
||||||
|
* @param req
|
||||||
|
*/
|
||||||
|
async handleConnection(client: AuthenticatedSocket, req: any) {
|
||||||
|
const ip = req.socket.remoteAddress || 'unknown';
|
||||||
|
this.logger.log(`收到来自 ${ip} 的 WebSocket 连接请求..`);
|
||||||
|
|
||||||
|
client.heartbeat = WsTools.setUpHeartbeat(client);
|
||||||
|
|
||||||
|
client.on('message', async (raw) => {
|
||||||
|
this.logger.debug(`Received raw message from ${ip}: ${raw.toString()}`);
|
||||||
|
|
||||||
|
const msg = WsTools.parseMessage<WSMessage>(raw);
|
||||||
|
if (!msg) return this.handleInvalidMessage(client, ip);
|
||||||
|
|
||||||
|
await this.routeMessage(client, msg, ip);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('error', (err) => {
|
||||||
|
this.logger.error(`WS error from ${ip}: ${err.message}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 断开某个连接
|
||||||
|
* @param client 客户端
|
||||||
|
*/
|
||||||
|
async handleDisconnect(client: AuthenticatedSocket) {
|
||||||
|
if (client.heartbeat) clearInterval(client.heartbeat);
|
||||||
|
if (client.clientId) {
|
||||||
|
this.wsClientManager.remove(client.clientId);
|
||||||
|
this.logger.log(`Removed client ${client.clientId} from manager`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 不合法消息
|
||||||
|
* @param client 客户端
|
||||||
|
* @param ip
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async handleInvalidMessage(client: WebSocket, ip: string) {
|
||||||
|
this.logger.warn(`Invalid message received from ${ip}`);
|
||||||
|
await WsTools.send(client, {
|
||||||
|
type: 'error',
|
||||||
|
message: 'Invalid message format',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息路由
|
||||||
|
* @param client 客户端
|
||||||
|
* @param msg 消息
|
||||||
|
* @param ip
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async routeMessage(
|
||||||
|
client: AuthenticatedSocket,
|
||||||
|
msg: WSMessage,
|
||||||
|
ip: string,
|
||||||
|
) {
|
||||||
|
if (!client.isAuthed) {
|
||||||
|
if (this.isAuthMessage(msg)) {
|
||||||
|
this.logger.log(`Attempting auth from ${ip} as ${msg.clientId}`);
|
||||||
|
await this.handleAuth(client, msg, ip);
|
||||||
|
} else {
|
||||||
|
this.logger.warn(
|
||||||
|
`Received message before auth from ${ip}: ${JSON.stringify(msg)}`,
|
||||||
|
);
|
||||||
|
await this.handleInvalidMessage(client, ip);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`Routing message from ${client.clientId}: ${JSON.stringify(msg)}`,
|
||||||
|
);
|
||||||
|
await this.wsMessageHandler.handle(client, client.clientId!, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isAuthMessage(msg: WSMessage): msg is AuthMessage {
|
||||||
|
return msg.type === 'auth';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接验证
|
||||||
|
* @param client 客户端
|
||||||
|
* @param msg 消息
|
||||||
|
* @param ip
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async handleAuth(
|
||||||
|
client: AuthenticatedSocket,
|
||||||
|
msg: AuthMessage,
|
||||||
|
ip: string,
|
||||||
|
) {
|
||||||
|
if (msg.secret === this.secret) {
|
||||||
|
client.isAuthed = true;
|
||||||
|
client.clientId = msg.clientId;
|
||||||
|
this.wsClientManager.add(msg.clientId, client);
|
||||||
|
this.logger.log(`Auth success from ${ip}, clientId: ${msg.clientId}`);
|
||||||
|
await WsTools.send(client, { type: 'auth', success: true });
|
||||||
|
} else {
|
||||||
|
this.logger.warn(
|
||||||
|
`Auth failed from ${ip} (invalid secret), clientId: ${msg.clientId}`,
|
||||||
|
);
|
||||||
|
await WsTools.send(client, { type: 'auth', success: false });
|
||||||
|
client.close(4001, 'Authentication failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
src/core/ws/ws.module.ts
Normal file
44
src/core/ws/ws.module.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { WsGateway } from './ws.gateway';
|
||||||
|
import { WsClientManager } from './ws-client.manager';
|
||||||
|
import { AppConfigModule } from '../../config/config.module';
|
||||||
|
import { WsMessageHandler } from './ws-message.handler';
|
||||||
|
import { TestHandler } from './handlers/test.handler';
|
||||||
|
import { PingHandler } from './handlers/ping.handler';
|
||||||
|
import { PongHandler } from './handlers/pong.handler';
|
||||||
|
import { ReportBotsHandler } from './handlers/report-bots.handler';
|
||||||
|
import { UnknownHandler } from './handlers/unknown.handler';
|
||||||
|
import { RedisModule } from '../redis/redis.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [AppConfigModule, RedisModule],
|
||||||
|
providers: [
|
||||||
|
WsGateway,
|
||||||
|
WsClientManager,
|
||||||
|
WsMessageHandler,
|
||||||
|
TestHandler,
|
||||||
|
PingHandler,
|
||||||
|
PongHandler,
|
||||||
|
ReportBotsHandler,
|
||||||
|
UnknownHandler,
|
||||||
|
{
|
||||||
|
provide: 'WS_HANDLERS',
|
||||||
|
useFactory: (
|
||||||
|
test: TestHandler,
|
||||||
|
ping: PingHandler,
|
||||||
|
pong: PongHandler,
|
||||||
|
reportBots: ReportBotsHandler,
|
||||||
|
unknown: UnknownHandler,
|
||||||
|
) => [test, ping, pong, reportBots, unknown],
|
||||||
|
inject: [
|
||||||
|
TestHandler,
|
||||||
|
PingHandler,
|
||||||
|
PongHandler,
|
||||||
|
ReportBotsHandler,
|
||||||
|
UnknownHandler,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
exports: [WsClientManager, WsMessageHandler, WsGateway],
|
||||||
|
})
|
||||||
|
export class WsModule {}
|
47
src/core/ws/ws.tools.ts
Normal file
47
src/core/ws/ws.tools.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import type { WebSocket, RawData } from 'ws';
|
||||||
|
import { Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
export class WsTools {
|
||||||
|
private static readonly logger = new Logger(WsTools.name);
|
||||||
|
|
||||||
|
static async send(socket: WebSocket, data: unknown): Promise<boolean> {
|
||||||
|
if (socket.readyState !== 1) {
|
||||||
|
this.logger.warn('尝试向非 OPEN 状态的 socket 发送消息,已丢弃');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
try {
|
||||||
|
socket.send(JSON.stringify(data), (err) => {
|
||||||
|
if (err) {
|
||||||
|
this.logger.error(`WS send error: ${err.message}`);
|
||||||
|
resolve(false);
|
||||||
|
} else {
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
this.logger.error(`WS send exception: ${err.message}`);
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static parseMessage<T>(data: RawData): T | null {
|
||||||
|
try {
|
||||||
|
return JSON.parse(data.toString()) as T;
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(`WS parse error: ${err}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static setUpHeartbeat(socket: WebSocket, interval = 30000): NodeJS.Timeout {
|
||||||
|
const heartbeat = async () => {
|
||||||
|
if (socket.readyState === 1) {
|
||||||
|
await WsTools.send(socket, { type: 'ping' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return setInterval(heartbeat, interval);
|
||||||
|
}
|
||||||
|
}
|
65
src/main.ts
65
src/main.ts
@ -1,31 +1,44 @@
|
|||||||
import apps from './app';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import logger from './utils/core/logger';
|
import { AppModule } from './app.module';
|
||||||
import config from './utils/core/config';
|
import { Logger, RequestMethod } from '@nestjs/common';
|
||||||
import redis from './services/redis/redis';
|
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||||
import autoUpdater from './utils/core/autoUpdater';
|
import { ResponseInterceptor } from './common/interceptors/response.interceptor';
|
||||||
import System from './utils/core/system';
|
import { AllExceptionsFilter } from './common/filters/all-exception.filter';
|
||||||
|
import { SystemService } from './core/system/system.service';
|
||||||
|
import { WsAdapter } from '@nestjs/platform-ws';
|
||||||
|
|
||||||
config.check(['PORT', 'DEBUG', 'RD_PORT', 'RD_ADD', 'WS_SECRET', 'WS_PORT']);
|
async function bootstrap() {
|
||||||
const PORT = config.get('PORT') || 3000;
|
Logger.log('晶灵核心初始化..');
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
apps
|
app.setGlobalPrefix('api', {
|
||||||
.createApp()
|
exclude: [
|
||||||
.then(async (app) => {
|
'cdn',
|
||||||
app.listen(PORT, () => {
|
{ path: 'cdn/(.*)', method: RequestMethod.ALL },
|
||||||
logger.info(`Crystelf-core listening on ${PORT}`);
|
'public',
|
||||||
|
{ path: 'public/(.*)', method: RequestMethod.ALL },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
const isUpdated = await autoUpdater.checkForUpdates();
|
app.useGlobalInterceptors(new ResponseInterceptor());
|
||||||
if (isUpdated) {
|
app.useGlobalFilters(new AllExceptionsFilter());
|
||||||
logger.warn(`检测到更新,正在重启..`);
|
const systemService = app.get(SystemService);
|
||||||
await System.restart();
|
const restartDuration = systemService.checkRestartTime();
|
||||||
|
if (restartDuration) {
|
||||||
|
new Logger('System').warn(`重启完成!耗时 ${restartDuration} 秒`);
|
||||||
}
|
}
|
||||||
})
|
const config = new DocumentBuilder()
|
||||||
.catch((err) => {
|
.setTitle('晶灵核心')
|
||||||
logger.error('Crystelf-core启动失败:', err);
|
.setDescription('为晶灵提供API服务')
|
||||||
process.exit(1);
|
.setVersion('1.0')
|
||||||
|
.build();
|
||||||
|
const document = () => SwaggerModule.createDocument(app, config);
|
||||||
|
SwaggerModule.setup('docs', app, document);
|
||||||
|
app.useWebSocketAdapter(new WsAdapter(app));
|
||||||
|
await app.listen(7000);
|
||||||
|
await systemService.checkUpdate().catch((err) => {
|
||||||
|
Logger.error(`自动更新失败: ${err?.message}`, '', 'System');
|
||||||
});
|
});
|
||||||
|
}
|
||||||
process.on('SIGTERM', async () => {
|
bootstrap().then(() => {
|
||||||
await redis.disconnect();
|
Logger.log(`API服务已启动:http://localhost:7000/api`);
|
||||||
process.exit(0);
|
Logger.log(`API文档: http://localhost:7000/docs`);
|
||||||
});
|
});
|
||||||
|
@ -1,156 +1,70 @@
|
|||||||
import express from 'express';
|
import { Body, Controller, Inject, Post, UseGuards } from '@nestjs/common';
|
||||||
import response from '../../utils/core/response';
|
import { ApiOperation, ApiTags, ApiBody } from '@nestjs/swagger';
|
||||||
import BotService from './bot.service';
|
import { BotService } from './bot.service';
|
||||||
import tools from '../../utils/modules/tools';
|
import { WsClientManager } from 'src/core/ws/ws-client.manager';
|
||||||
import logger from '../../utils/core/logger';
|
import { TokenAuthGuard } from 'src/core/tools/token-auth.guard';
|
||||||
import wsClientManager from '../../services/ws/wsClientManager';
|
import {
|
||||||
|
BroadcastDto,
|
||||||
|
GroupInfoDto,
|
||||||
|
SendMessageDto,
|
||||||
|
TokenDto,
|
||||||
|
} from './bot.dto';
|
||||||
|
|
||||||
class BotController {
|
@ApiTags('Bot相关操作')
|
||||||
private readonly router: express.Router;
|
@Controller('bot')
|
||||||
|
export class BotController {
|
||||||
|
constructor(
|
||||||
|
@Inject(BotService)
|
||||||
|
private readonly botService: BotService,
|
||||||
|
@Inject(WsClientManager)
|
||||||
|
private readonly wsClientManager: WsClientManager,
|
||||||
|
) {}
|
||||||
|
|
||||||
constructor() {
|
@Post('getBotId')
|
||||||
this.router = express.Router();
|
@UseGuards(TokenAuthGuard)
|
||||||
this.init();
|
@ApiOperation({ summary: '获取当前连接到核心的全部 botId 数组' })
|
||||||
|
async postBotsId(@Body() dto: TokenDto) {
|
||||||
|
return this.botService.getBotId();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRouter(): express.Router {
|
@Post('getGroupInfo')
|
||||||
return this.router;
|
@UseGuards(TokenAuthGuard)
|
||||||
|
@ApiOperation({ summary: '获取群聊信息' })
|
||||||
|
@ApiBody({ type: GroupInfoDto })
|
||||||
|
async postGroupInfo(@Body() dto: GroupInfoDto) {
|
||||||
|
return this.botService.getGroupInfo({ groupId: dto.groupId });
|
||||||
}
|
}
|
||||||
|
|
||||||
private init(): void {
|
@Post('reportBots')
|
||||||
this.router.post(`/getBotId`, this.postBotsId);
|
@UseGuards(TokenAuthGuard)
|
||||||
this.router.post('/getGroupInfo', this.postGroupInfo);
|
@ApiOperation({ summary: '广播:要求同步群聊信息和 bot 连接情况' })
|
||||||
this.router.post('/sendMessage', this.sendMessage);
|
async reportBots(@Body() dto: TokenDto) {
|
||||||
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 = {
|
const sendMessage = {
|
||||||
type: 'reportBots',
|
type: 'reportBots',
|
||||||
data: {},
|
data: {},
|
||||||
};
|
};
|
||||||
logger.info(`正在请求同步bot数据..`);
|
await this.wsClientManager.broadcast(sendMessage);
|
||||||
await response.success(res, {});
|
return { message: '正在请求同步 bot 数据..' };
|
||||||
await wsClientManager.broadcast(sendMessage);
|
|
||||||
} else {
|
|
||||||
await tools.tokenCheckFailed(res, token);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
await response.error(res);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
@Post('sendMessage')
|
||||||
* 发送消息到群聊,自动获取client
|
@UseGuards(TokenAuthGuard)
|
||||||
* @param req
|
@ApiOperation({ summary: '发送消息到群聊', description: '自动选择bot发送' })
|
||||||
* @param res
|
@ApiBody({ type: SendMessageDto })
|
||||||
*/
|
async sendMessage(@Body() dto: SendMessageDto) {
|
||||||
// TODO 测试接口可用性
|
const flag = await this.botService.sendMessage(dto.groupId, dto.message);
|
||||||
private sendMessage = async (req: express.Request, res: express.Response): Promise<void> => {
|
if (!flag) {
|
||||||
try {
|
return { message: '消息发送失败' };
|
||||||
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 {
|
return { message: '消息发送成功' };
|
||||||
await tools.tokenCheckFailed(res, token);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
await response.error(res);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
@Post('broadcast')
|
||||||
* 智能广播消息到全部群聊
|
@UseGuards(TokenAuthGuard)
|
||||||
* @param req
|
@ApiOperation({ summary: '广播消息到全部群聊', description: '随机延迟' })
|
||||||
* @param res
|
@ApiBody({ type: BroadcastDto })
|
||||||
*/
|
async smartBroadcast(@Body() dto: BroadcastDto) {
|
||||||
// TODO 测试接口可用性
|
await this.botService.broadcastToAllGroups(dto.message);
|
||||||
private smartBroadcast = async (req: express.Request, res: express.Response): Promise<void> => {
|
return { message: '广播任务已开始,正在后台执行..' };
|
||||||
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();
|
|
||||||
|
24
src/modules/bot/bot.dto.ts
Normal file
24
src/modules/bot/bot.dto.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class TokenDto {
|
||||||
|
@ApiProperty({ description: '访问核心的鉴权 token' })
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GroupInfoDto extends TokenDto {
|
||||||
|
@ApiProperty({ description: '群号', example: 114514 })
|
||||||
|
groupId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SendMessageDto extends GroupInfoDto {
|
||||||
|
@ApiProperty({ description: '要发送的消息', example: 'Ciallo~(∠・ω< )⌒★' })
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BroadcastDto extends TokenDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: '要广播的消息',
|
||||||
|
example: '全体目光向我看齐!我宣布个事儿..',
|
||||||
|
})
|
||||||
|
message: string;
|
||||||
|
}
|
14
src/modules/bot/bot.module.ts
Normal file
14
src/modules/bot/bot.module.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { RedisModule } from '../../core/redis/redis.module';
|
||||||
|
import { WsModule } from '../../core/ws/ws.module';
|
||||||
|
import { ToolsModule } from '../../core/tools/tools.module';
|
||||||
|
import { PathModule } from '../../core/path/path.module';
|
||||||
|
import { BotController } from './bot.controller';
|
||||||
|
import { BotService } from './bot.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [RedisModule, WsModule, ToolsModule, PathModule],
|
||||||
|
controllers: [BotController],
|
||||||
|
providers: [BotService],
|
||||||
|
})
|
||||||
|
export class BotModule {}
|
@ -1,18 +1,32 @@
|
|||||||
import logger from '../../utils/core/logger';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import paths from '../../utils/core/path';
|
import * as fs from 'fs/promises';
|
||||||
import fs from 'fs/promises';
|
import * as path from 'path';
|
||||||
import path from 'path';
|
import { RedisService } from 'src/core/redis/redis.service';
|
||||||
import redisService from '../../services/redis/redis';
|
import { WsClientManager } from 'src/core/ws/ws-client.manager';
|
||||||
import wsClientManager from '../../services/ws/wsClientManager';
|
import { ToolsService } from '../../core/tools/tools.service';
|
||||||
import tools from '../../utils/core/tool';
|
import { PathService } from '../../core/path/path.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BotService {
|
||||||
|
private readonly logger = new Logger(BotService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(RedisService)
|
||||||
|
private readonly redisService: RedisService,
|
||||||
|
@Inject(WsClientManager)
|
||||||
|
private readonly wsClientManager: WsClientManager,
|
||||||
|
@Inject(ToolsService)
|
||||||
|
private readonly tools: ToolsService,
|
||||||
|
@Inject(PathService)
|
||||||
|
private readonly paths: PathService,
|
||||||
|
) {}
|
||||||
|
|
||||||
class BotService {
|
|
||||||
/**
|
/**
|
||||||
* 获取botId数组
|
* 获取botId数组
|
||||||
*/
|
*/
|
||||||
public async getBotId(): Promise<{ uin: number; nickName: string }[]> {
|
async getBotId(): Promise<{ uin: number; nickName: string }[]> {
|
||||||
logger.debug('GetBotId..');
|
this.logger.debug('正在请求获取在线的bot..');
|
||||||
const userPath = paths.get('userData');
|
const userPath = this.paths.get('userData');
|
||||||
const botsPath = path.join(userPath, '/crystelfBots');
|
const botsPath = path.join(userPath, '/crystelfBots');
|
||||||
const dirData = await fs.readdir(botsPath);
|
const dirData = await fs.readdir(botsPath);
|
||||||
const uins: { uin: number; nickName: string }[] = [];
|
const uins: { uin: number; nickName: string }[] = [];
|
||||||
@ -21,7 +35,7 @@ class BotService {
|
|||||||
if (!fileName.endsWith('.json')) continue;
|
if (!fileName.endsWith('.json')) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const raw = await redisService.fetch('crystelfBots', fileName);
|
const raw = await this.redisService.fetch('crystelfBots', fileName);
|
||||||
if (!raw || !Array.isArray(raw)) continue;
|
if (!raw || !Array.isArray(raw)) continue;
|
||||||
|
|
||||||
for (const bot of raw) {
|
for (const bot of raw) {
|
||||||
@ -32,10 +46,9 @@ class BotService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`读取或解析 ${fileName} 出错: ${err}`);
|
this.logger.error(`读取或解析 ${fileName} 出错: ${err}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.debug(uins);
|
|
||||||
return uins;
|
return uins;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,15 +56,16 @@ class BotService {
|
|||||||
* 获取群聊信息
|
* 获取群聊信息
|
||||||
* @param data
|
* @param data
|
||||||
*/
|
*/
|
||||||
public async getGroupInfo(data: {
|
async getGroupInfo(data: {
|
||||||
botId?: number;
|
botId?: number;
|
||||||
groupId: number;
|
groupId: number;
|
||||||
clientId?: string;
|
clientId?: string;
|
||||||
}): Promise<any> {
|
}): Promise<any> {
|
||||||
logger.debug('GetGroupInfo..');
|
this.logger.debug(`正在尝试获取${data.groupId}的信息..)`);
|
||||||
const sendBot: number | undefined = data.botId ?? (await this.getGroupBot(data.groupId));
|
const sendBot: number | undefined =
|
||||||
|
data.botId ?? (await this.getGroupBot(data.groupId));
|
||||||
if (!sendBot) {
|
if (!sendBot) {
|
||||||
logger.warn(`不存在能向群聊${data.groupId}发送消息的Bot!`);
|
this.logger.warn(`不存在能向群聊${data.groupId}发送消息的Bot!`);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,7 +79,10 @@ class BotService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (sendData.data.clientID) {
|
if (sendData.data.clientID) {
|
||||||
const returnData = await wsClientManager.sendAndWait(sendData.data.clientID, sendData);
|
const returnData = await this.wsClientManager.sendAndWait(
|
||||||
|
sendData.data.clientID,
|
||||||
|
sendData,
|
||||||
|
);
|
||||||
return returnData ?? undefined;
|
return returnData ?? undefined;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -76,47 +93,42 @@ class BotService {
|
|||||||
* @param groupId 群号
|
* @param groupId 群号
|
||||||
* @param message 消息
|
* @param message 消息
|
||||||
*/
|
*/
|
||||||
public async sendMessage(groupId: number, message: string): Promise<boolean> {
|
async sendMessage(groupId: number, message: string): Promise<boolean> {
|
||||||
logger.info(`发送${message}到${groupId}..`);
|
this.logger.log(`发送${message}到${groupId}..`);
|
||||||
const sendBot = await this.getGroupBot(groupId);
|
const sendBot = await this.getGroupBot(groupId);
|
||||||
if (!sendBot) {
|
if (!sendBot) {
|
||||||
logger.warn(`不存在能向群聊${groupId}发送消息的Bot!`);
|
this.logger.warn(`不存在能向群聊${groupId}发送消息的Bot!`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const client = await this.getBotClient(sendBot);
|
const client = await this.getBotClient(sendBot);
|
||||||
if (!client) {
|
if (!client) {
|
||||||
logger.warn(`不存在${sendBot}对应的client!`);
|
this.logger.warn(`不存在${sendBot}对应的client!`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const sendData = {
|
const sendData = {
|
||||||
type: 'sendMessage',
|
type: 'sendMessage',
|
||||||
data: {
|
data: { botId: sendBot, groupId, clientId: client, message },
|
||||||
botId: sendBot,
|
|
||||||
groupId: groupId,
|
|
||||||
clientId: client,
|
|
||||||
message: message,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
await wsClientManager.send(client, sendData);
|
await this.wsClientManager.send(client, sendData);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 智能投放广播消息实现
|
* 广播消息
|
||||||
* @param message 要广播的消息
|
* @param message 要广播的消息
|
||||||
*/
|
*/
|
||||||
// TODO 添加群聊信誉分机制,低于30分的群聊不播报等..
|
async broadcastToAllGroups(message: string): Promise<void> {
|
||||||
public async broadcastToAllGroups(message: string): Promise<void> {
|
const userPath = this.paths.get('userData');
|
||||||
const userPath = paths.get('userData');
|
|
||||||
const botsPath = path.join(userPath, '/crystelfBots');
|
const botsPath = path.join(userPath, '/crystelfBots');
|
||||||
const dirData = await fs.readdir(botsPath);
|
const dirData = await fs.readdir(botsPath);
|
||||||
const groupMap: Map<number, { botId: number; clientId: string }[]> = new Map();
|
const groupMap: Map<number, { botId: number; clientId: string }[]> =
|
||||||
|
new Map();
|
||||||
|
this.logger.log(`广播消息:${message}`);
|
||||||
for (const fileName of dirData) {
|
for (const fileName of dirData) {
|
||||||
if (!fileName.endsWith('.json')) continue;
|
if (!fileName.endsWith('.json')) continue;
|
||||||
|
|
||||||
const clientId = path.basename(fileName, '.json');
|
const clientId = path.basename(fileName, '.json');
|
||||||
const botList = await redisService.fetch('crystelfBots', fileName);
|
const botList = await this.redisService.fetch('crystelfBots', fileName);
|
||||||
if (!Array.isArray(botList)) continue;
|
if (!Array.isArray(botList)) continue;
|
||||||
|
|
||||||
for (const bot of botList) {
|
for (const bot of botList) {
|
||||||
@ -139,7 +151,9 @@ class BotService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const [groupId, botEntries] of groupMap.entries()) {
|
for (const [groupId, botEntries] of groupMap.entries()) {
|
||||||
logger.debug(`[群 ${groupId}] 候选Bot列表: ${JSON.stringify(botEntries)}`);
|
this.logger.debug(
|
||||||
|
`[群 ${groupId}] 候选Bot列表: ${JSON.stringify(botEntries)}`,
|
||||||
|
);
|
||||||
|
|
||||||
const clientGroups = new Map<string, number[]>();
|
const clientGroups = new Map<string, number[]>();
|
||||||
botEntries.forEach(({ botId, clientId }) => {
|
botEntries.forEach(({ botId, clientId }) => {
|
||||||
@ -147,30 +161,32 @@ class BotService {
|
|||||||
clientGroups.get(clientId)!.push(botId);
|
clientGroups.get(clientId)!.push(botId);
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedClientId = tools.getRandomItem([...clientGroups.keys()]);
|
const selectedClientId = this.tools.getRandomItem([
|
||||||
|
...clientGroups.keys(),
|
||||||
|
]);
|
||||||
const botCandidates = clientGroups.get(selectedClientId)!;
|
const botCandidates = clientGroups.get(selectedClientId)!;
|
||||||
const selectedBotId = tools.getRandomItem(botCandidates);
|
const selectedBotId = this.tools.getRandomItem(botCandidates);
|
||||||
const delay = tools.getRandomDelay(10_000, 150_000);
|
const delay = this.tools.getRandomDelay(10_000, 150_000);
|
||||||
|
|
||||||
((groupId, selectedClientId, selectedBotId, delay) => {
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const sendData = {
|
const sendData = {
|
||||||
type: 'sendMessage',
|
type: 'sendMessage',
|
||||||
data: {
|
data: {
|
||||||
botId: selectedBotId,
|
botId: selectedBotId,
|
||||||
groupId: groupId,
|
groupId,
|
||||||
clientId: selectedClientId,
|
clientId: selectedClientId,
|
||||||
message: message,
|
message,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
logger.info(
|
this.logger.log(
|
||||||
`[广播] 向群 ${groupId} 使用Bot ${selectedBotId}(客户端 ${selectedClientId})发送消息${message},延迟 ${delay / 1000} 秒`
|
`[广播] 向群 ${groupId} 使用Bot ${selectedBotId}(客户端 ${selectedClientId})发送消息${message},延迟 ${
|
||||||
|
delay / 1000
|
||||||
|
} 秒`,
|
||||||
);
|
);
|
||||||
wsClientManager.send(selectedClientId, sendData).catch((e) => {
|
this.wsClientManager.send(selectedClientId, sendData).catch((e) => {
|
||||||
logger.error(`发送到群${groupId}失败:`, e);
|
this.logger.error(`发送到群${groupId}失败:`, e);
|
||||||
});
|
});
|
||||||
}, delay);
|
}, delay);
|
||||||
})(groupId, selectedClientId, selectedBotId, delay);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,7 +196,7 @@ class BotService {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private async getBotClient(botId: number): Promise<string | undefined> {
|
private async getBotClient(botId: number): Promise<string | undefined> {
|
||||||
const userPath = paths.get('userData');
|
const userPath = this.paths.get('userData');
|
||||||
const botsPath = path.join(userPath, '/crystelfBots');
|
const botsPath = path.join(userPath, '/crystelfBots');
|
||||||
const dirData = await fs.readdir(botsPath);
|
const dirData = await fs.readdir(botsPath);
|
||||||
|
|
||||||
@ -188,7 +204,7 @@ class BotService {
|
|||||||
if (!clientId.endsWith('.json')) continue;
|
if (!clientId.endsWith('.json')) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const raw = await redisService.fetch('crystelfBots', clientId);
|
const raw = await this.redisService.fetch('crystelfBots', clientId);
|
||||||
if (!Array.isArray(raw)) continue;
|
if (!Array.isArray(raw)) continue;
|
||||||
|
|
||||||
for (const bot of raw) {
|
for (const bot of raw) {
|
||||||
@ -198,7 +214,7 @@ class BotService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`读取${clientId}出错..`);
|
this.logger.error(`读取${clientId}出错..`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -210,7 +226,7 @@ class BotService {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private async getGroupBot(groupId: number): Promise<number | undefined> {
|
private async getGroupBot(groupId: number): Promise<number | undefined> {
|
||||||
const userPath = paths.get('userData');
|
const userPath = this.paths.get('userData');
|
||||||
const botsPath = path.join(userPath, '/crystelfBots');
|
const botsPath = path.join(userPath, '/crystelfBots');
|
||||||
const dirData = await fs.readdir(botsPath);
|
const dirData = await fs.readdir(botsPath);
|
||||||
|
|
||||||
@ -218,7 +234,7 @@ class BotService {
|
|||||||
if (!clientId.endsWith('.json')) continue;
|
if (!clientId.endsWith('.json')) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const raw = await redisService.fetch('crystelfBots', clientId);
|
const raw = await this.redisService.fetch('crystelfBots', clientId);
|
||||||
if (!Array.isArray(raw)) continue;
|
if (!Array.isArray(raw)) continue;
|
||||||
|
|
||||||
for (const bot of raw) {
|
for (const bot of raw) {
|
||||||
@ -231,11 +247,9 @@ class BotService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`读取${clientId}出错..`);
|
this.logger.error(`读取${clientId}出错..`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new BotService();
|
|
||||||
|
79
src/modules/cdn/cdn.controller.ts
Normal file
79
src/modules/cdn/cdn.controller.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Res,
|
||||||
|
Logger,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Inject,
|
||||||
|
Req,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { CdnService } from './cdn.service';
|
||||||
|
import { Response } from 'express';
|
||||||
|
import { ApiOperation } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class CdnController {
|
||||||
|
private readonly logger = new Logger(CdnController.name);
|
||||||
|
|
||||||
|
constructor(@Inject(CdnService) private readonly fileService: CdnService) {}
|
||||||
|
|
||||||
|
private async deliverFile(relativePath: string, res: Response) {
|
||||||
|
try {
|
||||||
|
this.logger.log(`有个小可爱正在请求 /cdn/${relativePath} ..`);
|
||||||
|
|
||||||
|
const filePath = await this.fileService.getFile(relativePath);
|
||||||
|
if (!filePath) {
|
||||||
|
this.logger.warn(`${relativePath}:文件不存在..`);
|
||||||
|
throw new HttpException('文件不存在啦!', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.sendFile(filePath, (err) => {
|
||||||
|
if (err) {
|
||||||
|
this.logger.error(`文件投递失败: ${err.message}`);
|
||||||
|
throw new HttpException(
|
||||||
|
'Crystelf-CDN处理文件请求时出错..',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`成功投递文件: ${filePath}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('晶灵数据请求处理失败:', error);
|
||||||
|
throw new HttpException(
|
||||||
|
'Crystelf-CDN处理文件请求时出错..',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('cdn/*')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '获取资源',
|
||||||
|
description: '由晶灵资源分发服务器(CDN)提供支持',
|
||||||
|
})
|
||||||
|
async getFile(@Res() res: Response, @Req() req: Request) {
|
||||||
|
const relativePath = req.url.replace('/cdn/', ''); //params.path;
|
||||||
|
return this.deliverFile(relativePath, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('public/files/*')
|
||||||
|
async fromPublicFiles(@Res() res: Response, @Req() req: Request) {
|
||||||
|
const relativePath = req.url.replace('/public/files/', '');
|
||||||
|
this.logger.debug(
|
||||||
|
`请求 /public/files/${relativePath} → 代理到 /cdn/${relativePath}`,
|
||||||
|
);
|
||||||
|
return this.deliverFile(relativePath, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('public/cdn/*')
|
||||||
|
async fromPublicCdn(@Req() req: Request, @Res() res: Response) {
|
||||||
|
const relativePath = req.url.replace('/public/cdn/', '');
|
||||||
|
this.logger.debug(
|
||||||
|
`请求 /public/cdn/${relativePath} → 代理到 /cdn/${relativePath}`,
|
||||||
|
);
|
||||||
|
return this.deliverFile(relativePath, res);
|
||||||
|
}
|
||||||
|
}
|
11
src/modules/cdn/cdn.module.ts
Normal file
11
src/modules/cdn/cdn.module.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { CdnController } from './cdn.controller';
|
||||||
|
import { CdnService } from './cdn.service';
|
||||||
|
import { PathModule } from '../../core/path/path.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PathModule],
|
||||||
|
controllers: [CdnController],
|
||||||
|
providers: [CdnService],
|
||||||
|
})
|
||||||
|
export class CdnModule {}
|
54
src/modules/cdn/cdn.service.ts
Normal file
54
src/modules/cdn/cdn.service.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
|
import { PathService } from '../../core/path/path.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CdnService {
|
||||||
|
private readonly logger = new Logger(CdnService.name);
|
||||||
|
private filePath: string;
|
||||||
|
@Inject(PathService)
|
||||||
|
private readonly paths: PathService;
|
||||||
|
constructor() {
|
||||||
|
this.logger.log(`晶灵云图数据中心初始化.. 数据存储在: ${this.filePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件
|
||||||
|
* @param relativePath 文件相对路径
|
||||||
|
*/
|
||||||
|
async getFile(relativePath: string): Promise<string | null> {
|
||||||
|
if (!this.filePath) this.filePath = this.paths.get('public');
|
||||||
|
if (
|
||||||
|
!this.isValidPath(relativePath) &&
|
||||||
|
!this.isValidFilename(path.basename(relativePath))
|
||||||
|
) {
|
||||||
|
throw new Error('非法路径请求');
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = path.join(this.filePath, relativePath);
|
||||||
|
this.logger.debug(`尝试访问文件路径: ${filePath}`);
|
||||||
|
|
||||||
|
return existsSync(filePath) ? filePath : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查路径合法性
|
||||||
|
*/
|
||||||
|
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) {
|
||||||
|
this.logger.error(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isValidFilename(filename: string): boolean {
|
||||||
|
return /^[a-zA-Z0-9_\-.]+$/.test(filename);
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
|
53
src/modules/system/systemWeb.controller.ts
Normal file
53
src/modules/system/systemWeb.controller.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { Controller, Post, Inject, UseGuards, Param } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation, ApiBody, ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { SystemWebService } from './systemWeb.service';
|
||||||
|
import { ToolsService } from '../../core/tools/tools.service';
|
||||||
|
import { TokenAuthGuard } from '../../core/tools/token-auth.guard';
|
||||||
|
|
||||||
|
class WebServerDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: '密钥',
|
||||||
|
example: '1111',
|
||||||
|
})
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiTags('System')
|
||||||
|
@Controller('system')
|
||||||
|
export class SystemWebController {
|
||||||
|
constructor(
|
||||||
|
@Inject(SystemWebService)
|
||||||
|
private readonly systemService: SystemWebService,
|
||||||
|
@Inject(ToolsService)
|
||||||
|
private readonly toolService: ToolsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重启系统
|
||||||
|
*/
|
||||||
|
@Post('restart')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '系统重启',
|
||||||
|
description: '核心执行重启',
|
||||||
|
})
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
|
@ApiBody({ type: WebServerDto })
|
||||||
|
async systemRestart(@Param('token') token: string): Promise<string> {
|
||||||
|
this.systemService.systemRestart();
|
||||||
|
return '核心正在重启..';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取系统重启耗时
|
||||||
|
*/
|
||||||
|
@Post('getRestartTime')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '获取重启所需时间',
|
||||||
|
description: '返回上次核心重启的耗时',
|
||||||
|
})
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
|
@ApiBody({ type: WebServerDto })
|
||||||
|
async getRestartTime(@Param('token') token: string): Promise<string> {
|
||||||
|
return await this.systemService.getRestartTime();
|
||||||
|
}
|
||||||
|
}
|
13
src/modules/system/systemWeb.module.ts
Normal file
13
src/modules/system/systemWeb.module.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { SystemWebController } from './systemWeb.controller';
|
||||||
|
import { SystemWebService } from './systemWeb.service';
|
||||||
|
import { ToolsModule } from '../../core/tools/tools.module';
|
||||||
|
import { PathModule } from '../../core/path/path.module';
|
||||||
|
import { SystemModule } from '../../core/system/system.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [ToolsModule, SystemModule, PathModule],
|
||||||
|
controllers: [SystemWebController],
|
||||||
|
providers: [SystemWebService],
|
||||||
|
})
|
||||||
|
export class SystemWebModule {}
|
34
src/modules/system/systemWeb.service.ts
Normal file
34
src/modules/system/systemWeb.service.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { PathService } from '../../core/path/path.service';
|
||||||
|
import { SystemService } from 'src/core/system/system.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SystemWebService {
|
||||||
|
private readonly logger = new Logger(SystemWebService.name);
|
||||||
|
@Inject(SystemService)
|
||||||
|
private readonly system: SystemService;
|
||||||
|
@Inject(PathService)
|
||||||
|
private readonly pathService: PathService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重启系统
|
||||||
|
*/
|
||||||
|
async systemRestart(): Promise<void> {
|
||||||
|
this.logger.debug(`有个小可爱正在请求重启核心..`);
|
||||||
|
await this.system.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取上次重启所耗时间
|
||||||
|
*/
|
||||||
|
async getRestartTime(): Promise<string> {
|
||||||
|
this.logger.debug(`有个小可爱想知道核心重启花了多久..`);
|
||||||
|
const restartTimePath = path.join(
|
||||||
|
this.pathService.get('temp'),
|
||||||
|
'restart_time',
|
||||||
|
);
|
||||||
|
return await fs.readFile(restartTimePath, 'utf8');
|
||||||
|
}
|
||||||
|
}
|
@ -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 +1,83 @@
|
|||||||
import express from 'express';
|
import {
|
||||||
import WordsService from './words.service';
|
Controller,
|
||||||
import response from '../../utils/core/response';
|
Get,
|
||||||
import tools from '../../utils/modules/tools';
|
Param,
|
||||||
|
Post,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Logger,
|
||||||
|
Inject,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { WordsService } from './words.service';
|
||||||
|
import { TokenAuthGuard } from '../../core/tools/token-auth.guard';
|
||||||
|
import { ApiBody, ApiOperation, ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
class WordsController {
|
class WordsDto {
|
||||||
private readonly router: express.Router;
|
@ApiProperty({
|
||||||
|
description: '文案id',
|
||||||
|
example: 'poke',
|
||||||
|
})
|
||||||
|
id: string;
|
||||||
|
@ApiProperty({
|
||||||
|
description: '密钥',
|
||||||
|
example: '1111',
|
||||||
|
})
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
@Controller('words')
|
||||||
this.router = express.Router();
|
export class WordsController {
|
||||||
this.init();
|
private readonly logger = new Logger(WordsController.name);
|
||||||
}
|
|
||||||
|
|
||||||
public getRouter(): express.Router {
|
constructor(
|
||||||
return this.router;
|
@Inject(WordsService) private readonly wordsService: WordsService,
|
||||||
}
|
) {}
|
||||||
|
|
||||||
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> => {
|
@Get('getText/:id')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '获取随机文案',
|
||||||
|
})
|
||||||
|
async getText(@Param('id') id: string) {
|
||||||
try {
|
try {
|
||||||
const id = req.params.id;
|
const texts = await this.wordsService.loadWordById(id);
|
||||||
const texts = await WordsService.loadWordById(id.toString());
|
|
||||||
if (!texts || texts.length === 0) {
|
if (!texts || texts.length === 0) {
|
||||||
return await response.error(res, `文案${id}不存在或为空..`, 404);
|
throw new HttpException(
|
||||||
|
`文案 ${id} 不存在或为空..`,
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const randomIndex = Math.floor(Math.random() * texts.length);
|
const randomIndex = Math.floor(Math.random() * texts.length);
|
||||||
const result = texts[randomIndex];
|
return texts[randomIndex];
|
||||||
await response.success(res, result);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await response.error(res);
|
this.logger.error(`getText 失败: ${e?.message}`);
|
||||||
|
throw new HttpException('服务器错误', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重载文案
|
* 重载文案
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
*/
|
||||||
private reloadWord = async (req: express.Request, res: express.Response): Promise<void> => {
|
@Post('reloadText/:id')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '重载某条文案',
|
||||||
|
})
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
|
@ApiBody({ type: WordsDto })
|
||||||
|
async reloadWord(@Param('id') id: string, @Param('token') token: string) {
|
||||||
try {
|
try {
|
||||||
const id = req.params.id;
|
const success = await this.wordsService.reloadWord(id);
|
||||||
const token = req.params.token;
|
if (success) {
|
||||||
if (tools.checkToken(token)) {
|
return '成功重载..';
|
||||||
if (await WordsService.reloadWord(id.toString())) {
|
|
||||||
await response.success(res, '成功重载..');
|
|
||||||
} else {
|
} else {
|
||||||
await response.error(res, '重载失败..');
|
throw new HttpException('重载失败..', HttpStatus.BAD_REQUEST);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await tools.tokenCheckFailed(res, token);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await response.error(res);
|
this.logger.error(`reloadWord 失败: ${e?.message}`);
|
||||||
|
throw new HttpException('服务器错误', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}
|
}
|
||||||
export default new WordsController();
|
|
||||||
|
14
src/modules/words/words.module.ts
Normal file
14
src/modules/words/words.module.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { WordsController } from './words.controller';
|
||||||
|
import { WordsService } from './words.service';
|
||||||
|
import { PathModule } from '../../core/path/path.module';
|
||||||
|
import { ToolsModule } from '../../core/tools/tools.module';
|
||||||
|
import { AutoUpdateModule } from '../../core/auto-update/auto-update.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PathModule, ToolsModule, AutoUpdateModule],
|
||||||
|
controllers: [WordsController],
|
||||||
|
providers: [WordsService],
|
||||||
|
exports: [WordsService],
|
||||||
|
})
|
||||||
|
export class WordsModule {}
|
@ -1,30 +1,62 @@
|
|||||||
import path from 'path';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import paths from '../../utils/core/path';
|
import * as path from 'path';
|
||||||
import fs from 'fs/promises';
|
import * as fs from 'fs/promises';
|
||||||
import logger from '../../utils/core/logger';
|
import { PathService } from '../../core/path/path.service';
|
||||||
|
import { AutoUpdateService } from '../../core/auto-update/auto-update.service';
|
||||||
|
|
||||||
class WordsService {
|
@Injectable()
|
||||||
private wordCache: Record<string, string[]> = {}; //缓存
|
export class WordsService {
|
||||||
private readonly clearIntervalMs = 30 * 60 * 1000; //30min
|
private readonly logger = new Logger(WordsService.name);
|
||||||
|
private wordCache: Record<string, string[]> = {};
|
||||||
|
private readonly clearIntervalMs = 30 * 60 * 1000; // 30min
|
||||||
|
|
||||||
|
@Inject(PathService)
|
||||||
|
private readonly paths: PathService;
|
||||||
|
|
||||||
|
@Inject(AutoUpdateService)
|
||||||
|
private readonly autoUpdateService: AutoUpdateService;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.startAutoClear();
|
this.startAutoClear();
|
||||||
|
this.startAutoUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动定时清理缓存
|
||||||
|
*/
|
||||||
private startAutoClear() {
|
private startAutoClear() {
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
logger.info('[WordsService] Clearing wordCache..');
|
this.logger.log('清理文案缓存..');
|
||||||
this.wordCache = {};
|
this.wordCache = {};
|
||||||
}, this.clearIntervalMs);
|
}, this.clearIntervalMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从本地加载json到内存&返回
|
* 启动定时检查 words 仓库更新
|
||||||
* @param id 文件名
|
|
||||||
*/
|
*/
|
||||||
public async loadWordById(id: string): Promise<string[] | null> {
|
private startAutoUpdate() {
|
||||||
logger.info(`Loading words ${id}..`);
|
setInterval(async () => {
|
||||||
|
const wordsPath = this.paths.get('words');
|
||||||
|
this.logger.log('定时检查文案仓库更新..');
|
||||||
|
const updated = await this.autoUpdateService.checkRepoForUpdates(
|
||||||
|
wordsPath,
|
||||||
|
'words 仓库',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
this.logger.log('文案仓库已更新,清理缓存..');
|
||||||
|
this.wordCache = {};
|
||||||
|
}
|
||||||
|
}, this.clearIntervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从本地加载文案到内存
|
||||||
|
*/
|
||||||
|
async loadWordById(id: string): Promise<string[] | null> {
|
||||||
|
this.logger.log(`加载文案 ${id}..`);
|
||||||
if (this.wordCache[id]) return this.wordCache[id];
|
if (this.wordCache[id]) return this.wordCache[id];
|
||||||
const filePath = path.join(paths.get('words'), `${id}.json`);
|
const filePath = path.join(this.paths.get('words'), `${id}.json`);
|
||||||
try {
|
try {
|
||||||
const content = await fs.readFile(filePath, 'utf-8');
|
const content = await fs.readFile(filePath, 'utf-8');
|
||||||
const parsed = JSON.parse(content);
|
const parsed = JSON.parse(content);
|
||||||
@ -32,35 +64,31 @@ class WordsService {
|
|||||||
const texts = parsed.filter((item) => typeof item === 'string');
|
const texts = parsed.filter((item) => typeof item === 'string');
|
||||||
this.wordCache[id] = texts;
|
this.wordCache[id] = texts;
|
||||||
return texts;
|
return texts;
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
return null;
|
||||||
logger.error(`Failed to loadWordById: ${id}`);
|
} catch (e) {
|
||||||
|
this.logger.error(`加载文案失败: ${id}..`, e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重载json到内存
|
* 重载 json 到内存
|
||||||
* @param id 文件名
|
|
||||||
*/
|
*/
|
||||||
public async reloadWord(id: string): Promise<boolean> {
|
async reloadWord(id: string): Promise<boolean> {
|
||||||
logger.info(`Reloading word: ${id}..`);
|
this.logger.log(`重载文案: ${id}..`);
|
||||||
const filePath = path.join(paths.get('words'), `${id}.json`);
|
const filePath = path.join(this.paths.get('words'), `${id}.json`);
|
||||||
try {
|
try {
|
||||||
const content = await fs.readFile(filePath, 'utf-8');
|
const content = await fs.readFile(filePath, 'utf-8');
|
||||||
const parsed = JSON.parse(content);
|
const parsed = JSON.parse(content);
|
||||||
if (Array.isArray(parsed)) {
|
if (Array.isArray(parsed)) {
|
||||||
this.wordCache[id] = parsed.filter((item) => typeof item === 'string');
|
this.wordCache[id] = parsed.filter((item) => typeof item === 'string');
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(`Failed to reloadWordById: ${id}..`);
|
this.logger.error(`重载文案失败: ${id}`, e);
|
||||||
return false;
|
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,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;
|
|
6
src/types/ws/ws.handlers.interface.ts
Normal file
6
src/types/ws/ws.handlers.interface.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { AuthenticatedSocket } from './ws.interface';
|
||||||
|
|
||||||
|
export interface IMessageHandler {
|
||||||
|
type: string; //消息类型
|
||||||
|
handle(socket: AuthenticatedSocket, msg: any): Promise<void>;
|
||||||
|
}
|
@ -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,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": {
|
"compilerOptions": {
|
||||||
"target": "es2016",
|
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"esModuleInterop": true,
|
"declaration": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"removeComments": true,
|
||||||
"strict": true,
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "ES2023",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"outDir": "dist"
|
"strictNullChecks": true,
|
||||||
},
|
"forceConsistentCasingInFileNames": true,
|
||||||
"include": ["src"]
|
"noImplicitAny": false,
|
||||||
|
"strictBindCallApply": false,
|
||||||
|
"noFallthroughCasesInSwitch": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user