chore(v1): bump version to 0.1.2; enforce ESLint alias boundaries; add tests/docs
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"NODE_ENV": "development",
|
"NODE_ENV": "test",
|
||||||
"PORT": 3000,
|
"GLOBAL_PREFIX": "api",
|
||||||
"AI_ENABLED": true,
|
"AI_ENABLED": true,
|
||||||
|
"AI_SIMULATE_DIRECT_ENQUEUE": true,
|
||||||
"PROMETHEUS_ENABLED": true,
|
"PROMETHEUS_ENABLED": true,
|
||||||
"AUTH_ENABLED": false,
|
"AUTH_ENABLED": false,
|
||||||
"RBAC_ENABLED": false,
|
"RBAC_ENABLED": false,
|
||||||
"RATE_LIMIT_ENABLED": false,
|
"RATE_LIMIT_ENABLED": false
|
||||||
"REDIS_ENABLED": false
|
|
||||||
}
|
}
|
||||||
@@ -3,16 +3,16 @@ import { APP_FILTER, APP_INTERCEPTOR, APP_GUARD } from '@nestjs/core';
|
|||||||
import { AuthGuard } from '@wwjCommon/auth/auth.guard';
|
import { AuthGuard } from '@wwjCommon/auth/auth.guard';
|
||||||
import { RbacGuard } from '@wwjCommon/auth/rbac.guard';
|
import { RbacGuard } from '@wwjCommon/auth/rbac.guard';
|
||||||
import { BootModule } from '@wwjBoot/wwjcloud-boot.module';
|
import { BootModule } from '@wwjBoot/wwjcloud-boot.module';
|
||||||
import { BootI18nModule } from '@wwjCommon/i18n/boot-i18n.module';
|
import { BootLangModule } from '@wwjCommon/lang/boot-lang.module';
|
||||||
import { HttpExceptionFilter } from '@wwjCommon/http/http-exception.filter';
|
import { HttpExceptionFilter } from '@wwjCommon/http/http-exception.filter';
|
||||||
import { LoggingInterceptor } from '@wwjCommon/http/logging.interceptor';
|
import { LoggingInterceptor } from '@wwjCommon/http/logging.interceptor';
|
||||||
import { MetricsInterceptor } from '@wwjCommon/metrics/metrics.interceptor';
|
import { MetricsInterceptor } from '@wwjCommon/metrics/metrics.interceptor';
|
||||||
import { ResponseInterceptor } from '@wwjCommon/response/response.interceptor';
|
import { ResponseInterceptor } from '@wwjCommon/response/response.interceptor';
|
||||||
import { SecureController } from './secure.controller';
|
import { SecureController } from './secure.controller';
|
||||||
import { AiModule } from '@wwjAi/wwjcloud-ai.module';
|
import { WwjcloudAiModule as AiModule } from '@wwjAi/wwjcloud-ai.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [BootModule, BootI18nModule, AiModule],
|
imports: [BootModule, BootLangModule, AiModule],
|
||||||
controllers: [SecureController],
|
controllers: [SecureController],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: APP_FILTER, useClass: HttpExceptionFilter },
|
{ provide: APP_FILTER, useClass: HttpExceptionFilter },
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ async function bootstrap() {
|
|||||||
await BootHttp.start(app);
|
await BootHttp.start(app);
|
||||||
const config = app.get(ConfigService);
|
const config = app.get(ConfigService);
|
||||||
const raw = config.get<string | number>('PORT');
|
const raw = config.get<string | number>('PORT');
|
||||||
const port = typeof raw === 'number' ? raw : parseInt(String(raw ?? '3000'), 10) || 3000;
|
const port =
|
||||||
|
typeof raw === 'number' ? raw : parseInt(String(raw ?? '3000'), 10) || 3000;
|
||||||
await app.listen(port);
|
await app.listen(port);
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|||||||
@@ -128,3 +128,22 @@
|
|||||||
## 20. 变更提交流程
|
## 20. 变更提交流程
|
||||||
- PR 必须附带文档更新、Swagger 更新与前端类型更新。
|
- PR 必须附带文档更新、Swagger 更新与前端类型更新。
|
||||||
- 使用标签 `consistency:v1` 标注合并项;在 CHANGELOG 记录对齐影响范围。
|
- 使用标签 `consistency:v1` 标注合并项;在 CHANGELOG 记录对齐影响范围。
|
||||||
|
|
||||||
|
|
||||||
|
## 别名与模块边界(一致性约束)
|
||||||
|
- 映射规范:
|
||||||
|
- `@wwjBoot`:仅用于顶层平台装配与入口(`BootModule`、`preset`)。
|
||||||
|
- `@wwjCommon`:统一基础设施入口(`http`、`response`、`metrics`、`cache`、`queue`、`auth`、`tenant`、`lang`)。
|
||||||
|
- `@wwjVendor`:第三方驱动适配层,按接口/Token 注入,默认“可选/存根”。
|
||||||
|
- `@wwjAi`:AI 能力模块,允许依赖 `@wwjCommon`,不得依赖 `@wwjBoot`。
|
||||||
|
|
||||||
|
- 强制规则:
|
||||||
|
- 禁止使用 `@wwjBoot/infra/*` 引入基础设施,统一改为 `@wwjCommon/*`(保证语义与边界一致)。
|
||||||
|
- 文档、示例与测试需统一遵循以上映射与规则;PR 不得混用别名语义。
|
||||||
|
|
||||||
|
- 预设入口与编译耦合(建议):
|
||||||
|
- 提供 `preset.core`(不含 AI)与 `preset.full`(含 AI);应用可按业务选择以降低编译期耦合。
|
||||||
|
|
||||||
|
- i18n 软依赖与兜底:
|
||||||
|
- 拦截器与异常过滤器不强制注入 `I18nService`;未启用 `BootLangModule` 时返回 `msg_key`。
|
||||||
|
- 参考 `LANG-GUIDE.md` 的 `ModuleRef.get(I18nService, { strict:false })` 方案。
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# 多语言(i18n)实现与对齐指南(Java-first)
|
# 多语言(i18n)实现与对齐指南(Java-first)
|
||||||
|
|
||||||
本指南说明在 `wwjcloud-nest-v1` 中接入与落地国际化(i18n),并与 Java 项目的语言包与 key 规范保持一致(Java-first)。PHP 只作为业务逻辑层使用同样的 key 获取文案,不维护独立规范。
|
本指南说明在 `wwjcloud-nest-v1` 中接入与落地国际化(i18n),并与 Java 项目的语言包与 key 规范保持一致(Java-first)。PHP 只作为业务逻辑层使用同样的 key 获取文案,不维护独立规范。
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ wwjcloud-nest-v1/
|
|||||||
common.json
|
common.json
|
||||||
error.json
|
error.json
|
||||||
user.json
|
user.json
|
||||||
libs/wwjcloud-boot/src/infra/i18n/
|
libs/wwjcloud-boot/src/infra/lang/
|
||||||
boot-i18n.module.ts
|
boot-i18n.module.ts
|
||||||
resolvers.ts # 可选:自定义解析器集合(Query/Header)
|
resolvers.ts # 可选:自定义解析器集合(Query/Header)
|
||||||
apps/api/src/common/
|
apps/api/src/common/
|
||||||
@@ -37,16 +37,16 @@ wwjcloud-nest-v1/
|
|||||||
### 1) 安装依赖
|
### 1) 安装依赖
|
||||||
使用你项目的包管理器安装:
|
使用你项目的包管理器安装:
|
||||||
```
|
```
|
||||||
pnpm add @nestjs/i18n i18n accept-language-parser
|
pnpm add nestjs-i18n i18n accept-language-parser
|
||||||
# 或
|
# 或
|
||||||
npm i @nestjs/i18n i18n accept-language-parser
|
npm i nestjs-i18n i18n accept-language-parser
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2) 创建 i18n 模块(BootI18nModule)
|
### 2) 创建 i18n 模块(BootI18nModule)
|
||||||
文件:`libs/wwjcloud-boot/src/infra/i18n/boot-i18n.module.ts`
|
文件:`libs/wwjcloud-boot/src/infra/lang/boot-i18n.module.ts`
|
||||||
```ts
|
```ts
|
||||||
import { Global, Module } from '@nestjs/common';
|
import { Global, Module } from '@nestjs/common';
|
||||||
import { I18nModule, I18nJsonParser, HeaderResolver, QueryResolver } from '@nestjs/i18n';
|
import { I18nModule, I18nJsonLoader, HeaderResolver, QueryResolver } from 'nestjs-i18n';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@@ -54,10 +54,10 @@ import { join } from 'path';
|
|||||||
imports: [
|
imports: [
|
||||||
I18nModule.forRoot({
|
I18nModule.forRoot({
|
||||||
fallbackLanguage: 'zh-CN',
|
fallbackLanguage: 'zh-CN',
|
||||||
parser: I18nJsonParser,
|
loader: I18nJsonLoader,
|
||||||
parserOptions: {
|
loaderOptions: {
|
||||||
path: join(process.cwd(), 'wwjcloud-nest-v1/apps/api/src/lang'),
|
path: join(process.cwd(), 'apps/api/src/lang'),
|
||||||
watch: true,
|
watch: process.env.NODE_ENV !== 'test',
|
||||||
},
|
},
|
||||||
resolvers: [
|
resolvers: [
|
||||||
{ use: QueryResolver, options: ['lang'] },
|
{ use: QueryResolver, options: ['lang'] },
|
||||||
@@ -70,14 +70,14 @@ import { join } from 'path';
|
|||||||
export class BootI18nModule {}
|
export class BootI18nModule {}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3) 在 AppModule 导入
|
### 3) 在 AppModule 导入(推荐使用 BootLangModule 软别名)
|
||||||
文件:`apps/api/src/app.module.ts`
|
文件:`apps/api/src/app.module.ts`
|
||||||
```ts
|
```ts
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { BootI18nModule } from '@libs/wwjcloud-boot/src/infra/i18n/boot-i18n.module';
|
import { BootLangModule } from '@libs/wwjcloud-boot/src/infra/lang/boot-lang.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [BootI18nModule /* 以及其他模块 */],
|
imports: [BootLangModule /* 以及其他模块 */],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
```
|
```
|
||||||
@@ -86,7 +86,7 @@ export class AppModule {}
|
|||||||
文件:`apps/api/src/common/interceptors/response.interceptor.ts`
|
文件:`apps/api/src/common/interceptors/response.interceptor.ts`
|
||||||
```ts
|
```ts
|
||||||
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
|
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
|
||||||
import { I18nService } from '@nestjs/i18n';
|
import { I18nService } from 'nestjs-i18n';
|
||||||
import { Observable, map } from 'rxjs';
|
import { Observable, map } from 'rxjs';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -114,7 +114,7 @@ export class ResponseInterceptor implements NestInterceptor {
|
|||||||
文件:`apps/api/src/common/filters/http-exception.filter.ts`
|
文件:`apps/api/src/common/filters/http-exception.filter.ts`
|
||||||
```ts
|
```ts
|
||||||
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
|
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
|
||||||
import { I18nService } from '@nestjs/i18n';
|
import { I18nService } from 'nestjs-i18n';
|
||||||
|
|
||||||
@Catch()
|
@Catch()
|
||||||
export class HttpExceptionFilter implements ExceptionFilter {
|
export class HttpExceptionFilter implements ExceptionFilter {
|
||||||
@@ -204,7 +204,7 @@ return { code: 0, msg_key: 'user.profile.updated', data: { id: 1 } };
|
|||||||
|
|
||||||
## 语言协商与 DI 导入规范
|
## 语言协商与 DI 导入规范
|
||||||
- 解析优先级:`Query(lang)` > `Header(Accept-Language)` > 默认 `zh-CN`。
|
- 解析优先级:`Query(lang)` > `Header(Accept-Language)` > 默认 `zh-CN`。
|
||||||
- DI 与导入:`BootI18nModule` 仅在 `AppModule` 里导入一次(全局模块),遵循项目的「Nest DI 与导入规范」。拦截器与过滤器以 Provider 方式注入 `I18nService`。
|
- DI 与导入:推荐使用 `BootLangModule`(底层为 `BootI18nModule`)仅在 `AppModule` 里导入一次(全局模块),遵循项目的「Nest DI 与导入规范」。拦截器与过滤器以 Provider 方式注入 `I18nService`。
|
||||||
|
|
||||||
## 测试与验证
|
## 测试与验证
|
||||||
- 默认语言:
|
- 默认语言:
|
||||||
@@ -233,5 +233,91 @@ curl "http://localhost:3000/api/ping?lang=en-US"
|
|||||||
- Java:沿用 `.properties` 的模块化与 key 命名;Nest 端资源内容与 Java 的 key 同名对齐。
|
- Java:沿用 `.properties` 的模块化与 key 命名;Nest 端资源内容与 Java 的 key 同名对齐。
|
||||||
- PHP:继续使用 `get_lang(key)`,逐步统一到 Java 的点分 key,无需维护独立资源规范。
|
- PHP:继续使用 `get_lang(key)`,逐步统一到 Java 的点分 key,无需维护独立资源规范。
|
||||||
|
|
||||||
---
|
// 术语对齐:对外事件与模块名统一使用 `lang`;内部技术栈保留 `i18n`。
|
||||||
如需我在 `wwjcloud-nest-v1` 中继续完成代码接入(创建 `BootI18nModule`、改造拦截器与异常过滤器、添加示例语言资源),请在本指南基础上确认,我将按以上目录与步骤实施。
|
如需我在 `wwjcloud-nest-v1` 中继续完成代码接入(创建 `BootI18nModule`、改造拦截器与异常过滤器、添加示例语言资源),请在本指南基础上确认,我将按以上目录与步骤实施。
|
||||||
|
|
||||||
|
|
||||||
|
## 依赖解耦合与兜底(推荐)
|
||||||
|
- 软依赖:拦截器/过滤器不对 `I18nService` 形成硬依赖;当未导入 `BootLangModule` 时,功能自动降级为直接返回 `msg_key`。
|
||||||
|
- 实现方式:运行时从 `ModuleRef` 中“可选获取” `I18nService`,未获取到则兜底。
|
||||||
|
|
||||||
|
示例:可选 i18n 的响应拦截器
|
||||||
|
```ts
|
||||||
|
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
|
||||||
|
import { ModuleRef } from '@nestjs/core';
|
||||||
|
import { I18nService } from 'nestjs-i18n';
|
||||||
|
import { Observable, map } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ResponseInterceptor implements NestInterceptor {
|
||||||
|
constructor(private readonly moduleRef: ModuleRef) {}
|
||||||
|
|
||||||
|
private getI18n(): I18nService | undefined {
|
||||||
|
// strict:false → 未注册时返回 undefined
|
||||||
|
return this.moduleRef.get(I18nService, { strict: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
intercept(ctx: ExecutionContext, next: CallHandler): Observable<any> {
|
||||||
|
const req = ctx.switchToHttp().getRequest();
|
||||||
|
const i18n = this.getI18n();
|
||||||
|
return next.handle().pipe(
|
||||||
|
map((original) => {
|
||||||
|
const { code = 0, data = null, msg_key } = original ?? {};
|
||||||
|
const key = msg_key || 'common.success';
|
||||||
|
let msg = key;
|
||||||
|
if (i18n) {
|
||||||
|
try {
|
||||||
|
const translated = i18n.translate(key, { lang: req.i18nLang });
|
||||||
|
msg = translated || key;
|
||||||
|
} catch {
|
||||||
|
msg = key; // 兜底:翻译失败返回 key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { code, msg_key: key, msg, data };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
异常过滤器同理:
|
||||||
|
```ts
|
||||||
|
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
|
||||||
|
import { ModuleRef } from '@nestjs/core';
|
||||||
|
import { I18nService } from 'nestjs-i18n';
|
||||||
|
|
||||||
|
@Catch()
|
||||||
|
export class HttpExceptionFilter implements ExceptionFilter {
|
||||||
|
constructor(private readonly moduleRef: ModuleRef) {}
|
||||||
|
private getI18n(): I18nService | undefined { return this.moduleRef.get(I18nService, { strict: false }); }
|
||||||
|
|
||||||
|
catch(exception: unknown, host: ArgumentsHost) {
|
||||||
|
const ctx = host.switchToHttp();
|
||||||
|
const req = ctx.getRequest();
|
||||||
|
const res = ctx.getResponse();
|
||||||
|
const i18n = this.getI18n();
|
||||||
|
|
||||||
|
let code = 500;
|
||||||
|
let msgKey = 'error.common.unknown';
|
||||||
|
let args: Record<string, any> | undefined;
|
||||||
|
|
||||||
|
if (exception instanceof HttpException) {
|
||||||
|
const response: any = exception.getResponse();
|
||||||
|
code = exception.getStatus();
|
||||||
|
msgKey = response?.msg_key || msgKey;
|
||||||
|
args = response?.args;
|
||||||
|
}
|
||||||
|
|
||||||
|
let msg = msgKey;
|
||||||
|
if (i18n) {
|
||||||
|
try { msg = i18n.translate(msgKey, { lang: req.i18nLang /* args */ }) || msgKey; } catch { msg = msgKey; }
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(code).json({ code, msg_key: msgKey, msg, data: null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
落地建议:
|
||||||
|
- 在 `apps/api/src/app.module.ts` 导入 `BootLangModule` 即启用翻译;测试或最简环境可跳过导入,系统仍可工作(只返回 `msg_key`)。
|
||||||
|
- 当引入 i18n 时,建议在 `LANG_READY` 就绪服务中校验语言资源目录存在并上报状态(见 `V11-BOOT-READINESS.md`)。
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
- AI 开发与安全:`AI-RECOVERY-DEV.md`、`AI-RECOVERY-SECURITY.md`
|
- AI 开发与安全:`AI-RECOVERY-DEV.md`、`AI-RECOVERY-SECURITY.md`
|
||||||
- 基础设施与配置:`V11-INFRA-SETUP.md`
|
- 基础设施与配置:`V11-INFRA-SETUP.md`
|
||||||
- 一致性与对齐:`CONSISTENCY-GUIDE.md`
|
- 一致性与对齐:`CONSISTENCY-GUIDE.md`
|
||||||
- 国际化指南:`I18N-GUIDE.md`
|
- 国际化指南:`LANG-GUIDE.md`
|
||||||
|
|
||||||
维护约定:
|
维护约定:
|
||||||
- v1 专属文档仅在本目录维护,主 `docs/` 不承载 v1 内容。
|
- v1 专属文档仅在本目录维护,主 `docs/` 不承载 v1 内容。
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
- 指标暴露:`GET /api/metrics`(`PROMETHEUS_ENABLED=true`),含 `http_requests_total`、`ai_events_total` 等。
|
- 指标暴露:`GET /api/metrics`(`PROMETHEUS_ENABLED=true`),含 `http_requests_total`、`ai_events_total` 等。
|
||||||
- 弹性策略:`ResilienceService` 支持重试/超时/断路器,`HttpClientService.getWithFallback` 已集成。
|
- 弹性策略:`ResilienceService` 支持重试/超时/断路器,`HttpClientService.getWithFallback` 已集成。
|
||||||
- DI 导入规范:Boot 层提供与导出,业务按类型消费,不重复定义令牌/别名。
|
- DI 导入规范:Boot 层提供与导出,业务按类型消费,不重复定义令牌/别名。
|
||||||
- I18N:`BootI18nModule` 全局导入,`apps/api/src/lang` 存放多语言资源,拦截器/过滤器使用 i18n 翻译。
|
- I18N:`BootLangModule`(底层为 `BootI18nModule`)全局导入,`apps/api/src/lang` 存放多语言资源,拦截器/过滤器使用 i18n 翻译。
|
||||||
|
|
||||||
## AI 自愈系统(恢复与守卫)
|
## AI 自愈系统(恢复与守卫)
|
||||||
- 控制器与路由(受 `RateLimitGuard`,开发期可 `@Public()`):
|
- 控制器与路由(受 `RateLimitGuard`,开发期可 `@Public()`):
|
||||||
@@ -73,8 +73,28 @@
|
|||||||
- 详细 AI 开发与安全:`AI-RECOVERY-DEV.md`、`AI-RECOVERY-SECURITY.md`
|
- 详细 AI 开发与安全:`AI-RECOVERY-DEV.md`、`AI-RECOVERY-SECURITY.md`
|
||||||
- 基础设施与配置:`V11-INFRA-SETUP.md`
|
- 基础设施与配置:`V11-INFRA-SETUP.md`
|
||||||
- 一致性与对齐:`CONSISTENCY-GUIDE.md`
|
- 一致性与对齐:`CONSISTENCY-GUIDE.md`
|
||||||
- 国际化接入:`I18N-GUIDE.md`
|
- 国际化接入:`LANG-GUIDE.md`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
注:本页为 v1 的“一体化总览”,作为开发与运维的统一入口。若新增能力(如 Addon 注册、OpenTelemetry、速率限制扩展),请在此页与对应子文档同步更新。
|
注:本页为 v1 的“一体化总览”,作为开发与运维的统一入口。若新增能力(如 Addon 注册、OpenTelemetry、速率限制扩展),请在此页与对应子文档同步更新。
|
||||||
|
|
||||||
|
## 别名与模块边界约定(强制)
|
||||||
|
- 别名映射:
|
||||||
|
- `@wwjBoot` → 顶层平台装配与入口(`BootModule`、`preset`),不用于引入具体基础设施。
|
||||||
|
- `@wwjCommon` → 跨领域基础设施(`http/*`、`response/*`、`metrics/*`、`cache/*`、`queue/*`、`auth/*`、`tenant/*`、`lang/*`)。
|
||||||
|
- `@wwjVendor` → 第三方驱动适配(`pay/*`、`sms/*`、`upload/*`、`notice/*`),按接口/Token 注入,保持“可选”。
|
||||||
|
- `@wwjAi` → AI 能力(Tuner/Safe/Manager/Healing 等),允许依赖 `@wwjCommon`,禁止反向依赖 `@wwjBoot`。
|
||||||
|
|
||||||
|
- 使用规则:
|
||||||
|
- 业务与 AI 层只从 `@wwjCommon/*` 引入基础设施;禁用 `@wwjBoot/infra/*` 形式(语义不一致)。
|
||||||
|
- `BootLangModule`(软别名)用于在应用层一次性导入 i18n;拦截器/过滤器对 i18n 采取软依赖与兜底,详见 `LANG-GUIDE.md`。
|
||||||
|
- Vendor 驱动均为“可选”并按接口注入,业务避免直接耦合具体实现;文档需标注启用条件与默认存根行为。
|
||||||
|
|
||||||
|
- 预设入口(建议):
|
||||||
|
- `preset.core`:不含 AI,仅基础设施(Boot 核心)。
|
||||||
|
- `preset.full`:含 AI(当前默认)。应用可按 `AI_ENABLED` 切换或选择入口以降低编译耦合。
|
||||||
|
|
||||||
|
- 代码规范(建议):
|
||||||
|
- ESLint `no-restricted-imports` 禁止 `@wwjBoot/infra/*` 导入;统一走 `@wwjCommon/*`。
|
||||||
|
- 文档在 `CONSISTENCY-GUIDE.md` 与本页保持别名与边界约定的一致说明。
|
||||||
49
wwjcloud-nest-v1/docs/V11-AI-READINESS.md
Normal file
49
wwjcloud-nest-v1/docs/V11-AI-READINESS.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# V11 AI Readiness 事件说明
|
||||||
|
|
||||||
|
本文件说明 AI 层(Tuner/Safe)在 v11 中的就绪事件上报约定,以及与 Boot 层的协作关系。
|
||||||
|
|
||||||
|
## 统一事件
|
||||||
|
- 事件名:`module.state.changed`
|
||||||
|
- 载荷:
|
||||||
|
- `module`: 模块名(如 `ai.tuner`、`ai.safe`、`startup`、`cache`、`auth`、`rbac`、`queue`、`lang`、`metrics`)
|
||||||
|
- `previousState`: 之前状态(通常为 `initializing`)
|
||||||
|
- `currentState`: 当前状态(`ready` 或 `unavailable`)
|
||||||
|
- `meta`: 可选扩展(如 `{ enabled: true }`)
|
||||||
|
|
||||||
|
## AI 层模块
|
||||||
|
### ai.tuner
|
||||||
|
- 入口:`libs/wwjcloud-ai/src/tuner/services/tuner-ready.service.ts`
|
||||||
|
- 触发时机:`OnModuleInit`
|
||||||
|
- 依赖组件:`PerformanceAnalyzer`、`ResourceMonitor`、`CacheOptimizer`、`QueryOptimizer`
|
||||||
|
- 环境开关:`AI_TUNER_ENABLED`(默认 `true`)
|
||||||
|
- 判定逻辑:
|
||||||
|
- 当开关启用且核心组件均成功注入 → `ready`
|
||||||
|
- 当开关启用但组件缺失/异常 → `unavailable`
|
||||||
|
- 当开关关闭 → `unavailable`
|
||||||
|
|
||||||
|
### ai.safe
|
||||||
|
- 入口:`libs/wwjcloud-ai/src/safe/services/safe-ready.service.ts`
|
||||||
|
- 触发时机:`OnModuleInit`
|
||||||
|
- 依赖组件:`SecurityAnalyzer`、`VulnerabilityDetector`、`AccessProtector`、`AiSecurityService`
|
||||||
|
- 环境开关:`AI_SAFE_ENABLED`(默认 `true`)
|
||||||
|
- 判定逻辑:
|
||||||
|
- 当开关启用且核心组件均成功注入 → `ready`
|
||||||
|
- 当开关启用但组件缺失/异常 → `unavailable`
|
||||||
|
- 当开关关闭 → `unavailable`
|
||||||
|
|
||||||
|
## Boot 层(参考)
|
||||||
|
- `startup`:`StartupValidatorService` 在初始化时生成启动报告,并基于 `NODE_ENV` 是否缺失和 `Redis` 连接状态上报 `ready/unavailable`。
|
||||||
|
- `cache`:`CacheReadyService` 在 Redis 禁用时回退为 `ready`,启用时根据 `PING` 成功与否上报状态。
|
||||||
|
- `auth/rbac`:`AuthReadyService` 基于 `AUTH_ENABLED` 与 `RBAC_ENABLED` 分别上报 `ready/unavailable`。
|
||||||
|
- `queue`:`QueueReadyService` 依据 `QUEUE_ENABLED` 与驱动类型(`bullmq/kafka` → `ready`,未知 → `unavailable`)。
|
||||||
|
- `i18n`、`metrics`:分别在初始化时根据语言目录存在与 `PROMETHEUS_ENABLED` 开关上报状态。
|
||||||
|
|
||||||
|
## 测试覆盖
|
||||||
|
- 位置:`src/ai-layer/*.spec.ts`、`src/boot-layer/*.spec.ts`
|
||||||
|
- 已覆盖用例:
|
||||||
|
- AI:`tuner-ready.service`、`safe-ready.service` 启用/禁用、缺失组件场景
|
||||||
|
- Boot:`startup`、`cache`、`auth/rbac`、`queue` 等常见启用/禁用与依赖失败场景
|
||||||
|
|
||||||
|
## 约定与扩展
|
||||||
|
- 所有模块应在 `OnModuleInit` 或初始化阶段发出首次状态,用于协调器与观测层消费。
|
||||||
|
- 新增模块应复用 `module.state.changed` 事件,保持载荷格式一致性,必要时在 `meta` 补充上下文。
|
||||||
40
wwjcloud-nest-v1/docs/V11-BOOT-READINESS.md
Normal file
40
wwjcloud-nest-v1/docs/V11-BOOT-READINESS.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# WWJCloud v11 - Boot 模块就绪上报与规范
|
||||||
|
|
||||||
|
本文档说明在 NestJS v11 下 Boot 层的事件通信与生命周期规范化改造,以及如何验证就绪状态上报。
|
||||||
|
|
||||||
|
## 改动概述
|
||||||
|
- 事件总线:统一通过 `EventBus` 依赖注入,禁止手动实例化。
|
||||||
|
- 生命周期钩子:使用 `OnModuleInit` 完成模块就绪状态上报;使用 `OnModuleDestroy` 做句柄清理(如定时器、注册表)。
|
||||||
|
- 就绪事件主题:`module.state.changed`,载荷示例:
|
||||||
|
- `{ module: 'metrics', previousState: 'initializing', currentState: 'ready' }`
|
||||||
|
- `{ module: 'lang', previousState: 'initializing', currentState: 'unavailable' }`
|
||||||
|
|
||||||
|
## 代码改动
|
||||||
|
- Metrics 就绪上报
|
||||||
|
- 文件:`libs/wwjcloud-boot/src/infra/metrics/metrics.service.ts`
|
||||||
|
- 变更:注入 `EventBus`,实现 `onModuleInit()`,根据 `PROMETHEUS_ENABLED` 上报 `ready/unavailable`。
|
||||||
|
- I18n 就绪上报
|
||||||
|
- 文件:`libs/wwjcloud-boot/src/infra/lang/lang-ready.service.ts`(新增)
|
||||||
|
- 变更:在 `onModuleInit()` 检查语言目录 `apps/api/src/lang` 是否存在,上报 `ready/unavailable`。
|
||||||
|
- 注册:`libs/wwjcloud-boot/src/infra/lang/boot-i18n.module.ts` 中新增 `providers: [LangReadyService]`。
|
||||||
|
|
||||||
|
## 验证与测试
|
||||||
|
- 单元测试
|
||||||
|
- Metrics:`src/boot-layer/metrics.service.spec.ts` 验证 `ready/unavailable` 就绪事件。
|
||||||
|
- Lang:`src/boot-layer/i18n-ready.service.spec.ts` 通过 mock `fs.existsSync` 验证 `ready` 事件。
|
||||||
|
|
||||||
|
- 运行测试:`npm run test`
|
||||||
|
- 端到端(已存在):`test/jest-e2e.json`,可结合 `BootHttp.start(app)` 与环境变量验证基础设施行为。
|
||||||
|
|
||||||
|
## 使用建议
|
||||||
|
- 订阅事件:协调器/管理器通过 `@OnEvent('module.state.changed')` 同步内部状态映射,控制任务可用性。
|
||||||
|
- 配置校验:`libs/wwjcloud-boot/src/config/validation.ts` 中维持严格校验,不提供默认值;默认值在具体实现兜底。
|
||||||
|
- 文档参考:
|
||||||
|
- Nest v11 依赖注入:https://docs.nestjs.com/fundamentals/custom-providers
|
||||||
|
- 生命周期钩子:https://docs.nestjs.com/fundamentals/lifecycle-events
|
||||||
|
- 事件与事件总线:https://docs.nestjs.com/techniques/events
|
||||||
|
|
||||||
|
## 后续工作(可选)
|
||||||
|
- 为 `cache/auth/queue/tenant` 等模块补充就绪上报(必要时)。
|
||||||
|
- 在 `V1-GUIDE.md` 与 `V11-INFRA-SETUP.md` 中补充统一事件订阅与状态流转章节。
|
||||||
|
- 增加 e2e 场景:在 `apps/api` 中通过专用路由触发各模块状态变更并断言指标与任务协调。
|
||||||
@@ -25,11 +25,39 @@ export default tseslint.config(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
files: ['src/boot-layer/**/*.ts'],
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
'no-restricted-imports': [
|
||||||
'@typescript-eslint/no-floating-promises': 'warn',
|
'error',
|
||||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
{
|
||||||
"prettier/prettier": ["error", { endOfLine: "auto" }],
|
patterns: [
|
||||||
|
{ group: ['@wwjBoot/infra/*'], message: '禁止从 @wwjBoot/infra/* 导入,请改为 @wwjCommon/*' },
|
||||||
|
{ group: ['@wwjAi/*'], message: 'Boot 层禁止依赖 AI 层,请改为事件驱动或 preset.full 动态集成' },
|
||||||
|
{ group: ['@wwjVendor/*'], message: '禁止直接引用 @wwjVendor/*,请通过 @wwjCommon/* 的适配服务' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['src/ai-layer/**/*.ts'],
|
||||||
|
rules: {
|
||||||
|
'no-restricted-imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
patterns: [
|
||||||
|
{ group: ['@wwjBoot/infra/*'], message: '禁止从 @wwjBoot/infra/* 导入,请改为 @wwjCommon/*' },
|
||||||
|
{ group: ['@wwjBoot/*'], message: 'AI 层禁止依赖 Boot 内部实现;仅依赖 @wwjCommon/* 或通过事件总线' },
|
||||||
|
{ group: ['@wwjVendor/*'], message: '禁止直接引用 @wwjVendor/*,请通过 @wwjCommon/* 的适配服务' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.spec.ts'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/unbound-method': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
1112
wwjcloud-nest-v1/libs/wwjcloud-ai/API.md
Normal file
1112
wwjcloud-nest-v1/libs/wwjcloud-ai/API.md
Normal file
File diff suppressed because it is too large
Load Diff
585
wwjcloud-nest-v1/libs/wwjcloud-ai/ARCHITECTURE.md
Normal file
585
wwjcloud-nest-v1/libs/wwjcloud-ai/ARCHITECTURE.md
Normal file
@@ -0,0 +1,585 @@
|
|||||||
|
# WWJCloud AI Layer - 架构设计文档
|
||||||
|
|
||||||
|
## 📋 架构概述
|
||||||
|
|
||||||
|
WWJCloud AI Layer 采用模块化、微服务导向的架构设计,基于 NestJS v11 框架构建,提供企业级的智能化系统管理能力。
|
||||||
|
|
||||||
|
## 🏗️ 整体架构
|
||||||
|
|
||||||
|
### 架构层次
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Application Layer │
|
||||||
|
│ (业务应用层) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ WWJCloud AI Layer │
|
||||||
|
│ (AI 智能化层) │
|
||||||
|
│ ┌─────────────┬─────────────┬─────────────┬─────────────┐ │
|
||||||
|
│ │ Manager │ Healing │ Safe │ Tuner │ │
|
||||||
|
│ │ (管理) │ (自愈) │ (安全) │ (调优) │ │
|
||||||
|
│ └─────────────┴─────────────┴─────────────┴─────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Infrastructure Layer │
|
||||||
|
│ (基础设施层) │
|
||||||
|
│ Database │ Cache │ Message Queue │ Monitoring │ Logging │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 核心设计原则
|
||||||
|
|
||||||
|
1. **单一职责原则** - 每个模块专注于特定的 AI 能力
|
||||||
|
2. **开放封闭原则** - 对扩展开放,对修改封闭
|
||||||
|
3. **依赖倒置原则** - 依赖抽象而非具体实现
|
||||||
|
4. **接口隔离原则** - 使用细粒度的接口设计
|
||||||
|
5. **最小知识原则** - 模块间松耦合设计
|
||||||
|
|
||||||
|
## 🎯 模块架构详解
|
||||||
|
|
||||||
|
### 1. Manager 模块 - AI 核心管理
|
||||||
|
|
||||||
|
#### 架构图
|
||||||
|
|
||||||
|
```
|
||||||
|
Manager Module
|
||||||
|
├── Controllers/
|
||||||
|
│ └── AiController # AI 管理控制器
|
||||||
|
├── Services/
|
||||||
|
│ ├── AiOrchestratorService # 工作流程编排
|
||||||
|
│ ├── AiRegistryService # 服务注册发现
|
||||||
|
│ └── AiCoordinatorService # 模块间协调
|
||||||
|
├── Interfaces/
|
||||||
|
│ └── ai-manager.interface.ts # 管理接口定义
|
||||||
|
└── Bootstrap/
|
||||||
|
└── AiBootstrapProvider # AI 启动提供者
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 核心职责
|
||||||
|
|
||||||
|
- **工作流程编排**: 管理 AI 智能体的执行顺序和依赖关系
|
||||||
|
- **服务注册**: 维护所有 AI 服务的注册信息和健康状态
|
||||||
|
- **模块协调**: 处理模块间的通信和数据交换
|
||||||
|
- **统一管理**: 提供统一的 AI 服务管理入口
|
||||||
|
|
||||||
|
#### 关键设计模式
|
||||||
|
|
||||||
|
- **编排者模式** (Orchestrator Pattern): 中央协调多个服务
|
||||||
|
- **注册表模式** (Registry Pattern): 服务发现和管理
|
||||||
|
- **观察者模式** (Observer Pattern): 事件驱动的状态通知
|
||||||
|
|
||||||
|
### 2. Healing 模块 - AI 自愈
|
||||||
|
|
||||||
|
#### 架构图
|
||||||
|
|
||||||
|
```
|
||||||
|
Healing Module
|
||||||
|
├── Listeners/
|
||||||
|
│ ├── AiSelfHealListener # 自愈事件监听
|
||||||
|
│ └── AiRecoveryListener # 恢复事件监听
|
||||||
|
├── Services/
|
||||||
|
│ ├── AiRecoveryService # 故障恢复服务
|
||||||
|
│ └── AiStrategyService # 策略管理服务
|
||||||
|
├── Strategies/
|
||||||
|
│ ├── RetryStrategy # 重试策略
|
||||||
|
│ └── FallbackStrategy # 降级策略
|
||||||
|
└── Interfaces/
|
||||||
|
└── healing.interface.ts # 自愈接口定义
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 核心职责
|
||||||
|
|
||||||
|
- **故障检测**: 实时监控系统状态,识别异常情况
|
||||||
|
- **智能诊断**: 分析故障原因,确定恢复策略
|
||||||
|
- **自动恢复**: 执行恢复操作,最小化服务中断
|
||||||
|
- **效果评估**: 评估恢复效果,优化恢复策略
|
||||||
|
|
||||||
|
#### 关键设计模式
|
||||||
|
|
||||||
|
- **策略模式** (Strategy Pattern): 可插拔的恢复策略
|
||||||
|
- **责任链模式** (Chain of Responsibility): 多级恢复处理
|
||||||
|
- **状态模式** (State Pattern): 系统健康状态管理
|
||||||
|
|
||||||
|
### 3. Safe 模块 - AI 安全
|
||||||
|
|
||||||
|
#### 架构图
|
||||||
|
|
||||||
|
```
|
||||||
|
Safe Module
|
||||||
|
├── Analyzers/
|
||||||
|
│ └── SecurityAnalyzer # 安全状态分析
|
||||||
|
├── Detectors/
|
||||||
|
│ └── VulnerabilityDetector # 漏洞检测器
|
||||||
|
├── Protectors/
|
||||||
|
│ └── AccessProtector # 访问保护器
|
||||||
|
└── Services/
|
||||||
|
├── AiSecurityService # 统一安全管理
|
||||||
|
└── AiAuditService # 安全审计服务
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 核心职责
|
||||||
|
|
||||||
|
- **威胁检测**: 实时识别安全威胁和异常行为
|
||||||
|
- **漏洞扫描**: 定期扫描系统漏洞和安全风险
|
||||||
|
- **访问控制**: 管理用户权限和资源访问
|
||||||
|
- **审计日志**: 记录安全事件和操作轨迹
|
||||||
|
|
||||||
|
#### 关键设计模式
|
||||||
|
|
||||||
|
- **装饰器模式** (Decorator Pattern): 安全功能增强
|
||||||
|
- **代理模式** (Proxy Pattern): 访问控制和监控
|
||||||
|
- **模板方法模式** (Template Method): 标准化安全检查流程
|
||||||
|
|
||||||
|
### 4. Tuner 模块 - AI 性能调优
|
||||||
|
|
||||||
|
#### 架构图
|
||||||
|
|
||||||
|
```
|
||||||
|
Tuner Module
|
||||||
|
├── Analyzers/
|
||||||
|
│ └── PerformanceAnalyzer # 性能分析器
|
||||||
|
├── Monitors/
|
||||||
|
│ └── ResourceMonitor # 资源监控器
|
||||||
|
├── Optimizers/
|
||||||
|
│ ├── CacheOptimizer # 缓存优化器
|
||||||
|
│ └── QueryOptimizer # 查询优化器
|
||||||
|
└── Services/
|
||||||
|
├── AiTunerService # 调优管理服务
|
||||||
|
└── AiMetricsService # 指标收集服务
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 核心职责
|
||||||
|
|
||||||
|
- **性能监控**: 实时收集系统性能指标
|
||||||
|
- **瓶颈识别**: 智能识别性能瓶颈和优化机会
|
||||||
|
- **自动优化**: 执行性能优化策略和配置调整
|
||||||
|
- **效果评估**: 评估优化效果和投资回报率
|
||||||
|
|
||||||
|
#### 关键设计模式
|
||||||
|
|
||||||
|
- **建造者模式** (Builder Pattern): 复杂优化策略构建
|
||||||
|
- **工厂方法模式** (Factory Method): 优化器实例创建
|
||||||
|
- **命令模式** (Command Pattern): 优化操作的封装和撤销
|
||||||
|
|
||||||
|
## 🔄 模块间交互
|
||||||
|
|
||||||
|
### 事件驱动架构
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 全局事件定义
|
||||||
|
export enum AiEventType {
|
||||||
|
// Manager 事件
|
||||||
|
WORKFLOW_STARTED = 'workflow.started',
|
||||||
|
WORKFLOW_COMPLETED = 'workflow.completed',
|
||||||
|
SERVICE_REGISTERED = 'service.registered',
|
||||||
|
|
||||||
|
// Healing 事件
|
||||||
|
FAILURE_DETECTED = 'failure.detected',
|
||||||
|
RECOVERY_STARTED = 'recovery.started',
|
||||||
|
RECOVERY_COMPLETED = 'recovery.completed',
|
||||||
|
|
||||||
|
// Safe 事件
|
||||||
|
THREAT_DETECTED = 'threat.detected',
|
||||||
|
SECURITY_SCAN_COMPLETED = 'security.scan.completed',
|
||||||
|
ACCESS_DENIED = 'access.denied',
|
||||||
|
|
||||||
|
// Tuner 事件
|
||||||
|
PERFORMANCE_DEGRADED = 'performance.degraded',
|
||||||
|
OPTIMIZATION_STARTED = 'optimization.started',
|
||||||
|
OPTIMIZATION_COMPLETED = 'optimization.completed',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 模块依赖关系
|
||||||
|
|
||||||
|
```
|
||||||
|
Manager (核心协调)
|
||||||
|
├── 依赖 → Healing (故障恢复)
|
||||||
|
├── 依赖 → Safe (安全检查)
|
||||||
|
└── 依赖 → Tuner (性能优化)
|
||||||
|
|
||||||
|
Healing (自愈模块)
|
||||||
|
├── 监听 → Manager 事件
|
||||||
|
└── 发布 → 恢复事件
|
||||||
|
|
||||||
|
Safe (安全模块)
|
||||||
|
├── 监听 → 所有模块事件
|
||||||
|
└── 发布 → 安全事件
|
||||||
|
|
||||||
|
Tuner (调优模块)
|
||||||
|
├── 监听 → Manager 事件
|
||||||
|
└── 发布 → 性能事件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ 技术栈
|
||||||
|
|
||||||
|
### 核心框架
|
||||||
|
|
||||||
|
- **NestJS v11**: 企业级 Node.js 框架
|
||||||
|
- **TypeScript 5.x**: 类型安全的 JavaScript 超集
|
||||||
|
- **RxJS**: 响应式编程库
|
||||||
|
- **Reflect Metadata**: 元数据反射支持
|
||||||
|
|
||||||
|
### 数据存储
|
||||||
|
|
||||||
|
- **Redis**: 缓存和会话存储
|
||||||
|
- **MongoDB**: 文档数据库(可选)
|
||||||
|
- **PostgreSQL**: 关系型数据库(可选)
|
||||||
|
|
||||||
|
### 监控和日志
|
||||||
|
|
||||||
|
- **Prometheus**: 指标收集
|
||||||
|
- **Grafana**: 可视化监控
|
||||||
|
- **Winston**: 结构化日志
|
||||||
|
- **Jaeger**: 分布式追踪
|
||||||
|
|
||||||
|
### 消息队列
|
||||||
|
|
||||||
|
- **Redis Pub/Sub**: 轻量级消息传递
|
||||||
|
- **RabbitMQ**: 企业级消息队列(可选)
|
||||||
|
- **Apache Kafka**: 大数据流处理(可选)
|
||||||
|
|
||||||
|
## 🔧 配置管理
|
||||||
|
|
||||||
|
### 配置层次结构
|
||||||
|
|
||||||
|
```
|
||||||
|
Configuration Hierarchy
|
||||||
|
├── Default Config (默认配置)
|
||||||
|
├── Environment Config (环境配置)
|
||||||
|
├── Runtime Config (运行时配置)
|
||||||
|
└── Dynamic Config (动态配置)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface AiModuleConfig {
|
||||||
|
// 全局配置
|
||||||
|
global: {
|
||||||
|
enabled: boolean;
|
||||||
|
logLevel: 'debug' | 'info' | 'warn' | 'error';
|
||||||
|
metricsEnabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Manager 配置
|
||||||
|
manager: {
|
||||||
|
orchestration: {
|
||||||
|
maxConcurrentWorkflows: number;
|
||||||
|
workflowTimeout: number;
|
||||||
|
};
|
||||||
|
registry: {
|
||||||
|
healthCheckInterval: number;
|
||||||
|
serviceTimeout: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Healing 配置
|
||||||
|
healing: {
|
||||||
|
enabled: boolean;
|
||||||
|
strategies: string[];
|
||||||
|
maxRetries: number;
|
||||||
|
retryDelay: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Safe 配置
|
||||||
|
safe: {
|
||||||
|
enabled: boolean;
|
||||||
|
scanInterval: number;
|
||||||
|
threatThreshold: number;
|
||||||
|
auditRetention: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tuner 配置
|
||||||
|
tuner: {
|
||||||
|
enabled: boolean;
|
||||||
|
autoOptimize: boolean;
|
||||||
|
monitoringInterval: number;
|
||||||
|
optimizationThreshold: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 性能考虑
|
||||||
|
|
||||||
|
### 性能优化策略
|
||||||
|
|
||||||
|
1. **异步处理**: 所有 I/O 操作使用异步模式
|
||||||
|
2. **缓存策略**: 多层缓存减少重复计算
|
||||||
|
3. **连接池**: 数据库和外部服务连接复用
|
||||||
|
4. **批处理**: 批量处理减少网络开销
|
||||||
|
5. **懒加载**: 按需加载减少启动时间
|
||||||
|
|
||||||
|
### 内存管理
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 内存优化配置
|
||||||
|
export const MEMORY_CONFIG = {
|
||||||
|
// 指标数据保留策略
|
||||||
|
metricsRetention: {
|
||||||
|
maxDataPoints: 10000,
|
||||||
|
retentionPeriod: 30 * 24 * 60 * 60 * 1000, // 30天
|
||||||
|
cleanupInterval: 60 * 60 * 1000, // 1小时
|
||||||
|
},
|
||||||
|
|
||||||
|
// 缓存配置
|
||||||
|
cache: {
|
||||||
|
maxSize: 1000,
|
||||||
|
ttl: 5 * 60 * 1000, // 5分钟
|
||||||
|
checkPeriod: 60 * 1000, // 1分钟
|
||||||
|
},
|
||||||
|
|
||||||
|
// 事件队列配置
|
||||||
|
eventQueue: {
|
||||||
|
maxSize: 5000,
|
||||||
|
batchSize: 100,
|
||||||
|
flushInterval: 1000, // 1秒
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 安全架构
|
||||||
|
|
||||||
|
### 安全层次
|
||||||
|
|
||||||
|
```
|
||||||
|
Security Layers
|
||||||
|
├── Network Security (网络安全)
|
||||||
|
│ ├── TLS/SSL 加密
|
||||||
|
│ ├── 防火墙规则
|
||||||
|
│ └── DDoS 防护
|
||||||
|
├── Application Security (应用安全)
|
||||||
|
│ ├── 身份认证
|
||||||
|
│ ├── 权限控制
|
||||||
|
│ └── 输入验证
|
||||||
|
├── Data Security (数据安全)
|
||||||
|
│ ├── 数据加密
|
||||||
|
│ ├── 敏感信息脱敏
|
||||||
|
│ └── 数据备份
|
||||||
|
└── Operational Security (运营安全)
|
||||||
|
├── 安全审计
|
||||||
|
├── 威胁检测
|
||||||
|
└── 事件响应
|
||||||
|
```
|
||||||
|
|
||||||
|
### 安全最佳实践
|
||||||
|
|
||||||
|
1. **最小权限原则**: 用户和服务只获得必要的最小权限
|
||||||
|
2. **深度防御**: 多层安全控制,避免单点失效
|
||||||
|
3. **零信任架构**: 不信任任何网络流量,验证所有访问
|
||||||
|
4. **持续监控**: 实时监控安全事件和异常行为
|
||||||
|
5. **快速响应**: 自动化安全事件响应和恢复
|
||||||
|
|
||||||
|
## 🧪 测试策略
|
||||||
|
|
||||||
|
### 测试金字塔
|
||||||
|
|
||||||
|
```
|
||||||
|
Testing Pyramid
|
||||||
|
├── Unit Tests (单元测试) - 70%
|
||||||
|
│ ├── Service 测试
|
||||||
|
│ ├── Controller 测试
|
||||||
|
│ └── Utility 测试
|
||||||
|
├── Integration Tests (集成测试) - 20%
|
||||||
|
│ ├── Module 集成测试
|
||||||
|
│ ├── Database 集成测试
|
||||||
|
│ └── External API 测试
|
||||||
|
└── E2E Tests (端到端测试) - 10%
|
||||||
|
├── 完整工作流程测试
|
||||||
|
├── 性能测试
|
||||||
|
└── 安全测试
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试工具链
|
||||||
|
|
||||||
|
- **Jest**: 单元测试框架
|
||||||
|
- **Supertest**: HTTP 接口测试
|
||||||
|
- **Test Containers**: 集成测试环境
|
||||||
|
- **Artillery**: 性能压力测试
|
||||||
|
- **OWASP ZAP**: 安全漏洞扫描
|
||||||
|
|
||||||
|
## 📈 监控和可观测性
|
||||||
|
|
||||||
|
### 三大支柱
|
||||||
|
|
||||||
|
1. **Metrics (指标)**
|
||||||
|
- 业务指标: 成功率、响应时间、吞吐量
|
||||||
|
- 系统指标: CPU、内存、磁盘、网络
|
||||||
|
- 应用指标: 错误率、队列长度、连接数
|
||||||
|
|
||||||
|
2. **Logging (日志)**
|
||||||
|
- 结构化日志: JSON 格式,便于查询分析
|
||||||
|
- 日志级别: DEBUG、INFO、WARN、ERROR
|
||||||
|
- 上下文信息: 请求ID、用户ID、会话ID
|
||||||
|
|
||||||
|
3. **Tracing (追踪)**
|
||||||
|
- 分布式追踪: 跨服务请求链路追踪
|
||||||
|
- 性能分析: 识别性能瓶颈和优化机会
|
||||||
|
- 错误定位: 快速定位问题根因
|
||||||
|
|
||||||
|
### 告警策略
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 告警规则配置
|
||||||
|
export const ALERT_RULES = {
|
||||||
|
// 系统级告警
|
||||||
|
system: {
|
||||||
|
cpuUsage: { threshold: 80, severity: 'warning' },
|
||||||
|
memoryUsage: { threshold: 85, severity: 'critical' },
|
||||||
|
diskUsage: { threshold: 90, severity: 'critical' },
|
||||||
|
},
|
||||||
|
|
||||||
|
// 应用级告警
|
||||||
|
application: {
|
||||||
|
errorRate: { threshold: 5, severity: 'warning' },
|
||||||
|
responseTime: { threshold: 1000, severity: 'warning' },
|
||||||
|
throughput: { threshold: 100, severity: 'info' },
|
||||||
|
},
|
||||||
|
|
||||||
|
// 业务级告警
|
||||||
|
business: {
|
||||||
|
healingFailureRate: { threshold: 10, severity: 'critical' },
|
||||||
|
securityThreatCount: { threshold: 5, severity: 'warning' },
|
||||||
|
optimizationEffectiveness: { threshold: 5, severity: 'info' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 部署架构
|
||||||
|
|
||||||
|
### 容器化部署
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# Dockerfile 示例
|
||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 复制依赖文件
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --only=production
|
||||||
|
|
||||||
|
# 复制源代码
|
||||||
|
COPY dist/ ./dist/
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:3000/health || exit 1
|
||||||
|
|
||||||
|
# 启动应用
|
||||||
|
CMD ["node", "dist/main.js"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kubernetes 部署
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# deployment.yaml
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: wwjcloud-ai
|
||||||
|
spec:
|
||||||
|
replicas: 3
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: wwjcloud-ai
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: wwjcloud-ai
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: wwjcloud-ai
|
||||||
|
image: wwjcloud/ai:latest
|
||||||
|
ports:
|
||||||
|
- containerPort: 3000
|
||||||
|
env:
|
||||||
|
- name: NODE_ENV
|
||||||
|
value: "production"
|
||||||
|
- name: AI_ENABLED
|
||||||
|
value: "true"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "256Mi"
|
||||||
|
cpu: "250m"
|
||||||
|
limits:
|
||||||
|
memory: "512Mi"
|
||||||
|
cpu: "500m"
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 3000
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 10
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /ready
|
||||||
|
port: 3000
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 5
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 运维指南
|
||||||
|
|
||||||
|
### 日常运维任务
|
||||||
|
|
||||||
|
1. **健康检查**: 定期检查各模块健康状态
|
||||||
|
2. **性能监控**: 监控关键性能指标和趋势
|
||||||
|
3. **日志分析**: 分析错误日志和异常模式
|
||||||
|
4. **容量规划**: 根据使用情况调整资源配置
|
||||||
|
5. **安全审计**: 定期进行安全检查和漏洞扫描
|
||||||
|
|
||||||
|
### 故障处理流程
|
||||||
|
|
||||||
|
```
|
||||||
|
故障处理流程
|
||||||
|
├── 1. 故障检测
|
||||||
|
│ ├── 自动监控告警
|
||||||
|
│ └── 用户反馈报告
|
||||||
|
├── 2. 故障分析
|
||||||
|
│ ├── 日志分析
|
||||||
|
│ ├── 指标分析
|
||||||
|
│ └── 链路追踪
|
||||||
|
├── 3. 故障定位
|
||||||
|
│ ├── 根因分析
|
||||||
|
│ └── 影响评估
|
||||||
|
├── 4. 故障恢复
|
||||||
|
│ ├── 自动恢复
|
||||||
|
│ └── 手动干预
|
||||||
|
└── 5. 事后总结
|
||||||
|
├── 故障报告
|
||||||
|
└── 改进措施
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔮 未来规划
|
||||||
|
|
||||||
|
### 短期目标 (3-6个月)
|
||||||
|
|
||||||
|
- [ ] 完善单元测试覆盖率到 90%+
|
||||||
|
- [ ] 集成更多第三方监控工具
|
||||||
|
- [ ] 优化性能,减少内存占用
|
||||||
|
- [ ] 增加更多自愈策略
|
||||||
|
- [ ] 完善安全检测规则
|
||||||
|
|
||||||
|
### 中期目标 (6-12个月)
|
||||||
|
|
||||||
|
- [ ] 支持多租户架构
|
||||||
|
- [ ] 实现机器学习驱动的智能优化
|
||||||
|
- [ ] 集成 APM 工具进行深度性能分析
|
||||||
|
- [ ] 支持云原生部署模式
|
||||||
|
- [ ] 实现跨区域容灾
|
||||||
|
|
||||||
|
### 长期目标 (1-2年)
|
||||||
|
|
||||||
|
- [ ] 构建 AI 驱动的运维平台
|
||||||
|
- [ ] 实现预测性维护能力
|
||||||
|
- [ ] 支持边缘计算场景
|
||||||
|
- [ ] 建立 AI 模型市场
|
||||||
|
- [ ] 实现完全自治的系统运维
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
本架构文档将随着系统演进持续更新,确保架构设计与实际实现保持一致。
|
||||||
1210
wwjcloud-nest-v1/libs/wwjcloud-ai/DEPLOYMENT.md
Normal file
1210
wwjcloud-nest-v1/libs/wwjcloud-ai/DEPLOYMENT.md
Normal file
File diff suppressed because it is too large
Load Diff
470
wwjcloud-nest-v1/libs/wwjcloud-ai/README.md
Normal file
470
wwjcloud-nest-v1/libs/wwjcloud-ai/README.md
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
# WWJCloud AI Layer - 智能化系统架构
|
||||||
|
|
||||||
|
## 📋 概述
|
||||||
|
|
||||||
|
WWJCloud AI Layer 是基于 NestJS v11 构建的企业级智能化系统架构,提供全面的 AI 驱动的系统管理、自愈、安全和性能优化能力。
|
||||||
|
|
||||||
|
## 🏗️ 架构设计
|
||||||
|
|
||||||
|
### 核心设计原则
|
||||||
|
|
||||||
|
1. **模块化架构** - 按功能域划分,支持独立开发和部署
|
||||||
|
2. **智能化驱动** - AI 算法驱动的自动化决策和优化
|
||||||
|
3. **企业级可靠性** - 高可用、容错和自愈能力
|
||||||
|
4. **性能优先** - 内置性能监控和自动优化
|
||||||
|
5. **安全第一** - 全方位安全防护和威胁检测
|
||||||
|
|
||||||
|
### 模块架构图
|
||||||
|
|
||||||
|
```
|
||||||
|
wwjcloud-ai/
|
||||||
|
├── manager/ # AI 核心管理模块
|
||||||
|
├── healing/ # AI 自愈模块
|
||||||
|
├── safe/ # AI 安全模块
|
||||||
|
├── tuner/ # AI 性能调优模块
|
||||||
|
├── events.ts # 全局事件定义
|
||||||
|
├── types.ts # 全局类型定义
|
||||||
|
└── index.ts # 统一导出入口
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 核心模块
|
||||||
|
|
||||||
|
### 1. Manager 模块 - AI 核心管理
|
||||||
|
|
||||||
|
**职责**: 统一管理和协调所有 AI 服务
|
||||||
|
|
||||||
|
**核心组件**:
|
||||||
|
- `AiOrchestratorService` - AI 工作流程编排
|
||||||
|
- `AiRegistryService` - AI 服务注册与发现
|
||||||
|
- `AiCoordinatorService` - AI 模块间协调
|
||||||
|
|
||||||
|
**主要功能**:
|
||||||
|
- 🔄 工作流程自动化编排
|
||||||
|
- 📋 服务注册与健康监控
|
||||||
|
- 🤝 模块间通信协调
|
||||||
|
- 📊 统一状态管理
|
||||||
|
|
||||||
|
### 2. Healing 模块 - AI 自愈
|
||||||
|
|
||||||
|
**职责**: 提供系统自动故障检测、诊断和恢复能力
|
||||||
|
|
||||||
|
**核心组件**:
|
||||||
|
- `AiSelfHealListener` - 自愈事件监听
|
||||||
|
- `AiRecoveryService` - 故障恢复服务
|
||||||
|
- `RetryStrategy` - 重试策略
|
||||||
|
- `FallbackStrategy` - 降级策略
|
||||||
|
|
||||||
|
**主要功能**:
|
||||||
|
- 🔍 实时故障检测
|
||||||
|
- 🩺 智能故障诊断
|
||||||
|
- 🔧 自动故障恢复
|
||||||
|
- 📈 恢复效果评估
|
||||||
|
|
||||||
|
### 3. Safe 模块 - AI 安全
|
||||||
|
|
||||||
|
**职责**: 提供全方位的安全防护和威胁检测
|
||||||
|
|
||||||
|
**核心组件**:
|
||||||
|
- `SecurityAnalyzer` - 安全状态分析
|
||||||
|
- `VulnerabilityDetector` - 漏洞检测
|
||||||
|
- `AccessProtector` - 访问控制保护
|
||||||
|
- `AiSecurityService` - 统一安全管理
|
||||||
|
|
||||||
|
**主要功能**:
|
||||||
|
- 🛡️ 实时威胁检测
|
||||||
|
- 🔒 访问权限控制
|
||||||
|
- 📋 安全审计日志
|
||||||
|
- ⚠️ 安全告警通知
|
||||||
|
|
||||||
|
### 4. Tuner 模块 - AI 性能调优
|
||||||
|
|
||||||
|
**职责**: 智能化性能监控、分析和优化
|
||||||
|
|
||||||
|
**核心组件**:
|
||||||
|
- `PerformanceAnalyzer` - 性能分析器
|
||||||
|
- `ResourceMonitor` - 资源监控器
|
||||||
|
- `CacheOptimizer` - 缓存优化器
|
||||||
|
- `QueryOptimizer` - 查询优化器
|
||||||
|
|
||||||
|
**主要功能**:
|
||||||
|
- 📊 实时性能监控
|
||||||
|
- 🔍 性能瓶颈识别
|
||||||
|
- ⚡ 自动性能优化
|
||||||
|
- 📈 优化效果评估
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @wwjcloud/ai
|
||||||
|
```
|
||||||
|
|
||||||
|
### 基础配置
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { WwjcloudAiModule } from '@wwjcloud/ai';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
WwjcloudAiModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 基本使用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AiOrchestratorService } from '@wwjcloud/ai';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MyService {
|
||||||
|
constructor(
|
||||||
|
private readonly orchestrator: AiOrchestratorService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async startAiWorkflow() {
|
||||||
|
// 启动 AI 工作流程
|
||||||
|
const workflow = await this.orchestrator.startWorkflow('system-optimization');
|
||||||
|
return workflow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📖 详细使用指南
|
||||||
|
|
||||||
|
### Manager 模块使用
|
||||||
|
|
||||||
|
#### 1. 工作流程编排
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AiOrchestratorService } from '@wwjcloud/ai/manager';
|
||||||
|
|
||||||
|
// 启动预定义工作流程
|
||||||
|
const workflow = await orchestrator.startWorkflow('self-healing');
|
||||||
|
|
||||||
|
// 自定义工作流程
|
||||||
|
const customWorkflow = await orchestrator.startWorkflow('custom', {
|
||||||
|
steps: [
|
||||||
|
{ agent: 'SecurityGuard', action: 'scan' },
|
||||||
|
{ agent: 'PerfTuner', action: 'optimize' },
|
||||||
|
]
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 服务注册
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AiRegistryService } from '@wwjcloud/ai/manager';
|
||||||
|
|
||||||
|
// 注册 AI 服务
|
||||||
|
await registry.registerService({
|
||||||
|
id: 'my-ai-service',
|
||||||
|
name: 'My AI Service',
|
||||||
|
type: 'analyzer',
|
||||||
|
version: '1.0.0',
|
||||||
|
capabilities: ['analysis', 'optimization'],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Healing 模块使用
|
||||||
|
|
||||||
|
#### 1. 自愈配置
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AiRecoveryService } from '@wwjcloud/ai/healing';
|
||||||
|
|
||||||
|
// 配置自愈策略
|
||||||
|
await recovery.configureStrategy('database-connection', {
|
||||||
|
maxRetries: 3,
|
||||||
|
retryDelay: 1000,
|
||||||
|
fallbackAction: 'use-cache',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 手动触发恢复
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 手动触发故障恢复
|
||||||
|
const result = await recovery.recoverFromFailure({
|
||||||
|
type: 'service-unavailable',
|
||||||
|
service: 'payment-service',
|
||||||
|
context: { orderId: '12345' },
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Safe 模块使用
|
||||||
|
|
||||||
|
#### 1. 安全扫描
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AiSecurityService } from '@wwjcloud/ai/safe';
|
||||||
|
|
||||||
|
// 执行全面安全评估
|
||||||
|
const assessment = await security.performSecurityAssessment({
|
||||||
|
scope: 'full',
|
||||||
|
includeVulnerabilities: true,
|
||||||
|
generateReport: true,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 访问控制
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AccessProtector } from '@wwjcloud/ai/safe';
|
||||||
|
|
||||||
|
// 验证访问权限
|
||||||
|
const hasAccess = await protector.validateAccess({
|
||||||
|
userId: 'user123',
|
||||||
|
resource: '/api/admin/users',
|
||||||
|
action: 'read',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tuner 模块使用
|
||||||
|
|
||||||
|
#### 1. 性能调优
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AiTunerService } from '@wwjcloud/ai/tuner';
|
||||||
|
|
||||||
|
// 启动调优会话
|
||||||
|
const session = await tuner.startTuningSession({
|
||||||
|
enableMonitoring: true,
|
||||||
|
aggressiveOptimization: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 执行全面调优
|
||||||
|
const results = await tuner.performComprehensiveTuning({
|
||||||
|
enableCacheOptimization: true,
|
||||||
|
enableQueryOptimization: true,
|
||||||
|
applyOptimizations: true,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 性能监控
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AiMetricsService } from '@wwjcloud/ai/tuner';
|
||||||
|
|
||||||
|
// 记录性能指标
|
||||||
|
metrics.recordMetric('response_time', 150, { endpoint: '/api/users' });
|
||||||
|
|
||||||
|
// 获取性能报告
|
||||||
|
const report = metrics.generateMetricsReport({
|
||||||
|
timeRange: {
|
||||||
|
start: Date.now() - 24 * 60 * 60 * 1000, // 24小时前
|
||||||
|
end: Date.now(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 高级配置
|
||||||
|
|
||||||
|
### 环境变量配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# AI 模块配置
|
||||||
|
AI_ENABLED=true
|
||||||
|
AI_LOG_LEVEL=info
|
||||||
|
AI_METRICS_RETENTION_DAYS=30
|
||||||
|
|
||||||
|
# 自愈配置
|
||||||
|
AI_HEALING_ENABLED=true
|
||||||
|
AI_HEALING_MAX_RETRIES=3
|
||||||
|
AI_HEALING_RETRY_DELAY=1000
|
||||||
|
|
||||||
|
# 安全配置
|
||||||
|
AI_SECURITY_ENABLED=true
|
||||||
|
AI_SECURITY_SCAN_INTERVAL=300000
|
||||||
|
AI_SECURITY_THREAT_THRESHOLD=0.7
|
||||||
|
|
||||||
|
# 性能调优配置
|
||||||
|
AI_TUNER_ENABLED=true
|
||||||
|
AI_TUNER_AUTO_OPTIMIZE=false
|
||||||
|
AI_TUNER_MONITORING_INTERVAL=60000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自定义配置
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { WwjcloudAiModule } from '@wwjcloud/ai';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
WwjcloudAiModule.forRoot({
|
||||||
|
healing: {
|
||||||
|
enabled: true,
|
||||||
|
maxRetries: 5,
|
||||||
|
strategies: ['retry', 'fallback', 'circuit-breaker'],
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
enabled: true,
|
||||||
|
scanInterval: 300000,
|
||||||
|
threatThreshold: 0.8,
|
||||||
|
},
|
||||||
|
tuner: {
|
||||||
|
enabled: true,
|
||||||
|
autoOptimize: true,
|
||||||
|
monitoringInterval: 30000,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 监控和指标
|
||||||
|
|
||||||
|
### 内置指标
|
||||||
|
|
||||||
|
- **系统健康度**: 整体系统健康状况评分
|
||||||
|
- **自愈成功率**: 自动故障恢复成功率
|
||||||
|
- **安全威胁数**: 检测到的安全威胁数量
|
||||||
|
- **性能改进率**: 性能优化带来的改进百分比
|
||||||
|
|
||||||
|
### 指标查询
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 获取系统概览
|
||||||
|
const overview = await metrics.getSystemMetricsOverview();
|
||||||
|
|
||||||
|
// 获取特定指标统计
|
||||||
|
const stats = metrics.calculateStatistics('response_time', {
|
||||||
|
start: Date.now() - 60 * 60 * 1000, // 1小时前
|
||||||
|
end: Date.now(),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 测试
|
||||||
|
|
||||||
|
### 单元测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:ai
|
||||||
|
```
|
||||||
|
|
||||||
|
### 集成测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:ai:e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
### 性能测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:ai:performance
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 故障排查
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
#### 1. AI 服务启动失败
|
||||||
|
|
||||||
|
**症状**: 应用启动时 AI 模块初始化失败
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
```bash
|
||||||
|
# 检查依赖
|
||||||
|
npm ls @wwjcloud/ai
|
||||||
|
|
||||||
|
# 检查配置
|
||||||
|
echo $AI_ENABLED
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
tail -f logs/ai.log
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 自愈功能不工作
|
||||||
|
|
||||||
|
**症状**: 系统故障时没有自动恢复
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
```typescript
|
||||||
|
// 检查自愈配置
|
||||||
|
const config = await healing.getConfiguration();
|
||||||
|
console.log('Healing enabled:', config.enabled);
|
||||||
|
|
||||||
|
// 手动触发自愈测试
|
||||||
|
await healing.testRecovery('connection-failure');
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 性能优化无效果
|
||||||
|
|
||||||
|
**症状**: 性能调优后没有明显改善
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
```typescript
|
||||||
|
// 检查基线性能
|
||||||
|
const baseline = await tuner.getPerformanceBaseline();
|
||||||
|
|
||||||
|
// 查看优化历史
|
||||||
|
const history = tuner.getTuningHistory(10);
|
||||||
|
|
||||||
|
// 获取优化建议
|
||||||
|
const recommendations = await tuner.getTuningRecommendations();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 调试模式
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 启用调试日志
|
||||||
|
process.env.AI_LOG_LEVEL = 'debug';
|
||||||
|
|
||||||
|
// 启用详细指标
|
||||||
|
process.env.AI_METRICS_DETAILED = 'true';
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🤝 贡献指南
|
||||||
|
|
||||||
|
### 开发环境设置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 克隆项目
|
||||||
|
git clone https://github.com/wwjcloud/wwjcloud-ai.git
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 启动开发模式
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 代码规范
|
||||||
|
|
||||||
|
- 遵循 NestJS 官方规范
|
||||||
|
- 使用 TypeScript 严格模式
|
||||||
|
- 100% 单元测试覆盖率
|
||||||
|
- 完整的 JSDoc 注释
|
||||||
|
|
||||||
|
### 提交规范
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 功能开发
|
||||||
|
git commit -m "feat(manager): add workflow orchestration"
|
||||||
|
|
||||||
|
# 问题修复
|
||||||
|
git commit -m "fix(healing): resolve retry strategy issue"
|
||||||
|
|
||||||
|
# 文档更新
|
||||||
|
git commit -m "docs(readme): update usage examples"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📄 许可证
|
||||||
|
|
||||||
|
MIT License - 详见 [LICENSE](LICENSE) 文件
|
||||||
|
|
||||||
|
## 🔗 相关链接
|
||||||
|
|
||||||
|
- [NestJS 官方文档](https://docs.nestjs.com/)
|
||||||
|
- [WWJCloud 官网](https://wwjcloud.com/)
|
||||||
|
- [问题反馈](https://github.com/wwjcloud/wwjcloud-ai/issues)
|
||||||
|
- [更新日志](CHANGELOG.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**WWJCloud AI Layer** - 让系统更智能,让运维更简单 🚀
|
||||||
1481
wwjcloud-nest-v1/libs/wwjcloud-ai/TESTING.md
Normal file
1481
wwjcloud-nest-v1/libs/wwjcloud-ai/TESTING.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AiSelfHealListener } from './listeners/ai-self-heal.listener';
|
||||||
|
import { AiRecoveryListener } from './listeners/ai-recovery.listener';
|
||||||
|
import { AiRecoveryService } from './services/ai-recovery.service';
|
||||||
|
import { AiStrategyService } from './services/ai-strategy.service';
|
||||||
|
import { RetryStrategy } from './strategies/retry.strategy';
|
||||||
|
import { FallbackStrategy } from './strategies/fallback.strategy';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI Healing Module - AI 自愈模块
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* - 监听系统错误和故障
|
||||||
|
* - 自动执行恢复策略
|
||||||
|
* - 提供多种恢复机制
|
||||||
|
* - 记录和分析恢复过程
|
||||||
|
*/
|
||||||
|
@Module({
|
||||||
|
providers: [
|
||||||
|
// 监听器
|
||||||
|
AiSelfHealListener,
|
||||||
|
AiRecoveryListener,
|
||||||
|
|
||||||
|
// 服务
|
||||||
|
AiRecoveryService,
|
||||||
|
AiStrategyService,
|
||||||
|
|
||||||
|
// 恢复策略
|
||||||
|
RetryStrategy,
|
||||||
|
FallbackStrategy,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
AiRecoveryService,
|
||||||
|
AiStrategyService,
|
||||||
|
RetryStrategy,
|
||||||
|
FallbackStrategy,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AiHealingModule {}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* Healing Module Interfaces - AI 自愈模块接口定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 恢复策略接口
|
||||||
|
*/
|
||||||
|
export interface RecoveryStrategy {
|
||||||
|
name: string;
|
||||||
|
priority: number;
|
||||||
|
canHandle(error: any): boolean;
|
||||||
|
execute(context: RecoveryContext): Promise<RecoveryResult>;
|
||||||
|
getEstimatedTime(): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 恢复上下文接口
|
||||||
|
*/
|
||||||
|
export interface RecoveryContext {
|
||||||
|
taskId: string;
|
||||||
|
error: any;
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
retryCount: number;
|
||||||
|
maxRetries: number;
|
||||||
|
startTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 恢复结果接口
|
||||||
|
*/
|
||||||
|
export interface RecoveryResult {
|
||||||
|
success: boolean;
|
||||||
|
strategy: string;
|
||||||
|
duration: number;
|
||||||
|
result?: any;
|
||||||
|
error?: string;
|
||||||
|
nextAction?: 'retry' | 'escalate' | 'abort';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自愈监听器接口
|
||||||
|
*/
|
||||||
|
export interface SelfHealListener {
|
||||||
|
canHandle(event: any): boolean;
|
||||||
|
handle(event: any): Promise<void>;
|
||||||
|
getPriority(): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误分析结果接口
|
||||||
|
*/
|
||||||
|
export interface ErrorAnalysis {
|
||||||
|
errorType: string;
|
||||||
|
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
category:
|
||||||
|
| 'network'
|
||||||
|
| 'database'
|
||||||
|
| 'service'
|
||||||
|
| 'validation'
|
||||||
|
| 'system'
|
||||||
|
| 'unknown';
|
||||||
|
recoverable: boolean;
|
||||||
|
suggestedStrategies: string[];
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 健康检查结果接口
|
||||||
|
*/
|
||||||
|
export interface HealthCheckResult {
|
||||||
|
component: string;
|
||||||
|
status: 'healthy' | 'degraded' | 'unhealthy';
|
||||||
|
details: Record<string, any>;
|
||||||
|
timestamp: number;
|
||||||
|
responseTime?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自愈统计信息接口
|
||||||
|
*/
|
||||||
|
export interface HealingStats {
|
||||||
|
totalRecoveries: number;
|
||||||
|
successfulRecoveries: number;
|
||||||
|
failedRecoveries: number;
|
||||||
|
averageRecoveryTime: number;
|
||||||
|
strategiesUsed: Record<string, number>;
|
||||||
|
errorCategories: Record<string, number>;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { OnEvent } from '@nestjs/event-emitter';
|
import { OnEvent } from '@wwjCommon/events/event-bus';
|
||||||
import { TASK_RECOVERY_REQUESTED_EVENT } from '@wwjAi';
|
import { TASK_RECOVERY_REQUESTED_EVENT } from '@wwjAi';
|
||||||
import type { TaskRecoveryRequestedPayload } from '@wwjAi';
|
import type { TaskRecoveryRequestedPayload } from '@wwjAi';
|
||||||
import { AiRecoveryService } from '../services/ai-recovery.service';
|
import { AiRecoveryService } from '../services/ai-recovery.service';
|
||||||
@@ -1,15 +1,9 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { OnEvent, EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventBus, OnEvent } from '@wwjCommon/events/event-bus';
|
||||||
// ModuleRef no longer used
|
// ModuleRef no longer used
|
||||||
import {
|
import { TASK_FAILED_EVENT, TASK_RECOVERY_REQUESTED_EVENT } from '@wwjAi';
|
||||||
TASK_FAILED_EVENT,
|
import type { TaskFailedPayload, TaskRecoveryRequestedPayload } from '@wwjAi';
|
||||||
TASK_RECOVERY_REQUESTED_EVENT,
|
|
||||||
} from '@wwjAi';
|
|
||||||
import type {
|
|
||||||
TaskFailedPayload,
|
|
||||||
TaskRecoveryRequestedPayload,
|
|
||||||
} from '@wwjAi';
|
|
||||||
import { MetricsService } from '@wwjCommon/metrics/metrics.service';
|
import { MetricsService } from '@wwjCommon/metrics/metrics.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -20,11 +14,22 @@ export class AiSelfHealListener {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
private readonly emitter: EventEmitter2,
|
private readonly eventBus: EventBus,
|
||||||
private readonly metrics: MetricsService,
|
private readonly metrics: MetricsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
onModuleInit() {}
|
onModuleInit() {
|
||||||
|
const enabled = this.readBoolean('AI_ENABLED');
|
||||||
|
const currentState = enabled ? 'ready' : 'unavailable';
|
||||||
|
this.logger.log(
|
||||||
|
`Healing module init: enabled=${enabled}, state=${currentState}`,
|
||||||
|
);
|
||||||
|
this.eventBus.emit('module.state.changed', {
|
||||||
|
module: 'healing',
|
||||||
|
previousState: 'initializing',
|
||||||
|
currentState,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@OnEvent(TASK_FAILED_EVENT)
|
@OnEvent(TASK_FAILED_EVENT)
|
||||||
handleTaskFailed(payload: TaskFailedPayload) {
|
handleTaskFailed(payload: TaskFailedPayload) {
|
||||||
@@ -52,7 +57,7 @@ export class AiSelfHealListener {
|
|||||||
undefined,
|
undefined,
|
||||||
strategy,
|
strategy,
|
||||||
);
|
);
|
||||||
this.emitter.emit(TASK_RECOVERY_REQUESTED_EVENT, request);
|
this.eventBus.emit(TASK_RECOVERY_REQUESTED_EVENT, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent(TASK_RECOVERY_REQUESTED_EVENT)
|
@OnEvent(TASK_RECOVERY_REQUESTED_EVENT)
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
Severity,
|
Severity,
|
||||||
TaskFailedPayload,
|
TaskFailedPayload,
|
||||||
} from '@wwjAi';
|
} from '@wwjAi';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||||
import { QueueService } from '@wwjCommon/queue/queue.service';
|
import { QueueService } from '@wwjCommon/queue/queue.service';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { AiStrategyService } from '@wwjAi';
|
import { AiStrategyService } from '@wwjAi';
|
||||||
@@ -28,7 +28,7 @@ export class AiRecoveryService {
|
|||||||
private readonly cache: CacheService,
|
private readonly cache: CacheService,
|
||||||
private readonly lock: LockService,
|
private readonly lock: LockService,
|
||||||
private readonly metrics: MetricsService,
|
private readonly metrics: MetricsService,
|
||||||
private readonly emitter: EventEmitter2,
|
private readonly eventBus: EventBus,
|
||||||
private readonly queue: QueueService,
|
private readonly queue: QueueService,
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
private readonly strategy: AiStrategyService,
|
private readonly strategy: AiStrategyService,
|
||||||
@@ -41,24 +41,30 @@ export class AiRecoveryService {
|
|||||||
});
|
});
|
||||||
if (this.queue.isBullmq() || this.queue.isKafka()) {
|
if (this.queue.isBullmq() || this.queue.isKafka()) {
|
||||||
// 注册 Worker 处理恢复请求(BullMQ/Kafka 共用统一处理器)
|
// 注册 Worker 处理恢复请求(BullMQ/Kafka 共用统一处理器)
|
||||||
this.queue.registerWorker(async (data: TaskRecoveryRequestedPayload) => {
|
this.queue.registerWorker(
|
||||||
const start = Date.now();
|
async (data: TaskRecoveryRequestedPayload) => {
|
||||||
this.logger.log(`Processing recovery (worker) for taskId=${data.taskId}`);
|
const start = Date.now();
|
||||||
const durationMs = Date.now() - start;
|
this.logger.log(
|
||||||
const payload: TaskRecoveryCompletedPayload = {
|
`Processing recovery (worker) for taskId=${data.taskId}`,
|
||||||
taskId: data.taskId,
|
);
|
||||||
strategy: data.strategy,
|
const durationMs = Date.now() - start;
|
||||||
result: 'success',
|
const payload: TaskRecoveryCompletedPayload = {
|
||||||
durationMs,
|
taskId: data.taskId,
|
||||||
timestamp: Date.now(),
|
strategy: data.strategy,
|
||||||
};
|
result: 'success',
|
||||||
this.emitter.emit(TASK_RECOVERY_COMPLETED_EVENT, payload);
|
durationMs,
|
||||||
this.metrics?.observeAiEvent(
|
timestamp: Date.now(),
|
||||||
TASK_RECOVERY_COMPLETED_EVENT,
|
};
|
||||||
undefined,
|
this.eventBus.emit(TASK_RECOVERY_COMPLETED_EVENT, payload);
|
||||||
data.strategy,
|
this.metrics?.observeAiEvent(
|
||||||
);
|
TASK_RECOVERY_COMPLETED_EVENT,
|
||||||
}, 1, 'ai-recovery');
|
undefined,
|
||||||
|
data.strategy,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
'ai-recovery',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +136,7 @@ export class AiRecoveryService {
|
|||||||
durationMs,
|
durationMs,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
this.emitter.emit(TASK_RECOVERY_COMPLETED_EVENT, payload);
|
this.eventBus.emit(TASK_RECOVERY_COMPLETED_EVENT, payload);
|
||||||
this.metrics?.observeAiEvent(
|
this.metrics?.observeAiEvent(
|
||||||
TASK_RECOVERY_COMPLETED_EVENT,
|
TASK_RECOVERY_COMPLETED_EVENT,
|
||||||
undefined,
|
undefined,
|
||||||
@@ -154,7 +160,11 @@ export class AiRecoveryService {
|
|||||||
}
|
}
|
||||||
return processed;
|
return processed;
|
||||||
}
|
}
|
||||||
async simulateFailure(params: { taskId?: string; severity?: Severity; reason?: string }): Promise<{ ok: true; emitted: boolean }> {
|
async simulateFailure(params: {
|
||||||
|
taskId?: string;
|
||||||
|
severity?: Severity;
|
||||||
|
reason?: string;
|
||||||
|
}): Promise<{ ok: true; emitted: boolean }> {
|
||||||
const taskId = params.taskId ?? 'demo-task';
|
const taskId = params.taskId ?? 'demo-task';
|
||||||
const severity: Severity = params.severity ?? 'medium';
|
const severity: Severity = params.severity ?? 'medium';
|
||||||
const reason = params.reason ?? 'demo failure';
|
const reason = params.reason ?? 'demo failure';
|
||||||
@@ -164,11 +174,15 @@ export class AiRecoveryService {
|
|||||||
severity,
|
severity,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
this.emitter.emit(TASK_FAILED_EVENT, payload);
|
this.eventBus.emit(TASK_FAILED_EVENT, payload);
|
||||||
this.metrics?.observeAiEvent(TASK_FAILED_EVENT, severity);
|
this.metrics?.observeAiEvent(TASK_FAILED_EVENT, severity);
|
||||||
if (this.readBoolean('AI_SIMULATE_DIRECT_ENQUEUE')) {
|
if (this.readBoolean('AI_SIMULATE_DIRECT_ENQUEUE')) {
|
||||||
const decided = this.strategy.decideStrategy(payload);
|
const decided = this.strategy.decideStrategy(payload);
|
||||||
this.metrics?.observeAiEvent(TASK_RECOVERY_REQUESTED_EVENT, undefined, decided);
|
this.metrics?.observeAiEvent(
|
||||||
|
TASK_RECOVERY_REQUESTED_EVENT,
|
||||||
|
undefined,
|
||||||
|
decided,
|
||||||
|
);
|
||||||
const request: TaskRecoveryRequestedPayload = {
|
const request: TaskRecoveryRequestedPayload = {
|
||||||
taskId,
|
taskId,
|
||||||
strategy: decided,
|
strategy: decided,
|
||||||
@@ -182,7 +196,8 @@ export class AiRecoveryService {
|
|||||||
private readBoolean(key: string): boolean {
|
private readBoolean(key: string): boolean {
|
||||||
const v = this.config.get<string | boolean>(key);
|
const v = this.config.get<string | boolean>(key);
|
||||||
if (typeof v === 'boolean') return v;
|
if (typeof v === 'boolean') return v;
|
||||||
if (typeof v === 'string') return ['true', '1', 'yes', 'on'].includes(v.toLowerCase());
|
if (typeof v === 'string')
|
||||||
|
return ['true', '1', 'yes', 'on'].includes(v.toLowerCase());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
RecoveryStrategy,
|
||||||
|
RecoveryContext,
|
||||||
|
RecoveryResult,
|
||||||
|
} from '../interfaces/healing.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fallback Recovery Strategy - 降级恢复策略
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* - 提供服务降级方案
|
||||||
|
* - 处理不可恢复的错误
|
||||||
|
* - 确保系统基本功能可用
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class FallbackStrategy implements RecoveryStrategy {
|
||||||
|
private readonly logger = new Logger(FallbackStrategy.name);
|
||||||
|
|
||||||
|
readonly name = 'fallback';
|
||||||
|
readonly priority = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否可以处理该错误
|
||||||
|
*/
|
||||||
|
canHandle(error: any): boolean {
|
||||||
|
// 需要降级处理的错误类型
|
||||||
|
const fallbackErrors = [
|
||||||
|
'SERVICE_UNAVAILABLE',
|
||||||
|
'DEPENDENCY_FAILURE',
|
||||||
|
'RESOURCE_EXHAUSTED',
|
||||||
|
'CIRCUIT_BREAKER_OPEN',
|
||||||
|
'RATE_LIMIT_EXCEEDED',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (error?.code && fallbackErrors.includes(error.code)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查错误严重程度
|
||||||
|
if (error?.severity === 'high' || error?.severity === 'critical') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行降级恢复
|
||||||
|
*/
|
||||||
|
async execute(context: RecoveryContext): Promise<RecoveryResult> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
this.logger.log(`Executing fallback strategy for task: ${context.taskId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 根据任务类型选择降级方案
|
||||||
|
const fallbackResult = await this.selectFallbackOption(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
strategy: this.name,
|
||||||
|
duration: Date.now() - startTime,
|
||||||
|
result: fallbackResult,
|
||||||
|
nextAction: 'abort', // 降级后通常不再重试
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`Fallback strategy failed for task: ${context.taskId}`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
strategy: this.name,
|
||||||
|
duration: Date.now() - startTime,
|
||||||
|
error: error instanceof Error ? error.message : 'Fallback failed',
|
||||||
|
nextAction: 'abort',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取预估执行时间
|
||||||
|
*/
|
||||||
|
getEstimatedTime(): number {
|
||||||
|
return 2000; // 2秒预估时间
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择降级方案
|
||||||
|
*/
|
||||||
|
private async selectFallbackOption(context: RecoveryContext): Promise<any> {
|
||||||
|
const taskType = context.metadata?.taskType || 'unknown';
|
||||||
|
|
||||||
|
this.logger.debug(`Selecting fallback option for task type: ${taskType}`);
|
||||||
|
|
||||||
|
switch (taskType) {
|
||||||
|
case 'database':
|
||||||
|
return await this.handleDatabaseFallback(context);
|
||||||
|
|
||||||
|
case 'api':
|
||||||
|
return await this.handleApiFallback(context);
|
||||||
|
|
||||||
|
case 'cache':
|
||||||
|
return await this.handleCacheFallback(context);
|
||||||
|
|
||||||
|
case 'file':
|
||||||
|
return await this.handleFileFallback(context);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return await this.handleGenericFallback(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据库降级处理
|
||||||
|
*/
|
||||||
|
private async handleDatabaseFallback(context: RecoveryContext): Promise<any> {
|
||||||
|
this.logger.warn(`Database fallback for task: ${context.taskId}`);
|
||||||
|
|
||||||
|
// 返回缓存数据或默认值
|
||||||
|
return {
|
||||||
|
fallbackType: 'database',
|
||||||
|
data: context.metadata?.cachedData || null,
|
||||||
|
message: 'Using cached data due to database unavailability',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 降级处理
|
||||||
|
*/
|
||||||
|
private async handleApiFallback(context: RecoveryContext): Promise<any> {
|
||||||
|
this.logger.warn(`API fallback for task: ${context.taskId}`);
|
||||||
|
|
||||||
|
// 返回默认响应或离线数据
|
||||||
|
return {
|
||||||
|
fallbackType: 'api',
|
||||||
|
data: context.metadata?.defaultResponse || { status: 'unavailable' },
|
||||||
|
message: 'Using default response due to API unavailability',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存降级处理
|
||||||
|
*/
|
||||||
|
private async handleCacheFallback(context: RecoveryContext): Promise<any> {
|
||||||
|
this.logger.warn(`Cache fallback for task: ${context.taskId}`);
|
||||||
|
|
||||||
|
// 直接访问数据源
|
||||||
|
return {
|
||||||
|
fallbackType: 'cache',
|
||||||
|
data: await this.fetchFromDataSource(context),
|
||||||
|
message: 'Bypassing cache and fetching from data source',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件降级处理
|
||||||
|
*/
|
||||||
|
private async handleFileFallback(context: RecoveryContext): Promise<any> {
|
||||||
|
this.logger.warn(`File fallback for task: ${context.taskId}`);
|
||||||
|
|
||||||
|
// 使用备用文件或默认内容
|
||||||
|
return {
|
||||||
|
fallbackType: 'file',
|
||||||
|
data: context.metadata?.backupContent || '',
|
||||||
|
message: 'Using backup content due to file access failure',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用降级处理
|
||||||
|
*/
|
||||||
|
private async handleGenericFallback(context: RecoveryContext): Promise<any> {
|
||||||
|
this.logger.warn(`Generic fallback for task: ${context.taskId}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fallbackType: 'generic',
|
||||||
|
data: null,
|
||||||
|
message: 'Service temporarily unavailable, please try again later',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从数据源获取数据
|
||||||
|
*/
|
||||||
|
private async fetchFromDataSource(context: RecoveryContext): Promise<any> {
|
||||||
|
// 这里应该实现从原始数据源获取数据的逻辑
|
||||||
|
// 具体实现取决于数据源类型
|
||||||
|
|
||||||
|
if (context.metadata?.dataSourceFunction) {
|
||||||
|
return await context.metadata.dataSourceFunction();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
RecoveryStrategy,
|
||||||
|
RecoveryContext,
|
||||||
|
RecoveryResult,
|
||||||
|
} from '../interfaces/healing.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry Recovery Strategy - 重试恢复策略
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* - 处理可重试的错误
|
||||||
|
* - 实现指数退避重试机制
|
||||||
|
* - 监控重试成功率
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class RetryStrategy implements RecoveryStrategy {
|
||||||
|
private readonly logger = new Logger(RetryStrategy.name);
|
||||||
|
|
||||||
|
readonly name = 'retry';
|
||||||
|
readonly priority = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否可以处理该错误
|
||||||
|
*/
|
||||||
|
canHandle(error: any): boolean {
|
||||||
|
// 可重试的错误类型
|
||||||
|
const retryableErrors = [
|
||||||
|
'ECONNRESET',
|
||||||
|
'ETIMEDOUT',
|
||||||
|
'ENOTFOUND',
|
||||||
|
'ECONNREFUSED',
|
||||||
|
'NETWORK_ERROR',
|
||||||
|
'TEMPORARY_FAILURE',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (error?.code && retryableErrors.includes(error.code)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error?.message) {
|
||||||
|
const message = error.message.toLowerCase();
|
||||||
|
return (
|
||||||
|
message.includes('timeout') ||
|
||||||
|
message.includes('connection') ||
|
||||||
|
message.includes('network') ||
|
||||||
|
message.includes('temporary')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行重试恢复
|
||||||
|
*/
|
||||||
|
async execute(context: RecoveryContext): Promise<RecoveryResult> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Executing retry strategy for task: ${context.taskId}, attempt: ${context.retryCount + 1}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 计算退避延迟
|
||||||
|
const delay = this.calculateBackoffDelay(context.retryCount);
|
||||||
|
|
||||||
|
if (delay > 0) {
|
||||||
|
this.logger.debug(`Waiting ${delay}ms before retry`);
|
||||||
|
await this.sleep(delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否超过最大重试次数
|
||||||
|
if (context.retryCount >= context.maxRetries) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
strategy: this.name,
|
||||||
|
duration: Date.now() - startTime,
|
||||||
|
error: 'Maximum retry attempts exceeded',
|
||||||
|
nextAction: 'escalate',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行重试逻辑
|
||||||
|
const result = await this.performRetry(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
strategy: this.name,
|
||||||
|
duration: Date.now() - startTime,
|
||||||
|
result,
|
||||||
|
nextAction: 'retry',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`Retry strategy failed for task: ${context.taskId}`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
strategy: this.name,
|
||||||
|
duration: Date.now() - startTime,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
nextAction:
|
||||||
|
context.retryCount < context.maxRetries ? 'retry' : 'escalate',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取预估执行时间
|
||||||
|
*/
|
||||||
|
getEstimatedTime(): number {
|
||||||
|
return 5000; // 5秒预估时间
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算指数退避延迟
|
||||||
|
*/
|
||||||
|
private calculateBackoffDelay(retryCount: number): number {
|
||||||
|
// 指数退避:2^retryCount * 1000ms,最大30秒
|
||||||
|
const baseDelay = 1000;
|
||||||
|
const maxDelay = 30000;
|
||||||
|
const delay = Math.min(baseDelay * Math.pow(2, retryCount), maxDelay);
|
||||||
|
|
||||||
|
// 添加随机抖动,避免雷群效应
|
||||||
|
const jitter = Math.random() * 0.1 * delay;
|
||||||
|
return Math.floor(delay + jitter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行重试
|
||||||
|
*/
|
||||||
|
private async performRetry(context: RecoveryContext): Promise<any> {
|
||||||
|
// 这里应该重新执行原始任务
|
||||||
|
// 具体实现取决于任务类型和上下文
|
||||||
|
|
||||||
|
this.logger.debug(`Performing retry for task: ${context.taskId}`);
|
||||||
|
|
||||||
|
// 模拟重试逻辑
|
||||||
|
if (context.metadata?.originalFunction) {
|
||||||
|
return await context.metadata.originalFunction();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有原始函数,返回成功标记
|
||||||
|
return { retried: true, timestamp: Date.now() };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 睡眠函数
|
||||||
|
*/
|
||||||
|
private sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export * from './wwjcloud-ai.module';
|
export * from './wwjcloud-ai.module';
|
||||||
export * from './events';
|
export * from './events';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './services/ai-strategy.service';
|
export * from './healing/services/ai-strategy.service';
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { Controller, Get, Query, UseGuards, Post } from '@nestjs/common';
|
import { Controller, Get, Query, UseGuards, Post } from '@nestjs/common';
|
||||||
import { RateLimitGuard } from '@wwjCommon/http/rate-limit.guard';
|
import { RateLimitGuard } from '@wwjCommon/http/rate-limit.guard';
|
||||||
import { AiRecoveryService } from '../services/ai-recovery.service';
|
import { AiRecoveryService } from '../../healing/services/ai-recovery.service';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { ApiQuery } from '@nestjs/swagger';
|
import { ApiQuery } from '@nestjs/swagger';
|
||||||
import { IsInt, IsOptional, Min, IsString, IsIn } from 'class-validator';
|
import { IsInt, IsOptional, Min, IsString, IsIn } from 'class-validator';
|
||||||
import { Public, Roles } from '@wwjCommon/auth/decorators';
|
import { Public, Roles } from '@wwjCommon/auth/decorators';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||||
import { TASK_FAILED_EVENT, TASK_RECOVERY_REQUESTED_EVENT } from '@wwjAi';
|
import { TASK_FAILED_EVENT, TASK_RECOVERY_REQUESTED_EVENT } from '@wwjAi';
|
||||||
import type { Severity, TaskFailedPayload, TaskRecoveryRequestedPayload } from '@wwjAi';
|
import type {
|
||||||
|
Severity,
|
||||||
|
TaskFailedPayload,
|
||||||
|
TaskRecoveryRequestedPayload,
|
||||||
|
} from '@wwjAi';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { MetricsService } from '@wwjCommon/metrics/metrics.service';
|
import { MetricsService } from '@wwjCommon/metrics/metrics.service';
|
||||||
import { AuthGuard } from '@wwjCommon/auth/auth.guard';
|
import { AuthGuard } from '@wwjCommon/auth/auth.guard';
|
||||||
@@ -42,7 +46,7 @@ class SimulateFailureQueryDto {
|
|||||||
export class AiController {
|
export class AiController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly recovery: AiRecoveryService,
|
private readonly recovery: AiRecoveryService,
|
||||||
private readonly emitter: EventEmitter2,
|
private readonly eventBus: EventBus,
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
private readonly metrics: MetricsService,
|
private readonly metrics: MetricsService,
|
||||||
private readonly strategy: AiStrategyService,
|
private readonly strategy: AiStrategyService,
|
||||||
@@ -65,7 +69,12 @@ export class AiController {
|
|||||||
@Get('drain')
|
@Get('drain')
|
||||||
@Post('drain')
|
@Post('drain')
|
||||||
@Roles('admin')
|
@Roles('admin')
|
||||||
@ApiQuery({ name: 'max', required: false, type: Number, description: '最大处理数量(默认10)' })
|
@ApiQuery({
|
||||||
|
name: 'max',
|
||||||
|
required: false,
|
||||||
|
type: Number,
|
||||||
|
description: '最大处理数量(默认10)',
|
||||||
|
})
|
||||||
async drain(@Query() query: DrainQueryDto) {
|
async drain(@Query() query: DrainQueryDto) {
|
||||||
const n = await this.recovery.drain(query.max ?? 10);
|
const n = await this.recovery.drain(query.max ?? 10);
|
||||||
return { processed: n };
|
return { processed: n };
|
||||||
@@ -75,9 +84,15 @@ export class AiController {
|
|||||||
@Post('simulate-failure')
|
@Post('simulate-failure')
|
||||||
@Roles('admin')
|
@Roles('admin')
|
||||||
@ApiQuery({ name: 'taskId', required: false, type: String })
|
@ApiQuery({ name: 'taskId', required: false, type: String })
|
||||||
@ApiQuery({ name: 'severity', required: false, enum: ['low', 'medium', 'high'] })
|
@ApiQuery({
|
||||||
|
name: 'severity',
|
||||||
|
required: false,
|
||||||
|
enum: ['low', 'medium', 'high'],
|
||||||
|
})
|
||||||
@ApiQuery({ name: 'reason', required: false, type: String })
|
@ApiQuery({ name: 'reason', required: false, type: String })
|
||||||
async simulateFailure(@Query() q: SimulateFailureQueryDto): Promise<{ ok: true; emitted: boolean }> {
|
async simulateFailure(
|
||||||
|
@Query() q: SimulateFailureQueryDto,
|
||||||
|
): Promise<{ ok: true; emitted: boolean }> {
|
||||||
// 委派到服务层,控制器不再直接发事件或打点
|
// 委派到服务层,控制器不再直接发事件或打点
|
||||||
return await this.recovery.simulateFailure({
|
return await this.recovery.simulateFailure({
|
||||||
taskId: q.taskId,
|
taskId: q.taskId,
|
||||||
@@ -86,7 +101,7 @@ export class AiController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除 readBoolean 与直接依赖 emitter/metrics/strategy
|
// 移除 readBoolean 与直接依赖 metrics/strategy
|
||||||
private readBoolean(key: string): boolean {
|
private readBoolean(key: string): boolean {
|
||||||
const v = this.config.get<string | boolean>(key);
|
const v = this.config.get<string | boolean>(key);
|
||||||
if (typeof v === 'boolean') return v;
|
if (typeof v === 'boolean') return v;
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
/**
|
||||||
|
* AI Manager Interfaces - AI 管理模块接口定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流程配置接口
|
||||||
|
*/
|
||||||
|
export interface WorkflowConfig {
|
||||||
|
steps: string[];
|
||||||
|
timeout: number;
|
||||||
|
retryCount: number;
|
||||||
|
parallel?: boolean;
|
||||||
|
dependencies?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流程执行上下文接口
|
||||||
|
*/
|
||||||
|
export interface WorkflowContext {
|
||||||
|
taskId: string;
|
||||||
|
userId?: string;
|
||||||
|
siteId?: number;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
startTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流程执行结果接口
|
||||||
|
*/
|
||||||
|
export interface WorkflowResult {
|
||||||
|
success: boolean;
|
||||||
|
steps: StepResult[];
|
||||||
|
duration: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 步骤执行结果接口
|
||||||
|
*/
|
||||||
|
export interface StepResult {
|
||||||
|
step: string;
|
||||||
|
success: boolean;
|
||||||
|
duration: number;
|
||||||
|
result?: any;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模块状态接口
|
||||||
|
*/
|
||||||
|
export interface ModuleState {
|
||||||
|
name: string;
|
||||||
|
status: 'initializing' | 'ready' | 'active' | 'error' | 'unavailable';
|
||||||
|
version: string;
|
||||||
|
lastUpdate: number;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务协调请求接口
|
||||||
|
*/
|
||||||
|
export interface TaskCoordinationRequest {
|
||||||
|
taskId: string;
|
||||||
|
taskType: string;
|
||||||
|
priority: 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
payload: any;
|
||||||
|
requiredModules?: string[];
|
||||||
|
timeout?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务协调结果接口
|
||||||
|
*/
|
||||||
|
export interface TaskCoordinationResult {
|
||||||
|
taskId: string;
|
||||||
|
success: boolean;
|
||||||
|
result?: any;
|
||||||
|
error?: string;
|
||||||
|
duration: number;
|
||||||
|
modulesUsed: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 管理器统计信息接口
|
||||||
|
*/
|
||||||
|
export interface AiManagerStats {
|
||||||
|
modules: {
|
||||||
|
total: number;
|
||||||
|
active: number;
|
||||||
|
error: number;
|
||||||
|
};
|
||||||
|
services: {
|
||||||
|
total: number;
|
||||||
|
healthy: number;
|
||||||
|
byType: Record<string, number>;
|
||||||
|
};
|
||||||
|
workflows: {
|
||||||
|
active: string[];
|
||||||
|
completed: number;
|
||||||
|
failed: number;
|
||||||
|
};
|
||||||
|
tasks: {
|
||||||
|
pending: number;
|
||||||
|
byType: Record<string, number>;
|
||||||
|
oldestAge?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AiBootstrapProvider } from './bootstrap/ai-bootstrap.provider';
|
||||||
|
import { AiController } from './controllers/ai.controller';
|
||||||
|
import { AiOrchestratorService } from './services/ai-orchestrator.service';
|
||||||
|
import { AiRegistryService } from './services/ai-registry.service';
|
||||||
|
import { AiCoordinatorService } from './services/ai-coordinator.service';
|
||||||
|
import { AiHealingModule } from '../healing/healing.module';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI Manager Module - AI 核心管理模块
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* - AI 层的统一管理和协调
|
||||||
|
* - 各子模块的注册和发现
|
||||||
|
* - AI 服务的编排和调度
|
||||||
|
* - 系统启动和配置管理
|
||||||
|
*/
|
||||||
|
@Module({
|
||||||
|
imports: [AiHealingModule],
|
||||||
|
providers: [
|
||||||
|
AiBootstrapProvider,
|
||||||
|
AiOrchestratorService,
|
||||||
|
AiRegistryService,
|
||||||
|
AiCoordinatorService,
|
||||||
|
],
|
||||||
|
controllers: [AiController],
|
||||||
|
exports: [AiOrchestratorService, AiRegistryService, AiCoordinatorService],
|
||||||
|
})
|
||||||
|
export class AiManagerModule {}
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { EventBus, OnEvent } from '@wwjCommon/events/event-bus';
|
||||||
|
import { AiRegistryService } from './ai-registry.service';
|
||||||
|
import { AiOrchestratorService } from './ai-orchestrator.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI Coordinator Service - AI 协调服务
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* - 协调各 AI 模块间的通信
|
||||||
|
* - 处理跨模块的事件和消息
|
||||||
|
* - 管理模块间的依赖关系
|
||||||
|
* - 提供统一的协调接口
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AiCoordinatorService implements OnModuleInit {
|
||||||
|
private readonly logger = new Logger(AiCoordinatorService.name);
|
||||||
|
private readonly moduleStates = new Map<string, string>();
|
||||||
|
private readonly pendingTasks = new Map<string, any>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly eventBus: EventBus,
|
||||||
|
private readonly registryService: AiRegistryService,
|
||||||
|
private readonly orchestratorService: AiOrchestratorService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
this.logger.log('AI Coordinator Service initialized');
|
||||||
|
await this.initializeModuleStates();
|
||||||
|
// Mark manager as ready once coordinator has initialized
|
||||||
|
this.updateModuleState('manager', 'ready');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化模块状态
|
||||||
|
*/
|
||||||
|
private async initializeModuleStates(): Promise<void> {
|
||||||
|
const modules = ['healing', 'safe', 'tuner', 'manager'];
|
||||||
|
|
||||||
|
for (const module of modules) {
|
||||||
|
this.moduleStates.set(module, 'initializing');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log('Module states initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新模块状态
|
||||||
|
*/
|
||||||
|
updateModuleState(moduleName: string, state: string): void {
|
||||||
|
const previousState = this.moduleStates.get(moduleName);
|
||||||
|
this.moduleStates.set(moduleName, state);
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Module state updated: ${moduleName} ${previousState} -> ${state}`,
|
||||||
|
);
|
||||||
|
this.eventBus.emit('module.state.changed', {
|
||||||
|
module: moduleName,
|
||||||
|
previousState,
|
||||||
|
currentState: state,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取模块状态
|
||||||
|
*/
|
||||||
|
getModuleState(moduleName: string): string | undefined {
|
||||||
|
return this.moduleStates.get(moduleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有模块状态
|
||||||
|
*/
|
||||||
|
getAllModuleStates(): Record<string, string> {
|
||||||
|
return Object.fromEntries(this.moduleStates);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 协调任务执行
|
||||||
|
*/
|
||||||
|
async coordinateTask(
|
||||||
|
taskId: string,
|
||||||
|
taskType: string,
|
||||||
|
payload: any,
|
||||||
|
): Promise<any> {
|
||||||
|
this.logger.log(`Coordinating task: ${taskId} (${taskType})`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 检查相关模块状态
|
||||||
|
const requiredModules = this.getRequiredModules(taskType);
|
||||||
|
const moduleCheck = await this.checkModuleAvailability(requiredModules);
|
||||||
|
|
||||||
|
if (!moduleCheck.allAvailable) {
|
||||||
|
throw new Error(
|
||||||
|
`Required modules not available: ${moduleCheck.unavailable.join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行任务协调
|
||||||
|
const result = await this.executeCoordinatedTask(
|
||||||
|
taskId,
|
||||||
|
taskType,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.eventBus.emit('task.coordinated', { taskId, taskType, result });
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Task coordination failed: ${taskId}`, error);
|
||||||
|
this.eventBus.emit('task.coordination.failed', {
|
||||||
|
taskId,
|
||||||
|
taskType,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务所需模块
|
||||||
|
*/
|
||||||
|
private getRequiredModules(taskType: string): string[] {
|
||||||
|
const moduleMap: Record<string, string[]> = {
|
||||||
|
healing: ['healing', 'manager'],
|
||||||
|
security: ['safe', 'manager'],
|
||||||
|
performance: ['tuner', 'manager'],
|
||||||
|
comprehensive: ['healing', 'safe', 'tuner', 'manager'],
|
||||||
|
};
|
||||||
|
|
||||||
|
return moduleMap[taskType] || ['manager'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查模块可用性
|
||||||
|
*/
|
||||||
|
private async checkModuleAvailability(modules: string[]): Promise<{
|
||||||
|
allAvailable: boolean;
|
||||||
|
available: string[];
|
||||||
|
unavailable: string[];
|
||||||
|
}> {
|
||||||
|
const available: string[] = [];
|
||||||
|
const unavailable: string[] = [];
|
||||||
|
|
||||||
|
for (const module of modules) {
|
||||||
|
const state = this.moduleStates.get(module);
|
||||||
|
if (state === 'ready' || state === 'active') {
|
||||||
|
available.push(module);
|
||||||
|
} else {
|
||||||
|
unavailable.push(module);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allAvailable: unavailable.length === 0,
|
||||||
|
available,
|
||||||
|
unavailable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行协调任务
|
||||||
|
*/
|
||||||
|
private async executeCoordinatedTask(
|
||||||
|
taskId: string,
|
||||||
|
taskType: string,
|
||||||
|
payload: any,
|
||||||
|
): Promise<any> {
|
||||||
|
// 将任务添加到待处理队列
|
||||||
|
this.pendingTasks.set(taskId, { taskType, payload, startTime: Date.now() });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 根据任务类型选择执行策略
|
||||||
|
let result;
|
||||||
|
|
||||||
|
switch (taskType) {
|
||||||
|
case 'healing':
|
||||||
|
result = await this.orchestratorService.executeWorkflow(
|
||||||
|
'healing',
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'security':
|
||||||
|
result = await this.orchestratorService.executeWorkflow(
|
||||||
|
'security',
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'performance':
|
||||||
|
result = await this.orchestratorService.executeWorkflow(
|
||||||
|
'performance',
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
result = await this.executeCustomTask(taskType, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
// 从待处理队列中移除
|
||||||
|
this.pendingTasks.delete(taskId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行自定义任务
|
||||||
|
*/
|
||||||
|
private async executeCustomTask(
|
||||||
|
taskType: string,
|
||||||
|
payload: any,
|
||||||
|
): Promise<any> {
|
||||||
|
const services = this.registryService.getServicesByType(taskType);
|
||||||
|
|
||||||
|
if (services.length === 0) {
|
||||||
|
throw new Error(`No services available for task type: ${taskType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行第一个可用服务
|
||||||
|
return await services[0].execute(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理任务失败事件
|
||||||
|
*/
|
||||||
|
@OnEvent('task.failed')
|
||||||
|
async handleTaskFailed(payload: any): Promise<void> {
|
||||||
|
this.logger.warn(`Task failed: ${payload.taskId}`);
|
||||||
|
|
||||||
|
// 尝试协调恢复
|
||||||
|
if (payload.severity === 'high') {
|
||||||
|
await this.coordinateTask(`recovery-${payload.taskId}`, 'healing', {
|
||||||
|
originalTask: payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理模块状态变化事件
|
||||||
|
*/
|
||||||
|
@OnEvent('module.state.changed')
|
||||||
|
async handleModuleStateChanged(payload: any): Promise<void> {
|
||||||
|
this.logger.debug(
|
||||||
|
`Module state changed: ${payload.module} -> ${payload.currentState}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 同步内部状态映射,保持状态来源一致
|
||||||
|
this.moduleStates.set(payload.module, payload.currentState);
|
||||||
|
|
||||||
|
// 如果模块变为不可用,暂停相关任务
|
||||||
|
if (
|
||||||
|
payload.currentState === 'error' ||
|
||||||
|
payload.currentState === 'unavailable'
|
||||||
|
) {
|
||||||
|
await this.pauseModuleTasks(payload.module);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 暂停模块相关任务
|
||||||
|
*/
|
||||||
|
private async pauseModuleTasks(moduleName: string): Promise<void> {
|
||||||
|
this.logger.warn(`Pausing tasks for module: ${moduleName}`);
|
||||||
|
|
||||||
|
for (const [taskId, task] of this.pendingTasks) {
|
||||||
|
const requiredModules = this.getRequiredModules(task.taskType);
|
||||||
|
if (requiredModules.includes(moduleName)) {
|
||||||
|
this.logger.warn(`Pausing task: ${taskId}`);
|
||||||
|
this.eventBus.emit('task.paused', {
|
||||||
|
taskId,
|
||||||
|
reason: `Module unavailable: ${moduleName}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取待处理任务统计
|
||||||
|
*/
|
||||||
|
getPendingTasksStats(): {
|
||||||
|
total: number;
|
||||||
|
byType: Record<string, number>;
|
||||||
|
oldestTask?: { id: string; age: number };
|
||||||
|
} {
|
||||||
|
const total = this.pendingTasks.size;
|
||||||
|
const byType: Record<string, number> = {};
|
||||||
|
let oldestTask: { id: string; age: number } | undefined;
|
||||||
|
|
||||||
|
for (const [taskId, task] of this.pendingTasks) {
|
||||||
|
byType[task.taskType] = (byType[task.taskType] || 0) + 1;
|
||||||
|
|
||||||
|
const age = Date.now() - task.startTime;
|
||||||
|
if (!oldestTask || age > oldestTask.age) {
|
||||||
|
oldestTask = { id: taskId, age };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { total, byType, oldestTask };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||||
|
import { AiRegistryService } from './ai-registry.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI Orchestrator Service - AI 编排服务
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* - 协调各 AI 模块的工作流程
|
||||||
|
* - 管理 AI 任务的执行顺序
|
||||||
|
* - 处理模块间的依赖关系
|
||||||
|
* - 监控和调度 AI 服务
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AiOrchestratorService implements OnModuleInit {
|
||||||
|
private readonly logger = new Logger(AiOrchestratorService.name);
|
||||||
|
private readonly activeWorkflows = new Map<string, any>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly eventBus: EventBus,
|
||||||
|
private readonly registryService: AiRegistryService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
this.logger.log('AI Orchestrator Service initialized');
|
||||||
|
await this.initializeWorkflows();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化工作流程
|
||||||
|
*/
|
||||||
|
private async initializeWorkflows(): Promise<void> {
|
||||||
|
this.logger.log('Initializing AI workflows...');
|
||||||
|
// 注册默认工作流程
|
||||||
|
await this.registerDefaultWorkflows();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册默认工作流程
|
||||||
|
*/
|
||||||
|
private async registerDefaultWorkflows(): Promise<void> {
|
||||||
|
// 自愈工作流程
|
||||||
|
this.registerWorkflow('healing', {
|
||||||
|
steps: ['detect', 'analyze', 'recover', 'verify'],
|
||||||
|
timeout: 30000,
|
||||||
|
retryCount: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 安全检查工作流程
|
||||||
|
this.registerWorkflow('security', {
|
||||||
|
steps: ['scan', 'analyze', 'protect', 'report'],
|
||||||
|
timeout: 15000,
|
||||||
|
retryCount: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 性能优化工作流程
|
||||||
|
this.registerWorkflow('performance', {
|
||||||
|
steps: ['monitor', 'analyze', 'optimize', 'validate'],
|
||||||
|
timeout: 60000,
|
||||||
|
retryCount: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册工作流程
|
||||||
|
*/
|
||||||
|
registerWorkflow(name: string, config: any): void {
|
||||||
|
this.activeWorkflows.set(name, config);
|
||||||
|
this.logger.log(`Workflow registered: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行工作流程
|
||||||
|
*/
|
||||||
|
async executeWorkflow(name: string, context: any): Promise<any> {
|
||||||
|
const workflow = this.activeWorkflows.get(name);
|
||||||
|
if (!workflow) {
|
||||||
|
throw new Error(`Workflow not found: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`Executing workflow: ${name}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.processWorkflowSteps(workflow, context);
|
||||||
|
this.eventBus.emit('workflow.completed', { name, result });
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Workflow execution failed: ${name}`, error);
|
||||||
|
this.eventBus.emit('workflow.failed', { name, error });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理工作流程步骤
|
||||||
|
*/
|
||||||
|
private async processWorkflowSteps(
|
||||||
|
workflow: any,
|
||||||
|
context: any,
|
||||||
|
): Promise<any> {
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (const step of workflow.steps) {
|
||||||
|
this.logger.debug(`Processing workflow step: ${step}`);
|
||||||
|
const stepResult = await this.executeWorkflowStep(step, context);
|
||||||
|
results.push(stepResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行工作流程步骤
|
||||||
|
*/
|
||||||
|
private async executeWorkflowStep(step: string, context: any): Promise<any> {
|
||||||
|
// 根据步骤类型调用相应的服务
|
||||||
|
const services = this.registryService.getServicesByType(step);
|
||||||
|
|
||||||
|
if (services.length === 0) {
|
||||||
|
this.logger.warn(`No services found for step: ${step}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行第一个匹配的服务
|
||||||
|
const service = services[0];
|
||||||
|
return await service.execute(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取活动工作流程
|
||||||
|
*/
|
||||||
|
getActiveWorkflows(): string[] {
|
||||||
|
return Array.from(this.activeWorkflows.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止工作流程
|
||||||
|
*/
|
||||||
|
stopWorkflow(name: string): boolean {
|
||||||
|
const removed = this.activeWorkflows.delete(name);
|
||||||
|
if (removed) {
|
||||||
|
this.logger.log(`Workflow stopped: ${name}`);
|
||||||
|
this.eventBus.emit('workflow.stopped', { name });
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
Logger,
|
||||||
|
OnModuleInit,
|
||||||
|
OnModuleDestroy,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI Service Interface - AI 服务接口
|
||||||
|
*/
|
||||||
|
export interface AiService {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
version: string;
|
||||||
|
execute(context: any): Promise<any>;
|
||||||
|
isHealthy(): Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI Registry Service - AI 注册服务
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* - 管理 AI 服务的注册和注销
|
||||||
|
* - 提供服务发现功能
|
||||||
|
* - 监控服务健康状态
|
||||||
|
* - 维护服务元数据
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AiRegistryService implements OnModuleInit, OnModuleDestroy {
|
||||||
|
private readonly logger = new Logger(AiRegistryService.name);
|
||||||
|
private readonly services = new Map<string, AiService>();
|
||||||
|
private readonly servicesByType = new Map<string, AiService[]>();
|
||||||
|
private healthCheckInterval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
constructor(private readonly eventBus: EventBus) {}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
this.logger.log('AI Registry Service initialized');
|
||||||
|
await this.startHealthCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy() {
|
||||||
|
// 停止健康检查定时器,避免残留句柄
|
||||||
|
if (this.healthCheckInterval) {
|
||||||
|
clearInterval(this.healthCheckInterval);
|
||||||
|
this.healthCheckInterval = null;
|
||||||
|
this.logger.log('AI Registry Service health check stopped');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册 AI 服务
|
||||||
|
*/
|
||||||
|
registerService(service: AiService): void {
|
||||||
|
this.services.set(service.name, service);
|
||||||
|
|
||||||
|
// 按类型分组
|
||||||
|
if (!this.servicesByType.has(service.type)) {
|
||||||
|
this.servicesByType.set(service.type, []);
|
||||||
|
}
|
||||||
|
this.servicesByType.get(service.type)!.push(service);
|
||||||
|
|
||||||
|
this.logger.log(`Service registered: ${service.name} (${service.type})`);
|
||||||
|
this.eventBus.emit('service.registered', service);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注销 AI 服务
|
||||||
|
*/
|
||||||
|
unregisterService(serviceName: string): boolean {
|
||||||
|
const service = this.services.get(serviceName);
|
||||||
|
if (!service) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.services.delete(serviceName);
|
||||||
|
|
||||||
|
// 从类型分组中移除
|
||||||
|
const typeServices = this.servicesByType.get(service.type);
|
||||||
|
if (typeServices) {
|
||||||
|
const index = typeServices.findIndex((s) => s.name === serviceName);
|
||||||
|
if (index > -1) {
|
||||||
|
typeServices.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`Service unregistered: ${serviceName}`);
|
||||||
|
this.eventBus.emit('service.unregistered', service);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取服务
|
||||||
|
*/
|
||||||
|
getService(serviceName: string): AiService | undefined {
|
||||||
|
return this.services.get(serviceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据类型获取服务列表
|
||||||
|
*/
|
||||||
|
getServicesByType(type: string): AiService[] {
|
||||||
|
return this.servicesByType.get(type) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有服务
|
||||||
|
*/
|
||||||
|
getAllServices(): AiService[] {
|
||||||
|
return Array.from(this.services.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取服务统计信息
|
||||||
|
*/
|
||||||
|
getServiceStats(): {
|
||||||
|
total: number;
|
||||||
|
byType: Record<string, number>;
|
||||||
|
healthy: number;
|
||||||
|
} {
|
||||||
|
const total = this.services.size;
|
||||||
|
const byType: Record<string, number> = {};
|
||||||
|
|
||||||
|
for (const [type, services] of this.servicesByType) {
|
||||||
|
byType[type] = services.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
total,
|
||||||
|
byType,
|
||||||
|
healthy: 0, // 将在健康检查中更新
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动健康检查
|
||||||
|
*/
|
||||||
|
private async startHealthCheck(): Promise<void> {
|
||||||
|
this.healthCheckInterval = setInterval(async () => {
|
||||||
|
await this.performHealthCheck();
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行健康检查
|
||||||
|
*/
|
||||||
|
private async performHealthCheck(): Promise<void> {
|
||||||
|
let healthyCount = 0;
|
||||||
|
|
||||||
|
for (const service of this.services.values()) {
|
||||||
|
try {
|
||||||
|
const isHealthy = await service.isHealthy();
|
||||||
|
if (isHealthy) {
|
||||||
|
healthyCount++;
|
||||||
|
} else {
|
||||||
|
this.logger.warn(`Service unhealthy: ${service.name}`);
|
||||||
|
this.eventBus.emit('service.unhealthy', service);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`Health check failed for service: ${service.name}`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
this.eventBus.emit('service.error', { service, error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`Health check completed: ${healthyCount}/${this.services.size} services healthy`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找服务
|
||||||
|
*/
|
||||||
|
findServices(predicate: (service: AiService) => boolean): AiService[] {
|
||||||
|
return Array.from(this.services.values()).filter(predicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查服务是否存在
|
||||||
|
*/
|
||||||
|
hasService(serviceName: string): boolean {
|
||||||
|
return this.services.has(serviceName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,527 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Security Analyzer - 安全分析器
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* - 分析系统安全状态
|
||||||
|
* - 识别潜在安全威胁
|
||||||
|
* - 评估安全风险等级
|
||||||
|
* - 生成安全报告
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class SecurityAnalyzer {
|
||||||
|
private readonly logger = new Logger(SecurityAnalyzer.name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析系统安全状态
|
||||||
|
*/
|
||||||
|
async analyzeSystemSecurity(): Promise<SecurityAnalysisResult> {
|
||||||
|
this.logger.log('Starting system security analysis');
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 执行各项安全检查
|
||||||
|
const [authSecurity, dataSecurity, networkSecurity, codeSecurity] =
|
||||||
|
await Promise.all([
|
||||||
|
this.analyzeAuthSecurity(),
|
||||||
|
this.analyzeDataSecurity(),
|
||||||
|
this.analyzeNetworkSecurity(),
|
||||||
|
this.analyzeCodeSecurity(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const overallRisk = this.calculateOverallRisk([
|
||||||
|
authSecurity.riskLevel,
|
||||||
|
dataSecurity.riskLevel,
|
||||||
|
networkSecurity.riskLevel,
|
||||||
|
codeSecurity.riskLevel,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
duration: Date.now() - startTime,
|
||||||
|
overallRisk,
|
||||||
|
categories: {
|
||||||
|
authentication: authSecurity,
|
||||||
|
dataProtection: dataSecurity,
|
||||||
|
networkSecurity: networkSecurity,
|
||||||
|
codeQuality: codeSecurity,
|
||||||
|
},
|
||||||
|
recommendations: this.generateRecommendations(overallRisk),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Security analysis failed', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析认证安全
|
||||||
|
*/
|
||||||
|
private async analyzeAuthSecurity(): Promise<SecurityCategoryResult> {
|
||||||
|
// 检查认证机制
|
||||||
|
const checks = [
|
||||||
|
this.checkJwtSecurity(),
|
||||||
|
this.checkPasswordPolicy(),
|
||||||
|
this.checkSessionSecurity(),
|
||||||
|
this.checkMfaSecurity(),
|
||||||
|
];
|
||||||
|
|
||||||
|
const results = await Promise.all(checks);
|
||||||
|
const riskLevel = this.calculateCategoryRisk(results);
|
||||||
|
|
||||||
|
return {
|
||||||
|
category: 'authentication',
|
||||||
|
riskLevel,
|
||||||
|
checks: results,
|
||||||
|
score: this.calculateSecurityScore(results),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析数据安全
|
||||||
|
*/
|
||||||
|
private async analyzeDataSecurity(): Promise<SecurityCategoryResult> {
|
||||||
|
const checks = [
|
||||||
|
this.checkDataEncryption(),
|
||||||
|
this.checkDataAccess(),
|
||||||
|
this.checkDataBackup(),
|
||||||
|
this.checkDataRetention(),
|
||||||
|
];
|
||||||
|
|
||||||
|
const results = await Promise.all(checks);
|
||||||
|
const riskLevel = this.calculateCategoryRisk(results);
|
||||||
|
|
||||||
|
return {
|
||||||
|
category: 'dataProtection',
|
||||||
|
riskLevel,
|
||||||
|
checks: results,
|
||||||
|
score: this.calculateSecurityScore(results),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析网络安全
|
||||||
|
*/
|
||||||
|
private async analyzeNetworkSecurity(): Promise<SecurityCategoryResult> {
|
||||||
|
const checks = [
|
||||||
|
this.checkHttpsSecurity(),
|
||||||
|
this.checkCorsConfiguration(),
|
||||||
|
this.checkRateLimiting(),
|
||||||
|
this.checkFirewallRules(),
|
||||||
|
];
|
||||||
|
|
||||||
|
const results = await Promise.all(checks);
|
||||||
|
const riskLevel = this.calculateCategoryRisk(results);
|
||||||
|
|
||||||
|
return {
|
||||||
|
category: 'networkSecurity',
|
||||||
|
riskLevel,
|
||||||
|
checks: results,
|
||||||
|
score: this.calculateSecurityScore(results),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析代码安全
|
||||||
|
*/
|
||||||
|
private async analyzeCodeSecurity(): Promise<SecurityCategoryResult> {
|
||||||
|
const checks = [
|
||||||
|
this.checkInputValidation(),
|
||||||
|
this.checkSqlInjection(),
|
||||||
|
this.checkXssProtection(),
|
||||||
|
this.checkDependencyVulnerabilities(),
|
||||||
|
];
|
||||||
|
|
||||||
|
const results = await Promise.all(checks);
|
||||||
|
const riskLevel = this.calculateCategoryRisk(results);
|
||||||
|
|
||||||
|
return {
|
||||||
|
category: 'codeQuality',
|
||||||
|
riskLevel,
|
||||||
|
checks: results,
|
||||||
|
score: this.calculateSecurityScore(results),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT 安全检查
|
||||||
|
*/
|
||||||
|
private async checkJwtSecurity(): Promise<SecurityCheckResult> {
|
||||||
|
// 实现 JWT 安全检查逻辑
|
||||||
|
return {
|
||||||
|
name: 'JWT Security',
|
||||||
|
status: 'pass',
|
||||||
|
riskLevel: 'low',
|
||||||
|
message: 'JWT configuration is secure',
|
||||||
|
details: {
|
||||||
|
algorithm: 'RS256',
|
||||||
|
expiration: '1h',
|
||||||
|
secretRotation: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码策略检查
|
||||||
|
*/
|
||||||
|
private async checkPasswordPolicy(): Promise<SecurityCheckResult> {
|
||||||
|
return {
|
||||||
|
name: 'Password Policy',
|
||||||
|
status: 'pass',
|
||||||
|
riskLevel: 'low',
|
||||||
|
message: 'Password policy meets security requirements',
|
||||||
|
details: {
|
||||||
|
minLength: 8,
|
||||||
|
complexity: true,
|
||||||
|
expiration: 90,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话安全检查
|
||||||
|
*/
|
||||||
|
private async checkSessionSecurity(): Promise<SecurityCheckResult> {
|
||||||
|
return {
|
||||||
|
name: 'Session Security',
|
||||||
|
status: 'pass',
|
||||||
|
riskLevel: 'medium',
|
||||||
|
message: 'Session configuration needs improvement',
|
||||||
|
details: {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
sameSite: 'strict',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多因素认证检查
|
||||||
|
*/
|
||||||
|
private async checkMfaSecurity(): Promise<SecurityCheckResult> {
|
||||||
|
return {
|
||||||
|
name: 'Multi-Factor Authentication',
|
||||||
|
status: 'warning',
|
||||||
|
riskLevel: 'medium',
|
||||||
|
message: 'MFA is not enabled for all users',
|
||||||
|
details: {
|
||||||
|
enabled: false,
|
||||||
|
coverage: '30%',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据加密检查
|
||||||
|
*/
|
||||||
|
private async checkDataEncryption(): Promise<SecurityCheckResult> {
|
||||||
|
return {
|
||||||
|
name: 'Data Encryption',
|
||||||
|
status: 'pass',
|
||||||
|
riskLevel: 'low',
|
||||||
|
message: 'Data encryption is properly configured',
|
||||||
|
details: {
|
||||||
|
atRest: true,
|
||||||
|
inTransit: true,
|
||||||
|
algorithm: 'AES-256',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据访问检查
|
||||||
|
*/
|
||||||
|
private async checkDataAccess(): Promise<SecurityCheckResult> {
|
||||||
|
return {
|
||||||
|
name: 'Data Access Control',
|
||||||
|
status: 'pass',
|
||||||
|
riskLevel: 'low',
|
||||||
|
message: 'Data access controls are properly implemented',
|
||||||
|
details: {
|
||||||
|
rbac: true,
|
||||||
|
audit: true,
|
||||||
|
encryption: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据备份检查
|
||||||
|
*/
|
||||||
|
private async checkDataBackup(): Promise<SecurityCheckResult> {
|
||||||
|
return {
|
||||||
|
name: 'Data Backup',
|
||||||
|
status: 'pass',
|
||||||
|
riskLevel: 'low',
|
||||||
|
message: 'Data backup strategy is adequate',
|
||||||
|
details: {
|
||||||
|
frequency: 'daily',
|
||||||
|
encryption: true,
|
||||||
|
offsite: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据保留检查
|
||||||
|
*/
|
||||||
|
private async checkDataRetention(): Promise<SecurityCheckResult> {
|
||||||
|
return {
|
||||||
|
name: 'Data Retention',
|
||||||
|
status: 'pass',
|
||||||
|
riskLevel: 'low',
|
||||||
|
message: 'Data retention policies are compliant',
|
||||||
|
details: {
|
||||||
|
policy: 'defined',
|
||||||
|
automation: true,
|
||||||
|
compliance: 'GDPR',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTPS 安全检查
|
||||||
|
*/
|
||||||
|
private async checkHttpsSecurity(): Promise<SecurityCheckResult> {
|
||||||
|
return {
|
||||||
|
name: 'HTTPS Security',
|
||||||
|
status: 'pass',
|
||||||
|
riskLevel: 'low',
|
||||||
|
message: 'HTTPS is properly configured',
|
||||||
|
details: {
|
||||||
|
enforced: true,
|
||||||
|
tlsVersion: '1.3',
|
||||||
|
hsts: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CORS 配置检查
|
||||||
|
*/
|
||||||
|
private async checkCorsConfiguration(): Promise<SecurityCheckResult> {
|
||||||
|
return {
|
||||||
|
name: 'CORS Configuration',
|
||||||
|
status: 'warning',
|
||||||
|
riskLevel: 'medium',
|
||||||
|
message: 'CORS configuration may be too permissive',
|
||||||
|
details: {
|
||||||
|
origins: ['*'],
|
||||||
|
credentials: true,
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 速率限制检查
|
||||||
|
*/
|
||||||
|
private async checkRateLimiting(): Promise<SecurityCheckResult> {
|
||||||
|
return {
|
||||||
|
name: 'Rate Limiting',
|
||||||
|
status: 'pass',
|
||||||
|
riskLevel: 'low',
|
||||||
|
message: 'Rate limiting is properly configured',
|
||||||
|
details: {
|
||||||
|
enabled: true,
|
||||||
|
limits: '100/min',
|
||||||
|
burst: 10,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 防火墙规则检查
|
||||||
|
*/
|
||||||
|
private async checkFirewallRules(): Promise<SecurityCheckResult> {
|
||||||
|
return {
|
||||||
|
name: 'Firewall Rules',
|
||||||
|
status: 'pass',
|
||||||
|
riskLevel: 'low',
|
||||||
|
message: 'Firewall rules are properly configured',
|
||||||
|
details: {
|
||||||
|
enabled: true,
|
||||||
|
defaultDeny: true,
|
||||||
|
logging: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入验证检查
|
||||||
|
*/
|
||||||
|
private async checkInputValidation(): Promise<SecurityCheckResult> {
|
||||||
|
return {
|
||||||
|
name: 'Input Validation',
|
||||||
|
status: 'pass',
|
||||||
|
riskLevel: 'low',
|
||||||
|
message: 'Input validation is comprehensive',
|
||||||
|
details: {
|
||||||
|
sanitization: true,
|
||||||
|
validation: true,
|
||||||
|
whitelisting: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQL 注入检查
|
||||||
|
*/
|
||||||
|
private async checkSqlInjection(): Promise<SecurityCheckResult> {
|
||||||
|
return {
|
||||||
|
name: 'SQL Injection Protection',
|
||||||
|
status: 'pass',
|
||||||
|
riskLevel: 'low',
|
||||||
|
message: 'SQL injection protection is effective',
|
||||||
|
details: {
|
||||||
|
parameterizedQueries: true,
|
||||||
|
orm: 'TypeORM',
|
||||||
|
escaping: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XSS 保护检查
|
||||||
|
*/
|
||||||
|
private async checkXssProtection(): Promise<SecurityCheckResult> {
|
||||||
|
return {
|
||||||
|
name: 'XSS Protection',
|
||||||
|
status: 'pass',
|
||||||
|
riskLevel: 'low',
|
||||||
|
message: 'XSS protection is properly implemented',
|
||||||
|
details: {
|
||||||
|
csp: true,
|
||||||
|
sanitization: true,
|
||||||
|
encoding: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 依赖漏洞检查
|
||||||
|
*/
|
||||||
|
private async checkDependencyVulnerabilities(): Promise<SecurityCheckResult> {
|
||||||
|
return {
|
||||||
|
name: 'Dependency Vulnerabilities',
|
||||||
|
status: 'warning',
|
||||||
|
riskLevel: 'medium',
|
||||||
|
message: 'Some dependencies have known vulnerabilities',
|
||||||
|
details: {
|
||||||
|
total: 150,
|
||||||
|
vulnerable: 3,
|
||||||
|
critical: 0,
|
||||||
|
high: 1,
|
||||||
|
medium: 2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算分类风险等级
|
||||||
|
*/
|
||||||
|
private calculateCategoryRisk(results: SecurityCheckResult[]): RiskLevel {
|
||||||
|
const riskLevels = results.map((r) => r.riskLevel);
|
||||||
|
|
||||||
|
if (riskLevels.includes('critical')) return 'critical';
|
||||||
|
if (riskLevels.includes('high')) return 'high';
|
||||||
|
if (riskLevels.includes('medium')) return 'medium';
|
||||||
|
return 'low';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算整体风险等级
|
||||||
|
*/
|
||||||
|
private calculateOverallRisk(categoryRisks: RiskLevel[]): RiskLevel {
|
||||||
|
const riskWeights = { critical: 4, high: 3, medium: 2, low: 1 };
|
||||||
|
const totalWeight = categoryRisks.reduce(
|
||||||
|
(sum, risk) => sum + riskWeights[risk],
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const avgWeight = totalWeight / categoryRisks.length;
|
||||||
|
|
||||||
|
if (avgWeight >= 3.5) return 'critical';
|
||||||
|
if (avgWeight >= 2.5) return 'high';
|
||||||
|
if (avgWeight >= 1.5) return 'medium';
|
||||||
|
return 'low';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算安全评分
|
||||||
|
*/
|
||||||
|
private calculateSecurityScore(results: SecurityCheckResult[]): number {
|
||||||
|
const statusWeights = { pass: 100, warning: 60, fail: 0 };
|
||||||
|
const totalScore = results.reduce(
|
||||||
|
(sum, result) => sum + statusWeights[result.status],
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
return Math.round(totalScore / results.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成安全建议
|
||||||
|
*/
|
||||||
|
private generateRecommendations(riskLevel: RiskLevel): string[] {
|
||||||
|
const recommendations: Record<RiskLevel, string[]> = {
|
||||||
|
critical: [
|
||||||
|
'Immediately address critical security vulnerabilities',
|
||||||
|
'Implement emergency security patches',
|
||||||
|
'Review and strengthen access controls',
|
||||||
|
'Conduct comprehensive security audit',
|
||||||
|
],
|
||||||
|
high: [
|
||||||
|
'Address high-priority security issues within 24 hours',
|
||||||
|
'Implement additional security monitoring',
|
||||||
|
'Review security policies and procedures',
|
||||||
|
'Consider security training for development team',
|
||||||
|
],
|
||||||
|
medium: [
|
||||||
|
'Address medium-priority security issues within a week',
|
||||||
|
'Implement security best practices',
|
||||||
|
'Regular security assessments',
|
||||||
|
'Update security documentation',
|
||||||
|
],
|
||||||
|
low: [
|
||||||
|
'Maintain current security posture',
|
||||||
|
'Continue regular security monitoring',
|
||||||
|
'Keep security tools and policies updated',
|
||||||
|
'Periodic security reviews',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return recommendations[riskLevel] || recommendations.low;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类型定义
|
||||||
|
export interface SecurityAnalysisResult {
|
||||||
|
timestamp: number;
|
||||||
|
duration: number;
|
||||||
|
overallRisk: RiskLevel;
|
||||||
|
categories: {
|
||||||
|
authentication: SecurityCategoryResult;
|
||||||
|
dataProtection: SecurityCategoryResult;
|
||||||
|
networkSecurity: SecurityCategoryResult;
|
||||||
|
codeQuality: SecurityCategoryResult;
|
||||||
|
};
|
||||||
|
recommendations: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecurityCategoryResult {
|
||||||
|
category: string;
|
||||||
|
riskLevel: RiskLevel;
|
||||||
|
checks: SecurityCheckResult[];
|
||||||
|
score: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecurityCheckResult {
|
||||||
|
name: string;
|
||||||
|
status: 'pass' | 'warning' | 'fail';
|
||||||
|
riskLevel: RiskLevel;
|
||||||
|
message: string;
|
||||||
|
details: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RiskLevel = 'low' | 'medium' | 'high' | 'critical';
|
||||||
@@ -0,0 +1,410 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vulnerability Detector - 漏洞检测器
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* - 扫描系统漏洞
|
||||||
|
* - 检测安全威胁
|
||||||
|
* - 监控异常行为
|
||||||
|
* - 生成威胁报告
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class VulnerabilityDetector {
|
||||||
|
private readonly logger = new Logger(VulnerabilityDetector.name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行全面漏洞扫描
|
||||||
|
*/
|
||||||
|
async scanVulnerabilities(): Promise<VulnerabilityScanResult> {
|
||||||
|
this.logger.log('Starting comprehensive vulnerability scan');
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [
|
||||||
|
codeVulnerabilities,
|
||||||
|
dependencyVulnerabilities,
|
||||||
|
configVulnerabilities,
|
||||||
|
networkVulnerabilities,
|
||||||
|
] = await Promise.all([
|
||||||
|
this.scanCodeVulnerabilities(),
|
||||||
|
this.scanDependencyVulnerabilities(),
|
||||||
|
this.scanConfigurationVulnerabilities(),
|
||||||
|
this.scanNetworkVulnerabilities(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const allVulnerabilities = [
|
||||||
|
...codeVulnerabilities,
|
||||||
|
...dependencyVulnerabilities,
|
||||||
|
...configVulnerabilities,
|
||||||
|
...networkVulnerabilities,
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
duration: Date.now() - startTime,
|
||||||
|
totalVulnerabilities: allVulnerabilities.length,
|
||||||
|
severityBreakdown: this.calculateSeverityBreakdown(allVulnerabilities),
|
||||||
|
vulnerabilities: allVulnerabilities,
|
||||||
|
recommendations:
|
||||||
|
this.generateVulnerabilityRecommendations(allVulnerabilities),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Vulnerability scan failed', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫描代码漏洞
|
||||||
|
*/
|
||||||
|
private async scanCodeVulnerabilities(): Promise<Vulnerability[]> {
|
||||||
|
this.logger.debug('Scanning code vulnerabilities');
|
||||||
|
|
||||||
|
// 模拟代码漏洞扫描
|
||||||
|
const vulnerabilities: Vulnerability[] = [];
|
||||||
|
|
||||||
|
// SQL 注入检测
|
||||||
|
const sqlInjectionVulns = await this.detectSqlInjection();
|
||||||
|
vulnerabilities.push(...sqlInjectionVulns);
|
||||||
|
|
||||||
|
// XSS 检测
|
||||||
|
const xssVulns = await this.detectXssVulnerabilities();
|
||||||
|
vulnerabilities.push(...xssVulns);
|
||||||
|
|
||||||
|
// 认证绕过检测
|
||||||
|
const authBypassVulns = await this.detectAuthenticationBypass();
|
||||||
|
vulnerabilities.push(...authBypassVulns);
|
||||||
|
|
||||||
|
// 权限提升检测
|
||||||
|
const privilegeEscalationVulns = await this.detectPrivilegeEscalation();
|
||||||
|
vulnerabilities.push(...privilegeEscalationVulns);
|
||||||
|
|
||||||
|
return vulnerabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫描依赖漏洞
|
||||||
|
*/
|
||||||
|
private async scanDependencyVulnerabilities(): Promise<Vulnerability[]> {
|
||||||
|
this.logger.debug('Scanning dependency vulnerabilities');
|
||||||
|
|
||||||
|
// 模拟依赖漏洞扫描结果
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'CVE-2023-1234',
|
||||||
|
type: 'dependency',
|
||||||
|
severity: 'high',
|
||||||
|
title: 'Remote Code Execution in lodash',
|
||||||
|
description:
|
||||||
|
'A prototype pollution vulnerability in lodash allows remote code execution',
|
||||||
|
affectedComponent: 'lodash@4.17.20',
|
||||||
|
cweId: 'CWE-1321',
|
||||||
|
cvssScore: 8.5,
|
||||||
|
discoveredAt: Date.now(),
|
||||||
|
status: 'open',
|
||||||
|
remediation: {
|
||||||
|
type: 'update',
|
||||||
|
description: 'Update lodash to version 4.17.21 or later',
|
||||||
|
effort: 'low',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'CVE-2023-5678',
|
||||||
|
type: 'dependency',
|
||||||
|
severity: 'medium',
|
||||||
|
title: 'Information Disclosure in express',
|
||||||
|
description:
|
||||||
|
'Express middleware may leak sensitive information in error messages',
|
||||||
|
affectedComponent: 'express@4.18.0',
|
||||||
|
cweId: 'CWE-200',
|
||||||
|
cvssScore: 5.3,
|
||||||
|
discoveredAt: Date.now(),
|
||||||
|
status: 'open',
|
||||||
|
remediation: {
|
||||||
|
type: 'configuration',
|
||||||
|
description:
|
||||||
|
'Configure error handling to prevent information leakage',
|
||||||
|
effort: 'medium',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫描配置漏洞
|
||||||
|
*/
|
||||||
|
private async scanConfigurationVulnerabilities(): Promise<Vulnerability[]> {
|
||||||
|
this.logger.debug('Scanning configuration vulnerabilities');
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'CONFIG-001',
|
||||||
|
type: 'configuration',
|
||||||
|
severity: 'medium',
|
||||||
|
title: 'Weak CORS Configuration',
|
||||||
|
description:
|
||||||
|
'CORS is configured to allow all origins which may lead to security issues',
|
||||||
|
affectedComponent: 'CORS Middleware',
|
||||||
|
cweId: 'CWE-346',
|
||||||
|
cvssScore: 4.3,
|
||||||
|
discoveredAt: Date.now(),
|
||||||
|
status: 'open',
|
||||||
|
remediation: {
|
||||||
|
type: 'configuration',
|
||||||
|
description: 'Restrict CORS origins to specific trusted domains',
|
||||||
|
effort: 'low',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫描网络漏洞
|
||||||
|
*/
|
||||||
|
private async scanNetworkVulnerabilities(): Promise<Vulnerability[]> {
|
||||||
|
this.logger.debug('Scanning network vulnerabilities');
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'NET-001',
|
||||||
|
type: 'network',
|
||||||
|
severity: 'low',
|
||||||
|
title: 'Missing Security Headers',
|
||||||
|
description: 'Some security headers are not configured properly',
|
||||||
|
affectedComponent: 'HTTP Headers',
|
||||||
|
cweId: 'CWE-693',
|
||||||
|
cvssScore: 3.1,
|
||||||
|
discoveredAt: Date.now(),
|
||||||
|
status: 'open',
|
||||||
|
remediation: {
|
||||||
|
type: 'configuration',
|
||||||
|
description:
|
||||||
|
'Add missing security headers (CSP, HSTS, X-Frame-Options)',
|
||||||
|
effort: 'low',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测 SQL 注入漏洞
|
||||||
|
*/
|
||||||
|
private async detectSqlInjection(): Promise<Vulnerability[]> {
|
||||||
|
// 模拟 SQL 注入检测
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测 XSS 漏洞
|
||||||
|
*/
|
||||||
|
private async detectXssVulnerabilities(): Promise<Vulnerability[]> {
|
||||||
|
// 模拟 XSS 检测
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测认证绕过漏洞
|
||||||
|
*/
|
||||||
|
private async detectAuthenticationBypass(): Promise<Vulnerability[]> {
|
||||||
|
// 模拟认证绕过检测
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测权限提升漏洞
|
||||||
|
*/
|
||||||
|
private async detectPrivilegeEscalation(): Promise<Vulnerability[]> {
|
||||||
|
// 模拟权限提升检测
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算严重程度分布
|
||||||
|
*/
|
||||||
|
private calculateSeverityBreakdown(
|
||||||
|
vulnerabilities: Vulnerability[],
|
||||||
|
): SeverityBreakdown {
|
||||||
|
const breakdown = {
|
||||||
|
critical: 0,
|
||||||
|
high: 0,
|
||||||
|
medium: 0,
|
||||||
|
low: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
vulnerabilities.forEach((vuln) => {
|
||||||
|
breakdown[vuln.severity]++;
|
||||||
|
});
|
||||||
|
|
||||||
|
return breakdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成漏洞修复建议
|
||||||
|
*/
|
||||||
|
private generateVulnerabilityRecommendations(
|
||||||
|
vulnerabilities: Vulnerability[],
|
||||||
|
): string[] {
|
||||||
|
const recommendations: string[] = [];
|
||||||
|
|
||||||
|
const criticalCount = vulnerabilities.filter(
|
||||||
|
(v) => v.severity === 'critical',
|
||||||
|
).length;
|
||||||
|
const highCount = vulnerabilities.filter(
|
||||||
|
(v) => v.severity === 'high',
|
||||||
|
).length;
|
||||||
|
|
||||||
|
if (criticalCount > 0) {
|
||||||
|
recommendations.push(
|
||||||
|
`Immediately address ${criticalCount} critical vulnerabilities`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (highCount > 0) {
|
||||||
|
recommendations.push(
|
||||||
|
`Address ${highCount} high-severity vulnerabilities within 24 hours`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
recommendations.push(
|
||||||
|
'Implement automated vulnerability scanning in CI/CD pipeline',
|
||||||
|
);
|
||||||
|
recommendations.push('Regular security training for development team');
|
||||||
|
recommendations.push(
|
||||||
|
'Establish vulnerability disclosure and response process',
|
||||||
|
);
|
||||||
|
|
||||||
|
return recommendations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测实时威胁
|
||||||
|
*/
|
||||||
|
async detectRealTimeThreats(): Promise<ThreatDetectionResult> {
|
||||||
|
this.logger.log('Starting real-time threat detection');
|
||||||
|
|
||||||
|
const threats = await Promise.all([
|
||||||
|
this.detectSuspiciousActivity(),
|
||||||
|
this.detectAnomalousTraffic(),
|
||||||
|
this.detectBruteForceAttacks(),
|
||||||
|
this.detectMaliciousPayloads(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const allThreats = threats.flat();
|
||||||
|
|
||||||
|
return {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
threatsDetected: allThreats.length,
|
||||||
|
threats: allThreats,
|
||||||
|
riskLevel: this.calculateThreatRiskLevel(allThreats),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测可疑活动
|
||||||
|
*/
|
||||||
|
private async detectSuspiciousActivity(): Promise<Threat[]> {
|
||||||
|
// 模拟可疑活动检测
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测异常流量
|
||||||
|
*/
|
||||||
|
private async detectAnomalousTraffic(): Promise<Threat[]> {
|
||||||
|
// 模拟异常流量检测
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测暴力破解攻击
|
||||||
|
*/
|
||||||
|
private async detectBruteForceAttacks(): Promise<Threat[]> {
|
||||||
|
// 模拟暴力破解检测
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测恶意载荷
|
||||||
|
*/
|
||||||
|
private async detectMaliciousPayloads(): Promise<Threat[]> {
|
||||||
|
// 模拟恶意载荷检测
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算威胁风险等级
|
||||||
|
*/
|
||||||
|
private calculateThreatRiskLevel(
|
||||||
|
threats: Threat[],
|
||||||
|
): 'low' | 'medium' | 'high' | 'critical' {
|
||||||
|
if (threats.length === 0) return 'low';
|
||||||
|
|
||||||
|
const highSeverityThreats = threats.filter(
|
||||||
|
(t) => t.severity === 'high' || t.severity === 'critical',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (highSeverityThreats.length > 5) return 'critical';
|
||||||
|
if (highSeverityThreats.length > 2) return 'high';
|
||||||
|
if (threats.length > 10) return 'medium';
|
||||||
|
|
||||||
|
return 'low';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类型定义
|
||||||
|
export interface VulnerabilityScanResult {
|
||||||
|
timestamp: number;
|
||||||
|
duration: number;
|
||||||
|
totalVulnerabilities: number;
|
||||||
|
severityBreakdown: SeverityBreakdown;
|
||||||
|
vulnerabilities: Vulnerability[];
|
||||||
|
recommendations: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Vulnerability {
|
||||||
|
id: string;
|
||||||
|
type: 'code' | 'dependency' | 'configuration' | 'network';
|
||||||
|
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
affectedComponent: string;
|
||||||
|
cweId?: string;
|
||||||
|
cvssScore?: number;
|
||||||
|
discoveredAt: number;
|
||||||
|
status: 'open' | 'in_progress' | 'resolved' | 'false_positive';
|
||||||
|
remediation: {
|
||||||
|
type: 'update' | 'patch' | 'configuration' | 'code_change';
|
||||||
|
description: string;
|
||||||
|
effort: 'low' | 'medium' | 'high';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SeverityBreakdown {
|
||||||
|
critical: number;
|
||||||
|
high: number;
|
||||||
|
medium: number;
|
||||||
|
low: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThreatDetectionResult {
|
||||||
|
timestamp: number;
|
||||||
|
threatsDetected: number;
|
||||||
|
threats: Threat[];
|
||||||
|
riskLevel: 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Threat {
|
||||||
|
id: string;
|
||||||
|
type:
|
||||||
|
| 'suspicious_activity'
|
||||||
|
| 'anomalous_traffic'
|
||||||
|
| 'brute_force'
|
||||||
|
| 'malicious_payload';
|
||||||
|
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
source: string;
|
||||||
|
description: string;
|
||||||
|
detectedAt: number;
|
||||||
|
indicators: string[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,603 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
Logger,
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access Protector - 访问保护器
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* - 控制系统访问权限
|
||||||
|
* - 实施安全策略
|
||||||
|
* - 监控访问行为
|
||||||
|
* - 防止未授权访问
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AccessProtector implements CanActivate {
|
||||||
|
private readonly logger = new Logger(AccessProtector.name);
|
||||||
|
private readonly accessAttempts = new Map<string, AccessAttempt[]>();
|
||||||
|
private readonly blockedIps = new Set<string>();
|
||||||
|
private readonly suspiciousActivities = new Map<
|
||||||
|
string,
|
||||||
|
SuspiciousActivity[]
|
||||||
|
>();
|
||||||
|
|
||||||
|
constructor(private reflector: Reflector) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 守卫方法 - 检查访问权限
|
||||||
|
*/
|
||||||
|
canActivate(
|
||||||
|
context: ExecutionContext,
|
||||||
|
): boolean | Promise<boolean> | Observable<boolean> {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const clientIp = this.getClientIp(request);
|
||||||
|
|
||||||
|
// 检查 IP 是否被阻止
|
||||||
|
if (this.blockedIps.has(clientIp)) {
|
||||||
|
this.logger.warn(`Blocked IP attempted access: ${clientIp}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录访问尝试
|
||||||
|
this.recordAccessAttempt(clientIp, request);
|
||||||
|
|
||||||
|
// 检查访问频率
|
||||||
|
if (this.isAccessRateLimited(clientIp)) {
|
||||||
|
this.logger.warn(`Rate limit exceeded for IP: ${clientIp}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查可疑活动
|
||||||
|
if (this.detectSuspiciousActivity(clientIp, request)) {
|
||||||
|
this.logger.warn(`Suspicious activity detected from IP: ${clientIp}`);
|
||||||
|
this.handleSuspiciousActivity(clientIp, request);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证用户权限
|
||||||
|
*/
|
||||||
|
async validateUserPermissions(
|
||||||
|
userId: string,
|
||||||
|
resource: string,
|
||||||
|
action: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
this.logger.debug(
|
||||||
|
`Validating permissions for user ${userId}: ${action} on ${resource}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取用户角色和权限
|
||||||
|
const userPermissions = await this.getUserPermissions(userId);
|
||||||
|
|
||||||
|
// 检查资源访问权限
|
||||||
|
const hasPermission = this.checkResourcePermission(
|
||||||
|
userPermissions,
|
||||||
|
resource,
|
||||||
|
action,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasPermission) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Permission denied for user ${userId}: ${action} on ${resource}`,
|
||||||
|
);
|
||||||
|
await this.logSecurityEvent('PERMISSION_DENIED', {
|
||||||
|
userId,
|
||||||
|
resource,
|
||||||
|
action,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasPermission;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`Error validating permissions for user ${userId}`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实施安全策略
|
||||||
|
*/
|
||||||
|
async enforceSecurityPolicy(
|
||||||
|
policyName: string,
|
||||||
|
context: SecurityContext,
|
||||||
|
): Promise<PolicyResult> {
|
||||||
|
this.logger.debug(`Enforcing security policy: ${policyName}`);
|
||||||
|
|
||||||
|
const policy = await this.getSecurityPolicy(policyName);
|
||||||
|
|
||||||
|
if (!policy) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
reason: 'Policy not found',
|
||||||
|
actions: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 评估策略条件
|
||||||
|
const evaluation = await this.evaluatePolicyConditions(policy, context);
|
||||||
|
|
||||||
|
if (!evaluation.allowed) {
|
||||||
|
// 执行策略动作
|
||||||
|
await this.executePolicyActions(policy.denyActions, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return evaluation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监控访问模式
|
||||||
|
*/
|
||||||
|
async monitorAccessPatterns(): Promise<AccessPatternAnalysis> {
|
||||||
|
this.logger.log('Analyzing access patterns');
|
||||||
|
|
||||||
|
const analysis = {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
totalAccesses: 0,
|
||||||
|
uniqueIps: 0,
|
||||||
|
suspiciousActivities: 0,
|
||||||
|
blockedAttempts: 0,
|
||||||
|
topSources: [] as AccessSource[],
|
||||||
|
anomalies: [] as AccessAnomaly[],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 分析访问尝试
|
||||||
|
for (const [ip, attempts] of this.accessAttempts.entries()) {
|
||||||
|
analysis.totalAccesses += attempts.length;
|
||||||
|
|
||||||
|
// 检查异常模式
|
||||||
|
const anomalies = this.detectAccessAnomalies(ip, attempts);
|
||||||
|
analysis.anomalies.push(...anomalies);
|
||||||
|
}
|
||||||
|
|
||||||
|
analysis.uniqueIps = this.accessAttempts.size;
|
||||||
|
analysis.suspiciousActivities = this.suspiciousActivities.size;
|
||||||
|
analysis.blockedAttempts = this.blockedIps.size;
|
||||||
|
analysis.topSources = this.getTopAccessSources();
|
||||||
|
|
||||||
|
return analysis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取客户端 IP
|
||||||
|
*/
|
||||||
|
private getClientIp(request: any): string {
|
||||||
|
return (
|
||||||
|
request.ip ||
|
||||||
|
request.connection?.remoteAddress ||
|
||||||
|
request.socket?.remoteAddress ||
|
||||||
|
request.headers['x-forwarded-for']?.split(',')[0] ||
|
||||||
|
'unknown'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录访问尝试
|
||||||
|
*/
|
||||||
|
private recordAccessAttempt(ip: string, request: any): void {
|
||||||
|
if (!this.accessAttempts.has(ip)) {
|
||||||
|
this.accessAttempts.set(ip, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const attempts = this.accessAttempts.get(ip)!;
|
||||||
|
attempts.push({
|
||||||
|
timestamp: Date.now(),
|
||||||
|
method: request.method,
|
||||||
|
url: request.url,
|
||||||
|
userAgent: request.headers['user-agent'],
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保持最近 1000 次访问记录
|
||||||
|
if (attempts.length > 1000) {
|
||||||
|
attempts.splice(0, attempts.length - 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查访问频率限制
|
||||||
|
*/
|
||||||
|
private isAccessRateLimited(ip: string): boolean {
|
||||||
|
const attempts = this.accessAttempts.get(ip) || [];
|
||||||
|
const recentAttempts = attempts.filter(
|
||||||
|
(attempt) => Date.now() - attempt.timestamp < 60000, // 1分钟内
|
||||||
|
);
|
||||||
|
|
||||||
|
return recentAttempts.length > 100; // 每分钟最多100次请求
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测可疑活动
|
||||||
|
*/
|
||||||
|
private detectSuspiciousActivity(ip: string, request: any): boolean {
|
||||||
|
const attempts = this.accessAttempts.get(ip) || [];
|
||||||
|
|
||||||
|
// 检查快速连续请求
|
||||||
|
const recentAttempts = attempts.filter(
|
||||||
|
(attempt) => Date.now() - attempt.timestamp < 10000, // 10秒内
|
||||||
|
);
|
||||||
|
|
||||||
|
if (recentAttempts.length > 50) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查异常 User-Agent
|
||||||
|
const userAgent = request.headers['user-agent'];
|
||||||
|
if (!userAgent || this.isSuspiciousUserAgent(userAgent)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查恶意路径
|
||||||
|
if (this.containsMaliciousPatterns(request.url)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理可疑活动
|
||||||
|
*/
|
||||||
|
private handleSuspiciousActivity(ip: string, request: any): void {
|
||||||
|
if (!this.suspiciousActivities.has(ip)) {
|
||||||
|
this.suspiciousActivities.set(ip, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const activities = this.suspiciousActivities.get(ip)!;
|
||||||
|
activities.push({
|
||||||
|
timestamp: Date.now(),
|
||||||
|
type: 'suspicious_request',
|
||||||
|
details: {
|
||||||
|
method: request.method,
|
||||||
|
url: request.url,
|
||||||
|
userAgent: request.headers['user-agent'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果可疑活动过多,阻止该 IP
|
||||||
|
if (activities.length > 10) {
|
||||||
|
this.blockedIps.add(ip);
|
||||||
|
this.logger.warn(
|
||||||
|
`IP ${ip} has been blocked due to excessive suspicious activities`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查可疑 User-Agent
|
||||||
|
*/
|
||||||
|
private isSuspiciousUserAgent(userAgent: string): boolean {
|
||||||
|
const suspiciousPatterns = [
|
||||||
|
/bot/i,
|
||||||
|
/crawler/i,
|
||||||
|
/spider/i,
|
||||||
|
/scanner/i,
|
||||||
|
/sqlmap/i,
|
||||||
|
/nikto/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
return suspiciousPatterns.some((pattern) => pattern.test(userAgent));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查恶意路径模式
|
||||||
|
*/
|
||||||
|
private containsMaliciousPatterns(url: string): boolean {
|
||||||
|
const maliciousPatterns = [
|
||||||
|
/\.\./, // 路径遍历
|
||||||
|
/\/etc\/passwd/,
|
||||||
|
/\/proc\/self/,
|
||||||
|
/<script/i, // XSS
|
||||||
|
/javascript:/i,
|
||||||
|
/union.*select/i, // SQL 注入
|
||||||
|
/drop.*table/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
return maliciousPatterns.some((pattern) => pattern.test(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户权限
|
||||||
|
*/
|
||||||
|
private async getUserPermissions(userId: string): Promise<UserPermissions> {
|
||||||
|
// 这里应该从数据库或缓存中获取用户权限
|
||||||
|
// 模拟返回权限数据
|
||||||
|
return {
|
||||||
|
userId,
|
||||||
|
roles: ['user'],
|
||||||
|
permissions: [
|
||||||
|
{ resource: 'user', actions: ['read', 'update'] },
|
||||||
|
{ resource: 'profile', actions: ['read', 'update'] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查资源权限
|
||||||
|
*/
|
||||||
|
private checkResourcePermission(
|
||||||
|
userPermissions: UserPermissions,
|
||||||
|
resource: string,
|
||||||
|
action: string,
|
||||||
|
): boolean {
|
||||||
|
return userPermissions.permissions.some(
|
||||||
|
(permission) =>
|
||||||
|
permission.resource === resource && permission.actions.includes(action),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取安全策略
|
||||||
|
*/
|
||||||
|
private async getSecurityPolicy(
|
||||||
|
policyName: string,
|
||||||
|
): Promise<SecurityPolicy | null> {
|
||||||
|
// 模拟安全策略
|
||||||
|
const policies: Record<string, SecurityPolicy> = {
|
||||||
|
rate_limit: {
|
||||||
|
name: 'rate_limit',
|
||||||
|
conditions: [
|
||||||
|
{ type: 'request_count', threshold: 100, timeWindow: 60000 },
|
||||||
|
],
|
||||||
|
denyActions: ['block_ip', 'log_event'],
|
||||||
|
},
|
||||||
|
geo_restriction: {
|
||||||
|
name: 'geo_restriction',
|
||||||
|
conditions: [
|
||||||
|
{ type: 'geo_location', allowedCountries: ['CN', 'US', 'EU'] },
|
||||||
|
],
|
||||||
|
denyActions: ['block_request', 'log_event'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return policies[policyName] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评估策略条件
|
||||||
|
*/
|
||||||
|
private async evaluatePolicyConditions(
|
||||||
|
policy: SecurityPolicy,
|
||||||
|
context: SecurityContext,
|
||||||
|
): Promise<PolicyResult> {
|
||||||
|
for (const condition of policy.conditions) {
|
||||||
|
const result = await this.evaluateCondition(condition, context);
|
||||||
|
if (!result.allowed) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: true, reason: 'All conditions passed', actions: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评估单个条件
|
||||||
|
*/
|
||||||
|
private async evaluateCondition(
|
||||||
|
condition: PolicyCondition,
|
||||||
|
context: SecurityContext,
|
||||||
|
): Promise<PolicyResult> {
|
||||||
|
switch (condition.type) {
|
||||||
|
case 'request_count':
|
||||||
|
return this.evaluateRequestCountCondition(condition, context);
|
||||||
|
case 'geo_location':
|
||||||
|
return this.evaluateGeoLocationCondition(condition, context);
|
||||||
|
default:
|
||||||
|
return { allowed: true, reason: 'Unknown condition type', actions: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评估请求计数条件
|
||||||
|
*/
|
||||||
|
private evaluateRequestCountCondition(
|
||||||
|
condition: PolicyCondition,
|
||||||
|
context: SecurityContext,
|
||||||
|
): PolicyResult {
|
||||||
|
const attempts = this.accessAttempts.get(context.clientIp) || [];
|
||||||
|
const recentAttempts = attempts.filter(
|
||||||
|
(attempt) => Date.now() - attempt.timestamp < condition.timeWindow!,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (recentAttempts.length > condition.threshold!) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
reason: `Request count exceeded: ${recentAttempts.length}/${condition.threshold}`,
|
||||||
|
actions: ['rate_limit'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed: true,
|
||||||
|
reason: 'Request count within limits',
|
||||||
|
actions: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评估地理位置条件
|
||||||
|
*/
|
||||||
|
private evaluateGeoLocationCondition(
|
||||||
|
condition: PolicyCondition,
|
||||||
|
context: SecurityContext,
|
||||||
|
): PolicyResult {
|
||||||
|
// 模拟地理位置检查
|
||||||
|
const userCountry = 'CN'; // 应该从 IP 地理位置服务获取
|
||||||
|
|
||||||
|
if (!condition.allowedCountries!.includes(userCountry)) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
reason: `Access from restricted country: ${userCountry}`,
|
||||||
|
actions: ['geo_block'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed: true,
|
||||||
|
reason: 'Geographic location allowed',
|
||||||
|
actions: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行策略动作
|
||||||
|
*/
|
||||||
|
private async executePolicyActions(
|
||||||
|
actions: string[],
|
||||||
|
context: SecurityContext,
|
||||||
|
): Promise<void> {
|
||||||
|
for (const action of actions) {
|
||||||
|
switch (action) {
|
||||||
|
case 'block_ip':
|
||||||
|
this.blockedIps.add(context.clientIp);
|
||||||
|
break;
|
||||||
|
case 'log_event':
|
||||||
|
await this.logSecurityEvent('POLICY_VIOLATION', context);
|
||||||
|
break;
|
||||||
|
case 'block_request':
|
||||||
|
// 请求已被阻止,无需额外操作
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录安全事件
|
||||||
|
*/
|
||||||
|
private async logSecurityEvent(eventType: string, data: any): Promise<void> {
|
||||||
|
this.logger.warn(`Security event: ${eventType}`, data);
|
||||||
|
// 这里应该将事件记录到安全日志系统
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测访问异常
|
||||||
|
*/
|
||||||
|
private detectAccessAnomalies(
|
||||||
|
ip: string,
|
||||||
|
attempts: AccessAttempt[],
|
||||||
|
): AccessAnomaly[] {
|
||||||
|
const anomalies: AccessAnomaly[] = [];
|
||||||
|
|
||||||
|
// 检查访问频率异常
|
||||||
|
const recentAttempts = attempts.filter(
|
||||||
|
(attempt) => Date.now() - attempt.timestamp < 3600000, // 1小时内
|
||||||
|
);
|
||||||
|
|
||||||
|
if (recentAttempts.length > 1000) {
|
||||||
|
anomalies.push({
|
||||||
|
type: 'high_frequency',
|
||||||
|
ip,
|
||||||
|
description: `Unusually high access frequency: ${recentAttempts.length} requests in 1 hour`,
|
||||||
|
severity: 'high',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return anomalies;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取访问来源统计
|
||||||
|
*/
|
||||||
|
private getTopAccessSources(): AccessSource[] {
|
||||||
|
const sources: AccessSource[] = [];
|
||||||
|
|
||||||
|
for (const [ip, attempts] of this.accessAttempts.entries()) {
|
||||||
|
sources.push({
|
||||||
|
ip,
|
||||||
|
requestCount: attempts.length,
|
||||||
|
lastAccess: Math.max(...attempts.map((a) => a.timestamp)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return sources.sort((a, b) => b.requestCount - a.requestCount).slice(0, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类型定义
|
||||||
|
export interface AccessAttempt {
|
||||||
|
timestamp: number;
|
||||||
|
method: string;
|
||||||
|
url: string;
|
||||||
|
userAgent: string;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SuspiciousActivity {
|
||||||
|
timestamp: number;
|
||||||
|
type: string;
|
||||||
|
details: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserPermissions {
|
||||||
|
userId: string;
|
||||||
|
roles: string[];
|
||||||
|
permissions: ResourcePermission[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourcePermission {
|
||||||
|
resource: string;
|
||||||
|
actions: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecurityPolicy {
|
||||||
|
name: string;
|
||||||
|
conditions: PolicyCondition[];
|
||||||
|
denyActions: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PolicyCondition {
|
||||||
|
type: string;
|
||||||
|
threshold?: number;
|
||||||
|
timeWindow?: number;
|
||||||
|
allowedCountries?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecurityContext {
|
||||||
|
clientIp: string;
|
||||||
|
userId?: string;
|
||||||
|
userAgent?: string;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PolicyResult {
|
||||||
|
allowed: boolean;
|
||||||
|
reason: string;
|
||||||
|
actions: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccessPatternAnalysis {
|
||||||
|
timestamp: number;
|
||||||
|
totalAccesses: number;
|
||||||
|
uniqueIps: number;
|
||||||
|
suspiciousActivities: number;
|
||||||
|
blockedAttempts: number;
|
||||||
|
topSources: AccessSource[];
|
||||||
|
anomalies: AccessAnomaly[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccessSource {
|
||||||
|
ip: string;
|
||||||
|
requestCount: number;
|
||||||
|
lastAccess: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccessAnomaly {
|
||||||
|
type: string;
|
||||||
|
ip: string;
|
||||||
|
description: string;
|
||||||
|
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
42
wwjcloud-nest-v1/libs/wwjcloud-ai/src/safe/safe.module.ts
Normal file
42
wwjcloud-nest-v1/libs/wwjcloud-ai/src/safe/safe.module.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { SecurityAnalyzer } from './analyzers/security.analyzer';
|
||||||
|
import { VulnerabilityDetector } from './detectors/vulnerability.detector';
|
||||||
|
import { AccessProtector } from './protectors/access.protector';
|
||||||
|
import { AiSecurityService } from './services/ai-security.service';
|
||||||
|
import { AiAuditService } from './services/ai-audit.service';
|
||||||
|
import { SafeReadyService } from './services/safe-ready.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI Safe Module - AI 安全模块
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* - 安全威胁检测和分析
|
||||||
|
* - 访问控制和权限管理
|
||||||
|
* - 安全审计和日志记录
|
||||||
|
* - 漏洞扫描和防护
|
||||||
|
*/
|
||||||
|
@Module({
|
||||||
|
providers: [
|
||||||
|
// 分析器
|
||||||
|
SecurityAnalyzer,
|
||||||
|
|
||||||
|
// 检测器
|
||||||
|
VulnerabilityDetector,
|
||||||
|
|
||||||
|
// 保护器
|
||||||
|
AccessProtector,
|
||||||
|
|
||||||
|
// 服务
|
||||||
|
AiSecurityService,
|
||||||
|
AiAuditService,
|
||||||
|
SafeReadyService,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
SecurityAnalyzer,
|
||||||
|
VulnerabilityDetector,
|
||||||
|
AccessProtector,
|
||||||
|
AiSecurityService,
|
||||||
|
AiAuditService,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AiSafeModule {}
|
||||||
@@ -0,0 +1,799 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI Audit Service - AI 审计服务
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* - 记录和跟踪安全事件
|
||||||
|
* - 生成审计报告
|
||||||
|
* - 合规性检查
|
||||||
|
* - 安全日志管理
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AiAuditService {
|
||||||
|
private readonly logger = new Logger(AiAuditService.name);
|
||||||
|
private readonly auditLogs: AuditLog[] = [];
|
||||||
|
private readonly complianceRules: ComplianceRule[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.initializeComplianceRules();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录审计事件
|
||||||
|
*/
|
||||||
|
async logAuditEvent(event: AuditEvent): Promise<void> {
|
||||||
|
this.logger.debug(`Logging audit event: ${event.type}`);
|
||||||
|
|
||||||
|
const auditLog: AuditLog = {
|
||||||
|
id: this.generateAuditId(),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
event,
|
||||||
|
severity: this.determineSeverity(event),
|
||||||
|
category: this.categorizeEvent(event),
|
||||||
|
compliance: await this.checkCompliance(event),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.auditLogs.push(auditLog);
|
||||||
|
|
||||||
|
// 保持最近 10000 条审计日志
|
||||||
|
if (this.auditLogs.length > 10000) {
|
||||||
|
this.auditLogs.splice(0, this.auditLogs.length - 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是高严重性事件,立即处理
|
||||||
|
if (auditLog.severity === 'high' || auditLog.severity === 'critical') {
|
||||||
|
await this.handleHighSeverityEvent(auditLog);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成审计报告
|
||||||
|
*/
|
||||||
|
async generateAuditReport(options: AuditReportOptions): Promise<AuditReport> {
|
||||||
|
this.logger.log('Generating audit report');
|
||||||
|
|
||||||
|
const startTime =
|
||||||
|
options.startTime || Date.now() - 30 * 24 * 60 * 60 * 1000; // 默认30天
|
||||||
|
const endTime = options.endTime || Date.now();
|
||||||
|
|
||||||
|
// 过滤指定时间范围内的日志
|
||||||
|
const filteredLogs = this.auditLogs.filter(
|
||||||
|
(log) => log.timestamp >= startTime && log.timestamp <= endTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 按类型分组统计
|
||||||
|
const eventsByType = this.groupEventsByType(filteredLogs);
|
||||||
|
const eventsBySeverity = this.groupEventsBySeverity(filteredLogs);
|
||||||
|
const eventsByCategory = this.groupEventsByCategory(filteredLogs);
|
||||||
|
|
||||||
|
// 合规性分析
|
||||||
|
const complianceAnalysis = await this.analyzeCompliance(filteredLogs);
|
||||||
|
|
||||||
|
// 安全趋势分析
|
||||||
|
const securityTrends = this.analyzeSecurityTrends(filteredLogs);
|
||||||
|
|
||||||
|
// 异常检测
|
||||||
|
const anomalies = this.detectAnomalies(filteredLogs);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: this.generateReportId(),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
period: { startTime, endTime },
|
||||||
|
summary: {
|
||||||
|
totalEvents: filteredLogs.length,
|
||||||
|
criticalEvents: eventsBySeverity.critical || 0,
|
||||||
|
highSeverityEvents: eventsBySeverity.high || 0,
|
||||||
|
complianceScore: complianceAnalysis.overallScore,
|
||||||
|
},
|
||||||
|
statistics: {
|
||||||
|
eventsByType,
|
||||||
|
eventsBySeverity,
|
||||||
|
eventsByCategory,
|
||||||
|
},
|
||||||
|
complianceAnalysis,
|
||||||
|
securityTrends,
|
||||||
|
anomalies,
|
||||||
|
recommendations: this.generateRecommendations(
|
||||||
|
filteredLogs,
|
||||||
|
complianceAnalysis,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行合规性检查
|
||||||
|
*/
|
||||||
|
async performComplianceCheck(): Promise<ComplianceCheckResult> {
|
||||||
|
this.logger.log('Performing compliance check');
|
||||||
|
|
||||||
|
const results: ComplianceRuleResult[] = [];
|
||||||
|
|
||||||
|
for (const rule of this.complianceRules) {
|
||||||
|
const result = await this.evaluateComplianceRule(rule);
|
||||||
|
results.push(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
const overallScore = this.calculateComplianceScore(results);
|
||||||
|
const violations = results.filter((r) => !r.compliant);
|
||||||
|
|
||||||
|
return {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
overallScore,
|
||||||
|
status: this.determineComplianceStatus(overallScore),
|
||||||
|
results,
|
||||||
|
violations,
|
||||||
|
recommendations: this.generateComplianceRecommendations(violations),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索审计日志
|
||||||
|
*/
|
||||||
|
async searchAuditLogs(
|
||||||
|
criteria: AuditSearchCriteria,
|
||||||
|
): Promise<AuditSearchResult> {
|
||||||
|
this.logger.debug('Searching audit logs', criteria);
|
||||||
|
|
||||||
|
let filteredLogs = [...this.auditLogs];
|
||||||
|
|
||||||
|
// 按时间范围过滤
|
||||||
|
if (criteria.startTime) {
|
||||||
|
filteredLogs = filteredLogs.filter(
|
||||||
|
(log) => log.timestamp >= criteria.startTime!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (criteria.endTime) {
|
||||||
|
filteredLogs = filteredLogs.filter(
|
||||||
|
(log) => log.timestamp <= criteria.endTime!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按事件类型过滤
|
||||||
|
if (criteria.eventType) {
|
||||||
|
filteredLogs = filteredLogs.filter(
|
||||||
|
(log) => log.event.type === criteria.eventType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按严重性过滤
|
||||||
|
if (criteria.severity) {
|
||||||
|
filteredLogs = filteredLogs.filter(
|
||||||
|
(log) => log.severity === criteria.severity,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按用户过滤
|
||||||
|
if (criteria.userId) {
|
||||||
|
filteredLogs = filteredLogs.filter(
|
||||||
|
(log) => log.event.userId === criteria.userId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按关键词搜索
|
||||||
|
if (criteria.keyword) {
|
||||||
|
const keyword = criteria.keyword.toLowerCase();
|
||||||
|
filteredLogs = filteredLogs.filter(
|
||||||
|
(log) =>
|
||||||
|
log.event.description.toLowerCase().includes(keyword) ||
|
||||||
|
log.event.type.toLowerCase().includes(keyword),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const total = filteredLogs.length;
|
||||||
|
const page = criteria.page || 1;
|
||||||
|
const limit = criteria.limit || 50;
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
const paginatedLogs = filteredLogs.slice(offset, offset + limit);
|
||||||
|
|
||||||
|
return {
|
||||||
|
logs: paginatedLogs,
|
||||||
|
pagination: {
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出审计数据
|
||||||
|
*/
|
||||||
|
async exportAuditData(
|
||||||
|
format: 'json' | 'csv' | 'xml',
|
||||||
|
options: ExportOptions,
|
||||||
|
): Promise<string> {
|
||||||
|
this.logger.log(`Exporting audit data in ${format} format`);
|
||||||
|
|
||||||
|
const startTime =
|
||||||
|
options.startTime || Date.now() - 30 * 24 * 60 * 60 * 1000;
|
||||||
|
const endTime = options.endTime || Date.now();
|
||||||
|
|
||||||
|
const filteredLogs = this.auditLogs.filter(
|
||||||
|
(log) => log.timestamp >= startTime && log.timestamp <= endTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (format) {
|
||||||
|
case 'json':
|
||||||
|
return JSON.stringify(filteredLogs, null, 2);
|
||||||
|
case 'csv':
|
||||||
|
return this.convertToCsv(filteredLogs);
|
||||||
|
case 'xml':
|
||||||
|
return this.convertToXml(filteredLogs);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported export format: ${format}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化合规性规则
|
||||||
|
*/
|
||||||
|
private initializeComplianceRules(): void {
|
||||||
|
this.complianceRules.push(
|
||||||
|
{
|
||||||
|
id: 'GDPR_DATA_ACCESS',
|
||||||
|
name: 'GDPR Data Access Logging',
|
||||||
|
description: 'All personal data access must be logged',
|
||||||
|
category: 'data_protection',
|
||||||
|
severity: 'high',
|
||||||
|
evaluator: this.evaluateDataAccessLogging.bind(this),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'SOX_FINANCIAL_ACCESS',
|
||||||
|
name: 'SOX Financial Data Access Control',
|
||||||
|
description:
|
||||||
|
'Financial data access must be properly controlled and audited',
|
||||||
|
category: 'financial_compliance',
|
||||||
|
severity: 'critical',
|
||||||
|
evaluator: this.evaluateFinancialAccessControl.bind(this),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ISO27001_INCIDENT_RESPONSE',
|
||||||
|
name: 'ISO 27001 Incident Response',
|
||||||
|
description:
|
||||||
|
'Security incidents must be properly documented and responded to',
|
||||||
|
category: 'security_management',
|
||||||
|
severity: 'high',
|
||||||
|
evaluator: this.evaluateIncidentResponse.bind(this),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成审计ID
|
||||||
|
*/
|
||||||
|
private generateAuditId(): string {
|
||||||
|
return `AUDIT_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成报告ID
|
||||||
|
*/
|
||||||
|
private generateReportId(): string {
|
||||||
|
return `REPORT_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确定事件严重性
|
||||||
|
*/
|
||||||
|
private determineSeverity(
|
||||||
|
event: AuditEvent,
|
||||||
|
): 'low' | 'medium' | 'high' | 'critical' {
|
||||||
|
const severityMap: Record<string, 'low' | 'medium' | 'high' | 'critical'> =
|
||||||
|
{
|
||||||
|
LOGIN_SUCCESS: 'low',
|
||||||
|
LOGIN_FAILURE: 'medium',
|
||||||
|
PERMISSION_DENIED: 'medium',
|
||||||
|
DATA_ACCESS: 'medium',
|
||||||
|
DATA_MODIFICATION: 'high',
|
||||||
|
SECURITY_VIOLATION: 'high',
|
||||||
|
SYSTEM_BREACH: 'critical',
|
||||||
|
DATA_BREACH: 'critical',
|
||||||
|
};
|
||||||
|
|
||||||
|
return severityMap[event.type] || 'medium';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 事件分类
|
||||||
|
*/
|
||||||
|
private categorizeEvent(event: AuditEvent): string {
|
||||||
|
const categoryMap: Record<string, string> = {
|
||||||
|
LOGIN_SUCCESS: 'authentication',
|
||||||
|
LOGIN_FAILURE: 'authentication',
|
||||||
|
PERMISSION_DENIED: 'authorization',
|
||||||
|
DATA_ACCESS: 'data_access',
|
||||||
|
DATA_MODIFICATION: 'data_modification',
|
||||||
|
SECURITY_VIOLATION: 'security',
|
||||||
|
SYSTEM_BREACH: 'security',
|
||||||
|
DATA_BREACH: 'security',
|
||||||
|
};
|
||||||
|
|
||||||
|
return categoryMap[event.type] || 'general';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查合规性
|
||||||
|
*/
|
||||||
|
private async checkCompliance(event: AuditEvent): Promise<ComplianceStatus> {
|
||||||
|
// 简化的合规性检查
|
||||||
|
const requiredFields = ['userId', 'timestamp', 'type', 'description'];
|
||||||
|
const hasRequiredFields = requiredFields.every(
|
||||||
|
(field) => event[field as keyof AuditEvent],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
compliant: hasRequiredFields,
|
||||||
|
violations: hasRequiredFields ? [] : ['Missing required fields'],
|
||||||
|
rules: ['BASIC_AUDIT_REQUIREMENTS'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理高严重性事件
|
||||||
|
*/
|
||||||
|
private async handleHighSeverityEvent(auditLog: AuditLog): Promise<void> {
|
||||||
|
this.logger.warn(`High severity audit event detected`, auditLog);
|
||||||
|
|
||||||
|
// 这里应该实现高严重性事件的处理逻辑
|
||||||
|
// 例如:发送告警、通知管理员、触发自动响应等
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按类型分组事件
|
||||||
|
*/
|
||||||
|
private groupEventsByType(logs: AuditLog[]): Record<string, number> {
|
||||||
|
const groups: Record<string, number> = {};
|
||||||
|
|
||||||
|
logs.forEach((log) => {
|
||||||
|
groups[log.event.type] = (groups[log.event.type] || 0) + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按严重性分组事件
|
||||||
|
*/
|
||||||
|
private groupEventsBySeverity(logs: AuditLog[]): Record<string, number> {
|
||||||
|
const groups: Record<string, number> = {};
|
||||||
|
|
||||||
|
logs.forEach((log) => {
|
||||||
|
groups[log.severity] = (groups[log.severity] || 0) + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按类别分组事件
|
||||||
|
*/
|
||||||
|
private groupEventsByCategory(logs: AuditLog[]): Record<string, number> {
|
||||||
|
const groups: Record<string, number> = {};
|
||||||
|
|
||||||
|
logs.forEach((log) => {
|
||||||
|
groups[log.category] = (groups[log.category] || 0) + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析合规性
|
||||||
|
*/
|
||||||
|
private async analyzeCompliance(
|
||||||
|
logs: AuditLog[],
|
||||||
|
): Promise<ComplianceAnalysis> {
|
||||||
|
const compliantLogs = logs.filter((log) => log.compliance.compliant);
|
||||||
|
const violationLogs = logs.filter((log) => !log.compliance.compliant);
|
||||||
|
|
||||||
|
const overallScore =
|
||||||
|
logs.length > 0 ? (compliantLogs.length / logs.length) * 100 : 100;
|
||||||
|
|
||||||
|
return {
|
||||||
|
overallScore: Math.round(overallScore),
|
||||||
|
compliantEvents: compliantLogs.length,
|
||||||
|
violationEvents: violationLogs.length,
|
||||||
|
violationsByRule: this.groupViolationsByRule(violationLogs),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按规则分组违规
|
||||||
|
*/
|
||||||
|
private groupViolationsByRule(logs: AuditLog[]): Record<string, number> {
|
||||||
|
const groups: Record<string, number> = {};
|
||||||
|
|
||||||
|
logs.forEach((log) => {
|
||||||
|
log.compliance.violations.forEach((violation) => {
|
||||||
|
groups[violation] = (groups[violation] || 0) + 1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析安全趋势
|
||||||
|
*/
|
||||||
|
private analyzeSecurityTrends(logs: AuditLog[]): SecurityTrend[] {
|
||||||
|
// 简化的趋势分析
|
||||||
|
const dailyStats = this.groupLogsByDay(logs);
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
metric: 'daily_events',
|
||||||
|
values: Object.entries(dailyStats).map(([date, count]) => ({
|
||||||
|
timestamp: new Date(date).getTime(),
|
||||||
|
value: count,
|
||||||
|
})),
|
||||||
|
trend: 'stable',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按天分组日志
|
||||||
|
*/
|
||||||
|
private groupLogsByDay(logs: AuditLog[]): Record<string, number> {
|
||||||
|
const groups: Record<string, number> = {};
|
||||||
|
|
||||||
|
logs.forEach((log) => {
|
||||||
|
const date = new Date(log.timestamp).toISOString().split('T')[0];
|
||||||
|
groups[date] = (groups[date] || 0) + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测异常
|
||||||
|
*/
|
||||||
|
private detectAnomalies(logs: AuditLog[]): AuditAnomaly[] {
|
||||||
|
const anomalies: AuditAnomaly[] = [];
|
||||||
|
|
||||||
|
// 检测异常高频事件
|
||||||
|
const eventCounts = this.groupEventsByType(logs);
|
||||||
|
const avgCount =
|
||||||
|
Object.values(eventCounts).reduce((sum, count) => sum + count, 0) /
|
||||||
|
Object.keys(eventCounts).length;
|
||||||
|
|
||||||
|
Object.entries(eventCounts).forEach(([type, count]) => {
|
||||||
|
if (count > avgCount * 3) {
|
||||||
|
// 超过平均值3倍
|
||||||
|
anomalies.push({
|
||||||
|
type: 'high_frequency_event',
|
||||||
|
description: `Unusually high frequency of ${type} events: ${count}`,
|
||||||
|
severity: 'medium',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
metadata: { eventType: type, count },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return anomalies;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成建议
|
||||||
|
*/
|
||||||
|
private generateRecommendations(
|
||||||
|
logs: AuditLog[],
|
||||||
|
complianceAnalysis: ComplianceAnalysis,
|
||||||
|
): string[] {
|
||||||
|
const recommendations: string[] = [];
|
||||||
|
|
||||||
|
if (complianceAnalysis.overallScore < 90) {
|
||||||
|
recommendations.push(
|
||||||
|
'Improve compliance by addressing identified violations',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (complianceAnalysis.violationEvents > 0) {
|
||||||
|
recommendations.push(
|
||||||
|
'Review and update security policies to prevent violations',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const criticalEvents = logs.filter(
|
||||||
|
(log) => log.severity === 'critical',
|
||||||
|
).length;
|
||||||
|
if (criticalEvents > 0) {
|
||||||
|
recommendations.push(
|
||||||
|
'Investigate and address critical security events immediately',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return recommendations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评估合规性规则
|
||||||
|
*/
|
||||||
|
private async evaluateComplianceRule(
|
||||||
|
rule: ComplianceRule,
|
||||||
|
): Promise<ComplianceRuleResult> {
|
||||||
|
try {
|
||||||
|
const result = await rule.evaluator();
|
||||||
|
return {
|
||||||
|
ruleId: rule.id,
|
||||||
|
ruleName: rule.name,
|
||||||
|
compliant: result.compliant,
|
||||||
|
score: result.score || (result.compliant ? 100 : 0),
|
||||||
|
violations: result.violations || [],
|
||||||
|
recommendations: result.recommendations || [],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to evaluate compliance rule ${rule.id}`, error);
|
||||||
|
return {
|
||||||
|
ruleId: rule.id,
|
||||||
|
ruleName: rule.name,
|
||||||
|
compliant: false,
|
||||||
|
score: 0,
|
||||||
|
violations: ['Rule evaluation failed'],
|
||||||
|
recommendations: ['Review rule implementation'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算合规性评分
|
||||||
|
*/
|
||||||
|
private calculateComplianceScore(results: ComplianceRuleResult[]): number {
|
||||||
|
if (results.length === 0) return 100;
|
||||||
|
|
||||||
|
const totalScore = results.reduce((sum, result) => sum + result.score, 0);
|
||||||
|
return Math.round(totalScore / results.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确定合规性状态
|
||||||
|
*/
|
||||||
|
private determineComplianceStatus(
|
||||||
|
score: number,
|
||||||
|
): 'compliant' | 'warning' | 'non_compliant' {
|
||||||
|
if (score >= 90) return 'compliant';
|
||||||
|
if (score >= 70) return 'warning';
|
||||||
|
return 'non_compliant';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成合规性建议
|
||||||
|
*/
|
||||||
|
private generateComplianceRecommendations(
|
||||||
|
violations: ComplianceRuleResult[],
|
||||||
|
): string[] {
|
||||||
|
const recommendations: string[] = [];
|
||||||
|
|
||||||
|
violations.forEach((violation) => {
|
||||||
|
recommendations.push(...violation.recommendations);
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...new Set(recommendations)]; // 去重
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为CSV格式
|
||||||
|
*/
|
||||||
|
private convertToCsv(logs: AuditLog[]): string {
|
||||||
|
const headers = [
|
||||||
|
'ID',
|
||||||
|
'Timestamp',
|
||||||
|
'Event Type',
|
||||||
|
'User ID',
|
||||||
|
'Description',
|
||||||
|
'Severity',
|
||||||
|
'Category',
|
||||||
|
];
|
||||||
|
const rows = logs.map((log) => [
|
||||||
|
log.id,
|
||||||
|
new Date(log.timestamp).toISOString(),
|
||||||
|
log.event.type,
|
||||||
|
log.event.userId || '',
|
||||||
|
log.event.description,
|
||||||
|
log.severity,
|
||||||
|
log.category,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [headers, ...rows].map((row) => row.join(',')).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为XML格式
|
||||||
|
*/
|
||||||
|
private convertToXml(logs: AuditLog[]): string {
|
||||||
|
const xmlLogs = logs
|
||||||
|
.map(
|
||||||
|
(log) => `
|
||||||
|
<log>
|
||||||
|
<id>${log.id}</id>
|
||||||
|
<timestamp>${new Date(log.timestamp).toISOString()}</timestamp>
|
||||||
|
<event>
|
||||||
|
<type>${log.event.type}</type>
|
||||||
|
<userId>${log.event.userId || ''}</userId>
|
||||||
|
<description>${log.event.description}</description>
|
||||||
|
</event>
|
||||||
|
<severity>${log.severity}</severity>
|
||||||
|
<category>${log.category}</category>
|
||||||
|
</log>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
return `<?xml version="1.0" encoding="UTF-8"?><auditLogs>${xmlLogs}</auditLogs>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评估数据访问日志记录
|
||||||
|
*/
|
||||||
|
private async evaluateDataAccessLogging(): Promise<ComplianceEvaluationResult> {
|
||||||
|
// 模拟评估逻辑
|
||||||
|
return {
|
||||||
|
compliant: true,
|
||||||
|
score: 95,
|
||||||
|
violations: [],
|
||||||
|
recommendations: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评估财务访问控制
|
||||||
|
*/
|
||||||
|
private async evaluateFinancialAccessControl(): Promise<ComplianceEvaluationResult> {
|
||||||
|
// 模拟评估逻辑
|
||||||
|
return {
|
||||||
|
compliant: true,
|
||||||
|
score: 90,
|
||||||
|
violations: [],
|
||||||
|
recommendations: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评估事件响应
|
||||||
|
*/
|
||||||
|
private async evaluateIncidentResponse(): Promise<ComplianceEvaluationResult> {
|
||||||
|
// 模拟评估逻辑
|
||||||
|
return {
|
||||||
|
compliant: false,
|
||||||
|
score: 70,
|
||||||
|
violations: ['Incident response time exceeds policy requirements'],
|
||||||
|
recommendations: ['Implement automated incident response procedures'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类型定义
|
||||||
|
export interface AuditEvent {
|
||||||
|
type: string;
|
||||||
|
userId?: string;
|
||||||
|
timestamp: number;
|
||||||
|
description: string;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuditLog {
|
||||||
|
id: string;
|
||||||
|
timestamp: number;
|
||||||
|
event: AuditEvent;
|
||||||
|
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
category: string;
|
||||||
|
compliance: ComplianceStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComplianceStatus {
|
||||||
|
compliant: boolean;
|
||||||
|
violations: string[];
|
||||||
|
rules: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuditReportOptions {
|
||||||
|
startTime?: number;
|
||||||
|
endTime?: number;
|
||||||
|
includeDetails?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuditReport {
|
||||||
|
id: string;
|
||||||
|
timestamp: number;
|
||||||
|
period: { startTime: number; endTime: number };
|
||||||
|
summary: {
|
||||||
|
totalEvents: number;
|
||||||
|
criticalEvents: number;
|
||||||
|
highSeverityEvents: number;
|
||||||
|
complianceScore: number;
|
||||||
|
};
|
||||||
|
statistics: {
|
||||||
|
eventsByType: Record<string, number>;
|
||||||
|
eventsBySeverity: Record<string, number>;
|
||||||
|
eventsByCategory: Record<string, number>;
|
||||||
|
};
|
||||||
|
complianceAnalysis: ComplianceAnalysis;
|
||||||
|
securityTrends: SecurityTrend[];
|
||||||
|
anomalies: AuditAnomaly[];
|
||||||
|
recommendations: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComplianceAnalysis {
|
||||||
|
overallScore: number;
|
||||||
|
compliantEvents: number;
|
||||||
|
violationEvents: number;
|
||||||
|
violationsByRule: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecurityTrend {
|
||||||
|
metric: string;
|
||||||
|
values: { timestamp: number; value: number }[];
|
||||||
|
trend: 'increasing' | 'decreasing' | 'stable';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuditAnomaly {
|
||||||
|
type: string;
|
||||||
|
description: string;
|
||||||
|
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
timestamp: number;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComplianceRule {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
category: string;
|
||||||
|
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
evaluator: () => Promise<ComplianceEvaluationResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComplianceEvaluationResult {
|
||||||
|
compliant: boolean;
|
||||||
|
score?: number;
|
||||||
|
violations?: string[];
|
||||||
|
recommendations?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComplianceCheckResult {
|
||||||
|
timestamp: number;
|
||||||
|
overallScore: number;
|
||||||
|
status: 'compliant' | 'warning' | 'non_compliant';
|
||||||
|
results: ComplianceRuleResult[];
|
||||||
|
violations: ComplianceRuleResult[];
|
||||||
|
recommendations: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComplianceRuleResult {
|
||||||
|
ruleId: string;
|
||||||
|
ruleName: string;
|
||||||
|
compliant: boolean;
|
||||||
|
score: number;
|
||||||
|
violations: string[];
|
||||||
|
recommendations: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuditSearchCriteria {
|
||||||
|
startTime?: number;
|
||||||
|
endTime?: number;
|
||||||
|
eventType?: string;
|
||||||
|
severity?: 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
userId?: string;
|
||||||
|
keyword?: string;
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuditSearchResult {
|
||||||
|
logs: AuditLog[];
|
||||||
|
pagination: {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExportOptions {
|
||||||
|
startTime?: number;
|
||||||
|
endTime?: number;
|
||||||
|
includeMetadata?: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,554 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { SecurityAnalyzer } from '../analyzers/security.analyzer';
|
||||||
|
import { VulnerabilityDetector } from '../detectors/vulnerability.detector';
|
||||||
|
import { AccessProtector } from '../protectors/access.protector';
|
||||||
|
import { AiAuditService } from './ai-audit.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI Security Service - AI 安全服务
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* - 统一安全管理接口
|
||||||
|
* - 协调各安全组件
|
||||||
|
* - 提供安全决策支持
|
||||||
|
* - 管理安全策略执行
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AiSecurityService {
|
||||||
|
private readonly logger = new Logger(AiSecurityService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly securityAnalyzer: SecurityAnalyzer,
|
||||||
|
private readonly vulnerabilityDetector: VulnerabilityDetector,
|
||||||
|
private readonly accessProtector: AccessProtector,
|
||||||
|
private readonly auditService: AiAuditService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行全面安全评估
|
||||||
|
*/
|
||||||
|
async performSecurityAssessment(): Promise<SecurityAssessmentResult> {
|
||||||
|
this.logger.log('Starting comprehensive security assessment');
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 并行执行各项安全检查
|
||||||
|
const [
|
||||||
|
securityAnalysis,
|
||||||
|
vulnerabilityScan,
|
||||||
|
accessPatternAnalysis,
|
||||||
|
threatDetection,
|
||||||
|
] = await Promise.all([
|
||||||
|
this.securityAnalyzer.analyzeSystemSecurity(),
|
||||||
|
this.vulnerabilityDetector.scanVulnerabilities(),
|
||||||
|
this.accessProtector.monitorAccessPatterns(),
|
||||||
|
this.vulnerabilityDetector.detectRealTimeThreats(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 计算综合安全评分
|
||||||
|
const overallScore = this.calculateOverallSecurityScore({
|
||||||
|
securityAnalysis,
|
||||||
|
vulnerabilityScan,
|
||||||
|
accessPatternAnalysis,
|
||||||
|
threatDetection,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 生成安全建议
|
||||||
|
const recommendations = this.generateSecurityRecommendations({
|
||||||
|
securityAnalysis,
|
||||||
|
vulnerabilityScan,
|
||||||
|
threatDetection,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
duration: Date.now() - startTime,
|
||||||
|
overallScore,
|
||||||
|
riskLevel: this.determineRiskLevel(overallScore),
|
||||||
|
components: {
|
||||||
|
securityAnalysis,
|
||||||
|
vulnerabilityScan,
|
||||||
|
accessPatternAnalysis,
|
||||||
|
threatDetection,
|
||||||
|
},
|
||||||
|
recommendations,
|
||||||
|
nextAssessmentTime: Date.now() + 24 * 60 * 60 * 1000, // 24小时后
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Security assessment failed', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证安全策略
|
||||||
|
*/
|
||||||
|
async validateSecurityPolicy(
|
||||||
|
policyName: string,
|
||||||
|
context: any,
|
||||||
|
): Promise<PolicyValidationResult> {
|
||||||
|
this.logger.debug(`Validating security policy: ${policyName}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.accessProtector.enforceSecurityPolicy(
|
||||||
|
policyName,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
policyName,
|
||||||
|
valid: result.allowed,
|
||||||
|
reason: result.reason,
|
||||||
|
actions: result.actions,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Policy validation failed for ${policyName}`, error);
|
||||||
|
return {
|
||||||
|
policyName,
|
||||||
|
valid: false,
|
||||||
|
reason: 'Policy validation error',
|
||||||
|
actions: [],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理安全事件
|
||||||
|
*/
|
||||||
|
async handleSecurityEvent(
|
||||||
|
event: SecurityEvent,
|
||||||
|
): Promise<SecurityEventResponse> {
|
||||||
|
this.logger.warn(`Handling security event: ${event.type}`, event);
|
||||||
|
|
||||||
|
const response: SecurityEventResponse = {
|
||||||
|
eventId: event.id,
|
||||||
|
handled: false,
|
||||||
|
actions: [],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (event.severity) {
|
||||||
|
case 'critical':
|
||||||
|
response.actions = await this.handleCriticalSecurityEvent(event);
|
||||||
|
break;
|
||||||
|
case 'high':
|
||||||
|
response.actions = await this.handleHighSecurityEvent(event);
|
||||||
|
break;
|
||||||
|
case 'medium':
|
||||||
|
response.actions = await this.handleMediumSecurityEvent(event);
|
||||||
|
break;
|
||||||
|
case 'low':
|
||||||
|
response.actions = await this.handleLowSecurityEvent(event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.handled = true;
|
||||||
|
this.logger.log(`Security event ${event.id} handled successfully`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to handle security event ${event.id}`, error);
|
||||||
|
response.error = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取安全状态仪表板
|
||||||
|
*/
|
||||||
|
async getSecurityDashboard(): Promise<SecurityDashboard> {
|
||||||
|
this.logger.debug('Generating security dashboard');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [recentThreats, vulnerabilityStats, accessStats] =
|
||||||
|
await Promise.all([
|
||||||
|
this.getRecentThreats(),
|
||||||
|
this.getVulnerabilityStatistics(),
|
||||||
|
this.getAccessStatistics(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
status: this.determineOverallSecurityStatus(),
|
||||||
|
metrics: {
|
||||||
|
threatsDetected: recentThreats.length,
|
||||||
|
vulnerabilitiesFound: vulnerabilityStats.total,
|
||||||
|
accessViolations: accessStats.violations,
|
||||||
|
securityScore: await this.getCurrentSecurityScore(),
|
||||||
|
},
|
||||||
|
recentEvents: await this.getRecentSecurityEvents(),
|
||||||
|
alerts: await this.getActiveSecurityAlerts(),
|
||||||
|
trends: await this.getSecurityTrends(),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Failed to generate security dashboard', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算综合安全评分
|
||||||
|
*/
|
||||||
|
private calculateOverallSecurityScore(assessmentData: any): number {
|
||||||
|
const weights = {
|
||||||
|
securityAnalysis: 0.3,
|
||||||
|
vulnerabilityScan: 0.4,
|
||||||
|
accessPatternAnalysis: 0.2,
|
||||||
|
threatDetection: 0.1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let totalScore = 0;
|
||||||
|
let totalWeight = 0;
|
||||||
|
|
||||||
|
// 安全分析评分
|
||||||
|
if (assessmentData.securityAnalysis) {
|
||||||
|
const categories = assessmentData.securityAnalysis.categories as Record<
|
||||||
|
string,
|
||||||
|
{ score: number }
|
||||||
|
>;
|
||||||
|
const avgCategoryScore =
|
||||||
|
Object.values(categories).reduce(
|
||||||
|
(sum: number, category) => sum + category.score,
|
||||||
|
0,
|
||||||
|
) / 4;
|
||||||
|
totalScore += avgCategoryScore * weights.securityAnalysis;
|
||||||
|
totalWeight += weights.securityAnalysis;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 漏洞扫描评分
|
||||||
|
if (assessmentData.vulnerabilityScan) {
|
||||||
|
const vulnScore = this.calculateVulnerabilityScore(
|
||||||
|
assessmentData.vulnerabilityScan,
|
||||||
|
);
|
||||||
|
totalScore += vulnScore * weights.vulnerabilityScan;
|
||||||
|
totalWeight += weights.vulnerabilityScan;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 访问模式评分
|
||||||
|
if (assessmentData.accessPatternAnalysis) {
|
||||||
|
const accessScore = this.calculateAccessScore(
|
||||||
|
assessmentData.accessPatternAnalysis,
|
||||||
|
);
|
||||||
|
totalScore += accessScore * weights.accessPatternAnalysis;
|
||||||
|
totalWeight += weights.accessPatternAnalysis;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 威胁检测评分
|
||||||
|
if (assessmentData.threatDetection) {
|
||||||
|
const threatScore = this.calculateThreatScore(
|
||||||
|
assessmentData.threatDetection,
|
||||||
|
);
|
||||||
|
totalScore += threatScore * weights.threatDetection;
|
||||||
|
totalWeight += weights.threatDetection;
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalWeight > 0 ? Math.round(totalScore / totalWeight) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算漏洞评分
|
||||||
|
*/
|
||||||
|
private calculateVulnerabilityScore(vulnerabilityScan: any): number {
|
||||||
|
const { severityBreakdown } = vulnerabilityScan;
|
||||||
|
|
||||||
|
// 根据漏洞严重程度计算扣分
|
||||||
|
const penalties = {
|
||||||
|
critical: 25,
|
||||||
|
high: 15,
|
||||||
|
medium: 5,
|
||||||
|
low: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalPenalty =
|
||||||
|
severityBreakdown.critical * penalties.critical +
|
||||||
|
severityBreakdown.high * penalties.high +
|
||||||
|
severityBreakdown.medium * penalties.medium +
|
||||||
|
severityBreakdown.low * penalties.low;
|
||||||
|
|
||||||
|
return Math.max(0, 100 - totalPenalty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算访问评分
|
||||||
|
*/
|
||||||
|
private calculateAccessScore(accessAnalysis: any): number {
|
||||||
|
const baseScore = 100;
|
||||||
|
const { suspiciousActivities, blockedAttempts, anomalies } = accessAnalysis;
|
||||||
|
|
||||||
|
// 根据可疑活动扣分
|
||||||
|
const penalty =
|
||||||
|
suspiciousActivities * 2 + blockedAttempts * 5 + anomalies.length * 3;
|
||||||
|
|
||||||
|
return Math.max(0, baseScore - penalty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算威胁评分
|
||||||
|
*/
|
||||||
|
private calculateThreatScore(threatDetection: any): number {
|
||||||
|
const baseScore = 100;
|
||||||
|
const { threatsDetected, riskLevel } = threatDetection;
|
||||||
|
|
||||||
|
const riskPenalties = {
|
||||||
|
low: 1,
|
||||||
|
medium: 5,
|
||||||
|
high: 15,
|
||||||
|
critical: 30,
|
||||||
|
};
|
||||||
|
|
||||||
|
const penalty = threatsDetected * riskPenalties[riskLevel];
|
||||||
|
|
||||||
|
return Math.max(0, baseScore - penalty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确定风险等级
|
||||||
|
*/
|
||||||
|
private determineRiskLevel(
|
||||||
|
score: number,
|
||||||
|
): 'low' | 'medium' | 'high' | 'critical' {
|
||||||
|
if (score >= 90) return 'low';
|
||||||
|
if (score >= 70) return 'medium';
|
||||||
|
if (score >= 50) return 'high';
|
||||||
|
return 'critical';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成安全建议
|
||||||
|
*/
|
||||||
|
private generateSecurityRecommendations(assessmentData: any): string[] {
|
||||||
|
const recommendations: string[] = [];
|
||||||
|
|
||||||
|
// 基于安全分析的建议
|
||||||
|
if (assessmentData.securityAnalysis?.recommendations) {
|
||||||
|
recommendations.push(...assessmentData.securityAnalysis.recommendations);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基于漏洞扫描的建议
|
||||||
|
if (assessmentData.vulnerabilityScan?.recommendations) {
|
||||||
|
recommendations.push(...assessmentData.vulnerabilityScan.recommendations);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基于威胁检测的建议
|
||||||
|
if (assessmentData.threatDetection?.threatsDetected > 0) {
|
||||||
|
recommendations.push(
|
||||||
|
'Implement enhanced threat monitoring and response procedures',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...new Set(recommendations)]; // 去重
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理关键安全事件
|
||||||
|
*/
|
||||||
|
private async handleCriticalSecurityEvent(
|
||||||
|
event: SecurityEvent,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const actions = [
|
||||||
|
'immediate_alert',
|
||||||
|
'block_source',
|
||||||
|
'escalate_to_admin',
|
||||||
|
'create_incident',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 统一记录到审计服务
|
||||||
|
await this.auditService.logAuditEvent({
|
||||||
|
type: 'security_critical_event',
|
||||||
|
userId: event.metadata?.userId,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
description: `Critical security event: ${event.type}`,
|
||||||
|
metadata: { event },
|
||||||
|
});
|
||||||
|
|
||||||
|
// 执行紧急响应措施
|
||||||
|
await this.executeEmergencyResponse(event);
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理高级安全事件
|
||||||
|
*/
|
||||||
|
private async handleHighSecurityEvent(
|
||||||
|
event: SecurityEvent,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const actions = [
|
||||||
|
'alert_security_team',
|
||||||
|
'increase_monitoring',
|
||||||
|
'log_detailed_info',
|
||||||
|
];
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理中级安全事件
|
||||||
|
*/
|
||||||
|
private async handleMediumSecurityEvent(
|
||||||
|
event: SecurityEvent,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const actions = ['log_event', 'monitor_source', 'update_metrics'];
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理低级安全事件
|
||||||
|
*/
|
||||||
|
private async handleLowSecurityEvent(
|
||||||
|
event: SecurityEvent,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const actions = ['log_event', 'update_statistics'];
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行紧急响应
|
||||||
|
*/
|
||||||
|
private async executeEmergencyResponse(event: SecurityEvent): Promise<void> {
|
||||||
|
this.logger.error(`CRITICAL SECURITY EVENT: ${event.type}`, event);
|
||||||
|
|
||||||
|
// 这里应该实现紧急响应逻辑
|
||||||
|
// 例如:发送告警、阻止IP、通知管理员等
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最近威胁
|
||||||
|
*/
|
||||||
|
private async getRecentThreats(): Promise<any[]> {
|
||||||
|
// 模拟获取最近威胁数据
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取漏洞统计
|
||||||
|
*/
|
||||||
|
private async getVulnerabilityStatistics(): Promise<{ total: number }> {
|
||||||
|
// 模拟获取漏洞统计
|
||||||
|
return { total: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取访问统计
|
||||||
|
*/
|
||||||
|
private async getAccessStatistics(): Promise<{ violations: number }> {
|
||||||
|
// 模拟获取访问统计
|
||||||
|
return { violations: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前安全评分
|
||||||
|
*/
|
||||||
|
private async getCurrentSecurityScore(): Promise<number> {
|
||||||
|
// 模拟获取当前安全评分
|
||||||
|
return 85;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确定整体安全状态
|
||||||
|
*/
|
||||||
|
private determineOverallSecurityStatus(): 'secure' | 'warning' | 'critical' {
|
||||||
|
// 模拟安全状态判断
|
||||||
|
return 'secure';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最近安全事件
|
||||||
|
*/
|
||||||
|
private async getRecentSecurityEvents(): Promise<SecurityEvent[]> {
|
||||||
|
// 模拟获取最近安全事件
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取活跃安全告警
|
||||||
|
*/
|
||||||
|
private async getActiveSecurityAlerts(): Promise<SecurityAlert[]> {
|
||||||
|
// 模拟获取活跃告警
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取安全趋势
|
||||||
|
*/
|
||||||
|
private async getSecurityTrends(): Promise<SecurityTrend[]> {
|
||||||
|
// 模拟获取安全趋势
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类型定义
|
||||||
|
export interface SecurityAssessmentResult {
|
||||||
|
timestamp: number;
|
||||||
|
duration: number;
|
||||||
|
overallScore: number;
|
||||||
|
riskLevel: 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
components: {
|
||||||
|
securityAnalysis: any;
|
||||||
|
vulnerabilityScan: any;
|
||||||
|
accessPatternAnalysis: any;
|
||||||
|
threatDetection: any;
|
||||||
|
};
|
||||||
|
recommendations: string[];
|
||||||
|
nextAssessmentTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PolicyValidationResult {
|
||||||
|
policyName: string;
|
||||||
|
valid: boolean;
|
||||||
|
reason: string;
|
||||||
|
actions: string[];
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecurityEvent {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
source: string;
|
||||||
|
description: string;
|
||||||
|
timestamp: number;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecurityEventResponse {
|
||||||
|
eventId: string;
|
||||||
|
handled: boolean;
|
||||||
|
actions: string[];
|
||||||
|
timestamp: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecurityDashboard {
|
||||||
|
timestamp: number;
|
||||||
|
status: 'secure' | 'warning' | 'critical';
|
||||||
|
metrics: {
|
||||||
|
threatsDetected: number;
|
||||||
|
vulnerabilitiesFound: number;
|
||||||
|
accessViolations: number;
|
||||||
|
securityScore: number;
|
||||||
|
};
|
||||||
|
recentEvents: SecurityEvent[];
|
||||||
|
alerts: SecurityAlert[];
|
||||||
|
trends: SecurityTrend[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecurityAlert {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
message: string;
|
||||||
|
timestamp: number;
|
||||||
|
acknowledged: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecurityTrend {
|
||||||
|
metric: string;
|
||||||
|
values: { timestamp: number; value: number }[];
|
||||||
|
trend: 'increasing' | 'decreasing' | 'stable';
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||||
|
import { SecurityAnalyzer } from '../analyzers/security.analyzer';
|
||||||
|
import { VulnerabilityDetector } from '../detectors/vulnerability.detector';
|
||||||
|
import { AccessProtector } from '../protectors/access.protector';
|
||||||
|
import { AiSecurityService } from './ai-security.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SafeReadyService implements OnModuleInit {
|
||||||
|
private readonly logger = new Logger(SafeReadyService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly eventBus: EventBus,
|
||||||
|
private readonly config: ConfigService,
|
||||||
|
private readonly securityAnalyzer: SecurityAnalyzer,
|
||||||
|
private readonly vulnerabilityDetector: VulnerabilityDetector,
|
||||||
|
private readonly accessProtector: AccessProtector,
|
||||||
|
private readonly aiSecurityService: AiSecurityService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
const enabled =
|
||||||
|
(this.config.get<string>('AI_SAFE_ENABLED') ?? 'true') === 'true';
|
||||||
|
let currentState: 'ready' | 'unavailable' = 'unavailable';
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (enabled) {
|
||||||
|
// 轻量健康检查:核心组件是否注入到位
|
||||||
|
const componentsOk = [
|
||||||
|
this.securityAnalyzer,
|
||||||
|
this.vulnerabilityDetector,
|
||||||
|
this.accessProtector,
|
||||||
|
this.aiSecurityService,
|
||||||
|
].every((c) => !!c);
|
||||||
|
|
||||||
|
currentState = componentsOk ? 'ready' : 'unavailable';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.warn(
|
||||||
|
`AI Safe readiness check failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
|
);
|
||||||
|
currentState = 'unavailable';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.eventBus.emit('module.state.changed', {
|
||||||
|
module: 'ai.safe',
|
||||||
|
previousState: 'initializing',
|
||||||
|
currentState,
|
||||||
|
meta: { enabled },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,837 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performance Analyzer - 性能分析器
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* - 分析系统性能指标
|
||||||
|
* - 识别性能瓶颈
|
||||||
|
* - 生成性能报告
|
||||||
|
* - 提供优化建议
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class PerformanceAnalyzer {
|
||||||
|
private readonly logger = new Logger(PerformanceAnalyzer.name);
|
||||||
|
private readonly performanceMetrics: PerformanceMetric[] = [];
|
||||||
|
private readonly analysisHistory: PerformanceAnalysis[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.initializeMetrics();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行性能分析
|
||||||
|
*/
|
||||||
|
async analyzePerformance(
|
||||||
|
options: AnalysisOptions = {},
|
||||||
|
): Promise<PerformanceAnalysis> {
|
||||||
|
this.logger.log('Starting performance analysis');
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// 收集当前性能指标
|
||||||
|
const currentMetrics = await this.collectCurrentMetrics();
|
||||||
|
|
||||||
|
// 分析CPU性能
|
||||||
|
const cpuAnalysis = await this.analyzeCpuPerformance(currentMetrics);
|
||||||
|
|
||||||
|
// 分析内存性能
|
||||||
|
const memoryAnalysis = await this.analyzeMemoryPerformance(currentMetrics);
|
||||||
|
|
||||||
|
// 分析I/O性能
|
||||||
|
const ioAnalysis = await this.analyzeIoPerformance(currentMetrics);
|
||||||
|
|
||||||
|
// 分析网络性能
|
||||||
|
const networkAnalysis =
|
||||||
|
await this.analyzeNetworkPerformance(currentMetrics);
|
||||||
|
|
||||||
|
// 分析数据库性能
|
||||||
|
const databaseAnalysis =
|
||||||
|
await this.analyzeDatabasePerformance(currentMetrics);
|
||||||
|
|
||||||
|
// 分析应用性能
|
||||||
|
const applicationAnalysis =
|
||||||
|
await this.analyzeApplicationPerformance(currentMetrics);
|
||||||
|
|
||||||
|
// 识别性能瓶颈
|
||||||
|
const bottlenecks = this.identifyBottlenecks([
|
||||||
|
cpuAnalysis,
|
||||||
|
memoryAnalysis,
|
||||||
|
ioAnalysis,
|
||||||
|
networkAnalysis,
|
||||||
|
databaseAnalysis,
|
||||||
|
applicationAnalysis,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 生成优化建议
|
||||||
|
const recommendations =
|
||||||
|
this.generateOptimizationRecommendations(bottlenecks);
|
||||||
|
|
||||||
|
// 计算整体性能评分
|
||||||
|
const overallScore = this.calculateOverallScore([
|
||||||
|
cpuAnalysis,
|
||||||
|
memoryAnalysis,
|
||||||
|
ioAnalysis,
|
||||||
|
networkAnalysis,
|
||||||
|
databaseAnalysis,
|
||||||
|
applicationAnalysis,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const analysis: PerformanceAnalysis = {
|
||||||
|
id: this.generateAnalysisId(),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
duration: Date.now() - startTime,
|
||||||
|
overallScore,
|
||||||
|
status: this.determinePerformanceStatus(overallScore),
|
||||||
|
metrics: currentMetrics,
|
||||||
|
summary: {
|
||||||
|
averageResponseTime: currentMetrics.response_time || 0,
|
||||||
|
requestsPerSecond: currentMetrics.throughput || 0,
|
||||||
|
errorRate: currentMetrics.error_rate || 0,
|
||||||
|
},
|
||||||
|
analyses: {
|
||||||
|
cpu: cpuAnalysis,
|
||||||
|
memory: memoryAnalysis,
|
||||||
|
io: ioAnalysis,
|
||||||
|
network: networkAnalysis,
|
||||||
|
database: databaseAnalysis,
|
||||||
|
application: applicationAnalysis,
|
||||||
|
},
|
||||||
|
bottlenecks,
|
||||||
|
recommendations,
|
||||||
|
trends: await this.analyzeTrends(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存分析结果
|
||||||
|
this.analysisHistory.push(analysis);
|
||||||
|
|
||||||
|
// 保持最近100次分析记录
|
||||||
|
if (this.analysisHistory.length > 100) {
|
||||||
|
this.analysisHistory.splice(0, this.analysisHistory.length - 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`Performance analysis completed in ${analysis.duration}ms`);
|
||||||
|
|
||||||
|
return analysis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取性能趋势
|
||||||
|
*/
|
||||||
|
async getPerformanceTrends(
|
||||||
|
period: 'hour' | 'day' | 'week' | 'month' = 'day',
|
||||||
|
): Promise<PerformanceTrend[]> {
|
||||||
|
this.logger.debug(`Getting performance trends for ${period}`);
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const periodMs = this.getPeriodInMs(period);
|
||||||
|
const startTime = now - periodMs;
|
||||||
|
|
||||||
|
const relevantAnalyses = this.analysisHistory.filter(
|
||||||
|
(analysis) => analysis.timestamp >= startTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (relevantAnalyses.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按时间分组数据
|
||||||
|
const groupedData = this.groupAnalysesByTime(relevantAnalyses, period);
|
||||||
|
|
||||||
|
// 生成趋势数据
|
||||||
|
const trends: PerformanceTrend[] = [
|
||||||
|
{
|
||||||
|
metric: 'overall_score',
|
||||||
|
name: 'Overall Performance Score',
|
||||||
|
values: groupedData.map((group) => ({
|
||||||
|
timestamp: group.timestamp,
|
||||||
|
value: this.calculateAverageScore(group.analyses, 'overallScore'),
|
||||||
|
})),
|
||||||
|
trend: this.calculateTrendDirection(groupedData, 'overallScore'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: 'cpu_usage',
|
||||||
|
name: 'CPU Usage',
|
||||||
|
values: groupedData.map((group) => ({
|
||||||
|
timestamp: group.timestamp,
|
||||||
|
value: this.calculateAverageScore(
|
||||||
|
group.analyses,
|
||||||
|
'analyses.cpu.score',
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
trend: this.calculateTrendDirection(groupedData, 'analyses.cpu.score'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: 'memory_usage',
|
||||||
|
name: 'Memory Usage',
|
||||||
|
values: groupedData.map((group) => ({
|
||||||
|
timestamp: group.timestamp,
|
||||||
|
value: this.calculateAverageScore(
|
||||||
|
group.analyses,
|
||||||
|
'analyses.memory.score',
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
trend: this.calculateTrendDirection(
|
||||||
|
groupedData,
|
||||||
|
'analyses.memory.score',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return trends;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取性能基准
|
||||||
|
*/
|
||||||
|
async getPerformanceBenchmarks(): Promise<PerformanceBenchmark[]> {
|
||||||
|
this.logger.debug('Getting performance benchmarks');
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
metric: 'response_time',
|
||||||
|
name: 'API Response Time',
|
||||||
|
target: 200, // ms
|
||||||
|
warning: 500,
|
||||||
|
critical: 1000,
|
||||||
|
unit: 'ms',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: 'cpu_usage',
|
||||||
|
name: 'CPU Usage',
|
||||||
|
target: 70, // %
|
||||||
|
warning: 85,
|
||||||
|
critical: 95,
|
||||||
|
unit: '%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: 'memory_usage',
|
||||||
|
name: 'Memory Usage',
|
||||||
|
target: 80, // %
|
||||||
|
warning: 90,
|
||||||
|
critical: 95,
|
||||||
|
unit: '%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: 'database_query_time',
|
||||||
|
name: 'Database Query Time',
|
||||||
|
target: 100, // ms
|
||||||
|
warning: 300,
|
||||||
|
critical: 1000,
|
||||||
|
unit: 'ms',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 比较性能分析结果
|
||||||
|
*/
|
||||||
|
async comparePerformance(
|
||||||
|
analysisId1: string,
|
||||||
|
analysisId2: string,
|
||||||
|
): Promise<PerformanceComparison> {
|
||||||
|
this.logger.debug(
|
||||||
|
`Comparing performance analyses: ${analysisId1} vs ${analysisId2}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const analysis1 = this.analysisHistory.find((a) => a.id === analysisId1);
|
||||||
|
const analysis2 = this.analysisHistory.find((a) => a.id === analysisId2);
|
||||||
|
|
||||||
|
if (!analysis1 || !analysis2) {
|
||||||
|
throw new Error('One or both analyses not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const comparison: PerformanceComparison = {
|
||||||
|
baseline: analysis1,
|
||||||
|
comparison: analysis2,
|
||||||
|
differences: {
|
||||||
|
overallScore: analysis2.overallScore - analysis1.overallScore,
|
||||||
|
cpu: analysis2.analyses.cpu.score - analysis1.analyses.cpu.score,
|
||||||
|
memory:
|
||||||
|
analysis2.analyses.memory.score - analysis1.analyses.memory.score,
|
||||||
|
io: analysis2.analyses.io.score - analysis1.analyses.io.score,
|
||||||
|
network:
|
||||||
|
analysis2.analyses.network.score - analysis1.analyses.network.score,
|
||||||
|
database:
|
||||||
|
analysis2.analyses.database.score - analysis1.analyses.database.score,
|
||||||
|
application:
|
||||||
|
analysis2.analyses.application.score -
|
||||||
|
analysis1.analyses.application.score,
|
||||||
|
},
|
||||||
|
improvements: [],
|
||||||
|
regressions: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 识别改进和退化
|
||||||
|
Object.entries(comparison.differences).forEach(([metric, diff]) => {
|
||||||
|
if (diff > 5) {
|
||||||
|
// 改进超过5分
|
||||||
|
comparison.improvements.push({
|
||||||
|
metric,
|
||||||
|
improvement: diff,
|
||||||
|
description: `${metric} improved by ${diff.toFixed(1)} points`,
|
||||||
|
});
|
||||||
|
} else if (diff < -5) {
|
||||||
|
// 退化超过5分
|
||||||
|
comparison.regressions.push({
|
||||||
|
metric,
|
||||||
|
regression: Math.abs(diff),
|
||||||
|
description: `${metric} regressed by ${Math.abs(diff).toFixed(1)} points`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return comparison;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化性能指标
|
||||||
|
*/
|
||||||
|
private initializeMetrics(): void {
|
||||||
|
this.performanceMetrics.push(
|
||||||
|
{ name: 'cpu_usage', type: 'percentage', threshold: 80 },
|
||||||
|
{ name: 'memory_usage', type: 'percentage', threshold: 85 },
|
||||||
|
{ name: 'disk_io', type: 'rate', threshold: 1000 },
|
||||||
|
{ name: 'network_io', type: 'rate', threshold: 100 },
|
||||||
|
{ name: 'response_time', type: 'duration', threshold: 500 },
|
||||||
|
{ name: 'throughput', type: 'rate', threshold: 1000 },
|
||||||
|
{ name: 'error_rate', type: 'percentage', threshold: 5 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收集当前性能指标
|
||||||
|
*/
|
||||||
|
private async collectCurrentMetrics(): Promise<Record<string, number>> {
|
||||||
|
// 模拟收集性能指标
|
||||||
|
return {
|
||||||
|
cpu_usage: Math.random() * 100,
|
||||||
|
memory_usage: Math.random() * 100,
|
||||||
|
disk_io: Math.random() * 2000,
|
||||||
|
network_io: Math.random() * 200,
|
||||||
|
response_time: Math.random() * 1000,
|
||||||
|
throughput: Math.random() * 2000,
|
||||||
|
error_rate: Math.random() * 10,
|
||||||
|
active_connections: Math.floor(Math.random() * 1000),
|
||||||
|
queue_length: Math.floor(Math.random() * 100),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析CPU性能
|
||||||
|
*/
|
||||||
|
private async analyzeCpuPerformance(
|
||||||
|
metrics: Record<string, number>,
|
||||||
|
): Promise<ComponentAnalysis> {
|
||||||
|
const cpuUsage = metrics.cpu_usage || 0;
|
||||||
|
|
||||||
|
let score = 100;
|
||||||
|
let status: 'excellent' | 'good' | 'warning' | 'critical' = 'excellent';
|
||||||
|
const issues: string[] = [];
|
||||||
|
const recommendations: string[] = [];
|
||||||
|
|
||||||
|
if (cpuUsage > 95) {
|
||||||
|
score = 20;
|
||||||
|
status = 'critical';
|
||||||
|
issues.push('CPU usage is critically high');
|
||||||
|
recommendations.push(
|
||||||
|
'Scale horizontally or optimize CPU-intensive operations',
|
||||||
|
);
|
||||||
|
} else if (cpuUsage > 85) {
|
||||||
|
score = 50;
|
||||||
|
status = 'warning';
|
||||||
|
issues.push('CPU usage is high');
|
||||||
|
recommendations.push('Monitor CPU usage and consider optimization');
|
||||||
|
} else if (cpuUsage > 70) {
|
||||||
|
score = 75;
|
||||||
|
status = 'good';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: 'cpu',
|
||||||
|
score,
|
||||||
|
status,
|
||||||
|
metrics: { usage: cpuUsage },
|
||||||
|
issues,
|
||||||
|
recommendations,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析内存性能
|
||||||
|
*/
|
||||||
|
private async analyzeMemoryPerformance(
|
||||||
|
metrics: Record<string, number>,
|
||||||
|
): Promise<ComponentAnalysis> {
|
||||||
|
const memoryUsage = metrics.memory_usage || 0;
|
||||||
|
|
||||||
|
let score = 100;
|
||||||
|
let status: 'excellent' | 'good' | 'warning' | 'critical' = 'excellent';
|
||||||
|
const issues: string[] = [];
|
||||||
|
const recommendations: string[] = [];
|
||||||
|
|
||||||
|
if (memoryUsage > 95) {
|
||||||
|
score = 20;
|
||||||
|
status = 'critical';
|
||||||
|
issues.push('Memory usage is critically high');
|
||||||
|
recommendations.push('Increase memory or optimize memory usage');
|
||||||
|
} else if (memoryUsage > 90) {
|
||||||
|
score = 50;
|
||||||
|
status = 'warning';
|
||||||
|
issues.push('Memory usage is high');
|
||||||
|
recommendations.push('Monitor memory usage and check for memory leaks');
|
||||||
|
} else if (memoryUsage > 80) {
|
||||||
|
score = 75;
|
||||||
|
status = 'good';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: 'memory',
|
||||||
|
score,
|
||||||
|
status,
|
||||||
|
metrics: { usage: memoryUsage },
|
||||||
|
issues,
|
||||||
|
recommendations,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析I/O性能
|
||||||
|
*/
|
||||||
|
private async analyzeIoPerformance(
|
||||||
|
metrics: Record<string, number>,
|
||||||
|
): Promise<ComponentAnalysis> {
|
||||||
|
const diskIo = metrics.disk_io || 0;
|
||||||
|
|
||||||
|
let score = 100;
|
||||||
|
let status: 'excellent' | 'good' | 'warning' | 'critical' = 'excellent';
|
||||||
|
const issues: string[] = [];
|
||||||
|
const recommendations: string[] = [];
|
||||||
|
|
||||||
|
if (diskIo > 2000) {
|
||||||
|
score = 50;
|
||||||
|
status = 'warning';
|
||||||
|
issues.push('Disk I/O is high');
|
||||||
|
recommendations.push('Optimize disk operations or use faster storage');
|
||||||
|
} else if (diskIo > 1500) {
|
||||||
|
score = 75;
|
||||||
|
status = 'good';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: 'io',
|
||||||
|
score,
|
||||||
|
status,
|
||||||
|
metrics: { disk_io: diskIo },
|
||||||
|
issues,
|
||||||
|
recommendations,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析网络性能
|
||||||
|
*/
|
||||||
|
private async analyzeNetworkPerformance(
|
||||||
|
metrics: Record<string, number>,
|
||||||
|
): Promise<ComponentAnalysis> {
|
||||||
|
const networkIo = metrics.network_io || 0;
|
||||||
|
|
||||||
|
let score = 100;
|
||||||
|
let status: 'excellent' | 'good' | 'warning' | 'critical' = 'excellent';
|
||||||
|
const issues: string[] = [];
|
||||||
|
const recommendations: string[] = [];
|
||||||
|
|
||||||
|
if (networkIo > 150) {
|
||||||
|
score = 50;
|
||||||
|
status = 'warning';
|
||||||
|
issues.push('Network I/O is high');
|
||||||
|
recommendations.push('Optimize network operations or increase bandwidth');
|
||||||
|
} else if (networkIo > 100) {
|
||||||
|
score = 75;
|
||||||
|
status = 'good';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: 'network',
|
||||||
|
score,
|
||||||
|
status,
|
||||||
|
metrics: { network_io: networkIo },
|
||||||
|
issues,
|
||||||
|
recommendations,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析数据库性能
|
||||||
|
*/
|
||||||
|
private async analyzeDatabasePerformance(
|
||||||
|
metrics: Record<string, number>,
|
||||||
|
): Promise<ComponentAnalysis> {
|
||||||
|
const responseTime = metrics.response_time || 0;
|
||||||
|
const activeConnections = metrics.active_connections || 0;
|
||||||
|
|
||||||
|
let score = 100;
|
||||||
|
let status: 'excellent' | 'good' | 'warning' | 'critical' = 'excellent';
|
||||||
|
const issues: string[] = [];
|
||||||
|
const recommendations: string[] = [];
|
||||||
|
|
||||||
|
if (responseTime > 1000) {
|
||||||
|
score = 20;
|
||||||
|
status = 'critical';
|
||||||
|
issues.push('Database response time is critically slow');
|
||||||
|
recommendations.push('Optimize queries and add indexes');
|
||||||
|
} else if (responseTime > 500) {
|
||||||
|
score = 50;
|
||||||
|
status = 'warning';
|
||||||
|
issues.push('Database response time is slow');
|
||||||
|
recommendations.push('Review and optimize slow queries');
|
||||||
|
} else if (responseTime > 200) {
|
||||||
|
score = 75;
|
||||||
|
status = 'good';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeConnections > 800) {
|
||||||
|
score = Math.min(score, 50);
|
||||||
|
status = status === 'excellent' ? 'warning' : status;
|
||||||
|
issues.push('High number of active database connections');
|
||||||
|
recommendations.push('Implement connection pooling');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: 'database',
|
||||||
|
score,
|
||||||
|
status,
|
||||||
|
metrics: {
|
||||||
|
response_time: responseTime,
|
||||||
|
active_connections: activeConnections,
|
||||||
|
},
|
||||||
|
issues,
|
||||||
|
recommendations,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析应用性能
|
||||||
|
*/
|
||||||
|
private async analyzeApplicationPerformance(
|
||||||
|
metrics: Record<string, number>,
|
||||||
|
): Promise<ComponentAnalysis> {
|
||||||
|
const throughput = metrics.throughput || 0;
|
||||||
|
const queueLength = metrics.queue_length || 0;
|
||||||
|
|
||||||
|
let score = 100;
|
||||||
|
let status: 'excellent' | 'good' | 'warning' | 'critical' = 'excellent';
|
||||||
|
const issues: string[] = [];
|
||||||
|
const recommendations: string[] = [];
|
||||||
|
|
||||||
|
if (throughput < 500) {
|
||||||
|
score = 50;
|
||||||
|
status = 'warning';
|
||||||
|
issues.push('Application throughput is low');
|
||||||
|
recommendations.push('Optimize application code and algorithms');
|
||||||
|
} else if (throughput < 1000) {
|
||||||
|
score = 75;
|
||||||
|
status = 'good';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queueLength > 50) {
|
||||||
|
score = Math.min(score, 50);
|
||||||
|
status = status === 'excellent' ? 'warning' : status;
|
||||||
|
issues.push('High queue length indicates processing bottleneck');
|
||||||
|
recommendations.push(
|
||||||
|
'Scale processing capacity or optimize queue handling',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: 'application',
|
||||||
|
score,
|
||||||
|
status,
|
||||||
|
metrics: { throughput, queue_length: queueLength },
|
||||||
|
issues,
|
||||||
|
recommendations,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 识别性能瓶颈
|
||||||
|
*/
|
||||||
|
private identifyBottlenecks(
|
||||||
|
analyses: ComponentAnalysis[],
|
||||||
|
): PerformanceBottleneck[] {
|
||||||
|
const bottlenecks: PerformanceBottleneck[] = [];
|
||||||
|
|
||||||
|
analyses.forEach((analysis) => {
|
||||||
|
if (analysis.status === 'critical' || analysis.status === 'warning') {
|
||||||
|
bottlenecks.push({
|
||||||
|
component: analysis.component,
|
||||||
|
severity: analysis.status === 'critical' ? 'critical' : 'high',
|
||||||
|
description: `${analysis.component} performance is ${analysis.status}`,
|
||||||
|
impact: analysis.score < 30 ? 'high' : 'medium',
|
||||||
|
recommendations: analysis.recommendations,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return bottlenecks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成优化建议
|
||||||
|
*/
|
||||||
|
private generateOptimizationRecommendations(
|
||||||
|
bottlenecks: PerformanceBottleneck[],
|
||||||
|
): OptimizationRecommendation[] {
|
||||||
|
const recommendations: OptimizationRecommendation[] = [];
|
||||||
|
|
||||||
|
bottlenecks.forEach((bottleneck) => {
|
||||||
|
bottleneck.recommendations.forEach((rec) => {
|
||||||
|
recommendations.push({
|
||||||
|
category: bottleneck.component,
|
||||||
|
priority: bottleneck.severity === 'critical' ? 'high' : 'medium',
|
||||||
|
description: rec,
|
||||||
|
estimatedImpact: bottleneck.impact,
|
||||||
|
effort: 'medium', // 简化处理
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return recommendations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算整体性能评分
|
||||||
|
*/
|
||||||
|
private calculateOverallScore(analyses: ComponentAnalysis[]): number {
|
||||||
|
if (analyses.length === 0) return 100;
|
||||||
|
|
||||||
|
const totalScore = analyses.reduce(
|
||||||
|
(sum, analysis) => sum + analysis.score,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
return Math.round(totalScore / analyses.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确定性能状态
|
||||||
|
*/
|
||||||
|
private determinePerformanceStatus(
|
||||||
|
score: number,
|
||||||
|
): 'excellent' | 'good' | 'warning' | 'critical' {
|
||||||
|
if (score >= 90) return 'excellent';
|
||||||
|
if (score >= 75) return 'good';
|
||||||
|
if (score >= 50) return 'warning';
|
||||||
|
return 'critical';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析趋势
|
||||||
|
*/
|
||||||
|
private async analyzeTrends(): Promise<PerformanceTrend[]> {
|
||||||
|
// 简化的趋势分析
|
||||||
|
return this.getPerformanceTrends('day');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成分析ID
|
||||||
|
*/
|
||||||
|
private generateAnalysisId(): string {
|
||||||
|
return `PERF_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取时间段毫秒数
|
||||||
|
*/
|
||||||
|
private getPeriodInMs(period: 'hour' | 'day' | 'week' | 'month'): number {
|
||||||
|
const periods = {
|
||||||
|
hour: 60 * 60 * 1000,
|
||||||
|
day: 24 * 60 * 60 * 1000,
|
||||||
|
week: 7 * 24 * 60 * 60 * 1000,
|
||||||
|
month: 30 * 24 * 60 * 60 * 1000,
|
||||||
|
};
|
||||||
|
return periods[period];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按时间分组分析结果
|
||||||
|
*/
|
||||||
|
private groupAnalysesByTime(
|
||||||
|
analyses: PerformanceAnalysis[],
|
||||||
|
period: 'hour' | 'day' | 'week' | 'month',
|
||||||
|
): GroupedAnalysis[] {
|
||||||
|
const groups: Record<string, PerformanceAnalysis[]> = {};
|
||||||
|
|
||||||
|
analyses.forEach((analysis) => {
|
||||||
|
const date = new Date(analysis.timestamp);
|
||||||
|
let key: string;
|
||||||
|
|
||||||
|
switch (period) {
|
||||||
|
case 'hour':
|
||||||
|
key = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}-${date.getHours()}`;
|
||||||
|
break;
|
||||||
|
case 'day':
|
||||||
|
key = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
||||||
|
break;
|
||||||
|
case 'week':
|
||||||
|
const weekStart = new Date(date);
|
||||||
|
weekStart.setDate(date.getDate() - date.getDay());
|
||||||
|
key = `${weekStart.getFullYear()}-${weekStart.getMonth()}-${weekStart.getDate()}`;
|
||||||
|
break;
|
||||||
|
case 'month':
|
||||||
|
key = `${date.getFullYear()}-${date.getMonth()}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!groups[key]) {
|
||||||
|
groups[key] = [];
|
||||||
|
}
|
||||||
|
groups[key].push(analysis);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.entries(groups).map(([key, groupAnalyses]) => ({
|
||||||
|
timestamp: groupAnalyses[0].timestamp,
|
||||||
|
analyses: groupAnalyses,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算平均分数
|
||||||
|
*/
|
||||||
|
private calculateAverageScore(
|
||||||
|
analyses: PerformanceAnalysis[],
|
||||||
|
path: string,
|
||||||
|
): number {
|
||||||
|
const values = analyses
|
||||||
|
.map((analysis) => this.getNestedValue(analysis, path))
|
||||||
|
.filter((v) => v !== undefined);
|
||||||
|
return values.length > 0
|
||||||
|
? values.reduce((sum, val) => sum + val, 0) / values.length
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取嵌套值
|
||||||
|
*/
|
||||||
|
private getNestedValue(obj: any, path: string): number | undefined {
|
||||||
|
return path.split('.').reduce((current, key) => current?.[key], obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算趋势方向
|
||||||
|
*/
|
||||||
|
private calculateTrendDirection(
|
||||||
|
groupedData: GroupedAnalysis[],
|
||||||
|
path: string,
|
||||||
|
): 'increasing' | 'decreasing' | 'stable' {
|
||||||
|
if (groupedData.length < 2) return 'stable';
|
||||||
|
|
||||||
|
const values = groupedData.map((group) =>
|
||||||
|
this.calculateAverageScore(group.analyses, path),
|
||||||
|
);
|
||||||
|
const first = values[0];
|
||||||
|
const last = values[values.length - 1];
|
||||||
|
const diff = last - first;
|
||||||
|
|
||||||
|
if (Math.abs(diff) < 5) return 'stable';
|
||||||
|
return diff > 0 ? 'increasing' : 'decreasing';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类型定义
|
||||||
|
export interface PerformanceMetric {
|
||||||
|
name: string;
|
||||||
|
type: 'percentage' | 'rate' | 'duration' | 'count';
|
||||||
|
threshold: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnalysisOptions {
|
||||||
|
includeHistory?: boolean;
|
||||||
|
detailedAnalysis?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PerformanceAnalysis {
|
||||||
|
id: string;
|
||||||
|
timestamp: number;
|
||||||
|
duration: number;
|
||||||
|
overallScore: number;
|
||||||
|
status: 'excellent' | 'good' | 'warning' | 'critical';
|
||||||
|
metrics: Record<string, number>;
|
||||||
|
summary: {
|
||||||
|
averageResponseTime: number;
|
||||||
|
requestsPerSecond: number;
|
||||||
|
errorRate: number;
|
||||||
|
};
|
||||||
|
analyses: {
|
||||||
|
cpu: ComponentAnalysis;
|
||||||
|
memory: ComponentAnalysis;
|
||||||
|
io: ComponentAnalysis;
|
||||||
|
network: ComponentAnalysis;
|
||||||
|
database: ComponentAnalysis;
|
||||||
|
application: ComponentAnalysis;
|
||||||
|
};
|
||||||
|
bottlenecks: PerformanceBottleneck[];
|
||||||
|
recommendations: OptimizationRecommendation[];
|
||||||
|
trends: PerformanceTrend[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComponentAnalysis {
|
||||||
|
component: string;
|
||||||
|
score: number;
|
||||||
|
status: 'excellent' | 'good' | 'warning' | 'critical';
|
||||||
|
metrics: Record<string, number>;
|
||||||
|
issues: string[];
|
||||||
|
recommendations: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PerformanceBottleneck {
|
||||||
|
component: string;
|
||||||
|
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
description: string;
|
||||||
|
impact: 'low' | 'medium' | 'high';
|
||||||
|
recommendations: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OptimizationRecommendation {
|
||||||
|
category: string;
|
||||||
|
priority: 'low' | 'medium' | 'high';
|
||||||
|
description: string;
|
||||||
|
estimatedImpact: 'low' | 'medium' | 'high';
|
||||||
|
effort: 'low' | 'medium' | 'high';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PerformanceTrend {
|
||||||
|
metric: string;
|
||||||
|
name: string;
|
||||||
|
values: { timestamp: number; value: number }[];
|
||||||
|
trend: 'increasing' | 'decreasing' | 'stable';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PerformanceBenchmark {
|
||||||
|
metric: string;
|
||||||
|
name: string;
|
||||||
|
target: number;
|
||||||
|
warning: number;
|
||||||
|
critical: number;
|
||||||
|
unit: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PerformanceComparison {
|
||||||
|
baseline: PerformanceAnalysis;
|
||||||
|
comparison: PerformanceAnalysis;
|
||||||
|
differences: Record<string, number>;
|
||||||
|
improvements: Array<{
|
||||||
|
metric: string;
|
||||||
|
improvement: number;
|
||||||
|
description: string;
|
||||||
|
}>;
|
||||||
|
regressions: Array<{
|
||||||
|
metric: string;
|
||||||
|
regression: number;
|
||||||
|
description: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GroupedAnalysis {
|
||||||
|
timestamp: number;
|
||||||
|
analyses: PerformanceAnalysis[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,695 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
Logger,
|
||||||
|
OnModuleInit,
|
||||||
|
OnModuleDestroy,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resource Monitor - 资源监控器
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* - 实时监控系统资源使用情况
|
||||||
|
* - 检测资源使用异常
|
||||||
|
* - 发送资源告警
|
||||||
|
* - 收集资源使用历史数据
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||||
|
private readonly logger = new Logger(ResourceMonitor.name);
|
||||||
|
private monitoringInterval: NodeJS.Timeout | null = null;
|
||||||
|
private readonly resourceHistory: ResourceSnapshot[] = [];
|
||||||
|
private readonly alertThresholds: ResourceThresholds;
|
||||||
|
private readonly monitoringConfig: MonitoringConfig;
|
||||||
|
|
||||||
|
constructor(private readonly eventBus: EventBus) {
|
||||||
|
this.alertThresholds = this.getDefaultThresholds();
|
||||||
|
this.monitoringConfig = this.getDefaultConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
this.logger.log('Initializing Resource Monitor');
|
||||||
|
await this.startMonitoring();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy() {
|
||||||
|
this.logger.log('Destroying Resource Monitor');
|
||||||
|
await this.stopMonitoring();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始监控
|
||||||
|
*/
|
||||||
|
async startMonitoring(): Promise<void> {
|
||||||
|
if (this.monitoringInterval) {
|
||||||
|
this.logger.warn('Monitoring is already running');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log('Starting resource monitoring');
|
||||||
|
|
||||||
|
this.monitoringInterval = setInterval(
|
||||||
|
() => this.collectResourceSnapshot(),
|
||||||
|
this.monitoringConfig.interval,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 立即执行一次
|
||||||
|
await this.collectResourceSnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止监控
|
||||||
|
*/
|
||||||
|
async stopMonitoring(): Promise<void> {
|
||||||
|
if (this.monitoringInterval) {
|
||||||
|
clearInterval(this.monitoringInterval);
|
||||||
|
this.monitoringInterval = null;
|
||||||
|
this.logger.log('Resource monitoring stopped');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前资源状态
|
||||||
|
*/
|
||||||
|
async getCurrentResourceStatus(): Promise<ResourceStatus> {
|
||||||
|
this.logger.debug('Getting current resource status');
|
||||||
|
|
||||||
|
const snapshot = await this.captureResourceSnapshot();
|
||||||
|
const alerts = this.checkAlerts(snapshot);
|
||||||
|
const trends = this.calculateTrends();
|
||||||
|
|
||||||
|
return {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
snapshot,
|
||||||
|
alerts,
|
||||||
|
trends,
|
||||||
|
health: this.assessResourceHealth(snapshot, alerts),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 兼容别名:获取当前资源状态
|
||||||
|
* 为旧代码调用提供兼容,内部转发到 getCurrentResourceStatus
|
||||||
|
*/
|
||||||
|
async getCurrentStatus(): Promise<ResourceStatus> {
|
||||||
|
return this.getCurrentResourceStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取资源历史数据
|
||||||
|
*/
|
||||||
|
getResourceHistory(
|
||||||
|
period: 'hour' | 'day' | 'week' = 'hour',
|
||||||
|
): ResourceSnapshot[] {
|
||||||
|
const now = Date.now();
|
||||||
|
const periodMs = this.getPeriodInMs(period);
|
||||||
|
const startTime = now - periodMs;
|
||||||
|
|
||||||
|
return this.resourceHistory.filter(
|
||||||
|
(snapshot) => snapshot.timestamp >= startTime,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置告警阈值
|
||||||
|
*/
|
||||||
|
setAlertThresholds(thresholds: Partial<ResourceThresholds>): void {
|
||||||
|
this.logger.log('Updating alert thresholds', thresholds);
|
||||||
|
Object.assign(this.alertThresholds, thresholds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取资源使用统计
|
||||||
|
*/
|
||||||
|
getResourceStatistics(
|
||||||
|
period: 'hour' | 'day' | 'week' = 'day',
|
||||||
|
): ResourceStatistics {
|
||||||
|
const history = this.getResourceHistory(period);
|
||||||
|
|
||||||
|
if (history.length === 0) {
|
||||||
|
return this.getEmptyStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
|
const cpuValues = history.map((h) => h.cpu.usage);
|
||||||
|
const memoryValues = history.map((h) => h.memory.usage);
|
||||||
|
const diskValues = history.map((h) => h.disk.usage);
|
||||||
|
const networkInValues = history.map((h) => h.network.bytesIn);
|
||||||
|
const networkOutValues = history.map((h) => h.network.bytesOut);
|
||||||
|
|
||||||
|
return {
|
||||||
|
period,
|
||||||
|
sampleCount: history.length,
|
||||||
|
cpu: {
|
||||||
|
min: Math.min(...cpuValues),
|
||||||
|
max: Math.max(...cpuValues),
|
||||||
|
avg: this.calculateAverage(cpuValues),
|
||||||
|
current: cpuValues[cpuValues.length - 1],
|
||||||
|
},
|
||||||
|
memory: {
|
||||||
|
min: Math.min(...memoryValues),
|
||||||
|
max: Math.max(...memoryValues),
|
||||||
|
avg: this.calculateAverage(memoryValues),
|
||||||
|
current: memoryValues[memoryValues.length - 1],
|
||||||
|
},
|
||||||
|
disk: {
|
||||||
|
min: Math.min(...diskValues),
|
||||||
|
max: Math.max(...diskValues),
|
||||||
|
avg: this.calculateAverage(diskValues),
|
||||||
|
current: diskValues[diskValues.length - 1],
|
||||||
|
},
|
||||||
|
network: {
|
||||||
|
bytesInTotal: networkInValues.reduce((sum, val) => sum + val, 0),
|
||||||
|
bytesOutTotal: networkOutValues.reduce((sum, val) => sum + val, 0),
|
||||||
|
avgBytesIn: this.calculateAverage(networkInValues),
|
||||||
|
avgBytesOut: this.calculateAverage(networkOutValues),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预测资源使用趋势
|
||||||
|
*/
|
||||||
|
predictResourceTrends(hours: number = 24): ResourcePrediction[] {
|
||||||
|
this.logger.debug(`Predicting resource trends for next ${hours} hours`);
|
||||||
|
|
||||||
|
const history = this.getResourceHistory('day');
|
||||||
|
|
||||||
|
if (history.length < 10) {
|
||||||
|
return []; // 数据不足,无法预测
|
||||||
|
}
|
||||||
|
|
||||||
|
const predictions: ResourcePrediction[] = [];
|
||||||
|
|
||||||
|
// 预测CPU使用率
|
||||||
|
const cpuTrend = this.calculateLinearTrend(history.map((h) => h.cpu.usage));
|
||||||
|
predictions.push({
|
||||||
|
resource: 'cpu',
|
||||||
|
metric: 'usage',
|
||||||
|
currentValue: history[history.length - 1].cpu.usage,
|
||||||
|
predictedValue: Math.max(0, Math.min(100, cpuTrend.predict(hours))),
|
||||||
|
confidence: cpuTrend.confidence,
|
||||||
|
trend: cpuTrend.direction,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 预测内存使用率
|
||||||
|
const memoryTrend = this.calculateLinearTrend(
|
||||||
|
history.map((h) => h.memory.usage),
|
||||||
|
);
|
||||||
|
predictions.push({
|
||||||
|
resource: 'memory',
|
||||||
|
metric: 'usage',
|
||||||
|
currentValue: history[history.length - 1].memory.usage,
|
||||||
|
predictedValue: Math.max(0, Math.min(100, memoryTrend.predict(hours))),
|
||||||
|
confidence: memoryTrend.confidence,
|
||||||
|
trend: memoryTrend.direction,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 预测磁盘使用率
|
||||||
|
const diskTrend = this.calculateLinearTrend(
|
||||||
|
history.map((h) => h.disk.usage),
|
||||||
|
);
|
||||||
|
predictions.push({
|
||||||
|
resource: 'disk',
|
||||||
|
metric: 'usage',
|
||||||
|
currentValue: history[history.length - 1].disk.usage,
|
||||||
|
predictedValue: Math.max(0, Math.min(100, diskTrend.predict(hours))),
|
||||||
|
confidence: diskTrend.confidence,
|
||||||
|
trend: diskTrend.direction,
|
||||||
|
});
|
||||||
|
|
||||||
|
return predictions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收集资源快照
|
||||||
|
*/
|
||||||
|
private async collectResourceSnapshot(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const snapshot = await this.captureResourceSnapshot();
|
||||||
|
|
||||||
|
// 保存到历史记录
|
||||||
|
this.resourceHistory.push(snapshot);
|
||||||
|
|
||||||
|
// 保持最近1000个快照
|
||||||
|
if (this.resourceHistory.length > 1000) {
|
||||||
|
this.resourceHistory.splice(0, this.resourceHistory.length - 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查告警
|
||||||
|
const alerts = this.checkAlerts(snapshot);
|
||||||
|
if (alerts.length > 0) {
|
||||||
|
this.handleAlerts(alerts, snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送监控事件
|
||||||
|
this.eventBus.emit('resource.snapshot', snapshot);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Failed to collect resource snapshot', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 捕获资源快照
|
||||||
|
*/
|
||||||
|
private async captureResourceSnapshot(): Promise<ResourceSnapshot> {
|
||||||
|
// 模拟获取系统资源信息
|
||||||
|
// 在实际实现中,这里应该调用系统API获取真实数据
|
||||||
|
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
return {
|
||||||
|
timestamp,
|
||||||
|
cpu: {
|
||||||
|
usage: Math.random() * 100,
|
||||||
|
cores: 8,
|
||||||
|
loadAverage: [Math.random() * 2, Math.random() * 2, Math.random() * 2],
|
||||||
|
},
|
||||||
|
memory: {
|
||||||
|
usage: Math.random() * 100,
|
||||||
|
total: 16 * 1024 * 1024 * 1024, // 16GB
|
||||||
|
used: Math.random() * 16 * 1024 * 1024 * 1024,
|
||||||
|
free: Math.random() * 16 * 1024 * 1024 * 1024,
|
||||||
|
cached: Math.random() * 4 * 1024 * 1024 * 1024,
|
||||||
|
},
|
||||||
|
disk: {
|
||||||
|
usage: Math.random() * 100,
|
||||||
|
total: 1024 * 1024 * 1024 * 1024, // 1TB
|
||||||
|
used: Math.random() * 1024 * 1024 * 1024 * 1024,
|
||||||
|
free: Math.random() * 1024 * 1024 * 1024 * 1024,
|
||||||
|
ioRead: Math.random() * 1000,
|
||||||
|
ioWrite: Math.random() * 1000,
|
||||||
|
},
|
||||||
|
network: {
|
||||||
|
bytesIn: Math.random() * 1000000,
|
||||||
|
bytesOut: Math.random() * 1000000,
|
||||||
|
packetsIn: Math.random() * 10000,
|
||||||
|
packetsOut: Math.random() * 10000,
|
||||||
|
errors: Math.random() * 10,
|
||||||
|
},
|
||||||
|
processes: {
|
||||||
|
total: Math.floor(Math.random() * 500) + 100,
|
||||||
|
running: Math.floor(Math.random() * 50) + 10,
|
||||||
|
sleeping: Math.floor(Math.random() * 400) + 50,
|
||||||
|
zombie: Math.floor(Math.random() * 5),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查告警
|
||||||
|
*/
|
||||||
|
private checkAlerts(snapshot: ResourceSnapshot): ResourceAlert[] {
|
||||||
|
const alerts: ResourceAlert[] = [];
|
||||||
|
|
||||||
|
// CPU告警
|
||||||
|
if (snapshot.cpu.usage > this.alertThresholds.cpu.critical) {
|
||||||
|
alerts.push({
|
||||||
|
type: 'cpu',
|
||||||
|
severity: 'critical',
|
||||||
|
message: `CPU usage is critically high: ${snapshot.cpu.usage.toFixed(1)}%`,
|
||||||
|
value: snapshot.cpu.usage,
|
||||||
|
threshold: this.alertThresholds.cpu.critical,
|
||||||
|
timestamp: snapshot.timestamp,
|
||||||
|
});
|
||||||
|
} else if (snapshot.cpu.usage > this.alertThresholds.cpu.warning) {
|
||||||
|
alerts.push({
|
||||||
|
type: 'cpu',
|
||||||
|
severity: 'warning',
|
||||||
|
message: `CPU usage is high: ${snapshot.cpu.usage.toFixed(1)}%`,
|
||||||
|
value: snapshot.cpu.usage,
|
||||||
|
threshold: this.alertThresholds.cpu.warning,
|
||||||
|
timestamp: snapshot.timestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内存告警
|
||||||
|
if (snapshot.memory.usage > this.alertThresholds.memory.critical) {
|
||||||
|
alerts.push({
|
||||||
|
type: 'memory',
|
||||||
|
severity: 'critical',
|
||||||
|
message: `Memory usage is critically high: ${snapshot.memory.usage.toFixed(1)}%`,
|
||||||
|
value: snapshot.memory.usage,
|
||||||
|
threshold: this.alertThresholds.memory.critical,
|
||||||
|
timestamp: snapshot.timestamp,
|
||||||
|
});
|
||||||
|
} else if (snapshot.memory.usage > this.alertThresholds.memory.warning) {
|
||||||
|
alerts.push({
|
||||||
|
type: 'memory',
|
||||||
|
severity: 'warning',
|
||||||
|
message: `Memory usage is high: ${snapshot.memory.usage.toFixed(1)}%`,
|
||||||
|
value: snapshot.memory.usage,
|
||||||
|
threshold: this.alertThresholds.memory.warning,
|
||||||
|
timestamp: snapshot.timestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 磁盘告警
|
||||||
|
if (snapshot.disk.usage > this.alertThresholds.disk.critical) {
|
||||||
|
alerts.push({
|
||||||
|
type: 'disk',
|
||||||
|
severity: 'critical',
|
||||||
|
message: `Disk usage is critically high: ${snapshot.disk.usage.toFixed(1)}%`,
|
||||||
|
value: snapshot.disk.usage,
|
||||||
|
threshold: this.alertThresholds.disk.critical,
|
||||||
|
timestamp: snapshot.timestamp,
|
||||||
|
});
|
||||||
|
} else if (snapshot.disk.usage > this.alertThresholds.disk.warning) {
|
||||||
|
alerts.push({
|
||||||
|
type: 'disk',
|
||||||
|
severity: 'warning',
|
||||||
|
message: `Disk usage is high: ${snapshot.disk.usage.toFixed(1)}%`,
|
||||||
|
value: snapshot.disk.usage,
|
||||||
|
threshold: this.alertThresholds.disk.warning,
|
||||||
|
timestamp: snapshot.timestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return alerts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理告警
|
||||||
|
*/
|
||||||
|
private handleAlerts(
|
||||||
|
alerts: ResourceAlert[],
|
||||||
|
snapshot: ResourceSnapshot,
|
||||||
|
): void {
|
||||||
|
alerts.forEach((alert) => {
|
||||||
|
this.logger.warn(`Resource Alert: ${alert.message}`);
|
||||||
|
|
||||||
|
// 发送告警事件
|
||||||
|
this.eventBus.emit('resource.alert', {
|
||||||
|
alert,
|
||||||
|
snapshot,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评估资源健康状态
|
||||||
|
*/
|
||||||
|
private assessResourceHealth(
|
||||||
|
snapshot: ResourceSnapshot,
|
||||||
|
alerts: ResourceAlert[],
|
||||||
|
): ResourceHealth {
|
||||||
|
const criticalAlerts = alerts.filter(
|
||||||
|
(a) => a.severity === 'critical',
|
||||||
|
).length;
|
||||||
|
const warningAlerts = alerts.filter((a) => a.severity === 'warning').length;
|
||||||
|
|
||||||
|
let status: 'healthy' | 'warning' | 'critical';
|
||||||
|
let score = 100;
|
||||||
|
|
||||||
|
if (criticalAlerts > 0) {
|
||||||
|
status = 'critical';
|
||||||
|
score = Math.max(0, 100 - criticalAlerts * 30 - warningAlerts * 10);
|
||||||
|
} else if (warningAlerts > 0) {
|
||||||
|
status = 'warning';
|
||||||
|
score = Math.max(50, 100 - warningAlerts * 15);
|
||||||
|
} else {
|
||||||
|
status = 'healthy';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
score,
|
||||||
|
criticalAlerts,
|
||||||
|
warningAlerts,
|
||||||
|
lastCheck: snapshot.timestamp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算趋势
|
||||||
|
*/
|
||||||
|
private calculateTrends(): ResourceTrends {
|
||||||
|
const recentHistory = this.resourceHistory.slice(-20); // 最近20个快照
|
||||||
|
|
||||||
|
if (recentHistory.length < 2) {
|
||||||
|
return {
|
||||||
|
cpu: 'stable',
|
||||||
|
memory: 'stable',
|
||||||
|
disk: 'stable',
|
||||||
|
network: 'stable',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const cpuTrend = this.calculateTrendDirection(
|
||||||
|
recentHistory.map((h) => h.cpu.usage),
|
||||||
|
);
|
||||||
|
const memoryTrend = this.calculateTrendDirection(
|
||||||
|
recentHistory.map((h) => h.memory.usage),
|
||||||
|
);
|
||||||
|
const diskTrend = this.calculateTrendDirection(
|
||||||
|
recentHistory.map((h) => h.disk.usage),
|
||||||
|
);
|
||||||
|
const networkTrend = this.calculateTrendDirection(
|
||||||
|
recentHistory.map((h) => h.network.bytesIn + h.network.bytesOut),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
cpu: cpuTrend,
|
||||||
|
memory: memoryTrend,
|
||||||
|
disk: diskTrend,
|
||||||
|
network: networkTrend,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算趋势方向
|
||||||
|
*/
|
||||||
|
private calculateTrendDirection(
|
||||||
|
values: number[],
|
||||||
|
): 'increasing' | 'decreasing' | 'stable' {
|
||||||
|
if (values.length < 2) return 'stable';
|
||||||
|
|
||||||
|
const first = values[0];
|
||||||
|
const last = values[values.length - 1];
|
||||||
|
const diff = ((last - first) / first) * 100;
|
||||||
|
|
||||||
|
if (Math.abs(diff) < 5) return 'stable';
|
||||||
|
return diff > 0 ? 'increasing' : 'decreasing';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算线性趋势
|
||||||
|
*/
|
||||||
|
private calculateLinearTrend(values: number[]): TrendAnalysis {
|
||||||
|
if (values.length < 2) {
|
||||||
|
return {
|
||||||
|
predict: () => values[0] || 0,
|
||||||
|
confidence: 0,
|
||||||
|
direction: 'stable',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简单线性回归
|
||||||
|
const n = values.length;
|
||||||
|
const x = Array.from({ length: n }, (_, i) => i);
|
||||||
|
const y = values;
|
||||||
|
|
||||||
|
const sumX = x.reduce((sum, val) => sum + val, 0);
|
||||||
|
const sumY = y.reduce((sum, val) => sum + val, 0);
|
||||||
|
const sumXY = x.reduce((sum, val, i) => sum + val * y[i], 0);
|
||||||
|
const sumXX = x.reduce((sum, val) => sum + val * val, 0);
|
||||||
|
|
||||||
|
const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
|
||||||
|
const intercept = (sumY - slope * sumX) / n;
|
||||||
|
|
||||||
|
// 计算R²
|
||||||
|
const yMean = sumY / n;
|
||||||
|
const ssTotal = y.reduce((sum, val) => sum + Math.pow(val - yMean, 2), 0);
|
||||||
|
const ssRes = y.reduce((sum, val, i) => {
|
||||||
|
const predicted = slope * x[i] + intercept;
|
||||||
|
return sum + Math.pow(val - predicted, 2);
|
||||||
|
}, 0);
|
||||||
|
const rSquared = 1 - ssRes / ssTotal;
|
||||||
|
|
||||||
|
return {
|
||||||
|
predict: (hours: number) => slope * (n + hours) + intercept,
|
||||||
|
confidence: Math.max(0, Math.min(1, rSquared)),
|
||||||
|
direction:
|
||||||
|
Math.abs(slope) < 0.1
|
||||||
|
? 'stable'
|
||||||
|
: slope > 0
|
||||||
|
? 'increasing'
|
||||||
|
: 'decreasing',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算平均值
|
||||||
|
*/
|
||||||
|
private calculateAverage(values: number[]): number {
|
||||||
|
return values.length > 0
|
||||||
|
? values.reduce((sum, val) => sum + val, 0) / values.length
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取时间段毫秒数
|
||||||
|
*/
|
||||||
|
private getPeriodInMs(period: 'hour' | 'day' | 'week'): number {
|
||||||
|
const periods = {
|
||||||
|
hour: 60 * 60 * 1000,
|
||||||
|
day: 24 * 60 * 60 * 1000,
|
||||||
|
week: 7 * 24 * 60 * 60 * 1000,
|
||||||
|
};
|
||||||
|
return periods[period];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取默认阈值
|
||||||
|
*/
|
||||||
|
private getDefaultThresholds(): ResourceThresholds {
|
||||||
|
return {
|
||||||
|
cpu: { warning: 80, critical: 95 },
|
||||||
|
memory: { warning: 85, critical: 95 },
|
||||||
|
disk: { warning: 85, critical: 95 },
|
||||||
|
network: { warning: 80, critical: 95 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取默认配置
|
||||||
|
*/
|
||||||
|
private getDefaultConfig(): MonitoringConfig {
|
||||||
|
return {
|
||||||
|
interval: 30000, // 30秒
|
||||||
|
historySize: 1000,
|
||||||
|
enableAlerts: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取空统计数据
|
||||||
|
*/
|
||||||
|
private getEmptyStatistics(): ResourceStatistics {
|
||||||
|
return {
|
||||||
|
period: 'day',
|
||||||
|
sampleCount: 0,
|
||||||
|
cpu: { min: 0, max: 0, avg: 0, current: 0 },
|
||||||
|
memory: { min: 0, max: 0, avg: 0, current: 0 },
|
||||||
|
disk: { min: 0, max: 0, avg: 0, current: 0 },
|
||||||
|
network: {
|
||||||
|
bytesInTotal: 0,
|
||||||
|
bytesOutTotal: 0,
|
||||||
|
avgBytesIn: 0,
|
||||||
|
avgBytesOut: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类型定义
|
||||||
|
export interface ResourceSnapshot {
|
||||||
|
timestamp: number;
|
||||||
|
cpu: {
|
||||||
|
usage: number; // 百分比
|
||||||
|
cores: number;
|
||||||
|
loadAverage: [number, number, number]; // 1分钟、5分钟、15分钟
|
||||||
|
};
|
||||||
|
memory: {
|
||||||
|
usage: number; // 百分比
|
||||||
|
total: number; // 字节
|
||||||
|
used: number; // 字节
|
||||||
|
free: number; // 字节
|
||||||
|
cached: number; // 字节
|
||||||
|
};
|
||||||
|
disk: {
|
||||||
|
usage: number; // 百分比
|
||||||
|
total: number; // 字节
|
||||||
|
used: number; // 字节
|
||||||
|
free: number; // 字节
|
||||||
|
ioRead: number; // 每秒读取字节数
|
||||||
|
ioWrite: number; // 每秒写入字节数
|
||||||
|
};
|
||||||
|
network: {
|
||||||
|
bytesIn: number; // 每秒接收字节数
|
||||||
|
bytesOut: number; // 每秒发送字节数
|
||||||
|
packetsIn: number; // 每秒接收包数
|
||||||
|
packetsOut: number; // 每秒发送包数
|
||||||
|
errors: number; // 错误数
|
||||||
|
};
|
||||||
|
processes: {
|
||||||
|
total: number;
|
||||||
|
running: number;
|
||||||
|
sleeping: number;
|
||||||
|
zombie: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourceThresholds {
|
||||||
|
cpu: { warning: number; critical: number };
|
||||||
|
memory: { warning: number; critical: number };
|
||||||
|
disk: { warning: number; critical: number };
|
||||||
|
network: { warning: number; critical: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MonitoringConfig {
|
||||||
|
interval: number; // 监控间隔(毫秒)
|
||||||
|
historySize: number; // 历史记录大小
|
||||||
|
enableAlerts: boolean; // 是否启用告警
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourceAlert {
|
||||||
|
type: 'cpu' | 'memory' | 'disk' | 'network';
|
||||||
|
severity: 'warning' | 'critical';
|
||||||
|
message: string;
|
||||||
|
value: number;
|
||||||
|
threshold: number;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourceStatus {
|
||||||
|
timestamp: number;
|
||||||
|
snapshot: ResourceSnapshot;
|
||||||
|
alerts: ResourceAlert[];
|
||||||
|
trends: ResourceTrends;
|
||||||
|
health: ResourceHealth;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourceTrends {
|
||||||
|
cpu: 'increasing' | 'decreasing' | 'stable';
|
||||||
|
memory: 'increasing' | 'decreasing' | 'stable';
|
||||||
|
disk: 'increasing' | 'decreasing' | 'stable';
|
||||||
|
network: 'increasing' | 'decreasing' | 'stable';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourceHealth {
|
||||||
|
status: 'healthy' | 'warning' | 'critical';
|
||||||
|
score: number; // 0-100
|
||||||
|
criticalAlerts: number;
|
||||||
|
warningAlerts: number;
|
||||||
|
lastCheck: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourceStatistics {
|
||||||
|
period: 'hour' | 'day' | 'week';
|
||||||
|
sampleCount: number;
|
||||||
|
cpu: { min: number; max: number; avg: number; current: number };
|
||||||
|
memory: { min: number; max: number; avg: number; current: number };
|
||||||
|
disk: { min: number; max: number; avg: number; current: number };
|
||||||
|
network: {
|
||||||
|
bytesInTotal: number;
|
||||||
|
bytesOutTotal: number;
|
||||||
|
avgBytesIn: number;
|
||||||
|
avgBytesOut: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourcePrediction {
|
||||||
|
resource: 'cpu' | 'memory' | 'disk' | 'network';
|
||||||
|
metric: string;
|
||||||
|
currentValue: number;
|
||||||
|
predictedValue: number;
|
||||||
|
confidence: number; // 0-1
|
||||||
|
trend: 'increasing' | 'decreasing' | 'stable';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TrendAnalysis {
|
||||||
|
predict: (hours: number) => number;
|
||||||
|
confidence: number;
|
||||||
|
direction: 'increasing' | 'decreasing' | 'stable';
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,629 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI Metrics Service - AI指标服务
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* - 收集和聚合性能指标
|
||||||
|
* - 提供指标查询接口
|
||||||
|
* - 管理指标存储和清理
|
||||||
|
* - 生成指标报告
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AiMetricsService {
|
||||||
|
private readonly logger = new Logger(AiMetricsService.name);
|
||||||
|
private readonly metricsStore = new Map<string, MetricData[]>();
|
||||||
|
private readonly aggregatedMetrics = new Map<string, AggregatedMetric>();
|
||||||
|
private readonly retentionPeriod = 30 * 24 * 60 * 60 * 1000; // 30天
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录指标数据
|
||||||
|
*/
|
||||||
|
recordMetric(
|
||||||
|
name: string,
|
||||||
|
value: number,
|
||||||
|
tags: Record<string, string> = {},
|
||||||
|
): void {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const metric: MetricData = {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
timestamp,
|
||||||
|
tags,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 存储原始指标
|
||||||
|
if (!this.metricsStore.has(name)) {
|
||||||
|
this.metricsStore.set(name, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const metrics = this.metricsStore.get(name)!;
|
||||||
|
metrics.push(metric);
|
||||||
|
|
||||||
|
// 保持数据量在合理范围内
|
||||||
|
if (metrics.length > 10000) {
|
||||||
|
metrics.splice(0, metrics.length - 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新聚合指标
|
||||||
|
this.updateAggregatedMetric(name, value, timestamp);
|
||||||
|
|
||||||
|
this.logger.debug(`Recorded metric: ${name} = ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量记录指标
|
||||||
|
*/
|
||||||
|
recordMetrics(
|
||||||
|
metrics: Array<{
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
tags?: Record<string, string>;
|
||||||
|
}>,
|
||||||
|
): void {
|
||||||
|
metrics.forEach((metric) => {
|
||||||
|
this.recordMetric(metric.name, metric.value, metric.tags);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指标数据
|
||||||
|
*/
|
||||||
|
getMetrics(name: string, options: MetricQueryOptions = {}): MetricData[] {
|
||||||
|
const metrics = this.metricsStore.get(name) || [];
|
||||||
|
|
||||||
|
let filteredMetrics = metrics;
|
||||||
|
|
||||||
|
// 时间范围过滤
|
||||||
|
if (options.startTime || options.endTime) {
|
||||||
|
filteredMetrics = metrics.filter((metric) => {
|
||||||
|
if (options.startTime && metric.timestamp < options.startTime)
|
||||||
|
return false;
|
||||||
|
if (options.endTime && metric.timestamp > options.endTime) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标签过滤
|
||||||
|
if (options.tags) {
|
||||||
|
filteredMetrics = filteredMetrics.filter((metric) => {
|
||||||
|
return Object.entries(options.tags!).every(
|
||||||
|
([key, value]) => metric.tags[key] === value,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 限制数量
|
||||||
|
if (options.limit) {
|
||||||
|
filteredMetrics = filteredMetrics.slice(-options.limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredMetrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取聚合指标
|
||||||
|
*/
|
||||||
|
getAggregatedMetric(name: string): AggregatedMetric | null {
|
||||||
|
return this.aggregatedMetrics.get(name) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有指标名称
|
||||||
|
*/
|
||||||
|
getMetricNames(): string[] {
|
||||||
|
return Array.from(this.metricsStore.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算指标统计信息
|
||||||
|
*/
|
||||||
|
calculateStatistics(
|
||||||
|
name: string,
|
||||||
|
timeRange?: TimeRange,
|
||||||
|
): MetricStatistics | null {
|
||||||
|
const metrics = this.getMetrics(name, {
|
||||||
|
startTime: timeRange?.start,
|
||||||
|
endTime: timeRange?.end,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (metrics.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = metrics.map((m) => m.value);
|
||||||
|
const sortedValues = [...values].sort((a, b) => a - b);
|
||||||
|
|
||||||
|
const sum = values.reduce((acc, val) => acc + val, 0);
|
||||||
|
const mean = sum / values.length;
|
||||||
|
const variance =
|
||||||
|
values.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) /
|
||||||
|
values.length;
|
||||||
|
const stdDev = Math.sqrt(variance);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
count: values.length,
|
||||||
|
sum,
|
||||||
|
mean,
|
||||||
|
min: sortedValues[0],
|
||||||
|
max: sortedValues[sortedValues.length - 1],
|
||||||
|
median: this.calculatePercentile(sortedValues, 50),
|
||||||
|
p95: this.calculatePercentile(sortedValues, 95),
|
||||||
|
p99: this.calculatePercentile(sortedValues, 99),
|
||||||
|
stdDev,
|
||||||
|
variance,
|
||||||
|
timeRange: {
|
||||||
|
start: metrics[0].timestamp,
|
||||||
|
end: metrics[metrics.length - 1].timestamp,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指标趋势
|
||||||
|
*/
|
||||||
|
getMetricTrend(name: string, interval: number = 60000): MetricTrend[] {
|
||||||
|
const metrics = this.getMetrics(name);
|
||||||
|
if (metrics.length === 0) return [];
|
||||||
|
|
||||||
|
const trends: MetricTrend[] = [];
|
||||||
|
const startTime = metrics[0].timestamp;
|
||||||
|
const endTime = metrics[metrics.length - 1].timestamp;
|
||||||
|
|
||||||
|
for (let time = startTime; time <= endTime; time += interval) {
|
||||||
|
const intervalMetrics = metrics.filter(
|
||||||
|
(m) => m.timestamp >= time && m.timestamp < time + interval,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (intervalMetrics.length > 0) {
|
||||||
|
const values = intervalMetrics.map((m) => m.value);
|
||||||
|
const sum = values.reduce((acc, val) => acc + val, 0);
|
||||||
|
const avg = sum / values.length;
|
||||||
|
|
||||||
|
trends.push({
|
||||||
|
timestamp: time,
|
||||||
|
value: avg,
|
||||||
|
count: values.length,
|
||||||
|
min: Math.min(...values),
|
||||||
|
max: Math.max(...values),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return trends;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成指标报告
|
||||||
|
*/
|
||||||
|
generateMetricsReport(options: ReportOptions = {}): MetricsReport {
|
||||||
|
const reportTime = Date.now();
|
||||||
|
const timeRange = options.timeRange || {
|
||||||
|
start: reportTime - 24 * 60 * 60 * 1000, // 默认24小时
|
||||||
|
end: reportTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
const metricNames = options.metrics || this.getMetricNames();
|
||||||
|
const metricReports: MetricReport[] = [];
|
||||||
|
|
||||||
|
for (const name of metricNames) {
|
||||||
|
const statistics = this.calculateStatistics(name, timeRange);
|
||||||
|
if (statistics) {
|
||||||
|
const trend = this.getMetricTrend(name, options.trendInterval);
|
||||||
|
const alerts = this.checkMetricAlerts(name, statistics);
|
||||||
|
|
||||||
|
metricReports.push({
|
||||||
|
name,
|
||||||
|
statistics,
|
||||||
|
trend,
|
||||||
|
alerts,
|
||||||
|
status: alerts.length > 0 ? 'warning' : 'healthy',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
timestamp: reportTime,
|
||||||
|
timeRange,
|
||||||
|
metrics: metricReports,
|
||||||
|
summary: this.generateReportSummary(metricReports),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置指标告警
|
||||||
|
*/
|
||||||
|
setMetricAlert(name: string, alert: MetricAlert): void {
|
||||||
|
if (!this.metricAlerts.has(name)) {
|
||||||
|
this.metricAlerts.set(name, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const alerts = this.metricAlerts.get(name)!;
|
||||||
|
alerts.push(alert);
|
||||||
|
|
||||||
|
this.logger.log(`Set alert for metric ${name}: ${alert.condition}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查指标告警
|
||||||
|
*/
|
||||||
|
checkMetricAlerts(
|
||||||
|
name: string,
|
||||||
|
statistics: MetricStatistics,
|
||||||
|
): MetricAlertResult[] {
|
||||||
|
const alerts = this.metricAlerts.get(name) || [];
|
||||||
|
const results: MetricAlertResult[] = [];
|
||||||
|
|
||||||
|
for (const alert of alerts) {
|
||||||
|
const triggered = this.evaluateAlertCondition(alert, statistics);
|
||||||
|
|
||||||
|
if (triggered) {
|
||||||
|
results.push({
|
||||||
|
alertId: alert.id,
|
||||||
|
name: alert.name,
|
||||||
|
severity: alert.severity,
|
||||||
|
message: alert.message,
|
||||||
|
triggeredAt: Date.now(),
|
||||||
|
value: this.getAlertValue(alert, statistics),
|
||||||
|
threshold: alert.threshold,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理过期指标
|
||||||
|
*/
|
||||||
|
cleanupExpiredMetrics(): void {
|
||||||
|
const cutoffTime = Date.now() - this.retentionPeriod;
|
||||||
|
let totalCleaned = 0;
|
||||||
|
|
||||||
|
for (const [name, metrics] of this.metricsStore.entries()) {
|
||||||
|
const originalLength = metrics.length;
|
||||||
|
const filteredMetrics = metrics.filter((m) => m.timestamp > cutoffTime);
|
||||||
|
|
||||||
|
if (filteredMetrics.length !== originalLength) {
|
||||||
|
this.metricsStore.set(name, filteredMetrics);
|
||||||
|
totalCleaned += originalLength - filteredMetrics.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalCleaned > 0) {
|
||||||
|
this.logger.log(`Cleaned up ${totalCleaned} expired metrics`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取系统指标概览
|
||||||
|
*/
|
||||||
|
getSystemMetricsOverview(): SystemMetricsOverview {
|
||||||
|
const totalMetrics = Array.from(this.metricsStore.values()).reduce(
|
||||||
|
(sum, metrics) => sum + metrics.length,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeMetrics = this.getMetricNames().filter((name) => {
|
||||||
|
const metrics = this.metricsStore.get(name) || [];
|
||||||
|
const recentMetrics = metrics.filter(
|
||||||
|
(m) => m.timestamp > Date.now() - 5 * 60 * 1000, // 最近5分钟
|
||||||
|
);
|
||||||
|
return recentMetrics.length > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const memoryUsage = this.calculateMemoryUsage();
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalMetrics,
|
||||||
|
activeMetrics: activeMetrics.length,
|
||||||
|
metricNames: this.getMetricNames(),
|
||||||
|
memoryUsage,
|
||||||
|
retentionPeriod: this.retentionPeriod,
|
||||||
|
lastCleanup: this.lastCleanupTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出指标数据
|
||||||
|
*/
|
||||||
|
exportMetrics(options: ExportOptions = {}): ExportedMetrics {
|
||||||
|
const metricNames = options.metrics || this.getMetricNames();
|
||||||
|
const exportedData: Record<string, MetricData[]> = {};
|
||||||
|
|
||||||
|
for (const name of metricNames) {
|
||||||
|
exportedData[name] = this.getMetrics(name, {
|
||||||
|
startTime: options.timeRange?.start,
|
||||||
|
endTime: options.timeRange?.end,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
timeRange: options.timeRange,
|
||||||
|
format: options.format || 'json',
|
||||||
|
data: exportedData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入指标数据
|
||||||
|
*/
|
||||||
|
importMetrics(exportedMetrics: ExportedMetrics): void {
|
||||||
|
for (const [name, metrics] of Object.entries(exportedMetrics.data)) {
|
||||||
|
for (const metric of metrics) {
|
||||||
|
this.recordMetric(metric.name, metric.value, metric.tags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Imported metrics for ${Object.keys(exportedMetrics.data).length} metric types`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 私有方法
|
||||||
|
|
||||||
|
private readonly metricAlerts = new Map<string, MetricAlert[]>();
|
||||||
|
private lastCleanupTime = Date.now();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新聚合指标
|
||||||
|
*/
|
||||||
|
private updateAggregatedMetric(
|
||||||
|
name: string,
|
||||||
|
value: number,
|
||||||
|
timestamp: number,
|
||||||
|
): void {
|
||||||
|
let aggregated = this.aggregatedMetrics.get(name);
|
||||||
|
|
||||||
|
if (!aggregated) {
|
||||||
|
aggregated = {
|
||||||
|
name,
|
||||||
|
count: 0,
|
||||||
|
sum: 0,
|
||||||
|
min: value,
|
||||||
|
max: value,
|
||||||
|
lastValue: value,
|
||||||
|
lastUpdate: timestamp,
|
||||||
|
};
|
||||||
|
this.aggregatedMetrics.set(name, aggregated);
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregated.count++;
|
||||||
|
aggregated.sum += value;
|
||||||
|
aggregated.min = Math.min(aggregated.min, value);
|
||||||
|
aggregated.max = Math.max(aggregated.max, value);
|
||||||
|
aggregated.lastValue = value;
|
||||||
|
aggregated.lastUpdate = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算百分位数
|
||||||
|
*/
|
||||||
|
private calculatePercentile(
|
||||||
|
sortedValues: number[],
|
||||||
|
percentile: number,
|
||||||
|
): number {
|
||||||
|
const index = (percentile / 100) * (sortedValues.length - 1);
|
||||||
|
const lower = Math.floor(index);
|
||||||
|
const upper = Math.ceil(index);
|
||||||
|
|
||||||
|
if (lower === upper) {
|
||||||
|
return sortedValues[lower];
|
||||||
|
}
|
||||||
|
|
||||||
|
const weight = index - lower;
|
||||||
|
return sortedValues[lower] * (1 - weight) + sortedValues[upper] * weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成报告摘要
|
||||||
|
*/
|
||||||
|
private generateReportSummary(metricReports: MetricReport[]): ReportSummary {
|
||||||
|
const totalMetrics = metricReports.length;
|
||||||
|
const healthyMetrics = metricReports.filter(
|
||||||
|
(m) => m.status === 'healthy',
|
||||||
|
).length;
|
||||||
|
const warningMetrics = metricReports.filter(
|
||||||
|
(m) => m.status === 'warning',
|
||||||
|
).length;
|
||||||
|
const totalAlerts = metricReports.reduce(
|
||||||
|
(sum, m) => sum + m.alerts.length,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalMetrics,
|
||||||
|
healthyMetrics,
|
||||||
|
warningMetrics,
|
||||||
|
totalAlerts,
|
||||||
|
overallHealth: warningMetrics === 0 ? 'healthy' : 'warning',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评估告警条件
|
||||||
|
*/
|
||||||
|
private evaluateAlertCondition(
|
||||||
|
alert: MetricAlert,
|
||||||
|
statistics: MetricStatistics,
|
||||||
|
): boolean {
|
||||||
|
const value = this.getAlertValue(alert, statistics);
|
||||||
|
|
||||||
|
switch (alert.condition) {
|
||||||
|
case 'greater_than':
|
||||||
|
return value > alert.threshold;
|
||||||
|
case 'less_than':
|
||||||
|
return value < alert.threshold;
|
||||||
|
case 'equals':
|
||||||
|
return value === alert.threshold;
|
||||||
|
case 'not_equals':
|
||||||
|
return value !== alert.threshold;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取告警值
|
||||||
|
*/
|
||||||
|
private getAlertValue(
|
||||||
|
alert: MetricAlert,
|
||||||
|
statistics: MetricStatistics,
|
||||||
|
): number {
|
||||||
|
switch (alert.metric) {
|
||||||
|
case 'mean':
|
||||||
|
return statistics.mean;
|
||||||
|
case 'max':
|
||||||
|
return statistics.max;
|
||||||
|
case 'min':
|
||||||
|
return statistics.min;
|
||||||
|
case 'p95':
|
||||||
|
return statistics.p95;
|
||||||
|
case 'p99':
|
||||||
|
return statistics.p99;
|
||||||
|
case 'count':
|
||||||
|
return statistics.count;
|
||||||
|
default:
|
||||||
|
return statistics.mean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算内存使用量
|
||||||
|
*/
|
||||||
|
private calculateMemoryUsage(): number {
|
||||||
|
let totalSize = 0;
|
||||||
|
|
||||||
|
for (const metrics of this.metricsStore.values()) {
|
||||||
|
totalSize += metrics.length * 100; // 估算每个指标数据点约100字节
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类型定义
|
||||||
|
export interface MetricData {
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
timestamp: number;
|
||||||
|
tags: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AggregatedMetric {
|
||||||
|
name: string;
|
||||||
|
count: number;
|
||||||
|
sum: number;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
lastValue: number;
|
||||||
|
lastUpdate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetricQueryOptions {
|
||||||
|
startTime?: number;
|
||||||
|
endTime?: number;
|
||||||
|
tags?: Record<string, string>;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimeRange {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetricStatistics {
|
||||||
|
name: string;
|
||||||
|
count: number;
|
||||||
|
sum: number;
|
||||||
|
mean: number;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
median: number;
|
||||||
|
p95: number;
|
||||||
|
p99: number;
|
||||||
|
stdDev: number;
|
||||||
|
variance: number;
|
||||||
|
timeRange: TimeRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetricTrend {
|
||||||
|
timestamp: number;
|
||||||
|
value: number;
|
||||||
|
count: number;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportOptions {
|
||||||
|
timeRange?: TimeRange;
|
||||||
|
metrics?: string[];
|
||||||
|
trendInterval?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetricReport {
|
||||||
|
name: string;
|
||||||
|
statistics: MetricStatistics;
|
||||||
|
trend: MetricTrend[];
|
||||||
|
alerts: MetricAlertResult[];
|
||||||
|
status: 'healthy' | 'warning' | 'critical';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetricsReport {
|
||||||
|
timestamp: number;
|
||||||
|
timeRange: TimeRange;
|
||||||
|
metrics: MetricReport[];
|
||||||
|
summary: ReportSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportSummary {
|
||||||
|
totalMetrics: number;
|
||||||
|
healthyMetrics: number;
|
||||||
|
warningMetrics: number;
|
||||||
|
totalAlerts: number;
|
||||||
|
overallHealth: 'healthy' | 'warning' | 'critical';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetricAlert {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
condition: 'greater_than' | 'less_than' | 'equals' | 'not_equals';
|
||||||
|
threshold: number;
|
||||||
|
metric: 'mean' | 'max' | 'min' | 'p95' | 'p99' | 'count';
|
||||||
|
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetricAlertResult {
|
||||||
|
alertId: string;
|
||||||
|
name: string;
|
||||||
|
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
message: string;
|
||||||
|
triggeredAt: number;
|
||||||
|
value: number;
|
||||||
|
threshold: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemMetricsOverview {
|
||||||
|
totalMetrics: number;
|
||||||
|
activeMetrics: number;
|
||||||
|
metricNames: string[];
|
||||||
|
memoryUsage: number;
|
||||||
|
retentionPeriod: number;
|
||||||
|
lastCleanup: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExportOptions {
|
||||||
|
timeRange?: TimeRange;
|
||||||
|
metrics?: string[];
|
||||||
|
format?: 'json' | 'csv' | 'xml';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExportedMetrics {
|
||||||
|
timestamp: number;
|
||||||
|
timeRange?: TimeRange;
|
||||||
|
format: string;
|
||||||
|
data: Record<string, MetricData[]>;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
|||||||
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||||
|
import { PerformanceAnalyzer } from '../analyzers/performance.analyzer';
|
||||||
|
import { ResourceMonitor } from '../monitors/resource.monitor';
|
||||||
|
import { CacheOptimizer } from '../optimizers/cache.optimizer';
|
||||||
|
import { QueryOptimizer } from '../optimizers/query.optimizer';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TunerReadyService implements OnModuleInit {
|
||||||
|
private readonly logger = new Logger(TunerReadyService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly eventBus: EventBus,
|
||||||
|
private readonly config: ConfigService,
|
||||||
|
private readonly performanceAnalyzer: PerformanceAnalyzer,
|
||||||
|
private readonly resourceMonitor: ResourceMonitor,
|
||||||
|
private readonly cacheOptimizer: CacheOptimizer,
|
||||||
|
private readonly queryOptimizer: QueryOptimizer,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
const enabled =
|
||||||
|
(this.config.get<string>('AI_TUNER_ENABLED') ?? 'true') === 'true';
|
||||||
|
let currentState: 'ready' | 'unavailable' = 'unavailable';
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (enabled) {
|
||||||
|
// 轻量健康检查:核心组件是否注入到位
|
||||||
|
const componentsOk = [
|
||||||
|
this.performanceAnalyzer,
|
||||||
|
this.resourceMonitor,
|
||||||
|
this.cacheOptimizer,
|
||||||
|
this.queryOptimizer,
|
||||||
|
].every((c) => !!c);
|
||||||
|
|
||||||
|
currentState = componentsOk ? 'ready' : 'unavailable';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.warn(
|
||||||
|
`AI Tuner readiness check failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
|
);
|
||||||
|
currentState = 'unavailable';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.eventBus.emit('module.state.changed', {
|
||||||
|
module: 'ai.tuner',
|
||||||
|
previousState: 'initializing',
|
||||||
|
currentState,
|
||||||
|
meta: { enabled },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
46
wwjcloud-nest-v1/libs/wwjcloud-ai/src/tuner/tuner.module.ts
Normal file
46
wwjcloud-nest-v1/libs/wwjcloud-ai/src/tuner/tuner.module.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { PerformanceAnalyzer } from './analyzers/performance.analyzer';
|
||||||
|
import { ResourceMonitor } from './monitors/resource.monitor';
|
||||||
|
import { CacheOptimizer } from './optimizers/cache.optimizer';
|
||||||
|
import { QueryOptimizer } from './optimizers/query.optimizer';
|
||||||
|
import { AiTunerService } from './services/ai-tuner.service';
|
||||||
|
import { AiMetricsService } from './services/ai-metrics.service';
|
||||||
|
import { TunerReadyService } from './services/tuner-ready.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI Tuner Module - AI 性能调优模块
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* - 性能分析和监控
|
||||||
|
* - 资源使用优化
|
||||||
|
* - 缓存策略优化
|
||||||
|
* - 查询性能优化
|
||||||
|
* - 系统调优建议
|
||||||
|
*/
|
||||||
|
@Module({
|
||||||
|
providers: [
|
||||||
|
// 分析器
|
||||||
|
PerformanceAnalyzer,
|
||||||
|
|
||||||
|
// 监控器
|
||||||
|
ResourceMonitor,
|
||||||
|
|
||||||
|
// 优化器
|
||||||
|
CacheOptimizer,
|
||||||
|
QueryOptimizer,
|
||||||
|
|
||||||
|
// 服务
|
||||||
|
AiTunerService,
|
||||||
|
AiMetricsService,
|
||||||
|
TunerReadyService,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
AiTunerService,
|
||||||
|
AiMetricsService,
|
||||||
|
PerformanceAnalyzer,
|
||||||
|
ResourceMonitor,
|
||||||
|
CacheOptimizer,
|
||||||
|
QueryOptimizer,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AiTunerModule {}
|
||||||
@@ -1,27 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
import { AiManagerModule } from './manager/manager.module';
|
||||||
import { AiBootstrapProvider } from './bootstrap/ai-bootstrap.provider';
|
import { AiHealingModule } from './healing/healing.module';
|
||||||
import { AiSelfHealListener } from './listeners/ai-self-heal.listener';
|
import { AiSafeModule } from './safe/safe.module';
|
||||||
import { AiRecoveryListener } from './listeners/ai-recovery.listener';
|
import { AiTunerModule } from './tuner/tuner.module';
|
||||||
import { AiRecoveryService } from './services/ai-recovery.service';
|
|
||||||
import { AiController } from './controllers/ai.controller';
|
|
||||||
import { RateLimitGuard } from '@wwjCommon/http/rate-limit.guard';
|
|
||||||
import { BootMetricsModule } from '@wwjCommon/metrics/boot-metrics.module';
|
|
||||||
import { BootCacheModule } from '@wwjCommon/cache/boot-cache.module';
|
|
||||||
import { BootModule } from '@wwjBoot/wwjcloud-boot.module';
|
|
||||||
import { AiStrategyService } from './services/ai-strategy.service';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [BootModule, EventEmitterModule, BootMetricsModule, BootCacheModule],
|
imports: [AiManagerModule, AiHealingModule, AiSafeModule, AiTunerModule],
|
||||||
controllers: [AiController],
|
exports: [AiManagerModule, AiHealingModule, AiSafeModule, AiTunerModule],
|
||||||
providers: [
|
|
||||||
AiBootstrapProvider,
|
|
||||||
AiSelfHealListener,
|
|
||||||
AiRecoveryListener,
|
|
||||||
AiRecoveryService,
|
|
||||||
AiStrategyService,
|
|
||||||
RateLimitGuard,
|
|
||||||
],
|
|
||||||
exports: [AiRecoveryService, AiStrategyService],
|
|
||||||
})
|
})
|
||||||
export class AiModule {}
|
export class WwjcloudAiModule {}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||||
|
|
||||||
|
function readBoolean(
|
||||||
|
config: ConfigService,
|
||||||
|
key: string,
|
||||||
|
fallback = false,
|
||||||
|
): boolean {
|
||||||
|
const v = config.get<string | boolean>(key);
|
||||||
|
if (typeof v === 'boolean') return v;
|
||||||
|
if (typeof v === 'string')
|
||||||
|
return ['true', '1', 'yes', 'on'].includes(v.toLowerCase());
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthReadyService implements OnModuleInit {
|
||||||
|
private readonly logger = new Logger(AuthReadyService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly config: ConfigService,
|
||||||
|
private readonly eventBus: EventBus,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async onModuleInit(): Promise<void> {
|
||||||
|
const authEnabled = readBoolean(this.config, 'AUTH_ENABLED', false);
|
||||||
|
const rbacEnabled = readBoolean(this.config, 'RBAC_ENABLED', false);
|
||||||
|
|
||||||
|
const authState: 'ready' | 'unavailable' = authEnabled
|
||||||
|
? 'ready'
|
||||||
|
: 'unavailable';
|
||||||
|
const rbacState: 'ready' | 'unavailable' = rbacEnabled
|
||||||
|
? 'ready'
|
||||||
|
: 'unavailable';
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Auth module init -> AUTH_ENABLED=${authEnabled}, state=${authState}`,
|
||||||
|
);
|
||||||
|
this.eventBus.emit('module.state.changed', {
|
||||||
|
module: 'auth',
|
||||||
|
previousState: 'initializing',
|
||||||
|
currentState: authState,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`RBAC module init -> RBAC_ENABLED=${rbacEnabled}, state=${rbacState}`,
|
||||||
|
);
|
||||||
|
this.eventBus.emit('module.state.changed', {
|
||||||
|
module: 'rbac',
|
||||||
|
previousState: 'initializing',
|
||||||
|
currentState: rbacState,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
import {
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
import { AuthService, UserClaims } from './auth.service';
|
import { AuthService, UserClaims } from './auth.service';
|
||||||
import { RequestContextService } from '../http/request-context.service';
|
import { RequestContextService } from '../http/request-context.service';
|
||||||
@@ -36,7 +41,7 @@ export class AuthGuard implements CanActivate {
|
|||||||
const claims: UserClaims = this.auth.verifyToken(token);
|
const claims: UserClaims = this.auth.verifyToken(token);
|
||||||
|
|
||||||
// 挂载到 request 与请求上下文
|
// 挂载到 request 与请求上下文
|
||||||
(req as any).user = claims;
|
req.user = claims;
|
||||||
const store = this.ctx.getContext();
|
const store = this.ctx.getContext();
|
||||||
if (store) {
|
if (store) {
|
||||||
if (claims.userId) store.userId = claims.userId;
|
if (claims.userId) store.userId = claims.userId;
|
||||||
|
|||||||
@@ -31,26 +31,27 @@ export class AuthService {
|
|||||||
issuer: issuer || undefined,
|
issuer: issuer || undefined,
|
||||||
audience: audience || undefined,
|
audience: audience || undefined,
|
||||||
});
|
});
|
||||||
const obj = typeof payload === 'string' ? JSON.parse(payload) : (payload as any);
|
const obj =
|
||||||
|
typeof payload === 'string' ? JSON.parse(payload) : (payload as any);
|
||||||
const claims: UserClaims = {
|
const claims: UserClaims = {
|
||||||
userId: obj.sub || obj.userId || obj.uid || undefined,
|
userId: obj.sub || obj.userId || obj.uid || undefined,
|
||||||
username: obj.name || obj.username || obj.uname || undefined,
|
username: obj.name || obj.username || obj.uname || undefined,
|
||||||
roles: Array.isArray(obj.roles)
|
roles: Array.isArray(obj.roles)
|
||||||
? obj.roles.map((s: any) => String(s))
|
? obj.roles.map((s: any) => String(s))
|
||||||
: typeof obj.roles === 'string'
|
: typeof obj.roles === 'string'
|
||||||
? String(obj.roles)
|
? String(obj.roles)
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
.filter((s) => s.length > 0)
|
.filter((s) => s.length > 0)
|
||||||
: undefined,
|
: undefined,
|
||||||
permissions: Array.isArray(obj.permissions)
|
permissions: Array.isArray(obj.permissions)
|
||||||
? obj.permissions.map((s: any) => String(s))
|
? obj.permissions.map((s: any) => String(s))
|
||||||
: typeof obj.permissions === 'string'
|
: typeof obj.permissions === 'string'
|
||||||
? String(obj.permissions)
|
? String(obj.permissions)
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
.filter((s) => s.length > 0)
|
.filter((s) => s.length > 0)
|
||||||
: undefined,
|
: undefined,
|
||||||
tenantId: obj.tenantId || obj.siteId || undefined,
|
tenantId: obj.tenantId || obj.siteId || undefined,
|
||||||
};
|
};
|
||||||
return claims;
|
return claims;
|
||||||
@@ -62,7 +63,8 @@ export class AuthService {
|
|||||||
private readBoolean(key: string, fallback = false): boolean {
|
private readBoolean(key: string, fallback = false): boolean {
|
||||||
const v = this.config.get<string | boolean>(key);
|
const v = this.config.get<string | boolean>(key);
|
||||||
if (typeof v === 'boolean') return v;
|
if (typeof v === 'boolean') return v;
|
||||||
if (typeof v === 'string') return ['true', '1', 'yes', 'on'].includes(v.toLowerCase());
|
if (typeof v === 'string')
|
||||||
|
return ['true', '1', 'yes', 'on'].includes(v.toLowerCase());
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,11 +3,12 @@ import { ConfigModule } from '@nestjs/config';
|
|||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { AuthGuard } from './auth.guard';
|
import { AuthGuard } from './auth.guard';
|
||||||
import { RbacGuard } from './rbac.guard';
|
import { RbacGuard } from './rbac.guard';
|
||||||
|
import { AuthReadyService } from './auth-ready.service';
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
providers: [AuthService, AuthGuard, RbacGuard],
|
providers: [AuthService, AuthGuard, RbacGuard, AuthReadyService],
|
||||||
exports: [AuthService, AuthGuard, RbacGuard],
|
exports: [AuthService, AuthGuard, RbacGuard],
|
||||||
})
|
})
|
||||||
export class BootAuthModule {}
|
export class BootAuthModule {}
|
||||||
@@ -1,11 +1,19 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable, ForbiddenException } from '@nestjs/common';
|
import {
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
ForbiddenException,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { PERMISSIONS_KEY, ROLES_KEY, IS_PUBLIC_KEY } from './decorators';
|
import { PERMISSIONS_KEY, ROLES_KEY, IS_PUBLIC_KEY } from './decorators';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RbacGuard implements CanActivate {
|
export class RbacGuard implements CanActivate {
|
||||||
constructor(private readonly reflector: Reflector, private readonly config: ConfigService) {}
|
constructor(
|
||||||
|
private readonly reflector: Reflector,
|
||||||
|
private readonly config: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
// 公共路由直接放行
|
// 公共路由直接放行
|
||||||
@@ -19,30 +27,40 @@ export class RbacGuard implements CanActivate {
|
|||||||
const enabled = this.readBoolean('RBAC_ENABLED', false);
|
const enabled = this.readBoolean('RBAC_ENABLED', false);
|
||||||
if (!enabled) return true;
|
if (!enabled) return true;
|
||||||
|
|
||||||
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
|
const requiredRoles =
|
||||||
context.getHandler(),
|
this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
|
||||||
context.getClass(),
|
context.getHandler(),
|
||||||
]) || [];
|
context.getClass(),
|
||||||
const requiredPermissions = this.reflector.getAllAndOverride<string[]>(PERMISSIONS_KEY, [
|
]) || [];
|
||||||
context.getHandler(),
|
const requiredPermissions =
|
||||||
context.getClass(),
|
this.reflector.getAllAndOverride<string[]>(PERMISSIONS_KEY, [
|
||||||
]) || [];
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]) || [];
|
||||||
|
|
||||||
const req = context.switchToHttp().getRequest();
|
const req = context.switchToHttp().getRequest();
|
||||||
const user = (req as any).user || {};
|
const user = req.user || {};
|
||||||
const userRoles: string[] = Array.isArray(user.roles) ? user.roles : [];
|
const userRoles: string[] = Array.isArray(user.roles) ? user.roles : [];
|
||||||
const userPerms: string[] = Array.isArray(user.permissions) ? user.permissions : [];
|
const userPerms: string[] = Array.isArray(user.permissions)
|
||||||
|
? user.permissions
|
||||||
|
: [];
|
||||||
|
|
||||||
// 角色:至少命中一个
|
// 角色:至少命中一个
|
||||||
if (requiredRoles.length > 0) {
|
if (requiredRoles.length > 0) {
|
||||||
const ok = requiredRoles.some((r) => userRoles.includes(r));
|
const ok = requiredRoles.some((r) => userRoles.includes(r));
|
||||||
if (!ok) throw new ForbiddenException({ msg_key: 'error.auth.insufficient_role' });
|
if (!ok)
|
||||||
|
throw new ForbiddenException({
|
||||||
|
msg_key: 'error.auth.insufficient_role',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 权限:需要全部满足
|
// 权限:需要全部满足
|
||||||
if (requiredPermissions.length > 0) {
|
if (requiredPermissions.length > 0) {
|
||||||
const ok = requiredPermissions.every((p) => userPerms.includes(p));
|
const ok = requiredPermissions.every((p) => userPerms.includes(p));
|
||||||
if (!ok) throw new ForbiddenException({ msg_key: 'error.auth.insufficient_permission' });
|
if (!ok)
|
||||||
|
throw new ForbiddenException({
|
||||||
|
msg_key: 'error.auth.insufficient_permission',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -51,7 +69,8 @@ export class RbacGuard implements CanActivate {
|
|||||||
private readBoolean(key: string, fallback = false): boolean {
|
private readBoolean(key: string, fallback = false): boolean {
|
||||||
const v = this.config.get<string | boolean>(key);
|
const v = this.config.get<string | boolean>(key);
|
||||||
if (typeof v === 'boolean') return v;
|
if (typeof v === 'boolean') return v;
|
||||||
if (typeof v === 'string') return ['true', '1', 'yes', 'on'].includes(v.toLowerCase());
|
if (typeof v === 'string')
|
||||||
|
return ['true', '1', 'yes', 'on'].includes(v.toLowerCase());
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import { CacheService } from './cache.service';
|
|||||||
import { LockService } from './lock.service';
|
import { LockService } from './lock.service';
|
||||||
import { CacheController } from './cache.controller';
|
import { CacheController } from './cache.controller';
|
||||||
import { CACHE_SERVICE, LOCK_SERVICE } from './tokens';
|
import { CACHE_SERVICE, LOCK_SERVICE } from './tokens';
|
||||||
|
import { CacheReadyService } from './cache-ready.service';
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
@@ -14,6 +15,7 @@ import { CACHE_SERVICE, LOCK_SERVICE } from './tokens';
|
|||||||
RedisService,
|
RedisService,
|
||||||
CacheService,
|
CacheService,
|
||||||
LockService,
|
LockService,
|
||||||
|
CacheReadyService,
|
||||||
{ provide: CACHE_SERVICE, useExisting: CacheService },
|
{ provide: CACHE_SERVICE, useExisting: CacheService },
|
||||||
{ provide: LOCK_SERVICE, useExisting: LockService },
|
{ provide: LOCK_SERVICE, useExisting: LockService },
|
||||||
],
|
],
|
||||||
|
|||||||
38
wwjcloud-nest-v1/libs/wwjcloud-boot/src/infra/cache/cache-ready.service.ts
vendored
Normal file
38
wwjcloud-nest-v1/libs/wwjcloud-boot/src/infra/cache/cache-ready.service.ts
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||||
|
import { RedisService } from './redis.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CacheReadyService implements OnModuleInit {
|
||||||
|
private readonly logger = new Logger(CacheReadyService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly eventBus: EventBus,
|
||||||
|
private readonly redis: RedisService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async onModuleInit(): Promise<void> {
|
||||||
|
let state: 'ready' | 'unavailable' = 'ready';
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!this.redis.isEnabled()) {
|
||||||
|
// 使用内存回退,视为可用
|
||||||
|
state = 'ready';
|
||||||
|
} else {
|
||||||
|
const client = this.redis.getClient();
|
||||||
|
await client.ping();
|
||||||
|
state = 'ready';
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
this.logger.warn(`Cache readiness check failed: ${err?.message || err}`);
|
||||||
|
state = 'unavailable';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`Cache module init -> state=${state}`);
|
||||||
|
this.eventBus.emit('module.state.changed', {
|
||||||
|
module: 'cache',
|
||||||
|
previousState: 'initializing',
|
||||||
|
currentState: state,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
|
export type EventBus = EventEmitter2;
|
||||||
|
export const EventBus = EventEmitter2;
|
||||||
|
|
||||||
|
export { OnEvent } from '@nestjs/event-emitter';
|
||||||
@@ -8,12 +8,15 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { I18nService } from 'nestjs-i18n';
|
import { I18nService } from 'nestjs-i18n';
|
||||||
import { mapAlias } from '../i18n/aliases';
|
import { mapAlias } from '../lang/aliases';
|
||||||
|
|
||||||
@Catch()
|
@Catch()
|
||||||
export class HttpExceptionFilter implements ExceptionFilter {
|
export class HttpExceptionFilter implements ExceptionFilter {
|
||||||
private readonly logger = new Logger(HttpExceptionFilter.name);
|
private readonly logger = new Logger(HttpExceptionFilter.name);
|
||||||
constructor(private readonly config: ConfigService, private readonly i18n: I18nService) {}
|
constructor(
|
||||||
|
private readonly config: ConfigService,
|
||||||
|
private readonly i18n: I18nService,
|
||||||
|
) {}
|
||||||
|
|
||||||
catch(exception: unknown, host: ArgumentsHost) {
|
catch(exception: unknown, host: ArgumentsHost) {
|
||||||
const ctx = host.switchToHttp();
|
const ctx = host.switchToHttp();
|
||||||
@@ -30,21 +33,21 @@ export class HttpExceptionFilter implements ExceptionFilter {
|
|||||||
let args: Record<string, any> | undefined;
|
let args: Record<string, any> | undefined;
|
||||||
|
|
||||||
if (exception instanceof HttpException) {
|
if (exception instanceof HttpException) {
|
||||||
const res: any = exception.getResponse();
|
const res: any = exception.getResponse();
|
||||||
// 支持自定义异常响应携带 msg_key 与 args
|
// 支持自定义异常响应携带 msg_key 与 args
|
||||||
if (res && typeof res === 'object') {
|
if (res && typeof res === 'object') {
|
||||||
if (typeof res.msg_key === 'string') msgKey = res.msg_key;
|
if (typeof res.msg_key === 'string') msgKey = res.msg_key;
|
||||||
if (res.args && typeof res.args === 'object') args = res.args;
|
if (res.args && typeof res.args === 'object') args = res.args;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 历史 key 别名映射
|
// 历史 key 别名映射
|
||||||
msgKey = mapAlias(msgKey);
|
msgKey = mapAlias(msgKey);
|
||||||
|
|
||||||
const message = this.i18n.translate(msgKey, {
|
const message = this.i18n.translate(msgKey, {
|
||||||
lang: this.resolveLang(request),
|
lang: this.resolveLang(request),
|
||||||
args,
|
args,
|
||||||
});
|
});
|
||||||
|
|
||||||
const requestId =
|
const requestId =
|
||||||
request?.headers?.['x-request-id'] ||
|
request?.headers?.['x-request-id'] ||
|
||||||
@@ -95,7 +98,11 @@ export class HttpExceptionFilter implements ExceptionFilter {
|
|||||||
if (!u) return false;
|
if (!u) return false;
|
||||||
if (u.startsWith('/metrics') || u.startsWith('/health')) return true;
|
if (u.startsWith('/metrics') || u.startsWith('/health')) return true;
|
||||||
const hasPrefix = prefix.length > 0;
|
const hasPrefix = prefix.length > 0;
|
||||||
return hasPrefix && (u.startsWith(`/${prefix}/metrics`) || u.startsWith(`/${prefix}/health`));
|
return (
|
||||||
|
hasPrefix &&
|
||||||
|
(u.startsWith(`/${prefix}/metrics`) ||
|
||||||
|
u.startsWith(`/${prefix}/health`))
|
||||||
|
);
|
||||||
};
|
};
|
||||||
const infra = isInfra(url);
|
const infra = isInfra(url);
|
||||||
const preserveStatuses = new Set<number>([429]);
|
const preserveStatuses = new Set<number>([429]);
|
||||||
@@ -108,7 +115,11 @@ export class HttpExceptionFilter implements ExceptionFilter {
|
|||||||
if (!u) return false;
|
if (!u) return false;
|
||||||
if (u.startsWith('/metrics') || u.startsWith('/health')) return true;
|
if (u.startsWith('/metrics') || u.startsWith('/health')) return true;
|
||||||
const hasPrefix = prefix.length > 0;
|
const hasPrefix = prefix.length > 0;
|
||||||
return hasPrefix && (u.startsWith(`/${prefix}/metrics`) || u.startsWith(`/${prefix}/health`));
|
return (
|
||||||
|
hasPrefix &&
|
||||||
|
(u.startsWith(`/${prefix}/metrics`) ||
|
||||||
|
u.startsWith(`/${prefix}/health`))
|
||||||
|
);
|
||||||
};
|
};
|
||||||
const infra = isInfra(url);
|
const infra = isInfra(url);
|
||||||
const preserveStatuses = new Set<number>([429]);
|
const preserveStatuses = new Set<number>([429]);
|
||||||
|
|||||||
@@ -24,8 +24,14 @@ export function buildIpFilterMiddleware(config: ConfigService) {
|
|||||||
return function ipFilter(req: Request, res: Response, next: NextFunction) {
|
return function ipFilter(req: Request, res: Response, next: NextFunction) {
|
||||||
if (!enabled) return next();
|
if (!enabled) return next();
|
||||||
// best-effort to get client IP when behind proxies
|
// best-effort to get client IP when behind proxies
|
||||||
const forwarded = (req.headers['x-forwarded-for'] as string | undefined)?.split(',')[0]?.trim();
|
const forwarded = (req.headers['x-forwarded-for'] as string | undefined)
|
||||||
const ip = forwarded || (req.ip as string) || (req.connection as any)?.remoteAddress || '';
|
?.split(',')[0]
|
||||||
|
?.trim();
|
||||||
|
const ip =
|
||||||
|
forwarded ||
|
||||||
|
(req.ip as string) ||
|
||||||
|
(req.connection as any)?.remoteAddress ||
|
||||||
|
'';
|
||||||
if (blacklist.length && blacklist.includes(ip)) {
|
if (blacklist.length && blacklist.includes(ip)) {
|
||||||
return res.status(403).json({ message: 'IP forbidden' });
|
return res.status(403).json({ message: 'IP forbidden' });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,11 +45,11 @@ export class RateLimitGuard implements CanActivate {
|
|||||||
const roles: string[] = Array.isArray(req.user?.roles)
|
const roles: string[] = Array.isArray(req.user?.roles)
|
||||||
? req.user.roles
|
? req.user.roles
|
||||||
: typeof req.user?.roles === 'string'
|
: typeof req.user?.roles === 'string'
|
||||||
? String(req.user.roles)
|
? String(req.user.roles)
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
: [];
|
: [];
|
||||||
const isAdmin = roles.includes('admin');
|
const isAdmin = roles.includes('admin');
|
||||||
const limit = isAdmin ? this.adminMax : this.max;
|
const limit = isAdmin ? this.adminMax : this.max;
|
||||||
|
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ export function buildRequestContextMiddleware(ctx: RequestContextService) {
|
|||||||
|
|
||||||
const rolesHeader = req.headers['x-roles'];
|
const rolesHeader = req.headers['x-roles'];
|
||||||
const roles = Array.isArray(rolesHeader)
|
const roles = Array.isArray(rolesHeader)
|
||||||
? (rolesHeader as string[])
|
|
||||||
: typeof rolesHeader === 'string'
|
|
||||||
? rolesHeader
|
? rolesHeader
|
||||||
.split(',')
|
: typeof rolesHeader === 'string'
|
||||||
.map((s) => s.trim())
|
? rolesHeader
|
||||||
.filter((s) => s.length > 0)
|
.split(',')
|
||||||
: undefined;
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => s.length > 0)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const lang = (req.headers['x-lang'] as string) || undefined;
|
const lang = (req.headers['x-lang'] as string) || undefined;
|
||||||
const channel = (req.headers['x-channel'] as string) || undefined;
|
const channel = (req.headers['x-channel'] as string) || undefined;
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { Global, Module } from '@nestjs/common';
|
import { Global, Module } from '@nestjs/common';
|
||||||
import { I18nModule, I18nJsonLoader, HeaderResolver, QueryResolver } from 'nestjs-i18n';
|
import {
|
||||||
|
I18nModule,
|
||||||
|
I18nJsonLoader,
|
||||||
|
HeaderResolver,
|
||||||
|
QueryResolver,
|
||||||
|
} from 'nestjs-i18n';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@@ -11,7 +16,7 @@ import { join } from 'path';
|
|||||||
loaderOptions: {
|
loaderOptions: {
|
||||||
// 以项目根目录为基准,定位到 API 应用的语言资源目录
|
// 以项目根目录为基准,定位到 API 应用的语言资源目录
|
||||||
path: join(process.cwd(), 'apps/api/src/lang'),
|
path: join(process.cwd(), 'apps/api/src/lang'),
|
||||||
watch: true,
|
watch: process.env.NODE_ENV !== 'test',
|
||||||
},
|
},
|
||||||
resolvers: [
|
resolvers: [
|
||||||
{ use: QueryResolver, options: ['lang'] },
|
{ use: QueryResolver, options: ['lang'] },
|
||||||
@@ -19,6 +24,7 @@ import { join } from 'path';
|
|||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
providers: [require('./lang-ready.service').LangReadyService],
|
||||||
exports: [I18nModule],
|
exports: [I18nModule],
|
||||||
})
|
})
|
||||||
export class BootI18nModule {}
|
export class BootI18nModule {}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { BootI18nModule } from './boot-i18n.module';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BootLangModule (软别名)
|
||||||
|
*
|
||||||
|
* 提供对语言资源能力的语义化别名,底层仍使用 BootI18nModule。
|
||||||
|
* 不更改事件命名、就绪服务或载荷结构,仅作为导出包装。
|
||||||
|
*/
|
||||||
|
@Module({
|
||||||
|
imports: [BootI18nModule],
|
||||||
|
exports: [BootI18nModule],
|
||||||
|
})
|
||||||
|
export class BootLangModule {}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||||
|
import { join } from 'path';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LangReadyService implements OnModuleInit {
|
||||||
|
private readonly logger = new Logger(LangReadyService.name);
|
||||||
|
|
||||||
|
constructor(private readonly eventBus: EventBus) {}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
const langDir = join(process.cwd(), 'apps/api/src/lang');
|
||||||
|
const exists = fs.existsSync(langDir);
|
||||||
|
const state = exists ? 'ready' : 'unavailable';
|
||||||
|
this.logger.log(
|
||||||
|
`Lang module init: dir=${langDir}, exists=${exists}, state=${state}`,
|
||||||
|
);
|
||||||
|
this.eventBus.emit('module.state.changed', {
|
||||||
|
module: 'lang',
|
||||||
|
previousState: 'initializing',
|
||||||
|
currentState: state,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||||
import {
|
import {
|
||||||
Registry,
|
Registry,
|
||||||
collectDefaultMetrics,
|
collectDefaultMetrics,
|
||||||
@@ -8,7 +9,7 @@ import {
|
|||||||
} from 'prom-client';
|
} from 'prom-client';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MetricsService {
|
export class MetricsService implements OnModuleInit {
|
||||||
private readonly registry = new Registry();
|
private readonly registry = new Registry();
|
||||||
private readonly httpCounter: Counter<string>;
|
private readonly httpCounter: Counter<string>;
|
||||||
private readonly httpDuration: Histogram<string>;
|
private readonly httpDuration: Histogram<string>;
|
||||||
@@ -16,11 +17,19 @@ export class MetricsService {
|
|||||||
private readonly externalDuration: Histogram<string>;
|
private readonly externalDuration: Histogram<string>;
|
||||||
private readonly aiEvents: Counter<string>;
|
private readonly aiEvents: Counter<string>;
|
||||||
private readonly enabled: boolean;
|
private readonly enabled: boolean;
|
||||||
|
private metricsInterval?: any;
|
||||||
|
|
||||||
constructor(private readonly config: ConfigService) {
|
constructor(
|
||||||
|
private readonly config: ConfigService,
|
||||||
|
private readonly eventBus: EventBus,
|
||||||
|
) {
|
||||||
this.enabled = this.readBoolean('PROMETHEUS_ENABLED');
|
this.enabled = this.readBoolean('PROMETHEUS_ENABLED');
|
||||||
if (this.enabled) {
|
if (this.enabled) {
|
||||||
collectDefaultMetrics({ register: this.registry, prefix: 'wwjcloud_' });
|
// 保存默认指标收集的定时器引用,便于销毁时清理
|
||||||
|
this.metricsInterval = collectDefaultMetrics({
|
||||||
|
register: this.registry,
|
||||||
|
prefix: 'wwjcloud_',
|
||||||
|
}) as any;
|
||||||
}
|
}
|
||||||
this.httpCounter = new Counter({
|
this.httpCounter = new Counter({
|
||||||
name: 'http_requests_total',
|
name: 'http_requests_total',
|
||||||
@@ -98,6 +107,19 @@ export class MetricsService {
|
|||||||
return await this.registry.metrics();
|
return await this.registry.metrics();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 在模块销毁时清理默认指标的采集定时器与注册表,避免测试残留句柄
|
||||||
|
onModuleDestroy() {
|
||||||
|
if (this.metricsInterval) {
|
||||||
|
try {
|
||||||
|
clearInterval(this.metricsInterval);
|
||||||
|
} catch {}
|
||||||
|
this.metricsInterval = undefined;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.registry.clear();
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
private readBoolean(key: string): boolean {
|
private readBoolean(key: string): boolean {
|
||||||
const v = this.config.get<string | boolean>(key);
|
const v = this.config.get<string | boolean>(key);
|
||||||
if (typeof v === 'boolean') return v;
|
if (typeof v === 'boolean') return v;
|
||||||
@@ -106,4 +128,13 @@ export class MetricsService {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
const state = this.enabled ? 'ready' : 'unavailable';
|
||||||
|
this.eventBus.emit('module.state.changed', {
|
||||||
|
module: 'metrics',
|
||||||
|
previousState: 'initializing',
|
||||||
|
currentState: state,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import { Global, Module } from '@nestjs/common';
|
|||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { QueueService } from './queue.service';
|
import { QueueService } from './queue.service';
|
||||||
import { QueueController } from './queue.controller';
|
import { QueueController } from './queue.controller';
|
||||||
|
import { QueueReadyService } from './queue-ready.service';
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
providers: [QueueService],
|
providers: [QueueService, QueueReadyService],
|
||||||
controllers: [/* management */ require('./queue.controller').QueueController],
|
controllers: [/* management */ require('./queue.controller').QueueController],
|
||||||
exports: [QueueService],
|
exports: [QueueService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||||
|
|
||||||
|
function readBoolean(
|
||||||
|
config: ConfigService,
|
||||||
|
key: string,
|
||||||
|
fallback = false,
|
||||||
|
): boolean {
|
||||||
|
const v = config.get<string | boolean>(key);
|
||||||
|
if (typeof v === 'boolean') return v;
|
||||||
|
if (typeof v === 'string')
|
||||||
|
return ['true', '1', 'yes', 'on'].includes(v.toLowerCase());
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class QueueReadyService implements OnModuleInit {
|
||||||
|
private readonly logger = new Logger(QueueReadyService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly config: ConfigService,
|
||||||
|
private readonly eventBus: EventBus,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async onModuleInit(): Promise<void> {
|
||||||
|
const enabled = readBoolean(this.config, 'QUEUE_ENABLED', false);
|
||||||
|
const driver = (
|
||||||
|
this.config.get<string>('QUEUE_DRIVER') || ''
|
||||||
|
).toLowerCase();
|
||||||
|
|
||||||
|
let state: 'ready' | 'unavailable' = 'unavailable';
|
||||||
|
if (!enabled) {
|
||||||
|
// 禁用情况下对上层而言不阻塞,标记 ready 以避免误暂停
|
||||||
|
state = 'ready';
|
||||||
|
} else if (['bullmq', 'kafka'].includes(driver)) {
|
||||||
|
state = 'ready';
|
||||||
|
} else {
|
||||||
|
state = 'unavailable';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Queue module init -> enabled=${enabled}, driver=${driver}, state=${state}`,
|
||||||
|
);
|
||||||
|
this.eventBus.emit('module.state.changed', {
|
||||||
|
module: 'queue',
|
||||||
|
previousState: 'initializing',
|
||||||
|
currentState: state,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,7 +57,10 @@ export class QueueService {
|
|||||||
this.kafka = { Kafka };
|
this.kafka = { Kafka };
|
||||||
const brokersRaw =
|
const brokersRaw =
|
||||||
this.config.get<string>('QUEUE_KAFKA_BROKERS') ?? '127.0.0.1:9092';
|
this.config.get<string>('QUEUE_KAFKA_BROKERS') ?? '127.0.0.1:9092';
|
||||||
const brokers = brokersRaw.split(',').map((s) => s.trim()).filter(Boolean);
|
const brokers = brokersRaw
|
||||||
|
.split(',')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
const clientId =
|
const clientId =
|
||||||
this.config.get<string>('QUEUE_KAFKA_CLIENT_ID') ?? 'wwjcloud-api';
|
this.config.get<string>('QUEUE_KAFKA_CLIENT_ID') ?? 'wwjcloud-api';
|
||||||
const groupId =
|
const groupId =
|
||||||
@@ -74,7 +77,9 @@ export class QueueService {
|
|||||||
await producer.connect();
|
await producer.connect();
|
||||||
await consumer.connect();
|
await consumer.connect();
|
||||||
await consumer.subscribe({ topic, fromBeginning: false });
|
await consumer.subscribe({ topic, fromBeginning: false });
|
||||||
this.logger.log(`Queue(kafka) initialized: topic=${topic}, group=${groupId}`);
|
this.logger.log(
|
||||||
|
`Queue(kafka) initialized: topic=${topic}, group=${groupId}`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.driver = 'none';
|
this.driver = 'none';
|
||||||
this.logger.warn('Queue driver not set, queue disabled');
|
this.logger.warn('Queue driver not set, queue disabled');
|
||||||
@@ -97,7 +102,8 @@ export class QueueService {
|
|||||||
if (this.driver === 'bullmq' && this.queue) {
|
if (this.driver === 'bullmq' && this.queue) {
|
||||||
const attempts = this.readNumber('QUEUE_MAX_ATTEMPTS', 3);
|
const attempts = this.readNumber('QUEUE_MAX_ATTEMPTS', 3);
|
||||||
const backoff = this.readNumber('QUEUE_BACKOFF_MS', 500);
|
const backoff = this.readNumber('QUEUE_BACKOFF_MS', 500);
|
||||||
const idempotencyKey = payload?.idempotencyKey || payload?.taskId || undefined;
|
const idempotencyKey =
|
||||||
|
payload?.idempotencyKey || payload?.taskId || undefined;
|
||||||
const job = await this.queue.add(jobName, payload, {
|
const job = await this.queue.add(jobName, payload, {
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
attempts,
|
attempts,
|
||||||
@@ -121,7 +127,9 @@ export class QueueService {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this.logger.error(`Kafka produce failed: topic=${topic}, err=${err?.message || err}`);
|
this.logger.error(
|
||||||
|
`Kafka produce failed: topic=${topic}, err=${err?.message || err}`,
|
||||||
|
);
|
||||||
// DLQ produce
|
// DLQ produce
|
||||||
if (this.readBoolean('QUEUE_DLQ_ENABLED')) {
|
if (this.readBoolean('QUEUE_DLQ_ENABLED')) {
|
||||||
const dlqTopic = `${topic}.dlq`;
|
const dlqTopic = `${topic}.dlq`;
|
||||||
@@ -129,11 +137,19 @@ export class QueueService {
|
|||||||
await this.kafka.producer.send({
|
await this.kafka.producer.send({
|
||||||
topic: dlqTopic,
|
topic: dlqTopic,
|
||||||
messages: [
|
messages: [
|
||||||
{ key: payload?.idempotencyKey || jobName, value: JSON.stringify({ error: err?.message || String(err), payload }) },
|
{
|
||||||
|
key: payload?.idempotencyKey || jobName,
|
||||||
|
value: JSON.stringify({
|
||||||
|
error: err?.message || String(err),
|
||||||
|
payload,
|
||||||
|
}),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
} catch (err2: any) {
|
} catch (err2: any) {
|
||||||
this.logger.error(`Kafka DLQ produce failed: topic=${dlqTopic}, err=${err2?.message || err2}`);
|
this.logger.error(
|
||||||
|
`Kafka DLQ produce failed: topic=${dlqTopic}, err=${err2?.message || err2}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,21 +158,30 @@ export class QueueService {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerWorker(processor: (data: any) => Promise<void>, concurrency = 1, name = 'default'): void {
|
registerWorker(
|
||||||
|
processor: (data: any) => Promise<void>,
|
||||||
|
concurrency = 1,
|
||||||
|
name = 'default',
|
||||||
|
): void {
|
||||||
if (this.driver === 'bullmq' && this.bull && this.queue) {
|
if (this.driver === 'bullmq' && this.bull && this.queue) {
|
||||||
const { Worker, Queue } = this.bull;
|
const { Worker, Queue } = this.bull;
|
||||||
const host = this.config.get<string>('QUEUE_REDIS_HOST');
|
const host = this.config.get<string>('QUEUE_REDIS_HOST');
|
||||||
const port = this.config.get<number>('QUEUE_REDIS_PORT') ?? 6379;
|
const port = this.config.get<number>('QUEUE_REDIS_PORT') ?? 6379;
|
||||||
const password = this.config.get<string>('QUEUE_REDIS_PASSWORD');
|
const password = this.config.get<string>('QUEUE_REDIS_PASSWORD');
|
||||||
const namespace = this.config.get<string>('QUEUE_REDIS_NAMESPACE') ?? 'wwjcloud';
|
const namespace =
|
||||||
|
this.config.get<string>('QUEUE_REDIS_NAMESPACE') ?? 'wwjcloud';
|
||||||
const connection = { host, port, password };
|
const connection = { host, port, password };
|
||||||
const queueName = `${namespace}-${name}`;
|
const queueName = `${namespace}-${name}`;
|
||||||
const dlqEnabled = this.readBoolean('QUEUE_DLQ_ENABLED');
|
const dlqEnabled = this.readBoolean('QUEUE_DLQ_ENABLED');
|
||||||
const dlqName = `${queueName}-dlq`;
|
const dlqName = `${queueName}-dlq`;
|
||||||
const dlqQueue = dlqEnabled ? new Queue(dlqName, { connection }) : null;
|
const dlqQueue = dlqEnabled ? new Queue(dlqName, { connection }) : null;
|
||||||
this.worker = new Worker(queueName, async (job: any) => {
|
this.worker = new Worker(
|
||||||
await processor(job.data);
|
queueName,
|
||||||
}, { connection, concurrency });
|
async (job: any) => {
|
||||||
|
await processor(job.data);
|
||||||
|
},
|
||||||
|
{ connection, concurrency },
|
||||||
|
);
|
||||||
this.worker.on('completed', (job: any) => {
|
this.worker.on('completed', (job: any) => {
|
||||||
this.logger.log(`Job completed: ${job.id}`);
|
this.logger.log(`Job completed: ${job.id}`);
|
||||||
});
|
});
|
||||||
@@ -164,7 +189,15 @@ export class QueueService {
|
|||||||
this.logger.error(`Job failed: ${job?.id}: ${err?.message || err}`);
|
this.logger.error(`Job failed: ${job?.id}: ${err?.message || err}`);
|
||||||
if (dlqEnabled && dlqQueue) {
|
if (dlqEnabled && dlqQueue) {
|
||||||
try {
|
try {
|
||||||
await dlqQueue.add('dlq', { originalJobId: job?.id, error: err?.message || String(err), payload: job?.data }, { removeOnComplete: true });
|
await dlqQueue.add(
|
||||||
|
'dlq',
|
||||||
|
{
|
||||||
|
originalJobId: job?.id,
|
||||||
|
error: err?.message || String(err),
|
||||||
|
payload: job?.data,
|
||||||
|
},
|
||||||
|
{ removeOnComplete: true },
|
||||||
|
);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.logger.error(`DLQ enqueue failed: ${e?.message || e}`);
|
this.logger.error(`DLQ enqueue failed: ${e?.message || e}`);
|
||||||
}
|
}
|
||||||
@@ -184,7 +217,9 @@ export class QueueService {
|
|||||||
const data = value ? JSON.parse(value) : undefined;
|
const data = value ? JSON.parse(value) : undefined;
|
||||||
await processor(data);
|
await processor(data);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this.logger.error(`Kafka message processing failed: ${err?.message || err}`);
|
this.logger.error(
|
||||||
|
`Kafka message processing failed: ${err?.message || err}`,
|
||||||
|
);
|
||||||
if (this.readBoolean('QUEUE_DLQ_ENABLED')) {
|
if (this.readBoolean('QUEUE_DLQ_ENABLED')) {
|
||||||
const dlqTopic = `${topic}.dlq`;
|
const dlqTopic = `${topic}.dlq`;
|
||||||
try {
|
try {
|
||||||
@@ -203,7 +238,9 @@ export class QueueService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err2: any) {
|
} catch (err2: any) {
|
||||||
this.logger.error(`Kafka DLQ produce failed: topic=${dlqTopic}, err=${err2?.message || err2}`);
|
this.logger.error(
|
||||||
|
`Kafka DLQ produce failed: topic=${dlqTopic}, err=${err2?.message || err2}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,10 +250,19 @@ export class QueueService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getQueueCounts(): Promise<{ waiting: number; delayed: number; active: number; completed: number; failed: number } | undefined> {
|
async getQueueCounts(): Promise<
|
||||||
|
| {
|
||||||
|
waiting: number;
|
||||||
|
delayed: number;
|
||||||
|
active: number;
|
||||||
|
completed: number;
|
||||||
|
failed: number;
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
> {
|
||||||
if (this.driver !== 'bullmq' || !this.queue) return undefined;
|
if (this.driver !== 'bullmq' || !this.queue) return undefined;
|
||||||
const counts = await this.queue.getJobCounts();
|
const counts = await this.queue.getJobCounts();
|
||||||
return counts as any;
|
return counts;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readBoolean(key: string): boolean {
|
private readBoolean(key: string): boolean {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { map } from 'rxjs/operators';
|
|||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { I18nService } from 'nestjs-i18n';
|
import { I18nService } from 'nestjs-i18n';
|
||||||
import { mapAlias } from '../i18n/aliases';
|
import { mapAlias } from '../lang/aliases';
|
||||||
|
|
||||||
interface WrappedResponse<T = any> {
|
interface WrappedResponse<T = any> {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
@@ -19,7 +19,10 @@ interface WrappedResponse<T = any> {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ResponseInterceptor implements NestInterceptor {
|
export class ResponseInterceptor implements NestInterceptor {
|
||||||
constructor(private readonly config: ConfigService, private readonly i18n: I18nService) {}
|
constructor(
|
||||||
|
private readonly config: ConfigService,
|
||||||
|
private readonly i18n: I18nService,
|
||||||
|
) {}
|
||||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||||
const ctx = context.switchToHttp();
|
const ctx = context.switchToHttp();
|
||||||
const req = ctx.getRequest<Request>();
|
const req = ctx.getRequest<Request>();
|
||||||
@@ -43,7 +46,7 @@ export class ResponseInterceptor implements NestInterceptor {
|
|||||||
('msg' in data || 'msg_key' in data)
|
('msg' in data || 'msg_key' in data)
|
||||||
) {
|
) {
|
||||||
// 如果存在 msg_key,则补充翻译后的 msg
|
// 如果存在 msg_key,则补充翻译后的 msg
|
||||||
const keyRaw = (data as any).msg_key;
|
const keyRaw = data.msg_key;
|
||||||
if (keyRaw && typeof keyRaw === 'string') {
|
if (keyRaw && typeof keyRaw === 'string') {
|
||||||
const key = mapAlias(keyRaw);
|
const key = mapAlias(keyRaw);
|
||||||
const msg = this.i18n.translate(key, {
|
const msg = this.i18n.translate(key, {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ConfigService } from '@nestjs/config';
|
|||||||
import { RedisService } from '../cache/redis.service';
|
import { RedisService } from '../cache/redis.service';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||||
|
|
||||||
interface StartupReport {
|
interface StartupReport {
|
||||||
nodeVersion: string;
|
nodeVersion: string;
|
||||||
@@ -19,6 +20,7 @@ export class StartupValidatorService implements OnModuleInit {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
private readonly redis: RedisService,
|
private readonly redis: RedisService,
|
||||||
|
private readonly eventBus: EventBus,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
@@ -90,6 +92,18 @@ export class StartupValidatorService implements OnModuleInit {
|
|||||||
this.writeJson('application-boot.json', snapshot);
|
this.writeJson('application-boot.json', snapshot);
|
||||||
|
|
||||||
this.logger.log('Startup validation completed, reports generated');
|
this.logger.log('Startup validation completed, reports generated');
|
||||||
|
|
||||||
|
// Emit startup readiness state
|
||||||
|
const state: 'ready' | 'unavailable' =
|
||||||
|
report.envMissing.length === 0 &&
|
||||||
|
(!report.redis.enabled || report.redis.connected)
|
||||||
|
? 'ready'
|
||||||
|
: 'unavailable';
|
||||||
|
this.eventBus.emit('module.state.changed', {
|
||||||
|
module: 'startup',
|
||||||
|
previousState: 'initializing',
|
||||||
|
currentState: state,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private writeJson(fileName: string, data: any) {
|
private writeJson(fileName: string, data: any) {
|
||||||
|
|||||||
@@ -2,29 +2,43 @@ import type { Request, Response, NextFunction } from 'express';
|
|||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { TenantService } from './tenant.service';
|
import { TenantService } from './tenant.service';
|
||||||
|
|
||||||
function readBoolean(config: ConfigService, key: string, fallback = false): boolean {
|
function readBoolean(
|
||||||
|
config: ConfigService,
|
||||||
|
key: string,
|
||||||
|
fallback = false,
|
||||||
|
): boolean {
|
||||||
const v = config.get<string | boolean>(key);
|
const v = config.get<string | boolean>(key);
|
||||||
if (typeof v === 'boolean') return v;
|
if (typeof v === 'boolean') return v;
|
||||||
if (typeof v === 'string') return ['true', '1', 'yes', 'on'].includes(v.toLowerCase());
|
if (typeof v === 'string')
|
||||||
|
return ['true', '1', 'yes', 'on'].includes(v.toLowerCase());
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildTenantMiddleware(config: ConfigService, tenant: TenantService) {
|
export function buildTenantMiddleware(
|
||||||
return function tenantMiddleware(req: Request, _res: Response, next: NextFunction) {
|
config: ConfigService,
|
||||||
|
tenant: TenantService,
|
||||||
|
) {
|
||||||
|
return function tenantMiddleware(
|
||||||
|
req: Request,
|
||||||
|
_res: Response,
|
||||||
|
next: NextFunction,
|
||||||
|
) {
|
||||||
const enabled = readBoolean(config, 'TENANT_ENABLED', false);
|
const enabled = readBoolean(config, 'TENANT_ENABLED', false);
|
||||||
if (!enabled) return next();
|
if (!enabled) return next();
|
||||||
|
|
||||||
const strategy = (config.get<string>('TENANT_RESOLVE_STRATEGY') || 'header').toLowerCase();
|
const strategy = (
|
||||||
|
config.get<string>('TENANT_RESOLVE_STRATEGY') || 'header'
|
||||||
|
).toLowerCase();
|
||||||
let id: string | undefined;
|
let id: string | undefined;
|
||||||
|
|
||||||
if (strategy === 'header') {
|
if (strategy === 'header') {
|
||||||
// 与 Java 保持一致:仅支持 'site-id' 作为租户头;不再允许通过配置修改
|
// 与 Java 保持一致:仅支持 'site-id' 作为租户头;不再允许通过配置修改
|
||||||
const key = 'site-id';
|
const key = 'site-id';
|
||||||
const value = req.headers[key];
|
const value = req.headers[key];
|
||||||
id = Array.isArray(value) ? value[0] : (value as string | undefined);
|
id = Array.isArray(value) ? value[0] : value;
|
||||||
} else if (strategy === 'path') {
|
} else if (strategy === 'path') {
|
||||||
const prefix = config.get<string>('TENANT_PATH_PREFIX') || '/t/';
|
const prefix = config.get<string>('TENANT_PATH_PREFIX') || '/t/';
|
||||||
const url = (req.originalUrl || req.url || '') as string;
|
const url = req.originalUrl || req.url || '';
|
||||||
const idx = url.indexOf(prefix);
|
const idx = url.indexOf(prefix);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
const sliced = url.slice(idx + prefix.length);
|
const sliced = url.slice(idx + prefix.length);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { DynamicModule, Module } from '@nestjs/common';
|
import { DynamicModule, Module } from '@nestjs/common';
|
||||||
import { BootModule } from './wwjcloud-boot.module';
|
import { BootModule } from './wwjcloud-boot.module';
|
||||||
import { AddonModule } from '@wwjAddon/wwjcloud-addon.module';
|
import { AddonModule } from '@wwjAddon/wwjcloud-addon.module';
|
||||||
import { AiModule } from '@wwjAi/wwjcloud-ai.module';
|
import { WwjcloudAiModule } from '@wwjAi/wwjcloud-ai.module';
|
||||||
import { BootI18nModule } from './infra/i18n/boot-i18n.module';
|
import { BootLangModule } from './infra/lang/boot-lang.module';
|
||||||
|
|
||||||
function readBooleanEnv(key: string, fallback = false): boolean {
|
function readBooleanEnv(key: string, fallback = false): boolean {
|
||||||
const v = process.env[key];
|
const v = process.env[key];
|
||||||
@@ -13,11 +13,11 @@ function readBooleanEnv(key: string, fallback = false): boolean {
|
|||||||
@Module({})
|
@Module({})
|
||||||
export class WwjCloudPlatformPreset {
|
export class WwjCloudPlatformPreset {
|
||||||
static full(): DynamicModule {
|
static full(): DynamicModule {
|
||||||
const imports: any[] = [BootModule, BootI18nModule, AddonModule.register()];
|
const imports: any[] = [BootModule, BootLangModule, AddonModule.register()];
|
||||||
const exportsArr: any[] = [];
|
const exportsArr: any[] = [];
|
||||||
if (readBooleanEnv('AI_ENABLED', false)) {
|
if (readBooleanEnv('AI_ENABLED', false)) {
|
||||||
imports.push(AiModule);
|
imports.push(WwjcloudAiModule);
|
||||||
exportsArr.push(AiModule);
|
exportsArr.push(WwjcloudAiModule);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
module: WwjCloudPlatformPreset,
|
module: WwjCloudPlatformPreset,
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ import { BootStartupModule } from './infra/startup/boot-startup.module';
|
|||||||
BootStartupModule,
|
BootStartupModule,
|
||||||
],
|
],
|
||||||
providers: [ResilienceService, HttpClientService, RequestContextService],
|
providers: [ResilienceService, HttpClientService, RequestContextService],
|
||||||
exports: [EventEmitterModule, HttpClientService, RequestContextService, BootMetricsModule],
|
exports: [
|
||||||
|
EventEmitterModule,
|
||||||
|
HttpClientService,
|
||||||
|
RequestContextService,
|
||||||
|
BootMetricsModule,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class BootModule {}
|
export class BootModule {}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wwjcloud-nest-v1",
|
"name": "wwjcloud-nest-v1",
|
||||||
"version": "0.0.1",
|
"version": "0.1.2",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -85,6 +85,14 @@
|
|||||||
"transform": {
|
"transform": {
|
||||||
"^.+\\.(t|j)s$": "ts-jest"
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
},
|
},
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"^@wwjBoot(.*)$": "<rootDir>/../libs/wwjcloud-boot/src$1",
|
||||||
|
"^@wwjCommon(.*)$": "<rootDir>/../libs/wwjcloud-boot/src/infra$1",
|
||||||
|
"^@wwjCore(.*)$": "<rootDir>/../libs/wwjcloud-core/src$1",
|
||||||
|
"^@wwjAi(.*)$": "<rootDir>/../libs/wwjcloud-ai/src$1",
|
||||||
|
"^@wwjAddon(.*)$": "<rootDir>/../libs/wwjcloud-addon/src$1",
|
||||||
|
"^@wwjVendor(.*)$": "<rootDir>/../libs/wwjcloud-boot/src/vendor$1"
|
||||||
|
},
|
||||||
"collectCoverageFrom": [
|
"collectCoverageFrom": [
|
||||||
"**/*.(t|j)s"
|
"**/*.(t|j)s"
|
||||||
],
|
],
|
||||||
|
|||||||
51
wwjcloud-nest-v1/src/ai-layer/ai-coordinator.service.spec.ts
Normal file
51
wwjcloud-nest-v1/src/ai-layer/ai-coordinator.service.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||||
|
import { AiCoordinatorService } from '@wwjAi/manager/services/ai-coordinator.service';
|
||||||
|
import { AiRegistryService } from '@wwjAi/manager/services/ai-registry.service';
|
||||||
|
import { AiOrchestratorService } from '@wwjAi/manager/services/ai-orchestrator.service';
|
||||||
|
|
||||||
|
describe('AiCoordinatorService module states', () => {
|
||||||
|
let svc: AiCoordinatorService;
|
||||||
|
let testingModule: TestingModule;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
testingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
AiCoordinatorService,
|
||||||
|
{ provide: EventBus, useValue: { emit: jest.fn() } },
|
||||||
|
{
|
||||||
|
provide: AiRegistryService,
|
||||||
|
useValue: {
|
||||||
|
getServicesByType: jest
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue([{ execute: jest.fn() }]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: AiOrchestratorService,
|
||||||
|
useValue: { executeWorkflow: jest.fn() },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
svc = testingModule.get(AiCoordinatorService);
|
||||||
|
await svc.onModuleInit();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await testingModule?.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets manager ready on init', () => {
|
||||||
|
expect(svc.getModuleState('manager')).toBe('ready');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates state on module.state.changed event', async () => {
|
||||||
|
await (svc as any).handleModuleStateChanged({
|
||||||
|
module: 'healing',
|
||||||
|
previousState: 'initializing',
|
||||||
|
currentState: 'ready',
|
||||||
|
});
|
||||||
|
expect(svc.getModuleState('healing')).toBe('ready');
|
||||||
|
});
|
||||||
|
});
|
||||||
73
wwjcloud-nest-v1/src/ai-layer/ai-security.service.spec.ts
Normal file
73
wwjcloud-nest-v1/src/ai-layer/ai-security.service.spec.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { AiSecurityService } from '../../libs/wwjcloud-ai/src/safe/services/ai-security.service';
|
||||||
|
import { AiAuditService } from '../../libs/wwjcloud-ai/src/safe/services/ai-audit.service';
|
||||||
|
import { SecurityAnalyzer } from '../../libs/wwjcloud-ai/src/safe/analyzers/security.analyzer';
|
||||||
|
import { VulnerabilityDetector } from '../../libs/wwjcloud-ai/src/safe/detectors/vulnerability.detector';
|
||||||
|
import { AccessProtector } from '../../libs/wwjcloud-ai/src/safe/protectors/access.protector';
|
||||||
|
|
||||||
|
describe('AiSecurityService audit integration', () => {
|
||||||
|
let service: AiSecurityService;
|
||||||
|
let auditService: jest.Mocked<AiAuditService>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
AiSecurityService,
|
||||||
|
{
|
||||||
|
provide: SecurityAnalyzer,
|
||||||
|
useValue: { analyzeSystemSecurity: jest.fn() },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: VulnerabilityDetector,
|
||||||
|
useValue: {
|
||||||
|
scanVulnerabilities: jest.fn(),
|
||||||
|
detectRealTimeThreats: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: AccessProtector,
|
||||||
|
useValue: {
|
||||||
|
monitorAccessPatterns: jest.fn(),
|
||||||
|
enforceSecurityPolicy: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: AiAuditService,
|
||||||
|
useValue: { logAuditEvent: jest.fn().mockResolvedValue(undefined) },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<AiSecurityService>(AiSecurityService);
|
||||||
|
auditService = module.get(AiAuditService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handleSecurityEvent(critical) should log audit event and return actions', async () => {
|
||||||
|
const event = {
|
||||||
|
id: 'evt-1',
|
||||||
|
type: 'intrusion_detected',
|
||||||
|
severity: 'critical' as const,
|
||||||
|
source: 'waf',
|
||||||
|
description: 'Malicious traffic detected',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
metadata: { userId: 'user-123' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await service.handleSecurityEvent(event);
|
||||||
|
|
||||||
|
expect(response.handled).toBe(true);
|
||||||
|
expect(response.actions).toEqual([
|
||||||
|
'immediate_alert',
|
||||||
|
'block_source',
|
||||||
|
'escalate_to_admin',
|
||||||
|
'create_incident',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(auditService.logAuditEvent).toHaveBeenCalledTimes(1);
|
||||||
|
const callArg = auditService.logAuditEvent.mock.calls[0][0];
|
||||||
|
expect(callArg.type).toBe('security_critical_event');
|
||||||
|
expect(callArg.userId).toBe('user-123');
|
||||||
|
expect(callArg.description).toContain('Critical security event');
|
||||||
|
expect(callArg.metadata?.event).toEqual(event);
|
||||||
|
});
|
||||||
|
});
|
||||||
30
wwjcloud-nest-v1/src/ai-layer/performance.analyzer.spec.ts
Normal file
30
wwjcloud-nest-v1/src/ai-layer/performance.analyzer.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { PerformanceAnalyzer } from '../../libs/wwjcloud-ai/src/tuner/analyzers/performance.analyzer';
|
||||||
|
|
||||||
|
describe('PerformanceAnalyzer summary', () => {
|
||||||
|
let analyzer: PerformanceAnalyzer;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
analyzer = new PerformanceAnalyzer();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('analyzePerformance should include summary with expected fields', async () => {
|
||||||
|
const result = await analyzer.analyzePerformance();
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
|
||||||
|
// Summary exists
|
||||||
|
expect(result.summary).toBeDefined();
|
||||||
|
expect(typeof result.summary.averageResponseTime).toBe('number');
|
||||||
|
expect(typeof result.summary.requestsPerSecond).toBe('number');
|
||||||
|
expect(typeof result.summary.errorRate).toBe('number');
|
||||||
|
|
||||||
|
// Metrics include error_rate for unified source
|
||||||
|
expect(result.metrics).toBeDefined();
|
||||||
|
expect(typeof result.metrics['error_rate']).toBe('number');
|
||||||
|
|
||||||
|
// Basic structure still intact
|
||||||
|
expect(result.analyses).toBeDefined();
|
||||||
|
expect(result.bottlenecks).toBeDefined();
|
||||||
|
expect(Array.isArray(result.trends)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
40
wwjcloud-nest-v1/src/ai-layer/resource-monitor.spec.ts
Normal file
40
wwjcloud-nest-v1/src/ai-layer/resource-monitor.spec.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||||
|
import { ResourceMonitor } from '@wwjAi/tuner/monitors/resource.monitor';
|
||||||
|
|
||||||
|
describe('ResourceMonitor DI and cleanup', () => {
|
||||||
|
let module: TestingModule;
|
||||||
|
let monitor: ResourceMonitor;
|
||||||
|
const emitterMock = { emit: jest.fn() } as unknown as EventBus;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
module = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
ResourceMonitor,
|
||||||
|
{ provide: EventBus, useValue: emitterMock },
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
monitor = module.get(ResourceMonitor);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
await module?.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits resource.snapshot via injected EventBus', async () => {
|
||||||
|
await (monitor as any).collectResourceSnapshot();
|
||||||
|
expect(emitterMock.emit as any).toHaveBeenCalledWith(
|
||||||
|
'resource.snapshot',
|
||||||
|
expect.any(Object),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cleans up interval on onModuleDestroy', async () => {
|
||||||
|
await monitor.startMonitoring();
|
||||||
|
expect((monitor as any).monitoringInterval).toBeTruthy();
|
||||||
|
await monitor.onModuleDestroy();
|
||||||
|
expect((monitor as any).monitoringInterval).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
72
wwjcloud-nest-v1/src/ai-layer/resource.monitor.spec.ts
Normal file
72
wwjcloud-nest-v1/src/ai-layer/resource.monitor.spec.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { ResourceMonitor } from '../../libs/wwjcloud-ai/src/tuner/monitors/resource.monitor';
|
||||||
|
|
||||||
|
describe('ResourceMonitor alias getCurrentStatus', () => {
|
||||||
|
let monitor: ResourceMonitor;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
monitor = new ResourceMonitor();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getCurrentStatus should delegate to getCurrentResourceStatus', async () => {
|
||||||
|
const dummyStatus = {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
snapshot: {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
cpu: { usage: 50, cores: 8, loadAverage: [0.5, 0.6, 0.7] },
|
||||||
|
memory: {
|
||||||
|
usage: 60,
|
||||||
|
total: 16 * 1024 ** 3,
|
||||||
|
used: 9 * 1024 ** 3,
|
||||||
|
free: 7 * 1024 ** 3,
|
||||||
|
cached: 2 * 1024 ** 3,
|
||||||
|
},
|
||||||
|
disk: {
|
||||||
|
usage: 40,
|
||||||
|
total: 512 * 1024 ** 3,
|
||||||
|
used: 200 * 1024 ** 3,
|
||||||
|
free: 312 * 1024 ** 3,
|
||||||
|
ioRead: 1024,
|
||||||
|
ioWrite: 2048,
|
||||||
|
},
|
||||||
|
network: {
|
||||||
|
bytesIn: 1000,
|
||||||
|
bytesOut: 1500,
|
||||||
|
packetsIn: 100,
|
||||||
|
packetsOut: 120,
|
||||||
|
errors: 0,
|
||||||
|
},
|
||||||
|
processes: { total: 200, running: 15, sleeping: 180, zombie: 5 },
|
||||||
|
},
|
||||||
|
alerts: [],
|
||||||
|
trends: {
|
||||||
|
cpu: 'stable',
|
||||||
|
memory: 'stable',
|
||||||
|
disk: 'stable',
|
||||||
|
network: 'stable',
|
||||||
|
},
|
||||||
|
health: {
|
||||||
|
status: 'healthy',
|
||||||
|
score: 95,
|
||||||
|
criticalAlerts: 0,
|
||||||
|
warningAlerts: 0,
|
||||||
|
lastCheck: Date.now(),
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(monitor, 'getCurrentResourceStatus')
|
||||||
|
.mockResolvedValue(dummyStatus);
|
||||||
|
const statusViaAlias = await monitor.getCurrentStatus();
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(statusViaAlias).toBe(dummyStatus);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getCurrentResourceStatus returns status shape', async () => {
|
||||||
|
const status = await monitor.getCurrentResourceStatus();
|
||||||
|
expect(status).toBeDefined();
|
||||||
|
expect(status.snapshot).toBeDefined();
|
||||||
|
expect(status.health).toBeDefined();
|
||||||
|
expect(status.trends).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
98
wwjcloud-nest-v1/src/ai-layer/safe-ready.service.spec.ts
Normal file
98
wwjcloud-nest-v1/src/ai-layer/safe-ready.service.spec.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { SafeReadyService } from '@wwjAi/safe/services/safe-ready.service';
|
||||||
|
import { SecurityAnalyzer } from '@wwjAi/safe/analyzers/security.analyzer';
|
||||||
|
import { VulnerabilityDetector } from '@wwjAi/safe/detectors/vulnerability.detector';
|
||||||
|
import { AccessProtector } from '@wwjAi/safe/protectors/access.protector';
|
||||||
|
import { AiSecurityService } from '@wwjAi/safe/services/ai-security.service';
|
||||||
|
|
||||||
|
describe('SafeReadyService readiness emission', () => {
|
||||||
|
const emitterMock = { emit: jest.fn() } as unknown as EventBus;
|
||||||
|
|
||||||
|
const buildConfigMock = (enabled: boolean) =>
|
||||||
|
({
|
||||||
|
get: jest.fn((k: string) =>
|
||||||
|
k === 'AI_SAFE_ENABLED' ? (enabled ? 'true' : 'false') : undefined,
|
||||||
|
),
|
||||||
|
}) as unknown as ConfigService;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits ready when enabled and all components injected', async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
SafeReadyService,
|
||||||
|
{ provide: EventBus, useValue: emitterMock },
|
||||||
|
{ provide: ConfigService, useValue: buildConfigMock(true) },
|
||||||
|
{ provide: SecurityAnalyzer, useValue: {} },
|
||||||
|
{ provide: VulnerabilityDetector, useValue: {} },
|
||||||
|
{ provide: AccessProtector, useValue: {} },
|
||||||
|
{ provide: AiSecurityService, useValue: {} },
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
const svc = module.get(SafeReadyService);
|
||||||
|
await svc.onModuleInit();
|
||||||
|
expect(emitterMock.emit as any).toHaveBeenCalledWith(
|
||||||
|
'module.state.changed',
|
||||||
|
expect.objectContaining({
|
||||||
|
module: 'ai.safe',
|
||||||
|
currentState: 'ready',
|
||||||
|
meta: { enabled: true },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await module.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits unavailable when enabled but component missing', async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
SafeReadyService,
|
||||||
|
{ provide: EventBus, useValue: emitterMock },
|
||||||
|
{ provide: ConfigService, useValue: buildConfigMock(true) },
|
||||||
|
{ provide: SecurityAnalyzer, useValue: {} },
|
||||||
|
{ provide: VulnerabilityDetector, useValue: {} },
|
||||||
|
{ provide: AccessProtector, useValue: undefined },
|
||||||
|
{ provide: AiSecurityService, useValue: {} },
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
const svc = module.get(SafeReadyService);
|
||||||
|
await svc.onModuleInit();
|
||||||
|
expect(emitterMock.emit as any).toHaveBeenCalledWith(
|
||||||
|
'module.state.changed',
|
||||||
|
expect.objectContaining({
|
||||||
|
module: 'ai.safe',
|
||||||
|
currentState: 'unavailable',
|
||||||
|
meta: { enabled: true },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await module.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits unavailable when disabled', async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
SafeReadyService,
|
||||||
|
{ provide: EventBus, useValue: emitterMock },
|
||||||
|
{ provide: ConfigService, useValue: buildConfigMock(false) },
|
||||||
|
{ provide: SecurityAnalyzer, useValue: {} },
|
||||||
|
{ provide: VulnerabilityDetector, useValue: {} },
|
||||||
|
{ provide: AccessProtector, useValue: {} },
|
||||||
|
{ provide: AiSecurityService, useValue: {} },
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
const svc = module.get(SafeReadyService);
|
||||||
|
await svc.onModuleInit();
|
||||||
|
expect(emitterMock.emit as any).toHaveBeenCalledWith(
|
||||||
|
'module.state.changed',
|
||||||
|
expect.objectContaining({
|
||||||
|
module: 'ai.safe',
|
||||||
|
currentState: 'unavailable',
|
||||||
|
meta: { enabled: false },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await module.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
98
wwjcloud-nest-v1/src/ai-layer/tuner-ready.service.spec.ts
Normal file
98
wwjcloud-nest-v1/src/ai-layer/tuner-ready.service.spec.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { TunerReadyService } from '@wwjAi/tuner/services/tuner-ready.service';
|
||||||
|
import { PerformanceAnalyzer } from '@wwjAi/tuner/analyzers/performance.analyzer';
|
||||||
|
import { ResourceMonitor } from '@wwjAi/tuner/monitors/resource.monitor';
|
||||||
|
import { CacheOptimizer } from '@wwjAi/tuner/optimizers/cache.optimizer';
|
||||||
|
import { QueryOptimizer } from '@wwjAi/tuner/optimizers/query.optimizer';
|
||||||
|
|
||||||
|
describe('TunerReadyService readiness emission', () => {
|
||||||
|
const emitterMock = { emit: jest.fn() } as unknown as EventBus;
|
||||||
|
|
||||||
|
const buildConfigMock = (enabled: boolean) =>
|
||||||
|
({
|
||||||
|
get: jest.fn((k: string) =>
|
||||||
|
k === 'AI_TUNER_ENABLED' ? (enabled ? 'true' : 'false') : undefined,
|
||||||
|
),
|
||||||
|
}) as unknown as ConfigService;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits ready when enabled and all components injected', async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
TunerReadyService,
|
||||||
|
{ provide: EventBus, useValue: emitterMock },
|
||||||
|
{ provide: ConfigService, useValue: buildConfigMock(true) },
|
||||||
|
{ provide: PerformanceAnalyzer, useValue: {} },
|
||||||
|
{ provide: ResourceMonitor, useValue: {} },
|
||||||
|
{ provide: CacheOptimizer, useValue: {} },
|
||||||
|
{ provide: QueryOptimizer, useValue: {} },
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
const svc = module.get(TunerReadyService);
|
||||||
|
await svc.onModuleInit();
|
||||||
|
expect(emitterMock.emit as any).toHaveBeenCalledWith(
|
||||||
|
'module.state.changed',
|
||||||
|
expect.objectContaining({
|
||||||
|
module: 'ai.tuner',
|
||||||
|
currentState: 'ready',
|
||||||
|
meta: { enabled: true },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await module.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits unavailable when enabled but component missing', async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
TunerReadyService,
|
||||||
|
{ provide: EventBus, useValue: emitterMock },
|
||||||
|
{ provide: ConfigService, useValue: buildConfigMock(true) },
|
||||||
|
{ provide: PerformanceAnalyzer, useValue: {} },
|
||||||
|
{ provide: ResourceMonitor, useValue: undefined },
|
||||||
|
{ provide: CacheOptimizer, useValue: {} },
|
||||||
|
{ provide: QueryOptimizer, useValue: {} },
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
const svc = module.get(TunerReadyService);
|
||||||
|
await svc.onModuleInit();
|
||||||
|
expect(emitterMock.emit as any).toHaveBeenCalledWith(
|
||||||
|
'module.state.changed',
|
||||||
|
expect.objectContaining({
|
||||||
|
module: 'ai.tuner',
|
||||||
|
currentState: 'unavailable',
|
||||||
|
meta: { enabled: true },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await module.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits unavailable when disabled', async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
TunerReadyService,
|
||||||
|
{ provide: EventBus, useValue: emitterMock },
|
||||||
|
{ provide: ConfigService, useValue: buildConfigMock(false) },
|
||||||
|
{ provide: PerformanceAnalyzer, useValue: {} },
|
||||||
|
{ provide: ResourceMonitor, useValue: {} },
|
||||||
|
{ provide: CacheOptimizer, useValue: {} },
|
||||||
|
{ provide: QueryOptimizer, useValue: {} },
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
const svc = module.get(TunerReadyService);
|
||||||
|
await svc.onModuleInit();
|
||||||
|
expect(emitterMock.emit as any).toHaveBeenCalledWith(
|
||||||
|
'module.state.changed',
|
||||||
|
expect.objectContaining({
|
||||||
|
module: 'ai.tuner',
|
||||||
|
currentState: 'unavailable',
|
||||||
|
meta: { enabled: false },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await module.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { buildControllerTestModule } from '../test/testing-preset';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
|
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { HttpClientService } from '@wwjCommon/resilience/http-client.service';
|
||||||
|
import { RateLimitGuard } from '@wwjCommon/http/rate-limit.guard';
|
||||||
|
|
||||||
describe('AppController', () => {
|
describe('AppController', () => {
|
||||||
let appController: AppController;
|
let appController: AppController;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const app: TestingModule = await Test.createTestingModule({
|
const app = await buildControllerTestModule(AppController, [AppService]);
|
||||||
controllers: [AppController],
|
|
||||||
providers: [AppService],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
appController = app.get<AppController>(AppController);
|
appController = app.get<AppController>(AppController);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
|
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { TASK_FAILED_EVENT } from '@wwjAi';
|
import { TASK_FAILED_EVENT } from '@wwjAi';
|
||||||
import type { Severity, TaskFailedPayload } from '@wwjAi';
|
import type { Severity, TaskFailedPayload } from '@wwjAi';
|
||||||
@@ -11,9 +11,13 @@ import { IsIn, IsOptional, IsString } from 'class-validator';
|
|||||||
import { MetricsService } from '@wwjCommon/metrics/metrics.service';
|
import { MetricsService } from '@wwjCommon/metrics/metrics.service';
|
||||||
import { AiRecoveryService } from '@wwjAi/services/ai-recovery.service';
|
import { AiRecoveryService } from '@wwjAi/services/ai-recovery.service';
|
||||||
|
|
||||||
function readBooleanEnvRelaxed(v: string | boolean | undefined, fallback = false): boolean {
|
function readBooleanEnvRelaxed(
|
||||||
|
v: string | boolean | undefined,
|
||||||
|
fallback = false,
|
||||||
|
): boolean {
|
||||||
if (typeof v === 'boolean') return v;
|
if (typeof v === 'boolean') return v;
|
||||||
if (typeof v === 'string') return ['true', '1', 'yes', 'on'].includes(v.toLowerCase());
|
if (typeof v === 'string')
|
||||||
|
return ['true', '1', 'yes', 'on'].includes(v.toLowerCase());
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +40,7 @@ class SimulateFailureQueryDto {
|
|||||||
export class AppController {
|
export class AppController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly appService: AppService,
|
private readonly appService: AppService,
|
||||||
private readonly emitter: EventEmitter2,
|
private readonly eventBus: EventBus,
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
private readonly httpClient: HttpClientService,
|
private readonly httpClient: HttpClientService,
|
||||||
) {}
|
) {}
|
||||||
@@ -58,9 +62,10 @@ export class AppController {
|
|||||||
@UseGuards(RateLimitGuard)
|
@UseGuards(RateLimitGuard)
|
||||||
@ApiTags('AI')
|
@ApiTags('AI')
|
||||||
@Get('ai/simulate-failure')
|
@Get('ai/simulate-failure')
|
||||||
simulateFailure(
|
simulateFailure(@Query() q: SimulateFailureQueryDto): {
|
||||||
@Query() q: SimulateFailureQueryDto,
|
ok: true;
|
||||||
): { ok: true; emitted: boolean } {
|
emitted: boolean;
|
||||||
|
} {
|
||||||
const taskId = q.taskId ?? 'demo-task';
|
const taskId = q.taskId ?? 'demo-task';
|
||||||
const severity: Severity = q.severity ?? 'medium';
|
const severity: Severity = q.severity ?? 'medium';
|
||||||
const reason = q.reason ?? 'demo failure';
|
const reason = q.reason ?? 'demo failure';
|
||||||
@@ -71,7 +76,7 @@ export class AppController {
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
// 仅发出事件,由专用监听器与服务负责入队与观测
|
// 仅发出事件,由专用监听器与服务负责入队与观测
|
||||||
this.emitter.emit(TASK_FAILED_EVENT, payload);
|
this.eventBus.emit(TASK_FAILED_EVENT, payload);
|
||||||
|
|
||||||
return { ok: true, emitted: true };
|
return { ok: true, emitted: true };
|
||||||
}
|
}
|
||||||
|
|||||||
74
wwjcloud-nest-v1/src/boot-layer/auth-ready.service.spec.ts
Normal file
74
wwjcloud-nest-v1/src/boot-layer/auth-ready.service.spec.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { AuthReadyService } from '@wwjCommon/auth/auth-ready.service';
|
||||||
|
|
||||||
|
describe('AuthReadyService readiness emission', () => {
|
||||||
|
let testingModule: TestingModule;
|
||||||
|
let svc: AuthReadyService;
|
||||||
|
const emitterMock = { emit: jest.fn() } as unknown as EventBus;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const csMock = {
|
||||||
|
get: jest.fn().mockImplementation((k: string) => {
|
||||||
|
if (k === 'AUTH_ENABLED') return 'true';
|
||||||
|
if (k === 'RBAC_ENABLED') return 'false';
|
||||||
|
return undefined;
|
||||||
|
}),
|
||||||
|
} as unknown as ConfigService;
|
||||||
|
|
||||||
|
testingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
AuthReadyService,
|
||||||
|
{ provide: EventBus, useValue: emitterMock },
|
||||||
|
{ provide: ConfigService, useValue: csMock },
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
svc = testingModule.get(AuthReadyService);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
await testingModule?.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits auth ready and rbac unavailable based on flags', async () => {
|
||||||
|
await svc.onModuleInit();
|
||||||
|
expect(emitterMock.emit as any).toHaveBeenCalledWith(
|
||||||
|
'module.state.changed',
|
||||||
|
expect.objectContaining({ module: 'auth', currentState: 'ready' }),
|
||||||
|
);
|
||||||
|
expect(emitterMock.emit as any).toHaveBeenCalledWith(
|
||||||
|
'module.state.changed',
|
||||||
|
expect.objectContaining({ module: 'rbac', currentState: 'unavailable' }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits both unavailable when disabled', async () => {
|
||||||
|
const cs = testingModule.get(ConfigService);
|
||||||
|
cs.get.mockImplementation((k: string) => {
|
||||||
|
if (k === 'AUTH_ENABLED') return 'false';
|
||||||
|
if (k === 'RBAC_ENABLED') return 'false';
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
const module2 = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
AuthReadyService,
|
||||||
|
{ provide: EventBus, useValue: emitterMock },
|
||||||
|
{ provide: ConfigService, useValue: cs },
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
const svc2 = module2.get(AuthReadyService);
|
||||||
|
await svc2.onModuleInit();
|
||||||
|
expect(emitterMock.emit as any).toHaveBeenCalledWith(
|
||||||
|
'module.state.changed',
|
||||||
|
expect.objectContaining({ module: 'auth', currentState: 'unavailable' }),
|
||||||
|
);
|
||||||
|
expect(emitterMock.emit as any).toHaveBeenCalledWith(
|
||||||
|
'module.state.changed',
|
||||||
|
expect.objectContaining({ module: 'rbac', currentState: 'unavailable' }),
|
||||||
|
);
|
||||||
|
await module2.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
85
wwjcloud-nest-v1/src/boot-layer/cache-ready.service.spec.ts
Normal file
85
wwjcloud-nest-v1/src/boot-layer/cache-ready.service.spec.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||||
|
import { CacheReadyService } from '@wwjCommon/cache/cache-ready.service';
|
||||||
|
import { RedisService } from '@wwjCommon/cache/redis.service';
|
||||||
|
|
||||||
|
jest.mock('fs', () => ({ writeFileSync: jest.fn() }));
|
||||||
|
|
||||||
|
describe('CacheReadyService readiness emission', () => {
|
||||||
|
let testingModule: TestingModule;
|
||||||
|
let svc: CacheReadyService;
|
||||||
|
const emitterMock = { emit: jest.fn() } as unknown as EventBus;
|
||||||
|
const redisMock = {
|
||||||
|
isEnabled: jest.fn(),
|
||||||
|
getClient: jest.fn(),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
redisMock.isEnabled.mockReturnValue(true);
|
||||||
|
redisMock.getClient.mockReturnValue({
|
||||||
|
ping: jest.fn().mockResolvedValue('PONG'),
|
||||||
|
});
|
||||||
|
|
||||||
|
testingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
CacheReadyService,
|
||||||
|
{ provide: EventBus, useValue: emitterMock },
|
||||||
|
{ provide: RedisService, useValue: redisMock },
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
svc = testingModule.get(CacheReadyService);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
await testingModule?.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits ready when Redis enabled and ping ok', async () => {
|
||||||
|
await svc.onModuleInit();
|
||||||
|
expect(emitterMock.emit as any).toHaveBeenCalledWith(
|
||||||
|
'module.state.changed',
|
||||||
|
expect.objectContaining({ module: 'cache', currentState: 'ready' }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits ready when Redis disabled (fallback)', async () => {
|
||||||
|
redisMock.isEnabled.mockReturnValue(false);
|
||||||
|
const module2 = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
CacheReadyService,
|
||||||
|
{ provide: EventBus, useValue: emitterMock },
|
||||||
|
{ provide: RedisService, useValue: redisMock },
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
const svc2 = module2.get(CacheReadyService);
|
||||||
|
await svc2.onModuleInit();
|
||||||
|
expect(emitterMock.emit as any).toHaveBeenCalledWith(
|
||||||
|
'module.state.changed',
|
||||||
|
expect.objectContaining({ module: 'cache', currentState: 'ready' }),
|
||||||
|
);
|
||||||
|
await module2.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits unavailable when Redis ping fails', async () => {
|
||||||
|
redisMock.isEnabled.mockReturnValue(true);
|
||||||
|
redisMock.getClient.mockReturnValue({
|
||||||
|
ping: jest.fn().mockRejectedValue(new Error('no-conn')),
|
||||||
|
});
|
||||||
|
const module2 = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
CacheReadyService,
|
||||||
|
{ provide: EventBus, useValue: emitterMock },
|
||||||
|
{ provide: RedisService, useValue: redisMock },
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
const svc2 = module2.get(CacheReadyService);
|
||||||
|
await svc2.onModuleInit();
|
||||||
|
expect(emitterMock.emit as any).toHaveBeenCalledWith(
|
||||||
|
'module.state.changed',
|
||||||
|
expect.objectContaining({ module: 'cache', currentState: 'unavailable' }),
|
||||||
|
);
|
||||||
|
await module2.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
37
wwjcloud-nest-v1/src/boot-layer/i18n-ready.service.spec.ts
Normal file
37
wwjcloud-nest-v1/src/boot-layer/i18n-ready.service.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||||
|
import { LangReadyService } from '@wwjCommon/lang/lang-ready.service';
|
||||||
|
|
||||||
|
jest.mock('fs', () => ({
|
||||||
|
existsSync: jest.fn().mockReturnValue(true),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('LangReadyService readiness emission', () => {
|
||||||
|
let testingModule: TestingModule;
|
||||||
|
let svc: LangReadyService;
|
||||||
|
const eventBusMock = { emit: jest.fn() } as unknown as EventBus;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
testingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
LangReadyService,
|
||||||
|
{ provide: EventBus, useValue: eventBusMock },
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
svc = testingModule.get(LangReadyService);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
await testingModule?.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits module.state.changed with ready on init when lang dir exists', async () => {
|
||||||
|
await svc.onModuleInit();
|
||||||
|
expect(eventBusMock.emit as any).toHaveBeenCalledWith(
|
||||||
|
'module.state.changed',
|
||||||
|
expect.objectContaining({ module: 'lang', currentState: 'ready' }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
68
wwjcloud-nest-v1/src/boot-layer/metrics.service.spec.ts
Normal file
68
wwjcloud-nest-v1/src/boot-layer/metrics.service.spec.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { MetricsService } from '@wwjCommon/metrics/metrics.service';
|
||||||
|
|
||||||
|
describe('MetricsService readiness emission', () => {
|
||||||
|
let testingModule: TestingModule;
|
||||||
|
let svc: MetricsService;
|
||||||
|
const emitterMock = { emit: jest.fn() } as unknown as EventBus;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
testingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
MetricsService,
|
||||||
|
{ provide: EventBus, useValue: emitterMock },
|
||||||
|
{
|
||||||
|
provide: ConfigService,
|
||||||
|
useValue: {
|
||||||
|
get: jest.fn().mockImplementation((k: string) => {
|
||||||
|
if (k === 'PROMETHEUS_ENABLED') return 'true';
|
||||||
|
return undefined;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
svc = testingModule.get(MetricsService);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
await testingModule?.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits module.state.changed with ready on init when enabled', async () => {
|
||||||
|
await svc.onModuleInit();
|
||||||
|
expect(emitterMock.emit as any).toHaveBeenCalledWith(
|
||||||
|
'module.state.changed',
|
||||||
|
expect.objectContaining({ module: 'metrics', currentState: 'ready' }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits unavailable when disabled', async () => {
|
||||||
|
const cs = testingModule.get(ConfigService);
|
||||||
|
cs.get.mockImplementation((k: string) =>
|
||||||
|
k === 'PROMETHEUS_ENABLED' ? 'false' : undefined,
|
||||||
|
);
|
||||||
|
// Recreate service to pick new enabled flag
|
||||||
|
const module2 = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
MetricsService,
|
||||||
|
{ provide: EventBus, useValue: emitterMock },
|
||||||
|
{ provide: ConfigService, useValue: cs },
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
const svc2 = module2.get(MetricsService);
|
||||||
|
await svc2.onModuleInit();
|
||||||
|
expect(emitterMock.emit as any).toHaveBeenCalledWith(
|
||||||
|
'module.state.changed',
|
||||||
|
expect.objectContaining({
|
||||||
|
module: 'metrics',
|
||||||
|
currentState: 'unavailable',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await module2.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
87
wwjcloud-nest-v1/src/boot-layer/queue-ready.service.spec.ts
Normal file
87
wwjcloud-nest-v1/src/boot-layer/queue-ready.service.spec.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { QueueReadyService } from '@wwjCommon/queue/queue-ready.service';
|
||||||
|
|
||||||
|
describe('QueueReadyService readiness emission', () => {
|
||||||
|
let testingModule: TestingModule;
|
||||||
|
let svc: QueueReadyService;
|
||||||
|
const emitterMock = { emit: jest.fn() } as unknown as EventBus;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const csMock = {
|
||||||
|
get: jest.fn().mockImplementation((k: string) => {
|
||||||
|
if (k === 'QUEUE_ENABLED') return 'true';
|
||||||
|
if (k === 'QUEUE_DRIVER') return 'bullmq';
|
||||||
|
return undefined;
|
||||||
|
}),
|
||||||
|
} as unknown as ConfigService;
|
||||||
|
|
||||||
|
testingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
QueueReadyService,
|
||||||
|
{ provide: EventBus, useValue: emitterMock },
|
||||||
|
{ provide: ConfigService, useValue: csMock },
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
svc = testingModule.get(QueueReadyService);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
await testingModule?.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits ready when enabled with supported driver', async () => {
|
||||||
|
await svc.onModuleInit();
|
||||||
|
expect(emitterMock.emit as any).toHaveBeenCalledWith(
|
||||||
|
'module.state.changed',
|
||||||
|
expect.objectContaining({ module: 'queue', currentState: 'ready' }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits ready when disabled (non-blocking)', async () => {
|
||||||
|
const cs = testingModule.get(ConfigService);
|
||||||
|
cs.get.mockImplementation((k: string) =>
|
||||||
|
k === 'QUEUE_ENABLED' ? 'false' : undefined,
|
||||||
|
);
|
||||||
|
const module2 = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
QueueReadyService,
|
||||||
|
{ provide: EventBus, useValue: emitterMock },
|
||||||
|
{ provide: ConfigService, useValue: cs },
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
const svc2 = module2.get(QueueReadyService);
|
||||||
|
await svc2.onModuleInit();
|
||||||
|
expect(emitterMock.emit as any).toHaveBeenCalledWith(
|
||||||
|
'module.state.changed',
|
||||||
|
expect.objectContaining({ module: 'queue', currentState: 'ready' }),
|
||||||
|
);
|
||||||
|
await module2.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits unavailable for unknown driver when enabled', async () => {
|
||||||
|
const cs = testingModule.get(ConfigService);
|
||||||
|
cs.get.mockImplementation((k: string) => {
|
||||||
|
if (k === 'QUEUE_ENABLED') return 'true';
|
||||||
|
if (k === 'QUEUE_DRIVER') return 'unknown';
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
const module2 = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
QueueReadyService,
|
||||||
|
{ provide: EventBus, useValue: emitterMock },
|
||||||
|
{ provide: ConfigService, useValue: cs },
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
const svc2 = module2.get(QueueReadyService);
|
||||||
|
await svc2.onModuleInit();
|
||||||
|
expect(emitterMock.emit as any).toHaveBeenCalledWith(
|
||||||
|
'module.state.changed',
|
||||||
|
expect.objectContaining({ module: 'queue', currentState: 'unavailable' }),
|
||||||
|
);
|
||||||
|
await module2.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { StartupValidatorService } from '@wwjCommon/startup/startup-validator.service';
|
||||||
|
import { RedisService } from '@wwjCommon/cache/redis.service';
|
||||||
|
|
||||||
|
jest.mock('fs', () => ({ writeFileSync: jest.fn() }));
|
||||||
|
|
||||||
|
describe('StartupValidatorService readiness emission', () => {
|
||||||
|
let testingModule: TestingModule;
|
||||||
|
let svc: StartupValidatorService;
|
||||||
|
const emitterMock = { emit: jest.fn() } as unknown as EventBus;
|
||||||
|
|
||||||
|
const buildConfigMock = (overrides: Record<string, any> = {}) => {
|
||||||
|
const base: Record<string, any> = { NODE_ENV: 'test' };
|
||||||
|
const all = { ...base, ...overrides };
|
||||||
|
return {
|
||||||
|
get: jest.fn((k: string) => all[k]),
|
||||||
|
} as unknown as ConfigService;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildRedisMock = (enabled: boolean, pingOk: boolean) => {
|
||||||
|
return {
|
||||||
|
isEnabled: jest.fn(() => enabled),
|
||||||
|
getClient: jest.fn(() => ({
|
||||||
|
ping: jest.fn(() =>
|
||||||
|
pingOk ? Promise.resolve('PONG') : Promise.reject(new Error('fail')),
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
} as unknown as RedisService;
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
await testingModule?.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits ready when Redis disabled and env complete', async () => {
|
||||||
|
testingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
StartupValidatorService,
|
||||||
|
{ provide: EventBus, useValue: emitterMock },
|
||||||
|
{ provide: ConfigService, useValue: buildConfigMock() },
|
||||||
|
{ provide: RedisService, useValue: buildRedisMock(false, true) },
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
svc = testingModule.get(StartupValidatorService);
|
||||||
|
|
||||||
|
await svc.onModuleInit();
|
||||||
|
expect(emitterMock.emit as any).toHaveBeenCalledWith(
|
||||||
|
'module.state.changed',
|
||||||
|
expect.objectContaining({ module: 'startup', currentState: 'ready' }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits ready when Redis enabled and ping ok', async () => {
|
||||||
|
testingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
StartupValidatorService,
|
||||||
|
{ provide: EventBus, useValue: emitterMock },
|
||||||
|
{ provide: ConfigService, useValue: buildConfigMock() },
|
||||||
|
{ provide: RedisService, useValue: buildRedisMock(true, true) },
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
svc = testingModule.get(StartupValidatorService);
|
||||||
|
|
||||||
|
await svc.onModuleInit();
|
||||||
|
expect(emitterMock.emit as any).toHaveBeenCalledWith(
|
||||||
|
'module.state.changed',
|
||||||
|
expect.objectContaining({ module: 'startup', currentState: 'ready' }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits unavailable when Redis enabled and ping fails', async () => {
|
||||||
|
testingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
StartupValidatorService,
|
||||||
|
{ provide: EventBus, useValue: emitterMock },
|
||||||
|
{ provide: ConfigService, useValue: buildConfigMock() },
|
||||||
|
{ provide: RedisService, useValue: buildRedisMock(true, false) },
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
svc = testingModule.get(StartupValidatorService);
|
||||||
|
|
||||||
|
await svc.onModuleInit();
|
||||||
|
expect(emitterMock.emit as any).toHaveBeenCalledWith(
|
||||||
|
'module.state.changed',
|
||||||
|
expect.objectContaining({
|
||||||
|
module: 'startup',
|
||||||
|
currentState: 'unavailable',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits unavailable when NODE_ENV missing', async () => {
|
||||||
|
testingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
StartupValidatorService,
|
||||||
|
{ provide: EventBus, useValue: emitterMock },
|
||||||
|
{
|
||||||
|
provide: ConfigService,
|
||||||
|
useValue: buildConfigMock({ NODE_ENV: undefined }),
|
||||||
|
},
|
||||||
|
{ provide: RedisService, useValue: buildRedisMock(false, true) },
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
svc = testingModule.get(StartupValidatorService);
|
||||||
|
|
||||||
|
await svc.onModuleInit();
|
||||||
|
expect(emitterMock.emit as any).toHaveBeenCalledWith(
|
||||||
|
'module.state.changed',
|
||||||
|
expect.objectContaining({
|
||||||
|
module: 'startup',
|
||||||
|
currentState: 'unavailable',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -9,7 +9,8 @@ async function bootstrap() {
|
|||||||
// 固定端口为 3000,不再受环境变量影响 -> 改为可配置
|
// 固定端口为 3000,不再受环境变量影响 -> 改为可配置
|
||||||
const config = app.get(ConfigService);
|
const config = app.get(ConfigService);
|
||||||
const raw = config.get<string | number>('PORT');
|
const raw = config.get<string | number>('PORT');
|
||||||
const port = typeof raw === 'number' ? raw : parseInt(String(raw ?? '3000'), 10) || 3000;
|
const port =
|
||||||
|
typeof raw === 'number' ? raw : parseInt(String(raw ?? '3000'), 10) || 3000;
|
||||||
await app.listen(port);
|
await app.listen(port);
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"nodeVersion": "v20.13.1",
|
"nodeVersion": "v20.13.1",
|
||||||
"nodeEnv": "development",
|
"nodeEnv": "test",
|
||||||
"timestamp": 1760860301672,
|
"timestamp": 1760885126089,
|
||||||
"redis": {
|
"redis": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"connected": false,
|
"connected": false,
|
||||||
|
|||||||
@@ -7,18 +7,22 @@ import { BootHttp } from '@wwjCommon/http/boot-http';
|
|||||||
// 启用 AI 与 Prometheus,并允许速率限制。
|
// 启用 AI 与 Prometheus,并允许速率限制。
|
||||||
process.env.AI_ENABLED = 'true';
|
process.env.AI_ENABLED = 'true';
|
||||||
process.env.PROMETHEUS_ENABLED = 'true';
|
process.env.PROMETHEUS_ENABLED = 'true';
|
||||||
process.env.RATE_LIMIT_ENABLED = 'true';
|
process.env.RATE_LIMIT_ENABLED = 'false';
|
||||||
// 在 e2e 中开启直接指标与入队开关,确保端到端断言稳定
|
// 在 e2e 中开启直接指标与入队开关,确保端到端断言稳定
|
||||||
process.env.AI_SIMULATE_DIRECT_ENQUEUE = 'true';
|
process.env.AI_SIMULATE_DIRECT_ENQUEUE = 'true';
|
||||||
// 配置全局前缀,确保异常过滤器将 /api/metrics 识别为基础设施路由
|
// 配置全局前缀,确保异常过滤器将 /api/metrics 识别为基础设施路由
|
||||||
process.env.GLOBAL_PREFIX = 'api';
|
process.env.GLOBAL_PREFIX = 'api';
|
||||||
|
// 关闭鉴权与 RBAC,避免无令牌访问受阻
|
||||||
|
process.env.AUTH_ENABLED = 'false';
|
||||||
|
process.env.RBAC_ENABLED = 'false';
|
||||||
|
|
||||||
import { AppModule } from './../src/app.module';
|
// AppModule 将在 beforeAll 中通过 require 加载,确保环境变量生效
|
||||||
|
|
||||||
describe('WWJCloud v11 E2E', () => {
|
describe('WWJCloud v11 E2E', () => {
|
||||||
let app: INestApplication;
|
let app: INestApplication;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
const { AppModule } = require('../apps/api/src/app.module');
|
||||||
const moduleFixture = await Test.createTestingModule({
|
const moduleFixture = await Test.createTestingModule({
|
||||||
imports: [AppModule],
|
imports: [AppModule],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|||||||
34
wwjcloud-nest-v1/test/testing-preset.ts
Normal file
34
wwjcloud-nest-v1/test/testing-preset.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import type { Provider, Type } from '@nestjs/common';
|
||||||
|
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { HttpClientService } from '@wwjCommon/resilience/http-client.service';
|
||||||
|
import { RateLimitGuard } from '@wwjCommon/http/rate-limit.guard';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a robust TestingModule for controller unit tests with common mocks
|
||||||
|
* and guard overrides to avoid external infrastructure dependencies.
|
||||||
|
*/
|
||||||
|
export async function buildControllerTestModule(
|
||||||
|
controller: Type<any>,
|
||||||
|
extraProviders: Provider[] = [],
|
||||||
|
guardOverrides: Array<[Type<any>, any]> = [
|
||||||
|
[RateLimitGuard, { canActivate: jest.fn().mockReturnValue(true) }],
|
||||||
|
],
|
||||||
|
): Promise<TestingModule> {
|
||||||
|
const moduleBuilder = Test.createTestingModule({
|
||||||
|
controllers: [controller],
|
||||||
|
providers: [
|
||||||
|
{ provide: EventBus, useValue: { emit: jest.fn() } },
|
||||||
|
{ provide: ConfigService, useValue: { get: jest.fn() } },
|
||||||
|
{ provide: HttpClientService, useValue: { getWithFallback: jest.fn() } },
|
||||||
|
...extraProviders,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const [guard, value] of guardOverrides) {
|
||||||
|
moduleBuilder.overrideGuard(guard).useValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await moduleBuilder.compile();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user