feat: 优化部分代码及提示词

This commit is contained in:
2026-09-25 17:12:38 +08:00
parent db41f2c679
commit 0e5738788e
16 changed files with 520 additions and 47 deletions
+42
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,4 @@
"""移动组件的后端模拟执行"""
"""在大屏 JSON 副本上移动组件,用于后端校验动作能否执行"""
from app.actions.base import BaseActionHandler
from app.schemas.actions import MoveAction
+231
View File
@@ -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()
)
]
+2 -2
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
"""设置组件尺寸的后端模拟执行"""
"""在大屏 JSON 副本上设置组件尺寸,用于后端校验动作能否执行"""
from app.actions.base import BaseActionHandler
from app.schemas.actions import ResizeAction
+1 -1
View File
@@ -1,4 +1,4 @@
"""按路径更新组件属性"""
"""在大屏 JSON 副本上按路径更新组件属性,用于后端校验动作能否执行"""
from copy import deepcopy