@josephyan/qingflow-cli 1.0.11 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/npm/bin/qingflow.mjs +40 -2
- package/npm/lib/runtime.mjs +386 -15
- package/npm/scripts/postinstall.mjs +7 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-cli/SKILL.md +440 -0
- package/skills/qingflow-cli/manifest.yaml +10 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +94 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md +485 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md +237 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_MATCH_RULES.md +137 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md +263 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md +304 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_WORKSPACE_ICONS.md +41 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +139 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +84 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +129 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +195 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +159 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +20 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md +176 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +163 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md +107 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md +151 -0
- package/skills/qingflow-cli/reference/_batch_schema_complex.json +18 -0
- package/skills/qingflow-cli/reference/_batch_schema_scalar.json +17 -0
- package/skills/qingflow-cli/reference/charts_remove.example.json +1 -0
- package/skills/qingflow-cli/reference/charts_reorder.example.json +1 -0
- package/skills/qingflow-cli/reference/charts_upsert_bar.example.json +8 -0
- package/skills/qingflow-cli/reference/charts_upsert_dashboard_starter.example.json +37 -0
- package/skills/qingflow-cli/reference/charts_upsert_minimal.example.json +13 -0
- package/skills/qingflow-cli/reference/portal_sections_all_types.example.json +131 -0
- package/skills/qingflow-cli/reference/portal_sections_five_types.example.json +126 -0
- package/skills/qingflow-cli/reference/portal_sections_standard_workbench.example.json +128 -0
- package/skills/qingflow-cli/reference/schema_add_fields_minimal.example.json +7 -0
- package/skills/qingflow-cli/reference/schema_apply_add_fields_all_types.json +78 -0
- package/skills/qingflow-cli/reference/views_upsert_table_minimal.example.json +7 -0
- package/skills/qingflow-cli/scripts/builder-package-from-app-list.py +140 -0
- package/skills/qingflow-cli/scripts/find-app-by-keyword.py +132 -0
- package/skills/qingflow-cli/scripts/validate_qingflow_output_files.py +87 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/builder_facade/models.py +532 -48
- package/src/qingflow_mcp/builder_facade/service.py +9194 -2384
- package/src/qingflow_mcp/builder_facade/workflow_spec.py +111 -0
- package/src/qingflow_mcp/cli/commands/app.py +3 -16
- package/src/qingflow_mcp/cli/commands/builder.py +354 -56
- package/src/qingflow_mcp/cli/commands/record.py +89 -2
- package/src/qingflow_mcp/cli/formatters.py +32 -1
- package/src/qingflow_mcp/cli/main.py +245 -3
- package/src/qingflow_mcp/public_surface.py +11 -8
- package/src/qingflow_mcp/response_trim.py +143 -14
- package/src/qingflow_mcp/server.py +15 -12
- package/src/qingflow_mcp/server_app_builder.py +108 -30
- package/src/qingflow_mcp/server_app_user.py +17 -18
- package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +294 -0
- package/src/qingflow_mcp/solution/executor.py +3 -133
- package/src/qingflow_mcp/tools/ai_builder_tools.py +2617 -440
- package/src/qingflow_mcp/tools/app_tools.py +53 -8
- package/src/qingflow_mcp/tools/package_tools.py +16 -2
- package/src/qingflow_mcp/tools/record_tools.py +2095 -176
- package/src/qingflow_mcp/tools/resource_read_tools.py +3 -0
- package/src/qingflow_mcp/tools/solution_tools.py +30 -2
- package/src/qingflow_mcp/tools/workflow_tools.py +3 -31
- package/src/qingflow_mcp/version.py +110 -0
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +0 -173
|
@@ -6,6 +6,7 @@ from typing import Any
|
|
|
6
6
|
from ..errors import QingflowApiError, raise_tool_error
|
|
7
7
|
from ..json_types import JSONObject
|
|
8
8
|
from ..list_type_labels import SYSTEM_VIEW_DEFINITIONS
|
|
9
|
+
from ..solution.compiler.icon_utils import workspace_icon_config
|
|
9
10
|
from .app_tools import _analysis_supported_for_view_type
|
|
10
11
|
from .base import ToolBase, tool_cn_name
|
|
11
12
|
from .qingbi_report_tools import QingbiReportTools
|
|
@@ -75,6 +76,7 @@ class ResourceReadTools(ToolBase):
|
|
|
75
76
|
"dash_key": dash_key,
|
|
76
77
|
"dash_name": dash_name,
|
|
77
78
|
"dash_icon": dash_icon,
|
|
79
|
+
"icon_config": workspace_icon_config(dash_icon),
|
|
78
80
|
"package_tag_ids": package_tag_ids,
|
|
79
81
|
"component_count": len(components),
|
|
80
82
|
"components": components,
|
|
@@ -368,6 +370,7 @@ def _normalize_portal_list_items(raw_items: Any) -> list[dict[str, Any]]:
|
|
|
368
370
|
"dash_key": dash_key or None,
|
|
369
371
|
"dash_name": dash_name or None,
|
|
370
372
|
"dash_icon": dash_icon,
|
|
373
|
+
"icon_config": workspace_icon_config(dash_icon),
|
|
371
374
|
"package_tag_ids": package_tag_ids,
|
|
372
375
|
}
|
|
373
376
|
)
|
|
@@ -38,7 +38,6 @@ from .qingbi_report_tools import QingbiReportTools
|
|
|
38
38
|
from .record_tools import RecordTools
|
|
39
39
|
from .role_tools import RoleTools
|
|
40
40
|
from .view_tools import ViewTools
|
|
41
|
-
from .workflow_tools import WorkflowTools
|
|
42
41
|
from .workspace_tools import WorkspaceTools
|
|
43
42
|
|
|
44
43
|
STAGED_BUILD_MODES = {"preflight", "plan", "apply", "repair"}
|
|
@@ -737,6 +736,36 @@ class SolutionTools(ToolBase):
|
|
|
737
736
|
) -> dict[str, Any]:
|
|
738
737
|
"""执行方案相关逻辑。"""
|
|
739
738
|
mode = _normalize_staged_build_mode(mode)
|
|
739
|
+
app_key = str(flow_spec.get("app_key") or "").strip()
|
|
740
|
+
spec_payload = flow_spec.get("spec")
|
|
741
|
+
if app_key and isinstance(spec_payload, dict) and spec_payload:
|
|
742
|
+
from .ai_builder_tools import AiBuilderTools
|
|
743
|
+
|
|
744
|
+
builder = AiBuilderTools(self.sessions, self.backend)
|
|
745
|
+
if mode in {"preflight", "plan"}:
|
|
746
|
+
return {
|
|
747
|
+
"build_id": build_id or _generate_build_id(run_label=run_label, stage_name="flow"),
|
|
748
|
+
"mode": mode,
|
|
749
|
+
"stage": "flow",
|
|
750
|
+
"status": "planned" if mode == "plan" else "preflighted",
|
|
751
|
+
"normalized_args": {"app_key": app_key, "spec": spec_payload},
|
|
752
|
+
"tool_name": "solution_build_flow",
|
|
753
|
+
}
|
|
754
|
+
result = builder.app_flow_apply(
|
|
755
|
+
profile=profile,
|
|
756
|
+
app_key=app_key,
|
|
757
|
+
spec=spec_payload,
|
|
758
|
+
publish=publish,
|
|
759
|
+
schema_version=flow_spec.get("schema_version") or flow_spec.get("schemaVersion"),
|
|
760
|
+
)
|
|
761
|
+
return {
|
|
762
|
+
"build_id": build_id,
|
|
763
|
+
"mode": mode,
|
|
764
|
+
"stage": "flow",
|
|
765
|
+
"status": result.get("status"),
|
|
766
|
+
"result": result,
|
|
767
|
+
"tool_name": "solution_build_flow",
|
|
768
|
+
}
|
|
740
769
|
return self._stage_build(
|
|
741
770
|
profile=profile,
|
|
742
771
|
mode=mode,
|
|
@@ -1409,7 +1438,6 @@ class SolutionTools(ToolBase):
|
|
|
1409
1438
|
role_tools=RoleTools(self.sessions, self.backend),
|
|
1410
1439
|
app_tools=AppTools(self.sessions, self.backend),
|
|
1411
1440
|
record_tools=RecordTools(self.sessions, self.backend),
|
|
1412
|
-
workflow_tools=WorkflowTools(self.sessions, self.backend),
|
|
1413
1441
|
view_tools=ViewTools(self.sessions, self.backend),
|
|
1414
1442
|
chart_tools=QingbiReportTools(self.sessions, self.backend),
|
|
1415
1443
|
portal_tools=PortalTools(self.sessions, self.backend),
|
|
@@ -14,25 +14,13 @@ class WorkflowTools(ToolBase):
|
|
|
14
14
|
|
|
15
15
|
类型:流程配置工具。
|
|
16
16
|
主要职责:
|
|
17
|
-
1.
|
|
18
|
-
2.
|
|
19
|
-
|
|
17
|
+
1. 支持流程运行时校验与调试辅助能力;
|
|
18
|
+
2. 保留 legacy 节点写路径供内部/测试直接调用(非 MCP 公开)。
|
|
19
|
+
设计期流程配置读取统一走 WorkflowSpec(app_flow_get / GET /api/workflow/spec)。
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
22
|
def register(self, mcp: FastMCP) -> None:
|
|
23
23
|
"""注册当前工具到 MCP 服务。"""
|
|
24
|
-
@mcp.tool()
|
|
25
|
-
def workflow_list_nodes(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
|
|
26
|
-
return self.workflow_list_nodes(profile=profile, app_key=app_key)
|
|
27
|
-
|
|
28
|
-
@mcp.tool()
|
|
29
|
-
def workflow_get_node_detail(profile: str = DEFAULT_PROFILE, app_key: str = "", audit_node_id: int = 0) -> JSONObject:
|
|
30
|
-
return self.workflow_get_node_detail(profile=profile, app_key=app_key, audit_node_id=audit_node_id)
|
|
31
|
-
|
|
32
|
-
@mcp.tool()
|
|
33
|
-
def workflow_get_global_settings(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
|
|
34
|
-
return self.workflow_get_global_settings(profile=profile, app_key=app_key)
|
|
35
|
-
|
|
36
24
|
@mcp.tool()
|
|
37
25
|
def workflow_get_future_nodes(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0) -> JSONObject:
|
|
38
26
|
return self.workflow_get_future_nodes(profile=profile, app_key=app_key, apply_id=apply_id)
|
|
@@ -53,22 +41,6 @@ class WorkflowTools(ToolBase):
|
|
|
53
41
|
audit_node_id=audit_node_id,
|
|
54
42
|
)
|
|
55
43
|
|
|
56
|
-
@mcp.tool()
|
|
57
|
-
def workflow_get_qsource_active(profile: str = DEFAULT_PROFILE, app_key: str = "", qsource_id: int = 0) -> JSONObject:
|
|
58
|
-
return self.workflow_get_qsource_active(profile=profile, app_key=app_key, qsource_id=qsource_id)
|
|
59
|
-
|
|
60
|
-
@mcp.tool()
|
|
61
|
-
def workflow_get_qsource_passive(profile: str = DEFAULT_PROFILE, app_key: str = "", qsource_id: int = 0) -> JSONObject:
|
|
62
|
-
return self.workflow_get_qsource_passive(profile=profile, app_key=app_key, qsource_id=qsource_id)
|
|
63
|
-
|
|
64
|
-
@mcp.tool()
|
|
65
|
-
def workflow_get_editable_question_ids(profile: str = DEFAULT_PROFILE, app_key: str = "", audit_node_id: int = 0) -> JSONObject:
|
|
66
|
-
return self.workflow_get_editable_question_ids(profile=profile, app_key=app_key, audit_node_id=audit_node_id)
|
|
67
|
-
|
|
68
|
-
@mcp.tool()
|
|
69
|
-
def workflow_get_print_nodes(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
|
|
70
|
-
return self.workflow_get_print_nodes(profile=profile, app_key=app_key)
|
|
71
|
-
|
|
72
44
|
@tool_cn_name("流程节点列表")
|
|
73
45
|
def workflow_list_nodes(self, *, profile: str, app_key: str) -> JSONObject:
|
|
74
46
|
"""执行流程相关逻辑。"""
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
from importlib import metadata
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_cli_version() -> str:
|
|
12
|
+
package_json_version = _find_package_json_version()
|
|
13
|
+
if package_json_version:
|
|
14
|
+
return package_json_version
|
|
15
|
+
try:
|
|
16
|
+
return metadata.version("qingflow-mcp")
|
|
17
|
+
except metadata.PackageNotFoundError:
|
|
18
|
+
return "0+local"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_cli_version_info() -> dict[str, str | None]:
|
|
22
|
+
package_root = _find_package_root()
|
|
23
|
+
return {
|
|
24
|
+
"version": get_cli_version(),
|
|
25
|
+
"package": _find_package_name(package_root) or "@qingflow-tech/qingflow-cli",
|
|
26
|
+
"executable_path": _resolve_executable_path(),
|
|
27
|
+
"command_path": shutil.which("qingflow"),
|
|
28
|
+
"package_root": str(package_root) if package_root is not None else None,
|
|
29
|
+
"skill_version": _find_skill_version(package_root),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _find_package_json_version() -> str | None:
|
|
34
|
+
package_root = _find_package_root()
|
|
35
|
+
if package_root is None:
|
|
36
|
+
return None
|
|
37
|
+
try:
|
|
38
|
+
payload = json.loads((package_root / "package.json").read_text(encoding="utf-8"))
|
|
39
|
+
except (OSError, json.JSONDecodeError):
|
|
40
|
+
return None
|
|
41
|
+
version = str(payload.get("version") or "")
|
|
42
|
+
return version or None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _find_package_root() -> Path | None:
|
|
46
|
+
current = Path(__file__).resolve()
|
|
47
|
+
for parent in current.parents:
|
|
48
|
+
package_json = parent / "package.json"
|
|
49
|
+
if not package_json.exists():
|
|
50
|
+
continue
|
|
51
|
+
try:
|
|
52
|
+
payload = json.loads(package_json.read_text(encoding="utf-8"))
|
|
53
|
+
except (OSError, json.JSONDecodeError):
|
|
54
|
+
continue
|
|
55
|
+
name = str(payload.get("name") or "")
|
|
56
|
+
version = str(payload.get("version") or "")
|
|
57
|
+
if version and name in {
|
|
58
|
+
"qingflow-mcp-workspace",
|
|
59
|
+
"@qingflow-tech/qingflow-cli",
|
|
60
|
+
"@qingflow-tech/qingflow-app-user-mcp",
|
|
61
|
+
"@qingflow-tech/qingflow-app-builder-mcp",
|
|
62
|
+
"@josephyan/qingflow-cli",
|
|
63
|
+
"@josephyan/qingflow-app-user-mcp",
|
|
64
|
+
"@josephyan/qingflow-app-builder-mcp",
|
|
65
|
+
}:
|
|
66
|
+
return parent
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _find_package_name(package_root: Path | None) -> str | None:
|
|
71
|
+
if package_root is None:
|
|
72
|
+
return None
|
|
73
|
+
try:
|
|
74
|
+
payload = json.loads((package_root / "package.json").read_text(encoding="utf-8"))
|
|
75
|
+
except (OSError, json.JSONDecodeError):
|
|
76
|
+
return None
|
|
77
|
+
name = str(payload.get("name") or "")
|
|
78
|
+
return name or None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _resolve_executable_path() -> str | None:
|
|
82
|
+
if not sys.argv:
|
|
83
|
+
return None
|
|
84
|
+
raw = str(sys.argv[0] or "").strip()
|
|
85
|
+
if not raw:
|
|
86
|
+
return None
|
|
87
|
+
try:
|
|
88
|
+
return str(Path(raw).resolve())
|
|
89
|
+
except OSError:
|
|
90
|
+
return raw
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _find_skill_version(package_root: Path | None) -> str | None:
|
|
94
|
+
if package_root is None:
|
|
95
|
+
return None
|
|
96
|
+
candidates = [
|
|
97
|
+
package_root / "skills" / "qingflow-cli" / "SKILL.md",
|
|
98
|
+
package_root / "skill" / "qingflow-cli" / "SKILL.md",
|
|
99
|
+
]
|
|
100
|
+
for skill_file in candidates:
|
|
101
|
+
if not skill_file.exists():
|
|
102
|
+
continue
|
|
103
|
+
try:
|
|
104
|
+
text = skill_file.read_text(encoding="utf-8")
|
|
105
|
+
except OSError:
|
|
106
|
+
continue
|
|
107
|
+
match = re.search(r"Skill\s*版本\**[::]\s*`?([^`\s))]+)", text)
|
|
108
|
+
if match:
|
|
109
|
+
return match.group(1).strip()
|
|
110
|
+
return None
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
from ..spec_models import EntitySpec, WorkflowNodeType
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
WORKFLOW_TYPE_MAP = {
|
|
9
|
-
WorkflowNodeType.start: {"type": 0, "dealType": 3},
|
|
10
|
-
WorkflowNodeType.branch: {"type": 1, "dealType": None},
|
|
11
|
-
WorkflowNodeType.audit: {"type": 0, "dealType": 0},
|
|
12
|
-
WorkflowNodeType.fill: {"type": 0, "dealType": 1},
|
|
13
|
-
WorkflowNodeType.copy: {"type": 0, "dealType": 2},
|
|
14
|
-
WorkflowNodeType.webhook: {"type": 3, "dealType": 10},
|
|
15
|
-
WorkflowNodeType.condition: {"type": 2, "dealType": None},
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _default_audit_user_infos() -> dict[str, Any]:
|
|
20
|
-
return {
|
|
21
|
-
"member": [],
|
|
22
|
-
"depart": [],
|
|
23
|
-
"role": [],
|
|
24
|
-
"dynamic": [],
|
|
25
|
-
"includeSubDeparts": None,
|
|
26
|
-
"externalMemberList": [],
|
|
27
|
-
"externalDepartList": [],
|
|
28
|
-
"role_refs": [],
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def compile_workflow(entity: EntitySpec) -> dict[str, Any] | None:
|
|
33
|
-
workflow = entity.workflow
|
|
34
|
-
if workflow is None or not workflow.enabled:
|
|
35
|
-
return None
|
|
36
|
-
actions: list[dict[str, Any]] = []
|
|
37
|
-
seen_node_ids: set[str] = set()
|
|
38
|
-
created_extra_branch_lanes: set[str] = set()
|
|
39
|
-
start_node_ids = {
|
|
40
|
-
node.node_id
|
|
41
|
-
for node in workflow.nodes
|
|
42
|
-
if node.node_type == WorkflowNodeType.start
|
|
43
|
-
}
|
|
44
|
-
for node in workflow.nodes:
|
|
45
|
-
if node.node_type == WorkflowNodeType.start:
|
|
46
|
-
seen_node_ids.add(node.node_id)
|
|
47
|
-
continue
|
|
48
|
-
if node.parent_node_id and node.parent_node_id not in seen_node_ids:
|
|
49
|
-
raise ValueError(f"workflow node '{node.node_id}' must appear after parent node '{node.parent_node_id}'")
|
|
50
|
-
if node.branch_parent_id and node.branch_parent_id not in seen_node_ids:
|
|
51
|
-
raise ValueError(f"workflow node '{node.node_id}' must appear after branch node '{node.branch_parent_id}'")
|
|
52
|
-
branch_index = _branch_index(node)
|
|
53
|
-
branch_lane_ref = _branch_lane_ref(node.branch_parent_id, branch_index) if node.branch_parent_id else None
|
|
54
|
-
if branch_lane_ref and branch_index > 2 and branch_lane_ref not in created_extra_branch_lanes:
|
|
55
|
-
actions.append(
|
|
56
|
-
{
|
|
57
|
-
"action": "create_sub_branch",
|
|
58
|
-
"node_id": branch_lane_ref,
|
|
59
|
-
"payload": {
|
|
60
|
-
"editVersionNo": 1,
|
|
61
|
-
"auditNodeRef": node.branch_parent_id,
|
|
62
|
-
},
|
|
63
|
-
}
|
|
64
|
-
)
|
|
65
|
-
created_extra_branch_lanes.add(branch_lane_ref)
|
|
66
|
-
lane_only = bool((node.config or {}).get("__lane_only__")) and node.node_type == WorkflowNodeType.condition and branch_lane_ref
|
|
67
|
-
if lane_only:
|
|
68
|
-
lane_payload = {key: value for key, value in node.config.items() if key != "__lane_only__"}
|
|
69
|
-
actions.append(
|
|
70
|
-
{
|
|
71
|
-
"action": "update_node",
|
|
72
|
-
"node_id": branch_lane_ref,
|
|
73
|
-
"node_name": node.name,
|
|
74
|
-
"node_type": node.node_type.value,
|
|
75
|
-
"payload": {
|
|
76
|
-
"editVersionNo": 1,
|
|
77
|
-
"auditNodeName": node.name,
|
|
78
|
-
"type": WORKFLOW_TYPE_MAP[node.node_type]["type"],
|
|
79
|
-
"dealType": WORKFLOW_TYPE_MAP[node.node_type]["dealType"],
|
|
80
|
-
**lane_payload,
|
|
81
|
-
},
|
|
82
|
-
}
|
|
83
|
-
)
|
|
84
|
-
seen_node_ids.add(node.node_id)
|
|
85
|
-
continue
|
|
86
|
-
actions.append(
|
|
87
|
-
{
|
|
88
|
-
"action": "add_node",
|
|
89
|
-
"node_id": node.node_id,
|
|
90
|
-
"node_name": node.name,
|
|
91
|
-
"node_type": node.node_type.value,
|
|
92
|
-
"payload": {
|
|
93
|
-
"editVersionNo": 1,
|
|
94
|
-
"auditNodeName": node.name,
|
|
95
|
-
"type": WORKFLOW_TYPE_MAP[node.node_type]["type"],
|
|
96
|
-
"dealType": WORKFLOW_TYPE_MAP[node.node_type]["dealType"],
|
|
97
|
-
"prevNodeRef": _prev_node_ref(node, branch_lane_ref, start_node_ids),
|
|
98
|
-
"auditUserInfos": _build_audit_user_infos(node)
|
|
99
|
-
if node.node_type in {WorkflowNodeType.audit, WorkflowNodeType.fill, WorkflowNodeType.copy}
|
|
100
|
-
else None,
|
|
101
|
-
**node.config,
|
|
102
|
-
},
|
|
103
|
-
}
|
|
104
|
-
)
|
|
105
|
-
seen_node_ids.add(node.node_id)
|
|
106
|
-
return {
|
|
107
|
-
"global_settings": {
|
|
108
|
-
"editVersionNo": 1,
|
|
109
|
-
**workflow.global_settings,
|
|
110
|
-
},
|
|
111
|
-
"actions": actions,
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def _build_audit_user_infos(node) -> dict[str, Any]:
|
|
116
|
-
audit_user_infos = _default_audit_user_infos()
|
|
117
|
-
assignees = node.assignees or {}
|
|
118
|
-
member_uids = assignees.get("member_uids") or []
|
|
119
|
-
if member_uids:
|
|
120
|
-
audit_user_infos["member"] = [
|
|
121
|
-
{"uid": uid, "beingFrontendConfig": True}
|
|
122
|
-
for uid in member_uids
|
|
123
|
-
if isinstance(uid, int) and uid > 0
|
|
124
|
-
]
|
|
125
|
-
role_refs = assignees.get("role_refs") or []
|
|
126
|
-
if role_refs:
|
|
127
|
-
audit_user_infos["role_refs"] = [role_ref for role_ref in role_refs if role_ref]
|
|
128
|
-
role_entries = assignees.get("role_entries") or []
|
|
129
|
-
if role_entries:
|
|
130
|
-
audit_user_infos["role"] = [
|
|
131
|
-
{
|
|
132
|
-
"roleId": int(entry.get("roleId") or entry.get("role_id")),
|
|
133
|
-
"roleName": entry.get("roleName") or entry.get("role_name") or str(entry.get("roleId") or entry.get("role_id")),
|
|
134
|
-
"roleIcon": entry.get("roleIcon") or entry.get("role_icon") or "ex-user-outlined",
|
|
135
|
-
"beingFrontendConfig": True,
|
|
136
|
-
}
|
|
137
|
-
for entry in role_entries
|
|
138
|
-
if isinstance(entry, dict) and isinstance(entry.get("roleId") or entry.get("role_id"), int) and int(entry.get("roleId") or entry.get("role_id")) > 0
|
|
139
|
-
]
|
|
140
|
-
include_sub_departs = assignees.get("include_sub_departs")
|
|
141
|
-
if include_sub_departs is not None:
|
|
142
|
-
audit_user_infos["includeSubDeparts"] = bool(include_sub_departs)
|
|
143
|
-
return audit_user_infos
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def _prev_node_ref(node, branch_lane_ref: str | None, start_node_ids: set[str]) -> str:
|
|
147
|
-
if branch_lane_ref:
|
|
148
|
-
if node.parent_node_id and node.parent_node_id != node.branch_parent_id:
|
|
149
|
-
return node.parent_node_id
|
|
150
|
-
return branch_lane_ref
|
|
151
|
-
if node.parent_node_id in start_node_ids:
|
|
152
|
-
return "__applicant__"
|
|
153
|
-
return node.parent_node_id or "__applicant__"
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def _branch_index(node) -> int:
|
|
157
|
-
config = node.config or {}
|
|
158
|
-
raw_value = getattr(node, "branch_index", None)
|
|
159
|
-
if raw_value is None:
|
|
160
|
-
raw_value = config.get("branch_index", config.get("branchIndex", config.get("lane_index", config.get("laneIndex", 1))))
|
|
161
|
-
try:
|
|
162
|
-
branch_index = int(raw_value)
|
|
163
|
-
except (TypeError, ValueError) as exc:
|
|
164
|
-
raise ValueError(f"workflow node '{node.node_id}' has invalid branch index '{raw_value}'") from exc
|
|
165
|
-
if branch_index <= 0:
|
|
166
|
-
raise ValueError(f"workflow node '{node.node_id}' branch index must be positive")
|
|
167
|
-
return branch_index
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def _branch_lane_ref(branch_parent_id: str | None, branch_index: int) -> str:
|
|
171
|
-
if not branch_parent_id:
|
|
172
|
-
raise ValueError("branch_parent_id is required for branch lane references")
|
|
173
|
-
return f"__branch_lane__{branch_parent_id}__{branch_index}"
|