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:
wanwu
2026-04-11 22:39:33 +08:00
parent e53d2a4a3f
commit 57034138ca
90 changed files with 4661 additions and 140 deletions

View File

@@ -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',
},
},
); );

View File

@@ -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'),
};
}
}

View File

@@ -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 {}

View File

@@ -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[]) || [],
};
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,197 @@
/**
* WWJCloud v1 框架规范知识库
*
* 将 4 份规范文件结构化为 AI 可消费的知识格式,
* 供代码生成 Skills 查询和使用。
*
* 知识来源:
* - common-layer-standards.mdCommon 层模块化设计标准)
* - development_constraints.md开发约束规范
* - nestjs_file_generation_standards.mdNestJS 文件生成标准)
* - 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;

View File

@@ -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'],
},
},
];

View File

@@ -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';

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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,

View File

@@ -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 {}

View File

@@ -0,0 +1,3 @@
export * from './short-term-memory.service';
export * from './long-term-memory.service';
export * from './ai-memory.module';

View File

@@ -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);
}
}

View File

@@ -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}`);
}
}
}

View File

@@ -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 {
// 忽略解析错误
}
}
}
}
}

View File

@@ -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 {
// 忽略解析错误
}
}
}
}
}

View File

@@ -0,0 +1,4 @@
export * from './llm-provider.interface';
export * from './llm-provider.factory';
export * from './impls/openai.provider';
export * from './impls/ollama.provider';

View File

@@ -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();
}
}

View File

@@ -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>;
}

View File

@@ -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],
};
}
}

View File

@@ -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;
/** 人工审批请求 IDstatus=pending_human 时) */
requestId?: string;
}

View File

@@ -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));
}
}
}

View File

@@ -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 {}

View File

@@ -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';

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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);
} }

View File

@@ -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 {}

View File

@@ -0,0 +1,4 @@
export * from './skill.interface';
export * from './skill-registry.service';
export * from './skill-executor.service';
export * from './ai-skills.module';

View File

@@ -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();
}
}

View File

@@ -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),
};
}
}
}

View File

@@ -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>;
}

View File

@@ -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 RuntimeReAct 循环 + 循环检测 + 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 {}

View File

@@ -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";

View File

@@ -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(),
}, },
); );

View File

@@ -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;

View File

@@ -0,0 +1,2 @@
export * from './vendor.exception';
export * from './vendor-error.filter';

View 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);
}
}

View 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}` : ''}`);
}
}

View File

@@ -0,0 +1,3 @@
export * from './vendor-gateway.service';
export * from './vendor-gateway.controller';
export * from './vendor-gateway.module';

View 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);
}
}

View 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 {}

View 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}`;
}
}

View 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';

View 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>;
}

View 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>;
}

View 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>;
}

View 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>;
}

View File

@@ -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;
}

View File

@@ -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(),
};
}
}

View File

@@ -0,0 +1,3 @@
export * from './local-upload.provider';
export * from './aliyun-sms.provider';
export * from './wechat-pay.provider';

View File

@@ -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(),
};
}
}

View File

@@ -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(),
};
}
}

View File

@@ -0,0 +1,3 @@
export * from './provider-metadata.interface';
export * from './provider-health.interface';
export * from './provider-registry.service';

View 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>;
}

View 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;
}

View 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 {}

View 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 已清理所有健康检查定时器');
}
}

View File

@@ -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');
} }

View File

@@ -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,
], ],
}; };
} }

View File

@@ -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")

View File

@@ -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")

View File

@@ -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("商品管理")

View File

@@ -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("优惠券管理")

View File

@@ -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("满减活动管理")

View File

@@ -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("订单管理")

View File

@@ -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;
} }

View File

@@ -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),
); );

View File

@@ -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;

View File

@@ -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);
} }
/** /**

View File

@@ -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);
} }
/** /**

View File

@@ -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;

View File

@@ -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();
} }
/** /**

View File

@@ -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)
});
} }
} }

View File

@@ -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);
// 时间范围 // 时间范围

View File

@@ -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);
} }
/** /**

View File

@@ -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);
} }
/** /**

View File

@@ -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;

View File

@@ -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();

View File

@@ -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")

View File

@@ -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();

View File

@@ -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;

View File

@@ -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)) {

View File

@@ -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}`);

View File

@@ -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/*"],