@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,181 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from copy import deepcopy
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ import tempfile
9
+ from typing import Any
10
+
11
+ from ..config import get_mcp_home
12
+ from .run_store import resolve_storage_path, utc_now
13
+
14
+
15
+ def get_build_assemblies_path() -> Path:
16
+ custom_home = os.getenv("QINGFLOW_MCP_BUILD_HOME")
17
+ if custom_home:
18
+ return Path(custom_home).expanduser()
19
+ return get_mcp_home() / "build-assemblies"
20
+
21
+
22
+ def default_manifest() -> dict[str, Any]:
23
+ return {
24
+ "solution_name": "",
25
+ "summary": None,
26
+ "business_context": {},
27
+ "package": {"enabled": True, "ordinal": 1},
28
+ "entities": [],
29
+ "roles": [],
30
+ "requirements": [],
31
+ "success_metrics": [],
32
+ "portal": {"enabled": False, "sections": []},
33
+ "navigation": {"enabled": False, "items": []},
34
+ "publish_policy": {"apps": True, "portal": True, "navigation": True},
35
+ "preferences": {
36
+ "multi_app": None,
37
+ "create_package": True,
38
+ "create_portal": False,
39
+ "create_navigation": False,
40
+ "naming_style": "title",
41
+ },
42
+ "assumptions": [],
43
+ "constraints": [],
44
+ "metadata": {},
45
+ }
46
+
47
+
48
+ def default_artifacts() -> dict[str, Any]:
49
+ return {
50
+ "package": {},
51
+ "roles": {},
52
+ "apps": {},
53
+ "views": {},
54
+ "charts": {},
55
+ "records": {},
56
+ "portal": {},
57
+ "navigation": {},
58
+ "field_maps": {},
59
+ }
60
+
61
+
62
+ def default_builder_state() -> dict[str, Any]:
63
+ return {
64
+ "generated_specs": {},
65
+ "failure_signatures": {},
66
+ "last_failure_signature": None,
67
+ "last_failure_stage": None,
68
+ "last_failure_count": 0,
69
+ }
70
+
71
+
72
+ @dataclass(slots=True)
73
+ class BuildAssemblyStore:
74
+ path: Path
75
+ data: dict[str, Any]
76
+
77
+ @classmethod
78
+ def open(cls, *, build_id: str, create: bool = True) -> "BuildAssemblyStore":
79
+ base_dir = get_build_assemblies_path()
80
+ try:
81
+ base_dir.mkdir(parents=True, exist_ok=True)
82
+ except PermissionError:
83
+ base_dir = Path(tempfile.gettempdir()) / "qingflow-mcp-build-assemblies"
84
+ base_dir.mkdir(parents=True, exist_ok=True)
85
+ path = resolve_storage_path(base_dir, key=build_id, id_field="build_id")
86
+ if path.exists():
87
+ data = json.loads(path.read_text(encoding="utf-8"))
88
+ stored_build_id = data.get("build_id")
89
+ if stored_build_id != build_id:
90
+ raise ValueError(f"existing build assembly at '{path}' belongs to '{stored_build_id}', not '{build_id}'")
91
+ elif not create:
92
+ raise FileNotFoundError(build_id)
93
+ else:
94
+ data = {
95
+ "build_id": build_id,
96
+ "status": "draft",
97
+ "manifest": default_manifest(),
98
+ "stage_specs": {
99
+ "app_flow": {},
100
+ "views": {},
101
+ "analytics_portal": {},
102
+ "navigation": {},
103
+ },
104
+ "artifacts": default_artifacts(),
105
+ "builder_state": default_builder_state(),
106
+ "stage_history": [],
107
+ "created_at": utc_now(),
108
+ "updated_at": utc_now(),
109
+ }
110
+ try:
111
+ path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
112
+ except PermissionError:
113
+ base_dir = Path(tempfile.gettempdir()) / "qingflow-mcp-build-assemblies"
114
+ base_dir.mkdir(parents=True, exist_ok=True)
115
+ path = resolve_storage_path(base_dir, key=build_id, id_field="build_id")
116
+ path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
117
+ return cls(path=path, data=data)
118
+
119
+ def get_manifest(self) -> dict[str, Any]:
120
+ return deepcopy(self.data.get("manifest", default_manifest()))
121
+
122
+ def set_manifest(self, manifest: dict[str, Any]) -> None:
123
+ self.data["manifest"] = deepcopy(manifest)
124
+ self._flush()
125
+
126
+ def get_artifacts(self) -> dict[str, Any]:
127
+ return deepcopy(self.data.get("artifacts", default_artifacts()))
128
+
129
+ def set_artifacts(self, artifacts: dict[str, Any]) -> None:
130
+ self.data["artifacts"] = deepcopy(artifacts)
131
+ self._flush()
132
+
133
+ def set_stage_spec(self, stage_name: str, spec: dict[str, Any]) -> None:
134
+ self.data.setdefault("stage_specs", {})
135
+ self.data["stage_specs"][stage_name] = deepcopy(spec)
136
+ self._flush()
137
+
138
+ def add_stage_history(self, entry: dict[str, Any]) -> None:
139
+ self.data.setdefault("stage_history", []).append(deepcopy(entry))
140
+ self._flush()
141
+
142
+ def mark_status(self, status: str) -> None:
143
+ self.data["status"] = status
144
+ self._flush()
145
+
146
+ def get_builder_state(self) -> dict[str, Any]:
147
+ return deepcopy(self.data.get("builder_state", default_builder_state()))
148
+
149
+ def set_builder_state(self, builder_state: dict[str, Any]) -> None:
150
+ self.data["builder_state"] = deepcopy(builder_state)
151
+ self._flush()
152
+
153
+ def get_builder_value(self, key: str, default: Any = None) -> Any:
154
+ return deepcopy(self.data.get("builder_state", {}).get(key, default))
155
+
156
+ def set_builder_value(self, key: str, value: Any) -> None:
157
+ self.data.setdefault("builder_state", default_builder_state())
158
+ self.data["builder_state"][key] = deepcopy(value)
159
+ self._flush()
160
+
161
+ def summary(self) -> dict[str, Any]:
162
+ return {
163
+ "build_id": self.data["build_id"],
164
+ "status": self.data["status"],
165
+ "manifest": deepcopy(self.data.get("manifest", {})),
166
+ "artifacts": deepcopy(self.data.get("artifacts", {})),
167
+ "builder_state": deepcopy(self.data.get("builder_state", {})),
168
+ "stage_specs": deepcopy(self.data.get("stage_specs", {})),
169
+ "stage_history": deepcopy(self.data.get("stage_history", [])),
170
+ "build_path": str(self.path),
171
+ }
172
+
173
+ def _flush(self) -> None:
174
+ self.data["updated_at"] = utc_now()
175
+ try:
176
+ self.path.write_text(json.dumps(self.data, ensure_ascii=False, indent=2), encoding="utf-8")
177
+ except PermissionError:
178
+ fallback_dir = Path(tempfile.gettempdir()) / "qingflow-mcp-build-assemblies"
179
+ fallback_dir.mkdir(parents=True, exist_ok=True)
180
+ self.path = resolve_storage_path(fallback_dir, key=str(self.data.get("build_id") or "build"), id_field="build_id")
181
+ self.path.write_text(json.dumps(self.data, ensure_ascii=False, indent=2), encoding="utf-8")
@@ -0,0 +1,282 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+ from ..spec_models import EntitySpec, PortalSourceType, RoleSpec, SolutionSpec
7
+ from .chart_compiler import compile_charts
8
+ from .form_compiler import compile_entity_form
9
+ from .navigation_compiler import compile_navigation
10
+ from .package_compiler import compile_package
11
+ from .portal_compiler import compile_portal
12
+ from .view_compiler import compile_views
13
+ from .workflow_compiler import compile_workflow
14
+
15
+
16
+ @dataclass(slots=True)
17
+ class ExecutionStep:
18
+ step_name: str
19
+ resource_type: str
20
+ resource_ref: str
21
+ description: str
22
+ depends_on: list[str] = field(default_factory=list)
23
+
24
+
25
+ @dataclass(slots=True)
26
+ class ExecutionPlan:
27
+ steps: list[ExecutionStep]
28
+
29
+ def as_dict(self) -> dict[str, Any]:
30
+ return {
31
+ "steps": [
32
+ {
33
+ "step_name": step.step_name,
34
+ "resource_type": step.resource_type,
35
+ "resource_ref": step.resource_ref,
36
+ "description": step.description,
37
+ "depends_on": step.depends_on,
38
+ }
39
+ for step in self.steps
40
+ ]
41
+ }
42
+
43
+
44
+ @dataclass(slots=True)
45
+ class CompiledRole:
46
+ role_id: str
47
+ name: str
48
+ payload: dict[str, Any]
49
+
50
+
51
+ @dataclass(slots=True)
52
+ class CompiledEntity:
53
+ entity_id: str
54
+ display_name: str
55
+ app_create_payload: dict[str, Any]
56
+ form_base_payload: dict[str, Any]
57
+ form_relation_payload: dict[str, Any] | None
58
+ field_specs: dict[str, dict[str, Any]]
59
+ field_labels: dict[str, str]
60
+ workflow_plan: dict[str, Any] | None
61
+ view_plans: list[dict[str, Any]]
62
+ chart_plans: list[dict[str, Any]]
63
+ sample_records: list[dict[str, Any]]
64
+
65
+
66
+ @dataclass(slots=True)
67
+ class CompiledSolution:
68
+ normalized_spec: SolutionSpec
69
+ package_payload: dict[str, Any] | None
70
+ roles: list[CompiledRole]
71
+ entities: list[CompiledEntity]
72
+ portal_plan: dict[str, Any] | None
73
+ navigation_plan: list[dict[str, Any]]
74
+ execution_plan: ExecutionPlan
75
+
76
+ def as_dict(self) -> dict[str, Any]:
77
+ return {
78
+ "normalized_solution_spec": self.normalized_spec.model_dump(mode="json"),
79
+ "execution_plan": self.execution_plan.as_dict(),
80
+ }
81
+
82
+
83
+ def compile_solution(spec: SolutionSpec) -> CompiledSolution:
84
+ package_payload = compile_package(spec)
85
+ roles = [compile_role(role) for role in spec.roles]
86
+ entities = [compile_entity(entity, include_package=package_payload is not None) for entity in spec.entities]
87
+ _apply_portal_chart_semantics(spec, entities)
88
+ portal_plan = compile_portal(spec)
89
+ navigation_plan = compile_navigation(spec)
90
+ execution_plan = build_execution_plan(spec, package_payload is not None)
91
+ return CompiledSolution(
92
+ normalized_spec=spec,
93
+ package_payload=package_payload,
94
+ roles=roles,
95
+ entities=entities,
96
+ portal_plan=portal_plan,
97
+ navigation_plan=navigation_plan,
98
+ execution_plan=execution_plan,
99
+ )
100
+
101
+
102
+ def compile_entity(entity: EntitySpec, *, include_package: bool) -> CompiledEntity:
103
+ app_create_payload, form_base_payload, form_relation_payload, field_specs, field_labels = compile_entity_form(entity, include_package=include_package)
104
+ workflow_plan = compile_workflow(entity)
105
+ view_plans = compile_views(entity)
106
+ chart_plans = compile_charts(entity)
107
+ return CompiledEntity(
108
+ entity_id=entity.entity_id,
109
+ display_name=entity.display_name,
110
+ app_create_payload=app_create_payload,
111
+ form_base_payload=form_base_payload,
112
+ form_relation_payload=form_relation_payload,
113
+ field_specs=field_specs,
114
+ field_labels=field_labels,
115
+ workflow_plan=workflow_plan,
116
+ view_plans=view_plans,
117
+ chart_plans=chart_plans,
118
+ sample_records=[record.model_dump(mode="json") for record in entity.sample_records],
119
+ )
120
+
121
+
122
+ def compile_role(role: RoleSpec) -> CompiledRole:
123
+ payload = {
124
+ "roleName": role.name,
125
+ "roleIcon": role.icon or role.config.get("roleIcon") or "ex-user-outlined",
126
+ "users": list(role.users),
127
+ }
128
+ return CompiledRole(role_id=role.role_id, name=role.name, payload=payload)
129
+
130
+
131
+ def build_execution_plan(
132
+ spec: SolutionSpec,
133
+ include_package: bool,
134
+ attach_package: bool | None = None,
135
+ ) -> ExecutionPlan:
136
+ steps: list[ExecutionStep] = []
137
+ attach_package = include_package if attach_package is None else attach_package
138
+ if include_package:
139
+ steps.append(ExecutionStep("package.create", "package", spec.package.name or spec.solution_name, "创建应用包"))
140
+ for role in spec.roles:
141
+ dependencies = ["package.create"] if include_package else []
142
+ steps.append(ExecutionStep(f"role.create.{role.role_id}", "role", role.role_id, f"创建角色 {role.name}", dependencies))
143
+ for entity in spec.entities:
144
+ entity_ref = entity.entity_id
145
+ dependencies = ["package.create"] if include_package else []
146
+ steps.append(ExecutionStep(f"app.create.{entity_ref}", "app", entity_ref, f"创建应用 {entity.display_name}", dependencies.copy()))
147
+ last_form_step = f"app.create.{entity_ref}"
148
+ if attach_package:
149
+ steps.append(
150
+ ExecutionStep(
151
+ f"package.attach.{entity_ref}",
152
+ "package_attach",
153
+ entity_ref,
154
+ f"将应用加入应用包 {entity.display_name}",
155
+ [f"app.create.{entity_ref}"],
156
+ )
157
+ )
158
+ last_form_step = f"package.attach.{entity_ref}"
159
+ steps.append(ExecutionStep(f"form.base.{entity_ref}", "form", entity_ref, f"写入基础表单 {entity.display_name}", [last_form_step]))
160
+ last_form_step = f"form.base.{entity_ref}"
161
+ if any(field.type.value == "relation" for field in entity.fields):
162
+ steps.append(ExecutionStep(f"form.relations.{entity_ref}", "form", entity_ref, f"回填关联字段 {entity.display_name}", [f"form.base.{entity_ref}"]))
163
+ last_form_step = f"form.relations.{entity_ref}"
164
+ steps.append(ExecutionStep(f"publish.form.{entity_ref}", "app_publish", entity_ref, f"发布表单 {entity.display_name}", [last_form_step]))
165
+
166
+ last_app_step = f"publish.form.{entity_ref}"
167
+ if entity.workflow and entity.workflow.enabled:
168
+ workflow_dependencies = [last_app_step]
169
+ workflow_dependencies.extend(f"role.create.{role_ref}" for role_ref in _collect_workflow_role_refs(entity))
170
+ steps.append(ExecutionStep(f"workflow.{entity_ref}", "workflow", entity_ref, f"搭建流程 {entity.display_name}", workflow_dependencies))
171
+ steps.append(ExecutionStep(f"publish.workflow.{entity_ref}", "app_publish", entity_ref, f"发布流程 {entity.display_name}", [f"workflow.{entity_ref}"]))
172
+ last_app_step = f"publish.workflow.{entity_ref}"
173
+ publish_app_dependencies: list[str] = []
174
+ if entity.views:
175
+ steps.append(ExecutionStep(f"views.{entity_ref}", "view", entity_ref, f"搭建视图 {entity.display_name}", [last_app_step]))
176
+ publish_app_dependencies.append(f"views.{entity_ref}")
177
+ if entity.charts:
178
+ steps.append(ExecutionStep(f"charts.{entity_ref}", "chart", entity_ref, f"搭建报表 {entity.display_name}", [last_app_step]))
179
+ publish_app_dependencies.append(f"charts.{entity_ref}")
180
+ if not publish_app_dependencies:
181
+ publish_app_dependencies.append(last_app_step)
182
+ steps.append(ExecutionStep(f"publish.app.{entity_ref}", "app_publish", entity_ref, f"发布应用 {entity.display_name}", publish_app_dependencies))
183
+ if entity.sample_records:
184
+ seed_dependencies = [f"publish.app.{entity_ref}"]
185
+ seed_dependencies.extend(f"seed_data.{target_entity_id}" for target_entity_id in _collect_seed_dependencies(entity) if target_entity_id != entity.entity_id)
186
+ steps.append(ExecutionStep(f"seed_data.{entity_ref}", "record", entity_ref, f"写入模拟数据 {entity.display_name}", seed_dependencies))
187
+ if _should_create_portal(spec):
188
+ deps = [_entity_terminal_step(entity) for entity in spec.entities]
189
+ steps.append(ExecutionStep("portal.create", "portal", spec.portal.name or f"{spec.solution_name} 首页", "创建门户", deps))
190
+ steps.append(ExecutionStep("publish.portal", "portal_publish", spec.portal.name or f"{spec.solution_name} 首页", "发布门户", ["portal.create"]))
191
+ if _should_create_navigation(spec):
192
+ deps = [_entity_terminal_step(entity) for entity in spec.entities]
193
+ if _should_create_portal(spec):
194
+ deps.append("publish.portal")
195
+ steps.append(ExecutionStep("navigation.create", "navigation", spec.solution_name, "创建导航", deps))
196
+ steps.append(ExecutionStep("publish.navigation", "navigation_publish", spec.solution_name, "发布导航", ["navigation.create"]))
197
+ return ExecutionPlan(steps=steps)
198
+
199
+
200
+ def _should_create_portal(spec: SolutionSpec) -> bool:
201
+ return spec.preferences.create_portal and spec.portal.enabled and bool(spec.portal.sections)
202
+
203
+
204
+ def _should_create_navigation(spec: SolutionSpec) -> bool:
205
+ return spec.preferences.create_navigation and spec.navigation.enabled and bool(spec.navigation.items)
206
+
207
+
208
+ def _collect_workflow_role_refs(entity: EntitySpec) -> list[str]:
209
+ if entity.workflow is None:
210
+ return []
211
+ role_refs: list[str] = []
212
+ for node in entity.workflow.nodes:
213
+ role_refs.extend(node.assignees.get("role_refs", []))
214
+ return list(dict.fromkeys(role_refs))
215
+
216
+
217
+ def _collect_seed_dependencies(entity: EntitySpec) -> list[str]:
218
+ target_entity_ids: list[str] = []
219
+ reference_fields = {field.field_id: field.target_entity_id for field in entity.fields if field.target_entity_id}
220
+ for record in entity.sample_records:
221
+ for field_id, value in record.values.items():
222
+ target_entity_id = reference_fields.get(field_id)
223
+ if not target_entity_id:
224
+ continue
225
+ if value is None:
226
+ continue
227
+ target_entity_ids.append(target_entity_id)
228
+ return list(dict.fromkeys(target_entity_ids))
229
+
230
+
231
+ def _entity_terminal_step(entity: EntitySpec) -> str:
232
+ return f"seed_data.{entity.entity_id}" if entity.sample_records else f"publish.app.{entity.entity_id}"
233
+
234
+
235
+ def _apply_portal_chart_semantics(spec: SolutionSpec, entities: list[CompiledEntity]) -> None:
236
+ if not _should_create_portal(spec):
237
+ return
238
+ portal_chart_refs = _collect_portal_chart_refs(spec)
239
+ if not portal_chart_refs:
240
+ return
241
+ for entity in entities:
242
+ for chart_plan in entity.chart_plans:
243
+ if (entity.entity_id, chart_plan["chart_id"]) not in portal_chart_refs:
244
+ continue
245
+ if chart_plan.get("preserve_portal_query_conditions"):
246
+ continue
247
+ chart_plan["config_payload"]["beforeAggregationFilterMatrix"] = []
248
+ chart_plan["config_payload"]["afterAggregationFilterMatrix"] = []
249
+ chart_plan["config_payload"]["queryConditionFieldIds"] = []
250
+ chart_plan["config_payload"]["queryConditionStatus"] = False
251
+ chart_plan["config_payload"]["queryConditionExact"] = False
252
+
253
+
254
+ def _collect_portal_chart_refs(spec: SolutionSpec) -> set[tuple[str, str]]:
255
+ refs: set[tuple[str, str]] = set()
256
+ for section in spec.portal.sections:
257
+ if section.source_type == PortalSourceType.chart and section.entity_id and section.chart_id:
258
+ refs.add((section.entity_id, section.chart_id))
259
+ continue
260
+ if section.source_type != PortalSourceType.filter:
261
+ continue
262
+ filter_config = section.config.get("filterConfig", [])
263
+ graph_list = section.config.get("graphList", [])
264
+ if isinstance(filter_config, dict):
265
+ graph_list = filter_config.get("graphList", graph_list)
266
+ filter_config = filter_config.get("filterConfig", [])
267
+ refs.update(_collect_chart_refs_from_graphs(graph_list))
268
+ for group in filter_config or []:
269
+ for item in group.get("filterGroupConfig", []) or []:
270
+ refs.update(_collect_chart_refs_from_graphs(item.get("filterCondition", []) or []))
271
+ return refs
272
+
273
+
274
+ def _collect_chart_refs_from_graphs(items: list[dict[str, Any]]) -> set[tuple[str, str]]:
275
+ refs: set[tuple[str, str]] = set()
276
+ for item in items:
277
+ graph_ref = item.get("graphRef") if isinstance(item.get("graphRef"), dict) else {}
278
+ entity_id = item.get("entity_id") or graph_ref.get("entity_id")
279
+ chart_id = item.get("chart_id") or graph_ref.get("chart_id")
280
+ if entity_id and chart_id:
281
+ refs.add((str(entity_id), str(chart_id)))
282
+ return refs
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ..spec_models import ChartType, EntitySpec
6
+
7
+
8
+ QINGBI_CHART_TYPE_MAP = {
9
+ ChartType.bar: "bar",
10
+ ChartType.line: "line",
11
+ ChartType.pie: "pie",
12
+ ChartType.summary: "indicator",
13
+ ChartType.data: "detail",
14
+ ChartType.target: "indicator",
15
+ ChartType.funnel: "funnel",
16
+ }
17
+
18
+
19
+ def qingbi_workspace_visible_auth() -> dict[str, Any]:
20
+ return {
21
+ "type": "ws",
22
+ "contactAuth": {
23
+ "type": "all",
24
+ "authMembers": {
25
+ "member": [],
26
+ "depart": [],
27
+ "role": [],
28
+ "includeSubDeparts": True,
29
+ },
30
+ },
31
+ "externalMemberAuth": {
32
+ "type": "not",
33
+ "authMembers": {
34
+ "member": [],
35
+ "depart": [],
36
+ "role": [],
37
+ },
38
+ },
39
+ }
40
+
41
+
42
+ def compile_charts(entity: EntitySpec) -> list[dict[str, Any]]:
43
+ plans: list[dict[str, Any]] = []
44
+ field_labels = {field.field_id: field.label for field in entity.fields}
45
+ for ordinal, chart in enumerate(entity.charts, start=1):
46
+ config = dict(chart.config)
47
+ aggregate = config.pop("aggregate", "count")
48
+ after_filters = list(config.pop("after_aggregation_filters", []))
49
+ query_condition_field_ids = list(config.pop("query_condition_field_ids", []))
50
+ query_condition_status = bool(config.pop("query_condition_status", bool(query_condition_field_ids)))
51
+ query_condition_exact = bool(config.pop("query_condition_exact", False))
52
+ plans.append(
53
+ {
54
+ "chart_id": chart.chart_id,
55
+ "name": chart.name,
56
+ "chart_type": QINGBI_CHART_TYPE_MAP[chart.chart_type],
57
+ "preserve_portal_query_conditions": bool(chart.config.get("preserve_portal_query_conditions", False)),
58
+ "create_payload": {
59
+ "chartId": "__BI_CHART_ID__",
60
+ "chartName": chart.name,
61
+ "chartType": QINGBI_CHART_TYPE_MAP[chart.chart_type],
62
+ "dataSourceType": "qingflow",
63
+ "tagId": "0",
64
+ "parentId": "0",
65
+ "dataSourceId": "__APP_KEY__",
66
+ "visibleAuth": qingbi_workspace_visible_auth(),
67
+ "editAuthList": [],
68
+ "editAuthType": "ws",
69
+ "editAuthIncludeSubDept": True,
70
+ },
71
+ "config_payload": {
72
+ "chartName": chart.name,
73
+ "chartType": QINGBI_CHART_TYPE_MAP[chart.chart_type],
74
+ "selectedDimensionFieldIds": list(chart.dimension_field_ids),
75
+ "selectedMetricFieldIds": list(chart.indicator_field_ids),
76
+ "aggregate": aggregate,
77
+ "beforeAggregationFilterMatrix": list(chart.filters),
78
+ "afterAggregationFilterMatrix": after_filters,
79
+ "queryConditionFieldIds": query_condition_field_ids,
80
+ "queryConditionStatus": query_condition_status,
81
+ "queryConditionExact": query_condition_exact,
82
+ "displayLimitConfig": config.pop("displayLimitConfig", {"status": 1, "type": "asc", "limit": 20}),
83
+ "rawDataConfigDTO": config.pop(
84
+ "rawDataConfigDTO",
85
+ {
86
+ "beingOpen": False,
87
+ "authInfo": qingbi_workspace_visible_auth(),
88
+ "fieldInfoList": [],
89
+ },
90
+ ),
91
+ **config,
92
+ },
93
+ "ordinal": ordinal,
94
+ }
95
+ )
96
+ return plans