@josephyan/qingflow-cli 1.1.4 → 1.1.5
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 +7 -3
- package/docs/local-agent-install.md +57 -6
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-skills.mjs +5 -0
- package/npm/bin/qingflow.mjs +1 -34
- package/npm/lib/runtime.mjs +21 -101
- package/npm/scripts/postinstall.mjs +1 -10
- package/package.json +3 -2
- package/pyproject.toml +1 -1
- package/skills/qingflow-cli/SKILL.md +58 -44
- package/skills/qingflow-cli/manifest.yaml +1 -1
- package/skills/qingflow-cli/reference/00-INDEX.md +35 -0
- package/skills/qingflow-cli/reference/builder/10-build-single-app.md +38 -0
- package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +39 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md → builder/30-schema-fields.md} +52 -10
- package/skills/qingflow-cli/reference/builder/40-layout.md +52 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md → builder/50-views.md} +39 -15
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md → builder/60-charts.md} +36 -13
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md → builder/70-portal.md} +36 -13
- package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +41 -0
- package/skills/qingflow-cli/reference/builder/90-workflow.md +34 -0
- package/skills/qingflow-cli/reference/builder/99-publish-verify.md +46 -0
- package/skills/qingflow-cli/reference/builder/README.md +41 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/README.md +130 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/code-block.md +66 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/q-linker.md +77 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md → builder/reference/app-delivery-sop.md} +26 -16
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/README.md +293 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-complete-system.md +809 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-single-app.md +830 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/complete-system-development-guide.md +123 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/create-app.md +182 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/environments.md +63 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/flow-actors-and-permissions.md +142 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/gotchas.md +108 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/match-rules.md +114 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/public-surface-sync.md +75 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/single-app-development-guide.md +58 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/solution-playbooks.md +52 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/tool-selection.md +107 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-flow.md +7 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-layout.md +7 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-schema.md +7 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-views.md +7 -0
- package/skills/qingflow-cli/reference/builder/workflow/01-overview.md +45 -0
- package/skills/qingflow-cli/reference/builder/workflow/02-update-mode.md +53 -0
- package/skills/qingflow-cli/reference/builder/workflow/03-flow-patterns.md +57 -0
- package/skills/qingflow-cli/reference/builder/workflow/04-stage1-business-modeling.md +131 -0
- package/skills/qingflow-cli/reference/builder/workflow/05-stage2-members-roles.md +29 -0
- package/skills/qingflow-cli/reference/builder/workflow/06-stage3-build-spec.md +165 -0
- package/skills/qingflow-cli/reference/builder/workflow/07-stage4-validate-spec.md +33 -0
- package/skills/qingflow-cli/reference/builder/workflow/08-stage5-apply-verify.md +51 -0
- package/skills/qingflow-cli/reference/builder/workflow/09-stage6-summary.md +88 -0
- package/skills/qingflow-cli/reference/builder/workflow/10-node-config-reference.md +93 -0
- package/skills/qingflow-cli/reference/builder/workflow/11-troubleshooting.md +15 -0
- package/skills/qingflow-cli/reference/builder/workflow/README.md +88 -0
- package/skills/qingflow-cli/reference/builder/workflow/workflow-schema.json +1754 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ADMIN_CHEATSHEET.md → core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md} +3 -3
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md → core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md} +6 -6
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_EXPLORATION_REPORT.md → core/QINGFLOW_CLI_EXPLORATION_REPORT.md} +2 -2
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_FIELD_DATA_TYPES.md → core/QINGFLOW_CLI_FIELD_DATA_TYPES.md} +11 -11
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_MEMBER_CHEATSHEET.md → core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md} +4 -4
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md → core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md} +4 -4
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md} +3 -3
- package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_DELETE_WORKFLOW.md +31 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md} +4 -4
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md} +7 -7
- package/skills/qingflow-cli/reference/record/analysis/README.md +130 -0
- package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +91 -0
- package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +112 -0
- package/skills/qingflow-cli/reference/record/analysis/business-context.md +74 -0
- package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +69 -0
- package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +106 -0
- package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +172 -0
- package/skills/qingflow-cli/reference/record/analysis/report-format.md +76 -0
- package/skills/qingflow-cli/reference/record/insert/README.md +75 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md → task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md} +5 -5
- package/skills/qingflow-cli/reference/task/ops/README.md +131 -0
- package/skills/qingflow-cli/reference/task/ops/environments.md +43 -0
- package/skills/qingflow-cli/reference/task/ops/workflow-usage.md +26 -0
- package/skills/qingflow-cli/scripts/validate_system_build_summary.py +124 -0
- package/skills/qingflow-cli/scripts/workflow/diff_flow_spec.py +275 -0
- package/skills/qingflow-cli/scripts/workflow/validate_flow_spec.py +605 -0
- package/skills/qingflow-mcp-setup/SKILL.md +115 -0
- package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
- package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
- package/skills/qingflow-mcp-setup/references/environments.md +62 -0
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
- package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/__main__.py +6 -2
- package/src/qingflow_mcp/builder_facade/models.py +282 -102
- package/src/qingflow_mcp/builder_facade/service.py +4166 -929
- package/src/qingflow_mcp/cli/commands/builder.py +316 -298
- package/src/qingflow_mcp/cli/commands/chart.py +1 -1
- package/src/qingflow_mcp/cli/commands/common.py +12 -3
- package/src/qingflow_mcp/cli/commands/exports.py +2 -2
- package/src/qingflow_mcp/cli/commands/imports.py +3 -3
- package/src/qingflow_mcp/cli/commands/portal.py +2 -2
- package/src/qingflow_mcp/cli/commands/record.py +101 -27
- package/src/qingflow_mcp/cli/commands/task.py +28 -47
- package/src/qingflow_mcp/cli/commands/view.py +1 -1
- package/src/qingflow_mcp/cli/context.py +0 -3
- package/src/qingflow_mcp/cli/formatters.py +784 -16
- package/src/qingflow_mcp/cli/main.py +117 -33
- package/src/qingflow_mcp/errors.py +43 -2
- package/src/qingflow_mcp/public_surface.py +26 -17
- package/src/qingflow_mcp/response_trim.py +81 -17
- package/src/qingflow_mcp/server.py +14 -12
- package/src/qingflow_mcp/server_app_builder.py +65 -21
- package/src/qingflow_mcp/server_app_user.py +22 -16
- package/src/qingflow_mcp/session_store.py +11 -7
- package/src/qingflow_mcp/solution/compiler/__init__.py +3 -1
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
- package/src/qingflow_mcp/solution/executor.py +245 -18
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1780 -406
- package/src/qingflow_mcp/tools/app_tools.py +184 -43
- package/src/qingflow_mcp/tools/approval_tools.py +197 -35
- package/src/qingflow_mcp/tools/auth_tools.py +92 -16
- package/src/qingflow_mcp/tools/code_block_tools.py +298 -40
- package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
- package/src/qingflow_mcp/tools/directory_tools.py +236 -72
- package/src/qingflow_mcp/tools/export_tools.py +244 -34
- package/src/qingflow_mcp/tools/feedback_tools.py +9 -0
- package/src/qingflow_mcp/tools/file_tools.py +9 -3
- package/src/qingflow_mcp/tools/import_tools.py +336 -49
- package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
- package/src/qingflow_mcp/tools/package_tools.py +118 -6
- package/src/qingflow_mcp/tools/portal_tools.py +39 -3
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
- package/src/qingflow_mcp/tools/record_tools.py +1141 -356
- package/src/qingflow_mcp/tools/resource_read_tools.py +188 -39
- package/src/qingflow_mcp/tools/role_tools.py +80 -9
- package/src/qingflow_mcp/tools/solution_tools.py +59 -45
- package/src/qingflow_mcp/tools/task_context_tools.py +662 -158
- package/src/qingflow_mcp/tools/task_tools.py +113 -29
- package/src/qingflow_mcp/tools/view_tools.py +106 -3
- package/src/qingflow_mcp/tools/workflow_tools.py +48 -4
- package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
- /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_MATCH_RULES.md → builder/reference/match-rules.md} +0 -0
- /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_WORKSPACE_ICONS.md → builder/reference/workspace-icons.md} +0 -0
- /package/skills/qingflow-cli/reference/{charts_remove.example.json → examples/charts/charts_remove.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_reorder.example.json → examples/charts/charts_reorder.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_upsert_bar.example.json → examples/charts/charts_upsert_bar.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_upsert_dashboard_starter.example.json → examples/charts/charts_upsert_dashboard_starter.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_upsert_minimal.example.json → examples/charts/charts_upsert_minimal.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{portal_sections_all_types.example.json → examples/portal/portal_sections_all_types.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{portal_sections_five_types.example.json → examples/portal/portal_sections_five_types.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{portal_sections_standard_workbench.example.json → examples/portal/portal_sections_standard_workbench.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{_batch_schema_complex.json → examples/schema/_batch_schema_complex.json} +0 -0
- /package/skills/qingflow-cli/reference/{_batch_schema_scalar.json → examples/schema/_batch_schema_scalar.json} +0 -0
- /package/skills/qingflow-cli/reference/{schema_add_fields_minimal.example.json → examples/schema/schema_add_fields_minimal.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{schema_apply_add_fields_all_types.json → examples/schema/schema_apply_add_fields_all_types.json} +0 -0
- /package/skills/qingflow-cli/reference/{views_upsert_table_minimal.example.json → examples/views/views_upsert_table_minimal.example.json} +0 -0
|
@@ -0,0 +1,173 @@
|
|
|
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}"
|
|
@@ -5,7 +5,7 @@ from copy import deepcopy
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
from uuid import uuid4
|
|
7
7
|
|
|
8
|
-
from ..errors import QingflowApiError
|
|
8
|
+
from ..errors import QingflowApiError, backend_code_int, is_auth_like_error
|
|
9
9
|
from ..tools.app_tools import AppTools
|
|
10
10
|
from ..tools.navigation_tools import NavigationTools
|
|
11
11
|
from ..tools.package_tools import PackageTools
|
|
@@ -14,6 +14,7 @@ from ..tools.qingbi_report_tools import QingbiReportTools
|
|
|
14
14
|
from ..tools.record_tools import RecordTools
|
|
15
15
|
from ..tools.role_tools import RoleTools
|
|
16
16
|
from ..tools.view_tools import ViewTools
|
|
17
|
+
from ..tools.workflow_tools import WorkflowTools
|
|
17
18
|
from ..tools.workspace_tools import WorkspaceTools
|
|
18
19
|
from .compiler import CompiledEntity, CompiledRole, CompiledSolution
|
|
19
20
|
from .compiler.form_compiler import QUESTION_TYPE_MAP
|
|
@@ -35,6 +36,7 @@ class SolutionExecutor:
|
|
|
35
36
|
role_tools: RoleTools,
|
|
36
37
|
app_tools: AppTools,
|
|
37
38
|
record_tools: RecordTools,
|
|
39
|
+
workflow_tools: WorkflowTools,
|
|
38
40
|
view_tools: ViewTools,
|
|
39
41
|
chart_tools: QingbiReportTools,
|
|
40
42
|
portal_tools: PortalTools,
|
|
@@ -45,6 +47,7 @@ class SolutionExecutor:
|
|
|
45
47
|
self.role_tools = role_tools
|
|
46
48
|
self.app_tools = app_tools
|
|
47
49
|
self.record_tools = record_tools
|
|
50
|
+
self.workflow_tools = workflow_tools
|
|
48
51
|
self.view_tools = view_tools
|
|
49
52
|
self.chart_tools = chart_tools
|
|
50
53
|
self.portal_tools = portal_tools
|
|
@@ -73,7 +76,8 @@ class SolutionExecutor:
|
|
|
73
76
|
except Exception as exc: # noqa: BLE001
|
|
74
77
|
store.record_step_failed(step.step_name, str(exc), debug_context=debug_context)
|
|
75
78
|
return store.summary()
|
|
76
|
-
store.
|
|
79
|
+
final_status = "partial_success" if _artifacts_have_post_write_readback_pending(store.data.get("artifacts", {})) else "success"
|
|
80
|
+
store.mark_finished(status=final_status)
|
|
77
81
|
return store.summary()
|
|
78
82
|
|
|
79
83
|
def _repair_start_index(self, compiled: CompiledSolution, store: RunArtifactStore) -> int:
|
|
@@ -200,6 +204,11 @@ class SolutionExecutor:
|
|
|
200
204
|
dash_key = store.get_artifact("portal", "dash_key")
|
|
201
205
|
if dash_key:
|
|
202
206
|
self.portal_tools.portal_publish(profile=profile, dash_key=dash_key)
|
|
207
|
+
store.set_artifact(
|
|
208
|
+
"portal",
|
|
209
|
+
"publish",
|
|
210
|
+
{"published": True, "write_executed": True, "safe_to_retry": False},
|
|
211
|
+
)
|
|
203
212
|
self._refresh_portal_artifact(profile=profile, store=store, being_draft=False, artifact_key="published_result")
|
|
204
213
|
return
|
|
205
214
|
if step_name == "publish.navigation" and publish and compiled.normalized_spec.publish_policy.navigation:
|
|
@@ -344,7 +353,32 @@ class SolutionExecutor:
|
|
|
344
353
|
updated_items.insert(insert_at, item)
|
|
345
354
|
self.package_tools.package_sort_items(profile=profile, tag_id=tag_id, tag_items=updated_items)
|
|
346
355
|
|
|
347
|
-
|
|
356
|
+
try:
|
|
357
|
+
verified_detail = _verify_package_attachment(self.package_tools, profile=profile, tag_id=tag_id, app_key=app_key)
|
|
358
|
+
except Exception as exc: # noqa: BLE001
|
|
359
|
+
api_error = _coerce_qingflow_error(exc)
|
|
360
|
+
if api_error is None or not _is_permission_restricted_error(api_error):
|
|
361
|
+
raise
|
|
362
|
+
store.set_artifact(
|
|
363
|
+
"package",
|
|
364
|
+
"attachment_readback",
|
|
365
|
+
_post_write_readback_artifact(
|
|
366
|
+
resource="package_attach",
|
|
367
|
+
target={"tag_id": tag_id, "app_key": app_key},
|
|
368
|
+
error=api_error,
|
|
369
|
+
),
|
|
370
|
+
)
|
|
371
|
+
self._record_package_attachment(
|
|
372
|
+
store,
|
|
373
|
+
entity.entity_id,
|
|
374
|
+
app_artifact,
|
|
375
|
+
tag_id=tag_id,
|
|
376
|
+
attached=True,
|
|
377
|
+
reused=False,
|
|
378
|
+
readback_status="unavailable",
|
|
379
|
+
readback_verified=False,
|
|
380
|
+
)
|
|
381
|
+
return
|
|
348
382
|
verified_result = verified_detail.get("result") if isinstance(verified_detail.get("result"), dict) else {}
|
|
349
383
|
verified_items = [deepcopy(existing) for existing in verified_result.get("tagItems", []) if isinstance(existing, dict)]
|
|
350
384
|
if not any(_package_item_app_key(existing) == app_key for existing in verified_items):
|
|
@@ -366,12 +400,18 @@ class SolutionExecutor:
|
|
|
366
400
|
tag_id: int,
|
|
367
401
|
attached: bool,
|
|
368
402
|
reused: bool,
|
|
403
|
+
readback_status: str = "verified",
|
|
404
|
+
readback_verified: bool = True,
|
|
369
405
|
) -> None:
|
|
370
406
|
next_artifact = deepcopy(app_artifact)
|
|
371
407
|
next_artifact["package_attachment"] = {
|
|
372
408
|
"tag_id": tag_id,
|
|
373
409
|
"attached": attached,
|
|
374
410
|
"reused": reused,
|
|
411
|
+
"readback_status": readback_status,
|
|
412
|
+
"readback_verified": readback_verified,
|
|
413
|
+
"write_executed": not reused,
|
|
414
|
+
"safe_to_retry": reused or not attached,
|
|
375
415
|
}
|
|
376
416
|
store.set_artifact("apps", entity_id, next_artifact)
|
|
377
417
|
|
|
@@ -416,10 +456,123 @@ class SolutionExecutor:
|
|
|
416
456
|
def _build_workflow(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
|
|
417
457
|
if entity.workflow_plan is None:
|
|
418
458
|
return
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
459
|
+
app_key = self._get_app_key(store, entity.entity_id)
|
|
460
|
+
workflow_edit_version_no = self._ensure_edit_version(
|
|
461
|
+
profile,
|
|
462
|
+
entity.entity_id,
|
|
463
|
+
store,
|
|
464
|
+
app_key=app_key,
|
|
465
|
+
force_new=True,
|
|
466
|
+
)
|
|
467
|
+
node_artifacts = store.get_artifact("apps", entity.entity_id, {}).get("workflow_nodes", {})
|
|
468
|
+
existing_nodes = self.workflow_tools.workflow_list_nodes(profile=profile, app_key=app_key).get("result") or {}
|
|
469
|
+
current_nodes = _coerce_workflow_nodes(existing_nodes)
|
|
470
|
+
existing_nodes_by_name = {
|
|
471
|
+
node.get("auditNodeName"): int(node_id)
|
|
472
|
+
for node_id, node in current_nodes.items()
|
|
473
|
+
if isinstance(node, dict) and node.get("auditNodeName")
|
|
474
|
+
}
|
|
475
|
+
applicant_node_id = next(
|
|
476
|
+
(
|
|
477
|
+
int(node_id)
|
|
478
|
+
for node_id, node in current_nodes.items()
|
|
479
|
+
if isinstance(node, dict) and node.get("type") == 0 and node.get("dealType") == 3
|
|
480
|
+
),
|
|
481
|
+
None,
|
|
422
482
|
)
|
|
483
|
+
if applicant_node_id is not None:
|
|
484
|
+
node_artifacts.setdefault("__applicant__", applicant_node_id)
|
|
485
|
+
|
|
486
|
+
desired_global_settings = deepcopy(entity.workflow_plan["global_settings"])
|
|
487
|
+
explicit_global_settings = _has_explicit_workflow_global_settings(desired_global_settings)
|
|
488
|
+
current_global_settings: dict[str, Any] = {}
|
|
489
|
+
if explicit_global_settings:
|
|
490
|
+
current_global_settings = self.workflow_tools.workflow_get_global_settings(profile=profile, app_key=app_key).get("result") or {}
|
|
491
|
+
else:
|
|
492
|
+
try:
|
|
493
|
+
current_global_settings = self.workflow_tools.workflow_get_global_settings(profile=profile, app_key=app_key).get("result") or {}
|
|
494
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
495
|
+
api_error = QingflowApiError(**_coerce_nested_error_payload(error))
|
|
496
|
+
if api_error.http_status != 404:
|
|
497
|
+
raise
|
|
498
|
+
current_global_settings = {}
|
|
499
|
+
if explicit_global_settings:
|
|
500
|
+
global_settings = deepcopy(current_global_settings if isinstance(current_global_settings, dict) else {})
|
|
501
|
+
global_settings.update(desired_global_settings)
|
|
502
|
+
global_settings["editVersionNo"] = workflow_edit_version_no or global_settings.get("editVersionNo") or 1
|
|
503
|
+
self.workflow_tools.workflow_update_global_settings(profile=profile, app_key=app_key, payload=global_settings)
|
|
504
|
+
for action in entity.workflow_plan["actions"]:
|
|
505
|
+
if action["action"] == "create_sub_branch" and node_artifacts.get(action["node_id"]) is not None:
|
|
506
|
+
continue
|
|
507
|
+
if action["action"] == "add_node":
|
|
508
|
+
if action.get("node_type") == "branch":
|
|
509
|
+
existing_branch_id = node_artifacts.get(action["node_id"])
|
|
510
|
+
if existing_branch_id is not None and not _workflow_node_is_branch(current_nodes, existing_branch_id):
|
|
511
|
+
existing_branch_id = None
|
|
512
|
+
if existing_branch_id is not None:
|
|
513
|
+
for branch_index, lane_id in enumerate(_find_branch_lane_ids(current_nodes, existing_branch_id), start=1):
|
|
514
|
+
node_artifacts[_branch_lane_ref(action["node_id"], branch_index)] = lane_id
|
|
515
|
+
apps_artifact = store.get_artifact("apps", entity.entity_id, {})
|
|
516
|
+
apps_artifact["workflow_nodes"] = node_artifacts
|
|
517
|
+
store.set_artifact("apps", entity.entity_id, apps_artifact)
|
|
518
|
+
continue
|
|
519
|
+
existing_node_id = node_artifacts.get(action["node_id"]) or existing_nodes_by_name.get(action.get("node_name"))
|
|
520
|
+
if existing_node_id is not None:
|
|
521
|
+
node_artifacts[action["node_id"]] = existing_node_id
|
|
522
|
+
apps_artifact = store.get_artifact("apps", entity.entity_id, {})
|
|
523
|
+
apps_artifact["workflow_nodes"] = node_artifacts
|
|
524
|
+
store.set_artifact("apps", entity.entity_id, apps_artifact)
|
|
525
|
+
continue
|
|
526
|
+
before_node_ids = set(current_nodes)
|
|
527
|
+
payload = self._resolve_workflow_payload(action["payload"], node_artifacts)
|
|
528
|
+
if workflow_edit_version_no is not None:
|
|
529
|
+
payload["editVersionNo"] = int(workflow_edit_version_no)
|
|
530
|
+
if action["action"] == "create_sub_branch":
|
|
531
|
+
result = self.workflow_tools.workflow_create_sub_branch(profile=profile, app_key=app_key, payload=payload)
|
|
532
|
+
elif action["action"] == "update_node":
|
|
533
|
+
target_node_id = node_artifacts.get(action["node_id"])
|
|
534
|
+
if target_node_id is None:
|
|
535
|
+
raise RuntimeError(f"workflow lane '{action['node_id']}' could not be resolved before update")
|
|
536
|
+
result = self.workflow_tools.workflow_update_node(
|
|
537
|
+
profile=profile,
|
|
538
|
+
app_key=app_key,
|
|
539
|
+
audit_node_id=target_node_id,
|
|
540
|
+
payload=payload,
|
|
541
|
+
)
|
|
542
|
+
else:
|
|
543
|
+
result = self.workflow_tools.workflow_add_node(profile=profile, app_key=app_key, payload=payload)
|
|
544
|
+
expected_type = 1 if action.get("node_type") == "branch" else None
|
|
545
|
+
audit_node_id = _extract_workflow_node_id(result.get("result"), expected_type=expected_type)
|
|
546
|
+
if action.get("node_type") == "branch" or action["action"] == "create_sub_branch":
|
|
547
|
+
current_nodes = _coerce_workflow_nodes(
|
|
548
|
+
self.workflow_tools.workflow_list_nodes(profile=profile, app_key=app_key).get("result") or {}
|
|
549
|
+
)
|
|
550
|
+
if audit_node_id is not None:
|
|
551
|
+
node_artifacts[action["node_id"]] = audit_node_id
|
|
552
|
+
if action.get("node_type") == "branch":
|
|
553
|
+
branch_node_id = node_artifacts.get(action["node_id"]) or _find_created_branch_node_id(
|
|
554
|
+
current_nodes,
|
|
555
|
+
before_node_ids=before_node_ids,
|
|
556
|
+
prev_id=payload.get("prevId"),
|
|
557
|
+
)
|
|
558
|
+
if branch_node_id is not None:
|
|
559
|
+
node_artifacts[action["node_id"]] = branch_node_id
|
|
560
|
+
for branch_index, lane_id in enumerate(_find_branch_lane_ids(current_nodes, branch_node_id), start=1):
|
|
561
|
+
node_artifacts[_branch_lane_ref(action["node_id"], branch_index)] = lane_id
|
|
562
|
+
if action["action"] == "create_sub_branch" and node_artifacts.get(action["node_id"]) is None:
|
|
563
|
+
created_lane_id = audit_node_id or _find_created_sub_branch_lane_id(
|
|
564
|
+
current_nodes,
|
|
565
|
+
before_node_ids=before_node_ids,
|
|
566
|
+
branch_node_id=payload.get("auditNodeId"),
|
|
567
|
+
)
|
|
568
|
+
if created_lane_id is not None:
|
|
569
|
+
node_artifacts[action["node_id"]] = created_lane_id
|
|
570
|
+
apps_artifact = store.get_artifact("apps", entity.entity_id, {})
|
|
571
|
+
apps_artifact["workflow_nodes"] = node_artifacts
|
|
572
|
+
store.set_artifact("apps", entity.entity_id, apps_artifact)
|
|
573
|
+
apps_artifact = store.get_artifact("apps", entity.entity_id, {})
|
|
574
|
+
apps_artifact["workflow_nodes"] = node_artifacts
|
|
575
|
+
store.set_artifact("apps", entity.entity_id, apps_artifact)
|
|
423
576
|
|
|
424
577
|
def _build_views(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
|
|
425
578
|
app_key = self._get_app_key(store, entity.entity_id)
|
|
@@ -582,12 +735,23 @@ class SolutionExecutor:
|
|
|
582
735
|
api_error = _coerce_qingflow_error(exc)
|
|
583
736
|
if api_error is None or not _is_permission_restricted_error(api_error):
|
|
584
737
|
raise
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
738
|
+
if not result:
|
|
739
|
+
raise _required_state_read_blocked_error(
|
|
740
|
+
resource="portal",
|
|
741
|
+
message=f"portal update requires readable draft state for dash '{dash_key}'",
|
|
742
|
+
error=api_error,
|
|
743
|
+
details={"dash_key": dash_key},
|
|
744
|
+
) from exc
|
|
745
|
+
base_payload = {}
|
|
746
|
+
store.set_artifact(
|
|
747
|
+
"portal",
|
|
748
|
+
"draft_readback_before_update",
|
|
749
|
+
_post_write_readback_artifact(
|
|
750
|
+
resource="portal",
|
|
751
|
+
target={"dash_key": dash_key, "phase": "created_portal_draft_readback"},
|
|
752
|
+
error=api_error,
|
|
753
|
+
),
|
|
754
|
+
)
|
|
591
755
|
update_payload = self._resolve_portal_payload(compiled.portal_plan["update_payload"], store, base_payload=base_payload)
|
|
592
756
|
self.portal_tools.portal_update(profile=profile, dash_key=dash_key, payload=update_payload)
|
|
593
757
|
self._refresh_portal_artifact(profile=profile, store=store, being_draft=True, artifact_key="draft_result")
|
|
@@ -628,7 +792,19 @@ class SolutionExecutor:
|
|
|
628
792
|
return
|
|
629
793
|
try:
|
|
630
794
|
result = self.portal_tools.portal_get(profile=profile, dash_key=dash_key, being_draft=being_draft)
|
|
631
|
-
except Exception: # noqa: BLE001
|
|
795
|
+
except Exception as exc: # noqa: BLE001
|
|
796
|
+
api_error = _coerce_qingflow_error(exc)
|
|
797
|
+
if api_error is None:
|
|
798
|
+
raise
|
|
799
|
+
store.set_artifact(
|
|
800
|
+
"portal",
|
|
801
|
+
f"{artifact_key}_readback",
|
|
802
|
+
_post_write_readback_artifact(
|
|
803
|
+
resource="portal",
|
|
804
|
+
target={"dash_key": dash_key, "being_draft": being_draft, "artifact_key": artifact_key},
|
|
805
|
+
error=api_error,
|
|
806
|
+
),
|
|
807
|
+
)
|
|
632
808
|
return
|
|
633
809
|
store.set_artifact("portal", artifact_key, result)
|
|
634
810
|
store.set_artifact("portal", "result", result)
|
|
@@ -1981,11 +2157,24 @@ def _find_created_sub_branch_lane_id(
|
|
|
1981
2157
|
return candidates[0] if candidates else None
|
|
1982
2158
|
|
|
1983
2159
|
|
|
2160
|
+
def _has_explicit_workflow_global_settings(global_settings: dict[str, Any] | None) -> bool:
|
|
2161
|
+
if not isinstance(global_settings, dict):
|
|
2162
|
+
return False
|
|
2163
|
+
for key, value in global_settings.items():
|
|
2164
|
+
if key == "editVersionNo":
|
|
2165
|
+
continue
|
|
2166
|
+
if value is None:
|
|
2167
|
+
continue
|
|
2168
|
+
if isinstance(value, (list, dict)) and not value:
|
|
2169
|
+
continue
|
|
2170
|
+
return True
|
|
2171
|
+
return False
|
|
2172
|
+
|
|
2173
|
+
|
|
1984
2174
|
def _is_navigation_plugin_unavailable(error: QingflowApiError) -> bool:
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
backend_code = None
|
|
2175
|
+
if is_auth_like_error(error):
|
|
2176
|
+
return False
|
|
2177
|
+
backend_code = backend_code_int(error)
|
|
1989
2178
|
if backend_code != 50004:
|
|
1990
2179
|
return False
|
|
1991
2180
|
message = error.message or ""
|
|
@@ -2022,7 +2211,9 @@ def _coerce_qingflow_error(error: Exception) -> QingflowApiError | None:
|
|
|
2022
2211
|
|
|
2023
2212
|
|
|
2024
2213
|
def _is_permission_restricted_error(error: QingflowApiError) -> bool:
|
|
2025
|
-
|
|
2214
|
+
if is_auth_like_error(error):
|
|
2215
|
+
return False
|
|
2216
|
+
return backend_code_int(error) in {40002, 40027}
|
|
2026
2217
|
|
|
2027
2218
|
|
|
2028
2219
|
def _required_state_read_blocked_error(
|
|
@@ -2052,6 +2243,42 @@ def _required_state_read_blocked_error(
|
|
|
2052
2243
|
)
|
|
2053
2244
|
|
|
2054
2245
|
|
|
2246
|
+
def _post_write_readback_artifact(
|
|
2247
|
+
*,
|
|
2248
|
+
resource: str,
|
|
2249
|
+
target: dict[str, Any],
|
|
2250
|
+
error: QingflowApiError,
|
|
2251
|
+
) -> dict[str, Any]:
|
|
2252
|
+
return {
|
|
2253
|
+
"resource": resource,
|
|
2254
|
+
"target": deepcopy(target),
|
|
2255
|
+
"readback_status": "unavailable",
|
|
2256
|
+
"readback_verified": False,
|
|
2257
|
+
"write_executed": True,
|
|
2258
|
+
"safe_to_retry": False,
|
|
2259
|
+
"transport_error": {
|
|
2260
|
+
"http_status": error.http_status,
|
|
2261
|
+
"backend_code": error.backend_code,
|
|
2262
|
+
"category": error.category,
|
|
2263
|
+
"request_id": error.request_id,
|
|
2264
|
+
},
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
|
|
2268
|
+
def _artifacts_have_post_write_readback_pending(value: Any) -> bool:
|
|
2269
|
+
if isinstance(value, dict):
|
|
2270
|
+
if (
|
|
2271
|
+
value.get("write_executed") is True
|
|
2272
|
+
and value.get("safe_to_retry") is False
|
|
2273
|
+
and value.get("readback_status") == "unavailable"
|
|
2274
|
+
):
|
|
2275
|
+
return True
|
|
2276
|
+
return any(_artifacts_have_post_write_readback_pending(item) for item in value.values())
|
|
2277
|
+
if isinstance(value, list):
|
|
2278
|
+
return any(_artifacts_have_post_write_readback_pending(item) for item in value)
|
|
2279
|
+
return False
|
|
2280
|
+
|
|
2281
|
+
|
|
2055
2282
|
def _portal_component_position(
|
|
2056
2283
|
source_type: Any,
|
|
2057
2284
|
*,
|