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:
co-authored by
Claude Opus 4.8
parent
5aab8a568f
commit
ca1e50d32c
@@ -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)})
|
||||
|
||||
Reference in New Issue
Block a user