@josephyan/qingflow-mcp 0.1.0-beta.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.
Files changed (52) hide show
  1. package/README.md +517 -0
  2. package/docs/local-agent-install.md +213 -0
  3. package/entry_point.py +13 -0
  4. package/npm/bin/qingflow-mcp.mjs +7 -0
  5. package/npm/lib/runtime.mjs +146 -0
  6. package/npm/scripts/postinstall.mjs +12 -0
  7. package/package.json +34 -0
  8. package/pyproject.toml +63 -0
  9. package/qingflow-mcp +15 -0
  10. package/src/qingflow_mcp/__init__.py +5 -0
  11. package/src/qingflow_mcp/__main__.py +5 -0
  12. package/src/qingflow_mcp/backend_client.py +336 -0
  13. package/src/qingflow_mcp/config.py +166 -0
  14. package/src/qingflow_mcp/errors.py +66 -0
  15. package/src/qingflow_mcp/json_types.py +18 -0
  16. package/src/qingflow_mcp/server.py +70 -0
  17. package/src/qingflow_mcp/session_store.py +235 -0
  18. package/src/qingflow_mcp/solution/__init__.py +6 -0
  19. package/src/qingflow_mcp/solution/build_assembly_store.py +137 -0
  20. package/src/qingflow_mcp/solution/compiler/__init__.py +265 -0
  21. package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
  22. package/src/qingflow_mcp/solution/compiler/form_compiler.py +456 -0
  23. package/src/qingflow_mcp/solution/compiler/icon_utils.py +113 -0
  24. package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
  25. package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
  26. package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
  27. package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
  28. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +134 -0
  29. package/src/qingflow_mcp/solution/design_session.py +222 -0
  30. package/src/qingflow_mcp/solution/design_store.py +100 -0
  31. package/src/qingflow_mcp/solution/executor.py +2064 -0
  32. package/src/qingflow_mcp/solution/normalizer.py +23 -0
  33. package/src/qingflow_mcp/solution/run_store.py +221 -0
  34. package/src/qingflow_mcp/solution/spec_models.py +755 -0
  35. package/src/qingflow_mcp/tools/__init__.py +1 -0
  36. package/src/qingflow_mcp/tools/app_tools.py +239 -0
  37. package/src/qingflow_mcp/tools/approval_tools.py +481 -0
  38. package/src/qingflow_mcp/tools/auth_tools.py +496 -0
  39. package/src/qingflow_mcp/tools/base.py +81 -0
  40. package/src/qingflow_mcp/tools/directory_tools.py +476 -0
  41. package/src/qingflow_mcp/tools/file_tools.py +375 -0
  42. package/src/qingflow_mcp/tools/navigation_tools.py +177 -0
  43. package/src/qingflow_mcp/tools/package_tools.py +142 -0
  44. package/src/qingflow_mcp/tools/portal_tools.py +100 -0
  45. package/src/qingflow_mcp/tools/qingbi_report_tools.py +258 -0
  46. package/src/qingflow_mcp/tools/record_tools.py +4305 -0
  47. package/src/qingflow_mcp/tools/role_tools.py +94 -0
  48. package/src/qingflow_mcp/tools/solution_tools.py +1860 -0
  49. package/src/qingflow_mcp/tools/task_tools.py +677 -0
  50. package/src/qingflow_mcp/tools/view_tools.py +324 -0
  51. package/src/qingflow_mcp/tools/workflow_tools.py +311 -0
  52. package/src/qingflow_mcp/tools/workspace_tools.py +143 -0
