裂变补图:中转站波动导致单张失败时只重生失败分镜
实测发现 codeproxy 偶发不稳,单套中某张图重试3次仍失败会记 error。
本次加按需补图:只重生标 error 的分镜,成功的图与 token 不浪费。
- 019迁移+模型:fission_tasks 加 source_task_id(新架构不扇出GenerationTask,
补图需复刻首轮上下文:产品/operator key/张数,故落库回查)
- fission_image_retry:只补失败 role(引擎 target_role 单张重生,R2零改动),
merge 回 images_json,全恢复则 status=image_done
- tasks.py:retry_fission_images Celery task,key归属源任务operator(基石B),
幂等锁防重复补图
- api:POST /fission/{id}/notes/{seq}/retry-images,无失败分镜守卫 queued=false
- 前端:FissionNoteCard 失败提示改"重试补图"按钮+loading,父页轮询刷新
实测task5 seq1(hook图首轮codeproxy重试3次失败)补图跑通:只补hook(1024×1536
去AI化)、applied_proof原图不动、recovered=1 still_failed=0、status=image_done;
apiports503回落codeproxy gpt-5.5强档兜底;seq0无失败图守卫queued=false
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -286,3 +286,72 @@ def run_fission_pipeline(self, fission_id: int, source_task_id: int) -> dict:
|
||||
finally:
|
||||
_r.delete(lock_key)
|
||||
db.close()
|
||||
|
||||
|
||||
@celery_app.task(
|
||||
bind=True,
|
||||
name="app.workers.tasks.retry_fission_images",
|
||||
max_retries=1,
|
||||
default_retry_delay=20,
|
||||
queue="generation",
|
||||
)
|
||||
def retry_fission_images(self, fission_id: int, note_id: int) -> dict:
|
||||
"""裂变补图:对某一套只重生标了 error 的失败分镜(中转站波动兜底)。
|
||||
|
||||
只接收 id(基石B),不接收 key。复用源产品参考图与 image_count。
|
||||
解密 key→构建 clients→委托 fission_image_retry.retry_fission_note_images。
|
||||
"""
|
||||
logger.info("retry_fission_images start: fission_id=%s note_id=%s", fission_id, note_id)
|
||||
lock_key = f"fission:retry:lock:{note_id}"
|
||||
import redis as _redis
|
||||
from app.core.config import get_settings as _gs
|
||||
_r = _redis.from_url(_gs().REDIS_URL, decode_responses=True)
|
||||
if not _r.set(lock_key, "1", nx=True, ex=1800):
|
||||
logger.warning("retry_fission_images 跳过(已在跑): note_id=%s", note_id)
|
||||
return {"note_id": note_id, "status": "skipped_locked"}
|
||||
|
||||
db = _get_db()
|
||||
try:
|
||||
from app.models.fission import FissionTask
|
||||
from app.models.task import GenerationTask
|
||||
from app.models.product import Product
|
||||
from app.workers.pipeline_steps import (
|
||||
decrypt_user_key, build_clients_and_clear_key, build_product_dict,
|
||||
)
|
||||
from app.services.fission_image_retry import retry_fission_note_images
|
||||
|
||||
ft = db.query(FissionTask).filter(FissionTask.id == fission_id).first()
|
||||
if not ft:
|
||||
return {"note_id": note_id, "status": "not_found"}
|
||||
if not ft.source_task_id:
|
||||
return {"note_id": note_id, "status": "no_source_task"}
|
||||
src = db.query(GenerationTask).filter(
|
||||
GenerationTask.id == ft.source_task_id,
|
||||
).first()
|
||||
if not src:
|
||||
return {"note_id": note_id, "status": "source_task_gone"}
|
||||
product_row = db.query(Product).filter(Product.id == src.product_id).first()
|
||||
if not product_row:
|
||||
return {"note_id": note_id, "status": "no_product"}
|
||||
|
||||
# key 归属源任务 operator,与首轮生图一致(基石B:不接收明文 key)
|
||||
plain_key = decrypt_user_key(db, src.operator_id, ft.workspace_id)
|
||||
clients = build_clients_and_clear_key(plain_key)
|
||||
plain_key = None # 用完即清,基石B
|
||||
|
||||
product = build_product_dict(product_row)
|
||||
image_count = max(1, (src.image_count or 3))
|
||||
result = retry_fission_note_images(
|
||||
db, clients, ft, product, image_count, note_id,
|
||||
)
|
||||
result["status"] = "completed"
|
||||
return result
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.error("retry_fission_images failed: note_id=%s err=%s", note_id, exc)
|
||||
exhausted = self.request.retries >= self.max_retries
|
||||
if exhausted:
|
||||
return {"note_id": note_id, "status": "failed", "error": str(exc)}
|
||||
raise self.retry(exc=exc)
|
||||
finally:
|
||||
_r.delete(lock_key)
|
||||
db.close()
|
||||
|
||||
Reference in New Issue
Block a user