@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.
- package/README.md +30 -0
- package/docs/local-agent-install.md +235 -0
- package/entry_point.py +13 -0
- package/npm/bin/qingflow.mjs +5 -0
- package/npm/lib/runtime.mjs +204 -0
- package/npm/scripts/postinstall.mjs +16 -0
- package/package.json +34 -0
- package/pyproject.toml +67 -0
- package/qingflow +15 -0
- package/src/qingflow_mcp/__init__.py +5 -0
- package/src/qingflow_mcp/__main__.py +5 -0
- package/src/qingflow_mcp/backend_client.py +547 -0
- package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
- package/src/qingflow_mcp/builder_facade/models.py +985 -0
- package/src/qingflow_mcp/builder_facade/service.py +8243 -0
- package/src/qingflow_mcp/cli/__init__.py +1 -0
- package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
- package/src/qingflow_mcp/cli/commands/app.py +40 -0
- package/src/qingflow_mcp/cli/commands/auth.py +78 -0
- package/src/qingflow_mcp/cli/commands/builder.py +184 -0
- package/src/qingflow_mcp/cli/commands/common.py +47 -0
- package/src/qingflow_mcp/cli/commands/imports.py +86 -0
- package/src/qingflow_mcp/cli/commands/record.py +202 -0
- package/src/qingflow_mcp/cli/commands/task.py +87 -0
- package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
- package/src/qingflow_mcp/cli/context.py +48 -0
- package/src/qingflow_mcp/cli/formatters.py +269 -0
- package/src/qingflow_mcp/cli/json_io.py +50 -0
- package/src/qingflow_mcp/cli/main.py +147 -0
- package/src/qingflow_mcp/config.py +221 -0
- package/src/qingflow_mcp/errors.py +66 -0
- package/src/qingflow_mcp/import_store.py +121 -0
- package/src/qingflow_mcp/json_types.py +18 -0
- package/src/qingflow_mcp/list_type_labels.py +76 -0
- package/src/qingflow_mcp/server.py +211 -0
- package/src/qingflow_mcp/server_app_builder.py +387 -0
- package/src/qingflow_mcp/server_app_user.py +317 -0
- package/src/qingflow_mcp/session_store.py +289 -0
- package/src/qingflow_mcp/solution/__init__.py +6 -0
- package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
- package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
- package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +466 -0
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +113 -0
- package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
- package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
- package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
- package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
- package/src/qingflow_mcp/solution/design_session.py +222 -0
- package/src/qingflow_mcp/solution/design_store.py +100 -0
- package/src/qingflow_mcp/solution/executor.py +2339 -0
- package/src/qingflow_mcp/solution/normalizer.py +23 -0
- package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
- package/src/qingflow_mcp/solution/run_store.py +244 -0
- package/src/qingflow_mcp/solution/spec_models.py +853 -0
- package/src/qingflow_mcp/tools/__init__.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +2063 -0
- package/src/qingflow_mcp/tools/app_tools.py +850 -0
- package/src/qingflow_mcp/tools/approval_tools.py +833 -0
- package/src/qingflow_mcp/tools/auth_tools.py +697 -0
- package/src/qingflow_mcp/tools/base.py +81 -0
- package/src/qingflow_mcp/tools/code_block_tools.py +679 -0
- package/src/qingflow_mcp/tools/directory_tools.py +648 -0
- package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
- package/src/qingflow_mcp/tools/file_tools.py +385 -0
- package/src/qingflow_mcp/tools/import_tools.py +1971 -0
- package/src/qingflow_mcp/tools/navigation_tools.py +177 -0
- package/src/qingflow_mcp/tools/package_tools.py +240 -0
- package/src/qingflow_mcp/tools/portal_tools.py +131 -0
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +269 -0
- package/src/qingflow_mcp/tools/record_tools.py +12739 -0
- package/src/qingflow_mcp/tools/role_tools.py +94 -0
- package/src/qingflow_mcp/tools/solution_tools.py +3887 -0
- package/src/qingflow_mcp/tools/task_context_tools.py +1423 -0
- package/src/qingflow_mcp/tools/task_tools.py +843 -0
- package/src/qingflow_mcp/tools/view_tools.py +280 -0
- package/src/qingflow_mcp/tools/workflow_tools.py +312 -0
- 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
|