Compare commits
6 Commits
e53d2a4a3f
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c3ee4bfbb | ||
|
|
5f3b6f93b5 | ||
|
|
214a95f687 | ||
|
|
26c9cea362 | ||
|
|
45bdc7ceb2 | ||
|
|
57034138ca |
58
wwjcloud-nest-v1/.dockerignore
Normal file
58
wwjcloud-nest-v1/.dockerignore
Normal file
@@ -0,0 +1,58 @@
|
||||
# ============================================================
|
||||
# Docker 构建忽略文件
|
||||
# ============================================================
|
||||
|
||||
# 依赖目录(由 Dockerfile 内 npm ci 安装)
|
||||
node_modules
|
||||
**/node_modules
|
||||
|
||||
# 构建产物(由 Dockerfile 内 nest build 生成)
|
||||
dist
|
||||
**/dist
|
||||
|
||||
# 版本控制
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# 文档
|
||||
*.md
|
||||
docs/
|
||||
|
||||
# PHP 参考项目
|
||||
niucloud-php
|
||||
niucloud-java
|
||||
|
||||
# 前端项目
|
||||
admin
|
||||
web
|
||||
uniappx
|
||||
|
||||
# Docker 相关(避免递归)
|
||||
docker/
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
.dockerignore
|
||||
|
||||
# IDE 和编辑器
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS 生成文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# 测试和覆盖率
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# Husky
|
||||
.husky
|
||||
|
||||
# 临时文件
|
||||
*.log
|
||||
*.tmp
|
||||
.env.local
|
||||
.env.*.local
|
||||
1
wwjcloud-nest-v1/.husky/pre-commit
Normal file
1
wwjcloud-nest-v1/.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
||||
npm test
|
||||
78
wwjcloud-nest-v1/Dockerfile
Normal file
78
wwjcloud-nest-v1/Dockerfile
Normal file
@@ -0,0 +1,78 @@
|
||||
# ============================================================
|
||||
# WWJCLOUD NestJS Monorepo - 多阶段 Docker 构建
|
||||
# ============================================================
|
||||
# 构建上下文: wwjcloud-nest-v1/ (项目根目录)
|
||||
# 工作目录: /app/wwjcloud (对应 wwjcloud/ 子目录)
|
||||
# ============================================================
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 阶段 1: 安装全部依赖 (含 devDependencies,用于构建)
|
||||
# ----------------------------------------------------------
|
||||
FROM node:20-alpine AS deps
|
||||
|
||||
WORKDIR /app/wwjcloud
|
||||
|
||||
# 先复制依赖声明文件,利用 Docker 层缓存
|
||||
COPY wwjcloud/package.json wwjcloud/package-lock.json ./
|
||||
|
||||
# 安装全部依赖(构建阶段需要 devDependencies)
|
||||
RUN npm ci
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 阶段 2: TypeScript 编译构建
|
||||
# ----------------------------------------------------------
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app/wwjcloud
|
||||
|
||||
# 从 deps 阶段复制 node_modules
|
||||
COPY --from=deps /app/wwjcloud/node_modules ./node_modules
|
||||
|
||||
# 复制源码和配置文件
|
||||
COPY wwjcloud/ ./
|
||||
|
||||
# 执行 NestJS 构建 (nest build -> 输出到 dist/)
|
||||
RUN npm run build
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 阶段 3: 生产运行镜像
|
||||
# ----------------------------------------------------------
|
||||
FROM node:20-alpine AS runner
|
||||
|
||||
WORKDIR /app/wwjcloud
|
||||
|
||||
# 设置生产环境变量
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# 仅复制生产依赖声明
|
||||
COPY wwjcloud/package.json wwjcloud/package-lock.json ./
|
||||
|
||||
# 安装生产依赖(排除 devDependencies)
|
||||
RUN npm ci --omit=dev && npm cache clean --force
|
||||
|
||||
# 从 builder 阶段复制构建产物
|
||||
COPY --from=builder /app/wwjcloud/dist ./dist
|
||||
|
||||
# 复制运行时必需的非 TS 文件(i18n 语言包、静态资源等)
|
||||
COPY wwjcloud/apps/api/src/lang ./apps/api/src/lang
|
||||
|
||||
# 创建非 root 用户(安全最佳实践)
|
||||
RUN addgroup -g 1001 -S nodejs && \
|
||||
adduser -S nestjs -u 1001 -G nodejs
|
||||
|
||||
# 设置目录所有权
|
||||
RUN chown -R nestjs:nodejs /app/wwjcloud
|
||||
|
||||
USER nestjs
|
||||
|
||||
# module-alias 用于运行时解析 @wwjBoot/@wwjCore 等路径别名
|
||||
ENV NODE_OPTIONS="--require module-alias/register"
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
# 健康检查(使用 Node.js 内置模块,无需安装 curl)
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \
|
||||
CMD node -e "require('http').get('http://localhost:3000/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) }).on('error', () => process.exit(1))"
|
||||
|
||||
# 启动主应用
|
||||
CMD ["node", "dist/apps/api/src/main.js"]
|
||||
201
wwjcloud-nest-v1/docker-compose.yml
Normal file
201
wwjcloud-nest-v1/docker-compose.yml
Normal file
@@ -0,0 +1,201 @@
|
||||
# ============================================================
|
||||
# WWJCLOUD NestJS V1 - Docker Compose 编排配置
|
||||
# ============================================================
|
||||
# 用法:
|
||||
# 启动全部服务: docker compose up -d
|
||||
# 仅启动基础设施: docker compose up -d mysql redis kafka
|
||||
# 查看日志: docker compose logs -f app
|
||||
# 停止并清理: docker compose down -v
|
||||
# ============================================================
|
||||
|
||||
services:
|
||||
# ========================================
|
||||
# NestJS 应用服务
|
||||
# ========================================
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: wwjcloud-app
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${APP_PORT:-3000}:3000"
|
||||
environment:
|
||||
# 运行环境
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- LOG_LEVEL=info
|
||||
# 数据库(连接 mysql 服务)
|
||||
- DB_HOST=mysql
|
||||
- DB_PORT=3306
|
||||
- DB_USERNAME=root
|
||||
- DB_PASSWORD=wwjcloud2024
|
||||
- DB_DATABASE=wwjcloud
|
||||
- DB_SYNCHRONIZE=false
|
||||
# Redis(连接 redis 服务)
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- REDIS_PASSWORD=
|
||||
- REDIS_NAMESPACE=wwjcloud
|
||||
# JWT
|
||||
- JWT_SECRET=wwjcloud-jwt-secret-key-v1-2025
|
||||
- JWT_EXPIRATION=7d
|
||||
# 功能开关
|
||||
- AUTH_ENABLED=true
|
||||
- RBAC_ENABLED=true
|
||||
- RATE_LIMIT_ENABLED=true
|
||||
- RATE_LIMIT_WINDOW_MS=1000
|
||||
- RATE_LIMIT_MAX=100
|
||||
- SWAGGER_ENABLED=true
|
||||
- PROMETHEUS_ENABLED=true
|
||||
- METRICS_ENABLED=true
|
||||
- RESPONSE_WRAPPER_ENABLED=true
|
||||
# 队列(BullMQ,连接 redis)
|
||||
- QUEUE_ENABLED=true
|
||||
- QUEUE_DRIVER=bullmq
|
||||
- QUEUE_REDIS_HOST=redis
|
||||
- QUEUE_REDIS_PORT=6379
|
||||
# Kafka
|
||||
- KAFKA_BROKERS=kafka:9092
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
kafka:
|
||||
condition: service_started
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
networks:
|
||||
- wwjcloud-net
|
||||
|
||||
# ========================================
|
||||
# MySQL 8.0 数据库
|
||||
# ========================================
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: wwjcloud-mysql
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${MYSQL_PORT:-3306}:3306"
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: wwjcloud2024
|
||||
MYSQL_DATABASE: wwjcloud
|
||||
MYSQL_CHARACTER_SET_SERVER: utf8mb4
|
||||
MYSQL_COLLATION_SERVER: utf8mb4_unicode_ci
|
||||
TZ: Asia/Shanghai
|
||||
volumes:
|
||||
- wwjcloud_mysql_data:/var/lib/mysql
|
||||
- ./sql:/docker-entrypoint-initdb.d:ro
|
||||
command:
|
||||
- --character-set-server=utf8mb4
|
||||
- --collation-server=utf8mb4_unicode_ci
|
||||
- --default-authentication-plugin=caching_sha2_password
|
||||
- --max-connections=200
|
||||
- --innodb-buffer-pool-size=256M
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-pwwjcloud2024"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
networks:
|
||||
- wwjcloud-net
|
||||
|
||||
# ========================================
|
||||
# Redis 7 缓存
|
||||
# ========================================
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: wwjcloud-redis
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${REDIS_PORT:-6379}:6379"
|
||||
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
|
||||
volumes:
|
||||
- wwjcloud_redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
networks:
|
||||
- wwjcloud-net
|
||||
|
||||
# ========================================
|
||||
# Kafka (Confluent Platform,内置 Zookeeper)
|
||||
# ========================================
|
||||
kafka:
|
||||
image: confluentinc/cp-kafka:7.6.1
|
||||
container_name: wwjcloud-kafka
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${KAFKA_PORT:-9092}:9092"
|
||||
environment:
|
||||
KAFKA_BROKER_ID: 1
|
||||
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
|
||||
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:9092
|
||||
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
|
||||
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
|
||||
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
||||
KAFKA_LOG_RETENTION_HOURS: 168
|
||||
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
|
||||
CLUSTER_ID: wwjcloud-kafka-cluster-1
|
||||
depends_on:
|
||||
zookeeper:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "kafka-broker-api-versions", "--bootstrap-server", "localhost:9092"]
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
networks:
|
||||
- wwjcloud-net
|
||||
|
||||
# ========================================
|
||||
# Zookeeper (Kafka 依赖)
|
||||
# ========================================
|
||||
zookeeper:
|
||||
image: confluentinc/cp-zookeeper:7.6.1
|
||||
container_name: wwjcloud-zookeeper
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${ZK_PORT:-2181}:2181"
|
||||
environment:
|
||||
ZOOKEEPER_CLIENT_PORT: 2181
|
||||
ZOOKEEPER_TICK_TIME: 2000
|
||||
volumes:
|
||||
- wwjcloud_zk_data:/var/lib/zookeeper/data
|
||||
- wwjcloud_zk_log:/var/lib/zookeeper/log
|
||||
healthcheck:
|
||||
test: ["CMD", "nc", "-z", "localhost", "2181"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
networks:
|
||||
- wwjcloud-net
|
||||
|
||||
# ========================================
|
||||
# 数据卷
|
||||
# ========================================
|
||||
volumes:
|
||||
wwjcloud_mysql_data:
|
||||
driver: local
|
||||
wwjcloud_redis_data:
|
||||
driver: local
|
||||
wwjcloud_zk_data:
|
||||
driver: local
|
||||
wwjcloud_zk_log:
|
||||
driver: local
|
||||
|
||||
# ========================================
|
||||
# 网络
|
||||
# ========================================
|
||||
networks:
|
||||
wwjcloud-net:
|
||||
driver: bridge
|
||||
@@ -3,5 +3,8 @@
|
||||
"@types/node": "^24.10.0",
|
||||
"glob": "^11.0.3",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "husky"
|
||||
}
|
||||
}
|
||||
|
||||
159
wwjcloud-nest-v1/scripts/e2e-test.sh
Normal file
159
wwjcloud-nest-v1/scripts/e2e-test.sh
Normal file
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# WWJCLOUD API E2E 端到端测试脚本
|
||||
# ============================================================
|
||||
# 使用 curl 测试核心业务流程
|
||||
# 用法: bash scripts/e2e-test.sh [BASE_URL]
|
||||
# ============================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---------- 配置 ----------
|
||||
BASE_URL="${1:-http://localhost:3000}"
|
||||
PASS_COUNT=0
|
||||
FAIL_COUNT=0
|
||||
TOTAL_COUNT=0
|
||||
|
||||
# ---------- 辅助函数:格式化耗时 ----------
|
||||
format_ms() {
|
||||
local start="$1"
|
||||
local end="$2"
|
||||
echo "$(( (end - start) ))ms"
|
||||
}
|
||||
|
||||
# ---------- 辅助函数:执行单次测试 ----------
|
||||
# 参数: $1=测试名称 $2=方法 $3=URL $4=请求体(可选) $5=期望状态码(默认200)
|
||||
run_test() {
|
||||
local name="$1"
|
||||
local method="$2"
|
||||
local url="$3"
|
||||
local body="${4:-}"
|
||||
local expected_status="${5:-200}"
|
||||
|
||||
TOTAL_COUNT=$((TOTAL_COUNT + 1))
|
||||
|
||||
echo ""
|
||||
echo "------------------------------------------"
|
||||
echo " [${TOTAL_COUNT}] ${name}"
|
||||
echo " ${method} ${url}"
|
||||
|
||||
local start_time end_time elapsed http_code response_body
|
||||
start_time="$(date +%s%3N 2>/dev/null || python3 -c 'import time; print(int(time.time()*1000))')"
|
||||
|
||||
# 构建 curl 命令
|
||||
local curl_args=(
|
||||
-s # 静默模式
|
||||
-w "\n%{http_code}" # 输出状态码
|
||||
-o /tmp/wwjcloud_e2e_body # 响应体写入临时文件
|
||||
-X "${method}"
|
||||
-H "Content-Type: application/json"
|
||||
--connect-timeout 5
|
||||
--max-time 10
|
||||
)
|
||||
|
||||
# 附加请求体
|
||||
if [[ -n "${body}" ]]; then
|
||||
curl_args+=(-d "${body}")
|
||||
fi
|
||||
|
||||
curl_args+=("${url}")
|
||||
|
||||
# 执行请求
|
||||
local curl_output
|
||||
curl_output="$(curl "${curl_args[@]}" 2>&1)" || true
|
||||
|
||||
end_time="$(date +%s%3N 2>/dev/null || python3 -c 'import time; print(int(time.time()*1000))')"
|
||||
elapsed="$(format_ms "${start_time}" "${end_time}")"
|
||||
|
||||
# 解析状态码(curl 输出最后一行)
|
||||
http_code="$(echo "${curl_output}" | tail -1 | tr -d '[:space:]')"
|
||||
response_body="$(cat /tmp/wwjcloud_e2e_body 2>/dev/null || echo '')"
|
||||
|
||||
# 判定结果
|
||||
if [[ "${http_code}" == "${expected_status}" ]]; then
|
||||
PASS_COUNT=$((PASS_COUNT + 1))
|
||||
echo " 结果: PASS [${http_code}] 耗时: ${elapsed}"
|
||||
else
|
||||
FAIL_COUNT=$((FAIL_COUNT + 1))
|
||||
echo " 结果: FAIL 期望 ${expected_status},实际 ${http_code} 耗时: ${elapsed}"
|
||||
# 输出前 200 字符的响应体用于调试
|
||||
echo " 响应: $(echo "${response_body}" | head -c 200)"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------- 主流程 ----------
|
||||
main() {
|
||||
echo "============================================================"
|
||||
echo " WWJCLOUD API E2E 测试"
|
||||
echo " 目标: ${BASE_URL}"
|
||||
echo " 时间: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "============================================================"
|
||||
|
||||
# ---------- 测试 1: 健康检查 ----------
|
||||
run_test "健康检查" "GET" "${BASE_URL}/health"
|
||||
|
||||
# ---------- 测试 2: 登录接口 ----------
|
||||
LOGIN_BODY='{"username":"admin","password":"123456"}'
|
||||
run_test "管理员登录" "POST" "${BASE_URL}/adminapi/login/login" "${LOGIN_BODY}" "200"
|
||||
|
||||
# 从登录响应中提取 token(用于后续需要认证的请求)
|
||||
TOKEN=""
|
||||
if [[ -f /tmp/wwjcloud_e2e_body ]]; then
|
||||
# 尝试从 JSON 响应中提取 data.token 字段
|
||||
TOKEN="$(cat /tmp/wwjcloud_e2e_body 2>/dev/null | grep -o '"token":"[^"]*"' | head -1 | cut -d'"' -f4 || true)"
|
||||
if [[ -z "${TOKEN}" ]]; then
|
||||
# 备选:尝试 data.access_token
|
||||
TOKEN="$(cat /tmp/wwjcloud_e2e_body 2>/dev/null | grep -o '"access_token":"[^"]*"' | head -1 | cut -d'"' -f4 || true)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "${TOKEN}" ]]; then
|
||||
echo " [INFO] Token 获取成功: ${TOKEN:0:20}..."
|
||||
else
|
||||
echo " [WARN] Token 获取失败,后续认证测试可能失败"
|
||||
fi
|
||||
|
||||
# ---------- 测试 3: 获取用户列表(需认证) ----------
|
||||
AUTH_HEADER="Authorization: Bearer ${TOKEN}"
|
||||
if [[ -n "${TOKEN}" ]]; then
|
||||
run_test "获取用户列表" "GET" "${BASE_URL}/adminapi/user/lists" "" "200"
|
||||
else
|
||||
run_test "获取用户列表(无Token,预期失败)" "GET" "${BASE_URL}/adminapi/user/lists" "" "401"
|
||||
fi
|
||||
|
||||
# ---------- 测试 4: 获取站点信息(需认证) ----------
|
||||
if [[ -n "${TOKEN}" ]]; then
|
||||
run_test "获取站点信息" "GET" "${BASE_URL}/adminapi/site/info" "" "200"
|
||||
else
|
||||
run_test "获取站点信息(无Token,预期失败)" "GET" "${BASE_URL}/adminapi/site/info" "" "401"
|
||||
fi
|
||||
|
||||
# ---------- 测试 5: Prometheus 指标 ----------
|
||||
run_test "Prometheus 指标" "GET" "${BASE_URL}/metrics" "" "200"
|
||||
|
||||
# ---------- 测试 6: Swagger 文档 ----------
|
||||
run_test "Swagger JSON 文档" "GET" "${BASE_URL}/api-docs-json" "" "200"
|
||||
|
||||
# ---------- 汇总报告 ----------
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo " E2E 测试报告"
|
||||
echo " 总计: ${TOTAL_COUNT} 通过: ${PASS_COUNT} 失败: ${FAIL_COUNT}"
|
||||
if [[ "${FAIL_COUNT}" -eq 0 ]]; then
|
||||
echo " 结果: ALL PASSED"
|
||||
else
|
||||
echo " 结果: ${FAIL_COUNT} FAILED"
|
||||
fi
|
||||
echo " 时间: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "============================================================"
|
||||
|
||||
# 清理临时文件
|
||||
rm -f /tmp/wwjcloud_e2e_body
|
||||
|
||||
# 返回退出码
|
||||
if [[ "${FAIL_COUNT}" -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
138
wwjcloud-nest-v1/scripts/stress-test.sh
Normal file
138
wwjcloud-nest-v1/scripts/stress-test.sh
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# WWJCLOUD API 压力测试脚本
|
||||
# ============================================================
|
||||
# 使用 autocannon 对核心接口进行并发压力测试
|
||||
# 用法: bash scripts/stress-test.sh [BASE_URL]
|
||||
# ============================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---------- 配置 ----------
|
||||
BASE_URL="${1:-http://localhost:3000}"
|
||||
DURATION="${STRESS_DURATION:-30}"
|
||||
RESULT_FILE="stress-test-results.txt"
|
||||
|
||||
# ---------- 依赖检查 ----------
|
||||
check_autocannon() {
|
||||
if ! command -v autocannon &>/dev/null; then
|
||||
echo "[ERROR] autocannon 未安装,正在安装..."
|
||||
npm install -g autocannon
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------- 辅助函数:运行单次压测 ----------
|
||||
# 参数: $1=测试名称 $2=URL $3=方法 $4=并发数 $5=请求体(JSON)
|
||||
run_test() {
|
||||
local name="$1"
|
||||
local url="$2"
|
||||
local method="${3:-GET}"
|
||||
local concurrency="$4"
|
||||
local body="${5:-}"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " 测试: ${name}"
|
||||
echo " URL: ${method} ${url}"
|
||||
echo " 并发: ${concurrency} 持续: ${DURATION}s"
|
||||
echo "=========================================="
|
||||
|
||||
local args=(
|
||||
"-c" "${concurrency}"
|
||||
"-d" "${DURATION}"
|
||||
"-m" "${method}"
|
||||
"-j" # JSON 输出
|
||||
)
|
||||
|
||||
# 如果有请求体,写入临时文件
|
||||
if [[ -n "${body}" ]]; then
|
||||
local tmp_body
|
||||
tmp_body="$(mktemp)"
|
||||
echo "${body}" > "${tmp_body}"
|
||||
args+=("-b" "@${tmp_body}")
|
||||
fi
|
||||
|
||||
args+=("${url}")
|
||||
|
||||
# 执行压测并捕获输出
|
||||
local json_output
|
||||
json_output="$(autocannon "${args[@]}" 2>&1)" || true
|
||||
|
||||
# 清理临时文件
|
||||
[[ -n "${body:-}" && -n "${tmp_body:-}" ]] && rm -f "${tmp_body}"
|
||||
|
||||
# 解析关键指标
|
||||
local qps avg_latency p99_latency errors total_requests total_timeouts
|
||||
qps="$(echo "${json_output}" | grep -o '"requests":{"average":[0-9.]*' | grep -o '[0-9.]*$' || echo 'N/A')"
|
||||
avg_latency="$(echo "${json_output}" | grep -o '"latency":{"average":[0-9.]*' | grep -o '[0-9.]*$' || echo 'N/A')"
|
||||
p99_latency="$(echo "${json_output}" | grep -o '"p99":[0-9.]*' | grep -o '[0-9.]*$' || echo 'N/A')"
|
||||
errors="$(echo "${json_output}" | grep -o '"errors":[0-9]*' | grep -o '[0-9]*$' || echo '0')"
|
||||
total_requests="$(echo "${json_output}" | grep -o '"total":' | wc -l | tr -d ' ')"
|
||||
total_timeouts="$(echo "${json_output}" | grep -o '"timeouts":[0-9]*' | grep -o '[0-9]*$' || echo '0')"
|
||||
|
||||
# 如果 JSON 解析失败,直接输出原始结果
|
||||
if [[ "${qps}" == "N/A" ]]; then
|
||||
echo "${json_output}"
|
||||
return
|
||||
fi
|
||||
|
||||
echo " QPS: ${qps} req/s"
|
||||
echo " 平均延迟: ${avg_latency} ms"
|
||||
echo " P99 延迟: ${p99_latency} ms"
|
||||
echo " 错误数: ${errors}"
|
||||
echo " 超时数: ${total_timeouts}"
|
||||
}
|
||||
|
||||
# ---------- 主流程 ----------
|
||||
main() {
|
||||
check_autocannon
|
||||
|
||||
echo "============================================================"
|
||||
echo " WWJCLOUD API 压力测试"
|
||||
echo " 目标: ${BASE_URL}"
|
||||
echo " 时间: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "============================================================"
|
||||
|
||||
# 初始化结果文件
|
||||
{
|
||||
echo "============================================================"
|
||||
echo " WWJCLOUD API 压力测试报告"
|
||||
echo " 目标: ${BASE_URL}"
|
||||
echo " 时间: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo " 持续: ${DURATION}s"
|
||||
echo "============================================================"
|
||||
} > "${RESULT_FILE}"
|
||||
|
||||
# ---------- 测试 1: 健康检查 ----------
|
||||
{
|
||||
echo ""
|
||||
echo "--- 测试 1: 健康检查 GET /health ---"
|
||||
run_test "健康检查" "${BASE_URL}/health" "GET" 1000
|
||||
} | tee -a "${RESULT_FILE}"
|
||||
|
||||
# ---------- 测试 2: 登录接口 ----------
|
||||
{
|
||||
echo ""
|
||||
echo "--- 测试 2: 登录接口 POST /adminapi/login/login ---"
|
||||
run_test "登录接口" "${BASE_URL}/adminapi/login/login" "POST" 100 \
|
||||
'{"username":"admin","password":"123456"}'
|
||||
} | tee -a "${RESULT_FILE}"
|
||||
|
||||
# ---------- 测试 3: 用户列表 ----------
|
||||
{
|
||||
echo ""
|
||||
echo "--- 测试 3: 用户列表 GET /adminapi/user/lists ---"
|
||||
run_test "用户列表" "${BASE_URL}/adminapi/user/lists" "GET" 200
|
||||
} | tee -a "${RESULT_FILE}"
|
||||
|
||||
# ---------- 汇总 ----------
|
||||
{
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo " 压力测试完成"
|
||||
echo " 结果已保存至: ${RESULT_FILE}"
|
||||
echo "============================================================"
|
||||
} | tee -a "${RESULT_FILE}"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
6
wwjcloud-nest-v1/wwjcloud/.husky/pre-commit
Normal file
6
wwjcloud-nest-v1/wwjcloud/.husky/pre-commit
Normal file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
# 质量门禁 pre-commit hook
|
||||
# 仅对暂存区的 TypeScript 文件执行 lint-staged
|
||||
npx lint-staged
|
||||
@@ -60,4 +60,114 @@ export default tseslint.config(
|
||||
'@typescript-eslint/unbound-method': 'off',
|
||||
},
|
||||
},
|
||||
// 旧代码(listeners/ 和部分 services/)历史遗留的 no-unused-vars 和 require-await 降级为 warn
|
||||
{
|
||||
files: [
|
||||
'libs/wwjcloud-core/src/listeners/**/*.ts',
|
||||
'libs/wwjcloud-core/src/services/**/*.ts',
|
||||
'libs/wwjcloud-core/src/controllers/**/*.ts',
|
||||
'libs/wwjcloud-core/src/dtos/**/*.ts',
|
||||
'libs/wwjcloud-core/src/common/**/*.ts',
|
||||
'libs/wwjcloud-core/src/entities/**/*.ts',
|
||||
'libs/wwjcloud-core/src/jobs/**/*.ts',
|
||||
'libs/wwjcloud-boot/src/**/*.ts',
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
'@typescript-eslint/require-await': 'warn',
|
||||
'@typescript-eslint/ban-ts-comment': 'warn',
|
||||
'@typescript-eslint/no-unsafe-enum-comparison': 'warn',
|
||||
'@typescript-eslint/no-require-imports': 'warn',
|
||||
'no-case-declarations': 'warn',
|
||||
'no-empty': 'warn',
|
||||
},
|
||||
},
|
||||
// AI 模块中的旧代码(safe/tuner/manager)历史遗留降级为 warn
|
||||
{
|
||||
files: [
|
||||
'libs/wwjcloud-ai/src/safe/**/*.ts',
|
||||
'libs/wwjcloud-ai/src/tuner/**/*.ts',
|
||||
'libs/wwjcloud-ai/src/manager/**/*.ts',
|
||||
'libs/wwjcloud-ai/src/healing/**/*.ts',
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
'@typescript-eslint/require-await': 'warn',
|
||||
'@typescript-eslint/no-floating-promises': 'warn',
|
||||
'@typescript-eslint/no-misused-promises': 'warn',
|
||||
'@typescript-eslint/no-base-to-string': 'warn',
|
||||
'@typescript-eslint/restrict-template-expressions': 'warn',
|
||||
'@typescript-eslint/no-redundant-type-constituents': 'warn',
|
||||
'@typescript-eslint/await-thenable': 'warn',
|
||||
'@typescript-eslint/no-unsafe-enum-comparison': 'warn',
|
||||
'@typescript-eslint/no-duplicate-enum-values': 'warn',
|
||||
'@typescript-eslint/prefer-promise-reject-errors': 'warn',
|
||||
'no-useless-catch': 'warn',
|
||||
'no-useless-escape': 'warn',
|
||||
'no-case-declarations': 'warn',
|
||||
'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',
|
||||
'libs/wwjcloud-boot/src/vendor/notice/**/*.ts',
|
||||
'libs/wwjcloud-boot/src/vendor/sms/**/*.ts',
|
||||
'libs/wwjcloud-boot/src/vendor/pay/**/*.ts',
|
||||
'libs/wwjcloud-boot/src/vendor/upload/**/*.ts',
|
||||
'libs/wwjcloud-boot/src/vendor/mappers/**/*.ts',
|
||||
'libs/wwjcloud-boot/src/vendor/utils/**/*.ts',
|
||||
'libs/wwjcloud-boot/src/infra/serializer/**/*.ts',
|
||||
'libs/wwjcloud-boot/src/infra/websocket/**/*.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',
|
||||
'@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',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from "./wwjcloud-addon.module";
|
||||
export * from './wwjcloud-addon.module';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Module, DynamicModule, ForwardReference, Type } from "@nestjs/common";
|
||||
import { ADDON_REGISTRY } from "./registry";
|
||||
import { Module, DynamicModule, ForwardReference, Type } from '@nestjs/common';
|
||||
import { ADDON_REGISTRY } from './registry';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
@@ -11,7 +11,7 @@ export class AddonModule {
|
||||
const enabledKeys = Object.keys(process.env).filter(
|
||||
(k) =>
|
||||
/^ADDON_.+_ENABLED$/.test(k) &&
|
||||
["true", "1", "yes"].includes(String(process.env[k]).toLowerCase()),
|
||||
['true', '1', 'yes'].includes(String(process.env[k]).toLowerCase()),
|
||||
);
|
||||
|
||||
const imports = enabledKeys
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export const TASK_FAILED_EVENT = "task.failed";
|
||||
export const TASK_RECOVERY_REQUESTED_EVENT = "task.recovery.requested";
|
||||
export const TASK_RECOVERY_COMPLETED_EVENT = "task.recovery.completed";
|
||||
export const TASK_FAILED_EVENT = 'task.failed';
|
||||
export const TASK_RECOVERY_REQUESTED_EVENT = 'task.recovery.requested';
|
||||
export const TASK_RECOVERY_COMPLETED_EVENT = 'task.recovery.completed';
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
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 } 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: '直接生成业务模块(结构化参数)' })
|
||||
generateModuleDirect(@Body() request: ModuleGenerateRequest) {
|
||||
const files = this.moduleGenerator.generate(request);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
moduleName: request.moduleName,
|
||||
fileCount: files.length,
|
||||
files: files.map((f) => ({
|
||||
path: f.path,
|
||||
type: f.type,
|
||||
description: f.description,
|
||||
contentLength: f.content.length,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取框架规范信息
|
||||
*/
|
||||
@Get('knowledge')
|
||||
@ApiOperation({ summary: '获取框架规范知识' })
|
||||
getKnowledge() {
|
||||
return {
|
||||
techStack: this.knowledge.getTechStack(),
|
||||
layers: this.knowledge.getLayerArchitecture(),
|
||||
naming: this.knowledge.getNamingConventions(),
|
||||
existingModules: this.knowledge.getExistingModules(),
|
||||
prohibitions: this.knowledge.getProhibitions(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已有模块列表
|
||||
*/
|
||||
@Get('modules')
|
||||
@ApiOperation({ summary: '获取已有业务模块列表' })
|
||||
getModules() {
|
||||
return {
|
||||
adminapi: this.knowledge.getExistingModules('adminapi'),
|
||||
api: this.knowledge.getExistingModules('api'),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { FrameworkKnowledgeService } from './framework-knowledge.service';
|
||||
import { EntityGenerator } from './entity.generator';
|
||||
import { ControllerGenerator } from './controller.generator';
|
||||
import { ServiceGenerator } from './service.generator';
|
||||
import { DtoGenerator } from './dto.generator';
|
||||
import { SqlGenerator } from './sql.generator';
|
||||
import { ModuleGenerator } from './module.generator';
|
||||
import { CodegenSkill } from './codegen.skill';
|
||||
import { AiGenerateController } from './ai-generate.controller';
|
||||
|
||||
/**
|
||||
* AI 代码生成模块
|
||||
*
|
||||
* 借鉴 NiuCloud Lite AI 的 Skills 模块化开发规范,
|
||||
* 提供框架级代码生成能力:
|
||||
* - 实体生成器(MySQL → TypeORM)
|
||||
* - 控制器生成器(PHP → NestJS)
|
||||
* - 服务生成器(PHP Service → NestJS Service)
|
||||
* - DTO 生成器(PHP Validate → class-validator)
|
||||
* - SQL 生成器(建表脚本)
|
||||
* - 完整模块脚手架(一键生成全套)
|
||||
*/
|
||||
@Module({
|
||||
providers: [
|
||||
FrameworkKnowledgeService,
|
||||
EntityGenerator,
|
||||
ControllerGenerator,
|
||||
ServiceGenerator,
|
||||
DtoGenerator,
|
||||
SqlGenerator,
|
||||
ModuleGenerator,
|
||||
CodegenSkill,
|
||||
AiGenerateController,
|
||||
],
|
||||
exports: [FrameworkKnowledgeService, ModuleGenerator, CodegenSkill],
|
||||
controllers: [AiGenerateController],
|
||||
})
|
||||
export class AiGeneratorModule {}
|
||||
@@ -0,0 +1,165 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
ISkill,
|
||||
SkillDefinition,
|
||||
SkillContext,
|
||||
SkillResult,
|
||||
} from '../skills/skill.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 执行上下文
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
async execute(
|
||||
toolName: string,
|
||||
argsJson: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_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_${String(args.moduleName) || 'demo'}`,
|
||||
fields:
|
||||
(args.fields as import('./generator.interface').TableField[]) || [],
|
||||
endpoints:
|
||||
(args.endpoints as ModuleGenerateRequest['endpoints']) || 'adminapi',
|
||||
phpMethods:
|
||||
(args.methods as import('./generator.interface').PhpMethod[]) || [],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
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);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
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 {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
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 方法
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
private getDefaultMethods(_endpoint: 'adminapi' | 'api'): PhpMethod[] {
|
||||
return [
|
||||
{
|
||||
name: 'lists',
|
||||
httpMethod: 'GET',
|
||||
route: 'lists',
|
||||
description: '获取列表',
|
||||
params: [],
|
||||
},
|
||||
{
|
||||
name: 'info',
|
||||
httpMethod: 'GET',
|
||||
route: 'info/:id',
|
||||
description: '获取详情',
|
||||
params: ['id'],
|
||||
},
|
||||
{
|
||||
name: 'add',
|
||||
httpMethod: 'POST',
|
||||
route: 'add',
|
||||
description: '新增',
|
||||
params: [],
|
||||
},
|
||||
{
|
||||
name: 'edit',
|
||||
httpMethod: 'PUT',
|
||||
route: 'edit/:id',
|
||||
description: '编辑',
|
||||
params: ['id'],
|
||||
},
|
||||
{
|
||||
name: 'del',
|
||||
httpMethod: 'DELETE',
|
||||
route: 'del/:id',
|
||||
description: '删除',
|
||||
params: ['id'],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 下划线转驼峰
|
||||
*/
|
||||
private toCamelCase(str: string): string {
|
||||
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* 转 PascalCase
|
||||
*/
|
||||
private toPascalCase(str: string): string {
|
||||
const camel = this.toCamelCase(str);
|
||||
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
ICodeGenerator,
|
||||
GeneratedFile,
|
||||
ModuleGenerateRequest,
|
||||
TableField,
|
||||
} from './generator.interface';
|
||||
import { DB_TYPE_MAPPING } from './framework-knowledge';
|
||||
|
||||
/**
|
||||
* DTO 文件生成器
|
||||
*
|
||||
* 生成 class-validator 风格的参数(Param)和视图(VO)对象,
|
||||
* 对应 PHP 的 validate 验证器。
|
||||
*/
|
||||
@Injectable()
|
||||
export class DtoGenerator implements ICodeGenerator {
|
||||
/**
|
||||
* 生成 DTO 文件
|
||||
*/
|
||||
generate(request: ModuleGenerateRequest): GeneratedFile[] {
|
||||
const files: GeneratedFile[] = [];
|
||||
|
||||
// 生成参数 DTO
|
||||
files.push(this.generateParamDto(request, 'admin'));
|
||||
if (request.endpoints === 'api' || request.endpoints === 'both') {
|
||||
files.push(this.generateParamDto(request, 'api'));
|
||||
}
|
||||
|
||||
// 生成 VO DTO
|
||||
files.push(this.generateVoDto(request, 'admin'));
|
||||
if (request.endpoints === 'api' || request.endpoints === 'both') {
|
||||
files.push(this.generateVoDto(request, 'api'));
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成参数 DTO(对应 PHP validate)
|
||||
*/
|
||||
private generateParamDto(
|
||||
request: ModuleGenerateRequest,
|
||||
layer: 'admin' | 'api',
|
||||
): GeneratedFile {
|
||||
const { moduleName, fields } = request;
|
||||
const className = `${this.toPascalCase(moduleName)}Param`;
|
||||
|
||||
// 排除主键和系统字段
|
||||
const inputFields = fields.filter(
|
||||
(f) =>
|
||||
!f.isPrimary &&
|
||||
!f.isAutoIncrement &&
|
||||
f.name !== 'create_time' &&
|
||||
f.name !== 'update_time' &&
|
||||
f.name !== 'delete_time' &&
|
||||
f.name !== 'is_del',
|
||||
);
|
||||
|
||||
const propertiesCode = inputFields
|
||||
.map((f) => this.generateParamProperty(f))
|
||||
.join('\n\n ');
|
||||
|
||||
const content = `import { IsString, IsNumber, IsOptional, IsArray, IsBoolean } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* ${request.description || moduleName} 参数 DTO
|
||||
* 对应 PHP: app/validate/${moduleName}/${this.toPascalCase(moduleName)}.php
|
||||
*/
|
||||
export class ${className} {
|
||||
${propertiesCode}
|
||||
}
|
||||
`;
|
||||
|
||||
return {
|
||||
path: `libs/wwjcloud-core/src/dtos/${layer}/${moduleName}/param/${moduleName}.param.ts`,
|
||||
content,
|
||||
type: 'dto',
|
||||
description: `${moduleName} ${layer} 参数 DTO`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 VO DTO(视图对象)
|
||||
*/
|
||||
private generateVoDto(
|
||||
request: ModuleGenerateRequest,
|
||||
layer: 'admin' | 'api',
|
||||
): GeneratedFile {
|
||||
const { moduleName, fields } = request;
|
||||
const className = `${this.toPascalCase(moduleName)}Vo`;
|
||||
|
||||
const propertiesCode = fields
|
||||
.map((f) => this.generateVoProperty(f))
|
||||
.join('\n\n ');
|
||||
|
||||
const content = `import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* ${request.description || moduleName} 视图对象
|
||||
*/
|
||||
export class ${className} {
|
||||
${propertiesCode}
|
||||
}
|
||||
`;
|
||||
|
||||
return {
|
||||
path: `libs/wwjcloud-core/src/dtos/${layer}/${moduleName}/vo/${moduleName}.vo.ts`,
|
||||
content,
|
||||
type: 'dto',
|
||||
description: `${moduleName} ${layer} VO DTO`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成参数属性(带 class-validator 装饰器)
|
||||
*/
|
||||
private generateParamProperty(field: TableField): string {
|
||||
const camelName = this.toCamelCase(field.name);
|
||||
const typeMapping = this.mapType(field.mysqlType);
|
||||
const decorators: string[] = [];
|
||||
const apiDecorator = field.nullable ? 'ApiPropertyOptional' : 'ApiProperty';
|
||||
|
||||
if (field.nullable) {
|
||||
decorators.push('IsOptional()');
|
||||
}
|
||||
|
||||
switch (typeMapping.tsType) {
|
||||
case 'string':
|
||||
decorators.push('IsString()');
|
||||
break;
|
||||
case 'number':
|
||||
decorators.push('IsNumber()');
|
||||
break;
|
||||
case 'boolean':
|
||||
decorators.push('IsBoolean()');
|
||||
break;
|
||||
}
|
||||
|
||||
const decoratorStr = decorators.map((d) => ` @${d}`).join('\n');
|
||||
const apiStr = ` @${apiDecorator}({ description: '${field.comment || field.name}' })`;
|
||||
|
||||
return `${decoratorStr}\n${apiStr}\n ${camelName}: ${typeMapping.tsType};`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 VO 属性(仅 Swagger 注解)
|
||||
*/
|
||||
private generateVoProperty(field: TableField): string {
|
||||
const camelName = this.toCamelCase(field.name);
|
||||
const typeMapping = this.mapType(field.mysqlType);
|
||||
|
||||
return ` @ApiProperty({ description: '${field.comment || field.name}' })
|
||||
${camelName}: ${typeMapping.tsType};`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射 MySQL 类型
|
||||
*/
|
||||
private mapType(mysqlType: string): { typeormType: string; tsType: string } {
|
||||
const baseType = mysqlType
|
||||
.replace(/\(.*\)/, '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
return (
|
||||
DB_TYPE_MAPPING[baseType] ?? { typeormType: 'varchar', tsType: 'string' }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下划线转驼峰
|
||||
*/
|
||||
private toCamelCase(str: string): string {
|
||||
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* 转 PascalCase
|
||||
*/
|
||||
private toPascalCase(str: string): string {
|
||||
const camel = this.toCamelCase(str);
|
||||
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
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 { tableName, fields } = request;
|
||||
const className = this.toPascalCase(
|
||||
tableName.replace(/^nc_/, '').replace(/_/g, ' '),
|
||||
);
|
||||
const columnsCode = fields
|
||||
.map((f) => this.generateColumn(f))
|
||||
.join('\n\n ');
|
||||
|
||||
const content = `import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||
|
||||
/**
|
||||
* ${request.description || `${tableName} 实体`}
|
||||
* 对应数据库表: ${tableName}
|
||||
*/
|
||||
@Entity('${tableName}')
|
||||
export class ${className} {
|
||||
${columnsCode}
|
||||
}
|
||||
`;
|
||||
|
||||
return {
|
||||
path: `libs/wwjcloud-core/src/entities/${tableName.replace(/^nc_/, '')}.entity.ts`,
|
||||
content,
|
||||
type: 'entity',
|
||||
description: `${tableName} 实体文件`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成单个字段的 TypeORM 列定义
|
||||
*/
|
||||
private generateColumn(field: TableField): string {
|
||||
if (field.isPrimary) {
|
||||
return ` @PrimaryGeneratedColumn({ name: '${field.name}'${field.comment ? `, comment: '${field.comment}'` : ''} })
|
||||
${this.toCamelCase(field.name)}: number;`;
|
||||
}
|
||||
|
||||
const typeMapping = this.mapType(field.mysqlType);
|
||||
const columnOptions: string[] = [];
|
||||
|
||||
columnOptions.push(`name: '${field.name}'`);
|
||||
columnOptions.push(`type: '${typeMapping.typeormType}'`);
|
||||
|
||||
if (field.mysqlType.match(/\(\d+\)/)) {
|
||||
const length = field.mysqlType.match(/\((\d+)\)/)?.[1];
|
||||
if (length) columnOptions.push(`length: ${length}`);
|
||||
}
|
||||
if (field.unsigned) columnOptions.push(`unsigned: true`);
|
||||
if (field.defaultValue !== undefined) {
|
||||
columnOptions.push(`default: ${this.formatDefault(field.defaultValue)}`);
|
||||
}
|
||||
if (!field.nullable && field.defaultValue === undefined) {
|
||||
columnOptions.push(`default: ''`);
|
||||
}
|
||||
if (field.nullable) columnOptions.push(`nullable: true`);
|
||||
if (field.comment) columnOptions.push(`comment: '${field.comment}'`);
|
||||
|
||||
return ` @Column({ ${columnOptions.join(', ')} })
|
||||
${this.toCamelCase(field.name)}: ${typeMapping.tsType};`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射 MySQL 类型到 TypeORM + TypeScript 类型
|
||||
*/
|
||||
private mapType(mysqlType: string): { typeormType: string; tsType: string } {
|
||||
const baseType = mysqlType
|
||||
.replace(/\(.*\)/, '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
return (
|
||||
DB_TYPE_MAPPING[baseType] ?? { typeormType: 'varchar', tsType: 'string' }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化默认值
|
||||
*/
|
||||
private formatDefault(value: string): string {
|
||||
if (value === 'NULL' || value === 'null') return 'null';
|
||||
if (value.match(/^\d+$/)) return value;
|
||||
return `'${value}'`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下划线命名转驼峰
|
||||
*/
|
||||
private toCamelCase(str: string): string {
|
||||
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* 下划线命名转 PascalCase
|
||||
*/
|
||||
private toPascalCase(str: string): string {
|
||||
const camel = this.toCamelCase(str);
|
||||
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
FRAMEWORK_TECH_STACK,
|
||||
LAYER_ARCHITECTURE,
|
||||
ACTUAL_DIRECTORY_STRUCTURE,
|
||||
NAMING_CONVENTIONS,
|
||||
PHP_NESTJS_MAPPING,
|
||||
STRICT_PROHIBITIONS,
|
||||
DB_TYPE_MAPPING,
|
||||
EXISTING_MODULES,
|
||||
CODE_TEMPLATES,
|
||||
QUALITY_STANDARDS,
|
||||
} from './framework-knowledge';
|
||||
|
||||
/**
|
||||
* 框架规范知识查询服务
|
||||
*
|
||||
* 为代码生成 Skills 提供规范知识的统一查询入口,
|
||||
* 确保 AI 生成的代码严格符合项目规范。
|
||||
*/
|
||||
@Injectable()
|
||||
export class FrameworkKnowledgeService {
|
||||
private readonly logger = new Logger(FrameworkKnowledgeService.name);
|
||||
|
||||
/**
|
||||
* 获取框架技术栈信息
|
||||
*/
|
||||
getTechStack(): typeof FRAMEWORK_TECH_STACK {
|
||||
return FRAMEWORK_TECH_STACK;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分层架构信息
|
||||
*/
|
||||
getLayerArchitecture(): typeof LAYER_ARCHITECTURE {
|
||||
return LAYER_ARCHITECTURE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实际目录结构
|
||||
*/
|
||||
getDirectoryStructure(): typeof ACTUAL_DIRECTORY_STRUCTURE {
|
||||
return ACTUAL_DIRECTORY_STRUCTURE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取命名规范
|
||||
*/
|
||||
getNamingConventions(): typeof NAMING_CONVENTIONS {
|
||||
return NAMING_CONVENTIONS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 PHP → NestJS 映射规则
|
||||
*/
|
||||
getPhpNestjsMapping(): typeof PHP_NESTJS_MAPPING {
|
||||
return PHP_NESTJS_MAPPING;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取绝对禁止规则
|
||||
*/
|
||||
getProhibitions(): readonly string[] {
|
||||
return STRICT_PROHIBITIONS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模块名生成文件路径
|
||||
* @param module 模块名(如 'member')
|
||||
* @param type 文件类型
|
||||
* @param layer 层级(adminapi/api/admin/api/core)
|
||||
* @param fileName 文件名
|
||||
*/
|
||||
resolveFilePath(
|
||||
module: string,
|
||||
type: 'controller' | 'service' | 'entity' | 'dto',
|
||||
layer?: string,
|
||||
fileName?: string,
|
||||
): string {
|
||||
const structure = ACTUAL_DIRECTORY_STRUCTURE;
|
||||
|
||||
switch (type) {
|
||||
case 'controller': {
|
||||
const ctrlLayer = layer === 'api' ? 'api' : 'adminapi';
|
||||
const name = fileName ?? `${module}.controller.ts`;
|
||||
return `${structure.controllers[ctrlLayer]}${module}/${name}`;
|
||||
}
|
||||
case 'service': {
|
||||
const svcLayer =
|
||||
layer === 'api' ? 'api' : layer === 'core' ? 'core' : 'admin';
|
||||
const name = fileName ?? `${module}-service-impl.service.ts`;
|
||||
return `${structure.services[svcLayer]}${module}/${name}`;
|
||||
}
|
||||
case 'entity': {
|
||||
const name = fileName ?? `${module}.entity.ts`;
|
||||
return `${structure.entities}${name}`;
|
||||
}
|
||||
case 'dto': {
|
||||
const dtoLayer =
|
||||
layer === 'api' ? 'api' : layer === 'core' ? 'core' : 'admin';
|
||||
const name = fileName ?? `${module}.param.ts`;
|
||||
return `${structure.dtos[dtoLayer]}${module}/${name}`;
|
||||
}
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 MySQL → TypeORM 类型映射
|
||||
* @param mysqlType MySQL 列类型
|
||||
*/
|
||||
mapDbType(
|
||||
mysqlType: string,
|
||||
): { typeormType: string; tsType: string } | undefined {
|
||||
// 提取基础类型(去掉长度修饰符,如 varchar(255) → varchar)
|
||||
const baseType = mysqlType
|
||||
.replace(/\(.*\)/, '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
return DB_TYPE_MAPPING[baseType];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查模块是否已存在
|
||||
* @param moduleName 模块名
|
||||
* @param layer 层级
|
||||
*/
|
||||
isModuleExists(
|
||||
moduleName: string,
|
||||
layer: 'adminapi' | 'api' = 'adminapi',
|
||||
): boolean {
|
||||
return (EXISTING_MODULES[layer] as readonly string[]).includes(moduleName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已有模块列表
|
||||
*/
|
||||
getExistingModules(layer?: 'adminapi' | 'api'): string[] {
|
||||
if (layer) return [...EXISTING_MODULES[layer]];
|
||||
return [
|
||||
...new Set([...EXISTING_MODULES.adminapi, ...EXISTING_MODULES.api]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取代码模板片段
|
||||
*/
|
||||
getCodeTemplates(): typeof CODE_TEMPLATES {
|
||||
return CODE_TEMPLATES;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取质量标准
|
||||
*/
|
||||
getQualityStandards(): typeof QUALITY_STANDARDS {
|
||||
return QUALITY_STANDARDS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成规范合规性提示(嵌入到 LLM System Prompt 中)
|
||||
*/
|
||||
getSystemPromptAddendum(): string {
|
||||
return `
|
||||
## WWJCloud v1 框架规范(必须严格遵守)
|
||||
|
||||
### 技术栈
|
||||
- 后端: NestJS 11 + TypeScript 5.5+ + TypeORM 0.3
|
||||
- 数据库: MySQL 8.0,字段名与 PHP 项目 100% 一致
|
||||
|
||||
### 绝对禁止
|
||||
${STRICT_PROHIBITIONS.map((p, i) => `${i + 1}. ${p}`).join('\n')}
|
||||
|
||||
### 目录结构(实际采用)
|
||||
- 控制器: controllers/adminapi/{module}/ 或 controllers/api/{module}/
|
||||
- 服务: services/admin/{module}/impl/ 或 services/api/{module}/impl/
|
||||
- 实体: entities/(扁平目录)
|
||||
- DTO: dtos/admin/{module}/param/ 和 dtos/admin/{module}/vo/
|
||||
|
||||
### 命名规范
|
||||
- 实体文件: {name}.entity.ts,类名 PascalCase(与 PHP 模型一致)
|
||||
- 控制器: {name}.controller.ts
|
||||
- 服务: {name}-service-impl.service.ts
|
||||
- DTO 参数: {name}.param.ts
|
||||
- DTO 视图: {name}.vo.ts
|
||||
|
||||
### 质量标准
|
||||
- 数据库字段映射准确率: 100%
|
||||
- PHP 方法对应准确率: 100%
|
||||
- 代码必须可直接运行(npm run build 零错误)
|
||||
`.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* WWJCloud v1 框架规范知识库
|
||||
*
|
||||
* 将 4 份规范文件结构化为 AI 可消费的知识格式,
|
||||
* 供代码生成 Skills 查询和使用。
|
||||
*
|
||||
* 知识来源:
|
||||
* - common-layer-standards.md(Common 层模块化设计标准)
|
||||
* - development_constraints.md(开发约束规范)
|
||||
* - nestjs_file_generation_standards.md(NestJS 文件生成标准)
|
||||
* - project_rules.md(项目规则)
|
||||
*/
|
||||
|
||||
/** 框架技术栈 */
|
||||
export const FRAMEWORK_TECH_STACK = {
|
||||
backend: 'NestJS 11',
|
||||
language: 'TypeScript 5.5+',
|
||||
orm: 'TypeORM 0.3',
|
||||
database: 'MySQL 8.0',
|
||||
cache: 'Redis (ioredis)',
|
||||
queue: 'BullMQ',
|
||||
validation: 'class-validator + class-transformer',
|
||||
apiDoc: 'Swagger (@nestjs/swagger)',
|
||||
auth: 'JWT (passport-jwt)',
|
||||
config: '@nestjs/config + joi',
|
||||
} as const;
|
||||
|
||||
/** 项目分层架构 */
|
||||
export const LAYER_ARCHITECTURE = {
|
||||
layers: [
|
||||
{
|
||||
name: 'boot',
|
||||
alias: '@wwjBoot',
|
||||
path: 'libs/wwjcloud-boot/src',
|
||||
description: '基础设施层(认证/缓存/队列/Vendor)',
|
||||
},
|
||||
{
|
||||
name: 'core',
|
||||
alias: '@wwjCore',
|
||||
path: 'libs/wwjcloud-core/src',
|
||||
description: '核心业务层(控制器/服务/实体/DTO)',
|
||||
},
|
||||
{
|
||||
name: 'ai',
|
||||
alias: '@wwjAi',
|
||||
path: 'libs/wwjcloud-ai/src',
|
||||
description: 'AI 智能层(Agent/Skills/Memory)',
|
||||
},
|
||||
{
|
||||
name: 'addon',
|
||||
alias: '@wwjAddon',
|
||||
path: 'libs/wwjcloud-addon/src',
|
||||
description: '插件扩展层',
|
||||
},
|
||||
],
|
||||
dependencyRule: 'boot → core (单向),ai 独立,addon 独立',
|
||||
} as const;
|
||||
|
||||
/** 实际目录结构(当前 v1 采用的方式) */
|
||||
export const ACTUAL_DIRECTORY_STRUCTURE = {
|
||||
description: '按技术层级分目录(非按业务域分目录)',
|
||||
controllers: {
|
||||
adminapi: 'libs/wwjcloud-core/src/controllers/adminapi/{module}/',
|
||||
api: 'libs/wwjcloud-core/src/controllers/api/{module}/',
|
||||
core: 'libs/wwjcloud-core/src/controllers/core/',
|
||||
},
|
||||
services: {
|
||||
admin: 'libs/wwjcloud-core/src/services/admin/{module}/impl/',
|
||||
api: 'libs/wwjcloud-core/src/services/api/{module}/impl/',
|
||||
core: 'libs/wwjcloud-core/src/services/core/{module}/',
|
||||
},
|
||||
entities: 'libs/wwjcloud-core/src/entities/',
|
||||
dtos: {
|
||||
admin: 'libs/wwjcloud-core/src/dtos/admin/{module}/',
|
||||
api: 'libs/wwjcloud-core/src/dtos/api/{module}/',
|
||||
core: 'libs/wwjcloud-core/src/dtos/core/{module}/',
|
||||
},
|
||||
modules: {
|
||||
controller: 'libs/wwjcloud-core/src/controller.module.ts',
|
||||
service: 'libs/wwjcloud-core/src/service.module.ts',
|
||||
entity: 'libs/wwjcloud-core/src/entity.module.ts',
|
||||
common: 'libs/wwjcloud-core/src/common.module.ts',
|
||||
},
|
||||
} as const;
|
||||
|
||||
/** 规范文档定义的目录结构(目标结构,当前未完全采用) */
|
||||
export const STANDARD_DIRECTORY_STRUCTURE = {
|
||||
description: '按业务域模块化分目录(规范文档定义的目标结构)',
|
||||
pattern: 'src/common/{module-name}/',
|
||||
structure: [
|
||||
'{module-name}.module.ts',
|
||||
'controllers/adminapi/',
|
||||
'controllers/api/',
|
||||
'services/admin/',
|
||||
'services/api/',
|
||||
'services/core/',
|
||||
'entity/',
|
||||
'dto/admin/',
|
||||
'dto/api/',
|
||||
],
|
||||
} as const;
|
||||
|
||||
/** 文件命名规范 */
|
||||
export const NAMING_CONVENTIONS = {
|
||||
entity: {
|
||||
pattern: '{name}.entity.ts',
|
||||
example: 'sys-user.entity.ts',
|
||||
classPattern: 'PascalCase',
|
||||
classExample: 'SysUser',
|
||||
note: '与 PHP 模型类名保持一致',
|
||||
},
|
||||
controller: {
|
||||
pattern: '{name}.controller.ts',
|
||||
example: 'user.controller.ts',
|
||||
classPattern: 'PascalCase + Controller',
|
||||
classExample: 'UserController',
|
||||
},
|
||||
service: {
|
||||
pattern: '{name}-service-impl.service.ts',
|
||||
example: 'user-service-impl.service.ts',
|
||||
classPattern: 'PascalCase + ServiceImpl',
|
||||
classExample: 'UserServiceImpl',
|
||||
note: '实际项目中使用 -service-impl 后缀',
|
||||
},
|
||||
dto: {
|
||||
param: 'dtos/{layer}/{module}/param/{name}.param.ts',
|
||||
vo: 'dtos/{layer}/{module}/vo/{name}.vo.ts',
|
||||
example: 'dtos/admin/member/param/create-member.param.ts',
|
||||
},
|
||||
module: {
|
||||
pattern: '{name}.module.ts',
|
||||
example: 'user.module.ts',
|
||||
},
|
||||
} as const;
|
||||
|
||||
/** PHP → NestJS 映射规则 */
|
||||
export const PHP_NESTJS_MAPPING = {
|
||||
controller: {
|
||||
php: 'app/adminapi/controller/{module}/{Name}.php',
|
||||
nestjs: 'controllers/adminapi/{module}/{name}.controller.ts',
|
||||
methodMapping: 'public 方法直接对应 @Get/@Post/@Put/@Delete 装饰器方法',
|
||||
},
|
||||
service: {
|
||||
php: 'app/service/{layer}/{module}/{Name}Service.php',
|
||||
nestjs: 'services/{layer}/{module}/impl/{name}-service-impl.service.ts',
|
||||
},
|
||||
model: {
|
||||
php: 'app/model/{module}/{Name}.php',
|
||||
nestjs: 'entities/{name}.entity.ts',
|
||||
fieldMapping: '字段名 100% 保持一致,类型对应 MySQL 列类型',
|
||||
},
|
||||
validate: {
|
||||
php: 'app/validate/{module}/{Name}.php',
|
||||
nestjs: 'dtos/{layer}/{module}/param/{name}.param.ts (class-validator)',
|
||||
},
|
||||
} as const;
|
||||
|
||||
/** 六条绝对禁止规则 */
|
||||
export const STRICT_PROHIBITIONS = [
|
||||
'禁止自创业务逻辑 — 所有业务逻辑必须严格按照 PHP 项目实现',
|
||||
'禁止假设数据结构 — 所有数据结构必须基于真实数据库表结构',
|
||||
'禁止使用默认值 — 所有字段、方法、配置必须基于真实 PHP 代码',
|
||||
'禁止编写骨架代码 — 不允许生成空方法或 TODO 注释',
|
||||
'禁止写死数据 — 不允许硬编码任何业务数据',
|
||||
'禁止猜测 API 接口 — 所有接口必须基于 PHP 控制器真实方法',
|
||||
] as const;
|
||||
|
||||
/** 数据库字段类型映射(MySQL → TypeORM) */
|
||||
export const DB_TYPE_MAPPING: Record<
|
||||
string,
|
||||
{ typeormType: string; tsType: string }
|
||||
> = {
|
||||
int: { typeormType: 'int', tsType: 'number' },
|
||||
tinyint: { typeormType: 'tinyint', tsType: 'number' },
|
||||
bigint: { typeormType: 'bigint', tsType: 'string' },
|
||||
varchar: { typeormType: 'varchar', tsType: 'string' },
|
||||
text: { typeormType: 'text', tsType: 'string' },
|
||||
longtext: { typeormType: 'longtext', tsType: 'string' },
|
||||
decimal: { typeormType: 'decimal', tsType: 'number' },
|
||||
float: { typeormType: 'float', tsType: 'number' },
|
||||
double: { typeormType: 'double', tsType: 'number' },
|
||||
datetime: { typeormType: 'datetime', tsType: 'Date' },
|
||||
timestamp: { typeormType: 'timestamp', tsType: 'number' },
|
||||
json: { typeormType: 'json', tsType: 'Record<string, unknown>' },
|
||||
};
|
||||
|
||||
/** 已有业务模块清单(从 PHP 项目提取) */
|
||||
export const EXISTING_MODULES = {
|
||||
adminapi: [
|
||||
'addon',
|
||||
'aliapp',
|
||||
'applet',
|
||||
'auth',
|
||||
'channel',
|
||||
'dict',
|
||||
'diy',
|
||||
'generator',
|
||||
'home',
|
||||
'index',
|
||||
'login',
|
||||
'member',
|
||||
'niucloud',
|
||||
'notice',
|
||||
'pay',
|
||||
'poster',
|
||||
'site',
|
||||
'stat',
|
||||
'sys',
|
||||
'upload',
|
||||
'user',
|
||||
'verify',
|
||||
'weapp',
|
||||
'wechat',
|
||||
'wxoplatform',
|
||||
],
|
||||
api: [
|
||||
'addon',
|
||||
'agreement',
|
||||
'channel',
|
||||
'diy',
|
||||
'login',
|
||||
'member',
|
||||
'pay',
|
||||
'poster',
|
||||
'sys',
|
||||
'upload',
|
||||
'weapp',
|
||||
'wechat',
|
||||
],
|
||||
} as const;
|
||||
|
||||
/** 代码模板片段 */
|
||||
export const CODE_TEMPLATES = {
|
||||
entityHeader: `import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';`,
|
||||
controllerHeader: `import { Controller, Get, Post, Put, Delete, Body, Param, Query } from '@nestjs/common';`,
|
||||
serviceHeader: `import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';`,
|
||||
dtoParamHeader: `import { IsString, IsNumber, IsOptional, IsArray } from 'class-validator';`,
|
||||
} as const;
|
||||
|
||||
/** 质量标准 */
|
||||
export const QUALITY_STANDARDS = {
|
||||
dbFieldMappingAccuracy: '100%',
|
||||
phpMethodCorrespondence: '100%',
|
||||
businessLogicConsistency: '100%',
|
||||
codeRunnable: '100%',
|
||||
namingConventionCompliance: '100%',
|
||||
buildMustPass: true,
|
||||
swaggerAnnotationsRequired: true,
|
||||
} as const;
|
||||
@@ -0,0 +1,210 @@
|
||||
import { LlmToolDefinition } from '../providers/llm-provider.interface';
|
||||
|
||||
/**
|
||||
* 代码生成结果
|
||||
*/
|
||||
export interface GeneratedFile {
|
||||
/** 文件路径(相对于项目根目录) */
|
||||
path: string;
|
||||
/** 文件内容 */
|
||||
content: string;
|
||||
/** 文件类型 */
|
||||
type:
|
||||
| 'entity'
|
||||
| 'controller'
|
||||
| 'service'
|
||||
| 'dto'
|
||||
| 'sql'
|
||||
| 'module'
|
||||
| 'other';
|
||||
/** 描述 */
|
||||
description: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模块生成请求
|
||||
*/
|
||||
export interface ModuleGenerateRequest {
|
||||
/** 模块名(如 'member', 'order') */
|
||||
moduleName: string;
|
||||
/** 模块描述(中文) */
|
||||
description: string;
|
||||
/** 数据库表名 */
|
||||
tableName: string;
|
||||
/** 表字段定义 */
|
||||
fields: TableField[];
|
||||
/** 需要生成的端(adminapi / api / both) */
|
||||
endpoints: 'adminapi' | 'api' | 'both';
|
||||
/** PHP 控制器方法列表(可选,用于对齐) */
|
||||
phpMethods?: PhpMethod[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库表字段定义
|
||||
*/
|
||||
export interface TableField {
|
||||
/** 字段名 */
|
||||
name: string;
|
||||
/** MySQL 类型(如 varchar(255), int, text) */
|
||||
mysqlType: string;
|
||||
/** 是否主键 */
|
||||
isPrimary?: boolean;
|
||||
/** 是否自增 */
|
||||
isAutoIncrement?: boolean;
|
||||
/** 是否允许 NULL */
|
||||
nullable?: boolean;
|
||||
/** 默认值 */
|
||||
defaultValue?: string;
|
||||
/** 字段注释 */
|
||||
comment?: string;
|
||||
/** 是否无符号 */
|
||||
unsigned?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* PHP 控制器方法定义
|
||||
*/
|
||||
export interface PhpMethod {
|
||||
/** 方法名 */
|
||||
name: string;
|
||||
/** HTTP 方法 */
|
||||
httpMethod: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
/** 路由路径 */
|
||||
route: string;
|
||||
/** 方法描述 */
|
||||
description: string;
|
||||
/** 参数列表 */
|
||||
params?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 代码生成器接口
|
||||
* 所有具体的生成 Skill 实现此接口
|
||||
*/
|
||||
export interface ICodeGenerator {
|
||||
/**
|
||||
* 生成代码
|
||||
* @param request 生成请求
|
||||
* @returns 生成的文件列表
|
||||
*/
|
||||
generate(request: ModuleGenerateRequest): GeneratedFile[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 代码生成 Skill 的工具定义(供 LLM Function Calling 使用)
|
||||
*/
|
||||
export const CODEGEN_TOOL_DEFINITIONS: LlmToolDefinition[] = [
|
||||
{
|
||||
name: 'generate_entity',
|
||||
description: '根据数据库表结构生成 TypeORM 实体文件',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
moduleName: { type: 'string', description: '模块名' },
|
||||
tableName: { type: 'string', description: '数据库表名' },
|
||||
fields: {
|
||||
type: 'array',
|
||||
description: '字段定义数组',
|
||||
items: { type: 'object' },
|
||||
},
|
||||
},
|
||||
required: ['moduleName', 'tableName', 'fields'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'generate_controller',
|
||||
description: '根据 PHP 控制器方法生成 NestJS 控制器文件',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
moduleName: { type: 'string', description: '模块名' },
|
||||
endpoint: { type: 'string', description: '端类型: adminapi 或 api' },
|
||||
methods: {
|
||||
type: 'array',
|
||||
description: '方法列表',
|
||||
items: { type: 'object' },
|
||||
},
|
||||
},
|
||||
required: ['moduleName', 'endpoint', 'methods'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'generate_service',
|
||||
description: '根据模块需求生成 NestJS 服务文件',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
moduleName: { type: 'string', description: '模块名' },
|
||||
endpoint: {
|
||||
type: 'string',
|
||||
description: '端类型: admin 或 api 或 core',
|
||||
},
|
||||
methods: {
|
||||
type: 'array',
|
||||
description: '方法列表',
|
||||
items: { type: 'object' },
|
||||
},
|
||||
},
|
||||
required: ['moduleName', 'endpoint'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'generate_dto',
|
||||
description: '生成 DTO 参数和视图对象',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
moduleName: { type: 'string', description: '模块名' },
|
||||
endpoint: { type: 'string', description: '端类型: admin 或 api' },
|
||||
fields: {
|
||||
type: 'array',
|
||||
description: '字段定义',
|
||||
items: { type: 'object' },
|
||||
},
|
||||
},
|
||||
required: ['moduleName', 'endpoint'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'generate_sql',
|
||||
description: '生成数据库建表 SQL 脚本',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tableName: { type: 'string', description: '表名' },
|
||||
fields: {
|
||||
type: 'array',
|
||||
description: '字段定义',
|
||||
items: { type: 'object' },
|
||||
},
|
||||
comment: { type: 'string', description: '表注释' },
|
||||
},
|
||||
required: ['tableName', 'fields'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'generate_module',
|
||||
description:
|
||||
'生成完整的业务模块(Entity + Controller + Service + DTO + SQL)',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
moduleName: { type: 'string', description: '模块名' },
|
||||
description: { type: 'string', description: '模块描述' },
|
||||
tableName: { type: 'string', description: '数据库表名' },
|
||||
fields: {
|
||||
type: 'array',
|
||||
description: '字段定义',
|
||||
items: { type: 'object' },
|
||||
},
|
||||
endpoints: { type: 'string', description: '端类型: adminapi/api/both' },
|
||||
methods: {
|
||||
type: 'array',
|
||||
description: 'PHP 方法列表',
|
||||
items: { type: 'object' },
|
||||
},
|
||||
},
|
||||
required: ['moduleName', 'tableName', 'fields'],
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,12 @@
|
||||
export * from './generator.interface';
|
||||
export * from './framework-knowledge';
|
||||
export * from './framework-knowledge.service';
|
||||
export * from './entity.generator';
|
||||
export * from './controller.generator';
|
||||
export * from './service.generator';
|
||||
export * from './dto.generator';
|
||||
export * from './sql.generator';
|
||||
export * from './module.generator';
|
||||
export * from './codegen.skill';
|
||||
export * from './ai-generate.controller';
|
||||
export * from './ai-generator.module';
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
ICodeGenerator,
|
||||
GeneratedFile,
|
||||
ModuleGenerateRequest,
|
||||
} from './generator.interface';
|
||||
import { EntityGenerator } from './entity.generator';
|
||||
import { ControllerGenerator } from './controller.generator';
|
||||
import { ServiceGenerator } from './service.generator';
|
||||
import { DtoGenerator } from './dto.generator';
|
||||
import { SqlGenerator } from './sql.generator';
|
||||
|
||||
/**
|
||||
* 完整模块生成器(脚手架)
|
||||
*
|
||||
* 组合所有子生成器,一键生成完整的业务模块:
|
||||
* Entity + Controller + Service (admin/api/core) + DTO (param/vo) + SQL
|
||||
*
|
||||
* 借鉴 NiuCloud Lite AI 的 Skills 模块化开发规范,
|
||||
* 适配 NestJS 技术栈。
|
||||
*/
|
||||
@Injectable()
|
||||
export class ModuleGenerator implements ICodeGenerator {
|
||||
private readonly logger = new Logger(ModuleGenerator.name);
|
||||
|
||||
constructor(
|
||||
private readonly entityGenerator: EntityGenerator,
|
||||
private readonly controllerGenerator: ControllerGenerator,
|
||||
private readonly serviceGenerator: ServiceGenerator,
|
||||
private readonly dtoGenerator: DtoGenerator,
|
||||
private readonly sqlGenerator: SqlGenerator,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 生成完整业务模块
|
||||
* @param request 模块生成请求
|
||||
* @returns 所有生成的文件列表
|
||||
*/
|
||||
generate(request: ModuleGenerateRequest): GeneratedFile[] {
|
||||
this.logger.log(`[ModuleGenerator] 开始生成模块: ${request.moduleName}`);
|
||||
const startTime = Date.now();
|
||||
|
||||
const files: GeneratedFile[] = [];
|
||||
|
||||
// 1. 生成实体
|
||||
files.push(...this.entityGenerator.generate(request));
|
||||
|
||||
// 2. 生成控制器
|
||||
files.push(...this.controllerGenerator.generate(request));
|
||||
|
||||
// 3. 生成服务
|
||||
files.push(...this.serviceGenerator.generate(request));
|
||||
|
||||
// 4. 生成 DTO
|
||||
files.push(...this.dtoGenerator.generate(request));
|
||||
|
||||
// 5. 生成 SQL
|
||||
files.push(...this.sqlGenerator.generate(request));
|
||||
|
||||
// 6. 生成模块注册代码提示
|
||||
files.push(this.generateModuleRegistrationHint(request));
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
this.logger.log(
|
||||
`[ModuleGenerator] 模块 ${request.moduleName} 生成完成: ${files.length} 个文件 (${duration}ms)`,
|
||||
);
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成模块注册提示文件
|
||||
*/
|
||||
private generateModuleRegistrationHint(
|
||||
request: ModuleGenerateRequest,
|
||||
): GeneratedFile {
|
||||
const { moduleName } = request;
|
||||
const entityName = this.toPascalCase(
|
||||
request.tableName.replace(/^nc_/, '').replace(/_/g, ' '),
|
||||
);
|
||||
|
||||
const content = `/**
|
||||
* ${moduleName} 模块注册指南
|
||||
*
|
||||
* 生成完成后,需要手动完成以下注册步骤:
|
||||
*
|
||||
* 1. 在 libs/wwjcloud-core/src/entity.module.ts 中注册实体:
|
||||
* TypeOrmModule.forFeature([${entityName}])
|
||||
*
|
||||
* 2. 在 libs/wwjcloud-core/src/service.module.ts 中注册服务:
|
||||
* providers: [Core${this.toPascalCase(moduleName)}Service, ${this.toPascalCase(moduleName)}ServiceImpl]
|
||||
*
|
||||
* 3. 在 libs/wwjcloud-core/src/controller.module.ts 中注册控制器:
|
||||
* controllers: [${this.toPascalCase(moduleName)}Controller]
|
||||
*
|
||||
* 4. 执行 SQL 脚本创建数据库表
|
||||
*
|
||||
* 5. 运行 npm run build 验证编译通过
|
||||
*/
|
||||
`;
|
||||
|
||||
return {
|
||||
path: `libs/wwjcloud-core/src/${moduleName}.REGISTRATION.md`,
|
||||
content,
|
||||
type: 'other',
|
||||
description: `${moduleName} 模块注册指南`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 下划线转驼峰
|
||||
*/
|
||||
private toCamelCase(str: string): string {
|
||||
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* 转 PascalCase
|
||||
*/
|
||||
private toPascalCase(str: string): string {
|
||||
const camel = this.toCamelCase(str);
|
||||
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
ICodeGenerator,
|
||||
GeneratedFile,
|
||||
ModuleGenerateRequest,
|
||||
} from './generator.interface';
|
||||
|
||||
/**
|
||||
* 服务文件生成器
|
||||
*
|
||||
* 根据 PHP 服务层生成 NestJS 服务文件,
|
||||
* 包含 admin/api/core 三层服务。
|
||||
*/
|
||||
@Injectable()
|
||||
export class ServiceGenerator implements ICodeGenerator {
|
||||
/**
|
||||
* 生成服务文件
|
||||
*/
|
||||
generate(request: ModuleGenerateRequest): GeneratedFile[] {
|
||||
const files: GeneratedFile[] = [];
|
||||
|
||||
// 生成 core 服务(核心业务逻辑)
|
||||
files.push(this.generateCoreService(request));
|
||||
|
||||
// 根据端类型生成 admin/api 服务
|
||||
if (request.endpoints === 'adminapi' || request.endpoints === 'both') {
|
||||
files.push(this.generateLayerService(request, 'admin'));
|
||||
}
|
||||
if (request.endpoints === 'api' || request.endpoints === 'both') {
|
||||
files.push(this.generateLayerService(request, 'api'));
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成核心服务文件
|
||||
*/
|
||||
private generateCoreService(request: ModuleGenerateRequest): GeneratedFile {
|
||||
const { moduleName, tableName, fields } = request;
|
||||
const className = `Core${this.toPascalCase(moduleName)}Service`;
|
||||
const entityName = this.toPascalCase(
|
||||
tableName.replace(/^nc_/, '').replace(/_/g, ' '),
|
||||
);
|
||||
|
||||
const content = `import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
/**
|
||||
* ${request.description || moduleName} 核心服务
|
||||
* 对应 PHP: app/service/core/${moduleName}/Core${this.toPascalCase(moduleName)}Service.php
|
||||
*/
|
||||
@Injectable()
|
||||
export class ${className} {
|
||||
constructor(
|
||||
@InjectRepository(${entityName})
|
||||
private readonly repository: Repository<${entityName}>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 获取列表(分页)
|
||||
* 对应 PHP 核心服务方法
|
||||
*/
|
||||
async getPage(where: Record<string, unknown>, page: number = 1, limit: number = 20) {
|
||||
const queryBuilder = this.repository.createQueryBuilder('${tableName}');
|
||||
${this.generateWhereConditions(fields)}
|
||||
queryBuilder.orderBy('id', 'DESC');
|
||||
queryBuilder.skip((page - 1) * limit).take(limit);
|
||||
|
||||
const [list, count] = await queryBuilder.getManyAndCount();
|
||||
return { list, count };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取详情
|
||||
*/
|
||||
async getInfo(id: number) {
|
||||
return await this.repository.findOne({ where: { id } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增
|
||||
*/
|
||||
async add(data: Record<string, unknown>) {
|
||||
const entity = this.repository.create(data);
|
||||
return await this.repository.save(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*/
|
||||
async edit(id: number, data: Record<string, unknown>) {
|
||||
await this.repository.update(id, data);
|
||||
return await this.getInfo(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
async delete(id: number) {
|
||||
return await this.repository.softDelete(id);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
return {
|
||||
path: `libs/wwjcloud-core/src/services/core/${moduleName}/core-${moduleName}.service.ts`,
|
||||
content,
|
||||
type: 'service',
|
||||
description: `${moduleName} 核心服务`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 admin/api 层服务文件
|
||||
*/
|
||||
private generateLayerService(
|
||||
request: ModuleGenerateRequest,
|
||||
layer: 'admin' | 'api',
|
||||
): GeneratedFile {
|
||||
const { moduleName } = request;
|
||||
const className = `${this.toPascalCase(moduleName)}ServiceImpl`;
|
||||
const coreClassName = `Core${this.toPascalCase(moduleName)}Service`;
|
||||
|
||||
const content = `import { Injectable } from '@nestjs/common';
|
||||
import { ${coreClassName} } from '../../core/${moduleName}/core-${moduleName}.service';
|
||||
|
||||
/**
|
||||
* ${request.description || moduleName} ${layer === 'admin' ? '管理端' : '前台'}服务
|
||||
* 对应 PHP: app/service/${layer}/${moduleName}/${this.toPascalCase(moduleName)}Service.php
|
||||
*/
|
||||
@Injectable()
|
||||
export class ${className} {
|
||||
constructor(
|
||||
private readonly coreService: ${coreClassName},
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 获取分页列表
|
||||
*/
|
||||
async getPage(data: Record<string, unknown>) {
|
||||
const page = (data.page as number) || 1;
|
||||
const limit = (data.limit as number) || 20;
|
||||
return await this.coreService.getPage(data, page, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取详情
|
||||
*/
|
||||
async getInfo(id: number) {
|
||||
return await this.coreService.getInfo(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增
|
||||
*/
|
||||
async add(data: Record<string, unknown>) {
|
||||
return await this.coreService.add(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*/
|
||||
async edit(id: number, data: Record<string, unknown>) {
|
||||
return await this.coreService.edit(id, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
async delete(id: number) {
|
||||
return await this.coreService.delete(id);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
return {
|
||||
path: `libs/wwjcloud-core/src/services/${layer}/${moduleName}/impl/${moduleName}-service-impl.service.ts`,
|
||||
content,
|
||||
type: 'service',
|
||||
description: `${moduleName} ${layer} 服务`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 where 条件代码
|
||||
*/
|
||||
private generateWhereConditions(
|
||||
fields: import('./generator.interface').TableField[],
|
||||
): string {
|
||||
const searchableFields = fields.filter(
|
||||
(f) =>
|
||||
!f.isPrimary &&
|
||||
!f.isAutoIncrement &&
|
||||
(f.mysqlType.startsWith('varchar') || f.mysqlType.startsWith('text')),
|
||||
);
|
||||
|
||||
if (searchableFields.length === 0) return '';
|
||||
|
||||
const lines = searchableFields.slice(0, 3).map((f) => {
|
||||
const camelName = this.toCamelCase(f.name);
|
||||
return ` if (where.${camelName}) {
|
||||
queryBuilder.andWhere('${f.name} LIKE :${camelName}', { ${camelName}: '%' + where.${camelName} + '%' });
|
||||
}`;
|
||||
});
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 下划线转驼峰
|
||||
*/
|
||||
private toCamelCase(str: string): string {
|
||||
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* 转 PascalCase
|
||||
*/
|
||||
private toPascalCase(str: string): string {
|
||||
const camel = this.toCamelCase(str);
|
||||
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
ICodeGenerator,
|
||||
GeneratedFile,
|
||||
ModuleGenerateRequest,
|
||||
TableField,
|
||||
} from './generator.interface';
|
||||
|
||||
/**
|
||||
* SQL 文件生成器
|
||||
*
|
||||
* 生成数据库建表 SQL 脚本,
|
||||
* 表名和字段与 PHP 项目 100% 保持一致。
|
||||
*/
|
||||
@Injectable()
|
||||
export class SqlGenerator implements ICodeGenerator {
|
||||
/**
|
||||
* 生成 SQL 文件
|
||||
*/
|
||||
generate(request: ModuleGenerateRequest): GeneratedFile[] {
|
||||
const sql = this.generateCreateTableSql(request);
|
||||
return [sql];
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成建表 SQL
|
||||
*/
|
||||
private generateCreateTableSql(
|
||||
request: ModuleGenerateRequest,
|
||||
): GeneratedFile {
|
||||
const { tableName, fields, description } = request;
|
||||
const columns = fields.map((f) => this.generateColumn(f));
|
||||
const tableComment = description || tableName;
|
||||
|
||||
const content = `-- ----------------------------
|
||||
-- ${tableComment}
|
||||
-- 对应实体: ${tableName.replace(/^nc_/, '')}.entity.ts
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS \`${tableName}\`;
|
||||
CREATE TABLE \`${tableName}\` (
|
||||
${columns.join(',\n')},
|
||||
PRIMARY KEY (\`${this.getPrimaryField(fields)?.name || 'id'}\`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='${tableComment}';
|
||||
`;
|
||||
|
||||
return {
|
||||
path: `sql/${tableName}.sql`,
|
||||
content,
|
||||
type: 'sql',
|
||||
description: `${tableName} 建表 SQL`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成列定义
|
||||
*/
|
||||
private generateColumn(field: TableField): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
// 字段名
|
||||
parts.push(` \`${field.name}\``);
|
||||
|
||||
// 类型
|
||||
parts.push(field.mysqlType);
|
||||
|
||||
// 无符号
|
||||
if (field.unsigned) parts.push('UNSIGNED');
|
||||
|
||||
// 是否允许 NULL
|
||||
if (field.isPrimary) {
|
||||
parts.push('NOT NULL');
|
||||
if (field.isAutoIncrement) parts.push('AUTO_INCREMENT');
|
||||
} else if (field.nullable) {
|
||||
parts.push('DEFAULT NULL');
|
||||
} else {
|
||||
parts.push('NOT NULL');
|
||||
if (field.defaultValue !== undefined) {
|
||||
parts.push(`DEFAULT '${field.defaultValue}'`);
|
||||
} else if (
|
||||
field.mysqlType.startsWith('varchar') ||
|
||||
field.mysqlType.startsWith('text')
|
||||
) {
|
||||
parts.push("DEFAULT ''");
|
||||
} else if (
|
||||
field.mysqlType.startsWith('int') ||
|
||||
field.mysqlType.startsWith('tinyint')
|
||||
) {
|
||||
parts.push('DEFAULT 0');
|
||||
}
|
||||
}
|
||||
|
||||
// 注释
|
||||
if (field.comment) {
|
||||
parts.push(`COMMENT '${field.comment}'`);
|
||||
}
|
||||
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取主键字段
|
||||
*/
|
||||
private getPrimaryField(fields: TableField[]): TableField | undefined {
|
||||
return fields.find((f) => f.isPrimary);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { AiSelfHealListener } from "./listeners/ai-self-heal.listener";
|
||||
import { AiRecoveryListener } from "./listeners/ai-recovery.listener";
|
||||
import { AiRecoveryService } from "./services/ai-recovery.service";
|
||||
import { AiStrategyService } from "./services/ai-strategy.service";
|
||||
import { RetryStrategy } from "./strategies/retry.strategy";
|
||||
import { FallbackStrategy } from "./strategies/fallback.strategy";
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AiSelfHealListener } from './listeners/ai-self-heal.listener';
|
||||
import { AiRecoveryListener } from './listeners/ai-recovery.listener';
|
||||
import { AiRecoveryService } from './services/ai-recovery.service';
|
||||
import { AiStrategyService } from './services/ai-strategy.service';
|
||||
import { RetryStrategy } from './strategies/retry.strategy';
|
||||
import { FallbackStrategy } from './strategies/fallback.strategy';
|
||||
|
||||
/**
|
||||
* AI Healing Module - AI 自愈模块
|
||||
|
||||
@@ -34,7 +34,7 @@ export interface RecoveryResult {
|
||||
duration: number;
|
||||
result?: any;
|
||||
error?: string;
|
||||
nextAction?: "retry" | "escalate" | "abort";
|
||||
nextAction?: 'retry' | 'escalate' | 'abort';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,14 +51,14 @@ export interface SelfHealListener {
|
||||
*/
|
||||
export interface ErrorAnalysis {
|
||||
errorType: string;
|
||||
severity: "low" | "medium" | "high" | "critical";
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
category:
|
||||
| "network"
|
||||
| "database"
|
||||
| "service"
|
||||
| "validation"
|
||||
| "system"
|
||||
| "unknown";
|
||||
| 'network'
|
||||
| 'database'
|
||||
| 'service'
|
||||
| 'validation'
|
||||
| 'system'
|
||||
| 'unknown';
|
||||
recoverable: boolean;
|
||||
suggestedStrategies: string[];
|
||||
metadata: Record<string, any>;
|
||||
@@ -69,7 +69,7 @@ export interface ErrorAnalysis {
|
||||
*/
|
||||
export interface HealthCheckResult {
|
||||
component: string;
|
||||
status: "healthy" | "degraded" | "unhealthy";
|
||||
status: 'healthy' | 'degraded' | 'unhealthy';
|
||||
details: Record<string, any>;
|
||||
timestamp: number;
|
||||
responseTime?: number;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { OnEvent } from "@wwjCommon/events/event-bus";
|
||||
import { TASK_RECOVERY_REQUESTED_EVENT } from "@wwjAi";
|
||||
import type { TaskRecoveryRequestedPayload } from "@wwjAi";
|
||||
import { AiRecoveryService } from "../services/ai-recovery.service";
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { OnEvent } from '@wwjCommon/events/event-bus';
|
||||
import { TASK_RECOVERY_REQUESTED_EVENT } from '@wwjAi';
|
||||
import type { TaskRecoveryRequestedPayload } from '@wwjAi';
|
||||
import { AiRecoveryService } from '../services/ai-recovery.service';
|
||||
|
||||
@Injectable()
|
||||
export class AiRecoveryListener {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { EventBus, OnEvent } from "@wwjCommon/events/event-bus";
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { EventBus, OnEvent } from '@wwjCommon/events/event-bus';
|
||||
// ModuleRef no longer used
|
||||
import { TASK_FAILED_EVENT, TASK_RECOVERY_REQUESTED_EVENT } from "@wwjAi";
|
||||
import type { TaskFailedPayload, TaskRecoveryRequestedPayload } from "@wwjAi";
|
||||
import { MetricsService } from "@wwjCommon/metrics/metrics.service";
|
||||
import { TASK_FAILED_EVENT, TASK_RECOVERY_REQUESTED_EVENT } from '@wwjAi';
|
||||
import type { TaskFailedPayload, TaskRecoveryRequestedPayload } from '@wwjAi';
|
||||
import { MetricsService } from '@wwjCommon/metrics/metrics.service';
|
||||
|
||||
@Injectable()
|
||||
export class AiSelfHealListener {
|
||||
@@ -19,34 +19,34 @@ export class AiSelfHealListener {
|
||||
) {}
|
||||
|
||||
onModuleInit() {
|
||||
const enabled = this.readBoolean("AI_ENABLED");
|
||||
const currentState = enabled ? "ready" : "unavailable";
|
||||
const enabled = this.readBoolean('AI_ENABLED');
|
||||
const currentState = enabled ? 'ready' : 'unavailable';
|
||||
this.logger.log(
|
||||
`Healing module init: enabled=${enabled}, state=${currentState}`,
|
||||
);
|
||||
this.eventBus.emit("module.state.changed", {
|
||||
module: "healing",
|
||||
previousState: "initializing",
|
||||
this.eventBus.emit('module.state.changed', {
|
||||
module: 'healing',
|
||||
previousState: 'initializing',
|
||||
currentState,
|
||||
});
|
||||
}
|
||||
|
||||
@OnEvent(TASK_FAILED_EVENT)
|
||||
handleTaskFailed(payload: TaskFailedPayload) {
|
||||
const enabled = this.readBoolean("AI_ENABLED");
|
||||
const enabled = this.readBoolean('AI_ENABLED');
|
||||
this.logger.log(
|
||||
`Received task.failed for ${payload.taskId}, enabled=${enabled}, severity=${payload.severity}`,
|
||||
);
|
||||
this.metrics.observeAiEvent(TASK_FAILED_EVENT, payload.severity);
|
||||
if (!enabled) return;
|
||||
|
||||
const strategy: TaskRecoveryRequestedPayload["strategy"] =
|
||||
payload.severity === "high" ? "fallback" : "retry";
|
||||
const strategy: TaskRecoveryRequestedPayload['strategy'] =
|
||||
payload.severity === 'high' ? 'fallback' : 'retry';
|
||||
|
||||
const request: TaskRecoveryRequestedPayload = {
|
||||
taskId: payload.taskId,
|
||||
strategy,
|
||||
requestedBy: "ai",
|
||||
requestedBy: 'ai',
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
this.logger.log(
|
||||
@@ -75,8 +75,8 @@ export class AiSelfHealListener {
|
||||
|
||||
private readBoolean(key: string): boolean {
|
||||
const v = this.config.get<string | boolean>(key);
|
||||
if (typeof v === "boolean") return v;
|
||||
if (typeof v === "string") return v === "true" || v === "1" || v === "yes";
|
||||
if (typeof v === 'boolean') return v;
|
||||
if (typeof v === 'string') return v === 'true' || v === '1' || v === 'yes';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { CacheService } from "@wwjCommon/cache/cache.service";
|
||||
import { LockService } from "@wwjCommon/cache/lock.service";
|
||||
import { MetricsService } from "@wwjCommon/metrics/metrics.service";
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { CacheService } from '@wwjCommon/cache/cache.service';
|
||||
import { LockService } from '@wwjCommon/cache/lock.service';
|
||||
import { MetricsService } from '@wwjCommon/metrics/metrics.service';
|
||||
import {
|
||||
TASK_RECOVERY_COMPLETED_EVENT,
|
||||
TaskRecoveryRequestedPayload,
|
||||
@@ -10,14 +10,14 @@ import {
|
||||
TASK_RECOVERY_REQUESTED_EVENT,
|
||||
Severity,
|
||||
TaskFailedPayload,
|
||||
} from "@wwjAi";
|
||||
import { EventBus } from "@wwjCommon/events/event-bus";
|
||||
import { QueueService } from "@wwjCommon/queue/queue.service";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { AiStrategyService } from "./ai-strategy.service";
|
||||
} from '@wwjAi';
|
||||
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||
import { QueueService } from '@wwjCommon/queue/queue.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AiStrategyService } from './ai-strategy.service';
|
||||
|
||||
const QUEUE_KEY = "ai:recovery:queue";
|
||||
const QUEUE_LOCK_KEY = "ai:recovery:lock";
|
||||
const QUEUE_KEY = 'ai:recovery:queue';
|
||||
const QUEUE_LOCK_KEY = 'ai:recovery:lock';
|
||||
|
||||
@Injectable()
|
||||
export class AiRecoveryService {
|
||||
@@ -36,7 +36,7 @@ export class AiRecoveryService {
|
||||
|
||||
onModuleInit() {
|
||||
// 初始化可选的队列(BullMQ / Kafka)
|
||||
this.queue.init("ai-recovery").catch((err) => {
|
||||
this.queue.init('ai-recovery').catch((err) => {
|
||||
this.logger.error(`Queue init failed: ${err?.message || err}`);
|
||||
});
|
||||
if (this.queue.isBullmq() || this.queue.isKafka()) {
|
||||
@@ -51,7 +51,7 @@ export class AiRecoveryService {
|
||||
const payload: TaskRecoveryCompletedPayload = {
|
||||
taskId: data.taskId,
|
||||
strategy: data.strategy,
|
||||
result: "success",
|
||||
result: 'success',
|
||||
durationMs,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
@@ -63,7 +63,7 @@ export class AiRecoveryService {
|
||||
);
|
||||
},
|
||||
1,
|
||||
"ai-recovery",
|
||||
'ai-recovery',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ export class AiRecoveryService {
|
||||
async enqueue(req: TaskRecoveryRequestedPayload): Promise<number> {
|
||||
// 若启用队列,优先走队列(BullMQ/Kafka)
|
||||
if (this.queue.isBullmq() || this.queue.isKafka()) {
|
||||
await this.queue.enqueue("ai.recovery", req);
|
||||
await this.queue.enqueue('ai.recovery', req);
|
||||
// BullMQ 可以返回计数,Kafka 不易获取队列深度,这里统一返回 0 或 BullMQ 等待数量
|
||||
if (this.queue.isBullmq()) {
|
||||
const counts = await this.queue.getQueueCounts();
|
||||
@@ -132,7 +132,7 @@ export class AiRecoveryService {
|
||||
const payload: TaskRecoveryCompletedPayload = {
|
||||
taskId: req.taskId,
|
||||
strategy: req.strategy,
|
||||
result: "success",
|
||||
result: 'success',
|
||||
durationMs,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
@@ -165,9 +165,9 @@ export class AiRecoveryService {
|
||||
severity?: Severity;
|
||||
reason?: string;
|
||||
}): Promise<{ ok: true; emitted: boolean }> {
|
||||
const taskId = params.taskId ?? "demo-task";
|
||||
const severity: Severity = params.severity ?? "medium";
|
||||
const reason = params.reason ?? "demo failure";
|
||||
const taskId = params.taskId ?? 'demo-task';
|
||||
const severity: Severity = params.severity ?? 'medium';
|
||||
const reason = params.reason ?? 'demo failure';
|
||||
const payload: TaskFailedPayload = {
|
||||
taskId,
|
||||
reason,
|
||||
@@ -176,7 +176,7 @@ export class AiRecoveryService {
|
||||
};
|
||||
this.eventBus.emit(TASK_FAILED_EVENT, payload);
|
||||
this.metrics?.observeAiEvent(TASK_FAILED_EVENT, severity);
|
||||
if (this.readBoolean("AI_SIMULATE_DIRECT_ENQUEUE")) {
|
||||
if (this.readBoolean('AI_SIMULATE_DIRECT_ENQUEUE')) {
|
||||
const decided = this.strategy.decideStrategy(payload);
|
||||
this.metrics?.observeAiEvent(
|
||||
TASK_RECOVERY_REQUESTED_EVENT,
|
||||
@@ -186,7 +186,7 @@ export class AiRecoveryService {
|
||||
const request: TaskRecoveryRequestedPayload = {
|
||||
taskId,
|
||||
strategy: decided,
|
||||
requestedBy: "manual",
|
||||
requestedBy: 'manual',
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
await this.enqueue(request);
|
||||
@@ -195,9 +195,9 @@ export class AiRecoveryService {
|
||||
}
|
||||
private readBoolean(key: string): boolean {
|
||||
const v = this.config.get<string | boolean>(key);
|
||||
if (typeof v === "boolean") return v;
|
||||
if (typeof v === "string")
|
||||
return ["true", "1", "yes", "on"].includes(v.toLowerCase());
|
||||
if (typeof v === 'boolean') return v;
|
||||
if (typeof v === 'string')
|
||||
return ['true', '1', 'yes', 'on'].includes(v.toLowerCase());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import type {
|
||||
Severity,
|
||||
RecoveryStrategy,
|
||||
TaskFailedPayload,
|
||||
} from "../../types";
|
||||
} from '../../types';
|
||||
|
||||
@Injectable()
|
||||
export class AiStrategyService {
|
||||
@@ -12,25 +12,25 @@ export class AiStrategyService {
|
||||
|
||||
decideStrategy(evt: TaskFailedPayload): RecoveryStrategy {
|
||||
// allow override via env for quick testing
|
||||
const override = this.config.get<string>("AI_STRATEGY_OVERRIDE");
|
||||
const override = this.config.get<string>('AI_STRATEGY_OVERRIDE');
|
||||
if (override && this.isValidStrategy(override)) {
|
||||
return override as RecoveryStrategy;
|
||||
}
|
||||
// simple mapping by severity; can be extended to rules by metadata
|
||||
const map: Record<Severity, RecoveryStrategy> = {
|
||||
low: "retry",
|
||||
medium: "retry",
|
||||
high: "fallback",
|
||||
low: 'retry',
|
||||
medium: 'retry',
|
||||
high: 'fallback',
|
||||
};
|
||||
const s = map[evt.severity] ?? "retry";
|
||||
const s = map[evt.severity] ?? 'retry';
|
||||
// extend: if metadata.hint === 'reroute', do reroute
|
||||
const hint = evt.metadata?.hint as string | undefined;
|
||||
if (hint === "reroute") return "reroute";
|
||||
if (hint === "restart") return "restart";
|
||||
if (hint === 'reroute') return 'reroute';
|
||||
if (hint === 'restart') return 'restart';
|
||||
return s;
|
||||
}
|
||||
|
||||
private isValidStrategy(v: string): boolean {
|
||||
return ["retry", "restart", "reroute", "fallback", "noop"].includes(v);
|
||||
return ['retry', 'restart', 'reroute', 'fallback', 'noop'].includes(v);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
RecoveryStrategy,
|
||||
RecoveryContext,
|
||||
RecoveryResult,
|
||||
} from "../interfaces/healing.interface";
|
||||
} from '../interfaces/healing.interface';
|
||||
|
||||
/**
|
||||
* Fallback Recovery Strategy - 降级恢复策略
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
export class FallbackStrategy implements RecoveryStrategy {
|
||||
private readonly logger = new Logger(FallbackStrategy.name);
|
||||
|
||||
readonly name = "fallback";
|
||||
readonly name = 'fallback';
|
||||
readonly priority = 3;
|
||||
|
||||
/**
|
||||
@@ -26,11 +26,11 @@ export class FallbackStrategy implements RecoveryStrategy {
|
||||
canHandle(error: any): boolean {
|
||||
// 需要降级处理的错误类型
|
||||
const fallbackErrors = [
|
||||
"SERVICE_UNAVAILABLE",
|
||||
"DEPENDENCY_FAILURE",
|
||||
"RESOURCE_EXHAUSTED",
|
||||
"CIRCUIT_BREAKER_OPEN",
|
||||
"RATE_LIMIT_EXCEEDED",
|
||||
'SERVICE_UNAVAILABLE',
|
||||
'DEPENDENCY_FAILURE',
|
||||
'RESOURCE_EXHAUSTED',
|
||||
'CIRCUIT_BREAKER_OPEN',
|
||||
'RATE_LIMIT_EXCEEDED',
|
||||
];
|
||||
|
||||
if (error?.code && fallbackErrors.includes(error.code)) {
|
||||
@@ -38,7 +38,7 @@ export class FallbackStrategy implements RecoveryStrategy {
|
||||
}
|
||||
|
||||
// 检查错误严重程度
|
||||
if (error?.severity === "high" || error?.severity === "critical") {
|
||||
if (error?.severity === 'high' || error?.severity === 'critical') {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ export class FallbackStrategy implements RecoveryStrategy {
|
||||
strategy: this.name,
|
||||
duration: Date.now() - startTime,
|
||||
result: fallbackResult,
|
||||
nextAction: "abort", // 降级后通常不再重试
|
||||
nextAction: 'abort', // 降级后通常不再重试
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
@@ -74,8 +74,8 @@ export class FallbackStrategy implements RecoveryStrategy {
|
||||
success: false,
|
||||
strategy: this.name,
|
||||
duration: Date.now() - startTime,
|
||||
error: error instanceof Error ? error.message : "Fallback failed",
|
||||
nextAction: "abort",
|
||||
error: error instanceof Error ? error.message : 'Fallback failed',
|
||||
nextAction: 'abort',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -91,21 +91,21 @@ export class FallbackStrategy implements RecoveryStrategy {
|
||||
* 选择降级方案
|
||||
*/
|
||||
private async selectFallbackOption(context: RecoveryContext): Promise<any> {
|
||||
const taskType = context.metadata?.taskType || "unknown";
|
||||
const taskType = context.metadata?.taskType || 'unknown';
|
||||
|
||||
this.logger.debug(`Selecting fallback option for task type: ${taskType}`);
|
||||
|
||||
switch (taskType) {
|
||||
case "database":
|
||||
case 'database':
|
||||
return await this.handleDatabaseFallback(context);
|
||||
|
||||
case "api":
|
||||
case 'api':
|
||||
return await this.handleApiFallback(context);
|
||||
|
||||
case "cache":
|
||||
case 'cache':
|
||||
return await this.handleCacheFallback(context);
|
||||
|
||||
case "file":
|
||||
case 'file':
|
||||
return await this.handleFileFallback(context);
|
||||
|
||||
default:
|
||||
@@ -121,9 +121,9 @@ export class FallbackStrategy implements RecoveryStrategy {
|
||||
|
||||
// 返回缓存数据或默认值
|
||||
return {
|
||||
fallbackType: "database",
|
||||
fallbackType: 'database',
|
||||
data: context.metadata?.cachedData || null,
|
||||
message: "Using cached data due to database unavailability",
|
||||
message: 'Using cached data due to database unavailability',
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
@@ -136,9 +136,9 @@ export class FallbackStrategy implements RecoveryStrategy {
|
||||
|
||||
// 返回默认响应或离线数据
|
||||
return {
|
||||
fallbackType: "api",
|
||||
data: context.metadata?.defaultResponse || { status: "unavailable" },
|
||||
message: "Using default response due to API unavailability",
|
||||
fallbackType: 'api',
|
||||
data: context.metadata?.defaultResponse || { status: 'unavailable' },
|
||||
message: 'Using default response due to API unavailability',
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
@@ -151,9 +151,9 @@ export class FallbackStrategy implements RecoveryStrategy {
|
||||
|
||||
// 直接访问数据源
|
||||
return {
|
||||
fallbackType: "cache",
|
||||
fallbackType: 'cache',
|
||||
data: await this.fetchFromDataSource(context),
|
||||
message: "Bypassing cache and fetching from data source",
|
||||
message: 'Bypassing cache and fetching from data source',
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
@@ -166,9 +166,9 @@ export class FallbackStrategy implements RecoveryStrategy {
|
||||
|
||||
// 使用备用文件或默认内容
|
||||
return {
|
||||
fallbackType: "file",
|
||||
data: context.metadata?.backupContent || "",
|
||||
message: "Using backup content due to file access failure",
|
||||
fallbackType: 'file',
|
||||
data: context.metadata?.backupContent || '',
|
||||
message: 'Using backup content due to file access failure',
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
@@ -180,9 +180,9 @@ export class FallbackStrategy implements RecoveryStrategy {
|
||||
this.logger.warn(`Generic fallback for task: ${context.taskId}`);
|
||||
|
||||
return {
|
||||
fallbackType: "generic",
|
||||
fallbackType: 'generic',
|
||||
data: null,
|
||||
message: "Service temporarily unavailable, please try again later",
|
||||
message: 'Service temporarily unavailable, please try again later',
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
RecoveryStrategy,
|
||||
RecoveryContext,
|
||||
RecoveryResult,
|
||||
} from "../interfaces/healing.interface";
|
||||
} from '../interfaces/healing.interface';
|
||||
|
||||
/**
|
||||
* Retry Recovery Strategy - 重试恢复策略
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
export class RetryStrategy implements RecoveryStrategy {
|
||||
private readonly logger = new Logger(RetryStrategy.name);
|
||||
|
||||
readonly name = "retry";
|
||||
readonly name = 'retry';
|
||||
readonly priority = 1;
|
||||
|
||||
/**
|
||||
@@ -26,12 +26,12 @@ export class RetryStrategy implements RecoveryStrategy {
|
||||
canHandle(error: any): boolean {
|
||||
// 可重试的错误类型
|
||||
const retryableErrors = [
|
||||
"ECONNRESET",
|
||||
"ETIMEDOUT",
|
||||
"ENOTFOUND",
|
||||
"ECONNREFUSED",
|
||||
"NETWORK_ERROR",
|
||||
"TEMPORARY_FAILURE",
|
||||
'ECONNRESET',
|
||||
'ETIMEDOUT',
|
||||
'ENOTFOUND',
|
||||
'ECONNREFUSED',
|
||||
'NETWORK_ERROR',
|
||||
'TEMPORARY_FAILURE',
|
||||
];
|
||||
|
||||
if (error?.code && retryableErrors.includes(error.code)) {
|
||||
@@ -41,10 +41,10 @@ export class RetryStrategy implements RecoveryStrategy {
|
||||
if (error?.message) {
|
||||
const message = error.message.toLowerCase();
|
||||
return (
|
||||
message.includes("timeout") ||
|
||||
message.includes("connection") ||
|
||||
message.includes("network") ||
|
||||
message.includes("temporary")
|
||||
message.includes('timeout') ||
|
||||
message.includes('connection') ||
|
||||
message.includes('network') ||
|
||||
message.includes('temporary')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,8 +76,8 @@ export class RetryStrategy implements RecoveryStrategy {
|
||||
success: false,
|
||||
strategy: this.name,
|
||||
duration: Date.now() - startTime,
|
||||
error: "Maximum retry attempts exceeded",
|
||||
nextAction: "escalate",
|
||||
error: 'Maximum retry attempts exceeded',
|
||||
nextAction: 'escalate',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ export class RetryStrategy implements RecoveryStrategy {
|
||||
strategy: this.name,
|
||||
duration: Date.now() - startTime,
|
||||
result,
|
||||
nextAction: "retry",
|
||||
nextAction: 'retry',
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
@@ -101,9 +101,9 @@ export class RetryStrategy implements RecoveryStrategy {
|
||||
success: false,
|
||||
strategy: this.name,
|
||||
duration: Date.now() - startTime,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
nextAction:
|
||||
context.retryCount < context.maxRetries ? "retry" : "escalate",
|
||||
context.retryCount < context.maxRetries ? 'retry' : 'escalate',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,45 @@
|
||||
export * from "./wwjcloud-ai.module";
|
||||
export * from "./events";
|
||||
export * from "./types";
|
||||
export * from "./healing/services/ai-strategy.service";
|
||||
export * from "./manager/services/ai-registry.service";
|
||||
export * from "./manager/services/ai-orchestrator.service";
|
||||
export * from "./manager/services/ai-coordinator.service";
|
||||
export * from "./manager/services/framework-equivalence.service";
|
||||
// 模块导出
|
||||
export * from './wwjcloud-ai.module';
|
||||
export * from './events';
|
||||
export * from './types';
|
||||
|
||||
// 导出AI层集成的Boot层组件
|
||||
// Manager 层服务
|
||||
export * from './healing/services/ai-strategy.service';
|
||||
export * from './manager/services/ai-registry.service';
|
||||
export * from './manager/services/ai-orchestrator.service';
|
||||
export * from './manager/services/ai-coordinator.service';
|
||||
export * from './manager/services/framework-equivalence.service';
|
||||
|
||||
// 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 {
|
||||
// Provider Factories
|
||||
UploadProviderFactory,
|
||||
@@ -29,4 +61,4 @@ export {
|
||||
// Types & Decorators
|
||||
type InitializeProvider,
|
||||
Initializer,
|
||||
} from "@wwjBoot";
|
||||
} from '@wwjBoot';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable, Logger, OnModuleInit } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { InitializeProvider, Initializer } from "@wwjBoot";
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InitializeProvider } from '@wwjBoot';
|
||||
|
||||
@Injectable()
|
||||
export class AiBootstrapProvider implements OnModuleInit, InitializeProvider {
|
||||
@@ -10,7 +10,7 @@ export class AiBootstrapProvider implements OnModuleInit, InitializeProvider {
|
||||
|
||||
// 实现InitializeProvider接口
|
||||
async initialize(): Promise<void> {
|
||||
this.logger.log("AI Bootstrap Provider initializing...");
|
||||
this.logger.log('AI Bootstrap Provider initializing...');
|
||||
// AI层特有的初始化逻辑
|
||||
}
|
||||
|
||||
@@ -19,21 +19,21 @@ export class AiBootstrapProvider implements OnModuleInit, InitializeProvider {
|
||||
}
|
||||
|
||||
onModuleInit() {
|
||||
const flag = this.readBoolean("AI_ENABLED");
|
||||
const flag = this.readBoolean('AI_ENABLED');
|
||||
if (flag) {
|
||||
this.logger.log(
|
||||
"AI layer enabled: registering orchestrators and listeners",
|
||||
'AI layer enabled: registering orchestrators and listeners',
|
||||
);
|
||||
} else {
|
||||
this.logger.log("AI layer disabled: skip registering orchestrators");
|
||||
this.logger.log('AI layer disabled: skip registering orchestrators');
|
||||
}
|
||||
}
|
||||
|
||||
private readBoolean(key: string): boolean {
|
||||
const v = this.config.get<string | boolean>(key);
|
||||
if (typeof v === "boolean") return v;
|
||||
if (typeof v === "string") {
|
||||
return v === "true" || v === "1" || v === "yes";
|
||||
if (typeof v === 'boolean') return v;
|
||||
if (typeof v === 'string') {
|
||||
return v === 'true' || v === '1' || v === 'yes';
|
||||
}
|
||||
return false; // 未设置时视为关闭,但不设默认值
|
||||
}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import { Controller, Get, Query } from "@nestjs/common";
|
||||
import { ApiQuery, ApiTags } from "@nestjs/swagger";
|
||||
import { Public } from "@wwjCommon/auth/decorators";
|
||||
import { FrameworkEquivalenceService } from "../services/framework-equivalence.service";
|
||||
import { Controller, Get, Query } from '@nestjs/common';
|
||||
import { ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||
import { Public } from '@wwjCommon/auth/decorators';
|
||||
import { FrameworkEquivalenceService } from '../services/framework-equivalence.service';
|
||||
|
||||
@ApiTags("AI")
|
||||
@Controller("ai/knowledge")
|
||||
@ApiTags('AI')
|
||||
@Controller('ai/knowledge')
|
||||
export class AiKnowledgeController {
|
||||
constructor(
|
||||
private readonly frameworkEquivalence: FrameworkEquivalenceService,
|
||||
) {}
|
||||
|
||||
@Get("equivalence")
|
||||
@Get('equivalence')
|
||||
@Public()
|
||||
@ApiQuery({ name: "key", required: false, type: String })
|
||||
async getEquivalence(@Query("key") key?: string): Promise<any> {
|
||||
@ApiQuery({ name: 'key', required: false, type: String })
|
||||
async getEquivalence(@Query('key') key?: string): Promise<any> {
|
||||
return await this.frameworkEquivalence.execute({ key });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import { Controller, Get, Query, UseGuards, Post } from "@nestjs/common";
|
||||
import { RateLimitGuard } from "@wwjCommon/http/rate-limit.guard";
|
||||
import { AiRecoveryService } from "../../healing/services/ai-recovery.service";
|
||||
import { ApiTags } from "@nestjs/swagger";
|
||||
import { ApiQuery } from "@nestjs/swagger";
|
||||
import { IsInt, IsOptional, Min, IsString, IsIn } from "class-validator";
|
||||
import { Public, Roles } from "@wwjCommon/auth/decorators";
|
||||
import { EventBus } from "@wwjCommon/events/event-bus";
|
||||
import { TASK_FAILED_EVENT, TASK_RECOVERY_REQUESTED_EVENT } from "@wwjAi";
|
||||
import { Controller, Get, Query, UseGuards, Post } from '@nestjs/common';
|
||||
import { RateLimitGuard } from '@wwjCommon/http/rate-limit.guard';
|
||||
import { AiRecoveryService } from '../../healing/services/ai-recovery.service';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { ApiQuery } from '@nestjs/swagger';
|
||||
import { IsInt, IsOptional, Min, IsString, IsIn } from 'class-validator';
|
||||
import { Public, Roles } from '@wwjCommon/auth/decorators';
|
||||
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||
import type {
|
||||
Severity,
|
||||
TaskFailedPayload,
|
||||
TaskRecoveryRequestedPayload,
|
||||
} from "@wwjAi";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { MetricsService } from "@wwjCommon/metrics/metrics.service";
|
||||
import { AuthGuard } from "@wwjCommon/auth/auth.guard";
|
||||
import { RbacGuard } from "@wwjCommon/auth/rbac.guard";
|
||||
import { AiStrategyService } from "../../healing/services/ai-strategy.service";
|
||||
} from '@wwjAi';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { MetricsService } from '@wwjCommon/metrics/metrics.service';
|
||||
import { AuthGuard } from '@wwjCommon/auth/auth.guard';
|
||||
import { RbacGuard } from '@wwjCommon/auth/rbac.guard';
|
||||
import { AiStrategyService } from '../../healing/services/ai-strategy.service';
|
||||
|
||||
class DrainQueryDto {
|
||||
@IsInt()
|
||||
@@ -31,7 +30,7 @@ class SimulateFailureQueryDto {
|
||||
taskId?: string;
|
||||
|
||||
@IsString()
|
||||
@IsIn(["low", "medium", "high"])
|
||||
@IsIn(['low', 'medium', 'high'])
|
||||
@IsOptional()
|
||||
severity?: Severity;
|
||||
|
||||
@@ -41,8 +40,8 @@ class SimulateFailureQueryDto {
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard, RbacGuard, RateLimitGuard)
|
||||
@ApiTags("AI")
|
||||
@Controller("ai/recovery")
|
||||
@ApiTags('AI')
|
||||
@Controller('ai/recovery')
|
||||
export class AiController {
|
||||
constructor(
|
||||
private readonly recovery: AiRecoveryService,
|
||||
@@ -52,44 +51,44 @@ export class AiController {
|
||||
private readonly strategy: AiStrategyService,
|
||||
) {}
|
||||
|
||||
@Get("status")
|
||||
@Get('status')
|
||||
@Public()
|
||||
async status() {
|
||||
return await this.recovery.status();
|
||||
}
|
||||
|
||||
@Get("process-one")
|
||||
@Post("process-one")
|
||||
@Roles("admin")
|
||||
@Get('process-one')
|
||||
@Post('process-one')
|
||||
@Roles('admin')
|
||||
async processOne() {
|
||||
const ok = await this.recovery.processOne();
|
||||
return { ok };
|
||||
}
|
||||
|
||||
@Get("drain")
|
||||
@Post("drain")
|
||||
@Roles("admin")
|
||||
@Get('drain')
|
||||
@Post('drain')
|
||||
@Roles('admin')
|
||||
@ApiQuery({
|
||||
name: "max",
|
||||
name: 'max',
|
||||
required: false,
|
||||
type: Number,
|
||||
description: "最大处理数量(默认10)",
|
||||
description: '最大处理数量(默认10)',
|
||||
})
|
||||
async drain(@Query() query: DrainQueryDto) {
|
||||
const n = await this.recovery.drain(query.max ?? 10);
|
||||
return { processed: n };
|
||||
}
|
||||
|
||||
@Get("simulate-failure")
|
||||
@Post("simulate-failure")
|
||||
@Roles("admin")
|
||||
@ApiQuery({ name: "taskId", required: false, type: String })
|
||||
@Get('simulate-failure')
|
||||
@Post('simulate-failure')
|
||||
@Roles('admin')
|
||||
@ApiQuery({ name: 'taskId', required: false, type: String })
|
||||
@ApiQuery({
|
||||
name: "severity",
|
||||
name: 'severity',
|
||||
required: false,
|
||||
enum: ["low", "medium", "high"],
|
||||
enum: ['low', 'medium', 'high'],
|
||||
})
|
||||
@ApiQuery({ name: "reason", required: false, type: String })
|
||||
@ApiQuery({ name: 'reason', required: false, type: String })
|
||||
async simulateFailure(
|
||||
@Query() q: SimulateFailureQueryDto,
|
||||
): Promise<{ ok: true; emitted: boolean }> {
|
||||
@@ -104,8 +103,8 @@ export class AiController {
|
||||
// 移除 readBoolean 与直接依赖 metrics/strategy
|
||||
private readBoolean(key: string): boolean {
|
||||
const v = this.config.get<string | boolean>(key);
|
||||
if (typeof v === "boolean") return v;
|
||||
if (typeof v === "string") return v === "true" || v === "1" || v === "yes";
|
||||
if (typeof v === 'boolean') return v;
|
||||
if (typeof v === 'string') return v === 'true' || v === '1' || v === 'yes';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export interface StepResult {
|
||||
*/
|
||||
export interface ModuleState {
|
||||
name: string;
|
||||
status: "initializing" | "ready" | "active" | "error" | "unavailable";
|
||||
status: 'initializing' | 'ready' | 'active' | 'error' | 'unavailable';
|
||||
version: string;
|
||||
lastUpdate: number;
|
||||
metadata?: Record<string, any>;
|
||||
@@ -62,7 +62,7 @@ export interface ModuleState {
|
||||
export interface TaskCoordinationRequest {
|
||||
taskId: string;
|
||||
taskType: string;
|
||||
priority: "low" | "medium" | "high" | "critical";
|
||||
priority: 'low' | 'medium' | 'high' | 'critical';
|
||||
payload: any;
|
||||
requiredModules?: string[];
|
||||
timeout?: number;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { AiBootstrapProvider } from "./bootstrap/ai-bootstrap.provider";
|
||||
import { AiController } from "./controllers/ai.controller";
|
||||
import { AiKnowledgeController } from "./controllers/ai-knowledge.controller";
|
||||
import { AiOrchestratorService } from "./services/ai-orchestrator.service";
|
||||
import { AiRegistryService } from "./services/ai-registry.service";
|
||||
import { AiCoordinatorService } from "./services/ai-coordinator.service";
|
||||
import { FrameworkEquivalenceService } from "./services/framework-equivalence.service";
|
||||
import { AiHealingModule } from "../healing/healing.module";
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AiBootstrapProvider } from './bootstrap/ai-bootstrap.provider';
|
||||
import { AiController } from './controllers/ai.controller';
|
||||
import { AiKnowledgeController } from './controllers/ai-knowledge.controller';
|
||||
import { AiOrchestratorService } from './services/ai-orchestrator.service';
|
||||
import { AiRegistryService } from './services/ai-registry.service';
|
||||
import { AiCoordinatorService } from './services/ai-coordinator.service';
|
||||
import { FrameworkEquivalenceService } from './services/framework-equivalence.service';
|
||||
import { AiHealingModule } from '../healing/healing.module';
|
||||
// 集成Boot层的所有关键组件
|
||||
import {
|
||||
// Provider Factories
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
QueueService,
|
||||
RequestContextService,
|
||||
EventBus,
|
||||
} from "@wwjBoot";
|
||||
} from '@wwjBoot';
|
||||
|
||||
/**
|
||||
* AI Manager Module - AI 核心管理模块
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { Injectable, Logger, OnModuleInit } from "@nestjs/common";
|
||||
import { EventBus, OnEvent } from "@wwjCommon/events/event-bus";
|
||||
import { AiRegistryService } from "./ai-registry.service";
|
||||
import { AiOrchestratorService } from "./ai-orchestrator.service";
|
||||
import {
|
||||
Injectable,
|
||||
Logger,
|
||||
OnModuleInit,
|
||||
BadRequestException,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { EventBus, OnEvent } from '@wwjCommon/events/event-bus';
|
||||
import { AiRegistryService } from './ai-registry.service';
|
||||
import { AiOrchestratorService } from './ai-orchestrator.service';
|
||||
// 集成Boot层的所有关键组件
|
||||
import {
|
||||
UploadProviderFactory,
|
||||
@@ -12,7 +18,7 @@ import {
|
||||
LockService,
|
||||
QueueService,
|
||||
RequestContextService,
|
||||
} from "@wwjBoot";
|
||||
} from '@wwjBoot';
|
||||
|
||||
/**
|
||||
* AI Coordinator Service - AI 协调服务
|
||||
@@ -45,24 +51,24 @@ export class AiCoordinatorService implements OnModuleInit {
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
this.logger.log("AI Coordinator Service initialized");
|
||||
this.logger.log('AI Coordinator Service initialized');
|
||||
await this.initializeModuleStates();
|
||||
await this.initializeBootComponents();
|
||||
// Mark manager as ready once coordinator has initialized
|
||||
this.updateModuleState("manager", "ready");
|
||||
this.updateModuleState('manager', 'ready');
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化模块状态
|
||||
*/
|
||||
private async initializeModuleStates(): Promise<void> {
|
||||
const modules = ["healing", "safe", "tuner", "manager"];
|
||||
const modules = ['healing', 'safe', 'tuner', 'manager'];
|
||||
|
||||
for (const module of modules) {
|
||||
this.moduleStates.set(module, "initializing");
|
||||
this.moduleStates.set(module, 'initializing');
|
||||
}
|
||||
|
||||
this.logger.log("Module states initialized");
|
||||
this.logger.log('Module states initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,7 +81,7 @@ export class AiCoordinatorService implements OnModuleInit {
|
||||
this.logger.log(
|
||||
`Module state updated: ${moduleName} ${previousState} -> ${state}`,
|
||||
);
|
||||
this.eventBus.emit("module.state.changed", {
|
||||
this.eventBus.emit('module.state.changed', {
|
||||
module: moduleName,
|
||||
previousState,
|
||||
currentState: state,
|
||||
@@ -112,8 +118,8 @@ export class AiCoordinatorService implements OnModuleInit {
|
||||
const moduleCheck = await this.checkModuleAvailability(requiredModules);
|
||||
|
||||
if (!moduleCheck.allAvailable) {
|
||||
throw new Error(
|
||||
`Required modules not available: ${moduleCheck.unavailable.join(", ")}`,
|
||||
throw new BadRequestException(
|
||||
`Required modules not available: ${moduleCheck.unavailable.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -124,11 +130,11 @@ export class AiCoordinatorService implements OnModuleInit {
|
||||
payload,
|
||||
);
|
||||
|
||||
this.eventBus.emit("task.coordinated", { taskId, taskType, result });
|
||||
this.eventBus.emit('task.coordinated', { taskId, taskType, result });
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error(`Task coordination failed: ${taskId}`, error);
|
||||
this.eventBus.emit("task.coordination.failed", {
|
||||
this.eventBus.emit('task.coordination.failed', {
|
||||
taskId,
|
||||
taskType,
|
||||
error,
|
||||
@@ -142,13 +148,13 @@ export class AiCoordinatorService implements OnModuleInit {
|
||||
*/
|
||||
private getRequiredModules(taskType: string): string[] {
|
||||
const moduleMap: Record<string, string[]> = {
|
||||
healing: ["healing", "manager"],
|
||||
security: ["safe", "manager"],
|
||||
performance: ["tuner", "manager"],
|
||||
comprehensive: ["healing", "safe", "tuner", "manager"],
|
||||
healing: ['healing', 'manager'],
|
||||
security: ['safe', 'manager'],
|
||||
performance: ['tuner', 'manager'],
|
||||
comprehensive: ['healing', 'safe', 'tuner', 'manager'],
|
||||
};
|
||||
|
||||
return moduleMap[taskType] || ["manager"];
|
||||
return moduleMap[taskType] || ['manager'];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,7 +170,7 @@ export class AiCoordinatorService implements OnModuleInit {
|
||||
|
||||
for (const module of modules) {
|
||||
const state = this.moduleStates.get(module);
|
||||
if (state === "ready" || state === "active") {
|
||||
if (state === 'ready' || state === 'active') {
|
||||
available.push(module);
|
||||
} else {
|
||||
unavailable.push(module);
|
||||
@@ -194,21 +200,21 @@ export class AiCoordinatorService implements OnModuleInit {
|
||||
let result;
|
||||
|
||||
switch (taskType) {
|
||||
case "healing":
|
||||
case 'healing':
|
||||
result = await this.orchestratorService.executeWorkflow(
|
||||
"healing",
|
||||
'healing',
|
||||
payload,
|
||||
);
|
||||
break;
|
||||
case "security":
|
||||
case 'security':
|
||||
result = await this.orchestratorService.executeWorkflow(
|
||||
"security",
|
||||
'security',
|
||||
payload,
|
||||
);
|
||||
break;
|
||||
case "performance":
|
||||
case 'performance':
|
||||
result = await this.orchestratorService.executeWorkflow(
|
||||
"performance",
|
||||
'performance',
|
||||
payload,
|
||||
);
|
||||
break;
|
||||
@@ -233,7 +239,9 @@ export class AiCoordinatorService implements OnModuleInit {
|
||||
const services = this.registryService.getServicesByType(taskType);
|
||||
|
||||
if (services.length === 0) {
|
||||
throw new Error(`No services available for task type: ${taskType}`);
|
||||
throw new NotFoundException(
|
||||
`No services available for task type: ${taskType}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 执行第一个可用服务
|
||||
@@ -243,13 +251,13 @@ export class AiCoordinatorService implements OnModuleInit {
|
||||
/**
|
||||
* 处理任务失败事件
|
||||
*/
|
||||
@OnEvent("task.failed")
|
||||
@OnEvent('task.failed')
|
||||
async handleTaskFailed(payload: any): Promise<void> {
|
||||
this.logger.warn(`Task failed: ${payload.taskId}`);
|
||||
|
||||
// 尝试协调恢复
|
||||
if (payload.severity === "high") {
|
||||
await this.coordinateTask(`recovery-${payload.taskId}`, "healing", {
|
||||
if (payload.severity === 'high') {
|
||||
await this.coordinateTask(`recovery-${payload.taskId}`, 'healing', {
|
||||
originalTask: payload,
|
||||
});
|
||||
}
|
||||
@@ -258,7 +266,7 @@ export class AiCoordinatorService implements OnModuleInit {
|
||||
/**
|
||||
* 处理模块状态变化事件
|
||||
*/
|
||||
@OnEvent("module.state.changed")
|
||||
@OnEvent('module.state.changed')
|
||||
async handleModuleStateChanged(payload: any): Promise<void> {
|
||||
this.logger.debug(
|
||||
`Module state changed: ${payload.module} -> ${payload.currentState}`,
|
||||
@@ -269,8 +277,8 @@ export class AiCoordinatorService implements OnModuleInit {
|
||||
|
||||
// 如果模块变为不可用,暂停相关任务
|
||||
if (
|
||||
payload.currentState === "error" ||
|
||||
payload.currentState === "unavailable"
|
||||
payload.currentState === 'error' ||
|
||||
payload.currentState === 'unavailable'
|
||||
) {
|
||||
await this.pauseModuleTasks(payload.module);
|
||||
}
|
||||
@@ -286,7 +294,7 @@ export class AiCoordinatorService implements OnModuleInit {
|
||||
const requiredModules = this.getRequiredModules(task.taskType);
|
||||
if (requiredModules.includes(moduleName)) {
|
||||
this.logger.warn(`Pausing task: ${taskId}`);
|
||||
this.eventBus.emit("task.paused", {
|
||||
this.eventBus.emit('task.paused', {
|
||||
taskId,
|
||||
reason: `Module unavailable: ${moduleName}`,
|
||||
});
|
||||
@@ -322,7 +330,7 @@ export class AiCoordinatorService implements OnModuleInit {
|
||||
* 初始化Boot层组件
|
||||
*/
|
||||
private async initializeBootComponents(): Promise<void> {
|
||||
this.logger.log("Initializing Boot layer components for AI coordination");
|
||||
this.logger.log('Initializing Boot layer components for AI coordination');
|
||||
|
||||
try {
|
||||
// 初始化Provider工厂
|
||||
@@ -343,9 +351,9 @@ export class AiCoordinatorService implements OnModuleInit {
|
||||
// 初始化Request Context
|
||||
await this.initializeRequestContext();
|
||||
|
||||
this.logger.log("Boot layer components initialized successfully");
|
||||
this.logger.log('Boot layer components initialized successfully');
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to initialize Boot layer components:", error);
|
||||
this.logger.error('Failed to initialize Boot layer components:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,7 +361,7 @@ export class AiCoordinatorService implements OnModuleInit {
|
||||
* 初始化Provider工厂
|
||||
*/
|
||||
private async initializeProviderFactories(): Promise<void> {
|
||||
this.logger.log("Initializing Provider Factories");
|
||||
this.logger.log('Initializing Provider Factories');
|
||||
|
||||
// 注册AI相关的上传Provider
|
||||
// 这里可以注册AI特化的Provider
|
||||
@@ -361,24 +369,24 @@ export class AiCoordinatorService implements OnModuleInit {
|
||||
// 注册AI相关的支付Provider
|
||||
// 这里可以注册AI特化的支付Provider
|
||||
|
||||
this.logger.log("Provider Factories initialized");
|
||||
this.logger.log('Provider Factories initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化缓存管理器
|
||||
*/
|
||||
private async initializeCacheManager(): Promise<void> {
|
||||
this.logger.log("Initializing Cache Manager for AI coordination");
|
||||
this.logger.log('Initializing Cache Manager for AI coordination');
|
||||
|
||||
// 设置AI特定的缓存标签
|
||||
try {
|
||||
await this.cacheManager.set("ai:coordinator:initialized", true, 3600, [
|
||||
"ai",
|
||||
"coordinator",
|
||||
await this.cacheManager.set('ai:coordinator:initialized', true, 3600, [
|
||||
'ai',
|
||||
'coordinator',
|
||||
]);
|
||||
this.logger.log("Cache Manager initialized with AI tags");
|
||||
this.logger.log('Cache Manager initialized with AI tags');
|
||||
} catch (error) {
|
||||
this.logger.warn("Failed to set cache initialization marker:", error);
|
||||
this.logger.warn('Failed to set cache initialization marker:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,7 +394,7 @@ export class AiCoordinatorService implements OnModuleInit {
|
||||
* 初始化Mapper注册表
|
||||
*/
|
||||
private async initializeMapperRegistry(): Promise<void> {
|
||||
this.logger.log("Initializing Mapper Registry for AI coordination");
|
||||
this.logger.log('Initializing Mapper Registry for AI coordination');
|
||||
|
||||
// 这里可以注册AI相关的Mapper
|
||||
const allMappers = this.mapperRegistry.getAllMappers();
|
||||
@@ -397,7 +405,7 @@ export class AiCoordinatorService implements OnModuleInit {
|
||||
* 初始化Metrics服务
|
||||
*/
|
||||
private async initializeMetricsService(): Promise<void> {
|
||||
this.logger.log("Initializing Metrics Service for AI coordination");
|
||||
this.logger.log('Initializing Metrics Service for AI coordination');
|
||||
// Metrics服务通常自动初始化,这里可以进行AI特定的配置
|
||||
}
|
||||
|
||||
@@ -405,12 +413,12 @@ export class AiCoordinatorService implements OnModuleInit {
|
||||
* 初始化Queue服务
|
||||
*/
|
||||
private async initializeQueueService(): Promise<void> {
|
||||
this.logger.log("Initializing Queue Service for AI coordination");
|
||||
this.logger.log('Initializing Queue Service for AI coordination');
|
||||
try {
|
||||
await this.queueService.init("ai-tasks");
|
||||
this.logger.log("Queue Service initialized for AI tasks");
|
||||
await this.queueService.init('ai-tasks');
|
||||
this.logger.log('Queue Service initialized for AI tasks');
|
||||
} catch (error) {
|
||||
this.logger.warn("Queue Service initialization failed:", error);
|
||||
this.logger.warn('Queue Service initialization failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,14 +426,14 @@ export class AiCoordinatorService implements OnModuleInit {
|
||||
* 初始化Request Context
|
||||
*/
|
||||
private async initializeRequestContext(): Promise<void> {
|
||||
this.logger.log("Initializing Request Context for AI coordination");
|
||||
this.logger.log('Initializing Request Context for AI coordination');
|
||||
// RequestContext通常在中间件层自动管理,这里记录初始化
|
||||
}
|
||||
|
||||
/**
|
||||
* AI协调器特有的Provider管理方法
|
||||
*/
|
||||
async getUploadProviderForAiTask(providerName = "default"): Promise<any> {
|
||||
async getUploadProviderForAiTask(providerName = 'default'): Promise<any> {
|
||||
try {
|
||||
return this.uploadProviderFactory.getProvider(providerName);
|
||||
} catch (error) {
|
||||
@@ -437,7 +445,7 @@ export class AiCoordinatorService implements OnModuleInit {
|
||||
}
|
||||
}
|
||||
|
||||
async getPayProviderForAiTask(providerName = "default"): Promise<any> {
|
||||
async getPayProviderForAiTask(providerName = 'default'): Promise<any> {
|
||||
try {
|
||||
return this.payProviderFactory.getProvider(providerName);
|
||||
} catch (error) {
|
||||
@@ -458,13 +466,13 @@ export class AiCoordinatorService implements OnModuleInit {
|
||||
await this.cacheManager.setWithTags(
|
||||
cacheKey,
|
||||
result,
|
||||
["ai", "task-result"],
|
||||
['ai', 'task-result'],
|
||||
ttl,
|
||||
);
|
||||
}
|
||||
|
||||
async invalidateAiCache(): Promise<void> {
|
||||
await this.cacheManager.invalidateByTag("ai");
|
||||
await this.cacheManager.invalidateByTag('ai');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -478,13 +486,13 @@ export class AiCoordinatorService implements OnModuleInit {
|
||||
try {
|
||||
// 使用Boot层的Metrics服务记录AI任务指标
|
||||
// 通过事件总线发送AI事件,由Metrics服务自动处理
|
||||
this.eventBus.emit("ai.task.completed", {
|
||||
this.eventBus.emit('ai.task.completed', {
|
||||
type: taskType,
|
||||
duration,
|
||||
success,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.warn("Failed to record AI task metrics:", error);
|
||||
this.logger.warn('Failed to record AI task metrics:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable, Logger, OnModuleInit } from "@nestjs/common";
|
||||
import { EventBus } from "@wwjCommon/events/event-bus";
|
||||
import { AiRegistryService } from "./ai-registry.service";
|
||||
import { Injectable, Logger, OnModuleInit, NotFoundException } from '@nestjs/common';
|
||||
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||
import { AiRegistryService } from './ai-registry.service';
|
||||
|
||||
/**
|
||||
* AI Orchestrator Service - AI 编排服务
|
||||
@@ -22,7 +22,7 @@ export class AiOrchestratorService implements OnModuleInit {
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
this.logger.log("AI Orchestrator Service initialized");
|
||||
this.logger.log('AI Orchestrator Service initialized');
|
||||
await this.initializeWorkflows();
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export class AiOrchestratorService implements OnModuleInit {
|
||||
* 初始化工作流程
|
||||
*/
|
||||
private async initializeWorkflows(): Promise<void> {
|
||||
this.logger.log("Initializing AI workflows...");
|
||||
this.logger.log('Initializing AI workflows...');
|
||||
// 注册默认工作流程
|
||||
await this.registerDefaultWorkflows();
|
||||
}
|
||||
@@ -40,22 +40,22 @@ export class AiOrchestratorService implements OnModuleInit {
|
||||
*/
|
||||
private async registerDefaultWorkflows(): Promise<void> {
|
||||
// 自愈工作流程
|
||||
this.registerWorkflow("healing", {
|
||||
steps: ["detect", "analyze", "recover", "verify"],
|
||||
this.registerWorkflow('healing', {
|
||||
steps: ['detect', 'analyze', 'recover', 'verify'],
|
||||
timeout: 30000,
|
||||
retryCount: 3,
|
||||
});
|
||||
|
||||
// 安全检查工作流程
|
||||
this.registerWorkflow("security", {
|
||||
steps: ["scan", "analyze", "protect", "report"],
|
||||
this.registerWorkflow('security', {
|
||||
steps: ['scan', 'analyze', 'protect', 'report'],
|
||||
timeout: 15000,
|
||||
retryCount: 2,
|
||||
});
|
||||
|
||||
// 性能优化工作流程
|
||||
this.registerWorkflow("performance", {
|
||||
steps: ["monitor", "analyze", "optimize", "validate"],
|
||||
this.registerWorkflow('performance', {
|
||||
steps: ['monitor', 'analyze', 'optimize', 'validate'],
|
||||
timeout: 60000,
|
||||
retryCount: 1,
|
||||
});
|
||||
@@ -75,18 +75,18 @@ export class AiOrchestratorService implements OnModuleInit {
|
||||
async executeWorkflow(name: string, context: any): Promise<any> {
|
||||
const workflow = this.activeWorkflows.get(name);
|
||||
if (!workflow) {
|
||||
throw new Error(`Workflow not found: ${name}`);
|
||||
throw new NotFoundException(`Workflow not found: ${name}`);
|
||||
}
|
||||
|
||||
this.logger.log(`Executing workflow: ${name}`);
|
||||
|
||||
try {
|
||||
const result = await this.processWorkflowSteps(workflow, context);
|
||||
this.eventBus.emit("workflow.completed", { name, result });
|
||||
this.eventBus.emit('workflow.completed', { name, result });
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error(`Workflow execution failed: ${name}`, error);
|
||||
this.eventBus.emit("workflow.failed", { name, error });
|
||||
this.eventBus.emit('workflow.failed', { name, error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -140,7 +140,7 @@ export class AiOrchestratorService implements OnModuleInit {
|
||||
const removed = this.activeWorkflows.delete(name);
|
||||
if (removed) {
|
||||
this.logger.log(`Workflow stopped: ${name}`);
|
||||
this.eventBus.emit("workflow.stopped", { name });
|
||||
this.eventBus.emit('workflow.stopped', { name });
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
Logger,
|
||||
OnModuleInit,
|
||||
OnModuleDestroy,
|
||||
} from "@nestjs/common";
|
||||
import { EventBus } from "@wwjCommon/events/event-bus";
|
||||
} from '@nestjs/common';
|
||||
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||
|
||||
/**
|
||||
* AI Service Interface - AI 服务接口
|
||||
@@ -36,7 +36,7 @@ export class AiRegistryService implements OnModuleInit, OnModuleDestroy {
|
||||
constructor(private readonly eventBus: EventBus) {}
|
||||
|
||||
async onModuleInit() {
|
||||
this.logger.log("AI Registry Service initialized");
|
||||
this.logger.log('AI Registry Service initialized');
|
||||
await this.startHealthCheck();
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export class AiRegistryService implements OnModuleInit, OnModuleDestroy {
|
||||
if (this.healthCheckInterval) {
|
||||
clearInterval(this.healthCheckInterval);
|
||||
this.healthCheckInterval = null;
|
||||
this.logger.log("AI Registry Service health check stopped");
|
||||
this.logger.log('AI Registry Service health check stopped');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ export class AiRegistryService implements OnModuleInit, OnModuleDestroy {
|
||||
this.servicesByType.get(service.type)!.push(service);
|
||||
|
||||
this.logger.log(`Service registered: ${service.name} (${service.type})`);
|
||||
this.eventBus.emit("service.registered", service);
|
||||
this.eventBus.emit('service.registered', service);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,7 +86,7 @@ export class AiRegistryService implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
|
||||
this.logger.log(`Service unregistered: ${serviceName}`);
|
||||
this.eventBus.emit("service.unregistered", service);
|
||||
this.eventBus.emit('service.unregistered', service);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -155,14 +155,14 @@ export class AiRegistryService implements OnModuleInit, OnModuleDestroy {
|
||||
healthyCount++;
|
||||
} else {
|
||||
this.logger.warn(`Service unhealthy: ${service.name}`);
|
||||
this.eventBus.emit("service.unhealthy", service);
|
||||
this.eventBus.emit('service.unhealthy', service);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Health check failed for service: ${service.name}`,
|
||||
error,
|
||||
);
|
||||
this.eventBus.emit("service.error", { service, error });
|
||||
this.eventBus.emit('service.error', { service, error });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Injectable, OnModuleInit } from "@nestjs/common";
|
||||
import { AiService } from "./ai-registry.service";
|
||||
import { AiRegistryService } from "./ai-registry.service";
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { AiService } from './ai-registry.service';
|
||||
import { AiRegistryService } from './ai-registry.service';
|
||||
|
||||
@Injectable()
|
||||
export class FrameworkEquivalenceService implements AiService, OnModuleInit {
|
||||
name = "framework-equivalence";
|
||||
type = "knowledge";
|
||||
version = "v1";
|
||||
name = 'framework-equivalence';
|
||||
type = 'knowledge';
|
||||
version = 'v1';
|
||||
|
||||
constructor(private readonly registry: AiRegistryService) {}
|
||||
|
||||
@@ -16,59 +16,59 @@ export class FrameworkEquivalenceService implements AiService, OnModuleInit {
|
||||
|
||||
private readonly equivalence = {
|
||||
beanRetrieval: {
|
||||
java: "ApplicationContext#getBean",
|
||||
nest: "ModuleRef.get(Service,{strict:false})",
|
||||
java: 'ApplicationContext#getBean',
|
||||
nest: 'ModuleRef.get(Service,{strict:false})',
|
||||
},
|
||||
lazyInjection: {
|
||||
java: "@Lazy",
|
||||
nest: "ModuleRef.get(...)/forwardRef",
|
||||
java: '@Lazy',
|
||||
nest: 'ModuleRef.get(...)/forwardRef',
|
||||
},
|
||||
diRepositories: {
|
||||
java: "@Autowired/@Resource Repository",
|
||||
nest: "InjectRepository/constructor inject",
|
||||
java: '@Autowired/@Resource Repository',
|
||||
nest: 'InjectRepository/constructor inject',
|
||||
},
|
||||
dynamicRegistration: {
|
||||
java: "registerBean/removeBean",
|
||||
nest: "DynamicModule providers/useClass",
|
||||
java: 'registerBean/removeBean',
|
||||
nest: 'DynamicModule providers/useClass',
|
||||
},
|
||||
config: {
|
||||
java: "@ConfigurationProperties/Environment",
|
||||
nest: "ConfigModule/ConfigService.get",
|
||||
java: '@ConfigurationProperties/Environment',
|
||||
nest: 'ConfigModule/ConfigService.get',
|
||||
},
|
||||
events: {
|
||||
java: "ApplicationEventPublisher",
|
||||
nest: "EventEmitterModule/EventBus",
|
||||
java: 'ApplicationEventPublisher',
|
||||
nest: 'EventEmitterModule/EventBus',
|
||||
},
|
||||
schedule: {
|
||||
java: "@Scheduled",
|
||||
nest: "@nestjs/schedule/SchedulerRegistry",
|
||||
java: '@Scheduled',
|
||||
nest: '@nestjs/schedule/SchedulerRegistry',
|
||||
},
|
||||
orm: {
|
||||
java: "Mapper/@Transactional",
|
||||
nest: "TypeORM Repository/transaction",
|
||||
java: 'Mapper/@Transactional',
|
||||
nest: 'TypeORM Repository/transaction',
|
||||
},
|
||||
validation: {
|
||||
java: "Validator",
|
||||
nest: "class-validator/ValidationPipe",
|
||||
java: 'Validator',
|
||||
nest: 'class-validator/ValidationPipe',
|
||||
},
|
||||
cache: {
|
||||
java: "Cache/RedisTemplate",
|
||||
nest: "CacheManager/ioredis",
|
||||
java: 'Cache/RedisTemplate',
|
||||
nest: 'CacheManager/ioredis',
|
||||
},
|
||||
controllerRoutes: {
|
||||
java: "@Controller/@RequestMapping",
|
||||
nest: "@Controller/@Get/@Post",
|
||||
java: '@Controller/@RequestMapping',
|
||||
nest: '@Controller/@Get/@Post',
|
||||
},
|
||||
exceptions: {
|
||||
java: "@ControllerAdvice/Exception",
|
||||
nest: "BadRequestException/filters",
|
||||
java: '@ControllerAdvice/Exception',
|
||||
nest: 'BadRequestException/filters',
|
||||
},
|
||||
guards: {
|
||||
java: "Spring Security",
|
||||
nest: "@UseGuards/Auth/RBAC",
|
||||
java: 'Spring Security',
|
||||
nest: '@UseGuards/Auth/RBAC',
|
||||
},
|
||||
policy: {
|
||||
rule: "controller thin, service logic, siteId service-side",
|
||||
rule: 'controller thin, service logic, siteId service-side',
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -82,4 +82,3 @@ export class FrameworkEquivalenceService implements AiService, OnModuleInit {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ShortTermMemoryService } from './short-term-memory.service';
|
||||
import { LongTermMemoryService } from './long-term-memory.service';
|
||||
|
||||
/**
|
||||
* AI Memory 模块
|
||||
* 借鉴 OpenClaw 双模记忆设计
|
||||
* - 短期记忆:会话级别的消息历史(Redis/内存)
|
||||
* - 长期记忆:跨会话的经验积累(向量数据库/内存)
|
||||
*/
|
||||
@Module({
|
||||
providers: [ShortTermMemoryService, LongTermMemoryService],
|
||||
exports: [ShortTermMemoryService, LongTermMemoryService],
|
||||
})
|
||||
export class AiMemoryModule {}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './short-term-memory.service';
|
||||
export * from './long-term-memory.service';
|
||||
export * from './ai-memory.module';
|
||||
@@ -0,0 +1,124 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* 保存记忆
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索相关记忆(基于关键词匹配)
|
||||
* 生产环境应替换为向量相似度搜索
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除记忆
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
async forget(memoryId: string): Promise<boolean> {
|
||||
return this.memories.delete(memoryId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { LlmMessage } from '../providers/llm-provider.interface';
|
||||
|
||||
/**
|
||||
* 会话消息存储条目
|
||||
*/
|
||||
interface SessionMessages {
|
||||
messages: LlmMessage[];
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 短期记忆服务 — 借鉴 OpenClaw 短期记忆
|
||||
* 基于 Redis 存储会话级别的消息历史
|
||||
* 当前使用内存 Map 作为降级方案
|
||||
*/
|
||||
@Injectable()
|
||||
export class ShortTermMemoryService {
|
||||
private readonly logger = new Logger(ShortTermMemoryService.name);
|
||||
private readonly sessions = new Map<string, SessionMessages>();
|
||||
private readonly maxMessagesPerSession = 50;
|
||||
private cleanupInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor() {
|
||||
// 每 10 分钟清理超过 30 分钟未活动的会话
|
||||
this.cleanupInterval = setInterval(() => this.cleanup(), 10 * 60 * 1000);
|
||||
if (this.cleanupInterval.unref) this.cleanupInterval.unref();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加消息到会话
|
||||
*/
|
||||
addMessage(sessionId: string, message: LlmMessage): void {
|
||||
let session = this.sessions.get(sessionId);
|
||||
if (!session) {
|
||||
session = { messages: [], updatedAt: Date.now() };
|
||||
this.sessions.set(sessionId, session);
|
||||
}
|
||||
|
||||
session.messages.push(message);
|
||||
session.updatedAt = Date.now();
|
||||
|
||||
// 限制消息数量,保留最近的 N 条
|
||||
if (session.messages.length > this.maxMessagesPerSession) {
|
||||
session.messages = session.messages.slice(-this.maxMessagesPerSession);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话消息历史
|
||||
*/
|
||||
getMessages(sessionId: string): LlmMessage[] {
|
||||
return this.sessions.get(sessionId)?.messages || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话消息数量
|
||||
*/
|
||||
getMessageCount(sessionId: string): number {
|
||||
return this.sessions.get(sessionId)?.messages.length || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除会话
|
||||
*/
|
||||
clearSession(sessionId: string): void {
|
||||
this.sessions.delete(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取活跃会话数
|
||||
*/
|
||||
getActiveSessionCount(): number {
|
||||
return this.sessions.size;
|
||||
}
|
||||
|
||||
/** 清理过期会话 */
|
||||
private cleanup(): void {
|
||||
const now = Date.now();
|
||||
const threshold = 30 * 60 * 1000; // 30 分钟
|
||||
for (const [id, session] of this.sessions) {
|
||||
if (now - session.updatedAt > threshold) {
|
||||
this.sessions.delete(id);
|
||||
}
|
||||
}
|
||||
if (this.sessions.size > 0) {
|
||||
this.logger.debug(`[ShortTermMemory] 活跃会话: ${this.sessions.size}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Injectable, Logger, BadGatewayException } 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 BadGatewayException(`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 BadGatewayException(`Ollama Stream error ${response.status}`);
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
|
||||
const message = parsed.message as Record<string, unknown> | undefined;
|
||||
yield {
|
||||
content: message?.content as string | undefined,
|
||||
finishReason: parsed.done ? 'stop' : undefined,
|
||||
};
|
||||
} catch {
|
||||
// 忽略解析错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { Injectable, Logger, BadGatewayException } from '@nestjs/common';
|
||||
import {
|
||||
ILlmProvider,
|
||||
LlmChatParams,
|
||||
LlmResponse,
|
||||
LlmChunk,
|
||||
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 BadGatewayException(`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 BadGatewayException(`OpenAI Stream error ${response.status}`);
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || !trimmed.startsWith('data: ')) continue;
|
||||
const data = trimmed.slice(6);
|
||||
if (data === '[DONE]') return;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data) as Record<string, unknown>;
|
||||
const choices = parsed.choices as
|
||||
| Array<Record<string, unknown>>
|
||||
| undefined;
|
||||
const delta = choices?.[0]?.delta as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
|
||||
yield {
|
||||
content: delta?.content as string | undefined,
|
||||
toolCalls: delta?.tool_calls as LlmToolCall[] | undefined,
|
||||
finishReason: choices?.[0]?.finish_reason as
|
||||
| LlmChunk['finishReason']
|
||||
| undefined,
|
||||
};
|
||||
} catch {
|
||||
// 忽略解析错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './llm-provider.interface';
|
||||
export * from './llm-provider.factory';
|
||||
export * from './impls/openai.provider';
|
||||
export * from './impls/ollama.provider';
|
||||
@@ -0,0 +1,136 @@
|
||||
import { Injectable, Logger, OnModuleDestroy, BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { ILlmProvider } 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 BadRequestException('未配置任何 LLM Provider,请先调用 registerProvider()');
|
||||
}
|
||||
const provider = this.providers.get(providerName);
|
||||
if (!provider) {
|
||||
throw new NotFoundException(`LLM Provider [${providerName}] 未注册`);
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认 Provider
|
||||
*/
|
||||
getDefaultProvider(): ILlmProvider {
|
||||
return this.getProvider();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下一个可用的 API Key(带轮换和冷却)
|
||||
*/
|
||||
getNextApiKey(providerName: string): string | undefined {
|
||||
const keys = this.apiKeys.get(providerName);
|
||||
if (!keys || keys.length === 0) return undefined;
|
||||
|
||||
const now = Date.now();
|
||||
// 找到第一个不在冷却中的 Key
|
||||
for (const entry of keys) {
|
||||
if (entry.cooldownUntil <= now) {
|
||||
entry.totalCalls++;
|
||||
return entry.key;
|
||||
}
|
||||
}
|
||||
|
||||
// 所有 Key 都在冷却中,返回第一个(降级)
|
||||
this.logger.warn(
|
||||
`Provider [${providerName}] 所有 API Key 都在冷却中,使用降级策略`,
|
||||
);
|
||||
return keys[0].key;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记 API Key 为冷却状态
|
||||
*/
|
||||
markKeyCooldown(providerName: string, apiKey: string): void {
|
||||
const keys = this.apiKeys.get(providerName);
|
||||
if (!keys) return;
|
||||
const entry = keys.find((k) => k.key === apiKey);
|
||||
if (entry) {
|
||||
entry.cooldownUntil = Date.now() + this.keyCooldownMs;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已注册的 Provider 名称
|
||||
*/
|
||||
getProviderNames(): string[] {
|
||||
return Array.from(this.providers.keys());
|
||||
}
|
||||
|
||||
onModuleDestroy(): void {
|
||||
this.providers.clear();
|
||||
this.apiKeys.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* LLM 消息角色
|
||||
*/
|
||||
export type LlmMessageRole = 'system' | 'user' | 'assistant' | 'tool';
|
||||
|
||||
/**
|
||||
* LLM 消息
|
||||
*/
|
||||
export interface LlmMessage {
|
||||
role: LlmMessageRole;
|
||||
content: string;
|
||||
name?: string;
|
||||
toolCallId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* LLM 工具定义
|
||||
*/
|
||||
export interface LlmToolDefinition {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* LLM 工具调用
|
||||
*/
|
||||
export interface LlmToolCall {
|
||||
id: string;
|
||||
name: string;
|
||||
arguments: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* LLM 响应块(流式)
|
||||
*/
|
||||
export interface LlmChunk {
|
||||
content?: string;
|
||||
toolCalls?: LlmToolCall[];
|
||||
finishReason?: 'stop' | 'tool_calls' | 'length';
|
||||
}
|
||||
|
||||
/**
|
||||
* LLM 完整响应
|
||||
*/
|
||||
export interface LlmResponse {
|
||||
content: string;
|
||||
toolCalls?: LlmToolCall[];
|
||||
finishReason: 'stop' | 'tool_calls' | 'length';
|
||||
usage?: {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* LLM 调用参数
|
||||
*/
|
||||
export interface LlmChatParams {
|
||||
messages: LlmMessage[];
|
||||
tools?: LlmToolDefinition[];
|
||||
stream?: boolean;
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* LLM Provider 统一接口
|
||||
* 借鉴 OpenClaw 的多模型 Provider 抽象
|
||||
* 支持 OpenAI / DeepSeek / Qwen / Ollama 等任意 LLM
|
||||
*/
|
||||
export interface ILlmProvider {
|
||||
/** 模型标识 */
|
||||
readonly model: string;
|
||||
|
||||
/** 同步对话 */
|
||||
chat(params: LlmChatParams): Promise<LlmResponse>;
|
||||
|
||||
/** 流式对话 */
|
||||
chatStream(params: LlmChatParams): AsyncIterable<LlmChunk>;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Module, DynamicModule } from '@nestjs/common';
|
||||
import { LlmProviderFactory } from './llm-provider.factory';
|
||||
|
||||
/**
|
||||
* LLM Provider 模块
|
||||
* 借鉴 OpenClaw 多模型驱动引擎
|
||||
* 支持运行时注册多个 LLM Provider
|
||||
*/
|
||||
@Module({})
|
||||
export class LlmProviderModule {
|
||||
static register(): DynamicModule {
|
||||
return {
|
||||
module: LlmProviderModule,
|
||||
providers: [LlmProviderFactory],
|
||||
exports: [LlmProviderFactory],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { ToolCallRecord } from './loop-detector.interface';
|
||||
|
||||
/**
|
||||
* AI 任务定义
|
||||
*/
|
||||
export interface AiTask {
|
||||
/** 任务描述 */
|
||||
description: string;
|
||||
/** 任务类型 */
|
||||
type: string;
|
||||
/** 上下文信息 */
|
||||
context?: Record<string, unknown>;
|
||||
/** 关联的会话标识 */
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 循环执行选项
|
||||
*/
|
||||
export interface LoopOptions {
|
||||
/** 最大迭代次数(默认 50) */
|
||||
maxIterations?: number;
|
||||
/** 是否启用循环检测(默认 true) */
|
||||
enableLoopDetection?: boolean;
|
||||
/** 是否启用信任边界检查(默认 true) */
|
||||
enableTrustBoundary?: boolean;
|
||||
/** 超时时间(毫秒,默认 300000 = 5分钟) */
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 循环执行结果
|
||||
*/
|
||||
export interface LoopResult {
|
||||
/** 是否成功 */
|
||||
success: boolean;
|
||||
/** 最终响应内容 */
|
||||
response?: string;
|
||||
/** 迭代次数 */
|
||||
iterations: number;
|
||||
/** 工具调用历史 */
|
||||
toolCalls: ToolCallRecord[];
|
||||
/** 错误信息 */
|
||||
error?: string;
|
||||
/** 总耗时(毫秒) */
|
||||
durationMs: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 信任边界检查结果
|
||||
*/
|
||||
export interface TrustBoundaryResult {
|
||||
/** 审批状态 */
|
||||
status: 'approved' | 'denied' | 'pending_human';
|
||||
/** 原因 */
|
||||
reason?: string;
|
||||
/** 人工审批请求 ID(status=pending_human 时) */
|
||||
requestId?: string;
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
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 列表
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
private async injectShortTermMemory(
|
||||
messages: LlmMessage[],
|
||||
sessionId: string,
|
||||
): Promise<void> {
|
||||
if (!this.shortTermMemory) return;
|
||||
|
||||
try {
|
||||
const history = this.shortTermMemory.getMessages(sessionId);
|
||||
if (history.length === 0) return;
|
||||
|
||||
// 只追加非 system 消息(system 消息通常由调用方设置)
|
||||
const nonSystemHistory = history.filter((m) => m.role !== 'system');
|
||||
if (nonSystemHistory.length > 0) {
|
||||
messages.push(...nonSystemHistory);
|
||||
this.logger.debug(
|
||||
`[AgenticLoop] 注入 ${nonSystemHistory.length} 条短期记忆`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`[AgenticLoop] 注入短期记忆失败`,
|
||||
error instanceof Error ? error.stack : String(error),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取经验并保存到长期记忆
|
||||
* 策略:将任务描述 + 工具调用链 + 最终结果摘要保存为经验
|
||||
*/
|
||||
private async extractAndSaveExperience(
|
||||
sessionId: string,
|
||||
task: AiTask,
|
||||
response: string,
|
||||
toolCalls: ToolCallRecord[],
|
||||
): Promise<void> {
|
||||
if (!this.longTermMemory) return;
|
||||
|
||||
try {
|
||||
// 保存任务完成经验
|
||||
const toolSummary =
|
||||
toolCalls.length > 0
|
||||
? `使用了 ${toolCalls.map((tc) => tc.name).join(', ')}`
|
||||
: '未使用工具';
|
||||
|
||||
await this.longTermMemory.remember({
|
||||
content: `任务 [${task.description}] 成功完成。${toolSummary}。结果摘要: ${response.slice(0, 200)}`,
|
||||
type: 'experience',
|
||||
sessionId,
|
||||
tags: [task.type, 'success'],
|
||||
});
|
||||
|
||||
this.logger.debug(`[AgenticLoop] 已保存任务经验到长期记忆`);
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`[AgenticLoop] 保存长期记忆失败`,
|
||||
error instanceof Error ? error.stack : String(error),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AgenticLoopService } from './agentic-loop.service';
|
||||
import { LoopDetectorService } from './loop-detector.service';
|
||||
import { LlmProviderModule } from '../providers/llm-provider.module';
|
||||
import { AiSkillsModule } from '../skills/ai-skills.module';
|
||||
import { AiMemoryModule } from '../memory/ai-memory.module';
|
||||
|
||||
/**
|
||||
* AI Runtime 模块
|
||||
* 借鉴 OpenClaw Agent Runtime
|
||||
* 提供 ReAct 循环 + 循环检测 + LLM Provider 管理 + Skills 执行 + 双模记忆
|
||||
*/
|
||||
@Module({
|
||||
imports: [LlmProviderModule.register(), AiSkillsModule, AiMemoryModule],
|
||||
providers: [AgenticLoopService, LoopDetectorService],
|
||||
exports: [AgenticLoopService, LoopDetectorService],
|
||||
})
|
||||
export class AiRuntimeModule {}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './agentic-loop.service';
|
||||
export * from './agentic-loop.interface';
|
||||
export * from './loop-detector.service';
|
||||
export * from './loop-detector.interface';
|
||||
export * from './ai-runtime.module';
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 循环检测警告
|
||||
*/
|
||||
export interface LoopWarning {
|
||||
/** 是否应停止循环 */
|
||||
shouldStop: boolean;
|
||||
/** 警告原因 */
|
||||
reason: string;
|
||||
/** 严重程度 */
|
||||
severity: 'info' | 'warning' | 'critical';
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具调用记录
|
||||
*/
|
||||
export interface ToolCallRecord {
|
||||
id: string;
|
||||
name: string;
|
||||
arguments: string;
|
||||
result?: string;
|
||||
timestamp: number;
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* Security Analyzer - 安全分析器
|
||||
@@ -17,7 +17,7 @@ export class SecurityAnalyzer {
|
||||
* 分析系统安全状态
|
||||
*/
|
||||
async analyzeSystemSecurity(): Promise<SecurityAnalysisResult> {
|
||||
this.logger.log("Starting system security analysis");
|
||||
this.logger.log('Starting system security analysis');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
@@ -51,7 +51,7 @@ export class SecurityAnalyzer {
|
||||
recommendations: this.generateRecommendations(overallRisk),
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error("Security analysis failed", error);
|
||||
this.logger.error('Security analysis failed', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ export class SecurityAnalyzer {
|
||||
const riskLevel = this.calculateCategoryRisk(results);
|
||||
|
||||
return {
|
||||
category: "authentication",
|
||||
category: 'authentication',
|
||||
riskLevel,
|
||||
checks: results,
|
||||
score: this.calculateSecurityScore(results),
|
||||
@@ -94,7 +94,7 @@ export class SecurityAnalyzer {
|
||||
const riskLevel = this.calculateCategoryRisk(results);
|
||||
|
||||
return {
|
||||
category: "dataProtection",
|
||||
category: 'dataProtection',
|
||||
riskLevel,
|
||||
checks: results,
|
||||
score: this.calculateSecurityScore(results),
|
||||
@@ -116,7 +116,7 @@ export class SecurityAnalyzer {
|
||||
const riskLevel = this.calculateCategoryRisk(results);
|
||||
|
||||
return {
|
||||
category: "networkSecurity",
|
||||
category: 'networkSecurity',
|
||||
riskLevel,
|
||||
checks: results,
|
||||
score: this.calculateSecurityScore(results),
|
||||
@@ -138,7 +138,7 @@ export class SecurityAnalyzer {
|
||||
const riskLevel = this.calculateCategoryRisk(results);
|
||||
|
||||
return {
|
||||
category: "codeQuality",
|
||||
category: 'codeQuality',
|
||||
riskLevel,
|
||||
checks: results,
|
||||
score: this.calculateSecurityScore(results),
|
||||
@@ -151,13 +151,13 @@ export class SecurityAnalyzer {
|
||||
private async checkJwtSecurity(): Promise<SecurityCheckResult> {
|
||||
// 实现 JWT 安全检查逻辑
|
||||
return {
|
||||
name: "JWT Security",
|
||||
status: "pass",
|
||||
riskLevel: "low",
|
||||
message: "JWT configuration is secure",
|
||||
name: 'JWT Security',
|
||||
status: 'pass',
|
||||
riskLevel: 'low',
|
||||
message: 'JWT configuration is secure',
|
||||
details: {
|
||||
algorithm: "RS256",
|
||||
expiration: "1h",
|
||||
algorithm: 'RS256',
|
||||
expiration: '1h',
|
||||
secretRotation: true,
|
||||
},
|
||||
};
|
||||
@@ -168,10 +168,10 @@ export class SecurityAnalyzer {
|
||||
*/
|
||||
private async checkPasswordPolicy(): Promise<SecurityCheckResult> {
|
||||
return {
|
||||
name: "Password Policy",
|
||||
status: "pass",
|
||||
riskLevel: "low",
|
||||
message: "Password policy meets security requirements",
|
||||
name: 'Password Policy',
|
||||
status: 'pass',
|
||||
riskLevel: 'low',
|
||||
message: 'Password policy meets security requirements',
|
||||
details: {
|
||||
minLength: 8,
|
||||
complexity: true,
|
||||
@@ -185,14 +185,14 @@ export class SecurityAnalyzer {
|
||||
*/
|
||||
private async checkSessionSecurity(): Promise<SecurityCheckResult> {
|
||||
return {
|
||||
name: "Session Security",
|
||||
status: "pass",
|
||||
riskLevel: "medium",
|
||||
message: "Session configuration needs improvement",
|
||||
name: 'Session Security',
|
||||
status: 'pass',
|
||||
riskLevel: 'medium',
|
||||
message: 'Session configuration needs improvement',
|
||||
details: {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "strict",
|
||||
sameSite: 'strict',
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -202,13 +202,13 @@ export class SecurityAnalyzer {
|
||||
*/
|
||||
private async checkMfaSecurity(): Promise<SecurityCheckResult> {
|
||||
return {
|
||||
name: "Multi-Factor Authentication",
|
||||
status: "warning",
|
||||
riskLevel: "medium",
|
||||
message: "MFA is not enabled for all users",
|
||||
name: 'Multi-Factor Authentication',
|
||||
status: 'warning',
|
||||
riskLevel: 'medium',
|
||||
message: 'MFA is not enabled for all users',
|
||||
details: {
|
||||
enabled: false,
|
||||
coverage: "30%",
|
||||
coverage: '30%',
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -218,14 +218,14 @@ export class SecurityAnalyzer {
|
||||
*/
|
||||
private async checkDataEncryption(): Promise<SecurityCheckResult> {
|
||||
return {
|
||||
name: "Data Encryption",
|
||||
status: "pass",
|
||||
riskLevel: "low",
|
||||
message: "Data encryption is properly configured",
|
||||
name: 'Data Encryption',
|
||||
status: 'pass',
|
||||
riskLevel: 'low',
|
||||
message: 'Data encryption is properly configured',
|
||||
details: {
|
||||
atRest: true,
|
||||
inTransit: true,
|
||||
algorithm: "AES-256",
|
||||
algorithm: 'AES-256',
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -235,10 +235,10 @@ export class SecurityAnalyzer {
|
||||
*/
|
||||
private async checkDataAccess(): Promise<SecurityCheckResult> {
|
||||
return {
|
||||
name: "Data Access Control",
|
||||
status: "pass",
|
||||
riskLevel: "low",
|
||||
message: "Data access controls are properly implemented",
|
||||
name: 'Data Access Control',
|
||||
status: 'pass',
|
||||
riskLevel: 'low',
|
||||
message: 'Data access controls are properly implemented',
|
||||
details: {
|
||||
rbac: true,
|
||||
audit: true,
|
||||
@@ -252,12 +252,12 @@ export class SecurityAnalyzer {
|
||||
*/
|
||||
private async checkDataBackup(): Promise<SecurityCheckResult> {
|
||||
return {
|
||||
name: "Data Backup",
|
||||
status: "pass",
|
||||
riskLevel: "low",
|
||||
message: "Data backup strategy is adequate",
|
||||
name: 'Data Backup',
|
||||
status: 'pass',
|
||||
riskLevel: 'low',
|
||||
message: 'Data backup strategy is adequate',
|
||||
details: {
|
||||
frequency: "daily",
|
||||
frequency: 'daily',
|
||||
encryption: true,
|
||||
offsite: true,
|
||||
},
|
||||
@@ -269,14 +269,14 @@ export class SecurityAnalyzer {
|
||||
*/
|
||||
private async checkDataRetention(): Promise<SecurityCheckResult> {
|
||||
return {
|
||||
name: "Data Retention",
|
||||
status: "pass",
|
||||
riskLevel: "low",
|
||||
message: "Data retention policies are compliant",
|
||||
name: 'Data Retention',
|
||||
status: 'pass',
|
||||
riskLevel: 'low',
|
||||
message: 'Data retention policies are compliant',
|
||||
details: {
|
||||
policy: "defined",
|
||||
policy: 'defined',
|
||||
automation: true,
|
||||
compliance: "GDPR",
|
||||
compliance: 'GDPR',
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -286,13 +286,13 @@ export class SecurityAnalyzer {
|
||||
*/
|
||||
private async checkHttpsSecurity(): Promise<SecurityCheckResult> {
|
||||
return {
|
||||
name: "HTTPS Security",
|
||||
status: "pass",
|
||||
riskLevel: "low",
|
||||
message: "HTTPS is properly configured",
|
||||
name: 'HTTPS Security',
|
||||
status: 'pass',
|
||||
riskLevel: 'low',
|
||||
message: 'HTTPS is properly configured',
|
||||
details: {
|
||||
enforced: true,
|
||||
tlsVersion: "1.3",
|
||||
tlsVersion: '1.3',
|
||||
hsts: true,
|
||||
},
|
||||
};
|
||||
@@ -303,14 +303,14 @@ export class SecurityAnalyzer {
|
||||
*/
|
||||
private async checkCorsConfiguration(): Promise<SecurityCheckResult> {
|
||||
return {
|
||||
name: "CORS Configuration",
|
||||
status: "warning",
|
||||
riskLevel: "medium",
|
||||
message: "CORS configuration may be too permissive",
|
||||
name: 'CORS Configuration',
|
||||
status: 'warning',
|
||||
riskLevel: 'medium',
|
||||
message: 'CORS configuration may be too permissive',
|
||||
details: {
|
||||
origins: ["*"],
|
||||
origins: ['*'],
|
||||
credentials: true,
|
||||
methods: ["GET", "POST", "PUT", "DELETE"],
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -320,13 +320,13 @@ export class SecurityAnalyzer {
|
||||
*/
|
||||
private async checkRateLimiting(): Promise<SecurityCheckResult> {
|
||||
return {
|
||||
name: "Rate Limiting",
|
||||
status: "pass",
|
||||
riskLevel: "low",
|
||||
message: "Rate limiting is properly configured",
|
||||
name: 'Rate Limiting',
|
||||
status: 'pass',
|
||||
riskLevel: 'low',
|
||||
message: 'Rate limiting is properly configured',
|
||||
details: {
|
||||
enabled: true,
|
||||
limits: "100/min",
|
||||
limits: '100/min',
|
||||
burst: 10,
|
||||
},
|
||||
};
|
||||
@@ -337,10 +337,10 @@ export class SecurityAnalyzer {
|
||||
*/
|
||||
private async checkFirewallRules(): Promise<SecurityCheckResult> {
|
||||
return {
|
||||
name: "Firewall Rules",
|
||||
status: "pass",
|
||||
riskLevel: "low",
|
||||
message: "Firewall rules are properly configured",
|
||||
name: 'Firewall Rules',
|
||||
status: 'pass',
|
||||
riskLevel: 'low',
|
||||
message: 'Firewall rules are properly configured',
|
||||
details: {
|
||||
enabled: true,
|
||||
defaultDeny: true,
|
||||
@@ -354,10 +354,10 @@ export class SecurityAnalyzer {
|
||||
*/
|
||||
private async checkInputValidation(): Promise<SecurityCheckResult> {
|
||||
return {
|
||||
name: "Input Validation",
|
||||
status: "pass",
|
||||
riskLevel: "low",
|
||||
message: "Input validation is comprehensive",
|
||||
name: 'Input Validation',
|
||||
status: 'pass',
|
||||
riskLevel: 'low',
|
||||
message: 'Input validation is comprehensive',
|
||||
details: {
|
||||
sanitization: true,
|
||||
validation: true,
|
||||
@@ -371,13 +371,13 @@ export class SecurityAnalyzer {
|
||||
*/
|
||||
private async checkSqlInjection(): Promise<SecurityCheckResult> {
|
||||
return {
|
||||
name: "SQL Injection Protection",
|
||||
status: "pass",
|
||||
riskLevel: "low",
|
||||
message: "SQL injection protection is effective",
|
||||
name: 'SQL Injection Protection',
|
||||
status: 'pass',
|
||||
riskLevel: 'low',
|
||||
message: 'SQL injection protection is effective',
|
||||
details: {
|
||||
parameterizedQueries: true,
|
||||
orm: "TypeORM",
|
||||
orm: 'TypeORM',
|
||||
escaping: true,
|
||||
},
|
||||
};
|
||||
@@ -388,10 +388,10 @@ export class SecurityAnalyzer {
|
||||
*/
|
||||
private async checkXssProtection(): Promise<SecurityCheckResult> {
|
||||
return {
|
||||
name: "XSS Protection",
|
||||
status: "pass",
|
||||
riskLevel: "low",
|
||||
message: "XSS protection is properly implemented",
|
||||
name: 'XSS Protection',
|
||||
status: 'pass',
|
||||
riskLevel: 'low',
|
||||
message: 'XSS protection is properly implemented',
|
||||
details: {
|
||||
csp: true,
|
||||
sanitization: true,
|
||||
@@ -405,10 +405,10 @@ export class SecurityAnalyzer {
|
||||
*/
|
||||
private async checkDependencyVulnerabilities(): Promise<SecurityCheckResult> {
|
||||
return {
|
||||
name: "Dependency Vulnerabilities",
|
||||
status: "warning",
|
||||
riskLevel: "medium",
|
||||
message: "Some dependencies have known vulnerabilities",
|
||||
name: 'Dependency Vulnerabilities',
|
||||
status: 'warning',
|
||||
riskLevel: 'medium',
|
||||
message: 'Some dependencies have known vulnerabilities',
|
||||
details: {
|
||||
total: 150,
|
||||
vulnerable: 3,
|
||||
@@ -425,10 +425,10 @@ export class SecurityAnalyzer {
|
||||
private calculateCategoryRisk(results: SecurityCheckResult[]): RiskLevel {
|
||||
const riskLevels = results.map((r) => r.riskLevel);
|
||||
|
||||
if (riskLevels.includes("critical")) return "critical";
|
||||
if (riskLevels.includes("high")) return "high";
|
||||
if (riskLevels.includes("medium")) return "medium";
|
||||
return "low";
|
||||
if (riskLevels.includes('critical')) return 'critical';
|
||||
if (riskLevels.includes('high')) return 'high';
|
||||
if (riskLevels.includes('medium')) return 'medium';
|
||||
return 'low';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -442,10 +442,10 @@ export class SecurityAnalyzer {
|
||||
);
|
||||
const avgWeight = totalWeight / categoryRisks.length;
|
||||
|
||||
if (avgWeight >= 3.5) return "critical";
|
||||
if (avgWeight >= 2.5) return "high";
|
||||
if (avgWeight >= 1.5) return "medium";
|
||||
return "low";
|
||||
if (avgWeight >= 3.5) return 'critical';
|
||||
if (avgWeight >= 2.5) return 'high';
|
||||
if (avgWeight >= 1.5) return 'medium';
|
||||
return 'low';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -466,28 +466,28 @@ export class SecurityAnalyzer {
|
||||
private generateRecommendations(riskLevel: RiskLevel): string[] {
|
||||
const recommendations: Record<RiskLevel, string[]> = {
|
||||
critical: [
|
||||
"Immediately address critical security vulnerabilities",
|
||||
"Implement emergency security patches",
|
||||
"Review and strengthen access controls",
|
||||
"Conduct comprehensive security audit",
|
||||
'Immediately address critical security vulnerabilities',
|
||||
'Implement emergency security patches',
|
||||
'Review and strengthen access controls',
|
||||
'Conduct comprehensive security audit',
|
||||
],
|
||||
high: [
|
||||
"Address high-priority security issues within 24 hours",
|
||||
"Implement additional security monitoring",
|
||||
"Review security policies and procedures",
|
||||
"Consider security training for development team",
|
||||
'Address high-priority security issues within 24 hours',
|
||||
'Implement additional security monitoring',
|
||||
'Review security policies and procedures',
|
||||
'Consider security training for development team',
|
||||
],
|
||||
medium: [
|
||||
"Address medium-priority security issues within a week",
|
||||
"Implement security best practices",
|
||||
"Regular security assessments",
|
||||
"Update security documentation",
|
||||
'Address medium-priority security issues within a week',
|
||||
'Implement security best practices',
|
||||
'Regular security assessments',
|
||||
'Update security documentation',
|
||||
],
|
||||
low: [
|
||||
"Maintain current security posture",
|
||||
"Continue regular security monitoring",
|
||||
"Keep security tools and policies updated",
|
||||
"Periodic security reviews",
|
||||
'Maintain current security posture',
|
||||
'Continue regular security monitoring',
|
||||
'Keep security tools and policies updated',
|
||||
'Periodic security reviews',
|
||||
],
|
||||
};
|
||||
|
||||
@@ -518,10 +518,10 @@ export interface SecurityCategoryResult {
|
||||
|
||||
export interface SecurityCheckResult {
|
||||
name: string;
|
||||
status: "pass" | "warning" | "fail";
|
||||
status: 'pass' | 'warning' | 'fail';
|
||||
riskLevel: RiskLevel;
|
||||
message: string;
|
||||
details: Record<string, any>;
|
||||
}
|
||||
|
||||
export type RiskLevel = "low" | "medium" | "high" | "critical";
|
||||
export type RiskLevel = 'low' | 'medium' | 'high' | 'critical';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* Vulnerability Detector - 漏洞检测器
|
||||
@@ -17,7 +17,7 @@ export class VulnerabilityDetector {
|
||||
* 执行全面漏洞扫描
|
||||
*/
|
||||
async scanVulnerabilities(): Promise<VulnerabilityScanResult> {
|
||||
this.logger.log("Starting comprehensive vulnerability scan");
|
||||
this.logger.log('Starting comprehensive vulnerability scan');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
@@ -51,7 +51,7 @@ export class VulnerabilityDetector {
|
||||
this.generateVulnerabilityRecommendations(allVulnerabilities),
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error("Vulnerability scan failed", error);
|
||||
this.logger.error('Vulnerability scan failed', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ export class VulnerabilityDetector {
|
||||
* 扫描代码漏洞
|
||||
*/
|
||||
private async scanCodeVulnerabilities(): Promise<Vulnerability[]> {
|
||||
this.logger.debug("Scanning code vulnerabilities");
|
||||
this.logger.debug('Scanning code vulnerabilities');
|
||||
|
||||
// 模拟代码漏洞扫描
|
||||
const vulnerabilities: Vulnerability[] = [];
|
||||
@@ -88,45 +88,45 @@ export class VulnerabilityDetector {
|
||||
* 扫描依赖漏洞
|
||||
*/
|
||||
private async scanDependencyVulnerabilities(): Promise<Vulnerability[]> {
|
||||
this.logger.debug("Scanning dependency vulnerabilities");
|
||||
this.logger.debug('Scanning dependency vulnerabilities');
|
||||
|
||||
// 模拟依赖漏洞扫描结果
|
||||
return [
|
||||
{
|
||||
id: "CVE-2023-1234",
|
||||
type: "dependency",
|
||||
severity: "high",
|
||||
title: "Remote Code Execution in lodash",
|
||||
id: 'CVE-2023-1234',
|
||||
type: 'dependency',
|
||||
severity: 'high',
|
||||
title: 'Remote Code Execution in lodash',
|
||||
description:
|
||||
"A prototype pollution vulnerability in lodash allows remote code execution",
|
||||
affectedComponent: "lodash@4.17.20",
|
||||
cweId: "CWE-1321",
|
||||
'A prototype pollution vulnerability in lodash allows remote code execution',
|
||||
affectedComponent: 'lodash@4.17.20',
|
||||
cweId: 'CWE-1321',
|
||||
cvssScore: 8.5,
|
||||
discoveredAt: Date.now(),
|
||||
status: "open",
|
||||
status: 'open',
|
||||
remediation: {
|
||||
type: "update",
|
||||
description: "Update lodash to version 4.17.21 or later",
|
||||
effort: "low",
|
||||
type: 'update',
|
||||
description: 'Update lodash to version 4.17.21 or later',
|
||||
effort: 'low',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "CVE-2023-5678",
|
||||
type: "dependency",
|
||||
severity: "medium",
|
||||
title: "Information Disclosure in express",
|
||||
id: 'CVE-2023-5678',
|
||||
type: 'dependency',
|
||||
severity: 'medium',
|
||||
title: 'Information Disclosure in express',
|
||||
description:
|
||||
"Express middleware may leak sensitive information in error messages",
|
||||
affectedComponent: "express@4.18.0",
|
||||
cweId: "CWE-200",
|
||||
'Express middleware may leak sensitive information in error messages',
|
||||
affectedComponent: 'express@4.18.0',
|
||||
cweId: 'CWE-200',
|
||||
cvssScore: 5.3,
|
||||
discoveredAt: Date.now(),
|
||||
status: "open",
|
||||
status: 'open',
|
||||
remediation: {
|
||||
type: "configuration",
|
||||
type: 'configuration',
|
||||
description:
|
||||
"Configure error handling to prevent information leakage",
|
||||
effort: "medium",
|
||||
'Configure error handling to prevent information leakage',
|
||||
effort: 'medium',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -136,25 +136,25 @@ export class VulnerabilityDetector {
|
||||
* 扫描配置漏洞
|
||||
*/
|
||||
private async scanConfigurationVulnerabilities(): Promise<Vulnerability[]> {
|
||||
this.logger.debug("Scanning configuration vulnerabilities");
|
||||
this.logger.debug('Scanning configuration vulnerabilities');
|
||||
|
||||
return [
|
||||
{
|
||||
id: "CONFIG-001",
|
||||
type: "configuration",
|
||||
severity: "medium",
|
||||
title: "Weak CORS Configuration",
|
||||
id: 'CONFIG-001',
|
||||
type: 'configuration',
|
||||
severity: 'medium',
|
||||
title: 'Weak CORS Configuration',
|
||||
description:
|
||||
"CORS is configured to allow all origins which may lead to security issues",
|
||||
affectedComponent: "CORS Middleware",
|
||||
cweId: "CWE-346",
|
||||
'CORS is configured to allow all origins which may lead to security issues',
|
||||
affectedComponent: 'CORS Middleware',
|
||||
cweId: 'CWE-346',
|
||||
cvssScore: 4.3,
|
||||
discoveredAt: Date.now(),
|
||||
status: "open",
|
||||
status: 'open',
|
||||
remediation: {
|
||||
type: "configuration",
|
||||
description: "Restrict CORS origins to specific trusted domains",
|
||||
effort: "low",
|
||||
type: 'configuration',
|
||||
description: 'Restrict CORS origins to specific trusted domains',
|
||||
effort: 'low',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -164,25 +164,25 @@ export class VulnerabilityDetector {
|
||||
* 扫描网络漏洞
|
||||
*/
|
||||
private async scanNetworkVulnerabilities(): Promise<Vulnerability[]> {
|
||||
this.logger.debug("Scanning network vulnerabilities");
|
||||
this.logger.debug('Scanning network vulnerabilities');
|
||||
|
||||
return [
|
||||
{
|
||||
id: "NET-001",
|
||||
type: "network",
|
||||
severity: "low",
|
||||
title: "Missing Security Headers",
|
||||
description: "Some security headers are not configured properly",
|
||||
affectedComponent: "HTTP Headers",
|
||||
cweId: "CWE-693",
|
||||
id: 'NET-001',
|
||||
type: 'network',
|
||||
severity: 'low',
|
||||
title: 'Missing Security Headers',
|
||||
description: 'Some security headers are not configured properly',
|
||||
affectedComponent: 'HTTP Headers',
|
||||
cweId: 'CWE-693',
|
||||
cvssScore: 3.1,
|
||||
discoveredAt: Date.now(),
|
||||
status: "open",
|
||||
status: 'open',
|
||||
remediation: {
|
||||
type: "configuration",
|
||||
type: 'configuration',
|
||||
description:
|
||||
"Add missing security headers (CSP, HSTS, X-Frame-Options)",
|
||||
effort: "low",
|
||||
'Add missing security headers (CSP, HSTS, X-Frame-Options)',
|
||||
effort: 'low',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -249,10 +249,10 @@ export class VulnerabilityDetector {
|
||||
const recommendations: string[] = [];
|
||||
|
||||
const criticalCount = vulnerabilities.filter(
|
||||
(v) => v.severity === "critical",
|
||||
(v) => v.severity === 'critical',
|
||||
).length;
|
||||
const highCount = vulnerabilities.filter(
|
||||
(v) => v.severity === "high",
|
||||
(v) => v.severity === 'high',
|
||||
).length;
|
||||
|
||||
if (criticalCount > 0) {
|
||||
@@ -268,11 +268,11 @@ export class VulnerabilityDetector {
|
||||
}
|
||||
|
||||
recommendations.push(
|
||||
"Implement automated vulnerability scanning in CI/CD pipeline",
|
||||
'Implement automated vulnerability scanning in CI/CD pipeline',
|
||||
);
|
||||
recommendations.push("Regular security training for development team");
|
||||
recommendations.push('Regular security training for development team');
|
||||
recommendations.push(
|
||||
"Establish vulnerability disclosure and response process",
|
||||
'Establish vulnerability disclosure and response process',
|
||||
);
|
||||
|
||||
return recommendations;
|
||||
@@ -282,7 +282,7 @@ export class VulnerabilityDetector {
|
||||
* 检测实时威胁
|
||||
*/
|
||||
async detectRealTimeThreats(): Promise<ThreatDetectionResult> {
|
||||
this.logger.log("Starting real-time threat detection");
|
||||
this.logger.log('Starting real-time threat detection');
|
||||
|
||||
const threats = await Promise.all([
|
||||
this.detectSuspiciousActivity(),
|
||||
@@ -338,18 +338,18 @@ export class VulnerabilityDetector {
|
||||
*/
|
||||
private calculateThreatRiskLevel(
|
||||
threats: Threat[],
|
||||
): "low" | "medium" | "high" | "critical" {
|
||||
if (threats.length === 0) return "low";
|
||||
): 'low' | 'medium' | 'high' | 'critical' {
|
||||
if (threats.length === 0) return 'low';
|
||||
|
||||
const highSeverityThreats = threats.filter(
|
||||
(t) => t.severity === "high" || t.severity === "critical",
|
||||
(t) => t.severity === 'high' || t.severity === 'critical',
|
||||
);
|
||||
|
||||
if (highSeverityThreats.length > 5) return "critical";
|
||||
if (highSeverityThreats.length > 2) return "high";
|
||||
if (threats.length > 10) return "medium";
|
||||
if (highSeverityThreats.length > 5) return 'critical';
|
||||
if (highSeverityThreats.length > 2) return 'high';
|
||||
if (threats.length > 10) return 'medium';
|
||||
|
||||
return "low";
|
||||
return 'low';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,19 +365,19 @@ export interface VulnerabilityScanResult {
|
||||
|
||||
export interface Vulnerability {
|
||||
id: string;
|
||||
type: "code" | "dependency" | "configuration" | "network";
|
||||
severity: "low" | "medium" | "high" | "critical";
|
||||
type: 'code' | 'dependency' | 'configuration' | 'network';
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
title: string;
|
||||
description: string;
|
||||
affectedComponent: string;
|
||||
cweId?: string;
|
||||
cvssScore?: number;
|
||||
discoveredAt: number;
|
||||
status: "open" | "in_progress" | "resolved" | "false_positive";
|
||||
status: 'open' | 'in_progress' | 'resolved' | 'false_positive';
|
||||
remediation: {
|
||||
type: "update" | "patch" | "configuration" | "code_change";
|
||||
type: 'update' | 'patch' | 'configuration' | 'code_change';
|
||||
description: string;
|
||||
effort: "low" | "medium" | "high";
|
||||
effort: 'low' | 'medium' | 'high';
|
||||
};
|
||||
}
|
||||
|
||||
@@ -392,17 +392,17 @@ export interface ThreatDetectionResult {
|
||||
timestamp: number;
|
||||
threatsDetected: number;
|
||||
threats: Threat[];
|
||||
riskLevel: "low" | "medium" | "high" | "critical";
|
||||
riskLevel: 'low' | 'medium' | 'high' | 'critical';
|
||||
}
|
||||
|
||||
export interface Threat {
|
||||
id: string;
|
||||
type:
|
||||
| "suspicious_activity"
|
||||
| "anomalous_traffic"
|
||||
| "brute_force"
|
||||
| "malicious_payload";
|
||||
severity: "low" | "medium" | "high" | "critical";
|
||||
| 'suspicious_activity'
|
||||
| 'anomalous_traffic'
|
||||
| 'brute_force'
|
||||
| 'malicious_payload';
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
source: string;
|
||||
description: string;
|
||||
detectedAt: number;
|
||||
|
||||
@@ -3,9 +3,9 @@ import {
|
||||
Logger,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
} from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
import { Observable } from "rxjs";
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Access Protector - 访问保护器
|
||||
@@ -89,7 +89,7 @@ export class AccessProtector implements CanActivate {
|
||||
this.logger.warn(
|
||||
`Permission denied for user ${userId}: ${action} on ${resource}`,
|
||||
);
|
||||
await this.logSecurityEvent("PERMISSION_DENIED", {
|
||||
await this.logSecurityEvent('PERMISSION_DENIED', {
|
||||
userId,
|
||||
resource,
|
||||
action,
|
||||
@@ -121,7 +121,7 @@ export class AccessProtector implements CanActivate {
|
||||
if (!policy) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: "Policy not found",
|
||||
reason: 'Policy not found',
|
||||
actions: [],
|
||||
};
|
||||
}
|
||||
@@ -141,7 +141,7 @@ export class AccessProtector implements CanActivate {
|
||||
* 监控访问模式
|
||||
*/
|
||||
async monitorAccessPatterns(): Promise<AccessPatternAnalysis> {
|
||||
this.logger.log("Analyzing access patterns");
|
||||
this.logger.log('Analyzing access patterns');
|
||||
|
||||
const analysis = {
|
||||
timestamp: Date.now(),
|
||||
@@ -178,8 +178,8 @@ export class AccessProtector implements CanActivate {
|
||||
request.ip ||
|
||||
request.connection?.remoteAddress ||
|
||||
request.socket?.remoteAddress ||
|
||||
request.headers["x-forwarded-for"]?.split(",")[0] ||
|
||||
"unknown"
|
||||
request.headers['x-forwarded-for']?.split(',')[0] ||
|
||||
'unknown'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ export class AccessProtector implements CanActivate {
|
||||
timestamp: Date.now(),
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
userAgent: request.headers["user-agent"],
|
||||
userAgent: request.headers['user-agent'],
|
||||
success: true,
|
||||
});
|
||||
|
||||
@@ -234,7 +234,7 @@ export class AccessProtector implements CanActivate {
|
||||
}
|
||||
|
||||
// 检查异常 User-Agent
|
||||
const userAgent = request.headers["user-agent"];
|
||||
const userAgent = request.headers['user-agent'];
|
||||
if (!userAgent || this.isSuspiciousUserAgent(userAgent)) {
|
||||
return true;
|
||||
}
|
||||
@@ -258,11 +258,11 @@ export class AccessProtector implements CanActivate {
|
||||
const activities = this.suspiciousActivities.get(ip)!;
|
||||
activities.push({
|
||||
timestamp: Date.now(),
|
||||
type: "suspicious_request",
|
||||
type: 'suspicious_request',
|
||||
details: {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
userAgent: request.headers["user-agent"],
|
||||
userAgent: request.headers['user-agent'],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -316,10 +316,10 @@ export class AccessProtector implements CanActivate {
|
||||
// 模拟返回权限数据
|
||||
return {
|
||||
userId,
|
||||
roles: ["user"],
|
||||
roles: ['user'],
|
||||
permissions: [
|
||||
{ resource: "user", actions: ["read", "update"] },
|
||||
{ resource: "profile", actions: ["read", "update"] },
|
||||
{ resource: 'user', actions: ['read', 'update'] },
|
||||
{ resource: 'profile', actions: ['read', 'update'] },
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -347,18 +347,18 @@ export class AccessProtector implements CanActivate {
|
||||
// 模拟安全策略
|
||||
const policies: Record<string, SecurityPolicy> = {
|
||||
rate_limit: {
|
||||
name: "rate_limit",
|
||||
name: 'rate_limit',
|
||||
conditions: [
|
||||
{ type: "request_count", threshold: 100, timeWindow: 60000 },
|
||||
{ type: 'request_count', threshold: 100, timeWindow: 60000 },
|
||||
],
|
||||
denyActions: ["block_ip", "log_event"],
|
||||
denyActions: ['block_ip', 'log_event'],
|
||||
},
|
||||
geo_restriction: {
|
||||
name: "geo_restriction",
|
||||
name: 'geo_restriction',
|
||||
conditions: [
|
||||
{ type: "geo_location", allowedCountries: ["CN", "US", "EU"] },
|
||||
{ type: 'geo_location', allowedCountries: ['CN', 'US', 'EU'] },
|
||||
],
|
||||
denyActions: ["block_request", "log_event"],
|
||||
denyActions: ['block_request', 'log_event'],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -379,7 +379,7 @@ export class AccessProtector implements CanActivate {
|
||||
}
|
||||
}
|
||||
|
||||
return { allowed: true, reason: "All conditions passed", actions: [] };
|
||||
return { allowed: true, reason: 'All conditions passed', actions: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -390,12 +390,12 @@ export class AccessProtector implements CanActivate {
|
||||
context: SecurityContext,
|
||||
): Promise<PolicyResult> {
|
||||
switch (condition.type) {
|
||||
case "request_count":
|
||||
case 'request_count':
|
||||
return this.evaluateRequestCountCondition(condition, context);
|
||||
case "geo_location":
|
||||
case 'geo_location':
|
||||
return this.evaluateGeoLocationCondition(condition, context);
|
||||
default:
|
||||
return { allowed: true, reason: "Unknown condition type", actions: [] };
|
||||
return { allowed: true, reason: 'Unknown condition type', actions: [] };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,13 +415,13 @@ export class AccessProtector implements CanActivate {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Request count exceeded: ${recentAttempts.length}/${condition.threshold}`,
|
||||
actions: ["rate_limit"],
|
||||
actions: ['rate_limit'],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
allowed: true,
|
||||
reason: "Request count within limits",
|
||||
reason: 'Request count within limits',
|
||||
actions: [],
|
||||
};
|
||||
}
|
||||
@@ -434,19 +434,19 @@ export class AccessProtector implements CanActivate {
|
||||
context: SecurityContext,
|
||||
): PolicyResult {
|
||||
// 模拟地理位置检查
|
||||
const userCountry = "CN"; // 应该从 IP 地理位置服务获取
|
||||
const userCountry = 'CN'; // 应该从 IP 地理位置服务获取
|
||||
|
||||
if (!condition.allowedCountries!.includes(userCountry)) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Access from restricted country: ${userCountry}`,
|
||||
actions: ["geo_block"],
|
||||
actions: ['geo_block'],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
allowed: true,
|
||||
reason: "Geographic location allowed",
|
||||
reason: 'Geographic location allowed',
|
||||
actions: [],
|
||||
};
|
||||
}
|
||||
@@ -460,13 +460,13 @@ export class AccessProtector implements CanActivate {
|
||||
): Promise<void> {
|
||||
for (const action of actions) {
|
||||
switch (action) {
|
||||
case "block_ip":
|
||||
case 'block_ip':
|
||||
this.blockedIps.add(context.clientIp);
|
||||
break;
|
||||
case "log_event":
|
||||
await this.logSecurityEvent("POLICY_VIOLATION", context);
|
||||
case 'log_event':
|
||||
await this.logSecurityEvent('POLICY_VIOLATION', context);
|
||||
break;
|
||||
case "block_request":
|
||||
case 'block_request':
|
||||
// 请求已被阻止,无需额外操作
|
||||
break;
|
||||
}
|
||||
@@ -497,10 +497,10 @@ export class AccessProtector implements CanActivate {
|
||||
|
||||
if (recentAttempts.length > 1000) {
|
||||
anomalies.push({
|
||||
type: "high_frequency",
|
||||
type: 'high_frequency',
|
||||
ip,
|
||||
description: `Unusually high access frequency: ${recentAttempts.length} requests in 1 hour`,
|
||||
severity: "high",
|
||||
severity: 'high',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
@@ -598,6 +598,6 @@ export interface AccessAnomaly {
|
||||
type: string;
|
||||
ip: string;
|
||||
description: string;
|
||||
severity: "low" | "medium" | "high" | "critical";
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { SecurityAnalyzer } from "./analyzers/security.analyzer";
|
||||
import { VulnerabilityDetector } from "./detectors/vulnerability.detector";
|
||||
import { AccessProtector } from "./protectors/access.protector";
|
||||
import { AiSecurityService } from "./services/ai-security.service";
|
||||
import { AiAuditService } from "./services/ai-audit.service";
|
||||
import { SafeReadyService } from "./services/safe-ready.service";
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SecurityAnalyzer } from './analyzers/security.analyzer';
|
||||
import { VulnerabilityDetector } from './detectors/vulnerability.detector';
|
||||
import { AccessProtector } from './protectors/access.protector';
|
||||
import { AiSecurityService } from './services/ai-security.service';
|
||||
import { AiAuditService } from './services/ai-audit.service';
|
||||
import { SafeReadyService } from './services/safe-ready.service';
|
||||
|
||||
/**
|
||||
* AI Safe Module - AI 安全模块
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { Injectable, Logger, BadRequestException } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* AI Audit Service - AI 审计服务
|
||||
@@ -42,7 +42,7 @@ export class AiAuditService {
|
||||
}
|
||||
|
||||
// 如果是高严重性事件,立即处理
|
||||
if (auditLog.severity === "high" || auditLog.severity === "critical") {
|
||||
if (auditLog.severity === 'high' || auditLog.severity === 'critical') {
|
||||
await this.handleHighSeverityEvent(auditLog);
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ export class AiAuditService {
|
||||
* 生成审计报告
|
||||
*/
|
||||
async generateAuditReport(options: AuditReportOptions): Promise<AuditReport> {
|
||||
this.logger.log("Generating audit report");
|
||||
this.logger.log('Generating audit report');
|
||||
|
||||
const startTime =
|
||||
options.startTime || Date.now() - 30 * 24 * 60 * 60 * 1000; // 默认30天
|
||||
@@ -105,7 +105,7 @@ export class AiAuditService {
|
||||
* 执行合规性检查
|
||||
*/
|
||||
async performComplianceCheck(): Promise<ComplianceCheckResult> {
|
||||
this.logger.log("Performing compliance check");
|
||||
this.logger.log('Performing compliance check');
|
||||
|
||||
const results: ComplianceRuleResult[] = [];
|
||||
|
||||
@@ -133,7 +133,7 @@ export class AiAuditService {
|
||||
async searchAuditLogs(
|
||||
criteria: AuditSearchCriteria,
|
||||
): Promise<AuditSearchResult> {
|
||||
this.logger.debug("Searching audit logs", criteria);
|
||||
this.logger.debug('Searching audit logs', criteria);
|
||||
|
||||
let filteredLogs = [...this.auditLogs];
|
||||
|
||||
@@ -202,7 +202,7 @@ export class AiAuditService {
|
||||
* 导出审计数据
|
||||
*/
|
||||
async exportAuditData(
|
||||
format: "json" | "csv" | "xml",
|
||||
format: 'json' | 'csv' | 'xml',
|
||||
options: ExportOptions,
|
||||
): Promise<string> {
|
||||
this.logger.log(`Exporting audit data in ${format} format`);
|
||||
@@ -216,14 +216,14 @@ export class AiAuditService {
|
||||
);
|
||||
|
||||
switch (format) {
|
||||
case "json":
|
||||
case 'json':
|
||||
return JSON.stringify(filteredLogs, null, 2);
|
||||
case "csv":
|
||||
case 'csv':
|
||||
return this.convertToCsv(filteredLogs);
|
||||
case "xml":
|
||||
case 'xml':
|
||||
return this.convertToXml(filteredLogs);
|
||||
default:
|
||||
throw new Error(`Unsupported export format: ${format}`);
|
||||
throw new BadRequestException(`Unsupported export format: ${format}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,29 +233,29 @@ export class AiAuditService {
|
||||
private initializeComplianceRules(): void {
|
||||
this.complianceRules.push(
|
||||
{
|
||||
id: "GDPR_DATA_ACCESS",
|
||||
name: "GDPR Data Access Logging",
|
||||
description: "All personal data access must be logged",
|
||||
category: "data_protection",
|
||||
severity: "high",
|
||||
id: 'GDPR_DATA_ACCESS',
|
||||
name: 'GDPR Data Access Logging',
|
||||
description: 'All personal data access must be logged',
|
||||
category: 'data_protection',
|
||||
severity: 'high',
|
||||
evaluator: this.evaluateDataAccessLogging.bind(this),
|
||||
},
|
||||
{
|
||||
id: "SOX_FINANCIAL_ACCESS",
|
||||
name: "SOX Financial Data Access Control",
|
||||
id: 'SOX_FINANCIAL_ACCESS',
|
||||
name: 'SOX Financial Data Access Control',
|
||||
description:
|
||||
"Financial data access must be properly controlled and audited",
|
||||
category: "financial_compliance",
|
||||
severity: "critical",
|
||||
'Financial data access must be properly controlled and audited',
|
||||
category: 'financial_compliance',
|
||||
severity: 'critical',
|
||||
evaluator: this.evaluateFinancialAccessControl.bind(this),
|
||||
},
|
||||
{
|
||||
id: "ISO27001_INCIDENT_RESPONSE",
|
||||
name: "ISO 27001 Incident Response",
|
||||
id: 'ISO27001_INCIDENT_RESPONSE',
|
||||
name: 'ISO 27001 Incident Response',
|
||||
description:
|
||||
"Security incidents must be properly documented and responded to",
|
||||
category: "security_management",
|
||||
severity: "high",
|
||||
'Security incidents must be properly documented and responded to',
|
||||
category: 'security_management',
|
||||
severity: 'high',
|
||||
evaluator: this.evaluateIncidentResponse.bind(this),
|
||||
},
|
||||
);
|
||||
@@ -280,20 +280,20 @@ export class AiAuditService {
|
||||
*/
|
||||
private determineSeverity(
|
||||
event: AuditEvent,
|
||||
): "low" | "medium" | "high" | "critical" {
|
||||
const severityMap: Record<string, "low" | "medium" | "high" | "critical"> =
|
||||
): 'low' | 'medium' | 'high' | 'critical' {
|
||||
const severityMap: Record<string, 'low' | 'medium' | 'high' | 'critical'> =
|
||||
{
|
||||
LOGIN_SUCCESS: "low",
|
||||
LOGIN_FAILURE: "medium",
|
||||
PERMISSION_DENIED: "medium",
|
||||
DATA_ACCESS: "medium",
|
||||
DATA_MODIFICATION: "high",
|
||||
SECURITY_VIOLATION: "high",
|
||||
SYSTEM_BREACH: "critical",
|
||||
DATA_BREACH: "critical",
|
||||
LOGIN_SUCCESS: 'low',
|
||||
LOGIN_FAILURE: 'medium',
|
||||
PERMISSION_DENIED: 'medium',
|
||||
DATA_ACCESS: 'medium',
|
||||
DATA_MODIFICATION: 'high',
|
||||
SECURITY_VIOLATION: 'high',
|
||||
SYSTEM_BREACH: 'critical',
|
||||
DATA_BREACH: 'critical',
|
||||
};
|
||||
|
||||
return severityMap[event.type] || "medium";
|
||||
return severityMap[event.type] || 'medium';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -301,17 +301,17 @@ export class AiAuditService {
|
||||
*/
|
||||
private categorizeEvent(event: AuditEvent): string {
|
||||
const categoryMap: Record<string, string> = {
|
||||
LOGIN_SUCCESS: "authentication",
|
||||
LOGIN_FAILURE: "authentication",
|
||||
PERMISSION_DENIED: "authorization",
|
||||
DATA_ACCESS: "data_access",
|
||||
DATA_MODIFICATION: "data_modification",
|
||||
SECURITY_VIOLATION: "security",
|
||||
SYSTEM_BREACH: "security",
|
||||
DATA_BREACH: "security",
|
||||
LOGIN_SUCCESS: 'authentication',
|
||||
LOGIN_FAILURE: 'authentication',
|
||||
PERMISSION_DENIED: 'authorization',
|
||||
DATA_ACCESS: 'data_access',
|
||||
DATA_MODIFICATION: 'data_modification',
|
||||
SECURITY_VIOLATION: 'security',
|
||||
SYSTEM_BREACH: 'security',
|
||||
DATA_BREACH: 'security',
|
||||
};
|
||||
|
||||
return categoryMap[event.type] || "general";
|
||||
return categoryMap[event.type] || 'general';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -319,15 +319,15 @@ export class AiAuditService {
|
||||
*/
|
||||
private async checkCompliance(event: AuditEvent): Promise<ComplianceStatus> {
|
||||
// 简化的合规性检查
|
||||
const requiredFields = ["userId", "timestamp", "type", "description"];
|
||||
const requiredFields = ['userId', 'timestamp', 'type', 'description'];
|
||||
const hasRequiredFields = requiredFields.every(
|
||||
(field) => event[field as keyof AuditEvent],
|
||||
);
|
||||
|
||||
return {
|
||||
compliant: hasRequiredFields,
|
||||
violations: hasRequiredFields ? [] : ["Missing required fields"],
|
||||
rules: ["BASIC_AUDIT_REQUIREMENTS"],
|
||||
violations: hasRequiredFields ? [] : ['Missing required fields'],
|
||||
rules: ['BASIC_AUDIT_REQUIREMENTS'],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -424,12 +424,12 @@ export class AiAuditService {
|
||||
|
||||
return [
|
||||
{
|
||||
metric: "daily_events",
|
||||
metric: 'daily_events',
|
||||
values: Object.entries(dailyStats).map(([date, count]) => ({
|
||||
timestamp: new Date(date).getTime(),
|
||||
value: count,
|
||||
})),
|
||||
trend: "stable",
|
||||
trend: 'stable',
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -441,7 +441,7 @@ export class AiAuditService {
|
||||
const groups: Record<string, number> = {};
|
||||
|
||||
logs.forEach((log) => {
|
||||
const date = new Date(log.timestamp).toISOString().split("T")[0];
|
||||
const date = new Date(log.timestamp).toISOString().split('T')[0];
|
||||
groups[date] = (groups[date] || 0) + 1;
|
||||
});
|
||||
|
||||
@@ -464,9 +464,9 @@ export class AiAuditService {
|
||||
if (count > avgCount * 3) {
|
||||
// 超过平均值3倍
|
||||
anomalies.push({
|
||||
type: "high_frequency_event",
|
||||
type: 'high_frequency_event',
|
||||
description: `Unusually high frequency of ${type} events: ${count}`,
|
||||
severity: "medium",
|
||||
severity: 'medium',
|
||||
timestamp: Date.now(),
|
||||
metadata: { eventType: type, count },
|
||||
});
|
||||
@@ -487,22 +487,22 @@ export class AiAuditService {
|
||||
|
||||
if (complianceAnalysis.overallScore < 90) {
|
||||
recommendations.push(
|
||||
"Improve compliance by addressing identified violations",
|
||||
'Improve compliance by addressing identified violations',
|
||||
);
|
||||
}
|
||||
|
||||
if (complianceAnalysis.violationEvents > 0) {
|
||||
recommendations.push(
|
||||
"Review and update security policies to prevent violations",
|
||||
'Review and update security policies to prevent violations',
|
||||
);
|
||||
}
|
||||
|
||||
const criticalEvents = logs.filter(
|
||||
(log) => log.severity === "critical",
|
||||
(log) => log.severity === 'critical',
|
||||
).length;
|
||||
if (criticalEvents > 0) {
|
||||
recommendations.push(
|
||||
"Investigate and address critical security events immediately",
|
||||
'Investigate and address critical security events immediately',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -532,8 +532,8 @@ export class AiAuditService {
|
||||
ruleName: rule.name,
|
||||
compliant: false,
|
||||
score: 0,
|
||||
violations: ["Rule evaluation failed"],
|
||||
recommendations: ["Review rule implementation"],
|
||||
violations: ['Rule evaluation failed'],
|
||||
recommendations: ['Review rule implementation'],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -553,10 +553,10 @@ export class AiAuditService {
|
||||
*/
|
||||
private determineComplianceStatus(
|
||||
score: number,
|
||||
): "compliant" | "warning" | "non_compliant" {
|
||||
if (score >= 90) return "compliant";
|
||||
if (score >= 70) return "warning";
|
||||
return "non_compliant";
|
||||
): 'compliant' | 'warning' | 'non_compliant' {
|
||||
if (score >= 90) return 'compliant';
|
||||
if (score >= 70) return 'warning';
|
||||
return 'non_compliant';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -579,25 +579,25 @@ export class AiAuditService {
|
||||
*/
|
||||
private convertToCsv(logs: AuditLog[]): string {
|
||||
const headers = [
|
||||
"ID",
|
||||
"Timestamp",
|
||||
"Event Type",
|
||||
"User ID",
|
||||
"Description",
|
||||
"Severity",
|
||||
"Category",
|
||||
'ID',
|
||||
'Timestamp',
|
||||
'Event Type',
|
||||
'User ID',
|
||||
'Description',
|
||||
'Severity',
|
||||
'Category',
|
||||
];
|
||||
const rows = logs.map((log) => [
|
||||
log.id,
|
||||
new Date(log.timestamp).toISOString(),
|
||||
log.event.type,
|
||||
log.event.userId || "",
|
||||
log.event.userId || '',
|
||||
log.event.description,
|
||||
log.severity,
|
||||
log.category,
|
||||
]);
|
||||
|
||||
return [headers, ...rows].map((row) => row.join(",")).join("\n");
|
||||
return [headers, ...rows].map((row) => row.join(',')).join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -612,7 +612,7 @@ export class AiAuditService {
|
||||
<timestamp>${new Date(log.timestamp).toISOString()}</timestamp>
|
||||
<event>
|
||||
<type>${log.event.type}</type>
|
||||
<userId>${log.event.userId || ""}</userId>
|
||||
<userId>${log.event.userId || ''}</userId>
|
||||
<description>${log.event.description}</description>
|
||||
</event>
|
||||
<severity>${log.severity}</severity>
|
||||
@@ -620,7 +620,7 @@ export class AiAuditService {
|
||||
</log>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
.join('');
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?><auditLogs>${xmlLogs}</auditLogs>`;
|
||||
}
|
||||
@@ -659,8 +659,8 @@ export class AiAuditService {
|
||||
return {
|
||||
compliant: false,
|
||||
score: 70,
|
||||
violations: ["Incident response time exceeds policy requirements"],
|
||||
recommendations: ["Implement automated incident response procedures"],
|
||||
violations: ['Incident response time exceeds policy requirements'],
|
||||
recommendations: ['Implement automated incident response procedures'],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -678,7 +678,7 @@ export interface AuditLog {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
event: AuditEvent;
|
||||
severity: "low" | "medium" | "high" | "critical";
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
category: string;
|
||||
compliance: ComplianceStatus;
|
||||
}
|
||||
@@ -726,13 +726,13 @@ export interface ComplianceAnalysis {
|
||||
export interface SecurityTrend {
|
||||
metric: string;
|
||||
values: { timestamp: number; value: number }[];
|
||||
trend: "increasing" | "decreasing" | "stable";
|
||||
trend: 'increasing' | 'decreasing' | 'stable';
|
||||
}
|
||||
|
||||
export interface AuditAnomaly {
|
||||
type: string;
|
||||
description: string;
|
||||
severity: "low" | "medium" | "high" | "critical";
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
timestamp: number;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
@@ -742,7 +742,7 @@ export interface ComplianceRule {
|
||||
name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
severity: "low" | "medium" | "high" | "critical";
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
evaluator: () => Promise<ComplianceEvaluationResult>;
|
||||
}
|
||||
|
||||
@@ -756,7 +756,7 @@ export interface ComplianceEvaluationResult {
|
||||
export interface ComplianceCheckResult {
|
||||
timestamp: number;
|
||||
overallScore: number;
|
||||
status: "compliant" | "warning" | "non_compliant";
|
||||
status: 'compliant' | 'warning' | 'non_compliant';
|
||||
results: ComplianceRuleResult[];
|
||||
violations: ComplianceRuleResult[];
|
||||
recommendations: string[];
|
||||
@@ -775,7 +775,7 @@ export interface AuditSearchCriteria {
|
||||
startTime?: number;
|
||||
endTime?: number;
|
||||
eventType?: string;
|
||||
severity?: "low" | "medium" | "high" | "critical";
|
||||
severity?: 'low' | 'medium' | 'high' | 'critical';
|
||||
userId?: string;
|
||||
keyword?: string;
|
||||
page?: number;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { SecurityAnalyzer } from "../analyzers/security.analyzer";
|
||||
import { VulnerabilityDetector } from "../detectors/vulnerability.detector";
|
||||
import { AccessProtector } from "../protectors/access.protector";
|
||||
import { AiAuditService } from "./ai-audit.service";
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { SecurityAnalyzer } from '../analyzers/security.analyzer';
|
||||
import { VulnerabilityDetector } from '../detectors/vulnerability.detector';
|
||||
import { AccessProtector } from '../protectors/access.protector';
|
||||
import { AiAuditService } from './ai-audit.service';
|
||||
|
||||
/**
|
||||
* AI Security Service - AI 安全服务
|
||||
@@ -28,7 +28,7 @@ export class AiSecurityService {
|
||||
* 执行全面安全评估
|
||||
*/
|
||||
async performSecurityAssessment(): Promise<SecurityAssessmentResult> {
|
||||
this.logger.log("Starting comprehensive security assessment");
|
||||
this.logger.log('Starting comprehensive security assessment');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
@@ -76,7 +76,7 @@ export class AiSecurityService {
|
||||
nextAssessmentTime: Date.now() + 24 * 60 * 60 * 1000, // 24小时后
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error("Security assessment failed", error);
|
||||
this.logger.error('Security assessment failed', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ export class AiSecurityService {
|
||||
return {
|
||||
policyName,
|
||||
valid: false,
|
||||
reason: "Policy validation error",
|
||||
reason: 'Policy validation error',
|
||||
actions: [],
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
@@ -132,16 +132,16 @@ export class AiSecurityService {
|
||||
|
||||
try {
|
||||
switch (event.severity) {
|
||||
case "critical":
|
||||
case 'critical':
|
||||
response.actions = await this.handleCriticalSecurityEvent(event);
|
||||
break;
|
||||
case "high":
|
||||
case 'high':
|
||||
response.actions = await this.handleHighSecurityEvent(event);
|
||||
break;
|
||||
case "medium":
|
||||
case 'medium':
|
||||
response.actions = await this.handleMediumSecurityEvent(event);
|
||||
break;
|
||||
case "low":
|
||||
case 'low':
|
||||
response.actions = await this.handleLowSecurityEvent(event);
|
||||
break;
|
||||
}
|
||||
@@ -150,7 +150,7 @@ export class AiSecurityService {
|
||||
this.logger.log(`Security event ${event.id} handled successfully`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to handle security event ${event.id}`, error);
|
||||
response.error = error instanceof Error ? error.message : "Unknown error";
|
||||
response.error = error instanceof Error ? error.message : 'Unknown error';
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -160,7 +160,7 @@ export class AiSecurityService {
|
||||
* 获取安全状态仪表板
|
||||
*/
|
||||
async getSecurityDashboard(): Promise<SecurityDashboard> {
|
||||
this.logger.debug("Generating security dashboard");
|
||||
this.logger.debug('Generating security dashboard');
|
||||
|
||||
try {
|
||||
const [recentThreats, vulnerabilityStats, accessStats] =
|
||||
@@ -184,7 +184,7 @@ export class AiSecurityService {
|
||||
trends: await this.getSecurityTrends(),
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to generate security dashboard", error);
|
||||
this.logger.error('Failed to generate security dashboard', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -299,7 +299,8 @@ export class AiSecurityService {
|
||||
critical: 30,
|
||||
};
|
||||
|
||||
const penalty = threatsDetected * riskPenalties[riskLevel];
|
||||
const penalty =
|
||||
threatsDetected * riskPenalties[riskLevel as keyof typeof riskPenalties];
|
||||
|
||||
return Math.max(0, baseScore - penalty);
|
||||
}
|
||||
@@ -309,11 +310,11 @@ export class AiSecurityService {
|
||||
*/
|
||||
private determineRiskLevel(
|
||||
score: number,
|
||||
): "low" | "medium" | "high" | "critical" {
|
||||
if (score >= 90) return "low";
|
||||
if (score >= 70) return "medium";
|
||||
if (score >= 50) return "high";
|
||||
return "critical";
|
||||
): 'low' | 'medium' | 'high' | 'critical' {
|
||||
if (score >= 90) return 'low';
|
||||
if (score >= 70) return 'medium';
|
||||
if (score >= 50) return 'high';
|
||||
return 'critical';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -335,7 +336,7 @@ export class AiSecurityService {
|
||||
// 基于威胁检测的建议
|
||||
if (assessmentData.threatDetection?.threatsDetected > 0) {
|
||||
recommendations.push(
|
||||
"Implement enhanced threat monitoring and response procedures",
|
||||
'Implement enhanced threat monitoring and response procedures',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -349,15 +350,15 @@ export class AiSecurityService {
|
||||
event: SecurityEvent,
|
||||
): Promise<string[]> {
|
||||
const actions = [
|
||||
"immediate_alert",
|
||||
"block_source",
|
||||
"escalate_to_admin",
|
||||
"create_incident",
|
||||
'immediate_alert',
|
||||
'block_source',
|
||||
'escalate_to_admin',
|
||||
'create_incident',
|
||||
];
|
||||
|
||||
// 统一记录到审计服务
|
||||
await this.auditService.logAuditEvent({
|
||||
type: "security_critical_event",
|
||||
type: 'security_critical_event',
|
||||
userId: event.metadata?.userId,
|
||||
timestamp: Date.now(),
|
||||
description: `Critical security event: ${event.type}`,
|
||||
@@ -377,9 +378,9 @@ export class AiSecurityService {
|
||||
event: SecurityEvent,
|
||||
): Promise<string[]> {
|
||||
const actions = [
|
||||
"alert_security_team",
|
||||
"increase_monitoring",
|
||||
"log_detailed_info",
|
||||
'alert_security_team',
|
||||
'increase_monitoring',
|
||||
'log_detailed_info',
|
||||
];
|
||||
|
||||
return actions;
|
||||
@@ -391,7 +392,7 @@ export class AiSecurityService {
|
||||
private async handleMediumSecurityEvent(
|
||||
event: SecurityEvent,
|
||||
): Promise<string[]> {
|
||||
const actions = ["log_event", "monitor_source", "update_metrics"];
|
||||
const actions = ['log_event', 'monitor_source', 'update_metrics'];
|
||||
|
||||
return actions;
|
||||
}
|
||||
@@ -402,7 +403,7 @@ export class AiSecurityService {
|
||||
private async handleLowSecurityEvent(
|
||||
event: SecurityEvent,
|
||||
): Promise<string[]> {
|
||||
const actions = ["log_event", "update_statistics"];
|
||||
const actions = ['log_event', 'update_statistics'];
|
||||
|
||||
return actions;
|
||||
}
|
||||
@@ -452,9 +453,9 @@ export class AiSecurityService {
|
||||
/**
|
||||
* 确定整体安全状态
|
||||
*/
|
||||
private determineOverallSecurityStatus(): "secure" | "warning" | "critical" {
|
||||
private determineOverallSecurityStatus(): 'secure' | 'warning' | 'critical' {
|
||||
// 模拟安全状态判断
|
||||
return "secure";
|
||||
return 'secure';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -487,7 +488,7 @@ export interface SecurityAssessmentResult {
|
||||
timestamp: number;
|
||||
duration: number;
|
||||
overallScore: number;
|
||||
riskLevel: "low" | "medium" | "high" | "critical";
|
||||
riskLevel: 'low' | 'medium' | 'high' | 'critical';
|
||||
components: {
|
||||
securityAnalysis: any;
|
||||
vulnerabilityScan: any;
|
||||
@@ -509,7 +510,7 @@ export interface PolicyValidationResult {
|
||||
export interface SecurityEvent {
|
||||
id: string;
|
||||
type: string;
|
||||
severity: "low" | "medium" | "high" | "critical";
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
source: string;
|
||||
description: string;
|
||||
timestamp: number;
|
||||
@@ -526,7 +527,7 @@ export interface SecurityEventResponse {
|
||||
|
||||
export interface SecurityDashboard {
|
||||
timestamp: number;
|
||||
status: "secure" | "warning" | "critical";
|
||||
status: 'secure' | 'warning' | 'critical';
|
||||
metrics: {
|
||||
threatsDetected: number;
|
||||
vulnerabilitiesFound: number;
|
||||
@@ -541,7 +542,7 @@ export interface SecurityDashboard {
|
||||
export interface SecurityAlert {
|
||||
id: string;
|
||||
type: string;
|
||||
severity: "low" | "medium" | "high" | "critical";
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
message: string;
|
||||
timestamp: number;
|
||||
acknowledged: boolean;
|
||||
@@ -550,5 +551,5 @@ export interface SecurityAlert {
|
||||
export interface SecurityTrend {
|
||||
metric: string;
|
||||
values: { timestamp: number; value: number }[];
|
||||
trend: "increasing" | "decreasing" | "stable";
|
||||
trend: 'increasing' | 'decreasing' | 'stable';
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Injectable, Logger, OnModuleInit } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { EventBus } from "@wwjCommon/events/event-bus";
|
||||
import { SecurityAnalyzer } from "../analyzers/security.analyzer";
|
||||
import { VulnerabilityDetector } from "../detectors/vulnerability.detector";
|
||||
import { AccessProtector } from "../protectors/access.protector";
|
||||
import { AiSecurityService } from "./ai-security.service";
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||
import { SecurityAnalyzer } from '../analyzers/security.analyzer';
|
||||
import { VulnerabilityDetector } from '../detectors/vulnerability.detector';
|
||||
import { AccessProtector } from '../protectors/access.protector';
|
||||
import { AiSecurityService } from './ai-security.service';
|
||||
|
||||
@Injectable()
|
||||
export class SafeReadyService implements OnModuleInit {
|
||||
@@ -21,8 +21,8 @@ export class SafeReadyService implements OnModuleInit {
|
||||
|
||||
async onModuleInit() {
|
||||
const enabled =
|
||||
(this.config.get<string>("AI_SAFE_ENABLED") ?? "true") === "true";
|
||||
let currentState: "ready" | "unavailable" = "unavailable";
|
||||
(this.config.get<string>('AI_SAFE_ENABLED') ?? 'true') === 'true';
|
||||
let currentState: 'ready' | 'unavailable' = 'unavailable';
|
||||
|
||||
try {
|
||||
if (enabled) {
|
||||
@@ -34,18 +34,18 @@ export class SafeReadyService implements OnModuleInit {
|
||||
this.aiSecurityService,
|
||||
].every((c) => !!c);
|
||||
|
||||
currentState = componentsOk ? "ready" : "unavailable";
|
||||
currentState = componentsOk ? 'ready' : 'unavailable';
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.warn(
|
||||
`AI Safe readiness check failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
currentState = "unavailable";
|
||||
currentState = 'unavailable';
|
||||
}
|
||||
|
||||
this.eventBus.emit("module.state.changed", {
|
||||
module: "ai.safe",
|
||||
previousState: "initializing",
|
||||
this.eventBus.emit('module.state.changed', {
|
||||
module: 'ai.safe',
|
||||
previousState: 'initializing',
|
||||
currentState,
|
||||
meta: { enabled },
|
||||
});
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SkillRegistryService } from './skill-registry.service';
|
||||
import { SkillExecutorService } from './skill-executor.service';
|
||||
|
||||
/**
|
||||
* AI Skills 模块
|
||||
* 借鉴 OpenClaw Skills 系统
|
||||
* 提供技能注册、发现和执行能力
|
||||
*/
|
||||
@Module({
|
||||
providers: [SkillRegistryService, SkillExecutorService],
|
||||
exports: [SkillRegistryService, SkillExecutorService],
|
||||
})
|
||||
export class AiSkillsModule {}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './skill.interface';
|
||||
export * from './skill-registry.service';
|
||||
export * from './skill-executor.service';
|
||||
export * from './ai-skills.module';
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { SkillRegistryService } from './skill-registry.service';
|
||||
import { SkillContext, SkillResult } from './skill.interface';
|
||||
import { ToolCallRecord } from '../runtime/loop-detector.interface';
|
||||
|
||||
/**
|
||||
* 技能执行器 — 借鉴 OpenClaw Skill Executor
|
||||
* 作为 AgenticLoop 和具体 Skill 之间的桥梁
|
||||
*/
|
||||
@Injectable()
|
||||
export class SkillExecutorService {
|
||||
private readonly logger = new Logger(SkillExecutorService.name);
|
||||
|
||||
constructor(private readonly skillRegistry: SkillRegistryService) {}
|
||||
|
||||
/**
|
||||
* 执行工具调用并返回结果
|
||||
* @param toolName 工具名称
|
||||
* @param argsJson 工具参数 JSON 字符串
|
||||
* @param context 执行上下文
|
||||
* @returns 工具调用记录(包含结果)
|
||||
*/
|
||||
async execute(
|
||||
toolName: string,
|
||||
argsJson: string,
|
||||
context: SkillContext,
|
||||
): Promise<ToolCallRecord> {
|
||||
const startTime = Date.now();
|
||||
|
||||
const result: SkillResult = await this.skillRegistry.executeTool(
|
||||
toolName,
|
||||
argsJson,
|
||||
context,
|
||||
);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
const output = result.success ? result.output : `错误: ${result.error}`;
|
||||
|
||||
this.logger.log(
|
||||
`[SkillExecutor] ${toolName} ${result.success ? '成功' : '失败'} (${duration}ms)`,
|
||||
);
|
||||
|
||||
return {
|
||||
id: `tool_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
||||
name: toolName,
|
||||
arguments: argsJson,
|
||||
result: output,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用工具定义(供 LLM Function Calling 使用)
|
||||
*/
|
||||
getAvailableTools() {
|
||||
return this.skillRegistry.getAllToolDefinitions();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ISkill, SkillContext, SkillResult } from './skill.interface';
|
||||
import { LlmToolDefinition } from '../providers/llm-provider.interface';
|
||||
|
||||
/**
|
||||
* 技能注册中心 — 借鉴 OpenClaw Skills 系统
|
||||
* 管理所有已注册的技能,提供发现和执行能力
|
||||
*/
|
||||
@Injectable()
|
||||
export class SkillRegistryService {
|
||||
private readonly logger = new Logger(SkillRegistryService.name);
|
||||
private readonly skills = new Map<string, ISkill>();
|
||||
|
||||
/**
|
||||
* 注册技能
|
||||
*/
|
||||
registerSkill(skill: ISkill): void {
|
||||
const def = skill.getDefinition();
|
||||
this.skills.set(def.name, skill);
|
||||
this.logger.log(
|
||||
`技能注册: ${def.name} v${def.version} - ${def.description}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销技能
|
||||
*/
|
||||
unregisterSkill(name: string): void {
|
||||
this.skills.delete(name);
|
||||
this.logger.log(`技能注销: ${name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取技能
|
||||
*/
|
||||
getSkill(name: string): ISkill | undefined {
|
||||
return this.skills.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已注册技能名称
|
||||
*/
|
||||
getSkillNames(): string[] {
|
||||
return Array.from(this.skills.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有技能的工具定义(供 LLM Function Calling 使用)
|
||||
*/
|
||||
getAllToolDefinitions(): LlmToolDefinition[] {
|
||||
const tools: LlmToolDefinition[] = [];
|
||||
for (const skill of this.skills.values()) {
|
||||
const def = skill.getDefinition();
|
||||
if (def.tools) {
|
||||
tools.push(...def.tools);
|
||||
}
|
||||
}
|
||||
return tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据工具名称查找技能
|
||||
*/
|
||||
findSkillByToolName(toolName: string): ISkill | undefined {
|
||||
for (const skill of this.skills.values()) {
|
||||
const def = skill.getDefinition();
|
||||
if (def.tools?.some((t) => t.name === toolName)) {
|
||||
return skill;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行工具调用
|
||||
*/
|
||||
async executeTool(
|
||||
toolName: string,
|
||||
args: string,
|
||||
context: SkillContext,
|
||||
): Promise<SkillResult> {
|
||||
const skill = this.findSkillByToolName(toolName);
|
||||
if (!skill) {
|
||||
return {
|
||||
success: false,
|
||||
output: `工具 [${toolName}] 未注册`,
|
||||
error: `SKILL_NOT_FOUND: ${toolName}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
this.logger.debug(
|
||||
`[SkillExecutor] 执行工具: ${toolName} (技能: ${skill.getDefinition().name})`,
|
||||
);
|
||||
return await skill.execute(toolName, args, context);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`[SkillExecutor] 工具执行失败: ${toolName}`,
|
||||
error instanceof Error ? error.stack : String(error),
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
output: `工具 [${toolName}] 执行失败`,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
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>;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* Performance Analyzer - 性能分析器
|
||||
@@ -25,7 +25,7 @@ export class PerformanceAnalyzer {
|
||||
async analyzePerformance(
|
||||
options: AnalysisOptions = {},
|
||||
): Promise<PerformanceAnalysis> {
|
||||
this.logger.log("Starting performance analysis");
|
||||
this.logger.log('Starting performance analysis');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
@@ -119,7 +119,7 @@ export class PerformanceAnalyzer {
|
||||
* 获取性能趋势
|
||||
*/
|
||||
async getPerformanceTrends(
|
||||
period: "hour" | "day" | "week" | "month" = "day",
|
||||
period: 'hour' | 'day' | 'week' | 'month' = 'day',
|
||||
): Promise<PerformanceTrend[]> {
|
||||
this.logger.debug(`Getting performance trends for ${period}`);
|
||||
|
||||
@@ -141,39 +141,39 @@ export class PerformanceAnalyzer {
|
||||
// 生成趋势数据
|
||||
const trends: PerformanceTrend[] = [
|
||||
{
|
||||
metric: "overall_score",
|
||||
name: "Overall Performance Score",
|
||||
metric: 'overall_score',
|
||||
name: 'Overall Performance Score',
|
||||
values: groupedData.map((group) => ({
|
||||
timestamp: group.timestamp,
|
||||
value: this.calculateAverageScore(group.analyses, "overallScore"),
|
||||
value: this.calculateAverageScore(group.analyses, 'overallScore'),
|
||||
})),
|
||||
trend: this.calculateTrendDirection(groupedData, "overallScore"),
|
||||
trend: this.calculateTrendDirection(groupedData, 'overallScore'),
|
||||
},
|
||||
{
|
||||
metric: "cpu_usage",
|
||||
name: "CPU Usage",
|
||||
metric: 'cpu_usage',
|
||||
name: 'CPU Usage',
|
||||
values: groupedData.map((group) => ({
|
||||
timestamp: group.timestamp,
|
||||
value: this.calculateAverageScore(
|
||||
group.analyses,
|
||||
"analyses.cpu.score",
|
||||
'analyses.cpu.score',
|
||||
),
|
||||
})),
|
||||
trend: this.calculateTrendDirection(groupedData, "analyses.cpu.score"),
|
||||
trend: this.calculateTrendDirection(groupedData, 'analyses.cpu.score'),
|
||||
},
|
||||
{
|
||||
metric: "memory_usage",
|
||||
name: "Memory Usage",
|
||||
metric: 'memory_usage',
|
||||
name: 'Memory Usage',
|
||||
values: groupedData.map((group) => ({
|
||||
timestamp: group.timestamp,
|
||||
value: this.calculateAverageScore(
|
||||
group.analyses,
|
||||
"analyses.memory.score",
|
||||
'analyses.memory.score',
|
||||
),
|
||||
})),
|
||||
trend: this.calculateTrendDirection(
|
||||
groupedData,
|
||||
"analyses.memory.score",
|
||||
'analyses.memory.score',
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -185,40 +185,40 @@ export class PerformanceAnalyzer {
|
||||
* 获取性能基准
|
||||
*/
|
||||
async getPerformanceBenchmarks(): Promise<PerformanceBenchmark[]> {
|
||||
this.logger.debug("Getting performance benchmarks");
|
||||
this.logger.debug('Getting performance benchmarks');
|
||||
|
||||
return [
|
||||
{
|
||||
metric: "response_time",
|
||||
name: "API Response Time",
|
||||
metric: 'response_time',
|
||||
name: 'API Response Time',
|
||||
target: 200, // ms
|
||||
warning: 500,
|
||||
critical: 1000,
|
||||
unit: "ms",
|
||||
unit: 'ms',
|
||||
},
|
||||
{
|
||||
metric: "cpu_usage",
|
||||
name: "CPU Usage",
|
||||
metric: 'cpu_usage',
|
||||
name: 'CPU Usage',
|
||||
target: 70, // %
|
||||
warning: 85,
|
||||
critical: 95,
|
||||
unit: "%",
|
||||
unit: '%',
|
||||
},
|
||||
{
|
||||
metric: "memory_usage",
|
||||
name: "Memory Usage",
|
||||
metric: 'memory_usage',
|
||||
name: 'Memory Usage',
|
||||
target: 80, // %
|
||||
warning: 90,
|
||||
critical: 95,
|
||||
unit: "%",
|
||||
unit: '%',
|
||||
},
|
||||
{
|
||||
metric: "database_query_time",
|
||||
name: "Database Query Time",
|
||||
metric: 'database_query_time',
|
||||
name: 'Database Query Time',
|
||||
target: 100, // ms
|
||||
warning: 300,
|
||||
critical: 1000,
|
||||
unit: "ms",
|
||||
unit: 'ms',
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -238,7 +238,7 @@ export class PerformanceAnalyzer {
|
||||
const analysis2 = this.analysisHistory.find((a) => a.id === analysisId2);
|
||||
|
||||
if (!analysis1 || !analysis2) {
|
||||
throw new Error("One or both analyses not found");
|
||||
throw new NotFoundException('One or both analyses not found');
|
||||
}
|
||||
|
||||
const comparison: PerformanceComparison = {
|
||||
@@ -289,13 +289,13 @@ export class PerformanceAnalyzer {
|
||||
*/
|
||||
private initializeMetrics(): void {
|
||||
this.performanceMetrics.push(
|
||||
{ name: "cpu_usage", type: "percentage", threshold: 80 },
|
||||
{ name: "memory_usage", type: "percentage", threshold: 85 },
|
||||
{ name: "disk_io", type: "rate", threshold: 1000 },
|
||||
{ name: "network_io", type: "rate", threshold: 100 },
|
||||
{ name: "response_time", type: "duration", threshold: 500 },
|
||||
{ name: "throughput", type: "rate", threshold: 1000 },
|
||||
{ name: "error_rate", type: "percentage", threshold: 5 },
|
||||
{ name: 'cpu_usage', type: 'percentage', threshold: 80 },
|
||||
{ name: 'memory_usage', type: 'percentage', threshold: 85 },
|
||||
{ name: 'disk_io', type: 'rate', threshold: 1000 },
|
||||
{ name: 'network_io', type: 'rate', threshold: 100 },
|
||||
{ name: 'response_time', type: 'duration', threshold: 500 },
|
||||
{ name: 'throughput', type: 'rate', threshold: 1000 },
|
||||
{ name: 'error_rate', type: 'percentage', threshold: 5 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -326,29 +326,29 @@ export class PerformanceAnalyzer {
|
||||
const cpuUsage = metrics.cpu_usage || 0;
|
||||
|
||||
let score = 100;
|
||||
let status: "excellent" | "good" | "warning" | "critical" = "excellent";
|
||||
let status: 'excellent' | 'good' | 'warning' | 'critical' = 'excellent';
|
||||
const issues: string[] = [];
|
||||
const recommendations: string[] = [];
|
||||
|
||||
if (cpuUsage > 95) {
|
||||
score = 20;
|
||||
status = "critical";
|
||||
issues.push("CPU usage is critically high");
|
||||
status = 'critical';
|
||||
issues.push('CPU usage is critically high');
|
||||
recommendations.push(
|
||||
"Scale horizontally or optimize CPU-intensive operations",
|
||||
'Scale horizontally or optimize CPU-intensive operations',
|
||||
);
|
||||
} else if (cpuUsage > 85) {
|
||||
score = 50;
|
||||
status = "warning";
|
||||
issues.push("CPU usage is high");
|
||||
recommendations.push("Monitor CPU usage and consider optimization");
|
||||
status = 'warning';
|
||||
issues.push('CPU usage is high');
|
||||
recommendations.push('Monitor CPU usage and consider optimization');
|
||||
} else if (cpuUsage > 70) {
|
||||
score = 75;
|
||||
status = "good";
|
||||
status = 'good';
|
||||
}
|
||||
|
||||
return {
|
||||
component: "cpu",
|
||||
component: 'cpu',
|
||||
score,
|
||||
status,
|
||||
metrics: { usage: cpuUsage },
|
||||
@@ -366,27 +366,27 @@ export class PerformanceAnalyzer {
|
||||
const memoryUsage = metrics.memory_usage || 0;
|
||||
|
||||
let score = 100;
|
||||
let status: "excellent" | "good" | "warning" | "critical" = "excellent";
|
||||
let status: 'excellent' | 'good' | 'warning' | 'critical' = 'excellent';
|
||||
const issues: string[] = [];
|
||||
const recommendations: string[] = [];
|
||||
|
||||
if (memoryUsage > 95) {
|
||||
score = 20;
|
||||
status = "critical";
|
||||
issues.push("Memory usage is critically high");
|
||||
recommendations.push("Increase memory or optimize memory usage");
|
||||
status = 'critical';
|
||||
issues.push('Memory usage is critically high');
|
||||
recommendations.push('Increase memory or optimize memory usage');
|
||||
} else if (memoryUsage > 90) {
|
||||
score = 50;
|
||||
status = "warning";
|
||||
issues.push("Memory usage is high");
|
||||
recommendations.push("Monitor memory usage and check for memory leaks");
|
||||
status = 'warning';
|
||||
issues.push('Memory usage is high');
|
||||
recommendations.push('Monitor memory usage and check for memory leaks');
|
||||
} else if (memoryUsage > 80) {
|
||||
score = 75;
|
||||
status = "good";
|
||||
status = 'good';
|
||||
}
|
||||
|
||||
return {
|
||||
component: "memory",
|
||||
component: 'memory',
|
||||
score,
|
||||
status,
|
||||
metrics: { usage: memoryUsage },
|
||||
@@ -404,22 +404,22 @@ export class PerformanceAnalyzer {
|
||||
const diskIo = metrics.disk_io || 0;
|
||||
|
||||
let score = 100;
|
||||
let status: "excellent" | "good" | "warning" | "critical" = "excellent";
|
||||
let status: 'excellent' | 'good' | 'warning' | 'critical' = 'excellent';
|
||||
const issues: string[] = [];
|
||||
const recommendations: string[] = [];
|
||||
|
||||
if (diskIo > 2000) {
|
||||
score = 50;
|
||||
status = "warning";
|
||||
issues.push("Disk I/O is high");
|
||||
recommendations.push("Optimize disk operations or use faster storage");
|
||||
status = 'warning';
|
||||
issues.push('Disk I/O is high');
|
||||
recommendations.push('Optimize disk operations or use faster storage');
|
||||
} else if (diskIo > 1500) {
|
||||
score = 75;
|
||||
status = "good";
|
||||
status = 'good';
|
||||
}
|
||||
|
||||
return {
|
||||
component: "io",
|
||||
component: 'io',
|
||||
score,
|
||||
status,
|
||||
metrics: { disk_io: diskIo },
|
||||
@@ -437,22 +437,22 @@ export class PerformanceAnalyzer {
|
||||
const networkIo = metrics.network_io || 0;
|
||||
|
||||
let score = 100;
|
||||
let status: "excellent" | "good" | "warning" | "critical" = "excellent";
|
||||
let status: 'excellent' | 'good' | 'warning' | 'critical' = 'excellent';
|
||||
const issues: string[] = [];
|
||||
const recommendations: string[] = [];
|
||||
|
||||
if (networkIo > 150) {
|
||||
score = 50;
|
||||
status = "warning";
|
||||
issues.push("Network I/O is high");
|
||||
recommendations.push("Optimize network operations or increase bandwidth");
|
||||
status = 'warning';
|
||||
issues.push('Network I/O is high');
|
||||
recommendations.push('Optimize network operations or increase bandwidth');
|
||||
} else if (networkIo > 100) {
|
||||
score = 75;
|
||||
status = "good";
|
||||
status = 'good';
|
||||
}
|
||||
|
||||
return {
|
||||
component: "network",
|
||||
component: 'network',
|
||||
score,
|
||||
status,
|
||||
metrics: { network_io: networkIo },
|
||||
@@ -471,34 +471,34 @@ export class PerformanceAnalyzer {
|
||||
const activeConnections = metrics.active_connections || 0;
|
||||
|
||||
let score = 100;
|
||||
let status: "excellent" | "good" | "warning" | "critical" = "excellent";
|
||||
let status: 'excellent' | 'good' | 'warning' | 'critical' = 'excellent';
|
||||
const issues: string[] = [];
|
||||
const recommendations: string[] = [];
|
||||
|
||||
if (responseTime > 1000) {
|
||||
score = 20;
|
||||
status = "critical";
|
||||
issues.push("Database response time is critically slow");
|
||||
recommendations.push("Optimize queries and add indexes");
|
||||
status = 'critical';
|
||||
issues.push('Database response time is critically slow');
|
||||
recommendations.push('Optimize queries and add indexes');
|
||||
} else if (responseTime > 500) {
|
||||
score = 50;
|
||||
status = "warning";
|
||||
issues.push("Database response time is slow");
|
||||
recommendations.push("Review and optimize slow queries");
|
||||
status = 'warning';
|
||||
issues.push('Database response time is slow');
|
||||
recommendations.push('Review and optimize slow queries');
|
||||
} else if (responseTime > 200) {
|
||||
score = 75;
|
||||
status = "good";
|
||||
status = 'good';
|
||||
}
|
||||
|
||||
if (activeConnections > 800) {
|
||||
score = Math.min(score, 50);
|
||||
status = status === "excellent" ? "warning" : status;
|
||||
issues.push("High number of active database connections");
|
||||
recommendations.push("Implement connection pooling");
|
||||
status = status === 'excellent' ? 'warning' : status;
|
||||
issues.push('High number of active database connections');
|
||||
recommendations.push('Implement connection pooling');
|
||||
}
|
||||
|
||||
return {
|
||||
component: "database",
|
||||
component: 'database',
|
||||
score,
|
||||
status,
|
||||
metrics: {
|
||||
@@ -520,31 +520,31 @@ export class PerformanceAnalyzer {
|
||||
const queueLength = metrics.queue_length || 0;
|
||||
|
||||
let score = 100;
|
||||
let status: "excellent" | "good" | "warning" | "critical" = "excellent";
|
||||
let status: 'excellent' | 'good' | 'warning' | 'critical' = 'excellent';
|
||||
const issues: string[] = [];
|
||||
const recommendations: string[] = [];
|
||||
|
||||
if (throughput < 500) {
|
||||
score = 50;
|
||||
status = "warning";
|
||||
issues.push("Application throughput is low");
|
||||
recommendations.push("Optimize application code and algorithms");
|
||||
status = 'warning';
|
||||
issues.push('Application throughput is low');
|
||||
recommendations.push('Optimize application code and algorithms');
|
||||
} else if (throughput < 1000) {
|
||||
score = 75;
|
||||
status = "good";
|
||||
status = 'good';
|
||||
}
|
||||
|
||||
if (queueLength > 50) {
|
||||
score = Math.min(score, 50);
|
||||
status = status === "excellent" ? "warning" : status;
|
||||
issues.push("High queue length indicates processing bottleneck");
|
||||
status = status === 'excellent' ? 'warning' : status;
|
||||
issues.push('High queue length indicates processing bottleneck');
|
||||
recommendations.push(
|
||||
"Scale processing capacity or optimize queue handling",
|
||||
'Scale processing capacity or optimize queue handling',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
component: "application",
|
||||
component: 'application',
|
||||
score,
|
||||
status,
|
||||
metrics: { throughput, queue_length: queueLength },
|
||||
@@ -562,12 +562,12 @@ export class PerformanceAnalyzer {
|
||||
const bottlenecks: PerformanceBottleneck[] = [];
|
||||
|
||||
analyses.forEach((analysis) => {
|
||||
if (analysis.status === "critical" || analysis.status === "warning") {
|
||||
if (analysis.status === 'critical' || analysis.status === 'warning') {
|
||||
bottlenecks.push({
|
||||
component: analysis.component,
|
||||
severity: analysis.status === "critical" ? "critical" : "high",
|
||||
severity: analysis.status === 'critical' ? 'critical' : 'high',
|
||||
description: `${analysis.component} performance is ${analysis.status}`,
|
||||
impact: analysis.score < 30 ? "high" : "medium",
|
||||
impact: analysis.score < 30 ? 'high' : 'medium',
|
||||
recommendations: analysis.recommendations,
|
||||
});
|
||||
}
|
||||
@@ -588,10 +588,10 @@ export class PerformanceAnalyzer {
|
||||
bottleneck.recommendations.forEach((rec) => {
|
||||
recommendations.push({
|
||||
category: bottleneck.component,
|
||||
priority: bottleneck.severity === "critical" ? "high" : "medium",
|
||||
priority: bottleneck.severity === 'critical' ? 'high' : 'medium',
|
||||
description: rec,
|
||||
estimatedImpact: bottleneck.impact,
|
||||
effort: "medium", // 简化处理
|
||||
effort: 'medium', // 简化处理
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -617,11 +617,11 @@ export class PerformanceAnalyzer {
|
||||
*/
|
||||
private determinePerformanceStatus(
|
||||
score: number,
|
||||
): "excellent" | "good" | "warning" | "critical" {
|
||||
if (score >= 90) return "excellent";
|
||||
if (score >= 75) return "good";
|
||||
if (score >= 50) return "warning";
|
||||
return "critical";
|
||||
): 'excellent' | 'good' | 'warning' | 'critical' {
|
||||
if (score >= 90) return 'excellent';
|
||||
if (score >= 75) return 'good';
|
||||
if (score >= 50) return 'warning';
|
||||
return 'critical';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -629,7 +629,7 @@ export class PerformanceAnalyzer {
|
||||
*/
|
||||
private async analyzeTrends(): Promise<PerformanceTrend[]> {
|
||||
// 简化的趋势分析
|
||||
return this.getPerformanceTrends("day");
|
||||
return this.getPerformanceTrends('day');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -642,7 +642,7 @@ export class PerformanceAnalyzer {
|
||||
/**
|
||||
* 获取时间段毫秒数
|
||||
*/
|
||||
private getPeriodInMs(period: "hour" | "day" | "week" | "month"): number {
|
||||
private getPeriodInMs(period: 'hour' | 'day' | 'week' | 'month'): number {
|
||||
const periods = {
|
||||
hour: 60 * 60 * 1000,
|
||||
day: 24 * 60 * 60 * 1000,
|
||||
@@ -657,7 +657,7 @@ export class PerformanceAnalyzer {
|
||||
*/
|
||||
private groupAnalysesByTime(
|
||||
analyses: PerformanceAnalysis[],
|
||||
period: "hour" | "day" | "week" | "month",
|
||||
period: 'hour' | 'day' | 'week' | 'month',
|
||||
): GroupedAnalysis[] {
|
||||
const groups: Record<string, PerformanceAnalysis[]> = {};
|
||||
|
||||
@@ -666,18 +666,18 @@ export class PerformanceAnalyzer {
|
||||
let key: string;
|
||||
|
||||
switch (period) {
|
||||
case "hour":
|
||||
case 'hour':
|
||||
key = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}-${date.getHours()}`;
|
||||
break;
|
||||
case "day":
|
||||
case 'day':
|
||||
key = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
||||
break;
|
||||
case "week":
|
||||
case 'week':
|
||||
const weekStart = new Date(date);
|
||||
weekStart.setDate(date.getDate() - date.getDay());
|
||||
key = `${weekStart.getFullYear()}-${weekStart.getMonth()}-${weekStart.getDate()}`;
|
||||
break;
|
||||
case "month":
|
||||
case 'month':
|
||||
key = `${date.getFullYear()}-${date.getMonth()}`;
|
||||
break;
|
||||
}
|
||||
@@ -713,7 +713,7 @@ export class PerformanceAnalyzer {
|
||||
* 获取嵌套值
|
||||
*/
|
||||
private getNestedValue(obj: any, path: string): number | undefined {
|
||||
return path.split(".").reduce((current, key) => current?.[key], obj);
|
||||
return path.split('.').reduce((current, key) => current?.[key], obj);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -722,8 +722,8 @@ export class PerformanceAnalyzer {
|
||||
private calculateTrendDirection(
|
||||
groupedData: GroupedAnalysis[],
|
||||
path: string,
|
||||
): "increasing" | "decreasing" | "stable" {
|
||||
if (groupedData.length < 2) return "stable";
|
||||
): 'increasing' | 'decreasing' | 'stable' {
|
||||
if (groupedData.length < 2) return 'stable';
|
||||
|
||||
const values = groupedData.map((group) =>
|
||||
this.calculateAverageScore(group.analyses, path),
|
||||
@@ -732,15 +732,15 @@ export class PerformanceAnalyzer {
|
||||
const last = values[values.length - 1];
|
||||
const diff = last - first;
|
||||
|
||||
if (Math.abs(diff) < 5) return "stable";
|
||||
return diff > 0 ? "increasing" : "decreasing";
|
||||
if (Math.abs(diff) < 5) return 'stable';
|
||||
return diff > 0 ? 'increasing' : 'decreasing';
|
||||
}
|
||||
}
|
||||
|
||||
// 类型定义
|
||||
export interface PerformanceMetric {
|
||||
name: string;
|
||||
type: "percentage" | "rate" | "duration" | "count";
|
||||
type: 'percentage' | 'rate' | 'duration' | 'count';
|
||||
threshold: number;
|
||||
}
|
||||
|
||||
@@ -754,7 +754,7 @@ export interface PerformanceAnalysis {
|
||||
timestamp: number;
|
||||
duration: number;
|
||||
overallScore: number;
|
||||
status: "excellent" | "good" | "warning" | "critical";
|
||||
status: 'excellent' | 'good' | 'warning' | 'critical';
|
||||
metrics: Record<string, number>;
|
||||
summary: {
|
||||
averageResponseTime: number;
|
||||
@@ -777,7 +777,7 @@ export interface PerformanceAnalysis {
|
||||
export interface ComponentAnalysis {
|
||||
component: string;
|
||||
score: number;
|
||||
status: "excellent" | "good" | "warning" | "critical";
|
||||
status: 'excellent' | 'good' | 'warning' | 'critical';
|
||||
metrics: Record<string, number>;
|
||||
issues: string[];
|
||||
recommendations: string[];
|
||||
@@ -785,25 +785,25 @@ export interface ComponentAnalysis {
|
||||
|
||||
export interface PerformanceBottleneck {
|
||||
component: string;
|
||||
severity: "low" | "medium" | "high" | "critical";
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
description: string;
|
||||
impact: "low" | "medium" | "high";
|
||||
impact: 'low' | 'medium' | 'high';
|
||||
recommendations: string[];
|
||||
}
|
||||
|
||||
export interface OptimizationRecommendation {
|
||||
category: string;
|
||||
priority: "low" | "medium" | "high";
|
||||
priority: 'low' | 'medium' | 'high';
|
||||
description: string;
|
||||
estimatedImpact: "low" | "medium" | "high";
|
||||
effort: "low" | "medium" | "high";
|
||||
estimatedImpact: 'low' | 'medium' | 'high';
|
||||
effort: 'low' | 'medium' | 'high';
|
||||
}
|
||||
|
||||
export interface PerformanceTrend {
|
||||
metric: string;
|
||||
name: string;
|
||||
values: { timestamp: number; value: number }[];
|
||||
trend: "increasing" | "decreasing" | "stable";
|
||||
trend: 'increasing' | 'decreasing' | 'stable';
|
||||
}
|
||||
|
||||
export interface PerformanceBenchmark {
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
Logger,
|
||||
OnModuleInit,
|
||||
OnModuleDestroy,
|
||||
} from "@nestjs/common";
|
||||
import { EventBus } from "@wwjCommon/events/event-bus";
|
||||
} from '@nestjs/common';
|
||||
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||
|
||||
/**
|
||||
* Resource Monitor - 资源监控器
|
||||
@@ -29,12 +29,12 @@ export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
this.logger.log("Initializing Resource Monitor");
|
||||
this.logger.log('Initializing Resource Monitor');
|
||||
await this.startMonitoring();
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
this.logger.log("Destroying Resource Monitor");
|
||||
this.logger.log('Destroying Resource Monitor');
|
||||
await this.stopMonitoring();
|
||||
}
|
||||
|
||||
@@ -43,11 +43,11 @@ export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||
*/
|
||||
async startMonitoring(): Promise<void> {
|
||||
if (this.monitoringInterval) {
|
||||
this.logger.warn("Monitoring is already running");
|
||||
this.logger.warn('Monitoring is already running');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log("Starting resource monitoring");
|
||||
this.logger.log('Starting resource monitoring');
|
||||
|
||||
this.monitoringInterval = setInterval(
|
||||
() => this.collectResourceSnapshot(),
|
||||
@@ -65,7 +65,7 @@ export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||
if (this.monitoringInterval) {
|
||||
clearInterval(this.monitoringInterval);
|
||||
this.monitoringInterval = null;
|
||||
this.logger.log("Resource monitoring stopped");
|
||||
this.logger.log('Resource monitoring stopped');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||
* 获取当前资源状态
|
||||
*/
|
||||
async getCurrentResourceStatus(): Promise<ResourceStatus> {
|
||||
this.logger.debug("Getting current resource status");
|
||||
this.logger.debug('Getting current resource status');
|
||||
|
||||
const snapshot = await this.captureResourceSnapshot();
|
||||
const alerts = this.checkAlerts(snapshot);
|
||||
@@ -100,7 +100,7 @@ export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||
* 获取资源历史数据
|
||||
*/
|
||||
getResourceHistory(
|
||||
period: "hour" | "day" | "week" = "hour",
|
||||
period: 'hour' | 'day' | 'week' = 'hour',
|
||||
): ResourceSnapshot[] {
|
||||
const now = Date.now();
|
||||
const periodMs = this.getPeriodInMs(period);
|
||||
@@ -115,7 +115,7 @@ export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||
* 设置告警阈值
|
||||
*/
|
||||
setAlertThresholds(thresholds: Partial<ResourceThresholds>): void {
|
||||
this.logger.log("Updating alert thresholds", thresholds);
|
||||
this.logger.log('Updating alert thresholds', thresholds);
|
||||
Object.assign(this.alertThresholds, thresholds);
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||
* 获取资源使用统计
|
||||
*/
|
||||
getResourceStatistics(
|
||||
period: "hour" | "day" | "week" = "day",
|
||||
period: 'hour' | 'day' | 'week' = 'day',
|
||||
): ResourceStatistics {
|
||||
const history = this.getResourceHistory(period);
|
||||
|
||||
@@ -173,7 +173,7 @@ export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||
predictResourceTrends(hours: number = 24): ResourcePrediction[] {
|
||||
this.logger.debug(`Predicting resource trends for next ${hours} hours`);
|
||||
|
||||
const history = this.getResourceHistory("day");
|
||||
const history = this.getResourceHistory('day');
|
||||
|
||||
if (history.length < 10) {
|
||||
return []; // 数据不足,无法预测
|
||||
@@ -184,8 +184,8 @@ export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||
// 预测CPU使用率
|
||||
const cpuTrend = this.calculateLinearTrend(history.map((h) => h.cpu.usage));
|
||||
predictions.push({
|
||||
resource: "cpu",
|
||||
metric: "usage",
|
||||
resource: 'cpu',
|
||||
metric: 'usage',
|
||||
currentValue: history[history.length - 1].cpu.usage,
|
||||
predictedValue: Math.max(0, Math.min(100, cpuTrend.predict(hours))),
|
||||
confidence: cpuTrend.confidence,
|
||||
@@ -197,8 +197,8 @@ export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||
history.map((h) => h.memory.usage),
|
||||
);
|
||||
predictions.push({
|
||||
resource: "memory",
|
||||
metric: "usage",
|
||||
resource: 'memory',
|
||||
metric: 'usage',
|
||||
currentValue: history[history.length - 1].memory.usage,
|
||||
predictedValue: Math.max(0, Math.min(100, memoryTrend.predict(hours))),
|
||||
confidence: memoryTrend.confidence,
|
||||
@@ -210,8 +210,8 @@ export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||
history.map((h) => h.disk.usage),
|
||||
);
|
||||
predictions.push({
|
||||
resource: "disk",
|
||||
metric: "usage",
|
||||
resource: 'disk',
|
||||
metric: 'usage',
|
||||
currentValue: history[history.length - 1].disk.usage,
|
||||
predictedValue: Math.max(0, Math.min(100, diskTrend.predict(hours))),
|
||||
confidence: diskTrend.confidence,
|
||||
@@ -243,9 +243,9 @@ export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
|
||||
// 发送监控事件
|
||||
this.eventBus.emit("resource.snapshot", snapshot);
|
||||
this.eventBus.emit('resource.snapshot', snapshot);
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to collect resource snapshot", error);
|
||||
this.logger.error('Failed to collect resource snapshot', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,8 +305,8 @@ export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||
// CPU告警
|
||||
if (snapshot.cpu.usage > this.alertThresholds.cpu.critical) {
|
||||
alerts.push({
|
||||
type: "cpu",
|
||||
severity: "critical",
|
||||
type: 'cpu',
|
||||
severity: 'critical',
|
||||
message: `CPU usage is critically high: ${snapshot.cpu.usage.toFixed(1)}%`,
|
||||
value: snapshot.cpu.usage,
|
||||
threshold: this.alertThresholds.cpu.critical,
|
||||
@@ -314,8 +314,8 @@ export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||
});
|
||||
} else if (snapshot.cpu.usage > this.alertThresholds.cpu.warning) {
|
||||
alerts.push({
|
||||
type: "cpu",
|
||||
severity: "warning",
|
||||
type: 'cpu',
|
||||
severity: 'warning',
|
||||
message: `CPU usage is high: ${snapshot.cpu.usage.toFixed(1)}%`,
|
||||
value: snapshot.cpu.usage,
|
||||
threshold: this.alertThresholds.cpu.warning,
|
||||
@@ -326,8 +326,8 @@ export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||
// 内存告警
|
||||
if (snapshot.memory.usage > this.alertThresholds.memory.critical) {
|
||||
alerts.push({
|
||||
type: "memory",
|
||||
severity: "critical",
|
||||
type: 'memory',
|
||||
severity: 'critical',
|
||||
message: `Memory usage is critically high: ${snapshot.memory.usage.toFixed(1)}%`,
|
||||
value: snapshot.memory.usage,
|
||||
threshold: this.alertThresholds.memory.critical,
|
||||
@@ -335,8 +335,8 @@ export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||
});
|
||||
} else if (snapshot.memory.usage > this.alertThresholds.memory.warning) {
|
||||
alerts.push({
|
||||
type: "memory",
|
||||
severity: "warning",
|
||||
type: 'memory',
|
||||
severity: 'warning',
|
||||
message: `Memory usage is high: ${snapshot.memory.usage.toFixed(1)}%`,
|
||||
value: snapshot.memory.usage,
|
||||
threshold: this.alertThresholds.memory.warning,
|
||||
@@ -347,8 +347,8 @@ export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||
// 磁盘告警
|
||||
if (snapshot.disk.usage > this.alertThresholds.disk.critical) {
|
||||
alerts.push({
|
||||
type: "disk",
|
||||
severity: "critical",
|
||||
type: 'disk',
|
||||
severity: 'critical',
|
||||
message: `Disk usage is critically high: ${snapshot.disk.usage.toFixed(1)}%`,
|
||||
value: snapshot.disk.usage,
|
||||
threshold: this.alertThresholds.disk.critical,
|
||||
@@ -356,8 +356,8 @@ export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||
});
|
||||
} else if (snapshot.disk.usage > this.alertThresholds.disk.warning) {
|
||||
alerts.push({
|
||||
type: "disk",
|
||||
severity: "warning",
|
||||
type: 'disk',
|
||||
severity: 'warning',
|
||||
message: `Disk usage is high: ${snapshot.disk.usage.toFixed(1)}%`,
|
||||
value: snapshot.disk.usage,
|
||||
threshold: this.alertThresholds.disk.warning,
|
||||
@@ -379,7 +379,7 @@ export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||
this.logger.warn(`Resource Alert: ${alert.message}`);
|
||||
|
||||
// 发送告警事件
|
||||
this.eventBus.emit("resource.alert", {
|
||||
this.eventBus.emit('resource.alert', {
|
||||
alert,
|
||||
snapshot,
|
||||
});
|
||||
@@ -394,21 +394,21 @@ export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||
alerts: ResourceAlert[],
|
||||
): ResourceHealth {
|
||||
const criticalAlerts = alerts.filter(
|
||||
(a) => a.severity === "critical",
|
||||
(a) => a.severity === 'critical',
|
||||
).length;
|
||||
const warningAlerts = alerts.filter((a) => a.severity === "warning").length;
|
||||
const warningAlerts = alerts.filter((a) => a.severity === 'warning').length;
|
||||
|
||||
let status: "healthy" | "warning" | "critical";
|
||||
let status: 'healthy' | 'warning' | 'critical';
|
||||
let score = 100;
|
||||
|
||||
if (criticalAlerts > 0) {
|
||||
status = "critical";
|
||||
status = 'critical';
|
||||
score = Math.max(0, 100 - criticalAlerts * 30 - warningAlerts * 10);
|
||||
} else if (warningAlerts > 0) {
|
||||
status = "warning";
|
||||
status = 'warning';
|
||||
score = Math.max(50, 100 - warningAlerts * 15);
|
||||
} else {
|
||||
status = "healthy";
|
||||
status = 'healthy';
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -428,10 +428,10 @@ export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
if (recentHistory.length < 2) {
|
||||
return {
|
||||
cpu: "stable",
|
||||
memory: "stable",
|
||||
disk: "stable",
|
||||
network: "stable",
|
||||
cpu: 'stable',
|
||||
memory: 'stable',
|
||||
disk: 'stable',
|
||||
network: 'stable',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -461,15 +461,15 @@ export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||
*/
|
||||
private calculateTrendDirection(
|
||||
values: number[],
|
||||
): "increasing" | "decreasing" | "stable" {
|
||||
if (values.length < 2) return "stable";
|
||||
): 'increasing' | 'decreasing' | 'stable' {
|
||||
if (values.length < 2) return 'stable';
|
||||
|
||||
const first = values[0];
|
||||
const last = values[values.length - 1];
|
||||
const diff = ((last - first) / first) * 100;
|
||||
|
||||
if (Math.abs(diff) < 5) return "stable";
|
||||
return diff > 0 ? "increasing" : "decreasing";
|
||||
if (Math.abs(diff) < 5) return 'stable';
|
||||
return diff > 0 ? 'increasing' : 'decreasing';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -480,7 +480,7 @@ export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||
return {
|
||||
predict: () => values[0] || 0,
|
||||
confidence: 0,
|
||||
direction: "stable",
|
||||
direction: 'stable',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -511,10 +511,10 @@ export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||
confidence: Math.max(0, Math.min(1, rSquared)),
|
||||
direction:
|
||||
Math.abs(slope) < 0.1
|
||||
? "stable"
|
||||
? 'stable'
|
||||
: slope > 0
|
||||
? "increasing"
|
||||
: "decreasing",
|
||||
? 'increasing'
|
||||
: 'decreasing',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -530,7 +530,7 @@ export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||
/**
|
||||
* 获取时间段毫秒数
|
||||
*/
|
||||
private getPeriodInMs(period: "hour" | "day" | "week"): number {
|
||||
private getPeriodInMs(period: 'hour' | 'day' | 'week'): number {
|
||||
const periods = {
|
||||
hour: 60 * 60 * 1000,
|
||||
day: 24 * 60 * 60 * 1000,
|
||||
@@ -567,7 +567,7 @@ export class ResourceMonitor implements OnModuleInit, OnModuleDestroy {
|
||||
*/
|
||||
private getEmptyStatistics(): ResourceStatistics {
|
||||
return {
|
||||
period: "day",
|
||||
period: 'day',
|
||||
sampleCount: 0,
|
||||
cpu: { min: 0, max: 0, avg: 0, current: 0 },
|
||||
memory: { min: 0, max: 0, avg: 0, current: 0 },
|
||||
@@ -634,8 +634,8 @@ export interface MonitoringConfig {
|
||||
}
|
||||
|
||||
export interface ResourceAlert {
|
||||
type: "cpu" | "memory" | "disk" | "network";
|
||||
severity: "warning" | "critical";
|
||||
type: 'cpu' | 'memory' | 'disk' | 'network';
|
||||
severity: 'warning' | 'critical';
|
||||
message: string;
|
||||
value: number;
|
||||
threshold: number;
|
||||
@@ -651,14 +651,14 @@ export interface ResourceStatus {
|
||||
}
|
||||
|
||||
export interface ResourceTrends {
|
||||
cpu: "increasing" | "decreasing" | "stable";
|
||||
memory: "increasing" | "decreasing" | "stable";
|
||||
disk: "increasing" | "decreasing" | "stable";
|
||||
network: "increasing" | "decreasing" | "stable";
|
||||
cpu: 'increasing' | 'decreasing' | 'stable';
|
||||
memory: 'increasing' | 'decreasing' | 'stable';
|
||||
disk: 'increasing' | 'decreasing' | 'stable';
|
||||
network: 'increasing' | 'decreasing' | 'stable';
|
||||
}
|
||||
|
||||
export interface ResourceHealth {
|
||||
status: "healthy" | "warning" | "critical";
|
||||
status: 'healthy' | 'warning' | 'critical';
|
||||
score: number; // 0-100
|
||||
criticalAlerts: number;
|
||||
warningAlerts: number;
|
||||
@@ -666,7 +666,7 @@ export interface ResourceHealth {
|
||||
}
|
||||
|
||||
export interface ResourceStatistics {
|
||||
period: "hour" | "day" | "week";
|
||||
period: 'hour' | 'day' | 'week';
|
||||
sampleCount: number;
|
||||
cpu: { min: number; max: number; avg: number; current: number };
|
||||
memory: { min: number; max: number; avg: number; current: number };
|
||||
@@ -680,16 +680,16 @@ export interface ResourceStatistics {
|
||||
}
|
||||
|
||||
export interface ResourcePrediction {
|
||||
resource: "cpu" | "memory" | "disk" | "network";
|
||||
resource: 'cpu' | 'memory' | 'disk' | 'network';
|
||||
metric: string;
|
||||
currentValue: number;
|
||||
predictedValue: number;
|
||||
confidence: number; // 0-1
|
||||
trend: "increasing" | "decreasing" | "stable";
|
||||
trend: 'increasing' | 'decreasing' | 'stable';
|
||||
}
|
||||
|
||||
interface TrendAnalysis {
|
||||
predict: (hours: number) => number;
|
||||
confidence: number;
|
||||
direction: "increasing" | "decreasing" | "stable";
|
||||
direction: 'increasing' | 'decreasing' | 'stable';
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { CacheManagerService } from "@wwjBoot";
|
||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { CacheManagerService } from '@wwjBoot';
|
||||
|
||||
/**
|
||||
* Cache Optimizer - 缓存优化器
|
||||
@@ -25,7 +25,7 @@ export class CacheOptimizer {
|
||||
*/
|
||||
async analyzeCachePerformance(cacheKey?: string): Promise<CacheAnalysis> {
|
||||
this.logger.log(
|
||||
`Analyzing cache performance${cacheKey ? ` for ${cacheKey}` : ""}`,
|
||||
`Analyzing cache performance${cacheKey ? ` for ${cacheKey}` : ''}`,
|
||||
);
|
||||
|
||||
const startTime = Date.now();
|
||||
@@ -72,7 +72,7 @@ export class CacheOptimizer {
|
||||
async optimizeCacheConfiguration(
|
||||
options: CacheOptimizationOptions = {},
|
||||
): Promise<CacheOptimization> {
|
||||
this.logger.log("Starting cache optimization");
|
||||
this.logger.log('Starting cache optimization');
|
||||
|
||||
const analysis = await this.analyzeCachePerformance();
|
||||
const optimizations: CacheConfigOptimization[] = [];
|
||||
@@ -126,7 +126,7 @@ export class CacheOptimizer {
|
||||
(o) => o.id === optimizationId,
|
||||
);
|
||||
if (!optimization) {
|
||||
throw new Error(`Optimization not found: ${optimizationId}`);
|
||||
throw new NotFoundException(`Optimization not found: ${optimizationId}`);
|
||||
}
|
||||
|
||||
const results: ComponentOptimizationResult[] = [];
|
||||
@@ -155,7 +155,7 @@ export class CacheOptimizer {
|
||||
results.push({
|
||||
cacheKey: componentOpt.cacheKey,
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
appliedChanges: [],
|
||||
});
|
||||
failureCount++;
|
||||
@@ -317,33 +317,33 @@ export class CacheOptimizer {
|
||||
totalRequests > 0 ? (metrics.hits / totalRequests) * 100 : 0;
|
||||
|
||||
let score = 100;
|
||||
let status: "excellent" | "good" | "warning" | "critical" = "excellent";
|
||||
let status: 'excellent' | 'good' | 'warning' | 'critical' = 'excellent';
|
||||
const issues: string[] = [];
|
||||
const recommendations: string[] = [];
|
||||
|
||||
// 评估命中率
|
||||
if (hitRate < 50) {
|
||||
score = Math.min(score, 30);
|
||||
status = "critical";
|
||||
issues.push("Very low cache hit rate");
|
||||
recommendations.push("Review cache key strategy and TTL settings");
|
||||
status = 'critical';
|
||||
issues.push('Very low cache hit rate');
|
||||
recommendations.push('Review cache key strategy and TTL settings');
|
||||
} else if (hitRate < 70) {
|
||||
score = Math.min(score, 60);
|
||||
status = status === "excellent" ? "warning" : status;
|
||||
issues.push("Low cache hit rate");
|
||||
recommendations.push("Optimize cache key patterns");
|
||||
status = status === 'excellent' ? 'warning' : status;
|
||||
issues.push('Low cache hit rate');
|
||||
recommendations.push('Optimize cache key patterns');
|
||||
} else if (hitRate < 85) {
|
||||
score = Math.min(score, 80);
|
||||
status = status === "excellent" ? "good" : status;
|
||||
status = status === 'excellent' ? 'good' : status;
|
||||
}
|
||||
|
||||
// 评估内存使用
|
||||
if (metrics.memoryUsage > 1024 * 1024 * 1024) {
|
||||
// 1GB
|
||||
score = Math.min(score, 70);
|
||||
status = status === "excellent" ? "warning" : status;
|
||||
issues.push("High memory usage");
|
||||
recommendations.push("Consider implementing cache size limits");
|
||||
status = status === 'excellent' ? 'warning' : status;
|
||||
issues.push('High memory usage');
|
||||
recommendations.push('Consider implementing cache size limits');
|
||||
}
|
||||
|
||||
// 评估驱逐率
|
||||
@@ -351,9 +351,9 @@ export class CacheOptimizer {
|
||||
totalRequests > 0 ? (metrics.evictions / totalRequests) * 100 : 0;
|
||||
if (evictionRate > 20) {
|
||||
score = Math.min(score, 50);
|
||||
status = status === "excellent" || status === "good" ? "warning" : status;
|
||||
issues.push("High eviction rate");
|
||||
recommendations.push("Increase cache size or optimize TTL");
|
||||
status = status === 'excellent' || status === 'good' ? 'warning' : status;
|
||||
issues.push('High eviction rate');
|
||||
recommendations.push('Increase cache size or optimize TTL');
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -392,11 +392,11 @@ export class CacheOptimizer {
|
||||
*/
|
||||
private determineCacheStatus(
|
||||
score: number,
|
||||
): "excellent" | "good" | "warning" | "critical" {
|
||||
if (score >= 90) return "excellent";
|
||||
if (score >= 75) return "good";
|
||||
if (score >= 50) return "warning";
|
||||
return "critical";
|
||||
): 'excellent' | 'good' | 'warning' | 'critical' {
|
||||
if (score >= 90) return 'excellent';
|
||||
if (score >= 75) return 'good';
|
||||
if (score >= 50) return 'warning';
|
||||
return 'critical';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -412,7 +412,7 @@ export class CacheOptimizer {
|
||||
issues.push({
|
||||
cacheKey: analysis.cacheKey,
|
||||
type: this.categorizeIssue(issue),
|
||||
severity: analysis.status === "critical" ? "high" : "medium",
|
||||
severity: analysis.status === 'critical' ? 'high' : 'medium',
|
||||
description: issue,
|
||||
impact: this.assessIssueImpact(analysis.score),
|
||||
});
|
||||
@@ -437,7 +437,7 @@ export class CacheOptimizer {
|
||||
recommendations.push({
|
||||
cacheKey: analysis.cacheKey,
|
||||
type: this.categorizeRecommendation(rec),
|
||||
priority: analysis.status === "critical" ? "high" : "medium",
|
||||
priority: analysis.status === 'critical' ? 'high' : 'medium',
|
||||
description: rec,
|
||||
estimatedImpact: this.estimateRecommendationImpact(analysis.score),
|
||||
});
|
||||
@@ -445,15 +445,15 @@ export class CacheOptimizer {
|
||||
});
|
||||
|
||||
// 基于问题生成建议
|
||||
const criticalIssues = issues.filter((i) => i.severity === "high");
|
||||
const criticalIssues = issues.filter((i) => i.severity === 'high');
|
||||
if (criticalIssues.length > 0) {
|
||||
recommendations.push({
|
||||
cacheKey: "global",
|
||||
type: "configuration",
|
||||
priority: "high",
|
||||
cacheKey: 'global',
|
||||
type: 'configuration',
|
||||
priority: 'high',
|
||||
description:
|
||||
"Review and optimize cache configuration for critical issues",
|
||||
estimatedImpact: "high",
|
||||
'Review and optimize cache configuration for critical issues',
|
||||
estimatedImpact: 'high',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -468,12 +468,12 @@ export class CacheOptimizer {
|
||||
): CacheSummary {
|
||||
const totalCaches = analyses.length;
|
||||
const excellentCaches = analyses.filter(
|
||||
(a) => a.status === "excellent",
|
||||
(a) => a.status === 'excellent',
|
||||
).length;
|
||||
const goodCaches = analyses.filter((a) => a.status === "good").length;
|
||||
const warningCaches = analyses.filter((a) => a.status === "warning").length;
|
||||
const goodCaches = analyses.filter((a) => a.status === 'good').length;
|
||||
const warningCaches = analyses.filter((a) => a.status === 'warning').length;
|
||||
const criticalCaches = analyses.filter(
|
||||
(a) => a.status === "critical",
|
||||
(a) => a.status === 'critical',
|
||||
).length;
|
||||
|
||||
const avgHitRate =
|
||||
@@ -510,11 +510,11 @@ export class CacheOptimizer {
|
||||
// 基于命中率优化
|
||||
if (analysis.metrics.hitRate < 70) {
|
||||
optimizations.push({
|
||||
parameter: "ttl",
|
||||
parameter: 'ttl',
|
||||
currentValue: 3600, // 假设当前TTL
|
||||
recommendedValue: 7200, // 增加TTL
|
||||
reason: "Increase TTL to improve hit rate",
|
||||
impact: "medium",
|
||||
reason: 'Increase TTL to improve hit rate',
|
||||
impact: 'medium',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -522,22 +522,22 @@ export class CacheOptimizer {
|
||||
if (analysis.metrics.memoryUsage > 512 * 1024 * 1024) {
|
||||
// 512MB
|
||||
optimizations.push({
|
||||
parameter: "maxSize",
|
||||
parameter: 'maxSize',
|
||||
currentValue: analysis.metrics.memoryUsage,
|
||||
recommendedValue: Math.floor(analysis.metrics.memoryUsage * 0.8),
|
||||
reason: "Reduce memory usage",
|
||||
impact: "high",
|
||||
reason: 'Reduce memory usage',
|
||||
impact: 'high',
|
||||
});
|
||||
}
|
||||
|
||||
// 基于驱逐率优化
|
||||
if (analysis.metrics.evictionRate > 15) {
|
||||
optimizations.push({
|
||||
parameter: "evictionPolicy",
|
||||
currentValue: "LRU",
|
||||
recommendedValue: "LFU",
|
||||
reason: "Change eviction policy to reduce eviction rate",
|
||||
impact: "medium",
|
||||
parameter: 'evictionPolicy',
|
||||
currentValue: 'LRU',
|
||||
recommendedValue: 'LFU',
|
||||
reason: 'Change eviction policy to reduce eviction rate',
|
||||
impact: 'medium',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -561,25 +561,25 @@ export class CacheOptimizer {
|
||||
// 如果整体性能较差,建议全局优化
|
||||
if (analysis.overallScore < 70) {
|
||||
optimizations.push({
|
||||
type: "strategy",
|
||||
description: "Implement distributed caching strategy",
|
||||
impact: "high",
|
||||
effort: "high",
|
||||
timeline: "2-4 weeks",
|
||||
type: 'strategy',
|
||||
description: 'Implement distributed caching strategy',
|
||||
impact: 'high',
|
||||
effort: 'high',
|
||||
timeline: '2-4 weeks',
|
||||
});
|
||||
}
|
||||
|
||||
// 如果有多个缓存性能问题,建议统一配置
|
||||
const criticalCaches = analysis.analyses.filter(
|
||||
(a) => a.status === "critical",
|
||||
(a) => a.status === 'critical',
|
||||
).length;
|
||||
if (criticalCaches > 2) {
|
||||
optimizations.push({
|
||||
type: "configuration",
|
||||
description: "Standardize cache configuration across components",
|
||||
impact: "medium",
|
||||
effort: "medium",
|
||||
timeline: "1-2 weeks",
|
||||
type: 'configuration',
|
||||
description: 'Standardize cache configuration across components',
|
||||
impact: 'medium',
|
||||
effort: 'medium',
|
||||
timeline: '1-2 weeks',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -593,20 +593,20 @@ export class CacheOptimizer {
|
||||
optimizations: CacheConfigOptimization[],
|
||||
): EstimatedImpact {
|
||||
const highImpactCount = optimizations.filter((o) =>
|
||||
o.optimizations.some((opt) => opt.impact === "high"),
|
||||
o.optimizations.some((opt) => opt.impact === 'high'),
|
||||
).length;
|
||||
|
||||
const mediumImpactCount = optimizations.filter((o) =>
|
||||
o.optimizations.some((opt) => opt.impact === "medium"),
|
||||
o.optimizations.some((opt) => opt.impact === 'medium'),
|
||||
).length;
|
||||
|
||||
let overallImpact: "low" | "medium" | "high";
|
||||
let overallImpact: 'low' | 'medium' | 'high';
|
||||
if (highImpactCount > 0) {
|
||||
overallImpact = "high";
|
||||
overallImpact = 'high';
|
||||
} else if (mediumImpactCount > 0) {
|
||||
overallImpact = "medium";
|
||||
overallImpact = 'medium';
|
||||
} else {
|
||||
overallImpact = "low";
|
||||
overallImpact = 'low';
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -628,16 +628,16 @@ export class CacheOptimizer {
|
||||
|
||||
// 高优先级组件优化
|
||||
const highPriorityOptimizations = optimizations.filter((o) =>
|
||||
o.optimizations.some((opt) => opt.impact === "high"),
|
||||
o.optimizations.some((opt) => opt.impact === 'high'),
|
||||
);
|
||||
|
||||
if (highPriorityOptimizations.length > 0) {
|
||||
steps.push({
|
||||
phase: 1,
|
||||
description: "Apply high-impact cache optimizations",
|
||||
duration: "1-2 days",
|
||||
description: 'Apply high-impact cache optimizations',
|
||||
duration: '1-2 days',
|
||||
dependencies: [],
|
||||
risks: ["Temporary performance impact during configuration changes"],
|
||||
risks: ['Temporary performance impact during configuration changes'],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -645,20 +645,20 @@ export class CacheOptimizer {
|
||||
if (globalOptimizations.length > 0) {
|
||||
steps.push({
|
||||
phase: 2,
|
||||
description: "Implement global cache strategy improvements",
|
||||
duration: globalOptimizations[0]?.timeline || "1-2 weeks",
|
||||
dependencies: ["Phase 1 completion"],
|
||||
risks: ["Requires coordination across multiple services"],
|
||||
description: 'Implement global cache strategy improvements',
|
||||
duration: globalOptimizations[0]?.timeline || '1-2 weeks',
|
||||
dependencies: ['Phase 1 completion'],
|
||||
risks: ['Requires coordination across multiple services'],
|
||||
});
|
||||
}
|
||||
|
||||
// 监控和验证
|
||||
steps.push({
|
||||
phase: 3,
|
||||
description: "Monitor and validate optimization results",
|
||||
duration: "1 week",
|
||||
dependencies: ["Previous phases completion"],
|
||||
risks: ["May require rollback if performance degrades"],
|
||||
description: 'Monitor and validate optimization results',
|
||||
duration: '1 week',
|
||||
dependencies: ['Previous phases completion'],
|
||||
risks: ['May require rollback if performance degrades'],
|
||||
});
|
||||
|
||||
return steps;
|
||||
@@ -697,7 +697,7 @@ export class CacheOptimizer {
|
||||
return {
|
||||
cacheKey: optimization.cacheKey,
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
appliedChanges,
|
||||
};
|
||||
}
|
||||
@@ -732,7 +732,7 @@ export class CacheOptimizer {
|
||||
results.push({
|
||||
type: optimization.type,
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : "Unknown error",
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -781,7 +781,7 @@ export class CacheOptimizer {
|
||||
*/
|
||||
private initializeDefaultMetrics(): void {
|
||||
// 初始化一些示例缓存指标
|
||||
this.cacheMetrics.set("user-cache", {
|
||||
this.cacheMetrics.set('user-cache', {
|
||||
hits: 1000,
|
||||
misses: 200,
|
||||
evictions: 50,
|
||||
@@ -791,7 +791,7 @@ export class CacheOptimizer {
|
||||
lastUpdated: Date.now(),
|
||||
});
|
||||
|
||||
this.cacheMetrics.set("session-cache", {
|
||||
this.cacheMetrics.set('session-cache', {
|
||||
hits: 800,
|
||||
misses: 150,
|
||||
evictions: 30,
|
||||
@@ -836,29 +836,29 @@ export class CacheOptimizer {
|
||||
*/
|
||||
private categorizeIssue(
|
||||
issue: string,
|
||||
): "performance" | "memory" | "configuration" {
|
||||
): 'performance' | 'memory' | 'configuration' {
|
||||
if (
|
||||
issue.toLowerCase().includes("hit rate") ||
|
||||
issue.toLowerCase().includes("response")
|
||||
issue.toLowerCase().includes('hit rate') ||
|
||||
issue.toLowerCase().includes('response')
|
||||
) {
|
||||
return "performance";
|
||||
return 'performance';
|
||||
}
|
||||
if (
|
||||
issue.toLowerCase().includes("memory") ||
|
||||
issue.toLowerCase().includes("eviction")
|
||||
issue.toLowerCase().includes('memory') ||
|
||||
issue.toLowerCase().includes('eviction')
|
||||
) {
|
||||
return "memory";
|
||||
return 'memory';
|
||||
}
|
||||
return "configuration";
|
||||
return 'configuration';
|
||||
}
|
||||
|
||||
/**
|
||||
* 评估问题影响
|
||||
*/
|
||||
private assessIssueImpact(score: number): "low" | "medium" | "high" {
|
||||
if (score < 50) return "high";
|
||||
if (score < 75) return "medium";
|
||||
return "low";
|
||||
private assessIssueImpact(score: number): 'low' | 'medium' | 'high' {
|
||||
if (score < 50) return 'high';
|
||||
if (score < 75) return 'medium';
|
||||
return 'low';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -866,20 +866,20 @@ export class CacheOptimizer {
|
||||
*/
|
||||
private categorizeRecommendation(
|
||||
recommendation: string,
|
||||
): "performance" | "memory" | "configuration" {
|
||||
): 'performance' | 'memory' | 'configuration' {
|
||||
if (
|
||||
recommendation.toLowerCase().includes("ttl") ||
|
||||
recommendation.toLowerCase().includes("key")
|
||||
recommendation.toLowerCase().includes('ttl') ||
|
||||
recommendation.toLowerCase().includes('key')
|
||||
) {
|
||||
return "performance";
|
||||
return 'performance';
|
||||
}
|
||||
if (
|
||||
recommendation.toLowerCase().includes("size") ||
|
||||
recommendation.toLowerCase().includes("limit")
|
||||
recommendation.toLowerCase().includes('size') ||
|
||||
recommendation.toLowerCase().includes('limit')
|
||||
) {
|
||||
return "memory";
|
||||
return 'memory';
|
||||
}
|
||||
return "configuration";
|
||||
return 'configuration';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -887,10 +887,10 @@ export class CacheOptimizer {
|
||||
*/
|
||||
private estimateRecommendationImpact(
|
||||
score: number,
|
||||
): "low" | "medium" | "high" {
|
||||
if (score < 50) return "high";
|
||||
if (score < 75) return "medium";
|
||||
return "low";
|
||||
): 'low' | 'medium' | 'high' {
|
||||
if (score < 50) return 'high';
|
||||
if (score < 75) return 'medium';
|
||||
return 'low';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -898,18 +898,18 @@ export class CacheOptimizer {
|
||||
*/
|
||||
private calculateComponentImpact(
|
||||
optimizations: ConfigChange[],
|
||||
): "low" | "medium" | "high" {
|
||||
): 'low' | 'medium' | 'high' {
|
||||
const highImpactCount = optimizations.filter(
|
||||
(o) => o.impact === "high",
|
||||
(o) => o.impact === 'high',
|
||||
).length;
|
||||
if (highImpactCount > 0) return "high";
|
||||
if (highImpactCount > 0) return 'high';
|
||||
|
||||
const mediumImpactCount = optimizations.filter(
|
||||
(o) => o.impact === "medium",
|
||||
(o) => o.impact === 'medium',
|
||||
).length;
|
||||
if (mediumImpactCount > 0) return "medium";
|
||||
if (mediumImpactCount > 0) return 'medium';
|
||||
|
||||
return "low";
|
||||
return 'low';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1009,18 +1009,18 @@ export class CacheOptimizer {
|
||||
predictedRequests: number,
|
||||
predictedMemoryUsage: number,
|
||||
currentHitRate: number,
|
||||
): "maintain" | "scale_up" | "scale_down" | "optimize" {
|
||||
): 'maintain' | 'scale_up' | 'scale_down' | 'optimize' {
|
||||
if (predictedMemoryUsage > 2 * 1024 * 1024 * 1024) {
|
||||
// 2GB
|
||||
return "scale_up";
|
||||
return 'scale_up';
|
||||
}
|
||||
if (currentHitRate < 60) {
|
||||
return "optimize";
|
||||
return 'optimize';
|
||||
}
|
||||
if (predictedRequests < 100) {
|
||||
return "scale_down";
|
||||
return 'scale_down';
|
||||
}
|
||||
return "maintain";
|
||||
return 'maintain';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1040,7 +1040,7 @@ export interface CacheAnalysis {
|
||||
timestamp: number;
|
||||
duration: number;
|
||||
overallScore: number;
|
||||
status: "excellent" | "good" | "warning" | "critical";
|
||||
status: 'excellent' | 'good' | 'warning' | 'critical';
|
||||
cacheCount: number;
|
||||
analyses: CacheComponentAnalysis[];
|
||||
issues: CacheIssue[];
|
||||
@@ -1051,7 +1051,7 @@ export interface CacheAnalysis {
|
||||
export interface CacheComponentAnalysis {
|
||||
cacheKey: string;
|
||||
score: number;
|
||||
status: "excellent" | "good" | "warning" | "critical";
|
||||
status: 'excellent' | 'good' | 'warning' | 'critical';
|
||||
metrics: {
|
||||
hitRate: number;
|
||||
totalRequests: number;
|
||||
@@ -1065,18 +1065,18 @@ export interface CacheComponentAnalysis {
|
||||
|
||||
export interface CacheIssue {
|
||||
cacheKey: string;
|
||||
type: "performance" | "memory" | "configuration";
|
||||
severity: "low" | "medium" | "high";
|
||||
type: 'performance' | 'memory' | 'configuration';
|
||||
severity: 'low' | 'medium' | 'high';
|
||||
description: string;
|
||||
impact: "low" | "medium" | "high";
|
||||
impact: 'low' | 'medium' | 'high';
|
||||
}
|
||||
|
||||
export interface CacheRecommendation {
|
||||
cacheKey: string;
|
||||
type: "performance" | "memory" | "configuration";
|
||||
priority: "low" | "medium" | "high";
|
||||
type: 'performance' | 'memory' | 'configuration';
|
||||
priority: 'low' | 'medium' | 'high';
|
||||
description: string;
|
||||
estimatedImpact: "low" | "medium" | "high";
|
||||
estimatedImpact: 'low' | 'medium' | 'high';
|
||||
}
|
||||
|
||||
export interface CacheSummary {
|
||||
@@ -1110,7 +1110,7 @@ export interface CacheConfigOptimization {
|
||||
currentScore: number;
|
||||
targetScore: number;
|
||||
optimizations: ConfigChange[];
|
||||
estimatedImpact: "low" | "medium" | "high";
|
||||
estimatedImpact: 'low' | 'medium' | 'high';
|
||||
}
|
||||
|
||||
export interface ConfigChange {
|
||||
@@ -1118,19 +1118,19 @@ export interface ConfigChange {
|
||||
currentValue: any;
|
||||
recommendedValue: any;
|
||||
reason: string;
|
||||
impact: "low" | "medium" | "high";
|
||||
impact: 'low' | 'medium' | 'high';
|
||||
}
|
||||
|
||||
export interface GlobalCacheOptimization {
|
||||
type: "strategy" | "configuration" | "infrastructure";
|
||||
type: 'strategy' | 'configuration' | 'infrastructure';
|
||||
description: string;
|
||||
impact: "low" | "medium" | "high";
|
||||
effort: "low" | "medium" | "high";
|
||||
impact: 'low' | 'medium' | 'high';
|
||||
effort: 'low' | 'medium' | 'high';
|
||||
timeline: string;
|
||||
}
|
||||
|
||||
export interface EstimatedImpact {
|
||||
overallImpact: "low" | "medium" | "high";
|
||||
overallImpact: 'low' | 'medium' | 'high';
|
||||
performanceImprovement: string;
|
||||
memoryReduction: string;
|
||||
responseTimeImprovement: string;
|
||||
@@ -1211,5 +1211,5 @@ export interface CachePrediction {
|
||||
predictedHitRate: number;
|
||||
currentMemoryUsage: number;
|
||||
predictedMemoryUsage: number;
|
||||
recommendedAction: "maintain" | "scale_up" | "scale_down" | "optimize";
|
||||
recommendedAction: 'maintain' | 'scale_up' | 'scale_down' | 'optimize';
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* Query Optimizer - 查询优化器
|
||||
@@ -26,7 +26,7 @@ export class QueryOptimizer {
|
||||
async analyzeQueryPerformance(
|
||||
options: QueryAnalysisOptions = {},
|
||||
): Promise<QueryAnalysis> {
|
||||
this.logger.log("Analyzing query performance");
|
||||
this.logger.log('Analyzing query performance');
|
||||
|
||||
const startTime = Date.now();
|
||||
const queries = options.queryPattern
|
||||
@@ -82,7 +82,7 @@ export class QueryOptimizer {
|
||||
async optimizeQueries(
|
||||
options: QueryOptimizationOptions = {},
|
||||
): Promise<QueryOptimization> {
|
||||
this.logger.log("Starting query optimization");
|
||||
this.logger.log('Starting query optimization');
|
||||
|
||||
const analysis = await this.analyzeQueryPerformance();
|
||||
const optimizations: QueryConfigOptimization[] = [];
|
||||
@@ -152,7 +152,7 @@ export class QueryOptimizer {
|
||||
Array.from(this.queryMetrics.entries()).map(([id, metrics]) => ({
|
||||
queryId: id,
|
||||
score: 100,
|
||||
status: "excellent" as const,
|
||||
status: 'excellent' as const,
|
||||
metrics: {
|
||||
averageExecutionTime: metrics.executionTime,
|
||||
executionCount: metrics.executionCount,
|
||||
@@ -303,8 +303,8 @@ export class QueryOptimizer {
|
||||
predictedExecutionTime,
|
||||
currentExecutionCount,
|
||||
predictedExecutionCount,
|
||||
performanceTrend: performanceTrend > 0 ? "degrading" : "improving",
|
||||
usageTrend: usageTrend > 0 ? "increasing" : "decreasing",
|
||||
performanceTrend: performanceTrend > 0 ? 'degrading' : 'improving',
|
||||
usageTrend: usageTrend > 0 ? 'increasing' : 'decreasing',
|
||||
riskLevel: this.assessQueryRiskLevel(
|
||||
predictedExecutionTime,
|
||||
predictedExecutionCount,
|
||||
@@ -327,7 +327,7 @@ export class QueryOptimizer {
|
||||
metrics: QueryMetrics,
|
||||
): Promise<QueryComponentAnalysis> {
|
||||
let score = 100;
|
||||
let status: "excellent" | "good" | "warning" | "critical" = "excellent";
|
||||
let status: 'excellent' | 'good' | 'warning' | 'critical' = 'excellent';
|
||||
const issues: string[] = [];
|
||||
const recommendations: string[] = [];
|
||||
|
||||
@@ -335,27 +335,27 @@ export class QueryOptimizer {
|
||||
if (metrics.executionTime > 5000) {
|
||||
// 5秒
|
||||
score = Math.min(score, 20);
|
||||
status = "critical";
|
||||
issues.push("Very slow query execution");
|
||||
recommendations.push("Add database indexes or optimize query structure");
|
||||
status = 'critical';
|
||||
issues.push('Very slow query execution');
|
||||
recommendations.push('Add database indexes or optimize query structure');
|
||||
} else if (metrics.executionTime > 1000) {
|
||||
// 1秒
|
||||
score = Math.min(score, 50);
|
||||
status = status === "excellent" ? "warning" : status;
|
||||
issues.push("Slow query execution");
|
||||
recommendations.push("Consider adding indexes or query optimization");
|
||||
status = status === 'excellent' ? 'warning' : status;
|
||||
issues.push('Slow query execution');
|
||||
recommendations.push('Consider adding indexes or query optimization');
|
||||
} else if (metrics.executionTime > 500) {
|
||||
// 0.5秒
|
||||
score = Math.min(score, 75);
|
||||
status = status === "excellent" ? "good" : status;
|
||||
status = status === 'excellent' ? 'good' : status;
|
||||
}
|
||||
|
||||
// 评估索引使用
|
||||
if (metrics.indexUsage < 0.5) {
|
||||
score = Math.min(score, 60);
|
||||
status = status === "excellent" ? "warning" : status;
|
||||
issues.push("Poor index usage");
|
||||
recommendations.push("Review and optimize database indexes");
|
||||
status = status === 'excellent' ? 'warning' : status;
|
||||
issues.push('Poor index usage');
|
||||
recommendations.push('Review and optimize database indexes');
|
||||
}
|
||||
|
||||
// 评估扫描效率
|
||||
@@ -363,10 +363,10 @@ export class QueryOptimizer {
|
||||
metrics.rowsReturned / Math.max(1, metrics.rowsExamined);
|
||||
if (scanEfficiency < 0.1) {
|
||||
score = Math.min(score, 40);
|
||||
status = status === "excellent" || status === "good" ? "warning" : status;
|
||||
issues.push("Low scan efficiency - examining too many rows");
|
||||
status = status === 'excellent' || status === 'good' ? 'warning' : status;
|
||||
issues.push('Low scan efficiency - examining too many rows');
|
||||
recommendations.push(
|
||||
"Add more selective indexes or refine WHERE conditions",
|
||||
'Add more selective indexes or refine WHERE conditions',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -421,11 +421,11 @@ export class QueryOptimizer {
|
||||
*/
|
||||
private determineQueryStatus(
|
||||
score: number,
|
||||
): "excellent" | "good" | "warning" | "critical" {
|
||||
if (score >= 90) return "excellent";
|
||||
if (score >= 75) return "good";
|
||||
if (score >= 50) return "warning";
|
||||
return "critical";
|
||||
): 'excellent' | 'good' | 'warning' | 'critical' {
|
||||
if (score >= 90) return 'excellent';
|
||||
if (score >= 75) return 'good';
|
||||
if (score >= 50) return 'warning';
|
||||
return 'critical';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -440,7 +440,7 @@ export class QueryOptimizer {
|
||||
queryId: analysis.queryId,
|
||||
executionTime: analysis.metrics.averageExecutionTime,
|
||||
executionCount: analysis.metrics.executionCount,
|
||||
severity: analysis.status === "critical" ? "high" : "medium",
|
||||
severity: analysis.status === 'critical' ? 'high' : 'medium',
|
||||
impact: this.calculateSlowQueryImpact(analysis.metrics),
|
||||
suggestions: analysis.recommendations,
|
||||
}));
|
||||
@@ -462,7 +462,7 @@ export class QueryOptimizer {
|
||||
recommendations.push({
|
||||
queryId: analysis.queryId,
|
||||
type: this.categorizeQueryRecommendation(rec),
|
||||
priority: analysis.status === "critical" ? "high" : "medium",
|
||||
priority: analysis.status === 'critical' ? 'high' : 'medium',
|
||||
description: rec,
|
||||
estimatedImpact: this.estimateQueryRecommendationImpact(
|
||||
analysis.score,
|
||||
@@ -474,22 +474,22 @@ export class QueryOptimizer {
|
||||
// 基于N+1问题生成建议
|
||||
nPlusOneIssues.forEach((issue) => {
|
||||
recommendations.push({
|
||||
queryId: "multiple",
|
||||
type: "optimization",
|
||||
priority: issue.severity === "high" ? "high" : "medium",
|
||||
queryId: 'multiple',
|
||||
type: 'optimization',
|
||||
priority: issue.severity === 'high' ? 'high' : 'medium',
|
||||
description: `Resolve N+1 query issue: ${issue.suggestion}`,
|
||||
estimatedImpact: "high",
|
||||
estimatedImpact: 'high',
|
||||
});
|
||||
});
|
||||
|
||||
// 基于慢查询生成建议
|
||||
if (slowQueryIssues.length > 3) {
|
||||
recommendations.push({
|
||||
queryId: "global",
|
||||
type: "performance",
|
||||
priority: "high",
|
||||
description: "Review database configuration and connection pooling",
|
||||
estimatedImpact: "high",
|
||||
queryId: 'global',
|
||||
type: 'performance',
|
||||
priority: 'high',
|
||||
description: 'Review database configuration and connection pooling',
|
||||
estimatedImpact: 'high',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -504,14 +504,14 @@ export class QueryOptimizer {
|
||||
): QuerySummary {
|
||||
const totalQueries = analyses.length;
|
||||
const excellentQueries = analyses.filter(
|
||||
(a) => a.status === "excellent",
|
||||
(a) => a.status === 'excellent',
|
||||
).length;
|
||||
const goodQueries = analyses.filter((a) => a.status === "good").length;
|
||||
const goodQueries = analyses.filter((a) => a.status === 'good').length;
|
||||
const warningQueries = analyses.filter(
|
||||
(a) => a.status === "warning",
|
||||
(a) => a.status === 'warning',
|
||||
).length;
|
||||
const criticalQueries = analyses.filter(
|
||||
(a) => a.status === "critical",
|
||||
(a) => a.status === 'critical',
|
||||
).length;
|
||||
|
||||
const avgExecutionTime =
|
||||
@@ -548,24 +548,24 @@ export class QueryOptimizer {
|
||||
// 基于执行时间优化
|
||||
if (analysis.metrics.averageExecutionTime > 1000) {
|
||||
optimizations.push({
|
||||
type: "index",
|
||||
description: "Add database index to improve query performance",
|
||||
currentValue: "No optimized index",
|
||||
recommendedValue: "Composite index on frequently queried columns",
|
||||
estimatedImprovement: "50-80%",
|
||||
impact: "high",
|
||||
type: 'index',
|
||||
description: 'Add database index to improve query performance',
|
||||
currentValue: 'No optimized index',
|
||||
recommendedValue: 'Composite index on frequently queried columns',
|
||||
estimatedImprovement: '50-80%',
|
||||
impact: 'high',
|
||||
});
|
||||
}
|
||||
|
||||
// 基于索引使用优化
|
||||
if (analysis.metrics.indexUsage < 0.5) {
|
||||
optimizations.push({
|
||||
type: "query_structure",
|
||||
description: "Optimize query structure to better utilize indexes",
|
||||
currentValue: "Suboptimal query structure",
|
||||
recommendedValue: "Index-friendly query structure",
|
||||
estimatedImprovement: "30-60%",
|
||||
impact: "medium",
|
||||
type: 'query_structure',
|
||||
description: 'Optimize query structure to better utilize indexes',
|
||||
currentValue: 'Suboptimal query structure',
|
||||
recommendedValue: 'Index-friendly query structure',
|
||||
estimatedImprovement: '30-60%',
|
||||
impact: 'medium',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -575,12 +575,12 @@ export class QueryOptimizer {
|
||||
Math.max(1, analysis.metrics.rowsExamined);
|
||||
if (scanEfficiency < 0.1) {
|
||||
optimizations.push({
|
||||
type: "filtering",
|
||||
description: "Improve WHERE clause selectivity",
|
||||
type: 'filtering',
|
||||
description: 'Improve WHERE clause selectivity',
|
||||
currentValue: `Scanning ${analysis.metrics.rowsExamined} rows to return ${analysis.metrics.rowsReturned}`,
|
||||
recommendedValue: "More selective filtering conditions",
|
||||
estimatedImprovement: "40-70%",
|
||||
impact: "high",
|
||||
recommendedValue: 'More selective filtering conditions',
|
||||
estimatedImprovement: '40-70%',
|
||||
impact: 'high',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -604,11 +604,11 @@ export class QueryOptimizer {
|
||||
// 如果整体性能较差,建议全局优化
|
||||
if (analysis.overallScore < 70) {
|
||||
optimizations.push({
|
||||
type: "database_configuration",
|
||||
description: "Optimize database configuration and connection pooling",
|
||||
impact: "high",
|
||||
effort: "medium",
|
||||
timeline: "1-2 weeks",
|
||||
type: 'database_configuration',
|
||||
description: 'Optimize database configuration and connection pooling',
|
||||
impact: 'high',
|
||||
effort: 'medium',
|
||||
timeline: '1-2 weeks',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -618,22 +618,22 @@ export class QueryOptimizer {
|
||||
).length;
|
||||
if (slowQueries > 5) {
|
||||
optimizations.push({
|
||||
type: "monitoring",
|
||||
description: "Implement comprehensive query performance monitoring",
|
||||
impact: "medium",
|
||||
effort: "low",
|
||||
timeline: "3-5 days",
|
||||
type: 'monitoring',
|
||||
description: 'Implement comprehensive query performance monitoring',
|
||||
impact: 'medium',
|
||||
effort: 'low',
|
||||
timeline: '3-5 days',
|
||||
});
|
||||
}
|
||||
|
||||
// 如果有N+1问题,建议ORM优化
|
||||
if (analysis.nPlusOneIssues.length > 0) {
|
||||
optimizations.push({
|
||||
type: "orm_optimization",
|
||||
description: "Implement eager loading and query batching strategies",
|
||||
impact: "high",
|
||||
effort: "medium",
|
||||
timeline: "1-2 weeks",
|
||||
type: 'orm_optimization',
|
||||
description: 'Implement eager loading and query batching strategies',
|
||||
impact: 'high',
|
||||
effort: 'medium',
|
||||
timeline: '1-2 weeks',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -647,20 +647,20 @@ export class QueryOptimizer {
|
||||
optimizations: QueryConfigOptimization[],
|
||||
): EstimatedQueryImpact {
|
||||
const highImpactCount = optimizations.filter((o) =>
|
||||
o.optimizations.some((opt) => opt.impact === "high"),
|
||||
o.optimizations.some((opt) => opt.impact === 'high'),
|
||||
).length;
|
||||
|
||||
const mediumImpactCount = optimizations.filter((o) =>
|
||||
o.optimizations.some((opt) => opt.impact === "medium"),
|
||||
o.optimizations.some((opt) => opt.impact === 'medium'),
|
||||
).length;
|
||||
|
||||
let overallImpact: "low" | "medium" | "high";
|
||||
let overallImpact: 'low' | 'medium' | 'high';
|
||||
if (highImpactCount > 0) {
|
||||
overallImpact = "high";
|
||||
overallImpact = 'high';
|
||||
} else if (mediumImpactCount > 0) {
|
||||
overallImpact = "medium";
|
||||
overallImpact = 'medium';
|
||||
} else {
|
||||
overallImpact = "low";
|
||||
overallImpact = 'low';
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -682,17 +682,17 @@ export class QueryOptimizer {
|
||||
|
||||
// 高优先级查询优化
|
||||
const criticalOptimizations = optimizations.filter((o) =>
|
||||
o.optimizations.some((opt) => opt.impact === "high"),
|
||||
o.optimizations.some((opt) => opt.impact === 'high'),
|
||||
);
|
||||
|
||||
if (criticalOptimizations.length > 0) {
|
||||
steps.push({
|
||||
phase: 1,
|
||||
description: "Optimize critical slow queries",
|
||||
duration: "3-5 days",
|
||||
description: 'Optimize critical slow queries',
|
||||
duration: '3-5 days',
|
||||
dependencies: [],
|
||||
risks: [
|
||||
"Potential impact on application performance during index creation",
|
||||
'Potential impact on application performance during index creation',
|
||||
],
|
||||
queries: criticalOptimizations.map((o) => o.queryId),
|
||||
});
|
||||
@@ -702,10 +702,10 @@ export class QueryOptimizer {
|
||||
if (globalOptimizations.length > 0) {
|
||||
steps.push({
|
||||
phase: 2,
|
||||
description: "Implement global query strategy improvements",
|
||||
duration: globalOptimizations[0]?.timeline || "1-2 weeks",
|
||||
dependencies: ["Phase 1 completion"],
|
||||
risks: ["Requires coordination with database administrators"],
|
||||
description: 'Implement global query strategy improvements',
|
||||
duration: globalOptimizations[0]?.timeline || '1-2 weeks',
|
||||
dependencies: ['Phase 1 completion'],
|
||||
risks: ['Requires coordination with database administrators'],
|
||||
queries: [],
|
||||
});
|
||||
}
|
||||
@@ -713,10 +713,10 @@ export class QueryOptimizer {
|
||||
// 监控和验证
|
||||
steps.push({
|
||||
phase: 3,
|
||||
description: "Monitor and validate query optimization results",
|
||||
duration: "1 week",
|
||||
dependencies: ["Previous phases completion"],
|
||||
risks: ["May require rollback if performance degrades"],
|
||||
description: 'Monitor and validate query optimization results',
|
||||
duration: '1 week',
|
||||
dependencies: ['Previous phases completion'],
|
||||
risks: ['May require rollback if performance degrades'],
|
||||
queries: optimizations.map((o) => o.queryId),
|
||||
});
|
||||
|
||||
@@ -832,7 +832,7 @@ export class QueryOptimizer {
|
||||
);
|
||||
}
|
||||
|
||||
return commonWords.join(" ") || "similar_query_pattern";
|
||||
return commonWords.join(' ') || 'similar_query_pattern';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -841,10 +841,10 @@ export class QueryOptimizer {
|
||||
private calculateNPlusOneSeverity(
|
||||
queryCount: number,
|
||||
totalExecutionTime: number,
|
||||
): "low" | "medium" | "high" {
|
||||
if (queryCount > 50 || totalExecutionTime > 10000) return "high";
|
||||
if (queryCount > 20 || totalExecutionTime > 5000) return "medium";
|
||||
return "low";
|
||||
): 'low' | 'medium' | 'high' {
|
||||
if (queryCount > 50 || totalExecutionTime > 10000) return 'high';
|
||||
if (queryCount > 20 || totalExecutionTime > 5000) return 'medium';
|
||||
return 'low';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -859,7 +859,7 @@ export class QueryOptimizer {
|
||||
*/
|
||||
private initializeDefaultMetrics(): void {
|
||||
// 初始化一些示例查询指标
|
||||
this.queryMetrics.set("SELECT_users_by_id", {
|
||||
this.queryMetrics.set('SELECT_users_by_id', {
|
||||
executionTime: 50,
|
||||
executionCount: 1000,
|
||||
rowsExamined: 1,
|
||||
@@ -868,7 +868,7 @@ export class QueryOptimizer {
|
||||
lastExecuted: Date.now(),
|
||||
});
|
||||
|
||||
this.queryMetrics.set("SELECT_orders_with_items", {
|
||||
this.queryMetrics.set('SELECT_orders_with_items', {
|
||||
executionTime: 1200,
|
||||
executionCount: 500,
|
||||
rowsExamined: 10000,
|
||||
@@ -877,7 +877,7 @@ export class QueryOptimizer {
|
||||
lastExecuted: Date.now(),
|
||||
});
|
||||
|
||||
this.queryMetrics.set("SELECT_products_search", {
|
||||
this.queryMetrics.set('SELECT_products_search', {
|
||||
executionTime: 800,
|
||||
executionCount: 2000,
|
||||
rowsExamined: 50000,
|
||||
@@ -921,11 +921,11 @@ export class QueryOptimizer {
|
||||
private calculateSlowQueryImpact(metrics: {
|
||||
averageExecutionTime: number;
|
||||
executionCount: number;
|
||||
}): "low" | "medium" | "high" {
|
||||
}): 'low' | 'medium' | 'high' {
|
||||
const totalImpact = metrics.averageExecutionTime * metrics.executionCount;
|
||||
if (totalImpact > 1000000) return "high"; // 1M ms
|
||||
if (totalImpact > 100000) return "medium"; // 100K ms
|
||||
return "low";
|
||||
if (totalImpact > 1000000) return 'high'; // 1M ms
|
||||
if (totalImpact > 100000) return 'medium'; // 100K ms
|
||||
return 'low';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -933,14 +933,14 @@ export class QueryOptimizer {
|
||||
*/
|
||||
private categorizeQueryRecommendation(
|
||||
recommendation: string,
|
||||
): "performance" | "index" | "optimization" {
|
||||
if (recommendation.toLowerCase().includes("index")) return "index";
|
||||
): 'performance' | 'index' | 'optimization' {
|
||||
if (recommendation.toLowerCase().includes('index')) return 'index';
|
||||
if (
|
||||
recommendation.toLowerCase().includes("optimize") ||
|
||||
recommendation.toLowerCase().includes("structure")
|
||||
recommendation.toLowerCase().includes('optimize') ||
|
||||
recommendation.toLowerCase().includes('structure')
|
||||
)
|
||||
return "optimization";
|
||||
return "performance";
|
||||
return 'optimization';
|
||||
return 'performance';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -948,10 +948,10 @@ export class QueryOptimizer {
|
||||
*/
|
||||
private estimateQueryRecommendationImpact(
|
||||
score: number,
|
||||
): "low" | "medium" | "high" {
|
||||
if (score < 50) return "high";
|
||||
if (score < 75) return "medium";
|
||||
return "low";
|
||||
): 'low' | 'medium' | 'high' {
|
||||
if (score < 50) return 'high';
|
||||
if (score < 75) return 'medium';
|
||||
return 'low';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -959,18 +959,18 @@ export class QueryOptimizer {
|
||||
*/
|
||||
private calculateQueryComponentImpact(
|
||||
optimizations: QueryConfigChange[],
|
||||
): "low" | "medium" | "high" {
|
||||
): 'low' | 'medium' | 'high' {
|
||||
const highImpactCount = optimizations.filter(
|
||||
(o) => o.impact === "high",
|
||||
(o) => o.impact === 'high',
|
||||
).length;
|
||||
if (highImpactCount > 0) return "high";
|
||||
if (highImpactCount > 0) return 'high';
|
||||
|
||||
const mediumImpactCount = optimizations.filter(
|
||||
(o) => o.impact === "medium",
|
||||
(o) => o.impact === 'medium',
|
||||
).length;
|
||||
if (mediumImpactCount > 0) return "medium";
|
||||
if (mediumImpactCount > 0) return 'medium';
|
||||
|
||||
return "low";
|
||||
return 'low';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1075,11 +1075,11 @@ export class QueryOptimizer {
|
||||
const recommendations: string[] = [];
|
||||
|
||||
if (slowQueries.length > 10) {
|
||||
recommendations.push("Consider implementing query result caching");
|
||||
recommendations.push('Consider implementing query result caching');
|
||||
}
|
||||
|
||||
if (slowQueries.some((q) => q.indexUsage < 0.5)) {
|
||||
recommendations.push("Review and optimize database indexes");
|
||||
recommendations.push('Review and optimize database indexes');
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -1088,7 +1088,7 @@ export class QueryOptimizer {
|
||||
)
|
||||
) {
|
||||
recommendations.push(
|
||||
"Improve query selectivity with better WHERE conditions",
|
||||
'Improve query selectivity with better WHERE conditions',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1117,11 +1117,11 @@ export class QueryOptimizer {
|
||||
private assessQueryRiskLevel(
|
||||
predictedExecutionTime: number,
|
||||
predictedExecutionCount: number,
|
||||
): "low" | "medium" | "high" {
|
||||
): 'low' | 'medium' | 'high' {
|
||||
const totalImpact = predictedExecutionTime * predictedExecutionCount;
|
||||
if (totalImpact > 5000000 || predictedExecutionTime > 10000) return "high";
|
||||
if (totalImpact > 1000000 || predictedExecutionTime > 2000) return "medium";
|
||||
return "low";
|
||||
if (totalImpact > 5000000 || predictedExecutionTime > 10000) return 'high';
|
||||
if (totalImpact > 1000000 || predictedExecutionTime > 2000) return 'medium';
|
||||
return 'low';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1130,14 +1130,14 @@ export class QueryOptimizer {
|
||||
private recommendQueryAction(
|
||||
predictedExecutionTime: number,
|
||||
performanceTrend: number,
|
||||
): "monitor" | "optimize" | "urgent_optimize" {
|
||||
): 'monitor' | 'optimize' | 'urgent_optimize' {
|
||||
if (predictedExecutionTime > 5000 || performanceTrend > 0.1) {
|
||||
return "urgent_optimize";
|
||||
return 'urgent_optimize';
|
||||
}
|
||||
if (predictedExecutionTime > 1000 || performanceTrend > 0.05) {
|
||||
return "optimize";
|
||||
return 'optimize';
|
||||
}
|
||||
return "monitor";
|
||||
return 'monitor';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1164,7 +1164,7 @@ export interface QueryAnalysis {
|
||||
timestamp: number;
|
||||
duration: number;
|
||||
overallScore: number;
|
||||
status: "excellent" | "good" | "warning" | "critical";
|
||||
status: 'excellent' | 'good' | 'warning' | 'critical';
|
||||
queryCount: number;
|
||||
analyses: QueryComponentAnalysis[];
|
||||
nPlusOneIssues: NPlusOneIssue[];
|
||||
@@ -1176,7 +1176,7 @@ export interface QueryAnalysis {
|
||||
export interface QueryComponentAnalysis {
|
||||
queryId: string;
|
||||
score: number;
|
||||
status: "excellent" | "good" | "warning" | "critical";
|
||||
status: 'excellent' | 'good' | 'warning' | 'critical';
|
||||
metrics: {
|
||||
averageExecutionTime: number;
|
||||
executionCount: number;
|
||||
@@ -1193,7 +1193,7 @@ export interface NPlusOneIssue {
|
||||
queryCount: number;
|
||||
totalExecutionTime: number;
|
||||
affectedQueries: string[];
|
||||
severity: "low" | "medium" | "high";
|
||||
severity: 'low' | 'medium' | 'high';
|
||||
detectedAt: number;
|
||||
suggestion: string;
|
||||
}
|
||||
@@ -1202,17 +1202,17 @@ export interface SlowQueryIssue {
|
||||
queryId: string;
|
||||
executionTime: number;
|
||||
executionCount: number;
|
||||
severity: "low" | "medium" | "high";
|
||||
impact: "low" | "medium" | "high";
|
||||
severity: 'low' | 'medium' | 'high';
|
||||
impact: 'low' | 'medium' | 'high';
|
||||
suggestions: string[];
|
||||
}
|
||||
|
||||
export interface QueryRecommendation {
|
||||
queryId: string;
|
||||
type: "performance" | "index" | "optimization";
|
||||
priority: "low" | "medium" | "high";
|
||||
type: 'performance' | 'index' | 'optimization';
|
||||
priority: 'low' | 'medium' | 'high';
|
||||
description: string;
|
||||
estimatedImpact: "low" | "medium" | "high";
|
||||
estimatedImpact: 'low' | 'medium' | 'high';
|
||||
}
|
||||
|
||||
export interface QuerySummary {
|
||||
@@ -1246,28 +1246,28 @@ export interface QueryConfigOptimization {
|
||||
currentScore: number;
|
||||
targetScore: number;
|
||||
optimizations: QueryConfigChange[];
|
||||
estimatedImpact: "low" | "medium" | "high";
|
||||
estimatedImpact: 'low' | 'medium' | 'high';
|
||||
}
|
||||
|
||||
export interface QueryConfigChange {
|
||||
type: "index" | "query_structure" | "filtering";
|
||||
type: 'index' | 'query_structure' | 'filtering';
|
||||
description: string;
|
||||
currentValue: string;
|
||||
recommendedValue: string;
|
||||
estimatedImprovement: string;
|
||||
impact: "low" | "medium" | "high";
|
||||
impact: 'low' | 'medium' | 'high';
|
||||
}
|
||||
|
||||
export interface GlobalQueryOptimization {
|
||||
type: "database_configuration" | "monitoring" | "orm_optimization";
|
||||
type: 'database_configuration' | 'monitoring' | 'orm_optimization';
|
||||
description: string;
|
||||
impact: "low" | "medium" | "high";
|
||||
effort: "low" | "medium" | "high";
|
||||
impact: 'low' | 'medium' | 'high';
|
||||
effort: 'low' | 'medium' | 'high';
|
||||
timeline: string;
|
||||
}
|
||||
|
||||
export interface EstimatedQueryImpact {
|
||||
overallImpact: "low" | "medium" | "high";
|
||||
overallImpact: 'low' | 'medium' | 'high';
|
||||
performanceImprovement: string;
|
||||
responseTimeReduction: string;
|
||||
throughputIncrease: string;
|
||||
@@ -1320,10 +1320,10 @@ export interface QueryPerformancePrediction {
|
||||
predictedExecutionTime: number;
|
||||
currentExecutionCount: number;
|
||||
predictedExecutionCount: number;
|
||||
performanceTrend: "improving" | "degrading";
|
||||
usageTrend: "increasing" | "decreasing";
|
||||
riskLevel: "low" | "medium" | "high";
|
||||
recommendedAction: "monitor" | "optimize" | "urgent_optimize";
|
||||
performanceTrend: 'improving' | 'degrading';
|
||||
usageTrend: 'increasing' | 'decreasing';
|
||||
riskLevel: 'low' | 'medium' | 'high';
|
||||
recommendedAction: 'monitor' | 'optimize' | 'urgent_optimize';
|
||||
}
|
||||
|
||||
export interface SimilarQueryPattern {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* AI Metrics Service - AI指标服务
|
||||
@@ -218,7 +218,7 @@ export class AiMetricsService {
|
||||
statistics,
|
||||
trend,
|
||||
alerts,
|
||||
status: alerts.length > 0 ? "warning" : "healthy",
|
||||
status: alerts.length > 0 ? 'warning' : 'healthy',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -342,7 +342,7 @@ export class AiMetricsService {
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
timeRange: options.timeRange,
|
||||
format: options.format || "json",
|
||||
format: options.format || 'json',
|
||||
data: exportedData,
|
||||
};
|
||||
}
|
||||
@@ -423,10 +423,10 @@ export class AiMetricsService {
|
||||
private generateReportSummary(metricReports: MetricReport[]): ReportSummary {
|
||||
const totalMetrics = metricReports.length;
|
||||
const healthyMetrics = metricReports.filter(
|
||||
(m) => m.status === "healthy",
|
||||
(m) => m.status === 'healthy',
|
||||
).length;
|
||||
const warningMetrics = metricReports.filter(
|
||||
(m) => m.status === "warning",
|
||||
(m) => m.status === 'warning',
|
||||
).length;
|
||||
const totalAlerts = metricReports.reduce(
|
||||
(sum, m) => sum + m.alerts.length,
|
||||
@@ -438,7 +438,7 @@ export class AiMetricsService {
|
||||
healthyMetrics,
|
||||
warningMetrics,
|
||||
totalAlerts,
|
||||
overallHealth: warningMetrics === 0 ? "healthy" : "warning",
|
||||
overallHealth: warningMetrics === 0 ? 'healthy' : 'warning',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -452,13 +452,13 @@ export class AiMetricsService {
|
||||
const value = this.getAlertValue(alert, statistics);
|
||||
|
||||
switch (alert.condition) {
|
||||
case "greater_than":
|
||||
case 'greater_than':
|
||||
return value > alert.threshold;
|
||||
case "less_than":
|
||||
case 'less_than':
|
||||
return value < alert.threshold;
|
||||
case "equals":
|
||||
case 'equals':
|
||||
return value === alert.threshold;
|
||||
case "not_equals":
|
||||
case 'not_equals':
|
||||
return value !== alert.threshold;
|
||||
default:
|
||||
return false;
|
||||
@@ -473,17 +473,17 @@ export class AiMetricsService {
|
||||
statistics: MetricStatistics,
|
||||
): number {
|
||||
switch (alert.metric) {
|
||||
case "mean":
|
||||
case 'mean':
|
||||
return statistics.mean;
|
||||
case "max":
|
||||
case 'max':
|
||||
return statistics.max;
|
||||
case "min":
|
||||
case 'min':
|
||||
return statistics.min;
|
||||
case "p95":
|
||||
case 'p95':
|
||||
return statistics.p95;
|
||||
case "p99":
|
||||
case 'p99':
|
||||
return statistics.p99;
|
||||
case "count":
|
||||
case 'count':
|
||||
return statistics.count;
|
||||
default:
|
||||
return statistics.mean;
|
||||
@@ -568,7 +568,7 @@ export interface MetricReport {
|
||||
statistics: MetricStatistics;
|
||||
trend: MetricTrend[];
|
||||
alerts: MetricAlertResult[];
|
||||
status: "healthy" | "warning" | "critical";
|
||||
status: 'healthy' | 'warning' | 'critical';
|
||||
}
|
||||
|
||||
export interface MetricsReport {
|
||||
@@ -583,23 +583,23 @@ export interface ReportSummary {
|
||||
healthyMetrics: number;
|
||||
warningMetrics: number;
|
||||
totalAlerts: number;
|
||||
overallHealth: "healthy" | "warning" | "critical";
|
||||
overallHealth: 'healthy' | 'warning' | 'critical';
|
||||
}
|
||||
|
||||
export interface MetricAlert {
|
||||
id: string;
|
||||
name: string;
|
||||
condition: "greater_than" | "less_than" | "equals" | "not_equals";
|
||||
condition: 'greater_than' | 'less_than' | 'equals' | 'not_equals';
|
||||
threshold: number;
|
||||
metric: "mean" | "max" | "min" | "p95" | "p99" | "count";
|
||||
severity: "low" | "medium" | "high" | "critical";
|
||||
metric: 'mean' | 'max' | 'min' | 'p95' | 'p99' | 'count';
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface MetricAlertResult {
|
||||
alertId: string;
|
||||
name: string;
|
||||
severity: "low" | "medium" | "high" | "critical";
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
message: string;
|
||||
triggeredAt: number;
|
||||
value: number;
|
||||
@@ -618,7 +618,7 @@ export interface SystemMetricsOverview {
|
||||
export interface ExportOptions {
|
||||
timeRange?: TimeRange;
|
||||
metrics?: string[];
|
||||
format?: "json" | "csv" | "xml";
|
||||
format?: 'json' | 'csv' | 'xml';
|
||||
}
|
||||
|
||||
export interface ExportedMetrics {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { PerformanceAnalyzer } from "../analyzers/performance.analyzer";
|
||||
import { ResourceMonitor } from "../monitors/resource.monitor";
|
||||
import { CacheOptimizer } from "../optimizers/cache.optimizer";
|
||||
import { QueryOptimizer } from "../optimizers/query.optimizer";
|
||||
import type { ResourceStatus } from "../monitors/resource.monitor";
|
||||
import { Injectable, Logger, BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { PerformanceAnalyzer } from '../analyzers/performance.analyzer';
|
||||
import { ResourceMonitor } from '../monitors/resource.monitor';
|
||||
import { CacheOptimizer } from '../optimizers/cache.optimizer';
|
||||
import { QueryOptimizer } from '../optimizers/query.optimizer';
|
||||
import type { ResourceStatus } from '../monitors/resource.monitor';
|
||||
|
||||
/**
|
||||
* AI Tuner Service - AI调优服务
|
||||
@@ -33,17 +33,17 @@ export class AiTunerService {
|
||||
async startTuningSession(
|
||||
options: TuningSessionOptions = {},
|
||||
): Promise<TuningSession> {
|
||||
this.logger.log("Starting performance tuning session");
|
||||
this.logger.log('Starting performance tuning session');
|
||||
|
||||
if (this.currentTuningSession) {
|
||||
throw new Error("A tuning session is already in progress");
|
||||
throw new BadRequestException('A tuning session is already in progress');
|
||||
}
|
||||
|
||||
const session: TuningSession = {
|
||||
id: this.generateSessionId(),
|
||||
startTime: Date.now(),
|
||||
endTime: null,
|
||||
status: "running",
|
||||
status: 'running',
|
||||
options,
|
||||
baseline: null,
|
||||
optimizations: [],
|
||||
@@ -70,11 +70,11 @@ export class AiTunerService {
|
||||
this.logger.log(`Tuning session ${session.id} started successfully`);
|
||||
return session;
|
||||
} catch (error) {
|
||||
session.status = "failed";
|
||||
session.status = 'failed';
|
||||
session.endTime = Date.now();
|
||||
this.currentTuningSession = null;
|
||||
|
||||
this.logger.error("Failed to start tuning session", error);
|
||||
this.logger.error('Failed to start tuning session', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -86,25 +86,25 @@ export class AiTunerService {
|
||||
options: ComprehensiveTuningOptions = {},
|
||||
): Promise<TuningResults> {
|
||||
if (!this.currentTuningSession) {
|
||||
throw new Error(
|
||||
"No active tuning session. Please start a session first.",
|
||||
throw new BadRequestException(
|
||||
'No active tuning session. Please start a session first.',
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log("Performing comprehensive performance tuning");
|
||||
this.logger.log('Performing comprehensive performance tuning');
|
||||
|
||||
const session = this.currentTuningSession;
|
||||
const optimizations: OptimizationResult[] = [];
|
||||
|
||||
try {
|
||||
// 1. 性能分析
|
||||
this.logger.debug("Analyzing system performance");
|
||||
this.logger.debug('Analyzing system performance');
|
||||
const performanceAnalysis =
|
||||
await this.performanceAnalyzer.analyzePerformance();
|
||||
|
||||
// 2. 缓存优化
|
||||
if (options.enableCacheOptimization !== false) {
|
||||
this.logger.debug("Optimizing cache configuration");
|
||||
this.logger.debug('Optimizing cache configuration');
|
||||
const cacheOptimization =
|
||||
await this.cacheOptimizer.optimizeCacheConfiguration();
|
||||
|
||||
@@ -115,7 +115,7 @@ export class AiTunerService {
|
||||
);
|
||||
|
||||
optimizations.push({
|
||||
type: "cache",
|
||||
type: 'cache',
|
||||
optimizationId: cacheOptimization.id,
|
||||
success: cacheResult.success,
|
||||
impact: cacheOptimization.estimatedImpact.overallImpact,
|
||||
@@ -126,11 +126,11 @@ export class AiTunerService {
|
||||
|
||||
// 3. 查询优化
|
||||
if (options.enableQueryOptimization !== false) {
|
||||
this.logger.debug("Optimizing database queries");
|
||||
this.logger.debug('Optimizing database queries');
|
||||
const queryOptimization = await this.queryOptimizer.optimizeQueries();
|
||||
|
||||
optimizations.push({
|
||||
type: "query",
|
||||
type: 'query',
|
||||
optimizationId: queryOptimization.id,
|
||||
success: true, // 查询优化通常是建议性的
|
||||
impact: queryOptimization.estimatedImpact.overallImpact,
|
||||
@@ -140,11 +140,11 @@ export class AiTunerService {
|
||||
|
||||
// 4. 资源优化
|
||||
if (options.enableResourceOptimization !== false) {
|
||||
this.logger.debug("Optimizing resource usage");
|
||||
this.logger.debug('Optimizing resource usage');
|
||||
const resourceOptimization = await this.optimizeResourceUsage();
|
||||
|
||||
optimizations.push({
|
||||
type: "resource",
|
||||
type: 'resource',
|
||||
optimizationId: resourceOptimization.id,
|
||||
success: resourceOptimization.success,
|
||||
impact: resourceOptimization.impact,
|
||||
@@ -154,11 +154,11 @@ export class AiTunerService {
|
||||
|
||||
// 5. 应用级优化
|
||||
if (options.enableApplicationOptimization !== false) {
|
||||
this.logger.debug("Optimizing application performance");
|
||||
this.logger.debug('Optimizing application performance');
|
||||
const appOptimization = await this.optimizeApplicationPerformance();
|
||||
|
||||
optimizations.push({
|
||||
type: "application",
|
||||
type: 'application',
|
||||
optimizationId: appOptimization.id,
|
||||
success: appOptimization.success,
|
||||
impact: appOptimization.impact,
|
||||
@@ -202,7 +202,7 @@ export class AiTunerService {
|
||||
);
|
||||
return results;
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to perform comprehensive tuning", error);
|
||||
this.logger.error('Failed to perform comprehensive tuning', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -212,14 +212,14 @@ export class AiTunerService {
|
||||
*/
|
||||
async endTuningSession(): Promise<TuningSessionSummary> {
|
||||
if (!this.currentTuningSession) {
|
||||
throw new Error("No active tuning session");
|
||||
throw new BadRequestException('No active tuning session');
|
||||
}
|
||||
|
||||
this.logger.log("Ending tuning session");
|
||||
this.logger.log('Ending tuning session');
|
||||
|
||||
const session = this.currentTuningSession;
|
||||
session.endTime = Date.now();
|
||||
session.status = "completed";
|
||||
session.status = 'completed';
|
||||
|
||||
// 停止监控
|
||||
await this.resourceMonitor.stopMonitoring();
|
||||
@@ -275,7 +275,7 @@ export class AiTunerService {
|
||||
* 获取调优建议
|
||||
*/
|
||||
async getTuningRecommendations(
|
||||
scope: TuningScope = "all",
|
||||
scope: TuningScope = 'all',
|
||||
): Promise<TuningRecommendation[]> {
|
||||
this.logger.debug(`Getting tuning recommendations for scope: ${scope}`);
|
||||
|
||||
@@ -285,23 +285,23 @@ export class AiTunerService {
|
||||
const performanceAnalysis =
|
||||
await this.performanceAnalyzer.analyzePerformance();
|
||||
|
||||
if (scope === "all" || scope === "performance") {
|
||||
if (scope === 'all' || scope === 'performance') {
|
||||
recommendations.push(
|
||||
...this.generatePerformanceRecommendations(performanceAnalysis),
|
||||
);
|
||||
}
|
||||
|
||||
if (scope === "all" || scope === "cache") {
|
||||
if (scope === 'all' || scope === 'cache') {
|
||||
const cacheAnalysis = await this.cacheOptimizer.analyzeCachePerformance();
|
||||
recommendations.push(...this.generateCacheRecommendations(cacheAnalysis));
|
||||
}
|
||||
|
||||
if (scope === "all" || scope === "query") {
|
||||
if (scope === 'all' || scope === 'query') {
|
||||
const queryAnalysis = await this.queryOptimizer.analyzeQueryPerformance();
|
||||
recommendations.push(...this.generateQueryRecommendations(queryAnalysis));
|
||||
}
|
||||
|
||||
if (scope === "all" || scope === "resource") {
|
||||
if (scope === 'all' || scope === 'resource') {
|
||||
const resourceStatus = await this.resourceMonitor.getCurrentStatus();
|
||||
recommendations.push(
|
||||
...this.generateResourceRecommendations(resourceStatus),
|
||||
@@ -338,7 +338,7 @@ export class AiTunerService {
|
||||
async predictTuningImpact(
|
||||
optimizations: string[],
|
||||
): Promise<TuningImpactPrediction> {
|
||||
this.logger.debug("Predicting tuning impact");
|
||||
this.logger.debug('Predicting tuning impact');
|
||||
|
||||
const predictions: ComponentImpactPrediction[] = [];
|
||||
let overallImpact = 0;
|
||||
@@ -365,7 +365,7 @@ export class AiTunerService {
|
||||
* 建立性能基线
|
||||
*/
|
||||
private async establishBaseline(): Promise<PerformanceBaseline> {
|
||||
this.logger.debug("Establishing performance baseline");
|
||||
this.logger.debug('Establishing performance baseline');
|
||||
|
||||
const [performanceAnalysis, resourceStatus, cacheStats, queryStats] =
|
||||
await Promise.all([
|
||||
@@ -414,19 +414,19 @@ export class AiTunerService {
|
||||
|
||||
// CPU优化
|
||||
if (resourceStatus.snapshot.cpu.usage > 80) {
|
||||
optimizations.push("Implement CPU throttling for non-critical processes");
|
||||
optimizations.push('Implement CPU throttling for non-critical processes');
|
||||
}
|
||||
|
||||
// 内存优化
|
||||
if (resourceStatus.snapshot.memory.usage > 85) {
|
||||
optimizations.push(
|
||||
"Enable memory compression and garbage collection tuning",
|
||||
'Enable memory compression and garbage collection tuning',
|
||||
);
|
||||
}
|
||||
|
||||
// 磁盘优化
|
||||
if (resourceStatus.snapshot.disk.usage > 90) {
|
||||
optimizations.push("Implement log rotation and temporary file cleanup");
|
||||
optimizations.push('Implement log rotation and temporary file cleanup');
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -434,10 +434,10 @@ export class AiTunerService {
|
||||
success,
|
||||
impact:
|
||||
optimizations.length > 2
|
||||
? "high"
|
||||
? 'high'
|
||||
: optimizations.length > 0
|
||||
? "medium"
|
||||
: "low",
|
||||
? 'medium'
|
||||
: 'low',
|
||||
optimizations,
|
||||
estimatedImprovement: optimizations.length * 5, // 每个优化预计5%改进
|
||||
};
|
||||
@@ -454,18 +454,18 @@ export class AiTunerService {
|
||||
|
||||
// 基于性能分析结果生成优化建议
|
||||
if (performanceAnalysis.overallScore < 70) {
|
||||
optimizations.push("Implement response caching middleware");
|
||||
optimizations.push("Enable gzip compression for API responses");
|
||||
optimizations.push('Implement response caching middleware');
|
||||
optimizations.push('Enable gzip compression for API responses');
|
||||
}
|
||||
|
||||
if (performanceAnalysis.summary.averageResponseTime > 1000) {
|
||||
optimizations.push("Optimize critical path rendering");
|
||||
optimizations.push("Implement request batching for external APIs");
|
||||
optimizations.push('Optimize critical path rendering');
|
||||
optimizations.push('Implement request batching for external APIs');
|
||||
}
|
||||
|
||||
if (performanceAnalysis.summary.errorRate > 5) {
|
||||
optimizations.push("Implement circuit breaker pattern");
|
||||
optimizations.push("Add retry logic with exponential backoff");
|
||||
optimizations.push('Implement circuit breaker pattern');
|
||||
optimizations.push('Add retry logic with exponential backoff');
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -473,10 +473,10 @@ export class AiTunerService {
|
||||
success,
|
||||
impact:
|
||||
optimizations.length > 3
|
||||
? "high"
|
||||
? 'high'
|
||||
: optimizations.length > 1
|
||||
? "medium"
|
||||
: "low",
|
||||
? 'medium'
|
||||
: 'low',
|
||||
optimizations,
|
||||
estimatedImprovement: optimizations.length * 8, // 每个优化预计8%改进
|
||||
};
|
||||
@@ -522,26 +522,26 @@ export class AiTunerService {
|
||||
const failedOptimizations = optimizations.filter((o) => !o.success);
|
||||
if (failedOptimizations.length > 0) {
|
||||
recommendations.push({
|
||||
category: "optimization",
|
||||
priority: "high",
|
||||
title: "Address Failed Optimizations",
|
||||
category: 'optimization',
|
||||
priority: 'high',
|
||||
title: 'Address Failed Optimizations',
|
||||
description: `${failedOptimizations.length} optimizations failed and need attention`,
|
||||
impact: "high",
|
||||
effort: "medium",
|
||||
timeline: "1-2 weeks",
|
||||
impact: 'high',
|
||||
effort: 'medium',
|
||||
timeline: '1-2 weeks',
|
||||
});
|
||||
}
|
||||
|
||||
// 基于性能分析生成建议
|
||||
if (performanceAnalysis.overallScore < 60) {
|
||||
recommendations.push({
|
||||
category: "performance",
|
||||
priority: "high",
|
||||
title: "Critical Performance Issues",
|
||||
description: "System performance is below acceptable thresholds",
|
||||
impact: "high",
|
||||
effort: "high",
|
||||
timeline: "2-4 weeks",
|
||||
category: 'performance',
|
||||
priority: 'high',
|
||||
title: 'Critical Performance Issues',
|
||||
description: 'System performance is below acceptable thresholds',
|
||||
impact: 'high',
|
||||
effort: 'high',
|
||||
timeline: '2-4 weeks',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -557,18 +557,18 @@ export class AiTunerService {
|
||||
// 监控优化效果
|
||||
steps.push({
|
||||
phase: 1,
|
||||
description: "Monitor optimization effects",
|
||||
duration: "1 week",
|
||||
priority: "high",
|
||||
description: 'Monitor optimization effects',
|
||||
duration: '1 week',
|
||||
priority: 'high',
|
||||
});
|
||||
|
||||
// 验证改进
|
||||
if (optimizations.some((o) => o.success)) {
|
||||
steps.push({
|
||||
phase: 2,
|
||||
description: "Validate performance improvements",
|
||||
duration: "3-5 days",
|
||||
priority: "medium",
|
||||
description: 'Validate performance improvements',
|
||||
duration: '3-5 days',
|
||||
priority: 'medium',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -577,9 +577,9 @@ export class AiTunerService {
|
||||
if (failedOptimizations.length > 0) {
|
||||
steps.push({
|
||||
phase: 3,
|
||||
description: "Address failed optimizations",
|
||||
duration: "1-2 weeks",
|
||||
priority: "high",
|
||||
description: 'Address failed optimizations',
|
||||
duration: '1-2 weeks',
|
||||
priority: 'high',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -605,7 +605,7 @@ export class AiTunerService {
|
||||
}
|
||||
|
||||
if (session.metrics.failedOptimizations === 0) {
|
||||
achievements.push("All optimizations applied successfully");
|
||||
achievements.push('All optimizations applied successfully');
|
||||
}
|
||||
|
||||
return achievements;
|
||||
@@ -619,13 +619,13 @@ export class AiTunerService {
|
||||
|
||||
if (session.metrics.failedOptimizations > 0) {
|
||||
lessons.push(
|
||||
"Some optimizations require more careful planning and testing",
|
||||
'Some optimizations require more careful planning and testing',
|
||||
);
|
||||
}
|
||||
|
||||
if (session.metrics.overallImprovement < 10) {
|
||||
lessons.push(
|
||||
"Consider more aggressive optimization strategies for better results",
|
||||
'Consider more aggressive optimization strategies for better results',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -638,12 +638,12 @@ export class AiTunerService {
|
||||
private generateFutureRecommendations(session: TuningSession): string[] {
|
||||
const recommendations: string[] = [];
|
||||
|
||||
recommendations.push("Schedule regular performance tuning sessions");
|
||||
recommendations.push("Implement continuous performance monitoring");
|
||||
recommendations.push('Schedule regular performance tuning sessions');
|
||||
recommendations.push('Implement continuous performance monitoring');
|
||||
|
||||
if (session.metrics.overallImprovement > 15) {
|
||||
recommendations.push(
|
||||
"Document successful optimization patterns for reuse",
|
||||
'Document successful optimization patterns for reuse',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -667,10 +667,10 @@ export class AiTunerService {
|
||||
* 获取当前调优阶段
|
||||
*/
|
||||
private getCurrentTuningPhase(session: TuningSession): string {
|
||||
if (!session.baseline) return "establishing_baseline";
|
||||
if (session.optimizations.length === 0) return "analyzing";
|
||||
if (session.results === null) return "optimizing";
|
||||
return "completing";
|
||||
if (!session.baseline) return 'establishing_baseline';
|
||||
if (session.optimizations.length === 0) return 'analyzing';
|
||||
if (session.results === null) return 'optimizing';
|
||||
return 'completing';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -683,13 +683,13 @@ export class AiTunerService {
|
||||
|
||||
if (analysis.overallScore < 70) {
|
||||
recommendations.push({
|
||||
category: "performance",
|
||||
priority: "high",
|
||||
title: "Improve Overall Performance",
|
||||
description: "System performance is below optimal levels",
|
||||
impact: "high",
|
||||
effort: "medium",
|
||||
timeline: "2-3 weeks",
|
||||
category: 'performance',
|
||||
priority: 'high',
|
||||
title: 'Improve Overall Performance',
|
||||
description: 'System performance is below optimal levels',
|
||||
impact: 'high',
|
||||
effort: 'medium',
|
||||
timeline: '2-3 weeks',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -704,13 +704,13 @@ export class AiTunerService {
|
||||
|
||||
if (analysis.overallHitRate < 70) {
|
||||
recommendations.push({
|
||||
category: "cache",
|
||||
priority: "medium",
|
||||
title: "Improve Cache Hit Rate",
|
||||
description: "Cache hit rate is below optimal levels",
|
||||
impact: "medium",
|
||||
effort: "low",
|
||||
timeline: "1 week",
|
||||
category: 'cache',
|
||||
priority: 'medium',
|
||||
title: 'Improve Cache Hit Rate',
|
||||
description: 'Cache hit rate is below optimal levels',
|
||||
impact: 'medium',
|
||||
effort: 'low',
|
||||
timeline: '1 week',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -725,13 +725,13 @@ export class AiTunerService {
|
||||
|
||||
if (analysis.slowQueries > 5) {
|
||||
recommendations.push({
|
||||
category: "query",
|
||||
priority: "high",
|
||||
title: "Optimize Slow Queries",
|
||||
category: 'query',
|
||||
priority: 'high',
|
||||
title: 'Optimize Slow Queries',
|
||||
description: `${analysis.slowQueries} slow queries detected`,
|
||||
impact: "high",
|
||||
effort: "medium",
|
||||
timeline: "1-2 weeks",
|
||||
impact: 'high',
|
||||
effort: 'medium',
|
||||
timeline: '1-2 weeks',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -748,13 +748,13 @@ export class AiTunerService {
|
||||
|
||||
if (status.snapshot.memory.usage > 85) {
|
||||
recommendations.push({
|
||||
category: "resource",
|
||||
priority: "high",
|
||||
title: "Optimize Memory Usage",
|
||||
description: "Memory usage is critically high",
|
||||
impact: "high",
|
||||
effort: "medium",
|
||||
timeline: "1 week",
|
||||
category: 'resource',
|
||||
priority: 'high',
|
||||
title: 'Optimize Memory Usage',
|
||||
description: 'Memory usage is critically high',
|
||||
impact: 'high',
|
||||
effort: 'medium',
|
||||
timeline: '1 week',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -779,7 +779,7 @@ export class AiTunerService {
|
||||
component: optimization,
|
||||
expectedImprovement: impactMap[optimization] || 5,
|
||||
confidence: 0.8,
|
||||
timeToRealize: "1-2 weeks",
|
||||
timeToRealize: '1-2 weeks',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -814,10 +814,10 @@ export class AiTunerService {
|
||||
0,
|
||||
);
|
||||
|
||||
if (totalDays <= 7) return "1 week";
|
||||
if (totalDays <= 14) return "2 weeks";
|
||||
if (totalDays <= 30) return "1 month";
|
||||
return "1+ months";
|
||||
if (totalDays <= 7) return '1 week';
|
||||
if (totalDays <= 14) return '2 weeks';
|
||||
if (totalDays <= 30) return '1 month';
|
||||
return '1+ months';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -826,19 +826,19 @@ export class AiTunerService {
|
||||
private identifyOptimizationRisks(optimizations: string[]): string[] {
|
||||
const risks: string[] = [];
|
||||
|
||||
if (optimizations.includes("query")) {
|
||||
risks.push("Database schema changes may require downtime");
|
||||
if (optimizations.includes('query')) {
|
||||
risks.push('Database schema changes may require downtime');
|
||||
}
|
||||
|
||||
if (optimizations.includes("cache")) {
|
||||
if (optimizations.includes('cache')) {
|
||||
risks.push(
|
||||
"Cache configuration changes may temporarily impact performance",
|
||||
'Cache configuration changes may temporarily impact performance',
|
||||
);
|
||||
}
|
||||
|
||||
if (optimizations.length > 3) {
|
||||
risks.push(
|
||||
"Multiple simultaneous optimizations may have unexpected interactions",
|
||||
'Multiple simultaneous optimizations may have unexpected interactions',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -850,7 +850,7 @@ export class AiTunerService {
|
||||
*/
|
||||
private extractImprovementFromDetails(details: any): number {
|
||||
// 简化的提取逻辑,实际应该根据具体的details结构来解析
|
||||
if (details && typeof details === "object") {
|
||||
if (details && typeof details === 'object') {
|
||||
if (details.estimatedImprovement) return details.estimatedImprovement;
|
||||
if (details.overallImprovement) return details.overallImprovement;
|
||||
}
|
||||
@@ -883,7 +883,7 @@ export interface TuningSession {
|
||||
id: string;
|
||||
startTime: number;
|
||||
endTime: number | null;
|
||||
status: "running" | "completed" | "failed";
|
||||
status: 'running' | 'completed' | 'failed';
|
||||
options: TuningSessionOptions;
|
||||
baseline: PerformanceBaseline | null;
|
||||
optimizations: OptimizationResult[];
|
||||
@@ -927,10 +927,10 @@ export interface ComprehensiveTuningOptions {
|
||||
}
|
||||
|
||||
export interface OptimizationResult {
|
||||
type: "cache" | "query" | "resource" | "application";
|
||||
type: 'cache' | 'query' | 'resource' | 'application';
|
||||
optimizationId: string;
|
||||
success: boolean;
|
||||
impact: "low" | "medium" | "high";
|
||||
impact: 'low' | 'medium' | 'high';
|
||||
details: any;
|
||||
}
|
||||
|
||||
@@ -954,7 +954,7 @@ export interface TuningMetrics {
|
||||
export interface TuningSessionSummary {
|
||||
sessionId: string;
|
||||
duration: number;
|
||||
status: "running" | "completed" | "failed";
|
||||
status: 'running' | 'completed' | 'failed';
|
||||
metrics: TuningMetrics;
|
||||
keyAchievements: string[];
|
||||
lessonsLearned: string[];
|
||||
@@ -963,7 +963,7 @@ export interface TuningSessionSummary {
|
||||
|
||||
export interface TuningStatus {
|
||||
sessionId: string;
|
||||
status: "running" | "completed" | "failed";
|
||||
status: 'running' | 'completed' | 'failed';
|
||||
duration: number;
|
||||
progress: number; // 0-100
|
||||
currentPhase: string;
|
||||
@@ -972,19 +972,19 @@ export interface TuningStatus {
|
||||
}
|
||||
|
||||
export type TuningScope =
|
||||
| "all"
|
||||
| "performance"
|
||||
| "cache"
|
||||
| "query"
|
||||
| "resource";
|
||||
| 'all'
|
||||
| 'performance'
|
||||
| 'cache'
|
||||
| 'query'
|
||||
| 'resource';
|
||||
|
||||
export interface TuningRecommendation {
|
||||
category: "performance" | "cache" | "query" | "resource" | "optimization";
|
||||
priority: "low" | "medium" | "high";
|
||||
category: 'performance' | 'cache' | 'query' | 'resource' | 'optimization';
|
||||
priority: 'low' | 'medium' | 'high';
|
||||
title: string;
|
||||
description: string;
|
||||
impact: "low" | "medium" | "high";
|
||||
effort: "low" | "medium" | "high";
|
||||
impact: 'low' | 'medium' | 'high';
|
||||
effort: 'low' | 'medium' | 'high';
|
||||
timeline: string;
|
||||
}
|
||||
|
||||
@@ -992,7 +992,7 @@ export interface NextStep {
|
||||
phase: number;
|
||||
description: string;
|
||||
duration: string;
|
||||
priority: "low" | "medium" | "high";
|
||||
priority: 'low' | 'medium' | 'high';
|
||||
}
|
||||
|
||||
export interface TuningImpactPrediction {
|
||||
@@ -1013,7 +1013,7 @@ export interface ComponentImpactPrediction {
|
||||
export interface ResourceOptimizationResult {
|
||||
id: string;
|
||||
success: boolean;
|
||||
impact: "low" | "medium" | "high";
|
||||
impact: 'low' | 'medium' | 'high';
|
||||
optimizations: string[];
|
||||
estimatedImprovement: number;
|
||||
}
|
||||
@@ -1021,7 +1021,7 @@ export interface ResourceOptimizationResult {
|
||||
export interface ApplicationOptimizationResult {
|
||||
id: string;
|
||||
success: boolean;
|
||||
impact: "low" | "medium" | "high";
|
||||
impact: 'low' | 'medium' | 'high';
|
||||
optimizations: string[];
|
||||
estimatedImprovement: number;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Injectable, Logger, OnModuleInit } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { EventBus } from "@wwjCommon/events/event-bus";
|
||||
import { PerformanceAnalyzer } from "../analyzers/performance.analyzer";
|
||||
import { ResourceMonitor } from "../monitors/resource.monitor";
|
||||
import { CacheOptimizer } from "../optimizers/cache.optimizer";
|
||||
import { QueryOptimizer } from "../optimizers/query.optimizer";
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||
import { PerformanceAnalyzer } from '../analyzers/performance.analyzer';
|
||||
import { ResourceMonitor } from '../monitors/resource.monitor';
|
||||
import { CacheOptimizer } from '../optimizers/cache.optimizer';
|
||||
import { QueryOptimizer } from '../optimizers/query.optimizer';
|
||||
|
||||
@Injectable()
|
||||
export class TunerReadyService implements OnModuleInit {
|
||||
@@ -21,8 +21,8 @@ export class TunerReadyService implements OnModuleInit {
|
||||
|
||||
async onModuleInit() {
|
||||
const enabled =
|
||||
(this.config.get<string>("AI_TUNER_ENABLED") ?? "true") === "true";
|
||||
let currentState: "ready" | "unavailable" = "unavailable";
|
||||
(this.config.get<string>('AI_TUNER_ENABLED') ?? 'true') === 'true';
|
||||
let currentState: 'ready' | 'unavailable' = 'unavailable';
|
||||
|
||||
try {
|
||||
if (enabled) {
|
||||
@@ -34,18 +34,18 @@ export class TunerReadyService implements OnModuleInit {
|
||||
this.queryOptimizer,
|
||||
].every((c) => !!c);
|
||||
|
||||
currentState = componentsOk ? "ready" : "unavailable";
|
||||
currentState = componentsOk ? 'ready' : 'unavailable';
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.warn(
|
||||
`AI Tuner readiness check failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
currentState = "unavailable";
|
||||
currentState = 'unavailable';
|
||||
}
|
||||
|
||||
this.eventBus.emit("module.state.changed", {
|
||||
module: "ai.tuner",
|
||||
previousState: "initializing",
|
||||
this.eventBus.emit('module.state.changed', {
|
||||
module: 'ai.tuner',
|
||||
previousState: 'initializing',
|
||||
currentState,
|
||||
meta: { enabled },
|
||||
});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { PerformanceAnalyzer } from "./analyzers/performance.analyzer";
|
||||
import { ResourceMonitor } from "./monitors/resource.monitor";
|
||||
import { CacheOptimizer } from "./optimizers/cache.optimizer";
|
||||
import { QueryOptimizer } from "./optimizers/query.optimizer";
|
||||
import { AiTunerService } from "./services/ai-tuner.service";
|
||||
import { AiMetricsService } from "./services/ai-metrics.service";
|
||||
import { TunerReadyService } from "./services/tuner-ready.service";
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PerformanceAnalyzer } from './analyzers/performance.analyzer';
|
||||
import { ResourceMonitor } from './monitors/resource.monitor';
|
||||
import { CacheOptimizer } from './optimizers/cache.optimizer';
|
||||
import { QueryOptimizer } from './optimizers/query.optimizer';
|
||||
import { AiTunerService } from './services/ai-tuner.service';
|
||||
import { AiMetricsService } from './services/ai-metrics.service';
|
||||
import { TunerReadyService } from './services/tuner-ready.service';
|
||||
// 集成Boot层组件
|
||||
import { CacheManagerService } from "@wwjBoot";
|
||||
import { CacheManagerService } from '@wwjBoot';
|
||||
|
||||
/**
|
||||
* AI Tuner Module - AI 性能调优模块
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type Severity = "low" | "medium" | "high";
|
||||
export type Severity = 'low' | 'medium' | 'high';
|
||||
|
||||
export interface TaskFailedPayload {
|
||||
taskId: string;
|
||||
@@ -9,24 +9,24 @@ export interface TaskFailedPayload {
|
||||
}
|
||||
|
||||
export type RecoveryStrategy =
|
||||
| "retry"
|
||||
| "restart"
|
||||
| "reroute"
|
||||
| "fallback"
|
||||
| "noop";
|
||||
| 'retry'
|
||||
| 'restart'
|
||||
| 'reroute'
|
||||
| 'fallback'
|
||||
| 'noop';
|
||||
|
||||
export interface TaskRecoveryRequestedPayload {
|
||||
taskId: string;
|
||||
strategy: RecoveryStrategy;
|
||||
cause?: string;
|
||||
requestedBy?: "ai" | "manual" | "system";
|
||||
requestedBy?: 'ai' | 'manual' | 'system';
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface TaskRecoveryCompletedPayload {
|
||||
taskId: string;
|
||||
strategy: RecoveryStrategy;
|
||||
result: "success" | "failed" | "skipped";
|
||||
result: 'success' | 'failed' | 'skipped';
|
||||
durationMs: number;
|
||||
timestamp: number;
|
||||
details?: string;
|
||||
|
||||
@@ -1,11 +1,46 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { AiManagerModule } from "./manager/manager.module";
|
||||
import { AiHealingModule } from "./healing/healing.module";
|
||||
import { AiSafeModule } from "./safe/safe.module";
|
||||
import { AiTunerModule } from "./tuner/tuner.module";
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AiManagerModule } from './manager/manager.module';
|
||||
import { AiHealingModule } from './healing/healing.module';
|
||||
import { AiSafeModule } from './safe/safe.module';
|
||||
import { AiTunerModule } from './tuner/tuner.module';
|
||||
import { AiRuntimeModule } from './runtime/ai-runtime.module';
|
||||
import { AiSkillsModule } from './skills/ai-skills.module';
|
||||
import { AiMemoryModule } from './memory/ai-memory.module';
|
||||
import { AiGeneratorModule } from './generator/ai-generator.module';
|
||||
|
||||
/**
|
||||
* WWJCloud AI 根模块
|
||||
*
|
||||
* 架构设计借鉴 OpenClaw + NiuCloud Lite AI:
|
||||
* - Manager: 工作流编排 + 服务注册发现 + 跨模块协调
|
||||
* - Healing: 自愈(故障检测 → 策略决策 → 恢复执行)
|
||||
* - Safe: 安全(漏洞检测 + 访问保护 + 审计)
|
||||
* - Tuner: 性能调优(资源监控 + 缓存/查询优化)
|
||||
* - Runtime: Agent Runtime(ReAct 循环 + 循环检测 + LLM Provider)
|
||||
* - Skills: 技能系统(注册/发现/执行,借鉴 OpenClaw Skills)
|
||||
* - Memory: 记忆系统(短期会话 + 长期经验,借鉴 OpenClaw 双模记忆)
|
||||
* - Generator: 🆕 代码生成(框架级技能包,借鉴 NiuCloud Lite AI Skills 模块化开发)
|
||||
*/
|
||||
@Module({
|
||||
imports: [AiManagerModule, AiHealingModule, AiSafeModule, AiTunerModule],
|
||||
exports: [AiManagerModule, AiHealingModule, AiSafeModule, AiTunerModule],
|
||||
imports: [
|
||||
AiManagerModule,
|
||||
AiHealingModule,
|
||||
AiSafeModule,
|
||||
AiTunerModule,
|
||||
AiRuntimeModule,
|
||||
AiSkillsModule,
|
||||
AiMemoryModule,
|
||||
AiGeneratorModule,
|
||||
],
|
||||
exports: [
|
||||
AiManagerModule,
|
||||
AiHealingModule,
|
||||
AiSafeModule,
|
||||
AiTunerModule,
|
||||
AiRuntimeModule,
|
||||
AiSkillsModule,
|
||||
AiMemoryModule,
|
||||
AiGeneratorModule,
|
||||
],
|
||||
})
|
||||
export class WwjcloudAiModule {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import * as path from "path";
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 应用配置服务
|
||||
@@ -21,7 +21,7 @@ export class AppConfigService {
|
||||
* 对应 Java: GlobalConfig.tablePrefix
|
||||
*/
|
||||
get tablePrefix(): string {
|
||||
return this.configService.get<string>("TABLE_PREFIX", "nc_");
|
||||
return this.configService.get<string>('TABLE_PREFIX', 'nc_');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,7 +29,7 @@ export class AppConfigService {
|
||||
* 对应 Java: GlobalConfig.applicationName
|
||||
*/
|
||||
get applicationName(): string {
|
||||
return this.configService.get<string>("APP_NAME", "wwjcloud-admin");
|
||||
return this.configService.get<string>('APP_NAME', 'wwjcloud-admin');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,7 +37,7 @@ export class AppConfigService {
|
||||
* 对应 Java: GlobalConfig.runActive
|
||||
*/
|
||||
get runActive(): string {
|
||||
return process.env.NODE_ENV || "dev";
|
||||
return process.env.NODE_ENV || 'dev';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,7 +45,7 @@ export class AppConfigService {
|
||||
* 对应 Java: GlobalConfig.isDemo
|
||||
*/
|
||||
get isDemo(): boolean {
|
||||
return this.configService.get<string>("IS_DEMO", "false") === "true";
|
||||
return this.configService.get<string>('IS_DEMO', 'false') === 'true';
|
||||
}
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
@@ -57,7 +57,7 @@ export class AppConfigService {
|
||||
* 对应 Java: GlobalConfig.defaultLanguage
|
||||
*/
|
||||
get defaultLanguage(): string {
|
||||
return this.configService.get<string>("DEFAULT_LANGUAGE", "zh_CN");
|
||||
return this.configService.get<string>('DEFAULT_LANGUAGE', 'zh_CN');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,7 +65,7 @@ export class AppConfigService {
|
||||
* 对应 Java: GlobalConfig.version
|
||||
*/
|
||||
get version(): string {
|
||||
return "1.0.1";
|
||||
return '1.0.1';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,7 +73,7 @@ export class AppConfigService {
|
||||
* 对应 Java: GlobalConfig.appKey
|
||||
*/
|
||||
get appKey(): string {
|
||||
return "wwjcloud-admin";
|
||||
return 'wwjcloud-admin';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,7 +81,7 @@ export class AppConfigService {
|
||||
* 对应 Java: GlobalConfig.adminDomain
|
||||
*/
|
||||
get adminDomain(): string {
|
||||
return this.configService.get<string>("ADMIN_DOMAIN", "");
|
||||
return this.configService.get<string>('ADMIN_DOMAIN', '');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,7 +89,7 @@ export class AppConfigService {
|
||||
* 对应 Java: GlobalConfig.wapDomain
|
||||
*/
|
||||
get wapDomain(): string {
|
||||
return this.configService.get<string>("WAP_DOMAIN", "");
|
||||
return this.configService.get<string>('WAP_DOMAIN', '');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,7 +97,7 @@ export class AppConfigService {
|
||||
* 对应 Java: GlobalConfig.webDomain
|
||||
*/
|
||||
get webDomain(): string {
|
||||
return this.configService.get<string>("WEB_DOMAIN", "");
|
||||
return this.configService.get<string>('WEB_DOMAIN', '');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,7 +105,7 @@ export class AppConfigService {
|
||||
* 对应 Java: GlobalConfig.defaultAccessPath
|
||||
*/
|
||||
get defaultAccessPath(): string {
|
||||
return this.configService.get<string>("DEFAULT_ACCESS_PATH", "");
|
||||
return this.configService.get<string>('DEFAULT_ACCESS_PATH', '');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,7 +141,7 @@ export class AppConfigService {
|
||||
* 对应 Java: WebAppEnvs.get().projectwwjcloudAddon
|
||||
*/
|
||||
get projectNiucloudAddon(): string {
|
||||
return path.join(this.projectRoot, "wwjcloud-addon/");
|
||||
return path.join(this.projectRoot, 'wwjcloud-addon/');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -149,7 +149,7 @@ export class AppConfigService {
|
||||
* 对应 Java: WebAppEnvs.get().webRoot
|
||||
*/
|
||||
get webRoot(): string {
|
||||
return path.join(this.projectRoot, "webroot/");
|
||||
return path.join(this.projectRoot, 'webroot/');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,7 +157,7 @@ export class AppConfigService {
|
||||
* 对应 Java: WebAppEnvs.get().webRootDownAddon
|
||||
*/
|
||||
get webRootDownAddon(): string {
|
||||
return path.join(this.projectRoot, "webroot/addon/");
|
||||
return path.join(this.projectRoot, 'webroot/addon/');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,7 +165,7 @@ export class AppConfigService {
|
||||
* 对应 Java: WebAppEnvs.get().webRootDownJar
|
||||
*/
|
||||
get webRootDownJar(): string {
|
||||
return path.join(this.projectRoot, "webroot/jar/");
|
||||
return path.join(this.projectRoot, 'webroot/jar/');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,7 +173,7 @@ export class AppConfigService {
|
||||
* 对应 Java: WebAppEnvs.get().webRootDownPublic
|
||||
*/
|
||||
get webRootDownPublic(): string {
|
||||
return path.join(this.projectRoot, "webroot/public/");
|
||||
return path.join(this.projectRoot, 'webroot/public/');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,7 +181,7 @@ export class AppConfigService {
|
||||
* 对应 Java: WebAppEnvs.get().webRootDownResource
|
||||
*/
|
||||
get webRootDownResource(): string {
|
||||
return path.join(this.projectRoot, "webroot/resource/");
|
||||
return path.join(this.projectRoot, 'webroot/resource/');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,6 +189,6 @@ export class AppConfigService {
|
||||
* 对应 Java: WebAppEnvs.get().webRootDownRuntime
|
||||
*/
|
||||
get webRootDownRuntime(): string {
|
||||
return path.join(this.projectRoot, "webroot/runtime/");
|
||||
return path.join(this.projectRoot, 'webroot/runtime/');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,43 @@
|
||||
import { DynamicModule, Module, ValidationPipe } from "@nestjs/common";
|
||||
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from "@nestjs/core";
|
||||
import { BootModule } from "../wwjcloud-boot.module";
|
||||
import { AddonModule } from "@wwjAddon/wwjcloud-addon.module";
|
||||
import { DynamicModule, Module, ValidationPipe, ExecutionContext } from '@nestjs/common';
|
||||
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { BootModule } from '../wwjcloud-boot.module';
|
||||
import { AddonModule } from '@wwjAddon/wwjcloud-addon.module';
|
||||
|
||||
import { BootLangModule } from "../infra/lang/boot-lang.module";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { HttpExceptionFilter } from "@wwjCommon/http/http-exception.filter";
|
||||
import { LoggingInterceptor } from "@wwjCommon/http/logging.interceptor";
|
||||
import { MetricsInterceptor } from "@wwjCommon/metrics/metrics.interceptor";
|
||||
import { ResponseInterceptor } from "@wwjCommon/response/response.interceptor";
|
||||
import { AuthGuard } from "@wwjCommon/auth/auth.guard";
|
||||
import { RbacGuard } from "@wwjCommon/auth/rbac.guard";
|
||||
import { RateLimitGuard } from "@wwjCommon/http/rate-limit.guard";
|
||||
import { BootLangModule } from '../infra/lang/boot-lang.module';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { HttpExceptionFilter } from '@wwjCommon/http/http-exception.filter';
|
||||
import { LoggingInterceptor } from '@wwjCommon/http/logging.interceptor';
|
||||
import { MetricsInterceptor } from '@wwjCommon/metrics/metrics.interceptor';
|
||||
import { ResponseInterceptor } from '@wwjCommon/response/response.interceptor';
|
||||
import { AppSerializerInterceptor } from '@wwjCommon/serializer/serializer.interceptor';
|
||||
import { AuthGuard } from '@wwjCommon/auth/auth.guard';
|
||||
import { RbacGuard } from '@wwjCommon/auth/rbac.guard';
|
||||
import { RateLimitGuard } from '@wwjCommon/http/rate-limit.guard';
|
||||
import { RedisService } from '@wwjCommon/cache/redis.service';
|
||||
|
||||
function readBooleanEnv(key: string, fallback = false): boolean {
|
||||
const v = process.env[key];
|
||||
if (v == null) return fallback;
|
||||
return ["true", "1", "yes", "on"].includes(String(v).toLowerCase());
|
||||
return ['true', '1', 'yes', 'on'].includes(String(v).toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* 健康检查端点路径集合,限流守卫应跳过这些端点
|
||||
*/
|
||||
const HEALTH_ENDPOINTS = ['/health', '/health/quick'];
|
||||
|
||||
/**
|
||||
* 判断当前请求是否为健康检查端点,若是则跳过限流
|
||||
* @param context - NestJS 执行上下文
|
||||
* @returns 是否跳过限流
|
||||
*/
|
||||
function rateLimitSkipHealthCheck(context: ExecutionContext): boolean {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const url = (req.originalUrl || req.url || '') as string;
|
||||
return HEALTH_ENDPOINTS.some(
|
||||
(ep) => url === ep || url.startsWith(ep + '/'),
|
||||
);
|
||||
}
|
||||
|
||||
@Module({})
|
||||
@@ -29,10 +50,10 @@ export class WwjCloudPlatformPreset {
|
||||
provide: APP_PIPE,
|
||||
useFactory: (config: ConfigService) =>
|
||||
new ValidationPipe({
|
||||
transform: config.get<boolean>("VALIDATION_TRANSFORM") ?? true,
|
||||
whitelist: config.get<boolean>("VALIDATION_WHITELIST") ?? true,
|
||||
transform: config.get<boolean>('VALIDATION_TRANSFORM') ?? true,
|
||||
whitelist: config.get<boolean>('VALIDATION_WHITELIST') ?? true,
|
||||
forbidNonWhitelisted:
|
||||
config.get<boolean>("VALIDATION_FORBID_NON_WHITELISTED") ?? false,
|
||||
config.get<boolean>('VALIDATION_FORBID_NON_WHITELISTED') ?? false,
|
||||
forbidUnknownValues: false,
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
@@ -40,45 +61,51 @@ export class WwjCloudPlatformPreset {
|
||||
{ provide: APP_FILTER, useClass: HttpExceptionFilter },
|
||||
{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
|
||||
{ provide: APP_INTERCEPTOR, useClass: MetricsInterceptor },
|
||||
{ provide: APP_INTERCEPTOR, useClass: AppSerializerInterceptor },
|
||||
{ provide: APP_INTERCEPTOR, useClass: ResponseInterceptor },
|
||||
{ provide: APP_GUARD, useClass: AuthGuard },
|
||||
{ provide: APP_GUARD, useClass: RbacGuard },
|
||||
];
|
||||
|
||||
if (readBooleanEnv("RATE_LIMIT_ENABLED", false)) {
|
||||
providers.push({ provide: APP_GUARD, useClass: RateLimitGuard });
|
||||
if (readBooleanEnv('RATE_LIMIT_ENABLED', false)) {
|
||||
providers.push({
|
||||
provide: APP_GUARD,
|
||||
useFactory: (config: ConfigService, redis: RedisService, reflector: Reflector) =>
|
||||
new RateLimitGuard(config, redis, reflector, rateLimitSkipHealthCheck),
|
||||
inject: [ConfigService, RedisService, Reflector],
|
||||
});
|
||||
}
|
||||
|
||||
if (readBooleanEnv("AI_ENABLED", false)) {
|
||||
if (readBooleanEnv('AI_ENABLED', false)) {
|
||||
try {
|
||||
let WwjcloudAiModule: any = null;
|
||||
try {
|
||||
// 首先尝试路径别名
|
||||
const aiModule = require("@wwjAi/wwjcloud-ai.module");
|
||||
const aiModule = require('@wwjAi/wwjcloud-ai.module');
|
||||
WwjcloudAiModule = aiModule.WwjcloudAiModule;
|
||||
} catch (err1) {
|
||||
try {
|
||||
// 尝试直接路径别名
|
||||
const aiModule = require("@wwjAi");
|
||||
const aiModule = require('@wwjAi');
|
||||
WwjcloudAiModule = aiModule.WwjcloudAiModule;
|
||||
} catch (err2) {
|
||||
try {
|
||||
// 尝试运行时绝对路径
|
||||
const path = require("path");
|
||||
const path = require('path');
|
||||
const aiModulePath = path.join(
|
||||
process.cwd(),
|
||||
"dist",
|
||||
"libs",
|
||||
"wwjcloud-ai",
|
||||
"src",
|
||||
"wwjcloud-ai.module",
|
||||
'dist',
|
||||
'libs',
|
||||
'wwjcloud-ai',
|
||||
'src',
|
||||
'wwjcloud-ai.module',
|
||||
);
|
||||
const aiModule = require(aiModulePath);
|
||||
WwjcloudAiModule = aiModule.WwjcloudAiModule;
|
||||
} catch (err3) {
|
||||
try {
|
||||
// 尝试相对路径备用
|
||||
const aiModule = require("./dist/libs/wwjcloud-ai/src/wwjcloud-ai.module");
|
||||
const aiModule = require('./dist/libs/wwjcloud-ai/src/wwjcloud-ai.module');
|
||||
WwjcloudAiModule = aiModule.WwjcloudAiModule;
|
||||
} catch (err4) {
|
||||
// AI模块不可用,继续运行
|
||||
@@ -91,7 +118,7 @@ export class WwjCloudPlatformPreset {
|
||||
exportsArr.push(WwjcloudAiModule);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("[Preset] AI module loading failed:", err?.message ?? err);
|
||||
console.warn('[Preset] AI module loading failed:', err?.message ?? err);
|
||||
}
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as Joi from "joi";
|
||||
import * as Joi from 'joi';
|
||||
|
||||
// 配置中心:集中管理 Boot 层的环境变量校验
|
||||
// 严格遵循:不设置默认值(validation 层),默认值在具体实现中按需兜底
|
||||
export const validationSchema = Joi.object({
|
||||
NODE_ENV: Joi.string().valid("development", "production", "test").required(),
|
||||
NODE_ENV: Joi.string().valid('development', 'production', 'test').required(),
|
||||
GLOBAL_PREFIX: Joi.string().optional(),
|
||||
PORT: Joi.number().optional(),
|
||||
|
||||
@@ -31,7 +31,7 @@ export const validationSchema = Joi.object({
|
||||
// Tenant
|
||||
TENANT_ENABLED: Joi.boolean().optional(),
|
||||
TENANT_RESOLVE_STRATEGY: Joi.string()
|
||||
.valid("header", "subdomain", "path")
|
||||
.valid('header', 'subdomain', 'path')
|
||||
.optional(),
|
||||
TENANT_HEADER_KEY: Joi.string().optional(),
|
||||
TENANT_PATH_PREFIX: Joi.string().optional(),
|
||||
@@ -40,7 +40,7 @@ export const validationSchema = Joi.object({
|
||||
RATE_LIMIT_ENABLED: Joi.boolean().optional(),
|
||||
RATE_LIMIT_WINDOW_MS: Joi.number().optional(),
|
||||
RATE_LIMIT_MAX: Joi.number().optional(),
|
||||
RATE_LIMIT_STRATEGY: Joi.string().valid("fixed", "sliding").optional(),
|
||||
RATE_LIMIT_STRATEGY: Joi.string().valid('fixed', 'sliding').optional(),
|
||||
RATE_LIMIT_MAX_ADMIN: Joi.number().optional(),
|
||||
|
||||
// IP filter
|
||||
@@ -81,7 +81,7 @@ export const validationSchema = Joi.object({
|
||||
|
||||
// Queue
|
||||
QUEUE_ENABLED: Joi.boolean().optional(),
|
||||
QUEUE_DRIVER: Joi.string().valid("bullmq", "kafka").optional(),
|
||||
QUEUE_DRIVER: Joi.string().valid('bullmq', 'kafka').optional(),
|
||||
QUEUE_REDIS_HOST: Joi.string().optional(),
|
||||
QUEUE_REDIS_PORT: Joi.number().optional(),
|
||||
QUEUE_REDIS_PASSWORD: Joi.string().optional(),
|
||||
|
||||
@@ -1,46 +1,55 @@
|
||||
export * from "./wwjcloud-boot.module";
|
||||
export * from "./config/preset";
|
||||
export * from "./infra/http/boot-http";
|
||||
export * from "./infra/resilience/http-client.service";
|
||||
export * from "./infra/metrics/metrics.service";
|
||||
export * from "./infra/cache/cache.service";
|
||||
export * from "./infra/cache/lock.service";
|
||||
export * from "./infra/cache/cache-manager.service";
|
||||
export * from "./infra/queue/queue.service";
|
||||
export * from "./infra/http/request-context.service";
|
||||
export * from "./infra/http/rate-limit.guard";
|
||||
export * from "./infra/context/thread-local-holder";
|
||||
export * from "./infra/metrics/tokens";
|
||||
export * from "./infra/cache/tokens";
|
||||
export * from "./infra/events/callback-publisher.service";
|
||||
export * from './wwjcloud-boot.module';
|
||||
export * from './config/preset';
|
||||
export * from './infra/http/boot-http';
|
||||
export * from './infra/resilience/http-client.service';
|
||||
export * from './infra/metrics/metrics.service';
|
||||
export * from './infra/cache/cache.service';
|
||||
export * from './infra/cache/lock.service';
|
||||
export * from './infra/cache/cache-manager.service';
|
||||
export * from './infra/queue/queue.service';
|
||||
export * from './infra/http/request-context.service';
|
||||
export * from './infra/http/rate-limit.guard';
|
||||
export * from './infra/http/throttle.decorator';
|
||||
export * from './infra/context/thread-local-holder';
|
||||
export * from './infra/metrics/tokens';
|
||||
export * from './infra/cache/tokens';
|
||||
export * from './infra/events/callback-publisher.service';
|
||||
|
||||
// websocket exports
|
||||
export * from './infra/websocket/websocket.module';
|
||||
export * from './infra/websocket/notification.gateway';
|
||||
export * from './infra/websocket/ws-event-emitter';
|
||||
|
||||
// serializer exports
|
||||
export * from './infra/serializer';
|
||||
|
||||
// vendor exports
|
||||
export * from "./vendor/vendor.module";
|
||||
export * from "./vendor/pay";
|
||||
export * from "./vendor/sms";
|
||||
export * from "./vendor/notice";
|
||||
export * from "./vendor/upload";
|
||||
export * from "./vendor/provider-factories/upload-provider.factory";
|
||||
export * from "./vendor/provider-factories/pay-provider.factory";
|
||||
export * from "./vendor/provider-factories/sms-provider.factory";
|
||||
export * from "./vendor/provider-factories/job-provider.factory";
|
||||
export * from "./vendor/provider-factories/handler-provider.factory";
|
||||
export * from "./vendor/provider-factories/loader-provider.factory";
|
||||
export * from "./vendor/provider-factories/upgrade-provider.factory";
|
||||
export * from "./vendor/mappers/mapper-registry.service";
|
||||
export * from "./vendor/utils";
|
||||
export * from './vendor/vendor.module';
|
||||
export * from './vendor/pay';
|
||||
export * from './vendor/sms';
|
||||
export * from './vendor/notice';
|
||||
export * from './vendor/upload';
|
||||
export * from './vendor/provider-factories/upload-provider.factory';
|
||||
export * from './vendor/provider-factories/pay-provider.factory';
|
||||
export * from './vendor/provider-factories/sms-provider.factory';
|
||||
export * from './vendor/provider-factories/job-provider.factory';
|
||||
export * from './vendor/provider-factories/handler-provider.factory';
|
||||
export * from './vendor/provider-factories/loader-provider.factory';
|
||||
export * from './vendor/provider-factories/upgrade-provider.factory';
|
||||
export * from './vendor/mappers/mapper-registry.service';
|
||||
export * from './vendor/utils';
|
||||
|
||||
// infra exports
|
||||
export * from "./infra/auth/boot-auth.module";
|
||||
export * from "./infra/auth/auth.service";
|
||||
export * from "./infra/auth/auth.guard";
|
||||
export * from "./infra/auth/rbac.guard";
|
||||
export * from "./infra/auth/decorators";
|
||||
export * from "./infra/tenant/boot-tenant.module";
|
||||
export * from "./infra/startup/initialize-provider.service";
|
||||
export * from "./infra/queue/job-scheduler.service";
|
||||
export * from "./infra/events/event-listener.service";
|
||||
export * from "./infra/events/event-bus";
|
||||
export * from "./infra/events/callback-publisher.service";
|
||||
export { ConfigService } from "@nestjs/config";
|
||||
export { AppConfigService } from "./config/app-config.service";
|
||||
export * from './infra/auth/boot-auth.module';
|
||||
export * from './infra/auth/auth.service';
|
||||
export * from './infra/auth/auth.guard';
|
||||
export * from './infra/auth/rbac.guard';
|
||||
export * from './infra/auth/decorators';
|
||||
export * from './infra/tenant/boot-tenant.module';
|
||||
export * from './infra/startup/initialize-provider.service';
|
||||
export * from './infra/queue/job-scheduler.service';
|
||||
export * from './infra/events/event-listener.service';
|
||||
export * from './infra/events/event-bus';
|
||||
export * from './infra/events/callback-publisher.service';
|
||||
export { ConfigService } from '@nestjs/config';
|
||||
export { AppConfigService } from './config/app-config.service';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable, Logger, OnModuleInit } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { EventBus } from "@wwjCommon/events/event-bus";
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||
|
||||
function readBoolean(
|
||||
config: ConfigService,
|
||||
@@ -8,9 +8,9 @@ function readBoolean(
|
||||
fallback = false,
|
||||
): boolean {
|
||||
const v = config.get<string | boolean>(key);
|
||||
if (typeof v === "boolean") return v;
|
||||
if (typeof v === "string")
|
||||
return ["true", "1", "yes", "on"].includes(v.toLowerCase());
|
||||
if (typeof v === 'boolean') return v;
|
||||
if (typeof v === 'string')
|
||||
return ['true', '1', 'yes', 'on'].includes(v.toLowerCase());
|
||||
return fallback;
|
||||
}
|
||||
|
||||
@@ -24,31 +24,31 @@ export class AuthReadyService implements OnModuleInit {
|
||||
) {}
|
||||
|
||||
async onModuleInit(): Promise<void> {
|
||||
const authEnabled = readBoolean(this.config, "AUTH_ENABLED", false);
|
||||
const rbacEnabled = readBoolean(this.config, "RBAC_ENABLED", false);
|
||||
const authEnabled = readBoolean(this.config, 'AUTH_ENABLED', false);
|
||||
const rbacEnabled = readBoolean(this.config, 'RBAC_ENABLED', false);
|
||||
|
||||
const authState: "ready" | "unavailable" = authEnabled
|
||||
? "ready"
|
||||
: "unavailable";
|
||||
const rbacState: "ready" | "unavailable" = rbacEnabled
|
||||
? "ready"
|
||||
: "unavailable";
|
||||
const authState: 'ready' | 'unavailable' = authEnabled
|
||||
? 'ready'
|
||||
: 'unavailable';
|
||||
const rbacState: 'ready' | 'unavailable' = rbacEnabled
|
||||
? 'ready'
|
||||
: 'unavailable';
|
||||
|
||||
this.logger.log(
|
||||
`Auth module init -> AUTH_ENABLED=${authEnabled}, state=${authState}`,
|
||||
);
|
||||
this.eventBus.emit("module.state.changed", {
|
||||
module: "auth",
|
||||
previousState: "initializing",
|
||||
this.eventBus.emit('module.state.changed', {
|
||||
module: 'auth',
|
||||
previousState: 'initializing',
|
||||
currentState: authState,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`RBAC module init -> RBAC_ENABLED=${rbacEnabled}, state=${rbacState}`,
|
||||
);
|
||||
this.eventBus.emit("module.state.changed", {
|
||||
module: "rbac",
|
||||
previousState: "initializing",
|
||||
this.eventBus.emit('module.state.changed', {
|
||||
module: 'rbac',
|
||||
previousState: 'initializing',
|
||||
currentState: rbacState,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ import {
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
import { AuthService, UserClaims } from "./auth.service";
|
||||
import { RequestContextService } from "../http/request-context.service";
|
||||
import { IS_PUBLIC_KEY } from "./decorators";
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { AuthService, UserClaims } from './auth.service';
|
||||
import { RequestContextService } from '../http/request-context.service';
|
||||
import { IS_PUBLIC_KEY } from './decorators';
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuard implements CanActivate {
|
||||
@@ -28,14 +28,14 @@ export class AuthGuard implements CanActivate {
|
||||
if (!this.auth.isEnabled()) {
|
||||
// 认证未启用,标记匿名角色(便于后续 RBAC 判定)
|
||||
const store = this.ctx.getContext();
|
||||
if (store && !store.roles) store.roles = ["anonymous"];
|
||||
if (store && !store.roles) store.roles = ['anonymous'];
|
||||
return true;
|
||||
}
|
||||
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const authHeader: string | undefined = req.headers["authorization"];
|
||||
if (!authHeader || !authHeader.toLowerCase().startsWith("bearer ")) {
|
||||
throw new UnauthorizedException({ msg_key: "error.auth.invalid_token" });
|
||||
const authHeader: string | undefined = req.headers['authorization'];
|
||||
if (!authHeader || !authHeader.toLowerCase().startsWith('bearer ')) {
|
||||
throw new UnauthorizedException({ msg_key: 'error.auth.invalid_token' });
|
||||
}
|
||||
const token = authHeader.slice(7).trim();
|
||||
const claims: UserClaims = this.auth.verifyToken(token);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import * as jwt from "jsonwebtoken";
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
|
||||
export interface UserClaims {
|
||||
userId?: string;
|
||||
@@ -22,7 +22,7 @@ export class AuthService {
|
||||
constructor(private readonly config: ConfigService) {}
|
||||
|
||||
isEnabled(): boolean {
|
||||
return this.readBoolean("AUTH_ENABLED", false);
|
||||
return this.readBoolean('AUTH_ENABLED', false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,54 +32,54 @@ export class AuthService {
|
||||
* @returns 生成的token字符串
|
||||
*/
|
||||
signToken(payload: Record<string, any>, options?: SignTokenOptions): string {
|
||||
const secret = this.config.get<string>("JWT_SECRET");
|
||||
const secret = this.config.get<string>('JWT_SECRET');
|
||||
if (!secret || secret.trim().length === 0) {
|
||||
throw new UnauthorizedException({ msg_key: "error.auth.invalid_secret" });
|
||||
throw new UnauthorizedException({ msg_key: 'error.auth.invalid_secret' });
|
||||
}
|
||||
|
||||
const signOptions: any = {
|
||||
issuer:
|
||||
options?.issuer || this.config.get<string>("JWT_ISSUER") || undefined,
|
||||
options?.issuer || this.config.get<string>('JWT_ISSUER') || undefined,
|
||||
audience:
|
||||
options?.audience ||
|
||||
this.config.get<string>("JWT_AUDIENCE") ||
|
||||
this.config.get<string>('JWT_AUDIENCE') ||
|
||||
undefined,
|
||||
expiresIn: options?.expiresIn || "24h",
|
||||
expiresIn: options?.expiresIn || '24h',
|
||||
};
|
||||
|
||||
return jwt.sign(payload, secret, signOptions);
|
||||
}
|
||||
|
||||
verifyToken(token: string): UserClaims {
|
||||
const secret = this.config.get<string>("JWT_SECRET");
|
||||
const secret = this.config.get<string>('JWT_SECRET');
|
||||
if (!secret || secret.trim().length === 0) {
|
||||
throw new UnauthorizedException({ msg_key: "error.auth.invalid_token" });
|
||||
throw new UnauthorizedException({ msg_key: 'error.auth.invalid_token' });
|
||||
}
|
||||
const issuer = this.config.get<string>("JWT_ISSUER");
|
||||
const audience = this.config.get<string>("JWT_AUDIENCE");
|
||||
const issuer = this.config.get<string>('JWT_ISSUER');
|
||||
const audience = this.config.get<string>('JWT_AUDIENCE');
|
||||
try {
|
||||
const payload = jwt.verify(token, secret, {
|
||||
issuer: issuer || undefined,
|
||||
audience: audience || undefined,
|
||||
});
|
||||
const obj =
|
||||
typeof payload === "string" ? JSON.parse(payload) : (payload as any);
|
||||
typeof payload === 'string' ? JSON.parse(payload) : (payload as any);
|
||||
const claims: UserClaims = {
|
||||
userId: obj.sub || obj.userId || obj.uid || undefined,
|
||||
username: obj.name || obj.username || obj.uname || undefined,
|
||||
roles: Array.isArray(obj.roles)
|
||||
? obj.roles.map((s: any) => String(s))
|
||||
: typeof obj.roles === "string"
|
||||
: typeof obj.roles === 'string'
|
||||
? String(obj.roles)
|
||||
.split(",")
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0)
|
||||
: undefined,
|
||||
permissions: Array.isArray(obj.permissions)
|
||||
? obj.permissions.map((s: any) => String(s))
|
||||
: typeof obj.permissions === "string"
|
||||
: typeof obj.permissions === 'string'
|
||||
? String(obj.permissions)
|
||||
.split(",")
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0)
|
||||
: undefined,
|
||||
@@ -87,15 +87,15 @@ export class AuthService {
|
||||
};
|
||||
return claims;
|
||||
} catch (err) {
|
||||
throw new UnauthorizedException({ msg_key: "error.auth.invalid_token" });
|
||||
throw new UnauthorizedException({ msg_key: 'error.auth.invalid_token' });
|
||||
}
|
||||
}
|
||||
|
||||
private readBoolean(key: string, fallback = false): boolean {
|
||||
const v = this.config.get<string | boolean>(key);
|
||||
if (typeof v === "boolean") return v;
|
||||
if (typeof v === "string")
|
||||
return ["true", "1", "yes", "on"].includes(v.toLowerCase());
|
||||
if (typeof v === 'boolean') return v;
|
||||
if (typeof v === 'string')
|
||||
return ['true', '1', 'yes', 'on'].includes(v.toLowerCase());
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Global, Module } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { AuthGuard } from "./auth.guard";
|
||||
import { RbacGuard } from "./rbac.guard";
|
||||
import { AuthReadyService } from "./auth-ready.service";
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthGuard } from './auth.guard';
|
||||
import { RbacGuard } from './rbac.guard';
|
||||
import { AuthReadyService } from './auth-ready.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { SetMetadata } from "@nestjs/common";
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const IS_PUBLIC_KEY = "isPublic";
|
||||
export const ROLES_KEY = "roles";
|
||||
export const PERMISSIONS_KEY = "permissions";
|
||||
export const IS_PUBLIC_KEY = 'isPublic';
|
||||
export const ROLES_KEY = 'roles';
|
||||
export const PERMISSIONS_KEY = 'permissions';
|
||||
|
||||
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
|
||||
|
||||
@@ -3,10 +3,10 @@ import {
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
ForbiddenException,
|
||||
} from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { PERMISSIONS_KEY, ROLES_KEY, IS_PUBLIC_KEY } from "./decorators";
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PERMISSIONS_KEY, ROLES_KEY, IS_PUBLIC_KEY } from './decorators';
|
||||
|
||||
@Injectable()
|
||||
export class RbacGuard implements CanActivate {
|
||||
@@ -24,7 +24,7 @@ export class RbacGuard implements CanActivate {
|
||||
if (isPublic) return true;
|
||||
|
||||
// RBAC 开关
|
||||
const enabled = this.readBoolean("RBAC_ENABLED", false);
|
||||
const enabled = this.readBoolean('RBAC_ENABLED', false);
|
||||
if (!enabled) return true;
|
||||
|
||||
const requiredRoles =
|
||||
@@ -50,7 +50,7 @@ export class RbacGuard implements CanActivate {
|
||||
const ok = requiredRoles.some((r) => userRoles.includes(r));
|
||||
if (!ok)
|
||||
throw new ForbiddenException({
|
||||
msg_key: "error.auth.insufficient_role",
|
||||
msg_key: 'error.auth.insufficient_role',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ export class RbacGuard implements CanActivate {
|
||||
const ok = requiredPermissions.every((p) => userPerms.includes(p));
|
||||
if (!ok)
|
||||
throw new ForbiddenException({
|
||||
msg_key: "error.auth.insufficient_permission",
|
||||
msg_key: 'error.auth.insufficient_permission',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -68,9 +68,9 @@ export class RbacGuard implements CanActivate {
|
||||
|
||||
private readBoolean(key: string, fallback = false): boolean {
|
||||
const v = this.config.get<string | boolean>(key);
|
||||
if (typeof v === "boolean") return v;
|
||||
if (typeof v === "string")
|
||||
return ["true", "1", "yes", "on"].includes(v.toLowerCase());
|
||||
if (typeof v === 'boolean') return v;
|
||||
if (typeof v === 'string')
|
||||
return ['true', '1', 'yes', 'on'].includes(v.toLowerCase());
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Global, Module } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { RedisService } from "./redis.service";
|
||||
import { CacheService } from "./cache.service";
|
||||
import { LockService } from "./lock.service";
|
||||
import { CacheController } from "./cache.controller";
|
||||
import { CACHE_SERVICE, LOCK_SERVICE } from "./tokens";
|
||||
import { CacheReadyService } from "./cache-ready.service";
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { RedisService } from './redis.service';
|
||||
import { CacheService } from './cache.service';
|
||||
import { LockService } from './lock.service';
|
||||
import { CacheController } from './cache.controller';
|
||||
import { CACHE_SERVICE, LOCK_SERVICE } from './tokens';
|
||||
import { CacheReadyService } from './cache-ready.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { CacheService } from "./cache.service";
|
||||
import { RedisService } from "./redis.service";
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { CacheService } from './cache.service';
|
||||
import { RedisService } from './redis.service';
|
||||
|
||||
export interface CacheTag {
|
||||
key: string;
|
||||
@@ -75,7 +75,7 @@ export class CacheManagerService {
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Invalidating cache by tag: ${tag}, keys: ${Array.from(keys).join(", ")}`,
|
||||
`Invalidating cache by tag: ${tag}, keys: ${Array.from(keys).join(', ')}`,
|
||||
);
|
||||
|
||||
// 删除所有相关缓存
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable, Logger, OnModuleInit } from "@nestjs/common";
|
||||
import { EventBus } from "@wwjCommon/events/event-bus";
|
||||
import { RedisService } from "./redis.service";
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { EventBus } from '@wwjCommon/events/event-bus';
|
||||
import { RedisService } from './redis.service';
|
||||
|
||||
@Injectable()
|
||||
export class CacheReadyService implements OnModuleInit {
|
||||
@@ -12,26 +12,26 @@ export class CacheReadyService implements OnModuleInit {
|
||||
) {}
|
||||
|
||||
async onModuleInit(): Promise<void> {
|
||||
let state: "ready" | "unavailable" = "ready";
|
||||
let state: 'ready' | 'unavailable' = 'ready';
|
||||
|
||||
try {
|
||||
if (!this.redis.isEnabled()) {
|
||||
// 使用内存回退,视为可用
|
||||
state = "ready";
|
||||
state = 'ready';
|
||||
} else {
|
||||
const client = this.redis.getClient();
|
||||
await client.ping();
|
||||
state = "ready";
|
||||
state = 'ready';
|
||||
}
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`Cache readiness check failed: ${err?.message || err}`);
|
||||
state = "unavailable";
|
||||
state = 'unavailable';
|
||||
}
|
||||
|
||||
this.logger.log(`Cache module init -> state=${state}`);
|
||||
this.eventBus.emit("module.state.changed", {
|
||||
module: "cache",
|
||||
previousState: "initializing",
|
||||
this.eventBus.emit('module.state.changed', {
|
||||
module: 'cache',
|
||||
previousState: 'initializing',
|
||||
currentState: state,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
import { Controller, Get, Query } from "@nestjs/common";
|
||||
import { CacheService } from "./cache.service";
|
||||
import { RedisService } from "./redis.service";
|
||||
import { Controller, Get, Query } from '@nestjs/common';
|
||||
import { CacheService } from './cache.service';
|
||||
import { RedisService } from './redis.service';
|
||||
|
||||
@Controller("cache")
|
||||
@Controller('cache')
|
||||
export class CacheController {
|
||||
constructor(
|
||||
private readonly cache: CacheService,
|
||||
private readonly redis: RedisService,
|
||||
) {}
|
||||
|
||||
@Get("ping")
|
||||
@Get('ping')
|
||||
ping() {
|
||||
return { redisEnabled: this.redis.isEnabled() };
|
||||
}
|
||||
|
||||
@Get("set")
|
||||
@Get('set')
|
||||
async set(
|
||||
@Query("key") key: string,
|
||||
@Query("value") value: string,
|
||||
@Query("ttlSeconds") ttlSeconds?: string,
|
||||
@Query('key') key: string,
|
||||
@Query('value') value: string,
|
||||
@Query('ttlSeconds') ttlSeconds?: string,
|
||||
) {
|
||||
const ttl = ttlSeconds ? parseInt(ttlSeconds, 10) : undefined;
|
||||
await this.cache.set(key, value, ttl);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@Get("get")
|
||||
async get(@Query("key") key: string) {
|
||||
@Get('get')
|
||||
async get(@Query('key') key: string) {
|
||||
const value = await this.cache.get(key);
|
||||
return { value };
|
||||
}
|
||||
|
||||
@Get("del")
|
||||
async del(@Query("key") key: string) {
|
||||
@Get('del')
|
||||
async del(@Query('key') key: string) {
|
||||
await this.cache.del(key);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { RedisService } from "./redis.service";
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { RedisService } from './redis.service';
|
||||
|
||||
interface MemoryEntry {
|
||||
payload: string;
|
||||
@@ -44,7 +44,7 @@ export class CacheService {
|
||||
|
||||
const client = this.redis.getClient();
|
||||
if (ttlSeconds && ttlSeconds > 0) {
|
||||
await client.set(key, payload, "EX", ttlSeconds);
|
||||
await client.set(key, payload, 'EX', ttlSeconds);
|
||||
} else {
|
||||
await client.set(key, payload);
|
||||
}
|
||||
@@ -65,19 +65,19 @@ export class CacheService {
|
||||
return;
|
||||
}
|
||||
const client = this.redis.getClient();
|
||||
let cursor = "0";
|
||||
let cursor = '0';
|
||||
do {
|
||||
const res = await client.scan(cursor, "MATCH", "*", "COUNT", 1000);
|
||||
const res = await client.scan(cursor, 'MATCH', '*', 'COUNT', 1000);
|
||||
cursor = res[0];
|
||||
const keys: string[] = res[1] as unknown as string[];
|
||||
if (keys && keys.length > 0) {
|
||||
await client.del(...keys);
|
||||
}
|
||||
} while (cursor !== "0");
|
||||
} while (cursor !== '0');
|
||||
}
|
||||
|
||||
private serialize(value: any): string {
|
||||
return typeof value === "string" ? value : JSON.stringify(value);
|
||||
return typeof value === 'string' ? value : JSON.stringify(value);
|
||||
}
|
||||
|
||||
private deserialize<T = any>(val: string): T {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { randomUUID } from "crypto";
|
||||
import { RedisService } from "./redis.service";
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { RedisService } from './redis.service';
|
||||
|
||||
interface MemLockEntry {
|
||||
token: string;
|
||||
@@ -29,8 +29,8 @@ export class LockService {
|
||||
return lockToken;
|
||||
}
|
||||
const client = this.redis.getClient();
|
||||
const res = await client.set(key, lockToken, "PX", ttlMs, "NX");
|
||||
return res === "OK" ? lockToken : null;
|
||||
const res = await client.set(key, lockToken, 'PX', ttlMs, 'NX');
|
||||
return res === 'OK' ? lockToken : null;
|
||||
}
|
||||
|
||||
async release(key: string, token: string): Promise<boolean> {
|
||||
|
||||
@@ -3,9 +3,10 @@ import {
|
||||
OnModuleInit,
|
||||
OnModuleDestroy,
|
||||
Logger,
|
||||
} from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import Redis from "ioredis";
|
||||
InternalServerErrorException,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import Redis from 'ioredis';
|
||||
|
||||
@Injectable()
|
||||
export class RedisService implements OnModuleInit, OnModuleDestroy {
|
||||
@@ -16,19 +17,19 @@ export class RedisService implements OnModuleInit, OnModuleDestroy {
|
||||
constructor(private readonly config: ConfigService) {}
|
||||
|
||||
async onModuleInit() {
|
||||
this.enabled = this.readBoolean("REDIS_ENABLED");
|
||||
this.enabled = this.readBoolean('REDIS_ENABLED');
|
||||
if (!this.enabled) {
|
||||
this.logger.log("Redis disabled by environment");
|
||||
this.logger.log('Redis disabled by environment');
|
||||
return;
|
||||
}
|
||||
const host = this.config.get<string>("REDIS_HOST");
|
||||
const port = this.readNumber("REDIS_PORT", 6379);
|
||||
const password = this.config.get<string>("REDIS_PASSWORD");
|
||||
const namespace = this.config.get<string>("REDIS_NAMESPACE") || "wwjcloud";
|
||||
const host = this.config.get<string>('REDIS_HOST');
|
||||
const port = this.readNumber('REDIS_PORT', 6379);
|
||||
const password = this.config.get<string>('REDIS_PASSWORD');
|
||||
const namespace = this.config.get<string>('REDIS_NAMESPACE') || 'wwjcloud';
|
||||
|
||||
if (!host) {
|
||||
this.logger.error("REDIS_HOST is not set while REDIS_ENABLED=true");
|
||||
throw new Error("REDIS_HOST not configured");
|
||||
this.logger.error('REDIS_HOST is not set while REDIS_ENABLED=true');
|
||||
throw new InternalServerErrorException('REDIS_HOST not configured');
|
||||
}
|
||||
|
||||
this.client = new Redis({
|
||||
@@ -37,10 +38,10 @@ export class RedisService implements OnModuleInit, OnModuleDestroy {
|
||||
password,
|
||||
keyPrefix: `${namespace}:`,
|
||||
});
|
||||
this.client.on("connect", () =>
|
||||
this.client.on('connect', () =>
|
||||
this.logger.log(`Redis connected: ${host}:${port}`),
|
||||
);
|
||||
this.client.on("error", (err) =>
|
||||
this.client.on('error', (err) =>
|
||||
this.logger.error(`Redis error: ${err?.message || err}`),
|
||||
);
|
||||
}
|
||||
@@ -58,15 +59,17 @@ export class RedisService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
getClient(): Redis {
|
||||
if (!this.enabled || !this.client) {
|
||||
throw new Error("Redis is not enabled or not connected");
|
||||
throw new InternalServerErrorException(
|
||||
'Redis is not enabled or not connected',
|
||||
);
|
||||
}
|
||||
return this.client;
|
||||
}
|
||||
|
||||
private readNumber(key: string, fallback: number): number {
|
||||
const v = this.config.get<string | number>(key);
|
||||
if (typeof v === "number") return v;
|
||||
if (typeof v === "string") {
|
||||
if (typeof v === 'number') return v;
|
||||
if (typeof v === 'string') {
|
||||
const parsed = parseInt(v, 10);
|
||||
if (!Number.isNaN(parsed)) return parsed;
|
||||
}
|
||||
@@ -75,9 +78,9 @@ export class RedisService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
private readBoolean(key: string): boolean {
|
||||
const v = this.config.get<string | boolean>(key);
|
||||
if (typeof v === "boolean") return v;
|
||||
if (typeof v === "string") {
|
||||
return ["true", "1", "yes", "on"].includes(v.toLowerCase());
|
||||
if (typeof v === 'boolean') return v;
|
||||
if (typeof v === 'string') {
|
||||
return ['true', '1', 'yes', 'on'].includes(v.toLowerCase());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export const CACHE_SERVICE = "CACHE_SERVICE";
|
||||
export const LOCK_SERVICE = "LOCK_SERVICE";
|
||||
export const CACHE_SERVICE = 'CACHE_SERVICE';
|
||||
export const LOCK_SERVICE = 'LOCK_SERVICE';
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
*
|
||||
* 注意:Node.js使用AsyncLocalStorage实现ThreadLocal功能
|
||||
*/
|
||||
import { AsyncLocalStorage } from "async_hooks";
|
||||
import { AsyncLocalStorage } from 'async_hooks';
|
||||
import { InternalServerErrorException } from '@nestjs/common';
|
||||
|
||||
interface ThreadLocalStore {
|
||||
[key: string]: any;
|
||||
@@ -30,8 +31,8 @@ class ThreadLocalHolderImpl {
|
||||
static put(key: string, value: any): void {
|
||||
const store = this.storage.getStore();
|
||||
if (!store) {
|
||||
throw new Error(
|
||||
"ThreadLocal context not initialized. Use runWith() first.",
|
||||
throw new InternalServerErrorException(
|
||||
'ThreadLocal context not initialized. Use runWith() first.',
|
||||
);
|
||||
}
|
||||
store[key] = value;
|
||||
@@ -129,11 +130,11 @@ class ThreadLocalHolderImpl {
|
||||
const result = this.get(key);
|
||||
try {
|
||||
if (result === null || result === undefined) {
|
||||
return "";
|
||||
return '';
|
||||
}
|
||||
return String(result);
|
||||
} catch (e) {
|
||||
return "";
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user