feat: 添加质量门禁工具链 + 类型安全强化

- 新增 quality-gate.sh 质量门禁脚本(tsc/eslint/build/any 计数)
- ESLint 新增代码目录 any 规则升级为 error(generator/runtime/skills/memory/providers/vendor)
- 修复新增代码中 30+ 处 any 类型(unknown 替代、具体类型定义)
- 新增 9 个 NestJS 自定义参数装饰器(@CurrentUser/@SiteId/@MemberId/@ReqId/@CurrentLang/@CurrentChannel/@ClientIp/@UserAgent/@SiteIdStr)
- RequestContextService 导出 RequestContextStore 接口和 REQUEST_CONTEXT_KEY 常量
- 请求中间件绑定上下文到 req 对象供装饰器使用
- 配置 husky + lint-staged pre-commit hook
- 新增 npm scripts: quality-gate / quality-gate:tsc / quality-gate:lint / quality-gate:build / quality-gate:any
- 新增代码目录 ESLint: 0 errors, 37 warnings
- tsc --noEmit: 0 errors
This commit is contained in:
wanwu
2026-04-13 14:07:36 +08:00
parent 45bdc7ceb2
commit 26c9cea362
21 changed files with 713 additions and 84 deletions

View File

@@ -0,0 +1 @@
npm test

View File

@@ -3,5 +3,8 @@
"@types/node": "^24.10.0",
"glob": "^11.0.3",
"typescript": "^5.9.3"
},
"scripts": {
"prepare": "husky"
}
}

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# 质量门禁 pre-commit hook
# 仅对暂存区的 TypeScript 文件执行 lint-staged
npx lint-staged

View File

