feat: 接入新动作 update_canvas

This commit is contained in:
2026-09-27 18:03:48 +08:00
parent 216051cd8d
commit eda1db1f88
12 changed files with 128 additions and 36 deletions
+5 -4
View File
@@ -41,8 +41,8 @@ def route_after_scope(state: QueryState) -> str:
# 需要规划时先获取组件能力,供规划节点选择可用组件
if state["change_scope"].needs_planning:
return "component_resource_node"
# 已支持的简单修改直接生成动作,尚不支持的需求暂时结束流程
return "action_node" if can_generate_actions(state) else END
# 已支持的简单修改直接生成动作,尚不支持的需求进入失败收尾
return "action_node" if can_generate_actions(state) else "fallback_node"
def build_graph(checkpointer=None):
@@ -129,13 +129,13 @@ def build_graph(checkpointer=None):
{
"component_resource_node": "component_resource_node",
"action_node": "action_node",
END: END,
"fallback_node": "fallback_node",
},
)
workflow.add_conditional_edges(
"component_resource_node",
lambda state: state["component_resources"].status == "resolved",
{True: "planner_node", False: END},
{True: "planner_node", False: "fallback_node"},
)
workflow.add_edge("planner_node", "action_node")
workflow.add_conditional_edges(
@@ -185,6 +185,7 @@ def build_graph(checkpointer=None):
("app.schemas.actions.move", "MoveAction"),
("app.schemas.actions.resize", "ResizeAction"),
("app.schemas.actions.update", "UpdateAction"),
("app.schemas.actions.update_canvas", "UpdateCanvasAction"),
("app.schemas.action_validation", "ActionValidationError"),
("app.schemas.action_repair", "ActionRepairPlan"),
("app.schemas.screen", "Screen"),
+28 -19
View File
@@ -16,29 +16,38 @@ from app.schemas.actions import ActionBatch, AddAction
def can_generate_actions(state: QueryState) -> bool:
return action_block_reason(state) is None
def action_block_reason(state: QueryState) -> str | None:
# 路由和失败说明共用能力判断,避免支持范围不一致
if state.get("screen_plan") is not None:
return plan_block_reason(state) is None
return plan_block_reason(state)
requirement = state["requirement"]
scope = state["change_scope"]
# 简单操作直接生成动作,复杂变更走规划分支
return (
requirement.intent == "modify_screen"
and not requirement.needs_clarification
and not scope.needs_planning
and not scope.resource_needs
and bool(requirement.changes)
and all(
if requirement.needs_clarification:
return "需求尚未澄清完整,不能生成动作"
if scope.needs_planning or requirement.intent != "modify_screen":
return "当前需求需要先完成规划,不能直接生成动作"
if scope.resource_needs:
return "尚未接入所需资源查询:" + ";".join(scope.resource_needs)
if not requirement.changes:
return "需求中没有可执行的变更"
unsupported = [
f"{change.target_scope}/{change.operation}"
for change in requirement.changes
if not (
change.target_scope == "nodes"
and change.operation
in (
"move",
"resize",
"update",
"delete",
)
for change in requirement.changes
and change.operation in ("move", "resize", "update", "delete")
or change.target_scope == "canvas"
and change.operation in ("update", "resize", "update_canvas")
)
)
]
if unsupported:
return "当前尚未支持的直接操作:" + "、".join(dict.fromkeys(unsupported))
return None
class ActionNode(BaseNode):
@@ -60,7 +69,7 @@ class ActionNode(BaseNode):
return updated_state
if not can_generate_actions(state):
raise ValueError(
"ActionNode 当前只支持无需规划和资源的 move、resize、update、delete 需求"
"ActionNode 当前只支持无需规划和资源的 move、resize、update、delete、update_canvas 需求"
)
payload = {
"input_context": state["input_context"].model_dump(mode="json"),
+6 -3
View File
@@ -1,4 +1,4 @@
"""停止自动修正并保留诊断信息"""
"""统一收尾能力不足、资源缺失和动作修正失败"""
from app.processor.base import BaseNode
from app.processor.state import QueryState
@@ -11,9 +11,12 @@ class FallbackNode(BaseNode):
updated_state: QueryState = state.copy()
reason = state.get("failure_reason")
if not reason:
reason = f"动作校验未通过,已达到自动修正上限,共重试 {state.get('retry_count', 0)} 次"
if state.get("validation_errors"):
reason = f"动作校验未通过,已达到自动修正上限,共重试 {state.get('retry_count', 0)} 次"
else:
reason = "当前需求未能完成,缺少具体失败原因"
# actions 仅保留作诊断,不交付部分结果
# 失败动作仅保留作诊断,validation_passed=False 表示不可交付执行
updated_state["status"] = "failed"
updated_state["failure_reason"] = reason
updated_state["validation_passed"] = False
+6
View File
@@ -6,6 +6,7 @@ from langchain_core.messages import HumanMessage, SystemMessage
from app.clients.llm import get_llm
from app.processor.base import BaseNode
from app.processor.nodes.action_node import action_block_reason
from app.processor.state import QueryState
from app.prompt.scope import SCOPE_SYSTEM_PROMPT
from app.schemas.change_scope import ChangeScope
@@ -55,6 +56,11 @@ class ScopeNode(BaseNode):
raise ValueError(f"范围遗漏了需求中的节点: {sorted(missing_ids)}")
updated_state["change_scope"] = scope
# 规划分支继续获取组件能力,直接操作分支提前记录缺失能力
reason = None if scope.needs_planning else action_block_reason(updated_state)
updated_state["failure_reason"] = reason
updated_state["status"] = "failed" if reason else "running"
return updated_state
+20 -6
View File
@@ -7,7 +7,7 @@ from app.actions.registry import apply_action
from app.processor.base import BaseNode
from app.processor.state import QueryState
from app.schemas.action_validation import ActionValidationError, schema_errors
from app.schemas.actions import ActionBatch, AddAction
from app.schemas.actions import ActionBatch, AddAction, UpdateCanvasAction
class ValidatorNode(BaseNode):
@@ -60,19 +60,33 @@ class ValidatorNode(BaseNode):
updated_state["simulated_screen"] = None
return updated_state
allowed = {
(target, change.operation)
("nodes", target, change.operation)
for change in requirement.changes
if change.target_scope == "nodes"
for target in change.target_ids
}
actual = {(action.target_id, action.type) for action in batch.actions}
allowed.update(
("canvas", "", "update_canvas")
for change in requirement.changes
if change.target_scope == "canvas"
and change.operation in ("update", "resize", "update_canvas")
)
# 画布没有组件 ID,使用独立作用域避免与同名组件混淆
targets = [
("canvas", "", action.type)
if isinstance(action, UpdateCanvasAction)
else ("nodes", action.target_id, action.type)
for action in batch.actions
]
actual = set(targets)
errors = [
ActionValidationError(
action_index=index,
code="scope_error",
message=f"动作超出需求范围: {action.target_id} / {action.type}",
message=f"动作超出需求范围: {' / '.join(target)}",
)
for index, action in enumerate(batch.actions)
if (action.target_id, action.type) not in allowed
for index, target in enumerate(targets)
if target not in allowed
]
if missing := allowed - actual:
errors.append(