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
+75 -2
View File
@@ -1,6 +1,8 @@
"""平台超管后台 · 跨团队端点。所有视图统一挂 IsPlatformAdmin,非超管一律 403,写操作记审计。"""
from django.db.models import Count, Q
from decimal import Decimal
from django.db.models import Count, F, Q
from rest_framework import status
from rest_framework.authtoken.models import Token
from rest_framework.decorators import api_view, permission_classes
@@ -10,13 +12,16 @@ from apps.accounts.audit import log_admin_action
from apps.accounts.models import Invitation, Team, User
from apps.accounts.permissions import IsPlatformAdmin
from apps.accounts.serializers import InvitationSerializer
from apps.ai.models import QualityWord
from apps.ai.models import AITask, QualityWord
from apps.assets.models import Asset
from apps.assets.review import poll_asset_review, submit_asset_for_review
from apps.common.pagination import DefaultPagination
from .serializers import (
COST_ANOMALY_RATIO,
AdminReviewAssetSerializer,
AdminTaskDetailSerializer,
AdminTaskSerializer,
AdminTeamMemberSerializer,
AdminTeamSerializer,
AdminUserSerializer,
@@ -326,3 +331,71 @@ def admin_asset_reviews_poll(request):
for asset in qs:
statuses[str(asset.id)] = poll_asset_review(asset)
return Response({"polled": len(statuses), "statuses": statuses})
# ─────────────────────────── AI 任务监控 + 成本异常 ───────────────────────────
@api_view(["GET"])
@permission_classes([IsPlatformAdmin])
def admin_tasks(request):
"""全局 AITask 列表(?status= / ?task_type= / ?team= / ?anomaly=1 成本异常 筛 + 分页)。"""
qs = AITask.objects.select_related("team", "model_config").order_by("-created_at")
st = request.query_params.get("status")
if st in dict(AITask.Status.choices):
qs = qs.filter(status=st)
tt = request.query_params.get("task_type")
if tt in dict(AITask.Type.choices):
qs = qs.filter(task_type=tt)
team_id = request.query_params.get("team")
if team_id:
qs = qs.filter(team_id=team_id)
if request.query_params.get("anomaly") in {"1", "true"}:
qs = qs.filter(estimated_cost__gt=0, actual_cost__gt=F("estimated_cost") * COST_ANOMALY_RATIO)
paginator = DefaultPagination()
page = paginator.paginate_queryset(qs, request)
return paginator.get_paginated_response(AdminTaskSerializer(page, many=True).data)
@api_view(["GET"])
@permission_classes([IsPlatformAdmin])
def admin_task_detail(request, task_id):
task = AITask.objects.select_related("team", "model_config").filter(id=task_id).first()
if task is None:
return Response({"detail": "not found"}, status=status.HTTP_404_NOT_FOUND)
return Response(AdminTaskDetailSerializer(task).data)
@api_view(["POST"])
@permission_classes([IsPlatformAdmin])
def admin_task_retry(request, task_id):
"""失败任务重投(best-effort):仅 FAILED + 可重投的图像类(基础资产 / 独立生图)。
其余类型(脚本 / 故事板 / 视频 / 配音 / 导出)请到对应页面重跑,这里 400 不冒险误投。"""
task = AITask.objects.filter(id=task_id).first()
if task is None:
return Response({"detail": "not found"}, status=status.HTTP_404_NOT_FOUND)
if task.status != AITask.Status.FAILED:
return Response({"detail": "仅失败任务可重投"}, status=status.HTTP_400_BAD_REQUEST)
image_types = {AITask.Type.PRODUCT_IMAGE, AITask.Type.PERSON_IMAGE, AITask.Type.SCENE_IMAGE}
if task.task_type not in image_types:
return Response({"detail": "该任务类型暂不支持后台重投,请到对应页面重新生成"}, status=status.HTTP_400_BAD_REQUEST)
from apps.ai.tasks import generate_base_asset_task, generate_standalone_image_task
try:
if task.project_id:
generate_base_asset_task.delay(str(task.id))
else:
generate_standalone_image_task.delay(str(task.id))
except Exception: # noqa: BLE001 — 投递失败如实返回,不静默
return Response({"detail": "重投调度失败,请确认 worker 在线"}, status=status.HTTP_502_BAD_GATEWAY)
log_admin_action(
request,
"task.retry",
target_type="ai_task",
target_id=task.id,
target_name=task.task_type,
)
return Response({"retried": True, "task_id": str(task.id)})