175 lines
7.2 KiB
Python
175 lines
7.2 KiB
Python
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()
|
|
},
|
|
)
|