from copy import deepcopy from datetime import UTC, datetime from app.domain.models import ( CameraView, CommandRequest, DesignBrief, ProjectSnapshot, RoomProfile, WorkflowCommand, WorkflowDefinition, WorkflowStage, WorkflowStageDefinition, ) class WorkflowConflictError(RuntimeError): pass class InvalidTransitionError(RuntimeError): pass TRANSITIONS: dict[WorkflowStage, dict[WorkflowCommand, WorkflowStage]] = { WorkflowStage.UPLOADED: { WorkflowCommand.START_INGESTION: WorkflowStage.REGION_SELECTION, }, WorkflowStage.REGION_SELECTION: { WorkflowCommand.SELECT_REGION: WorkflowStage.PLAN_REVIEW, }, WorkflowStage.PLAN_REVIEW: { WorkflowCommand.CONFIRM_PLAN: WorkflowStage.BLOCKOUT, }, WorkflowStage.BLOCKOUT: { WorkflowCommand.BUILD_BLOCKOUT: WorkflowStage.STYLE_BRIEF, }, WorkflowStage.STYLE_BRIEF: { WorkflowCommand.SUBMIT_STYLE_BRIEF: WorkflowStage.DIRECTION_SELECTION, }, WorkflowStage.DIRECTION_SELECTION: { WorkflowCommand.SELECT_DIRECTION: WorkflowStage.RENDER_REVIEW, }, WorkflowStage.RENDER_REVIEW: { WorkflowCommand.REQUEST_RENDER: WorkflowStage.EDITING, WorkflowCommand.COMPLETE_PROJECT: WorkflowStage.COMPLETED, }, WorkflowStage.EDITING: { WorkflowCommand.APPLY_EDIT: WorkflowStage.EDITING, WorkflowCommand.REQUEST_RENDER: WorkflowStage.EDITING, WorkflowCommand.COMPLETE_PROJECT: WorkflowStage.COMPLETED, }, WorkflowStage.FAILED: { WorkflowCommand.RETRY: WorkflowStage.PLAN_REVIEW, }, WorkflowStage.COMPLETED: { WorkflowCommand.APPLY_EDIT: WorkflowStage.EDITING, }, } def available_commands(stage: WorkflowStage) -> list[WorkflowCommand]: return list(TRANSITIONS.get(stage, {}).keys()) def apply_command(project: ProjectSnapshot, request: CommandRequest) -> ProjectSnapshot: if request.expected_revision != project.revision: raise WorkflowConflictError( f"Expected revision {request.expected_revision}, current revision is {project.revision}." ) target = TRANSITIONS.get(project.stage, {}).get(request.command) if target is None: raise InvalidTransitionError( f"Command '{request.command}' is not allowed while project is in '{project.stage}'." ) updated = deepcopy(project) updated.last_stable_stage = project.stage updated.stage = target updated.revision += 1 updated.updated_at = datetime.now(UTC) updated.failure_reason = None if request.command == WorkflowCommand.SELECT_REGION: region_id = str(request.payload.get("region_id", "")) if not any(region.id == region_id for region in updated.plan.regions): raise InvalidTransitionError(f"Region '{region_id}' does not exist in this project.") updated.plan.selected_region_id = region_id for issue in updated.plan.issues: if issue.kind == "region": issue.resolved = True elif request.command == WorkflowCommand.CONFIRM_PLAN: updated.plan.ceiling_height_mm = int( request.payload.get("ceiling_height_mm", updated.plan.ceiling_height_mm) ) if request.payload.get("gross_area_sqm") is not None: updated.plan.gross_area_sqm = float(request.payload["gross_area_sqm"]) if request.payload.get("room_names"): existing = {room.name: room for room in updated.plan.structure.rooms} updated.plan.structure.rooms = [ existing.get(name) or RoomProfile( id=f"room-{index + 1}", name=name, kind="other", confidence=1, notes="用户确认", ) for index, raw_name in enumerate(request.payload["room_names"]) if (name := str(raw_name).strip()) ] for issue in updated.plan.issues: if issue.kind in {"scale", "ocr"}: issue.resolved = True elif request.command == WorkflowCommand.BUILD_BLOCKOUT: room_names = [room.name for room in updated.plan.structure.rooms] focus = room_names[:3] or ["客餐厅", "主卧", "入口"] updated.scene.cameras = [ CameraView(id=f"camera-{index + 1}", label=f"{room}主视角", room=room) for index, room in enumerate(focus) ] elif request.command == WorkflowCommand.SUBMIT_STYLE_BRIEF: updated.brief = DesignBrief.model_validate({**updated.brief.model_dump(), **request.payload, "completed": True}) for field in ("concept", "lighting", "forms", "density"): if field in request.payload: setattr(updated.style, field, request.payload[field]) for field in ("keywords", "materials", "avoid", "locked_decisions"): if field in request.payload: setattr(updated.style, field, list(request.payload[field])) elif request.command == WorkflowCommand.SELECT_DIRECTION: direction_id = str(request.payload.get("direction_id", "")) direction = next((item for item in updated.directions if item.id == direction_id), None) if direction is None: raise InvalidTransitionError(f"Direction '{direction_id}' does not exist in this project.") updated.style.selected_direction_id = direction_id updated.style.concept = direction.thesis updated.style.keywords = direction.keywords updated.style.palette = direction.palette updated.style.materials = direction.materials updated.style.lighting = direction.lighting updated.available_commands = available_commands(updated.stage) return updated def workflow_definition() -> WorkflowDefinition: labels = { WorkflowStage.UPLOADED: ("文件已上传", "验证文件并准备解析", False), WorkflowStage.REGION_SELECTION: ("选择户型", "从复杂图纸中选择目标平面区域", True), WorkflowStage.PLAN_REVIEW: ("确认结构", "检查墙、门窗、比例和保留家具", True), WorkflowStage.BLOCKOUT: ("生成白模", "建立空间骨架、家具占位和相机", False), WorkflowStage.STYLE_BRIEF: ("明确风格", "形成结构化 Style DNA", True), WorkflowStage.DIRECTION_SELECTION: ("选择方向", "比较并锁定主设计方向", True), WorkflowStage.RENDER_REVIEW: ("审阅效果", "检查空间忠实度与审美一致性", True), WorkflowStage.EDITING: ("多轮修改", "通过结构化操作局部修改", True), WorkflowStage.COMPLETED: ("方案完成", "导出概念方案与版本记录", False), WorkflowStage.FAILED: ("需要处理", "展示错误并从稳定阶段恢复", True), } return WorkflowDefinition( stages=[ WorkflowStageDefinition( id=stage, label=labels[stage][0], purpose=labels[stage][1], human_confirmation=labels[stage][2], ) for stage in WorkflowStage ], transitions={ stage.value: [command.value for command in commands] for stage, commands in TRANSITIONS.items() }, )