mirror of
https://github.com/crystelf/crystelf-core.git
synced 2025-10-14 05:19:19 +00:00
Compare commits
5 Commits
ccda3ec271
...
839df5170a
Author | SHA1 | Date | |
---|---|---|---|
839df5170a | |||
de264b1244 | |||
e775bcdf77 | |||
6809d07bcf | |||
35eea17a8f |
@ -8,6 +8,10 @@ import { AutoUpdateModule } from './core/auto-update/auto-update.module';
|
|||||||
import { PersistenceModule } from './core/persistence/persistence.module';
|
import { PersistenceModule } from './core/persistence/persistence.module';
|
||||||
import { RedisModule } from './core/redis/redis.module';
|
import { RedisModule } from './core/redis/redis.module';
|
||||||
import { WsModule } from './core/ws/ws.module';
|
import { WsModule } from './core/ws/ws.module';
|
||||||
|
import { SystemWebModule } from './modules/system/systemWeb.module';
|
||||||
|
import { BotModule } from './modules/bot/bot.module';
|
||||||
|
import { CdnModule } from './modules/cdn/cdn.module';
|
||||||
|
import { WordsModule } from './modules/words/words.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -20,6 +24,10 @@ import { WsModule } from './core/ws/ws.module';
|
|||||||
AutoUpdateModule,
|
AutoUpdateModule,
|
||||||
RedisModule,
|
RedisModule,
|
||||||
WsModule,
|
WsModule,
|
||||||
|
SystemWebModule,
|
||||||
|
BotModule,
|
||||||
|
CdnModule,
|
||||||
|
WordsModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
@ -13,57 +13,65 @@ export class AutoUpdateService {
|
|||||||
private readonly git: SimpleGit;
|
private readonly git: SimpleGit;
|
||||||
private readonly repoPath: string;
|
private readonly repoPath: string;
|
||||||
|
|
||||||
constructor(
|
constructor(@Inject(PathService) private readonly pathService: PathService) {
|
||||||
@Inject(PathService)
|
|
||||||
private readonly pathService: PathService,
|
|
||||||
) {
|
|
||||||
this.git = simpleGit();
|
this.git = simpleGit();
|
||||||
this.repoPath = this.pathService.get('root');
|
this.repoPath = this.pathService.get('root');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查远程更新
|
* 检查主仓库远程更新
|
||||||
*/
|
*/
|
||||||
async checkForUpdates(): Promise<boolean> {
|
async checkForUpdates(): Promise<boolean> {
|
||||||
try {
|
return this.checkRepoForUpdates(this.repoPath, 'crystelf-core');
|
||||||
this.logger.log('检查仓库更新中...');
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查指定文件夹的更新
|
||||||
|
*/
|
||||||
|
async checkRepoForUpdates(
|
||||||
|
folderPath: string,
|
||||||
|
label = '子仓库',
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
this.logger.log(`[${label}] 检查仓库更新中...`);
|
||||||
|
|
||||||
|
const repoGit = simpleGit(folderPath);
|
||||||
|
const status = await repoGit.status();
|
||||||
|
|
||||||
const status = await this.git.status();
|
|
||||||
if (status.ahead > 0) {
|
if (status.ahead > 0) {
|
||||||
this.logger.warn('检测到本地仓库有未提交的更改,跳过更新');
|
this.logger.warn(`[${label}] 检测到本地仓库有未提交的更改,跳过更新`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log('正在获取远程仓库信息...');
|
this.logger.log(`[${label}] 正在获取远程仓库信息...`);
|
||||||
await this.git.fetch();
|
await repoGit.fetch();
|
||||||
|
|
||||||
const localBranch = status.current;
|
const localBranch = status.current;
|
||||||
const diffSummary = await this.git.diffSummary([
|
const diffSummary = await repoGit.diffSummary([
|
||||||
`${localBranch}..origin/${localBranch}`,
|
`${localBranch}..origin/${localBranch}`,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (diffSummary.files.length > 0) {
|
if (diffSummary.files.length > 0) {
|
||||||
this.logger.log('检测到远程仓库有更新!');
|
this.logger.log(`[${label}] 检测到远程仓库有更新!`);
|
||||||
|
|
||||||
if (localBranch) {
|
if (localBranch) {
|
||||||
this.logger.log('正在拉取远程代码...');
|
this.logger.log(`[${label}] 正在拉取远程代码...`);
|
||||||
await this.git.pull('origin', localBranch);
|
await repoGit.pull('origin', localBranch);
|
||||||
} else {
|
} else {
|
||||||
this.logger.error('当前分支名称未知,无法执行拉取操作。');
|
this.logger.error(`[${label}] 当前分支名称未知,无法执行拉取操作。`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log('代码更新成功,开始更新依赖...');
|
this.logger.log(`[${label}] 代码更新成功,开始更新依赖...`);
|
||||||
await this.updateDependencies();
|
await this.updateDependencies(folderPath, label);
|
||||||
|
|
||||||
this.logger.log('自动更新流程完成');
|
this.logger.log(`[${label}] 自动更新流程完成`);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
this.logger.log('远程仓库没有新变化');
|
this.logger.log(`[${label}] 远程仓库没有新变化`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('检查仓库更新失败:', error);
|
this.logger.error(`[${label}] 检查仓库更新失败:`, error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -71,24 +79,34 @@ export class AutoUpdateService {
|
|||||||
/**
|
/**
|
||||||
* 自动安装依赖和构建
|
* 自动安装依赖和构建
|
||||||
*/
|
*/
|
||||||
private async updateDependencies(): Promise<void> {
|
private async updateDependencies(
|
||||||
|
folderPath: string,
|
||||||
|
label = '仓库',
|
||||||
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.logger.log('执行 pnpm install...');
|
this.logger.log(`[${label}] 执行 pnpm install...`);
|
||||||
await execAsync('pnpm install', { cwd: this.repoPath });
|
await execAsync('pnpm install', { cwd: folderPath });
|
||||||
this.logger.log('依赖安装完成');
|
this.logger.log(`[${label}] 依赖安装完成`);
|
||||||
|
|
||||||
const pkgPath = this.pathService.get('package');
|
const pkgPath = `${folderPath}/package.json`;
|
||||||
const pkgJson = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
let pkgJson: any;
|
||||||
|
|
||||||
|
try {
|
||||||
|
pkgJson = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
||||||
|
} catch {
|
||||||
|
this.logger.warn(`[${label}] 未找到 package.json,跳过依赖构建`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (pkgJson.scripts?.build) {
|
if (pkgJson.scripts?.build) {
|
||||||
this.logger.log('检测到 build 脚本,执行 pnpm build...');
|
this.logger.log(`[${label}] 检测到 build 脚本,执行 pnpm build...`);
|
||||||
await execAsync('pnpm build', { cwd: this.repoPath });
|
await execAsync('pnpm build', { cwd: folderPath });
|
||||||
this.logger.log('构建完成');
|
this.logger.log(`[${label}] 构建完成`);
|
||||||
} else {
|
} else {
|
||||||
this.logger.log('未检测到 build 脚本,跳过构建');
|
this.logger.log(`[${label}] 未检测到 build 脚本,跳过构建`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('更新依赖或构建失败:', error);
|
this.logger.error(`[${label}] 更新依赖或构建失败:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,17 +21,13 @@ export class PathService {
|
|||||||
const mappings: Record<PathType, string> = {
|
const mappings: Record<PathType, string> = {
|
||||||
root: this.baseDir,
|
root: this.baseDir,
|
||||||
public: path.join(this.baseDir, 'public'),
|
public: path.join(this.baseDir, 'public'),
|
||||||
images: path.join(this.baseDir, 'public/files/image'),
|
|
||||||
log: path.join(this.baseDir, 'logs'),
|
log: path.join(this.baseDir, 'logs'),
|
||||||
config: path.join(this.baseDir, 'config'),
|
config: path.join(this.baseDir, 'config'),
|
||||||
temp: path.join(this.baseDir, 'temp'),
|
temp: path.join(this.baseDir, 'temp'),
|
||||||
userData: path.join(this.baseDir, 'private/data'),
|
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'),
|
package: path.join(this.baseDir, 'package.json'),
|
||||||
modules: path.join(this.baseDir, 'src/modules'),
|
modules: path.join(this.baseDir, 'src/modules'),
|
||||||
uploads: path.join(this.baseDir, 'public/files/uploads'),
|
words: path.join(this.baseDir, 'private/word'),
|
||||||
words: path.join(this.baseDir, 'private/data/word'),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return type ? mappings[type] : this.baseDir;
|
return type ? mappings[type] : this.baseDir;
|
||||||
@ -46,9 +42,8 @@ export class PathService {
|
|||||||
this.get('log'),
|
this.get('log'),
|
||||||
this.get('config'),
|
this.get('config'),
|
||||||
this.get('userData'),
|
this.get('userData'),
|
||||||
this.get('media'),
|
|
||||||
this.get('temp'),
|
this.get('temp'),
|
||||||
this.get('uploads'),
|
this.get('public'),
|
||||||
this.get('words'),
|
this.get('words'),
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -105,14 +100,10 @@ export class PathService {
|
|||||||
export type PathType =
|
export type PathType =
|
||||||
| 'root'
|
| 'root'
|
||||||
| 'public'
|
| 'public'
|
||||||
| 'images'
|
|
||||||
| 'log'
|
| 'log'
|
||||||
| 'config'
|
| 'config'
|
||||||
| 'temp'
|
| 'temp'
|
||||||
| 'userData'
|
| 'userData'
|
||||||
| 'files'
|
|
||||||
| 'package'
|
| 'package'
|
||||||
| 'media'
|
|
||||||
| 'modules'
|
| 'modules'
|
||||||
| 'words'
|
| 'words';
|
||||||
| 'uploads';
|
|
||||||
|
37
src/core/tools/token-auth.guard.ts
Normal file
37
src/core/tools/token-auth.guard.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
UnauthorizedException,
|
||||||
|
Logger,
|
||||||
|
Inject,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ToolsService } from './tools.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* token验证守卫
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class TokenAuthGuard implements CanActivate {
|
||||||
|
private readonly logger = new Logger(TokenAuthGuard.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(ToolsService) private readonly toolsService: ToolsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
canActivate(context: ExecutionContext): boolean {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const token = request.body?.token || request.headers['x-token']; //两种传入方式
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
this.logger.warn('请求缺少 token');
|
||||||
|
throw new UnauthorizedException('缺少 token');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.toolsService.checkToken(token)) {
|
||||||
|
this.toolsService.tokenCheckFailed(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
17
src/main.ts
17
src/main.ts
@ -1,6 +1,6 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger, RequestMethod } from '@nestjs/common';
|
||||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||||
import { ResponseInterceptor } from './common/interceptors/response.interceptor';
|
import { ResponseInterceptor } from './common/interceptors/response.interceptor';
|
||||||
import { AllExceptionsFilter } from './common/filters/all-exception.filter';
|
import { AllExceptionsFilter } from './common/filters/all-exception.filter';
|
||||||
@ -10,7 +10,14 @@ import { WsAdapter } from '@nestjs/platform-ws';
|
|||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
Logger.log('晶灵核心初始化..');
|
Logger.log('晶灵核心初始化..');
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
app.setGlobalPrefix('api');
|
app.setGlobalPrefix('api', {
|
||||||
|
exclude: [
|
||||||
|
'cdn',
|
||||||
|
{ path: 'cdn/(.*)', method: RequestMethod.ALL },
|
||||||
|
'public',
|
||||||
|
{ path: 'public/(.*)', method: RequestMethod.ALL },
|
||||||
|
],
|
||||||
|
});
|
||||||
app.useGlobalInterceptors(new ResponseInterceptor());
|
app.useGlobalInterceptors(new ResponseInterceptor());
|
||||||
app.useGlobalFilters(new AllExceptionsFilter());
|
app.useGlobalFilters(new AllExceptionsFilter());
|
||||||
const systemService = app.get(SystemService);
|
const systemService = app.get(SystemService);
|
||||||
@ -24,7 +31,7 @@ async function bootstrap() {
|
|||||||
.setVersion('1.0')
|
.setVersion('1.0')
|
||||||
.build();
|
.build();
|
||||||
const document = () => SwaggerModule.createDocument(app, config);
|
const document = () => SwaggerModule.createDocument(app, config);
|
||||||
SwaggerModule.setup('', app, document);
|
SwaggerModule.setup('docs', app, document);
|
||||||
app.useWebSocketAdapter(new WsAdapter(app));
|
app.useWebSocketAdapter(new WsAdapter(app));
|
||||||
await app.listen(7000);
|
await app.listen(7000);
|
||||||
await systemService.checkUpdate().catch((err) => {
|
await systemService.checkUpdate().catch((err) => {
|
||||||
@ -32,6 +39,6 @@ async function bootstrap() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
bootstrap().then(() => {
|
bootstrap().then(() => {
|
||||||
Logger.log(`API服务已启动:http://localhost:7000`);
|
Logger.log(`API服务已启动:http://localhost:7000/api`);
|
||||||
Logger.log(`API文档: http://localhost:7000/api`);
|
Logger.log(`API文档: http://localhost:7000/docs`);
|
||||||
});
|
});
|
||||||
|
70
src/modules/bot/bot.controller.ts
Normal file
70
src/modules/bot/bot.controller.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { Body, Controller, Inject, Post, UseGuards } from '@nestjs/common';
|
||||||
|
import { ApiOperation, ApiTags, ApiBody } from '@nestjs/swagger';
|
||||||
|
import { BotService } from './bot.service';
|
||||||
|
import { WsClientManager } from 'src/core/ws/ws-client.manager';
|
||||||
|
import { TokenAuthGuard } from 'src/core/tools/token-auth.guard';
|
||||||
|
import {
|
||||||
|
BroadcastDto,
|
||||||
|
GroupInfoDto,
|
||||||
|
SendMessageDto,
|
||||||
|
TokenDto,
|
||||||
|
} from './bot.dto';
|
||||||
|
|
||||||
|
@ApiTags('Bot相关操作')
|
||||||
|
@Controller('bot')
|
||||||
|
export class BotController {
|
||||||
|
constructor(
|
||||||
|
@Inject(BotService)
|
||||||
|
private readonly botService: BotService,
|
||||||
|
@Inject(WsClientManager)
|
||||||
|
private readonly wsClientManager: WsClientManager,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post('getBotId')
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
|
@ApiOperation({ summary: '获取当前连接到核心的全部 botId 数组' })
|
||||||
|
async postBotsId(@Body() dto: TokenDto) {
|
||||||
|
return this.botService.getBotId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('getGroupInfo')
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
|
@ApiOperation({ summary: '获取群聊信息' })
|
||||||
|
@ApiBody({ type: GroupInfoDto })
|
||||||
|
async postGroupInfo(@Body() dto: GroupInfoDto) {
|
||||||
|
return this.botService.getGroupInfo({ groupId: dto.groupId });
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('reportBots')
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
|
@ApiOperation({ summary: '广播:要求同步群聊信息和 bot 连接情况' })
|
||||||
|
async reportBots(@Body() dto: TokenDto) {
|
||||||
|
const sendMessage = {
|
||||||
|
type: 'reportBots',
|
||||||
|
data: {},
|
||||||
|
};
|
||||||
|
await this.wsClientManager.broadcast(sendMessage);
|
||||||
|
return { message: '正在请求同步 bot 数据..' };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('sendMessage')
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
|
@ApiOperation({ summary: '发送消息到群聊', description: '自动选择bot发送' })
|
||||||
|
@ApiBody({ type: SendMessageDto })
|
||||||
|
async sendMessage(@Body() dto: SendMessageDto) {
|
||||||
|
const flag = await this.botService.sendMessage(dto.groupId, dto.message);
|
||||||
|
if (!flag) {
|
||||||
|
return { message: '消息发送失败' };
|
||||||
|
}
|
||||||
|
return { message: '消息发送成功' };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('broadcast')
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
|
@ApiOperation({ summary: '广播消息到全部群聊', description: '随机延迟' })
|
||||||
|
@ApiBody({ type: BroadcastDto })
|
||||||
|
async smartBroadcast(@Body() dto: BroadcastDto) {
|
||||||
|
await this.botService.broadcastToAllGroups(dto.message);
|
||||||
|
return { message: '广播任务已开始,正在后台执行..' };
|
||||||
|
}
|
||||||
|
}
|
24
src/modules/bot/bot.dto.ts
Normal file
24
src/modules/bot/bot.dto.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class TokenDto {
|
||||||
|
@ApiProperty({ description: '访问核心的鉴权 token' })
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GroupInfoDto extends TokenDto {
|
||||||
|
@ApiProperty({ description: '群号', example: 114514 })
|
||||||
|
groupId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SendMessageDto extends GroupInfoDto {
|
||||||
|
@ApiProperty({ description: '要发送的消息', example: 'Ciallo~(∠・ω< )⌒★' })
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BroadcastDto extends TokenDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: '要广播的消息',
|
||||||
|
example: '全体目光向我看齐!我宣布个事儿..',
|
||||||
|
})
|
||||||
|
message: string;
|
||||||
|
}
|
14
src/modules/bot/bot.module.ts
Normal file
14
src/modules/bot/bot.module.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { RedisModule } from '../../core/redis/redis.module';
|
||||||
|
import { WsModule } from '../../core/ws/ws.module';
|
||||||
|
import { ToolsModule } from '../../core/tools/tools.module';
|
||||||
|
import { PathModule } from '../../core/path/path.module';
|
||||||
|
import { BotController } from './bot.controller';
|
||||||
|
import { BotService } from './bot.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [RedisModule, WsModule, ToolsModule, PathModule],
|
||||||
|
controllers: [BotController],
|
||||||
|
providers: [BotService],
|
||||||
|
})
|
||||||
|
export class BotModule {}
|
255
src/modules/bot/bot.service.ts
Normal file
255
src/modules/bot/bot.service.ts
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
|
import * as fs from 'fs/promises';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { RedisService } from 'src/core/redis/redis.service';
|
||||||
|
import { WsClientManager } from 'src/core/ws/ws-client.manager';
|
||||||
|
import { ToolsService } from '../../core/tools/tools.service';
|
||||||
|
import { PathService } from '../../core/path/path.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BotService {
|
||||||
|
private readonly logger = new Logger(BotService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(RedisService)
|
||||||
|
private readonly redisService: RedisService,
|
||||||
|
@Inject(WsClientManager)
|
||||||
|
private readonly wsClientManager: WsClientManager,
|
||||||
|
@Inject(ToolsService)
|
||||||
|
private readonly tools: ToolsService,
|
||||||
|
@Inject(PathService)
|
||||||
|
private readonly paths: PathService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取botId数组
|
||||||
|
*/
|
||||||
|
async getBotId(): Promise<{ uin: number; nickName: string }[]> {
|
||||||
|
this.logger.debug('正在请求获取在线的bot..');
|
||||||
|
const userPath = this.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 this.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) {
|
||||||
|
this.logger.error(`读取或解析 ${fileName} 出错: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uins;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取群聊信息
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
async getGroupInfo(data: {
|
||||||
|
botId?: number;
|
||||||
|
groupId: number;
|
||||||
|
clientId?: string;
|
||||||
|
}): Promise<any> {
|
||||||
|
this.logger.debug(`正在尝试获取${data.groupId}的信息..)`);
|
||||||
|
const sendBot: number | undefined =
|
||||||
|
data.botId ?? (await this.getGroupBot(data.groupId));
|
||||||
|
if (!sendBot) {
|
||||||
|
this.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 this.wsClientManager.sendAndWait(
|
||||||
|
sendData.data.clientID,
|
||||||
|
sendData,
|
||||||
|
);
|
||||||
|
return returnData ?? undefined;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息到群聊
|
||||||
|
* @param groupId 群号
|
||||||
|
* @param message 消息
|
||||||
|
*/
|
||||||
|
async sendMessage(groupId: number, message: string): Promise<boolean> {
|
||||||
|
this.logger.log(`发送${message}到${groupId}..`);
|
||||||
|
const sendBot = await this.getGroupBot(groupId);
|
||||||
|
if (!sendBot) {
|
||||||
|
this.logger.warn(`不存在能向群聊${groupId}发送消息的Bot!`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const client = await this.getBotClient(sendBot);
|
||||||
|
if (!client) {
|
||||||
|
this.logger.warn(`不存在${sendBot}对应的client!`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const sendData = {
|
||||||
|
type: 'sendMessage',
|
||||||
|
data: { botId: sendBot, groupId, clientId: client, message },
|
||||||
|
};
|
||||||
|
await this.wsClientManager.send(client, sendData);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 广播消息
|
||||||
|
* @param message 要广播的消息
|
||||||
|
*/
|
||||||
|
async broadcastToAllGroups(message: string): Promise<void> {
|
||||||
|
const userPath = this.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();
|
||||||
|
this.logger.log(`广播消息:${message}`);
|
||||||
|
for (const fileName of dirData) {
|
||||||
|
if (!fileName.endsWith('.json')) continue;
|
||||||
|
|
||||||
|
const clientId = path.basename(fileName, '.json');
|
||||||
|
const botList = await this.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()) {
|
||||||
|
this.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 = this.tools.getRandomItem([
|
||||||
|
...clientGroups.keys(),
|
||||||
|
]);
|
||||||
|
const botCandidates = clientGroups.get(selectedClientId)!;
|
||||||
|
const selectedBotId = this.tools.getRandomItem(botCandidates);
|
||||||
|
const delay = this.tools.getRandomDelay(10_000, 150_000);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const sendData = {
|
||||||
|
type: 'sendMessage',
|
||||||
|
data: {
|
||||||
|
botId: selectedBotId,
|
||||||
|
groupId,
|
||||||
|
clientId: selectedClientId,
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.logger.log(
|
||||||
|
`[广播] 向群 ${groupId} 使用Bot ${selectedBotId}(客户端 ${selectedClientId})发送消息${message},延迟 ${
|
||||||
|
delay / 1000
|
||||||
|
} 秒`,
|
||||||
|
);
|
||||||
|
this.wsClientManager.send(selectedClientId, sendData).catch((e) => {
|
||||||
|
this.logger.error(`发送到群${groupId}失败:`, e);
|
||||||
|
});
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取botId对应的client
|
||||||
|
* @param botId
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async getBotClient(botId: number): Promise<string | undefined> {
|
||||||
|
const userPath = this.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 this.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) {
|
||||||
|
this.logger.error(`读取${clientId}出错..`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取groupId对应的botId
|
||||||
|
* @param groupId
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async getGroupBot(groupId: number): Promise<number | undefined> {
|
||||||
|
const userPath = this.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 this.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) {
|
||||||
|
this.logger.error(`读取${clientId}出错..`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
79
src/modules/cdn/cdn.controller.ts
Normal file
79
src/modules/cdn/cdn.controller.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Res,
|
||||||
|
Logger,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Inject,
|
||||||
|
Req,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { CdnService } from './cdn.service';
|
||||||
|
import { Response } from 'express';
|
||||||
|
import { ApiOperation } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class CdnController {
|
||||||
|
private readonly logger = new Logger(CdnController.name);
|
||||||
|
|
||||||
|
constructor(@Inject(CdnService) private readonly fileService: CdnService) {}
|
||||||
|
|
||||||
|
private async deliverFile(relativePath: string, res: Response) {
|
||||||
|
try {
|
||||||
|
this.logger.log(`有个小可爱正在请求 /cdn/${relativePath} ..`);
|
||||||
|
|
||||||
|
const filePath = await this.fileService.getFile(relativePath);
|
||||||
|
if (!filePath) {
|
||||||
|
this.logger.warn(`${relativePath}:文件不存在..`);
|
||||||
|
throw new HttpException('文件不存在啦!', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.sendFile(filePath, (err) => {
|
||||||
|
if (err) {
|
||||||
|
this.logger.error(`文件投递失败: ${err.message}`);
|
||||||
|
throw new HttpException(
|
||||||
|
'Crystelf-CDN处理文件请求时出错..',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`成功投递文件: ${filePath}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('晶灵数据请求处理失败:', error);
|
||||||
|
throw new HttpException(
|
||||||
|
'Crystelf-CDN处理文件请求时出错..',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('cdn/*')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '获取资源',
|
||||||
|
description: '由晶灵资源分发服务器(CDN)提供支持',
|
||||||
|
})
|
||||||
|
async getFile(@Res() res: Response, @Req() req: Request) {
|
||||||
|
const relativePath = req.url.replace('/cdn/', ''); //params.path;
|
||||||
|
return this.deliverFile(relativePath, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('public/files/*')
|
||||||
|
async fromPublicFiles(@Res() res: Response, @Req() req: Request) {
|
||||||
|
const relativePath = req.url.replace('/public/files/', '');
|
||||||
|
this.logger.debug(
|
||||||
|
`请求 /public/files/${relativePath} → 代理到 /cdn/${relativePath}`,
|
||||||
|
);
|
||||||
|
return this.deliverFile(relativePath, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('public/cdn/*')
|
||||||
|
async fromPublicCdn(@Req() req: Request, @Res() res: Response) {
|
||||||
|
const relativePath = req.url.replace('/public/cdn/', '');
|
||||||
|
this.logger.debug(
|
||||||
|
`请求 /public/cdn/${relativePath} → 代理到 /cdn/${relativePath}`,
|
||||||
|
);
|
||||||
|
return this.deliverFile(relativePath, res);
|
||||||
|
}
|
||||||
|
}
|
11
src/modules/cdn/cdn.module.ts
Normal file
11
src/modules/cdn/cdn.module.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { CdnController } from './cdn.controller';
|
||||||
|
import { CdnService } from './cdn.service';
|
||||||
|
import { PathModule } from '../../core/path/path.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PathModule],
|
||||||
|
controllers: [CdnController],
|
||||||
|
providers: [CdnService],
|
||||||
|
})
|
||||||
|
export class CdnModule {}
|
54
src/modules/cdn/cdn.service.ts
Normal file
54
src/modules/cdn/cdn.service.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
|
import { PathService } from '../../core/path/path.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CdnService {
|
||||||
|
private readonly logger = new Logger(CdnService.name);
|
||||||
|
private filePath: string;
|
||||||
|
@Inject(PathService)
|
||||||
|
private readonly paths: PathService;
|
||||||
|
constructor() {
|
||||||
|
this.logger.log(`晶灵云图数据中心初始化.. 数据存储在: ${this.filePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件
|
||||||
|
* @param relativePath 文件相对路径
|
||||||
|
*/
|
||||||
|
async getFile(relativePath: string): Promise<string | null> {
|
||||||
|
if (!this.filePath) this.filePath = this.paths.get('public');
|
||||||
|
if (
|
||||||
|
!this.isValidPath(relativePath) &&
|
||||||
|
!this.isValidFilename(path.basename(relativePath))
|
||||||
|
) {
|
||||||
|
throw new Error('非法路径请求');
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = path.join(this.filePath, relativePath);
|
||||||
|
this.logger.debug(`尝试访问文件路径: ${filePath}`);
|
||||||
|
|
||||||
|
return existsSync(filePath) ? filePath : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查路径合法性
|
||||||
|
*/
|
||||||
|
private isValidPath(relativePath: string): boolean {
|
||||||
|
try {
|
||||||
|
const normalized = path.normalize(relativePath);
|
||||||
|
let flag = true;
|
||||||
|
if (normalized.startsWith('../') && path.isAbsolute(normalized))
|
||||||
|
flag = false;
|
||||||
|
return flag;
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isValidFilename(filename: string): boolean {
|
||||||
|
return /^[a-zA-Z0-9_\-.]+$/.test(filename);
|
||||||
|
}
|
||||||
|
}
|
@ -1,58 +0,0 @@
|
|||||||
import {
|
|
||||||
Controller,
|
|
||||||
Post,
|
|
||||||
Body,
|
|
||||||
UnauthorizedException,
|
|
||||||
Inject,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { ApiTags, ApiOperation, ApiBody } from '@nestjs/swagger';
|
|
||||||
import { SystemWebService } from './system.service';
|
|
||||||
import { ToolsService } from '../../core/tools/tools.service';
|
|
||||||
|
|
||||||
class TokenDto {
|
|
||||||
token: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ApiTags('System')
|
|
||||||
@Controller('system')
|
|
||||||
export class SystemController {
|
|
||||||
constructor(
|
|
||||||
@Inject(SystemWebService)
|
|
||||||
private readonly systemService: SystemWebService,
|
|
||||||
@Inject(ToolsService)
|
|
||||||
private readonly toolService: ToolsService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重启系统
|
|
||||||
*/
|
|
||||||
@Post('restart')
|
|
||||||
@ApiOperation({
|
|
||||||
summary: '系统重启',
|
|
||||||
description: '传入正确的 token 后,核心将执行重启。',
|
|
||||||
})
|
|
||||||
@ApiBody({ type: TokenDto })
|
|
||||||
async systemRestart(@Body() body: TokenDto): Promise<string> {
|
|
||||||
if (!this.toolService.checkToken(body.token)) {
|
|
||||||
throw new UnauthorizedException('Token 无效');
|
|
||||||
}
|
|
||||||
await this.systemService.systemRestart();
|
|
||||||
return '核心正在重启..';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取系统重启耗时
|
|
||||||
*/
|
|
||||||
@Post('getRestartTime')
|
|
||||||
@ApiOperation({
|
|
||||||
summary: '获取重启所需时间',
|
|
||||||
description: '传入正确的 token,返回上次核心重启的耗时',
|
|
||||||
})
|
|
||||||
@ApiBody({ type: TokenDto })
|
|
||||||
async getRestartTime(@Body() body: TokenDto): Promise<string> {
|
|
||||||
if (!this.toolService.checkToken(body.token)) {
|
|
||||||
throw new UnauthorizedException('Token 无效');
|
|
||||||
}
|
|
||||||
return await this.systemService.getRestartTime();
|
|
||||||
}
|
|
||||||
}
|
|
53
src/modules/system/systemWeb.controller.ts
Normal file
53
src/modules/system/systemWeb.controller.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { Controller, Post, Inject, UseGuards, Param } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation, ApiBody, ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { SystemWebService } from './systemWeb.service';
|
||||||
|
import { ToolsService } from '../../core/tools/tools.service';
|
||||||
|
import { TokenAuthGuard } from '../../core/tools/token-auth.guard';
|
||||||
|
|
||||||
|
class WebServerDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: '密钥',
|
||||||
|
example: '1111',
|
||||||
|
})
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiTags('System')
|
||||||
|
@Controller('system')
|
||||||
|
export class SystemWebController {
|
||||||
|
constructor(
|
||||||
|
@Inject(SystemWebService)
|
||||||
|
private readonly systemService: SystemWebService,
|
||||||
|
@Inject(ToolsService)
|
||||||
|
private readonly toolService: ToolsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重启系统
|
||||||
|
*/
|
||||||
|
@Post('restart')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '系统重启',
|
||||||
|
description: '核心执行重启',
|
||||||
|
})
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
|
@ApiBody({ type: WebServerDto })
|
||||||
|
async systemRestart(@Param('token') token: string): Promise<string> {
|
||||||
|
this.systemService.systemRestart();
|
||||||
|
return '核心正在重启..';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取系统重启耗时
|
||||||
|
*/
|
||||||
|
@Post('getRestartTime')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '获取重启所需时间',
|
||||||
|
description: '返回上次核心重启的耗时',
|
||||||
|
})
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
|
@ApiBody({ type: WebServerDto })
|
||||||
|
async getRestartTime(@Param('token') token: string): Promise<string> {
|
||||||
|
return await this.systemService.getRestartTime();
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,13 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { SystemController } from './system.controller';
|
import { SystemWebController } from './systemWeb.controller';
|
||||||
import { SystemWebService } from './system.service';
|
import { SystemWebService } from './systemWeb.service';
|
||||||
import { ToolsModule } from '../../core/tools/tools.module';
|
import { ToolsModule } from '../../core/tools/tools.module';
|
||||||
import { PathModule } from '../../core/path/path.module';
|
import { PathModule } from '../../core/path/path.module';
|
||||||
|
import { SystemModule } from '../../core/system/system.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ToolsModule, SystemModule, PathModule],
|
imports: [ToolsModule, SystemModule, PathModule],
|
||||||
controllers: [SystemController],
|
controllers: [SystemWebController],
|
||||||
providers: [SystemWebService],
|
providers: [SystemWebService],
|
||||||
})
|
})
|
||||||
export class SystemModule {}
|
export class SystemWebModule {}
|
@ -1,6 +1,6 @@
|
|||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import path from 'path';
|
import * as path from 'path';
|
||||||
import { PathService } from '../../core/path/path.service';
|
import { PathService } from '../../core/path/path.service';
|
||||||
import { SystemService } from 'src/core/system/system.service';
|
import { SystemService } from 'src/core/system/system.service';
|
||||||
|
|
83
src/modules/words/words.controller.ts
Normal file
83
src/modules/words/words.controller.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Logger,
|
||||||
|
Inject,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { WordsService } from './words.service';
|
||||||
|
import { TokenAuthGuard } from '../../core/tools/token-auth.guard';
|
||||||
|
import { ApiBody, ApiOperation, ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
class WordsDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: '文案id',
|
||||||
|
example: 'poke',
|
||||||
|
})
|
||||||
|
id: string;
|
||||||
|
@ApiProperty({
|
||||||
|
description: '密钥',
|
||||||
|
example: '1111',
|
||||||
|
})
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Controller('words')
|
||||||
|
export class WordsController {
|
||||||
|
private readonly logger = new Logger(WordsController.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(WordsService) private readonly wordsService: WordsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取随机文案
|
||||||
|
*/
|
||||||
|
@Get('getText/:id')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '获取随机文案',
|
||||||
|
})
|
||||||
|
async getText(@Param('id') id: string) {
|
||||||
|
try {
|
||||||
|
const texts = await this.wordsService.loadWordById(id);
|
||||||
|
if (!texts || texts.length === 0) {
|
||||||
|
throw new HttpException(
|
||||||
|
`文案 ${id} 不存在或为空..`,
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const randomIndex = Math.floor(Math.random() * texts.length);
|
||||||
|
return texts[randomIndex];
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(`getText 失败: ${e?.message}`);
|
||||||
|
throw new HttpException('服务器错误', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重载文案
|
||||||
|
*/
|
||||||
|
@Post('reloadText/:id')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '重载某条文案',
|
||||||
|
})
|
||||||
|
@UseGuards(TokenAuthGuard)
|
||||||
|
@ApiBody({ type: WordsDto })
|
||||||
|
async reloadWord(@Param('id') id: string, @Param('token') token: string) {
|
||||||
|
try {
|
||||||
|
const success = await this.wordsService.reloadWord(id);
|
||||||
|
if (success) {
|
||||||
|
return '成功重载..';
|
||||||
|
} else {
|
||||||
|
throw new HttpException('重载失败..', HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(`reloadWord 失败: ${e?.message}`);
|
||||||
|
throw new HttpException('服务器错误', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
src/modules/words/words.module.ts
Normal file
14
src/modules/words/words.module.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { WordsController } from './words.controller';
|
||||||
|
import { WordsService } from './words.service';
|
||||||
|
import { PathModule } from '../../core/path/path.module';
|
||||||
|
import { ToolsModule } from '../../core/tools/tools.module';
|
||||||
|
import { AutoUpdateModule } from '../../core/auto-update/auto-update.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PathModule, ToolsModule, AutoUpdateModule],
|
||||||
|
controllers: [WordsController],
|
||||||
|
providers: [WordsService],
|
||||||
|
exports: [WordsService],
|
||||||
|
})
|
||||||
|
export class WordsModule {}
|
94
src/modules/words/words.service.ts
Normal file
94
src/modules/words/words.service.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs/promises';
|
||||||
|
import { PathService } from '../../core/path/path.service';
|
||||||
|
import { AutoUpdateService } from '../../core/auto-update/auto-update.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WordsService {
|
||||||
|
private readonly logger = new Logger(WordsService.name);
|
||||||
|
private wordCache: Record<string, string[]> = {};
|
||||||
|
private readonly clearIntervalMs = 30 * 60 * 1000; // 30min
|
||||||
|
|
||||||
|
@Inject(PathService)
|
||||||
|
private readonly paths: PathService;
|
||||||
|
|
||||||
|
@Inject(AutoUpdateService)
|
||||||
|
private readonly autoUpdateService: AutoUpdateService;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.startAutoClear();
|
||||||
|
this.startAutoUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动定时清理缓存
|
||||||
|
*/
|
||||||
|
private startAutoClear() {
|
||||||
|
setInterval(() => {
|
||||||
|
this.logger.log('清理文案缓存..');
|
||||||
|
this.wordCache = {};
|
||||||
|
}, this.clearIntervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动定时检查 words 仓库更新
|
||||||
|
*/
|
||||||
|
private startAutoUpdate() {
|
||||||
|
setInterval(async () => {
|
||||||
|
const wordsPath = this.paths.get('words');
|
||||||
|
this.logger.log('定时检查文案仓库更新..');
|
||||||
|
const updated = await this.autoUpdateService.checkRepoForUpdates(
|
||||||
|
wordsPath,
|
||||||
|
'words 仓库',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
this.logger.log('文案仓库已更新,清理缓存..');
|
||||||
|
this.wordCache = {};
|
||||||
|
}
|
||||||
|
}, this.clearIntervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从本地加载文案到内存
|
||||||
|
*/
|
||||||
|
async loadWordById(id: string): Promise<string[] | null> {
|
||||||
|
this.logger.log(`加载文案 ${id}..`);
|
||||||
|
if (this.wordCache[id]) return this.wordCache[id];
|
||||||
|
const filePath = path.join(this.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;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(`加载文案失败: ${id}..`, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重载 json 到内存
|
||||||
|
*/
|
||||||
|
async reloadWord(id: string): Promise<boolean> {
|
||||||
|
this.logger.log(`重载文案: ${id}..`);
|
||||||
|
const filePath = path.join(this.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;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(`重载文案失败: ${id}`, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user