From bee52f04b7ba8ffa6492cbd59246bec22ffe6628 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Wed, 15 Jul 2026 23:56:03 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E5=89=AA=E8=BE=91=E8=AE=A1=E5=88=92?= =?UTF-8?q?'=E7=BC=96=E8=BE=91=E5=86=8D=E7=94=9F=E6=88=90'P1=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20-=20completed/failed=E5=90=8E=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=88=87=E5=9B=9Eediting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EditPlan新增resume_editing方法(completed/failed → editing) - transition_status支持从completed/failed切回editing - 5个编辑操作入口自动回退状态:update_plan / update_plan_config / create_clip / update_clip / assign_asset - 新增7个单元测试,覆盖各种编辑操作触发重新编辑的场景 - 112个相关单测全绿,零回归 --- apps/api/app/services/edit_plan_service.py | 47 ++++++++++- packages/domain/edit_plan.py | 7 ++ tests/unit/test_edit_plan_service.py | 91 ++++++++++++++++++++++ 3 files changed, 144 insertions(+), 1 deletion(-) diff --git a/apps/api/app/services/edit_plan_service.py b/apps/api/app/services/edit_plan_service.py index 3b178de09..b3dfc0743 100755 --- a/apps/api/app/services/edit_plan_service.py +++ b/apps/api/app/services/edit_plan_service.py @@ -141,6 +141,19 @@ class EditPlanService: logger.info("创建剪辑计划: id=%s name=%s", created.id, created.name) return created + def _auto_resume_editing(self, plan_id: str) -> None: + """如果计划处于 completed/failed 状态,自动切回 editing(编辑操作前置)""" + plan = self._plan_repo.get(plan_id) + if plan is None: + return + if plan.status in (EditPlanStatus.COMPLETED, EditPlanStatus.FAILED): + try: + plan.resume_editing() + self._plan_repo.update(plan) + logger.info("自动重新编辑: plan_id=%s", plan_id) + except ValueError: + pass + def update_plan( self, plan_id: str, @@ -156,6 +169,10 @@ class EditPlanService: """ existing = self.get_plan_or_raise(plan_id) + # 自动从 completed/failed 切回 editing + self._auto_resume_editing(plan_id) + existing = self.get_plan_or_raise(plan_id) + updated = EditPlan( id=existing.id, template_id=existing.template_id, @@ -212,8 +229,26 @@ class EditPlanService: return plan # 根据目标状态调用对应的状态机方法 + # EDITING 支持从 draft / completed / failed 进入 + if target_status == EditPlanStatus.EDITING: + if plan.status == EditPlanStatus.DRAFT: + plan.start_editing() + elif plan.status in (EditPlanStatus.COMPLETED, EditPlanStatus.FAILED): + plan.resume_editing() + else: + raise ValueError( + f"无法从 {plan.status} 切换到 {target_status}" + ) + result = self._plan_repo.update(plan) + logger.info( + "状态流转: plan_id=%s %s → %s", + plan_id, + plan.status, + target_status, + ) + return result + transition_map = { - EditPlanStatus.EDITING: plan.start_editing, EditPlanStatus.RENDERING: plan.start_rendering, EditPlanStatus.COMPLETED: plan.mark_completed, EditPlanStatus.FAILED: plan.mark_failed, @@ -292,6 +327,8 @@ class EditPlanService: """ # 确保计划存在 self.get_plan_or_raise(plan_id) + # 自动从 completed/failed 切回 editing + self._auto_resume_editing(plan_id) clip = EditPlanClip.create( plan_id=plan_id, @@ -339,6 +376,9 @@ class EditPlanService: """ existing = self.get_clip_or_raise(clip_id) + # 自动从 completed/failed 切回 editing + self._auto_resume_editing(existing.plan_id) + # 速度边界钳制 if playback_speed is not None: if playback_speed <= 0: @@ -381,6 +421,8 @@ class EditPlanService: ValueError: 片段不存在或 asset_id 为空 """ clip = self.get_clip_or_raise(clip_id) + # 自动从 completed/failed 切回 editing + self._auto_resume_editing(clip.plan_id) clip.assign_asset(asset_id) result = self._clip_repo.update(clip) logger.info("分配素材: clip_id=%s asset_id=%s", clip_id, asset_id) @@ -511,6 +553,9 @@ class EditPlanService: 更新后的计划 """ plan = self.get_plan_or_raise(plan_id) + # 自动从 completed/failed 切回 editing + self._auto_resume_editing(plan_id) + plan = self.get_plan_or_raise(plan_id) new_config = {**plan.config, **config_updates} updated = EditPlan( diff --git a/packages/domain/edit_plan.py b/packages/domain/edit_plan.py index 4d8ad875b..7b7467da6 100755 --- a/packages/domain/edit_plan.py +++ b/packages/domain/edit_plan.py @@ -110,6 +110,13 @@ class EditPlan: self.status = EditPlanStatus.FAILED self.updated_at = datetime.now(timezone.utc) + def resume_editing(self) -> None: + """重新进入编辑状态(完成/失败后重新编辑)""" + if self.status not in (EditPlanStatus.COMPLETED, EditPlanStatus.FAILED): + raise ValueError(f"只有 completed/failed 状态的计划可以重新编辑,当前状态: {self.status}") + self.status = EditPlanStatus.EDITING + self.updated_at = datetime.now(timezone.utc) + def reset_to_draft(self) -> None: """重置为草稿状态(仅从 failed 状态可重置)""" if self.status != EditPlanStatus.FAILED: diff --git a/tests/unit/test_edit_plan_service.py b/tests/unit/test_edit_plan_service.py index f67335642..aae0e2b5e 100755 --- a/tests/unit/test_edit_plan_service.py +++ b/tests/unit/test_edit_plan_service.py @@ -636,3 +636,94 @@ class TestGenerationWorkflow: p = svc.create_plan("tpl-001", "测试", config={"key1": "val1"}) updated = svc.update_plan_config(p.id, {"key1": "new_val"}) assert updated.config["key1"] == "new_val" + + +# ── 重新编辑 & 再生成 ──────────────────────────────────────────────────────── + + +class TestResumeEditingAndRegenerate: + """完成/失败后重新编辑 → 再生成的状态流转测试""" + + def test_update_plan_from_completed_returns_to_editing(self): + """更新计划配置:completed → 自动切回 editing""" + svc = _make_service() + p = svc.create_plan("tpl-001", "测试") + svc.transition_status(p.id, EditPlanStatus.EDITING) + svc.transition_status(p.id, EditPlanStatus.RENDERING) + svc.transition_status(p.id, EditPlanStatus.COMPLETED) + + updated = svc.update_plan(p.id, name="新名字") + assert updated.status == EditPlanStatus.EDITING + assert updated.name == "新名字" + + def test_update_plan_config_from_completed_returns_to_editing(self): + """update_plan_config: completed → 自动切回 editing""" + svc = _make_service() + p = svc.create_plan("tpl-001", "测试") + svc.transition_status(p.id, EditPlanStatus.EDITING) + svc.transition_status(p.id, EditPlanStatus.RENDERING) + svc.transition_status(p.id, EditPlanStatus.COMPLETED) + + updated = svc.update_plan_config(p.id, {"foo": "bar"}) + assert updated.status == EditPlanStatus.EDITING + + def test_create_clip_from_completed_returns_to_editing(self): + """创建片段:completed → 自动切回 editing""" + svc = _make_service() + p = svc.create_plan("tpl-001", "测试") + svc.transition_status(p.id, EditPlanStatus.EDITING) + svc.transition_status(p.id, EditPlanStatus.RENDERING) + svc.transition_status(p.id, EditPlanStatus.COMPLETED) + + svc.create_clip(p.id, "main", 0) + plan_after = svc.get_plan(p.id) + assert plan_after.status == EditPlanStatus.EDITING + + def test_assign_asset_from_failed_returns_to_editing(self): + """分配素材:failed → 自动切回 editing""" + svc = _make_service() + p = svc.create_plan("tpl-001", "测试") + clip = svc.create_clip(p.id, "main", 0) + svc.transition_status(p.id, EditPlanStatus.EDITING) + svc.transition_status(p.id, EditPlanStatus.RENDERING) + svc.transition_status(p.id, EditPlanStatus.FAILED) + + svc.assign_asset(clip.id, "asset-001") + plan_after = svc.get_plan(p.id) + assert plan_after.status == EditPlanStatus.EDITING + + def test_completed_can_regenerate_after_edit(self): + """完成后编辑 → can_generate 返回 True,可再生成""" + svc = _make_service() + p = svc.create_plan("tpl-001", "测试") + svc.create_clip(p.id, "main", 0) + svc.transition_status(p.id, EditPlanStatus.EDITING) + svc.transition_status(p.id, EditPlanStatus.RENDERING) + svc.transition_status(p.id, EditPlanStatus.COMPLETED) + + # 完成后不能直接生成 + can, reason = svc.can_generate(p.id) + assert not can + assert "编辑" in reason + + # 编辑后自动切回 editing,可以生成 + svc.update_plan_config(p.id, {"edited": True}) + can, reason = svc.can_generate(p.id) + assert can, f"期望可生成,实际: {reason}" + + def test_transition_completed_to_editing_via_service(self): + """通过 transition_status 从 completed 切到 editing""" + svc = _make_service() + p = svc.create_plan("tpl-001", "测试") + svc.transition_status(p.id, EditPlanStatus.EDITING) + svc.transition_status(p.id, EditPlanStatus.RENDERING) + svc.transition_status(p.id, EditPlanStatus.COMPLETED) + + result = svc.transition_status(p.id, EditPlanStatus.EDITING) + assert result.status == EditPlanStatus.EDITING + + def test_resume_editing_from_draft_raises(self): + """从 draft 直接 resume_editing 应该报错""" + p = EditPlan.create("tpl-001", "测试") + with pytest.raises(ValueError): + p.resume_editing() -- 2.54.0 From fb1fc1cff2a1cd15bff77cda2b9887e4d033a59a Mon Sep 17 00:00:00 2001 From: CI Bot Date: Thu, 16 Jul 2026 08:03:01 +0800 Subject: [PATCH 2/2] =?UTF-8?q?style:=20black=E6=A0=BC=E5=BC=8F=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/app/services/edit_plan_service.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/api/app/services/edit_plan_service.py b/apps/api/app/services/edit_plan_service.py index b3dfc0743..5d3c90c90 100755 --- a/apps/api/app/services/edit_plan_service.py +++ b/apps/api/app/services/edit_plan_service.py @@ -236,9 +236,7 @@ class EditPlanService: elif plan.status in (EditPlanStatus.COMPLETED, EditPlanStatus.FAILED): plan.resume_editing() else: - raise ValueError( - f"无法从 {plan.status} 切换到 {target_status}" - ) + raise ValueError(f"无法从 {plan.status} 切换到 {target_status}") result = self._plan_repo.update(plan) logger.info( "状态流转: plan_id=%s %s → %s", -- 2.54.0