@josephyan/qingflow-app-builder-mcp 0.1.0-beta.10

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 (55) hide show
  1. package/README.md +21 -0
  2. package/docs/local-agent-install.md +228 -0
  3. package/entry_point.py +13 -0
  4. package/npm/bin/qingflow-app-builder-mcp.mjs +7 -0
  5. package/npm/lib/runtime.mjs +146 -0
  6. package/npm/scripts/postinstall.mjs +12 -0
  7. package/package.json +33 -0
  8. package/pyproject.toml +64 -0
  9. package/qingflow-app-builder-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 +182 -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/list_type_labels.py +52 -0
  17. package/src/qingflow_mcp/server.py +70 -0
  18. package/src/qingflow_mcp/server_app_builder.py +352 -0
  19. package/src/qingflow_mcp/server_app_user.py +334 -0
  20. package/src/qingflow_mcp/session_store.py +249 -0
  21. package/src/qingflow_mcp/solution/__init__.py +6 -0
  22. package/src/qingflow_mcp/solution/build_assembly_store.py +137 -0
  23. package/src/qingflow_mcp/solution/compiler/__init__.py +265 -0
  24. package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
  25. package/src/qingflow_mcp/solution/compiler/form_compiler.py +456 -0
  26. package/src/qingflow_mcp/solution/compiler/icon_utils.py +113 -0
  27. package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
  28. package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
  29. package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
  30. package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
  31. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +134 -0
  32. package/src/qingflow_mcp/solution/design_session.py +222 -0
  33. package/src/qingflow_mcp/solution/design_store.py +100 -0
  34. package/src/qingflow_mcp/solution/executor.py +2065 -0
  35. package/src/qingflow_mcp/solution/normalizer.py +23 -0
  36. package/src/qingflow_mcp/solution/run_store.py +221 -0
  37. package/src/qingflow_mcp/solution/spec_models.py +853 -0
  38. package/src/qingflow_mcp/tools/__init__.py +1 -0
  39. package/src/qingflow_mcp/tools/app_tools.py +406 -0
  40. package/src/qingflow_mcp/tools/approval_tools.py +498 -0
  41. package/src/qingflow_mcp/tools/auth_tools.py +514 -0
  42. package/src/qingflow_mcp/tools/base.py +81 -0
  43. package/src/qingflow_mcp/tools/directory_tools.py +476 -0
  44. package/src/qingflow_mcp/tools/file_tools.py +375 -0
  45. package/src/qingflow_mcp/tools/navigation_tools.py +177 -0
  46. package/src/qingflow_mcp/tools/package_tools.py +198 -0
  47. package/src/qingflow_mcp/tools/portal_tools.py +100 -0
  48. package/src/qingflow_mcp/tools/qingbi_report_tools.py +235 -0
  49. package/src/qingflow_mcp/tools/record_tools.py +4307 -0
  50. package/src/qingflow_mcp/tools/role_tools.py +94 -0
  51. package/src/qingflow_mcp/tools/solution_tools.py +2684 -0
  52. package/src/qingflow_mcp/tools/task_tools.py +692 -0
  53. package/src/qingflow_mcp/tools/view_tools.py +280 -0
  54. package/src/qingflow_mcp/tools/workflow_tools.py +238 -0
  55. package/src/qingflow_mcp/tools/workspace_tools.py +170 -0
