@josephyan/qingflow-cli 1.1.4 → 1.1.6

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.
Files changed (154) hide show
  1. package/README.md +7 -3
  2. package/docs/local-agent-install.md +57 -6
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-skills.mjs +5 -0
  5. package/npm/bin/qingflow.mjs +1 -34
  6. package/npm/lib/runtime.mjs +21 -101
  7. package/npm/scripts/postinstall.mjs +1 -10
  8. package/package.json +3 -2
  9. package/pyproject.toml +1 -1
  10. package/skills/qingflow-cli/SKILL.md +58 -44
  11. package/skills/qingflow-cli/manifest.yaml +1 -1
  12. package/skills/qingflow-cli/reference/00-INDEX.md +35 -0
  13. package/skills/qingflow-cli/reference/builder/10-build-single-app.md +38 -0
  14. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +39 -0
  15. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md → builder/30-schema-fields.md} +52 -10
  16. package/skills/qingflow-cli/reference/builder/40-layout.md +52 -0
  17. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md → builder/50-views.md} +39 -15
  18. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md → builder/60-charts.md} +36 -13
  19. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md → builder/70-portal.md} +36 -13
  20. package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +41 -0
  21. package/skills/qingflow-cli/reference/builder/90-workflow.md +34 -0
  22. package/skills/qingflow-cli/reference/builder/99-publish-verify.md +46 -0
  23. package/skills/qingflow-cli/reference/builder/README.md +41 -0
  24. package/skills/qingflow-cli/reference/builder/code-integrations/README.md +130 -0
  25. package/skills/qingflow-cli/reference/builder/code-integrations/code-block.md +66 -0
  26. package/skills/qingflow-cli/reference/builder/code-integrations/q-linker.md +77 -0
  27. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md → builder/reference/app-delivery-sop.md} +26 -16
  28. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/README.md +293 -0
  29. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-complete-system.md +809 -0
  30. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-single-app.md +830 -0
  31. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/complete-system-development-guide.md +123 -0
  32. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/create-app.md +182 -0
  33. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/environments.md +63 -0
  34. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/flow-actors-and-permissions.md +142 -0
  35. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/gotchas.md +108 -0
  36. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/match-rules.md +114 -0
  37. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/public-surface-sync.md +75 -0
  38. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/single-app-development-guide.md +58 -0
  39. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/solution-playbooks.md +52 -0
  40. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/tool-selection.md +107 -0
  41. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-flow.md +7 -0
  42. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-layout.md +7 -0
  43. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-schema.md +7 -0
  44. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-views.md +7 -0
  45. package/skills/qingflow-cli/reference/builder/workflow/01-overview.md +45 -0
  46. package/skills/qingflow-cli/reference/builder/workflow/02-update-mode.md +53 -0
  47. package/skills/qingflow-cli/reference/builder/workflow/03-flow-patterns.md +57 -0
  48. package/skills/qingflow-cli/reference/builder/workflow/04-stage1-business-modeling.md +131 -0
  49. package/skills/qingflow-cli/reference/builder/workflow/05-stage2-members-roles.md +29 -0
  50. package/skills/qingflow-cli/reference/builder/workflow/06-stage3-build-spec.md +165 -0
  51. package/skills/qingflow-cli/reference/builder/workflow/07-stage4-validate-spec.md +33 -0
  52. package/skills/qingflow-cli/reference/builder/workflow/08-stage5-apply-verify.md +51 -0
  53. package/skills/qingflow-cli/reference/builder/workflow/09-stage6-summary.md +88 -0
  54. package/skills/qingflow-cli/reference/builder/workflow/10-node-config-reference.md +93 -0
  55. package/skills/qingflow-cli/reference/builder/workflow/11-troubleshooting.md +15 -0
  56. package/skills/qingflow-cli/reference/builder/workflow/README.md +88 -0
  57. package/skills/qingflow-cli/reference/builder/workflow/workflow-schema.json +1754 -0
  58. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ADMIN_CHEATSHEET.md → core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md} +3 -3
  59. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md → core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md} +6 -6
  60. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_EXPLORATION_REPORT.md → core/QINGFLOW_CLI_EXPLORATION_REPORT.md} +2 -2
  61. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_FIELD_DATA_TYPES.md → core/QINGFLOW_CLI_FIELD_DATA_TYPES.md} +11 -11
  62. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_MEMBER_CHEATSHEET.md → core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md} +4 -4
  63. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md → core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md} +4 -4
  64. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md} +3 -3
  65. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_DELETE_WORKFLOW.md +31 -0
  66. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md} +4 -4
  67. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md} +7 -7
  68. package/skills/qingflow-cli/reference/record/analysis/README.md +130 -0
  69. package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +91 -0
  70. package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +112 -0
  71. package/skills/qingflow-cli/reference/record/analysis/business-context.md +74 -0
  72. package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +69 -0
  73. package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +106 -0
  74. package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +172 -0
  75. package/skills/qingflow-cli/reference/record/analysis/report-format.md +76 -0
  76. package/skills/qingflow-cli/reference/record/insert/README.md +75 -0
  77. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md → task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md} +5 -5
  78. package/skills/qingflow-cli/reference/task/ops/README.md +131 -0
  79. package/skills/qingflow-cli/reference/task/ops/environments.md +43 -0
  80. package/skills/qingflow-cli/reference/task/ops/workflow-usage.md +26 -0
  81. package/skills/qingflow-cli/scripts/validate_system_build_summary.py +124 -0
  82. package/skills/qingflow-cli/scripts/workflow/diff_flow_spec.py +275 -0
  83. package/skills/qingflow-cli/scripts/workflow/validate_flow_spec.py +605 -0
  84. package/skills/qingflow-mcp-setup/SKILL.md +115 -0
  85. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  86. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  87. package/skills/qingflow-mcp-setup/references/environments.md +62 -0
  88. package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
  89. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  90. package/src/qingflow_mcp/__init__.py +1 -1
  91. package/src/qingflow_mcp/__main__.py +6 -2
  92. package/src/qingflow_mcp/builder_facade/models.py +282 -102
  93. package/src/qingflow_mcp/builder_facade/service.py +4192 -935
  94. package/src/qingflow_mcp/cli/commands/builder.py +316 -298
  95. package/src/qingflow_mcp/cli/commands/chart.py +1 -1
  96. package/src/qingflow_mcp/cli/commands/common.py +12 -3
  97. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  98. package/src/qingflow_mcp/cli/commands/imports.py +3 -3
  99. package/src/qingflow_mcp/cli/commands/portal.py +2 -2
  100. package/src/qingflow_mcp/cli/commands/record.py +101 -27
  101. package/src/qingflow_mcp/cli/commands/task.py +28 -47
  102. package/src/qingflow_mcp/cli/commands/view.py +1 -1
  103. package/src/qingflow_mcp/cli/context.py +0 -3
  104. package/src/qingflow_mcp/cli/formatters.py +784 -16
  105. package/src/qingflow_mcp/cli/main.py +117 -33
  106. package/src/qingflow_mcp/errors.py +43 -2
  107. package/src/qingflow_mcp/public_surface.py +26 -17
  108. package/src/qingflow_mcp/response_trim.py +81 -17
  109. package/src/qingflow_mcp/server.py +14 -12
  110. package/src/qingflow_mcp/server_app_builder.py +65 -21
  111. package/src/qingflow_mcp/server_app_user.py +22 -16
  112. package/src/qingflow_mcp/session_store.py +11 -7
  113. package/src/qingflow_mcp/solution/compiler/__init__.py +3 -1
  114. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  115. package/src/qingflow_mcp/solution/executor.py +245 -18
  116. package/src/qingflow_mcp/tools/ai_builder_tools.py +1780 -406
  117. package/src/qingflow_mcp/tools/app_tools.py +184 -43
  118. package/src/qingflow_mcp/tools/approval_tools.py +197 -35
  119. package/src/qingflow_mcp/tools/auth_tools.py +92 -16
  120. package/src/qingflow_mcp/tools/code_block_tools.py +298 -40
  121. package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
  122. package/src/qingflow_mcp/tools/directory_tools.py +236 -72
  123. package/src/qingflow_mcp/tools/export_tools.py +244 -34
  124. package/src/qingflow_mcp/tools/feedback_tools.py +9 -0
  125. package/src/qingflow_mcp/tools/file_tools.py +9 -3
  126. package/src/qingflow_mcp/tools/import_tools.py +336 -49
  127. package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
  128. package/src/qingflow_mcp/tools/package_tools.py +118 -6
  129. package/src/qingflow_mcp/tools/portal_tools.py +39 -3
  130. package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
  131. package/src/qingflow_mcp/tools/record_tools.py +1141 -356
  132. package/src/qingflow_mcp/tools/resource_read_tools.py +188 -39
  133. package/src/qingflow_mcp/tools/role_tools.py +80 -9
  134. package/src/qingflow_mcp/tools/solution_tools.py +59 -45
  135. package/src/qingflow_mcp/tools/task_context_tools.py +662 -158
  136. package/src/qingflow_mcp/tools/task_tools.py +113 -29
  137. package/src/qingflow_mcp/tools/view_tools.py +106 -3
  138. package/src/qingflow_mcp/tools/workflow_tools.py +48 -4
  139. package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
  140. /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_MATCH_RULES.md → builder/reference/match-rules.md} +0 -0
  141. /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_WORKSPACE_ICONS.md → builder/reference/workspace-icons.md} +0 -0
  142. /package/skills/qingflow-cli/reference/{charts_remove.example.json → examples/charts/charts_remove.example.json} +0 -0
  143. /package/skills/qingflow-cli/reference/{charts_reorder.example.json → examples/charts/charts_reorder.example.json} +0 -0
  144. /package/skills/qingflow-cli/reference/{charts_upsert_bar.example.json → examples/charts/charts_upsert_bar.example.json} +0 -0
  145. /package/skills/qingflow-cli/reference/{charts_upsert_dashboard_starter.example.json → examples/charts/charts_upsert_dashboard_starter.example.json} +0 -0
  146. /package/skills/qingflow-cli/reference/{charts_upsert_minimal.example.json → examples/charts/charts_upsert_minimal.example.json} +0 -0
  147. /package/skills/qingflow-cli/reference/{portal_sections_all_types.example.json → examples/portal/portal_sections_all_types.example.json} +0 -0
  148. /package/skills/qingflow-cli/reference/{portal_sections_five_types.example.json → examples/portal/portal_sections_five_types.example.json} +0 -0
  149. /package/skills/qingflow-cli/reference/{portal_sections_standard_workbench.example.json → examples/portal/portal_sections_standard_workbench.example.json} +0 -0
  150. /package/skills/qingflow-cli/reference/{_batch_schema_complex.json → examples/schema/_batch_schema_complex.json} +0 -0
  151. /package/skills/qingflow-cli/reference/{_batch_schema_scalar.json → examples/schema/_batch_schema_scalar.json} +0 -0
  152. /package/skills/qingflow-cli/reference/{schema_add_fields_minimal.example.json → examples/schema/schema_add_fields_minimal.example.json} +0 -0
  153. /package/skills/qingflow-cli/reference/{schema_apply_add_fields_all_types.json → examples/schema/schema_apply_add_fields_all_types.json} +0 -0
  154. /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.mark_finished(status="success")
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
- verified_detail = _verify_package_attachment(self.package_tools, profile=profile, tag_id=tag_id, app_key=app_key)
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
- raise RuntimeError(
420
- "Legacy auditNode workflow execution was removed. "
421
- "Pass {app_key, spec} to solution_build_flow or use qingflow builder flow apply."
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
- raise _required_state_read_blocked_error(
586
- resource="portal",
587
- message=f"portal update requires readable draft state for dash '{dash_key}'",
588
- error=api_error,
589
- details={"dash_key": dash_key},
590
- ) from exc
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
- try:
1986
- backend_code = int(error.backend_code)
1987
- except (TypeError, ValueError):
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
- return error.backend_code in {40002, 40027}
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
  *,