feat(admin): Phase 6 AI任务监控+成本异常 — 全局任务列/筛/详情抽屉/失败重投

后端:adminpanel tasks(全局 AITask + status/type/team/anomaly 筛分页)+ detail(payload)+ retry(失败重投,
仅图像类 best-effort 否则 400);is_cost_anomaly(实际>预估×1.5)+ F() 异常筛;IsPlatformAdmin + 审计。
前端:adminApi tasks 系列;Admin 任务监控页(状态 tab + 仅成本异常 chip + 表格成本预估→实际+异常标 + 详情抽屉 payload + 失败重投)。
修真 bug:抽屉缺 .show 类停屏外,补上正常滑入。
测试:adminpanel 36 单测过(筛/异常/详情/重投 mock celery + 非失败/不支持/权限拒);
无头 e2e _admin-p6.mjs 6 断言过 + 0 console error(不点真重投);tsc+build 绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
seaislee1209
2026-06-19 22:19:24 +08:00
co-authored by Claude Opus 4.8
parent 5aab8a568f
commit ca1e50d32c
11 changed files with 528 additions and 3 deletions
+38 -1
View File
@@ -1,9 +1,20 @@
from decimal import Decimal
from rest_framework import serializers
from apps.accounts.models import Team, TeamMember, User
from apps.ai.models import QualityWord
from apps.ai.models import AITask, QualityWord
from apps.assets.models import Asset
# 成本异常阈值:实际成本 > 预估 × 此倍数(且预估 > 0)即标异常
COST_ANOMALY_RATIO = Decimal("1.5")
def is_cost_anomaly(estimated, actual) -> bool:
est = estimated or Decimal("0")
act = actual or Decimal("0")
return bool(est > 0 and act > est * COST_ANOMALY_RATIO)
class QualityWordSerializer(serializers.ModelSerializer):
class Meta:
@@ -73,3 +84,29 @@ class AdminUserSerializer(serializers.ModelSerializer):
{"team_id": str(m.team_id), "team_name": m.team.name, "role": m.role}
for m in obj.team_memberships.select_related("team").all()
]
class AdminTaskSerializer(serializers.ModelSerializer):
team_name = serializers.CharField(source="team.name", read_only=True, default=None)
model_name = serializers.CharField(source="model_config.name", read_only=True, default=None)
cost_anomaly = serializers.SerializerMethodField()
class Meta:
model = AITask
fields = [
"id", "task_type", "status", "team", "team_name", "model_name",
"estimated_cost", "actual_cost", "cost_anomaly", "error_code", "created_at",
]
read_only_fields = fields
def get_cost_anomaly(self, obj) -> bool:
return is_cost_anomaly(obj.estimated_cost, obj.actual_cost)
class AdminTaskDetailSerializer(AdminTaskSerializer):
class Meta(AdminTaskSerializer.Meta):
fields = AdminTaskSerializer.Meta.fields + [
"project", "idempotency_key", "request_payload", "response_payload",
"error_message", "submitted_at", "completed_at",
]
read_only_fields = fields