@@ -108,6 +108,50 @@ export default tseslint.config(
'no-empty': 'warn',
},
},
// ========================================================================
// 新增代码generator/runtime/skills/memory/providers/vendor-gateway
// any 相关规则升级为 error — 严格类型安全
// ========================================================================
{
files: [
'libs/wwjcloud-ai/src/generator/**/*.ts',
'libs/wwjcloud-ai/src/runtime/**/*.ts',
'libs/wwjcloud-ai/src/skills/**/*.ts',
'libs/wwjcloud-ai/src/memory/**/*.ts',
'libs/wwjcloud-ai/src/providers/**/*.ts',
'libs/wwjcloud-boot/src/vendor/gateway/**/*.ts',
'libs/wwjcloud-boot/src/vendor/registry/**/*.ts',
'libs/wwjcloud-boot/src/vendor/interfaces/**/*.ts',
'libs/wwjcloud-boot/src/vendor/errors/**/*.ts',
'libs/wwjcloud-boot/src/vendor/provider-factories/**/*.ts',
],
rules: {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unsafe-assignment': 'error',
'@typescript-eslint/no-unsafe-call': 'error',
'@typescript-eslint/no-unsafe-member-access': 'error',
'@typescript-eslint/no-unsafe-return': 'error',
'@typescript-eslint/no-unsafe-argument': 'error',
'@typescript-eslint/no-unsafe-enum-comparison': 'error',
'@typescript-eslint/no-base-to-string': 'error',
'@typescript-eslint/restrict-template-expressions': 'error',
'@typescript-eslint/no-redundant-type-constituents': 'error',
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-misused-promises': 'error',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/require-await': 'error',
'@typescript-eslint/ban-ts-comment': 'error',
'@typescript-eslint/no-require-imports': 'error',
'no-case-declarations': 'error',
'no-empty': 'error',
'no-useless-catch': 'error',
'no-useless-escape': 'error',
},
},
// ========================================================================
// 全局默认规则(兜底 — 旧代码为 warn 级别)
// ========================================================================
{
rules: {
'@typescript-eslint/no-explicit-any': 'warn',

View File

@@ -84,6 +84,7 @@ ${methodsCode}
);
const params = this.extractParams(method);
const paramName = this.toCamelCase(method.name);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const returnType = 'Promise<any>';
return ` /**
@@ -130,6 +131,7 @@ ${methodsCode}
} else if (method.httpMethod === 'GET') {
parts.push(`@Query('${param}') ${param}: string`);
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
parts.push(`@Body('${param}') ${param}: any`);
}
}

View File

@@ -0,0 +1,198 @@
/**
* 自定义参数装饰器模块
*
* 充分发挥 NestJS 自定义参数装饰器特性,从 RequestContextService (AsyncLocalStorage)
* 中提取请求上下文信息,避免控制器中手动注入 RequestContextService。
*
* 使用方式:
* @CurrentUser() user: UserClaims
* @SiteId() siteId: number
* @MemberId() memberId: number
* @ReqId() requestId: string
* @CurrentLang() lang: string
* @CurrentChannel() channel: string
* @ClientIp() ip: string
* @UserAgent() ua: string
*/
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import {
REQUEST_CONTEXT_KEY,
type RequestContextStore,
} from './request-context.service';
/**
* 从 Express Request 对象中提取请求上下文
*/
function getStore(
ctx: ExecutionContext,
): RequestContextStore | undefined {
const request = ctx.switchToHttp().getRequest();
return request[REQUEST_CONTEXT_KEY] as RequestContextStore | undefined;
}
/**
* 从请求上下文中提取当前用户信息
*
* @example
* ```typescript
* @Get('profile')
* getProfile(@CurrentUser() user: { userId?: string; username?: string; roles?: string[] }) {
* return user;
* }
* ```
*/
export const CurrentUser = createParamDecorator(
(_data: unknown, ctx: ExecutionContext) => {
const rcs = getStore(ctx);
if (!rcs) return undefined;
return {
userId: rcs.userId,
username: rcs.username,
roles: rcs.roles,
};
},
);
/**
* 从请求上下文中提取当前站点ID数值类型
*
* @example
* ```typescript
* @Get('config')
* getConfig(@SiteId() siteId: number) {
* return this.service.getConfig(siteId);
* }
* ```
*/
export const SiteId = createParamDecorator(
(_data: unknown, ctx: ExecutionContext): number => {
const rcs = getStore(ctx);
return rcs?.siteId ? Number(rcs.siteId) : 0;
},
);
/**
* 从请求上下文中提取当前站点ID字符串类型
*
* @example
* ```typescript
* @Get('info')
* getInfo(@SiteIdStr() siteId: string) {
* return this.service.getInfo(siteId);
* }
* ```
*/
export const SiteIdStr = createParamDecorator(
(_data: unknown, ctx: ExecutionContext): string | undefined => {
const rcs = getStore(ctx);
return rcs?.siteId;
},
);
/**
* 从请求上下文中提取当前会员ID
*
* @example
* ```typescript
* @Get('orders')
* getOrders(@MemberId() memberId: number) {
* return this.orderService.getByMember(memberId);
* }
* ```
*/
export const MemberId = createParamDecorator(
(_data: unknown, ctx: ExecutionContext): number | undefined => {
const rcs = getStore(ctx);
return rcs?.memberId;
},
);
/**
* 从请求上下文中提取请求ID链路追踪
*
* @example
* ```typescript
* @Post('submit')
* submit(@ReqId() requestId: string, @Body() dto: SubmitDto) {
* this.logger.log(`[${requestId}] Processing submission`);
* }
* ```
*/
export const ReqId = createParamDecorator(
(_data: unknown, ctx: ExecutionContext): string | undefined => {
const rcs = getStore(ctx);
return rcs?.requestId;
},
);
/**
* 从请求上下文中提取当前语言
*
* @example
* ```typescript
* @Get('content')
* getContent(@CurrentLang() lang: string) {
* return this.i18nService.translate('hello', { lang });
* }
* ```
*/
export const CurrentLang = createParamDecorator(
(_data: unknown, ctx: ExecutionContext): string | undefined => {
const rcs = getStore(ctx);
return rcs?.lang;
},
);
/**
* 从请求上下文中提取当前渠道h5/weapp/pc 等)
*
* @example
* ```typescript
* @Get('page')
* getPage(@CurrentChannel() channel: string) {
* return this.diyService.getPage(channel);
* }
* ```
*/
export const CurrentChannel = createParamDecorator(
(_data: unknown, ctx: ExecutionContext): string | undefined => {
const rcs = getStore(ctx);
return rcs?.channel;
},
);
/**
* 从请求上下文中提取客户端IP
*
* @example
* ```typescript
* @Post('login')
* login(@ClientIp() ip: string, @Body() dto: LoginDto) {
* return this.authService.login(dto, ip);
* }
* ```
*/
export const ClientIp = createParamDecorator(
(_data: unknown, ctx: ExecutionContext): string | undefined => {
const rcs = getStore(ctx);
return rcs?.ip;
},
);
/**
* 从请求上下文中提取 User-Agent
*
* @example
* ```typescript
* @Get('device')
* getDeviceInfo(@UserAgent() ua: string) {
* return this.deviceService.parse(ua);
* }
* ```
*/
export const UserAgent = createParamDecorator(
(_data: unknown, ctx: ExecutionContext): string | undefined => {
const rcs = getStore(ctx);
return rcs?.ua;
},
);

View File

@@ -41,20 +41,22 @@ export function buildRequestContextMiddleware(ctx: RequestContextService) {
undefined;
const ua = (req.headers['user-agent'] as string) || undefined;
ctx.runWith(
{
requestId: id,
siteId,
userId,
username,
roles,
lang,
channel,
appType,
ip,
ua,
},
() => next(),
);
const store = {
requestId: id,
siteId,
userId,
username,
roles,
lang,
channel,
appType,
ip,
ua,
};
// 将上下文绑定到 req 对象,供自定义参数装饰器使用
ctx.bindToRequest(req as unknown as Record<string, unknown>);
ctx.runWith(store, () => next());
};
}

View File

@@ -2,7 +2,8 @@ import { Injectable } from '@nestjs/common';
import { AsyncLocalStorage } from 'async_hooks';
import { ThreadLocalHolder } from '../context/thread-local-holder';
interface RequestContextStore {
/** 请求上下文存储结构 */
export interface RequestContextStore {
requestId?: string;
siteId?: string;
memberId?: number;
@@ -16,6 +17,11 @@ interface RequestContextStore {
ua?: string;
}
/**
* 挂载到 Express Request 对象上的 key供自定义参数装饰器使用
*/
export const REQUEST_CONTEXT_KEY = '__wwj_request_context';
@Injectable()
export class RequestContextService {
private readonly storage = new AsyncLocalStorage<RequestContextStore>();
@@ -27,6 +33,14 @@ export class RequestContextService {
});
}
/**
* 将当前上下文绑定到 Express Request 对象上
* 供自定义参数装饰器(@CurrentUser, @SiteId 等)使用
*/
bindToRequest(req: Record<string, unknown>): void {
req[REQUEST_CONTEXT_KEY] = this.storage.getStore();
}
getContext(): RequestContextStore | undefined {
return this.storage.getStore();
}

View File

@@ -5,7 +5,7 @@ import {
FactoryProvider,
} from '@nestjs/common';
export interface HandlerProvider<M = any, R = any> {
export interface HandlerProvider<M = unknown, R = unknown> {
handle(bean: M): R | Promise<R>;
}
@@ -16,14 +16,14 @@ export const DEFAULT_HANDLER_PROVIDER = 'DEFAULT_HANDLER_PROVIDER';
export class HandlerProviderFactory {
private handlerProviderMap = new Map<
string,
Set<new (...args: any[]) => HandlerProvider>
Set<new (...args: unknown[]) => HandlerProvider>
>();
private handlerProviderClassMap = new Map<string, Set<string>>();
private handlerProviderModelMap = new Map<string, Set<string>>();
register<T extends HandlerProvider>(
name: string,
handlerProviderClass: new (...args: any[]) => T,
handlerProviderClass: new (...args: unknown[]) => T,
modelType?: string,
) {
// 根据名称注册
@@ -48,6 +48,7 @@ export class HandlerProviderFactory {
// 同步调用处理器
async invoke<M, R>(bean: M): Promise<R[]> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const beanType = (bean as any).constructor?.name || 'Unknown';
const handlerProviderClassSet = this.handlerProviderMap.get(beanType);
const resultList: R[] = [];
@@ -57,7 +58,7 @@ export class HandlerProviderFactory {
try {
const handlerProvider = new HandlerProviderClass();
const result = await handlerProvider.handle(bean);
resultList.push(result);
resultList.push(result as R);
} catch (error) {
console.error(
`Error invoking handler ${HandlerProviderClass.name}:`,
@@ -82,7 +83,7 @@ export class HandlerProviderFactory {
getHandlerProviderMap(): Map<
string,
Set<new (...args: any[]) => HandlerProvider>
Set<new (...args: unknown[]) => HandlerProvider>
> {
return new Map(this.handlerProviderMap);
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/require-await */
import { Injectable, Logger } from '@nestjs/common';
import {
SmsSendParams,

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/require-await */
import { Injectable, Logger } from '@nestjs/common';
import * as fs from 'fs';
import * as path from 'path';

View File

@@ -210,6 +210,7 @@ export class WechatPayProvider
}
/** 健康检查:验证微信支付是否已配置 */
// eslint-disable-next-line @typescript-eslint/require-await
async healthCheck(): Promise<HealthCheckResult> {
const start = Date.now();
const configured = !!(this.appId && this.mchId && this.apiKey);

View File

@@ -10,7 +10,7 @@ export interface JobExecutionContext {
jobGroup: string;
fireTime: Date;
scheduledFireTime: Date;
data: Record<string, any>;
data: Record<string, unknown>;
}
export interface JobProvider {
@@ -32,13 +32,13 @@ export const DEFAULT_JOB_PROVIDER = 'DEFAULT_JOB_PROVIDER';
export class JobProviderFactory {
private jobProviderClassMap = new Map<
string,
new (...args: any[]) => JobProvider
new (...args: unknown[]) => JobProvider
>();
private jobProviderNameMap = new Map<string, Set<string>>();
register(
key: string,
jobProviderClass: new (...args: any[]) => JobProvider,
jobProviderClass: new (...args: unknown[]) => JobProvider,
source?: string,
) {
this.jobProviderClassMap.set(key, jobProviderClass);
@@ -50,7 +50,7 @@ export class JobProviderFactory {
}
}
getJobProvider(key: string): new (...args: any[]) => JobProvider {
getJobProvider(key: string): new (...args: unknown[]) => JobProvider {
const jobProviderClass = this.jobProviderClassMap.get(key);
if (!jobProviderClass) {
throw new Error(`Job provider not found: ${key}`);
@@ -63,7 +63,10 @@ export class JobProviderFactory {
return new JobProviderClass();
}
getJobProviderClassMap(): Map<string, new (...args: any[]) => JobProvider> {
getJobProviderClassMap(): Map<
string,
new (...args: unknown[]) => JobProvider
> {
return new Map(this.jobProviderClassMap);
}

View File

@@ -8,11 +8,11 @@ import * as fs from 'fs';
import * as path from 'path';
export interface LoaderProvider {
loadJSON(addon: string, location: string): any;
loadJSONObject(addon: string, location: string): Record<string, any>;
loadJSONArray(addon: string, location: string): any[];
mergeJSONObject(location: string): Record<string, any>;
mergeJSONArray(location: string): any[];
loadJSON(addon: string, location: string): unknown;
loadJSONObject(addon: string, location: string): Record<string, unknown>;
loadJSONArray(addon: string, location: string): unknown[];
mergeJSONObject(location: string): Record<string, unknown>;
mergeJSONArray(location: string): unknown[];
}
export const LOADER_PROVIDERS = 'LOADER_PROVIDERS';
@@ -21,7 +21,7 @@ export const DEFAULT_LOADER_PROVIDER = 'DEFAULT_LOADER_PROVIDER';
class LoaderProviderImpl implements LoaderProvider {
private basePath = process.cwd();
loadJSON(addon: string, location: string): any {
loadJSON(addon: string, location: string): unknown {
try {
const filePath = this.resolveFilePath(addon, location);
const content = fs.readFileSync(filePath, 'utf-8');
@@ -32,19 +32,21 @@ class LoaderProviderImpl implements LoaderProvider {
}
}
loadJSONObject(addon: string, location: string): Record<string, any> {
loadJSONObject(addon: string, location: string): Record<string, unknown> {
const json = this.loadJSON(addon, location);
return json && typeof json === 'object' && !Array.isArray(json) ? json : {};
return json && typeof json === 'object' && !Array.isArray(json)
? (json as Record<string, unknown>)
: {};
}
loadJSONArray(addon: string, location: string): any[] {
loadJSONArray(addon: string, location: string): unknown[] {
const json = this.loadJSON(addon, location);
return Array.isArray(json) ? json : [];
}
mergeJSONObject(location: string): Record<string, any> {
mergeJSONObject(location: string): Record<string, unknown> {
try {
const merged: Record<string, any> = {};
const merged: Record<string, unknown> = {};
const addonsDir = path.join(this.basePath, 'addon');
if (!fs.existsSync(addonsDir)) {
@@ -62,25 +64,22 @@ class LoaderProviderImpl implements LoaderProvider {
if (jsonObject && Object.keys(jsonObject).length > 0) {
Object.assign(merged, jsonObject);
}
} catch (error) {
} catch {
// 忽略单个插件加载失败
continue;
}
}
return merged;
} catch (error) {
console.error(
`Error merging JSON objects from location ${location}:`,
error,
);
} catch {
console.error(`Error merging JSON objects from location ${location}:`);
return {};
}
}
mergeJSONArray(location: string): any[] {
mergeJSONArray(location: string): unknown[] {
try {
const merged: any[] = [];
const merged: unknown[] = [];
const addonsDir = path.join(this.basePath, 'addon');
if (!fs.existsSync(addonsDir)) {
@@ -98,18 +97,15 @@ class LoaderProviderImpl implements LoaderProvider {
if (Array.isArray(jsonArray) && jsonArray.length > 0) {
merged.push(...jsonArray);
}
} catch (error) {
} catch {
// 忽略单个插件加载失败
continue;
}
}
return merged;
} catch (error) {
console.error(
`Error merging JSON arrays from location ${location}:`,
error,
);
} catch {
console.error(`Error merging JSON arrays from location ${location}:`);
return [];
}
}

View File

@@ -2,9 +2,9 @@ import { Injectable, Module } from '@nestjs/common';
import { DynamicModule } from '@nestjs/common';
export interface IPayProvider {
createOrder(params: Record<string, any>): Promise<any>;
refund(params: Record<string, any>): Promise<any>;
queryOrder(orderId: string): Promise<any>;
createOrder(params: Record<string, unknown>): Promise<unknown>;
refund(params: Record<string, unknown>): Promise<unknown>;
queryOrder(orderId: string): Promise<unknown>;
}
export interface PayProviderConfig {
@@ -27,7 +27,7 @@ export class PayProviderFactory {
*/
static init(
annotationImplClassList: Array<
{ new (): IPayProvider } & { [key: string]: any }
{ new (): IPayProvider } & { [key: string]: unknown }
>,
): void {
if (!annotationImplClassList || annotationImplClassList.length <= 0) {
@@ -111,7 +111,8 @@ export class PayProviderFactory {
*/
getDefaultProvider(): IPayProvider | null {
// 尝试获取第一个可用的提供者
const firstProvider = PayProviderFactory.payProviderMap.keys().next().value;
const firstProvider = PayProviderFactory.payProviderMap.keys().next()
.value as string | undefined;
if (firstProvider) {
return PayProviderFactory.create(firstProvider);
}
@@ -121,7 +122,12 @@ export class PayProviderFactory {
/**
* 提取提供者配置(模拟注解解析)
*/
private static extractProviderConfig(cls: any): PayProviderConfig | null {
private static extractProviderConfig(cls: {
new (...args: unknown[]): IPayProvider;
providerConfig?: PayProviderConfig;
source?: string;
name?: string;
}): PayProviderConfig | null {
// 这里应该实现注解解析逻辑
// 简化版本,实际需要更复杂的反射机制
return {

View File

@@ -9,7 +9,7 @@ export interface SmsProvider {
send(
phoneNumber: string,
content: string,
config?: Record<string, any>,
config?: Record<string, unknown>,
): Promise<{
ok: boolean;
messageId: string;

View File

@@ -16,13 +16,13 @@ export const DEFAULT_UPGRADE_PROVIDER = 'DEFAULT_UPGRADE_PROVIDER';
export class UpgradeProviderFactory {
private upgradeProviderMap = new Map<
string,
new (...args: any[]) => UpgradeProvider
new (...args: unknown[]) => UpgradeProvider
>();
register(
addon: string,
version: string,
upgradeProviderClass: new (...args: any[]) => UpgradeProvider,
upgradeProviderClass: new (...args: unknown[]) => UpgradeProvider,
) {
const key = addon + version;
this.upgradeProviderMap.set(key, upgradeProviderClass);
@@ -31,7 +31,7 @@ export class UpgradeProviderFactory {
getUpgradeProvider(
addon: string,
version: string,
): (new (...args: any[]) => UpgradeProvider) | undefined {
): (new (...args: unknown[]) => UpgradeProvider) | undefined {
const key = addon + version;
return this.upgradeProviderMap.get(key);
}
@@ -72,7 +72,7 @@ export class UpgradeProviderFactory {
getUpgradeProviderMap(): Map<
string,
new (...args: any[]) => UpgradeProvider
new (...args: unknown[]) => UpgradeProvider
> {
return new Map(this.upgradeProviderMap);
}

View File

@@ -5,11 +5,27 @@ import {
DynamicModule,
} from '@nestjs/common';
/**
* 上传文件描述接口
* 对齐 Express.Multer.File 的核心字段
*/
export interface UploadFileDescriptor {
fieldname: string;
originalname: string;
encoding: string;
mimetype: string;
destination: string;
filename: string;
path: string;
size: number;
buffer?: Buffer;
}
/**
* 上传模型
*/
export interface UploadModel {
uploadFile?: any; // Express.Multer.File
uploadFile?: UploadFileDescriptor;
uploadType?: string;
uploadFilePath?: string;
uploadFileName?: string;
@@ -84,7 +100,7 @@ export interface UploadProvider {
* 初始化上传提供者
* 严格对齐Java: IUploadProvider.init(JSONObject configObject)
*/
init(configObject: Record<string, any>): void;
init(configObject: Record<string, unknown>): void;
/**
* 获取访问URL
@@ -183,7 +199,7 @@ export class UploadProviderFactory {
}
try {
return new Constructor();
} catch (error) {
} catch {
return null;
}
}
@@ -194,7 +210,7 @@ export class UploadProviderFactory {
*/
static createAndInit(
name: string,
configObject: Record<string, any>,
configObject: Record<string, unknown>,
): UploadProvider | null {
const provider = this.create(name);
if (provider && typeof provider.init === 'function') {

View File

@@ -3,8 +3,6 @@ import {
ProviderMetadata,
ProviderHealthStatus,
RegisteredProvider,
ProviderRegisteredEvent,
ProviderUnregisteredEvent,
} from './provider-metadata.interface';
import {
HealthCheckResult,
@@ -49,12 +47,6 @@ export class ProviderRegistryService implements OnModuleDestroy {
// 启动健康检查(借鉴 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(', ')})`,
);
@@ -70,11 +62,6 @@ export class ProviderRegistryService implements OnModuleDestroy {
this.stopHealthCheck(name);
this.providers.delete(name);
const event: ProviderUnregisteredEvent = {
name,
reason,
timestamp: Date.now(),
};
this.logger.log(`Provider 注销: ${name} (原因: ${reason})`);
}

