Files
EP-Hub-Skill/.agents/skills/dingtalk-misc/scripts/yida_process_flow.py
T
2026-09-02 11:44:52 +08:00

1479 lines
59 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Shared Yida process-flow schema builder.
The schema consumed by integration automation and design process update is the
same shape:
{"props": {"processCode": "TPROC-xxx", ...}, "nodes": [...]}
This module accepts either that ready-to-save shape or the flatter node objects
described in references/yida-process-node.md, then normalizes them into
the save payload used by `dws yida design process update --content`.
"""
from __future__ import annotations
import copy
from typing import Any
FLOW_PROP_KEYS = ("allowWithdraw", "allowCollaboration", "allowTemporaryStorage")
FLOW_TOP_LEVEL_KEYS = ("flowConfig", "formulaRules", "approvalSummary", "nodeI18nKeyMap")
NODE_CORE_KEYS = {
"type",
"componentName",
"nodeType",
"tool",
"toolName",
"kind",
"nodeId",
"name",
"description",
"title",
"approvalType",
"nextId",
"prevId",
"props",
"childNodes",
}
TYPE_ALIASES = {
"create_trigger_node": "trigger",
"trigger_node": "trigger",
"trigger": "trigger",
"create_timer_trigger_node": "trigger",
"timer_trigger_node": "trigger",
"timerTrigger": "trigger",
"create_data_create_node": "dataCreate",
"data_create_node": "dataCreate",
"AddDataNode": "dataCreate",
"addData": "dataCreate",
"dataCreate": "dataCreate",
"create_data_update_node": "dataUpdate",
"data_update_node": "dataUpdate",
"UpdateDataNode": "dataUpdate",
"updateData": "dataUpdate",
"dataUpdate": "dataUpdate",
"create_data_retrieve_node": "dataRetrieve",
"data_retrieve_node": "dataRetrieve",
"GetSingleDataNode": "dataRetrieve",
"GetBatchDataNode": "dataRetrieve",
"getSingleData": "dataRetrieve",
"getBatchData": "dataRetrieve",
"dataRetrieve": "dataRetrieve",
"DeleteDataNode": "dataDelete",
"deleteData": "dataDelete",
"dataDelete": "dataDelete",
"create_connector_node": "connector",
"connector_node": "connector",
"ConnectorNode": "connector",
"connector": "connector",
"create_send_message_node": "sendMessage",
"send_message_node": "sendMessage",
"SendMessageNode": "sendMessage",
"sendMessage": "sendMessage",
"create_send_email_node": "sendEmail",
"send_email_node": "sendEmail",
"SendEmailNode": "sendEmail",
"sendEmail": "sendEmail",
"create_send_card_node": "sendCard",
"send_card_node": "sendCard",
"SendCardNode": "sendCard",
"CardNode": "sendCard",
"sendCard": "sendCard",
"create_update_card_node": "updateCard",
"update_card_node": "updateCard",
"UpdateCardNode": "updateCard",
"CardUpdateNode": "updateCard",
"updateCard": "updateCard",
"create_route_node": "route",
"route_node": "route",
"ConditionContainer": "route",
"route": "route",
"ConditionNode": "condition",
"ParallelNode": "condition",
"condition": "condition",
"create_foreach_node": "forEach",
"foreach_node": "forEach",
"CycleContainer": "forEach",
"forEach": "forEach",
"foreach": "forEach",
"create_initiate_approval_node": "initiateApproval",
"initiate_approval_node": "initiateApproval",
"InitiateApprovalNode": "initiateApproval",
"subProcess": "initiateApproval",
"initiateApproval": "initiateApproval",
"create_approver_node": "approver",
"approver_node": "approver",
"ApprovalNode": "approval",
"MultiApprovalNode": "approval",
"approver": "approver",
"create_executor_node": "executor",
"executor_node": "executor",
"OperatorNode": "operator",
"executor": "executor",
"create_carbon_copy_node": "carbonCopy",
"carbon_copy_node": "carbonCopy",
"CarbonNode": "carbon",
"carbonCopy": "carbonCopy",
"GroovyNode": "CodeExecutor",
"groovy": "CodeExecutor",
"JavaScriptNode": "CodeExecutor",
"javascript": "CodeExecutor",
"AINode": "AIExecutor",
"ai": "AIExecutor",
"create_finish_node": "finish",
"finish_node": "finish",
"EndNode": "finish",
"finish": "finish",
}
DEFAULT_NODE_NAMES = {
"apply": "发起",
"approval": "审批人",
"operator": "执行人",
"carbon": "抄送人",
"trigger": "表单事件触发",
"dataCreate": "新增数据",
"dataUpdate": "更新数据",
"dataRetrieve": "获取数据",
"dataDelete": "删除数据",
"connector": "连接器",
"sendMessage": "消息通知",
"sendEmail": "发送邮件",
"sendCard": "发送卡片",
"updateCard": "更新卡片",
"route": "ConditionContainer",
"condition": "条件",
"forEach": "循环容器",
"foreach": "循环容器",
"initiateApproval": "发起审批",
"CodeExecutor": "脚本",
"AIExecutor": "AI 节点",
"approver": "审批人",
"executor": "执行人",
"carbonCopy": "抄送人",
"finish": "结束",
}
VIEW_COMPONENT_BY_TYPE = {
"apply": "ApplyNode",
"approval": "ApprovalNode",
"operator": "OperatorNode",
"carbon": "CarbonNode",
"connector": "ConnectorNode",
"dataCreate": "AddDataNode",
"dataUpdate": "UpdateDataNode",
"dataRetrieve": "GetSingleDataNode",
"dataDelete": "DeleteDataNode",
"sendMessage": "SendMessageNode",
"sendEmail": "SendEmailNode",
"sendCard": "CardNode",
"updateCard": "CardUpdateNode",
"route": "ConditionContainer",
"condition": "ConditionNode",
"initiateApproval": "InitiateApprovalNode",
"approver": "ApprovalNode",
"executor": "OperatorNode",
"carbonCopy": "CarbonNode",
"forEach": "CycleContainer",
"foreach": "CycleContainer",
"AIExecutor": "AINode",
"CodeExecutor": "JavaScriptNode",
"finish": "EndNode",
}
VIEW_COMPONENT_RULE_KEY = {
"ConnectorNode": "connectorRules",
"AddDataNode": "addDataRules",
"UpdateDataNode": "updateDataRules",
"DeleteDataNode": "deleteData",
"GetSingleDataNode": "getData",
"GetBatchDataNode": "getData",
"SendMessageNode": "sendMessageRules",
"SendEmailNode": "sendEmailRules",
"CardNode": "cardRules",
"CardUpdateNode": "cardUpdateRules",
"InitiateApprovalNode": "initiateApprovalRules",
"CycleContainer": "cycleContainerRules",
"AINode": "workFlowRules",
"GroovyNode": "groovy",
"JavaScriptNode": "JavaScript",
}
TRIGGER_INPUT_KEYS = {
"formUuid",
"formEventType",
"formEventField",
"triggerFormEventRecursively",
"conditions",
"startDate",
"repeat",
"end",
}
UNRESOLVED_CARD_BIZ_ID_VALUES = {
"__masterdata_form_inst_id",
}
class FlowSchemaError(ValueError):
"""Raised when a flow spec cannot be normalized safely."""
def i18n_name(value: Any, default: str) -> dict[str, str]:
if isinstance(value, dict):
result = copy.deepcopy(value)
result.setdefault("zh_CN", result.get("name") or default)
result.setdefault("en_US", "")
result.setdefault("type", "i18n")
return result
text = str(value or default)
return {"zh_CN": text, "en_US": "", "type": "i18n"}
def normalize_next_id(value: Any) -> list[str]:
if value is None:
return []
if isinstance(value, str):
return [value] if value else []
if isinstance(value, list):
return [str(item) for item in value if item not in (None, "")]
raise FlowSchemaError(f"nextId must be string or list, got {type(value).__name__}")
def normalize_node_type(node: dict[str, Any]) -> str:
raw = (
node.get("componentName")
or node.get("nodeType")
or node.get("tool")
or node.get("toolName")
or node.get("kind")
or node.get("type")
)
if not raw:
raise FlowSchemaError(f"node {node.get('nodeId') or '<unknown>'} missing type")
raw = str(raw)
return TYPE_ALIASES.get(raw, raw)
def _normalize_trigger_props(props: dict[str, Any]) -> dict[str, Any]:
result = copy.deepcopy(props)
inputs = result.get("inputs")
if inputs is None:
inputs = {}
if not isinstance(inputs, dict):
raise FlowSchemaError("trigger props.inputs must be object")
for key in list(TRIGGER_INPUT_KEYS):
if key in result:
inputs[key] = result.pop(key)
if "triggerType" not in result:
result["triggerType"] = "TimerEvent" if ("repeat" in inputs or "startDate" in inputs) else "FormEvent"
result["inputs"] = inputs
return result
def _script_outputs_schema(outputs: Any) -> list[dict[str, Any]]:
if not isinstance(outputs, list):
return []
result = []
for item in outputs:
if not isinstance(item, dict):
continue
result.append({
"description": item.get("desc") or item.get("description") or "",
"name": item.get("name"),
"type": item.get("type"),
"valueType": item.get("valueType"),
})
return result
def _normalize_process_payload(node_type: str, props: dict[str, Any]) -> tuple[str, dict[str, Any]]:
result = copy.deepcopy(props)
if node_type in {"approval", "approver", "operator", "executor"} and isinstance(result.get("approver"), dict):
approver = result["approver"]
if approver.get("approvalType") and not result.get("approvalType"):
result["approvalType"] = copy.deepcopy(approver["approvalType"])
if approver.get("approvals") is not None and "approvals" not in result:
result["approvals"] = copy.deepcopy(approver.get("approvals"))
if isinstance(approver.get("approverRules"), dict) and "approverRules" not in result:
result["approverRules"] = copy.deepcopy(approver["approverRules"])
if node_type == "dataRetrieve" and isinstance(result.get("getData"), dict):
return node_type, copy.deepcopy(result["getData"])
if node_type == "dataUpdate" and isinstance(result.get("updateDataRules"), dict):
return node_type, copy.deepcopy(result["updateDataRules"])
if node_type == "dataDelete" and isinstance(result.get("deleteData"), dict):
return node_type, copy.deepcopy(result["deleteData"])
if node_type == "sendMessage" and isinstance(result.get("sendMessageRules"), dict):
payload = copy.deepcopy(result["sendMessageRules"])
payload.pop("description", None)
return node_type, payload
if node_type == "sendEmail" and isinstance(result.get("sendEmailRules"), dict):
return node_type, copy.deepcopy(result["sendEmailRules"])
if node_type == "sendCard":
if isinstance(result.get("sendCardRules"), dict):
return node_type, copy.deepcopy(result["sendCardRules"])
if isinstance(result.get("cardRules"), dict):
return node_type, copy.deepcopy(result["cardRules"])
if node_type == "updateCard":
if isinstance(result.get("updateCardRules"), dict):
return node_type, copy.deepcopy(result["updateCardRules"])
if isinstance(result.get("cardUpdateRules"), dict):
return node_type, copy.deepcopy(result["cardUpdateRules"])
if node_type == "initiateApproval" and isinstance(result.get("initiateApprovalRules"), dict):
return node_type, copy.deepcopy(result["initiateApprovalRules"])
if node_type == "forEach":
if isinstance(result.get("cycleContainerRules"), dict):
return "foreach", copy.deepcopy(result["cycleContainerRules"])
if isinstance(result.get("cycleRules"), dict):
return "foreach", copy.deepcopy(result["cycleRules"])
if isinstance(result.get("foreachRules"), dict):
return "foreach", copy.deepcopy(result["foreachRules"])
if node_type == "AIExecutor" and isinstance(result.get("workFlowRules"), dict):
workflow = result["workFlowRules"]
return node_type, {
"type": "aiFlow",
"action": {"flowId": workflow.get("flowId")},
"outputs": copy.deepcopy(workflow.get("outputs") or []),
"yidaFieldIdList": copy.deepcopy(workflow.get("yidaFieldIdList") or []),
}
if node_type == "CodeExecutor":
script = result.get("JavaScript") if isinstance(result.get("JavaScript"), dict) else None
script_type = "JavaScript"
if script is None and isinstance(result.get("groovy"), dict):
script = result.get("groovy")
script_type = "groovy"
if script is not None:
return node_type, {
"inputs": copy.deepcopy(script.get("inputs")),
"action": copy.deepcopy(script.get("action")),
"scriptType": script.get("scriptType") or script_type,
"outputsSchema": _script_outputs_schema(script.get("outputs")),
}
return node_type, result
def _contains_originator_keyword(value: Any) -> bool:
text = _i18n_text(value, "").lower()
return any(keyword in text for keyword in ("申请人", "发起人", "applicant", "originator"))
def _actioner_targets_originator(value: Any) -> bool:
if isinstance(value, str):
return value.lower() in {"originator", "$originator", "applicant", "$applicant"} or value in {"申请人", "发起人"}
if not isinstance(value, dict):
return False
rule_type = str(value.get("type") or value.get("approvalType") or "").lower()
if rule_type in {"originator", "ext_target_approval_originator"}:
return True
for key in ("value", "id", "userId", "fieldId", "fieldName", "label", "name", "description"):
if _actioner_targets_originator(value.get(key)):
return True
return False
def _is_originator_operator_node(node_type: str, node: dict[str, Any], props: dict[str, Any]) -> bool:
if node_type not in {"operator", "executor"}:
return False
if _actioner_targets_originator(props.get("actionerRule")):
return True
if _actioner_targets_originator(props.get("executor")) or _actioner_targets_originator(props.get("approver")):
return True
if _contains_originator_keyword(node.get("name")) or _contains_originator_keyword(node.get("description")):
return True
if _contains_originator_keyword(props.get("name")) or _contains_originator_keyword(props.get("description")):
return True
return False
def _operator_action_to_approval_action(action: Any) -> dict[str, Any] | None:
if isinstance(action, dict):
normalized = copy.deepcopy(action)
raw_action = str(normalized.get("action") or "").lower()
if raw_action in {"submit", "confirm"}:
normalized["action"] = "agree"
normalized.setdefault("hidden", False)
normalized.setdefault("name", i18n_name(normalized.get("name") or normalized.get("text"), "确认"))
normalized.setdefault("text", copy.deepcopy(normalized["name"]))
normalized.setdefault("alias", copy.deepcopy(normalized["name"]))
return normalized
raw = str(action or "").lower()
if raw in {"submit", "confirm", "agree"}:
return {
"hidden": False,
"name": {"en_US": "Confirm", "zh_CN": "确认", "type": "i18n"},
"text": {"en_US": "Confirm", "zh_CN": "确认", "type": "i18n"},
"action": "agree",
"alias": {"en_US": "Confirm", "zh_CN": "确认", "type": "i18n"},
}
if raw in {"transfer", "forward"}:
return {
"hidden": True,
"name": {"en_US": "Forward", "zh_CN": "转交", "type": "i18n"},
"text": {"en_US": "Forward", "zh_CN": "转交", "type": "i18n"},
"action": "forward",
"alias": {"en_US": "Forward", "zh_CN": "转交", "type": "i18n"},
}
return None
def _originator_approval_props_from_operator(props: dict[str, Any]) -> dict[str, Any]:
result = copy.deepcopy(props)
result.pop("actionerRule", None)
result.pop("finishRule", None)
result.pop("executor", None)
result.pop("approver", None)
result["approvals"] = [["originator"]]
approver_rules = copy.deepcopy(result.get("approverRules") if isinstance(result.get("approverRules"), dict) else {})
approver_rules.update({
"type": "ext_target_approval_originator",
"mode": "ApprovalNode_rules_only",
"approverList": [{"type": "ext_target_approval"}],
"multiApproverType": approver_rules.get("multiApproverType", "all"),
"conditionalMode": approver_rules.get("conditionalMode", "conditional"),
"approvals": [["originator"]],
"description": approver_rules.get("description") or "发起人本人",
})
result["approverRules"] = approver_rules
raw_actions = result.get("actions")
converted_actions = [
converted
for converted in (_operator_action_to_approval_action(item) for item in raw_actions or [])
if converted is not None
]
if not converted_actions:
converted_actions = [
_operator_action_to_approval_action("submit"),
_operator_action_to_approval_action("transfer"),
]
result["actions"] = [item for item in converted_actions if item is not None]
result.setdefault("appendActions", [])
result.setdefault("openDigitalSign", False)
result.setdefault("noActionersType", "stopProcess")
return result
def normalize_node(node: dict[str, Any]) -> dict[str, Any]:
if not isinstance(node, dict):
raise FlowSchemaError(f"node must be object, got {type(node).__name__}")
node_type = normalize_node_type(node)
node_id = node.get("nodeId")
if not isinstance(node_id, str) or not node_id:
raise FlowSchemaError(f"{node_type} node missing nodeId")
props = copy.deepcopy(node.get("props") or {})
if not isinstance(props, dict):
raise FlowSchemaError(f"node {node_id} props must be object")
for key, value in node.items():
if key == "type" and (
node.get("componentName")
or node.get("nodeType")
or node.get("tool")
or node.get("toolName")
or node.get("kind")
):
props[key] = copy.deepcopy(value)
continue
if key not in NODE_CORE_KEYS:
props[key] = copy.deepcopy(value)
if node_type == "trigger":
props = _normalize_trigger_props(props)
node_type, props = _normalize_process_payload(node_type, props)
approval_type = copy.deepcopy(node.get("approvalType")) if "approvalType" in node else copy.deepcopy(props.get("approvalType"))
if _is_originator_operator_node(node_type, node, props):
node_type = "approval"
approval_type = "ext_target_approval_originator"
props = _originator_approval_props_from_operator(props)
child_nodes = node.get("childNodes") or []
if not isinstance(child_nodes, list):
raise FlowSchemaError(f"node {node_id} childNodes must be list")
normalized = {
"name": i18n_name(node.get("name"), DEFAULT_NODE_NAMES.get(node_type, node_type)),
"type": node_type,
"nodeId": node_id,
"nextId": normalize_next_id(node.get("nextId")),
"props": props,
"childNodes": [normalize_node(child) for child in child_nodes],
}
if "prevId" in node:
normalized["prevId"] = node["prevId"]
if "description" in node:
normalized["description"] = copy.deepcopy(node.get("description"))
if "title" in node:
normalized["title"] = copy.deepcopy(node.get("title"))
if approval_type is not None:
normalized["approvalType"] = approval_type
return normalized
def _walk_nodes(nodes: list[dict[str, Any]]) -> list[dict[str, Any]]:
result: list[dict[str, Any]] = []
for node in nodes:
result.append(node)
result.extend(_walk_nodes(node.get("childNodes") or []))
return result
def _node_label(node: dict[str, Any]) -> str:
return f"{node.get('nodeId') or '<unknown>'}({ _i18n_text(node.get('name'), str(node.get('type') or 'node')) })"
def _non_empty_assignments(value: Any) -> bool:
return isinstance(value, list) and any(isinstance(item, dict) for item in value)
def _rule_list_has_entries(value: Any) -> bool:
if not isinstance(value, dict):
return False
rules = value.get("rules")
if isinstance(rules, list):
return any(isinstance(item, dict) for item in rules)
if isinstance(rules, dict):
return _rule_list_has_entries(rules)
return False
def _validate_assignment_columns(node: dict[str, Any], assignments: Any) -> None:
if not isinstance(assignments, list):
return
for index, assignment in enumerate(assignments, start=1):
if not isinstance(assignment, dict):
raise FlowSchemaError(f"{_node_label(node)} assignment #{index} must be object")
if not assignment.get("column"):
raise FlowSchemaError(f"{_node_label(node)} assignment #{index} missing column")
if not assignment.get("valueType"):
raise FlowSchemaError(f"{_node_label(node)} assignment #{index} missing valueType")
def _validate_designer_runtime_node(node: dict[str, Any]) -> None:
node_type = str(node.get("type") or "")
props = node.get("props")
if not isinstance(props, dict):
return
if node_type == "dataCreate":
if not props.get("formUuid"):
raise FlowSchemaError(f"{_node_label(node)} AddDataNode missing target formUuid")
assignments = props.get("assignments")
has_rules = _rule_list_has_entries(props.get("rules"))
add_data_rules = props.get("addDataRules") if isinstance(props.get("addDataRules"), dict) else {}
has_add_rules = _rule_list_has_entries(add_data_rules.get("rules"))
if not (_non_empty_assignments(assignments) or has_rules or has_add_rules):
raise FlowSchemaError(f"{_node_label(node)} AddDataNode missing field assignment rules")
_validate_assignment_columns(node, assignments)
if node_type == "dataUpdate":
if not props.get("sourceId"):
raise FlowSchemaError(f"{_node_label(node)} UpdateDataNode missing sourceId")
assignments = props.get("assignments")
update_rules = props.get("updateDataRules") if isinstance(props.get("updateDataRules"), dict) else {}
has_update_rules = _rule_list_has_entries(update_rules.get("rules"))
if not (_non_empty_assignments(assignments) or has_update_rules):
raise FlowSchemaError(f"{_node_label(node)} UpdateDataNode missing field assignment rules")
_validate_assignment_columns(node, assignments)
if node_type == "dataRetrieve":
if not props.get("sourceId"):
raise FlowSchemaError(f"{_node_label(node)} GetDataNode missing sourceId")
if node_type == "sendCard":
if not props.get("cardAppId"):
raise FlowSchemaError(f"{_node_label(node)} CardNode missing cardAppId")
if not props.get("cardPageCode"):
raise FlowSchemaError(f"{_node_label(node)} CardNode missing cardPageCode")
if props.get("sendType") == "update":
biz_id = props.get("bizId") if isinstance(props.get("bizId"), dict) else {}
value = str(biz_id.get("value") or "")
if not value:
raise FlowSchemaError(f"{_node_label(node)} CardNode sendType=update missing bizId.value")
if value in UNRESOLVED_CARD_BIZ_ID_VALUES:
raise FlowSchemaError(
f"{_node_label(node)} CardNode bizId.value={value!r} is not designer-visible; "
"choose a real variable from the designer dropdown, such as a current form fieldId"
)
def validate_flow_schema(flow: dict[str, Any]) -> None:
if not isinstance(flow, dict):
raise FlowSchemaError("flow schema must be object")
props = flow.get("props")
nodes = flow.get("nodes")
if not isinstance(props, dict):
raise FlowSchemaError("flow schema missing props object")
if not props.get("processCode"):
raise FlowSchemaError("flow schema missing props.processCode")
if not isinstance(nodes, list) or not nodes:
raise FlowSchemaError("flow schema nodes must be non-empty array")
seen: set[str] = set()
for node in _walk_nodes(nodes):
node_id = node.get("nodeId")
if node_id in seen:
raise FlowSchemaError(f"duplicate nodeId: {node_id}")
seen.add(node_id)
_validate_designer_runtime_node(node)
def build_automation_flow(
process_code: str,
nodes: list[dict[str, Any]],
*,
allow_withdraw: bool = True,
allow_collaboration: bool = True,
allow_temporary_storage: bool = True,
) -> dict[str, Any]:
if not process_code:
raise FlowSchemaError("processCode is required")
if not isinstance(nodes, list) or not nodes:
raise FlowSchemaError("nodes must be non-empty array")
flow = {
"props": {
"processCode": process_code,
"allowWithdraw": allow_withdraw,
"allowCollaboration": allow_collaboration,
"allowTemporaryStorage": allow_temporary_storage,
},
"nodes": [normalize_node(node) for node in nodes],
}
validate_flow_schema(flow)
return flow
def build_flow_schema(spec: Any, *, process_code: str | None = None) -> dict[str, Any]:
if isinstance(spec, list):
if not process_code:
raise FlowSchemaError("processCode is required when spec is a node array")
return build_automation_flow(process_code, spec)
if not isinstance(spec, dict):
raise FlowSchemaError("flow spec must be object or node array")
if "props" in spec and "nodes" in spec:
props = copy.deepcopy(spec["props"])
if not isinstance(props, dict):
raise FlowSchemaError("props must be object")
if process_code:
props["processCode"] = process_code
if "bindingForm" in spec and not props.get("bindingForm"):
props["bindingForm"] = copy.deepcopy(spec["bindingForm"])
nodes = spec["nodes"]
if not isinstance(nodes, list):
raise FlowSchemaError("nodes must be array")
flow = {"props": props, "nodes": [normalize_node(node) for node in nodes]}
for key in FLOW_PROP_KEYS:
flow["props"].setdefault(key, True)
for key in FLOW_TOP_LEVEL_KEYS:
if key in spec:
flow[key] = copy.deepcopy(spec[key])
validate_flow_schema(flow)
return flow
nodes = spec.get("nodes")
if not isinstance(nodes, list):
raise FlowSchemaError("flow spec missing nodes array")
code = process_code or spec.get("processCode")
flow = build_automation_flow(
str(code or ""),
nodes,
allow_withdraw=bool(spec.get("allowWithdraw", True)),
allow_collaboration=bool(spec.get("allowCollaboration", True)),
allow_temporary_storage=bool(spec.get("allowTemporaryStorage", True)),
)
if "bindingForm" in spec:
flow["props"]["bindingForm"] = copy.deepcopy(spec["bindingForm"])
return flow
def _i18n_text(value: Any, default: str = "") -> str:
if isinstance(value, dict):
return str(value.get("zh_CN") or value.get("en_US") or value.get("value") or default)
return str(value or default)
def _default_component_props() -> dict[str, Any]:
return {
"defaultDataSource": {},
"relateAppType": "",
"relateOrderEnable": False,
"relateOrderConfig": [],
}
def _rule_for_assignment(assignment: dict[str, Any], fields_by_id: dict[str, dict[str, Any]]) -> dict[str, Any]:
column = str(assignment.get("column") or "")
field = copy.deepcopy(fields_by_id.get(column) or {})
rule = {
"ruleId": f"rule-{column}" if column else "rule-unknown",
"name": column,
"label": field.get("label") or column,
"componentName": field.get("componentName"),
"componentOption": field.get("componentOption", "[]"),
"componentProps": copy.deepcopy(field.get("componentProps") or _default_component_props()),
"required": bool(field.get("required", False)),
"valueType": assignment.get("valueType", "literal"),
"value": assignment.get("value"),
}
for key in ("__display", "__source"):
if key in assignment:
rule[key] = copy.deepcopy(assignment[key])
return rule
def _component_option_map(child_list: list[dict[str, Any]]) -> dict[str, Any]:
return {
str(field.get("fieldId") or field.get("name") or ""): copy.deepcopy(field.get("componentOption", "[]"))
for field in child_list
if field.get("fieldId") or field.get("name")
}
def _rules_for_assignments(
assignments: list[dict[str, Any]],
child_list: list[dict[str, Any]],
existing_rules: Any = None,
) -> dict[str, Any]:
rules = copy.deepcopy(existing_rules or {})
component_option_map = _component_option_map(child_list)
if assignments and not rules.get("rules"):
fields_by_id = {str(field.get("fieldId") or field.get("name")): field for field in child_list}
rules = {
"ruleId": "rule-root",
"childList": copy.deepcopy(child_list),
"componentOptionMap": copy.deepcopy(component_option_map),
"rules": [_rule_for_assignment(item, fields_by_id) for item in assignments],
}
else:
rules.setdefault("ruleId", "rule-root")
rules.setdefault("childList", copy.deepcopy(child_list))
rules.setdefault("componentOptionMap", copy.deepcopy(component_option_map))
rules.setdefault("rules", [])
return rules
def _approval_option_lookup(props: dict[str, Any]) -> dict[str, dict[str, Any]]:
result: dict[str, dict[str, Any]] = {}
for key in ("approvalUsers", "approvalOptions", "approverOptions"):
options = props.get(key)
if not isinstance(options, list):
continue
for item in options:
if not isinstance(item, dict):
continue
user_id = item.get("id") or item.get("userId") or item.get("value")
if isinstance(user_id, str) and user_id:
normalized = copy.deepcopy(item)
normalized.setdefault("id", user_id)
normalized.setdefault("label", item.get("name") or item.get("userName") or user_id)
normalized.setdefault("type", "employee")
normalized.setdefault("roleType", "DINGTALK")
result[user_id] = normalized
return result
def _approval_value_for_view(value: Any, options_by_id: dict[str, dict[str, Any]]) -> Any:
if isinstance(value, dict):
item = copy.deepcopy(value)
user_id = item.get("id") or item.get("userId") or item.get("value")
if isinstance(user_id, str) and user_id:
item.setdefault("id", user_id)
item.setdefault("label", item.get("name") or item.get("userName") or item.get("id") or "")
item.setdefault("type", "employee")
item.setdefault("roleType", "DINGTALK")
return item
if isinstance(value, str):
if value in options_by_id:
return copy.deepcopy(options_by_id[value])
return {
"id": value,
"label": value,
"type": "employee",
"roleType": "DINGTALK",
}
return value
def _approver_rules_for_view(node: dict[str, Any], props: dict[str, Any]) -> dict[str, Any]:
raw_rules = props.get("approverRules") if isinstance(props.get("approverRules"), dict) else {}
result = copy.deepcopy(raw_rules)
result.setdefault("type", node.get("approvalType") or props.get("approvalType") or "ext_target_approval")
result.setdefault("mode", "ApprovalNode_rules_only")
if result.get("type") == "ext_target_approval_originator":
approver_list = result.get("approverList")
if isinstance(approver_list, list) and approver_list:
normalized_list = []
for item in approver_list:
if isinstance(item, dict):
normalized = copy.deepcopy(item)
if normalized.get("type") == "ext_target_approval_originator":
normalized["type"] = "ext_target_approval"
normalized_list.append(normalized)
else:
normalized_list.append(item)
result["approverList"] = normalized_list
else:
result["approverList"] = [{"type": "ext_target_approval"}]
result.setdefault("description", "发起人本人")
else:
result.setdefault("approverList", [{"type": result["type"]}])
result.setdefault("multiApproverType", "all")
result.setdefault("conditionalMode", props.get("conditionalMode", "conditional"))
approvals = result.get("approvals", props.get("approvals", []))
if isinstance(approvals, list):
options_by_id = _approval_option_lookup(props)
result["approvals"] = [_approval_value_for_view(item, options_by_id) for item in approvals]
labels = [
str(item.get("label") or item.get("id") or "")
for item in result["approvals"]
if isinstance(item, dict)
]
if labels and not result.get("description"):
result["description"] = "、".join(label for label in labels if label)
return result
def _operator_actions_for_view(props: dict[str, Any]) -> dict[str, Any]:
normal_actions = []
for action in props.get("actions") or []:
if isinstance(action, dict):
normalized = copy.deepcopy(action)
normalized.setdefault("hidden", False)
normalized.setdefault("name", i18n_name(normalized.get("name") or normalized.get("text"), "提交"))
normalized.setdefault("text", copy.deepcopy(normalized["name"]))
normalized.setdefault("alias", copy.deepcopy(normalized["name"]))
normal_actions.append(normalized)
continue
raw = str(action or "").lower()
if raw in {"submit", "agree", "confirm"}:
normal_actions.append({
"hidden": False,
"name": {"en_US": "Submit", "zh_CN": "提交", "type": "i18n"},
"text": {"en_US": "Submit", "zh_CN": "提交", "type": "i18n"},
"action": "submit",
"alias": {"en_US": "Submit", "zh_CN": "提交", "type": "i18n"},
})
elif raw in {"transfer", "forward"}:
normal_actions.append({
"hidden": True,
"name": {"en_US": "Forward", "zh_CN": "转交", "type": "i18n"},
"text": {"en_US": "Forward", "zh_CN": "转交", "type": "i18n"},
"action": "forward",
"alias": {"en_US": "Forward", "zh_CN": "转交", "type": "i18n"},
})
if not normal_actions:
normal_actions.append({
"hidden": False,
"name": {"en_US": "Submit", "zh_CN": "提交", "type": "i18n"},
"text": {"en_US": "Submit", "zh_CN": "提交", "type": "i18n"},
"action": "submit",
"alias": {"en_US": "Submit", "zh_CN": "提交", "type": "i18n"},
})
return {
"normalActions": normal_actions,
"appendActions": copy.deepcopy(props.get("appendActions") or []),
}
def _operator_rules_for_view(node: dict[str, Any], props: dict[str, Any]) -> dict[str, Any]:
actioner_rule = props.get("actionerRule") if isinstance(props.get("actionerRule"), dict) else {}
if actioner_rule and not props.get("approvals") and not props.get("approverRules"):
rule_type = str(actioner_rule.get("type") or "")
value = str(actioner_rule.get("value") or "")
if rule_type == "static" and value:
props = copy.deepcopy(props)
props["approvals"] = [item.strip() for item in value.split(",") if item.strip()]
props.setdefault("approvalType", "ext_target_approval")
return _approver_rules_for_view(node, props)
def _message_recipient_description(props: dict[str, Any]) -> str:
parts: list[str] = []
for item in props.get("toUsers") or []:
if not isinstance(item, dict):
continue
label = item.get("userName") or item.get("name") or item.get("label") or item.get("userId")
if label:
parts.append(str(label))
for item in props.get("toRoles") or []:
if not isinstance(item, dict):
continue
label = item.get("roleName") or item.get("name") or item.get("label") or item.get("roleCode")
if label:
parts.append(str(label))
field_labels = {
"PROCESS_PARTICIPANTS": "流程参与人",
"PROCESS_APPROVERS": "流程审批人",
"PROCESS_CARBONS": "流程抄送人",
"FORM_INST_MODIFIER": "表单修改人",
}
for item in props.get("userFields") or []:
label = field_labels.get(str(item), str(item))
if label:
parts.append(label)
for key, label in (
("toVars", "变量接收人"),
("toGroups", "群组"),
("toGroupVars", "变量群组"),
("robotUrls", "机器人"),
):
values = props.get(key) or []
if values:
parts.append(label)
seen: set[str] = set()
result: list[str] = []
for part in parts:
if part not in seen:
seen.add(part)
result.append(part)
return "、".join(result)
def _send_message_rules_for_view(node: dict[str, Any], props: dict[str, Any]) -> tuple[dict[str, Any], str]:
result = copy.deepcopy(props)
description = result.get("description")
if isinstance(description, dict):
description = _i18n_text(description)
if not description and node.get("description") not in (None, ""):
description = _i18n_text(node.get("description"))
if not description:
description = _message_recipient_description(result)
if description:
result["description"] = description
return result, str(description or "")
def _condition_description(condition_group: Any) -> str:
if not isinstance(condition_group, dict):
return ""
rules = condition_group.get("rules")
if not isinstance(rules, list):
return ""
parts = []
for rule in rules:
if not isinstance(rule, dict):
continue
name = str(rule.get("name") or rule.get("id") or "")
op = str(rule.get("op") or rule.get("opCode") or "")
value = rule.get("ruleValue", rule.get("value", ""))
if isinstance(value, list):
value_text = "、".join(str(item.get("label") or item.get("name") or item.get("id") or item) if isinstance(item, dict) else str(item) for item in value)
else:
value_text = str(value)
if name and op:
parts.append(f"{name}{op}{value_text}")
return "".join(parts)
def _condition_rules_for_view(props: dict[str, Any]) -> dict[str, Any]:
raw_conditions = props.get("conditions") if isinstance(props.get("conditions"), dict) else {}
result = copy.deepcopy(raw_conditions)
result.setdefault("condition", "AND")
result.setdefault("conditionCode", "&&")
result.setdefault("rules", [])
result["calculate"] = props.get("calculate") or raw_conditions.get("calculate") or "condition"
result["conditions"] = copy.deepcopy(raw_conditions)
result["isDefault"] = bool(props.get("isDefault", raw_conditions.get("isDefault", False)))
if "priority" in props:
result["priority"] = props["priority"]
elif "priority" in raw_conditions:
result["priority"] = raw_conditions["priority"]
description = props.get("description") or raw_conditions.get("description") or _condition_description(raw_conditions)
if not description and result.get("calculate") == "all" and not result.get("rules"):
description = "全部数据"
if description:
result["description"] = description
return result
def _build_add_data_rules(
node_props: dict[str, Any],
*,
fields: list[dict[str, Any]] | None = None,
form_title: str = "",
) -> dict[str, Any]:
raw_add_rules = node_props.get("addDataRules") if isinstance(node_props.get("addDataRules"), dict) else {}
child_list = copy.deepcopy(fields or raw_add_rules.get("inputs", {}).get("childList") or [])
component_option_map = _component_option_map(child_list)
assignments = copy.deepcopy(node_props.get("assignments") or raw_add_rules.get("assignments") or [])
rules = _rules_for_assignments(assignments, child_list, raw_add_rules.get("rules") or node_props.get("rules"))
inputs = copy.deepcopy(raw_add_rules.get("inputs") or node_props.get("inputs") or {})
inputs.setdefault("childList", copy.deepcopy(child_list))
inputs.setdefault("componentOptionMap", component_option_map)
description = raw_add_rules.get("description")
if isinstance(description, dict):
description = _i18n_text(description)
if not description and form_title:
description = f"在 [{form_title}] 中新增数据"
if not description:
description = node_props.get("description")
if isinstance(description, dict):
description = _i18n_text(description)
return {
"formUuid": node_props.get("formUuid", raw_add_rules.get("formUuid", "")),
"appType": node_props.get("appType", raw_add_rules.get("appType", "")),
"subFormUuid": node_props.get("subFormUuid", raw_add_rules.get("subFormUuid", "")),
"insertType": (
node_props.get("insertType")
or raw_add_rules.get("insertType")
or node_props.get("originalType")
or raw_add_rules.get("originalType")
or "form"
),
"originalType": node_props.get("originalType", raw_add_rules.get("originalType", "form")),
"type": node_props.get("type", raw_add_rules.get("type", "single")),
"sourceId": node_props.get("sourceId", raw_add_rules.get("sourceId", "")),
"assignments": copy.deepcopy(raw_add_rules.get("assignments") or []),
"inputs": inputs,
"rules": rules,
"description": description or None,
}
def _normalize_none_operation(value: Any) -> str:
raw = str(value or "").strip()
lowered = raw.lower()
if lowered in {"add", "新增一条数据"} or raw == "ADD":
return "add"
if lowered in {"ignored", "ignore", "skip", "跳过当前节点"} or raw == "IGNORED":
return "ignored"
return lowered or "ignored"
def _condition_rule_text(rule: Any) -> str:
if not isinstance(rule, dict):
return ""
name = str(rule.get("name") or rule.get("label") or rule.get("id") or rule.get("column") or "")
op = str(rule.get("op") or rule.get("opCode") or "")
value = rule.get("ruleValue") if rule.get("ruleValue") not in (None, "") else rule.get("value")
value_text = str(value or "")
if name and op and value_text:
return f"{name}{op}{value_text}"
if name and op:
return f"{name}{op}"
return name or value_text
def _condition_description_for_get_data(condition: Any) -> str:
if not isinstance(condition, dict):
return ""
rules = condition.get("rules")
if not isinstance(rules, list):
return ""
parts = [_condition_rule_text(rule) for rule in rules]
return "".join(part for part in parts if part)
def _build_get_data_rules(
node_props: dict[str, Any],
*,
form_title: str = "",
) -> tuple[dict[str, Any], str]:
raw_get_data = node_props.get("getData") if isinstance(node_props.get("getData"), dict) else {}
get_data = copy.deepcopy(raw_get_data or node_props)
get_data.setdefault("type", node_props.get("type") or "single")
get_data.setdefault("sourceId", node_props.get("sourceId") or "")
get_data.setdefault("appType", node_props.get("appType") or "")
get_data.setdefault("originalType", node_props.get("originalType") or "form")
get_data.setdefault("subSourceId", node_props.get("subSourceId") or "")
get_data.setdefault("filterType", node_props.get("filterType") or "condition")
get_data.setdefault("quantity", str(node_props.get("quantity") or "1"))
get_data.setdefault("assignments", copy.deepcopy(node_props.get("assignments") or []))
get_data.setdefault("sort", copy.deepcopy(node_props.get("sort") or {"type": "none", "column": ""}))
description = get_data.get("description") or node_props.get("description")
if isinstance(description, dict):
description = _i18n_text(description)
if not description:
condition_text = _condition_description_for_get_data(get_data.get("condition"))
if condition_text:
description = condition_text
elif form_title:
quantity = get_data.get("quantity") or "1"
description = f"在 [{form_title}] 中查询 {quantity} 条数据"
if description:
get_data["description"] = description
condition = get_data.get("condition")
if isinstance(condition, dict):
condition.setdefault("description", description)
condition.setdefault("conditions", copy.deepcopy(condition))
return get_data, str(description or "")
def _find_data_source_form(
node_props: dict[str, Any],
*,
nodes_by_id: dict[str, dict[str, Any]] | None = None,
) -> tuple[str, str]:
app_type = str(node_props.get("appType") or "")
direct_form = node_props.get("formUuid") or node_props.get("sourceFormUuid")
if isinstance(direct_form, str) and direct_form:
return app_type, direct_form
source_id = str(node_props.get("sourceId") or "")
if source_id.startswith("FORM-"):
return app_type, source_id
source_node = (nodes_by_id or {}).get(source_id)
if isinstance(source_node, dict):
props = source_node.get("props") if isinstance(source_node.get("props"), dict) else {}
source_app = str(props.get("appType") or app_type)
source_form = props.get("formUuid") or props.get("sourceId")
if isinstance(source_form, str) and source_form.startswith("FORM-"):
return source_app, source_form
return app_type, ""
def _build_update_data_rules(
node_props: dict[str, Any],
*,
fields: list[dict[str, Any]] | None = None,
form_title: str = "",
) -> tuple[dict[str, Any], str]:
raw_update_rules = node_props.get("updateDataRules") if isinstance(node_props.get("updateDataRules"), dict) else {}
rules = copy.deepcopy(raw_update_rules or node_props)
child_list = copy.deepcopy(fields or raw_update_rules.get("inputs", {}).get("childList") or [])
component_option_map = _component_option_map(child_list)
assignments = copy.deepcopy(node_props.get("assignments") or raw_update_rules.get("assignments") or [])
rules.setdefault("type", node_props.get("type") or "node")
rules.setdefault("sourceId", node_props.get("sourceId") or "")
rules.setdefault("subSourceId", node_props.get("subSourceId") or "")
rules.setdefault("condition", copy.deepcopy(node_props.get("condition") or {}))
rules.setdefault("subCondition", copy.deepcopy(node_props.get("subCondition") or {}))
rules["assignments"] = assignments
display_rules = _rules_for_assignments(assignments, child_list, raw_update_rules.get("rules") or node_props.get("rules"))
rules["rules"] = display_rules
rules["dataRules"] = copy.deepcopy(
raw_update_rules.get("dataRules")
or node_props.get("dataRules")
or display_rules
)
inputs = copy.deepcopy(raw_update_rules.get("inputs") or node_props.get("inputs") or {})
inputs.setdefault("childList", copy.deepcopy(child_list))
inputs.setdefault("componentOptionMap", component_option_map)
rules["inputs"] = inputs
rules.setdefault("rulesFilter", copy.deepcopy(node_props.get("rulesFilter") or []))
rules.setdefault("tableRulesFilter", copy.deepcopy(node_props.get("tableRulesFilter") or []))
rules["noneOperation"] = _normalize_none_operation(
raw_update_rules.get("noneOperation") or node_props.get("noneOperation")
)
description = rules.get("description") or node_props.get("description")
if isinstance(description, dict):
description = _i18n_text(description)
if not description and form_title:
description = f"更新 [{form_title}] 中的数据"
if description:
rules["description"] = description
return rules, str(description or "")
def _view_component_name_for_node(node: dict[str, Any]) -> str:
node_type = str(node.get("type") or "")
props = node.get("props") if isinstance(node.get("props"), dict) else {}
if node_type == "dataRetrieve":
get_data = props.get("getData") if isinstance(props.get("getData"), dict) else props
retrieve_type = str(get_data.get("type") or "").lower()
return "GetBatchDataNode" if retrieve_type in {"batch", "multiple", "multi"} else "GetSingleDataNode"
if node_type == "CodeExecutor":
if "groovy" in props or str(props.get("scriptType") or "").lower() == "groovy":
return "GroovyNode"
return "JavaScriptNode"
return VIEW_COMPONENT_BY_TYPE.get(node_type, str(node.get("componentName") or node_type))
def _view_props_for_node(
node: dict[str, Any],
*,
data_create_metadata: dict[tuple[str, str], dict[str, Any]] | None = None,
nodes_by_id: dict[str, dict[str, Any]] | None = None,
) -> dict[str, Any]:
node_type = str(node.get("type") or "")
component_name = _view_component_name_for_node(node)
props = copy.deepcopy(node.get("props") or {})
name = copy.deepcopy(node.get("name") or i18n_name(None, DEFAULT_NODE_NAMES.get(node_type, node_type)))
if node.get("description") not in (None, ""):
props.setdefault("description", copy.deepcopy(node["description"]))
view_props: dict[str, Any] = {
"nodeName": component_name,
"name": name,
}
if node.get("description") not in (None, ""):
view_props["description"] = copy.deepcopy(node["description"])
if component_name == "AddDataNode":
target_key = (str(props.get("appType") or ""), str(props.get("formUuid") or ""))
metadata = (data_create_metadata or {}).get(target_key) or {}
add_data_rules = _build_add_data_rules(
props,
fields=metadata.get("fields") or [],
form_title=str(metadata.get("title") or ""),
)
view_props["addDataRules"] = add_data_rules
elif component_name in {"GetSingleDataNode", "GetBatchDataNode"}:
target_app, target_form = _find_data_source_form(props, nodes_by_id=nodes_by_id)
metadata = (data_create_metadata or {}).get((target_app, target_form)) or {}
get_data, description = _build_get_data_rules(
props,
form_title=str(metadata.get("title") or ""),
)
view_props["getData"] = get_data
if description:
view_props.setdefault("description", description)
elif component_name == "UpdateDataNode":
target_app, target_form = _find_data_source_form(props, nodes_by_id=nodes_by_id)
metadata = (data_create_metadata or {}).get((target_app, target_form)) or {}
update_data_rules, description = _build_update_data_rules(
props,
fields=metadata.get("fields") or [],
form_title=str(metadata.get("title") or ""),
)
view_props["updateDataRules"] = update_data_rules
if description:
view_props.setdefault("description", description)
elif component_name == "ApprovalNode":
view_props["approverRules"] = _approver_rules_for_view(node, props)
if props.get("actions") or props.get("appendActions"):
view_props["actions"] = {
"normalActions": copy.deepcopy(props.get("actions") or []),
"appendActions": copy.deepcopy(props.get("appendActions") or []),
}
elif component_name == "OperatorNode":
view_props["approverRules"] = _operator_rules_for_view(node, props)
view_props["actions"] = _operator_actions_for_view(props)
elif component_name == "SendMessageNode":
send_message_rules, description = _send_message_rules_for_view(node, props)
view_props["sendMessageRules"] = send_message_rules
if description:
view_props.setdefault("description", description)
elif node_type == "condition":
for key, value in props.items():
view_props.setdefault(key, copy.deepcopy(value))
view_props["conditions"] = _condition_rules_for_view(props)
elif node_type == "route":
for key, value in props.items():
view_props.setdefault(key, copy.deepcopy(value))
else:
rule_key = VIEW_COMPONENT_RULE_KEY.get(component_name)
if rule_key:
view_props[rule_key] = copy.deepcopy(props.get(rule_key) or props)
view_props["title"] = copy.deepcopy(node.get("title") or name)
return view_props
def _view_child_for_node(
node: dict[str, Any],
*,
data_create_metadata: dict[tuple[str, str], dict[str, Any]] | None = None,
nodes_by_id: dict[str, dict[str, Any]] | None = None,
) -> dict[str, Any]:
child = {
"componentName": _view_component_name_for_node(node),
"id": node.get("nodeId"),
"props": _view_props_for_node(node, data_create_metadata=data_create_metadata, nodes_by_id=nodes_by_id),
}
nested = node.get("childNodes") or []
if isinstance(nested, list) and nested:
child["children"] = [
_view_child_for_node(item, data_create_metadata=data_create_metadata, nodes_by_id=nodes_by_id)
for item in nested
]
return child
def build_process_view_schema(
flow: dict[str, Any],
*,
data_create_metadata: dict[tuple[str, str], dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Build the designer viewJson that matches a normalized process schema.
The backend processJson can execute dataCreate nodes with plain
``props.assignments``. The Yida designer, however, renders and re-saves
AddDataNode from ``props.addDataRules.rules.rules`` plus target field
metadata. This helper keeps both surfaces aligned.
"""
validate_flow_schema(flow)
nodes_by_id = {str(node.get("nodeId")): node for node in _walk_nodes(flow.get("nodes") or [])}
children = []
for node in flow.get("nodes") or []:
children.append(_view_child_for_node(node, data_create_metadata=data_create_metadata, nodes_by_id=nodes_by_id))
return {
"schema": {
"componentName": "CanvasEngine",
"id": "canvas",
"props": {},
"children": children,
},
"bindingForm": flow.get("props", {}).get("bindingForm"),
"formulaRules": flow.get("formulaRules", []),
"globalSetting": {
"enableSignature": False,
"stopAssociationRulesIfFailed": flow.get("props", {}).get("stopAssociationRulesIfFailed", False),
"nodeMerge": False,
"originatorMerge": False,
"allNodeMerge": False,
"behaviorList": [],
"needOpenDigitalSignNodes": [],
"approvalSummary": flow.get("approvalSummary", []),
"noRecordRecall": flow.get("props", {}).get("noRecordRecall", False),
"untimedRule": flow.get("props", {}).get("untimedRule", []),
},
}
def _sync_view_top_level_defaults(view: dict[str, Any], flow: dict[str, Any]) -> None:
props = flow.get("props") if isinstance(flow.get("props"), dict) else {}
binding_form = props.get("bindingForm")
if binding_form:
view["bindingForm"] = binding_form
view.setdefault("formulaRules", copy.deepcopy(flow.get("formulaRules") or []))
view.setdefault("flowConfig", copy.deepcopy(flow.get("flowConfig") or {}))
global_setting = view.setdefault("globalSetting", {})
if not isinstance(global_setting, dict):
global_setting = {}
view["globalSetting"] = global_setting
global_setting.setdefault("enableSignature", False)
global_setting.setdefault("stopAssociationRulesIfFailed", props.get("stopAssociationRulesIfFailed", False))
global_setting.setdefault("nodeMerge", False)
global_setting.setdefault("originatorMerge", False)
global_setting.setdefault("allNodeMerge", False)
global_setting.setdefault("behaviorList", [])
global_setting.setdefault("needOpenDigitalSignNodes", [])
global_setting.setdefault("approvalSummary", flow.get("approvalSummary", []))
global_setting.setdefault("noRecordRecall", props.get("noRecordRecall", False))
global_setting.setdefault("untimedRule", props.get("untimedRule", []))
def _iter_view_children(children: list[dict[str, Any]]):
for child in children:
yield child
nested = child.get("children") or []
if isinstance(nested, list):
yield from _iter_view_children(nested)
def merge_process_view_schema(
base_view: dict[str, Any],
flow: dict[str, Any],
*,
data_create_metadata: dict[tuple[str, str], dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Patch a designer viewJson with normalized flow-node configuration.
Prefer this over building a view from scratch when a source process version
already has a valid designer skeleton. It preserves designer-specific node
ids and props, while refreshing advanced component settings such as
AddDataNode's UI-visible addDataRules.
"""
validate_flow_schema(flow)
view = copy.deepcopy(base_view)
schema = view.setdefault("schema", {})
children = schema.setdefault("children", [])
nodes_by_id = {str(node.get("nodeId")): node for node in _walk_nodes(flow.get("nodes") or [])}
seen: set[str] = set()
def sync_children(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
synced: list[dict[str, Any]] = []
for child in items:
node_id = str(child.get("id") or "")
node = nodes_by_id.get(node_id)
if node:
fresh_child = _view_child_for_node(
node,
data_create_metadata=data_create_metadata,
nodes_by_id=nodes_by_id,
)
component_name = fresh_child["componentName"]
if (
child.get("componentName") == component_name
and str(node.get("type") or "") in {"apply", "finish"}
):
seen.add(node_id)
synced.append(child)
continue
child["componentName"] = component_name
child["props"] = fresh_child["props"]
if "children" in fresh_child:
child["children"] = fresh_child["children"]
else:
child.pop("children", None)
seen.add(node_id)
synced.append(child)
continue
nested = child.get("children")
if isinstance(nested, list):
child["children"] = sync_children(nested)
synced.append(child)
return synced
schema["children"] = sync_children(children)
children = schema.setdefault("children", [])
for child in _iter_view_children(children):
node_id = str(child.get("id") or "")
node = nodes_by_id.get(node_id)
if not node:
continue
seen.add(node_id)
existing_top_level = {str(child.get("id") or ""): child for child in children if child.get("id")}
ordered_children: list[dict[str, Any]] = []
for child in children:
node_id = str(child.get("id") or "")
if child.get("componentName") == "ApplyNode" and node_id not in nodes_by_id:
ordered_children.append(child)
for node in flow.get("nodes") or []:
node_id = str(node.get("nodeId") or "")
if str(node.get("type") or "") == "apply" and not existing_top_level.get(node_id):
continue
child = existing_top_level.get(node_id)
if not child:
child = _view_child_for_node(
node,
data_create_metadata=data_create_metadata,
nodes_by_id=nodes_by_id,
)
ordered_children.append(child)
if ordered_children:
schema["children"] = ordered_children
_sync_view_top_level_defaults(view, flow)
return view