fix: 统一AI生成失败提示
This commit is contained in:
@@ -18,6 +18,7 @@ from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.ai.models import AITask, ModelConfig
|
||||
from apps.ai.generation_errors import TASK_OPERATIONS, classify_generation_error, public_error_for_task
|
||||
from apps.ai.providers import (
|
||||
OpenAICompatibleProvider,
|
||||
TtsNotConfigured,
|
||||
@@ -579,14 +580,13 @@ def run_extract_entities_task(*, task_id: str) -> None:
|
||||
seg.save(update_fields=["entity_refs", "updated_at"])
|
||||
except Exception as exc: # noqa: BLE001 — 失败退费并把错误记进 AITask 供前端轮询;不向上抛(避免 celery 重试二次扣费)
|
||||
# ValueError 是我们给用户写好的可读话术(解析失败 / 没识别到角色等);其余(网络/模型异常)给通用话术。
|
||||
msg = str(exc) if isinstance(exc, ValueError) else "提取调用失败,请重试"
|
||||
with transaction.atomic():
|
||||
task.status = AITask.Status.FAILED
|
||||
task.error_message = msg
|
||||
task.error_message = str(exc)[:2000]
|
||||
task.response_payload = response # 即便失败也存下模型输出片段(content/reasoning 长度等),供事后定位
|
||||
task.completed_at = timezone.now()
|
||||
task.save(update_fields=["status", "response_payload", "error_message", "completed_at", "updated_at"])
|
||||
release_credit(reservation=reservation, reason=msg)
|
||||
release_credit(reservation=reservation, reason=str(exc)[:200])
|
||||
|
||||
|
||||
def split_script_into_segments(content: str, count: int = 4) -> list[str]:
|
||||
@@ -2174,18 +2174,26 @@ def friendly_generation_error(raw: str) -> str:
|
||||
def notify_generation_failure(
|
||||
*, task, project, recipient, stage_label: str, raw: str, hint: str = ""
|
||||
) -> None:
|
||||
"""生成失败时落一条「失败」站内通知,**把第三方服务商 API 返回的原始报错原样写进通知正文**,
|
||||
让用户/排查能直接看到真因(如 `400 moderation_blocked ...`),而不是只看到翻译后的友好提示。
|
||||
"""生成失败时落一条普通用户可见的安全通知。
|
||||
|
||||
best-effort:同一任务用 dedupe_key 去重(重跑只更新不重复),通知本身出错绝不反过来弄挂主失败流程。"""
|
||||
原始错误只留在 AITask、日志和管理员任务详情,绝不能写进通知正文或 metadata。
|
||||
best-effort:同一任务用 dedupe_key 去重,通知本身出错绝不反过来弄挂主失败流程。
|
||||
"""
|
||||
from apps.ops.models import Notification
|
||||
|
||||
raw_clean = (raw or "").strip()
|
||||
friendly = (hint or friendly_generation_error(raw_clean) or "生成失败").strip()
|
||||
body = (
|
||||
f"{friendly}\n\n—— 第三方服务商 API 返回的原始报错 ——\n{raw_clean}"
|
||||
if raw_clean else friendly
|
||||
)
|
||||
public_error = public_error_for_task(task)
|
||||
if public_error is None:
|
||||
public_error = classify_generation_error(
|
||||
RuntimeError(raw_clean or hint or "generation failed"),
|
||||
operation=TASK_OPERATIONS.get(getattr(task, "task_type", ""), "image_generate"),
|
||||
reference_id=str(getattr(task, "id", "") or "") or None,
|
||||
)
|
||||
friendly = public_error.fallback_message
|
||||
# hint 可能来自旧调用方;仅作为空回退,不能覆盖统一安全文案。
|
||||
if not friendly:
|
||||
friendly = (hint or "生成失败").strip()
|
||||
body = friendly
|
||||
try:
|
||||
Notification.objects.update_or_create(
|
||||
team=task.team,
|
||||
@@ -2206,7 +2214,7 @@ def notify_generation_failure(
|
||||
metadata={
|
||||
"task_id": str(task.id),
|
||||
"task_type": getattr(task, "task_type", ""),
|
||||
"api_error": raw_clean[:1000],
|
||||
"generation_error": public_error.as_dict(),
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -2295,7 +2303,9 @@ def _storyboard_shot_worker(task_id, shot_id, user_id) -> None:
|
||||
transaction.on_commit(lambda a=asset: submit_asset_for_review(a))
|
||||
except Exception as exc: # noqa: BLE001 — 失败回滚额度,标记任务+shot 失败供 poll 上报
|
||||
raw = str(exc)
|
||||
hint = friendly_generation_error(raw) # 翻成前端友好提示;原始报错(含 yunqi body)留在 task 供排查
|
||||
public_error = classify_generation_error(
|
||||
exc, operation="storyboard_generate", reference_id=str(task.id)
|
||||
)
|
||||
task.status = AITask.Status.FAILED
|
||||
task.error_message = raw[:2000]
|
||||
task.completed_at = timezone.now()
|
||||
@@ -2303,11 +2313,11 @@ def _storyboard_shot_worker(task_id, shot_id, user_id) -> None:
|
||||
release_credit(reservation=reservation, reason=raw[:200])
|
||||
# 重跑失败时保留旧 adopted_version(画面不丢),只把状态标 FAILED + 友好提示供前端显示
|
||||
StoryboardShot.objects.filter(id=shot.id).update(
|
||||
status=StoryboardShot.Status.FAILED, error_message=hint, updated_at=timezone.now())
|
||||
status=StoryboardShot.Status.FAILED, error_message=public_error.fallback_message, updated_at=timezone.now())
|
||||
# 落一条失败通知,正文带上第三方服务商 API 的原始报错(真因),供用户/排查直接查看
|
||||
notify_generation_failure(
|
||||
task=task, project=project, recipient=user,
|
||||
stage_label=f"故事板·场 {shot.sort_order + 1}", raw=raw, hint=hint,
|
||||
stage_label=f"故事板·场 {shot.sort_order + 1}", raw=raw, hint=public_error.fallback_message,
|
||||
)
|
||||
finally:
|
||||
connections.close_all() # 释放该线程的 DB 连接
|
||||
@@ -2545,7 +2555,7 @@ def submit_video_segment(*, video_segment: VideoSegment, user, prompt: str) -> V
|
||||
try:
|
||||
provider = build_provider(model_config)
|
||||
# 不再静默退文生兜底:火山报错(如人脸需走素材库的 InputImageSensitiveContentDetected)直接抛,
|
||||
# 由下方 except 落 error_message → 前端如实显示真实报错,绝不假装成功出一条没锁脸的视频。
|
||||
# 由下方 except 保存原始错误供排障,并将安全提示写给普通用户。
|
||||
response = provider.create_video_task(
|
||||
model=model_config.name,
|
||||
endpoint=model_config.endpoint,
|
||||
@@ -2564,18 +2574,21 @@ def submit_video_segment(*, video_segment: VideoSegment, user, prompt: str) -> V
|
||||
video_segment.save(update_fields=["status", "updated_at"])
|
||||
return None
|
||||
except Exception as exc:
|
||||
public_error = classify_generation_error(
|
||||
exc, operation="video_generate", reference_id=str(task.id)
|
||||
)
|
||||
task.status = AITask.Status.FAILED
|
||||
task.error_message = str(exc)
|
||||
task.error_message = str(exc)[:2000]
|
||||
task.completed_at = timezone.now()
|
||||
task.save(update_fields=["status", "error_message", "completed_at", "updated_at"])
|
||||
release_credit(reservation=task.credit_reservation, reason=str(exc))
|
||||
video_segment.status = VideoSegment.Status.FAILED
|
||||
video_segment.error_message = str(exc)
|
||||
video_segment.error_message = public_error.fallback_message
|
||||
video_segment.save(update_fields=["status", "error_message", "updated_at"])
|
||||
notify_generation_failure(
|
||||
task=task, project=project, recipient=user,
|
||||
stage_label=f"视频·段 {video_segment.sort_order + 1}",
|
||||
raw=str(exc), hint=friendly_generation_error(str(exc)),
|
||||
raw=str(exc), hint=public_error.fallback_message,
|
||||
)
|
||||
raise
|
||||
|
||||
@@ -2620,17 +2633,24 @@ def poll_video_segment(*, video_segment: VideoSegment, user) -> VideoSegmentVers
|
||||
if remote_status in {"failed", "expired", "cancelled"}:
|
||||
ai_task.status = AITask.Status.FAILED
|
||||
ai_task.response_payload = response
|
||||
ai_task.error_message = response.get("error", {}).get("message", "video generation failed")
|
||||
error_info = response.get("error") or {}
|
||||
ai_task.error_message = error_info.get("message", "video generation failed")
|
||||
public_error = classify_generation_error(
|
||||
RuntimeError(ai_task.error_message),
|
||||
operation="video_generate",
|
||||
provider_code=str(error_info.get("code") or ""),
|
||||
reference_id=str(ai_task.id),
|
||||
)
|
||||
ai_task.completed_at = timezone.now()
|
||||
ai_task.save(update_fields=["status", "response_payload", "error_message", "completed_at", "updated_at"])
|
||||
release_credit(reservation=ai_task.credit_reservation, reason=ai_task.error_message)
|
||||
video_segment.status = VideoSegment.Status.FAILED
|
||||
video_segment.error_message = ai_task.error_message
|
||||
video_segment.error_message = public_error.fallback_message
|
||||
video_segment.save(update_fields=["status", "error_message", "updated_at"])
|
||||
notify_generation_failure(
|
||||
task=ai_task, project=video_segment.project, recipient=user,
|
||||
stage_label=f"视频·段 {video_segment.sort_order + 1}",
|
||||
raw=ai_task.error_message, hint=friendly_generation_error(ai_task.error_message),
|
||||
raw=ai_task.error_message, hint=public_error.fallback_message,
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -3212,7 +3232,7 @@ def synthesize_project_voiceover(*, project, user, items: list[dict], voice_type
|
||||
return metadata["voiceover"]
|
||||
except Exception as exc:
|
||||
task.status = AITask.Status.FAILED
|
||||
task.error_message = str(exc)
|
||||
task.error_message = str(exc)[:2000]
|
||||
task.completed_at = timezone.now()
|
||||
task.save(update_fields=["status", "error_message", "completed_at", "updated_at"])
|
||||
release_credit(reservation=reservation, reason=str(exc))
|
||||
|
||||
Reference in New Issue
Block a user