View File

@@ -20,7 +20,19 @@
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"di:ai": "node -r ts-node/register -r tsconfig-paths/register scripts/di-debug-ai-only.ts",
"di:full": "node -r ts-node/register -r tsconfig-paths/register scripts/di-debug.ts"
"di:full": "node -r ts-node/register -r tsconfig-paths/register scripts/di-debug.ts",
"quality-gate": "bash scripts/quality-gate.sh",
"quality-gate:tsc": "bash scripts/quality-gate.sh --tsc",
"quality-gate:lint": "bash scripts/quality-gate.sh --lint",
"quality-gate:build": "bash scripts/quality-gate.sh --build",
"quality-gate:any": "bash scripts/quality-gate.sh --any",
"prepare": "cd .. && husky wwjcloud/.husky"
},
"lint-staged": {
"*.ts": [
"eslint --fix --max-warnings=0",
"prettier --write"
]
},
"dependencies": {
"@nestjs/axios": "^4.0.1",
@@ -53,6 +65,7 @@
"joi": "^18.0.1",
"jsonwebtoken": "^9.0.2",
"kafkajs": "^2.2.4",
"module-alias": "^2.2.3",
"mysql2": "^3.15.3",
"nestjs-i18n": "^10.5.1",
"passport-jwt": "^4.0.1",
@@ -60,8 +73,7 @@
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"swagger-ui-express": "^5.0.1",
"typeorm": "^0.3.27",
"module-alias": "^2.2.3"
"typeorm": "^0.3.27"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
@@ -79,7 +91,9 @@
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0",
"husky": "^9.1.7",
"jest": "^30.0.0",
"lint-staged": "^16.4.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
@@ -114,8 +128,7 @@
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
,
},
"_moduleAliases": {
"@wwjBoot": "dist/libs/wwjcloud-boot/src",
"@wwjCommon": "dist/libs/wwjcloud-boot/src/infra",

View File

@@ -0,0 +1,334 @@
#!/usr/bin/env bash
# ============================================================================
# WWJCloud-Nest v1 质量门禁脚本 (Quality Gate)
# ============================================================================
# 用法:
# ./scripts/quality-gate.sh # 运行全部检查
# ./scripts/quality-gate.sh --tsc # 仅类型检查
# ./scripts/quality-gate.sh --lint # 仅 ESLint 检查
# ./scripts/quality-gate.sh --build # 仅构建检查
# ./scripts/quality-gate.sh --any # 仅 any 类型计数
# ./scripts/quality-gate.sh --fix # 自动修复后运行门禁
# ============================================================================
set -euo pipefail
# ---- 颜色定义 ----
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m' # No Color
# ---- 阈值配置 ----
# any 类型计数阈值(新增代码目录)
ANY_THRESHOLD_NEW_CODE=0
# any 类型计数阈值(旧代码目录,渐进降低)
ANY_THRESHOLD_OLD_CODE=99999
# ESLint 错误数阈值
ESLINT_ERROR_THRESHOLD=0
# ESLint 警告数阈值(旧代码允许较多警告)
ESLINT_WARN_THRESHOLD=15000
# ---- 项目根目录 ----
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$PROJECT_ROOT"
# ---- 新增代码目录(严格模式) ----
NEW_CODE_DIRS=(
"libs/wwjcloud-ai/src/generator"
"libs/wwjcloud-ai/src/runtime"
"libs/wwjcloud-ai/src/skills"
"libs/wwjcloud-ai/src/memory"
"libs/wwjcloud-ai/src/providers"
"libs/wwjcloud-boot/src/vendor/gateway"
"libs/wwjcloud-boot/src/vendor/registry"
"libs/wwjcloud-boot/src/vendor/interfaces"
"libs/wwjcloud-boot/src/vendor/errors"
"libs/wwjcloud-boot/src/vendor/provider-factories"
)
# ---- 旧代码目录(渐进模式) ----
OLD_CODE_DIRS=(
"libs/wwjcloud-core/src"
"libs/wwjcloud-boot/src"
"libs/wwjcloud-ai/src/safe"
"libs/wwjcloud-ai/src/tuner"
"libs/wwjcloud-ai/src/manager"
"libs/wwjcloud-ai/src/healing"
)
# ---- 统计变量 ----
TOTAL_SCORE=0
TOTAL_CHECKS=0
FAILED_CHECKS=0
RESULTS=()
# ---- 工具函数 ----
# 打印质量门禁头部信息
print_header() {
echo -e "${BLUE}${BOLD}═══════════════════════════════════════════════════════════════${NC}"
echo -e "${BLUE}${BOLD} WWJCloud-Nest v1 质量门禁 (Quality Gate)${NC}"
echo -e "${BLUE}${BOLD} $(date '+%Y-%m-%d %H:%M:%S')${NC}"
echo -e "${BLUE}${BOLD}═══════════════════════════════════════════════════════════════${NC}"
echo ""
}
# 打印检查段落标题
print_section() {
echo -e "${CYAN}${BOLD}$1${NC}"
echo -e "${CYAN}───────────────────────────────────────────────────────────────────${NC}"
}
# 记录检查通过
pass_check() {
local name="$1"
local detail="$2"
TOTAL_SCORE=$((TOTAL_SCORE + 1))
TOTAL_CHECKS=$((TOTAL_CHECKS + 1))
RESULTS+=("$name: $detail")
echo -e " ${GREEN}✅ PASS${NC}$name: $detail"
}
# 记录检查失败
fail_check() {
local name="$1"
local detail="$2"
TOTAL_CHECKS=$((TOTAL_CHECKS + 1))
FAILED_CHECKS=$((FAILED_CHECKS + 1))
RESULTS+=("$name: $detail")
echo -e " ${RED}❌ FAIL${NC}$name: $detail"
}
# 记录检查警告
warn_check() {
local name="$1"
local detail="$2"
TOTAL_CHECKS=$((TOTAL_CHECKS + 1))
RESULTS+=("⚠️ $name: $detail")
echo -e " ${YELLOW}⚠️ WARN${NC}$name: $detail"
}
# ---- 检查函数 ----
# TypeScript 类型检查tsc --noEmit
check_tsc() {
print_section "TypeScript 类型检查 (tsc --noEmit)"
if npx tsc --noEmit 2>&1; then
pass_check "TypeScript" "0 类型错误"
else
local errors
errors=$(npx tsc --noEmit 2>&1 | grep -c "error TS" || true)
fail_check "TypeScript" "${errors} 个类型错误"
fi
echo ""
}
# ESLint 代码质量检查
check_eslint() {
print_section "ESLint 代码质量检查"
local output
output=$(npx eslint "{src,libs,test}/**/*.ts" --format json 2>/dev/null || true)
if [ -z "$output" ]; then
pass_check "ESLint" "0 错误, 0 警告"
echo ""
return
fi
# 解析 ESLint JSON 输出
local total_errors total_warnings
total_errors=$(echo "$output" | node -e "
const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8'));
let errors = 0, warnings = 0;
for (const file of data) {
for (const msg of file.messages) {
if (msg.severity === 2) errors++;
else if (msg.severity === 1) warnings++;
}
}
console.log(errors + ' ' + warnings);
" 2>/dev/null || echo "0 0")
total_errors=$(echo "$total_errors" | awk '{print $1}')
total_warnings=$(echo "$total_errors" | awk '{print $2}')
if [ "$total_errors" -le "$ESLINT_ERROR_THRESHOLD" ]; then
if [ "$total_warnings" -le "$ESLINT_WARN_THRESHOLD" ]; then
pass_check "ESLint" "${total_errors} 错误, ${total_warnings} 警告"
else
warn_check "ESLint" "${total_errors} 错误, ${total_warnings} 警告 (超过警告阈值 ${ESLINT_WARN_THRESHOLD})"
fi
else
fail_check "ESLint" "${total_errors} 错误 (超过阈值 ${ESLINT_ERROR_THRESHOLD}), ${total_warnings} 警告"
fi
echo ""
}
# NestJS 构建检查
check_build() {
print_section "NestJS 构建 (nest build)"
if npx nest build 2>&1; then
pass_check "Build" "构建成功"
else
fail_check "Build" "构建失败"
fi
echo ""
}
# TypeScript any 类型计数检查
check_any() {
print_section "TypeScript any 类型计数"
# ---- 新增代码 any 计数 ----
local new_any_count=0
local new_any_files=""
for dir in "${NEW_CODE_DIRS[@]}"; do
if [ -d "$PROJECT_ROOT/$dir" ]; then
local result
result=$(grep -rn --include="*.ts" -E '(: any\b|as any\b|<any>|: any\)|: any,|: any;|: any =|: any\])' "$PROJECT_ROOT/$dir" 2>/dev/null || true)
if [ -n "$result" ]; then
local count
count=$(echo "$result" | wc -l)
new_any_count=$((new_any_count + count))
new_any_files="${new_any_files}\n$(echo "$result" | head -20)"
fi
fi
done
echo -e " ${BOLD}新增代码 any 计数:${NC}"
echo -e " 检查目录: ${NEW_CODE_DIRS[*]}"
echo -e " 发现: ${new_any_count}"
if [ "$new_any_count" -le "$ANY_THRESHOLD_NEW_CODE" ]; then
pass_check "Any-新代码" "${new_any_count} 处 (阈值: ${ANY_THRESHOLD_NEW_CODE})"
else
fail_check "Any-新代码" "${new_any_count} 处 (超过阈值 ${ANY_THRESHOLD_NEW_CODE})"
if [ -n "$new_any_files" ]; then
echo -e "${YELLOW} 详情 (前20条):${NC}"
echo -e "$new_any_files" | head -20 | while read -r line; do
echo -e " ${YELLOW}$line${NC}"
done
fi
fi
echo ""
# ---- 旧代码 any 计数(仅统计,不阻断) ----
local old_any_count=0
for dir in "${OLD_CODE_DIRS[@]}"; do
if [ -d "$PROJECT_ROOT/$dir" ]; then
local result
result=$(grep -rc --include="*.ts" -E '(: any\b|as any\b|<any>|: any\)|: any,|: any;|: any =|: any\])' "$PROJECT_ROOT/$dir" 2>/dev/null || true)
if [ -n "$result" ]; then
while read -r count; do
old_any_count=$((old_any_count + count))
done <<< "$result"
fi
fi
done
echo -e " ${BOLD}旧代码 any 计数:${NC}"
echo -e " 检查目录: ${OLD_CODE_DIRS[*]}"
echo -e " 发现: ${old_any_count}${YELLOW}(渐进降低中,当前不阻断)${NC}"
echo ""
if [ "$old_any_count" -gt "$ANY_THRESHOLD_OLD_CODE" ]; then
warn_check "Any-旧代码" "${old_any_count} 处 (超过渐进阈值 ${ANY_THRESHOLD_OLD_CODE})"
else
pass_check "Any-旧代码" "${old_any_count} 处 (渐进模式)"
fi
echo ""
}
# ---- 主流程 ----
main() {
print_header
local run_all=true
local run_tsc=false
local run_lint=false
local run_build=false
local run_any=false
local auto_fix=false
for arg in "$@"; do
case "$arg" in
--tsc) run_tsc=true; run_all=false ;;
--lint) run_lint=true; run_all=false ;;
--build) run_build=true; run_all=false ;;
--any) run_any=true; run_all=false ;;
--fix) auto_fix=true ;;
--help|-h)
echo "用法: $0 [--tsc] [--lint] [--build] [--any] [--fix] [--help]"
exit 0
;;
esac
done
# 自动修复模式
if [ "$auto_fix" = true ]; then
print_section "自动修复模式"
echo -e " 运行 eslint --fix ..."
npx eslint "{src,libs,test}/**/*.ts" --fix 2>/dev/null || true
echo -e " ${GREEN}自动修复完成${NC}"
echo ""
fi
# 执行检查
if [ "$run_all" = true ] || [ "$run_tsc" = true ]; then
check_tsc
fi
if [ "$run_all" = true ] || [ "$run_lint" = true ]; then
check_eslint
fi
if [ "$run_all" = true ] || [ "$run_build" = true ]; then
check_build
fi
if [ "$run_all" = true ] || [ "$run_any" = true ]; then
check_any
fi
# ---- 总结报告 ----
echo -e "${BLUE}${BOLD}═══════════════════════════════════════════════════════════════${NC}"
echo -e "${BLUE}${BOLD} 质量门禁报告${NC}"
echo -e "${BLUE}${BOLD}═══════════════════════════════════════════════════════════════${NC}"
echo ""
for result in "${RESULTS[@]}"; do
echo -e " $result"
done
echo ""
local score_pct=0
if [ "$TOTAL_CHECKS" -gt 0 ]; then
score_pct=$((TOTAL_SCORE * 100 / TOTAL_CHECKS))
fi
echo -e " ${BOLD}通过率: ${score_pct}% (${TOTAL_SCORE}/${TOTAL_CHECKS})${NC}"
echo ""
if [ "$FAILED_CHECKS" -gt 0 ]; then
echo -e " ${RED}${BOLD}🚫 质量门禁未通过!${NC}${RED} (${FAILED_CHECKS} 项检查失败)${NC}"
echo -e " ${RED}请修复以上问题后重新提交。${NC}"
echo ""
exit 1
else
echo -e " ${GREEN}${BOLD}✅ 质量门禁通过!${NC}${GREEN} 所有检查项均达标。${NC}"
echo ""
exit 0
fi
}
main "$@"