@josephyan/qingflow-cli 0.2.0-beta.55

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