@@ -0,0 +1,2065 @@
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
+ include_raw=True,
625
+ )
626
+ result = schema.get("result") or {}
627
+ field_map = extract_field_map(result)
628
+ label_to_field_id = {label: field_id for field_id, label in entity.field_labels.items()}
629
+ mapped = {field_id: field_map[label] for label, field_id in label_to_field_id.items() if label in field_map}
630
+ current_edit_version_no = self._get_edit_version_no(store, entity.entity_id)
631
+ response_edit_version_no = result.get("editVersionNo")
632
+ store.set_artifact(
633
+ "field_maps",
634
+ entity.entity_id,
635
+ {
636
+ "by_field_id": mapped,
637
+ "bi_by_field_id": {field_id: f"{app_key}:{que_id}" for field_id, que_id in mapped.items()},
638
+ "by_label": field_map,
639
+ "edit_version_no": int(response_edit_version_no or current_edit_version_no or 1),
640
+ "schema": result,
641
+ },
642
+ )
643
+
644
+ def _resolve_app_payload(self, payload: dict[str, Any], store: RunArtifactStore) -> dict[str, Any]:
645
+ data = deepcopy(payload)
646
+ if "tagIds" in data:
647
+ tag_id = store.get_artifact("package", "tag_id")
648
+ data["tagIds"] = [tag_id] if tag_id is not None else []
649
+ return data
650
+
651
+ def _resolve_relation_payload(self, entity: CompiledEntity, store: RunArtifactStore) -> dict[str, Any]:
652
+ payload = deepcopy(entity.form_relation_payload or {})
653
+ for line in payload.get("formQues", []):
654
+ self._resolve_reference_questions(line, entity, store)
655
+ payload["questionRelations"] = []
656
+ return payload
657
+
658
+ def _resolve_reference_questions(self, questions: list[dict[str, Any]], entity: CompiledEntity, store: RunArtifactStore) -> None:
659
+ for question in questions:
660
+ if question.get("queType") == 24:
661
+ for inner_row in question.get("innerQuestions", []):
662
+ self._resolve_reference_questions(inner_row, entity, store)
663
+ continue
664
+ if question.get("queType") != 25:
665
+ continue
666
+ target_entity_id = self._target_entity_id_from_label(entity, question["queTitle"])
667
+ if not target_entity_id:
668
+ continue
669
+ target_field_label = self._target_field_label(entity, question["queTitle"], store)
670
+ target_field_id = self._target_field_id_from_label(entity, question["queTitle"])
671
+ target_meta = store.get_artifact("field_maps", target_entity_id, {})
672
+ target_app = store.get_artifact("apps", target_entity_id, {})
673
+ target_label_map = target_meta.get("by_label", {})
674
+ target_que_id = target_label_map.get(target_field_label, 0)
675
+ reference_config = deepcopy(question.get("referenceConfig") or {})
676
+ reference_config["referAppKey"] = target_app.get("app_key")
677
+ reference_config["referQueId"] = target_que_id
678
+ for ordinal, refer_question in enumerate(reference_config.get("referQuestions", []), start=1):
679
+ refer_field_id = target_field_id if ordinal == 1 else refer_question.get("_field_id") or target_field_id
680
+ refer_label = self._field_label_from_id(target_entity_id, refer_field_id, store)
681
+ refer_que_id = target_meta.get("by_field_id", {}).get(refer_field_id) or target_label_map.get(refer_label, 0)
682
+ refer_field_spec = self._field_spec_from_store(store, target_entity_id, refer_field_id)
683
+ refer_field_type = refer_field_spec.get("type")
684
+ refer_question["queId"] = refer_que_id
685
+ refer_question["queTitle"] = refer_label
686
+ if refer_field_type:
687
+ refer_question["queType"] = str(QUESTION_TYPE_MAP[FieldType(refer_field_type)])
688
+ refer_question["ordinal"] = ordinal
689
+ refer_question.pop("_field_id", None)
690
+ auth_ques = []
691
+ for auth_que in reference_config.get("referAuthQues", []):
692
+ resolved = deepcopy(auth_que)
693
+ refer_field_id = resolved.pop("_field_id", None) or target_field_id
694
+ refer_que_id = target_meta.get("by_field_id", {}).get(refer_field_id)
695
+ if refer_que_id is None:
696
+ continue
697
+ resolved["queId"] = refer_que_id
698
+ resolved["queAuth"] = int(resolved.get("queAuth", 1))
699
+ auth_ques.append(resolved)
700
+ if not auth_ques:
701
+ fallback_que_id = target_meta.get("by_field_id", {}).get(target_field_id)
702
+ if fallback_que_id is not None:
703
+ auth_ques.append({"queId": fallback_que_id, "queAuth": 1})
704
+ reference_config["referAuthQues"] = auth_ques
705
+ reference_config["fieldNameShow"] = bool(reference_config.get("fieldNameShow", True))
706
+ fill_rules = []
707
+ for fill_rule in reference_config.get("referFillRules", []):
708
+ resolved = deepcopy(fill_rule)
709
+ current_field_id = resolved.pop("field_id", None)
710
+ target_fill_field_id = resolved.pop("target_field_id", None)
711
+ if not current_field_id or not target_fill_field_id:
712
+ continue
713
+ resolved["queId"] = store.get_artifact("field_maps", entity.entity_id, {}).get("by_field_id", {}).get(current_field_id)
714
+ resolved["relatedQueId"] = target_meta.get("by_field_id", {}).get(target_fill_field_id)
715
+ resolved["queTitle"] = self._field_label_from_id(entity.entity_id, current_field_id, store)
716
+ resolved["relatedQueTitle"] = self._field_label_from_id(target_entity_id, target_fill_field_id, store)
717
+ resolved["currentQuoteQueId"] = question.get("queId") or question.get("queTempId")
718
+ if resolved["queId"] and resolved["relatedQueId"]:
719
+ fill_rules.append(resolved)
720
+ reference_config["referFillRules"] = fill_rules
721
+ reference_config.pop("_targetFieldId", None)
722
+ reference_config.pop("_targetEntityId", None)
723
+ question["referenceConfig"] = reference_config
724
+
725
+ def _resolve_workflow_payload(self, payload: dict[str, Any], node_artifacts: dict[str, int]) -> dict[str, Any]:
726
+ data = deepcopy(payload)
727
+ prev_ref = data.pop("prevNodeRef", None)
728
+ if prev_ref:
729
+ data["prevId"] = node_artifacts.get(prev_ref, 0)
730
+ audit_ref = data.pop("auditNodeRef", None)
731
+ if audit_ref:
732
+ data["auditNodeId"] = node_artifacts.get(audit_ref, 0)
733
+ audit_user_infos = data.get("auditUserInfos")
734
+ if isinstance(audit_user_infos, dict):
735
+ role_refs = audit_user_infos.pop("role_refs", [])
736
+ if role_refs:
737
+ roles = []
738
+ for role_ref in role_refs:
739
+ role_artifact = self._current_store.get_artifact("roles", role_ref, {}) if hasattr(self, "_current_store") else {}
740
+ role_id = role_artifact.get("role_id")
741
+ if role_id is None:
742
+ raise QingflowApiError.config_error(f"workflow role '{role_ref}' has not been created")
743
+ roles.append(
744
+ {
745
+ "roleId": role_id,
746
+ "roleName": role_artifact.get("role_name") or role_ref,
747
+ "roleIcon": role_artifact.get("role_icon") or "ex-user-outlined",
748
+ "beingFrontendConfig": True,
749
+ }
750
+ )
751
+ audit_user_infos["role"] = roles
752
+ if data.get("auditUserInfos") is None:
753
+ data.pop("auditUserInfos", None)
754
+ return data
755
+
756
+ def _resolve_view_payload(
757
+ self,
758
+ payload: dict[str, Any],
759
+ *,
760
+ app_key: str,
761
+ field_map: dict[str, int],
762
+ schema: dict[str, Any],
763
+ group_by_field_id: str | None,
764
+ ) -> dict[str, Any]:
765
+ data = deepcopy(payload)
766
+ data["appKey"] = app_key
767
+ visible_que_ids = [field_map[field_id] for field_id in data.get("viewgraphQueIds", []) if field_id in field_map]
768
+ data["viewgraphQueIds"] = visible_que_ids
769
+ data.setdefault("defaultRowHigh", "compact")
770
+ data.setdefault("asosChartVisible", False)
771
+ data.setdefault("beingNeedPass", False)
772
+ data.setdefault("beingAuditRecordVisible", True)
773
+ data.setdefault("beingQrobotRecordVisible", False)
774
+ data.setdefault("beingPrintStatus", False)
775
+ data.setdefault("beingDefaultPrintTplStatus", False)
776
+ data.setdefault("beingCommentStatus", False)
777
+ data.setdefault("beingWorkflowNodeFutureListVisible", True)
778
+ data.setdefault("dataPermissionType", "CUSTOM")
779
+ data.setdefault("dataScope", "ALL")
780
+ data.setdefault("needPass", False)
781
+ data.setdefault("viewgraphPass", "")
782
+ data.setdefault("beingImageAdaption", False)
783
+ data.setdefault("clippingMode", "default")
784
+ data.setdefault("frontCoverQueId", None)
785
+ data.setdefault("printTpls", [])
786
+ data.setdefault("usages", [])
787
+ data.setdefault("asosChartConfig", {"limitType": 1, "asosChartIdList": []})
788
+ data.setdefault("viewgraphGanttConfigVO", None)
789
+ data.setdefault("viewgraphHierarchyConfigVO", None)
790
+ data.setdefault("viewgraphLimitFormula", "")
791
+ data.setdefault("buttonConfigDTOList", [])
792
+ if not data.get("viewgraphQuestions"):
793
+ data["viewgraphQuestions"] = _build_viewgraph_questions(schema, visible_que_ids)
794
+ if not data.get("viewgraphSorts"):
795
+ data["viewgraphSorts"] = [{"queId": 0, "beingSortAscend": True, "queType": 8}]
796
+ if data.get("viewgraphType") in {"cardView", "boardView", "ganttView", "hierarchyView"}:
797
+ data["titleQue"] = visible_que_ids[0] if visible_que_ids else None
798
+ else:
799
+ data.setdefault("titleQue", None)
800
+ if data.get("viewgraphType") == "boardView":
801
+ data["groupQueId"] = field_map.get(group_by_field_id) if group_by_field_id else 1
802
+ if data.get("viewgraphType") == "ganttView":
803
+ gantt_payload = self._resolve_gantt_payload(payload, field_map)
804
+ if gantt_payload.get("viewgraphGanttConfigVO"):
805
+ data["viewgraphGanttConfigVO"] = gantt_payload["viewgraphGanttConfigVO"]
806
+ return data
807
+
808
+ def _resolve_gantt_payload(self, config: dict[str, Any], field_map: dict[str, int]) -> dict[str, Any]:
809
+ start_field_id = config.get("start_field_id")
810
+ end_field_id = config.get("end_field_id")
811
+ title_field_id = config.get("title_field_id")
812
+ if not any((start_field_id, end_field_id, title_field_id)):
813
+ return {}
814
+ gantt_config = {
815
+ "titleQueId": field_map.get(title_field_id),
816
+ "startTimeQueId": field_map.get(start_field_id),
817
+ "endTimeQueId": field_map.get(end_field_id),
818
+ "defaultTimeDimension": "week",
819
+ "ganttGroupVOList": [],
820
+ "ganttDependencyVO": {
821
+ "dependencyQueId": None,
822
+ "predecessorTaskQueId": None,
823
+ "startEndOptionId": None,
824
+ "startStartOptionId": None,
825
+ "endEndOptionId": None,
826
+ "endStartOptionId": None,
827
+ },
828
+ "ganttAutoCalibrationVO": {
829
+ "autoCalibrationRuleVO": {
830
+ "startStartBegin": False,
831
+ "startEndBegin": False,
832
+ "startEndFinish": False,
833
+ "endStartBegin": True,
834
+ "endStartFinish": True,
835
+ "endEndFinish": False,
836
+ },
837
+ "beingAutoCalibration": False,
838
+ "userAutoCalibration": False,
839
+ },
840
+ }
841
+ return {
842
+ "viewgraphGanttConfigVO": gantt_config,
843
+ **{key: value for key, value in config.items() if key not in {"start_field_id", "end_field_id", "title_field_id"}},
844
+ }
845
+
846
+ def _resolve_qingbi_chart_config_payload(
847
+ self,
848
+ payload: dict[str, Any],
849
+ *,
850
+ entity: CompiledEntity,
851
+ field_map: dict[str, int],
852
+ bi_field_map: dict[str, str],
853
+ qingbi_fields_by_id: dict[str, dict[str, Any]],
854
+ app_key: str,
855
+ ) -> dict[str, Any]:
856
+ data = {
857
+ "chartName": payload.get("chartName"),
858
+ "chartType": payload.get("chartType"),
859
+ "dataSource": {"dataSourceId": app_key, "dataSourceType": "qingflow"},
860
+ "selectedDimensions": self._resolve_qingbi_dimension_fields(
861
+ payload.get("selectedDimensionFieldIds", []),
862
+ entity=entity,
863
+ field_map=field_map,
864
+ bi_field_map=bi_field_map,
865
+ qingbi_fields_by_id=qingbi_fields_by_id,
866
+ ),
867
+ "selectedMetrics": self._resolve_qingbi_metric_fields(
868
+ payload.get("selectedMetricFieldIds", []),
869
+ aggregate=str(payload.get("aggregate") or "count").lower(),
870
+ entity=entity,
871
+ field_map=field_map,
872
+ bi_field_map=bi_field_map,
873
+ qingbi_fields_by_id=qingbi_fields_by_id,
874
+ ),
875
+ "beforeAggregationFilterMatrix": [],
876
+ "afterAggregationFilterMatrix": [],
877
+ "chartStyleConfigs": deepcopy(payload.get("chartStyleConfigs", [])),
878
+ "conditionFormatMatrix": deepcopy(payload.get("conditionFormatMatrix", [])),
879
+ "displayLimitConfig": deepcopy(payload.get("displayLimitConfig", {"status": 1, "type": "asc", "limit": 20})),
880
+ "rawDataConfigDTO": deepcopy(payload.get("rawDataConfigDTO", {"beingOpen": False, "authInfo": {"type": "ws", "contactAuth": {"type": "all", "authMembers": {}}, "externalMemberAuth": {"type": "not", "authMembers": {}}}, "fieldInfoList": []})),
881
+ }
882
+ for key in (
883
+ "selectedTime",
884
+ "xDimensions",
885
+ "yDimensions",
886
+ "xMetrics",
887
+ "yMetrics",
888
+ "leftMetrics",
889
+ "rightMetrics",
890
+ "pieType",
891
+ "radarType",
892
+ "queryConditionFieldIds",
893
+ "queryConditionStatus",
894
+ "queryConditionExact",
895
+ ):
896
+ if key in payload:
897
+ data[key] = deepcopy(payload[key])
898
+ return data
899
+
900
+ def _resolve_graph_payload(self, payload: dict[str, Any], field_map: dict[str, int]) -> dict[str, Any]:
901
+ data = deepcopy(payload)
902
+ if "field_id" in data:
903
+ field_id = data.pop("field_id")
904
+ que_id = field_map.get(field_id)
905
+ if que_id is None:
906
+ return {}
907
+ data["queId"] = que_id
908
+ return data
909
+
910
+ def _resolve_match_rule_groups(self, groups: list[list[dict[str, Any]]], field_map: dict[str, int]) -> list[list[dict[str, Any]]]:
911
+ resolved_groups: list[list[dict[str, Any]]] = []
912
+ for group in groups or []:
913
+ resolved_group: list[dict[str, Any]] = []
914
+ for rule in group or []:
915
+ resolved_rule = deepcopy(rule)
916
+ field_id = resolved_rule.pop("field_id", None)
917
+ if field_id is not None:
918
+ que_id = field_map.get(field_id)
919
+ if que_id is None:
920
+ continue
921
+ resolved_rule["queId"] = que_id
922
+ resolved_group.append(resolved_rule)
923
+ if resolved_group:
924
+ resolved_groups.append(resolved_group)
925
+ return resolved_groups
926
+
927
+ def _resolve_portal_payload(
928
+ self,
929
+ payload: dict[str, Any],
930
+ store: RunArtifactStore,
931
+ *,
932
+ base_payload: dict[str, Any] | None = None,
933
+ ) -> dict[str, Any]:
934
+ data = deepcopy(base_payload) if isinstance(base_payload, dict) else deepcopy(payload)
935
+ data["dashName"] = payload.get("dashName") or data.get("dashName")
936
+ data["dashIcon"] = payload.get("dashIcon") or data.get("dashIcon")
937
+ data["auth"] = deepcopy(payload.get("auth") or data.get("auth"))
938
+ data["hideCopyright"] = payload.get("hideCopyright", data.get("hideCopyright", False))
939
+ tag_id = store.get_artifact("package", "tag_id")
940
+ data["tags"] = deepcopy(payload.get("tags") or data.get("tags") or [])
941
+ for tag in data.get("tags", []):
942
+ if tag.get("tagId") == "__PACKAGE_TAG_ID__":
943
+ tag["tagId"] = tag_id
944
+ dash_global_config = deepcopy(payload.get("dashGlobalConfig") or data.get("dashGlobalConfig") or {})
945
+ dash_global_config.pop("layout", None)
946
+ dash_global_config["interval"] = dash_global_config.get("interval") or 60
947
+ dash_global_config["beingAutoRefresh"] = bool(dash_global_config.get("beingAutoRefresh", False))
948
+ data["dashGlobalConfig"] = dash_global_config
949
+ if "components" in payload:
950
+ data["components"] = self._build_portal_components(payload.get("components", []), store)
951
+ return data
952
+
953
+ def _resolve_navigation_payload(self, payload: dict[str, Any], store: RunArtifactStore, ordinal: int) -> dict[str, Any]:
954
+ data = deepcopy(payload)
955
+ if data.get("tagId") == "__PACKAGE_TAG_ID__":
956
+ data["tagId"] = store.get_artifact("package", "tag_id")
957
+ if "appRef" in data:
958
+ ref = data.pop("appRef")
959
+ data["appKey"] = self._get_app_key(store, ref["entity_id"])
960
+ if "chartRef" in data:
961
+ ref = data.pop("chartRef")
962
+ chart = store.get_artifact("charts", f"{ref['entity_id']}:{ref['chart_id']}", {})
963
+ data["chartKey"] = chart.get("chart_key")
964
+ if data.get("dashKey") == "__PORTAL_DASH_KEY__":
965
+ data["dashKey"] = store.get_artifact("portal", "dash_key")
966
+ if "viewRef" in data:
967
+ ref = data.pop("viewRef")
968
+ view = store.get_artifact("views", f"{ref['entity_id']}:{ref['view_id']}", {})
969
+ data["viewgraphKey"] = view.get("viewgraph_key")
970
+ data["ordinal"] = ordinal
971
+ return data
972
+
973
+ def _resolve_view_reorder_payload(self, profile: str, app_key: str, created_view_keys: list[str]) -> list[dict[str, Any]]:
974
+ current_view_list = self.view_tools.view_list(profile=profile, app_key=app_key).get("result") or []
975
+ if not isinstance(current_view_list, list) or not current_view_list:
976
+ return [{"ordinalType": "FIXED_VIEW_LIST", "viewKeyList": created_view_keys}]
977
+
978
+ created_key_set = set(created_view_keys)
979
+ assigned_keys: set[str] = set()
980
+ payload: list[dict[str, Any]] = []
981
+ for group in current_view_list:
982
+ if not isinstance(group, dict):
983
+ continue
984
+ ordinal_type = group.get("ordinalType")
985
+ view_keys = [
986
+ view.get("viewKey")
987
+ for view in group.get("viewList", [])
988
+ if isinstance(view, dict) and view.get("viewKey")
989
+ ]
990
+ if not ordinal_type or not view_keys:
991
+ continue
992
+ prioritized_keys = [key for key in created_view_keys if key in view_keys]
993
+ if prioritized_keys:
994
+ assigned_keys.update(prioritized_keys)
995
+ remaining_keys = [key for key in view_keys if key not in created_key_set]
996
+ payload.append({"ordinalType": ordinal_type, "viewKeyList": prioritized_keys + remaining_keys})
997
+ else:
998
+ payload.append({"ordinalType": ordinal_type, "viewKeyList": view_keys})
999
+
1000
+ unassigned_keys = [key for key in created_view_keys if key not in assigned_keys]
1001
+ if unassigned_keys:
1002
+ fixed_group = next((group for group in payload if group.get("ordinalType") == "FIXED_VIEW_LIST"), None)
1003
+ if fixed_group is None:
1004
+ payload.insert(0, {"ordinalType": "FIXED_VIEW_LIST", "viewKeyList": unassigned_keys})
1005
+ else:
1006
+ fixed_group["viewKeyList"] = unassigned_keys + [key for key in fixed_group["viewKeyList"] if key not in set(unassigned_keys)]
1007
+ return payload or [{"ordinalType": "FIXED_VIEW_LIST", "viewKeyList": created_view_keys}]
1008
+
1009
+ def _build_portal_components(self, components: list[dict[str, Any]], store: RunArtifactStore) -> list[dict[str, Any]]:
1010
+ resolved_components: list[dict[str, Any]] = []
1011
+ pc_x = 0
1012
+ pc_y = 0
1013
+ pc_row_height = 0
1014
+ mobile_y = 0
1015
+ reserved_keys = {"sectionId", "title", "sourceType", "ordinal", "chartRef", "viewRef", "text", "url"}
1016
+ for component in components:
1017
+ source_type = component.get("sourceType")
1018
+ extra = {key: deepcopy(value) for key, value in component.items() if key not in reserved_keys}
1019
+ position = deepcopy(extra.pop("position", None))
1020
+ if position is None:
1021
+ position, pc_x, pc_y, pc_row_height, mobile_y = _portal_component_position(
1022
+ source_type,
1023
+ pc_x=pc_x,
1024
+ pc_y=pc_y,
1025
+ pc_row_height=pc_row_height,
1026
+ mobile_y=mobile_y,
1027
+ )
1028
+ dash_style = deepcopy(extra.pop("dashStyleConfigBO", None))
1029
+ if source_type == "chart" and component.get("chartRef"):
1030
+ ref = component["chartRef"]
1031
+ chart = store.get_artifact("charts", f"{ref['entity_id']}:{ref['chart_id']}", {})
1032
+ chart_config = {
1033
+ "biChartId": chart.get("bi_chart_id") or chart.get("chart_key"),
1034
+ "chartComponentTitle": component.get("title"),
1035
+ "beingShowTitle": True,
1036
+ }
1037
+ chart_config.update(extra.pop("chartConfig", {}) if isinstance(extra.get("chartConfig"), dict) else {})
1038
+ if not chart_config.get("chartComponentTitle"):
1039
+ chart_config["chartComponentTitle"] = component.get("title")
1040
+ resolved_component: dict[str, Any] = {
1041
+ "type": BI_CHART_COMPONENT_TYPE,
1042
+ "position": position,
1043
+ "chartConfig": _compact_dict(chart_config),
1044
+ }
1045
+ if dash_style is not None:
1046
+ resolved_component["dashStyleConfigBO"] = dash_style
1047
+ resolved_component.update(extra)
1048
+ resolved_components.append(
1049
+ resolved_component
1050
+ )
1051
+ elif source_type == "view" and component.get("viewRef"):
1052
+ ref = component["viewRef"]
1053
+ view = store.get_artifact("views", f"{ref['entity_id']}:{ref['view_id']}", {})
1054
+ app_key = store.get_artifact("apps", ref["entity_id"], {}).get("app_key")
1055
+ view_config = {
1056
+ "appKey": app_key,
1057
+ "viewgraphKey": view.get("viewgraph_key"),
1058
+ "viewgraphName": component.get("title"),
1059
+ "formTitle": self._entity_display_name(store, ref["entity_id"]),
1060
+ "componentTitle": component.get("title"),
1061
+ "viewgraphType": _portal_view_type(store, ref["entity_id"], ref["view_id"]),
1062
+ "beingShowTitle": True,
1063
+ "dataManageStatus": True,
1064
+ }
1065
+ view_config.update(extra.pop("viewgraphConfig", {}) if isinstance(extra.get("viewgraphConfig"), dict) else {})
1066
+ if not view_config.get("componentTitle"):
1067
+ view_config["componentTitle"] = component.get("title")
1068
+ resolved_component = {
1069
+ "type": 10,
1070
+ "position": position,
1071
+ "viewgraphConfig": _compact_dict(view_config),
1072
+ }
1073
+ if dash_style is not None:
1074
+ resolved_component["dashStyleConfigBO"] = dash_style
1075
+ resolved_component.update(extra)
1076
+ resolved_components.append(
1077
+ resolved_component
1078
+ )
1079
+ elif source_type == "grid":
1080
+ raw_grid_config = extra.pop("gridConfig", {}) if isinstance(extra.get("gridConfig"), dict) else {}
1081
+ grid_config = deepcopy(raw_grid_config)
1082
+ raw_items = grid_config.pop("items", None)
1083
+ if raw_items is None:
1084
+ raw_items = extra.pop("items", [])
1085
+ resolved_component = {
1086
+ "type": 2,
1087
+ "position": position,
1088
+ "gridConfig": _compact_dict(
1089
+ {
1090
+ "gridTitle": grid_config.pop("gridTitle", component.get("title")),
1091
+ "beingShowTitle": bool(grid_config.pop("beingShowTitle", True)),
1092
+ "items": self._resolve_grid_items(raw_items if isinstance(raw_items, list) else [], store),
1093
+ **grid_config,
1094
+ }
1095
+ ),
1096
+ }
1097
+ if dash_style is not None:
1098
+ resolved_component["dashStyleConfigBO"] = dash_style
1099
+ resolved_component.update(extra)
1100
+ resolved_components.append(resolved_component)
1101
+ elif source_type == "filter":
1102
+ filter_groups = extra.pop("filterConfig", [])
1103
+ graph_list = extra.pop("graphList", [])
1104
+ if isinstance(filter_groups, dict):
1105
+ graph_list = filter_groups.get("graphList", graph_list)
1106
+ filter_groups = filter_groups.get("filterConfig", [])
1107
+ resolved_component = {
1108
+ "type": 6,
1109
+ "position": position,
1110
+ "filterConfig": {
1111
+ "filterConfig": self._resolve_dash_filter_groups(filter_groups, store),
1112
+ "graphList": self._resolve_dash_filter_graphs(graph_list, store),
1113
+ },
1114
+ }
1115
+ if dash_style is not None:
1116
+ resolved_component["dashStyleConfigBO"] = dash_style
1117
+ resolved_component.update(extra)
1118
+ resolved_components.append(resolved_component)
1119
+ elif source_type == "text":
1120
+ text_config = {"text": component.get("text", "")}
1121
+ text_config.update(extra.pop("textConfig", {}) if isinstance(extra.get("textConfig"), dict) else {})
1122
+ resolved_component = {
1123
+ "type": 5,
1124
+ "position": position,
1125
+ "textConfig": text_config,
1126
+ }
1127
+ if dash_style is not None:
1128
+ resolved_component["dashStyleConfigBO"] = dash_style
1129
+ resolved_component.update(extra)
1130
+ resolved_components.append(
1131
+ resolved_component
1132
+ )
1133
+ elif source_type == "link":
1134
+ link_config = {
1135
+ "url": component.get("url", ""),
1136
+ "beingLoginAuth": False,
1137
+ }
1138
+ link_config.update(extra.pop("linkConfig", {}) if isinstance(extra.get("linkConfig"), dict) else {})
1139
+ resolved_component = {
1140
+ "type": 4,
1141
+ "position": position,
1142
+ "linkConfig": link_config,
1143
+ }
1144
+ if dash_style is not None:
1145
+ resolved_component["dashStyleConfigBO"] = dash_style
1146
+ resolved_component.update(extra)
1147
+ resolved_components.append(
1148
+ resolved_component
1149
+ )
1150
+ return resolved_components
1151
+
1152
+ def _resolve_grid_items(self, items: list[dict[str, Any]], store: RunArtifactStore) -> list[dict[str, Any]]:
1153
+ resolved_items: list[dict[str, Any]] = []
1154
+ for ordinal, item in enumerate(items):
1155
+ resolved = self._resolve_grid_item(item, store, ordinal)
1156
+ if resolved is not None:
1157
+ resolved_items.append(resolved)
1158
+ return resolved_items
1159
+
1160
+ def _resolve_grid_item(self, item: dict[str, Any], store: RunArtifactStore, ordinal: int) -> dict[str, Any] | None:
1161
+ data = deepcopy(item)
1162
+ target_type = str(data.pop("target_type", data.pop("targetType", "app")) or "app").lower()
1163
+ display_title = data.pop("title", None)
1164
+ being_show_title = bool(data.pop("beingShowTitle", True))
1165
+ jump_mode = int(data.pop("jumpMode", 1))
1166
+ icon_size = data.pop("iconSize", "40px")
1167
+ icon_url = data.pop("iconUrl", "")
1168
+
1169
+ if target_type == "app":
1170
+ entity_id = data.pop("entity_id", None) or data.pop("entityId", None)
1171
+ if not entity_id:
1172
+ return None
1173
+ app_key = self._get_app_key(store, str(entity_id))
1174
+ default_title = self._entity_display_name(store, str(entity_id))
1175
+ custom_title = display_title or default_title
1176
+ being_custom_title = bool(data.pop("beingCustomTitle", False) or custom_title != default_title)
1177
+ return _compact_dict(
1178
+ {
1179
+ "ordinal": ordinal,
1180
+ "type": 1,
1181
+ "jumpMode": jump_mode,
1182
+ "iconUrl": icon_url,
1183
+ "title": custom_title,
1184
+ "beingShowTitle": being_show_title,
1185
+ "beingCustomTitle": being_custom_title,
1186
+ "customTitle": custom_title,
1187
+ "linkAppKey": app_key,
1188
+ "linkFormType": int(data.pop("linkFormType", 2)),
1189
+ "iconSize": icon_size,
1190
+ **data,
1191
+ }
1192
+ )
1193
+ if target_type == "portal":
1194
+ dash_key = data.pop("dashKey", None) or store.get_artifact("portal", "dash_key")
1195
+ if not dash_key:
1196
+ return None
1197
+ custom_title = display_title or "门户"
1198
+ return _compact_dict(
1199
+ {
1200
+ "ordinal": ordinal,
1201
+ "type": 2,
1202
+ "jumpMode": jump_mode,
1203
+ "iconUrl": icon_url,
1204
+ "title": custom_title,
1205
+ "beingShowTitle": being_show_title,
1206
+ "beingCustomTitle": bool(data.pop("beingCustomTitle", True)),
1207
+ "customTitle": custom_title,
1208
+ "linkDashKey": dash_key,
1209
+ "iconSize": icon_size,
1210
+ **data,
1211
+ }
1212
+ )
1213
+ if target_type == "package":
1214
+ tag_id = data.pop("tagId", None) or store.get_artifact("package", "tag_id")
1215
+ if tag_id is None:
1216
+ return None
1217
+ custom_title = display_title or "应用包"
1218
+ return _compact_dict(
1219
+ {
1220
+ "ordinal": ordinal,
1221
+ "type": 3,
1222
+ "jumpMode": jump_mode,
1223
+ "iconUrl": icon_url,
1224
+ "title": custom_title,
1225
+ "beingShowTitle": being_show_title,
1226
+ "beingCustomTitle": bool(data.pop("beingCustomTitle", True)),
1227
+ "customTitle": custom_title,
1228
+ "linkTagId": tag_id,
1229
+ "iconSize": icon_size,
1230
+ **data,
1231
+ }
1232
+ )
1233
+ if target_type in {"link", "url", "custom_url"}:
1234
+ url = data.pop("url", None) or data.pop("linkUrl", None)
1235
+ if not url:
1236
+ return None
1237
+ custom_title = display_title or "链接"
1238
+ return _compact_dict(
1239
+ {
1240
+ "ordinal": ordinal,
1241
+ "type": 4,
1242
+ "jumpMode": jump_mode,
1243
+ "iconUrl": icon_url,
1244
+ "title": custom_title,
1245
+ "beingShowTitle": being_show_title,
1246
+ "beingCustomTitle": bool(data.pop("beingCustomTitle", True)),
1247
+ "customTitle": custom_title,
1248
+ "linkUrl": url,
1249
+ "iconSize": icon_size,
1250
+ **data,
1251
+ }
1252
+ )
1253
+ return None
1254
+
1255
+ def _seed_records(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
1256
+ if not entity.sample_records:
1257
+ return
1258
+ app_key = self._get_app_key(store, entity.entity_id)
1259
+ field_meta = store.get_artifact("field_maps", entity.entity_id, {}) or {}
1260
+ field_map = field_meta.get("by_field_id", {})
1261
+ record_artifacts = store.get_artifact("records", entity.entity_id, {}) or {}
1262
+ for index, sample_record in enumerate(entity.sample_records, start=1):
1263
+ record_key = sample_record.get("record_id") or f"record_{index}"
1264
+ if record_key in record_artifacts:
1265
+ continue
1266
+ answers = self._build_seed_answers(profile, entity, sample_record.get("values", {}), field_map, store)
1267
+ if not answers:
1268
+ continue
1269
+ result = self.record_tools.record_create(
1270
+ profile=profile,
1271
+ app_key=app_key,
1272
+ answers=answers,
1273
+ submit_type=int(sample_record.get("submit_type", 1)),
1274
+ )
1275
+ apply_result = result.get("result") or {}
1276
+ apply_id = apply_result.get("applyId") or apply_result.get("id")
1277
+ record_artifacts[record_key] = {"apply_id": apply_id, "result": result}
1278
+ store.set_artifact("records", entity.entity_id, record_artifacts)
1279
+
1280
+ def _build_seed_answers(
1281
+ self,
1282
+ profile: str,
1283
+ entity: CompiledEntity,
1284
+ values: dict[str, Any],
1285
+ field_map: dict[str, int],
1286
+ store: RunArtifactStore,
1287
+ ) -> list[dict[str, Any]]:
1288
+ answers: list[dict[str, Any]] = []
1289
+ for field_id, raw_value in values.items():
1290
+ field_spec = entity.field_specs.get(field_id)
1291
+ que_id = field_map.get(field_id)
1292
+ if field_spec is None or que_id is None:
1293
+ continue
1294
+ answer = self._build_answer_detail(profile, field_spec, que_id, raw_value, store)
1295
+ if answer is not None:
1296
+ answers.append(answer)
1297
+ return answers
1298
+
1299
+ def _build_answer_detail(
1300
+ self,
1301
+ profile: str,
1302
+ field_spec: dict[str, Any],
1303
+ que_id: int,
1304
+ raw_value: Any,
1305
+ store: RunArtifactStore,
1306
+ ) -> dict[str, Any] | None:
1307
+ field_type = FieldType(field_spec["type"])
1308
+ if raw_value is None:
1309
+ return None
1310
+ if field_type == FieldType.relation:
1311
+ references = raw_value if isinstance(raw_value, list) else [raw_value]
1312
+ values = []
1313
+ for record_ref in references:
1314
+ apply_id = self._resolve_sample_record_apply_id(field_spec["target_entity_id"], record_ref, store)
1315
+ if apply_id is not None:
1316
+ values.append({"value": str(apply_id)})
1317
+ if not values:
1318
+ return None
1319
+ return {"queId": que_id, "queType": 25, "values": values, "tableValues": []}
1320
+ if field_type == FieldType.multi_select:
1321
+ items = raw_value if isinstance(raw_value, list) else [raw_value]
1322
+ return {"queId": que_id, "queType": QUESTION_TYPE_MAP[field_type], "values": [{"value": str(item)} for item in items], "tableValues": []}
1323
+ if field_type == FieldType.boolean:
1324
+ return {"queId": que_id, "queType": QUESTION_TYPE_MAP[field_type], "values": [{"value": "是" if bool(raw_value) else "否"}], "tableValues": []}
1325
+ if field_type == FieldType.member:
1326
+ member_value = self._resolve_seed_member_value(profile, raw_value)
1327
+ if member_value is None:
1328
+ return None
1329
+ return {"queId": que_id, "queType": QUESTION_TYPE_MAP[field_type], "values": [member_value], "tableValues": []}
1330
+ if field_type == FieldType.attachment:
1331
+ items = raw_value if isinstance(raw_value, list) else [raw_value]
1332
+ return {"queId": que_id, "queType": QUESTION_TYPE_MAP[field_type], "values": items, "tableValues": []}
1333
+ if field_type in {FieldType.subtable, FieldType.department}:
1334
+ return None
1335
+ return {"queId": que_id, "queType": QUESTION_TYPE_MAP[field_type], "values": [{"value": str(raw_value)}], "tableValues": []}
1336
+
1337
+ def _resolve_seed_member_value(self, profile: str, raw_value: Any) -> dict[str, Any] | None:
1338
+ if isinstance(raw_value, dict):
1339
+ member_id = raw_value.get("id") or raw_value.get("uid")
1340
+ if not isinstance(member_id, int) or member_id <= 0:
1341
+ return None
1342
+ value = raw_value.get("value") or raw_value.get("name") or str(member_id)
1343
+ member_payload = {
1344
+ "id": member_id,
1345
+ "value": str(value),
1346
+ }
1347
+ if raw_value.get("email"):
1348
+ member_payload["email"] = raw_value.get("email")
1349
+ if raw_value.get("otherInfo") or raw_value.get("other_info"):
1350
+ member_payload["otherInfo"] = raw_value.get("otherInfo") or raw_value.get("other_info")
1351
+ return member_payload
1352
+ if not isinstance(raw_value, int) or raw_value <= 0:
1353
+ return None
1354
+ session_profile = self.record_tools.sessions.get_profile(profile)
1355
+ value = str(raw_value)
1356
+ member_payload: dict[str, Any] = {"id": raw_value, "value": value}
1357
+ if session_profile is not None and raw_value == session_profile.uid:
1358
+ member_payload["value"] = session_profile.nick_name or session_profile.email or value
1359
+ if session_profile.email:
1360
+ member_payload["email"] = session_profile.email
1361
+ return member_payload
1362
+
1363
+ def _resolve_sample_record_apply_id(self, entity_id: str, record_ref: Any, store: RunArtifactStore) -> int | None:
1364
+ record_key = str(record_ref)
1365
+ entity_records = store.get_artifact("records", entity_id, {}) or {}
1366
+ apply_id = (entity_records.get(record_key) or {}).get("apply_id")
1367
+ return int(apply_id) if apply_id else None
1368
+
1369
+ def _entity_from_step(self, compiled: CompiledSolution, step_name: str) -> CompiledEntity:
1370
+ entity_id = step_name.split(".")[-1]
1371
+ for entity in compiled.entities:
1372
+ if entity.entity_id == entity_id:
1373
+ return entity
1374
+ raise QingflowApiError.config_error(f"unknown entity '{entity_id}' in execution plan")
1375
+
1376
+ def _role_from_step(self, compiled: CompiledSolution, step_name: str) -> CompiledRole:
1377
+ role_id = step_name.split(".")[-1]
1378
+ for role in compiled.roles:
1379
+ if role.role_id == role_id:
1380
+ return role
1381
+ raise QingflowApiError.config_error(f"unknown role '{role_id}' in execution plan")
1382
+
1383
+ def _get_app_key(self, store: RunArtifactStore, entity_id: str) -> str:
1384
+ app_key = store.get_artifact("apps", entity_id, {}).get("app_key")
1385
+ if not app_key:
1386
+ raise QingflowApiError.config_error(f"app_key for entity '{entity_id}' is missing")
1387
+ return app_key
1388
+
1389
+ def _get_edit_version_no(self, store: RunArtifactStore, entity_id: str) -> int | None:
1390
+ app_artifact = store.get_artifact("apps", entity_id, {}) or {}
1391
+ edit_version_no = app_artifact.get("edit_version_no")
1392
+ if edit_version_no is not None:
1393
+ return int(edit_version_no)
1394
+ field_meta = store.get_artifact("field_maps", entity_id, {}) or {}
1395
+ edit_version_no = field_meta.get("edit_version_no")
1396
+ if edit_version_no is not None:
1397
+ return int(edit_version_no)
1398
+ return None
1399
+
1400
+ def _set_edit_version_no(self, store: RunArtifactStore, entity_id: str, edit_version_no: int) -> None:
1401
+ app_artifact = store.get_artifact("apps", entity_id, {}) or {}
1402
+ app_artifact["edit_version_no"] = int(edit_version_no)
1403
+ store.set_artifact("apps", entity_id, app_artifact)
1404
+
1405
+ def _ensure_edit_version(
1406
+ self,
1407
+ profile: str,
1408
+ entity_id: str,
1409
+ store: RunArtifactStore,
1410
+ *,
1411
+ app_key: str | None = None,
1412
+ force_new: bool = False,
1413
+ ) -> int | None:
1414
+ current = None if force_new else self._get_edit_version_no(store, entity_id)
1415
+ if current is not None:
1416
+ return current
1417
+ effective_app_key = app_key or self._get_app_key(store, entity_id)
1418
+ version_result = self.app_tools.app_get_edit_version_no(profile=profile, app_key=effective_app_key).get("result") or {}
1419
+ edit_version_no = version_result.get("editVersionNo") or version_result.get("versionNo")
1420
+ if edit_version_no is None:
1421
+ return None
1422
+ self._set_edit_version_no(store, entity_id, int(edit_version_no))
1423
+ return int(edit_version_no)
1424
+
1425
+ def _get_field_map(self, store: RunArtifactStore, entity_id: str) -> dict[str, int]:
1426
+ return store.get_artifact("field_maps", entity_id, {}).get("by_field_id", {})
1427
+
1428
+ def _publish_payload(self, store: RunArtifactStore, entity_id: str) -> dict[str, Any]:
1429
+ edit_version_no = self._get_edit_version_no(store, entity_id) or 1
1430
+ return {"editVersionNo": int(edit_version_no)}
1431
+
1432
+ def _target_entity_id_from_label(self, entity: CompiledEntity, label: str) -> str | None:
1433
+ for field in entity.field_specs.values():
1434
+ if field["label"] == label and field.get("target_entity_id"):
1435
+ return field["target_entity_id"]
1436
+ return None
1437
+
1438
+ def _target_field_label(self, entity: CompiledEntity, label: str, store: RunArtifactStore) -> str:
1439
+ for field in entity.field_specs.values():
1440
+ if field["label"] == label and field.get("target_field_id"):
1441
+ target_field_id = field["target_field_id"]
1442
+ target_entity_id = field["target_entity_id"]
1443
+ return self._field_label_from_id(target_entity_id, target_field_id, store)
1444
+ return "名称"
1445
+
1446
+ def _target_field_id_from_label(self, entity: CompiledEntity, label: str) -> str:
1447
+ for field in entity.field_specs.values():
1448
+ if field["label"] == label and field.get("target_field_id"):
1449
+ return field["target_field_id"]
1450
+ return "title"
1451
+
1452
+ def _field_label_from_id(self, entity_id: str, field_id: str, store: RunArtifactStore | None) -> str:
1453
+ if store is not None:
1454
+ entity_field_map = store.data.get("normalized_solution_spec", {}).get("entities", [])
1455
+ for entity in entity_field_map:
1456
+ if entity["entity_id"] == entity_id:
1457
+ for field in entity["fields"]:
1458
+ if field["field_id"] == field_id:
1459
+ return field["label"]
1460
+ return "名称" if field_id == "title" else field_id
1461
+
1462
+ def _entity_display_name(self, store: RunArtifactStore, target_entity_id: str) -> str:
1463
+ for entity in store.data.get("normalized_solution_spec", {}).get("entities", []):
1464
+ if entity["entity_id"] == target_entity_id:
1465
+ return entity["display_name"]
1466
+ return target_entity_id
1467
+
1468
+ def _entity_data(self, store: RunArtifactStore, entity_id: str) -> dict[str, Any]:
1469
+ for entity in store.data.get("normalized_solution_spec", {}).get("entities", []):
1470
+ if entity["entity_id"] == entity_id:
1471
+ return entity
1472
+ return {}
1473
+
1474
+ def _field_spec_from_store(self, store: RunArtifactStore, entity_id: str, field_id: str) -> dict[str, Any]:
1475
+ entity = self._entity_data(store, entity_id)
1476
+ for field in entity.get("fields", []):
1477
+ if field.get("field_id") == field_id:
1478
+ return field
1479
+ return {}
1480
+
1481
+ def _resolve_dash_filter_graphs(self, graph_list: list[dict[str, Any]], store: RunArtifactStore) -> list[dict[str, Any]]:
1482
+ resolved_graphs: list[dict[str, Any]] = []
1483
+ for graph in graph_list:
1484
+ data = deepcopy(graph)
1485
+ graph_ref = data.pop("graphRef", None) if isinstance(data.get("graphRef"), dict) else {}
1486
+ entity_id = data.pop("entity_id", None) or graph_ref.get("entity_id")
1487
+ chart_id = data.pop("chart_id", None) or graph_ref.get("chart_id")
1488
+ view_id = data.pop("view_id", None) or graph_ref.get("view_id")
1489
+ graph_type = str(data.get("graphType") or graph_ref.get("graphType") or ("VIEW" if view_id else "CHART")).upper()
1490
+ if entity_id and chart_id and not data.get("graphKey"):
1491
+ chart = store.get_artifact("charts", f"{entity_id}:{chart_id}", {})
1492
+ chart_info = chart.get("chart_info", {})
1493
+ data["graphKey"] = chart.get("bi_chart_id") or chart.get("chart_key")
1494
+ data.setdefault("graphName", chart_info.get("chartName") or chart_id)
1495
+ if entity_id and view_id and not data.get("graphKey"):
1496
+ view = store.get_artifact("views", f"{entity_id}:{view_id}", {})
1497
+ data["graphKey"] = view.get("viewgraph_key")
1498
+ data.setdefault("graphName", self._entity_display_name(store, entity_id))
1499
+ data["graphType"] = graph_type
1500
+ if data.get("graphKey"):
1501
+ resolved_graphs.append(_compact_dict(data))
1502
+ return resolved_graphs
1503
+
1504
+ def _resolve_dash_filter_groups(self, groups: list[dict[str, Any]], store: RunArtifactStore) -> list[dict[str, Any]]:
1505
+ resolved_groups: list[dict[str, Any]] = []
1506
+ for group in groups:
1507
+ group_data = deepcopy(group)
1508
+ resolved_items: list[dict[str, Any]] = []
1509
+ for item in group_data.get("filterGroupConfig", []):
1510
+ item_data = deepcopy(item)
1511
+ conditions = item_data.get("filterCondition", []) or []
1512
+ item_data["filterCondition"] = [
1513
+ resolved
1514
+ for condition in conditions
1515
+ if (resolved := self._resolve_dash_filter_condition(condition, store)) is not None
1516
+ ]
1517
+ resolved_items.append(item_data)
1518
+ group_data["filterGroupConfig"] = resolved_items
1519
+ resolved_groups.append(group_data)
1520
+ return resolved_groups
1521
+
1522
+ def _resolve_dash_filter_condition(self, condition: dict[str, Any], store: RunArtifactStore) -> dict[str, Any] | None:
1523
+ data = deepcopy(condition)
1524
+ graph_ref = data.pop("graphRef", None) if isinstance(data.get("graphRef"), dict) else {}
1525
+ entity_id = data.pop("entity_id", None) or graph_ref.get("entity_id")
1526
+ field_id = data.pop("field_id", None) or graph_ref.get("field_id")
1527
+ chart_id = data.pop("chart_id", None) or graph_ref.get("chart_id")
1528
+ view_id = data.pop("view_id", None) or graph_ref.get("view_id")
1529
+ graph_type = str(data.get("graphType") or graph_ref.get("graphType") or ("VIEW" if view_id else "CHART")).upper()
1530
+ if entity_id and field_id:
1531
+ field_meta = store.get_artifact("field_maps", entity_id, {})
1532
+ field_spec = self._field_spec_from_store(store, entity_id, field_id)
1533
+ field_label = field_spec.get("label") or self._field_label_from_id(entity_id, field_id, store)
1534
+ qingflow_field_type = field_spec.get("type")
1535
+ if qingflow_field_type:
1536
+ try:
1537
+ data.setdefault("queType", QUESTION_TYPE_MAP[FieldType(qingflow_field_type)])
1538
+ except (KeyError, ValueError):
1539
+ pass
1540
+ data.setdefault("queTitle", field_label)
1541
+ data.setdefault("queId", field_meta.get("by_field_id", {}).get(field_id))
1542
+ data.setdefault("queOriginType", data.get("queType"))
1543
+ if qingflow_field_type == "date":
1544
+ data.setdefault("dateType", 0)
1545
+ elif qingflow_field_type == "datetime":
1546
+ data.setdefault("dateType", 1)
1547
+ if entity_id and chart_id and not data.get("chartKey"):
1548
+ chart = store.get_artifact("charts", f"{entity_id}:{chart_id}", {})
1549
+ chart_info = chart.get("chart_info", {})
1550
+ data["chartKey"] = chart.get("bi_chart_id") or chart.get("chart_key")
1551
+ bi_field_id = store.get_artifact("field_maps", entity_id, {}).get("bi_by_field_id", {}).get(field_id)
1552
+ if bi_field_id:
1553
+ data["biFieldId"] = bi_field_id
1554
+ data.setdefault("chartName", chart_info.get("chartName") or chart_id)
1555
+ if entity_id and view_id and not data.get("chartKey"):
1556
+ view = store.get_artifact("views", f"{entity_id}:{view_id}", {})
1557
+ data["chartKey"] = view.get("viewgraph_key")
1558
+ data.setdefault("chartName", self._entity_display_name(store, entity_id))
1559
+ data["graphType"] = graph_type
1560
+ if not data.get("chartKey") or data.get("queId") is None:
1561
+ return None
1562
+ return _compact_dict(data)
1563
+
1564
+ def _resolve_chart_headers(self, headers: list[dict[str, Any]], field_map: dict[str, int]) -> list[dict[str, Any]]:
1565
+ resolved_headers: list[dict[str, Any]] = []
1566
+ for header in headers:
1567
+ field_id = header.get("field_id")
1568
+ que_id = field_map.get(field_id) if field_id else header.get("queId")
1569
+ if que_id is None:
1570
+ continue
1571
+ resolved = deepcopy(header)
1572
+ resolved["queId"] = que_id
1573
+ resolved.pop("field_id", None)
1574
+ resolved_headers.append(resolved)
1575
+ return resolved_headers
1576
+
1577
+ def _resolve_chart_targets(self, targets: list[dict[str, Any]], field_map: dict[str, int]) -> list[dict[str, Any]]:
1578
+ resolved_targets: list[dict[str, Any]] = []
1579
+ for target in targets:
1580
+ field_id = target.get("field_id")
1581
+ que_id = field_map.get(field_id) if field_id else target.get("queId")
1582
+ if que_id is None and not target.get("createByFormula"):
1583
+ continue
1584
+ resolved = deepcopy(target)
1585
+ resolved["queId"] = que_id
1586
+ resolved["aggreType"] = self._chart_aggre_type(str(target.get("aggregate") or "count").lower())
1587
+ resolved.setdefault("createByFormula", False)
1588
+ resolved.setdefault("numberFormat", 1)
1589
+ resolved.setdefault("currencyType", 1)
1590
+ resolved.setdefault("decimalDigit", 0)
1591
+ resolved.setdefault("sort", "none")
1592
+ resolved.pop("field_id", None)
1593
+ resolved.pop("aggregate", None)
1594
+ resolved_targets.append(resolved)
1595
+ return resolved_targets
1596
+
1597
+ def _chart_aggre_type(self, aggregate: str) -> int:
1598
+ return {
1599
+ "sum": 0,
1600
+ "average": 1,
1601
+ "avg": 1,
1602
+ "max": 2,
1603
+ "min": 3,
1604
+ "first": 4,
1605
+ "last": 5,
1606
+ "count": 6,
1607
+ "count_all": 8,
1608
+ }.get(aggregate, 6)
1609
+
1610
+ def _resolve_qingbi_dimension_fields(
1611
+ self,
1612
+ field_ids: list[str],
1613
+ *,
1614
+ entity: CompiledEntity,
1615
+ field_map: dict[str, int],
1616
+ bi_field_map: dict[str, str],
1617
+ qingbi_fields_by_id: dict[str, dict[str, Any]],
1618
+ ) -> list[dict[str, Any]]:
1619
+ dimensions: list[dict[str, Any]] = []
1620
+ for field_id in field_ids:
1621
+ resolved = self._resolve_qingbi_field(
1622
+ entity=entity,
1623
+ field_id=field_id,
1624
+ field_map=field_map,
1625
+ bi_field_map=bi_field_map,
1626
+ qingbi_fields_by_id=qingbi_fields_by_id,
1627
+ field_usage="dimension",
1628
+ aggregate="sum",
1629
+ )
1630
+ if resolved is not None:
1631
+ dimensions.append(resolved)
1632
+ return dimensions
1633
+
1634
+ def _resolve_qingbi_metric_fields(
1635
+ self,
1636
+ field_ids: list[str],
1637
+ *,
1638
+ aggregate: str,
1639
+ entity: CompiledEntity,
1640
+ field_map: dict[str, int],
1641
+ bi_field_map: dict[str, str],
1642
+ qingbi_fields_by_id: dict[str, dict[str, Any]],
1643
+ ) -> list[dict[str, Any]]:
1644
+ if aggregate == "count" or not field_ids:
1645
+ return [self._default_qingbi_total_metric()]
1646
+ metrics: list[dict[str, Any]] = []
1647
+ for field_id in field_ids:
1648
+ resolved = self._resolve_qingbi_field(
1649
+ entity=entity,
1650
+ field_id=field_id,
1651
+ field_map=field_map,
1652
+ bi_field_map=bi_field_map,
1653
+ qingbi_fields_by_id=qingbi_fields_by_id,
1654
+ field_usage="metric",
1655
+ aggregate=aggregate,
1656
+ )
1657
+ if resolved is not None:
1658
+ metrics.append(resolved)
1659
+ return metrics or [self._default_qingbi_total_metric()]
1660
+
1661
+ def _resolve_qingbi_field(
1662
+ self,
1663
+ *,
1664
+ entity: CompiledEntity,
1665
+ field_id: str,
1666
+ field_map: dict[str, int],
1667
+ bi_field_map: dict[str, str],
1668
+ qingbi_fields_by_id: dict[str, dict[str, Any]],
1669
+ field_usage: str,
1670
+ aggregate: str,
1671
+ ) -> dict[str, Any] | None:
1672
+ que_id = field_map.get(field_id)
1673
+ bi_field_id = bi_field_map.get(field_id)
1674
+ if que_id is None or not bi_field_id:
1675
+ return None
1676
+ field_spec = entity.field_specs.get(field_id, {})
1677
+ field_label = entity.field_labels.get(field_id, field_id)
1678
+ qingbi_field = deepcopy(qingbi_fields_by_id.get(bi_field_id, {}))
1679
+ field_type = qingbi_field.get("fieldType") or self._qingbi_field_type_from_spec(field_spec.get("type"))
1680
+ data = {
1681
+ "fieldId": bi_field_id,
1682
+ "fieldName": qingbi_field.get("fieldName") or field_label,
1683
+ "fieldType": field_type,
1684
+ "orderType": "default",
1685
+ "alignType": "left",
1686
+ "dateFormat": "yyyy-MM-dd",
1687
+ "numberFormat": "default",
1688
+ "numberConfig": {
1689
+ "format": "splitter",
1690
+ "unit": "DEFAULT",
1691
+ "prefix": "",
1692
+ "suffix": "",
1693
+ "digit": None,
1694
+ },
1695
+ "digit": None,
1696
+ "aggreType": self._qingbi_aggregate_type(aggregate if field_usage == "metric" else "sum"),
1697
+ "orderPriority": None,
1698
+ "width": None,
1699
+ "verticalAlign": "middle",
1700
+ "formula": qingbi_field.get("formula"),
1701
+ "fieldSource": qingbi_field.get("fieldSource") or "default",
1702
+ "status": qingbi_field.get("status"),
1703
+ "supId": qingbi_field.get("supId"),
1704
+ "beingTable": bool(qingbi_field.get("beingTable", False)),
1705
+ "returnType": qingbi_field.get("returnType"),
1706
+ }
1707
+ return data
1708
+
1709
+ def _default_qingbi_total_metric(self) -> dict[str, Any]:
1710
+ return {
1711
+ "fieldId": ":-100",
1712
+ "fieldName": "数据总量",
1713
+ "fieldType": "decimal",
1714
+ "orderType": "default",
1715
+ "alignType": "left",
1716
+ "dateFormat": "yyyy-MM-dd",
1717
+ "numberFormat": "default",
1718
+ "numberConfig": {
1719
+ "format": "splitter",
1720
+ "unit": "DEFAULT",
1721
+ "prefix": "",
1722
+ "suffix": "",
1723
+ "digit": None,
1724
+ },
1725
+ "digit": None,
1726
+ "aggreType": "sum",
1727
+ "orderPriority": None,
1728
+ "width": None,
1729
+ "verticalAlign": "middle",
1730
+ "beingTable": False,
1731
+ "supId": None,
1732
+ }
1733
+
1734
+ def _qingbi_aggregate_type(self, aggregate: str) -> str:
1735
+ return {
1736
+ "sum": "sum",
1737
+ "avg": "avg",
1738
+ "average": "avg",
1739
+ "max": "max",
1740
+ "min": "min",
1741
+ "count": "sum",
1742
+ "distinct_count": "sum",
1743
+ }.get(aggregate, "sum")
1744
+
1745
+ def _qingbi_field_type_from_spec(self, field_type: str | None) -> str:
1746
+ return {
1747
+ "single_select": "singleSelect",
1748
+ "multi_select": "multiSelect",
1749
+ "member": "member",
1750
+ "department": "dept",
1751
+ "date": "datetime",
1752
+ "datetime": "datetime",
1753
+ "number": "decimal",
1754
+ "amount": "decimal",
1755
+ "boolean": "singleSelect",
1756
+ }.get(str(field_type or ""), "string")
1757
+
1758
+
1759
+ def extract_field_map(schema: dict[str, Any]) -> dict[str, int]:
1760
+ result: dict[str, int] = {}
1761
+ for line in schema.get("formQues", []):
1762
+ _extract_question_line(line, result, None)
1763
+ return result
1764
+
1765
+
1766
+ def _extract_question_line(questions: list[dict[str, Any]], result: dict[str, int], parent_title: str | None) -> None:
1767
+ for question in questions:
1768
+ title = question.get("queTitle")
1769
+ que_id = question.get("queId")
1770
+ que_type = question.get("queType")
1771
+ if title and que_id and que_type != 24:
1772
+ composite_title = title if parent_title is None else f"{parent_title}.{title}"
1773
+ result[composite_title] = que_id
1774
+ result.setdefault(title, que_id)
1775
+ for sub_question in question.get("subQuestions", []) or []:
1776
+ sub_title = sub_question.get("queTitle")
1777
+ sub_que_id = sub_question.get("queId")
1778
+ if title and sub_title and sub_que_id:
1779
+ result[f"{title}.{sub_title}"] = sub_que_id
1780
+ result.setdefault(sub_title, sub_que_id)
1781
+ for inner_row in question.get("innerQuestions", []) or []:
1782
+ _extract_question_line(inner_row, result, title if que_type == 24 else parent_title)
1783
+
1784
+
1785
+ def _compact_dict(data: dict[str, Any]) -> dict[str, Any]:
1786
+ return {key: value for key, value in data.items() if value is not None}
1787
+
1788
+
1789
+ def _extract_workflow_node_id(result: Any, *, expected_type: int | None = None) -> int | None:
1790
+ if expected_type is not None:
1791
+ exact_match = _extract_workflow_node_id_exact(result, expected_type=expected_type)
1792
+ if exact_match is not None:
1793
+ return exact_match
1794
+ return _extract_workflow_node_id_exact(result, expected_type=None)
1795
+
1796
+
1797
+ def _extract_workflow_node_id_exact(result: Any, *, expected_type: int | None) -> int | None:
1798
+ if isinstance(result, dict):
1799
+ node_id = result.get("auditNodeId")
1800
+ node_type = result.get("type")
1801
+ if isinstance(node_id, int) and (expected_type is None or node_type == expected_type):
1802
+ return node_id
1803
+ for node in result.values():
1804
+ extracted = _extract_workflow_node_id_exact(node, expected_type=expected_type)
1805
+ if extracted is not None:
1806
+ return extracted
1807
+ if isinstance(result, list):
1808
+ for node in result:
1809
+ extracted = _extract_workflow_node_id_exact(node, expected_type=expected_type)
1810
+ if extracted is not None:
1811
+ return extracted
1812
+ return None
1813
+
1814
+
1815
+ def _extract_workflow_branch_lane_ids(result: Any) -> list[int]:
1816
+ if isinstance(result, dict):
1817
+ branches = result.get("branches")
1818
+ if isinstance(branches, list):
1819
+ lane_ids = [
1820
+ branch.get("auditNodeId")
1821
+ for branch in branches
1822
+ if isinstance(branch, dict) and isinstance(branch.get("auditNodeId"), int)
1823
+ ]
1824
+ if lane_ids:
1825
+ return lane_ids
1826
+ for node in result.values():
1827
+ lane_ids = _extract_workflow_branch_lane_ids(node)
1828
+ if lane_ids:
1829
+ return lane_ids
1830
+ if isinstance(result, list):
1831
+ for node in result:
1832
+ lane_ids = _extract_workflow_branch_lane_ids(node)
1833
+ if lane_ids:
1834
+ return lane_ids
1835
+ return []
1836
+
1837
+
1838
+ def _branch_lane_ref(branch_node_id: str, branch_index: int) -> str:
1839
+ return f"__branch_lane__{branch_node_id}__{branch_index}"
1840
+
1841
+
1842
+ def _coerce_workflow_nodes(result: Any) -> dict[int, dict[str, Any]]:
1843
+ if not isinstance(result, dict):
1844
+ return {}
1845
+ nodes: dict[int, dict[str, Any]] = {}
1846
+ for node_id, node in result.items():
1847
+ try:
1848
+ parsed_node_id = int(node_id)
1849
+ except (TypeError, ValueError):
1850
+ continue
1851
+ if isinstance(node, dict):
1852
+ nodes[parsed_node_id] = node
1853
+ return nodes
1854
+
1855
+
1856
+ def _workflow_node_is_branch(nodes: dict[int, dict[str, Any]], node_id: int | None) -> bool:
1857
+ if node_id is None:
1858
+ return False
1859
+ node = nodes.get(int(node_id))
1860
+ return isinstance(node, dict) and node.get("type") == 1
1861
+
1862
+
1863
+ def _find_branch_lane_ids(nodes: dict[int, dict[str, Any]], branch_node_id: int | None) -> list[int]:
1864
+ if branch_node_id is None:
1865
+ return []
1866
+ lane_ids = [
1867
+ node_id
1868
+ for node_id, node in nodes.items()
1869
+ if isinstance(node, dict) and node.get("type") == 2 and node.get("prevId") == int(branch_node_id)
1870
+ ]
1871
+ return sorted(lane_ids)
1872
+
1873
+
1874
+ def _find_created_branch_node_id(
1875
+ nodes: dict[int, dict[str, Any]],
1876
+ *,
1877
+ before_node_ids: set[int],
1878
+ prev_id: int | None,
1879
+ ) -> int | None:
1880
+ candidates = [
1881
+ node_id
1882
+ for node_id, node in nodes.items()
1883
+ if node_id not in before_node_ids and isinstance(node, dict) and node.get("type") == 1
1884
+ ]
1885
+ if prev_id is not None:
1886
+ prev_candidates = [node_id for node_id in candidates if nodes[node_id].get("prevId") == int(prev_id)]
1887
+ if prev_candidates:
1888
+ return prev_candidates[0]
1889
+ return candidates[0] if candidates else None
1890
+
1891
+
1892
+ def _find_created_sub_branch_lane_id(
1893
+ nodes: dict[int, dict[str, Any]],
1894
+ *,
1895
+ before_node_ids: set[int],
1896
+ branch_node_id: int | None,
1897
+ ) -> int | None:
1898
+ if branch_node_id is None:
1899
+ return None
1900
+ candidates = [
1901
+ node_id
1902
+ for node_id, node in nodes.items()
1903
+ 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)
1904
+ ]
1905
+ return candidates[0] if candidates else None
1906
+
1907
+
1908
+ def _is_navigation_plugin_unavailable(error: QingflowApiError) -> bool:
1909
+ try:
1910
+ backend_code = int(error.backend_code)
1911
+ except (TypeError, ValueError):
1912
+ backend_code = None
1913
+ if backend_code != 50004:
1914
+ return False
1915
+ message = error.message or ""
1916
+ return "导航菜单不可用" in message or "插件未安装" in message
1917
+
1918
+
1919
+ def _portal_plan_has_source_type(portal_plan: dict[str, Any], source_type: str) -> bool:
1920
+ components = ((portal_plan.get("update_payload") or {}) if isinstance(portal_plan, dict) else {}).get("components", [])
1921
+ return any(component.get("sourceType") == source_type for component in components if isinstance(component, dict))
1922
+
1923
+
1924
+ def _coerce_qingflow_error(error: Exception) -> QingflowApiError | None:
1925
+ if isinstance(error, QingflowApiError):
1926
+ return error
1927
+ if not isinstance(error, RuntimeError):
1928
+ return None
1929
+ try:
1930
+ payload = json.loads(str(error))
1931
+ except ValueError:
1932
+ return None
1933
+ if not isinstance(payload, dict):
1934
+ return None
1935
+ message = payload.get("message")
1936
+ category = payload.get("category")
1937
+ if not category or not message:
1938
+ return None
1939
+ return QingflowApiError(
1940
+ category=str(category),
1941
+ message=str(message),
1942
+ backend_code=payload.get("backend_code"),
1943
+ request_id=payload.get("request_id"),
1944
+ http_status=payload.get("http_status"),
1945
+ )
1946
+
1947
+
1948
+ def _portal_component_position(
1949
+ source_type: Any,
1950
+ *,
1951
+ pc_x: int,
1952
+ pc_y: int,
1953
+ pc_row_height: int,
1954
+ mobile_y: int,
1955
+ ) -> tuple[dict[str, Any], int, int, int, int]:
1956
+ source_name = str(source_type or "").lower()
1957
+ if source_name == "filter":
1958
+ cols = 24
1959
+ rows = 2
1960
+ elif source_name == "grid":
1961
+ cols = 24
1962
+ rows = 4
1963
+ elif source_name == "text":
1964
+ cols = 24
1965
+ rows = 2
1966
+ elif source_name == "view":
1967
+ cols = 24
1968
+ rows = 8
1969
+ elif source_name == "link":
1970
+ cols = 12
1971
+ rows = 2
1972
+ else:
1973
+ cols = 8
1974
+ rows = 4
1975
+
1976
+ if cols == 24:
1977
+ if pc_x != 0:
1978
+ pc_y += pc_row_height
1979
+ pc_x = 0
1980
+ pc_row_height = 0
1981
+ position = {
1982
+ "pc": {"cols": 24, "rows": rows, "x": 0, "y": pc_y},
1983
+ "mobile": {"cols": 6, "rows": rows, "x": 0, "y": mobile_y},
1984
+ }
1985
+ pc_y += rows
1986
+ mobile_y += rows
1987
+ return position, 0, pc_y, 0, mobile_y
1988
+
1989
+ if pc_x + cols > 24:
1990
+ pc_y += pc_row_height
1991
+ pc_x = 0
1992
+ pc_row_height = 0
1993
+
1994
+ position = {
1995
+ "pc": {"cols": cols, "rows": rows, "x": pc_x, "y": pc_y},
1996
+ "mobile": {"cols": 6, "rows": rows, "x": 0, "y": mobile_y},
1997
+ }
1998
+ pc_x += cols
1999
+ pc_row_height = max(pc_row_height, rows)
2000
+ if pc_x >= 24:
2001
+ pc_y += pc_row_height
2002
+ pc_x = 0
2003
+ pc_row_height = 0
2004
+ mobile_y += rows
2005
+ return position, pc_x, pc_y, pc_row_height, mobile_y
2006
+
2007
+
2008
+ def _portal_view_type(store: RunArtifactStore, entity_id: str, view_id: str) -> str:
2009
+ view_type_map = {
2010
+ "table": "tableView",
2011
+ "card": "cardView",
2012
+ "board": "boardView",
2013
+ "gantt": "ganttView",
2014
+ "hierarchy": "hierarchyView",
2015
+ }
2016
+ for entity in store.data.get("normalized_solution_spec", {}).get("entities", []):
2017
+ if entity["entity_id"] != entity_id:
2018
+ continue
2019
+ for view in entity.get("views", []):
2020
+ if view.get("view_id") == view_id:
2021
+ return view_type_map.get(view.get("type"), "tableView")
2022
+ return "tableView"
2023
+
2024
+
2025
+ def _build_viewgraph_questions(schema: dict[str, Any], visible_que_ids: list[int]) -> list[dict[str, Any]]:
2026
+ visible_map = {que_id: index for index, que_id in enumerate(visible_que_ids, start=1)}
2027
+ questions: list[dict[str, Any]] = []
2028
+ for base_question in schema.get("baseQues", []):
2029
+ question = _build_viewgraph_question(base_question, visible_map, default_auth=3)
2030
+ if question is not None:
2031
+ questions.append(question)
2032
+ for line in schema.get("formQues", []):
2033
+ for question in line:
2034
+ resolved = _build_viewgraph_question(question, visible_map, default_auth=1)
2035
+ if resolved is not None:
2036
+ questions.append(resolved)
2037
+ return questions
2038
+
2039
+
2040
+ def _build_viewgraph_question(question: dict[str, Any], visible_map: dict[int, int], *, default_auth: int) -> dict[str, Any] | None:
2041
+ que_id = question.get("queId")
2042
+ if que_id is None:
2043
+ return None
2044
+ sub_questions = [
2045
+ resolved
2046
+ for sub_question in question.get("subQuestions", []) or []
2047
+ if (resolved := _build_viewgraph_question(sub_question, visible_map, default_auth=1)) is not None
2048
+ ]
2049
+ inner_questions = [
2050
+ resolved
2051
+ for inner_row in question.get("innerQuestions", []) or []
2052
+ for inner_question in inner_row or []
2053
+ if (resolved := _build_viewgraph_question(inner_question, visible_map, default_auth=1)) is not None
2054
+ ]
2055
+ return {
2056
+ "queId": que_id,
2057
+ "queTitle": question.get("queTitle"),
2058
+ "queType": question.get("queType"),
2059
+ "queAuth": default_auth if que_id <= 5 else 1,
2060
+ "beingListDisplay": que_id in visible_map,
2061
+ "displayOrdinal": visible_map.get(que_id, -1),
2062
+ "subQues": sub_questions,
2063
+ "innerQues": inner_questions,
2064
+ "beingDownload": True,
2065
+ }