From 4bfe456ca7c1dd8b4cc0cf3f4ee4872d238b14f2 Mon Sep 17 00:00:00 2001 From: SaaS Frontend Agent Date: Fri, 7 Aug 2026 22:15:40 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20#1286=20can=5Fgenerate=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=9C=80=E5=90=8E=E9=98=B2=E7=BA=BF=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=8C=E8=A7=A3=E5=86=B3=E3=80=8C=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E5=8F=AF=E6=B8=B2=E6=9F=93=E7=9A=84=E5=B0=B1=E7=BB=AA?= =?UTF-8?q?=E7=89=87=E6=AE=B5=E3=80=8D=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:智能剪辑流程走完素材选择后,can_generate 在 mark_clips_ready 之前执行, fallback 链路任何一环出问题都导致最终验证失败。 修复: 1. can_generate 内部增加自动修复:clips 无素材但 config.asset_ids 非空时, 直接分配素材,不再依赖前置 fallback 链路 2. _auto_fallback_assign_assets 增加详细诊断日志和异常捕获 3. generation.py fallback 链路结束后增加防御性诊断日志 新增 10 个单元测试覆盖自动修复各场景。 --- .../api/routes/templates_editor/_fallback.py | 46 ++- .../api/routes/templates_editor/generation.py | 17 +- apps/api/app/services/edit_plan_service.py | 39 +- .../test_1286_can_generate_auto_repair.py | 332 ++++++++++++++++++ 4 files changed, 428 insertions(+), 6 deletions(-) create mode 100644 tests/unit/test_1286_can_generate_auto_repair.py diff --git a/apps/api/app/api/routes/templates_editor/_fallback.py b/apps/api/app/api/routes/templates_editor/_fallback.py index dc745f9f6..deafea778 100755 --- a/apps/api/app/api/routes/templates_editor/_fallback.py +++ b/apps/api/app/api/routes/templates_editor/_fallback.py @@ -98,6 +98,15 @@ def _auto_fallback_assign_assets( clips_without_asset = [c for c in all_clips if not c.asset_id] config_asset_ids = (plan_check.config or {}).get("asset_ids", []) + logger.info( + "模板编辑器自动兜底3 诊断: plan=%s total_clips=%d " + "clips_without_asset=%d config_asset_ids=%r", + plan_id, + len(all_clips), + len(clips_without_asset), + config_asset_ids[:5] if config_asset_ids else [], + ) + if clips_without_asset and config_asset_ids: logger.info( "模板编辑器自动兜底3: plan=%s 为 %d 个无素材片段分配 %d 个指定素材", @@ -105,11 +114,42 @@ def _auto_fallback_assign_assets( len(clips_without_asset), len(config_asset_ids), ) + assigned = 0 for i, clip in enumerate(clips_without_asset): asset_idx = i % len(config_asset_ids) - svc.assign_asset(clip.id, config_asset_ids[asset_idx]) - logger.info("模板编辑器自动兜底3: plan=%s 素材分配完成", plan_id) - clips_without_asset = [] + try: + svc.assign_asset(clip.id, config_asset_ids[asset_idx]) + assigned += 1 + except Exception as exc: + logger.error( + "模板编辑器自动兜底3: plan=%s clip=%s 分配素材 %s 失败: %s", + plan_id, + clip.id, + config_asset_ids[asset_idx], + exc, + ) + logger.info( + "模板编辑器自动兜底3: plan=%s 素材分配完成 assigned=%d/%d", + plan_id, + assigned, + len(clips_without_asset), + ) + # 重新检查剩余无素材片段 + all_clips_after = svc.list_clips(plan_id) + clips_without_asset = [c for c in all_clips_after if not c.asset_id] + if clips_without_asset: + logger.warning( + "模板编辑器自动兜底3: plan=%s 仍有 %d 个片段无素材", + plan_id, + len(clips_without_asset), + ) + elif not clips_without_asset: + logger.info("模板编辑器自动兜底3: plan=%s 所有片段已有素材,跳过", plan_id) + elif not config_asset_ids: + logger.info( + "模板编辑器自动兜底3: plan=%s config.asset_ids 为空,跳过分配", + plan_id, + ) return clips_without_asset diff --git a/apps/api/app/api/routes/templates_editor/generation.py b/apps/api/app/api/routes/templates_editor/generation.py index f90303b43..41fccfdce 100755 --- a/apps/api/app/api/routes/templates_editor/generation.py +++ b/apps/api/app/api/routes/templates_editor/generation.py @@ -74,7 +74,22 @@ def generate_editor_draft( plan_svc, plan_id, plan_check, clips_without_asset, asset_library_repo, asset_repo ) - # 检查是否可生成 + # ── 防御性诊断日志:fallback 链路结束后、can_generate 前 ── + _post_fallback_plan = plan_svc.get_plan_or_raise(plan_id) + _post_fallback_clips = plan_svc.list_clips(plan_id) + _post_fallback_asset_ids = (_post_fallback_plan.config or {}).get("asset_ids", []) + _post_fallback_clips_with_asset = sum(1 for c in _post_fallback_clips if c.asset_id) + logger.info( + "模板编辑器 fallback 链路完成: plan=%s status=%s " + "total_clips=%d clips_with_asset=%d config_asset_ids_count=%d", + plan_id, + _post_fallback_plan.status, + len(_post_fallback_clips), + _post_fallback_clips_with_asset, + len(_post_fallback_asset_ids), + ) + + # 检查是否可生成(含最后防线自动修复) try: can_gen, reason = plan_svc.can_generate(plan_id) except ValueError as exc: diff --git a/apps/api/app/services/edit_plan_service.py b/apps/api/app/services/edit_plan_service.py index 195d6321b..efb6db326 100755 --- a/apps/api/app/services/edit_plan_service.py +++ b/apps/api/app/services/edit_plan_service.py @@ -571,6 +571,10 @@ class EditPlanService: def can_generate(self, plan_id: str) -> tuple[bool, str]: """检查是否可以触发渲染 + 包含最后一道防线的自动修复: + - 如果 clips 存在但都没有 asset_id,且 config.asset_ids 非空, + 直接在内部执行素材分配,不再依赖前置 fallback 链路。 + Returns: tuple: (can_generate, reason) """ @@ -586,8 +590,39 @@ class EditPlanService: return False, "请先添加片段后再生成视频" # 检查是否至少有一个片段分配了素材 - if not any(c.asset_id for c in clips): - return False, "没有可渲染的就绪片段,请确保已选择素材" + has_asset = any(c.asset_id for c in clips) + if not has_asset: + # ── 最后防线:自动从 config.asset_ids 分配素材 ── + config_asset_ids = (plan.config or {}).get("asset_ids", []) + if config_asset_ids: + logger.warning( + "can_generate 最后防线触发: plan=%s clips=%d 均无素材," + "从 config.asset_ids(%d个) 自动分配", + plan_id, + len(clips), + len(config_asset_ids), + ) + clips_without_asset = [c for c in clips if not c.asset_id] + for i, clip in enumerate(clips_without_asset): + asset_idx = i % len(config_asset_ids) + self.assign_asset(clip.id, config_asset_ids[asset_idx]) + logger.info( + "can_generate 最后防线: plan=%s 已为 %d 个片段分配素材", + plan_id, + len(clips_without_asset), + ) + # 重新加载 clips 验证分配结果 + clips = self._clip_repo.list_by_plan(plan_id) + if not any(c.asset_id for c in clips): + return False, "没有可渲染的就绪片段,自动修复后仍未分配素材" + else: + logger.warning( + "can_generate 失败: plan=%s clips=%d 均无素材," + "且 config.asset_ids 为空,无法自动修复", + plan_id, + len(clips), + ) + return False, "没有可渲染的就绪片段,请确保已选择素材" return True, "" diff --git a/tests/unit/test_1286_can_generate_auto_repair.py b/tests/unit/test_1286_can_generate_auto_repair.py new file mode 100644 index 000000000..8f1fc4035 --- /dev/null +++ b/tests/unit/test_1286_can_generate_auto_repair.py @@ -0,0 +1,332 @@ +""" +#1286 can_generate 最后防线自动修复 — 单元测试 + +覆盖场景: +1. clips 无素材 + config.asset_ids 非空 → 自动分配成功 → can_generate 返回 True +2. clips 无素材 + config.asset_ids 为空 → 无法修复 → can_generate 返回 False +3. clips 已有素材 → 正常通过,不触发自动修复 +4. 自动修复后素材数量与 clips 数量一致(循环分配验证) +5. 自动修复不影响预览生成流程(预览不依赖 EditPlan clips) +6. config 为 None 时安全降级 +7. 无 clips 时仍返回 False +8. 部分 clips 有素材时正常通过(不触发自动修复) +""" + +from __future__ import annotations + +import os +import sys +from pathlib import Path +from typing import Any, List, Optional +from unittest.mock import MagicMock + +os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing") +os.environ.setdefault("DATABASE_URL", "sqlite:///test.db") + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api")) + +from packages.domain.edit_plan import EditPlan, EditPlanStatus +from packages.domain.edit_plan_clip import EditPlanClip, EditPlanClipStatus + + +# --------------------------------------------------------------------------- +# Stub Repositories +# --------------------------------------------------------------------------- + + +class StubEditPlanRepository: + def __init__(self) -> None: + self._plans: dict[str, EditPlan] = {} + self._counter = 0 + + def _next_id(self) -> str: + self._counter += 1 + return f"plan-auto-{self._counter:03d}" + + def get(self, plan_id: str) -> Optional[EditPlan]: + return self._plans.get(plan_id) + + def create(self, plan: EditPlan) -> EditPlan: + if not plan.id: + plan = EditPlan( + id=self._next_id(), + template_id=plan.template_id, + name=plan.name, + status=plan.status, + total_duration=plan.total_duration, + source_edit_plan_id=plan.source_edit_plan_id, + project_id=plan.project_id, + created_by_user_id=plan.created_by_user_id, + config=plan.config, + ) + self._plans[plan.id] = plan + return plan + + def update(self, plan: EditPlan) -> EditPlan: + self._plans[plan.id] = plan + return plan + + def delete(self, plan_id: str) -> bool: + return self._plans.pop(plan_id, None) is not None + + def list_all(self, **kwargs): + return list(self._plans.values()) + + +class StubEditPlanClipRepository: + def __init__(self) -> None: + self._clips: dict[str, EditPlanClip] = {} + self._counter = 0 + + def _next_id(self) -> str: + self._counter += 1 + return f"clip-auto-{self._counter:03d}" + + def get(self, clip_id: str) -> Optional[EditPlanClip]: + return self._clips.get(clip_id) + + def create(self, clip: EditPlanClip) -> EditPlanClip: + if not clip.id: + clip = EditPlanClip( + id=self._next_id(), + plan_id=clip.plan_id, + clip_type=clip.clip_type, + order=clip.order, + duration=clip.duration, + status=clip.status, + asset_id=clip.asset_id, + text_content=clip.text_content, + config=clip.config, + template_clip_config_id=clip.template_clip_config_id, + transition_effect=clip.transition_effect, + ) + self._clips[clip.id] = clip + return clip + + def update(self, clip: EditPlanClip) -> EditPlanClip: + self._clips[clip.id] = clip + return clip + + def delete(self, clip_id: str) -> bool: + return self._clips.pop(clip_id, None) is not None + + def list_by_plan( + self, + plan_id: str, + *, + status: Optional[EditPlanClipStatus] = None, + skip: int = 0, + limit: int = 100, + ) -> List[EditPlanClip]: + clips = [c for c in self._clips.values() if c.plan_id == plan_id] + if status is not None: + clips = [c for c in clips if c.status == status] + return sorted(clips, key=lambda c: c.order)[skip : skip + limit] + + def delete_by_plan(self, plan_id: str) -> int: + to_del = [cid for cid, c in self._clips.items() if c.plan_id == plan_id] + for cid in to_del: + del self._clips[cid] + return len(to_del) + + def count_by_plan(self, plan_id: str) -> int: + return sum(1 for c in self._clips.values() if c.plan_id == plan_id) + + +class StubGenerationTaskRepository: + def __init__(self): + pass + + +def _make_service(): + from app.services.edit_plan_service import EditPlanService + + db = MagicMock() + svc = EditPlanService(db) + svc._plan_repo = StubEditPlanRepository() + svc._clip_repo = StubEditPlanClipRepository() + svc._generation_task_repo = StubGenerationTaskRepository() + return svc + + +# =========================================================================== +# 测试用例 +# =========================================================================== + + +class TestCanGenerateAutoRepair: + """#1286 can_generate 最后防线自动修复""" + + def test_auto_repair_with_config_asset_ids(self): + """clips 无素材 + config.asset_ids 非空 → 自动分配成功 → can_generate True""" + svc = _make_service() + p = svc.create_plan( + "tpl-001", + "测试", + config={"asset_ids": ["asset-001", "asset-002"]}, + ) + svc.transition_status(p.id, EditPlanStatus.EDITING) + svc.create_clip(p.id, "intro", 0) + svc.create_clip(p.id, "main", 1) + + # clips 无素材 + clips = svc.list_clips(p.id) + assert all(not c.asset_id for c in clips) + + # can_generate 应触发自动修复 + can, reason = svc.can_generate(p.id) + assert can is True + assert reason == "" + + # 验证 clips 已被分配素材 + clips_after = svc.list_clips(p.id) + assert all(c.asset_id is not None for c in clips_after) + + def test_auto_repair_fails_without_config_asset_ids(self): + """clips 无素材 + config.asset_ids 为空 → 无法修复 → False""" + svc = _make_service() + p = svc.create_plan("tpl-001", "测试", config={}) + svc.transition_status(p.id, EditPlanStatus.EDITING) + svc.create_clip(p.id, "intro", 0) + + can, reason = svc.can_generate(p.id) + assert can is False + assert "没有可渲染" in reason or "素材" in reason + + def test_no_repair_when_clips_have_assets(self): + """clips 已有素材 → 正常通过,不触发自动修复""" + svc = _make_service() + p = svc.create_plan( + "tpl-001", + "测试", + config={"asset_ids": ["asset-001", "asset-002"]}, + ) + svc.transition_status(p.id, EditPlanStatus.EDITING) + clip = svc.create_clip(p.id, "intro", 0) + svc.assign_asset(clip.id, "asset-001") + + can, reason = svc.can_generate(p.id) + assert can is True + # 确认 clip 的 asset_id 没有被改变 + clips = svc.list_clips(p.id) + assert clips[0].asset_id == "asset-001" + + def test_auto_repair_circular_assignment(self): + """素材少于 clips 时循环分配(取模)""" + svc = _make_service() + p = svc.create_plan( + "tpl-001", + "测试", + config={"asset_ids": ["asset-A"]}, + ) + svc.transition_status(p.id, EditPlanStatus.EDITING) + svc.create_clip(p.id, "intro", 0) + svc.create_clip(p.id, "main", 1) + svc.create_clip(p.id, "outro", 2) + + can, reason = svc.can_generate(p.id) + assert can is True + + clips = svc.list_clips(p.id) + # 所有 3 个 clips 都应被分配了同一个 asset-A + assert all(c.asset_id == "asset-A" for c in clips) + + def test_auto_repair_does_not_affect_preview_flow(self): + """预览生成走 generation_preview.py,不依赖 EditPlan clips 的 can_generate""" + svc = _make_service() + p = svc.create_plan( + "tpl-001", + "测试", + config={"asset_ids": ["asset-001"]}, + ) + # 不切到 editing 状态,模拟预览场景 + svc.create_clip(p.id, "intro", 0) + + can, reason = svc.can_generate(p.id) + assert can is False + assert "编辑" in reason or "模板" in reason + + def test_auto_repair_empty_config(self): + """config 为 None 时也不报错""" + svc = _make_service() + p = svc.create_plan("tpl-001", "测试") + svc.transition_status(p.id, EditPlanStatus.EDITING) + svc.create_clip(p.id, "intro", 0) + + can, reason = svc.can_generate(p.id) + assert can is False + assert "没有可渲染" in reason or "素材" in reason + + def test_auto_repair_no_clips_still_fails(self): + """没有 clips 时仍然返回 False(不进入自动修复分支)""" + svc = _make_service() + p = svc.create_plan( + "tpl-001", + "测试", + config={"asset_ids": ["asset-001"]}, + ) + svc.transition_status(p.id, EditPlanStatus.EDITING) + + can, reason = svc.can_generate(p.id) + assert can is False + assert "请先添加片段" in reason + + def test_auto_repair_partial_assets_still_passes(self): + """部分 clips 有素材、部分没有 → 至少有一个有素材 → 通过(原有逻辑)""" + svc = _make_service() + p = svc.create_plan( + "tpl-001", + "测试", + config={"asset_ids": ["asset-001"]}, + ) + svc.transition_status(p.id, EditPlanStatus.EDITING) + clip1 = svc.create_clip(p.id, "intro", 0) + svc.create_clip(p.id, "main", 1) + svc.assign_asset(clip1.id, "asset-001") + + # 至少一个 clip 有素材 → 通过(不触发自动修复) + can, reason = svc.can_generate(p.id) + assert can is True + + def test_auto_repair_multiple_assets_distributed(self): + """多个素材按顺序分配给多个 clips""" + svc = _make_service() + p = svc.create_plan( + "tpl-001", + "测试", + config={"asset_ids": ["asset-A", "asset-B", "asset-C"]}, + ) + svc.transition_status(p.id, EditPlanStatus.EDITING) + svc.create_clip(p.id, "intro", 0) + svc.create_clip(p.id, "main", 1) + svc.create_clip(p.id, "outro", 2) + + can, reason = svc.can_generate(p.id) + assert can is True + + clips = svc.list_clips(p.id) + assert clips[0].asset_id == "asset-A" + assert clips[1].asset_id == "asset-B" + assert clips[2].asset_id == "asset-C" + + def test_auto_repair_idempotent_on_second_call(self): + """第二次调用 can_generate 不会重复分配(已有素材则跳过自动修复)""" + svc = _make_service() + p = svc.create_plan( + "tpl-001", + "测试", + config={"asset_ids": ["asset-001"]}, + ) + svc.transition_status(p.id, EditPlanStatus.EDITING) + svc.create_clip(p.id, "intro", 0) + + # 第一次调用触发自动修复 + can1, _ = svc.can_generate(p.id) + assert can1 is True + + # 第二次调用应该直接通过,不再触发修复 + can2, reason2 = svc.can_generate(p.id) + assert can2 is True + assert reason2 == "" -- 2.54.0 From aa49c5fd861293e6f1222cd69d8e4eb0532197eb Mon Sep 17 00:00:00 2001 From: CI Bot Date: Fri, 7 Aug 2026 14:21:01 +0000 Subject: [PATCH 2/4] style: auto-format with black + isort + prettier [skip ci-format-check] --- apps/api/app/services/edit_plan_service.py | 6 ++---- tests/unit/test_1286_can_generate_auto_repair.py | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/api/app/services/edit_plan_service.py b/apps/api/app/services/edit_plan_service.py index efb6db326..ef8e81f35 100755 --- a/apps/api/app/services/edit_plan_service.py +++ b/apps/api/app/services/edit_plan_service.py @@ -596,8 +596,7 @@ class EditPlanService: config_asset_ids = (plan.config or {}).get("asset_ids", []) if config_asset_ids: logger.warning( - "can_generate 最后防线触发: plan=%s clips=%d 均无素材," - "从 config.asset_ids(%d个) 自动分配", + "can_generate 最后防线触发: plan=%s clips=%d 均无素材," "从 config.asset_ids(%d个) 自动分配", plan_id, len(clips), len(config_asset_ids), @@ -617,8 +616,7 @@ class EditPlanService: return False, "没有可渲染的就绪片段,自动修复后仍未分配素材" else: logger.warning( - "can_generate 失败: plan=%s clips=%d 均无素材," - "且 config.asset_ids 为空,无法自动修复", + "can_generate 失败: plan=%s clips=%d 均无素材," "且 config.asset_ids 为空,无法自动修复", plan_id, len(clips), ) diff --git a/tests/unit/test_1286_can_generate_auto_repair.py b/tests/unit/test_1286_can_generate_auto_repair.py index 8f1fc4035..4562983c5 100644 --- a/tests/unit/test_1286_can_generate_auto_repair.py +++ b/tests/unit/test_1286_can_generate_auto_repair.py @@ -30,7 +30,6 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api")) from packages.domain.edit_plan import EditPlan, EditPlanStatus from packages.domain.edit_plan_clip import EditPlanClip, EditPlanClipStatus - # --------------------------------------------------------------------------- # Stub Repositories # --------------------------------------------------------------------------- -- 2.54.0 From afc46a97cfb6248f99c10cf91aa024285bb9b171 Mon Sep 17 00:00:00 2001 From: SaaS Frontend Agent Date: Fri, 7 Aug 2026 22:27:48 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20AI=20Code=20Rev?= =?UTF-8?q?iew=20=E9=98=BB=E5=A1=9E=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. can_generate 自动修复中 assign_asset 加 try-except,避免单个失败导致整体崩溃 2. 移除 generation.py 冗余 DB 查询,诊断日志移入 can_generate 内部 3. 增加详细诊断日志:clips_with_asset 数量、config_asset_ids 数量 --- .../api/routes/templates_editor/generation.py | 17 +----------- apps/api/app/services/edit_plan_service.py | 27 +++++++++++++++++-- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/apps/api/app/api/routes/templates_editor/generation.py b/apps/api/app/api/routes/templates_editor/generation.py index 41fccfdce..c9bda4ce1 100755 --- a/apps/api/app/api/routes/templates_editor/generation.py +++ b/apps/api/app/api/routes/templates_editor/generation.py @@ -74,22 +74,7 @@ def generate_editor_draft( plan_svc, plan_id, plan_check, clips_without_asset, asset_library_repo, asset_repo ) - # ── 防御性诊断日志:fallback 链路结束后、can_generate 前 ── - _post_fallback_plan = plan_svc.get_plan_or_raise(plan_id) - _post_fallback_clips = plan_svc.list_clips(plan_id) - _post_fallback_asset_ids = (_post_fallback_plan.config or {}).get("asset_ids", []) - _post_fallback_clips_with_asset = sum(1 for c in _post_fallback_clips if c.asset_id) - logger.info( - "模板编辑器 fallback 链路完成: plan=%s status=%s " - "total_clips=%d clips_with_asset=%d config_asset_ids_count=%d", - plan_id, - _post_fallback_plan.status, - len(_post_fallback_clips), - _post_fallback_clips_with_asset, - len(_post_fallback_asset_ids), - ) - - # 检查是否可生成(含最后防线自动修复) + # 检查是否可生成(含最后防线自动修复 + 诊断日志) try: can_gen, reason = plan_svc.can_generate(plan_id) except ValueError as exc: diff --git a/apps/api/app/services/edit_plan_service.py b/apps/api/app/services/edit_plan_service.py index ef8e81f35..783ab0c76 100755 --- a/apps/api/app/services/edit_plan_service.py +++ b/apps/api/app/services/edit_plan_service.py @@ -591,6 +591,17 @@ class EditPlanService: # 检查是否至少有一个片段分配了素材 has_asset = any(c.asset_id for c in clips) + config_asset_ids_count = len((plan.config or {}).get("asset_ids", [])) + clips_with_asset_count = sum(1 for c in clips if c.asset_id) + logger.info( + "can_generate 诊断: plan=%s status=%s total_clips=%d " + "clips_with_asset=%d config_asset_ids_count=%d", + plan_id, + plan.status, + len(clips), + clips_with_asset_count, + config_asset_ids_count, + ) if not has_asset: # ── 最后防线:自动从 config.asset_ids 分配素材 ── config_asset_ids = (plan.config or {}).get("asset_ids", []) @@ -602,12 +613,24 @@ class EditPlanService: len(config_asset_ids), ) clips_without_asset = [c for c in clips if not c.asset_id] + assigned_count = 0 for i, clip in enumerate(clips_without_asset): asset_idx = i % len(config_asset_ids) - self.assign_asset(clip.id, config_asset_ids[asset_idx]) + try: + self.assign_asset(clip.id, config_asset_ids[asset_idx]) + assigned_count += 1 + except Exception as exc: + logger.warning( + "can_generate 最后防线: plan=%s clip=%s 分配素材 %s 失败: %s", + plan_id, + clip.id, + config_asset_ids[asset_idx], + exc, + ) logger.info( - "can_generate 最后防线: plan=%s 已为 %d 个片段分配素材", + "can_generate 最后防线: plan=%s 已为 %d/%d 个片段分配素材", plan_id, + assigned_count, len(clips_without_asset), ) # 重新加载 clips 验证分配结果 -- 2.54.0 From d969f1dfa672c88356077787fd6b6a5db150a783 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Fri, 7 Aug 2026 14:33:13 +0000 Subject: [PATCH 4/4] style: auto-format with black + isort + prettier [skip ci-format-check] --- apps/api/app/services/edit_plan_service.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/api/app/services/edit_plan_service.py b/apps/api/app/services/edit_plan_service.py index 783ab0c76..f28a2d08a 100755 --- a/apps/api/app/services/edit_plan_service.py +++ b/apps/api/app/services/edit_plan_service.py @@ -594,8 +594,7 @@ class EditPlanService: config_asset_ids_count = len((plan.config or {}).get("asset_ids", [])) clips_with_asset_count = sum(1 for c in clips if c.asset_id) logger.info( - "can_generate 诊断: plan=%s status=%s total_clips=%d " - "clips_with_asset=%d config_asset_ids_count=%d", + "can_generate 诊断: plan=%s status=%s total_clips=%d " "clips_with_asset=%d config_asset_ids_count=%d", plan_id, plan.status, len(clips), -- 2.54.0