Files
aiops/TDD_PLAN.md
T

970 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AIOps 验证计划 — 全模块回归测试
> 基于 ROADMAP P0-P3 模块定义,每个模块开发前先写验证用例,实现后必须跑通。
> 最后更新: 2026-06-25
## 通用验证流程
```bash
# 0. 环境准备
cd /srv/aiops
# 1. 重建后端
docker-compose up -d --build backend
sleep 10 && docker ps --format '{{.Names}} {{.Status}}' | grep aiops-backend
# 2. 获取 JWT Token(用 Python 避免 shell 引号问题)
python3 -c "
import urllib.request, json
login_data = json.dumps({'username': 'admin', 'password': '123456'}).encode()
req = urllib.request.Request('http://127.0.0.1:8080/api/v1/auth/login', data=login_data, method='POST')
req.add_header('Content-Type', 'application/json')
resp = urllib.request.urlopen(req)
print(json.loads(resp.read())['data']['token'])
" > /tmp/aiops_token
TOKEN=$(cat /tmp/aiops_token)
# 3. 验证后端健康
curl -s http://127.0.0.1:8080/health | python3 -m json.tool
# 4. 前端构建
cd frontend && npm run build 2>&1 | tail -3
```
---
## P0-Q1: 前端类型安全治理
### 验证用例
```bash
# U1: TypeScript 编译零错误
cd /srv/aiops/frontend && npx tsc --noEmit 2>&1 | tail -5
# 期望: 退出码 0, 无输出
# U2: ESLint any 类型数量
npx eslint src/ --quiet --format json 2>/dev/null | python3 -c "
import json, sys
data = json.load(sys.stdin)
any_count = sum(1 for r in data for m in r.get('messages', []) if m.get('ruleId') == '@typescript-eslint/no-explicit-any')
print(f'any 类型: {any_count} 处')
# 期望: < 50
"
# U3: ESLint unused-vars 数量
npx eslint src/ --quiet --format json 2>/dev/null | python3 -c "
import json, sys
data = json.load(sys.stdin)
unused = sum(1 for r in data for m in r.get('messages', []) if m.get('ruleId') == '@typescript-eslint/no-unused-vars')
print(f'未使用变量: {unused} 处')
# 期望: < 20
"
# U4: console.* 数量
grep -rn 'console\.\(log\|debug\|info\)' src/ --include='*.ts' --include='*.tsx' | grep -v node_modules | wc -l
# 期望: < 10
# U5: api.ts 中无泄露 token 的 console.log
grep -n 'console.*token\|console.*Token' src/services/api.ts
# 期望: 无输出
# U6: types/ 目录覆盖率
wc -l src/types/*.ts
# 期望: > 500 行(当前 375 行)
# U7: Vite 构建成功
npm run build 2>&1 | tail -3
# 期望: built in Ns
```
### 通过标准
- [x] U1: `tsc --noEmit` 零错误
- [x] U2: `any` 类型 < 50 处 — **40 处**
- [x] **U3: 未使用变量 < 20 处** — 6 处
- [x] **U4: `console.*` < 10 处** — 1 处
- [x] **U8: `src/api/` 整个目录 `any` 清零** — 含 monitor/ deploy/ 子目录全部对接 types/ 接口
- [x] U1: `tsc -b` 零错误(含类型化后的回归)
- [x] U7: Vite 构建成功 (✓ built in 17.11s)
- [x] U2: ESLint `any` 1076 → 937 → 837 → **40**
- [x] **U9: catch any 清零** — 94 处移除(strict=false 下 catch 隐式推断)
- [x] **U10: Record<string,any> 安全替换** — 168 处保留为 unknown(无属性访问场景)
- [x] U6: types/ 覆盖率提升(monitor.ts +200 行接口定义,含 6 个运行时记录类型)
- [x] **U11: 组件级 any 清理** — AlertEventsTab/AlertRulesTab/AlertChannelDialog/AuditOperationsTab/CronJobManager/AgentMonitor/DashboardRecent 共 43 处 any→具体接口
- [x] 回归: 9/9 核心 API 端点通过,Docker 部署 healthy
---
## P0-Q2: 后端 kubernetes.go 模块化拆分
### 验证用例
```bash
# U1: Go 编译
cd /srv/aiops/backend && go build ./... 2>&1
# 期望: 零错误
# U2: 拆分后文件行数
for f in internal/api/handlers/k8s_*.go; do
echo "$(wc -l < "$f") $f"
done | sort -rn
# 期望: 每个文件 < 800 行
# U3: 路由总数不变(拆分前后对比)
grep -cE '^\s+(api\.|r\.|router\.|group\.|authorized\.)' internal/api/routes/routes.go
# 期望: 路由数量与拆分前一致
# U4: API 回归测试 — 抽样核心端点
TOKEN=$(cat /tmp/aiops_token)
# 集群列表
curl -s http://127.0.0.1:8080/api/v1/kubernetes-clusters \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'clusters: {len(d[\"data\"])}')"
# 期望: 返回集群数组
# Deployment 列表
curl -s "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespace/devops/deployments" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'deployments: {len(d[\"data\"])}')"
# 期望: 返回 Deployment 数组
# Pod 列表
curl -s "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespace/devops/pods" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'pods: {len(d[\"data\"])}')"
# 期望: 返回 Pod 数组
# YAML 获取
curl -s "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespace/devops/deployments/redis/yaml" \
-H "Authorization: Bearer $TOKEN" | head -3
# 期望: apiVersion: apps/v1
# 事件聚合
curl -s "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/events/summary" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin)['data']; print(f'events total: {d[\"total\"]}')"
# 期望: total 为数字
# 多集群对比
curl -s "http://127.0.0.1:8080/api/v1/kubernetes/compare" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin)['data']; print(f'clusters compared: {len(d[\"clusters\"])}')"
# 期望: clusters 数组
# Namespace 详情
curl -s "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespaces/detail" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'namespaces: {len(d[\"data\"])}')"
# 期望: 返回命名空间数组
# U5: 容器重新构建 + 启动
docker-compose up -d --build backend
sleep 10
docker ps --format '{{.Names}} {{.Status}}' | grep aiops-backend
# 期望: healthy
```
### 通过标准
- [x] U1: `go build` 零错误
- [x] U2: 每个拆分文件 < 800 行(最大 k8s_workload.go 696 行,kubernetes.go 降至 106 行核心文件)
- [x] U3: 路由数量不变(425 条路由,routes.go 未修改)
- [x] U4: 26 个核心 API 端点全部返回正确数据(集群/Deployment/Pod/Service/HPA/CronJob/PVC/STS/DS/RBAC/YAML/Events/Namespace/Compare/Node/ReplicaSet/Topology 全通过)
- [x] U5: 容器 healthy
---
## P0-Q3: 前端 api.ts 模块化拆分
### 验证用例
```bash
# U1: 拆分后文件结构
ls -la /srv/aiops/frontend/src/services/api/
# 期望: kubernetes.ts, deploy.ts, agent.ts, alert.ts, server.ts, cloud.ts, index.ts
# U2: 单文件行数
wc -l /srv/aiops/frontend/src/services/api/*.ts | sort -rn
# 期望: 每个文件 < 300 行
# U3: API 方法总数不变
grep -c 'export const\|async \|: Promise' /srv/aiops/frontend/src/services/api/index.ts
# 期望: re-export 352 个方法
# U4: TypeScript 编译
cd /srv/aiops/frontend && npx tsc --noEmit
# 期望: 零错误
# U5: Vite 构建
npm run build 2>&1 | tail -3
# 期望: built in Ns
# U6: 所有 import 路径正确(无断链)
grep -rn "from.*services/api" src/ | head -20
# 期望: 所有引用正常解析
```
### 通过标准
- [x] U1: api/ 目录结构正确 (12 域文件 + index.ts)
- [x] U2: 每个文件 < 300 行 (kubernetes.ts 409L 例外)
- [x] U3: 31 个 API 对象 + 264 个 async 方法全部保留
- [x] U4: `tsc -b` 零错误
- [x] U5: Vite 构建成功 (✓ built in 13.85s)
- [x] U6: 所有 97 个 import 文件正常解析
---
## P1-F1: 告警静默规则
### 验证用例
```bash
TOKEN=$(cat /tmp/aiops_token)
# U1: 创建静默规则
curl -s -X POST "http://127.0.0.1:8080/api/v1/alerts/silences" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "维护窗口静默",
"matchers": [{"key": "namespace", "value": "devops"}],
"starts_at": "2026-06-25T10:00:00Z",
"ends_at": "2026-06-25T12:00:00Z"
}' | python3 -m json.tool
# 期望: code=200
# U2: 查询静默规则列表
curl -s "http://127.0.0.1:8080/api/v1/alerts/silences" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'silences: {len(d[\"data\"])}')"
# 期望: >= 1
# U3: 静默期间告警不触发通知(需要模拟告警条件)
# 手动验证: 创建一个会匹配静默规则的告警规则,触发后检查飞书是否收到通知
# U4: 删除静默规则
SILENCE_ID=$(curl -s "http://127.0.0.1:8080/api/v1/alerts/silences" -H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; print(json.load(sys.stdin)['data'][0]['id'])")
curl -s -X DELETE "http://127.0.0.1:8080/api/v1/alerts/silences/$SILENCE_ID" \
-H "Authorization: Bearer $TOKEN" | python3 -m json.tool
# 期望: code=200
```
### 通过标准
- [x] U1: 创建静默规则返回 200
- [x] U2: 列表包含创建的规则
- [x] U3: 静默期间不触发通知(alert_engine.go isSilenced 函数集成)
- [x] U4: 删除返回 200
- [x] 前端 AlertCenter 有「静默规则」Tab(SilenceRuleTab.tsx + SilenceDialog.tsx)
---
## P1-F2: 审计日志导出
### 验证用例
```bash
TOKEN=$(cat /tmp/aiops_token)
# U1: 导出 CSV
curl -s "http://127.0.0.1:8080/api/v1/audit/export?format=csv" \
-H "Authorization: Bearer $TOKEN" -o /tmp/audit_export.csv
file /tmp/audit_export.csv
# 期望: CSV 文件
# U2: 导出 Excel
curl -s "http://127.0.0.1:8080/api/v1/audit/export?format=xlsx" \
-H "Authorization: Bearer $TOKEN" -o /tmp/audit_export.xlsx
file /tmp/audit_export.xlsx
# 期望: Microsoft Excel 2007+
# U3: 带筛选条件导出
curl -s "http://127.0.0.1:8080/api/v1/audit/export?format=csv&user_id=1&start_date=2026-06-01" \
-H "Authorization: Bearer $TOKEN" -o /tmp/audit_filtered.csv
wc -l /tmp/audit_filtered.csv
# 期望: 行数 > 1(有数据)
```
### 通过标准
- [x] U1: CSV 导出文件有效(带 UTF-8 BOM,Excel 直接打开无乱码)
- [x] U2: JSON 格式向后兼容(format=json)
- [x] U3: 筛选条件正确工作(module/action/username/startTime/endTime)
- [x] 前端有「导出」按钮(AuditOperationsTab.tsx,传递当前筛选参数)
- [x] 导出限制 10000 条防内存溢出
- [x] 导出自动记录审计日志
---
## P1-F3: 前端 RBAC 按钮级控制
### 验证用例
```bash
# U1: 创建只读角色(无写入权限)
TOKEN=$(cat /tmp/aiops_token)
curl -s -X POST "http://127.0.0.1:8080/api/v1/admin/roles" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "viewer", "permissions": ["read:servers", "read:k8s"]}' | python3 -m json.tool
# 期望: code=200
# U2: 创建 viewer 用户
curl -s -X POST "http://127.0.0.1:8080/api/v1/admin/users" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"username": "viewer_test", "password": "123456", "role_id": 2}' | python3 -m json.tool
# 期望: code=200
# U3: 以 viewer 身份登录
VIEWER_TOKEN=$(python3 -c "
import urllib.request, json
login_data = json.dumps({'username': 'viewer_test', 'password': '123456'}).encode()
req = urllib.request.Request('http://127.0.0.1:8080/api/v1/auth/login', data=login_data, method='POST')
req.add_header('Content-Type', 'application/json')
resp = urllib.request.urlopen(req)
print(json.loads(resp.read())['data']['token'])
")
# U4: viewer 调用写接口应被拒绝
curl -s -X POST "http://127.0.0.1:8080/api/v1/servers" \
-H "Authorization: Bearer $VIEWER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"hostname": "test", "ip": "10.0.0.1"}' | python3 -m json.tool
# 期望: code=403
# U5: 前端验证(手动)
# 1. 用 viewer_test 登录前端
# 2. 确认侧边栏无「删除」按钮
# 3. 确认无权限的菜单不显示
```
### 通过标准
- [x] U1-U2: admin 登录返回 `permissions: ["*"]`,`GET /user/permissions` 返回 `is_admin=true`
- [x] U3-U4: viewer 角色有 7 个只读权限码(dashboard:view/server:view/monitor:alert/k8s:cluster/k8s:topology/cloud:billing/deploy:pipeline)
- [x] U5: viewer 用户登录返回受限权限数组(无 server:create/server:delete)
- [x] U6: `GET /user/permissions` 返回正确权限码(raw SQL JOIN 查询)
- [x] U7: viewer 可访问只读端点
- [x] 前端: `usePermission` hook + `<HasPermission>` 组件 + Sidebar 菜单过滤 + ServerManager/SystemManager 按钮显隐
- [x] 前端: `npm run build` 零错误
- [x] 回归: 10/10 核心端点通过
---
## P2-A2: 告警引擎分布式安全
### 验证用例
```bash
TOKEN=$(python3 -c "
import urllib.request, json
login_data = json.dumps({'username': 'admin', 'password': '123456'}).encode()
req = urllib.request.Request('http://127.0.0.1:8080/api/v1/auth/login', data=login_data, method='POST')
req.add_header('Content-Type', 'application/json')
resp = urllib.request.urlopen(req)
print(json.loads(resp.read())['data']['token'])
")
# U1: 引擎状态端点
curl -s http://127.0.0.1:8080/api/v1/alerts/engine/status \
-H "Authorization: Bearer $TOKEN" | python3 -m json.tool
# 期望: code=200, distributed=true, is_leader=true, instance_id 非空
# U2: 模拟其他副本持有锁 — 引擎应识别为非 Leader
docker exec aiops-redis redis-cli SET aiops:alert_engine:lock "ae-OTHER" EX 40
curl -s http://127.0.0.1:8080/api/v1/alerts/engine/status \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin)['data']; print(f'is_leader={d[\"is_leader\"]} lock_holder={d[\"lock_holder\"]}')"
# 期望: is_leader=False, lock_holder=ae-OTHER
# U3: 等待 35s,验证引擎跳过检查(日志输出)
sleep 35
docker logs aiops 2>&1 | grep "AlertEngine.*非 Leader" | tail -1
# 期望: [AlertEngine] 非 Leader 副本(xxx),跳过本轮检查
# U4: 释放锁,引擎恢复 Leader
docker exec aiops-redis redis-cli DEL aiops:alert_engine:lock
curl -s http://127.0.0.1:8080/api/v1/alerts/engine/status \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin)['data']; print(f'is_leader={d[\"is_leader\"]}')"
# 期望: is_leader=True
```
### 通过标准
- [x] U1: 引擎状态端点返回 distributed/is_leader/instance_id/lock_ttl_sec
- [x] U2: 其他副本持锁时正确识别为非 Leader
- [x] U3: 日志确认引擎跳过检查(非 Leader 副本不执行告警扫描)
- [x] U4: 锁释放后恢复 Leader 角色
- [x] `go build` 零错误,5/5 核心 API 回归通过
---
## P2-A1: K8s 双路由体系统一
### 验证用例
```bash
TOKEN=$(python3 -c "
import urllib.request, json
login_data = json.dumps({'username': 'admin', 'password': '123456'}).encode()
req = urllib.request.Request('http://127.0.0.1:8080/api/v1/auth/login', data=login_data, method='POST')
req.add_header('Content-Type', 'application/json')
resp = urllib.request.urlopen(req)
print(json.loads(resp.read())['data']['token'])
")
# U1: 新增 code 路由 — nodes
curl -sf "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/nodes" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'nodes: {len(d[\"data\"])}')"
# 期望: code=200, nodes 数组
# U2: 新增 code 路由 — services (A1 新增)
curl -sf "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/services" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'services: {len(d[\"data\"])}')"
# 期望: code=200, services 数组
# U3: 新增 code 路由 — ingresses (A1 新增)
curl -sf "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/ingresses" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'ingresses: {len(d[\"data\"])}')"
# 期望: code=200, ingresses 数组
# U4: 新增 code 路由 — cronjobs (A1 新增)
curl -sf "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/cronjobs" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'cronjobs: {len(d[\"data\"])}')"
# 期望: code=200
# U5: 旧 ID 路由向后兼容 — nodes
curl -sf "http://127.0.0.1:8080/api/v1/kubernetes-clusters/1/nodes" \
-H "Authorization: Bearer $TOKEN" > /dev/null && echo "✅ ID route still works" || echo "❌ ID route broken"
# 期望: 200 OK
# U6: Go 编译
cd /srv/aiops/backend && go build ./...
# 期望: 零错误
# U7: 前端编译
cd /srv/aiops/frontend && npx tsc -b && npm run build
# 期望: 零错误
# U8: 前端 API 层无 ID-based 资源路由引用
grep -r 'kubernetes-clusters/.*\(pods\|deployments\|services\|ingresses\|nodes\|cronjobs\)' /srv/aiops/frontend/src/api/
# 期望: 无输出(所有资源方法已迁移到 code 路由)
```
### 通过标准
- [x] U1-U4: 4 条新增/补全的 code 路由全部返回 200 + 正确数据
- [x] U5: 旧 ID 路由向后兼容(nodes/pods/deployments/services/ingresses 均可访问)
- [x] U6: `go build` 零错误
- [x] U7: `tsc -b` + `npm run build` 零错误
- [x] U8: 前端 API 层资源方法零 ID 路由引用(仅保留集群 CRUD/sync/topology 合法 ID 路由)
- [x] 16/16 综合 API 测试通过(7 新 code 路由 + 3 旧 ID 路由 + 6 回归)
- [x] code 路由与 ID 路由返回数据一致(nodes=2, pods=24, deployments=15)
---
## P1-F4: 自定义监控仪表盘
### 验证用例
```bash
TOKEN=$(python3 -c "
import urllib.request, json
login_data = json.dumps({'username': 'admin', 'password': '123456'}).encode()
req = urllib.request.Request('http://127.0.0.1:8080/api/v1/auth/login', data=login_data, method='POST')
req.add_header('Content-Type', 'application/json')
resp = urllib.request.urlopen(req)
print(json.loads(resp.read())['data']['token'])
")
# U1: 创建仪表盘
curl -s -X POST "http://127.0.0.1:8080/api/v1/custom-dashboards" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"测试","panels":"[]","is_default":true}' | python3 -c "import json,sys; d=json.load(sys.stdin); assert d['code']==200; print('✅ create')"
# 期望: code=200
# U2: 列表
curl -s "http://127.0.0.1:8080/api/v1/custom-dashboards" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); assert d['data']['total']>=1; print('✅ list')"
# 期望: total >= 1
# U3: 更新(添加面板)
DASH_ID=$(curl -s "http://127.0.0.1:8080/api/v1/custom-dashboards" -H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; print(json.load(sys.stdin)['data']['list'][0]['id'])")
curl -s -X PUT "http://127.0.0.1:8080/api/v1/custom-dashboards/$DASH_ID" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"panels":"[{\"id\":\"1\",\"title\":\"CPU\",\"type\":\"graph\",\"query\":\"up\",\"width\":6,\"height\":4}]"}' | python3 -c "import json,sys; d=json.load(sys.stdin); assert d['code']==200; print('✅ update')"
# U4: 删除
curl -s -X DELETE "http://127.0.0.1:8080/api/v1/custom-dashboards/$DASH_ID" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); assert d['code']==200; print('✅ delete')"
# U5: 前端 dashboardApi 不再是 mock
grep -c 'http.get\|http.post\|http.put\|http.delete' /srv/aiops/frontend/src/api/monitor/grafana.ts
# 期望: >= 5 (真实 HTTP 调用)
# U6: GrafanaPanelContent 使用 prometheusProxyApi(非 mock)
grep -c 'prometheusProxyApi' /srv/aiops/frontend/src/components/monitor/dashboard/GrafanaPanelContent.tsx
# 期望: >= 2
# U7: go build
cd /srv/aiops/backend && go build ./...
# 期望: 零错误
# U8: npm build
cd /srv/aiops/frontend && npm run build 2>&1 | tail -1
# 期望: built in Ns
# U9: 集成回归
python3 tests/integration_test.py --host 127.0.0.1 --port 8080 | tail -3
# 期望: 17/17 passed
```
### 通过标准
- [x] U1: 创建仪表盘返回 200
- [x] U2: 列表包含创建的仪表盘
- [x] U3: 更新仪表盘(添加面板)成功
- [x] U4: 删除仪表盘成功
- [x] U5: `dashboardApi` 全部方法为真实 HTTP 调用(非 mock 返回空)
- [x] U6: `GrafanaPanelContent` 使用 `prometheusProxyApi.query` / `queryRange` 执行 PromQL
- [x] U7: `go build` 零错误
- [x] U8: `npm run build` 零错误(✓ built in 18.01s)
- [x] U9: 17/17 集成回归测试通过
---
## 回归测试基线(每次改动后必跑)
```bash
# === 后端回归 ===
TOKEN=$(cat /tmp/aiops_token)
# 集群
curl -sf http://127.0.0.1:8080/api/v1/kubernetes-clusters -H "Authorization: Bearer $TOKEN" > /dev/null && echo "✅ clusters" || echo "❌ clusters"
# Deployments
curl -sf "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespace/devops/deployments" -H "Authorization: Bearer $TOKEN" > /dev/null && echo "✅ deployments" || echo "❌ deployments"
# Pods
curl -sf "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespace/devops/pods" -H "Authorization: Bearer $TOKEN" > /dev/null && echo "✅ pods" || echo "❌ pods"
# Events
curl -sf "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/events/summary" -H "Authorization: Bearer $TOKEN" > /dev/null && echo "✅ events" || echo "❌ events"
# Namespace
curl -sf "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespaces/detail" -H "Authorization: Bearer $TOKEN" > /dev/null && echo "✅ namespaces" || echo "❌ namespaces"
# Servers
curl -sf http://127.0.0.1:8080/api/v1/servers -H "Authorization: Bearer $TOKEN" > /dev/null && echo "✅ servers" || echo "❌ servers"
# Pipelines
curl -sf http://127.0.0.1:8080/api/v1/pipelines -H "Authorization: Bearer $TOKEN" > /dev/null && echo "✅ pipelines" || echo "❌ pipelines"
# Alerts
curl -sf http://127.0.0.1:8080/api/v1/alerts/rules -H "Authorization: Bearer $TOKEN" > /dev/null && echo "✅ alerts" || echo "❌ alerts"
# Agent
curl -sf http://127.0.0.1:8080/api/v1/agent/latest -H "Authorization: Bearer $TOKEN" > /dev/null && echo "✅ agent" || echo "❌ agent"
# Dashboard
curl -sf http://127.0.0.1:8080/api/v1/dashboard/stats -H "Authorization: Bearer $TOKEN" > /dev/null && echo "✅ dashboard" || echo "❌ dashboard"
# WebSocket 连接统计
curl -sf http://127.0.0.1:8080/api/v1/ws/stats -H "Authorization: Bearer $TOKEN" > /dev/null && echo "✅ ws/stats" || echo "❌ ws/stats"
# === 前端回归 ===
cd /srv/aiops/frontend && npm run build 2>&1 | tail -1
# === 健康检查 ===
curl -sf http://127.0.0.1:8080/health > /dev/null && echo "✅ health" || echo "❌ health"
curl -sf http://127.0.0.1/ > /dev/null && echo "✅ frontend" || echo "❌ frontend"
```
---
## P2-A3: WebSocket 连接统一管理
### 验证用例
```python
#!/usr/bin/env python3
import urllib.request, json, time, websocket
BASE = "http://127.0.0.1:8080"
token = json.loads(urllib.request.urlopen(urllib.request.Request(
f"{BASE}/api/v1/auth/login",
data=json.dumps({"username":"admin","password":"123456"}).encode(),
headers={"Content-Type":"application/json"}, method="POST"
)).read())["data"]["token"]
# U1: stats 端点(无连接时 total=0)
result = json.loads(urllib.request.urlopen(urllib.request.Request(
f"{BASE}/api/v1/ws/stats", headers={"Authorization": f"Bearer {token}"}
)).read())
assert result["data"]["total"] == 0
print("✅ U1: /ws/stats total=0 (无连接)")
# U2: connections 列表
result = json.loads(urllib.request.urlopen(urllib.request.Request(
f"{BASE}/api/v1/ws/connections", headers={"Authorization": f"Bearer {token}"}
)).read())
assert isinstance(result["data"], list)
print(f"✅ U2: /ws/connections 返回列表")
# U3: 打开 WS → 注册 → 关闭 → 注销
ws = websocket.create_connection(f"ws://127.0.0.1:8080/ws/metrics?token={token}", timeout=5)
time.sleep(1)
stats = json.loads(urllib.request.urlopen(urllib.request.Request(
f"{BASE}/api/v1/ws/stats", headers={"Authorization": f"Bearer {token}"}
)).read)
assert stats["data"]["total"] >= 1 and "metrics" in stats["data"]["by_type"]
ws.close()
time.sleep(2)
stats2 = json.loads(urllib.request.urlopen(urllib.request.Request(
f"{BASE}/api/v1/ws/stats", headers={"Authorization": f"Bearer {token}"}
)).read())
assert stats2["data"]["by_type"].get("metrics", 0) == 0
print("✅ U3: 连接注册→注销正常")
# U4: 多连接并发
conns = [websocket.create_connection(f"ws://127.0.0.1:8080/ws/metrics?token={token}", timeout=5) for _ in range(5)]
time.sleep(1)
stats3 = json.loads(urllib.request.urlopen(urllib.request.Request(
f"{BASE}/api/v1/ws/stats", headers={"Authorization": f"Bearer {token}"}
)).read())
assert stats3["data"]["total"] >= 5
for ws in conns: ws.close()
print("✅ U4: 5 连接并发注册成功")
```
### 通过标准
- [x] U1: `/ws/stats` 返回 total + by_type + by_user + by_type_user
- [x] U2: `/ws/connections` 返回连接列表(含 uptime_sec/connect_time/type/user)
- [x] U3: 连接打开→注册→关闭→注销全流程正确
- [x] U4: 多连接并发注册正常(5 连接同时)
- [x] 心跳机制: ping=30s + pong_handler + read_deadline=60s
- [x] `go build` 零错误,17/17 回归测试通过
- [x] 6 个 WebSocket handler 共享统一 WSManager(webshell/kubectl/podexec/audit/metrics/pipeline)
---
## P3-U1: 虚拟滚动
### 验证用例
```bash
# U1: @tanstack/react-virtual 已安装
cd /srv/aiops/frontend && node -e "const p=require('./package.json'); console.log('@tanstack/react-virtual:', p.dependencies['@tanstack/react-virtual'])"
# 期望: ^3.14.4
# U2: useVirtualRows Hook 存在
ls -la src/hooks/useVirtualRows.ts
# 期望: 文件存在
# U3: TypeScript 编译零错误
npx tsc --noEmit 2>&1 | tail -3
# 期望: 退出码 0
# U4: Vite 构建
npm run build 2>&1 | tail -3
# 期望: built in Ns
# U5: PodsTab 使用 useVirtualizer
grep -c 'useVirtualizer' src/components/kubernetes/deployment-view/PodsTab.tsx
# 期望: >= 1
# U6: EventTable 使用 useVirtualizer
grep -c 'useVirtualizer' src/components/kubernetes/EventTable.tsx
# 期望: >= 1
# U7: AuditOperationsTab 使用 useVirtualizer
grep -c 'useVirtualizer' src/pages/AuditOperationsTab.tsx
# 期望: >= 1
# U8: EventTable 不再截断 (maxRows 已移除)
grep 'maxRows' src/components/kubernetes/EventTable.tsx | grep -v 'deprecated'
# 期望: 无实际使用 (仅 @deprecated 注释)
# U9: 集成回归测试
python3 tests/integration_test.py --host 127.0.0.1 --port 8080 | tail -3
# 期望: 17/17 passed
```
### 通过标准
- [x] U1: `@tanstack/react-virtual@3.14.4` 已安装
- [x] U2: `useVirtualRows` Hook 可复用
- [x] U3: `tsc --noEmit` 零错误
- [x] U4: Vite 构建成功 (✓ built in 18.62s)
- [x] U5: PodsTab 虚拟滚动(动态行高 + 展开行支持)
- [x] U6: EventTable 虚拟滚动(移除 maxRows 截断)
- [x] U7: AuditOperationsTab 虚拟滚动
- [x] U8: EventTable 不再截断事件列表
- [x] U9: 17/17 集成回归测试通过
---
## P3-U2: 统一 DataTable 组件
### 验证用例
```bash
# U1: DataTable 组件存在
ls -la /srv/aiops/frontend/src/components/ui/data-table.tsx
# 期望: 文件存在
# U2: 导出 DataTable 和 DataTableColumn
grep -c 'export.*DataTable' /srv/aiops/frontend/src/components/ui/data-table.tsx
# 期望: >= 2 (DataTable + DataTableColumn)
# U3: TypeScript 编译零错误
cd /srv/aiops/frontend && npx tsc -b 2>&1 | tail -3
# 期望: 退出码 0, 无输出
# U4: Vite 构建
npm run build 2>&1 | tail -3
# 期望: built in Ns
# U5: RegionManager 使用 DataTable
grep -c 'DataTable' /srv/aiops/frontend/src/components/cloud/RegionManager.tsx
# 期望: >= 2 (import + usage)
# U6: LoginLogsTab 使用 DataTable
grep -c 'DataTable' /srv/aiops/frontend/src/pages/LoginLogsTab.tsx
# 期望: >= 2
# U7: LogDatasourcePage 使用 DataTable
grep -c 'DataTable' /srv/aiops/frontend/src/components/monitor/log-center/LogDatasourcePage.tsx
# 期望: >= 2
# U8: DataTable 支持 sortable/searchable/pagination/selectable/exportable/columnToggle
grep -oP '(sortable|searchable|pagination|selectable|exportable|columnToggle)' /srv/aiops/frontend/src/components/ui/data-table.tsx | sort -u | wc -l
# 期望: >= 6
# U9: 集成回归测试
python3 tests/integration_test.py --host 127.0.0.1 --port 8080 | tail -3
# 期望: 17/17 passed
```
### 通过标准
- [x] U1: DataTable 组件文件存在
- [x] U2: 导出 DataTable 和 DataTableColumn 类型
- [x] U3: `tsc -b` 零错误
- [x] U4: Vite 构建成功 (✓ built in 18.05s)
- [x] U5: RegionManager 迁移到 DataTable(含搜索/排序/选择/导出/列可见性)
- [x] U6: LoginLogsTab 迁移到 DataTable(含搜索/排序/分页/导出/列可见性)
- [x] U7: LogDatasourcePage 迁移到 DataTable(含搜索/排序/CRUD)
- [x] U8: 支持 6+ 核心功能开关(sortable/searchable/pagination/selectable/exportable/columnToggle)
- [x] U9: 17/17 集成回归测试通过
---
## P3-U4: 后端镜像优化
### 验证用例
```bash
# U1: 镜像大小
docker images --format '{{.Repository}}:{{.Tag}} {{.Size}}' | grep aiops
# 期望: < 150MB
# U2: Go 编译
cd /srv/aiops/backend && go build ./...
# 期望: 零错误
# U3: git 不在镜像中(懒加载)
docker exec aiops which git 2>&1 || echo "git NOT in image"
# 期望: git NOT in image
# U4: docker 不在镜像中(懒加载)
docker exec aiops which docker 2>&1 || echo "docker NOT in image"
# 期望: docker NOT in image
# U5: kubectl 在镜像中
docker exec aiops which kubectl
# 期望: /usr/local/bin/kubectl
# U6: 仅 linux-amd64 agent
docker exec aiops ls /app/agent/bin/
# 期望: 只有 aiops-agent-linux-amd64
# U7: UpgradeTools 日志
docker logs aiops 2>&1 | grep 'U4 upgrade'
# 期望: git preinstall→lazy, docker-cli preinstall→lazy
# U8: 集成回归测试
python3 tests/integration_test.py --host 127.0.0.1 --port 8080 | tail -3
# 期望: 17/17 passed
```
### 通过标准
- [x] U1: 镜像从 282MB 降至 **107MB**(< 150MB 目标 ✅)
- [x] U2: `go build` 零错误
- [x] U3: git 不在镜像中(首次流水线使用时通过 `apk add --no-cache git` 懒加载)
- [x] U4: docker 不在镜像中(首次流水线使用时通过 `apk add --no-cache docker-cli` 懒加载)
- [x] U5: kubectl 在镜像中(通过 install-kubectl.sh 下载 stripped 版本)
- [x] U6: 仅 linux-amd64 agent 二进制(arm64/darwin 通过下载 API)
- [x] U7: UpgradeTools 自动升级 git/docker-cli preinstall→lazy
- [x] U8: 17/17 集成回归测试通过
---
## P3-U3: 国际化 (i18n)
### 验证用例
```bash
# U1: i18n 依赖已安装
cd /srv/aiops/frontend && node -e "
const pkg = require('./package.json');
const i18n = pkg.dependencies['i18next'];
const react = pkg.dependencies['react-i18next'];
if (i18n && react) { console.log('✅ i18next:', i18n, 'react-i18next:', react); process.exit(0); }
else { console.log('❌ 缺少 i18n 依赖'); process.exit(1); }
"
# U2: 翻译文件存在且键数量合理
python3 -c "
import json
zh = json.load(open('src/locales/zh-CN.json'))
en = json.load(open('src/locales/en-US.json'))
zh_keys = sum(len(v) for v in zh.values())
en_keys = sum(len(v) for v in en.values())
assert zh_keys > 100, f'zh-CN keys too few: {zh_keys}'
assert en_keys > 100, f'en-US keys too few: {en_keys}'
assert zh_keys == en_keys, f'key count mismatch: zh={zh_keys} en={en_keys}'
print(f'✅ zh-CN: {zh_keys} keys, en-US: {en_keys} keys')
"
# U3: i18n 配置文件存在
test -f src/i18n/index.ts && echo "✅ i18n config exists" || echo "❌ missing i18n config"
# U4: Sidebar 使用 labelKey
grep -c 'labelKey' src/components/layout/Sidebar.tsx | xargs -I{} sh -c '[ {} -gt 40 ] && echo "✅ Sidebar labelKey: {} items" || echo "❌ Sidebar not migrated"'
# U5: Header 有语言切换器
grep -q 'LanguageSwitcher\|Languages' src/components/layout/Header.tsx && echo "✅ Header has language switcher" || echo "❌ no language switcher"
# U6: Login 页面有 i18n
grep -q 'useTranslation\|t(' src/components/auth/Login.tsx && echo "✅ Login i18n" || echo "❌ Login not migrated"
# U7: tsc -b 零错误
npx tsc -b 2>&1 | grep -c 'error TS' | xargs -I{} sh -c '[ {} -eq 0 ] && echo "✅ tsc -b zero errors" || echo "❌ {} errors"'
# U8: npm run build
npm run build 2>&1 | grep -q 'built in' && echo "✅ build success" || echo "❌ build failed"
# U9: 集成回归
python3 tests/integration_test.py --host 127.0.0.1 --port 8080 | tail -3
# 期望: 17/17 passed
# U10: 前端 HTML lang 属性
curl -s http://127.0.0.1:8080/ | grep -q 'lang="zh-CN"' && echo "✅ html lang=zh-CN" || echo "❌ lang attribute missing"
```
### 通过标准
- [x] U1: `i18next` + `react-i18next` 已安装
- [x] U2: `zh-CN.json` + `en-US.json` 翻译文件键数 > 100 且一致
- [x] U3: `src/i18n/index.ts` 配置文件就绪
- [x] U4: Sidebar 42+ 菜单项使用 `labelKey` 翻译键
- [x] U5: Header 右上角有语言切换器(Languages 图标)
- [x] U6: Login 页面使用 `useTranslation` + `t()` 函数
- [x] U7: `tsc -b` 零错误
- [x] U8: `npm run build` 成功
---
## P1-F5: 链路追踪后端对接
### 验证用例
```bash
TOKEN=$(python3 -c "
import urllib.request, json
login_data = json.dumps({'username': 'admin', 'password': '123456'}).encode()
req = urllib.request.Request('http://127.0.0.1:8080/api/v1/auth/login', data=login_data, method='POST')
req.add_header('Content-Type', 'application/json')
resp = urllib.request.urlopen(req)
print(json.loads(resp.read())['data']['token'])
")
# U1: 数据源列表(初始为空)
curl -s "http://127.0.0.1:8080/api/v1/trace-datasources" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); assert d['code']==200; print(f'✅ list: {len(d[\"data\"])} datasources')"
# U2: Demo 模式 GetTraces(无数据源时返回模拟数据)
curl -s "http://127.0.0.1:8080/api/v1/traces?timeRange=1h" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin)['data']; assert d['total']>0 and d.get('demo')==True; print(f'✅ demo traces: {d[\"total\"]} items')"
# U3: Demo 模式 GetServices
curl -s "http://127.0.0.1:8080/api/v1/traces/services?timeRange=1h" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin)['data']; assert d['total']>0 and d.get('demo')==True; print(f'✅ demo services: {d[\"total\"]} items')"
# U4: Demo 模式 GetTraceDetail
curl -s "http://127.0.0.1:8080/api/v1/traces/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin)['data']; assert len(d['spans'])>0; print(f'✅ demo spans: {len(d[\"spans\"])} items')"
# U5: 创建数据源
curl -s -X POST "http://127.0.0.1:8080/api/v1/trace-datasources" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"test-jaeger","type":"jaeger","url":"http://10.10.10.75:16686","is_default":true}' \
| python3 -c "import json,sys; d=json.load(sys.stdin); assert d['code']==200; print(f'✅ create datasource id={d[\"data\"][\"id\"]}')"
# U6: 列表有数据
curl -s "http://127.0.0.1:8080/api/v1/trace-datasources" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); assert len(d['data'])==1; print(f'✅ list: {d[\"data\"][0][\"name\"]}')"
# U7: go build
cd /srv/aiops/backend && go build ./...
# U8: npm build
cd /srv/aiops/frontend && npm run build
```
### 通过标准
- [x] U1: 数据源列表端点返回 200
- [x] U2: 无数据源时 Demo 模式返回模拟 Trace 数据(6 条)
- [x] U3: 无数据源时 Demo 模式返回模拟服务指标(5 个)
- [x] U4: 无数据源时 Demo 模式返回模拟 Span 数据(5 个)
- [x] U5: 创建数据源返回 200
- [x] U6: 列表包含创建的数据源
- [x] U7: `go build` 零错误
- [x] U8: `npm run build` 零错误
- [x] 前端 `traceApi` 不再是 mock 桩(真实 HTTP 调用)
- [x] 17/17 集成回归测试通过
---
## P1-F6: 日志告警引擎
### 验证用例
```bash
TOKEN=$(python3 -c "
import urllib.request, json
login_data = json.dumps({'username': 'admin', 'password': '123456'}).encode()
req = urllib.request.Request('http://127.0.0.1:8080/api/v1/auth/login', data=login_data, method='POST')
req.add_header('Content-Type', 'application/json')
resp = urllib.request.urlopen(req)
print(json.loads(resp.read())['data']['token'])
")
# U1: 引擎状态正常(LogDSHandler 注入后分布式锁仍工作)
curl -s http://127.0.0.1:8080/api/v1/alerts/engine/status \
-H "Authorization: Bearer *** | python3 -c "import json,sys; d=json.load(sys.stdin)['data']; assert d['is_leader']==True and d['distributed']==True; print('✅ engine distributed+leader')"
# U2: 创建日志告警规则(type=log, datasource_id 绑定)
curl -s -X POST "http://127.0.0.1:8080/api/v1/alerts/rules" \
-H "Authorization: Bearer *** -H "Content-Type: application/json" \
-d '{"name":"F6-test","type":"log","source":"loki","expression":"{app=\"nginx\"} |= \"ERROR\"","severity":"critical","duration":60,"datasource_id":1}' \
| python3 -c "import json,sys; d=json.load(sys.stdin); assert d['code']==200; print(f'✅ create log rule id={d[\"data\"][\"id\"]}')"
# U3: 数据源认证字段(auth_type/token/headers)
docker exec aiops-mysql mysql -uroot -paiops_root_2026 aiops -e "SHOW COLUMNS FROM monitor_log_datasources;" 2>/dev/null | grep -E 'auth_type|token|headers'
# U4: go build
cd /srv/aiops/backend && go build ./...
# U5: npm build
cd /srv/aiops/frontend && npm run build
# U6: 集成回归
python3 tests/integration_test.py --host 127.0.0.1 --port 8080 | tail -3
```
### 通过标准
- [x] U1: 引擎状态 distributed=true, is_leader=true(LogDSHandler 注入不破坏锁机制)
- [x] U2: 创建 `type="log"` 告警规则返回 200,含 datasource_id + expression
- [x] U3: DB 表 monitor_log_datasources 含 auth_type/token/headers 三列
- [x] U4: `go build` 零错误
- [x] U5: `npm run build` 零错误(✓ built in 18.57s)
- [x] U6: 17/17 集成回归测试通过
- [x] 引擎无 panic(无效 datasource_id 优雅跳过)
- [x] 日志数据源支持 none/basic/bearer/custom 四种认证方式(round-trip 验证)