mirror of
https://github.com/crystelf/crystelf-core.git
synced 2025-10-14 05:19:19 +00:00
nest初始化
feat:配置模块 feat:路径模块
This commit is contained in:
parent
4846e27e6a
commit
4100b4f0aa
59
.gitignore
vendored
59
.gitignore
vendored
@ -1,15 +1,56 @@
|
|||||||
/tmp
|
# 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
|
||||||
|
}
|
||||||
|
}
|
90
package.json
90
package.json
@ -1,33 +1,73 @@
|
|||||||
{
|
{
|
||||||
"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/swagger": "^11.2.0",
|
||||||
"mkdirp": "^3.0.1",
|
"axios": "^1.10.0",
|
||||||
"multer": "1.4.5-lts.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"simple-git": "^3.27.0",
|
"rxjs": "^7.8.1",
|
||||||
"uuid": "^11.1.0",
|
"ssh2": "^1.16.0"
|
||||||
"ws": "^8.18.1"
|
|
||||||
},
|
},
|
||||||
"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",
|
||||||
"@types/ws": "^8.18.1",
|
"@swc/cli": "^0.6.0",
|
||||||
"prettier": "^3.5.3",
|
"@swc/core": "^1.10.7",
|
||||||
"ts-node-dev": "^2.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"typescript": "^5.0.0"
|
"@types/jest": "^29.5.14",
|
||||||
|
"@types/node": "^22.16.4",
|
||||||
|
"@types/supertest": "^6.0.2",
|
||||||
|
"eslint": "^9.18.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-prettier": "^5.2.2",
|
||||||
|
"globals": "^16.0.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^7.0.0",
|
||||||
|
"ts-jest": "^29.2.5",
|
||||||
|
"ts-loader": "^9.5.2",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"typescript-eslint": "^8.20.0"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
7239
pnpm-lock.yaml
generated
7239
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
9
src/app.module.ts
Normal file
9
src/app.module.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { RootModule } from './root/root.module';
|
||||||
|
import { AppConfigModule } from './config/config.module';
|
||||||
|
import { PathModule } from './core/path/path.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [RootModule, AppConfigModule, PathModule],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
95
src/app.ts
95
src/app.ts
@ -1,95 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import compression from 'compression';
|
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import logger from './utils/core/logger';
|
|
||||||
import paths from './utils/core/path';
|
|
||||||
import config from './utils/core/config';
|
|
||||||
import './services/ws/wsServer';
|
|
||||||
import System from './utils/core/system';
|
|
||||||
|
|
||||||
const apps = {
|
|
||||||
async createApp() {
|
|
||||||
const app = express();
|
|
||||||
paths.init();
|
|
||||||
logger.info('晶灵核心初始化..');
|
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
const contentType = req.headers['content-type'] || '';
|
|
||||||
if (contentType.includes('multipart/form-data')) {
|
|
||||||
logger.debug('检测到form-data数据流,跳过加载 express.json() 中间件..');
|
|
||||||
next();
|
|
||||||
} else {
|
|
||||||
express.json()(req, res, next);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
app.use(compression());
|
|
||||||
logger.debug('成功加载 express.json() 中间件..');
|
|
||||||
|
|
||||||
const publicPath = paths.get('public');
|
|
||||||
app.use('/public', express.static(publicPath));
|
|
||||||
logger.debug(`静态资源路由挂载: /public => ${publicPath}`);
|
|
||||||
|
|
||||||
const modulesDir = path.resolve(__dirname, './modules');
|
|
||||||
const controllerPattern = /\.controller\.[jt]s$/;
|
|
||||||
|
|
||||||
if (!fs.existsSync(modulesDir)) {
|
|
||||||
logger.warn(`未找到模块目录: ${modulesDir}`);
|
|
||||||
} else {
|
|
||||||
const moduleFolders = fs.readdirSync(modulesDir).filter((folder) => {
|
|
||||||
const fullPath = path.join(modulesDir, folder);
|
|
||||||
return fs.statSync(fullPath).isDirectory();
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const folder of moduleFolders) {
|
|
||||||
const folderPath = path.join(modulesDir, folder);
|
|
||||||
const files = fs.readdirSync(folderPath).filter((f) => controllerPattern.test(f));
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const filePath = path.join(folderPath, file);
|
|
||||||
|
|
||||||
try {
|
|
||||||
//logger.debug(`尝试加载模块: ${filePath}`);
|
|
||||||
const controllerModule = require(filePath);
|
|
||||||
const controller = controllerModule.default;
|
|
||||||
|
|
||||||
if (controller?.getRouter) {
|
|
||||||
const isPublic = folder === 'public';
|
|
||||||
const routePath = isPublic ? `/${folder}` : `/api/${folder}`;
|
|
||||||
app.use(routePath, controller.getRouter());
|
|
||||||
logger.debug(`模块路由挂载: ${routePath.padEnd(12)} => ${file}`);
|
|
||||||
|
|
||||||
if (config.get('DEBUG', false)) {
|
|
||||||
controller.getRouter().stack.forEach((layer: any) => {
|
|
||||||
if (layer.route) {
|
|
||||||
const methods = Object.keys(layer.route.methods || {})
|
|
||||||
.map((m) => m.toUpperCase())
|
|
||||||
.join(',');
|
|
||||||
logger.debug(` ↳ ${methods.padEnd(6)} ${routePath}${layer.route.path}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.warn(`模块 ${file} 没有导出 getRouter 方法,跳过..`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`模块 ${file} 加载失败:`, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const duration = System.checkRestartTime();
|
|
||||||
//logger.info(duration);
|
|
||||||
if (duration) {
|
|
||||||
logger.warn(`重启完成!耗时 ${duration} 秒..`);
|
|
||||||
const restartTimePath = path.join(paths.get('temp'), 'restart_time');
|
|
||||||
fs.writeFileSync(restartTimePath, duration.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('晶灵核心初始化完毕!');
|
|
||||||
return app;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default apps;
|
|
28
src/common/filters/all-exception.filter.ts
Normal file
28
src/common/filters/all-exception.filter.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import {
|
||||||
|
ArgumentsHost,
|
||||||
|
Catch,
|
||||||
|
ExceptionFilter,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
|
||||||
|
@Catch()
|
||||||
|
export class AllExceptionsFilter implements ExceptionFilter {
|
||||||
|
catch(exception: unknown, host: ArgumentsHost) {
|
||||||
|
const ctx = host.switchToHttp();
|
||||||
|
const response = ctx.getResponse();
|
||||||
|
const status =
|
||||||
|
exception instanceof HttpException
|
||||||
|
? exception.getStatus()
|
||||||
|
: HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
|
||||||
|
const message =
|
||||||
|
exception instanceof HttpException ? exception.message : '服务器内部错误';
|
||||||
|
|
||||||
|
response.status(status).json({
|
||||||
|
success: false,
|
||||||
|
data: null,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
26
src/common/interceptors/response.interceptor.ts
Normal file
26
src/common/interceptors/response.interceptor.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
NestInterceptor,
|
||||||
|
ExecutionContext,
|
||||||
|
CallHandler,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Observable, map } from 'rxjs';
|
||||||
|
import { ApiResponse } from '../response-format';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ResponseInterceptor<T>
|
||||||
|
implements NestInterceptor<T, ApiResponse<T>>
|
||||||
|
{
|
||||||
|
intercept(
|
||||||
|
context: ExecutionContext,
|
||||||
|
next: CallHandler,
|
||||||
|
): Observable<ApiResponse<T>> {
|
||||||
|
return next.handle().pipe(
|
||||||
|
map((data) => ({
|
||||||
|
success: true,
|
||||||
|
data,
|
||||||
|
message: '操作成功',
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
5
src/common/response-format.ts
Normal file
5
src/common/response-format.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export interface ApiResponse<T = any> {
|
||||||
|
success: boolean;
|
||||||
|
data: T;
|
||||||
|
message: string;
|
||||||
|
}
|
16
src/config/config.module.ts
Normal file
16
src/config/config.module.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule as NestConfigModule } from '@nestjs/config';
|
||||||
|
import { AppConfigService } from './config.service';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
NestConfigModule.forRoot({
|
||||||
|
envFilePath: path.resolve(__dirname, '../../.env'),
|
||||||
|
isGlobal: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
providers: [AppConfigService],
|
||||||
|
exports: [AppConfigService],
|
||||||
|
})
|
||||||
|
export class AppConfigModule {}
|
47
src/config/config.service.ts
Normal file
47
src/config/config.service.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { ConfigService as NestConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AppConfigService implements OnModuleInit {
|
||||||
|
private readonly logger = new Logger(AppConfigService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(NestConfigService)
|
||||||
|
private readonly nestConfigService: NestConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
onModuleInit() {
|
||||||
|
this.checkRequiredVariables();
|
||||||
|
}
|
||||||
|
|
||||||
|
get<T = string>(key: string, defaultValue?: T): T | undefined {
|
||||||
|
const value = this.nestConfigService.get<T>(key);
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
if (defaultValue !== undefined) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
this.logger.error(`环境变量 ${key} 未定义!`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkRequiredVariables(): void {
|
||||||
|
this.logger.log('检查必要环境变量..');
|
||||||
|
const requiredVariables = [
|
||||||
|
'PORT',
|
||||||
|
'RD_PORT',
|
||||||
|
'RD_ADD',
|
||||||
|
'WS_SECRET',
|
||||||
|
'WS_PORT',
|
||||||
|
];
|
||||||
|
|
||||||
|
requiredVariables.forEach((key) => {
|
||||||
|
const value = this.nestConfigService.get(key);
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
this.logger.fatal(`必需环境变量缺失: ${key}`);
|
||||||
|
} else {
|
||||||
|
this.logger.debug(`检测到环境变量: ${key}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1 +0,0 @@
|
|||||||
keep
|
|
8
src/core/path/path.module.ts
Normal file
8
src/core/path/path.module.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { PathService } from './path.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [PathService],
|
||||||
|
exports: [PathService],
|
||||||
|
})
|
||||||
|
export class PathModule {}
|
119
src/core/path/path.service.ts
Normal file
119
src/core/path/path.service.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { ConfigService } from '../../config/config.service';
|
||||||
|
import { Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PathService {
|
||||||
|
private readonly baseDir: string;
|
||||||
|
private readonly logger = new Logger(PathService.name);
|
||||||
|
|
||||||
|
constructor(private readonly configService: ConfigService) {
|
||||||
|
this.baseDir = path.join(__dirname, '../../..');
|
||||||
|
this.initializePaths();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取预定义路径
|
||||||
|
* @param type 路径类型
|
||||||
|
*/
|
||||||
|
get(type?: PathType): string {
|
||||||
|
const mappings: Record<PathType, string> = {
|
||||||
|
root: this.baseDir,
|
||||||
|
public: path.join(this.baseDir, 'public'),
|
||||||
|
images: path.join(this.baseDir, 'public/files/image'),
|
||||||
|
log: path.join(this.baseDir, 'logs'),
|
||||||
|
config: path.join(this.baseDir, 'config'),
|
||||||
|
temp: path.join(this.baseDir, 'temp'),
|
||||||
|
userData: path.join(this.baseDir, 'private/data'),
|
||||||
|
files: path.join(this.baseDir, 'public/files'),
|
||||||
|
media: path.join(this.baseDir, 'public/files/media'),
|
||||||
|
package: path.join(this.baseDir, 'package.json'),
|
||||||
|
modules: path.join(this.baseDir, 'src/modules'),
|
||||||
|
uploads: path.join(this.baseDir, 'public/files/uploads'),
|
||||||
|
words: path.join(this.baseDir, 'private/data/word'),
|
||||||
|
};
|
||||||
|
|
||||||
|
return type ? mappings[type] : this.baseDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化必要的目录
|
||||||
|
*/
|
||||||
|
private initializePaths(): void {
|
||||||
|
this.logger.log('path初始化..');
|
||||||
|
const pathsToInit = [
|
||||||
|
this.get('log'),
|
||||||
|
this.get('config'),
|
||||||
|
this.get('userData'),
|
||||||
|
this.get('media'),
|
||||||
|
this.get('temp'),
|
||||||
|
this.get('uploads'),
|
||||||
|
this.get('words'),
|
||||||
|
];
|
||||||
|
|
||||||
|
pathsToInit.forEach((dirPath) => {
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
this.createDir(dirPath);
|
||||||
|
this.logger.debug(`创建目录:${dirPath}..`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.logger.log('path初始化完毕!');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建目录
|
||||||
|
* @param targetPath 目标路径
|
||||||
|
* @param includeFile 是否包含文件路径
|
||||||
|
*/
|
||||||
|
createDir(targetPath: string, includeFile: boolean = false): void {
|
||||||
|
try {
|
||||||
|
const dirToCreate = includeFile ? path.dirname(targetPath) : targetPath;
|
||||||
|
fs.mkdirSync(dirToCreate, { recursive: true });
|
||||||
|
this.logger.debug(`成功创建目录: ${dirToCreate}`);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(`创建目录失败: ${err}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接路径
|
||||||
|
* @param paths 路径片段
|
||||||
|
*/
|
||||||
|
join(...paths: string[]): string {
|
||||||
|
return path.join(...paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件扩展名
|
||||||
|
* @param filePath 文件路径
|
||||||
|
*/
|
||||||
|
getExtension(filePath: string): string {
|
||||||
|
return path.extname(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件名(不含扩展名)
|
||||||
|
* @param filePath 文件路径
|
||||||
|
*/
|
||||||
|
getBasename(filePath: string): string {
|
||||||
|
return path.basename(filePath, path.extname(filePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PathType =
|
||||||
|
| 'root'
|
||||||
|
| 'public'
|
||||||
|
| 'images'
|
||||||
|
| 'log'
|
||||||
|
| 'config'
|
||||||
|
| 'temp'
|
||||||
|
| 'userData'
|
||||||
|
| 'files'
|
||||||
|
| 'package'
|
||||||
|
| 'media'
|
||||||
|
| 'modules'
|
||||||
|
| 'words'
|
||||||
|
| 'uploads';
|
53
src/main.ts
53
src/main.ts
@ -1,31 +1,26 @@
|
|||||||
import apps from './app';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import logger from './utils/core/logger';
|
import { AppModule } from './app.module';
|
||||||
import config from './utils/core/config';
|
import { Logger } 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';
|
||||||
|
|
||||||
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()
|
app.useGlobalInterceptors(new ResponseInterceptor());
|
||||||
.then(async (app) => {
|
app.useGlobalFilters(new AllExceptionsFilter());
|
||||||
app.listen(PORT, () => {
|
const config = new DocumentBuilder()
|
||||||
logger.info(`Crystelf-core listening on ${PORT}`);
|
.setTitle('晶灵核心')
|
||||||
});
|
.setDescription('为晶灵提供API服务')
|
||||||
const isUpdated = await autoUpdater.checkForUpdates();
|
.setVersion('1.0')
|
||||||
if (isUpdated) {
|
.build();
|
||||||
logger.warn(`检测到更新,正在重启..`);
|
const document = () => SwaggerModule.createDocument(app, config);
|
||||||
await System.restart();
|
SwaggerModule.setup('', app, document);
|
||||||
}
|
await app.listen(7000);
|
||||||
})
|
}
|
||||||
.catch((err) => {
|
bootstrap().then(() => {
|
||||||
logger.error('Crystelf-core启动失败:', err);
|
Logger.log(`API服务已启动:http://localhost:7000`);
|
||||||
process.exit(1);
|
Logger.log(`API文档: http://localhost:7000/api`);
|
||||||
});
|
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
|
||||||
await redis.disconnect();
|
|
||||||
process.exit(0);
|
|
||||||
});
|
});
|
||||||
|
@ -1,156 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import response from '../../utils/core/response';
|
|
||||||
import BotService from './bot.service';
|
|
||||||
import tools from '../../utils/modules/tools';
|
|
||||||
import logger from '../../utils/core/logger';
|
|
||||||
import wsClientManager from '../../services/ws/wsClientManager';
|
|
||||||
|
|
||||||
class BotController {
|
|
||||||
private readonly router: express.Router;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.router = express.Router();
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getRouter(): express.Router {
|
|
||||||
return this.router;
|
|
||||||
}
|
|
||||||
|
|
||||||
private init(): void {
|
|
||||||
this.router.post(`/getBotId`, this.postBotsId);
|
|
||||||
this.router.post('/getGroupInfo', this.postGroupInfo);
|
|
||||||
this.router.post('/sendMessage', this.sendMessage);
|
|
||||||
this.router.post('/reportBots', this.reportBots);
|
|
||||||
this.router.post('/broadcast', this.smartBroadcast);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前连接到核心的全部botId数组
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
private postBotsId = async (req: express.Request, res: express.Response): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const token = req.body.token;
|
|
||||||
if (tools.checkToken(token.toString())) {
|
|
||||||
const result = await BotService.getBotId();
|
|
||||||
await response.success(res, result);
|
|
||||||
} else {
|
|
||||||
await tools.tokenCheckFailed(res, token);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
await response.error(res, `请求失败..`, 500, err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取群聊信息
|
|
||||||
* @example req示例
|
|
||||||
* ```json
|
|
||||||
* {
|
|
||||||
* token: ‘114514’,
|
|
||||||
* groupId: 114514
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
private postGroupInfo = async (req: express.Request, res: express.Response): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const token = req.body.token;
|
|
||||||
if (tools.checkToken(token.toString())) {
|
|
||||||
const groupId: number = req.body.groupId;
|
|
||||||
let returnData = await BotService.getGroupInfo({ groupId: groupId });
|
|
||||||
if (returnData) {
|
|
||||||
await response.success(res, returnData);
|
|
||||||
logger.debug(returnData);
|
|
||||||
} else {
|
|
||||||
await response.error(res);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await tools.tokenCheckFailed(res, token);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
await response.error(res);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 广播要求同步群聊信息和bot连接情况
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
// TODO 测试接口可用性
|
|
||||||
private reportBots = async (req: express.Request, res: express.Response): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const token = req.body.token;
|
|
||||||
if (tools.checkToken(token.toString())) {
|
|
||||||
const sendMessage = {
|
|
||||||
type: 'reportBots',
|
|
||||||
data: {},
|
|
||||||
};
|
|
||||||
logger.info(`正在请求同步bot数据..`);
|
|
||||||
await response.success(res, {});
|
|
||||||
await wsClientManager.broadcast(sendMessage);
|
|
||||||
} else {
|
|
||||||
await tools.tokenCheckFailed(res, token);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
await response.error(res);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送消息到群聊,自动获取client
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
// TODO 测试接口可用性
|
|
||||||
private sendMessage = async (req: express.Request, res: express.Response): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const token = req.body.token;
|
|
||||||
if (tools.checkToken(token.toString())) {
|
|
||||||
const groupId: number = Number(req.body.groupId);
|
|
||||||
const message: string = req.body.message.toString();
|
|
||||||
const flag: boolean = await BotService.sendMessage(groupId, message);
|
|
||||||
if (flag) {
|
|
||||||
await response.success(res, { message: '消息发送成功..' });
|
|
||||||
} else {
|
|
||||||
await response.error(res);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await tools.tokenCheckFailed(res, token);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
await response.error(res);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 智能广播消息到全部群聊
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
// TODO 测试接口可用性
|
|
||||||
private smartBroadcast = async (req: express.Request, res: express.Response): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const token = req.body.token;
|
|
||||||
const message = req.body.message;
|
|
||||||
if (!message || typeof message !== 'string') {
|
|
||||||
return await response.error(res, '缺少 message 字段', 400);
|
|
||||||
}
|
|
||||||
if (tools.checkToken(token.toString())) {
|
|
||||||
logger.info(`广播任务已开始,正在后台执行..`);
|
|
||||||
await response.success(res, '广播任务已开始,正在后台执行..');
|
|
||||||
await BotService.broadcastToAllGroups(message);
|
|
||||||
} else {
|
|
||||||
await tools.tokenCheckFailed(res, token);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
await response.error(res);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new BotController();
|
|
@ -1,241 +0,0 @@
|
|||||||
import logger from '../../utils/core/logger';
|
|
||||||
import paths from '../../utils/core/path';
|
|
||||||
import fs from 'fs/promises';
|
|
||||||
import path from 'path';
|
|
||||||
import redisService from '../../services/redis/redis';
|
|
||||||
import wsClientManager from '../../services/ws/wsClientManager';
|
|
||||||
import tools from '../../utils/core/tool';
|
|
||||||
|
|
||||||
class BotService {
|
|
||||||
/**
|
|
||||||
* 获取botId数组
|
|
||||||
*/
|
|
||||||
public async getBotId(): Promise<{ uin: number; nickName: string }[]> {
|
|
||||||
logger.debug('GetBotId..');
|
|
||||||
const userPath = paths.get('userData');
|
|
||||||
const botsPath = path.join(userPath, '/crystelfBots');
|
|
||||||
const dirData = await fs.readdir(botsPath);
|
|
||||||
const uins: { uin: number; nickName: string }[] = [];
|
|
||||||
|
|
||||||
for (const fileName of dirData) {
|
|
||||||
if (!fileName.endsWith('.json')) continue;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const raw = await redisService.fetch('crystelfBots', fileName);
|
|
||||||
if (!raw || !Array.isArray(raw)) continue;
|
|
||||||
|
|
||||||
for (const bot of raw) {
|
|
||||||
const uin = Number(bot.uin);
|
|
||||||
const nickName = bot.nickName || '';
|
|
||||||
if (!isNaN(uin)) {
|
|
||||||
uins.push({ uin, nickName });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`读取或解析 ${fileName} 出错: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.debug(uins);
|
|
||||||
return uins;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取群聊信息
|
|
||||||
* @param data
|
|
||||||
*/
|
|
||||||
public async getGroupInfo(data: {
|
|
||||||
botId?: number;
|
|
||||||
groupId: number;
|
|
||||||
clientId?: string;
|
|
||||||
}): Promise<any> {
|
|
||||||
logger.debug('GetGroupInfo..');
|
|
||||||
const sendBot: number | undefined = data.botId ?? (await this.getGroupBot(data.groupId));
|
|
||||||
if (!sendBot) {
|
|
||||||
logger.warn(`不存在能向群聊${data.groupId}发送消息的Bot!`);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sendData = {
|
|
||||||
type: 'getGroupInfo',
|
|
||||||
data: {
|
|
||||||
botId: sendBot,
|
|
||||||
groupId: data.groupId,
|
|
||||||
clientID: data.clientId ?? (await this.getBotClient(sendBot)),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (sendData.data.clientID) {
|
|
||||||
const returnData = await wsClientManager.sendAndWait(sendData.data.clientID, sendData);
|
|
||||||
return returnData ?? undefined;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送消息到群聊
|
|
||||||
* @param groupId 群号
|
|
||||||
* @param message 消息
|
|
||||||
*/
|
|
||||||
public async sendMessage(groupId: number, message: string): Promise<boolean> {
|
|
||||||
logger.info(`发送${message}到${groupId}..`);
|
|
||||||
const sendBot = await this.getGroupBot(groupId);
|
|
||||||
if (!sendBot) {
|
|
||||||
logger.warn(`不存在能向群聊${groupId}发送消息的Bot!`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const client = await this.getBotClient(sendBot);
|
|
||||||
if (!client) {
|
|
||||||
logger.warn(`不存在${sendBot}对应的client!`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const sendData = {
|
|
||||||
type: 'sendMessage',
|
|
||||||
data: {
|
|
||||||
botId: sendBot,
|
|
||||||
groupId: groupId,
|
|
||||||
clientId: client,
|
|
||||||
message: message,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
await wsClientManager.send(client, sendData);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 智能投放广播消息实现
|
|
||||||
* @param message 要广播的消息
|
|
||||||
*/
|
|
||||||
// TODO 添加群聊信誉分机制,低于30分的群聊不播报等..
|
|
||||||
public async broadcastToAllGroups(message: string): Promise<void> {
|
|
||||||
const userPath = paths.get('userData');
|
|
||||||
const botsPath = path.join(userPath, '/crystelfBots');
|
|
||||||
const dirData = await fs.readdir(botsPath);
|
|
||||||
const groupMap: Map<number, { botId: number; clientId: string }[]> = new Map();
|
|
||||||
|
|
||||||
for (const fileName of dirData) {
|
|
||||||
if (!fileName.endsWith('.json')) continue;
|
|
||||||
|
|
||||||
const clientId = path.basename(fileName, '.json');
|
|
||||||
const botList = await redisService.fetch('crystelfBots', fileName);
|
|
||||||
if (!Array.isArray(botList)) continue;
|
|
||||||
|
|
||||||
for (const bot of botList) {
|
|
||||||
const botId = Number(bot.uin);
|
|
||||||
const groups = bot.groups;
|
|
||||||
|
|
||||||
if (!botId || !Array.isArray(groups)) continue;
|
|
||||||
|
|
||||||
for (const group of groups) {
|
|
||||||
if (group.group_id === '未知') continue;
|
|
||||||
const groupId = Number(group.group_id);
|
|
||||||
if (isNaN(groupId)) continue;
|
|
||||||
|
|
||||||
if (!groupMap.has(groupId)) {
|
|
||||||
groupMap.set(groupId, []);
|
|
||||||
}
|
|
||||||
groupMap.get(groupId)!.push({ botId, clientId });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [groupId, botEntries] of groupMap.entries()) {
|
|
||||||
logger.debug(`[群 ${groupId}] 候选Bot列表: ${JSON.stringify(botEntries)}`);
|
|
||||||
|
|
||||||
const clientGroups = new Map<string, number[]>();
|
|
||||||
botEntries.forEach(({ botId, clientId }) => {
|
|
||||||
if (!clientGroups.has(clientId)) clientGroups.set(clientId, []);
|
|
||||||
clientGroups.get(clientId)!.push(botId);
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedClientId = tools.getRandomItem([...clientGroups.keys()]);
|
|
||||||
const botCandidates = clientGroups.get(selectedClientId)!;
|
|
||||||
const selectedBotId = tools.getRandomItem(botCandidates);
|
|
||||||
const delay = tools.getRandomDelay(10_000, 150_000);
|
|
||||||
|
|
||||||
((groupId, selectedClientId, selectedBotId, delay) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
const sendData = {
|
|
||||||
type: 'sendMessage',
|
|
||||||
data: {
|
|
||||||
botId: selectedBotId,
|
|
||||||
groupId: groupId,
|
|
||||||
clientId: selectedClientId,
|
|
||||||
message: message,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
logger.info(
|
|
||||||
`[广播] 向群 ${groupId} 使用Bot ${selectedBotId}(客户端 ${selectedClientId})发送消息${message},延迟 ${delay / 1000} 秒`
|
|
||||||
);
|
|
||||||
wsClientManager.send(selectedClientId, sendData).catch((e) => {
|
|
||||||
logger.error(`发送到群${groupId}失败:`, e);
|
|
||||||
});
|
|
||||||
}, delay);
|
|
||||||
})(groupId, selectedClientId, selectedBotId, delay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取botId对应的client
|
|
||||||
* @param botId
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private async getBotClient(botId: number): Promise<string | undefined> {
|
|
||||||
const userPath = paths.get('userData');
|
|
||||||
const botsPath = path.join(userPath, '/crystelfBots');
|
|
||||||
const dirData = await fs.readdir(botsPath);
|
|
||||||
|
|
||||||
for (const clientId of dirData) {
|
|
||||||
if (!clientId.endsWith('.json')) continue;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const raw = await redisService.fetch('crystelfBots', clientId);
|
|
||||||
if (!Array.isArray(raw)) continue;
|
|
||||||
|
|
||||||
for (const bot of raw) {
|
|
||||||
const uin = Number(bot.uin);
|
|
||||||
if (!isNaN(uin) && uin === botId) {
|
|
||||||
return path.basename(clientId, '.json');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`读取${clientId}出错..`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取groupId对应的botId
|
|
||||||
* @param groupId
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private async getGroupBot(groupId: number): Promise<number | undefined> {
|
|
||||||
const userPath = paths.get('userData');
|
|
||||||
const botsPath = path.join(userPath, '/crystelfBots');
|
|
||||||
const dirData = await fs.readdir(botsPath);
|
|
||||||
|
|
||||||
for (const clientId of dirData) {
|
|
||||||
if (!clientId.endsWith('.json')) continue;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const raw = await redisService.fetch('crystelfBots', clientId);
|
|
||||||
if (!Array.isArray(raw)) continue;
|
|
||||||
|
|
||||||
for (const bot of raw) {
|
|
||||||
const uin = Number(bot.uin);
|
|
||||||
const groups = bot.groups;
|
|
||||||
if (!uin || !Array.isArray(groups)) continue;
|
|
||||||
|
|
||||||
if (groups.find((g) => Number(g.group_id) === groupId)) {
|
|
||||||
return uin;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`读取${clientId}出错..`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new BotService();
|
|
@ -1,112 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import FileService from './file.service';
|
|
||||||
import logger from '../../utils/core/logger';
|
|
||||||
import response from '../../utils/core/response';
|
|
||||||
import paths from '../../utils/core/path';
|
|
||||||
import multer from 'multer';
|
|
||||||
import tools from '../../utils/modules/tools';
|
|
||||||
|
|
||||||
const uploadDir = paths.get('uploads');
|
|
||||||
const upload = multer({
|
|
||||||
dest: uploadDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
class FileController {
|
|
||||||
private readonly router: express.Router;
|
|
||||||
private readonly FileService: FileService;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.router = express.Router();
|
|
||||||
this.FileService = new FileService();
|
|
||||||
this.initializeRoutes();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getRouter(): express.Router {
|
|
||||||
return this.router;
|
|
||||||
}
|
|
||||||
|
|
||||||
private initializeRoutes(): void {
|
|
||||||
this.router.get('*', this.handleGetFile);
|
|
||||||
this.router.post('/upload', upload.single('file'), this.handleUploadFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get文件
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
private handleGetFile = async (req: express.Request, res: express.Response): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const fullPath = req.params[0];
|
|
||||||
logger.debug(`有个小可爱正在请求${fullPath}噢..`);
|
|
||||||
const filePath = await this.FileService.getFile(fullPath);
|
|
||||||
if (!filePath) {
|
|
||||||
logger.warn(`${fullPath}:文件不存在..`);
|
|
||||||
await response.error(res, '文件不存在啦!', 404);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.sendFile(filePath);
|
|
||||||
logger.info(`成功投递文件: ${filePath}`);
|
|
||||||
} catch (error) {
|
|
||||||
await response.error(res, '晶灵服务处理文件请求时出错..', 500);
|
|
||||||
logger.error('晶灵数据请求处理失败:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理文件上传请求
|
|
||||||
* 客户端应以 `multipart/form-data` 格式上传文件,字段名为 `file`
|
|
||||||
* @example 示例请求(使用 axios 和 form-data)
|
|
||||||
* ```js
|
|
||||||
* const form = new FormData();
|
|
||||||
* const fileStream = fs.createReadStream(filePath);
|
|
||||||
* form.append('file', fileStream);
|
|
||||||
* const uploadUrl = `http://localhost:4000/upload?dir=example&expire=600`;
|
|
||||||
* const response = await axios.post(uploadUrl, form, {
|
|
||||||
* headers: {
|
|
||||||
* ...form.getHeaders(),
|
|
||||||
* },
|
|
||||||
* maxContentLength: Infinity,
|
|
||||||
* maxBodyLength: Infinity,
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @queryParam dir 上传到的相对目录,默认根目录
|
|
||||||
* @queryParam expire 文件保留时间,默认 600 秒
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
private handleUploadFile = async (req: express.Request, res: express.Response): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const token = req.body.token;
|
|
||||||
if (tools.checkToken(token.toString())) {
|
|
||||||
if (!req.file) {
|
|
||||||
await response.error(res, `未检测到上传文件`, 400);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`检测到上传文件..`);
|
|
||||||
const uploadDir = req.query.dir?.toString() || '';
|
|
||||||
const deleteAfter = parseInt(req.query.expire as string) || 10 * 60;
|
|
||||||
const { fullPath, relativePath } = await this.FileService.saveUploadedFile(
|
|
||||||
req.file,
|
|
||||||
uploadDir
|
|
||||||
);
|
|
||||||
await this.FileService.scheduleDelete(fullPath, deleteAfter * 1000);
|
|
||||||
await response.success(res, {
|
|
||||||
message: '文件上传成功..',
|
|
||||||
filePath: fullPath,
|
|
||||||
url: relativePath,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await tools.tokenCheckFailed(res, token);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
await response.error(res, `文件上传失败..`, 500);
|
|
||||||
logger.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new FileController();
|
|
@ -1,93 +0,0 @@
|
|||||||
import path from 'path';
|
|
||||||
import fs from 'fs/promises';
|
|
||||||
import paths from '../../utils/core/path';
|
|
||||||
import logger from '../../utils/core/logger';
|
|
||||||
import { existsSync } from 'fs';
|
|
||||||
|
|
||||||
class FileService {
|
|
||||||
private readonly filePath: string;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.filePath = paths.get('files');
|
|
||||||
logger.info(`晶灵云图数据中心初始化..数据存储在: ${this.filePath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取文件
|
|
||||||
* @param relativePath 文件绝对路径
|
|
||||||
*/
|
|
||||||
public async getFile(relativePath: string): Promise<string | null> {
|
|
||||||
if (!this.isValidPath(relativePath) && !this.isValidFilename(path.basename(relativePath))) {
|
|
||||||
throw new Error('非法路径请求');
|
|
||||||
}
|
|
||||||
|
|
||||||
const filePath = path.join(this.filePath, relativePath);
|
|
||||||
logger.debug(`尝试访问文件路径: ${filePath}`);
|
|
||||||
|
|
||||||
return existsSync(filePath) ? filePath : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存文件到目标目录
|
|
||||||
* @param file multer的file
|
|
||||||
* @param dir 可选上传目录,相对目录
|
|
||||||
*/
|
|
||||||
public async saveUploadedFile(
|
|
||||||
file: Express.Multer.File,
|
|
||||||
dir: string = ''
|
|
||||||
): Promise<{ fullPath: string; relativePath: string }> {
|
|
||||||
const baseDir = paths.get('uploads');
|
|
||||||
const targetDir = path.join(baseDir, dir);
|
|
||||||
if (!existsSync(targetDir)) {
|
|
||||||
await fs.mkdir(targetDir, { recursive: true });
|
|
||||||
logger.debug(`已创建上传目录: ${targetDir}`);
|
|
||||||
}
|
|
||||||
const fileName = `${Date.now()}-${file.originalname.replace(/\s+/g, '_')}`;
|
|
||||||
const finalPath = path.join(targetDir, fileName);
|
|
||||||
await fs.rename(file.path, finalPath);
|
|
||||||
logger.info(`保存上传文件: ${finalPath}`);
|
|
||||||
return {
|
|
||||||
fullPath: finalPath,
|
|
||||||
relativePath: `uploads/${dir}/${fileName}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定时删除文件
|
|
||||||
* @param filePath 文件绝对路径
|
|
||||||
* @param timeoutMs 毫秒,默认10分钟
|
|
||||||
*/
|
|
||||||
public async scheduleDelete(filePath: string, timeoutMs: number = 10 * 60 * 1000): Promise<void> {
|
|
||||||
setTimeout(async () => {
|
|
||||||
try {
|
|
||||||
await fs.unlink(filePath);
|
|
||||||
logger.info(`已自动删除文件: ${filePath}`);
|
|
||||||
} catch (err) {
|
|
||||||
logger.warn(`删除文件失败: ${filePath}`, err);
|
|
||||||
}
|
|
||||||
}, timeoutMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查路径合法性
|
|
||||||
* @param relativePath
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private isValidPath(relativePath: string): boolean {
|
|
||||||
try {
|
|
||||||
const normalized = path.normalize(relativePath);
|
|
||||||
let flag = true;
|
|
||||||
if (normalized.startsWith('../') && path.isAbsolute(normalized)) flag = false;
|
|
||||||
return flag;
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private isValidFilename(filename: string): boolean {
|
|
||||||
return /^[a-zA-Z0-9_\-.]+$/.test(filename);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FileService;
|
|
@ -1,45 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import sampleService from './sample.service';
|
|
||||||
import response from '../../utils/core/response';
|
|
||||||
|
|
||||||
class SampleController {
|
|
||||||
private readonly router: express.Router;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.router = express.Router();
|
|
||||||
this.initializeRoutes();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getRouter(): express.Router {
|
|
||||||
return this.router;
|
|
||||||
}
|
|
||||||
|
|
||||||
private initializeRoutes(): void {
|
|
||||||
this.router.get('/hello', this.getHello);
|
|
||||||
this.router.post('/greet', this.postGreet);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getHello = async (req: express.Request, res: express.Response): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const result = await sampleService.getHello();
|
|
||||||
await response.success(res, result);
|
|
||||||
} catch (error) {
|
|
||||||
await response.error(res, '请求失败了..', 500, error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private postGreet = async (req: express.Request, res: express.Response): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const { name } = req.body;
|
|
||||||
if (!name) {
|
|
||||||
return response.error(res, '姓名不能为空!', 400);
|
|
||||||
}
|
|
||||||
const result = await sampleService.generateGreeting(name);
|
|
||||||
await response.success(res, result);
|
|
||||||
} catch (error) {
|
|
||||||
await response.error(res, '请求失败了..', 500, error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new SampleController();
|
|
@ -1,19 +0,0 @@
|
|||||||
import logger from '../../utils/core/logger';
|
|
||||||
|
|
||||||
class SampleService {
|
|
||||||
public async getHello() {
|
|
||||||
logger.debug(`有个小可爱正在请求GetHello方法..`);
|
|
||||||
return { message: 'Hello World!' };
|
|
||||||
}
|
|
||||||
|
|
||||||
public async generateGreeting(name: string): Promise<object> {
|
|
||||||
logger.debug(`有个小可爱正在请求generateGreeting方法..`);
|
|
||||||
if (!name) {
|
|
||||||
logger.warn('Name is required');
|
|
||||||
throw new Error('Name is required');
|
|
||||||
}
|
|
||||||
return { message: `Hello, ${name}!` };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new SampleService();
|
|
@ -1,62 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import tools from '../../utils/modules/tools';
|
|
||||||
import response from '../../utils/core/response';
|
|
||||||
import SystemService from './system.service';
|
|
||||||
|
|
||||||
class SystemController {
|
|
||||||
private readonly router: express.Router;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.router = express.Router();
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getRouter(): express.Router {
|
|
||||||
return this.router;
|
|
||||||
}
|
|
||||||
|
|
||||||
private init(): void {
|
|
||||||
this.router.post('/restart', this.systemRestart);
|
|
||||||
this.router.post('/getRestartTime', this.getRestartTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 系统重启路由
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
private systemRestart = async (req: express.Request, res: express.Response): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const token = req.body.token;
|
|
||||||
if (tools.checkToken(token.toString())) {
|
|
||||||
await response.success(res, '核心正在重启..');
|
|
||||||
await SystemService.systemRestart();
|
|
||||||
} else {
|
|
||||||
await tools.tokenCheckFailed(res, token);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
await response.error(res);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取重启所需时间
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
private getRestartTime = async (req: express.Request, res: express.Response): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const token = req.body.token;
|
|
||||||
if (tools.checkToken(token.toString())) {
|
|
||||||
const time = await SystemService.getRestartTime();
|
|
||||||
await response.success(res, time);
|
|
||||||
} else {
|
|
||||||
await tools.tokenCheckFailed(res, token);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
await response.error(res);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new SystemController();
|
|
@ -1,20 +0,0 @@
|
|||||||
import System from '../../utils/core/system';
|
|
||||||
import fs from 'fs/promises';
|
|
||||||
import logger from '../../utils/core/logger';
|
|
||||||
import path from 'path';
|
|
||||||
import paths from '../../utils/core/path';
|
|
||||||
|
|
||||||
class SystemService {
|
|
||||||
public async systemRestart() {
|
|
||||||
logger.debug(`有个小可爱正在请求重启核心..`);
|
|
||||||
await System.restart();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getRestartTime() {
|
|
||||||
logger.debug(`有个小可爱想知道核心重启花了多久..`);
|
|
||||||
const restartTimePath = path.join(paths.get('temp'), 'restart_time');
|
|
||||||
return await fs.readFile(restartTimePath, 'utf8');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new SystemService();
|
|
@ -1,31 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import TestService from './test.service';
|
|
||||||
import response from '../../utils/core/response';
|
|
||||||
import logger from '../../utils/core/logger';
|
|
||||||
|
|
||||||
class TestController {
|
|
||||||
private readonly router: express.Router;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.router = express.Router();
|
|
||||||
this.initRouter();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getRouter(): express.Router {
|
|
||||||
return this.router;
|
|
||||||
}
|
|
||||||
|
|
||||||
public initRouter(): void {
|
|
||||||
this.router.get('/test', this.test);
|
|
||||||
}
|
|
||||||
private test = async (req: express.Request, res: express.Response): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const result = await TestService.test();
|
|
||||||
await response.success(res, result);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new TestController();
|
|
@ -1,21 +0,0 @@
|
|||||||
import wsClientManager from '../../services/ws/wsClientManager';
|
|
||||||
import logger from '../../utils/core/logger';
|
|
||||||
|
|
||||||
class TestService {
|
|
||||||
public async test() {
|
|
||||||
try {
|
|
||||||
const testData = {
|
|
||||||
type: 'getGroupInfo',
|
|
||||||
data: {
|
|
||||||
botId: 'stdin',
|
|
||||||
groupId: 'stdin',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return await wsClientManager.sendAndWait('test', testData);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new TestService();
|
|
@ -1,66 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import WordsService from './words.service';
|
|
||||||
import response from '../../utils/core/response';
|
|
||||||
import tools from '../../utils/modules/tools';
|
|
||||||
|
|
||||||
class WordsController {
|
|
||||||
private readonly router: express.Router;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.router = express.Router();
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getRouter(): express.Router {
|
|
||||||
return this.router;
|
|
||||||
}
|
|
||||||
|
|
||||||
private init(): void {
|
|
||||||
this.router.get('/getText/:id', this.getText);
|
|
||||||
this.router.post('/reloadText', this.reloadWord);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取随机文案
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
private getText = async (req: express.Request, res: express.Response): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const id = req.params.id;
|
|
||||||
const texts = await WordsService.loadWordById(id.toString());
|
|
||||||
if (!texts || texts.length === 0) {
|
|
||||||
return await response.error(res, `文案${id}不存在或为空..`, 404);
|
|
||||||
}
|
|
||||||
const randomIndex = Math.floor(Math.random() * texts.length);
|
|
||||||
const result = texts[randomIndex];
|
|
||||||
await response.success(res, result);
|
|
||||||
} catch (e) {
|
|
||||||
await response.error(res);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重载文案
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
private reloadWord = async (req: express.Request, res: express.Response): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const id = req.params.id;
|
|
||||||
const token = req.params.token;
|
|
||||||
if (tools.checkToken(token)) {
|
|
||||||
if (await WordsService.reloadWord(id.toString())) {
|
|
||||||
await response.success(res, '成功重载..');
|
|
||||||
} else {
|
|
||||||
await response.error(res, '重载失败..');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await tools.tokenCheckFailed(res, token);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
await response.error(res);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
export default new WordsController();
|
|
@ -1,66 +0,0 @@
|
|||||||
import path from 'path';
|
|
||||||
import paths from '../../utils/core/path';
|
|
||||||
import fs from 'fs/promises';
|
|
||||||
import logger from '../../utils/core/logger';
|
|
||||||
|
|
||||||
class WordsService {
|
|
||||||
private wordCache: Record<string, string[]> = {}; //缓存
|
|
||||||
private readonly clearIntervalMs = 30 * 60 * 1000; //30min
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.startAutoClear();
|
|
||||||
}
|
|
||||||
private startAutoClear() {
|
|
||||||
setInterval(() => {
|
|
||||||
logger.info('[WordsService] Clearing wordCache..');
|
|
||||||
this.wordCache = {};
|
|
||||||
}, this.clearIntervalMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从本地加载json到内存&返回
|
|
||||||
* @param id 文件名
|
|
||||||
*/
|
|
||||||
public async loadWordById(id: string): Promise<string[] | null> {
|
|
||||||
logger.info(`Loading words ${id}..`);
|
|
||||||
if (this.wordCache[id]) return this.wordCache[id];
|
|
||||||
const filePath = path.join(paths.get('words'), `${id}.json`);
|
|
||||||
try {
|
|
||||||
const content = await fs.readFile(filePath, 'utf-8');
|
|
||||||
const parsed = JSON.parse(content);
|
|
||||||
if (Array.isArray(parsed)) {
|
|
||||||
const texts = parsed.filter((item) => typeof item === 'string');
|
|
||||||
this.wordCache[id] = texts;
|
|
||||||
return texts;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Failed to loadWordById: ${id}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重载json到内存
|
|
||||||
* @param id 文件名
|
|
||||||
*/
|
|
||||||
public async reloadWord(id: string): Promise<boolean> {
|
|
||||||
logger.info(`Reloading word: ${id}..`);
|
|
||||||
const filePath = path.join(paths.get('words'), `${id}.json`);
|
|
||||||
try {
|
|
||||||
const content = await fs.readFile(filePath, 'utf-8');
|
|
||||||
const parsed = JSON.parse(content);
|
|
||||||
if (Array.isArray(parsed)) {
|
|
||||||
this.wordCache[id] = parsed.filter((item) => typeof item === 'string');
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(`Failed to reloadWordById: ${id}..`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export default new WordsService();
|
|
11
src/root/root.controller.ts
Normal file
11
src/root/root.controller.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class RootController {
|
||||||
|
@Get()
|
||||||
|
getWelcome() {
|
||||||
|
return {
|
||||||
|
message: '欢迎使用晶灵核心',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
7
src/root/root.module.ts
Normal file
7
src/root/root.module.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { RootController } from './root.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [RootController],
|
||||||
|
})
|
||||||
|
export class RootModule {}
|
@ -1,191 +0,0 @@
|
|||||||
import Redis from 'ioredis';
|
|
||||||
import logger from '../../utils/core/logger';
|
|
||||||
import tools from '../../utils/core/tool';
|
|
||||||
import config from '../../utils/core/config';
|
|
||||||
import redisTools from '../../utils/redis/redisTools';
|
|
||||||
import Persistence from '../../utils/redis/persistence';
|
|
||||||
import IUser from '../../types/user';
|
|
||||||
|
|
||||||
class RedisService {
|
|
||||||
private client!: Redis;
|
|
||||||
private isConnected = false;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.initialize().then();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async initialize() {
|
|
||||||
await this.connectWithRetry();
|
|
||||||
this.setupEventListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async connectWithRetry(): Promise<void> {
|
|
||||||
try {
|
|
||||||
await tools.retry(
|
|
||||||
async () => {
|
|
||||||
this.client = new Redis({
|
|
||||||
host: config.get('RD_ADD'),
|
|
||||||
port: Number(config.get('RD_PORT')),
|
|
||||||
retryStrategy: (times) => {
|
|
||||||
return Math.min(times * 1000, 5000);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.client.ping();
|
|
||||||
this.isConnected = true;
|
|
||||||
logger.info(`Redis连接成功!位于${config.get('RD_ADD')}:${config.get('RD_PORT')}`);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
maxAttempts: 5,
|
|
||||||
initialDelay: 1000,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Redis连接失败:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置Redis客户端事件监听器
|
|
||||||
*/
|
|
||||||
private setupEventListeners(): void {
|
|
||||||
this.client.on('error', (err) => {
|
|
||||||
if (!err.message.includes('ECONNREFUSED')) {
|
|
||||||
logger.error('Redis错误:', err);
|
|
||||||
}
|
|
||||||
this.isConnected = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.client.on('ready', () => {
|
|
||||||
this.isConnected = true;
|
|
||||||
logger.debug('Redis连接就绪!');
|
|
||||||
});
|
|
||||||
|
|
||||||
this.client.on('reconnecting', () => {
|
|
||||||
logger.warn('Redis重新连接中...');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async waitUntilReady(): Promise<void> {
|
|
||||||
if (this.isConnected) return;
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const check = () => {
|
|
||||||
if (this.isConnected) {
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
setTimeout(check, 100);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
check();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取Redis客户端实例
|
|
||||||
* @returns {Redis} Redis客户端
|
|
||||||
* @throws 如果未连接,则记录fatal日志
|
|
||||||
*/
|
|
||||||
public getClient(): Redis {
|
|
||||||
if (!this.isConnected) {
|
|
||||||
logger.fatal(1, 'Redis未连接..');
|
|
||||||
}
|
|
||||||
return this.client;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 断开Redis连接
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
public async disconnect(): Promise<void> {
|
|
||||||
await this.client.quit();
|
|
||||||
this.isConnected = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 存储对象到Redis
|
|
||||||
* @template T
|
|
||||||
* @param {string} key Redis键
|
|
||||||
* @param {T} value 要存储的对象
|
|
||||||
* @param {number} [ttl] 过期时间(秒)
|
|
||||||
*/
|
|
||||||
public async setObject<T>(key: string, value: T, ttl?: number): Promise<void> {
|
|
||||||
const serialized = redisTools.serialize(value);
|
|
||||||
await this.getClient().set(key, serialized);
|
|
||||||
|
|
||||||
if (ttl) {
|
|
||||||
await this.getClient().expire(key, ttl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从Redis获取对象
|
|
||||||
* @template T
|
|
||||||
* @param {string} key Redis键
|
|
||||||
* @returns {Promise<T | undefined>} 获取到的对象或undefined
|
|
||||||
*/
|
|
||||||
public async getObject<T>(key: string): Promise<T | undefined> {
|
|
||||||
const serialized = await this.getClient().get(key);
|
|
||||||
if (!serialized) return undefined;
|
|
||||||
|
|
||||||
const deserialized = redisTools.deserialize<T>(serialized);
|
|
||||||
return redisTools.reviveDates(deserialized);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新Redis中已存在的对象
|
|
||||||
* @template T
|
|
||||||
* @param {string} key Redis键
|
|
||||||
* @param {T} updates 更新内容
|
|
||||||
* @returns {Promise<T>} 更新后的对象
|
|
||||||
*/
|
|
||||||
public async update<T>(key: string, updates: T): Promise<T> {
|
|
||||||
const existing = await this.getObject<T>(key);
|
|
||||||
if (!existing) {
|
|
||||||
logger.error(`数据${key}不存在..`);
|
|
||||||
}
|
|
||||||
const updated = { ...existing, ...updates };
|
|
||||||
await this.setObject(key, updated);
|
|
||||||
return updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从Redis或本地文件获取数据
|
|
||||||
* @template T
|
|
||||||
* @param {string} key Redis键
|
|
||||||
* @param {string} fileName 本地文件名
|
|
||||||
* @returns {Promise<T | undefined>} 获取到的数据或undefined
|
|
||||||
*/
|
|
||||||
public async fetch<T>(key: string, fileName: string): Promise<T | undefined> {
|
|
||||||
const data = await this.getObject<T>(key);
|
|
||||||
if (data) return data;
|
|
||||||
const fromLocal = await Persistence.readDataLocal<T>(key, fileName);
|
|
||||||
if (fromLocal) {
|
|
||||||
await this.setObject(key, fromLocal);
|
|
||||||
return fromLocal;
|
|
||||||
}
|
|
||||||
logger.error(`数据${key}不存在..`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将数据持久化到Redis和本地文件
|
|
||||||
* @template T
|
|
||||||
* @param {string} key Redis键
|
|
||||||
* @param {T} data 要持久化的数据
|
|
||||||
* @param {string} fileName 本地文件名
|
|
||||||
*/
|
|
||||||
public async persistData<T>(key: string, data: T, fileName: string): Promise<void> {
|
|
||||||
await this.setObject(key, data);
|
|
||||||
await Persistence.writeDataLocal(key, data, fileName);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async test(): Promise<void> {
|
|
||||||
const user = await this.fetch<IUser>('Jerry', 'IUser');
|
|
||||||
logger.debug('User:', user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const redisService = new RedisService();
|
|
||||||
export default redisService;
|
|
@ -1,82 +0,0 @@
|
|||||||
import { AuthenticatedSocket } from '../../types/ws';
|
|
||||||
import wsTools from '../../utils/ws/wsTools';
|
|
||||||
import { WebSocket } from 'ws';
|
|
||||||
import logger from '../../utils/core/logger';
|
|
||||||
import redisService from '../redis/redis';
|
|
||||||
import wsClientManager from './wsClientManager';
|
|
||||||
|
|
||||||
type MessageHandler = (socket: WebSocket, msg: any) => Promise<void>;
|
|
||||||
|
|
||||||
class WSMessageHandler {
|
|
||||||
private handlers: Map<string, MessageHandler>;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.handlers = new Map([
|
|
||||||
['test', this.handleTest],
|
|
||||||
['ping', this.handlePing],
|
|
||||||
['pong', this.handlePong],
|
|
||||||
['reportBots', this.handleReportBots],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async handle(socket: AuthenticatedSocket, clientId: string, msg: any) {
|
|
||||||
try {
|
|
||||||
//如果是 pendingRequests 的回包
|
|
||||||
if (msg.requestId && wsClientManager.resolvePendingRequest(msg.requestId, msg)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handler = this.handlers.get(msg.type);
|
|
||||||
|
|
||||||
if (handler) {
|
|
||||||
await handler(socket, msg);
|
|
||||||
} else {
|
|
||||||
await this.handleUnknown(socket, msg);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`ws消息处理时出错: ${err}`);
|
|
||||||
await wsTools.send(socket, {
|
|
||||||
type: 'error',
|
|
||||||
message: 'error message',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleTest(socket: WebSocket, msg: any) {
|
|
||||||
await wsTools.send(socket, {
|
|
||||||
type: 'test',
|
|
||||||
data: { status: 'ok' },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handlePing(socket: WebSocket, msg: any) {
|
|
||||||
await wsTools.send(socket, { type: 'pong' });
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handlePong(socket: WebSocket, msg: any) {
|
|
||||||
//logger.debug(`received pong`);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleUnknown(socket: WebSocket, msg: any) {
|
|
||||||
logger.warn(`收到未知消息类型: ${msg.type}`);
|
|
||||||
await wsTools.send(socket, {
|
|
||||||
type: 'error',
|
|
||||||
message: `未知消息类型: ${msg.type}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleReportBots(socket: WebSocket, msg: any) {
|
|
||||||
logger.debug(`received reportBots: ${msg.data}`);
|
|
||||||
const clientId = msg.data[0].client;
|
|
||||||
const botsData = msg.data.slice(1);
|
|
||||||
await redisService.persistData('crystelfBots', botsData, clientId);
|
|
||||||
logger.debug(`保存了 ${botsData.length} 个 bot(client: ${clientId})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public registerHandler(type: string, handler: MessageHandler): void {
|
|
||||||
this.handlers.set(type, handler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const wsHandler = new WSMessageHandler();
|
|
||||||
export default wsHandler;
|
|
@ -1,127 +0,0 @@
|
|||||||
import WebSocket from 'ws';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import { clearTimeout } from 'node:timers';
|
|
||||||
|
|
||||||
type ClientID = string;
|
|
||||||
const pendingRequests = new Map<string, (data: any) => void>();
|
|
||||||
|
|
||||||
class WSClientManager {
|
|
||||||
private clients = new Map<ClientID, WebSocket>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加ws客户端实例
|
|
||||||
* @param id 标识符
|
|
||||||
* @param socket
|
|
||||||
*/
|
|
||||||
public add(id: ClientID, socket: WebSocket) {
|
|
||||||
this.clients.set(id, socket);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除ws客户端实例
|
|
||||||
* @param id
|
|
||||||
*/
|
|
||||||
public remove(id: ClientID) {
|
|
||||||
this.clients.delete(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取ws客户端实例
|
|
||||||
* @param id
|
|
||||||
*/
|
|
||||||
public get(id: ClientID): WebSocket | undefined {
|
|
||||||
return this.clients.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送消息到ws客户端
|
|
||||||
* @param id ws客户端标识符
|
|
||||||
* @param data 要发送的内容
|
|
||||||
*/
|
|
||||||
public async send(id: ClientID, data: any): Promise<boolean> {
|
|
||||||
const socket = this.clients.get(id);
|
|
||||||
if (!socket || socket.readyState !== WebSocket.OPEN) return false;
|
|
||||||
return this.safeSend(socket, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ws发送请求&等待回调
|
|
||||||
* @param id ws客户端标识符-id
|
|
||||||
* @param data 发送的信息
|
|
||||||
* @param timeout 超时时间 默认5秒
|
|
||||||
*/
|
|
||||||
public async sendAndWait(id: ClientID, data: any, timeout = 5000): Promise<any> {
|
|
||||||
const socket = this.clients.get(id);
|
|
||||||
if (!socket) return;
|
|
||||||
|
|
||||||
data.requestId = uuidv4();
|
|
||||||
const requestId = data.requestId;
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
pendingRequests.delete(requestId);
|
|
||||||
reject(new Error(`${requestId}: 请求超时`));
|
|
||||||
}, timeout);
|
|
||||||
|
|
||||||
pendingRequests.set(requestId, (response) => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
pendingRequests.delete(requestId);
|
|
||||||
resolve(response);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.safeSend(socket, data).catch((err) => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
pendingRequests.delete(requestId);
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理回调
|
|
||||||
* @param requestId
|
|
||||||
* @param data
|
|
||||||
*/
|
|
||||||
public resolvePendingRequest(requestId: string, data: any): boolean {
|
|
||||||
const callback = pendingRequests.get(requestId);
|
|
||||||
if (callback) {
|
|
||||||
pendingRequests.delete(requestId);
|
|
||||||
callback(data);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 广播消息到全部ws客户端
|
|
||||||
* @param data 消息
|
|
||||||
*/
|
|
||||||
public async broadcast(data: any): Promise<void> {
|
|
||||||
const tasks = Array.from(this.clients.values()).map((socket) => {
|
|
||||||
if (socket.readyState === WebSocket.OPEN) {
|
|
||||||
return this.safeSend(socket, data);
|
|
||||||
} else {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await Promise.all(tasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 安全发送消息到ws客户端
|
|
||||||
* @param socket ws客户端
|
|
||||||
* @param data 发送的内容,会自动格式化
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private async safeSend(socket: WebSocket, data: any): Promise<boolean> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
socket.send(JSON.stringify(data), (err) => {
|
|
||||||
if (err) reject(new Error('发送失败'));
|
|
||||||
else resolve(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const wsClientManager = new WSClientManager();
|
|
||||||
export default wsClientManager;
|
|
@ -1,100 +0,0 @@
|
|||||||
import WebSocket, { WebSocketServer } from 'ws';
|
|
||||||
import config from '../../utils/core/config';
|
|
||||||
import logger from '../../utils/core/logger';
|
|
||||||
import { AuthenticatedSocket, AuthMessage, WSMessage } from '../../types/ws';
|
|
||||||
import WsTools from '../../utils/ws/wsTools';
|
|
||||||
import wsHandler from './handler';
|
|
||||||
import { clearInterval } from 'node:timers';
|
|
||||||
import wsClientManager from './wsClientManager';
|
|
||||||
|
|
||||||
class WSServer {
|
|
||||||
private readonly wss: WebSocketServer;
|
|
||||||
private readonly port = Number(config.get('WS_PORT'));
|
|
||||||
private readonly secret = config.get('WS_SECRET');
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.wss = new WebSocketServer({ port: this.port });
|
|
||||||
this.init();
|
|
||||||
logger.info(`WS Server listening on ws://localhost:${this.port}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
private init(): void {
|
|
||||||
this.wss.on('connection', (socket: AuthenticatedSocket, req) => {
|
|
||||||
const ip = req.socket.remoteAddress || 'unknown';
|
|
||||||
logger.info(`收到来自 ${ip} 的 WebSocket 连接请求..`);
|
|
||||||
|
|
||||||
socket.heartbeat = WsTools.setUpHeartbeat(socket);
|
|
||||||
|
|
||||||
socket.on('message', async (raw) => {
|
|
||||||
logger.debug(`Received raw message from ${ip}: ${raw.toString()}`);
|
|
||||||
|
|
||||||
const msg = WsTools.parseMessage<WSMessage>(raw);
|
|
||||||
if (!msg) return this.handleInvalidMessage(socket, ip);
|
|
||||||
|
|
||||||
await this.routeMessage(socket, msg, ip);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('close', () => {
|
|
||||||
logger.info(`ws断开连接 ${ip} (${socket.clientId || 'unauthenticated'})`);
|
|
||||||
this.handleDisconnect(socket);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('error', (err) => {
|
|
||||||
logger.error(`WS error from ${ip}: ${err.message}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleInvalidMessage(socket: WebSocket, ip: string) {
|
|
||||||
logger.warn(`Invalid message received from ${ip}`);
|
|
||||||
await WsTools.send(socket, {
|
|
||||||
type: 'error',
|
|
||||||
message: 'Invalid message format',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async routeMessage(socket: AuthenticatedSocket, msg: WSMessage, ip: string) {
|
|
||||||
if (!socket.isAuthed) {
|
|
||||||
if (this.isAuthMessage(msg)) {
|
|
||||||
logger.info(`Attempting auth from ${ip} as ${msg.clientId}`);
|
|
||||||
await this.handleAuth(socket, msg, ip);
|
|
||||||
} else {
|
|
||||||
logger.warn(`Received message before auth from ${ip}: ${JSON.stringify(msg)}`);
|
|
||||||
await this.handleInvalidMessage(socket, ip);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Routing message from ${socket.clientId}: ${JSON.stringify(msg)}`);
|
|
||||||
await wsHandler.handle(socket, socket.clientId!, msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
private isAuthMessage(msg: WSMessage): msg is AuthMessage {
|
|
||||||
return msg.type === 'auth';
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleAuth(socket: AuthenticatedSocket, msg: AuthMessage, ip: string) {
|
|
||||||
if (msg.secret === this.secret) {
|
|
||||||
socket.isAuthed = true;
|
|
||||||
socket.clientId = msg.clientId;
|
|
||||||
wsClientManager.add(msg.clientId, socket);
|
|
||||||
logger.info(`Auth success from ${ip}, clientId: ${msg.clientId}`);
|
|
||||||
await WsTools.send(socket, { type: 'auth', success: true });
|
|
||||||
} else {
|
|
||||||
logger.warn(`Auth failed from ${ip} (invalid secret), clientId: ${msg.clientId}`);
|
|
||||||
await WsTools.send(socket, { type: 'auth', success: false });
|
|
||||||
socket.close(4001, 'Authentication failed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleDisconnect(socket: AuthenticatedSocket) {
|
|
||||||
if (socket.heartbeat) clearInterval(socket.heartbeat);
|
|
||||||
if (socket.clientId) {
|
|
||||||
wsClientManager.remove(socket.clientId);
|
|
||||||
logger.info(`Removed client ${socket.clientId} from manager`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const wsServer = new WSServer();
|
|
||||||
export default wsServer;
|
|
@ -1,70 +0,0 @@
|
|||||||
import WebSocket from 'ws';
|
|
||||||
import axios from 'axios';
|
|
||||||
import logger from '../utils/core/logger';
|
|
||||||
|
|
||||||
const WS_URL = 'ws://127.0.0.1:4001';
|
|
||||||
const WS_SECRET = '114514';
|
|
||||||
const CLIENT_ID = 'test';
|
|
||||||
|
|
||||||
function createWebSocketClient() {
|
|
||||||
const socket = new WebSocket(WS_URL);
|
|
||||||
|
|
||||||
socket.on('open', () => {
|
|
||||||
console.log('[WS] Connected to server');
|
|
||||||
|
|
||||||
const authPayload = {
|
|
||||||
type: 'auth',
|
|
||||||
secret: WS_SECRET,
|
|
||||||
clientId: CLIENT_ID,
|
|
||||||
};
|
|
||||||
socket.send(JSON.stringify(authPayload));
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('message', (raw) => {
|
|
||||||
const msg = JSON.parse(raw.toString());
|
|
||||||
console.log('[WS] Message from server:', msg);
|
|
||||||
|
|
||||||
if (msg.type === 'auth' && msg.success === true) {
|
|
||||||
socket.send(JSON.stringify({ type: 'test' }));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('close', () => {
|
|
||||||
console.log('[WS] Connection closed');
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('error', (err) => {
|
|
||||||
console.error('[WS] Error:', err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testGetAPI() {
|
|
||||||
try {
|
|
||||||
const response = await axios.get('http://localhost:4000/api/sample/hello');
|
|
||||||
console.log('[HTTP][GET] Response:', response.data);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[HTTP][GET] Error:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testPostAPI() {
|
|
||||||
try {
|
|
||||||
const response = await axios.post('https://core.crystelf.top/api/bot/getGroupInfo', {
|
|
||||||
token: '114113',
|
|
||||||
groupId: 796070855,
|
|
||||||
});
|
|
||||||
logger.info('[HTTP][POST] Response:', response.data);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[HTTP][POST] Error:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
createWebSocketClient();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
testPostAPI();
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
@ -1,5 +0,0 @@
|
|||||||
export default interface GroupInfo {
|
|
||||||
name: string;
|
|
||||||
groupId: number;
|
|
||||||
memberCount: number; //群人数
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
export default interface UserInfo {
|
|
||||||
qq: number;
|
|
||||||
email?: string;
|
|
||||||
labAccount?: string;
|
|
||||||
username: string;
|
|
||||||
nickname?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 管理的群
|
|
||||||
* 第一个number为群号,第二个number为在群内的botId
|
|
||||||
*/
|
|
||||||
manageGroups: Record<number, number[]>;
|
|
||||||
role: 'super' | 'admin' | 'user';
|
|
||||||
balance: number;
|
|
||||||
bots: number[];
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
interface RetryOptions {
|
|
||||||
maxAttempts: number;
|
|
||||||
initialDelay: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RetryOptions;
|
|
@ -1,10 +0,0 @@
|
|||||||
interface IUser {
|
|
||||||
name: string;
|
|
||||||
qq: string;
|
|
||||||
password: string;
|
|
||||||
isAdmin: boolean;
|
|
||||||
lastLogin?: Date;
|
|
||||||
createdAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default IUser;
|
|
@ -1,18 +0,0 @@
|
|||||||
import WebSocket from 'ws';
|
|
||||||
|
|
||||||
export interface AuthenticatedSocket extends WebSocket {
|
|
||||||
isAuthed?: boolean;
|
|
||||||
clientId?: string;
|
|
||||||
heartbeat?: NodeJS.Timeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WSMessage {
|
|
||||||
type: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AuthMessage extends WSMessage {
|
|
||||||
type: 'auth';
|
|
||||||
secret: string;
|
|
||||||
clientId: string;
|
|
||||||
}
|
|
@ -1,90 +0,0 @@
|
|||||||
import simpleGit, { SimpleGit } from 'simple-git';
|
|
||||||
import paths from './path';
|
|
||||||
import logger from './logger';
|
|
||||||
import { exec } from 'child_process';
|
|
||||||
import { promisify } from 'util';
|
|
||||||
import fs from 'fs';
|
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
|
||||||
|
|
||||||
class AutoUpdater {
|
|
||||||
private git: SimpleGit;
|
|
||||||
private readonly repoPath: string;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.git = simpleGit();
|
|
||||||
this.repoPath = paths.get('root');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查远程更新
|
|
||||||
*/
|
|
||||||
public async checkForUpdates(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
logger.info('检查仓库更新中..');
|
|
||||||
|
|
||||||
const status = await this.git.status();
|
|
||||||
if (status.ahead > 0) {
|
|
||||||
logger.info('检测到当地仓库有未提交的更改,跳过更新..');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('正在获取远程仓库信息..');
|
|
||||||
await this.git.fetch();
|
|
||||||
|
|
||||||
const localBranch = status.current;
|
|
||||||
const diffSummary = await this.git.diffSummary([`${localBranch}..origin/${localBranch}`]);
|
|
||||||
|
|
||||||
if (diffSummary.files.length > 0) {
|
|
||||||
logger.info('检测到远程仓库有更新!');
|
|
||||||
|
|
||||||
logger.info('正在拉取更新..');
|
|
||||||
if (localBranch) {
|
|
||||||
await this.git.pull('origin', localBranch);
|
|
||||||
} else {
|
|
||||||
logger.error('当前分支名称未知,无法执行拉取操作..');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('代码更新成功,开始更新依赖..');
|
|
||||||
await this.updateDependencies();
|
|
||||||
|
|
||||||
logger.info('自动更新流程完成。');
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
logger.info('远程仓库没有新变化..');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('检查仓库更新失败: ', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 自动更新依赖和构建
|
|
||||||
*/
|
|
||||||
private async updateDependencies(): Promise<void> {
|
|
||||||
try {
|
|
||||||
logger.info('执行 pnpm install...');
|
|
||||||
await execAsync('pnpm install', { cwd: this.repoPath });
|
|
||||||
logger.info('依赖安装完成。');
|
|
||||||
|
|
||||||
const pkgPath = paths.get('package');
|
|
||||||
const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
||||||
|
|
||||||
if (pkgJson.scripts?.build) {
|
|
||||||
logger.info('检测到 build 脚本,执行 pnpm build...');
|
|
||||||
await execAsync('pnpm build', { cwd: this.repoPath });
|
|
||||||
logger.info('构建完成。');
|
|
||||||
} else {
|
|
||||||
logger.info('未检测到 build 脚本,跳过构建。');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('更新依赖或构建失败: ', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const autoUpdater = new AutoUpdater();
|
|
||||||
export default autoUpdater;
|
|
@ -1,72 +0,0 @@
|
|||||||
import dotenv from 'dotenv';
|
|
||||||
import logger from './logger';
|
|
||||||
|
|
||||||
class ConfigManger {
|
|
||||||
private static instance: ConfigManger;
|
|
||||||
private readonly env: NodeJS.ProcessEnv;
|
|
||||||
|
|
||||||
private constructor() {
|
|
||||||
dotenv.config();
|
|
||||||
this.env = process.env;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取单例实例
|
|
||||||
*/
|
|
||||||
public static getInstance(): ConfigManger {
|
|
||||||
if (!ConfigManger.instance) {
|
|
||||||
ConfigManger.instance = new ConfigManger();
|
|
||||||
}
|
|
||||||
return ConfigManger.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取环境变量(带类型推断)
|
|
||||||
* @param key 环境变量键名
|
|
||||||
* @param defaultValue 默认值(决定返回类型)
|
|
||||||
*/
|
|
||||||
public get<T = string>(key: string, defaultValue?: T): T {
|
|
||||||
const value = this.env[key];
|
|
||||||
if (value === undefined) {
|
|
||||||
if (defaultValue !== undefined) return defaultValue;
|
|
||||||
logger.fatal(1, `环境变量${key}未定义!`);
|
|
||||||
return undefined as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (typeof defaultValue) {
|
|
||||||
case 'number':
|
|
||||||
return Number(value) as T;
|
|
||||||
case 'boolean':
|
|
||||||
return (value === 'true') as T;
|
|
||||||
default:
|
|
||||||
return value as T;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置环境变量(运行时有效)
|
|
||||||
* @param key 键名
|
|
||||||
* @param value 值
|
|
||||||
*/
|
|
||||||
public set(key: string, value: string | number | boolean): void {
|
|
||||||
this.env[key] = String(value);
|
|
||||||
logger.debug(`成功更改环境变量${key}为${value}!`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查环境变量是否已加载
|
|
||||||
*/
|
|
||||||
public check(keys: string[]): void {
|
|
||||||
keys.forEach((key) => {
|
|
||||||
if (!(key in this.env)) {
|
|
||||||
logger.fatal(1, `必须环境变量缺失:${key}`);
|
|
||||||
} else {
|
|
||||||
logger.debug(`检测到环境变量${key}!`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = ConfigManger.getInstance();
|
|
||||||
|
|
||||||
export default config;
|
|
@ -1,74 +0,0 @@
|
|||||||
class date {
|
|
||||||
/**
|
|
||||||
* 获取当前日期 (格式: YYYYMMDD)
|
|
||||||
*/
|
|
||||||
public static getCurrentDate(): string {
|
|
||||||
const now = new Date();
|
|
||||||
return [
|
|
||||||
now.getFullYear(),
|
|
||||||
(now.getMonth() + 1).toString().padStart(2, '0'),
|
|
||||||
now.getDate().toString().padStart(2, '0'),
|
|
||||||
].join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前时间 (格式: HH:mm:ss)
|
|
||||||
*/
|
|
||||||
public static getCurrentTime(): string {
|
|
||||||
return new Date().toLocaleTimeString('en-US', {
|
|
||||||
hour12: false,
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
second: '2-digit',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取格式化日期时间
|
|
||||||
* @param formatStr 格式字符串 (YYYY-年, MM-月, DD-日, HH-时, mm-分, ss-秒)
|
|
||||||
* @example format('YYYY-MM-DD HH:mm:ss') => '2023-10-15 14:30:45'
|
|
||||||
*/
|
|
||||||
public static format(formatStr: string = 'YYYY-MM-DD HH:mm:ss'): string {
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
const replacements: Record<string, string> = {
|
|
||||||
YYYY: now.getFullYear().toString(),
|
|
||||||
MM: (now.getMonth() + 1).toString().padStart(2, '0'),
|
|
||||||
DD: now.getDate().toString().padStart(2, '0'),
|
|
||||||
HH: now.getHours().toString().padStart(2, '0'),
|
|
||||||
mm: now.getMinutes().toString().padStart(2, '0'),
|
|
||||||
ss: now.getSeconds().toString().padStart(2, '0'),
|
|
||||||
};
|
|
||||||
|
|
||||||
return formatStr.replace(/YYYY|MM|DD|HH|mm|ss/g, (match) => replacements[match]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算日期差值
|
|
||||||
* @param start 开始日期
|
|
||||||
* @param end 结束日期 (默认当前时间)
|
|
||||||
* @param unit 返回单位 ('days' | 'hours' | 'minutes' | 'seconds')
|
|
||||||
*/
|
|
||||||
public static diff(
|
|
||||||
start: Date,
|
|
||||||
end: Date = new Date(),
|
|
||||||
unit: 'days' | 'hours' | 'minutes' | 'seconds' = 'days'
|
|
||||||
): number {
|
|
||||||
const msDiff = end.getTime() - start.getTime();
|
|
||||||
|
|
||||||
switch (unit) {
|
|
||||||
case 'seconds':
|
|
||||||
return Math.floor(msDiff / 1000);
|
|
||||||
case 'minutes':
|
|
||||||
return Math.floor(msDiff / (1000 * 60));
|
|
||||||
case 'hours':
|
|
||||||
return Math.floor(msDiff / (1000 * 60 * 60));
|
|
||||||
case 'days':
|
|
||||||
return Math.floor(msDiff / (1000 * 60 * 60 * 24));
|
|
||||||
default:
|
|
||||||
return msDiff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default date;
|
|
@ -1,57 +0,0 @@
|
|||||||
import path from 'path';
|
|
||||||
import fs from 'fs/promises';
|
|
||||||
import paths from './path';
|
|
||||||
import date from './date';
|
|
||||||
import logger from './logger';
|
|
||||||
import chalk from 'chalk';
|
|
||||||
|
|
||||||
class fc {
|
|
||||||
/**
|
|
||||||
* 创建目录
|
|
||||||
* @param targetPath 目标路径
|
|
||||||
* @param includeFile 是否包含文件路径
|
|
||||||
*/
|
|
||||||
public static async createDir(
|
|
||||||
targetPath: string = '',
|
|
||||||
includeFile: boolean = false
|
|
||||||
): Promise<void> {
|
|
||||||
const root = paths.get('root');
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (path.isAbsolute(targetPath)) {
|
|
||||||
const dirToCreate = includeFile ? path.dirname(targetPath) : targetPath;
|
|
||||||
await fs.mkdir(dirToCreate, { recursive: true });
|
|
||||||
//logger.debug(`成功创建绝对目录: ${dirToCreate}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullPath = includeFile
|
|
||||||
? path.join(root, path.dirname(targetPath))
|
|
||||||
: path.join(root, targetPath);
|
|
||||||
|
|
||||||
await fs.mkdir(fullPath, { recursive: true });
|
|
||||||
//logger.debug(`成功创建相对目录: ${fullPath}`);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`创建目录失败: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 输出日志到文件
|
|
||||||
* @param message
|
|
||||||
*/
|
|
||||||
public static async logToFile(message: string): Promise<void> {
|
|
||||||
const logFile = path.join(paths.get('log'), `${date.getCurrentDate()}.log`);
|
|
||||||
const logMessage = `${message}\n`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
//await this.createDir(paths.get('log'));
|
|
||||||
|
|
||||||
await fs.appendFile(logFile, logMessage);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(chalk.red('[LOGGER] 写入日志失败:'), err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default fc;
|
|
@ -1,69 +0,0 @@
|
|||||||
import chalk from 'chalk';
|
|
||||||
import config from './config';
|
|
||||||
import fc from './file';
|
|
||||||
import date from './date';
|
|
||||||
|
|
||||||
class Logger {
|
|
||||||
private static instance: Logger;
|
|
||||||
private readonly isDebug: boolean;
|
|
||||||
|
|
||||||
private constructor() {
|
|
||||||
this.isDebug = config.get('DEBUG', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getInstance(): Logger {
|
|
||||||
if (!Logger.instance) {
|
|
||||||
Logger.instance = new Logger();
|
|
||||||
}
|
|
||||||
return Logger.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public debug(...args: any[]): void {
|
|
||||||
if (this.isDebug) {
|
|
||||||
const message = this.formatMessage('DEBUG', args);
|
|
||||||
console.log(chalk.cyan(message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public info(...args: any[]): void {
|
|
||||||
const message = this.formatMessage('INFO', args);
|
|
||||||
console.log(chalk.green(message));
|
|
||||||
this.logToFile(message).then();
|
|
||||||
}
|
|
||||||
|
|
||||||
public warn(...args: any[]): void {
|
|
||||||
const message = this.formatMessage('WARN', args);
|
|
||||||
console.log(chalk.yellow(message));
|
|
||||||
this.logToFile(message).then();
|
|
||||||
}
|
|
||||||
|
|
||||||
public error(...args: any[]): void {
|
|
||||||
const message = this.formatMessage('ERROR', args);
|
|
||||||
console.error(chalk.red(message));
|
|
||||||
this.logToFile(message).then();
|
|
||||||
}
|
|
||||||
|
|
||||||
public fatal(exitCode: number = 1, ...args: any[]): never {
|
|
||||||
const message = this.formatMessage('FATAL', args);
|
|
||||||
console.error(chalk.red.bold(message));
|
|
||||||
this.logToFile(message).then();
|
|
||||||
process.exit(exitCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatMessage(level: string, args: any[]): string {
|
|
||||||
return `[${date.getCurrentTime()}][${level}] ${args
|
|
||||||
.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : arg))
|
|
||||||
.join(' ')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async logToFile(message: string): Promise<void> {
|
|
||||||
try {
|
|
||||||
await fc.logToFile(`${message}`);
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error(chalk.red(`[LOGGER] 写入日志失败: ${err.message}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const logger = Logger.getInstance();
|
|
||||||
export default logger;
|
|
@ -1,98 +0,0 @@
|
|||||||
import path from 'path';
|
|
||||||
import fs from 'fs';
|
|
||||||
import fc from './file';
|
|
||||||
|
|
||||||
class PathManager {
|
|
||||||
private static instance: PathManager;
|
|
||||||
private readonly baseDir: string;
|
|
||||||
|
|
||||||
private constructor() {
|
|
||||||
this.baseDir = path.join(__dirname, '../../..');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取单例实例
|
|
||||||
*/
|
|
||||||
public static getInstance(): PathManager {
|
|
||||||
if (!PathManager.instance) {
|
|
||||||
PathManager.instance = new PathManager();
|
|
||||||
}
|
|
||||||
return PathManager.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取预定义路径
|
|
||||||
* @param type 路径类型
|
|
||||||
*/
|
|
||||||
public get(type?: PathType): string {
|
|
||||||
const mappings: Record<PathType, string> = {
|
|
||||||
root: this.baseDir,
|
|
||||||
public: path.join(this.baseDir, 'public'),
|
|
||||||
images: path.join(this.baseDir, 'public/files/image'),
|
|
||||||
log: path.join(this.baseDir, 'logs'),
|
|
||||||
config: path.join(this.baseDir, 'config'),
|
|
||||||
temp: path.join(this.baseDir, 'temp'),
|
|
||||||
userData: path.join(this.baseDir, 'private/data'),
|
|
||||||
files: path.join(this.baseDir, 'public/files'),
|
|
||||||
media: path.join(this.baseDir, 'public/files/media'),
|
|
||||||
package: path.join(this.baseDir, 'package.json'),
|
|
||||||
modules: path.join(this.baseDir, 'src/modules'),
|
|
||||||
uploads: path.join(this.baseDir, 'public/files/uploads'),
|
|
||||||
words: path.join(this.baseDir, 'private/data/word'),
|
|
||||||
};
|
|
||||||
|
|
||||||
return type ? mappings[type] : this.baseDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化
|
|
||||||
*/
|
|
||||||
public init(): void {
|
|
||||||
/*
|
|
||||||
const logPath = this.get('log');
|
|
||||||
const imagePath = this.get('images');
|
|
||||||
const dataPath = this.get('userData');
|
|
||||||
const mediaPath = this.get('media');
|
|
||||||
fc.createDir(logPath, false);
|
|
||||||
fc.createDir(imagePath, false);
|
|
||||||
fc.createDir(mediaPath, false);
|
|
||||||
fc.createDir(dataPath, false);
|
|
||||||
logger.debug(`日志目录初始化: ${logPath}`);
|
|
||||||
logger.debug(`图像目录初始化: ${imagePath};${mediaPath}`);
|
|
||||||
logger.debug(`用户数据目录初始化: ${dataPath}`);
|
|
||||||
*/
|
|
||||||
const pathsToInit = [
|
|
||||||
this.get('log'),
|
|
||||||
this.get('config'),
|
|
||||||
this.get('userData'),
|
|
||||||
this.get('media'),
|
|
||||||
this.get('temp'),
|
|
||||||
this.get('uploads'),
|
|
||||||
this.get('words'),
|
|
||||||
];
|
|
||||||
|
|
||||||
pathsToInit.forEach((dirPath) => {
|
|
||||||
if (!fs.existsSync(dirPath)) {
|
|
||||||
fc.createDir(dirPath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type PathType =
|
|
||||||
| 'root'
|
|
||||||
| 'public'
|
|
||||||
| 'images'
|
|
||||||
| 'log'
|
|
||||||
| 'config'
|
|
||||||
| 'temp'
|
|
||||||
| 'userData'
|
|
||||||
| 'files'
|
|
||||||
| 'package'
|
|
||||||
| 'media'
|
|
||||||
| 'modules'
|
|
||||||
| 'words'
|
|
||||||
| 'uploads';
|
|
||||||
|
|
||||||
const paths = PathManager.getInstance();
|
|
||||||
export default paths;
|
|
@ -1,72 +0,0 @@
|
|||||||
import { Response } from 'express';
|
|
||||||
import logger from './logger';
|
|
||||||
|
|
||||||
class response {
|
|
||||||
/**
|
|
||||||
* 成功响应
|
|
||||||
* @param res Express响应对象
|
|
||||||
* @param data 返回数据
|
|
||||||
* @param statusCode HTTP状态码,默认200
|
|
||||||
*/
|
|
||||||
public static async success(res: Response, data: any, statusCode = 200) {
|
|
||||||
res.status(statusCode).json({
|
|
||||||
success: true,
|
|
||||||
data: data,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 错误响应
|
|
||||||
* @param res Express响应对象
|
|
||||||
* @param message 错误信息
|
|
||||||
* @param statusCode HTTP状态码,默认500
|
|
||||||
* @param error 原始错误对象(开发环境显示)
|
|
||||||
*/
|
|
||||||
public static async error(
|
|
||||||
res: Response,
|
|
||||||
message: string = '请求失败..',
|
|
||||||
statusCode = 500,
|
|
||||||
error?: any
|
|
||||||
) {
|
|
||||||
const response: Record<string, any> = {
|
|
||||||
success: false,
|
|
||||||
data: message,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.debug(error instanceof Error ? error.stack : error);
|
|
||||||
|
|
||||||
res.status(statusCode).json(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分页数据响应
|
|
||||||
* @param res Express响应对象
|
|
||||||
* @param data 数据数组
|
|
||||||
* @param total 总条数
|
|
||||||
* @param page 当前页码
|
|
||||||
* @param pageSize 每页条数
|
|
||||||
*/
|
|
||||||
public static async pagination(
|
|
||||||
res: Response,
|
|
||||||
data: any[],
|
|
||||||
total: number,
|
|
||||||
page: number,
|
|
||||||
pageSize: number
|
|
||||||
) {
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
data,
|
|
||||||
pagination: {
|
|
||||||
total,
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
totalPages: Math.ceil(total / pageSize),
|
|
||||||
},
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default response;
|
|
@ -1,40 +0,0 @@
|
|||||||
import path from 'path';
|
|
||||||
import paths from './path';
|
|
||||||
import fs from 'fs';
|
|
||||||
import logger from './logger';
|
|
||||||
|
|
||||||
const restartFile = path.join(paths.get('temp'), 'restart.timestamp');
|
|
||||||
class System {
|
|
||||||
/**
|
|
||||||
* 重启前保存时间戳
|
|
||||||
*/
|
|
||||||
private static markRestartTime() {
|
|
||||||
const now = Date.now();
|
|
||||||
fs.writeFileSync(restartFile, now.toString(), 'utf-8');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查重启时间戳
|
|
||||||
*/
|
|
||||||
public static checkRestartTime() {
|
|
||||||
if (fs.existsSync(restartFile)) {
|
|
||||||
const prev = Number(fs.readFileSync(restartFile, 'utf-8'));
|
|
||||||
const duration = ((Date.now() - prev) / 1000 - 5).toFixed(2);
|
|
||||||
fs.unlinkSync(restartFile);
|
|
||||||
return Number(duration);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重启服务
|
|
||||||
*/
|
|
||||||
public static async restart() {
|
|
||||||
this.markRestartTime();
|
|
||||||
logger.warn('服务即将重启..');
|
|
||||||
await new Promise((r) => setTimeout(r, 300));
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default System;
|
|
@ -1,48 +0,0 @@
|
|||||||
import RetryOptions from '../../types/retry';
|
|
||||||
import logger from './logger';
|
|
||||||
|
|
||||||
let tools = {
|
|
||||||
/**
|
|
||||||
* 异步重试机制
|
|
||||||
* @param operation
|
|
||||||
* @param options
|
|
||||||
*/
|
|
||||||
async retry(operation: () => Promise<any>, options: RetryOptions): Promise<any> {
|
|
||||||
let attempt = 0;
|
|
||||||
let lastError: any;
|
|
||||||
|
|
||||||
while (attempt < options.maxAttempts) {
|
|
||||||
try {
|
|
||||||
return await operation();
|
|
||||||
} catch (error) {
|
|
||||||
lastError = error;
|
|
||||||
attempt++;
|
|
||||||
|
|
||||||
if (attempt < options.maxAttempts) {
|
|
||||||
const delay = options.initialDelay * Math.pow(2, attempt - 1);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.error(lastError);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从一个可迭代列表中随机选择一个对象
|
|
||||||
* @param list 可迭代数据
|
|
||||||
*/
|
|
||||||
getRandomItem<T>(list: T[]): T {
|
|
||||||
return list[Math.floor(Math.random() * list.length)];
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取随机数
|
|
||||||
* @param min 最小值
|
|
||||||
* @param max 最大值
|
|
||||||
*/
|
|
||||||
getRandomDelay(min: number, max: number): number {
|
|
||||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default tools;
|
|
@ -1,29 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import response from '../core/response';
|
|
||||||
import Config from '../core/config';
|
|
||||||
|
|
||||||
let tools = {
|
|
||||||
/**
|
|
||||||
* token验证错误处理逻辑
|
|
||||||
* @param res
|
|
||||||
* @param token
|
|
||||||
*/
|
|
||||||
async tokenCheckFailed(res: express.Response, token: string): Promise<void> {
|
|
||||||
await response.error(
|
|
||||||
res,
|
|
||||||
'token验证失败..',
|
|
||||||
404,
|
|
||||||
`有个小可爱使用了错误的token:${JSON.stringify(token)}`
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查token是否正确
|
|
||||||
* @param token
|
|
||||||
*/
|
|
||||||
checkToken(token: string): boolean {
|
|
||||||
return token.toString() === Config.get('TOKEN').toString();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default tools;
|
|
@ -1,66 +0,0 @@
|
|||||||
import path from 'path';
|
|
||||||
import paths from '../core/path';
|
|
||||||
import fc from '../core/file';
|
|
||||||
import logger from '../core/logger';
|
|
||||||
import fs from 'fs/promises';
|
|
||||||
|
|
||||||
class Persistence {
|
|
||||||
private static getDataPath(dataName: string, fileName: string): string {
|
|
||||||
return path.join(paths.get('userData'), dataName, `${fileName}.json`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 确保数据目录存在
|
|
||||||
* @param dataName
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private static async ensureDataPath(dataName: string): Promise<void> {
|
|
||||||
const dataPath = path.join(paths.get('userData'), dataName);
|
|
||||||
try {
|
|
||||||
await fc.createDir(dataPath, false);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将数据写入本地,以json格式存储
|
|
||||||
* @param dataName 目录名
|
|
||||||
* @param data 文件内容
|
|
||||||
* @param fileName 文件名
|
|
||||||
*/
|
|
||||||
public static async writeDataLocal<T>(
|
|
||||||
dataName: string,
|
|
||||||
data: T,
|
|
||||||
fileName: string
|
|
||||||
): Promise<void> {
|
|
||||||
await this.ensureDataPath(dataName);
|
|
||||||
const filePath = this.getDataPath(dataName, fileName);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
||||||
logger.debug(`用户数据已持久化到本地${filePath}`);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从本地读取文件
|
|
||||||
* @param dataName 目录名
|
|
||||||
* @param fileName 文件名
|
|
||||||
*/
|
|
||||||
public static async readDataLocal<T>(dataName: string, fileName: string): Promise<T | undefined> {
|
|
||||||
const filePath = this.getDataPath(dataName, fileName);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = await fs.readFile(filePath, 'utf-8');
|
|
||||||
return JSON.parse(data) as T;
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(err);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Persistence;
|
|
@ -1,31 +0,0 @@
|
|||||||
import logger from '../core/logger';
|
|
||||||
|
|
||||||
class redisTools {
|
|
||||||
public static serialize<T>(data: T): string {
|
|
||||||
return JSON.stringify(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static deserialize<T>(jsonString: string): T | undefined {
|
|
||||||
try {
|
|
||||||
return JSON.parse(jsonString);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`redis反序列化失败:${err}`);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static reviveDates<T>(obj: T): T {
|
|
||||||
const dateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/;
|
|
||||||
|
|
||||||
const reviver = (_: string, value: any) => {
|
|
||||||
if (typeof value === 'string' && dateRegex.test(value)) {
|
|
||||||
return new Date(value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
return JSON.parse(JSON.stringify(obj), reviver);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const redisTool = redisTools;
|
|
||||||
export default redisTool;
|
|
@ -1,49 +0,0 @@
|
|||||||
import WebSocket from 'ws';
|
|
||||||
import logger from '../core/logger';
|
|
||||||
import { setInterval } from 'node:timers';
|
|
||||||
|
|
||||||
class WsTools {
|
|
||||||
/**
|
|
||||||
* 发送消息
|
|
||||||
* @param socket
|
|
||||||
* @param data
|
|
||||||
*/
|
|
||||||
static async send(socket: WebSocket, data: unknown): Promise<boolean> {
|
|
||||||
if (socket.readyState !== WebSocket.OPEN) return false;
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
socket.send(JSON.stringify(data), (err) => {
|
|
||||||
resolve(!err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析消息
|
|
||||||
* @param data
|
|
||||||
*/
|
|
||||||
static parseMessage<T>(data: WebSocket.RawData): T | null {
|
|
||||||
try {
|
|
||||||
return JSON.parse(data.toString()) as T;
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(err);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 心跳检测
|
|
||||||
* @param socket
|
|
||||||
* @param interval
|
|
||||||
*/
|
|
||||||
static setUpHeartbeat(socket: WebSocket, interval = 30000): NodeJS.Timeout {
|
|
||||||
const heartbeat = () => {
|
|
||||||
if (socket.readyState === WebSocket.OPEN) {
|
|
||||||
WsTools.send(socket, { type: 'ping' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return setInterval(heartbeat, interval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default WsTools;
|
|
4
tsconfig.build.json
Normal file
4
tsconfig.build.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
@ -1,12 +1,21 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"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