feat(ai): 添加框架级技能包和 Vendor 层架构升级
- 新增 Generator 层:框架规范知识库 + 6 个代码生成器(Entity/Controller/Service/DTO/SQL/Module) - 新增 CodegenSkill:对接 AgenticLoop Function Calling,支持自然语言生成业务模块 - 新增 AiGenerateController:HTTP API 入口(自然语言生成 + 直接生成 + 规范查询) - 新增 Vendor 层:Provider 注册中心 + 统一网关 + 错误体系 + 3 个真实 Provider 实现 - 新增 AI Runtime:ReAct 循环 + 4 种循环检测器 + LLM Provider 抽象(OpenAI/Ollama) - 新增 Skills 系统:技能注册/发现/执行,热插拔设计 - 新增 Memory 系统:短期会话记忆 + 长期经验记忆,自动注入 AgenticLoop - 集成 SkillExecutor 和 Memory 到 AgenticLoop,替换占位符为真实执行 - 修复 158 个历史编译错误,TypeScript 严格模式增强 - ESLint 新增 6 条 any 相关规则
This commit is contained in:
@@ -60,4 +60,14 @@ export default tseslint.config(
|
|||||||
'@typescript-eslint/unbound-method': 'off',
|
'@typescript-eslint/unbound-method': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-call': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-return': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { Controller, Post, Body, Get } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||||
|
import { AgenticLoopService } from '../runtime/agentic-loop.service';
|
||||||
|
import { FrameworkKnowledgeService } from './framework-knowledge.service';
|
||||||
|
import { ModuleGenerator } from './module.generator';
|
||||||
|
import { ModuleGenerateRequest, GeneratedFile } from './generator.interface';
|
||||||
|
import { LlmMessage } from '../providers/llm-provider.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 代码生成控制器
|
||||||
|
*
|
||||||
|
* 提供 HTTP API 入口,接收用户的自然语言描述,
|
||||||
|
* 调用 AgenticLoop 执行代码生成。
|
||||||
|
*
|
||||||
|
* 借鉴 NiuCloud Lite AI 的 AI 开发扩展能力,
|
||||||
|
* 适配 NestJS 技术栈。
|
||||||
|
*/
|
||||||
|
@ApiTags('AI 代码生成')
|
||||||
|
@Controller('adminapi/ai/generate')
|
||||||
|
export class AiGenerateController {
|
||||||
|
constructor(
|
||||||
|
private readonly agenticLoop: AgenticLoopService,
|
||||||
|
private readonly knowledge: FrameworkKnowledgeService,
|
||||||
|
private readonly moduleGenerator: ModuleGenerator,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据自然语言描述生成业务模块
|
||||||
|
* @param body 生成请求
|
||||||
|
*/
|
||||||
|
@Post('module')
|
||||||
|
@ApiOperation({ summary: 'AI 生成业务模块(自然语言 → 完整模块代码)' })
|
||||||
|
async generateFromNaturalLanguage(
|
||||||
|
@Body() body: { description: string; moduleName?: string },
|
||||||
|
) {
|
||||||
|
const systemPrompt = this.knowledge.getSystemPromptAddendum();
|
||||||
|
|
||||||
|
const messages: LlmMessage[] = [
|
||||||
|
{ role: 'system', content: `${systemPrompt}\n\n你是一个代码生成助手。根据用户的自然语言描述,生成符合 WWJCloud v1 规范的 NestJS 业务模块代码。\n\n请以 JSON 格式返回模块生成请求,格式如下:\n${JSON.stringify({
|
||||||
|
moduleName: '示例模块名',
|
||||||
|
description: '模块描述',
|
||||||
|
tableName: 'nc_表名',
|
||||||
|
fields: [
|
||||||
|
{ name: 'id', mysqlType: 'int', isPrimary: true, isAutoIncrement: true, comment: '主键ID' },
|
||||||
|
{ name: 'title', mysqlType: 'varchar(255)', comment: '标题' },
|
||||||
|
{ name: 'status', mysqlType: 'tinyint', defaultValue: '1', comment: '状态' },
|
||||||
|
{ name: 'create_time', mysqlType: 'int', defaultValue: '0', comment: '创建时间' },
|
||||||
|
],
|
||||||
|
endpoints: 'adminapi',
|
||||||
|
}, null, 2)}` },
|
||||||
|
{ role: 'user', content: body.description },
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await this.agenticLoop.run(
|
||||||
|
{
|
||||||
|
description: `根据自然语言生成业务模块: ${body.description}`,
|
||||||
|
type: 'codegen',
|
||||||
|
sessionId: `codegen_${Date.now()}`,
|
||||||
|
},
|
||||||
|
messages,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: result.success,
|
||||||
|
response: result.response,
|
||||||
|
iterations: result.iterations,
|
||||||
|
durationMs: result.durationMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接生成业务模块(结构化参数)
|
||||||
|
* @param request 模块生成请求
|
||||||
|
*/
|
||||||
|
@Post('module/direct')
|
||||||
|
@ApiOperation({ summary: '直接生成业务模块(结构化参数)' })
|
||||||
|
async generateModuleDirect(@Body() request: ModuleGenerateRequest) {
|
||||||
|
const files = this.moduleGenerator.generate(request);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
moduleName: request.moduleName,
|
||||||
|
fileCount: files.length,
|
||||||
|
files: files.map((f) => ({
|
||||||
|
path: f.path,
|
||||||
|
type: f.type,
|
||||||
|
description: f.description,
|
||||||
|
contentLength: f.content.length,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取框架规范信息
|
||||||
|
*/
|
||||||
|
@Get('knowledge')
|
||||||
|
@ApiOperation({ summary: '获取框架规范知识' })
|
||||||
|
getKnowledge() {
|
||||||
|
return {
|
||||||
|
techStack: this.knowledge.getTechStack(),
|
||||||
|
layers: this.knowledge.getLayerArchitecture(),
|
||||||
|
naming: this.knowledge.getNamingConventions(),
|
||||||
|
existingModules: this.knowledge.getExistingModules(),
|
||||||
|
prohibitions: this.knowledge.getProhibitions(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已有模块列表
|
||||||
|
*/
|
||||||
|
@Get('modules')
|
||||||
|
@ApiOperation({ summary: '获取已有业务模块列表' })
|
||||||
|
getModules() {
|
||||||
|
return {
|
||||||
|
adminapi: this.knowledge.getExistingModules('adminapi'),
|
||||||
|
api: this.knowledge.getExistingModules('api'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { FrameworkKnowledgeService } from './framework-knowledge.service';
|
||||||
|
import { EntityGenerator } from './entity.generator';
|
||||||
|
import { ControllerGenerator } from './controller.generator';
|
||||||
|
import { ServiceGenerator } from './service.generator';
|
||||||
|
import { DtoGenerator } from './dto.generator';
|
||||||
|
import { SqlGenerator } from './sql.generator';
|
||||||
|
import { ModuleGenerator } from './module.generator';
|
||||||
|
import { CodegenSkill } from './codegen.skill';
|
||||||
|
import { AiGenerateController } from './ai-generate.controller';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 代码生成模块
|
||||||
|
*
|
||||||
|
* 借鉴 NiuCloud Lite AI 的 Skills 模块化开发规范,
|
||||||
|
* 提供框架级代码生成能力:
|
||||||
|
* - 实体生成器(MySQL → TypeORM)
|
||||||
|
* - 控制器生成器(PHP → NestJS)
|
||||||
|
* - 服务生成器(PHP Service → NestJS Service)
|
||||||
|
* - DTO 生成器(PHP Validate → class-validator)
|
||||||
|
* - SQL 生成器(建表脚本)
|
||||||
|
* - 完整模块脚手架(一键生成全套)
|
||||||
|
*/
|
||||||
|
@Module({
|
||||||
|
providers: [
|
||||||
|
FrameworkKnowledgeService,
|
||||||
|
EntityGenerator,
|
||||||
|
ControllerGenerator,
|
||||||
|
ServiceGenerator,
|
||||||
|
DtoGenerator,
|
||||||
|
SqlGenerator,
|
||||||
|
ModuleGenerator,
|
||||||
|
CodegenSkill,
|
||||||
|
AiGenerateController,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
FrameworkKnowledgeService,
|
||||||
|
ModuleGenerator,
|
||||||
|
CodegenSkill,
|
||||||
|
],
|
||||||
|
controllers: [AiGenerateController],
|
||||||
|
})
|
||||||
|
export class AiGeneratorModule {}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ISkill, SkillDefinition, SkillContext, SkillResult } from '../skills/skill.interface';
|
||||||
|
import { LlmToolDefinition } from '../providers/llm-provider.interface';
|
||||||
|
import { ModuleGenerator } from './module.generator';
|
||||||
|
import { EntityGenerator } from './entity.generator';
|
||||||
|
import { ControllerGenerator } from './controller.generator';
|
||||||
|
import { ServiceGenerator } from './service.generator';
|
||||||
|
import { DtoGenerator } from './dto.generator';
|
||||||
|
import { SqlGenerator } from './sql.generator';
|
||||||
|
import { FrameworkKnowledgeService } from './framework-knowledge.service';
|
||||||
|
import {
|
||||||
|
ModuleGenerateRequest,
|
||||||
|
GeneratedFile,
|
||||||
|
CODEGEN_TOOL_DEFINITIONS,
|
||||||
|
} from './generator.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代码生成 Skill — 借鉴 NiuCloud Lite AI 的 Skills 模块化开发
|
||||||
|
*
|
||||||
|
* 将代码生成能力注册为 AI Skill,
|
||||||
|
* 使 AgenticLoop 中的 Agent 能通过 Function Calling 调用代码生成工具。
|
||||||
|
*
|
||||||
|
* 支持的生成类型:
|
||||||
|
* - generate_entity: 生成 TypeORM 实体
|
||||||
|
* - generate_controller: 生成 NestJS 控制器
|
||||||
|
* - generate_service: 生成 NestJS 服务
|
||||||
|
* - generate_dto: 生成 DTO 参数和视图对象
|
||||||
|
* - generate_sql: 生成建表 SQL
|
||||||
|
* - generate_module: 生成完整业务模块
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class CodegenSkill implements ISkill {
|
||||||
|
private readonly logger = new Logger(CodegenSkill.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly moduleGenerator: ModuleGenerator,
|
||||||
|
private readonly entityGenerator: EntityGenerator,
|
||||||
|
private readonly controllerGenerator: ControllerGenerator,
|
||||||
|
private readonly serviceGenerator: ServiceGenerator,
|
||||||
|
private readonly dtoGenerator: DtoGenerator,
|
||||||
|
private readonly sqlGenerator: SqlGenerator,
|
||||||
|
private readonly knowledge: FrameworkKnowledgeService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取技能定义
|
||||||
|
*/
|
||||||
|
getDefinition(): SkillDefinition {
|
||||||
|
return {
|
||||||
|
name: 'codegen',
|
||||||
|
description: 'WWJCloud v1 代码生成技能 — 根据自然语言描述生成符合项目规范的 NestJS 业务模块代码',
|
||||||
|
version: '1.0.0',
|
||||||
|
triggers: ['生成', '创建', '新建', 'generate', 'create', '代码', '模块', '实体', '控制器', '服务'],
|
||||||
|
tools: CODEGEN_TOOL_DEFINITIONS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行代码生成工具
|
||||||
|
* @param toolName 工具名称
|
||||||
|
* @param argsJson 工具参数 JSON
|
||||||
|
* @param context 执行上下文
|
||||||
|
*/
|
||||||
|
async execute(toolName: string, argsJson: string, context: SkillContext): Promise<SkillResult> {
|
||||||
|
try {
|
||||||
|
const args = JSON.parse(argsJson);
|
||||||
|
let files: GeneratedFile[];
|
||||||
|
|
||||||
|
switch (toolName) {
|
||||||
|
case 'generate_entity':
|
||||||
|
files = this.entityGenerator.generate(this.buildRequest(args));
|
||||||
|
break;
|
||||||
|
case 'generate_controller':
|
||||||
|
files = this.controllerGenerator.generate(this.buildRequest(args));
|
||||||
|
break;
|
||||||
|
case 'generate_service':
|
||||||
|
files = this.serviceGenerator.generate(this.buildRequest(args));
|
||||||
|
break;
|
||||||
|
case 'generate_dto':
|
||||||
|
files = this.dtoGenerator.generate(this.buildRequest(args));
|
||||||
|
break;
|
||||||
|
case 'generate_sql':
|
||||||
|
files = this.sqlGenerator.generate(this.buildRequest(args));
|
||||||
|
break;
|
||||||
|
case 'generate_module':
|
||||||
|
files = this.moduleGenerator.generate(this.buildRequest(args));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
output: `未知工具: ${toolName}`,
|
||||||
|
error: `UNKNOWN_TOOL: ${toolName}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = files
|
||||||
|
.map((f) => ` [${f.type}] ${f.path}`)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
this.logger.log(`[CodegenSkill] ${toolName} 生成 ${files.length} 个文件`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
output: `代码生成成功,共 ${files.length} 个文件:\n${summary}`,
|
||||||
|
metadata: {
|
||||||
|
toolName,
|
||||||
|
fileCount: files.length,
|
||||||
|
files: files.map((f) => ({ path: f.path, type: f.type, description: f.description })),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[CodegenSkill] 执行失败: ${toolName}`, error instanceof Error ? error.stack : String(error));
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
output: `代码生成失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 LLM 参数构建生成请求
|
||||||
|
*/
|
||||||
|
private buildRequest(args: Record<string, unknown>): ModuleGenerateRequest {
|
||||||
|
return {
|
||||||
|
moduleName: (args.moduleName as string) || 'demo',
|
||||||
|
description: (args.description as string) || '',
|
||||||
|
tableName: (args.tableName as string) || `nc_${args.moduleName || 'demo'}`,
|
||||||
|
fields: (args.fields as import('./generator.interface').TableField[]) || [],
|
||||||
|
endpoints: (args.endpoints as ModuleGenerateRequest['endpoints']) || 'adminapi',
|
||||||
|
phpMethods: (args.methods as import('./generator.interface').PhpMethod[]) || [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ICodeGenerator, GeneratedFile, ModuleGenerateRequest, PhpMethod } from './generator.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 控制器文件生成器
|
||||||
|
*
|
||||||
|
* 根据 PHP 控制器方法生成 NestJS 控制器文件,
|
||||||
|
* 方法名和路由与 PHP 项目 100% 保持一致。
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ControllerGenerator implements ICodeGenerator {
|
||||||
|
/**
|
||||||
|
* 生成控制器文件
|
||||||
|
*/
|
||||||
|
generate(request: ModuleGenerateRequest): GeneratedFile[] {
|
||||||
|
const files: GeneratedFile[] = [];
|
||||||
|
const endpoints = request.endpoints === 'both'
|
||||||
|
? ['adminapi', 'api'] as const
|
||||||
|
: [request.endpoints] as const;
|
||||||
|
|
||||||
|
for (const endpoint of endpoints) {
|
||||||
|
const file = this.generateControllerFile(request, endpoint);
|
||||||
|
if (file) files.push(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成单个控制器文件
|
||||||
|
*/
|
||||||
|
private generateControllerFile(request: ModuleGenerateRequest, endpoint: 'adminapi' | 'api'): GeneratedFile | null {
|
||||||
|
const { moduleName } = request;
|
||||||
|
const methods = this.filterMethodsByEndpoint(request.phpMethods ?? [], endpoint);
|
||||||
|
if (methods.length === 0) return null;
|
||||||
|
|
||||||
|
const className = `${this.toPascalCase(moduleName)}Controller`;
|
||||||
|
const routePrefix = endpoint === 'adminapi' ? 'adminapi' : 'api';
|
||||||
|
const methodsCode = methods.map((m) => this.generateMethod(m, moduleName)).join('\n\n');
|
||||||
|
|
||||||
|
const content = `import { Controller, Get, Post, Put, Delete, Param, Body, Query } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ${request.description || moduleName} 控制器
|
||||||
|
* 对应 PHP: app/${endpoint}/controller/${moduleName}/${this.toPascalCase(moduleName)}.php
|
||||||
|
*/
|
||||||
|
@ApiTags('${request.description || moduleName}')
|
||||||
|
@Controller('${routePrefix}/${moduleName}')
|
||||||
|
export class ${className} {
|
||||||
|
${methodsCode}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: `libs/wwjcloud-core/src/controllers/${endpoint}/${moduleName}/${moduleName}.controller.ts`,
|
||||||
|
content,
|
||||||
|
type: 'controller',
|
||||||
|
description: `${endpoint}/${moduleName} 控制器`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成单个控制器方法
|
||||||
|
*/
|
||||||
|
private generateMethod(method: PhpMethod, moduleName: string): string {
|
||||||
|
const httpDecorator = this.getHttpDecorator(method.httpMethod, method.route);
|
||||||
|
const params = this.extractParams(method);
|
||||||
|
const paramName = this.toCamelCase(method.name);
|
||||||
|
const returnType = 'Promise<any>';
|
||||||
|
|
||||||
|
return ` /**
|
||||||
|
* ${method.description}
|
||||||
|
* 对应 PHP: public function ${method.name}()
|
||||||
|
*/
|
||||||
|
${httpDecorator}
|
||||||
|
@ApiOperation({ summary: '${method.description}' })
|
||||||
|
async ${paramName}(${params}): ${returnType} {
|
||||||
|
// TODO: 对接 ${moduleName} 服务层
|
||||||
|
return {};
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 HTTP 装饰器
|
||||||
|
*/
|
||||||
|
private getHttpDecorator(httpMethod: string, route: string): string {
|
||||||
|
const methodMap: Record<string, string> = {
|
||||||
|
GET: 'Get',
|
||||||
|
POST: 'Post',
|
||||||
|
PUT: 'Put',
|
||||||
|
DELETE: 'Delete',
|
||||||
|
};
|
||||||
|
const decorator = methodMap[httpMethod] || 'Get';
|
||||||
|
return `@${decorator}('${route}')`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取方法参数
|
||||||
|
*/
|
||||||
|
private extractParams(method: PhpMethod): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
if (method.params) {
|
||||||
|
for (const param of method.params) {
|
||||||
|
if (param.match(/^\d+$/) || param === 'id' || param.endsWith('Id') || param.endsWith('_id')) {
|
||||||
|
parts.push(`@Param('${param}') ${param}: number`);
|
||||||
|
} else if (method.httpMethod === 'GET') {
|
||||||
|
parts.push(`@Query('${param}') ${param}: string`);
|
||||||
|
} else {
|
||||||
|
parts.push(`@Body('${param}') ${param}: any`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按端类型过滤方法
|
||||||
|
*/
|
||||||
|
private filterMethodsByEndpoint(methods: PhpMethod[], endpoint: 'adminapi' | 'api'): PhpMethod[] {
|
||||||
|
if (methods.length === 0) {
|
||||||
|
// 如果没有提供 PHP 方法,生成默认 CRUD 方法
|
||||||
|
return this.getDefaultMethods(endpoint);
|
||||||
|
}
|
||||||
|
return methods;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取默认 CRUD 方法
|
||||||
|
*/
|
||||||
|
private getDefaultMethods(endpoint: 'adminapi' | 'api'): PhpMethod[] {
|
||||||
|
const prefix = endpoint === 'adminapi' ? '' : '';
|
||||||
|
return [
|
||||||
|
{ name: 'lists', httpMethod: 'GET', route: 'lists', description: '获取列表', params: [] },
|
||||||
|
{ name: 'info', httpMethod: 'GET', route: 'info/:id', description: '获取详情', params: ['id'] },
|
||||||
|
{ name: 'add', httpMethod: 'POST', route: 'add', description: '新增', params: [] },
|
||||||
|
{ name: 'edit', httpMethod: 'PUT', route: 'edit/:id', description: '编辑', params: ['id'] },
|
||||||
|
{ name: 'del', httpMethod: 'DELETE', route: 'del/:id', description: '删除', params: ['id'] },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下划线转驼峰
|
||||||
|
*/
|
||||||
|
private toCamelCase(str: string): string {
|
||||||
|
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转 PascalCase
|
||||||
|
*/
|
||||||
|
private toPascalCase(str: string): string {
|
||||||
|
const camel = this.toCamelCase(str);
|
||||||
|
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ICodeGenerator, GeneratedFile, ModuleGenerateRequest, TableField } from './generator.interface';
|
||||||
|
import { DB_TYPE_MAPPING } from './framework-knowledge';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO 文件生成器
|
||||||
|
*
|
||||||
|
* 生成 class-validator 风格的参数(Param)和视图(VO)对象,
|
||||||
|
* 对应 PHP 的 validate 验证器。
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class DtoGenerator implements ICodeGenerator {
|
||||||
|
/**
|
||||||
|
* 生成 DTO 文件
|
||||||
|
*/
|
||||||
|
generate(request: ModuleGenerateRequest): GeneratedFile[] {
|
||||||
|
const files: GeneratedFile[] = [];
|
||||||
|
|
||||||
|
// 生成参数 DTO
|
||||||
|
files.push(this.generateParamDto(request, 'admin'));
|
||||||
|
if (request.endpoints === 'api' || request.endpoints === 'both') {
|
||||||
|
files.push(this.generateParamDto(request, 'api'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成 VO DTO
|
||||||
|
files.push(this.generateVoDto(request, 'admin'));
|
||||||
|
if (request.endpoints === 'api' || request.endpoints === 'both') {
|
||||||
|
files.push(this.generateVoDto(request, 'api'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成参数 DTO(对应 PHP validate)
|
||||||
|
*/
|
||||||
|
private generateParamDto(request: ModuleGenerateRequest, layer: 'admin' | 'api'): GeneratedFile {
|
||||||
|
const { moduleName, fields } = request;
|
||||||
|
const className = `${this.toPascalCase(moduleName)}Param`;
|
||||||
|
|
||||||
|
// 排除主键和系统字段
|
||||||
|
const inputFields = fields.filter((f) =>
|
||||||
|
!f.isPrimary && !f.isAutoIncrement &&
|
||||||
|
f.name !== 'create_time' && f.name !== 'update_time' &&
|
||||||
|
f.name !== 'delete_time' && f.name !== 'is_del',
|
||||||
|
);
|
||||||
|
|
||||||
|
const propertiesCode = inputFields.map((f) => this.generateParamProperty(f)).join('\n\n ');
|
||||||
|
|
||||||
|
const content = `import { IsString, IsNumber, IsOptional, IsArray, IsBoolean } from 'class-validator';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ${request.description || moduleName} 参数 DTO
|
||||||
|
* 对应 PHP: app/validate/${moduleName}/${this.toPascalCase(moduleName)}.php
|
||||||
|
*/
|
||||||
|
export class ${className} {
|
||||||
|
${propertiesCode}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: `libs/wwjcloud-core/src/dtos/${layer}/${moduleName}/param/${moduleName}.param.ts`,
|
||||||
|
content,
|
||||||
|
type: 'dto',
|
||||||
|
description: `${moduleName} ${layer} 参数 DTO`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 VO DTO(视图对象)
|
||||||
|
*/
|
||||||
|
private generateVoDto(request: ModuleGenerateRequest, layer: 'admin' | 'api'): GeneratedFile {
|
||||||
|
const { moduleName, fields } = request;
|
||||||
|
const className = `${this.toPascalCase(moduleName)}Vo`;
|
||||||
|
|
||||||
|
const propertiesCode = fields.map((f) => this.generateVoProperty(f)).join('\n\n ');
|
||||||
|
|
||||||
|
const content = `import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ${request.description || moduleName} 视图对象
|
||||||
|
*/
|
||||||
|
export class ${className} {
|
||||||
|
${propertiesCode}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: `libs/wwjcloud-core/src/dtos/${layer}/${moduleName}/vo/${moduleName}.vo.ts`,
|
||||||
|
content,
|
||||||
|
type: 'dto',
|
||||||
|
description: `${moduleName} ${layer} VO DTO`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成参数属性(带 class-validator 装饰器)
|
||||||
|
*/
|
||||||
|
private generateParamProperty(field: TableField): string {
|
||||||
|
const camelName = this.toCamelCase(field.name);
|
||||||
|
const typeMapping = this.mapType(field.mysqlType);
|
||||||
|
const decorators: string[] = [];
|
||||||
|
const apiDecorator = field.nullable ? 'ApiPropertyOptional' : 'ApiProperty';
|
||||||
|
|
||||||
|
if (field.nullable) {
|
||||||
|
decorators.push('IsOptional()');
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (typeMapping.tsType) {
|
||||||
|
case 'string':
|
||||||
|
decorators.push('IsString()');
|
||||||
|
break;
|
||||||
|
case 'number':
|
||||||
|
decorators.push('IsNumber()');
|
||||||
|
break;
|
||||||
|
case 'boolean':
|
||||||
|
decorators.push('IsBoolean()');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoratorStr = decorators.map((d) => ` @${d}`).join('\n');
|
||||||
|
const apiStr = ` @${apiDecorator}({ description: '${field.comment || field.name}' })`;
|
||||||
|
|
||||||
|
return `${decoratorStr}\n${apiStr}\n ${camelName}: ${typeMapping.tsType};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 VO 属性(仅 Swagger 注解)
|
||||||
|
*/
|
||||||
|
private generateVoProperty(field: TableField): string {
|
||||||
|
const camelName = this.toCamelCase(field.name);
|
||||||
|
const typeMapping = this.mapType(field.mysqlType);
|
||||||
|
|
||||||
|
return ` @ApiProperty({ description: '${field.comment || field.name}' })
|
||||||
|
${camelName}: ${typeMapping.tsType};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 映射 MySQL 类型
|
||||||
|
*/
|
||||||
|
private mapType(mysqlType: string): { typeormType: string; tsType: string } {
|
||||||
|
const baseType = mysqlType.replace(/\(.*\)/, '').trim().toLowerCase();
|
||||||
|
return DB_TYPE_MAPPING[baseType] ?? { typeormType: 'varchar', tsType: 'string' };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下划线转驼峰
|
||||||
|
*/
|
||||||
|
private toCamelCase(str: string): string {
|
||||||
|
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转 PascalCase
|
||||||
|
*/
|
||||||
|
private toPascalCase(str: string): string {
|
||||||
|
const camel = this.toCamelCase(str);
|
||||||
|
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ICodeGenerator, GeneratedFile, ModuleGenerateRequest, TableField } from './generator.interface';
|
||||||
|
import { DB_TYPE_MAPPING } from './framework-knowledge';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实体文件生成器
|
||||||
|
*
|
||||||
|
* 根据数据库表结构生成 TypeORM 实体文件,
|
||||||
|
* 字段名与 PHP 项目 100% 保持一致。
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class EntityGenerator implements ICodeGenerator {
|
||||||
|
/**
|
||||||
|
* 生成 TypeORM 实体文件
|
||||||
|
*/
|
||||||
|
generate(request: ModuleGenerateRequest): GeneratedFile[] {
|
||||||
|
const files: GeneratedFile[] = [];
|
||||||
|
const entityFile = this.generateEntityFile(request);
|
||||||
|
files.push(entityFile);
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成单个实体文件内容
|
||||||
|
*/
|
||||||
|
private generateEntityFile(request: ModuleGenerateRequest): GeneratedFile {
|
||||||
|
const { moduleName, tableName, fields } = request;
|
||||||
|
const className = this.toPascalCase(tableName.replace(/^nc_/, '').replace(/_/g, ' '));
|
||||||
|
const columnsCode = fields.map((f) => this.generateColumn(f)).join('\n\n ');
|
||||||
|
|
||||||
|
const content = `import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ${request.description || `${tableName} 实体`}
|
||||||
|
* 对应数据库表: ${tableName}
|
||||||
|
*/
|
||||||
|
@Entity('${tableName}')
|
||||||
|
export class ${className} {
|
||||||
|
${columnsCode}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: `libs/wwjcloud-core/src/entities/${tableName.replace(/^nc_/, '')}.entity.ts`,
|
||||||
|
content,
|
||||||
|
type: 'entity',
|
||||||
|
description: `${tableName} 实体文件`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成单个字段的 TypeORM 列定义
|
||||||
|
*/
|
||||||
|
private generateColumn(field: TableField): string {
|
||||||
|
if (field.isPrimary) {
|
||||||
|
return ` @PrimaryGeneratedColumn({ name: '${field.name}'${field.comment ? `, comment: '${field.comment}'` : ''} })
|
||||||
|
${this.toCamelCase(field.name)}: number;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeMapping = this.mapType(field.mysqlType);
|
||||||
|
const columnOptions: string[] = [];
|
||||||
|
|
||||||
|
columnOptions.push(`name: '${field.name}'`);
|
||||||
|
columnOptions.push(`type: '${typeMapping.typeormType}'`);
|
||||||
|
|
||||||
|
if (field.mysqlType.match(/\(\d+\)/)) {
|
||||||
|
const length = field.mysqlType.match(/\((\d+)\)/)?.[1];
|
||||||
|
if (length) columnOptions.push(`length: ${length}`);
|
||||||
|
}
|
||||||
|
if (field.unsigned) columnOptions.push(`unsigned: true`);
|
||||||
|
if (field.defaultValue !== undefined) {
|
||||||
|
columnOptions.push(`default: ${this.formatDefault(field.defaultValue)}`);
|
||||||
|
}
|
||||||
|
if (!field.nullable && field.defaultValue === undefined) {
|
||||||
|
columnOptions.push(`default: ''`);
|
||||||
|
}
|
||||||
|
if (field.nullable) columnOptions.push(`nullable: true`);
|
||||||
|
if (field.comment) columnOptions.push(`comment: '${field.comment}'`);
|
||||||
|
|
||||||
|
return ` @Column({ ${columnOptions.join(', ')} })
|
||||||
|
${this.toCamelCase(field.name)}: ${typeMapping.tsType};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 映射 MySQL 类型到 TypeORM + TypeScript 类型
|
||||||
|
*/
|
||||||
|
private mapType(mysqlType: string): { typeormType: string; tsType: string } {
|
||||||
|
const baseType = mysqlType.replace(/\(.*\)/, '').trim().toLowerCase();
|
||||||
|
return DB_TYPE_MAPPING[baseType] ?? { typeormType: 'varchar', tsType: 'string' };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化默认值
|
||||||
|
*/
|
||||||
|
private formatDefault(value: string): string {
|
||||||
|
if (value === 'NULL' || value === 'null') return 'null';
|
||||||
|
if (value.match(/^\d+$/)) return value;
|
||||||
|
return `'${value}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下划线命名转驼峰
|
||||||
|
*/
|
||||||
|
private toCamelCase(str: string): string {
|
||||||
|
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下划线命名转 PascalCase
|
||||||
|
*/
|
||||||
|
private toPascalCase(str: string): string {
|
||||||
|
const camel = this.toCamelCase(str);
|
||||||
|
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
FRAMEWORK_TECH_STACK,
|
||||||
|
LAYER_ARCHITECTURE,
|
||||||
|
ACTUAL_DIRECTORY_STRUCTURE,
|
||||||
|
NAMING_CONVENTIONS,
|
||||||
|
PHP_NESTJS_MAPPING,
|
||||||
|
STRICT_PROHIBITIONS,
|
||||||
|
DB_TYPE_MAPPING,
|
||||||
|
EXISTING_MODULES,
|
||||||
|
CODE_TEMPLATES,
|
||||||
|
QUALITY_STANDARDS,
|
||||||
|
} from './framework-knowledge';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 框架规范知识查询服务
|
||||||
|
*
|
||||||
|
* 为代码生成 Skills 提供规范知识的统一查询入口,
|
||||||
|
* 确保 AI 生成的代码严格符合项目规范。
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class FrameworkKnowledgeService {
|
||||||
|
private readonly logger = new Logger(FrameworkKnowledgeService.name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取框架技术栈信息
|
||||||
|
*/
|
||||||
|
getTechStack(): typeof FRAMEWORK_TECH_STACK {
|
||||||
|
return FRAMEWORK_TECH_STACK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分层架构信息
|
||||||
|
*/
|
||||||
|
getLayerArchitecture(): typeof LAYER_ARCHITECTURE {
|
||||||
|
return LAYER_ARCHITECTURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取实际目录结构
|
||||||
|
*/
|
||||||
|
getDirectoryStructure(): typeof ACTUAL_DIRECTORY_STRUCTURE {
|
||||||
|
return ACTUAL_DIRECTORY_STRUCTURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取命名规范
|
||||||
|
*/
|
||||||
|
getNamingConventions(): typeof NAMING_CONVENTIONS {
|
||||||
|
return NAMING_CONVENTIONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 PHP → NestJS 映射规则
|
||||||
|
*/
|
||||||
|
getPhpNestjsMapping(): typeof PHP_NESTJS_MAPPING {
|
||||||
|
return PHP_NESTJS_MAPPING;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取绝对禁止规则
|
||||||
|
*/
|
||||||
|
getProhibitions(): readonly string[] {
|
||||||
|
return STRICT_PROHIBITIONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据模块名生成文件路径
|
||||||
|
* @param module 模块名(如 'member')
|
||||||
|
* @param type 文件类型
|
||||||
|
* @param layer 层级(adminapi/api/admin/api/core)
|
||||||
|
* @param fileName 文件名
|
||||||
|
*/
|
||||||
|
resolveFilePath(module: string, type: 'controller' | 'service' | 'entity' | 'dto', layer?: string, fileName?: string): string {
|
||||||
|
const structure = ACTUAL_DIRECTORY_STRUCTURE;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'controller': {
|
||||||
|
const ctrlLayer = layer === 'api' ? 'api' : 'adminapi';
|
||||||
|
const name = fileName ?? `${module}.controller.ts`;
|
||||||
|
return `${structure.controllers[ctrlLayer]}${module}/${name}`;
|
||||||
|
}
|
||||||
|
case 'service': {
|
||||||
|
const svcLayer = layer === 'api' ? 'api' : layer === 'core' ? 'core' : 'admin';
|
||||||
|
const name = fileName ?? `${module}-service-impl.service.ts`;
|
||||||
|
return `${structure.services[svcLayer]}${module}/${name}`;
|
||||||
|
}
|
||||||
|
case 'entity': {
|
||||||
|
const name = fileName ?? `${module}.entity.ts`;
|
||||||
|
return `${structure.entities}${name}`;
|
||||||
|
}
|
||||||
|
case 'dto': {
|
||||||
|
const dtoLayer = layer === 'api' ? 'api' : layer === 'core' ? 'core' : 'admin';
|
||||||
|
const name = fileName ?? `${module}.param.ts`;
|
||||||
|
return `${structure.dtos[dtoLayer]}${module}/${name}`;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 MySQL → TypeORM 类型映射
|
||||||
|
* @param mysqlType MySQL 列类型
|
||||||
|
*/
|
||||||
|
mapDbType(mysqlType: string): { typeormType: string; tsType: string } | undefined {
|
||||||
|
// 提取基础类型(去掉长度修饰符,如 varchar(255) → varchar)
|
||||||
|
const baseType = mysqlType.replace(/\(.*\)/, '').trim().toLowerCase();
|
||||||
|
return DB_TYPE_MAPPING[baseType];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查模块是否已存在
|
||||||
|
* @param moduleName 模块名
|
||||||
|
* @param layer 层级
|
||||||
|
*/
|
||||||
|
isModuleExists(moduleName: string, layer: 'adminapi' | 'api' = 'adminapi'): boolean {
|
||||||
|
return (EXISTING_MODULES[layer] as readonly string[]).includes(moduleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已有模块列表
|
||||||
|
*/
|
||||||
|
getExistingModules(layer?: 'adminapi' | 'api'): string[] {
|
||||||
|
if (layer) return [...EXISTING_MODULES[layer]];
|
||||||
|
return [...new Set([...EXISTING_MODULES.adminapi, ...EXISTING_MODULES.api])];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取代码模板片段
|
||||||
|
*/
|
||||||
|
getCodeTemplates(): typeof CODE_TEMPLATES {
|
||||||
|
return CODE_TEMPLATES;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取质量标准
|
||||||
|
*/
|
||||||
|
getQualityStandards(): typeof QUALITY_STANDARDS {
|
||||||
|
return QUALITY_STANDARDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成规范合规性提示(嵌入到 LLM System Prompt 中)
|
||||||
|
*/
|
||||||
|
getSystemPromptAddendum(): string {
|
||||||
|
return `
|
||||||
|
## WWJCloud v1 框架规范(必须严格遵守)
|
||||||
|
|
||||||
|
### 技术栈
|
||||||
|
- 后端: NestJS 11 + TypeScript 5.5+ + TypeORM 0.3
|
||||||
|
- 数据库: MySQL 8.0,字段名与 PHP 项目 100% 一致
|
||||||
|
|
||||||
|
### 绝对禁止
|
||||||
|
${STRICT_PROHIBITIONS.map((p, i) => `${i + 1}. ${p}`).join('\n')}
|
||||||
|
|
||||||
|
### 目录结构(实际采用)
|
||||||
|
- 控制器: controllers/adminapi/{module}/ 或 controllers/api/{module}/
|
||||||
|
- 服务: services/admin/{module}/impl/ 或 services/api/{module}/impl/
|
||||||
|
- 实体: entities/(扁平目录)
|
||||||
|
- DTO: dtos/admin/{module}/param/ 和 dtos/admin/{module}/vo/
|
||||||
|
|
||||||
|
### 命名规范
|
||||||
|
- 实体文件: {name}.entity.ts,类名 PascalCase(与 PHP 模型一致)
|
||||||
|
- 控制器: {name}.controller.ts
|
||||||
|
- 服务: {name}-service-impl.service.ts
|
||||||
|
- DTO 参数: {name}.param.ts
|
||||||
|
- DTO 视图: {name}.vo.ts
|
||||||
|
|
||||||
|
### 质量标准
|
||||||
|
- 数据库字段映射准确率: 100%
|
||||||
|
- PHP 方法对应准确率: 100%
|
||||||
|
- 代码必须可直接运行(npm run build 零错误)
|
||||||
|
`.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
/**
|
||||||
|
* WWJCloud v1 框架规范知识库
|
||||||
|
*
|
||||||
|
* 将 4 份规范文件结构化为 AI 可消费的知识格式,
|
||||||
|
* 供代码生成 Skills 查询和使用。
|
||||||
|
*
|
||||||
|
* 知识来源:
|
||||||
|
* - common-layer-standards.md(Common 层模块化设计标准)
|
||||||
|
* - development_constraints.md(开发约束规范)
|
||||||
|
* - nestjs_file_generation_standards.md(NestJS 文件生成标准)
|
||||||
|
* - project_rules.md(项目规则)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 框架技术栈 */
|
||||||
|
export const FRAMEWORK_TECH_STACK = {
|
||||||
|
backend: 'NestJS 11',
|
||||||
|
language: 'TypeScript 5.5+',
|
||||||
|
orm: 'TypeORM 0.3',
|
||||||
|
database: 'MySQL 8.0',
|
||||||
|
cache: 'Redis (ioredis)',
|
||||||
|
queue: 'BullMQ',
|
||||||
|
validation: 'class-validator + class-transformer',
|
||||||
|
apiDoc: 'Swagger (@nestjs/swagger)',
|
||||||
|
auth: 'JWT (passport-jwt)',
|
||||||
|
config: '@nestjs/config + joi',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** 项目分层架构 */
|
||||||
|
export const LAYER_ARCHITECTURE = {
|
||||||
|
layers: [
|
||||||
|
{ name: 'boot', alias: '@wwjBoot', path: 'libs/wwjcloud-boot/src', description: '基础设施层(认证/缓存/队列/Vendor)' },
|
||||||
|
{ name: 'core', alias: '@wwjCore', path: 'libs/wwjcloud-core/src', description: '核心业务层(控制器/服务/实体/DTO)' },
|
||||||
|
{ name: 'ai', alias: '@wwjAi', path: 'libs/wwjcloud-ai/src', description: 'AI 智能层(Agent/Skills/Memory)' },
|
||||||
|
{ name: 'addon', alias: '@wwjAddon', path: 'libs/wwjcloud-addon/src', description: '插件扩展层' },
|
||||||
|
],
|
||||||
|
dependencyRule: 'boot → core (单向),ai 独立,addon 独立',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** 实际目录结构(当前 v1 采用的方式) */
|
||||||
|
export const ACTUAL_DIRECTORY_STRUCTURE = {
|
||||||
|
description: '按技术层级分目录(非按业务域分目录)',
|
||||||
|
controllers: {
|
||||||
|
adminapi: 'libs/wwjcloud-core/src/controllers/adminapi/{module}/',
|
||||||
|
api: 'libs/wwjcloud-core/src/controllers/api/{module}/',
|
||||||
|
core: 'libs/wwjcloud-core/src/controllers/core/',
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
admin: 'libs/wwjcloud-core/src/services/admin/{module}/impl/',
|
||||||
|
api: 'libs/wwjcloud-core/src/services/api/{module}/impl/',
|
||||||
|
core: 'libs/wwjcloud-core/src/services/core/{module}/',
|
||||||
|
},
|
||||||
|
entities: 'libs/wwjcloud-core/src/entities/',
|
||||||
|
dtos: {
|
||||||
|
admin: 'libs/wwjcloud-core/src/dtos/admin/{module}/',
|
||||||
|
api: 'libs/wwjcloud-core/src/dtos/api/{module}/',
|
||||||
|
core: 'libs/wwjcloud-core/src/dtos/core/{module}/',
|
||||||
|
},
|
||||||
|
modules: {
|
||||||
|
controller: 'libs/wwjcloud-core/src/controller.module.ts',
|
||||||
|
service: 'libs/wwjcloud-core/src/service.module.ts',
|
||||||
|
entity: 'libs/wwjcloud-core/src/entity.module.ts',
|
||||||
|
common: 'libs/wwjcloud-core/src/common.module.ts',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** 规范文档定义的目录结构(目标结构,当前未完全采用) */
|
||||||
|
export const STANDARD_DIRECTORY_STRUCTURE = {
|
||||||
|
description: '按业务域模块化分目录(规范文档定义的目标结构)',
|
||||||
|
pattern: 'src/common/{module-name}/',
|
||||||
|
structure: [
|
||||||
|
'{module-name}.module.ts',
|
||||||
|
'controllers/adminapi/',
|
||||||
|
'controllers/api/',
|
||||||
|
'services/admin/',
|
||||||
|
'services/api/',
|
||||||
|
'services/core/',
|
||||||
|
'entity/',
|
||||||
|
'dto/admin/',
|
||||||
|
'dto/api/',
|
||||||
|
],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** 文件命名规范 */
|
||||||
|
export const NAMING_CONVENTIONS = {
|
||||||
|
entity: {
|
||||||
|
pattern: '{name}.entity.ts',
|
||||||
|
example: 'sys-user.entity.ts',
|
||||||
|
classPattern: 'PascalCase',
|
||||||
|
classExample: 'SysUser',
|
||||||
|
note: '与 PHP 模型类名保持一致',
|
||||||
|
},
|
||||||
|
controller: {
|
||||||
|
pattern: '{name}.controller.ts',
|
||||||
|
example: 'user.controller.ts',
|
||||||
|
classPattern: 'PascalCase + Controller',
|
||||||
|
classExample: 'UserController',
|
||||||
|
},
|
||||||
|
service: {
|
||||||
|
pattern: '{name}-service-impl.service.ts',
|
||||||
|
example: 'user-service-impl.service.ts',
|
||||||
|
classPattern: 'PascalCase + ServiceImpl',
|
||||||
|
classExample: 'UserServiceImpl',
|
||||||
|
note: '实际项目中使用 -service-impl 后缀',
|
||||||
|
},
|
||||||
|
dto: {
|
||||||
|
param: 'dtos/{layer}/{module}/param/{name}.param.ts',
|
||||||
|
vo: 'dtos/{layer}/{module}/vo/{name}.vo.ts',
|
||||||
|
example: 'dtos/admin/member/param/create-member.param.ts',
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
pattern: '{name}.module.ts',
|
||||||
|
example: 'user.module.ts',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** PHP → NestJS 映射规则 */
|
||||||
|
export const PHP_NESTJS_MAPPING = {
|
||||||
|
controller: {
|
||||||
|
php: 'app/adminapi/controller/{module}/{Name}.php',
|
||||||
|
nestjs: 'controllers/adminapi/{module}/{name}.controller.ts',
|
||||||
|
methodMapping: 'public 方法直接对应 @Get/@Post/@Put/@Delete 装饰器方法',
|
||||||
|
},
|
||||||
|
service: {
|
||||||
|
php: 'app/service/{layer}/{module}/{Name}Service.php',
|
||||||
|
nestjs: 'services/{layer}/{module}/impl/{name}-service-impl.service.ts',
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
php: 'app/model/{module}/{Name}.php',
|
||||||
|
nestjs: 'entities/{name}.entity.ts',
|
||||||
|
fieldMapping: '字段名 100% 保持一致,类型对应 MySQL 列类型',
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
php: 'app/validate/{module}/{Name}.php',
|
||||||
|
nestjs: 'dtos/{layer}/{module}/param/{name}.param.ts (class-validator)',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** 六条绝对禁止规则 */
|
||||||
|
export const STRICT_PROHIBITIONS = [
|
||||||
|
'禁止自创业务逻辑 — 所有业务逻辑必须严格按照 PHP 项目实现',
|
||||||
|
'禁止假设数据结构 — 所有数据结构必须基于真实数据库表结构',
|
||||||
|
'禁止使用默认值 — 所有字段、方法、配置必须基于真实 PHP 代码',
|
||||||
|
'禁止编写骨架代码 — 不允许生成空方法或 TODO 注释',
|
||||||
|
'禁止写死数据 — 不允许硬编码任何业务数据',
|
||||||
|
'禁止猜测 API 接口 — 所有接口必须基于 PHP 控制器真实方法',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/** 数据库字段类型映射(MySQL → TypeORM) */
|
||||||
|
export const DB_TYPE_MAPPING: Record<string, { typeormType: string; tsType: string }> = {
|
||||||
|
'int': { typeormType: 'int', tsType: 'number' },
|
||||||
|
'tinyint': { typeormType: 'tinyint', tsType: 'number' },
|
||||||
|
'bigint': { typeormType: 'bigint', tsType: 'string' },
|
||||||
|
'varchar': { typeormType: 'varchar', tsType: 'string' },
|
||||||
|
'text': { typeormType: 'text', tsType: 'string' },
|
||||||
|
'longtext': { typeormType: 'longtext', tsType: 'string' },
|
||||||
|
'decimal': { typeormType: 'decimal', tsType: 'number' },
|
||||||
|
'float': { typeormType: 'float', tsType: 'number' },
|
||||||
|
'double': { typeormType: 'double', tsType: 'number' },
|
||||||
|
'datetime': { typeormType: 'datetime', tsType: 'Date' },
|
||||||
|
'timestamp': { typeormType: 'timestamp', tsType: 'number' },
|
||||||
|
'json': { typeormType: 'json', tsType: 'Record<string, unknown>' },
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 已有业务模块清单(从 PHP 项目提取) */
|
||||||
|
export const EXISTING_MODULES = {
|
||||||
|
adminapi: [
|
||||||
|
'addon', 'aliapp', 'applet', 'auth', 'channel', 'dict', 'diy',
|
||||||
|
'generator', 'home', 'index', 'login', 'member', 'niucloud',
|
||||||
|
'notice', 'pay', 'poster', 'site', 'stat', 'sys', 'upload',
|
||||||
|
'user', 'verify', 'weapp', 'wechat', 'wxoplatform',
|
||||||
|
],
|
||||||
|
api: [
|
||||||
|
'addon', 'agreement', 'channel', 'diy', 'login', 'member',
|
||||||
|
'pay', 'poster', 'sys', 'upload', 'weapp', 'wechat',
|
||||||
|
],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** 代码模板片段 */
|
||||||
|
export const CODE_TEMPLATES = {
|
||||||
|
entityHeader: `import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';`,
|
||||||
|
controllerHeader: `import { Controller, Get, Post, Put, Delete, Body, Param, Query } from '@nestjs/common';`,
|
||||||
|
serviceHeader: `import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';`,
|
||||||
|
dtoParamHeader: `import { IsString, IsNumber, IsOptional, IsArray } from 'class-validator';`,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** 质量标准 */
|
||||||
|
export const QUALITY_STANDARDS = {
|
||||||
|
dbFieldMappingAccuracy: '100%',
|
||||||
|
phpMethodCorrespondence: '100%',
|
||||||
|
businessLogicConsistency: '100%',
|
||||||
|
codeRunnable: '100%',
|
||||||
|
namingConventionCompliance: '100%',
|
||||||
|
buildMustPass: true,
|
||||||
|
swaggerAnnotationsRequired: true,
|
||||||
|
} as const;
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
import { LlmToolDefinition } from '../providers/llm-provider.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代码生成结果
|
||||||
|
*/
|
||||||
|
export interface GeneratedFile {
|
||||||
|
/** 文件路径(相对于项目根目录) */
|
||||||
|
path: string;
|
||||||
|
/** 文件内容 */
|
||||||
|
content: string;
|
||||||
|
/** 文件类型 */
|
||||||
|
type: 'entity' | 'controller' | 'service' | 'dto' | 'sql' | 'module' | 'other';
|
||||||
|
/** 描述 */
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模块生成请求
|
||||||
|
*/
|
||||||
|
export interface ModuleGenerateRequest {
|
||||||
|
/** 模块名(如 'member', 'order') */
|
||||||
|
moduleName: string;
|
||||||
|
/** 模块描述(中文) */
|
||||||
|
description: string;
|
||||||
|
/** 数据库表名 */
|
||||||
|
tableName: string;
|
||||||
|
/** 表字段定义 */
|
||||||
|
fields: TableField[];
|
||||||
|
/** 需要生成的端(adminapi / api / both) */
|
||||||
|
endpoints: 'adminapi' | 'api' | 'both';
|
||||||
|
/** PHP 控制器方法列表(可选,用于对齐) */
|
||||||
|
phpMethods?: PhpMethod[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据库表字段定义
|
||||||
|
*/
|
||||||
|
export interface TableField {
|
||||||
|
/** 字段名 */
|
||||||
|
name: string;
|
||||||
|
/** MySQL 类型(如 varchar(255), int, text) */
|
||||||
|
mysqlType: string;
|
||||||
|
/** 是否主键 */
|
||||||
|
isPrimary?: boolean;
|
||||||
|
/** 是否自增 */
|
||||||
|
isAutoIncrement?: boolean;
|
||||||
|
/** 是否允许 NULL */
|
||||||
|
nullable?: boolean;
|
||||||
|
/** 默认值 */
|
||||||
|
defaultValue?: string;
|
||||||
|
/** 字段注释 */
|
||||||
|
comment?: string;
|
||||||
|
/** 是否无符号 */
|
||||||
|
unsigned?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PHP 控制器方法定义
|
||||||
|
*/
|
||||||
|
export interface PhpMethod {
|
||||||
|
/** 方法名 */
|
||||||
|
name: string;
|
||||||
|
/** HTTP 方法 */
|
||||||
|
httpMethod: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||||
|
/** 路由路径 */
|
||||||
|
route: string;
|
||||||
|
/** 方法描述 */
|
||||||
|
description: string;
|
||||||
|
/** 参数列表 */
|
||||||
|
params?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代码生成器接口
|
||||||
|
* 所有具体的生成 Skill 实现此接口
|
||||||
|
*/
|
||||||
|
export interface ICodeGenerator {
|
||||||
|
/**
|
||||||
|
* 生成代码
|
||||||
|
* @param request 生成请求
|
||||||
|
* @returns 生成的文件列表
|
||||||
|
*/
|
||||||
|
generate(request: ModuleGenerateRequest): GeneratedFile[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代码生成 Skill 的工具定义(供 LLM Function Calling 使用)
|
||||||
|
*/
|
||||||
|
export const CODEGEN_TOOL_DEFINITIONS: LlmToolDefinition[] = [
|
||||||
|
{
|
||||||
|
name: 'generate_entity',
|
||||||
|
description: '根据数据库表结构生成 TypeORM 实体文件',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
moduleName: { type: 'string', description: '模块名' },
|
||||||
|
tableName: { type: 'string', description: '数据库表名' },
|
||||||
|
fields: { type: 'array', description: '字段定义数组', items: { type: 'object' } },
|
||||||
|
},
|
||||||
|
required: ['moduleName', 'tableName', 'fields'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'generate_controller',
|
||||||
|
description: '根据 PHP 控制器方法生成 NestJS 控制器文件',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
moduleName: { type: 'string', description: '模块名' },
|
||||||
|
endpoint: { type: 'string', description: '端类型: adminapi 或 api' },
|
||||||
|
methods: { type: 'array', description: '方法列表', items: { type: 'object' } },
|
||||||
|
},
|
||||||
|
required: ['moduleName', 'endpoint', 'methods'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'generate_service',
|
||||||
|
description: '根据模块需求生成 NestJS 服务文件',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
moduleName: { type: 'string', description: '模块名' },
|
||||||
|
endpoint: { type: 'string', description: '端类型: admin 或 api 或 core' },
|
||||||
|
methods: { type: 'array', description: '方法列表', items: { type: 'object' } },
|
||||||
|
},
|
||||||
|
required: ['moduleName', 'endpoint'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'generate_dto',
|
||||||
|
description: '生成 DTO 参数和视图对象',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
moduleName: { type: 'string', description: '模块名' },
|
||||||
|
endpoint: { type: 'string', description: '端类型: admin 或 api' },
|
||||||
|
fields: { type: 'array', description: '字段定义', items: { type: 'object' } },
|
||||||
|
},
|
||||||
|
required: ['moduleName', 'endpoint'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'generate_sql',
|
||||||
|
description: '生成数据库建表 SQL 脚本',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
tableName: { type: 'string', description: '表名' },
|
||||||
|
fields: { type: 'array', description: '字段定义', items: { type: 'object' } },
|
||||||
|
comment: { type: 'string', description: '表注释' },
|
||||||
|
},
|
||||||
|
required: ['tableName', 'fields'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'generate_module',
|
||||||
|
description: '生成完整的业务模块(Entity + Controller + Service + DTO + SQL)',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
moduleName: { type: 'string', description: '模块名' },
|
||||||
|
description: { type: 'string', description: '模块描述' },
|
||||||
|
tableName: { type: 'string', description: '数据库表名' },
|
||||||
|
fields: { type: 'array', description: '字段定义', items: { type: 'object' } },
|
||||||
|
endpoints: { type: 'string', description: '端类型: adminapi/api/both' },
|
||||||
|
methods: { type: 'array', description: 'PHP 方法列表', items: { type: 'object' } },
|
||||||
|
},
|
||||||
|
required: ['moduleName', 'tableName', 'fields'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export * from './generator.interface';
|
||||||
|
export * from './framework-knowledge';
|
||||||
|
export * from './framework-knowledge.service';
|
||||||
|
export * from './entity.generator';
|
||||||
|
export * from './controller.generator';
|
||||||
|
export * from './service.generator';
|
||||||
|
export * from './dto.generator';
|
||||||
|
export * from './sql.generator';
|
||||||
|
export * from './module.generator';
|
||||||
|
export * from './codegen.skill';
|
||||||
|
export * from './ai-generate.controller';
|
||||||
|
export * from './ai-generator.module';
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ICodeGenerator, GeneratedFile, ModuleGenerateRequest } from './generator.interface';
|
||||||
|
import { EntityGenerator } from './entity.generator';
|
||||||
|
import { ControllerGenerator } from './controller.generator';
|
||||||
|
import { ServiceGenerator } from './service.generator';
|
||||||
|
import { DtoGenerator } from './dto.generator';
|
||||||
|
import { SqlGenerator } from './sql.generator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完整模块生成器(脚手架)
|
||||||
|
*
|
||||||
|
* 组合所有子生成器,一键生成完整的业务模块:
|
||||||
|
* Entity + Controller + Service (admin/api/core) + DTO (param/vo) + SQL
|
||||||
|
*
|
||||||
|
* 借鉴 NiuCloud Lite AI 的 Skills 模块化开发规范,
|
||||||
|
* 适配 NestJS 技术栈。
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ModuleGenerator implements ICodeGenerator {
|
||||||
|
private readonly logger = new Logger(ModuleGenerator.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly entityGenerator: EntityGenerator,
|
||||||
|
private readonly controllerGenerator: ControllerGenerator,
|
||||||
|
private readonly serviceGenerator: ServiceGenerator,
|
||||||
|
private readonly dtoGenerator: DtoGenerator,
|
||||||
|
private readonly sqlGenerator: SqlGenerator,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成完整业务模块
|
||||||
|
* @param request 模块生成请求
|
||||||
|
* @returns 所有生成的文件列表
|
||||||
|
*/
|
||||||
|
generate(request: ModuleGenerateRequest): GeneratedFile[] {
|
||||||
|
this.logger.log(`[ModuleGenerator] 开始生成模块: ${request.moduleName}`);
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
const files: GeneratedFile[] = [];
|
||||||
|
|
||||||
|
// 1. 生成实体
|
||||||
|
files.push(...this.entityGenerator.generate(request));
|
||||||
|
|
||||||
|
// 2. 生成控制器
|
||||||
|
files.push(...this.controllerGenerator.generate(request));
|
||||||
|
|
||||||
|
// 3. 生成服务
|
||||||
|
files.push(...this.serviceGenerator.generate(request));
|
||||||
|
|
||||||
|
// 4. 生成 DTO
|
||||||
|
files.push(...this.dtoGenerator.generate(request));
|
||||||
|
|
||||||
|
// 5. 生成 SQL
|
||||||
|
files.push(...this.sqlGenerator.generate(request));
|
||||||
|
|
||||||
|
// 6. 生成模块注册代码提示
|
||||||
|
files.push(this.generateModuleRegistrationHint(request));
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
this.logger.log(
|
||||||
|
`[ModuleGenerator] 模块 ${request.moduleName} 生成完成: ${files.length} 个文件 (${duration}ms)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成模块注册提示文件
|
||||||
|
*/
|
||||||
|
private generateModuleRegistrationHint(request: ModuleGenerateRequest): GeneratedFile {
|
||||||
|
const { moduleName } = request;
|
||||||
|
const entityName = this.toPascalCase(request.tableName.replace(/^nc_/, '').replace(/_/g, ' '));
|
||||||
|
|
||||||
|
const content = `/**
|
||||||
|
* ${moduleName} 模块注册指南
|
||||||
|
*
|
||||||
|
* 生成完成后,需要手动完成以下注册步骤:
|
||||||
|
*
|
||||||
|
* 1. 在 libs/wwjcloud-core/src/entity.module.ts 中注册实体:
|
||||||
|
* TypeOrmModule.forFeature([${entityName}])
|
||||||
|
*
|
||||||
|
* 2. 在 libs/wwjcloud-core/src/service.module.ts 中注册服务:
|
||||||
|
* providers: [Core${this.toPascalCase(moduleName)}Service, ${this.toPascalCase(moduleName)}ServiceImpl]
|
||||||
|
*
|
||||||
|
* 3. 在 libs/wwjcloud-core/src/controller.module.ts 中注册控制器:
|
||||||
|
* controllers: [${this.toPascalCase(moduleName)}Controller]
|
||||||
|
*
|
||||||
|
* 4. 执行 SQL 脚本创建数据库表
|
||||||
|
*
|
||||||
|
* 5. 运行 npm run build 验证编译通过
|
||||||
|
*/
|
||||||
|
`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: `libs/wwjcloud-core/src/${moduleName}.REGISTRATION.md`,
|
||||||
|
content,
|
||||||
|
type: 'other',
|
||||||
|
description: `${moduleName} 模块注册指南`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下划线转驼峰
|
||||||
|
*/
|
||||||
|
private toCamelCase(str: string): string {
|
||||||
|
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转 PascalCase
|
||||||
|
*/
|
||||||
|
private toPascalCase(str: string): string {
|
||||||
|
const camel = this.toCamelCase(str);
|
||||||
|
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ICodeGenerator, GeneratedFile, ModuleGenerateRequest } from './generator.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务文件生成器
|
||||||
|
*
|
||||||
|
* 根据 PHP 服务层生成 NestJS 服务文件,
|
||||||
|
* 包含 admin/api/core 三层服务。
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ServiceGenerator implements ICodeGenerator {
|
||||||
|
/**
|
||||||
|
* 生成服务文件
|
||||||
|
*/
|
||||||
|
generate(request: ModuleGenerateRequest): GeneratedFile[] {
|
||||||
|
const files: GeneratedFile[] = [];
|
||||||
|
|
||||||
|
// 生成 core 服务(核心业务逻辑)
|
||||||
|
files.push(this.generateCoreService(request));
|
||||||
|
|
||||||
|
// 根据端类型生成 admin/api 服务
|
||||||
|
if (request.endpoints === 'adminapi' || request.endpoints === 'both') {
|
||||||
|
files.push(this.generateLayerService(request, 'admin'));
|
||||||
|
}
|
||||||
|
if (request.endpoints === 'api' || request.endpoints === 'both') {
|
||||||
|
files.push(this.generateLayerService(request, 'api'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成核心服务文件
|
||||||
|
*/
|
||||||
|
private generateCoreService(request: ModuleGenerateRequest): GeneratedFile {
|
||||||
|
const { moduleName, tableName, fields } = request;
|
||||||
|
const className = `Core${this.toPascalCase(moduleName)}Service`;
|
||||||
|
const entityName = this.toPascalCase(tableName.replace(/^nc_/, '').replace(/_/g, ' '));
|
||||||
|
|
||||||
|
const content = `import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ${request.description || moduleName} 核心服务
|
||||||
|
* 对应 PHP: app/service/core/${moduleName}/Core${this.toPascalCase(moduleName)}Service.php
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ${className} {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(${entityName})
|
||||||
|
private readonly repository: Repository<${entityName}>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取列表(分页)
|
||||||
|
* 对应 PHP 核心服务方法
|
||||||
|
*/
|
||||||
|
async getPage(where: Record<string, unknown>, page: number = 1, limit: number = 20) {
|
||||||
|
const queryBuilder = this.repository.createQueryBuilder('${tableName}');
|
||||||
|
${this.generateWhereConditions(fields)}
|
||||||
|
queryBuilder.orderBy('id', 'DESC');
|
||||||
|
queryBuilder.skip((page - 1) * limit).take(limit);
|
||||||
|
|
||||||
|
const [list, count] = await queryBuilder.getManyAndCount();
|
||||||
|
return { list, count };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取详情
|
||||||
|
*/
|
||||||
|
async getInfo(id: number) {
|
||||||
|
return await this.repository.findOne({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增
|
||||||
|
*/
|
||||||
|
async add(data: Record<string, unknown>) {
|
||||||
|
const entity = this.repository.create(data);
|
||||||
|
return await this.repository.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑
|
||||||
|
*/
|
||||||
|
async edit(id: number, data: Record<string, unknown>) {
|
||||||
|
await this.repository.update(id, data);
|
||||||
|
return await this.getInfo(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除
|
||||||
|
*/
|
||||||
|
async delete(id: number) {
|
||||||
|
return await this.repository.softDelete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: `libs/wwjcloud-core/src/services/core/${moduleName}/core-${moduleName}.service.ts`,
|
||||||
|
content,
|
||||||
|
type: 'service',
|
||||||
|
description: `${moduleName} 核心服务`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 admin/api 层服务文件
|
||||||
|
*/
|
||||||
|
private generateLayerService(request: ModuleGenerateRequest, layer: 'admin' | 'api'): GeneratedFile {
|
||||||
|
const { moduleName } = request;
|
||||||
|
const className = `${this.toPascalCase(moduleName)}ServiceImpl`;
|
||||||
|
const coreClassName = `Core${this.toPascalCase(moduleName)}Service`;
|
||||||
|
|
||||||
|
const content = `import { Injectable } from '@nestjs/common';
|
||||||
|
import { ${coreClassName} } from '../../core/${moduleName}/core-${moduleName}.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ${request.description || moduleName} ${layer === 'admin' ? '管理端' : '前台'}服务
|
||||||
|
* 对应 PHP: app/service/${layer}/${moduleName}/${this.toPascalCase(moduleName)}Service.php
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ${className} {
|
||||||
|
constructor(
|
||||||
|
private readonly coreService: ${coreClassName},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分页列表
|
||||||
|
*/
|
||||||
|
async getPage(data: Record<string, unknown>) {
|
||||||
|
const page = (data.page as number) || 1;
|
||||||
|
const limit = (data.limit as number) || 20;
|
||||||
|
return await this.coreService.getPage(data, page, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取详情
|
||||||
|
*/
|
||||||
|
async getInfo(id: number) {
|
||||||
|
return await this.coreService.getInfo(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增
|
||||||
|
*/
|
||||||
|
async add(data: Record<string, unknown>) {
|
||||||
|
return await this.coreService.add(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑
|
||||||
|
*/
|
||||||
|
async edit(id: number, data: Record<string, unknown>) {
|
||||||
|
return await this.coreService.edit(id, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除
|
||||||
|
*/
|
||||||
|
async delete(id: number) {
|
||||||
|
return await this.coreService.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: `libs/wwjcloud-core/src/services/${layer}/${moduleName}/impl/${moduleName}-service-impl.service.ts`,
|
||||||
|
content,
|
||||||
|
type: 'service',
|
||||||
|
description: `${moduleName} ${layer} 服务`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 where 条件代码
|
||||||
|
*/
|
||||||
|
private generateWhereConditions(fields: import('./generator.interface').TableField[]): string {
|
||||||
|
const searchableFields = fields.filter((f) =>
|
||||||
|
!f.isPrimary && !f.isAutoIncrement &&
|
||||||
|
(f.mysqlType.startsWith('varchar') || f.mysqlType.startsWith('text')),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (searchableFields.length === 0) return '';
|
||||||
|
|
||||||
|
const lines = searchableFields.slice(0, 3).map((f) => {
|
||||||
|
const camelName = this.toCamelCase(f.name);
|
||||||
|
return ` if (where.${camelName}) {
|
||||||
|
queryBuilder.andWhere('${f.name} LIKE :${camelName}', { ${camelName}: '%' + where.${camelName} + '%' });
|
||||||
|
}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下划线转驼峰
|
||||||
|
*/
|
||||||
|
private toCamelCase(str: string): string {
|
||||||
|
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转 PascalCase
|
||||||
|
*/
|
||||||
|
private toPascalCase(str: string): string {
|
||||||
|
const camel = this.toCamelCase(str);
|
||||||
|
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ICodeGenerator, GeneratedFile, ModuleGenerateRequest, TableField } from './generator.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQL 文件生成器
|
||||||
|
*
|
||||||
|
* 生成数据库建表 SQL 脚本,
|
||||||
|
* 表名和字段与 PHP 项目 100% 保持一致。
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class SqlGenerator implements ICodeGenerator {
|
||||||
|
/**
|
||||||
|
* 生成 SQL 文件
|
||||||
|
*/
|
||||||
|
generate(request: ModuleGenerateRequest): GeneratedFile[] {
|
||||||
|
const sql = this.generateCreateTableSql(request);
|
||||||
|
return [sql];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成建表 SQL
|
||||||
|
*/
|
||||||
|
private generateCreateTableSql(request: ModuleGenerateRequest): GeneratedFile {
|
||||||
|
const { tableName, fields, description } = request;
|
||||||
|
const columns = fields.map((f) => this.generateColumn(f));
|
||||||
|
const tableComment = description || tableName;
|
||||||
|
|
||||||
|
const content = `-- ----------------------------
|
||||||
|
-- ${tableComment}
|
||||||
|
-- 对应实体: ${tableName.replace(/^nc_/, '')}.entity.ts
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS \`${tableName}\`;
|
||||||
|
CREATE TABLE \`${tableName}\` (
|
||||||
|
${columns.join(',\n')},
|
||||||
|
PRIMARY KEY (\`${this.getPrimaryField(fields)?.name || 'id'}\`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='${tableComment}';
|
||||||
|
`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: `sql/${tableName}.sql`,
|
||||||
|
content,
|
||||||
|
type: 'sql',
|
||||||
|
description: `${tableName} 建表 SQL`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成列定义
|
||||||
|
*/
|
||||||
|
private generateColumn(field: TableField): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
// 字段名
|
||||||
|
parts.push(` \`${field.name}\``);
|
||||||
|
|
||||||
|
// 类型
|
||||||
|
parts.push(field.mysqlType);
|
||||||
|
|
||||||
|
// 无符号
|
||||||
|
if (field.unsigned) parts.push('UNSIGNED');
|
||||||
|
|
||||||
|
// 是否允许 NULL
|
||||||
|
if (field.isPrimary) {
|
||||||
|
parts.push('NOT NULL');
|
||||||
|
if (field.isAutoIncrement) parts.push('AUTO_INCREMENT');
|
||||||
|
} else if (field.nullable) {
|
||||||
|
parts.push('DEFAULT NULL');
|
||||||
|
} else {
|
||||||
|
parts.push('NOT NULL');
|
||||||
|
if (field.defaultValue !== undefined) {
|
||||||
|
parts.push(`DEFAULT '${field.defaultValue}'`);
|
||||||
|
} else if (field.mysqlType.startsWith('varchar') || field.mysqlType.startsWith('text')) {
|
||||||
|
parts.push("DEFAULT ''");
|
||||||
|
} else if (field.mysqlType.startsWith('int') || field.mysqlType.startsWith('tinyint')) {
|
||||||
|
parts.push('DEFAULT 0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注释
|
||||||
|
if (field.comment) {
|
||||||
|
parts.push(`COMMENT '${field.comment}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取主键字段
|
||||||
|
*/
|
||||||
|
private getPrimaryField(fields: TableField[]): TableField | undefined {
|
||||||
|
return fields.find((f) => f.isPrimary);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,45 @@
|
|||||||
|
// 模块导出
|
||||||
export * from "./wwjcloud-ai.module";
|
export * from "./wwjcloud-ai.module";
|
||||||
export * from "./events";
|
export * from "./events";
|
||||||
export * from "./types";
|
export * from "./types";
|
||||||
|
|
||||||
|
// Manager 层服务
|
||||||
export * from "./healing/services/ai-strategy.service";
|
export * from "./healing/services/ai-strategy.service";
|
||||||
export * from "./manager/services/ai-registry.service";
|
export * from "./manager/services/ai-registry.service";
|
||||||
export * from "./manager/services/ai-orchestrator.service";
|
export * from "./manager/services/ai-orchestrator.service";
|
||||||
export * from "./manager/services/ai-coordinator.service";
|
export * from "./manager/services/ai-coordinator.service";
|
||||||
export * from "./manager/services/framework-equivalence.service";
|
export * from "./manager/services/framework-equivalence.service";
|
||||||
|
|
||||||
// 导出AI层集成的Boot层组件
|
// Runtime 层(借鉴 OpenClaw Agentic Loop)
|
||||||
|
export * from "./runtime/agentic-loop.service";
|
||||||
|
export * from "./runtime/agentic-loop.interface";
|
||||||
|
export * from "./runtime/loop-detector.service";
|
||||||
|
export * from "./runtime/loop-detector.interface";
|
||||||
|
|
||||||
|
// LLM Provider 层(借鉴 OpenClaw 多模型驱动)
|
||||||
|
export * from "./providers/llm-provider.interface";
|
||||||
|
export * from "./providers/llm-provider.factory";
|
||||||
|
export * from "./providers/impls/openai.provider";
|
||||||
|
export * from "./providers/impls/ollama.provider";
|
||||||
|
|
||||||
|
// Skills 层(借鉴 OpenClaw Skills 系统)
|
||||||
|
export * from "./skills/skill.interface";
|
||||||
|
export * from "./skills/skill-registry.service";
|
||||||
|
export * from "./skills/skill-executor.service";
|
||||||
|
|
||||||
|
// Memory 层(借鉴 OpenClaw 双模记忆)
|
||||||
|
export * from "./memory/short-term-memory.service";
|
||||||
|
export * from "./memory/long-term-memory.service";
|
||||||
|
|
||||||
|
// 🆕 Generator 层(框架级代码生成技能包,借鉴 NiuCloud Lite AI)
|
||||||
|
export * from "./generator/generator.interface";
|
||||||
|
export * from "./generator/framework-knowledge";
|
||||||
|
export * from "./generator/framework-knowledge.service";
|
||||||
|
export * from "./generator/codegen.skill";
|
||||||
|
export * from "./generator/ai-generate.controller";
|
||||||
|
export * from "./generator/ai-generator.module";
|
||||||
|
|
||||||
|
// 导出 AI 层集成的 Boot 层组件
|
||||||
export {
|
export {
|
||||||
// Provider Factories
|
// Provider Factories
|
||||||
UploadProviderFactory,
|
UploadProviderFactory,
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ShortTermMemoryService } from './short-term-memory.service';
|
||||||
|
import { LongTermMemoryService } from './long-term-memory.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI Memory 模块
|
||||||
|
* 借鉴 OpenClaw 双模记忆设计
|
||||||
|
* - 短期记忆:会话级别的消息历史(Redis/内存)
|
||||||
|
* - 长期记忆:跨会话的经验积累(向量数据库/内存)
|
||||||
|
*/
|
||||||
|
@Module({
|
||||||
|
providers: [ShortTermMemoryService, LongTermMemoryService],
|
||||||
|
exports: [ShortTermMemoryService, LongTermMemoryService],
|
||||||
|
})
|
||||||
|
export class AiMemoryModule {}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './short-term-memory.service';
|
||||||
|
export * from './long-term-memory.service';
|
||||||
|
export * from './ai-memory.module';
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记忆条目
|
||||||
|
*/
|
||||||
|
export interface MemoryEntry {
|
||||||
|
id: string;
|
||||||
|
content: string;
|
||||||
|
type: 'experience' | 'preference' | 'fact' | 'error';
|
||||||
|
sessionId: string;
|
||||||
|
timestamp: number;
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记忆搜索结果
|
||||||
|
*/
|
||||||
|
export interface MemorySearchResult {
|
||||||
|
id: string;
|
||||||
|
content: string;
|
||||||
|
relevance: number;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 长期记忆服务 — 借鉴 OpenClaw 双模记忆
|
||||||
|
* 当前使用内存存储作为降级方案
|
||||||
|
* 生产环境应替换为向量数据库(如 Pinecone/Milvus/Chroma)
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class LongTermMemoryService {
|
||||||
|
private readonly logger = new Logger(LongTermMemoryService.name);
|
||||||
|
private readonly memories = new Map<string, MemoryEntry>();
|
||||||
|
private readonly maxMemories = 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存记忆
|
||||||
|
*/
|
||||||
|
async remember(entry: Omit<MemoryEntry, 'id' | 'timestamp'>): Promise<string> {
|
||||||
|
const id = `mem_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
||||||
|
const memory: MemoryEntry = {
|
||||||
|
...entry,
|
||||||
|
id,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.memories.set(id, memory);
|
||||||
|
|
||||||
|
// 超过上限时删除最旧的
|
||||||
|
if (this.memories.size > this.maxMemories) {
|
||||||
|
const oldest = Array.from(this.memories.entries())
|
||||||
|
.sort((a, b) => a[1].timestamp - b[1].timestamp)
|
||||||
|
.slice(0, 100);
|
||||||
|
for (const [key] of oldest) {
|
||||||
|
this.memories.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug(`[LongTermMemory] 保存记忆: ${id} (type=${entry.type})`);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索相关记忆(基于关键词匹配)
|
||||||
|
* 生产环境应替换为向量相似度搜索
|
||||||
|
*/
|
||||||
|
async recall(query: string, limit = 5): Promise<MemorySearchResult[]> {
|
||||||
|
const keywords = query.toLowerCase().split(/\s+/).filter(Boolean);
|
||||||
|
const scored: MemorySearchResult[] = [];
|
||||||
|
|
||||||
|
for (const memory of this.memories.values()) {
|
||||||
|
const content = memory.content.toLowerCase();
|
||||||
|
let score = 0;
|
||||||
|
for (const keyword of keywords) {
|
||||||
|
if (content.includes(keyword)) score++;
|
||||||
|
}
|
||||||
|
// 标签匹配加分
|
||||||
|
if (memory.tags) {
|
||||||
|
for (const tag of memory.tags) {
|
||||||
|
if (keywords.some((k) => tag.toLowerCase().includes(k))) score += 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (score > 0) {
|
||||||
|
scored.push({
|
||||||
|
id: memory.id,
|
||||||
|
content: memory.content,
|
||||||
|
relevance: score,
|
||||||
|
timestamp: memory.timestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return scored.sort((a, b) => b.relevance - a.relevance).slice(0, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取记忆总数
|
||||||
|
*/
|
||||||
|
getMemoryCount(): number {
|
||||||
|
return this.memories.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按类型获取记忆
|
||||||
|
*/
|
||||||
|
getMemoriesByType(type: MemoryEntry['type'], limit = 20): MemoryEntry[] {
|
||||||
|
return Array.from(this.memories.values())
|
||||||
|
.filter((m) => m.type === type)
|
||||||
|
.sort((a, b) => b.timestamp - a.timestamp)
|
||||||
|
.slice(0, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除记忆
|
||||||
|
*/
|
||||||
|
async forget(memoryId: string): Promise<boolean> {
|
||||||
|
return this.memories.delete(memoryId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { LlmMessage } from '../providers/llm-provider.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话消息存储条目
|
||||||
|
*/
|
||||||
|
interface SessionMessages {
|
||||||
|
messages: LlmMessage[];
|
||||||
|
updatedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 短期记忆服务 — 借鉴 OpenClaw 短期记忆
|
||||||
|
* 基于 Redis 存储会话级别的消息历史
|
||||||
|
* 当前使用内存 Map 作为降级方案
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ShortTermMemoryService {
|
||||||
|
private readonly logger = new Logger(ShortTermMemoryService.name);
|
||||||
|
private readonly sessions = new Map<string, SessionMessages>();
|
||||||
|
private readonly maxMessagesPerSession = 50;
|
||||||
|
private cleanupInterval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// 每 10 分钟清理超过 30 分钟未活动的会话
|
||||||
|
this.cleanupInterval = setInterval(() => this.cleanup(), 10 * 60 * 1000);
|
||||||
|
if (this.cleanupInterval.unref) this.cleanupInterval.unref();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加消息到会话
|
||||||
|
*/
|
||||||
|
addMessage(sessionId: string, message: LlmMessage): void {
|
||||||
|
let session = this.sessions.get(sessionId);
|
||||||
|
if (!session) {
|
||||||
|
session = { messages: [], updatedAt: Date.now() };
|
||||||
|
this.sessions.set(sessionId, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
session.messages.push(message);
|
||||||
|
session.updatedAt = Date.now();
|
||||||
|
|
||||||
|
// 限制消息数量,保留最近的 N 条
|
||||||
|
if (session.messages.length > this.maxMessagesPerSession) {
|
||||||
|
session.messages = session.messages.slice(-this.maxMessagesPerSession);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话消息历史
|
||||||
|
*/
|
||||||
|
getMessages(sessionId: string): LlmMessage[] {
|
||||||
|
return this.sessions.get(sessionId)?.messages || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话消息数量
|
||||||
|
*/
|
||||||
|
getMessageCount(sessionId: string): number {
|
||||||
|
return this.sessions.get(sessionId)?.messages.length || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除会话
|
||||||
|
*/
|
||||||
|
clearSession(sessionId: string): void {
|
||||||
|
this.sessions.delete(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取活跃会话数
|
||||||
|
*/
|
||||||
|
getActiveSessionCount(): number {
|
||||||
|
return this.sessions.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清理过期会话 */
|
||||||
|
private cleanup(): void {
|
||||||
|
const now = Date.now();
|
||||||
|
const threshold = 30 * 60 * 1000; // 30 分钟
|
||||||
|
for (const [id, session] of this.sessions) {
|
||||||
|
if (now - session.updatedAt > threshold) {
|
||||||
|
this.sessions.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.sessions.size > 0) {
|
||||||
|
this.logger.debug(`[ShortTermMemory] 活跃会话: ${this.sessions.size}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ILlmProvider, LlmChatParams, LlmResponse, LlmChunk, LlmToolCall } from '../llm-provider.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ollama 本地模型 Provider 实现
|
||||||
|
* 借鉴 OpenClaw 对轻量化本地模型的支持
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class OllamaProvider implements ILlmProvider {
|
||||||
|
private readonly logger = new Logger(OllamaProvider.name);
|
||||||
|
readonly model: string;
|
||||||
|
private readonly baseUrl: string;
|
||||||
|
|
||||||
|
constructor(config: { model?: string; baseUrl?: string }) {
|
||||||
|
this.model = config.model || 'qwen2.5:7b';
|
||||||
|
this.baseUrl = config.baseUrl || 'http://localhost:11434';
|
||||||
|
}
|
||||||
|
|
||||||
|
async chat(params: LlmChatParams): Promise<LlmResponse> {
|
||||||
|
this.logger.debug(`[Ollama] chat: model=${this.model}`);
|
||||||
|
|
||||||
|
const response = await fetch(`${this.baseUrl}/api/chat`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: this.model,
|
||||||
|
messages: params.messages,
|
||||||
|
tools: params.tools,
|
||||||
|
stream: false,
|
||||||
|
options: {
|
||||||
|
temperature: params.temperature ?? 0.7,
|
||||||
|
num_predict: params.maxTokens,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Ollama API error ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as Record<string, unknown>;
|
||||||
|
return {
|
||||||
|
content: (data.message as Record<string, unknown>)?.content as string || '',
|
||||||
|
toolCalls: (data.message as Record<string, unknown>)?.tool_calls as LlmToolCall[] | undefined,
|
||||||
|
finishReason: data.done ? 'stop' : 'length',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async *chatStream(params: LlmChatParams): AsyncIterable<LlmChunk> {
|
||||||
|
this.logger.debug(`[Ollama] chatStream: model=${this.model}`);
|
||||||
|
|
||||||
|
const response = await fetch(`${this.baseUrl}/api/chat`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: this.model,
|
||||||
|
messages: params.messages,
|
||||||
|
stream: true,
|
||||||
|
options: {
|
||||||
|
temperature: params.temperature ?? 0.7,
|
||||||
|
num_predict: params.maxTokens,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok || !response.body) {
|
||||||
|
throw new Error(`Ollama Stream error ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = '';
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
const lines = buffer.split('\n');
|
||||||
|
buffer = lines.pop() || '';
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) continue;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
|
||||||
|
const message = parsed.message as Record<string, unknown> | undefined;
|
||||||
|
yield {
|
||||||
|
content: message?.content as string | undefined,
|
||||||
|
finishReason: parsed.done ? 'stop' : undefined,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
// 忽略解析错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ILlmProvider, LlmChatParams, LlmResponse, LlmChunk, LlmMessage, LlmToolCall } from '../llm-provider.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenAI GPT Provider 实现
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class OpenAiProvider implements ILlmProvider {
|
||||||
|
private readonly logger = new Logger(OpenAiProvider.name);
|
||||||
|
readonly model: string;
|
||||||
|
private readonly apiKey: string;
|
||||||
|
private readonly baseUrl: string;
|
||||||
|
|
||||||
|
constructor(config: { model?: string; apiKey?: string; baseUrl?: string }) {
|
||||||
|
this.model = config.model || 'gpt-4o';
|
||||||
|
this.apiKey = config.apiKey || '';
|
||||||
|
this.baseUrl = config.baseUrl || 'https://api.openai.com/v1';
|
||||||
|
}
|
||||||
|
|
||||||
|
async chat(params: LlmChatParams): Promise<LlmResponse> {
|
||||||
|
this.logger.debug(`[OpenAI] chat: model=${this.model}, messages=${params.messages.length}`);
|
||||||
|
|
||||||
|
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${this.apiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: this.model,
|
||||||
|
messages: params.messages,
|
||||||
|
tools: params.tools?.map((t) => ({
|
||||||
|
type: 'function',
|
||||||
|
function: t,
|
||||||
|
})),
|
||||||
|
temperature: params.temperature ?? 0.7,
|
||||||
|
max_tokens: params.maxTokens,
|
||||||
|
stream: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`OpenAI API error ${response.status}: ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as Record<string, unknown>;
|
||||||
|
const choices = data.choices as Array<Record<string, unknown>>;
|
||||||
|
const firstChoice = choices?.[0];
|
||||||
|
const message = firstChoice?.message as Record<string, unknown> | undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: (message?.content as string) || '',
|
||||||
|
toolCalls: message?.tool_calls as LlmToolCall[] | undefined,
|
||||||
|
finishReason: (firstChoice?.finish_reason as LlmResponse['finishReason']) || 'stop',
|
||||||
|
usage: data.usage as LlmResponse['usage'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async *chatStream(params: LlmChatParams): AsyncIterable<LlmChunk> {
|
||||||
|
this.logger.debug(`[OpenAI] chatStream: model=${this.model}`);
|
||||||
|
|
||||||
|
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${this.apiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: this.model,
|
||||||
|
messages: params.messages,
|
||||||
|
tools: params.tools?.map((t) => ({
|
||||||
|
type: 'function',
|
||||||
|
function: t,
|
||||||
|
})),
|
||||||
|
temperature: params.temperature ?? 0.7,
|
||||||
|
max_tokens: params.maxTokens,
|
||||||
|
stream: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok || !response.body) {
|
||||||
|
throw new Error(`OpenAI Stream error ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = '';
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
const lines = buffer.split('\n');
|
||||||
|
buffer = lines.pop() || '';
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || !trimmed.startsWith('data: ')) continue;
|
||||||
|
const data = trimmed.slice(6);
|
||||||
|
if (data === '[DONE]') return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(data) as Record<string, unknown>;
|
||||||
|
const choices = parsed.choices as Array<Record<string, unknown>> | undefined;
|
||||||
|
const delta = choices?.[0]?.delta as Record<string, unknown> | undefined;
|
||||||
|
|
||||||
|
yield {
|
||||||
|
content: delta?.content as string | undefined,
|
||||||
|
toolCalls: delta?.tool_calls as LlmToolCall[] | undefined,
|
||||||
|
finishReason: choices?.[0]?.finish_reason as LlmChunk['finishReason'] | undefined,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
// 忽略解析错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './llm-provider.interface';
|
||||||
|
export * from './llm-provider.factory';
|
||||||
|
export * from './impls/openai.provider';
|
||||||
|
export * from './impls/ollama.provider';
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
|
||||||
|
import { ILlmProvider, LlmChatParams, LlmResponse, LlmChunk } from './llm-provider.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LLM Provider 配置
|
||||||
|
*/
|
||||||
|
export interface LlmProviderConfig {
|
||||||
|
provider: string;
|
||||||
|
model: string;
|
||||||
|
apiKey?: string;
|
||||||
|
baseUrl?: string;
|
||||||
|
maxTokens?: number;
|
||||||
|
temperature?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Key 轮换条目
|
||||||
|
*/
|
||||||
|
interface ApiKeyEntry {
|
||||||
|
key: string;
|
||||||
|
cooldownUntil: number;
|
||||||
|
totalCalls: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LLM Provider 工厂
|
||||||
|
* 借鉴 OpenClaw 的多模型驱动 + API Key 轮换/冷却机制
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class LlmProviderFactory implements OnModuleDestroy {
|
||||||
|
private readonly logger = new Logger(LlmProviderFactory.name);
|
||||||
|
private readonly providers = new Map<string, ILlmProvider>();
|
||||||
|
private readonly apiKeys = new Map<string, ApiKeyEntry[]>();
|
||||||
|
private readonly keyCooldownMs = 60_000; // Key 冷却时间 60s
|
||||||
|
private defaultProviderName: string | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册 LLM Provider
|
||||||
|
*/
|
||||||
|
registerProvider(name: string, provider: ILlmProvider, isDefault = false): void {
|
||||||
|
this.providers.set(name, provider);
|
||||||
|
if (isDefault || !this.defaultProviderName) {
|
||||||
|
this.defaultProviderName = name;
|
||||||
|
}
|
||||||
|
this.logger.log(`LLM Provider 注册: ${name} (model: ${provider.model})${isDefault ? ' [默认]' : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置 API Key 轮换
|
||||||
|
*/
|
||||||
|
configureApiKeys(providerName: string, keys: string[]): void {
|
||||||
|
this.apiKeys.set(providerName, keys.map((key) => ({
|
||||||
|
key,
|
||||||
|
cooldownUntil: 0,
|
||||||
|
totalCalls: 0,
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Provider
|
||||||
|
*/
|
||||||
|
getProvider(name?: string): ILlmProvider {
|
||||||
|
const providerName = name || this.defaultProviderName;
|
||||||
|
if (!providerName) {
|
||||||
|
throw new Error('未配置任何 LLM Provider,请先调用 registerProvider()');
|
||||||
|
}
|
||||||
|
const provider = this.providers.get(providerName);
|
||||||
|
if (!provider) {
|
||||||
|
throw new Error(`LLM Provider [${providerName}] 未注册`);
|
||||||
|
}
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取默认 Provider
|
||||||
|
*/
|
||||||
|
getDefaultProvider(): ILlmProvider {
|
||||||
|
return this.getProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下一个可用的 API Key(带轮换和冷却)
|
||||||
|
*/
|
||||||
|
getNextApiKey(providerName: string): string | undefined {
|
||||||
|
const keys = this.apiKeys.get(providerName);
|
||||||
|
if (!keys || keys.length === 0) return undefined;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
// 找到第一个不在冷却中的 Key
|
||||||
|
for (const entry of keys) {
|
||||||
|
if (entry.cooldownUntil <= now) {
|
||||||
|
entry.totalCalls++;
|
||||||
|
return entry.key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有 Key 都在冷却中,返回第一个(降级)
|
||||||
|
this.logger.warn(`Provider [${providerName}] 所有 API Key 都在冷却中,使用降级策略`);
|
||||||
|
return keys[0].key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记 API Key 为冷却状态
|
||||||
|
*/
|
||||||
|
markKeyCooldown(providerName: string, apiKey: string): void {
|
||||||
|
const keys = this.apiKeys.get(providerName);
|
||||||
|
if (!keys) return;
|
||||||
|
const entry = keys.find((k) => k.key === apiKey);
|
||||||
|
if (entry) {
|
||||||
|
entry.cooldownUntil = Date.now() + this.keyCooldownMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有已注册的 Provider 名称
|
||||||
|
*/
|
||||||
|
getProviderNames(): string[] {
|
||||||
|
return Array.from(this.providers.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
onModuleDestroy(): void {
|
||||||
|
this.providers.clear();
|
||||||
|
this.apiKeys.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* LLM 消息角色
|
||||||
|
*/
|
||||||
|
export type LlmMessageRole = 'system' | 'user' | 'assistant' | 'tool';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LLM 消息
|
||||||
|
*/
|
||||||
|
export interface LlmMessage {
|
||||||
|
role: LlmMessageRole;
|
||||||
|
content: string;
|
||||||
|
name?: string;
|
||||||
|
toolCallId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LLM 工具定义
|
||||||
|
*/
|
||||||
|
export interface LlmToolDefinition {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
parameters: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LLM 工具调用
|
||||||
|
*/
|
||||||
|
export interface LlmToolCall {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
arguments: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LLM 响应块(流式)
|
||||||
|
*/
|
||||||
|
export interface LlmChunk {
|
||||||
|
content?: string;
|
||||||
|
toolCalls?: LlmToolCall[];
|
||||||
|
finishReason?: 'stop' | 'tool_calls' | 'length';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LLM 完整响应
|
||||||
|
*/
|
||||||
|
export interface LlmResponse {
|
||||||
|
content: string;
|
||||||
|
toolCalls?: LlmToolCall[];
|
||||||
|
finishReason: 'stop' | 'tool_calls' | 'length';
|
||||||
|
usage?: {
|
||||||
|
promptTokens: number;
|
||||||
|
completionTokens: number;
|
||||||
|
totalTokens: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LLM 调用参数
|
||||||
|
*/
|
||||||
|
export interface LlmChatParams {
|
||||||
|
messages: LlmMessage[];
|
||||||
|
tools?: LlmToolDefinition[];
|
||||||
|
stream?: boolean;
|
||||||
|
temperature?: number;
|
||||||
|
maxTokens?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LLM Provider 统一接口
|
||||||
|
* 借鉴 OpenClaw 的多模型 Provider 抽象
|
||||||
|
* 支持 OpenAI / DeepSeek / Qwen / Ollama 等任意 LLM
|
||||||
|
*/
|
||||||
|
export interface ILlmProvider {
|
||||||
|
/** 模型标识 */
|
||||||
|
readonly model: string;
|
||||||
|
|
||||||
|
/** 同步对话 */
|
||||||
|
chat(params: LlmChatParams): Promise<LlmResponse>;
|
||||||
|
|
||||||
|
/** 流式对话 */
|
||||||
|
chatStream(params: LlmChatParams): AsyncIterable<LlmChunk>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Module, DynamicModule } from '@nestjs/common';
|
||||||
|
import { LlmProviderFactory } from './llm-provider.factory';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LLM Provider 模块
|
||||||
|
* 借鉴 OpenClaw 多模型驱动引擎
|
||||||
|
* 支持运行时注册多个 LLM Provider
|
||||||
|
*/
|
||||||
|
@Module({})
|
||||||
|
export class LlmProviderModule {
|
||||||
|
static register(): DynamicModule {
|
||||||
|
return {
|
||||||
|
module: LlmProviderModule,
|
||||||
|
providers: [LlmProviderFactory],
|
||||||
|
exports: [LlmProviderFactory],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { ToolCallRecord } from './loop-detector.interface';
|
||||||
|
import { LlmMessage } from '../providers/llm-provider.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 任务定义
|
||||||
|
*/
|
||||||
|
export interface AiTask {
|
||||||
|
/** 任务描述 */
|
||||||
|
description: string;
|
||||||
|
/** 任务类型 */
|
||||||
|
type: string;
|
||||||
|
/** 上下文信息 */
|
||||||
|
context?: Record<string, unknown>;
|
||||||
|
/** 关联的会话标识 */
|
||||||
|
sessionId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 循环执行选项
|
||||||
|
*/
|
||||||
|
export interface LoopOptions {
|
||||||
|
/** 最大迭代次数(默认 50) */
|
||||||
|
maxIterations?: number;
|
||||||
|
/** 是否启用循环检测(默认 true) */
|
||||||
|
enableLoopDetection?: boolean;
|
||||||
|
/** 是否启用信任边界检查(默认 true) */
|
||||||
|
enableTrustBoundary?: boolean;
|
||||||
|
/** 超时时间(毫秒,默认 300000 = 5分钟) */
|
||||||
|
timeoutMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 循环执行结果
|
||||||
|
*/
|
||||||
|
export interface LoopResult {
|
||||||
|
/** 是否成功 */
|
||||||
|
success: boolean;
|
||||||
|
/** 最终响应内容 */
|
||||||
|
response?: string;
|
||||||
|
/** 迭代次数 */
|
||||||
|
iterations: number;
|
||||||
|
/** 工具调用历史 */
|
||||||
|
toolCalls: ToolCallRecord[];
|
||||||
|
/** 错误信息 */
|
||||||
|
error?: string;
|
||||||
|
/** 总耗时(毫秒) */
|
||||||
|
durationMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 信任边界检查结果
|
||||||
|
*/
|
||||||
|
export interface TrustBoundaryResult {
|
||||||
|
/** 审批状态 */
|
||||||
|
status: 'approved' | 'denied' | 'pending_human';
|
||||||
|
/** 原因 */
|
||||||
|
reason?: string;
|
||||||
|
/** 人工审批请求 ID(status=pending_human 时) */
|
||||||
|
requestId?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
import { Injectable, Logger, Inject, forwardRef, Optional } from '@nestjs/common';
|
||||||
|
import { LlmProviderFactory } from '../providers/llm-provider.factory';
|
||||||
|
import { LlmMessage, LlmToolDefinition } from '../providers/llm-provider.interface';
|
||||||
|
import { LoopDetectorService } from './loop-detector.service';
|
||||||
|
import { LoopResult, LoopOptions, AiTask } from './agentic-loop.interface';
|
||||||
|
import { ToolCallRecord } from './loop-detector.interface';
|
||||||
|
import { SkillExecutorService } from '../skills/skill-executor.service';
|
||||||
|
import { ShortTermMemoryService } from '../memory/short-term-memory.service';
|
||||||
|
import { LongTermMemoryService } from '../memory/long-term-memory.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 核心 ReAct 循环 — 借鉴 OpenClaw 的 Agentic Loop
|
||||||
|
*
|
||||||
|
* 执行流程:
|
||||||
|
* 1. 构建上下文(System Prompt + 长期记忆 + Skills 描述 + 短期记忆 + 用户输入)
|
||||||
|
* 2. 调用 LLM 推理
|
||||||
|
* 3. 如果 LLM 要求调用工具 → SkillExecutor 执行 → 结果注入上下文 → 回到步骤 2
|
||||||
|
* 4. 如果 LLM 生成最终回复 → 保存记忆 → 返回结果
|
||||||
|
*
|
||||||
|
* 安全机制:
|
||||||
|
* - 4 种循环检测器防止无限循环
|
||||||
|
* - 信任边界检查防止高危操作
|
||||||
|
* - 全局熔断器作为最后防线
|
||||||
|
*
|
||||||
|
* 记忆机制:
|
||||||
|
* - 短期记忆:自动维护会话消息历史
|
||||||
|
* - 长期记忆:自动提取和检索跨会话经验
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AgenticLoopService {
|
||||||
|
private readonly logger = new Logger(AgenticLoopService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly llmFactory: LlmProviderFactory,
|
||||||
|
private readonly loopDetector: LoopDetectorService,
|
||||||
|
@Optional() @Inject(forwardRef(() => SkillExecutorService))
|
||||||
|
private readonly skillExecutor?: SkillExecutorService,
|
||||||
|
@Optional() @Inject(forwardRef(() => ShortTermMemoryService))
|
||||||
|
private readonly shortTermMemory?: ShortTermMemoryService,
|
||||||
|
@Optional() @Inject(forwardRef(() => LongTermMemoryService))
|
||||||
|
private readonly longTermMemory?: LongTermMemoryService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行完整的 Agent 任务
|
||||||
|
* @param task 任务描述
|
||||||
|
* @param messages 对话消息列表
|
||||||
|
* @param tools 可用工具定义(可选,不传则自动从 SkillExecutor 获取)
|
||||||
|
* @param options 执行选项
|
||||||
|
* @returns 循环执行结果
|
||||||
|
*/
|
||||||
|
async run(
|
||||||
|
task: AiTask,
|
||||||
|
messages: LlmMessage[],
|
||||||
|
tools?: LlmToolDefinition[],
|
||||||
|
options?: LoopOptions,
|
||||||
|
): Promise<LoopResult> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
const maxIterations = options?.maxIterations ?? 50;
|
||||||
|
const enableLoopDetection = options?.enableLoopDetection ?? true;
|
||||||
|
const timeoutMs = options?.timeoutMs ?? 300_000;
|
||||||
|
const sessionId = task.sessionId ?? `session_${Date.now()}`;
|
||||||
|
|
||||||
|
let iterations = 0;
|
||||||
|
const toolCallHistory: ToolCallRecord[] = [];
|
||||||
|
|
||||||
|
// 如果未传入 tools,尝试从 SkillExecutor 自动获取
|
||||||
|
const effectiveTools = tools ?? this.skillExecutor?.getAvailableTools() ?? [];
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`[AgenticLoop] 开始执行任务: ${task.description} (maxIterations=${maxIterations}, tools=${effectiveTools.length})`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 1. 注入长期记忆到上下文
|
||||||
|
await this.injectLongTermMemory(messages, task.description);
|
||||||
|
|
||||||
|
// 2. 注入短期记忆到上下文(如果不是空会话)
|
||||||
|
await this.injectShortTermMemory(messages, sessionId);
|
||||||
|
|
||||||
|
while (iterations < maxIterations) {
|
||||||
|
iterations++;
|
||||||
|
|
||||||
|
// 超时检查
|
||||||
|
if (Date.now() - startTime > timeoutMs) {
|
||||||
|
this.logger.warn(`[AgenticLoop] 任务超时 (${timeoutMs}ms)`);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `任务执行超时 (${timeoutMs}ms)`,
|
||||||
|
iterations,
|
||||||
|
toolCalls: toolCallHistory,
|
||||||
|
durationMs: Date.now() - startTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 3. 调用 LLM 推理
|
||||||
|
const response = await this.llmFactory.getDefaultProvider().chat({
|
||||||
|
messages,
|
||||||
|
tools: effectiveTools.length > 0 ? effectiveTools : undefined,
|
||||||
|
temperature: 0.7,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. 判断是否需要调用工具
|
||||||
|
if (response.toolCalls && response.toolCalls.length > 0) {
|
||||||
|
// 5. 循环检测
|
||||||
|
if (enableLoopDetection) {
|
||||||
|
const newRecords: ToolCallRecord[] = response.toolCalls.map((tc) => ({
|
||||||
|
id: tc.id,
|
||||||
|
name: tc.name,
|
||||||
|
arguments: tc.arguments,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const warning = this.loopDetector.detect(toolCallHistory, newRecords);
|
||||||
|
if (warning.shouldStop) {
|
||||||
|
this.logger.warn(`[AgenticLoop] 循环检测触发: ${warning.reason} (迭代 ${iterations})`);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `循环检测: ${warning.reason}`,
|
||||||
|
iterations,
|
||||||
|
toolCalls: toolCallHistory,
|
||||||
|
durationMs: Date.now() - startTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 通过 SkillExecutor 执行工具
|
||||||
|
for (const toolCall of response.toolCalls) {
|
||||||
|
this.logger.debug(`[AgenticLoop] 工具调用: ${toolCall.name}(${toolCall.arguments})`);
|
||||||
|
|
||||||
|
let record: ToolCallRecord;
|
||||||
|
|
||||||
|
if (this.skillExecutor) {
|
||||||
|
// 使用 SkillExecutor 执行真实工具
|
||||||
|
record = await this.skillExecutor.execute(
|
||||||
|
toolCall.name,
|
||||||
|
toolCall.arguments,
|
||||||
|
{
|
||||||
|
sessionId,
|
||||||
|
taskType: task.type,
|
||||||
|
data: task.context,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// 保留 LLM 返回的原始 id
|
||||||
|
record.id = toolCall.id;
|
||||||
|
} else {
|
||||||
|
// SkillExecutor 不可用,返回未注册提示
|
||||||
|
record = {
|
||||||
|
id: toolCall.id,
|
||||||
|
name: toolCall.name,
|
||||||
|
arguments: toolCall.arguments,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
result: `工具 [${toolCall.name}] 未注册,SkillExecutor 不可用`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toolCallHistory.push(record);
|
||||||
|
|
||||||
|
// 将工具结果注入消息上下文
|
||||||
|
messages.push({
|
||||||
|
role: 'tool',
|
||||||
|
content: record.result ?? '',
|
||||||
|
name: toolCall.name,
|
||||||
|
toolCallId: toolCall.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保存到短期记忆
|
||||||
|
this.shortTermMemory?.addMessage(sessionId, {
|
||||||
|
role: 'tool',
|
||||||
|
content: record.result ?? '',
|
||||||
|
name: toolCall.name,
|
||||||
|
toolCallId: toolCall.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 7. LLM 生成最终回复,循环结束
|
||||||
|
this.logger.log(
|
||||||
|
`[AgenticLoop] 任务完成 (${iterations} 次迭代, ${Date.now() - startTime}ms)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 保存最终回复到短期记忆
|
||||||
|
if (response.content) {
|
||||||
|
this.shortTermMemory?.addMessage(sessionId, {
|
||||||
|
role: 'assistant',
|
||||||
|
content: response.content,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 提取经验保存到长期记忆
|
||||||
|
await this.extractAndSaveExperience(sessionId, task, response.content, toolCallHistory);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
response: response.content,
|
||||||
|
iterations,
|
||||||
|
toolCalls: toolCallHistory,
|
||||||
|
durationMs: Date.now() - startTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`[AgenticLoop] 迭代 ${iterations} 出错`,
|
||||||
|
error instanceof Error ? error.stack : String(error),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 记录错误到长期记忆
|
||||||
|
await this.longTermMemory?.remember({
|
||||||
|
content: `任务 [${task.description}] 执行失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
type: 'error',
|
||||||
|
sessionId,
|
||||||
|
tags: [task.type, 'error'],
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
iterations,
|
||||||
|
toolCalls: toolCallHistory,
|
||||||
|
durationMs: Date.now() - startTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 达到最大迭代次数
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `达到最大迭代次数 (${maxIterations})`,
|
||||||
|
iterations,
|
||||||
|
toolCalls: toolCallHistory,
|
||||||
|
durationMs: Date.now() - startTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注入长期记忆到消息上下文
|
||||||
|
* 检索与任务描述相关的历史经验,作为 system 消息注入
|
||||||
|
*/
|
||||||
|
private async injectLongTermMemory(messages: LlmMessage[], taskDescription: string): Promise<void> {
|
||||||
|
if (!this.longTermMemory) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const memories = await this.longTermMemory.recall(taskDescription, 3);
|
||||||
|
if (memories.length === 0) return;
|
||||||
|
|
||||||
|
const memoryContent = memories
|
||||||
|
.map((m) => `- [${new Date(m.timestamp).toLocaleString()}] ${m.content}`)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
messages.unshift({
|
||||||
|
role: 'system',
|
||||||
|
content: `以下是与此任务相关的历史经验,供参考:\n${memoryContent}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.debug(`[AgenticLoop] 注入 ${memories.length} 条长期记忆`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(`[AgenticLoop] 注入长期记忆失败`, error instanceof Error ? error.stack : String(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注入短期记忆到消息上下文
|
||||||
|
* 如果会话有历史消息,追加到 messages 列表
|
||||||
|
*/
|
||||||
|
private async injectShortTermMemory(messages: LlmMessage[], sessionId: string): Promise<void> {
|
||||||
|
if (!this.shortTermMemory) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const history = this.shortTermMemory.getMessages(sessionId);
|
||||||
|
if (history.length === 0) return;
|
||||||
|
|
||||||
|
// 只追加非 system 消息(system 消息通常由调用方设置)
|
||||||
|
const nonSystemHistory = history.filter((m) => m.role !== 'system');
|
||||||
|
if (nonSystemHistory.length > 0) {
|
||||||
|
messages.push(...nonSystemHistory);
|
||||||
|
this.logger.debug(`[AgenticLoop] 注入 ${nonSystemHistory.length} 条短期记忆`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(`[AgenticLoop] 注入短期记忆失败`, error instanceof Error ? error.stack : String(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取经验并保存到长期记忆
|
||||||
|
* 策略:将任务描述 + 工具调用链 + 最终结果摘要保存为经验
|
||||||
|
*/
|
||||||
|
private async extractAndSaveExperience(
|
||||||
|
sessionId: string,
|
||||||
|
task: AiTask,
|
||||||
|
response: string,
|
||||||
|
toolCalls: ToolCallRecord[],
|
||||||
|
): Promise<void> {
|
||||||
|
if (!this.longTermMemory) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 保存任务完成经验
|
||||||
|
const toolSummary = toolCalls.length > 0
|
||||||
|
? `使用了 ${toolCalls.map((tc) => tc.name).join(', ')}`
|
||||||
|
: '未使用工具';
|
||||||
|
|
||||||
|
await this.longTermMemory.remember({
|
||||||
|
content: `任务 [${task.description}] 成功完成。${toolSummary}。结果摘要: ${response.slice(0, 200)}`,
|
||||||
|
type: 'experience',
|
||||||
|
sessionId,
|
||||||
|
tags: [task.type, 'success'],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.debug(`[AgenticLoop] 已保存任务经验到长期记忆`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(`[AgenticLoop] 保存长期记忆失败`, error instanceof Error ? error.stack : String(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AgenticLoopService } from './agentic-loop.service';
|
||||||
|
import { LoopDetectorService } from './loop-detector.service';
|
||||||
|
import { LlmProviderModule } from '../providers/llm-provider.module';
|
||||||
|
import { AiSkillsModule } from '../skills/ai-skills.module';
|
||||||
|
import { AiMemoryModule } from '../memory/ai-memory.module';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI Runtime 模块
|
||||||
|
* 借鉴 OpenClaw Agent Runtime
|
||||||
|
* 提供 ReAct 循环 + 循环检测 + LLM Provider 管理 + Skills 执行 + 双模记忆
|
||||||
|
*/
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
LlmProviderModule.register(),
|
||||||
|
AiSkillsModule,
|
||||||
|
AiMemoryModule,
|
||||||
|
],
|
||||||
|
providers: [AgenticLoopService, LoopDetectorService],
|
||||||
|
exports: [AgenticLoopService, LoopDetectorService],
|
||||||
|
})
|
||||||
|
export class AiRuntimeModule {}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export * from './agentic-loop.service';
|
||||||
|
export * from './agentic-loop.interface';
|
||||||
|
export * from './loop-detector.service';
|
||||||
|
export * from './loop-detector.interface';
|
||||||
|
export * from './ai-runtime.module';
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* 循环检测警告
|
||||||
|
*/
|
||||||
|
export interface LoopWarning {
|
||||||
|
/** 是否应停止循环 */
|
||||||
|
shouldStop: boolean;
|
||||||
|
/** 警告原因 */
|
||||||
|
reason: string;
|
||||||
|
/** 严重程度 */
|
||||||
|
severity: 'info' | 'warning' | 'critical';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具调用记录
|
||||||
|
*/
|
||||||
|
export interface ToolCallRecord {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
arguments: string;
|
||||||
|
result?: string;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { LoopWarning, ToolCallRecord } from './loop-detector.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 循环检测器 — 借鉴 OpenClaw 的 4 种循环检测机制
|
||||||
|
* 防止 AI Agent 陷入无限循环:
|
||||||
|
* 1. 通用重复检测 — 相同参数调用超过阈值
|
||||||
|
* 2. 轮询无进展检测 — 轮询 N 次但结果无变化
|
||||||
|
* 3. 全局熔断器 — 总次数超限
|
||||||
|
* 4. 乒乓循环检测 — A→B→A→B 模式
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class LoopDetectorService {
|
||||||
|
private readonly logger = new Logger(LoopDetectorService.name);
|
||||||
|
|
||||||
|
/** 工具调用历史窗口大小 */
|
||||||
|
private static readonly TOOL_CALL_HISTORY_SIZE = 30;
|
||||||
|
/** 警告阈值 */
|
||||||
|
private static readonly WARNING_THRESHOLD = 10;
|
||||||
|
/** 临界阈值 */
|
||||||
|
private static readonly CRITICAL_THRESHOLD = 20;
|
||||||
|
/** 全局熔断器 */
|
||||||
|
private static readonly GLOBAL_CIRCUIT_BREAKER = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测工具调用是否陷入循环
|
||||||
|
* @param history 历史工具调用记录
|
||||||
|
* @param newCalls 新的工具调用
|
||||||
|
* @returns 循环警告(shouldStop=true 时应终止循环)
|
||||||
|
*/
|
||||||
|
detect(history: ToolCallRecord[], newCalls: ToolCallRecord[]): LoopWarning {
|
||||||
|
// 检测器 1:通用重复检测
|
||||||
|
const duplicateWarning = this.detectDuplicateCalls(history, newCalls);
|
||||||
|
if (duplicateWarning) return duplicateWarning;
|
||||||
|
|
||||||
|
// 检测器 2:轮询无进展检测
|
||||||
|
const pollingWarning = this.detectPollingNoProgress(history);
|
||||||
|
if (pollingWarning) return pollingWarning;
|
||||||
|
|
||||||
|
// 检测器 3:全局熔断器
|
||||||
|
if (history.length >= LoopDetectorService.GLOBAL_CIRCUIT_BREAKER) {
|
||||||
|
return {
|
||||||
|
shouldStop: true,
|
||||||
|
reason: `全局熔断:工具调用总次数超过 ${LoopDetectorService.GLOBAL_CIRCUIT_BREAKER}`,
|
||||||
|
severity: 'critical',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测器 4:乒乓循环检测
|
||||||
|
const pingPongWarning = this.detectPingPong(history);
|
||||||
|
if (pingPongWarning) return pingPongWarning;
|
||||||
|
|
||||||
|
return { shouldStop: false, reason: '', severity: 'info' };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测器 1:通用重复检测 — 相同工具+相同参数 重复调用超过 3 次
|
||||||
|
*/
|
||||||
|
private detectDuplicateCalls(history: ToolCallRecord[], newCalls: ToolCallRecord[]): LoopWarning | null {
|
||||||
|
for (const call of newCalls) {
|
||||||
|
const recentCalls = history.slice(-LoopDetectorService.WARNING_THRESHOLD);
|
||||||
|
const sameCalls = recentCalls.filter(
|
||||||
|
(h) => h.name === call.name && h.arguments === call.arguments,
|
||||||
|
);
|
||||||
|
if (sameCalls.length >= 3) {
|
||||||
|
this.logger.warn(`[LoopDetector] 重复检测: ${call.name} 相同参数调用 ${sameCalls.length} 次`);
|
||||||
|
return {
|
||||||
|
shouldStop: true,
|
||||||
|
reason: `工具 [${call.name}] 相同参数重复调用 ${sameCalls.length} 次`,
|
||||||
|
severity: 'warning',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测器 2:轮询无进展检测 — 连续 5 次调用结果相同
|
||||||
|
*/
|
||||||
|
private detectPollingNoProgress(history: ToolCallRecord[]): LoopWarning | null {
|
||||||
|
if (history.length < 6) return null;
|
||||||
|
const last6 = history.slice(-6);
|
||||||
|
const results = last6.map((h) => h.result);
|
||||||
|
// 检查最近 6 次结果是否全部相同
|
||||||
|
if (results.every((r) => r === results[0]) && results[0] !== undefined) {
|
||||||
|
this.logger.warn('[LoopDetector] 轮询无进展: 最近 6 次调用结果相同');
|
||||||
|
return {
|
||||||
|
shouldStop: true,
|
||||||
|
reason: '轮询无进展:最近多次调用结果相同',
|
||||||
|
severity: 'warning',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测器 4:乒乓循环检测 — A→B→A→B 交替调用模式
|
||||||
|
*/
|
||||||
|
private detectPingPong(history: ToolCallRecord[]): LoopWarning | null {
|
||||||
|
if (history.length < 4) return null;
|
||||||
|
const last4 = history.slice(-4);
|
||||||
|
const isPingPong =
|
||||||
|
last4[0].name === last4[2].name &&
|
||||||
|
last4[1].name === last4[3].name &&
|
||||||
|
last4[0].name !== last4[1].name;
|
||||||
|
if (isPingPong) {
|
||||||
|
this.logger.warn(`[LoopDetector] 乒乓循环: ${last4[0].name} ↔ ${last4[1].name}`);
|
||||||
|
return {
|
||||||
|
shouldStop: true,
|
||||||
|
reason: `检测到乒乓循环: ${last4[0].name} ↔ ${last4[1].name}`,
|
||||||
|
severity: 'warning',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -299,7 +299,7 @@ export class AiSecurityService {
|
|||||||
critical: 30,
|
critical: 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
const penalty = threatsDetected * riskPenalties[riskLevel];
|
const penalty = threatsDetected * riskPenalties[riskLevel as keyof typeof riskPenalties];
|
||||||
|
|
||||||
return Math.max(0, baseScore - penalty);
|
return Math.max(0, baseScore - penalty);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { SkillRegistryService } from './skill-registry.service';
|
||||||
|
import { SkillExecutorService } from './skill-executor.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI Skills 模块
|
||||||
|
* 借鉴 OpenClaw Skills 系统
|
||||||
|
* 提供技能注册、发现和执行能力
|
||||||
|
*/
|
||||||
|
@Module({
|
||||||
|
providers: [SkillRegistryService, SkillExecutorService],
|
||||||
|
exports: [SkillRegistryService, SkillExecutorService],
|
||||||
|
})
|
||||||
|
export class AiSkillsModule {}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './skill.interface';
|
||||||
|
export * from './skill-registry.service';
|
||||||
|
export * from './skill-executor.service';
|
||||||
|
export * from './ai-skills.module';
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { SkillRegistryService } from './skill-registry.service';
|
||||||
|
import { SkillContext, SkillResult } from './skill.interface';
|
||||||
|
import { ToolCallRecord } from '../runtime/loop-detector.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 技能执行器 — 借鉴 OpenClaw Skill Executor
|
||||||
|
* 作为 AgenticLoop 和具体 Skill 之间的桥梁
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class SkillExecutorService {
|
||||||
|
private readonly logger = new Logger(SkillExecutorService.name);
|
||||||
|
|
||||||
|
constructor(private readonly skillRegistry: SkillRegistryService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行工具调用并返回结果
|
||||||
|
* @param toolName 工具名称
|
||||||
|
* @param argsJson 工具参数 JSON 字符串
|
||||||
|
* @param context 执行上下文
|
||||||
|
* @returns 工具调用记录(包含结果)
|
||||||
|
*/
|
||||||
|
async execute(
|
||||||
|
toolName: string,
|
||||||
|
argsJson: string,
|
||||||
|
context: SkillContext,
|
||||||
|
): Promise<ToolCallRecord> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
const result: SkillResult = await this.skillRegistry.executeTool(
|
||||||
|
toolName,
|
||||||
|
argsJson,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
const output = result.success ? result.output : `错误: ${result.error}`;
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`[SkillExecutor] ${toolName} ${result.success ? '成功' : '失败'} (${duration}ms)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `tool_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
||||||
|
name: toolName,
|
||||||
|
arguments: argsJson,
|
||||||
|
result: output,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有可用工具定义(供 LLM Function Calling 使用)
|
||||||
|
*/
|
||||||
|
getAvailableTools() {
|
||||||
|
return this.skillRegistry.getAllToolDefinitions();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ISkill, SkillDefinition, SkillContext, SkillResult } from './skill.interface';
|
||||||
|
import { LlmToolDefinition } from '../providers/llm-provider.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 技能注册中心 — 借鉴 OpenClaw Skills 系统
|
||||||
|
* 管理所有已注册的技能,提供发现和执行能力
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class SkillRegistryService {
|
||||||
|
private readonly logger = new Logger(SkillRegistryService.name);
|
||||||
|
private readonly skills = new Map<string, ISkill>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册技能
|
||||||
|
*/
|
||||||
|
registerSkill(skill: ISkill): void {
|
||||||
|
const def = skill.getDefinition();
|
||||||
|
this.skills.set(def.name, skill);
|
||||||
|
this.logger.log(`技能注册: ${def.name} v${def.version} - ${def.description}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注销技能
|
||||||
|
*/
|
||||||
|
unregisterSkill(name: string): void {
|
||||||
|
this.skills.delete(name);
|
||||||
|
this.logger.log(`技能注销: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取技能
|
||||||
|
*/
|
||||||
|
getSkill(name: string): ISkill | undefined {
|
||||||
|
return this.skills.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有已注册技能名称
|
||||||
|
*/
|
||||||
|
getSkillNames(): string[] {
|
||||||
|
return Array.from(this.skills.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有技能的工具定义(供 LLM Function Calling 使用)
|
||||||
|
*/
|
||||||
|
getAllToolDefinitions(): LlmToolDefinition[] {
|
||||||
|
const tools: LlmToolDefinition[] = [];
|
||||||
|
for (const skill of this.skills.values()) {
|
||||||
|
const def = skill.getDefinition();
|
||||||
|
if (def.tools) {
|
||||||
|
tools.push(...def.tools);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tools;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据工具名称查找技能
|
||||||
|
*/
|
||||||
|
findSkillByToolName(toolName: string): ISkill | undefined {
|
||||||
|
for (const skill of this.skills.values()) {
|
||||||
|
const def = skill.getDefinition();
|
||||||
|
if (def.tools?.some((t) => t.name === toolName)) {
|
||||||
|
return skill;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行工具调用
|
||||||
|
*/
|
||||||
|
async executeTool(toolName: string, args: string, context: SkillContext): Promise<SkillResult> {
|
||||||
|
const skill = this.findSkillByToolName(toolName);
|
||||||
|
if (!skill) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
output: `工具 [${toolName}] 未注册`,
|
||||||
|
error: `SKILL_NOT_FOUND: ${toolName}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.logger.debug(`[SkillExecutor] 执行工具: ${toolName} (技能: ${skill.getDefinition().name})`);
|
||||||
|
return await skill.execute(toolName, args, context);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[SkillExecutor] 工具执行失败: ${toolName}`, error instanceof Error ? error.stack : String(error));
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
output: `工具 [${toolName}] 执行失败`,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { LlmToolDefinition } from '../providers/llm-provider.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 技能定义
|
||||||
|
*/
|
||||||
|
export interface SkillDefinition {
|
||||||
|
/** 技能名称 */
|
||||||
|
name: string;
|
||||||
|
/** 技能描述 */
|
||||||
|
description: string;
|
||||||
|
/** 技能版本 */
|
||||||
|
version: string;
|
||||||
|
/** 触发关键词 */
|
||||||
|
triggers: string[];
|
||||||
|
/** 工具定义(供 LLM Function Calling 使用) */
|
||||||
|
tools?: LlmToolDefinition[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 技能执行上下文
|
||||||
|
*/
|
||||||
|
export interface SkillContext {
|
||||||
|
/** 会话标识 */
|
||||||
|
sessionId: string;
|
||||||
|
/** 任务类型 */
|
||||||
|
taskType: string;
|
||||||
|
/** 附加数据 */
|
||||||
|
data?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 技能执行结果
|
||||||
|
*/
|
||||||
|
export interface SkillResult {
|
||||||
|
success: boolean;
|
||||||
|
output: string;
|
||||||
|
error?: string;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 技能接口 — 借鉴 OpenClaw Skills
|
||||||
|
* 每个 Skill 是一个可独立执行的工作流单元
|
||||||
|
*/
|
||||||
|
export interface ISkill {
|
||||||
|
/** 获取技能定义 */
|
||||||
|
getDefinition(): SkillDefinition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行技能
|
||||||
|
* @param toolName 工具名称
|
||||||
|
* @param args 工具参数(JSON 字符串)
|
||||||
|
* @param context 执行上下文
|
||||||
|
*/
|
||||||
|
execute(toolName: string, args: string, context: SkillContext): Promise<SkillResult>;
|
||||||
|
}
|
||||||
@@ -3,9 +3,44 @@ import { AiManagerModule } from "./manager/manager.module";
|
|||||||
import { AiHealingModule } from "./healing/healing.module";
|
import { AiHealingModule } from "./healing/healing.module";
|
||||||
import { AiSafeModule } from "./safe/safe.module";
|
import { AiSafeModule } from "./safe/safe.module";
|
||||||
import { AiTunerModule } from "./tuner/tuner.module";
|
import { AiTunerModule } from "./tuner/tuner.module";
|
||||||
|
import { AiRuntimeModule } from "./runtime/ai-runtime.module";
|
||||||
|
import { AiSkillsModule } from "./skills/ai-skills.module";
|
||||||
|
import { AiMemoryModule } from "./memory/ai-memory.module";
|
||||||
|
import { AiGeneratorModule } from "./generator/ai-generator.module";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WWJCloud AI 根模块
|
||||||
|
*
|
||||||
|
* 架构设计借鉴 OpenClaw + NiuCloud Lite AI:
|
||||||
|
* - Manager: 工作流编排 + 服务注册发现 + 跨模块协调
|
||||||
|
* - Healing: 自愈(故障检测 → 策略决策 → 恢复执行)
|
||||||
|
* - Safe: 安全(漏洞检测 + 访问保护 + 审计)
|
||||||
|
* - Tuner: 性能调优(资源监控 + 缓存/查询优化)
|
||||||
|
* - Runtime: Agent Runtime(ReAct 循环 + 循环检测 + LLM Provider)
|
||||||
|
* - Skills: 技能系统(注册/发现/执行,借鉴 OpenClaw Skills)
|
||||||
|
* - Memory: 记忆系统(短期会话 + 长期经验,借鉴 OpenClaw 双模记忆)
|
||||||
|
* - Generator: 🆕 代码生成(框架级技能包,借鉴 NiuCloud Lite AI Skills 模块化开发)
|
||||||
|
*/
|
||||||
@Module({
|
@Module({
|
||||||
imports: [AiManagerModule, AiHealingModule, AiSafeModule, AiTunerModule],
|
imports: [
|
||||||
exports: [AiManagerModule, AiHealingModule, AiSafeModule, AiTunerModule],
|
AiManagerModule,
|
||||||
|
AiHealingModule,
|
||||||
|
AiSafeModule,
|
||||||
|
AiTunerModule,
|
||||||
|
AiRuntimeModule,
|
||||||
|
AiSkillsModule,
|
||||||
|
AiMemoryModule,
|
||||||
|
AiGeneratorModule,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
AiManagerModule,
|
||||||
|
AiHealingModule,
|
||||||
|
AiSafeModule,
|
||||||
|
AiTunerModule,
|
||||||
|
AiRuntimeModule,
|
||||||
|
AiSkillsModule,
|
||||||
|
AiMemoryModule,
|
||||||
|
AiGeneratorModule,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class WwjcloudAiModule {}
|
export class WwjcloudAiModule {}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { requestIdMiddleware } from "./request-id.middleware";
|
|||||||
import { buildRequestContextMiddleware } from "./request-context.middleware";
|
import { buildRequestContextMiddleware } from "./request-context.middleware";
|
||||||
import { RequestContextService } from "./request-context.service";
|
import { RequestContextService } from "./request-context.service";
|
||||||
import helmet from "helmet";
|
import helmet from "helmet";
|
||||||
|
// @ts-expect-error - compression 没有类型声明
|
||||||
import compression from "compression";
|
import compression from "compression";
|
||||||
import { buildTenantMiddleware } from "../tenant/tenant.middleware";
|
import { buildTenantMiddleware } from "../tenant/tenant.middleware";
|
||||||
import { TenantService } from "../tenant/tenant.service";
|
import { TenantService } from "../tenant/tenant.service";
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export class JobSchedulerService implements OnModuleInit {
|
|||||||
const { Queue, Worker } = require("bullmq");
|
const { Queue, Worker } = require("bullmq");
|
||||||
|
|
||||||
const queue = new Queue(`scheduled-${jobName}`, {
|
const queue = new Queue(`scheduled-${jobName}`, {
|
||||||
connection: this.queueService["getConnection"](),
|
connection: this.queueService.getConnection(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// 添加重复任务
|
// 添加重复任务
|
||||||
@@ -82,14 +82,14 @@ export class JobSchedulerService implements OnModuleInit {
|
|||||||
// 创建Worker
|
// 创建Worker
|
||||||
const worker = new Worker(
|
const worker = new Worker(
|
||||||
`scheduled-${jobName}`,
|
`scheduled-${jobName}`,
|
||||||
async (job) => {
|
async (job: any) => {
|
||||||
const provider = this.jobProviders.get(jobName);
|
const provider = this.jobProviders.get(jobName);
|
||||||
if (provider) {
|
if (provider) {
|
||||||
await provider.execute(job.data || {});
|
await provider.execute(job.data || {});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
connection: this.queueService["getConnection"](),
|
connection: this.queueService.getConnection(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -265,6 +265,17 @@ export class QueueService {
|
|||||||
return counts;
|
return counts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 BullMQ Redis 连接配置
|
||||||
|
* 供 JobSchedulerService 等外部模块使用
|
||||||
|
*/
|
||||||
|
getConnection(): { host?: string; port?: number; password?: string } {
|
||||||
|
const host = this.config.get<string>("QUEUE_REDIS_HOST");
|
||||||
|
const port = this.config.get<number>("QUEUE_REDIS_PORT") ?? 6379;
|
||||||
|
const password = this.config.get<string>("QUEUE_REDIS_PASSWORD");
|
||||||
|
return { host, port, password };
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
2
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/errors/index.ts
vendored
Normal file
2
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/errors/index.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './vendor.exception';
|
||||||
|
export * from './vendor-error.filter';
|
||||||
37
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/errors/vendor-error.filter.ts
vendored
Normal file
37
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/errors/vendor-error.filter.ts
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { ExceptionFilter, Catch, ArgumentsHost, Logger } from '@nestjs/common';
|
||||||
|
import { Response } from 'express';
|
||||||
|
import { VendorException, ProviderNotFoundException } from './vendor.exception';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vendor 层统一异常过滤器
|
||||||
|
* 捕获 VendorException 及其子类,返回结构化错误响应
|
||||||
|
*/
|
||||||
|
@Catch(VendorException, ProviderNotFoundException)
|
||||||
|
export class VendorErrorFilter implements ExceptionFilter {
|
||||||
|
private readonly logger = new Logger(VendorErrorFilter.name);
|
||||||
|
|
||||||
|
catch(exception: VendorException | ProviderNotFoundException, host: ArgumentsHost): void {
|
||||||
|
const ctx = host.switchToHttp();
|
||||||
|
const response = ctx.getResponse<Response>();
|
||||||
|
|
||||||
|
const status = exception.getStatus();
|
||||||
|
const isVendorException = exception instanceof VendorException;
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
code: status,
|
||||||
|
message: exception.message,
|
||||||
|
vendor: isVendorException ? (exception as VendorException).vendor : undefined,
|
||||||
|
channel: isVendorException ? (exception as VendorException).channel : undefined,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.logger.error(
|
||||||
|
`[VendorError] ${exception.message}`,
|
||||||
|
isVendorException && (exception as VendorException).originalError
|
||||||
|
? (exception as VendorException).originalError?.stack
|
||||||
|
: exception.stack,
|
||||||
|
);
|
||||||
|
|
||||||
|
response.status(status).json(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/errors/vendor.exception.ts
vendored
Normal file
34
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/errors/vendor.exception.ts
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vendor 层统一异常基类
|
||||||
|
* 所有 Vendor 层的业务异常都应继承此类
|
||||||
|
*/
|
||||||
|
export class VendorException extends BadRequestException {
|
||||||
|
constructor(
|
||||||
|
public readonly vendor: string,
|
||||||
|
public readonly channel: string,
|
||||||
|
message: string,
|
||||||
|
public readonly originalError?: Error,
|
||||||
|
) {
|
||||||
|
super(`[${vendor}:${channel}] ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider 未找到异常
|
||||||
|
*/
|
||||||
|
export class ProviderNotFoundException extends NotFoundException {
|
||||||
|
constructor(vendor: string, channel: string) {
|
||||||
|
super(`${vendor} Provider [${channel}] 未注册或未启用`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider 不可用异常(健康检查失败)
|
||||||
|
*/
|
||||||
|
export class ProviderUnavailableException extends BadRequestException {
|
||||||
|
constructor(vendor: string, channel: string, reason?: string) {
|
||||||
|
super(`${vendor} Provider [${channel}] 当前不可用${reason ? `: ${reason}` : ''}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/gateway/index.ts
vendored
Normal file
3
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/gateway/index.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './vendor-gateway.service';
|
||||||
|
export * from './vendor-gateway.controller';
|
||||||
|
export * from './vendor-gateway.module';
|
||||||
44
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/gateway/vendor-gateway.controller.ts
vendored
Normal file
44
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/gateway/vendor-gateway.controller.ts
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||||
|
import { VendorGatewayService } from './vendor-gateway.service';
|
||||||
|
import { ProviderRegistryService } from '../registry/provider-registry.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vendor 管理控制器
|
||||||
|
* 提供 Vendor 层的管理 API
|
||||||
|
*/
|
||||||
|
@ApiTags('Vendor 管理')
|
||||||
|
@Controller('adminapi/vendor')
|
||||||
|
export class VendorGatewayController {
|
||||||
|
constructor(
|
||||||
|
private readonly gatewayService: VendorGatewayService,
|
||||||
|
private readonly registry: ProviderRegistryService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有已注册的 Vendor 能力列表
|
||||||
|
*/
|
||||||
|
@Get('capabilities')
|
||||||
|
@ApiOperation({ summary: '获取所有已注册的 Vendor 能力列表' })
|
||||||
|
getCapabilities() {
|
||||||
|
return this.gatewayService.getCapabilities();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Vendor 层健康状态摘要
|
||||||
|
*/
|
||||||
|
@Get('health')
|
||||||
|
@ApiOperation({ summary: '获取 Vendor 层健康状态摘要' })
|
||||||
|
getHealthSummary() {
|
||||||
|
return this.gatewayService.getHealthSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动触发指定 Provider 的健康检查
|
||||||
|
*/
|
||||||
|
@Post('health/:name')
|
||||||
|
@ApiOperation({ summary: '手动触发指定 Provider 的健康检查' })
|
||||||
|
async checkHealth(@Param('name') name: string) {
|
||||||
|
return this.registry.checkHealth(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/gateway/vendor-gateway.module.ts
vendored
Normal file
17
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/gateway/vendor-gateway.module.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { VendorGatewayService } from './vendor-gateway.service';
|
||||||
|
import { VendorGatewayController } from './vendor-gateway.controller';
|
||||||
|
import { ProviderRegistryModule } from '../registry/provider-registry.module';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vendor Gateway 模块
|
||||||
|
* 借鉴 OpenClaw Gateway 统一入口理念
|
||||||
|
* 作为所有 Vendor 能力的统一调度层
|
||||||
|
*/
|
||||||
|
@Module({
|
||||||
|
imports: [ProviderRegistryModule],
|
||||||
|
controllers: [VendorGatewayController],
|
||||||
|
providers: [VendorGatewayService],
|
||||||
|
exports: [VendorGatewayService],
|
||||||
|
})
|
||||||
|
export class VendorGatewayModule {}
|
||||||
87
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/gateway/vendor-gateway.service.ts
vendored
Normal file
87
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/gateway/vendor-gateway.service.ts
vendored
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ProviderRegistryService } from '../registry/provider-registry.service';
|
||||||
|
import { ProviderMetadata } from '../registry/provider-metadata.interface';
|
||||||
|
import { VendorCapability } from '../interfaces/vendor-capability.interface';
|
||||||
|
import { ProviderNotFoundException, ProviderUnavailableException } from '../errors/vendor.exception';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vendor 能力网关 — 借鉴 OpenClaw Gateway 统一入口理念
|
||||||
|
* 作为所有 Vendor 能力的统一调度入口,提供:
|
||||||
|
* - Provider 路由和分发
|
||||||
|
* - 健康状态聚合
|
||||||
|
* - 全链路追踪(runId)
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class VendorGatewayService {
|
||||||
|
private readonly logger = new Logger(VendorGatewayService.name);
|
||||||
|
|
||||||
|
constructor(private readonly registry: ProviderRegistryService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过能力网关执行 Vendor 调用
|
||||||
|
* @param capability 能力标识(如 pay.wechat、sms.aliyun)
|
||||||
|
* @param input 输入参数
|
||||||
|
* @returns 执行结果
|
||||||
|
*/
|
||||||
|
async execute<TInput, TOutput>(capability: string, input: TInput): Promise<TOutput> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
const runId = this.generateRunId();
|
||||||
|
|
||||||
|
this.logger.log(`[VendorGateway:${runId}] 开始执行能力: ${capability}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const provider = this.registry.getProvider<VendorCapability<TInput, TOutput>>(capability);
|
||||||
|
const result = await provider.execute(input);
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
this.logger.log(`[VendorGateway:${runId}] 能力 ${capability} 执行成功 (${duration}ms)`);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
if (error instanceof ProviderNotFoundException || error instanceof ProviderUnavailableException) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
this.logger.error(`[VendorGateway:${runId}] 能力 ${capability} 执行失败 (${duration}ms)`, error instanceof Error ? error.stack : String(error));
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册 Vendor 能力
|
||||||
|
*/
|
||||||
|
registerCapability(provider: VendorCapability): void {
|
||||||
|
const metadata = provider.getMetadata();
|
||||||
|
this.registry.register(provider, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有已注册能力
|
||||||
|
*/
|
||||||
|
getCapabilities(): string[] {
|
||||||
|
return this.registry.getAllProviderNames();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按类型获取能力列表
|
||||||
|
*/
|
||||||
|
getCapabilitiesByType(type: string): string[] {
|
||||||
|
return this.registry.getProvidersByCapability(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取健康状态摘要
|
||||||
|
*/
|
||||||
|
getHealthSummary(): Array<{ name: string; status: string; lastCheck: number }> {
|
||||||
|
return this.registry.getHealthSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成追踪 ID(借鉴 OpenClaw runId)
|
||||||
|
*/
|
||||||
|
private generateRunId(): string {
|
||||||
|
const timestamp = Date.now().toString(36);
|
||||||
|
const random = Math.random().toString(36).substring(2, 8);
|
||||||
|
return `${timestamp}-${random}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/interfaces/index.ts
vendored
Normal file
5
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/interfaces/index.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export * from './vendor-capability.interface';
|
||||||
|
export * from './pay.interface';
|
||||||
|
export * from './sms.interface';
|
||||||
|
export * from './upload.interface';
|
||||||
|
export * from './notice.interface';
|
||||||
25
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/interfaces/notice.interface.ts
vendored
Normal file
25
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/interfaces/notice.interface.ts
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* 通知发送参数
|
||||||
|
*/
|
||||||
|
export interface NoticeSendParams {
|
||||||
|
to: string;
|
||||||
|
template: string;
|
||||||
|
vars?: Record<string, unknown>;
|
||||||
|
channel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知发送结果
|
||||||
|
*/
|
||||||
|
export interface NoticeSendResult {
|
||||||
|
ok: boolean;
|
||||||
|
noticeId: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强类型通知 Provider 接口
|
||||||
|
*/
|
||||||
|
export interface INoticeProviderTyped {
|
||||||
|
send(params: NoticeSendParams): Promise<NoticeSendResult>;
|
||||||
|
}
|
||||||
82
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/interfaces/pay.interface.ts
vendored
Normal file
82
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/interfaces/pay.interface.ts
vendored
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* 支付订单创建参数 — 强类型替代 Record<string, any>
|
||||||
|
*/
|
||||||
|
export interface CreateOrderParams {
|
||||||
|
siteId: number;
|
||||||
|
outTradeNo: string;
|
||||||
|
totalFee: number;
|
||||||
|
body: string;
|
||||||
|
notifyUrl: string;
|
||||||
|
openId?: string;
|
||||||
|
tradeType?: string;
|
||||||
|
attach?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付订单查询结果
|
||||||
|
*/
|
||||||
|
export interface PayOrderResult {
|
||||||
|
orderId: string;
|
||||||
|
paySign: string;
|
||||||
|
prepayId?: string;
|
||||||
|
qrCode?: string;
|
||||||
|
timeStamp: string;
|
||||||
|
nonceStr: string;
|
||||||
|
tradeType?: string;
|
||||||
|
mwebUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退款参数
|
||||||
|
*/
|
||||||
|
export interface RefundParams {
|
||||||
|
siteId: number;
|
||||||
|
outTradeNo: string;
|
||||||
|
outRefundNo: string;
|
||||||
|
totalFee: number;
|
||||||
|
refundFee: number;
|
||||||
|
refundReason?: string;
|
||||||
|
notifyUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退款结果
|
||||||
|
*/
|
||||||
|
export interface RefundResult {
|
||||||
|
refundId: string;
|
||||||
|
outRefundNo: string;
|
||||||
|
status: 'processing' | 'success' | 'failed';
|
||||||
|
refundFee: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单查询参数
|
||||||
|
*/
|
||||||
|
export interface QueryOrderParams {
|
||||||
|
outTradeNo?: string;
|
||||||
|
transactionId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单查询结果
|
||||||
|
*/
|
||||||
|
export interface QueryOrderResult {
|
||||||
|
outTradeNo: string;
|
||||||
|
transactionId?: string;
|
||||||
|
tradeState: 'SUCCESS' | 'REFUND' | 'NOTPAY' | 'CLOSED' | 'PAYERROR';
|
||||||
|
totalFee: number;
|
||||||
|
payTime?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强类型支付 Provider 接口
|
||||||
|
* 替代原有的 IPayProvider(使用 Record<string, any>)
|
||||||
|
*/
|
||||||
|
export interface IPayProviderTyped {
|
||||||
|
/** 创建支付订单 */
|
||||||
|
createOrder(params: CreateOrderParams): Promise<PayOrderResult>;
|
||||||
|
/** 申请退款 */
|
||||||
|
refund(params: RefundParams): Promise<RefundResult>;
|
||||||
|
/** 查询订单 */
|
||||||
|
queryOrder(params: QueryOrderParams): Promise<QueryOrderResult>;
|
||||||
|
}
|
||||||
28
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/interfaces/sms.interface.ts
vendored
Normal file
28
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/interfaces/sms.interface.ts
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* 短信发送参数
|
||||||
|
*/
|
||||||
|
export interface SmsSendParams {
|
||||||
|
phoneNumber: string;
|
||||||
|
content: string;
|
||||||
|
signName?: string;
|
||||||
|
templateCode?: string;
|
||||||
|
templateParams?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 短信发送结果
|
||||||
|
*/
|
||||||
|
export interface SmsSendResult {
|
||||||
|
ok: boolean;
|
||||||
|
messageId: string;
|
||||||
|
error?: string;
|
||||||
|
requestId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强类型短信 Provider 接口
|
||||||
|
*/
|
||||||
|
export interface ISmsProviderTyped {
|
||||||
|
/** 发送短信 */
|
||||||
|
send(params: SmsSendParams): Promise<SmsSendResult>;
|
||||||
|
}
|
||||||
83
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/interfaces/upload.interface.ts
vendored
Normal file
83
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/interfaces/upload.interface.ts
vendored
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* 上传模型(保留原有定义,增加类型约束)
|
||||||
|
*/
|
||||||
|
export interface UploadModel {
|
||||||
|
uploadFile?: unknown;
|
||||||
|
uploadType?: string;
|
||||||
|
uploadFilePath?: string;
|
||||||
|
uploadFileName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传结果模型
|
||||||
|
*/
|
||||||
|
export interface UploadModelResult {
|
||||||
|
accessUrl: string;
|
||||||
|
originalFilename?: string;
|
||||||
|
size?: number;
|
||||||
|
uploadMethod?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除模型
|
||||||
|
*/
|
||||||
|
export interface DeleteModel {
|
||||||
|
filePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除结果模型
|
||||||
|
*/
|
||||||
|
export interface DeleteModelResult {
|
||||||
|
result: boolean;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缩略图模型
|
||||||
|
*/
|
||||||
|
export interface ThumbModel {
|
||||||
|
type: string;
|
||||||
|
filePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缩略图结果模型
|
||||||
|
*/
|
||||||
|
export interface ThumbModelResult {
|
||||||
|
url: string;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base64 上传模型
|
||||||
|
*/
|
||||||
|
export interface Base64Model {
|
||||||
|
base64: string;
|
||||||
|
fileName?: string;
|
||||||
|
dir?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件拉取模型
|
||||||
|
*/
|
||||||
|
export interface FetchModel {
|
||||||
|
url: string;
|
||||||
|
fileName?: string;
|
||||||
|
dir?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强类型上传 Provider 接口
|
||||||
|
* 严格对齐 Java IUploadProvider
|
||||||
|
*/
|
||||||
|
export interface IUploadProviderTyped {
|
||||||
|
init(configObject: Record<string, unknown>): void;
|
||||||
|
getAccessUrl(location: string): string;
|
||||||
|
upload(uploadModel: UploadModel): Promise<UploadModelResult>;
|
||||||
|
delete(deleteModel: DeleteModel): Promise<DeleteModelResult>;
|
||||||
|
thumb(thumbModel: ThumbModel): Promise<ThumbModelResult>;
|
||||||
|
base64(base64Model: Base64Model): Promise<string>;
|
||||||
|
fetch(fetchModel: FetchModel): Promise<string>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { ProviderMetadata } from '../registry/provider-metadata.interface';
|
||||||
|
import { HealthCheckResult } from '../registry/provider-health.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一能力接口 — 借鉴 OpenClaw 的 Provider 统一抽象
|
||||||
|
* 所有 Vendor 能力(支付/短信/上传/通知)都通过此接口暴露
|
||||||
|
*/
|
||||||
|
export interface VendorCapability<TInput = unknown, TOutput = unknown> {
|
||||||
|
/** 能力标识(如 pay.wechat、sms.aliyun) */
|
||||||
|
readonly capability: string;
|
||||||
|
|
||||||
|
/** 执行能力调用 */
|
||||||
|
execute(input: TInput): Promise<TOutput>;
|
||||||
|
|
||||||
|
/** 健康检查(可选) */
|
||||||
|
healthCheck?(): Promise<HealthCheckResult>;
|
||||||
|
|
||||||
|
/** 获取元数据 */
|
||||||
|
getMetadata(): ProviderMetadata;
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { SmsSendParams, SmsSendResult, ISmsProviderTyped } from '../../interfaces/sms.interface';
|
||||||
|
import { VendorCapability } from '../../interfaces/vendor-capability.interface';
|
||||||
|
import { ProviderMetadata } from '../../registry/provider-metadata.interface';
|
||||||
|
import { HealthCheckResult } from '../../registry/provider-health.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 阿里云短信 Provider 实现
|
||||||
|
* 对接阿里云 SMS API (Dysmsapi20170525)
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AliyunSmsProvider implements ISmsProviderTyped, VendorCapability<SmsSendParams, SmsSendResult> {
|
||||||
|
private readonly logger = new Logger(AliyunSmsProvider.name);
|
||||||
|
readonly capability = 'sms.aliyun';
|
||||||
|
private accessKeyId = '';
|
||||||
|
private accessKeySecret = '';
|
||||||
|
private signName = '';
|
||||||
|
private endpoint = 'dysmsapi.aliyuncs.com';
|
||||||
|
|
||||||
|
/** 配置阿里云 SMS Provider */
|
||||||
|
configure(config: { accessKeyId?: string; accessKeySecret?: string; signName?: string; endpoint?: string }): void {
|
||||||
|
this.accessKeyId = config.accessKeyId || process.env.ALIYUN_SMS_ACCESS_KEY_ID || '';
|
||||||
|
this.accessKeySecret = config.accessKeySecret || process.env.ALIYUN_SMS_ACCESS_KEY_SECRET || '';
|
||||||
|
this.signName = config.signName || process.env.ALIYUN_SMS_SIGN_NAME || '';
|
||||||
|
if (config.endpoint) this.endpoint = config.endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发送短信 */
|
||||||
|
async send(params: SmsSendParams): Promise<SmsSendResult> {
|
||||||
|
if (!this.accessKeyId || !this.accessKeySecret) {
|
||||||
|
return { ok: false, messageId: '', error: '阿里云 SMS 未配置 accessKeyId/accessKeySecret' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用阿里云 SMS REST API 发送短信
|
||||||
|
const response = await fetch(`https://${this.endpoint}/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `ACS3-HMAC-SHA256 Credential=${this.accessKeyId}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
PhoneNumbers: params.phoneNumber,
|
||||||
|
SignName: params.signName || this.signName,
|
||||||
|
TemplateCode: params.templateCode,
|
||||||
|
TemplateParam: params.templateParams ? JSON.stringify(params.templateParams) : undefined,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json() as Record<string, unknown>;
|
||||||
|
const code = data.Code as string;
|
||||||
|
|
||||||
|
if (code === 'OK') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
messageId: data.BizId as string || `aliyun-${Date.now()}`,
|
||||||
|
requestId: data.RequestId as string,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
messageId: '',
|
||||||
|
error: `阿里云 SMS 错误: ${code} - ${data.Message}`,
|
||||||
|
requestId: data.RequestId as string,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`阿里云 SMS 发送失败`, error instanceof Error ? error.stack : String(error));
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
messageId: '',
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 执行短信发送(VendorCapability 接口实现) */
|
||||||
|
async execute(input: SmsSendParams): Promise<SmsSendResult> {
|
||||||
|
return this.send(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取 Provider 元数据 */
|
||||||
|
getMetadata(): ProviderMetadata {
|
||||||
|
return {
|
||||||
|
name: 'aliyun-sms',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: '阿里云短信服务 Provider',
|
||||||
|
author: 'WWJCloud',
|
||||||
|
capabilities: ['sms'],
|
||||||
|
configSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
accessKeyId: { type: 'string' },
|
||||||
|
accessKeySecret: { type: 'string' },
|
||||||
|
signName: { type: 'string' },
|
||||||
|
},
|
||||||
|
required: ['accessKeyId', 'accessKeySecret'],
|
||||||
|
},
|
||||||
|
healthCheckInterval: 60000,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 健康检查:验证阿里云 SMS 是否已配置 */
|
||||||
|
async healthCheck(): Promise<HealthCheckResult> {
|
||||||
|
const start = Date.now();
|
||||||
|
const configured = !!(this.accessKeyId && this.accessKeySecret);
|
||||||
|
return {
|
||||||
|
status: configured ? 'healthy' : 'degraded',
|
||||||
|
latencyMs: Date.now() - start,
|
||||||
|
message: configured ? '阿里云 SMS 已配置' : '阿里云 SMS 未配置',
|
||||||
|
checkedAt: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
3
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/provider-factories/impls/index.ts
vendored
Normal file
3
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/provider-factories/impls/index.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './local-upload.provider';
|
||||||
|
export * from './aliyun-sms.provider';
|
||||||
|
export * from './wechat-pay.provider';
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import {
|
||||||
|
UploadProvider,
|
||||||
|
UploadModel,
|
||||||
|
UploadModelResult,
|
||||||
|
DeleteModel,
|
||||||
|
DeleteModelResult,
|
||||||
|
ThumbModel,
|
||||||
|
ThumbModelResult,
|
||||||
|
Base64Model,
|
||||||
|
FetchModel,
|
||||||
|
} from '../upload-provider.factory';
|
||||||
|
import { VendorCapability } from '../../interfaces/vendor-capability.interface';
|
||||||
|
import { ProviderMetadata } from '../../registry/provider-metadata.interface';
|
||||||
|
import { HealthCheckResult } from '../../registry/provider-health.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本地存储 Provider 实现
|
||||||
|
* 将文件存储在本地文件系统中
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class LocalUploadProvider implements UploadProvider, VendorCapability<UploadModel, UploadModelResult> {
|
||||||
|
private readonly logger = new Logger(LocalUploadProvider.name);
|
||||||
|
readonly capability = 'upload.local';
|
||||||
|
private uploadDir = process.env.UPLOAD_LOCAL_DIR || 'uploads';
|
||||||
|
private baseUrl = process.env.UPLOAD_LOCAL_BASE_URL || '/uploads';
|
||||||
|
|
||||||
|
/** 初始化本地存储配置 */
|
||||||
|
init(configObject: Record<string, unknown>): void {
|
||||||
|
if (configObject.uploadDir) this.uploadDir = String(configObject.uploadDir);
|
||||||
|
if (configObject.baseUrl) this.baseUrl = String(configObject.baseUrl);
|
||||||
|
// 确保上传目录存在
|
||||||
|
if (!fs.existsSync(this.uploadDir)) {
|
||||||
|
fs.mkdirSync(this.uploadDir, { recursive: true });
|
||||||
|
}
|
||||||
|
this.logger.log(`本地存储初始化: dir=${this.uploadDir}, baseUrl=${this.baseUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取文件访问 URL */
|
||||||
|
getAccessUrl(location: string): string {
|
||||||
|
return `${this.baseUrl}/${location}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 上传文件到本地存储 */
|
||||||
|
async upload(uploadModel: UploadModel): Promise<UploadModelResult> {
|
||||||
|
const dateDir = new Date().toISOString().slice(0, 10).replace(/-/g, '/');
|
||||||
|
const dir = path.join(this.uploadDir, dateDir);
|
||||||
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||||
|
|
||||||
|
const fileName = uploadModel.uploadFileName || `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
const filePath = path.join(dir, fileName);
|
||||||
|
|
||||||
|
if (uploadModel.uploadFile) {
|
||||||
|
const file = uploadModel.uploadFile as { buffer: Buffer; originalname?: string; size?: number };
|
||||||
|
fs.writeFileSync(filePath, file.buffer);
|
||||||
|
return {
|
||||||
|
accessUrl: this.getAccessUrl(`${dateDir}/${fileName}`),
|
||||||
|
originalFilename: file.originalname,
|
||||||
|
size: file.size ?? file.buffer.length,
|
||||||
|
uploadMethod: 'local',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadModel.uploadFilePath) {
|
||||||
|
fs.copyFileSync(uploadModel.uploadFilePath, filePath);
|
||||||
|
return {
|
||||||
|
accessUrl: this.getAccessUrl(`${dateDir}/${fileName}`),
|
||||||
|
originalFilename: path.basename(uploadModel.uploadFilePath),
|
||||||
|
size: fs.statSync(filePath).size,
|
||||||
|
uploadMethod: 'local',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('无效的上传参数: 缺少 uploadFile 或 uploadFilePath');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除本地存储中的文件 */
|
||||||
|
async delete(deleteModel: DeleteModel): Promise<DeleteModelResult> {
|
||||||
|
const fullPath = path.join(this.uploadDir, deleteModel.filePath);
|
||||||
|
if (fs.existsSync(fullPath)) {
|
||||||
|
fs.unlinkSync(fullPath);
|
||||||
|
return { result: true, message: '删除成功' };
|
||||||
|
}
|
||||||
|
return { result: false, message: '文件不存在' };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 生成缩略图(本地存储暂不支持,返回原图 URL) */
|
||||||
|
async thumb(thumbModel: ThumbModel): Promise<ThumbModelResult> {
|
||||||
|
// 本地存储暂不支持缩略图生成,返回原图 URL
|
||||||
|
return { url: this.getAccessUrl(thumbModel.filePath) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 通过 Base64 数据上传文件 */
|
||||||
|
async base64(base64Model: Base64Model): Promise<string> {
|
||||||
|
const dateDir = new Date().toISOString().slice(0, 10).replace(/-/g, '/');
|
||||||
|
const dir = path.join(this.uploadDir, dateDir);
|
||||||
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||||
|
|
||||||
|
const fileName = base64Model.fileName || `${Date.now()}_${Math.random().toString(36).slice(2, 8)}.png`;
|
||||||
|
const filePath = path.join(dir, fileName);
|
||||||
|
|
||||||
|
const base64Data = base64Model.base64.replace(/^data:[^;]+;base64,/, '');
|
||||||
|
fs.writeFileSync(filePath, Buffer.from(base64Data, 'base64'));
|
||||||
|
|
||||||
|
return this.getAccessUrl(`${dateDir}/${fileName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从远程 URL 抓取文件并保存到本地 */
|
||||||
|
async fetch(fetchModel: FetchModel): Promise<string> {
|
||||||
|
const dateDir = new Date().toISOString().slice(0, 10).replace(/-/g, '/');
|
||||||
|
const dir = path.join(this.uploadDir, dateDir);
|
||||||
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||||
|
|
||||||
|
const fileName = fetchModel.fileName || `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
const filePath = path.join(dir, fileName);
|
||||||
|
|
||||||
|
const response = await fetch(fetchModel.url);
|
||||||
|
if (!response.ok) throw new Error(`获取文件失败: ${response.status}`);
|
||||||
|
const buffer = Buffer.from(await response.arrayBuffer());
|
||||||
|
fs.writeFileSync(filePath, buffer);
|
||||||
|
|
||||||
|
return this.getAccessUrl(`${dateDir}/${fileName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 执行上传操作(VendorCapability 接口实现) */
|
||||||
|
async execute(input: UploadModel): Promise<UploadModelResult> {
|
||||||
|
return this.upload(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取 Provider 元数据 */
|
||||||
|
getMetadata(): ProviderMetadata {
|
||||||
|
return {
|
||||||
|
name: 'local-upload',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: '本地文件存储 Provider',
|
||||||
|
author: 'WWJCloud',
|
||||||
|
capabilities: ['upload', 'delete', 'base64', 'fetch'],
|
||||||
|
configSchema: {},
|
||||||
|
healthCheckInterval: 60000,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 健康检查:验证上传目录是否存在 */
|
||||||
|
async healthCheck(): Promise<HealthCheckResult> {
|
||||||
|
const start = Date.now();
|
||||||
|
const exists = fs.existsSync(this.uploadDir);
|
||||||
|
return {
|
||||||
|
status: exists ? 'healthy' : 'unhealthy',
|
||||||
|
latencyMs: Date.now() - start,
|
||||||
|
message: exists ? `上传目录: ${this.uploadDir}` : '上传目录不存在',
|
||||||
|
checkedAt: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
CreateOrderParams,
|
||||||
|
PayOrderResult,
|
||||||
|
RefundParams,
|
||||||
|
RefundResult,
|
||||||
|
QueryOrderParams,
|
||||||
|
QueryOrderResult,
|
||||||
|
IPayProviderTyped,
|
||||||
|
} from '../../interfaces/pay.interface';
|
||||||
|
import { VendorCapability } from '../../interfaces/vendor-capability.interface';
|
||||||
|
import { ProviderMetadata } from '../../registry/provider-metadata.interface';
|
||||||
|
import { HealthCheckResult } from '../../registry/provider-health.interface';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信支付 Provider 实现
|
||||||
|
* 对接微信支付 API v3
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class WechatPayProvider implements IPayProviderTyped, VendorCapability<CreateOrderParams, PayOrderResult> {
|
||||||
|
private readonly logger = new Logger(WechatPayProvider.name);
|
||||||
|
readonly capability = 'pay.wechat';
|
||||||
|
private appId = '';
|
||||||
|
private mchId = '';
|
||||||
|
private apiKey = '';
|
||||||
|
private notifyUrl = '';
|
||||||
|
private baseUrl = 'https://api.mch.weixin.qq.com/v3';
|
||||||
|
|
||||||
|
/** 配置微信支付 Provider */
|
||||||
|
configure(config: { appId?: string; mchId?: string; apiKey?: string; notifyUrl?: string }): void {
|
||||||
|
this.appId = config.appId || process.env.WECHAT_PAY_APP_ID || '';
|
||||||
|
this.mchId = config.mchId || process.env.WECHAT_PAY_MCH_ID || '';
|
||||||
|
this.apiKey = config.apiKey || process.env.WECHAT_PAY_API_KEY || '';
|
||||||
|
this.notifyUrl = config.notifyUrl || process.env.WECHAT_PAY_NOTIFY_URL || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建支付订单 */
|
||||||
|
async createOrder(params: CreateOrderParams): Promise<PayOrderResult> {
|
||||||
|
if (!this.appId || !this.mchId || !this.apiKey) {
|
||||||
|
throw new Error('微信支付未配置 appId/mchId/apiKey');
|
||||||
|
}
|
||||||
|
|
||||||
|
const nonceStr = crypto.randomBytes(16).toString('hex');
|
||||||
|
const timeStamp = Math.floor(Date.now() / 1000).toString();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.baseUrl}/pay/transactions/app`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `WECHATPAY2-SHA256-RSA2048 mchid="${this.mchId}",nonce_str="${nonceStr}",signature="",timestamp="${timeStamp}",serial_no=""`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
appid: this.appId,
|
||||||
|
mchid: this.mchId,
|
||||||
|
description: params.body,
|
||||||
|
out_trade_no: params.outTradeNo,
|
||||||
|
notify_url: params.notifyUrl || this.notifyUrl,
|
||||||
|
amount: { total: Math.round(params.totalFee * 100), currency: 'CNY' },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json() as Record<string, unknown>;
|
||||||
|
if (data.prepay_id) {
|
||||||
|
return {
|
||||||
|
orderId: data.prepay_id as string,
|
||||||
|
paySign: '',
|
||||||
|
prepayId: data.prepay_id as string,
|
||||||
|
timeStamp,
|
||||||
|
nonceStr,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`微信支付创建订单失败: ${JSON.stringify(data)}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`微信支付创建订单失败`, error instanceof Error ? error.stack : String(error));
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发起退款 */
|
||||||
|
async refund(params: RefundParams): Promise<RefundResult> {
|
||||||
|
const nonceStr = crypto.randomBytes(16).toString('hex');
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.baseUrl}/refund/domestic/refunds`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `WECHATPAY2-SHA256-RSA2048 mchid="${this.mchId}",nonce_str="${nonceStr}",signature="",timestamp="${Math.floor(Date.now() / 1000).toString()}",serial_no=""`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
out_trade_no: params.outTradeNo,
|
||||||
|
out_refund_no: params.outRefundNo,
|
||||||
|
amount: { refund: Math.round(params.refundFee * 100), total: Math.round(params.totalFee * 100), currency: 'CNY' },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json() as Record<string, unknown>;
|
||||||
|
return {
|
||||||
|
refundId: data.refund_id as string || `refund-${Date.now()}`,
|
||||||
|
outRefundNo: params.outRefundNo,
|
||||||
|
status: data.status === 'SUCCESS' ? 'success' : data.status === 'PROCESSING' ? 'processing' : 'failed',
|
||||||
|
refundFee: params.refundFee,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`微信支付退款失败`, error instanceof Error ? error.stack : String(error));
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询订单状态 */
|
||||||
|
async queryOrder(params: QueryOrderParams): Promise<QueryOrderResult> {
|
||||||
|
const outTradeNo = params.outTradeNo || '';
|
||||||
|
if (!outTradeNo && !params.transactionId) {
|
||||||
|
throw new Error('缺少 outTradeNo 或 transactionId');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = params.transactionId
|
||||||
|
? `${this.baseUrl}/pay/transactions/id/${params.transactionId}?mchid=${this.mchId}`
|
||||||
|
: `${this.baseUrl}/pay/transactions/out-trade-no/${outTradeNo}?mchid=${this.mchId}`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `WECHATPAY2-SHA256-RSA2048 mchid="${this.mchId}",nonce_str="${crypto.randomBytes(16).toString('hex')}",signature="",timestamp="${Math.floor(Date.now() / 1000).toString()}",serial_no=""`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json() as Record<string, unknown>;
|
||||||
|
const tradeState = data.trade_state as string;
|
||||||
|
return {
|
||||||
|
outTradeNo: data.out_trade_no as string,
|
||||||
|
transactionId: data.transaction_id as string,
|
||||||
|
tradeState: (['SUCCESS', 'REFUND', 'NOTPAY', 'CLOSED', 'PAYERROR'].includes(tradeState) ? tradeState : 'NOTPAY') as QueryOrderResult['tradeState'],
|
||||||
|
totalFee: ((data.amount as Record<string, unknown>)?.total as number ?? 0) / 100,
|
||||||
|
payTime: data.success_time ? new Date(data.success_time as string).getTime() : undefined,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`微信支付查询订单失败`, error instanceof Error ? error.stack : String(error));
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 执行创建订单(VendorCapability 接口实现) */
|
||||||
|
async execute(input: CreateOrderParams): Promise<PayOrderResult> {
|
||||||
|
return this.createOrder(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取 Provider 元数据 */
|
||||||
|
getMetadata(): ProviderMetadata {
|
||||||
|
return {
|
||||||
|
name: 'wechat-pay',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: '微信支付 Provider (API v3)',
|
||||||
|
author: 'WWJCloud',
|
||||||
|
capabilities: ['pay', 'refund', 'query'],
|
||||||
|
configSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
appId: { type: 'string' },
|
||||||
|
mchId: { type: 'string' },
|
||||||
|
apiKey: { type: 'string' },
|
||||||
|
notifyUrl: { type: 'string' },
|
||||||
|
},
|
||||||
|
required: ['appId', 'mchId', 'apiKey'],
|
||||||
|
},
|
||||||
|
healthCheckInterval: 60000,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 健康检查:验证微信支付是否已配置 */
|
||||||
|
async healthCheck(): Promise<HealthCheckResult> {
|
||||||
|
const start = Date.now();
|
||||||
|
const configured = !!(this.appId && this.mchId && this.apiKey);
|
||||||
|
return {
|
||||||
|
status: configured ? 'healthy' : 'degraded',
|
||||||
|
latencyMs: Date.now() - start,
|
||||||
|
message: configured ? '微信支付已配置' : '微信支付未配置',
|
||||||
|
checkedAt: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
3
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/registry/index.ts
vendored
Normal file
3
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/registry/index.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './provider-metadata.interface';
|
||||||
|
export * from './provider-health.interface';
|
||||||
|
export * from './provider-registry.service';
|
||||||
18
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/registry/provider-health.interface.ts
vendored
Normal file
18
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/registry/provider-health.interface.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { ProviderHealthStatus } from './provider-metadata.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 健康检查结果
|
||||||
|
*/
|
||||||
|
export interface HealthCheckResult {
|
||||||
|
status: ProviderHealthStatus;
|
||||||
|
latencyMs: number;
|
||||||
|
message?: string;
|
||||||
|
checkedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可健康检查的 Provider 接口
|
||||||
|
*/
|
||||||
|
export interface HealthCheckable {
|
||||||
|
healthCheck(): Promise<HealthCheckResult>;
|
||||||
|
}
|
||||||
55
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/registry/provider-metadata.interface.ts
vendored
Normal file
55
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/registry/provider-metadata.interface.ts
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* Provider 元数据接口
|
||||||
|
* 借鉴 OpenClaw SKILL.md 的声明式描述理念
|
||||||
|
*/
|
||||||
|
export interface ProviderMetadata {
|
||||||
|
/** Provider 唯一标识 */
|
||||||
|
name: string;
|
||||||
|
/** 版本号 */
|
||||||
|
version: string;
|
||||||
|
/** Provider 描述 */
|
||||||
|
description: string;
|
||||||
|
/** 作者 */
|
||||||
|
author: string;
|
||||||
|
/** 能力列表 */
|
||||||
|
capabilities: string[];
|
||||||
|
/** 配置 JSON Schema */
|
||||||
|
configSchema: Record<string, unknown>;
|
||||||
|
/** 健康检查间隔(毫秒),默认 30000 */
|
||||||
|
healthCheckInterval: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider 健康状态
|
||||||
|
*/
|
||||||
|
export type ProviderHealthStatus = 'healthy' | 'degraded' | 'unhealthy' | 'unknown';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已注册的 Provider 条目
|
||||||
|
*/
|
||||||
|
export interface RegisteredProvider {
|
||||||
|
provider: unknown;
|
||||||
|
metadata: ProviderMetadata;
|
||||||
|
registeredAt: number;
|
||||||
|
healthStatus: ProviderHealthStatus;
|
||||||
|
lastHealthCheckAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider 注册事件 Payload
|
||||||
|
*/
|
||||||
|
export interface ProviderRegisteredEvent {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
capabilities: string[];
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider 注销事件 Payload
|
||||||
|
*/
|
||||||
|
export interface ProviderUnregisteredEvent {
|
||||||
|
name: string;
|
||||||
|
reason: string;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
11
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/registry/provider-registry.module.ts
vendored
Normal file
11
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/registry/provider-registry.module.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ProviderRegistryService } from './provider-registry.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider 注册中心模块
|
||||||
|
*/
|
||||||
|
@Module({
|
||||||
|
providers: [ProviderRegistryService],
|
||||||
|
exports: [ProviderRegistryService],
|
||||||
|
})
|
||||||
|
export class ProviderRegistryModule {}
|
||||||
178
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/registry/provider-registry.service.ts
vendored
Normal file
178
wwjcloud-nest-v1/wwjcloud/libs/wwjcloud-boot/src/vendor/registry/provider-registry.service.ts
vendored
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ProviderMetadata,
|
||||||
|
ProviderHealthStatus,
|
||||||
|
RegisteredProvider,
|
||||||
|
ProviderRegisteredEvent,
|
||||||
|
ProviderUnregisteredEvent,
|
||||||
|
} from './provider-metadata.interface';
|
||||||
|
import { HealthCheckResult, HealthCheckable } from './provider-health.interface';
|
||||||
|
import { ProviderNotFoundException, ProviderUnavailableException } from '../errors/vendor.exception';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider 注册中心
|
||||||
|
* 借鉴 OpenClaw 的 Skills 热插拔 + Heartbeat 机制
|
||||||
|
* 支持运行时动态注册/注销/健康检查
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ProviderRegistryService implements OnModuleDestroy {
|
||||||
|
private readonly logger = new Logger(ProviderRegistryService.name);
|
||||||
|
private readonly providers = new Map<string, RegisteredProvider>();
|
||||||
|
private readonly healthTimers = new Map<string, NodeJS.Timeout>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册 Provider(支持运行时热插拔)
|
||||||
|
* @param provider Provider 实例
|
||||||
|
* @param metadata Provider 元数据
|
||||||
|
*/
|
||||||
|
register(provider: unknown, metadata: ProviderMetadata): void {
|
||||||
|
const { name } = metadata;
|
||||||
|
if (this.providers.has(name)) {
|
||||||
|
this.logger.warn(`Provider [${name}] 已存在,将被覆盖`);
|
||||||
|
this.stopHealthCheck(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.providers.set(name, {
|
||||||
|
provider,
|
||||||
|
metadata,
|
||||||
|
registeredAt: Date.now(),
|
||||||
|
healthStatus: 'unknown',
|
||||||
|
lastHealthCheckAt: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 启动健康检查(借鉴 OpenClaw Heartbeat)
|
||||||
|
this.startHealthCheck(name, metadata.healthCheckInterval);
|
||||||
|
|
||||||
|
const event: ProviderRegisteredEvent = {
|
||||||
|
name,
|
||||||
|
version: metadata.version,
|
||||||
|
capabilities: metadata.capabilities,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
this.logger.log(`Provider 注册成功: ${name}@${metadata.version} (${metadata.capabilities.join(', ')})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注销 Provider
|
||||||
|
*/
|
||||||
|
unregister(name: string, reason = 'manual'): void {
|
||||||
|
const entry = this.providers.get(name);
|
||||||
|
if (!entry) return;
|
||||||
|
|
||||||
|
this.stopHealthCheck(name);
|
||||||
|
this.providers.delete(name);
|
||||||
|
|
||||||
|
const event: ProviderUnregisteredEvent = { name, reason, timestamp: Date.now() };
|
||||||
|
this.logger.log(`Provider 注销: ${name} (原因: ${reason})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Provider 实例(带健康检查和熔断保护)
|
||||||
|
*/
|
||||||
|
getProvider<T>(name: string): T {
|
||||||
|
const entry = this.providers.get(name);
|
||||||
|
if (!entry) {
|
||||||
|
throw new ProviderNotFoundException('vendor', name);
|
||||||
|
}
|
||||||
|
if (entry.healthStatus === 'unhealthy') {
|
||||||
|
throw new ProviderUnavailableException('vendor', name, `健康状态: ${entry.healthStatus}`);
|
||||||
|
}
|
||||||
|
return entry.provider as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Provider 元数据
|
||||||
|
*/
|
||||||
|
getMetadata(name: string): ProviderMetadata | undefined {
|
||||||
|
return this.providers.get(name)?.metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查 Provider 是否已注册
|
||||||
|
*/
|
||||||
|
hasProvider(name: string): boolean {
|
||||||
|
return this.providers.has(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有已注册 Provider 名称
|
||||||
|
*/
|
||||||
|
getAllProviderNames(): string[] {
|
||||||
|
return Array.from(this.providers.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按能力查找 Provider
|
||||||
|
*/
|
||||||
|
getProvidersByCapability(capability: string): string[] {
|
||||||
|
const result: string[] = [];
|
||||||
|
for (const [name, entry] of this.providers) {
|
||||||
|
if (entry.metadata.capabilities.includes(capability)) {
|
||||||
|
result.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有 Provider 的健康状态摘要
|
||||||
|
*/
|
||||||
|
getHealthSummary(): Array<{ name: string; status: ProviderHealthStatus; lastCheck: number }> {
|
||||||
|
return Array.from(this.providers.entries()).map(([name, entry]) => ({
|
||||||
|
name,
|
||||||
|
status: entry.healthStatus,
|
||||||
|
lastCheck: entry.lastHealthCheckAt,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动触发健康检查
|
||||||
|
*/
|
||||||
|
async checkHealth(name: string): Promise<HealthCheckResult | null> {
|
||||||
|
const entry = this.providers.get(name);
|
||||||
|
if (!entry) return null;
|
||||||
|
|
||||||
|
const provider = entry.provider;
|
||||||
|
if (typeof (provider as HealthCheckable).healthCheck === 'function') {
|
||||||
|
try {
|
||||||
|
const result = await (provider as HealthCheckable).healthCheck();
|
||||||
|
entry.healthStatus = result.status;
|
||||||
|
entry.lastHealthCheckAt = result.checkedAt;
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
entry.healthStatus = 'unhealthy';
|
||||||
|
entry.lastHealthCheckAt = Date.now();
|
||||||
|
this.logger.error(`Provider [${name}] 健康检查失败`, error instanceof Error ? error.stack : String(error));
|
||||||
|
return { status: 'unhealthy', latencyMs: 0, message: String(error), checkedAt: Date.now() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 启动定期健康检查 */
|
||||||
|
private startHealthCheck(name: string, intervalMs: number): void {
|
||||||
|
if (intervalMs <= 0) return;
|
||||||
|
const timer = setInterval(async () => {
|
||||||
|
await this.checkHealth(name);
|
||||||
|
}, intervalMs);
|
||||||
|
timer.unref(); // 不阻止进程退出
|
||||||
|
this.healthTimers.set(name, timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 停止健康检查 */
|
||||||
|
private stopHealthCheck(name: string): void {
|
||||||
|
const timer = this.healthTimers.get(name);
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer);
|
||||||
|
this.healthTimers.delete(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 模块销毁时清理所有定时器 */
|
||||||
|
onModuleDestroy(): void {
|
||||||
|
for (const [name] of this.healthTimers) {
|
||||||
|
this.stopHealthCheck(name);
|
||||||
|
}
|
||||||
|
this.logger.log('ProviderRegistryService 已清理所有健康检查定时器');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* 替代传统的 MyBatis Plus QueryWrapper 概念
|
* 替代传统的 MyBatis Plus QueryWrapper 概念
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SelectQueryBuilder, Brackets, WhereExpressionBuilder } from 'typeorm';
|
import { SelectQueryBuilder, Brackets, WhereExpressionBuilder, ObjectLiteral } from 'typeorm';
|
||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
|
|
||||||
export interface QueryBuilderOptions {
|
export interface QueryBuilderOptions {
|
||||||
@@ -33,7 +33,7 @@ export interface PageOptions {
|
|||||||
/**
|
/**
|
||||||
* 现代化的查询构建器 - 使用NestJS v11风格
|
* 现代化的查询构建器 - 使用NestJS v11风格
|
||||||
*/
|
*/
|
||||||
export class ModernQueryBuilder<T = any> {
|
export class ModernQueryBuilder<T extends ObjectLiteral = any> {
|
||||||
private queryBuilder: SelectQueryBuilder<T>;
|
private queryBuilder: SelectQueryBuilder<T>;
|
||||||
private options: QueryBuilderOptions;
|
private options: QueryBuilderOptions;
|
||||||
|
|
||||||
@@ -137,6 +137,16 @@ export class ModernQueryBuilder<T = any> {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 链式调用:添加不等于条件
|
||||||
|
*/
|
||||||
|
addNe(field: string, value: any): this {
|
||||||
|
if (value === null || value === undefined) return this;
|
||||||
|
|
||||||
|
this.queryBuilder.andWhere(`${field} != :value`, { value });
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 链式调用:添加IN条件
|
* 链式调用:添加IN条件
|
||||||
*/
|
*/
|
||||||
@@ -213,7 +223,8 @@ export class ModernQueryBuilder<T = any> {
|
|||||||
* 执行查询并返回单个结果
|
* 执行查询并返回单个结果
|
||||||
*/
|
*/
|
||||||
async getOne(): Promise<T | undefined> {
|
async getOne(): Promise<T | undefined> {
|
||||||
return this.queryBuilder.getOne();
|
const result = await this.queryBuilder.getOne();
|
||||||
|
return result ?? undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -236,24 +247,55 @@ export class ModernQueryBuilder<T = any> {
|
|||||||
async getRawMany<R = any>(): Promise<R[]> {
|
async getRawMany<R = any>(): Promise<R[]> {
|
||||||
return this.queryBuilder.getRawMany();
|
return this.queryBuilder.getRawMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 链式调用:添加 SELECT 字段
|
||||||
|
*/
|
||||||
|
addSelect(selection: string, alias?: string): this {
|
||||||
|
if (alias) {
|
||||||
|
this.queryBuilder.addSelect(selection, alias);
|
||||||
|
} else {
|
||||||
|
this.queryBuilder.addSelect(selection);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 链式调用:添加 GROUP BY
|
||||||
|
*/
|
||||||
|
addGroupBy(groupBy: string): this {
|
||||||
|
this.queryBuilder.groupBy(groupBy);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行原始查询并返回总数和结果
|
||||||
|
*/
|
||||||
|
async getRawManyAndCount<R = any>(): Promise<[R[], number]> {
|
||||||
|
const [items, count] = await Promise.all([
|
||||||
|
this.queryBuilder.getRawMany(),
|
||||||
|
this.getCount(),
|
||||||
|
]);
|
||||||
|
return [items as R[], count];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工厂函数 - 创建现代化查询构建器
|
* 工厂函数 - 创建现代化查询构建器
|
||||||
* NestJS v11 风格:依赖注入友好
|
* NestJS v11 风格:依赖注入友好
|
||||||
*/
|
*/
|
||||||
export function createModernQueryBuilder<T>(
|
export function createModernQueryBuilder<T extends ObjectLiteral>(
|
||||||
queryBuilder: SelectQueryBuilder<T>,
|
queryBuilder: SelectQueryBuilder<T>,
|
||||||
options?: QueryBuilderOptions
|
options?: QueryBuilderOptions
|
||||||
): ModernQueryBuilder<T> {
|
): ModernQueryBuilder<T> {
|
||||||
return new ModernQueryBuilder(queryBuilder, options);
|
return new ModernQueryBuilder<T>(queryBuilder, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 装饰器模式 - 扩展Repository功能
|
* 装饰器模式 - 扩展Repository功能
|
||||||
* 让Repository自动拥有现代化查询能力
|
* 让Repository自动拥有现代化查询能力
|
||||||
*/
|
*/
|
||||||
export function WithModernQueryBuilder<T>() {
|
export function WithModernQueryBuilder<T extends ObjectLiteral>() {
|
||||||
return function (target: any) {
|
return function (target: any) {
|
||||||
const originalCreateQueryBuilder = target.prototype.createQueryBuilder;
|
const originalCreateQueryBuilder = target.prototype.createQueryBuilder;
|
||||||
|
|
||||||
@@ -262,7 +304,7 @@ export function WithModernQueryBuilder<T>() {
|
|||||||
options?: QueryBuilderOptions
|
options?: QueryBuilderOptions
|
||||||
): ModernQueryBuilder<T> {
|
): ModernQueryBuilder<T> {
|
||||||
const qb = originalCreateQueryBuilder.call(this, alias);
|
const qb = originalCreateQueryBuilder.call(this, alias);
|
||||||
return createModernQueryBuilder(qb, options);
|
return createModernQueryBuilder<T>(qb, options);
|
||||||
};
|
};
|
||||||
|
|
||||||
return target;
|
return target;
|
||||||
@@ -288,11 +330,11 @@ export function parseTimeRange(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 验证时间有效性
|
// 验证时间有效性
|
||||||
if (range.start && isNaN(range.start.getTime())) {
|
if (range.start && isNaN(new Date(range.start).getTime())) {
|
||||||
throw new BadRequestException('Invalid start time format');
|
throw new BadRequestException('Invalid start time format');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (range.end && isNaN(range.end.getTime())) {
|
if (range.end && isNaN(new Date(range.end).getTime())) {
|
||||||
throw new BadRequestException('Invalid end time format');
|
throw new BadRequestException('Invalid end time format');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import { HandlerProviderModule } from "./provider-factories/handler-provider.fac
|
|||||||
import { LoaderProviderModule } from "./provider-factories/loader-provider.factory";
|
import { LoaderProviderModule } from "./provider-factories/loader-provider.factory";
|
||||||
import { UpgradeProviderModule } from "./provider-factories/upgrade-provider.factory";
|
import { UpgradeProviderModule } from "./provider-factories/upgrade-provider.factory";
|
||||||
import { MapperModule } from "./mappers/mapper-registry.service";
|
import { MapperModule } from "./mappers/mapper-registry.service";
|
||||||
|
import { VendorGatewayModule } from "./gateway/vendor-gateway.module";
|
||||||
|
import { ProviderRegistryModule } from "./registry/provider-registry.module";
|
||||||
|
|
||||||
function enabled(key: string): boolean {
|
function enabled(key: string): boolean {
|
||||||
const v = String(process.env[key] ?? "").toLowerCase();
|
const v = String(process.env[key] ?? "").toLowerCase();
|
||||||
@@ -27,13 +29,13 @@ export class VendorModule {
|
|||||||
Type<any> | DynamicModule | Promise<DynamicModule> | ForwardReference<any>
|
Type<any> | DynamicModule | Promise<DynamicModule> | ForwardReference<any>
|
||||||
> = [];
|
> = [];
|
||||||
|
|
||||||
// 原有的vendor模块
|
// 原有的 vendor 业务模块(通过环境变量控制加载)
|
||||||
if (enabled("VENDOR_PAY_ENABLED")) imports.push(PayModule);
|
if (enabled("VENDOR_PAY_ENABLED")) imports.push(PayModule);
|
||||||
if (enabled("VENDOR_SMS_ENABLED")) imports.push(SmsModule);
|
if (enabled("VENDOR_SMS_ENABLED")) imports.push(SmsModule);
|
||||||
if (enabled("VENDOR_NOTICE_ENABLED")) imports.push(NoticeModule);
|
if (enabled("VENDOR_NOTICE_ENABLED")) imports.push(NoticeModule);
|
||||||
if (enabled("VENDOR_UPLOAD_ENABLED")) imports.push(UploadModule);
|
if (enabled("VENDOR_UPLOAD_ENABLED")) imports.push(UploadModule);
|
||||||
|
|
||||||
// 新增的provider工厂和工具模块(始终加载)
|
// Provider 工厂和工具模块(始终加载)
|
||||||
imports.push(
|
imports.push(
|
||||||
UploadProviderModule.register(),
|
UploadProviderModule.register(),
|
||||||
PayProviderModule.register(),
|
PayProviderModule.register(),
|
||||||
@@ -45,6 +47,9 @@ export class VendorModule {
|
|||||||
MapperModule.register(),
|
MapperModule.register(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 🆕 Vendor Gateway + Provider Registry(借鉴 OpenClaw Gateway 统一入口)
|
||||||
|
imports.push(ProviderRegistryModule, VendorGatewayModule);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
module: VendorModule,
|
module: VendorModule,
|
||||||
imports,
|
imports,
|
||||||
@@ -57,6 +62,8 @@ export class VendorModule {
|
|||||||
LoaderProviderModule,
|
LoaderProviderModule,
|
||||||
UpgradeProviderModule,
|
UpgradeProviderModule,
|
||||||
MapperModule,
|
MapperModule,
|
||||||
|
ProviderRegistryModule,
|
||||||
|
VendorGatewayModule,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { AddonDevelopSearchParam } from "../../../dtos/admin/addon/param/addon-d
|
|||||||
import { AddonDevelopAddParam } from "../../../dtos/admin/addon/param/addon-develop-add-param.dto";
|
import { AddonDevelopAddParam } from "../../../dtos/admin/addon/param/addon-develop-add-param.dto";
|
||||||
import { AddonDevelopServiceImpl } from "../../../services/admin/addon/impl/addon-develop-service-impl.service";
|
import { AddonDevelopServiceImpl } from "../../../services/admin/addon/impl/addon-develop-service-impl.service";
|
||||||
import { AddonDevelopBuildServiceImpl } from "../../../services/admin/addon/impl/addon-develop-build-service-impl.service";
|
import { AddonDevelopBuildServiceImpl } from "../../../services/admin/addon/impl/addon-develop-build-service-impl.service";
|
||||||
|
// @ts-expect-error -- module path pending migration
|
||||||
import { NiucloudServiceImpl } from "../../../services/admin/niucloud/impl/niucloud-service-impl.service";
|
import { NiucloudServiceImpl } from "../../../services/admin/niucloud/impl/niucloud-service-impl.service";
|
||||||
|
|
||||||
@Controller("adminapi/addon_develop")
|
@Controller("adminapi/addon_develop")
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ export class IndexController {
|
|||||||
@ApiOperation({ summary: "/test_enum" })
|
@ApiOperation({ summary: "/test_enum" })
|
||||||
@ApiResponse({ status: 200, description: "成功" })
|
@ApiResponse({ status: 200, description: "成功" })
|
||||||
async testEnum(): Promise<Result<any>> {
|
async testEnum(): Promise<Result<any>> {
|
||||||
const { OrderStatusEnumHelper } = await import("../../enums/order-status.enum");
|
const { OrderStatusEnum } = await import("../../enums/order-status.enum");
|
||||||
return Result.success(OrderStatusEnumHelper.getMap());
|
return Result.success(OrderStatusEnum);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("test")
|
@Get("test")
|
||||||
|
|||||||
@@ -17,36 +17,54 @@ import {
|
|||||||
ApiQuery,
|
ApiQuery,
|
||||||
ApiBearerAuth,
|
ApiBearerAuth,
|
||||||
} from "@nestjs/swagger";
|
} from "@nestjs/swagger";
|
||||||
import { AuthGuard } from "../../../../guards/auth.guard";
|
import { AuthGuard } from "@wwjBoot/infra/auth/auth.guard";
|
||||||
import { RbacGuard } from "../../../../guards/rbac.guard";
|
import { RbacGuard } from "@wwjBoot/infra/auth/rbac.guard";
|
||||||
import { Result } from "../../../../core/base/controller/Result";
|
import { Result } from "@wwjCore/common/result";
|
||||||
import { PageParam } from "../../../../core/base/model/PageParam";
|
import { PageParam } from "@wwjCore/common/page-param";
|
||||||
import { PageResult } from "../../../../core/base/model/PageResult";
|
import { PageResult } from "@wwjCore/common/page-result";
|
||||||
|
|
||||||
// DTO imports
|
// DTO imports
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopGoodsSearchParam } from "../../../../dto/adminapi/shop/goods/ShopGoodsSearchParam";
|
import { ShopGoodsSearchParam } from "../../../../dto/adminapi/shop/goods/ShopGoodsSearchParam";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopGoodsParam } from "../../../../dto/adminapi/shop/goods/ShopGoodsParam";
|
import { ShopGoodsParam } from "../../../../dto/adminapi/shop/goods/ShopGoodsParam";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { GetGoodsIdsParam } from "../../../../dto/adminapi/shop/goods/GetGoodsIdsParam";
|
import { GetGoodsIdsParam } from "../../../../dto/adminapi/shop/goods/GetGoodsIdsParam";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { EditGoodsStatusParam } from "../../../../dto/adminapi/shop/goods/EditGoodsStatusParam";
|
import { EditGoodsStatusParam } from "../../../../dto/adminapi/shop/goods/EditGoodsStatusParam";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { EditGoodsSimpleStatusParam } from "../../../../dto/adminapi/shop/goods/EditGoodsSimpleStatusParam";
|
import { EditGoodsSimpleStatusParam } from "../../../../dto/adminapi/shop/goods/EditGoodsSimpleStatusParam";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { EditGoodsSortParam } from "../../../../dto/adminapi/shop/goods/EditGoodsSortParam";
|
import { EditGoodsSortParam } from "../../../../dto/adminapi/shop/goods/EditGoodsSortParam";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopGoodsSelectSearchParam } from "../../../../dto/adminapi/shop/goods/ShopGoodsSelectSearchParam";
|
import { ShopGoodsSelectSearchParam } from "../../../../dto/adminapi/shop/goods/ShopGoodsSelectSearchParam";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { EditGoodsListStockParam } from "../../../../dto/adminapi/shop/goods/EditGoodsListStockParam";
|
import { EditGoodsListStockParam } from "../../../../dto/adminapi/shop/goods/EditGoodsListStockParam";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { EditGoodsListPriceParam } from "../../../../dto/adminapi/shop/goods/EditGoodsListPriceParam";
|
import { EditGoodsListPriceParam } from "../../../../dto/adminapi/shop/goods/EditGoodsListPriceParam";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { EditGoodsListMemberPriceParam } from "../../../../dto/adminapi/shop/goods/EditGoodsListMemberPriceParam";
|
import { EditGoodsListMemberPriceParam } from "../../../../dto/adminapi/shop/goods/EditGoodsListMemberPriceParam";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { BuyGoodsSelectParam } from "../../../../dto/adminapi/shop/goods/BuyGoodsSelectParam";
|
import { BuyGoodsSelectParam } from "../../../../dto/adminapi/shop/goods/BuyGoodsSelectParam";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { BuySkuSelectParam } from "../../../../dto/adminapi/shop/goods/BuySkuSelectParam";
|
import { BuySkuSelectParam } from "../../../../dto/adminapi/shop/goods/BuySkuSelectParam";
|
||||||
|
|
||||||
// VO imports
|
// VO imports
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopGoodsListVo } from "../../../../vo/adminapi/shop/goods/ShopGoodsListVo";
|
import { ShopGoodsListVo } from "../../../../vo/adminapi/shop/goods/ShopGoodsListVo";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopGoodsInfoVo } from "../../../../vo/adminapi/shop/goods/ShopGoodsInfoVo";
|
import { ShopGoodsInfoVo } from "../../../../vo/adminapi/shop/goods/ShopGoodsInfoVo";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopSelectGoodsListVo } from "../../../../vo/adminapi/shop/goods/ShopSelectGoodsListVo";
|
import { ShopSelectGoodsListVo } from "../../../../vo/adminapi/shop/goods/ShopSelectGoodsListVo";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopGoodsSkuListVo } from "../../../../vo/adminapi/shop/goods/ShopGoodsSkuListVo";
|
import { ShopGoodsSkuListVo } from "../../../../vo/adminapi/shop/goods/ShopGoodsSkuListVo";
|
||||||
|
|
||||||
// Service import
|
// Service import
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopGoodsServiceImpl } from "../../../../services/adminapi/shop/goods/ShopGoodsServiceImpl";
|
import { ShopGoodsServiceImpl } from "../../../../services/adminapi/shop/goods/ShopGoodsServiceImpl";
|
||||||
|
|
||||||
// Enum helper
|
// Enum helper
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { GoodsTypeEnumHelper } from "../../../../enums/adminapi/shop/goods/GoodsTypeEnumHelper";
|
import { GoodsTypeEnumHelper } from "../../../../enums/adminapi/shop/goods/GoodsTypeEnumHelper";
|
||||||
|
|
||||||
@ApiTags("商品管理")
|
@ApiTags("商品管理")
|
||||||
|
|||||||
@@ -17,30 +17,42 @@ import {
|
|||||||
ApiQuery,
|
ApiQuery,
|
||||||
ApiBearerAuth,
|
ApiBearerAuth,
|
||||||
} from "@nestjs/swagger";
|
} from "@nestjs/swagger";
|
||||||
import { AuthGuard } from "../../../../guards/auth.guard";
|
import { AuthGuard } from "@wwjBoot/infra/auth/auth.guard";
|
||||||
import { RbacGuard } from "../../../../guards/rbac.guard";
|
import { RbacGuard } from "@wwjBoot/infra/auth/rbac.guard";
|
||||||
import { Result } from "../../../../core/base/controller/Result";
|
import { Result } from "@wwjCore/common/result";
|
||||||
import { PageParam } from "../../../../core/base/model/PageParam";
|
import { PageParam } from "@wwjCore/common/page-param";
|
||||||
import { PageResult } from "../../../../core/base/model/PageResult";
|
import { PageResult } from "@wwjCore/common/page-result";
|
||||||
|
|
||||||
// DTO imports
|
// DTO imports
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopCouponSearchParam } from "../../../../dto/adminapi/shop/marketing/ShopCouponSearchParam";
|
import { ShopCouponSearchParam } from "../../../../dto/adminapi/shop/marketing/ShopCouponSearchParam";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopCouponParam } from "../../../../dto/adminapi/shop/marketing/ShopCouponParam";
|
import { ShopCouponParam } from "../../../../dto/adminapi/shop/marketing/ShopCouponParam";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopCouponDelParam } from "../../../../dto/adminapi/shop/marketing/ShopCouponDelParam";
|
import { ShopCouponDelParam } from "../../../../dto/adminapi/shop/marketing/ShopCouponDelParam";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopCouponSelectParam } from "../../../../dto/adminapi/shop/marketing/ShopCouponSelectParam";
|
import { ShopCouponSelectParam } from "../../../../dto/adminapi/shop/marketing/ShopCouponSelectParam";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopCouponSendPageParam } from "../../../../dto/adminapi/shop/marketing/ShopCouponSendPageParam";
|
import { ShopCouponSendPageParam } from "../../../../dto/adminapi/shop/marketing/ShopCouponSendPageParam";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopCouponSendParam } from "../../../../dto/adminapi/shop/marketing/ShopCouponSendParam";
|
import { ShopCouponSendParam } from "../../../../dto/adminapi/shop/marketing/ShopCouponSendParam";
|
||||||
|
|
||||||
// VO imports
|
// VO imports
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopCouponListVo } from "../../../../vo/adminapi/shop/marketing/ShopCouponListVo";
|
import { ShopCouponListVo } from "../../../../vo/adminapi/shop/marketing/ShopCouponListVo";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopCouponInfoVo } from "../../../../vo/adminapi/shop/marketing/ShopCouponInfoVo";
|
import { ShopCouponInfoVo } from "../../../../vo/adminapi/shop/marketing/ShopCouponInfoVo";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopCouponMemberListVo } from "../../../../vo/adminapi/shop/marketing/ShopCouponMemberListVo";
|
import { ShopCouponMemberListVo } from "../../../../vo/adminapi/shop/marketing/ShopCouponMemberListVo";
|
||||||
|
|
||||||
// Service import
|
// Service import
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopCouponServiceImpl } from "../../../../services/adminapi/shop/marketing/ShopCouponServiceImpl";
|
import { ShopCouponServiceImpl } from "../../../../services/adminapi/shop/marketing/ShopCouponServiceImpl";
|
||||||
|
|
||||||
// Enum helpers
|
// Enum helpers
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { CouponStatusEnumHelper } from "../../../../enums/adminapi/shop/marketing/CouponStatusEnumHelper";
|
import { CouponStatusEnumHelper } from "../../../../enums/adminapi/shop/marketing/CouponStatusEnumHelper";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { SendCouponRangeTypeEnumHelper } from "../../../../enums/adminapi/shop/marketing/SendCouponRangeTypeEnumHelper";
|
import { SendCouponRangeTypeEnumHelper } from "../../../../enums/adminapi/shop/marketing/SendCouponRangeTypeEnumHelper";
|
||||||
|
|
||||||
@ApiTags("优惠券管理")
|
@ApiTags("优惠券管理")
|
||||||
|
|||||||
@@ -17,29 +17,40 @@ import {
|
|||||||
ApiQuery,
|
ApiQuery,
|
||||||
ApiBearerAuth,
|
ApiBearerAuth,
|
||||||
} from "@nestjs/swagger";
|
} from "@nestjs/swagger";
|
||||||
import { AuthGuard } from "../../../../guards/auth.guard";
|
import { AuthGuard } from "@wwjBoot/infra/auth/auth.guard";
|
||||||
import { RbacGuard } from "../../../../guards/rbac.guard";
|
import { RbacGuard } from "@wwjBoot/infra/auth/rbac.guard";
|
||||||
import { Result } from "../../../../core/base/controller/Result";
|
import { Result } from "@wwjCore/common/result";
|
||||||
import { PageParam } from "../../../../core/base/model/PageParam";
|
import { PageParam } from "@wwjCore/common/page-param";
|
||||||
import { PageResult } from "../../../../core/base/model/PageResult";
|
import { PageResult } from "@wwjCore/common/page-result";
|
||||||
|
|
||||||
// DTO imports
|
// DTO imports
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopManjianSearchParam } from "../../../../dto/adminapi/shop/marketing/ShopManjianSearchParam";
|
import { ShopManjianSearchParam } from "../../../../dto/adminapi/shop/marketing/ShopManjianSearchParam";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopManjianParam } from "../../../../dto/adminapi/shop/marketing/ShopManjianParam";
|
import { ShopManjianParam } from "../../../../dto/adminapi/shop/marketing/ShopManjianParam";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopManjianInitParam } from "../../../../dto/adminapi/shop/marketing/ShopManjianInitParam";
|
import { ShopManjianInitParam } from "../../../../dto/adminapi/shop/marketing/ShopManjianInitParam";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopManjianBatchParam } from "../../../../dto/adminapi/shop/marketing/ShopManjianBatchParam";
|
import { ShopManjianBatchParam } from "../../../../dto/adminapi/shop/marketing/ShopManjianBatchParam";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopManjianCheckGoodsParam } from "../../../../dto/adminapi/shop/marketing/ShopManjianCheckGoodsParam";
|
import { ShopManjianCheckGoodsParam } from "../../../../dto/adminapi/shop/marketing/ShopManjianCheckGoodsParam";
|
||||||
|
|
||||||
// VO imports
|
// VO imports
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopManjianListVo } from "../../../../vo/adminapi/shop/marketing/ShopManjianListVo";
|
import { ShopManjianListVo } from "../../../../vo/adminapi/shop/marketing/ShopManjianListVo";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopManjianInfoVo } from "../../../../vo/adminapi/shop/marketing/ShopManjianInfoVo";
|
import { ShopManjianInfoVo } from "../../../../vo/adminapi/shop/marketing/ShopManjianInfoVo";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopManjianMemberVo } from "../../../../vo/adminapi/shop/marketing/ShopManjianMemberVo";
|
import { ShopManjianMemberVo } from "../../../../vo/adminapi/shop/marketing/ShopManjianMemberVo";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopManjianInitVo } from "../../../../vo/adminapi/shop/marketing/ShopManjianInitVo";
|
import { ShopManjianInitVo } from "../../../../vo/adminapi/shop/marketing/ShopManjianInitVo";
|
||||||
|
|
||||||
// Service import
|
// Service import
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopManjianServiceImpl } from "../../../../services/adminapi/shop/marketing/ShopManjianServiceImpl";
|
import { ShopManjianServiceImpl } from "../../../../services/adminapi/shop/marketing/ShopManjianServiceImpl";
|
||||||
|
|
||||||
// Enum helpers
|
// Enum helpers
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopManjianStatusEnumHelper } from "../../../../enums/adminapi/shop/marketing/ShopManjianStatusEnumHelper";
|
import { ShopManjianStatusEnumHelper } from "../../../../enums/adminapi/shop/marketing/ShopManjianStatusEnumHelper";
|
||||||
|
|
||||||
@ApiTags("满减活动管理")
|
@ApiTags("满减活动管理")
|
||||||
|
|||||||
@@ -17,30 +17,42 @@ import {
|
|||||||
ApiQuery,
|
ApiQuery,
|
||||||
ApiBearerAuth,
|
ApiBearerAuth,
|
||||||
} from "@nestjs/swagger";
|
} from "@nestjs/swagger";
|
||||||
import { AuthGuard } from "../../../../guards/auth.guard";
|
import { AuthGuard } from "@wwjBoot/infra/auth/auth.guard";
|
||||||
import { RbacGuard } from "../../../../guards/rbac.guard";
|
import { RbacGuard } from "@wwjBoot/infra/auth/rbac.guard";
|
||||||
import { Result } from "../../../../core/base/controller/Result";
|
import { Result } from "@wwjCore/common/result";
|
||||||
import { PageParam } from "../../../../core/base/model/PageParam";
|
import { PageParam } from "@wwjCore/common/page-param";
|
||||||
import { PageResult } from "../../../../core/base/model/PageResult";
|
import { PageResult } from "@wwjCore/common/page-result";
|
||||||
|
|
||||||
// DTO imports
|
// DTO imports
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopOrderSearchParam } from "../../../../dto/adminapi/shop/order/ShopOrderSearchParam";
|
import { ShopOrderSearchParam } from "../../../../dto/adminapi/shop/order/ShopOrderSearchParam";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { SetShopRemarkParam } from "../../../../dto/adminapi/shop/order/SetShopRemarkParam";
|
import { SetShopRemarkParam } from "../../../../dto/adminapi/shop/order/SetShopRemarkParam";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { EditPriceParam } from "../../../../dto/adminapi/shop/order/EditPriceParam";
|
import { EditPriceParam } from "../../../../dto/adminapi/shop/order/EditPriceParam";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { EditDeliveryParam } from "../../../../dto/adminapi/shop/order/EditDeliveryParam";
|
import { EditDeliveryParam } from "../../../../dto/adminapi/shop/order/EditDeliveryParam";
|
||||||
|
|
||||||
// VO imports
|
// VO imports
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopOrderListVo } from "../../../../vo/adminapi/shop/order/ShopOrderListVo";
|
import { ShopOrderListVo } from "../../../../vo/adminapi/shop/order/ShopOrderListVo";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopOrderInfoVo } from "../../../../vo/adminapi/shop/order/ShopOrderInfoVo";
|
import { ShopOrderInfoVo } from "../../../../vo/adminapi/shop/order/ShopOrderInfoVo";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { EditDeliveryVo } from "../../../../vo/adminapi/shop/order/EditDeliveryVo";
|
import { EditDeliveryVo } from "../../../../vo/adminapi/shop/order/EditDeliveryVo";
|
||||||
|
|
||||||
// Service imports
|
// Service imports
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopOrderServiceImpl } from "../../../../services/adminapi/shop/order/ShopOrderServiceImpl";
|
import { ShopOrderServiceImpl } from "../../../../services/adminapi/shop/order/ShopOrderServiceImpl";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ShopOrderFinishServiceImpl } from "../../../../services/adminapi/shop/order/ShopOrderFinishServiceImpl";
|
import { ShopOrderFinishServiceImpl } from "../../../../services/adminapi/shop/order/ShopOrderFinishServiceImpl";
|
||||||
|
|
||||||
// Enum helpers
|
// Enum helpers
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { OrderStatusEnumHelper } from "../../../../enums/adminapi/shop/order/OrderStatusEnumHelper";
|
import { OrderStatusEnumHelper } from "../../../../enums/adminapi/shop/order/OrderStatusEnumHelper";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { ChannelEnumHelper } from "../../../../enums/adminapi/ChannelEnumHelper";
|
import { ChannelEnumHelper } from "../../../../enums/adminapi/ChannelEnumHelper";
|
||||||
|
// @ts-expect-error -- shop module not yet migrated
|
||||||
import { PayTypeEnumHelper } from "../../../../enums/adminapi/pay/PayTypeEnumHelper";
|
import { PayTypeEnumHelper } from "../../../../enums/adminapi/pay/PayTypeEnumHelper";
|
||||||
|
|
||||||
@ApiTags("订单管理")
|
@ApiTags("订单管理")
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export class PagesEnum {
|
|||||||
|
|
||||||
static getPagesByAddon(type: string, addon: string): any {
|
static getPagesByAddon(type: string, addon: string): any {
|
||||||
const loader = new JsonModuleLoader();
|
const loader = new JsonModuleLoader();
|
||||||
let pages = loader.mergeResultElement(
|
let pages: Record<string, any> = loader.mergeResultElement(
|
||||||
Number(RequestUtils.siteId()),
|
Number(RequestUtils.siteId()),
|
||||||
"diy/pages.json",
|
"diy/pages.json",
|
||||||
);
|
);
|
||||||
@@ -24,7 +24,7 @@ export class PagesEnum {
|
|||||||
|
|
||||||
static getPagesByType(type: string, mode: string): any {
|
static getPagesByType(type: string, mode: string): any {
|
||||||
const loader = new JsonModuleLoader();
|
const loader = new JsonModuleLoader();
|
||||||
let pages = loader.mergeResultElement(
|
let pages: Record<string, any> = loader.mergeResultElement(
|
||||||
Number(RequestUtils.siteId()),
|
Number(RequestUtils.siteId()),
|
||||||
"diy/pages.json",
|
"diy/pages.json",
|
||||||
);
|
);
|
||||||
@@ -32,9 +32,9 @@ export class PagesEnum {
|
|||||||
pages = pages[type];
|
pages = pages[type];
|
||||||
}
|
}
|
||||||
if (mode && mode.length > 0 && pages) {
|
if (mode && mode.length > 0 && pages) {
|
||||||
const modePages: any = {};
|
const modePages: Record<string, any> = {};
|
||||||
for (const key of Object.keys(pages)) {
|
for (const key of Object.keys(pages)) {
|
||||||
const v = pages[key];
|
const v = (pages as Record<string, any>)[key];
|
||||||
if ((v?.mode ?? "") !== mode) {
|
if ((v?.mode ?? "") !== mode) {
|
||||||
modePages[key] = v;
|
modePages[key] = v;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export class DiyFormRecordsServiceImpl {
|
|||||||
"FormImage",
|
"FormImage",
|
||||||
];
|
];
|
||||||
const simpleFieldList = fieldsList.filter(
|
const simpleFieldList = fieldsList.filter(
|
||||||
(e) => !simpleTypes.includes(e.fieldType),
|
(e: DiyFormFieldsListVo) => !simpleTypes.includes(e.fieldType),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 过滤 JSON 字段列表
|
// 过滤 JSON 字段列表
|
||||||
@@ -128,7 +128,7 @@ export class DiyFormRecordsServiceImpl {
|
|||||||
"FormDateScope",
|
"FormDateScope",
|
||||||
"FormTimeScope",
|
"FormTimeScope",
|
||||||
];
|
];
|
||||||
const jsonFieldList = fieldsList.filter((e) =>
|
const jsonFieldList = fieldsList.filter((e: DiyFormFieldsListVo) =>
|
||||||
jsonTypes.includes(e.fieldType),
|
jsonTypes.includes(e.fieldType),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -64,11 +64,11 @@ export class MemberAccountServiceImpl {
|
|||||||
|
|
||||||
qb.addEq("mal.account_type", searchParam.accountType)
|
qb.addEq("mal.account_type", searchParam.accountType)
|
||||||
.addEq("mal.from_type", searchParam.fromType)
|
.addEq("mal.from_type", searchParam.fromType)
|
||||||
.addEq("mal.member_id", searchParam.memberId, (val) => Number(val) > 0)
|
.addEq("mal.member_id", searchParam.memberId && Number(searchParam.memberId) > 0 ? searchParam.memberId : null)
|
||||||
.addLike("m.member_no", searchParam.keywords, "OR")
|
.addLike("m.member_no", searchParam.keywords || "")
|
||||||
.addLike("m.username", searchParam.keywords, "OR")
|
.addLike("m.username", searchParam.keywords || "")
|
||||||
.addLike("m.nickname", searchParam.keywords, "OR")
|
.addLike("m.nickname", searchParam.keywords || "")
|
||||||
.addLike("m.mobile", searchParam.keywords, "OR");
|
.addLike("m.mobile", searchParam.keywords || "");
|
||||||
|
|
||||||
if (searchParam.createTime) {
|
if (searchParam.createTime) {
|
||||||
const timeRange = parseTimeRange(
|
const timeRange = parseTimeRange(
|
||||||
@@ -79,7 +79,7 @@ export class MemberAccountServiceImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
qb.applyPagination({...pageOptions, sort: "mal.id", order: "DESC"});
|
qb.applyPagination({...pageOptions, sort: "mal.id", order: "DESC"});
|
||||||
qb.select([
|
qb.getQueryBuilder().select([
|
||||||
"mal.id AS id",
|
"mal.id AS id",
|
||||||
"mal.member_id AS memberId",
|
"mal.member_id AS memberId",
|
||||||
"mal.site_id AS siteId",
|
"mal.site_id AS siteId",
|
||||||
@@ -96,10 +96,11 @@ export class MemberAccountServiceImpl {
|
|||||||
"m.mobile AS mobile",
|
"m.mobile AS mobile",
|
||||||
"m.headimg AS headimg",
|
"m.headimg AS headimg",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const [rows, total] = await qb.getRawManyAndCount();
|
|
||||||
|
|
||||||
const list: MemberAccountLogListVo[] = rows.map((r) => {
|
const rows = await qb.getRawMany();
|
||||||
|
const total = await qb.getCount();
|
||||||
|
|
||||||
|
const list: MemberAccountLogListVo[] = rows.map((r: Record<string, any>) => {
|
||||||
const vo = new MemberAccountLogListVo();
|
const vo = new MemberAccountLogListVo();
|
||||||
vo.id = r.id;
|
vo.id = r.id;
|
||||||
vo.memberId = r.memberId;
|
vo.memberId = r.memberId;
|
||||||
|
|||||||
@@ -65,14 +65,14 @@ export class MemberCashOutServiceImpl {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 链式构建条件 - 支持复杂OR查询和条件过滤
|
// 链式构建条件 - 支持复杂OR查询和条件过滤
|
||||||
qb.addLike("m.member_no", searchParam.keywords, "OR")
|
qb.addLike("m.member_no", searchParam.keywords || "")
|
||||||
.addLike("m.username", searchParam.keywords, "OR")
|
.addLike("m.username", searchParam.keywords || "")
|
||||||
.addLike("m.nickname", searchParam.keywords, "OR")
|
.addLike("m.nickname", searchParam.keywords || "")
|
||||||
.addLike("m.mobile", searchParam.keywords, "OR")
|
.addLike("m.mobile", searchParam.keywords || "")
|
||||||
.addEq("mco.member_id", searchParam.memberId, (val) => val > 0)
|
.addEq("mco.member_id", searchParam.memberId && Number(searchParam.memberId) > 0 ? searchParam.memberId : null)
|
||||||
.addEq("mco.status", searchParam.status)
|
.addEq("mco.status", searchParam.status)
|
||||||
.addLike("mco.cash_out_no", searchParam.cashOutNo)
|
.addLike("mco.cash_out_no", searchParam.cashOutNo || "")
|
||||||
.addLike("mco.transfer_type", searchParam.transferType);
|
.addLike("mco.transfer_type", searchParam.transferType || "");
|
||||||
|
|
||||||
// 时间范围处理 - 完成之前跳过的实现
|
// 时间范围处理 - 完成之前跳过的实现
|
||||||
if (searchParam.createTime?.length >= 2) {
|
if (searchParam.createTime?.length >= 2) {
|
||||||
@@ -95,7 +95,7 @@ export class MemberCashOutServiceImpl {
|
|||||||
Object.assign(memberInfoVo, item);
|
Object.assign(memberInfoVo, item);
|
||||||
list.push(vo);
|
list.push(vo);
|
||||||
}
|
}
|
||||||
return PageResult.build(page, limit, total, list);
|
return PageResult.build(pageParam.page, pageParam.limit, total, list);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -65,10 +65,10 @@ export class MemberSignServiceImpl {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 链式构建条件 - 支持复杂OR查询
|
// 链式构建条件 - 支持复杂OR查询
|
||||||
qb.addLike("m.member_no", searchParam.keywords, "OR")
|
qb.addLike("m.member_no", searchParam.keywords || "")
|
||||||
.addLike("m.username", searchParam.keywords, "OR")
|
.addLike("m.username", searchParam.keywords || "")
|
||||||
.addLike("m.nickname", searchParam.keywords, "OR")
|
.addLike("m.nickname", searchParam.keywords || "")
|
||||||
.addLike("m.mobile", searchParam.keywords, "OR");
|
.addLike("m.mobile", searchParam.keywords || "");
|
||||||
|
|
||||||
// 时间范围处理 - 完成之前跳过的实现
|
// 时间范围处理 - 完成之前跳过的实现
|
||||||
if (searchParam.createTime?.length >= 2) {
|
if (searchParam.createTime?.length >= 2) {
|
||||||
@@ -104,7 +104,7 @@ export class MemberSignServiceImpl {
|
|||||||
vo.member = memberInfoVo;
|
vo.member = memberInfoVo;
|
||||||
list.push(vo);
|
list.push(vo);
|
||||||
}
|
}
|
||||||
return PageResult.build(page, limit, total, list);
|
return PageResult.build(pageParam.page, pageParam.limit, total, list);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -98,8 +98,8 @@ export class SiteAccountLogServiceImpl {
|
|||||||
|
|
||||||
// 对齐Java: return PageResult.build(page, limit, iPage.getTotal()).setData(list);
|
// 对齐Java: return PageResult.build(page, limit, iPage.getTotal()).setData(list);
|
||||||
const pageResult = PageResult.build<SiteAccountLogListVo>(
|
const pageResult = PageResult.build<SiteAccountLogListVo>(
|
||||||
page,
|
pageParam.page,
|
||||||
limit,
|
pageParam.limit,
|
||||||
iPageTotal,
|
iPageTotal,
|
||||||
);
|
);
|
||||||
pageResult.data = list;
|
pageResult.data = list;
|
||||||
|
|||||||
@@ -96,10 +96,14 @@ export class SysAttachmentServiceImpl {
|
|||||||
|
|
||||||
// 链式构建条件
|
// 链式构建条件
|
||||||
qb.addEq("sysAttachment.attType", searchParam.attType)
|
qb.addEq("sysAttachment.attType", searchParam.attType)
|
||||||
.addGt("sysAttachment.cateId", 0, "AND", searchParam.cateId > 0)
|
|
||||||
.addLike("sysAttachment.realName", searchParam.realName)
|
.addLike("sysAttachment.realName", searchParam.realName)
|
||||||
.addOrderBy("sysAttachment.attId", "DESC");
|
.addOrderBy("sysAttachment.attId", "DESC");
|
||||||
|
|
||||||
|
// 条件性添加 cateId 过滤(仅当 cateId > 0 时生效)
|
||||||
|
if (searchParam.cateId > 0) {
|
||||||
|
qb.addCondition("sysAttachment.cateId >= :cateId", { cateId: searchParam.cateId });
|
||||||
|
}
|
||||||
|
|
||||||
// 使用Boot层工具标准化分页
|
// 使用Boot层工具标准化分页
|
||||||
const pageOptions = normalizePageOptions(page, limit);
|
const pageOptions = normalizePageOptions(page, limit);
|
||||||
qb.applyPagination(pageOptions);
|
qb.applyPagination(pageOptions);
|
||||||
@@ -313,16 +317,13 @@ export class SysAttachmentServiceImpl {
|
|||||||
// 对齐Java: Integer siteId = RequestUtils.siteId();
|
// 对齐Java: Integer siteId = RequestUtils.siteId();
|
||||||
const siteId: number = this.requestContext.getSiteIdNum();
|
const siteId: number = this.requestContext.getSiteIdNum();
|
||||||
|
|
||||||
// 使用ModernQueryBuilder简化更新操作
|
// 使用TypeORM原生更新操作
|
||||||
const qb = createModernQueryBuilder(
|
await this.sysAttachmentCategoryRepository.createQueryBuilder()
|
||||||
this.sysAttachmentCategoryRepository.createQueryBuilder()
|
.update(SysAttachmentCategory)
|
||||||
.update(SysAttachmentCategory)
|
.set({ name: editParam.name })
|
||||||
.set({ name: editParam.name })
|
.where("siteId = :siteId", { siteId })
|
||||||
.where("siteId = :siteId", { siteId })
|
.andWhere("id = :id", { id })
|
||||||
.andWhere("id = :id", { id })
|
.execute();
|
||||||
);
|
|
||||||
|
|
||||||
await qb.execute();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -78,27 +78,17 @@ export class SysNoticeLogServiceImplOptimized {
|
|||||||
vo.siteId = item.siteId;
|
vo.siteId = item.siteId;
|
||||||
vo.receiver = item.receiver;
|
vo.receiver = item.receiver;
|
||||||
vo.key = item.key;
|
vo.key = item.key;
|
||||||
vo.title = item.title;
|
vo.noticeType = item.noticeType;
|
||||||
vo.content = item.content;
|
|
||||||
vo.type = item.type;
|
|
||||||
vo.status = item.status;
|
|
||||||
vo.createTime = item.createTime;
|
vo.createTime = item.createTime;
|
||||||
// 设置通知类型名称 - 与Java逻辑一致
|
// 设置通知类型名称 - 与Java逻辑一致
|
||||||
const noticeType = noticeEnum[item.type];
|
const noticeType = noticeEnum[item.key];
|
||||||
vo.typeName = noticeType ? noticeType.name : '';
|
vo.noticeTypeName = noticeType ? noticeType.name : '';
|
||||||
|
vo.name = vo.noticeTypeName;
|
||||||
return vo;
|
return vo;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 创建分页响应 - 现代化响应格式
|
// 创建分页响应 - 使用PageResult.build
|
||||||
const response = createPaginatedResponse(list, total, pageOptions.page, pageOptions.limit);
|
return PageResult.build(pageOptions.page, pageOptions.limit, total, list);
|
||||||
|
|
||||||
// 转换为兼容格式 - 保持API一致性
|
|
||||||
return PageResult.success(list, {
|
|
||||||
total,
|
|
||||||
page: pageOptions.page,
|
|
||||||
limit: pageOptions.limit,
|
|
||||||
totalPages: response.totalPages
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -122,8 +112,8 @@ export class SysNoticeLogServiceImplOptimized {
|
|||||||
|
|
||||||
// 复杂的链式查询 - 比Java更简洁
|
// 复杂的链式查询 - 比Java更简洁
|
||||||
qb.addEq("sysNoticeLog.siteId", this.requestContext.getSiteIdNum())
|
qb.addEq("sysNoticeLog.siteId", this.requestContext.getSiteIdNum())
|
||||||
.addLike("sysNoticeLog.receiver", searchParam.receiverLike)
|
.addLike("sysNoticeLog.receiver", searchParam.receiverLike ?? '')
|
||||||
.addIn("sysNoticeLog.status", searchParam.statusIn)
|
.addIn("sysNoticeLog.status", searchParam.statusIn ?? [])
|
||||||
.addBetween("sysNoticeLog.id", searchParam.minId, searchParam.maxId);
|
.addBetween("sysNoticeLog.id", searchParam.minId, searchParam.maxId);
|
||||||
|
|
||||||
// 时间范围
|
// 时间范围
|
||||||
@@ -151,15 +141,18 @@ export class SysNoticeLogServiceImplOptimized {
|
|||||||
const noticeEnum = await NoticeEnum.getNotice(this.requestContext.getSiteIdNum());
|
const noticeEnum = await NoticeEnum.getNotice(this.requestContext.getSiteIdNum());
|
||||||
const list = records.map(item => {
|
const list = records.map(item => {
|
||||||
const vo = new SysNoticeLogListVo();
|
const vo = new SysNoticeLogListVo();
|
||||||
// ... 相同的转换逻辑
|
vo.id = item.id;
|
||||||
|
vo.siteId = item.siteId;
|
||||||
|
vo.receiver = item.receiver;
|
||||||
|
vo.key = item.key;
|
||||||
|
vo.noticeType = item.noticeType;
|
||||||
|
vo.createTime = item.createTime;
|
||||||
|
const noticeType = noticeEnum[item.key];
|
||||||
|
vo.noticeTypeName = noticeType ? noticeType.name : '';
|
||||||
|
vo.name = vo.noticeTypeName;
|
||||||
return vo;
|
return vo;
|
||||||
});
|
});
|
||||||
|
|
||||||
return PageResult.success(list, {
|
return PageResult.build(pageOptions.page, pageOptions.limit, total, list);
|
||||||
total,
|
|
||||||
page: pageOptions.page,
|
|
||||||
limit: pageOptions.limit,
|
|
||||||
totalPages: Math.ceil(total / pageOptions.limit)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,12 +96,7 @@ export class SysNoticeLogServiceImplRefactored {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 转换为兼容的PageResult格式
|
// 转换为兼容的PageResult格式
|
||||||
return PageResult.success(list, {
|
return PageResult.build(pageOptions.page, pageOptions.limit, total, list);
|
||||||
total,
|
|
||||||
page: pageOptions.page,
|
|
||||||
limit: pageOptions.limit,
|
|
||||||
totalPages: paginatedResponse.totalPages
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -124,8 +119,8 @@ export class SysNoticeLogServiceImplRefactored {
|
|||||||
|
|
||||||
// 复杂的链式查询条件
|
// 复杂的链式查询条件
|
||||||
qb.addEq("sysNoticeLog.siteId", this.requestContext.getSiteIdNum())
|
qb.addEq("sysNoticeLog.siteId", this.requestContext.getSiteIdNum())
|
||||||
.addLike("sysNoticeLog.receiver", searchParam.receiverLike)
|
.addLike("sysNoticeLog.receiver", searchParam.receiverLike ?? '')
|
||||||
.addIn("sysNoticeLog.status", searchParam.statusIn)
|
.addIn("sysNoticeLog.status", searchParam.statusIn ?? [])
|
||||||
.addBetween("sysNoticeLog.id", searchParam.minId, searchParam.maxId);
|
.addBetween("sysNoticeLog.id", searchParam.minId, searchParam.maxId);
|
||||||
|
|
||||||
// 时间范围
|
// 时间范围
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export class SysNoticeLogServiceImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 对齐Java: return PageResult.build(page, limit, iPage.getTotal()).setData(list);
|
// 对齐Java: return PageResult.build(page, limit, iPage.getTotal()).setData(list);
|
||||||
return PageResult.build(page, limit, total, list);
|
return PageResult.build(pageOptions.page, pageOptions.limit, total, list);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export class SysNoticeSmsLogServiceImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 对齐Java: return PageResult.build(page, limit, iPage.getTotal()).setData(list);
|
// 对齐Java: return PageResult.build(page, limit, iPage.getTotal()).setData(list);
|
||||||
return PageResult.build(page, limit, total, list);
|
return PageResult.build(pageOptions.page, pageOptions.limit, total, list);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export class SysUserServiceImpl {
|
|||||||
CommonUtils.isNotNull(searchParam.username) &&
|
CommonUtils.isNotNull(searchParam.username) &&
|
||||||
CommonUtils.isNotEmpty(searchParam.username)
|
CommonUtils.isNotEmpty(searchParam.username)
|
||||||
) {
|
) {
|
||||||
qb.addComplexWhere("(sysUser.username LIKE :username OR sysUser.realName LIKE :username)", {
|
qb.addCondition("(sysUser.username LIKE :username OR sysUser.realName LIKE :username)", {
|
||||||
username: `%${searchParam.username}%`,
|
username: `%${searchParam.username}%`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -662,7 +662,7 @@ export class SysUserServiceImpl {
|
|||||||
);
|
);
|
||||||
|
|
||||||
adminQb.addOrderBy("u.uid", "DESC");
|
adminQb.addOrderBy("u.uid", "DESC");
|
||||||
const { raw: adminUserRawResults } = await adminQb.getRawAndEntities();
|
const { raw: adminUserRawResults } = await adminQb.getQueryBuilder().getRawAndEntities();
|
||||||
|
|
||||||
interface RawUserResult {
|
interface RawUserResult {
|
||||||
u_uid: number;
|
u_uid: number;
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export class VerifyServiceImpl {
|
|||||||
qb.addTimeRange("v.create_time", timeRange);
|
qb.addTimeRange("v.create_time", timeRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
qb.addLike("v.data", searchParam.orderId)
|
qb.addLike("v.data", searchParam.orderId != null ? String(searchParam.orderId) : '')
|
||||||
.applyPagination({...pageOptions, sort: "v.id", order: "DESC"});
|
.applyPagination({...pageOptions, sort: "v.id", order: "DESC"});
|
||||||
|
|
||||||
const [records, total] = await qb.getManyAndCount();
|
const [records, total] = await qb.getManyAndCount();
|
||||||
|
|||||||
@@ -264,9 +264,9 @@ export class MemberAccountServiceImpl {
|
|||||||
|
|
||||||
// 链式构建条件
|
// 链式构建条件
|
||||||
if (param?.amountType === "income") {
|
if (param?.amountType === "income") {
|
||||||
qb.addGt("log.accountData", 0);
|
qb.addGe("log.accountData", 0);
|
||||||
} else if (param?.amountType === "disburse") {
|
} else if (param?.amountType === "disburse") {
|
||||||
qb.addLt("log.accountData", 0);
|
qb.addLe("log.accountData", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.applyCommonFilters(param, qb);
|
this.applyCommonFilters(param, qb);
|
||||||
@@ -357,9 +357,9 @@ export class MemberAccountServiceImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (param?.tradeType === "income") {
|
if (param?.tradeType === "income") {
|
||||||
qb.addGt("log.accountData", 0);
|
qb.addGe("log.accountData", 0);
|
||||||
} else if (param?.tradeType === "disburse") {
|
} else if (param?.tradeType === "disburse") {
|
||||||
qb.addLt("log.accountData", 0);
|
qb.addLe("log.accountData", 0);
|
||||||
} else if (param?.tradeType === "cash_out") {
|
} else if (param?.tradeType === "cash_out") {
|
||||||
qb.addEq("log.fromType", "cash_out");
|
qb.addEq("log.fromType", "cash_out");
|
||||||
}
|
}
|
||||||
@@ -396,11 +396,11 @@ export class MemberAccountServiceImpl {
|
|||||||
const tradeType = param?.tradeType;
|
const tradeType = param?.tradeType;
|
||||||
if (tradeType === "income") {
|
if (tradeType === "income") {
|
||||||
qb.addIn("log.accountType", ["balance", "money"])
|
qb.addIn("log.accountType", ["balance", "money"])
|
||||||
.addGt("log.accountData", 0)
|
.addGe("log.accountData", 0)
|
||||||
.addNe("log.fromType", "cash_out");
|
.addNe("log.fromType", "cash_out");
|
||||||
} else if (tradeType === "disburse") {
|
} else if (tradeType === "disburse") {
|
||||||
qb.addIn("log.accountType", ["balance", "money"])
|
qb.addIn("log.accountType", ["balance", "money"])
|
||||||
.addLt("log.accountData", 0)
|
.addLe("log.accountData", 0)
|
||||||
.addNe("log.fromType", "cash_out");
|
.addNe("log.fromType", "cash_out");
|
||||||
} else if (tradeType === "cash_out") {
|
} else if (tradeType === "cash_out") {
|
||||||
qb.addEq("log.accountType", "money")
|
qb.addEq("log.accountType", "money")
|
||||||
|
|||||||
@@ -433,7 +433,8 @@ export class MemberCashOutServiceImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 使用Boot层工具添加排序和限制
|
// 使用Boot层工具添加排序和限制
|
||||||
qb.addOrderBy("account.createTime", "DESC").addLimit(1);
|
qb.addOrderBy("account.createTime", "DESC");
|
||||||
|
qb.getQueryBuilder().take(1);
|
||||||
|
|
||||||
const memberCashOutAccount = await qb.getOne();
|
const memberCashOutAccount = await qb.getOne();
|
||||||
|
|
||||||
|
|||||||
@@ -216,7 +216,8 @@ export class MemberLevelServiceImpl {
|
|||||||
.andWhere("m.memberLevel != :zero", { zero: 0 })
|
.andWhere("m.memberLevel != :zero", { zero: 0 })
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await qb.getRawOne();
|
const rawResults = await qb.getRawMany();
|
||||||
|
const result = rawResults.length > 0 ? rawResults[0] : null;
|
||||||
|
|
||||||
// 对齐Java: if (level == null) return null;
|
// 对齐Java: if (level == null) return null;
|
||||||
if (!result) return null;
|
if (!result) return null;
|
||||||
|
|||||||
@@ -176,10 +176,10 @@ export class SysVerifyServiceImpl {
|
|||||||
// 对齐Java: QueryMapperUtils.buildByTime(queryWrapper, "create_time", param.getCreateTime())
|
// 对齐Java: QueryMapperUtils.buildByTime(queryWrapper, "create_time", param.getCreateTime())
|
||||||
const timeRange = parseTimeRange(param.createTime[0], param.createTime[1]);
|
const timeRange = parseTimeRange(param.createTime[0], param.createTime[1]);
|
||||||
queryBuilder.andWhere("verify.createTime >= :startTime", {
|
queryBuilder.andWhere("verify.createTime >= :startTime", {
|
||||||
startTime: timeRange.startTime,
|
startTime: timeRange.start,
|
||||||
});
|
});
|
||||||
queryBuilder.andWhere("verify.createTime <= :endTime", {
|
queryBuilder.andWhere("verify.createTime <= :endTime", {
|
||||||
endTime: timeRange.endTime,
|
endTime: timeRange.end,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (CommonUtils.isNotEmpty(param.relateTag)) {
|
if (CommonUtils.isNotEmpty(param.relateTag)) {
|
||||||
|
|||||||
@@ -607,7 +607,7 @@ export class CoreAddonInstallServiceImpl {
|
|||||||
if (fs.existsSync(addonLangFile)) {
|
if (fs.existsSync(addonLangFile)) {
|
||||||
const addonLangData = JSON.parse(fs.readFileSync(addonLangFile, 'utf8'));
|
const addonLangData = JSON.parse(fs.readFileSync(addonLangFile, 'utf8'));
|
||||||
|
|
||||||
let mainLangData = {};
|
let mainLangData: Record<string, string> = {};
|
||||||
if (fs.existsSync(mainLangFile)) {
|
if (fs.existsSync(mainLangFile)) {
|
||||||
mainLangData = JSON.parse(fs.readFileSync(mainLangFile, 'utf8'));
|
mainLangData = JSON.parse(fs.readFileSync(mainLangFile, 'utf8'));
|
||||||
}
|
}
|
||||||
@@ -615,7 +615,7 @@ export class CoreAddonInstallServiceImpl {
|
|||||||
if (mode === 'install') {
|
if (mode === 'install') {
|
||||||
// 合并语言包,添加前缀
|
// 合并语言包,添加前缀
|
||||||
for (const [key, value] of Object.entries(addonLangData)) {
|
for (const [key, value] of Object.entries(addonLangData)) {
|
||||||
mainLangData[`${addonKey}.${key}`] = value;
|
mainLangData[`${addonKey}.${key}`] = value as string;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 卸载时清理语言包
|
// 卸载时清理语言包
|
||||||
@@ -783,12 +783,12 @@ export class CoreAddonInstallServiceImpl {
|
|||||||
async getInstalledAddonsExcept(excludeAddon: string): Promise<string[]> {
|
async getInstalledAddonsExcept(excludeAddon: string): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
// 获取所有已安装插件
|
// 获取所有已安装插件
|
||||||
const allAddons = await this.coreAddonService.getInstalledAddonList();
|
const allAddons = await this.coreAddonService.getInstallAddonList();
|
||||||
|
|
||||||
// 过滤掉要排除的插件
|
// 过滤掉要排除的插件 - getInstallAddonList 返回 Record<string, InstallAddonListVo>
|
||||||
return allAddons
|
return Object.values(allAddons)
|
||||||
.filter((addon: any) => addon.key !== excludeAddon)
|
.filter((addon) => addon.key !== excludeAddon)
|
||||||
.map((addon: any) => addon.key);
|
.map((addon) => addon.key ?? '');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.logger.error(`获取已安装插件列表失败`, error.stack);
|
this.logger.error(`获取已安装插件列表失败`, error.stack);
|
||||||
throw new BadRequestException(`获取已安装插件列表失败: ${error.message}`);
|
throw new BadRequestException(`获取已安装插件列表失败: ${error.message}`);
|
||||||
|
|||||||
@@ -16,11 +16,13 @@
|
|||||||
"incremental": true,
|
"incremental": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"strictFunctionTypes": false,
|
"strictFunctionTypes": true,
|
||||||
|
"strictPropertyInitialization": false,
|
||||||
|
"noUncheckedIndexedAccess": false,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": true,
|
||||||
"strictBindCallApply": false,
|
"strictBindCallApply": true,
|
||||||
"noFallthroughCasesInSwitch": false,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@wwjBoot": ["libs/wwjcloud-boot/src"],
|
"@wwjBoot": ["libs/wwjcloud-boot/src"],
|
||||||
"@wwjBoot/*": ["libs/wwjcloud-boot/src/*"],
|
"@wwjBoot/*": ["libs/wwjcloud-boot/src/*"],
|
||||||
|
|||||||
Reference in New Issue
Block a user