@@ -0,0 +1,2064 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from copy import deepcopy
5
+ from typing import Any
6
+ from uuid import uuid4
7
+
8
+ from ..errors import QingflowApiError
9
+ from ..tools.app_tools import AppTools
10
+ from ..tools.navigation_tools import NavigationTools
11
+ from ..tools.package_tools import PackageTools
12
+ from ..tools.portal_tools import PortalTools
13
+ from ..tools.qingbi_report_tools import QingbiReportTools
14
+ from ..tools.record_tools import RecordTools
15
+ from ..tools.role_tools import RoleTools
16
+ from ..tools.view_tools import ViewTools
17
+ from ..tools.workflow_tools import WorkflowTools
18
+ from ..tools.workspace_tools import WorkspaceTools
19
+ from .compiler import CompiledEntity, CompiledRole, CompiledSolution
20
+ from .compiler.form_compiler import QUESTION_TYPE_MAP
21
+ from .run_store import RunArtifactStore, fingerprint_payload
22
+ from .spec_models import FieldType
23
+
24
+ NAVIGATION_PLUGIN_ID = 45
25
+ GRID_COMPONENT_PLUGIN_ID = 34
26
+ BI_CHART_COMPONENT_TYPE = 9
27
+
28
+
29
+ class SolutionExecutor:
30
+ def __init__(
31
+ self,
32
+ *,
33
+ workspace_tools: WorkspaceTools,
34
+ package_tools: PackageTools,
35
+ role_tools: RoleTools,
36
+ app_tools: AppTools,
37
+ record_tools: RecordTools,
38
+ workflow_tools: WorkflowTools,
39
+ view_tools: ViewTools,
40
+ chart_tools: QingbiReportTools,
41
+ portal_tools: PortalTools,
42
+ navigation_tools: NavigationTools,
43
+ ) -> None:
44
+ self.workspace_tools = workspace_tools
45
+ self.package_tools = package_tools
46
+ self.role_tools = role_tools
47
+ self.app_tools = app_tools
48
+ self.record_tools = record_tools
49
+ self.workflow_tools = workflow_tools
50
+ self.view_tools = view_tools
51
+ self.chart_tools = chart_tools
52
+ self.portal_tools = portal_tools
53
+ self.navigation_tools = navigation_tools
54
+
55
+ def execute(
56
+ self,
57
+ *,
58
+ profile: str,
59
+ compiled: CompiledSolution,
60
+ store: RunArtifactStore,
61
+ publish: bool,
62
+ mode: str,
63
+ ) -> dict[str, Any]:
64
+ self._current_store = store
65
+ force_from_index = self._repair_start_index(compiled, store) if mode == "repair" else None
66
+ for index, step in enumerate(compiled.execution_plan.steps):
67
+ force = force_from_index is not None and index >= force_from_index
68
+ if not store.should_run(step.step_name, force=force):
69
+ continue
70
+ debug_context = self._step_debug_context(compiled, step.step_name)
71
+ try:
72
+ store.record_step_started(step.step_name, store.data["request_fingerprint"], debug_context=debug_context)
73
+ self._execute_step(profile=profile, compiled=compiled, store=store, step_name=step.step_name, publish=publish)
74
+ store.record_step_completed(step.step_name, debug_context=debug_context)
75
+ except Exception as exc: # noqa: BLE001
76
+ store.record_step_failed(step.step_name, str(exc), debug_context=debug_context)
77
+ return store.summary()
78
+ store.mark_finished(status="success")
79
+ return store.summary()
80
+
81
+ def _repair_start_index(self, compiled: CompiledSolution, store: RunArtifactStore) -> int:
82
+ for index, step in enumerate(compiled.execution_plan.steps):
83
+ if store.get_step_status(step.step_name) != "completed":
84
+ return index
85
+ return len(compiled.execution_plan.steps)
86
+
87
+ def _step_debug_context(self, compiled: CompiledSolution, step_name: str) -> dict[str, Any]:
88
+ context: dict[str, Any] = {"step_name": step_name}
89
+ if step_name == "package.create":
90
+ context["resource"] = "package"
91
+ context["package_name"] = compiled.normalized_spec.package.name or compiled.normalized_spec.solution_name
92
+ return context
93
+ if step_name == "portal.create":
94
+ context["resource"] = "portal"
95
+ context["portal_name"] = compiled.normalized_spec.portal.name
96
+ context["section_count"] = len(compiled.normalized_spec.portal.sections)
97
+ return context
98
+ if step_name == "navigation.create":
99
+ context["resource"] = "navigation"
100
+ context["item_count"] = len(compiled.normalized_spec.navigation.items)
101
+ return context
102
+ if ".create." in step_name and step_name.startswith("role.create."):
103
+ role = self._role_from_step(compiled, step_name)
104
+ context["resource"] = "role"
105
+ context["role_id"] = role.role_id
106
+ context["role_name"] = role.name
107
+ return context
108
+ if any(step_name.startswith(prefix) for prefix in ("app.create.", "form.base.", "form.relations.", "workflow.", "views.", "charts.", "seed_data.", "publish.form.", "publish.workflow.", "publish.app.")):
109
+ entity = self._entity_from_step(compiled, step_name)
110
+ context.update(
111
+ {
112
+ "resource": "entity",
113
+ "entity_id": entity.entity_id,
114
+ "display_name": entity.display_name,
115
+ "workflow_action_count": len((entity.workflow_plan or {}).get("actions", [])),
116
+ "view_count": len(entity.view_plans),
117
+ "chart_count": len(entity.chart_plans),
118
+ "sample_record_count": len(entity.sample_records),
119
+ }
120
+ )
121
+ return context
122
+ return context
123
+
124
+ def _execute_step(self, *, profile: str, compiled: CompiledSolution, store: RunArtifactStore, step_name: str, publish: bool) -> None:
125
+ if step_name == "package.create":
126
+ self._create_package(profile, compiled, store)
127
+ return
128
+ if step_name.startswith("role.create."):
129
+ role = self._role_from_step(compiled, step_name)
130
+ self._create_role(profile, role, store)
131
+ return
132
+ if step_name.startswith("app.create."):
133
+ entity = self._entity_from_step(compiled, step_name)
134
+ self._create_app(profile, entity, store)
135
+ return
136
+ if step_name.startswith("form.base."):
137
+ entity = self._entity_from_step(compiled, step_name)
138
+ self._update_form_base(profile, entity, store)
139
+ return
140
+ if step_name.startswith("form.relations."):
141
+ entity = self._entity_from_step(compiled, step_name)
142
+ self._update_form_relations(profile, entity, store)
143
+ return
144
+ if step_name.startswith("workflow."):
145
+ entity = self._entity_from_step(compiled, step_name)
146
+ self._build_workflow(profile, entity, store)
147
+ return
148
+ if step_name.startswith("views."):
149
+ entity = self._entity_from_step(compiled, step_name)
150
+ self._build_views(profile, entity, store)
151
+ return
152
+ if step_name.startswith("charts."):
153
+ entity = self._entity_from_step(compiled, step_name)
154
+ self._build_charts(profile, entity, store)
155
+ return
156
+ if step_name.startswith("seed_data."):
157
+ entity = self._entity_from_step(compiled, step_name)
158
+ self._seed_records(profile, entity, store)
159
+ return
160
+ if step_name == "portal.create":
161
+ self._build_portal(profile, compiled, store)
162
+ return
163
+ if step_name == "navigation.create":
164
+ self._build_navigation(profile, compiled, store)
165
+ return
166
+ if (
167
+ step_name.startswith("publish.form.")
168
+ or step_name.startswith("publish.workflow.")
169
+ or step_name.startswith("publish.app.")
170
+ ) and publish and compiled.normalized_spec.publish_policy.apps:
171
+ entity = self._entity_from_step(compiled, step_name)
172
+ app_key = self._get_app_key(store, entity.entity_id)
173
+ edit_version_no = self._get_edit_version_no(store, entity.entity_id)
174
+ self.app_tools.app_publish(
175
+ profile=profile,
176
+ app_key=app_key,
177
+ payload=self._publish_payload(store, entity.entity_id),
178
+ )
179
+ if edit_version_no is not None:
180
+ self.app_tools.app_edit_finished(
181
+ profile=profile,
182
+ app_key=app_key,
183
+ payload={"editVersionNo": int(edit_version_no)},
184
+ )
185
+ return
186
+ if step_name == "publish.portal" and publish and compiled.normalized_spec.publish_policy.portal:
187
+ dash_key = store.get_artifact("portal", "dash_key")
188
+ if dash_key:
189
+ self.portal_tools.portal_publish(profile=profile, dash_key=dash_key)
190
+ self._refresh_portal_artifact(profile=profile, store=store, being_draft=False, artifact_key="published_result")
191
+ return
192
+ if step_name == "publish.navigation" and publish and compiled.normalized_spec.publish_policy.navigation:
193
+ if store.get_artifact("navigation", "skipped"):
194
+ return
195
+ status = self.navigation_tools.navigation_get_status(profile=profile)
196
+ navigation_status = (status.get("result") or {}) if isinstance(status.get("result"), dict) else {}
197
+ navigation_id = navigation_status.get("navigationId") or navigation_status.get("id")
198
+ if navigation_id:
199
+ self.navigation_tools.navigation_publish(profile=profile, navigation_id=navigation_id)
200
+ return
201
+
202
+ def _create_package(self, profile: str, compiled: CompiledSolution, store: RunArtifactStore) -> None:
203
+ if compiled.package_payload is None:
204
+ return
205
+ result = self.package_tools.package_create(profile=profile, payload=compiled.package_payload)
206
+ package_result = result.get("result") or {}
207
+ tag_id = package_result.get("tagId") or package_result.get("id") or package_result.get("tag_id")
208
+ store.set_artifact("package", "result", result)
209
+ if tag_id is not None:
210
+ store.set_artifact("package", "tag_id", tag_id)
211
+
212
+ def _create_role(self, profile: str, role: CompiledRole, store: RunArtifactStore) -> None:
213
+ existing = store.get_artifact("roles", role.role_id, {}) or {}
214
+ existing_role_id = existing.get("role_id")
215
+ if existing_role_id:
216
+ return
217
+ page = self.role_tools.role_search(profile=profile, keyword=role.name, page_num=1, page_size=50).get("page") or {}
218
+ role_list = page.get("list") if isinstance(page, dict) else []
219
+ matched_role = next(
220
+ (
221
+ item
222
+ for item in (role_list or [])
223
+ if isinstance(item, dict) and item.get("roleName") == role.name and item.get("roleId")
224
+ ),
225
+ None,
226
+ )
227
+ if matched_role is None:
228
+ payload = deepcopy(role.payload)
229
+ if not payload.get("users"):
230
+ session_profile = self.role_tools.sessions.get_profile(profile)
231
+ if session_profile is not None:
232
+ payload["users"] = [session_profile.uid]
233
+ result = self.role_tools.role_create(profile=profile, payload=payload)
234
+ role_result = result.get("result") or {}
235
+ role_id = role_result.get("roleId") or role_result.get("id")
236
+ store.set_artifact(
237
+ "roles",
238
+ role.role_id,
239
+ {"role_id": role_id, "result": result, "role_name": role.name, "role_icon": payload.get("roleIcon")},
240
+ )
241
+ return
242
+ store.set_artifact(
243
+ "roles",
244
+ role.role_id,
245
+ {
246
+ "role_id": matched_role.get("roleId"),
247
+ "result": {"result": matched_role},
248
+ "role_name": role.name,
249
+ "role_icon": matched_role.get("roleIcon"),
250
+ "reused": True,
251
+ },
252
+ )
253
+
254
+ def _create_app(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
255
+ payload = self._resolve_app_payload(entity.app_create_payload, store)
256
+ result = self.app_tools.app_create(profile=profile, payload=payload)
257
+ app_result = result.get("result") or {}
258
+ app_key = None
259
+ if isinstance(app_result, dict):
260
+ app_key = app_result.get("appKey")
261
+ if app_key is None and isinstance(app_result.get("appKeys"), list) and app_result["appKeys"]:
262
+ app_key = app_result["appKeys"][0]
263
+ app_artifact = {"app_key": app_key, "result": result}
264
+ if app_key:
265
+ version_result = self.app_tools.app_get_edit_version_no(profile=profile, app_key=app_key).get("result") or {}
266
+ edit_version_no = version_result.get("editVersionNo") or version_result.get("versionNo")
267
+ if edit_version_no is not None:
268
+ app_artifact["edit_version_no"] = int(edit_version_no)
269
+ store.set_artifact("apps", entity.entity_id, app_artifact)
270
+
271
+ def _update_form_base(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
272
+ app_key = self._get_app_key(store, entity.entity_id)
273
+ payload = deepcopy(entity.form_base_payload)
274
+ edit_version_no = self._ensure_edit_version(profile, entity.entity_id, store, app_key=app_key)
275
+ if edit_version_no is not None:
276
+ payload["editVersionNo"] = int(edit_version_no)
277
+ self.app_tools.app_update_form_schema(profile=profile, app_key=app_key, payload=payload)
278
+ self._refresh_field_map(profile, entity, store)
279
+
280
+ def _update_form_relations(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
281
+ if entity.form_relation_payload is None:
282
+ return
283
+ app_key = self._get_app_key(store, entity.entity_id)
284
+ payload = self._resolve_relation_payload(entity, store)
285
+ edit_version_no = self._ensure_edit_version(profile, entity.entity_id, store, app_key=app_key)
286
+ if edit_version_no is not None:
287
+ payload["editVersionNo"] = int(edit_version_no)
288
+ self.app_tools.app_update_form_schema(profile=profile, app_key=app_key, payload=payload)
289
+ self._refresh_field_map(profile, entity, store)
290
+
291
+ def _build_workflow(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
292
+ if entity.workflow_plan is None:
293
+ return
294
+ app_key = self._get_app_key(store, entity.entity_id)
295
+ workflow_edit_version_no = self._ensure_edit_version(
296
+ profile,
297
+ entity.entity_id,
298
+ store,
299
+ app_key=app_key,
300
+ force_new=True,
301
+ )
302
+ node_artifacts = store.get_artifact("apps", entity.entity_id, {}).get("workflow_nodes", {})
303
+ existing_nodes = self.workflow_tools.workflow_list_nodes(profile=profile, app_key=app_key).get("result") or {}
304
+ current_nodes = _coerce_workflow_nodes(existing_nodes)
305
+ existing_nodes_by_name = {
306
+ node.get("auditNodeName"): int(node_id)
307
+ for node_id, node in existing_nodes.items()
308
+ if isinstance(node, dict) and node.get("auditNodeName")
309
+ }
310
+ applicant_node_id = next(
311
+ (
312
+ int(node_id)
313
+ for node_id, node in existing_nodes.items()
314
+ if isinstance(node, dict) and node.get("type") == 0 and node.get("dealType") == 3
315
+ ),
316
+ None,
317
+ )
318
+ if applicant_node_id is not None:
319
+ node_artifacts.setdefault("__applicant__", applicant_node_id)
320
+
321
+ current_global_settings = self.workflow_tools.workflow_get_global_settings(profile=profile, app_key=app_key).get("result") or {}
322
+ global_settings = deepcopy(current_global_settings if isinstance(current_global_settings, dict) else {})
323
+ global_settings.update(entity.workflow_plan["global_settings"])
324
+ global_settings["editVersionNo"] = workflow_edit_version_no or global_settings.get("editVersionNo") or 1
325
+ self.workflow_tools.workflow_update_global_settings(profile=profile, app_key=app_key, payload=global_settings)
326
+ for action in entity.workflow_plan["actions"]:
327
+ if action["action"] == "create_sub_branch" and node_artifacts.get(action["node_id"]) is not None:
328
+ continue
329
+ if action["action"] == "add_node":
330
+ if action.get("node_type") == "branch":
331
+ existing_branch_id = node_artifacts.get(action["node_id"])
332
+ if existing_branch_id is not None and not _workflow_node_is_branch(current_nodes, existing_branch_id):
333
+ existing_branch_id = None
334
+ if existing_branch_id is not None:
335
+ for branch_index, lane_id in enumerate(_find_branch_lane_ids(current_nodes, existing_branch_id), start=1):
336
+ node_artifacts[_branch_lane_ref(action["node_id"], branch_index)] = lane_id
337
+ apps_artifact = store.get_artifact("apps", entity.entity_id, {})
338
+ apps_artifact["workflow_nodes"] = node_artifacts
339
+ store.set_artifact("apps", entity.entity_id, apps_artifact)
340
+ continue
341
+ existing_node_id = node_artifacts.get(action["node_id"]) or existing_nodes_by_name.get(action.get("node_name"))
342
+ if existing_node_id is not None:
343
+ node_artifacts[action["node_id"]] = existing_node_id
344
+ apps_artifact = store.get_artifact("apps", entity.entity_id, {})
345
+ apps_artifact["workflow_nodes"] = node_artifacts
346
+ store.set_artifact("apps", entity.entity_id, apps_artifact)
347
+ continue
348
+ before_node_ids = set(current_nodes)
349
+ payload = self._resolve_workflow_payload(action["payload"], node_artifacts)
350
+ if workflow_edit_version_no is not None:
351
+ payload["editVersionNo"] = int(workflow_edit_version_no)
352
+ if action["action"] == "create_sub_branch":
353
+ result = self.workflow_tools.workflow_create_sub_branch(profile=profile, app_key=app_key, payload=payload)
354
+ else:
355
+ result = self.workflow_tools.workflow_add_node(profile=profile, app_key=app_key, payload=payload)
356
+ expected_type = 1 if action.get("node_type") == "branch" else None
357
+ audit_node_id = _extract_workflow_node_id(result.get("result"), expected_type=expected_type)
358
+ if action.get("node_type") == "branch" or action["action"] == "create_sub_branch":
359
+ current_nodes = _coerce_workflow_nodes(
360
+ self.workflow_tools.workflow_list_nodes(profile=profile, app_key=app_key).get("result") or {}
361
+ )
362
+ if audit_node_id is not None:
363
+ node_artifacts[action["node_id"]] = audit_node_id
364
+ if action.get("node_type") == "branch":
365
+ branch_node_id = node_artifacts.get(action["node_id"]) or _find_created_branch_node_id(
366
+ current_nodes,
367
+ before_node_ids=before_node_ids,
368
+ prev_id=payload.get("prevId"),
369
+ )
370
+ if branch_node_id is not None:
371
+ node_artifacts[action["node_id"]] = branch_node_id
372
+ for branch_index, lane_id in enumerate(_find_branch_lane_ids(current_nodes, branch_node_id), start=1):
373
+ node_artifacts[_branch_lane_ref(action["node_id"], branch_index)] = lane_id
374
+ if action["action"] == "create_sub_branch" and node_artifacts.get(action["node_id"]) is None:
375
+ created_lane_id = audit_node_id or _find_created_sub_branch_lane_id(
376
+ current_nodes,
377
+ before_node_ids=before_node_ids,
378
+ branch_node_id=payload.get("auditNodeId"),
379
+ )
380
+ if created_lane_id is not None:
381
+ node_artifacts[action["node_id"]] = created_lane_id
382
+ apps_artifact = store.get_artifact("apps", entity.entity_id, {})
383
+ apps_artifact["workflow_nodes"] = node_artifacts
384
+ store.set_artifact("apps", entity.entity_id, apps_artifact)
385
+ apps_artifact = store.get_artifact("apps", entity.entity_id, {})
386
+ apps_artifact["workflow_nodes"] = node_artifacts
387
+ store.set_artifact("apps", entity.entity_id, apps_artifact)
388
+
389
+ def _build_views(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
390
+ app_key = self._get_app_key(store, entity.entity_id)
391
+ field_meta = store.get_artifact("field_maps", entity.entity_id, {})
392
+ field_map = field_meta.get("by_field_id", {})
393
+ schema = field_meta.get("schema", {})
394
+ created_view_keys: list[str] = []
395
+ for view_plan in entity.view_plans:
396
+ create_payload = self._resolve_view_payload(
397
+ view_plan["create_payload"],
398
+ app_key=app_key,
399
+ field_map=field_map,
400
+ schema=schema,
401
+ group_by_field_id=view_plan["group_by_field_id"],
402
+ )
403
+ result = self.view_tools.view_create(profile=profile, payload=create_payload)
404
+ view_result = result.get("result")
405
+ view_key = ((view_result or {}) if isinstance(view_result, dict) else {}).get("viewgraphKey")
406
+ if not view_key and isinstance(view_result, str):
407
+ view_key = view_result
408
+ if not view_key:
409
+ continue
410
+ created_view_keys.append(view_key)
411
+ store.set_artifact("views", f"{entity.entity_id}:{view_plan['view_id']}", {"viewgraph_key": view_key, "result": result})
412
+ if view_plan["column_widths"]:
413
+ que_width_info_list = [
414
+ {"queId": que_id, "queWidth": width}
415
+ for field_id, width in view_plan["column_widths"].items()
416
+ if (que_id := field_map.get(field_id)) is not None
417
+ ]
418
+ if que_width_info_list:
419
+ self.view_tools.view_set_column_width(
420
+ profile=profile,
421
+ app_key=app_key,
422
+ payload={
423
+ "viewgraphKey": view_key,
424
+ "beingCustomViewgraph": True,
425
+ "beingReferenceQueWidth": False,
426
+ "queWidthInfoList": que_width_info_list,
427
+ },
428
+ )
429
+ if view_plan["member_config"]:
430
+ self.view_tools.view_update_member_config(profile=profile, viewgraph_key=view_key, payload=view_plan["member_config"])
431
+ if view_plan["apply_config"]:
432
+ self.view_tools.view_update_apply_config(profile=profile, viewgraph_key=view_key, payload=view_plan["apply_config"])
433
+ if view_plan["type"] == "board" and view_plan["group_by_field_id"] and view_plan["config"]:
434
+ que_id = field_map.get(view_plan["group_by_field_id"])
435
+ payload = deepcopy(view_plan["config"])
436
+ if que_id is not None and "queId" not in payload:
437
+ payload["queId"] = que_id
438
+ self.view_tools.view_board_set_lane_config(profile=profile, viewgraph_key=view_key, payload=payload)
439
+ if view_plan["type"] == "gantt":
440
+ if "being_auto_calibration" in view_plan["config"]:
441
+ self.view_tools.view_gantt_switch_auto_calibration(
442
+ profile=profile,
443
+ viewgraph_key=view_key,
444
+ payload={"userAutoCalibration": view_plan["config"]["being_auto_calibration"]},
445
+ )
446
+ if created_view_keys:
447
+ self.view_tools.view_reorder(profile=profile, app_key=app_key, payload=self._resolve_view_reorder_payload(profile, app_key, created_view_keys))
448
+
449
+ def _build_charts(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
450
+ app_key = self._get_app_key(store, entity.entity_id)
451
+ field_map = self._get_field_map(store, entity.entity_id)
452
+ bi_field_map = store.get_artifact("field_maps", entity.entity_id, {}).get("bi_by_field_id", {})
453
+ qingbi_fields = self.chart_tools.qingbi_report_list_fields(profile=profile, app_key=app_key).get("items") or []
454
+ qingbi_fields_by_id = {
455
+ field.get("fieldId"): field
456
+ for field in qingbi_fields
457
+ if isinstance(field, dict) and field.get("fieldId")
458
+ }
459
+ existing_charts = self.chart_tools.qingbi_report_list(profile=profile, app_key=app_key).get("items") or []
460
+ existing_chart_keys_by_name = {
461
+ chart.get("chartName"): chart.get("chartId") or chart.get("chartKey")
462
+ for chart in existing_charts
463
+ if isinstance(chart, dict) and chart.get("chartName") and (chart.get("chartId") or chart.get("chartKey"))
464
+ }
465
+ created_chart_keys: list[str] = []
466
+ for chart_plan in entity.chart_plans:
467
+ chart_artifact_key = f"{entity.entity_id}:{chart_plan['chart_id']}"
468
+ chart_artifact = store.get_artifact("charts", chart_artifact_key, {}) or {}
469
+ chart_key = (
470
+ chart_artifact.get("bi_chart_id")
471
+ or chart_artifact.get("chart_key")
472
+ or existing_chart_keys_by_name.get(chart_plan["name"])
473
+ )
474
+ result = chart_artifact.get("result")
475
+ if not chart_key:
476
+ create_payload = deepcopy(chart_plan["create_payload"])
477
+ create_payload["dataSourceId"] = app_key
478
+ create_payload["chartId"] = create_payload.get("chartId") if create_payload.get("chartId") != "__BI_CHART_ID__" else f"mcp_{uuid4().hex[:16]}"
479
+ result = self.chart_tools.qingbi_report_create(profile=profile, payload=create_payload)
480
+ chart_key = ((result.get("result") or {}) if isinstance(result.get("result"), dict) else {}).get("chartId")
481
+ if not chart_key:
482
+ continue
483
+ created_chart_keys.append(chart_key)
484
+ config_payload = self._resolve_qingbi_chart_config_payload(
485
+ chart_plan["config_payload"],
486
+ entity=entity,
487
+ field_map=field_map,
488
+ bi_field_map=bi_field_map,
489
+ qingbi_fields_by_id=qingbi_fields_by_id,
490
+ app_key=app_key,
491
+ )
492
+ self.chart_tools.qingbi_report_update_config(profile=profile, chart_id=chart_key, payload=config_payload)
493
+ chart_info = {
494
+ "chartName": chart_plan["name"],
495
+ "chartType": chart_plan["chart_type"],
496
+ "dataSourceId": app_key,
497
+ "dataSourceType": "qingflow",
498
+ }
499
+ chart_info.update(config_payload)
500
+ chart_info.setdefault("chartName", chart_plan["name"])
501
+ chart_info["biChartId"] = chart_key
502
+ store.set_artifact(
503
+ "charts",
504
+ chart_artifact_key,
505
+ {
506
+ "bi_chart_id": chart_key,
507
+ "chart_key": chart_key,
508
+ "result": result,
509
+ "chart_info": chart_info,
510
+ },
511
+ )
512
+ if created_chart_keys:
513
+ self.chart_tools.qingbi_report_reorder(profile=profile, app_key=app_key, chart_ids=created_chart_keys)
514
+
515
+ def _build_portal(self, profile: str, compiled: CompiledSolution, store: RunArtifactStore) -> None:
516
+ if compiled.portal_plan is None:
517
+ return
518
+ if _portal_plan_has_source_type(compiled.portal_plan, "grid"):
519
+ self.workspace_tools.workspace_set_plugin_status(
520
+ profile=profile,
521
+ plugin_id=GRID_COMPONENT_PLUGIN_ID,
522
+ being_installed=True,
523
+ )
524
+ store.set_artifact(
525
+ "portal",
526
+ "grid_plugin_install",
527
+ {
528
+ "plugin_id": GRID_COMPONENT_PLUGIN_ID,
529
+ "being_installed": True,
530
+ },
531
+ )
532
+ portal_artifact = store.get_artifact("portal", "result", {}) or {}
533
+ dash_key = store.get_artifact("portal", "dash_key")
534
+ result = portal_artifact
535
+ if not dash_key:
536
+ create_payload = self._resolve_portal_payload(compiled.portal_plan["create_payload"], store, base_payload=None)
537
+ result = self.portal_tools.portal_create(profile=profile, payload=create_payload)
538
+ portal_result = result.get("result") or {}
539
+ dash_key = portal_result.get("dashKey")
540
+ store.set_artifact("portal", "result", result)
541
+ if dash_key:
542
+ store.set_artifact("portal", "dash_key", dash_key)
543
+ if dash_key:
544
+ base_payload = self.portal_tools.portal_get(profile=profile, dash_key=dash_key, being_draft=True).get("result") or {}
545
+ update_payload = self._resolve_portal_payload(compiled.portal_plan["update_payload"], store, base_payload=base_payload)
546
+ self.portal_tools.portal_update(profile=profile, dash_key=dash_key, payload=update_payload)
547
+ self._refresh_portal_artifact(profile=profile, store=store, being_draft=True, artifact_key="draft_result")
548
+
549
+ def _build_navigation(self, profile: str, compiled: CompiledSolution, store: RunArtifactStore) -> None:
550
+ try:
551
+ created_items = self._create_navigation_items(profile, compiled, store)
552
+ except Exception as exc: # noqa: BLE001
553
+ api_error = _coerce_qingflow_error(exc)
554
+ if api_error is None or not _is_navigation_plugin_unavailable(api_error):
555
+ raise
556
+ self.workspace_tools.workspace_set_plugin_status(
557
+ profile=profile,
558
+ plugin_id=NAVIGATION_PLUGIN_ID,
559
+ being_installed=True,
560
+ )
561
+ created_items = self._create_navigation_items(profile, compiled, store)
562
+ store.set_artifact(
563
+ "navigation",
564
+ "plugin_install",
565
+ {
566
+ "plugin_id": NAVIGATION_PLUGIN_ID,
567
+ "being_installed": True,
568
+ },
569
+ )
570
+ store.set_artifact("navigation", "items", created_items)
571
+
572
+ def _refresh_portal_artifact(
573
+ self,
574
+ *,
575
+ profile: str,
576
+ store: RunArtifactStore,
577
+ being_draft: bool,
578
+ artifact_key: str,
579
+ ) -> None:
580
+ dash_key = store.get_artifact("portal", "dash_key")
581
+ if not dash_key:
582
+ return
583
+ try:
584
+ result = self.portal_tools.portal_get(profile=profile, dash_key=dash_key, being_draft=being_draft)
585
+ except Exception: # noqa: BLE001
586
+ return
587
+ store.set_artifact("portal", artifact_key, result)
588
+ store.set_artifact("portal", "result", result)
589
+
590
+ def _create_navigation_items(
591
+ self,
592
+ profile: str,
593
+ compiled: CompiledSolution,
594
+ store: RunArtifactStore,
595
+ ) -> list[dict[str, Any]]:
596
+ created_items: list[dict[str, Any]] = []
597
+ for ordinal, item in enumerate(compiled.navigation_plan, start=1):
598
+ result = self.navigation_tools.navigation_create(
599
+ profile=profile,
600
+ payload=self._resolve_navigation_payload(item["payload"], store, ordinal),
601
+ )
602
+ created = {"item_id": item["item_id"], "result": result}
603
+ created_items.append(created)
604
+ for child_ordinal, child in enumerate(item["children"], start=1):
605
+ child_payload = self._resolve_navigation_payload(child["payload"], store, child_ordinal)
606
+ child_payload["parentNavigationItemId"] = ((result.get("result") or {}) if isinstance(result.get("result"), dict) else {}).get("navigationItemId")
607
+ created_items.append(
608
+ {
609
+ "item_id": child["item_id"],
610
+ "result": self.navigation_tools.navigation_create(profile=profile, payload=child_payload),
611
+ }
612
+ )
613
+ return created_items
614
+
615
+ def _refresh_field_map(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
616
+ app_key = self._get_app_key(store, entity.entity_id)
617
+ schema = self.app_tools.app_get_form_schema(
618
+ profile=profile,
619
+ app_key=app_key,
620
+ form_type=1,
621
+ being_draft=True,
622
+ being_apply=None,
623
+ audit_node_id=None,
624
+ )
625
+ result = schema.get("result") or {}
626
+ field_map = extract_field_map(result)
627
+ label_to_field_id = {label: field_id for field_id, label in entity.field_labels.items()}
628
+ mapped = {field_id: field_map[label] for label, field_id in label_to_field_id.items() if label in field_map}
629
+ current_edit_version_no = self._get_edit_version_no(store, entity.entity_id)
630
+ response_edit_version_no = result.get("editVersionNo")
631
+ store.set_artifact(
632
+ "field_maps",
633
+ entity.entity_id,
634
+ {
635
+ "by_field_id": mapped,
636
+ "bi_by_field_id": {field_id: f"{app_key}:{que_id}" for field_id, que_id in mapped.items()},
637
+ "by_label": field_map,
638
+ "edit_version_no": int(response_edit_version_no or current_edit_version_no or 1),
639
+ "schema": result,
640
+ },
641
+ )
642
+
643
+ def _resolve_app_payload(self, payload: dict[str, Any], store: RunArtifactStore) -> dict[str, Any]:
644
+ data = deepcopy(payload)
645
+ if "tagIds" in data:
646
+ tag_id = store.get_artifact("package", "tag_id")
647
+ data["tagIds"] = [tag_id] if tag_id is not None else []
648
+ return data
649
+
650
+ def _resolve_relation_payload(self, entity: CompiledEntity, store: RunArtifactStore) -> dict[str, Any]:
651
+ payload = deepcopy(entity.form_relation_payload or {})
652
+ for line in payload.get("formQues", []):
653
+ self._resolve_reference_questions(line, entity, store)
654
+ payload["questionRelations"] = []
655
+ return payload
656
+
657
+ def _resolve_reference_questions(self, questions: list[dict[str, Any]], entity: CompiledEntity, store: RunArtifactStore) -> None:
658
+ for question in questions:
659
+ if question.get("queType") == 24:
660
+ for inner_row in question.get("innerQuestions", []):
661
+ self._resolve_reference_questions(inner_row, entity, store)
662
+ continue
663
+ if question.get("queType") != 25:
664
+ continue
665
+ target_entity_id = self._target_entity_id_from_label(entity, question["queTitle"])
666
+ if not target_entity_id:
667
+ continue
668
+ target_field_label = self._target_field_label(entity, question["queTitle"], store)
669
+ target_field_id = self._target_field_id_from_label(entity, question["queTitle"])
670
+ target_meta = store.get_artifact("field_maps", target_entity_id, {})
671
+ target_app = store.get_artifact("apps", target_entity_id, {})
672
+ target_label_map = target_meta.get("by_label", {})
673
+ target_que_id = target_label_map.get(target_field_label, 0)
674
+ reference_config = deepcopy(question.get("referenceConfig") or {})
675
+ reference_config["referAppKey"] = target_app.get("app_key")
676
+ reference_config["referQueId"] = target_que_id
677
+ for ordinal, refer_question in enumerate(reference_config.get("referQuestions", []), start=1):
678
+ refer_field_id = target_field_id if ordinal == 1 else refer_question.get("_field_id") or target_field_id
679
+ refer_label = self._field_label_from_id(target_entity_id, refer_field_id, store)
680
+ refer_que_id = target_meta.get("by_field_id", {}).get(refer_field_id) or target_label_map.get(refer_label, 0)
681
+ refer_field_spec = self._field_spec_from_store(store, target_entity_id, refer_field_id)
682
+ refer_field_type = refer_field_spec.get("type")
683
+ refer_question["queId"] = refer_que_id
684
+ refer_question["queTitle"] = refer_label
685
+ if refer_field_type:
686
+ refer_question["queType"] = str(QUESTION_TYPE_MAP[FieldType(refer_field_type)])
687
+ refer_question["ordinal"] = ordinal
688
+ refer_question.pop("_field_id", None)
689
+ auth_ques = []
690
+ for auth_que in reference_config.get("referAuthQues", []):
691
+ resolved = deepcopy(auth_que)
692
+ refer_field_id = resolved.pop("_field_id", None) or target_field_id
693
+ refer_que_id = target_meta.get("by_field_id", {}).get(refer_field_id)
694
+ if refer_que_id is None:
695
+ continue
696
+ resolved["queId"] = refer_que_id
697
+ resolved["queAuth"] = int(resolved.get("queAuth", 1))
698
+ auth_ques.append(resolved)
699
+ if not auth_ques:
700
+ fallback_que_id = target_meta.get("by_field_id", {}).get(target_field_id)
701
+ if fallback_que_id is not None:
702
+ auth_ques.append({"queId": fallback_que_id, "queAuth": 1})
703
+ reference_config["referAuthQues"] = auth_ques
704
+ reference_config["fieldNameShow"] = bool(reference_config.get("fieldNameShow", True))
705
+ fill_rules = []
706
+ for fill_rule in reference_config.get("referFillRules", []):
707
+ resolved = deepcopy(fill_rule)
708
+ current_field_id = resolved.pop("field_id", None)
709
+ target_fill_field_id = resolved.pop("target_field_id", None)
710
+ if not current_field_id or not target_fill_field_id:
711
+ continue
712
+ resolved["queId"] = store.get_artifact("field_maps", entity.entity_id, {}).get("by_field_id", {}).get(current_field_id)
713
+ resolved["relatedQueId"] = target_meta.get("by_field_id", {}).get(target_fill_field_id)
714
+ resolved["queTitle"] = self._field_label_from_id(entity.entity_id, current_field_id, store)
715
+ resolved["relatedQueTitle"] = self._field_label_from_id(target_entity_id, target_fill_field_id, store)
716
+ resolved["currentQuoteQueId"] = question.get("queId") or question.get("queTempId")
717
+ if resolved["queId"] and resolved["relatedQueId"]:
718
+ fill_rules.append(resolved)
719
+ reference_config["referFillRules"] = fill_rules
720
+ reference_config.pop("_targetFieldId", None)
721
+ reference_config.pop("_targetEntityId", None)
722
+ question["referenceConfig"] = reference_config
723
+
724
+ def _resolve_workflow_payload(self, payload: dict[str, Any], node_artifacts: dict[str, int]) -> dict[str, Any]:
725
+ data = deepcopy(payload)
726
+ prev_ref = data.pop("prevNodeRef", None)
727
+ if prev_ref:
728
+ data["prevId"] = node_artifacts.get(prev_ref, 0)
729
+ audit_ref = data.pop("auditNodeRef", None)
730
+ if audit_ref:
731
+ data["auditNodeId"] = node_artifacts.get(audit_ref, 0)
732
+ audit_user_infos = data.get("auditUserInfos")
733
+ if isinstance(audit_user_infos, dict):
734
+ role_refs = audit_user_infos.pop("role_refs", [])
735
+ if role_refs:
736
+ roles = []
737
+ for role_ref in role_refs:
738
+ role_artifact = self._current_store.get_artifact("roles", role_ref, {}) if hasattr(self, "_current_store") else {}
739
+ role_id = role_artifact.get("role_id")
740
+ if role_id is None:
741
+ raise QingflowApiError.config_error(f"workflow role '{role_ref}' has not been created")
742
+ roles.append(
743
+ {
744
+ "roleId": role_id,
745
+ "roleName": role_artifact.get("role_name") or role_ref,
746
+ "roleIcon": role_artifact.get("role_icon") or "ex-user-outlined",
747
+ "beingFrontendConfig": True,
748
+ }
749
+ )
750
+ audit_user_infos["role"] = roles
751
+ if data.get("auditUserInfos") is None:
752
+ data.pop("auditUserInfos", None)
753
+ return data
754
+
755
+ def _resolve_view_payload(
756
+ self,
757
+ payload: dict[str, Any],
758
+ *,
759
+ app_key: str,
760
+ field_map: dict[str, int],
761
+ schema: dict[str, Any],
762
+ group_by_field_id: str | None,
763
+ ) -> dict[str, Any]:
764
+ data = deepcopy(payload)
765
+ data["appKey"] = app_key
766
+ visible_que_ids = [field_map[field_id] for field_id in data.get("viewgraphQueIds", []) if field_id in field_map]
767
+ data["viewgraphQueIds"] = visible_que_ids
768
+ data.setdefault("defaultRowHigh", "compact")
769
+ data.setdefault("asosChartVisible", False)
770
+ data.setdefault("beingNeedPass", False)
771
+ data.setdefault("beingAuditRecordVisible", True)
772
+ data.setdefault("beingQrobotRecordVisible", False)
773
+ data.setdefault("beingPrintStatus", False)
774
+ data.setdefault("beingDefaultPrintTplStatus", False)
775
+ data.setdefault("beingCommentStatus", False)
776
+ data.setdefault("beingWorkflowNodeFutureListVisible", True)
777
+ data.setdefault("dataPermissionType", "CUSTOM")
778
+ data.setdefault("dataScope", "ALL")
779
+ data.setdefault("needPass", False)
780
+ data.setdefault("viewgraphPass", "")
781
+ data.setdefault("beingImageAdaption", False)
782
+ data.setdefault("clippingMode", "default")
783
+ data.setdefault("frontCoverQueId", None)
784
+ data.setdefault("printTpls", [])
785
+ data.setdefault("usages", [])
786
+ data.setdefault("asosChartConfig", {"limitType": 1, "asosChartIdList": []})
787
+ data.setdefault("viewgraphGanttConfigVO", None)
788
+ data.setdefault("viewgraphHierarchyConfigVO", None)
789
+ data.setdefault("viewgraphLimitFormula", "")
790
+ data.setdefault("buttonConfigDTOList", [])
791
+ if not data.get("viewgraphQuestions"):
792
+ data["viewgraphQuestions"] = _build_viewgraph_questions(schema, visible_que_ids)
793
+ if not data.get("viewgraphSorts"):
794
+ data["viewgraphSorts"] = [{"queId": 0, "beingSortAscend": True, "queType": 8}]
795
+ if data.get("viewgraphType") in {"cardView", "boardView", "ganttView", "hierarchyView"}:
796
+ data["titleQue"] = visible_que_ids[0] if visible_que_ids else None
797
+ else:
798
+ data.setdefault("titleQue", None)
799
+ if data.get("viewgraphType") == "boardView":
800
+ data["groupQueId"] = field_map.get(group_by_field_id) if group_by_field_id else 1
801
+ if data.get("viewgraphType") == "ganttView":
802
+ gantt_payload = self._resolve_gantt_payload(payload, field_map)
803
+ if gantt_payload.get("viewgraphGanttConfigVO"):
804
+ data["viewgraphGanttConfigVO"] = gantt_payload["viewgraphGanttConfigVO"]
805
+ return data
806
+
807
+ def _resolve_gantt_payload(self, config: dict[str, Any], field_map: dict[str, int]) -> dict[str, Any]:
808
+ start_field_id = config.get("start_field_id")
809
+ end_field_id = config.get("end_field_id")
810
+ title_field_id = config.get("title_field_id")
811
+ if not any((start_field_id, end_field_id, title_field_id)):
812
+ return {}
813
+ gantt_config = {
814
+ "titleQueId": field_map.get(title_field_id),
815
+ "startTimeQueId": field_map.get(start_field_id),
816
+ "endTimeQueId": field_map.get(end_field_id),
817
+ "defaultTimeDimension": "week",
818
+ "ganttGroupVOList": [],
819
+ "ganttDependencyVO": {
820
+ "dependencyQueId": None,
821
+ "predecessorTaskQueId": None,
822
+ "startEndOptionId": None,
823
+ "startStartOptionId": None,
824
+ "endEndOptionId": None,
825
+ "endStartOptionId": None,
826
+ },
827
+ "ganttAutoCalibrationVO": {
828
+ "autoCalibrationRuleVO": {
829
+ "startStartBegin": False,
830
+ "startEndBegin": False,
831
+ "startEndFinish": False,
832
+ "endStartBegin": True,
833
+ "endStartFinish": True,
834
+ "endEndFinish": False,
835
+ },
836
+ "beingAutoCalibration": False,
837
+ "userAutoCalibration": False,
838
+ },
839
+ }
840
+ return {
841
+ "viewgraphGanttConfigVO": gantt_config,
842
+ **{key: value for key, value in config.items() if key not in {"start_field_id", "end_field_id", "title_field_id"}},
843
+ }
844
+
845
+ def _resolve_qingbi_chart_config_payload(
846
+ self,
847
+ payload: dict[str, Any],
848
+ *,
849
+ entity: CompiledEntity,
850
+ field_map: dict[str, int],
851
+ bi_field_map: dict[str, str],
852
+ qingbi_fields_by_id: dict[str, dict[str, Any]],
853
+ app_key: str,
854
+ ) -> dict[str, Any]:
855
+ data = {
856
+ "chartName": payload.get("chartName"),
857
+ "chartType": payload.get("chartType"),
858
+ "dataSource": {"dataSourceId": app_key, "dataSourceType": "qingflow"},
859
+ "selectedDimensions": self._resolve_qingbi_dimension_fields(
860
+ payload.get("selectedDimensionFieldIds", []),
861
+ entity=entity,
862
+ field_map=field_map,
863
+ bi_field_map=bi_field_map,
864
+ qingbi_fields_by_id=qingbi_fields_by_id,
865
+ ),
866
+ "selectedMetrics": self._resolve_qingbi_metric_fields(
867
+ payload.get("selectedMetricFieldIds", []),
868
+ aggregate=str(payload.get("aggregate") or "count").lower(),
869
+ entity=entity,
870
+ field_map=field_map,
871
+ bi_field_map=bi_field_map,
872
+ qingbi_fields_by_id=qingbi_fields_by_id,
873
+ ),
874
+ "beforeAggregationFilterMatrix": [],
875
+ "afterAggregationFilterMatrix": [],
876
+ "chartStyleConfigs": deepcopy(payload.get("chartStyleConfigs", [])),
877
+ "conditionFormatMatrix": deepcopy(payload.get("conditionFormatMatrix", [])),
878
+ "displayLimitConfig": deepcopy(payload.get("displayLimitConfig", {"status": 1, "type": "asc", "limit": 20})),
879
+ "rawDataConfigDTO": deepcopy(payload.get("rawDataConfigDTO", {"beingOpen": False, "authInfo": {"type": "ws", "contactAuth": {"type": "all", "authMembers": {}}, "externalMemberAuth": {"type": "not", "authMembers": {}}}, "fieldInfoList": []})),
880
+ }
881
+ for key in (
882
+ "selectedTime",
883
+ "xDimensions",
884
+ "yDimensions",
885
+ "xMetrics",
886
+ "yMetrics",
887
+ "leftMetrics",
888
+ "rightMetrics",
889
+ "pieType",
890
+ "radarType",
891
+ "queryConditionFieldIds",
892
+ "queryConditionStatus",
893
+ "queryConditionExact",
894
+ ):
895
+ if key in payload:
896
+ data[key] = deepcopy(payload[key])
897
+ return data
898
+
899
+ def _resolve_graph_payload(self, payload: dict[str, Any], field_map: dict[str, int]) -> dict[str, Any]:
900
+ data = deepcopy(payload)
901
+ if "field_id" in data:
902
+ field_id = data.pop("field_id")
903
+ que_id = field_map.get(field_id)
904
+ if que_id is None:
905
+ return {}
906
+ data["queId"] = que_id
907
+ return data
908
+
909
+ def _resolve_match_rule_groups(self, groups: list[list[dict[str, Any]]], field_map: dict[str, int]) -> list[list[dict[str, Any]]]:
910
+ resolved_groups: list[list[dict[str, Any]]] = []
911
+ for group in groups or []:
912
+ resolved_group: list[dict[str, Any]] = []
913
+ for rule in group or []:
914
+ resolved_rule = deepcopy(rule)
915
+ field_id = resolved_rule.pop("field_id", None)
916
+ if field_id is not None:
917
+ que_id = field_map.get(field_id)
918
+ if que_id is None:
919
+ continue
920
+ resolved_rule["queId"] = que_id
921
+ resolved_group.append(resolved_rule)
922
+ if resolved_group:
923
+ resolved_groups.append(resolved_group)
924
+ return resolved_groups
925
+
926
+ def _resolve_portal_payload(
927
+ self,
928
+ payload: dict[str, Any],
929
+ store: RunArtifactStore,
930
+ *,
931
+ base_payload: dict[str, Any] | None = None,
932
+ ) -> dict[str, Any]:
933
+ data = deepcopy(base_payload) if isinstance(base_payload, dict) else deepcopy(payload)
934
+ data["dashName"] = payload.get("dashName") or data.get("dashName")
935
+ data["dashIcon"] = payload.get("dashIcon") or data.get("dashIcon")
936
+ data["auth"] = deepcopy(payload.get("auth") or data.get("auth"))
937
+ data["hideCopyright"] = payload.get("hideCopyright", data.get("hideCopyright", False))
938
+ tag_id = store.get_artifact("package", "tag_id")
939
+ data["tags"] = deepcopy(payload.get("tags") or data.get("tags") or [])
940
+ for tag in data.get("tags", []):
941
+ if tag.get("tagId") == "__PACKAGE_TAG_ID__":
942
+ tag["tagId"] = tag_id
943
+ dash_global_config = deepcopy(payload.get("dashGlobalConfig") or data.get("dashGlobalConfig") or {})
944
+ dash_global_config.pop("layout", None)
945
+ dash_global_config["interval"] = dash_global_config.get("interval") or 60
946
+ dash_global_config["beingAutoRefresh"] = bool(dash_global_config.get("beingAutoRefresh", False))
947
+ data["dashGlobalConfig"] = dash_global_config
948
+ if "components" in payload:
949
+ data["components"] = self._build_portal_components(payload.get("components", []), store)
950
+ return data
951
+
952
+ def _resolve_navigation_payload(self, payload: dict[str, Any], store: RunArtifactStore, ordinal: int) -> dict[str, Any]:
953
+ data = deepcopy(payload)
954
+ if data.get("tagId") == "__PACKAGE_TAG_ID__":
955
+ data["tagId"] = store.get_artifact("package", "tag_id")
956
+ if "appRef" in data:
957
+ ref = data.pop("appRef")
958
+ data["appKey"] = self._get_app_key(store, ref["entity_id"])
959
+ if "chartRef" in data:
960
+ ref = data.pop("chartRef")
961
+ chart = store.get_artifact("charts", f"{ref['entity_id']}:{ref['chart_id']}", {})
962
+ data["chartKey"] = chart.get("chart_key")
963
+ if data.get("dashKey") == "__PORTAL_DASH_KEY__":
964
+ data["dashKey"] = store.get_artifact("portal", "dash_key")
965
+ if "viewRef" in data:
966
+ ref = data.pop("viewRef")
967
+ view = store.get_artifact("views", f"{ref['entity_id']}:{ref['view_id']}", {})
968
+ data["viewgraphKey"] = view.get("viewgraph_key")
969
+ data["ordinal"] = ordinal
970
+ return data
971
+
972
+ def _resolve_view_reorder_payload(self, profile: str, app_key: str, created_view_keys: list[str]) -> list[dict[str, Any]]:
973
+ current_view_list = self.view_tools.view_list(profile=profile, app_key=app_key).get("result") or []
974
+ if not isinstance(current_view_list, list) or not current_view_list:
975
+ return [{"ordinalType": "FIXED_VIEW_LIST", "viewKeyList": created_view_keys}]
976
+
977
+ created_key_set = set(created_view_keys)
978
+ assigned_keys: set[str] = set()
979
+ payload: list[dict[str, Any]] = []
980
+ for group in current_view_list:
981
+ if not isinstance(group, dict):
982
+ continue
983
+ ordinal_type = group.get("ordinalType")
984
+ view_keys = [
985
+ view.get("viewKey")
986
+ for view in group.get("viewList", [])
987
+ if isinstance(view, dict) and view.get("viewKey")
988
+ ]
989
+ if not ordinal_type or not view_keys:
990
+ continue
991
+ prioritized_keys = [key for key in created_view_keys if key in view_keys]
992
+ if prioritized_keys:
993
+ assigned_keys.update(prioritized_keys)
994
+ remaining_keys = [key for key in view_keys if key not in created_key_set]
995
+ payload.append({"ordinalType": ordinal_type, "viewKeyList": prioritized_keys + remaining_keys})
996
+ else:
997
+ payload.append({"ordinalType": ordinal_type, "viewKeyList": view_keys})
998
+
999
+ unassigned_keys = [key for key in created_view_keys if key not in assigned_keys]
1000
+ if unassigned_keys:
1001
+ fixed_group = next((group for group in payload if group.get("ordinalType") == "FIXED_VIEW_LIST"), None)
1002
+ if fixed_group is None:
1003
+ payload.insert(0, {"ordinalType": "FIXED_VIEW_LIST", "viewKeyList": unassigned_keys})
1004
+ else:
1005
+ fixed_group["viewKeyList"] = unassigned_keys + [key for key in fixed_group["viewKeyList"] if key not in set(unassigned_keys)]
1006
+ return payload or [{"ordinalType": "FIXED_VIEW_LIST", "viewKeyList": created_view_keys}]
1007
+
1008
+ def _build_portal_components(self, components: list[dict[str, Any]], store: RunArtifactStore) -> list[dict[str, Any]]:
1009
+ resolved_components: list[dict[str, Any]] = []
1010
+ pc_x = 0
1011
+ pc_y = 0
1012
+ pc_row_height = 0
1013
+ mobile_y = 0
1014
+ reserved_keys = {"sectionId", "title", "sourceType", "ordinal", "chartRef", "viewRef", "text", "url"}
1015
+ for component in components:
1016
+ source_type = component.get("sourceType")
1017
+ extra = {key: deepcopy(value) for key, value in component.items() if key not in reserved_keys}
1018
+ position = deepcopy(extra.pop("position", None))
1019
+ if position is None:
1020
+ position, pc_x, pc_y, pc_row_height, mobile_y = _portal_component_position(
1021
+ source_type,
1022
+ pc_x=pc_x,
1023
+ pc_y=pc_y,
1024
+ pc_row_height=pc_row_height,
1025
+ mobile_y=mobile_y,
1026
+ )
1027
+ dash_style = deepcopy(extra.pop("dashStyleConfigBO", None))
1028
+ if source_type == "chart" and component.get("chartRef"):
1029
+ ref = component["chartRef"]
1030
+ chart = store.get_artifact("charts", f"{ref['entity_id']}:{ref['chart_id']}", {})
1031
+ chart_config = {
1032
+ "biChartId": chart.get("bi_chart_id") or chart.get("chart_key"),
1033
+ "chartComponentTitle": component.get("title"),
1034
+ "beingShowTitle": True,
1035
+ }
1036
+ chart_config.update(extra.pop("chartConfig", {}) if isinstance(extra.get("chartConfig"), dict) else {})
1037
+ if not chart_config.get("chartComponentTitle"):
1038
+ chart_config["chartComponentTitle"] = component.get("title")
1039
+ resolved_component: dict[str, Any] = {
1040
+ "type": BI_CHART_COMPONENT_TYPE,
1041
+ "position": position,
1042
+ "chartConfig": _compact_dict(chart_config),
1043
+ }
1044
+ if dash_style is not None:
1045
+ resolved_component["dashStyleConfigBO"] = dash_style
1046
+ resolved_component.update(extra)
1047
+ resolved_components.append(
1048
+ resolved_component
1049
+ )
1050
+ elif source_type == "view" and component.get("viewRef"):
1051
+ ref = component["viewRef"]
1052
+ view = store.get_artifact("views", f"{ref['entity_id']}:{ref['view_id']}", {})
1053
+ app_key = store.get_artifact("apps", ref["entity_id"], {}).get("app_key")
1054
+ view_config = {
1055
+ "appKey": app_key,
1056
+ "viewgraphKey": view.get("viewgraph_key"),
1057
+ "viewgraphName": component.get("title"),
1058
+ "formTitle": self._entity_display_name(store, ref["entity_id"]),
1059
+ "componentTitle": component.get("title"),
1060
+ "viewgraphType": _portal_view_type(store, ref["entity_id"], ref["view_id"]),
1061
+ "beingShowTitle": True,
1062
+ "dataManageStatus": True,
1063
+ }
1064
+ view_config.update(extra.pop("viewgraphConfig", {}) if isinstance(extra.get("viewgraphConfig"), dict) else {})
1065
+ if not view_config.get("componentTitle"):
1066
+ view_config["componentTitle"] = component.get("title")
1067
+ resolved_component = {
1068
+ "type": 10,
1069
+ "position": position,
1070
+ "viewgraphConfig": _compact_dict(view_config),
1071
+ }
1072
+ if dash_style is not None:
1073
+ resolved_component["dashStyleConfigBO"] = dash_style
1074
+ resolved_component.update(extra)
1075
+ resolved_components.append(
1076
+ resolved_component
1077
+ )
1078
+ elif source_type == "grid":
1079
+ raw_grid_config = extra.pop("gridConfig", {}) if isinstance(extra.get("gridConfig"), dict) else {}
1080
+ grid_config = deepcopy(raw_grid_config)
1081
+ raw_items = grid_config.pop("items", None)
1082
+ if raw_items is None:
1083
+ raw_items = extra.pop("items", [])
1084
+ resolved_component = {
1085
+ "type": 2,
1086
+ "position": position,
1087
+ "gridConfig": _compact_dict(
1088
+ {
1089
+ "gridTitle": grid_config.pop("gridTitle", component.get("title")),
1090
+ "beingShowTitle": bool(grid_config.pop("beingShowTitle", True)),
1091
+ "items": self._resolve_grid_items(raw_items if isinstance(raw_items, list) else [], store),
1092
+ **grid_config,
1093
+ }
1094
+ ),
1095
+ }
1096
+ if dash_style is not None:
1097
+ resolved_component["dashStyleConfigBO"] = dash_style
1098
+ resolved_component.update(extra)
1099
+ resolved_components.append(resolved_component)
1100
+ elif source_type == "filter":
1101
+ filter_groups = extra.pop("filterConfig", [])
1102
+ graph_list = extra.pop("graphList", [])
1103
+ if isinstance(filter_groups, dict):
1104
+ graph_list = filter_groups.get("graphList", graph_list)
1105
+ filter_groups = filter_groups.get("filterConfig", [])
1106
+ resolved_component = {
1107
+ "type": 6,
1108
+ "position": position,
1109
+ "filterConfig": {
1110
+ "filterConfig": self._resolve_dash_filter_groups(filter_groups, store),
1111
+ "graphList": self._resolve_dash_filter_graphs(graph_list, store),
1112
+ },
1113
+ }
1114
+ if dash_style is not None:
1115
+ resolved_component["dashStyleConfigBO"] = dash_style
1116
+ resolved_component.update(extra)
1117
+ resolved_components.append(resolved_component)
1118
+ elif source_type == "text":
1119
+ text_config = {"text": component.get("text", "")}
1120
+ text_config.update(extra.pop("textConfig", {}) if isinstance(extra.get("textConfig"), dict) else {})
1121
+ resolved_component = {
1122
+ "type": 5,
1123
+ "position": position,
1124
+ "textConfig": text_config,
1125
+ }
1126
+ if dash_style is not None:
1127
+ resolved_component["dashStyleConfigBO"] = dash_style
1128
+ resolved_component.update(extra)
1129
+ resolved_components.append(
1130
+ resolved_component
1131
+ )
1132
+ elif source_type == "link":
1133
+ link_config = {
1134
+ "url": component.get("url", ""),
1135
+ "beingLoginAuth": False,
1136
+ }
1137
+ link_config.update(extra.pop("linkConfig", {}) if isinstance(extra.get("linkConfig"), dict) else {})
1138
+ resolved_component = {
1139
+ "type": 4,
1140
+ "position": position,
1141
+ "linkConfig": link_config,
1142
+ }
1143
+ if dash_style is not None:
1144
+ resolved_component["dashStyleConfigBO"] = dash_style
1145
+ resolved_component.update(extra)
1146
+ resolved_components.append(
1147
+ resolved_component
1148
+ )
1149
+ return resolved_components
1150
+
1151
+ def _resolve_grid_items(self, items: list[dict[str, Any]], store: RunArtifactStore) -> list[dict[str, Any]]:
1152
+ resolved_items: list[dict[str, Any]] = []
1153
+ for ordinal, item in enumerate(items):
1154
+ resolved = self._resolve_grid_item(item, store, ordinal)
1155
+ if resolved is not None:
1156
+ resolved_items.append(resolved)
1157
+ return resolved_items
1158
+
1159
+ def _resolve_grid_item(self, item: dict[str, Any], store: RunArtifactStore, ordinal: int) -> dict[str, Any] | None:
1160
+ data = deepcopy(item)
1161
+ target_type = str(data.pop("target_type", data.pop("targetType", "app")) or "app").lower()
1162
+ display_title = data.pop("title", None)
1163
+ being_show_title = bool(data.pop("beingShowTitle", True))
1164
+ jump_mode = int(data.pop("jumpMode", 1))
1165
+ icon_size = data.pop("iconSize", "40px")
1166
+ icon_url = data.pop("iconUrl", "")
1167
+
1168
+ if target_type == "app":
1169
+ entity_id = data.pop("entity_id", None) or data.pop("entityId", None)
1170
+ if not entity_id:
1171
+ return None
1172
+ app_key = self._get_app_key(store, str(entity_id))
1173
+ default_title = self._entity_display_name(store, str(entity_id))
1174
+ custom_title = display_title or default_title
1175
+ being_custom_title = bool(data.pop("beingCustomTitle", False) or custom_title != default_title)
1176
+ return _compact_dict(
1177
+ {
1178
+ "ordinal": ordinal,
1179
+ "type": 1,
1180
+ "jumpMode": jump_mode,
1181
+ "iconUrl": icon_url,
1182
+ "title": custom_title,
1183
+ "beingShowTitle": being_show_title,
1184
+ "beingCustomTitle": being_custom_title,
1185
+ "customTitle": custom_title,
1186
+ "linkAppKey": app_key,
1187
+ "linkFormType": int(data.pop("linkFormType", 2)),
1188
+ "iconSize": icon_size,
1189
+ **data,
1190
+ }
1191
+ )
1192
+ if target_type == "portal":
1193
+ dash_key = data.pop("dashKey", None) or store.get_artifact("portal", "dash_key")
1194
+ if not dash_key:
1195
+ return None
1196
+ custom_title = display_title or "门户"
1197
+ return _compact_dict(
1198
+ {
1199
+ "ordinal": ordinal,
1200
+ "type": 2,
1201
+ "jumpMode": jump_mode,
1202
+ "iconUrl": icon_url,
1203
+ "title": custom_title,
1204
+ "beingShowTitle": being_show_title,
1205
+ "beingCustomTitle": bool(data.pop("beingCustomTitle", True)),
1206
+ "customTitle": custom_title,
1207
+ "linkDashKey": dash_key,
1208
+ "iconSize": icon_size,
1209
+ **data,
1210
+ }
1211
+ )
1212
+ if target_type == "package":
1213
+ tag_id = data.pop("tagId", None) or store.get_artifact("package", "tag_id")
1214
+ if tag_id is None:
1215
+ return None
1216
+ custom_title = display_title or "应用包"
1217
+ return _compact_dict(
1218
+ {
1219
+ "ordinal": ordinal,
1220
+ "type": 3,
1221
+ "jumpMode": jump_mode,
1222
+ "iconUrl": icon_url,
1223
+ "title": custom_title,
1224
+ "beingShowTitle": being_show_title,
1225
+ "beingCustomTitle": bool(data.pop("beingCustomTitle", True)),
1226
+ "customTitle": custom_title,
1227
+ "linkTagId": tag_id,
1228
+ "iconSize": icon_size,
1229
+ **data,
1230
+ }
1231
+ )
1232
+ if target_type in {"link", "url", "custom_url"}:
1233
+ url = data.pop("url", None) or data.pop("linkUrl", None)
1234
+ if not url:
1235
+ return None
1236
+ custom_title = display_title or "链接"
1237
+ return _compact_dict(
1238
+ {
1239
+ "ordinal": ordinal,
1240
+ "type": 4,
1241
+ "jumpMode": jump_mode,
1242
+ "iconUrl": icon_url,
1243
+ "title": custom_title,
1244
+ "beingShowTitle": being_show_title,
1245
+ "beingCustomTitle": bool(data.pop("beingCustomTitle", True)),
1246
+ "customTitle": custom_title,
1247
+ "linkUrl": url,
1248
+ "iconSize": icon_size,
1249
+ **data,
1250
+ }
1251
+ )
1252
+ return None
1253
+
1254
+ def _seed_records(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
1255
+ if not entity.sample_records:
1256
+ return
1257
+ app_key = self._get_app_key(store, entity.entity_id)
1258
+ field_meta = store.get_artifact("field_maps", entity.entity_id, {}) or {}
1259
+ field_map = field_meta.get("by_field_id", {})
1260
+ record_artifacts = store.get_artifact("records", entity.entity_id, {}) or {}
1261
+ for index, sample_record in enumerate(entity.sample_records, start=1):
1262
+ record_key = sample_record.get("record_id") or f"record_{index}"
1263
+ if record_key in record_artifacts:
1264
+ continue
1265
+ answers = self._build_seed_answers(profile, entity, sample_record.get("values", {}), field_map, store)
1266
+ if not answers:
1267
+ continue
1268
+ result = self.record_tools.record_create(
1269
+ profile=profile,
1270
+ app_key=app_key,
1271
+ answers=answers,
1272
+ submit_type=int(sample_record.get("submit_type", 1)),
1273
+ )
1274
+ apply_result = result.get("result") or {}
1275
+ apply_id = apply_result.get("applyId") or apply_result.get("id")
1276
+ record_artifacts[record_key] = {"apply_id": apply_id, "result": result}
1277
+ store.set_artifact("records", entity.entity_id, record_artifacts)
1278
+
1279
+ def _build_seed_answers(
1280
+ self,
1281
+ profile: str,
1282
+ entity: CompiledEntity,
1283
+ values: dict[str, Any],
1284
+ field_map: dict[str, int],
1285
+ store: RunArtifactStore,
1286
+ ) -> list[dict[str, Any]]:
1287
+ answers: list[dict[str, Any]] = []
1288
+ for field_id, raw_value in values.items():
1289
+ field_spec = entity.field_specs.get(field_id)
1290
+ que_id = field_map.get(field_id)
1291
+ if field_spec is None or que_id is None:
1292
+ continue
1293
+ answer = self._build_answer_detail(profile, field_spec, que_id, raw_value, store)
1294
+ if answer is not None:
1295
+ answers.append(answer)
1296
+ return answers
1297
+
1298
+ def _build_answer_detail(
1299
+ self,
1300
+ profile: str,
1301
+ field_spec: dict[str, Any],
1302
+ que_id: int,
1303
+ raw_value: Any,
1304
+ store: RunArtifactStore,
1305
+ ) -> dict[str, Any] | None:
1306
+ field_type = FieldType(field_spec["type"])
1307
+ if raw_value is None:
1308
+ return None
1309
+ if field_type == FieldType.relation:
1310
+ references = raw_value if isinstance(raw_value, list) else [raw_value]
1311
+ values = []
1312
+ for record_ref in references:
1313
+ apply_id = self._resolve_sample_record_apply_id(field_spec["target_entity_id"], record_ref, store)
1314
+ if apply_id is not None:
1315
+ values.append({"value": str(apply_id)})
1316
+ if not values:
1317
+ return None
1318
+ return {"queId": que_id, "queType": 25, "values": values, "tableValues": []}
1319
+ if field_type == FieldType.multi_select:
1320
+ items = raw_value if isinstance(raw_value, list) else [raw_value]
1321
+ return {"queId": que_id, "queType": QUESTION_TYPE_MAP[field_type], "values": [{"value": str(item)} for item in items], "tableValues": []}
1322
+ if field_type == FieldType.boolean:
1323
+ return {"queId": que_id, "queType": QUESTION_TYPE_MAP[field_type], "values": [{"value": "是" if bool(raw_value) else "否"}], "tableValues": []}
1324
+ if field_type == FieldType.member:
1325
+ member_value = self._resolve_seed_member_value(profile, raw_value)
1326
+ if member_value is None:
1327
+ return None
1328
+ return {"queId": que_id, "queType": QUESTION_TYPE_MAP[field_type], "values": [member_value], "tableValues": []}
1329
+ if field_type == FieldType.attachment:
1330
+ items = raw_value if isinstance(raw_value, list) else [raw_value]
1331
+ return {"queId": que_id, "queType": QUESTION_TYPE_MAP[field_type], "values": items, "tableValues": []}
1332
+ if field_type in {FieldType.subtable, FieldType.department}:
1333
+ return None
1334
+ return {"queId": que_id, "queType": QUESTION_TYPE_MAP[field_type], "values": [{"value": str(raw_value)}], "tableValues": []}
1335
+
1336
+ def _resolve_seed_member_value(self, profile: str, raw_value: Any) -> dict[str, Any] | None:
1337
+ if isinstance(raw_value, dict):
1338
+ member_id = raw_value.get("id") or raw_value.get("uid")
1339
+ if not isinstance(member_id, int) or member_id <= 0:
1340
+ return None
1341
+ value = raw_value.get("value") or raw_value.get("name") or str(member_id)
1342
+ member_payload = {
1343
+ "id": member_id,
1344
+ "value": str(value),
1345
+ }
1346
+ if raw_value.get("email"):
1347
+ member_payload["email"] = raw_value.get("email")
1348
+ if raw_value.get("otherInfo") or raw_value.get("other_info"):
1349
+ member_payload["otherInfo"] = raw_value.get("otherInfo") or raw_value.get("other_info")
1350
+ return member_payload
1351
+ if not isinstance(raw_value, int) or raw_value <= 0:
1352
+ return None
1353
+ session_profile = self.record_tools.sessions.get_profile(profile)
1354
+ value = str(raw_value)
1355
+ member_payload: dict[str, Any] = {"id": raw_value, "value": value}
1356
+ if session_profile is not None and raw_value == session_profile.uid:
1357
+ member_payload["value"] = session_profile.nick_name or session_profile.email or value
1358
+ if session_profile.email:
1359
+ member_payload["email"] = session_profile.email
1360
+ return member_payload
1361
+
1362
+ def _resolve_sample_record_apply_id(self, entity_id: str, record_ref: Any, store: RunArtifactStore) -> int | None:
1363
+ record_key = str(record_ref)
1364
+ entity_records = store.get_artifact("records", entity_id, {}) or {}
1365
+ apply_id = (entity_records.get(record_key) or {}).get("apply_id")
1366
+ return int(apply_id) if apply_id else None
1367
+
1368
+ def _entity_from_step(self, compiled: CompiledSolution, step_name: str) -> CompiledEntity:
1369
+ entity_id = step_name.split(".")[-1]
1370
+ for entity in compiled.entities:
1371
+ if entity.entity_id == entity_id:
1372
+ return entity
1373
+ raise QingflowApiError.config_error(f"unknown entity '{entity_id}' in execution plan")
1374
+
1375
+ def _role_from_step(self, compiled: CompiledSolution, step_name: str) -> CompiledRole:
1376
+ role_id = step_name.split(".")[-1]
1377
+ for role in compiled.roles:
1378
+ if role.role_id == role_id:
1379
+ return role
1380
+ raise QingflowApiError.config_error(f"unknown role '{role_id}' in execution plan")
1381
+
1382
+ def _get_app_key(self, store: RunArtifactStore, entity_id: str) -> str:
1383
+ app_key = store.get_artifact("apps", entity_id, {}).get("app_key")
1384
+ if not app_key:
1385
+ raise QingflowApiError.config_error(f"app_key for entity '{entity_id}' is missing")
1386
+ return app_key
1387
+
1388
+ def _get_edit_version_no(self, store: RunArtifactStore, entity_id: str) -> int | None:
1389
+ app_artifact = store.get_artifact("apps", entity_id, {}) or {}
1390
+ edit_version_no = app_artifact.get("edit_version_no")
1391
+ if edit_version_no is not None:
1392
+ return int(edit_version_no)
1393
+ field_meta = store.get_artifact("field_maps", entity_id, {}) or {}
1394
+ edit_version_no = field_meta.get("edit_version_no")
1395
+ if edit_version_no is not None:
1396
+ return int(edit_version_no)
1397
+ return None
1398
+
1399
+ def _set_edit_version_no(self, store: RunArtifactStore, entity_id: str, edit_version_no: int) -> None:
1400
+ app_artifact = store.get_artifact("apps", entity_id, {}) or {}
1401
+ app_artifact["edit_version_no"] = int(edit_version_no)
1402
+ store.set_artifact("apps", entity_id, app_artifact)
1403
+
1404
+ def _ensure_edit_version(
1405
+ self,
1406
+ profile: str,
1407
+ entity_id: str,
1408
+ store: RunArtifactStore,
1409
+ *,
1410
+ app_key: str | None = None,
1411
+ force_new: bool = False,
1412
+ ) -> int | None:
1413
+ current = None if force_new else self._get_edit_version_no(store, entity_id)
1414
+ if current is not None:
1415
+ return current
1416
+ effective_app_key = app_key or self._get_app_key(store, entity_id)
1417
+ version_result = self.app_tools.app_get_edit_version_no(profile=profile, app_key=effective_app_key).get("result") or {}
1418
+ edit_version_no = version_result.get("editVersionNo") or version_result.get("versionNo")
1419
+ if edit_version_no is None:
1420
+ return None
1421
+ self._set_edit_version_no(store, entity_id, int(edit_version_no))
1422
+ return int(edit_version_no)
1423
+
1424
+ def _get_field_map(self, store: RunArtifactStore, entity_id: str) -> dict[str, int]:
1425
+ return store.get_artifact("field_maps", entity_id, {}).get("by_field_id", {})
1426
+
1427
+ def _publish_payload(self, store: RunArtifactStore, entity_id: str) -> dict[str, Any]:
1428
+ edit_version_no = self._get_edit_version_no(store, entity_id) or 1
1429
+ return {"editVersionNo": int(edit_version_no)}
1430
+
1431
+ def _target_entity_id_from_label(self, entity: CompiledEntity, label: str) -> str | None:
1432
+ for field in entity.field_specs.values():
1433
+ if field["label"] == label and field.get("target_entity_id"):
1434
+ return field["target_entity_id"]
1435
+ return None
1436
+
1437
+ def _target_field_label(self, entity: CompiledEntity, label: str, store: RunArtifactStore) -> str:
1438
+ for field in entity.field_specs.values():
1439
+ if field["label"] == label and field.get("target_field_id"):
1440
+ target_field_id = field["target_field_id"]
1441
+ target_entity_id = field["target_entity_id"]
1442
+ return self._field_label_from_id(target_entity_id, target_field_id, store)
1443
+ return "名称"
1444
+
1445
+ def _target_field_id_from_label(self, entity: CompiledEntity, label: str) -> str:
1446
+ for field in entity.field_specs.values():
1447
+ if field["label"] == label and field.get("target_field_id"):
1448
+ return field["target_field_id"]
1449
+ return "title"
1450
+
1451
+ def _field_label_from_id(self, entity_id: str, field_id: str, store: RunArtifactStore | None) -> str:
1452
+ if store is not None:
1453
+ entity_field_map = store.data.get("normalized_solution_spec", {}).get("entities", [])
1454
+ for entity in entity_field_map:
1455
+ if entity["entity_id"] == entity_id:
1456
+ for field in entity["fields"]:
1457
+ if field["field_id"] == field_id:
1458
+ return field["label"]
1459
+ return "名称" if field_id == "title" else field_id
1460
+
1461
+ def _entity_display_name(self, store: RunArtifactStore, target_entity_id: str) -> str:
1462
+ for entity in store.data.get("normalized_solution_spec", {}).get("entities", []):
1463
+ if entity["entity_id"] == target_entity_id:
1464
+ return entity["display_name"]
1465
+ return target_entity_id
1466
+
1467
+ def _entity_data(self, store: RunArtifactStore, entity_id: str) -> dict[str, Any]:
1468
+ for entity in store.data.get("normalized_solution_spec", {}).get("entities", []):
1469
+ if entity["entity_id"] == entity_id:
1470
+ return entity
1471
+ return {}
1472
+
1473
+ def _field_spec_from_store(self, store: RunArtifactStore, entity_id: str, field_id: str) -> dict[str, Any]:
1474
+ entity = self._entity_data(store, entity_id)
1475
+ for field in entity.get("fields", []):
1476
+ if field.get("field_id") == field_id:
1477
+ return field
1478
+ return {}
1479
+
1480
+ def _resolve_dash_filter_graphs(self, graph_list: list[dict[str, Any]], store: RunArtifactStore) -> list[dict[str, Any]]:
1481
+ resolved_graphs: list[dict[str, Any]] = []
1482
+ for graph in graph_list:
1483
+ data = deepcopy(graph)
1484
+ graph_ref = data.pop("graphRef", None) if isinstance(data.get("graphRef"), dict) else {}
1485
+ entity_id = data.pop("entity_id", None) or graph_ref.get("entity_id")
1486
+ chart_id = data.pop("chart_id", None) or graph_ref.get("chart_id")
1487
+ view_id = data.pop("view_id", None) or graph_ref.get("view_id")
1488
+ graph_type = str(data.get("graphType") or graph_ref.get("graphType") or ("VIEW" if view_id else "CHART")).upper()
1489
+ if entity_id and chart_id and not data.get("graphKey"):
1490
+ chart = store.get_artifact("charts", f"{entity_id}:{chart_id}", {})
1491
+ chart_info = chart.get("chart_info", {})
1492
+ data["graphKey"] = chart.get("bi_chart_id") or chart.get("chart_key")
1493
+ data.setdefault("graphName", chart_info.get("chartName") or chart_id)
1494
+ if entity_id and view_id and not data.get("graphKey"):
1495
+ view = store.get_artifact("views", f"{entity_id}:{view_id}", {})
1496
+ data["graphKey"] = view.get("viewgraph_key")
1497
+ data.setdefault("graphName", self._entity_display_name(store, entity_id))
1498
+ data["graphType"] = graph_type
1499
+ if data.get("graphKey"):
1500
+ resolved_graphs.append(_compact_dict(data))
1501
+ return resolved_graphs
1502
+
1503
+ def _resolve_dash_filter_groups(self, groups: list[dict[str, Any]], store: RunArtifactStore) -> list[dict[str, Any]]:
1504
+ resolved_groups: list[dict[str, Any]] = []
1505
+ for group in groups:
1506
+ group_data = deepcopy(group)
1507
+ resolved_items: list[dict[str, Any]] = []
1508
+ for item in group_data.get("filterGroupConfig", []):
1509
+ item_data = deepcopy(item)
1510
+ conditions = item_data.get("filterCondition", []) or []
1511
+ item_data["filterCondition"] = [
1512
+ resolved
1513
+ for condition in conditions
1514
+ if (resolved := self._resolve_dash_filter_condition(condition, store)) is not None
1515
+ ]
1516
+ resolved_items.append(item_data)
1517
+ group_data["filterGroupConfig"] = resolved_items
1518
+ resolved_groups.append(group_data)
1519
+ return resolved_groups
1520
+
1521
+ def _resolve_dash_filter_condition(self, condition: dict[str, Any], store: RunArtifactStore) -> dict[str, Any] | None:
1522
+ data = deepcopy(condition)
1523
+ graph_ref = data.pop("graphRef", None) if isinstance(data.get("graphRef"), dict) else {}
1524
+ entity_id = data.pop("entity_id", None) or graph_ref.get("entity_id")
1525
+ field_id = data.pop("field_id", None) or graph_ref.get("field_id")
1526
+ chart_id = data.pop("chart_id", None) or graph_ref.get("chart_id")
1527
+ view_id = data.pop("view_id", None) or graph_ref.get("view_id")
1528
+ graph_type = str(data.get("graphType") or graph_ref.get("graphType") or ("VIEW" if view_id else "CHART")).upper()
1529
+ if entity_id and field_id:
1530
+ field_meta = store.get_artifact("field_maps", entity_id, {})
1531
+ field_spec = self._field_spec_from_store(store, entity_id, field_id)
1532
+ field_label = field_spec.get("label") or self._field_label_from_id(entity_id, field_id, store)
1533
+ qingflow_field_type = field_spec.get("type")
1534
+ if qingflow_field_type:
1535
+ try:
1536
+ data.setdefault("queType", QUESTION_TYPE_MAP[FieldType(qingflow_field_type)])
1537
+ except (KeyError, ValueError):
1538
+ pass
1539
+ data.setdefault("queTitle", field_label)
1540
+ data.setdefault("queId", field_meta.get("by_field_id", {}).get(field_id))
1541
+ data.setdefault("queOriginType", data.get("queType"))
1542
+ if qingflow_field_type == "date":
1543
+ data.setdefault("dateType", 0)
1544
+ elif qingflow_field_type == "datetime":
1545
+ data.setdefault("dateType", 1)
1546
+ if entity_id and chart_id and not data.get("chartKey"):
1547
+ chart = store.get_artifact("charts", f"{entity_id}:{chart_id}", {})
1548
+ chart_info = chart.get("chart_info", {})
1549
+ data["chartKey"] = chart.get("bi_chart_id") or chart.get("chart_key")
1550
+ bi_field_id = store.get_artifact("field_maps", entity_id, {}).get("bi_by_field_id", {}).get(field_id)
1551
+ if bi_field_id:
1552
+ data["biFieldId"] = bi_field_id
1553
+ data.setdefault("chartName", chart_info.get("chartName") or chart_id)
1554
+ if entity_id and view_id and not data.get("chartKey"):
1555
+ view = store.get_artifact("views", f"{entity_id}:{view_id}", {})
1556
+ data["chartKey"] = view.get("viewgraph_key")
1557
+ data.setdefault("chartName", self._entity_display_name(store, entity_id))
1558
+ data["graphType"] = graph_type
1559
+ if not data.get("chartKey") or data.get("queId") is None:
1560
+ return None
1561
+ return _compact_dict(data)
1562
+
1563
+ def _resolve_chart_headers(self, headers: list[dict[str, Any]], field_map: dict[str, int]) -> list[dict[str, Any]]:
1564
+ resolved_headers: list[dict[str, Any]] = []
1565
+ for header in headers:
1566
+ field_id = header.get("field_id")
1567
+ que_id = field_map.get(field_id) if field_id else header.get("queId")
1568
+ if que_id is None:
1569
+ continue
1570
+ resolved = deepcopy(header)
1571
+ resolved["queId"] = que_id
1572
+ resolved.pop("field_id", None)
1573
+ resolved_headers.append(resolved)
1574
+ return resolved_headers
1575
+
1576
+ def _resolve_chart_targets(self, targets: list[dict[str, Any]], field_map: dict[str, int]) -> list[dict[str, Any]]:
1577
+ resolved_targets: list[dict[str, Any]] = []
1578
+ for target in targets:
1579
+ field_id = target.get("field_id")
1580
+ que_id = field_map.get(field_id) if field_id else target.get("queId")
1581
+ if que_id is None and not target.get("createByFormula"):
1582
+ continue
1583
+ resolved = deepcopy(target)
1584
+ resolved["queId"] = que_id
1585
+ resolved["aggreType"] = self._chart_aggre_type(str(target.get("aggregate") or "count").lower())
1586
+ resolved.setdefault("createByFormula", False)
1587
+ resolved.setdefault("numberFormat", 1)
1588
+ resolved.setdefault("currencyType", 1)
1589
+ resolved.setdefault("decimalDigit", 0)
1590
+ resolved.setdefault("sort", "none")
1591
+ resolved.pop("field_id", None)
1592
+ resolved.pop("aggregate", None)
1593
+ resolved_targets.append(resolved)
1594
+ return resolved_targets
1595
+
1596
+ def _chart_aggre_type(self, aggregate: str) -> int:
1597
+ return {
1598
+ "sum": 0,
1599
+ "average": 1,
1600
+ "avg": 1,
1601
+ "max": 2,
1602
+ "min": 3,
1603
+ "first": 4,
1604
+ "last": 5,
1605
+ "count": 6,
1606
+ "count_all": 8,
1607
+ }.get(aggregate, 6)
1608
+
1609
+ def _resolve_qingbi_dimension_fields(
1610
+ self,
1611
+ field_ids: list[str],
1612
+ *,
1613
+ entity: CompiledEntity,
1614
+ field_map: dict[str, int],
1615
+ bi_field_map: dict[str, str],
1616
+ qingbi_fields_by_id: dict[str, dict[str, Any]],
1617
+ ) -> list[dict[str, Any]]:
1618
+ dimensions: list[dict[str, Any]] = []
1619
+ for field_id in field_ids:
1620
+ resolved = self._resolve_qingbi_field(
1621
+ entity=entity,
1622
+ field_id=field_id,
1623
+ field_map=field_map,
1624
+ bi_field_map=bi_field_map,
1625
+ qingbi_fields_by_id=qingbi_fields_by_id,
1626
+ field_usage="dimension",
1627
+ aggregate="sum",
1628
+ )
1629
+ if resolved is not None:
1630
+ dimensions.append(resolved)
1631
+ return dimensions
1632
+
1633
+ def _resolve_qingbi_metric_fields(
1634
+ self,
1635
+ field_ids: list[str],
1636
+ *,
1637
+ aggregate: str,
1638
+ entity: CompiledEntity,
1639
+ field_map: dict[str, int],
1640
+ bi_field_map: dict[str, str],
1641
+ qingbi_fields_by_id: dict[str, dict[str, Any]],
1642
+ ) -> list[dict[str, Any]]:
1643
+ if aggregate == "count" or not field_ids:
1644
+ return [self._default_qingbi_total_metric()]
1645
+ metrics: list[dict[str, Any]] = []
1646
+ for field_id in field_ids:
1647
+ resolved = self._resolve_qingbi_field(
1648
+ entity=entity,
1649
+ field_id=field_id,
1650
+ field_map=field_map,
1651
+ bi_field_map=bi_field_map,
1652
+ qingbi_fields_by_id=qingbi_fields_by_id,
1653
+ field_usage="metric",
1654
+ aggregate=aggregate,
1655
+ )
1656
+ if resolved is not None:
1657
+ metrics.append(resolved)
1658
+ return metrics or [self._default_qingbi_total_metric()]
1659
+
1660
+ def _resolve_qingbi_field(
1661
+ self,
1662
+ *,
1663
+ entity: CompiledEntity,
1664
+ field_id: str,
1665
+ field_map: dict[str, int],
1666
+ bi_field_map: dict[str, str],
1667
+ qingbi_fields_by_id: dict[str, dict[str, Any]],
1668
+ field_usage: str,
1669
+ aggregate: str,
1670
+ ) -> dict[str, Any] | None:
1671
+ que_id = field_map.get(field_id)
1672
+ bi_field_id = bi_field_map.get(field_id)
1673
+ if que_id is None or not bi_field_id:
1674
+ return None
1675
+ field_spec = entity.field_specs.get(field_id, {})
1676
+ field_label = entity.field_labels.get(field_id, field_id)
1677
+ qingbi_field = deepcopy(qingbi_fields_by_id.get(bi_field_id, {}))
1678
+ field_type = qingbi_field.get("fieldType") or self._qingbi_field_type_from_spec(field_spec.get("type"))
1679
+ data = {
1680
+ "fieldId": bi_field_id,
1681
+ "fieldName": qingbi_field.get("fieldName") or field_label,
1682
+ "fieldType": field_type,
1683
+ "orderType": "default",
1684
+ "alignType": "left",
1685
+ "dateFormat": "yyyy-MM-dd",
1686
+ "numberFormat": "default",
1687
+ "numberConfig": {
1688
+ "format": "splitter",
1689
+ "unit": "DEFAULT",
1690
+ "prefix": "",
1691
+ "suffix": "",
1692
+ "digit": None,
1693
+ },
1694
+ "digit": None,
1695
+ "aggreType": self._qingbi_aggregate_type(aggregate if field_usage == "metric" else "sum"),
1696
+ "orderPriority": None,
1697
+ "width": None,
1698
+ "verticalAlign": "middle",
1699
+ "formula": qingbi_field.get("formula"),
1700
+ "fieldSource": qingbi_field.get("fieldSource") or "default",
1701
+ "status": qingbi_field.get("status"),
1702
+ "supId": qingbi_field.get("supId"),
1703
+ "beingTable": bool(qingbi_field.get("beingTable", False)),
1704
+ "returnType": qingbi_field.get("returnType"),
1705
+ }
1706
+ return data
1707
+
1708
+ def _default_qingbi_total_metric(self) -> dict[str, Any]:
1709
+ return {
1710
+ "fieldId": ":-100",
1711
+ "fieldName": "数据总量",
1712
+ "fieldType": "decimal",
1713
+ "orderType": "default",
1714
+ "alignType": "left",
1715
+ "dateFormat": "yyyy-MM-dd",
1716
+ "numberFormat": "default",
1717
+ "numberConfig": {
1718
+ "format": "splitter",
1719
+ "unit": "DEFAULT",
1720
+ "prefix": "",
1721
+ "suffix": "",
1722
+ "digit": None,
1723
+ },
1724
+ "digit": None,
1725
+ "aggreType": "sum",
1726
+ "orderPriority": None,
1727
+ "width": None,
1728
+ "verticalAlign": "middle",
1729
+ "beingTable": False,
1730
+ "supId": None,
1731
+ }
1732
+
1733
+ def _qingbi_aggregate_type(self, aggregate: str) -> str:
1734
+ return {
1735
+ "sum": "sum",
1736
+ "avg": "avg",
1737
+ "average": "avg",
1738
+ "max": "max",
1739
+ "min": "min",
1740
+ "count": "sum",
1741
+ "distinct_count": "sum",
1742
+ }.get(aggregate, "sum")
1743
+
1744
+ def _qingbi_field_type_from_spec(self, field_type: str | None) -> str:
1745
+ return {
1746
+ "single_select": "singleSelect",
1747
+ "multi_select": "multiSelect",
1748
+ "member": "member",
1749
+ "department": "dept",
1750
+ "date": "datetime",
1751
+ "datetime": "datetime",
1752
+ "number": "decimal",
1753
+ "amount": "decimal",
1754
+ "boolean": "singleSelect",
1755
+ }.get(str(field_type or ""), "string")
1756
+
1757
+
1758
+ def extract_field_map(schema: dict[str, Any]) -> dict[str, int]:
1759
+ result: dict[str, int] = {}
1760
+ for line in schema.get("formQues", []):
1761
+ _extract_question_line(line, result, None)
1762
+ return result
1763
+
1764
+
1765
+ def _extract_question_line(questions: list[dict[str, Any]], result: dict[str, int], parent_title: str | None) -> None:
1766
+ for question in questions:
1767
+ title = question.get("queTitle")
1768
+ que_id = question.get("queId")
1769
+ que_type = question.get("queType")
1770
+ if title and que_id and que_type != 24:
1771
+ composite_title = title if parent_title is None else f"{parent_title}.{title}"
1772
+ result[composite_title] = que_id
1773
+ result.setdefault(title, que_id)
1774
+ for sub_question in question.get("subQuestions", []) or []:
1775
+ sub_title = sub_question.get("queTitle")
1776
+ sub_que_id = sub_question.get("queId")
1777
+ if title and sub_title and sub_que_id:
1778
+ result[f"{title}.{sub_title}"] = sub_que_id
1779
+ result.setdefault(sub_title, sub_que_id)
1780
+ for inner_row in question.get("innerQuestions", []) or []:
1781
+ _extract_question_line(inner_row, result, title if que_type == 24 else parent_title)
1782
+
1783
+
1784
+ def _compact_dict(data: dict[str, Any]) -> dict[str, Any]:
1785
+ return {key: value for key, value in data.items() if value is not None}
1786
+
1787
+
1788
+ def _extract_workflow_node_id(result: Any, *, expected_type: int | None = None) -> int | None:
1789
+ if expected_type is not None:
1790
+ exact_match = _extract_workflow_node_id_exact(result, expected_type=expected_type)
1791
+ if exact_match is not None:
1792
+ return exact_match
1793
+ return _extract_workflow_node_id_exact(result, expected_type=None)
1794
+
1795
+
1796
+ def _extract_workflow_node_id_exact(result: Any, *, expected_type: int | None) -> int | None:
1797
+ if isinstance(result, dict):
1798
+ node_id = result.get("auditNodeId")
1799
+ node_type = result.get("type")
1800
+ if isinstance(node_id, int) and (expected_type is None or node_type == expected_type):
1801
+ return node_id
1802
+ for node in result.values():
1803
+ extracted = _extract_workflow_node_id_exact(node, expected_type=expected_type)
1804
+ if extracted is not None:
1805
+ return extracted
1806
+ if isinstance(result, list):
1807
+ for node in result:
1808
+ extracted = _extract_workflow_node_id_exact(node, expected_type=expected_type)
1809
+ if extracted is not None:
1810
+ return extracted
1811
+ return None
1812
+
1813
+
1814
+ def _extract_workflow_branch_lane_ids(result: Any) -> list[int]:
1815
+ if isinstance(result, dict):
1816
+ branches = result.get("branches")
1817
+ if isinstance(branches, list):
1818
+ lane_ids = [
1819
+ branch.get("auditNodeId")
1820
+ for branch in branches
1821
+ if isinstance(branch, dict) and isinstance(branch.get("auditNodeId"), int)
1822
+ ]
1823
+ if lane_ids:
1824
+ return lane_ids
1825
+ for node in result.values():
1826
+ lane_ids = _extract_workflow_branch_lane_ids(node)
1827
+ if lane_ids:
1828
+ return lane_ids
1829
+ if isinstance(result, list):
1830
+ for node in result:
1831
+ lane_ids = _extract_workflow_branch_lane_ids(node)
1832
+ if lane_ids:
1833
+ return lane_ids
1834
+ return []
1835
+
1836
+
1837
+ def _branch_lane_ref(branch_node_id: str, branch_index: int) -> str:
1838
+ return f"__branch_lane__{branch_node_id}__{branch_index}"
1839
+
1840
+
1841
+ def _coerce_workflow_nodes(result: Any) -> dict[int, dict[str, Any]]:
1842
+ if not isinstance(result, dict):
1843
+ return {}
1844
+ nodes: dict[int, dict[str, Any]] = {}
1845
+ for node_id, node in result.items():
1846
+ try:
1847
+ parsed_node_id = int(node_id)
1848
+ except (TypeError, ValueError):
1849
+ continue
1850
+ if isinstance(node, dict):
1851
+ nodes[parsed_node_id] = node
1852
+ return nodes
1853
+
1854
+
1855
+ def _workflow_node_is_branch(nodes: dict[int, dict[str, Any]], node_id: int | None) -> bool:
1856
+ if node_id is None:
1857
+ return False
1858
+ node = nodes.get(int(node_id))
1859
+ return isinstance(node, dict) and node.get("type") == 1
1860
+
1861
+
1862
+ def _find_branch_lane_ids(nodes: dict[int, dict[str, Any]], branch_node_id: int | None) -> list[int]:
1863
+ if branch_node_id is None:
1864
+ return []
1865
+ lane_ids = [
1866
+ node_id
1867
+ for node_id, node in nodes.items()
1868
+ if isinstance(node, dict) and node.get("type") == 2 and node.get("prevId") == int(branch_node_id)
1869
+ ]
1870
+ return sorted(lane_ids)
1871
+
1872
+
1873
+ def _find_created_branch_node_id(
1874
+ nodes: dict[int, dict[str, Any]],
1875
+ *,
1876
+ before_node_ids: set[int],
1877
+ prev_id: int | None,
1878
+ ) -> int | None:
1879
+ candidates = [
1880
+ node_id
1881
+ for node_id, node in nodes.items()
1882
+ if node_id not in before_node_ids and isinstance(node, dict) and node.get("type") == 1
1883
+ ]
1884
+ if prev_id is not None:
1885
+ prev_candidates = [node_id for node_id in candidates if nodes[node_id].get("prevId") == int(prev_id)]
1886
+ if prev_candidates:
1887
+ return prev_candidates[0]
1888
+ return candidates[0] if candidates else None
1889
+
1890
+
1891
+ def _find_created_sub_branch_lane_id(
1892
+ nodes: dict[int, dict[str, Any]],
1893
+ *,
1894
+ before_node_ids: set[int],
1895
+ branch_node_id: int | None,
1896
+ ) -> int | None:
1897
+ if branch_node_id is None:
1898
+ return None
1899
+ candidates = [
1900
+ node_id
1901
+ for node_id, node in nodes.items()
1902
+ if node_id not in before_node_ids and isinstance(node, dict) and node.get("type") == 2 and node.get("prevId") == int(branch_node_id)
1903
+ ]
1904
+ return candidates[0] if candidates else None
1905
+
1906
+
1907
+ def _is_navigation_plugin_unavailable(error: QingflowApiError) -> bool:
1908
+ try:
1909
+ backend_code = int(error.backend_code)
1910
+ except (TypeError, ValueError):
1911
+ backend_code = None
1912
+ if backend_code != 50004:
1913
+ return False
1914
+ message = error.message or ""
1915
+ return "导航菜单不可用" in message or "插件未安装" in message
1916
+
1917
+
1918
+ def _portal_plan_has_source_type(portal_plan: dict[str, Any], source_type: str) -> bool:
1919
+ components = ((portal_plan.get("update_payload") or {}) if isinstance(portal_plan, dict) else {}).get("components", [])
1920
+ return any(component.get("sourceType") == source_type for component in components if isinstance(component, dict))
1921
+
1922
+
1923
+ def _coerce_qingflow_error(error: Exception) -> QingflowApiError | None:
1924
+ if isinstance(error, QingflowApiError):
1925
+ return error
1926
+ if not isinstance(error, RuntimeError):
1927
+ return None
1928
+ try:
1929
+ payload = json.loads(str(error))
1930
+ except ValueError:
1931
+ return None
1932
+ if not isinstance(payload, dict):
1933
+ return None
1934
+ message = payload.get("message")
1935
+ category = payload.get("category")
1936
+ if not category or not message:
1937
+ return None
1938
+ return QingflowApiError(
1939
+ category=str(category),
1940
+ message=str(message),
1941
+ backend_code=payload.get("backend_code"),
1942
+ request_id=payload.get("request_id"),
1943
+ http_status=payload.get("http_status"),
1944
+ )
1945
+
1946
+
1947
+ def _portal_component_position(
1948
+ source_type: Any,
1949
+ *,
1950
+ pc_x: int,
1951
+ pc_y: int,
1952
+ pc_row_height: int,
1953
+ mobile_y: int,
1954
+ ) -> tuple[dict[str, Any], int, int, int, int]:
1955
+ source_name = str(source_type or "").lower()
1956
+ if source_name == "filter":
1957
+ cols = 24
1958
+ rows = 2
1959
+ elif source_name == "grid":
1960
+ cols = 24
1961
+ rows = 4
1962
+ elif source_name == "text":
1963
+ cols = 24
1964
+ rows = 2
1965
+ elif source_name == "view":
1966
+ cols = 24
1967
+ rows = 8
1968
+ elif source_name == "link":
1969
+ cols = 12
1970
+ rows = 2
1971
+ else:
1972
+ cols = 8
1973
+ rows = 4
1974
+
1975
+ if cols == 24:
1976
+ if pc_x != 0:
1977
+ pc_y += pc_row_height
1978
+ pc_x = 0
1979
+ pc_row_height = 0
1980
+ position = {
1981
+ "pc": {"cols": 24, "rows": rows, "x": 0, "y": pc_y},
1982
+ "mobile": {"cols": 6, "rows": rows, "x": 0, "y": mobile_y},
1983
+ }
1984
+ pc_y += rows
1985
+ mobile_y += rows
1986
+ return position, 0, pc_y, 0, mobile_y
1987
+
1988
+ if pc_x + cols > 24:
1989
+ pc_y += pc_row_height
1990
+ pc_x = 0
1991
+ pc_row_height = 0
1992
+
1993
+ position = {
1994
+ "pc": {"cols": cols, "rows": rows, "x": pc_x, "y": pc_y},
1995
+ "mobile": {"cols": 6, "rows": rows, "x": 0, "y": mobile_y},
1996
+ }
1997
+ pc_x += cols
1998
+ pc_row_height = max(pc_row_height, rows)
1999
+ if pc_x >= 24:
2000
+ pc_y += pc_row_height
2001
+ pc_x = 0
2002
+ pc_row_height = 0
2003
+ mobile_y += rows
2004
+ return position, pc_x, pc_y, pc_row_height, mobile_y
2005
+
2006
+
2007
+ def _portal_view_type(store: RunArtifactStore, entity_id: str, view_id: str) -> str:
2008
+ view_type_map = {
2009
+ "table": "tableView",
2010
+ "card": "cardView",
2011
+ "board": "boardView",
2012
+ "gantt": "ganttView",
2013
+ "hierarchy": "hierarchyView",
2014
+ }
2015
+ for entity in store.data.get("normalized_solution_spec", {}).get("entities", []):
2016
+ if entity["entity_id"] != entity_id:
2017
+ continue
2018
+ for view in entity.get("views", []):
2019
+ if view.get("view_id") == view_id:
2020
+ return view_type_map.get(view.get("type"), "tableView")
2021
+ return "tableView"
2022
+
2023
+
2024
+ def _build_viewgraph_questions(schema: dict[str, Any], visible_que_ids: list[int]) -> list[dict[str, Any]]:
2025
+ visible_map = {que_id: index for index, que_id in enumerate(visible_que_ids, start=1)}
2026
+ questions: list[dict[str, Any]] = []
2027
+ for base_question in schema.get("baseQues", []):
2028
+ question = _build_viewgraph_question(base_question, visible_map, default_auth=3)
2029
+ if question is not None:
2030
+ questions.append(question)
2031
+ for line in schema.get("formQues", []):
2032
+ for question in line:
2033
+ resolved = _build_viewgraph_question(question, visible_map, default_auth=1)
2034
+ if resolved is not None:
2035
+ questions.append(resolved)
2036
+ return questions
2037
+
2038
+
2039
+ def _build_viewgraph_question(question: dict[str, Any], visible_map: dict[int, int], *, default_auth: int) -> dict[str, Any] | None:
2040
+ que_id = question.get("queId")
2041
+ if que_id is None:
2042
+ return None
2043
+ sub_questions = [
2044
+ resolved
2045
+ for sub_question in question.get("subQuestions", []) or []
2046
+ if (resolved := _build_viewgraph_question(sub_question, visible_map, default_auth=1)) is not None
2047
+ ]
2048
+ inner_questions = [
2049
+ resolved
2050
+ for inner_row in question.get("innerQuestions", []) or []
2051
+ for inner_question in inner_row or []
2052
+ if (resolved := _build_viewgraph_question(inner_question, visible_map, default_auth=1)) is not None
2053
+ ]
2054
+ return {
2055
+ "queId": que_id,
2056
+ "queTitle": question.get("queTitle"),
2057
+ "queType": question.get("queType"),
2058
+ "queAuth": default_auth if que_id <= 5 else 1,
2059
+ "beingListDisplay": que_id in visible_map,
2060
+ "displayOrdinal": visible_map.get(que_id, -1),
2061
+ "subQues": sub_questions,
2062
+ "innerQues": inner_questions,
2063
+ "beingDownload": True,
2064
+ }