diff --git a/package.json b/package.json index eb6b8a0..6731776 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "axios": "^1.10.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", + "simple-git": "^3.28.0", "ssh2": "^1.16.0" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94af222..f0f0b71 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ dependencies: rxjs: specifier: ^7.8.1 version: 7.8.2 + simple-git: + specifier: ^3.28.0 + version: 3.28.0 ssh2: specifier: ^1.16.0 version: 1.16.0 @@ -1159,6 +1162,18 @@ packages: '@jridgewell/sourcemap-codec': 1.5.4 dev: true + /@kwsites/file-exists@1.1.1: + resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} + dependencies: + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@kwsites/promise-deferred@1.1.1: + resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} + dev: false + /@lukeed/csprng@1.1.0: resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} @@ -5601,6 +5616,16 @@ packages: engines: {node: '>=14'} dev: true + /simple-git@3.28.0: + resolution: {integrity: sha512-Rs/vQRwsn1ILH1oBUy8NucJlXmnnLeLCfcvbSehkPzbv3wwoFWIdtfd6Ndo6ZPhlPsCZ60CPI4rxurnwAa+a2w==} + dependencies: + '@kwsites/file-exists': 1.1.1 + '@kwsites/promise-deferred': 1.1.1 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + dev: false + /sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} dev: true diff --git a/src/app.module.ts b/src/app.module.ts index 8a69d53..0544b93 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,8 +2,10 @@ import { Module } from '@nestjs/common'; import { RootModule } from './root/root.module'; import { AppConfigModule } from './config/config.module'; import { PathModule } from './core/path/path.module'; +import { SystemModule } from './core/system/system.module'; +import { ToolsModule } from './core/tools/tools.module'; @Module({ - imports: [RootModule, AppConfigModule, PathModule], + imports: [RootModule, AppConfigModule, PathModule, SystemModule, ToolsModule], }) export class AppModule {} diff --git a/src/core/auto-update/auto-update.module.ts b/src/core/auto-update/auto-update.module.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/core/auto-update/auto-update.service.ts b/src/core/auto-update/auto-update.service.ts new file mode 100644 index 0000000..2dfdf07 --- /dev/null +++ b/src/core/auto-update/auto-update.service.ts @@ -0,0 +1,94 @@ +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import { readFileSync } from 'fs'; +import simpleGit, { SimpleGit } from 'simple-git'; +import { PathService } from '../path/path.service'; + +const execAsync = promisify(exec); + +@Injectable() +export class AutoUpdateService { + private readonly logger = new Logger(AutoUpdateService.name); + private readonly git: SimpleGit; + private readonly repoPath: string; + + constructor( + @Inject(PathService) + private readonly pathService: PathService, + ) { + this.git = simpleGit(); + this.repoPath = this.pathService.get('root'); + } + + /** + * 检查是否有远程更新 + */ + async checkForUpdates(): Promise { + try { + this.logger.log('检查仓库更新中...'); + + const status = await this.git.status(); + if (status.ahead > 0) { + this.logger.warn('检测到本地仓库有未提交的更改,跳过更新'); + return false; + } + + this.logger.log('正在获取远程仓库信息...'); + await this.git.fetch(); + + const localBranch = status.current; + const diffSummary = await this.git.diffSummary([ + `${localBranch}..origin/${localBranch}`, + ]); + + if (diffSummary.files.length > 0) { + this.logger.log('检测到远程仓库有更新!'); + + if (localBranch) { + this.logger.log('正在拉取远程代码...'); + await this.git.pull('origin', localBranch); + } else { + this.logger.error('当前分支名称未知,无法执行拉取操作。'); + return false; + } + + this.logger.log('代码更新成功,开始更新依赖...'); + await this.updateDependencies(); + + this.logger.log('自动更新流程完成'); + return true; + } else { + this.logger.log('远程仓库没有新变化'); + return false; + } + } catch (error) { + this.logger.error('检查仓库更新失败:', error); + return false; + } + } + + /** + * 自动安装依赖和构建 + */ + private async updateDependencies(): Promise { + try { + this.logger.log('执行 pnpm install...'); + await execAsync('pnpm install', { cwd: this.repoPath }); + this.logger.log('依赖安装完成'); + + const pkgPath = this.pathService.get('package'); + const pkgJson = JSON.parse(readFileSync(pkgPath, 'utf-8')); + + if (pkgJson.scripts?.build) { + this.logger.log('检测到 build 脚本,执行 pnpm build...'); + await execAsync('pnpm build', { cwd: this.repoPath }); + this.logger.log('构建完成'); + } else { + this.logger.log('未检测到 build 脚本,跳过构建'); + } + } catch (error) { + this.logger.error('更新依赖或构建失败:', error); + } + } +} diff --git a/src/core/path/path.service.ts b/src/core/path/path.service.ts index 66efe44..c78b2b4 100644 --- a/src/core/path/path.service.ts +++ b/src/core/path/path.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@nestjs/common'; import * as path from 'path'; import * as fs from 'fs'; -import { AppConfigService } from '../../config/config.service'; import { Logger } from '@nestjs/common'; @Injectable() @@ -9,7 +8,7 @@ export class PathService { private readonly baseDir: string; private readonly logger = new Logger(PathService.name); - constructor(private readonly configService: AppConfigService) { + constructor() { this.baseDir = path.join(__dirname, '../../..'); this.initializePaths(); } diff --git a/src/core/system/system.module.ts b/src/core/system/system.module.ts new file mode 100644 index 0000000..5ca334d --- /dev/null +++ b/src/core/system/system.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { SystemService } from './system.service'; +import { PathModule } from '../path/path.module'; + +@Module({ + imports: [PathModule], + providers: [SystemService], + exports: [SystemService], +}) +export class SystemModule {} diff --git a/src/core/system/system.service.ts b/src/core/system/system.service.ts new file mode 100644 index 0000000..6159e03 --- /dev/null +++ b/src/core/system/system.service.ts @@ -0,0 +1,53 @@ +import { Inject, Injectable, Logger } from '@nestjs/common'; +import * as path from 'path'; +import * as fs from 'fs'; +import { PathService } from '../path/path.service'; + +@Injectable() +export class SystemService { + private readonly logger = new Logger(SystemService.name); + private readonly restartFile: string; + + constructor( + @Inject(PathService) + private readonly pathService: PathService, + ) { + this.restartFile = path.join( + this.pathService.get('temp'), + 'restart.timestamp', + ); + } + + /** + * 重启前保存时间戳 + */ + private markRestartTime(): void { + const now = Date.now(); + fs.writeFileSync(this.restartFile, now.toString(), 'utf-8'); + this.logger.debug(`记录重启时间戳: ${now}`); + } + + /** + * 检查重启时间戳 + */ + checkRestartTime(): number | null { + if (fs.existsSync(this.restartFile)) { + const prev = Number(fs.readFileSync(this.restartFile, 'utf-8')); + const duration = ((Date.now() - prev) / 1000 - 5).toFixed(2); + fs.unlinkSync(this.restartFile); + this.logger.debug(`检测到重启,耗时: ${duration}秒`); + return Number(duration); + } + return null; + } + + /** + * 重启服务 + */ + async restart(): Promise { + this.markRestartTime(); + this.logger.warn('服务即将重启..'); + await new Promise((resolve) => setTimeout(resolve, 300)); + process.exit(0); + } +} diff --git a/src/core/tools/retry-options.interface.ts b/src/core/tools/retry-options.interface.ts new file mode 100644 index 0000000..83dfe6b --- /dev/null +++ b/src/core/tools/retry-options.interface.ts @@ -0,0 +1,4 @@ +export interface RetryOptions { + maxAttempts: number; + initialDelay: number; +} diff --git a/src/core/tools/tools.module.ts b/src/core/tools/tools.module.ts new file mode 100644 index 0000000..c2bd867 --- /dev/null +++ b/src/core/tools/tools.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { ToolsService } from './tools.service'; + +@Module({ + providers: [ToolsService], + exports: [ToolsService], +}) +export class ToolsModule {} diff --git a/src/core/tools/tools.service.ts b/src/core/tools/tools.service.ts new file mode 100644 index 0000000..1da6a76 --- /dev/null +++ b/src/core/tools/tools.service.ts @@ -0,0 +1,51 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { RetryOptions } from './retry-options.interface'; + +@Injectable() +export class ToolsService { + private readonly logger = new Logger(ToolsService.name); + + /** + * 异步重试 + * @param operation + * @param options + */ + async retry( + operation: () => Promise, + options: RetryOptions, + ): Promise { + let attempt = 0; + let lastError: any; + + while (attempt < options.maxAttempts) { + try { + return await operation(); + } catch (error) { + lastError = error; + attempt++; + + if (attempt < options.maxAttempts) { + const delay = options.initialDelay * Math.pow(2, attempt - 1); + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + } + + this.logger.error('重试失败', lastError); + throw lastError; + } + + /** + * 从一个可迭代列表中随机选择一个对象 + */ + getRandomItem(list: T[]): T { + return list[Math.floor(Math.random() * list.length)]; + } + + /** + * 获取随机数 + */ + getRandomDelay(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; + } +} diff --git a/src/main.ts b/src/main.ts index 26ec574..5735f0e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,7 @@ import { Logger } from '@nestjs/common'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { ResponseInterceptor } from './common/interceptors/response.interceptor'; import { AllExceptionsFilter } from './common/filters/all-exception.filter'; +import { SystemService } from './core/system/system.service'; async function bootstrap() { Logger.log('晶灵核心初始化..'); @@ -11,6 +12,11 @@ async function bootstrap() { app.setGlobalPrefix('api'); app.useGlobalInterceptors(new ResponseInterceptor()); app.useGlobalFilters(new AllExceptionsFilter()); + const systemService = app.get(SystemService); + const restartDuration = systemService.checkRestartTime(); + if (restartDuration) { + new Logger('System').warn(`重启完成!耗时 ${restartDuration} 秒`); + } const config = new DocumentBuilder() .setTitle('晶灵核心') .setDescription('为晶灵提供API服务')