feat: 优化部分代码及提示词
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
"""在大屏 JSON 副本上新增组件,用于后端校验动作能否执行"""
|
||||
|
||||
from app.actions.base import BaseActionHandler
|
||||
from app.schemas.actions import AddAction
|
||||
from app.schemas.screen import Screen, ScreenNode
|
||||
|
||||
|
||||
class AddHandler(BaseActionHandler[AddAction]):
|
||||
def apply(self, screen: Screen, action: AddAction) -> Screen:
|
||||
# 重新校验节点,避免模型创建后被修改成无效内容
|
||||
node = ScreenNode.model_validate(action.node.model_dump())
|
||||
if any(existing.id == node.id for existing in screen.nodes):
|
||||
raise ValueError(f"新增组件 ID 已存在: {node.id}")
|
||||
result = screen.model_copy(deep=True)
|
||||
# 追加到末尾,不共享动作中的嵌套配置
|
||||
result.nodes.append(node.model_copy(deep=True))
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
screen = Screen.model_validate(
|
||||
{"canvas": {"width": 1920, "height": 1080}, "nodes": []}
|
||||
)
|
||||
action = AddAction.model_validate(
|
||||
{
|
||||
"type": "add",
|
||||
"node": {
|
||||
"id": "screen-title",
|
||||
"type": "text",
|
||||
"layout": {"x": 100, "y": 50, "width": 400, "height": 60},
|
||||
"props": {},
|
||||
},
|
||||
}
|
||||
)
|
||||
handler = AddHandler()
|
||||
final_screen = handler.apply(screen, action)
|
||||
print(final_screen.model_dump_json(indent=2))
|
||||
print("原始组件数:", len(screen.nodes))
|
||||
try:
|
||||
handler.apply(final_screen, action)
|
||||
except ValueError as error:
|
||||
print("重复新增:", error)
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
"""动作处理器公共接口"""
|
||||
"""动作处理器公共接口,供后端校验时修改大屏 JSON 副本,不操作前端画布"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Generic, TypeVar
|
||||
@@ -12,7 +12,7 @@ ActionT = TypeVar("ActionT", bound=ActionModel)
|
||||
class BaseActionHandler(ABC, Generic[ActionT]):
|
||||
@abstractmethod
|
||||
def apply(self, screen: Screen, action: ActionT) -> Screen:
|
||||
"""校验并在副本上执行动作,返回新的大屏"""
|
||||
"""检查动作能否执行,返回修改后的大屏副本,保留原始输入"""
|
||||
raise NotImplementedError
|
||||
|
||||
def copy_target(self, screen: Screen, target_id: str) -> tuple[Screen, ScreenNode]:
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
"""移动组件的后端模拟执行"""
|
||||
"""在大屏 JSON 副本上移动组件,用于后端校验动作能否执行"""
|
||||
|
||||
from app.actions.base import BaseActionHandler
|
||||
from app.schemas.actions import MoveAction
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
"""将物料默认配置补入新增动作,并检查动作是否符合规划,不执行画布更新"""
|
||||
|
||||
from copy import deepcopy
|
||||
from math import isclose
|
||||
|
||||
from app.processor.state import QueryState
|
||||
from app.schemas.action_validation import ActionValidationError
|
||||
from app.schemas.actions import (
|
||||
ActionBatch,
|
||||
AddAction,
|
||||
MoveAction,
|
||||
ResizeAction,
|
||||
UpdateAction,
|
||||
)
|
||||
from app.schemas.screen import Screen, ScreenNode
|
||||
|
||||
|
||||
def plan_block_reason(state: QueryState) -> str | None:
|
||||
"""检查规划是否在当前支持范围内,不支持时返回原因"""
|
||||
plan = state["screen_plan"]
|
||||
resources = state.get("component_resources")
|
||||
if state["requirement"].needs_clarification:
|
||||
return "需求尚未澄清,不能生成规划动作"
|
||||
if plan.canvas_changes:
|
||||
return "规划包含画布修改,当前尚未接入对应动作"
|
||||
if plan.resource_needs:
|
||||
return "规划仍有待获取资源,不能直接生成动作"
|
||||
if any(
|
||||
item.disposition not in {"add", "modify", "keep"} for item in plan.components
|
||||
):
|
||||
return "当前规划执行支持新增、调整和保留组件,移除后续接入"
|
||||
additions = [item for item in plan.components if item.disposition == "add"]
|
||||
modifications = {
|
||||
item.target_id: item for item in plan.components if item.disposition == "modify"
|
||||
}
|
||||
if not additions and not modifications:
|
||||
return "规划未包含可执行的新增或调整组件"
|
||||
if additions and (resources is None or resources.status != "resolved"):
|
||||
return "组件目录尚未就绪"
|
||||
supported = {item.type for item in resources.components} if resources else set()
|
||||
if any(item.component_type not in supported for item in additions):
|
||||
return "规划中的新增类型未在组件目录中找到"
|
||||
nodes = {node.id: node for node in state["input_context"].screen.nodes}
|
||||
for item in plan.components:
|
||||
if item.disposition == "add":
|
||||
continue
|
||||
if item.target_id not in nodes:
|
||||
return f"规划目标不存在: {item.target_id}"
|
||||
if (
|
||||
item.component_type is not None
|
||||
and item.component_type != nodes[item.target_id].type
|
||||
):
|
||||
return "当前不支持更换已有组件类型"
|
||||
# 已有目标的修改诉求不能通过 keep 或遗漏目标绕过
|
||||
supported_operations = {"move", "resize", "update", "restyle", "reflow", "layout"}
|
||||
changes = state["requirement"].changes
|
||||
for change in changes:
|
||||
if change.target_scope == "screen" and change.operation == "add":
|
||||
if not additions:
|
||||
return "规划遗漏新增需求"
|
||||
elif (
|
||||
change.target_scope in {"nodes", "screen"}
|
||||
and change.operation in supported_operations
|
||||
):
|
||||
if not modifications or any(
|
||||
target not in modifications for target in change.target_ids
|
||||
):
|
||||
return "规划遗漏需要调整的组件"
|
||||
else:
|
||||
return "需求包含未支持的操作,当前不能只执行部分规划"
|
||||
if (
|
||||
modifications
|
||||
and changes
|
||||
and not any(
|
||||
change.target_scope == "screen" and change.operation in supported_operations
|
||||
for change in changes
|
||||
)
|
||||
):
|
||||
allowed = {target for change in changes for target in change.target_ids}
|
||||
if set(modifications) - allowed:
|
||||
return "规划调整了需求范围外的组件"
|
||||
return None
|
||||
|
||||
|
||||
def plan_materials(state: QueryState) -> list[dict]:
|
||||
"""提取规划需要的完整物料配置,供动作生成和修正使用"""
|
||||
types = {
|
||||
item.component_type
|
||||
for item in state["screen_plan"].components
|
||||
if item.disposition == "add"
|
||||
}
|
||||
if not types:
|
||||
return []
|
||||
return [
|
||||
item.model_dump(mode="json", by_alias=True)
|
||||
for item in state["component_resources"].components
|
||||
if item.type in types
|
||||
]
|
||||
|
||||
|
||||
def merge_config(defaults: dict, overrides: dict) -> dict:
|
||||
# 对象逐层覆盖,数组和标量整体替换,不修改目录或模型输出
|
||||
result = deepcopy(defaults)
|
||||
for key, value in overrides.items():
|
||||
result[key] = (
|
||||
merge_config(result[key], value)
|
||||
if isinstance(result.get(key), dict) and isinstance(value, dict)
|
||||
else deepcopy(value)
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def assemble_plan_actions(state: QueryState, batch: ActionBatch) -> ActionBatch:
|
||||
"""用物料默认配置补全新增节点,模型显式提供的字段优先"""
|
||||
resources = state.get("component_resources")
|
||||
materials = {item.key: item for item in resources.components} if resources else {}
|
||||
result = batch.model_copy(deep=True)
|
||||
for action in result.actions:
|
||||
if not isinstance(action, AddAction) or action.component_key not in materials:
|
||||
continue
|
||||
material = materials[action.component_key]
|
||||
node = merge_config(
|
||||
material.schema_.model_dump(mode="json"),
|
||||
action.node.model_dump(mode="json", exclude_unset=True),
|
||||
)
|
||||
action.node = ScreenNode.model_validate(node)
|
||||
# 保守标记物料含示例数据,不能由模型声称已取得真实数据
|
||||
action.sample_data = material.sample_data
|
||||
return result
|
||||
|
||||
|
||||
def validate_plan_actions(
|
||||
state: QueryState, batch: ActionBatch
|
||||
) -> list[ActionValidationError]:
|
||||
"""检查动作是否覆盖规划,禁止修改 keep 或未提及的组件"""
|
||||
if reason := plan_block_reason(state):
|
||||
return [ActionValidationError(code="scope_error", message=reason)]
|
||||
expected = {
|
||||
item.plan_id: item
|
||||
for item in state["screen_plan"].components
|
||||
if item.disposition == "add"
|
||||
}
|
||||
modifications = {
|
||||
item.target_id: item
|
||||
for item in state["screen_plan"].components
|
||||
if item.disposition == "modify"
|
||||
}
|
||||
resources = state.get("component_resources")
|
||||
materials = {item.key: item for item in resources.components} if resources else {}
|
||||
seen = set()
|
||||
modified = set()
|
||||
errors = []
|
||||
for index, action in enumerate(batch.actions):
|
||||
message = None
|
||||
if not isinstance(action, AddAction):
|
||||
if not isinstance(action, (MoveAction, ResizeAction, UpdateAction)):
|
||||
message = "规划调整仅支持 move、resize 和 update"
|
||||
elif action.target_id not in modifications:
|
||||
message = f"不能修改保留或未提及的组件: {action.target_id}"
|
||||
elif (
|
||||
isinstance(action, UpdateAction)
|
||||
and not modifications[action.target_id].design
|
||||
):
|
||||
message = f"规划未要求修改属性: {action.target_id}"
|
||||
else:
|
||||
modified.add(action.target_id)
|
||||
elif action.plan_id not in expected:
|
||||
message = f"动作未关联有效的新增规划: {action.plan_id}"
|
||||
elif action.plan_id in seen:
|
||||
message = f"同一规划组件不能重复新增: {action.plan_id}"
|
||||
else:
|
||||
seen.add(action.plan_id)
|
||||
item = expected[action.plan_id]
|
||||
material = materials.get(action.component_key)
|
||||
if (
|
||||
material is None
|
||||
or material.type != item.component_type
|
||||
or action.node.type != item.component_type
|
||||
):
|
||||
message = "新增节点类型、物料和规划类型不一致"
|
||||
elif item.layout is not None and action.node.layout != item.layout:
|
||||
message = f"新增布局与规划不一致: {item.plan_id}"
|
||||
elif action.sample_data != material.sample_data:
|
||||
message = "示例数据标记与物料不一致"
|
||||
elif action.node.model_extra.get("dataId"):
|
||||
message = "当前尚未接入数据绑定,不能生成 dataId"
|
||||
if message:
|
||||
errors.append(
|
||||
ActionValidationError(
|
||||
action_index=index, code="scope_error", message=message
|
||||
)
|
||||
)
|
||||
if missing := set(expected) - seen:
|
||||
errors.append(
|
||||
ActionValidationError(
|
||||
code="scope_error", message=f"遗漏新增规划组件: {sorted(missing)}"
|
||||
)
|
||||
)
|
||||
if missing := set(modifications) - modified:
|
||||
errors.append(
|
||||
ActionValidationError(
|
||||
code="scope_error", message=f"遗漏调整规划组件: {sorted(missing)}"
|
||||
)
|
||||
)
|
||||
return errors
|
||||
|
||||
|
||||
def validate_plan_result(
|
||||
state: QueryState, screen: Screen
|
||||
) -> list[ActionValidationError]:
|
||||
"""整批执行后检查已有组件的最终布局,避免动作合法但没有达到规划位置"""
|
||||
nodes = {node.id: node for node in screen.nodes}
|
||||
return [
|
||||
ActionValidationError(
|
||||
code="scope_error", message=f"执行后的布局与规划不一致: {item.target_id}"
|
||||
)
|
||||
for item in state["screen_plan"].components
|
||||
if item.disposition == "modify"
|
||||
and item.layout is not None
|
||||
and not all(
|
||||
isclose(
|
||||
getattr(nodes[item.target_id].layout, key),
|
||||
value,
|
||||
rel_tol=0,
|
||||
abs_tol=1e-9,
|
||||
)
|
||||
if key in {"x", "y", "width", "height"}
|
||||
else nodes[item.target_id].layout.model_dump().get(key) == value
|
||||
for key, value in item.layout.model_dump().items()
|
||||
)
|
||||
]
|
||||
@@ -1,4 +1,4 @@
|
||||
"""显式注册动作处理器"""
|
||||
"""按动作 type 分配处理器,统一提供大屏 JSON 副本的执行入口"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
@@ -20,7 +20,7 @@ ACTION_HANDLERS: dict[str, BaseActionHandler[Any]] = {
|
||||
|
||||
|
||||
def apply_action(screen: Screen, action: Action) -> Screen:
|
||||
"""接收已通过 Schema 校验的动作,返回执行后的副本"""
|
||||
"""接收已通过 Schema 校验的动作,返回修改后的大屏副本,不保存或更新前端"""
|
||||
handler = ACTION_HANDLERS.get(action.type)
|
||||
if handler is None:
|
||||
raise ValueError(f"尚未实现动作处理器: {action.type}")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""设置组件尺寸的后端模拟执行"""
|
||||
"""在大屏 JSON 副本上设置组件尺寸,用于后端校验动作能否执行"""
|
||||
|
||||
from app.actions.base import BaseActionHandler
|
||||
from app.schemas.actions import ResizeAction
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""按路径更新组件属性"""
|
||||
"""在大屏 JSON 副本上按路径更新组件属性,用于后端校验动作能否执行"""
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
|
||||
Reference in New Issue
Block a user