@josephyan/qingflow-cli 0.2.0-beta.1000

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