@josephyan/qingflow-app-user-mcp 1.1.26 → 1.1.28
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 +4 -6
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-user/SKILL.md +5 -3
- package/skills/qingflow-app-user/references/public-surface-sync.md +4 -2
- package/src/qingflow_mcp/builder_facade/models.py +167 -71
- package/src/qingflow_mcp/builder_facade/service.py +617 -155
- package/src/qingflow_mcp/cli/commands/builder.py +3 -1
- package/src/qingflow_mcp/cli/main.py +8 -3
- package/src/qingflow_mcp/server_app_builder.py +9 -8
- package/src/qingflow_mcp/tools/ai_builder_tools.py +231 -180
- package/src/qingflow_mcp/version.py +2 -0
- package/skills/qingflow-app-builder/SKILL.md +0 -277
- package/skills/qingflow-app-builder/agents/openai.yaml +0 -4
- package/skills/qingflow-app-builder/references/build-complete-system.md +0 -428
- package/skills/qingflow-app-builder/references/build-single-app.md +0 -530
- package/skills/qingflow-app-builder/references/create-app.md +0 -149
- package/skills/qingflow-app-builder/references/environments.md +0 -63
- package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +0 -123
- package/skills/qingflow-app-builder/references/gotchas.md +0 -108
- package/skills/qingflow-app-builder/references/match-rules.md +0 -114
- package/skills/qingflow-app-builder/references/public-surface-sync.md +0 -75
- package/skills/qingflow-app-builder/references/solution-playbooks.md +0 -52
- package/skills/qingflow-app-builder/references/tool-selection.md +0 -107
- package/skills/qingflow-app-builder/references/update-flow.md +0 -186
- package/skills/qingflow-app-builder/references/update-layout.md +0 -68
- package/skills/qingflow-app-builder/references/update-schema.md +0 -72
- package/skills/qingflow-app-builder/references/update-views.md +0 -291
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +0 -137
- package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +0 -4
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +0 -66
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +0 -77
- package/skills/qingflow-workflow-builder/SKILL.md +0 -96
- package/skills/qingflow-workflow-builder/manifest.yaml +0 -8
- package/skills/qingflow-workflow-builder/references/01-overview.md +0 -45
- package/skills/qingflow-workflow-builder/references/02-update-mode.md +0 -53
- package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +0 -57
- package/skills/qingflow-workflow-builder/references/04-stage1-business-modeling.md +0 -131
- package/skills/qingflow-workflow-builder/references/05-stage2-members-roles.md +0 -29
- package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +0 -165
- package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +0 -33
- package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +0 -51
- package/skills/qingflow-workflow-builder/references/09-stage6-summary.md +0 -88
- package/skills/qingflow-workflow-builder/references/10-node-config-reference.md +0 -93
- package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +0 -15
- package/skills/qingflow-workflow-builder/scripts/diff_flow_spec.py +0 -275
- package/skills/qingflow-workflow-builder/scripts/validate_flow_spec.py +0 -605
|
@@ -11,7 +11,7 @@ import re
|
|
|
11
11
|
import string
|
|
12
12
|
import time
|
|
13
13
|
import tempfile
|
|
14
|
-
from typing import Any, cast
|
|
14
|
+
from typing import Any, NoReturn, cast
|
|
15
15
|
from urllib.parse import quote_plus, unquote_plus
|
|
16
16
|
from uuid import uuid4
|
|
17
17
|
|
|
@@ -49,6 +49,7 @@ from .models import (
|
|
|
49
49
|
AssociatedResourceUpsertPatch,
|
|
50
50
|
AssociatedResourceViewConfigPatch,
|
|
51
51
|
ChartApplyRequest,
|
|
52
|
+
ChartMetricPatch,
|
|
52
53
|
ChartPartialPatch,
|
|
53
54
|
ChartUpsertPatch,
|
|
54
55
|
CustomButtonsApplyRequest,
|
|
@@ -156,6 +157,7 @@ MATCH_TYPE_ACCURACY = 1
|
|
|
156
157
|
MATCH_TYPE_QUESTION = 2
|
|
157
158
|
JUDGE_EQUAL = 0
|
|
158
159
|
JUDGE_UNEQUAL = 1
|
|
160
|
+
JUDGE_INCLUDE = 2
|
|
159
161
|
JUDGE_GREATER_OR_EQUAL = 5
|
|
160
162
|
JUDGE_LESS_OR_EQUAL = 7
|
|
161
163
|
JUDGE_EQUAL_ANY = 9
|
|
@@ -5238,6 +5240,20 @@ class AiBuilderFacade:
|
|
|
5238
5240
|
result["message"] = "read app chart config"
|
|
5239
5241
|
return result
|
|
5240
5242
|
|
|
5243
|
+
def _chart_filter_field_names_by_id(
|
|
5244
|
+
self,
|
|
5245
|
+
*,
|
|
5246
|
+
profile: str,
|
|
5247
|
+
app_key: str,
|
|
5248
|
+
) -> tuple[dict[str, str], dict[str, Any] | None]:
|
|
5249
|
+
try:
|
|
5250
|
+
state = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
5251
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
5252
|
+
api_error = _coerce_api_error(error)
|
|
5253
|
+
return {}, _transport_error_payload(api_error)
|
|
5254
|
+
fields = cast(list[dict[str, Any]], state["parsed"]["fields"])
|
|
5255
|
+
return _chart_field_names_by_id_from_public_fields(app_key=app_key, fields=fields), None
|
|
5256
|
+
|
|
5241
5257
|
def app_get_buttons(self, *, profile: str, app_key: str) -> JSONObject:
|
|
5242
5258
|
"""Read custom buttons for a single app."""
|
|
5243
5259
|
self.apps._require_app_key(app_key)
|
|
@@ -5796,6 +5812,8 @@ class AiBuilderFacade:
|
|
|
5796
5812
|
)
|
|
5797
5813
|
charts = _summarize_charts(items)
|
|
5798
5814
|
chart_visibility_read_errors: list[dict[str, Any]] = []
|
|
5815
|
+
chart_config_read_errors: list[dict[str, Any]] = []
|
|
5816
|
+
field_name_by_id, field_name_read_error = self._chart_filter_field_names_by_id(profile=profile, app_key=resolved_app_key)
|
|
5799
5817
|
for chart in charts:
|
|
5800
5818
|
chart_id = str(chart.get("chart_id") or "").strip()
|
|
5801
5819
|
if not chart_id:
|
|
@@ -5818,6 +5836,24 @@ class AiBuilderFacade:
|
|
|
5818
5836
|
base_info.get("visibleAuth") if isinstance(base_info, dict) else None
|
|
5819
5837
|
)
|
|
5820
5838
|
)
|
|
5839
|
+
try:
|
|
5840
|
+
config_response = self.charts.qingbi_report_get_config(profile=profile, chart_id=chart_id)
|
|
5841
|
+
config = config_response.get("result") or {}
|
|
5842
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
5843
|
+
api_error = _coerce_api_error(error)
|
|
5844
|
+
chart_config_read_errors.append(
|
|
5845
|
+
{
|
|
5846
|
+
"chart_id": chart_id,
|
|
5847
|
+
"request_id": api_error.request_id,
|
|
5848
|
+
"http_status": api_error.http_status,
|
|
5849
|
+
"backend_code": api_error.backend_code,
|
|
5850
|
+
}
|
|
5851
|
+
)
|
|
5852
|
+
continue
|
|
5853
|
+
if isinstance(config, dict):
|
|
5854
|
+
chart["group_by"] = _public_chart_group_by_from_qingbi_config(config)
|
|
5855
|
+
chart["metrics"] = _public_chart_metrics_from_qingbi_config(config)
|
|
5856
|
+
chart["filters"] = _public_chart_filter_groups_from_qingbi_config(config, field_name_by_id=field_name_by_id)
|
|
5821
5857
|
response = AppChartsReadResponse(
|
|
5822
5858
|
app_key=resolved_app_key,
|
|
5823
5859
|
charts=charts,
|
|
@@ -5831,7 +5867,11 @@ class AiBuilderFacade:
|
|
|
5831
5867
|
"normalized_args": {"app_key": resolved_app_key},
|
|
5832
5868
|
"missing_fields": [],
|
|
5833
5869
|
"allowed_values": {},
|
|
5834
|
-
"details": {
|
|
5870
|
+
"details": {
|
|
5871
|
+
**({"chart_visibility_read_errors": chart_visibility_read_errors} if chart_visibility_read_errors else {}),
|
|
5872
|
+
**({"chart_config_read_errors": chart_config_read_errors} if chart_config_read_errors else {}),
|
|
5873
|
+
**({"chart_filter_field_name_read_error": field_name_read_error} if field_name_read_error else {}),
|
|
5874
|
+
},
|
|
5835
5875
|
"request_id": None,
|
|
5836
5876
|
"suggested_next_call": None,
|
|
5837
5877
|
"noop": False,
|
|
@@ -5842,12 +5882,24 @@ class AiBuilderFacade:
|
|
|
5842
5882
|
if chart_visibility_read_errors
|
|
5843
5883
|
else []
|
|
5844
5884
|
)
|
|
5885
|
+
+ (
|
|
5886
|
+
[_warning("CHART_CONFIG_READ_PARTIAL", "some chart configs could not be read back; metrics/group_by/filters are incomplete for those charts")]
|
|
5887
|
+
if chart_config_read_errors
|
|
5888
|
+
else []
|
|
5889
|
+
)
|
|
5890
|
+
+ (
|
|
5891
|
+
[_warning("CHART_FILTER_FIELD_NAMES_UNRESOLVED", "chart configs were read, but form fields could not be loaded to resolve filter field names")]
|
|
5892
|
+
if field_name_read_error
|
|
5893
|
+
else []
|
|
5894
|
+
)
|
|
5845
5895
|
),
|
|
5846
5896
|
"verification": {
|
|
5847
5897
|
"app_exists": True,
|
|
5848
5898
|
"chart_order_verified": list_source == "sorted",
|
|
5849
5899
|
"chart_list_source": list_source,
|
|
5850
5900
|
"chart_visibility_readback_complete": not chart_visibility_read_errors,
|
|
5901
|
+
"chart_config_readback_complete": not chart_config_read_errors,
|
|
5902
|
+
"chart_filter_field_names_resolved": field_name_read_error is None,
|
|
5851
5903
|
},
|
|
5852
5904
|
"verified": True,
|
|
5853
5905
|
**response.model_dump(mode="json"),
|
|
@@ -6877,10 +6929,27 @@ class AiBuilderFacade:
|
|
|
6877
6929
|
suggested_next_call={"tool_name": "chart_get", "arguments": {"profile": profile, "chart_id": chart_id}},
|
|
6878
6930
|
)
|
|
6879
6931
|
|
|
6932
|
+
field_name_by_id: dict[str, str] = {}
|
|
6933
|
+
data_source = config.get("dataSource") if isinstance(config.get("dataSource"), dict) else {}
|
|
6934
|
+
data_source_app_key = str(data_source.get("dataSourceId") or config.get("dataSourceId") or "").strip()
|
|
6935
|
+
if data_source_app_key:
|
|
6936
|
+
field_name_by_id, field_name_error = self._chart_filter_field_names_by_id(profile=profile, app_key=data_source_app_key)
|
|
6937
|
+
if field_name_error:
|
|
6938
|
+
warnings.append(
|
|
6939
|
+
_warning(
|
|
6940
|
+
"CHART_FILTER_FIELD_NAMES_UNRESOLVED",
|
|
6941
|
+
"chart config was read, but form fields could not be loaded to resolve filter field names",
|
|
6942
|
+
**field_name_error,
|
|
6943
|
+
)
|
|
6944
|
+
)
|
|
6945
|
+
|
|
6880
6946
|
response = ChartGetResponse(
|
|
6881
6947
|
chart_id=chart_id,
|
|
6882
6948
|
base=deepcopy(base) if isinstance(base, dict) else {},
|
|
6883
6949
|
visibility=_public_visibility_from_chart_visible_auth(base.get("visibleAuth")),
|
|
6950
|
+
filters=_public_chart_filter_groups_from_qingbi_config(config, field_name_by_id=field_name_by_id) if isinstance(config, dict) else [],
|
|
6951
|
+
group_by=_public_chart_group_by_from_qingbi_config(config) if isinstance(config, dict) else [],
|
|
6952
|
+
metrics=_public_chart_metrics_from_qingbi_config(config) if isinstance(config, dict) else [],
|
|
6884
6953
|
config=deepcopy(config) if isinstance(config, dict) else {},
|
|
6885
6954
|
)
|
|
6886
6955
|
return {
|
|
@@ -7380,6 +7449,8 @@ class AiBuilderFacade:
|
|
|
7380
7449
|
add_fields: list[FieldPatch],
|
|
7381
7450
|
update_fields: list[FieldUpdatePatch],
|
|
7382
7451
|
remove_fields: list[FieldRemovePatch],
|
|
7452
|
+
layout_mode: LayoutApplyMode = LayoutApplyMode.merge,
|
|
7453
|
+
layout_sections: list[LayoutSectionPatch] | None = None,
|
|
7383
7454
|
) -> JSONObject:
|
|
7384
7455
|
normalized_args = {
|
|
7385
7456
|
"app_key": app_key,
|
|
@@ -7394,6 +7465,10 @@ class AiBuilderFacade:
|
|
|
7394
7465
|
"update_fields": [patch.model_dump(mode="json") for patch in update_fields],
|
|
7395
7466
|
"remove_fields": [patch.model_dump(mode="json") for patch in remove_fields],
|
|
7396
7467
|
}
|
|
7468
|
+
layout_requested = layout_sections is not None
|
|
7469
|
+
requested_layout_sections = [section.model_dump(mode="json", exclude_none=True) for section in (layout_sections or [])]
|
|
7470
|
+
if layout_requested:
|
|
7471
|
+
normalized_args["form"] = {"mode": layout_mode.value, "sections": deepcopy(requested_layout_sections)}
|
|
7397
7472
|
try:
|
|
7398
7473
|
desired_auth = (
|
|
7399
7474
|
self._compile_visibility_to_member_auth(profile=profile, visibility=visibility)
|
|
@@ -7409,7 +7484,7 @@ class AiBuilderFacade:
|
|
|
7409
7484
|
suggested_next_call=None,
|
|
7410
7485
|
)
|
|
7411
7486
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
7412
|
-
requested_field_changes = bool(add_fields or update_fields or remove_fields)
|
|
7487
|
+
requested_field_changes = bool(add_fields or update_fields or remove_fields or layout_requested)
|
|
7413
7488
|
resolved: JSONObject
|
|
7414
7489
|
if app_key:
|
|
7415
7490
|
resolved = self.app_resolve(profile=profile, app_key=app_key)
|
|
@@ -7533,7 +7608,6 @@ class AiBuilderFacade:
|
|
|
7533
7608
|
"verification": {
|
|
7534
7609
|
"fields_verified": True,
|
|
7535
7610
|
"package_attached": None if package_tag_id is None else package_tag_id in target.tag_ids,
|
|
7536
|
-
"relation_field_limit_verified": True,
|
|
7537
7611
|
"app_visuals_verified": True,
|
|
7538
7612
|
"app_base_verified": True,
|
|
7539
7613
|
"publish_skipped": True,
|
|
@@ -7573,6 +7647,9 @@ class AiBuilderFacade:
|
|
|
7573
7647
|
current_fields = parsed["fields"]
|
|
7574
7648
|
original_fields = deepcopy(current_fields)
|
|
7575
7649
|
layout = parsed["layout"]
|
|
7650
|
+
original_layout = deepcopy(layout)
|
|
7651
|
+
layout_write_requested = False
|
|
7652
|
+
applied_layout = deepcopy(layout)
|
|
7576
7653
|
existing_index = {field["field_id"]: index for index, field in enumerate(current_fields)}
|
|
7577
7654
|
selector_map = _build_selector_map(current_fields)
|
|
7578
7655
|
added: list[str] = []
|
|
@@ -7727,6 +7804,71 @@ class AiBuilderFacade:
|
|
|
7727
7804
|
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
7728
7805
|
)
|
|
7729
7806
|
|
|
7807
|
+
if layout_requested:
|
|
7808
|
+
resolved_sections, missing_selectors = _resolve_layout_sections_to_names(requested_layout_sections, current_fields)
|
|
7809
|
+
normalized_args["form"] = {"mode": layout_mode.value, "sections": deepcopy(resolved_sections)}
|
|
7810
|
+
if missing_selectors:
|
|
7811
|
+
return _failed(
|
|
7812
|
+
"UNKNOWN_LAYOUT_FIELD",
|
|
7813
|
+
"form layout references unknown field selectors",
|
|
7814
|
+
normalized_args=normalized_args,
|
|
7815
|
+
details={"unknown_selectors": missing_selectors, "app_key": target.app_key},
|
|
7816
|
+
missing_fields=[str(item) for item in missing_selectors],
|
|
7817
|
+
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
7818
|
+
)
|
|
7819
|
+
fields_by_name = {field["name"]: field for field in current_fields}
|
|
7820
|
+
seen_layout_fields: list[str] = []
|
|
7821
|
+
for section in resolved_sections:
|
|
7822
|
+
for row in section.get("rows", []):
|
|
7823
|
+
for field_name in row:
|
|
7824
|
+
if field_name not in fields_by_name:
|
|
7825
|
+
return _failed(
|
|
7826
|
+
"UNKNOWN_LAYOUT_FIELD",
|
|
7827
|
+
f"form layout references unknown field '{field_name}'",
|
|
7828
|
+
normalized_args=normalized_args,
|
|
7829
|
+
details={"field_name": field_name, "app_key": target.app_key},
|
|
7830
|
+
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
7831
|
+
)
|
|
7832
|
+
if field_name in seen_layout_fields:
|
|
7833
|
+
return _failed(
|
|
7834
|
+
"DUPLICATE_LAYOUT_FIELD",
|
|
7835
|
+
f"form layout references field '{field_name}' more than once",
|
|
7836
|
+
normalized_args=normalized_args,
|
|
7837
|
+
details={"field_name": field_name, "app_key": target.app_key},
|
|
7838
|
+
suggested_next_call={"tool_name": "app_schema_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
7839
|
+
)
|
|
7840
|
+
seen_layout_fields.append(field_name)
|
|
7841
|
+
expected_layout_fields = {field["name"] for field in current_fields}
|
|
7842
|
+
if layout_mode == LayoutApplyMode.replace and set(seen_layout_fields) != expected_layout_fields:
|
|
7843
|
+
missing = sorted(expected_layout_fields.difference(seen_layout_fields))
|
|
7844
|
+
return _failed(
|
|
7845
|
+
"INCOMPLETE_LAYOUT",
|
|
7846
|
+
"form layout must reference every current field exactly once in replace mode",
|
|
7847
|
+
normalized_args=normalized_args,
|
|
7848
|
+
details={
|
|
7849
|
+
"missing_fields": missing,
|
|
7850
|
+
"auto_fill_preview": _merge_layout(
|
|
7851
|
+
current_layout=layout,
|
|
7852
|
+
requested_sections=resolved_sections,
|
|
7853
|
+
all_field_names=[field["name"] for field in current_fields],
|
|
7854
|
+
)["layout"],
|
|
7855
|
+
},
|
|
7856
|
+
missing_fields=missing,
|
|
7857
|
+
suggested_next_call={"tool_name": "app_schema_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
7858
|
+
)
|
|
7859
|
+
merged_layout = _merge_layout(
|
|
7860
|
+
current_layout=layout,
|
|
7861
|
+
requested_sections=resolved_sections,
|
|
7862
|
+
all_field_names=[field["name"] for field in current_fields],
|
|
7863
|
+
)
|
|
7864
|
+
applied_layout = (
|
|
7865
|
+
{"root_rows": [], "sections": resolved_sections}
|
|
7866
|
+
if layout_mode == LayoutApplyMode.replace
|
|
7867
|
+
else merged_layout["layout"]
|
|
7868
|
+
)
|
|
7869
|
+
layout_write_requested = not _layouts_equal(layout, applied_layout)
|
|
7870
|
+
layout = applied_layout
|
|
7871
|
+
|
|
7730
7872
|
schema_write_requested = bool(
|
|
7731
7873
|
added
|
|
7732
7874
|
or updated
|
|
@@ -7734,6 +7876,7 @@ class AiBuilderFacade:
|
|
|
7734
7876
|
or normalized_code_block_fields
|
|
7735
7877
|
or data_display_selection.has_any
|
|
7736
7878
|
or bool(resolved.get("created"))
|
|
7879
|
+
or layout_write_requested
|
|
7737
7880
|
)
|
|
7738
7881
|
if schema_write_requested:
|
|
7739
7882
|
try:
|
|
@@ -7756,12 +7899,6 @@ class AiBuilderFacade:
|
|
|
7756
7899
|
)
|
|
7757
7900
|
|
|
7758
7901
|
relation_field_count = _count_relation_fields(current_fields)
|
|
7759
|
-
relation_limit_verified = relation_field_count <= 1
|
|
7760
|
-
relation_warnings = (
|
|
7761
|
-
[_warning("RELATION_FIELD_LIMIT_RISK", "multiple relation fields are not a stable backend capability", relation_field_count=relation_field_count)]
|
|
7762
|
-
if not relation_limit_verified
|
|
7763
|
-
else []
|
|
7764
|
-
)
|
|
7765
7902
|
code_block_normalization_warnings = (
|
|
7766
7903
|
[
|
|
7767
7904
|
_warning(
|
|
@@ -7774,7 +7911,15 @@ class AiBuilderFacade:
|
|
|
7774
7911
|
else []
|
|
7775
7912
|
)
|
|
7776
7913
|
|
|
7777
|
-
if
|
|
7914
|
+
if (
|
|
7915
|
+
not added
|
|
7916
|
+
and not updated
|
|
7917
|
+
and not removed
|
|
7918
|
+
and not normalized_code_block_fields
|
|
7919
|
+
and not data_display_selection.has_any
|
|
7920
|
+
and not bool(resolved.get("created"))
|
|
7921
|
+
and not layout_write_requested
|
|
7922
|
+
):
|
|
7778
7923
|
base_info = self.apps.app_get_base(profile=profile, app_key=target.app_key, include_raw=True).get("result") or {}
|
|
7779
7924
|
tag_ids_after = _coerce_int_list(base_info.get("tagIds"))
|
|
7780
7925
|
package_attached = None if package_tag_id is None else package_tag_id in tag_ids_after
|
|
@@ -7796,10 +7941,10 @@ class AiBuilderFacade:
|
|
|
7796
7941
|
"request_id": None,
|
|
7797
7942
|
"suggested_next_call": None if package_attached is not False else {"tool_name": "package_attach_app", "arguments": {"profile": profile, "tag_id": package_tag_id, "app_key": target.app_key}},
|
|
7798
7943
|
"noop": not bool(visual_result.get("updated")),
|
|
7799
|
-
"warnings":
|
|
7944
|
+
"warnings": code_block_normalization_warnings,
|
|
7800
7945
|
"verification": {
|
|
7801
7946
|
"fields_verified": True,
|
|
7802
|
-
"
|
|
7947
|
+
"form_layout_verified": True,
|
|
7803
7948
|
"app_visuals_verified": app_base_verified,
|
|
7804
7949
|
"app_base_verified": app_base_verified,
|
|
7805
7950
|
"relation_target_metadata_verified": relation_target_metadata_verified,
|
|
@@ -7817,6 +7962,12 @@ class AiBuilderFacade:
|
|
|
7817
7962
|
"package_attached": package_attached,
|
|
7818
7963
|
}
|
|
7819
7964
|
response["details"]["relation_field_count"] = relation_field_count
|
|
7965
|
+
if layout_requested:
|
|
7966
|
+
response["details"]["form_layout_diff"] = {
|
|
7967
|
+
"mode": layout_mode.value,
|
|
7968
|
+
"changed": False,
|
|
7969
|
+
"requested_section_count": len(requested_layout_sections),
|
|
7970
|
+
}
|
|
7820
7971
|
if normalized_code_block_fields:
|
|
7821
7972
|
response["normalized_code_block_output_assignment"] = True
|
|
7822
7973
|
response["normalized_code_block_fields"] = normalized_code_block_fields
|
|
@@ -7850,26 +8001,6 @@ class AiBuilderFacade:
|
|
|
7850
8001
|
self.apps.app_update_form_schema(profile=profile, app_key=target.app_key, payload=payload)
|
|
7851
8002
|
except (QingflowApiError, RuntimeError) as error:
|
|
7852
8003
|
api_error = _coerce_api_error(error)
|
|
7853
|
-
if api_error.backend_code == 49614:
|
|
7854
|
-
return _failed(
|
|
7855
|
-
"MULTIPLE_RELATION_FIELDS_UNSUPPORTED",
|
|
7856
|
-
"backend currently rejects apps with more than one relation field; keep one real relation field and use text/reference summary fields for additional cross-object links.",
|
|
7857
|
-
normalized_args=normalized_args,
|
|
7858
|
-
details={
|
|
7859
|
-
"app_key": target.app_key,
|
|
7860
|
-
"field_diff": {"added": added, "updated": updated, "removed": removed},
|
|
7861
|
-
"relation_field_count": relation_field_count,
|
|
7862
|
-
"transport_error": {
|
|
7863
|
-
"http_status": api_error.http_status,
|
|
7864
|
-
"backend_code": api_error.backend_code,
|
|
7865
|
-
"category": api_error.category,
|
|
7866
|
-
},
|
|
7867
|
-
},
|
|
7868
|
-
request_id=api_error.request_id,
|
|
7869
|
-
backend_code=api_error.backend_code,
|
|
7870
|
-
http_status=None if api_error.http_status == 404 else api_error.http_status,
|
|
7871
|
-
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
7872
|
-
)
|
|
7873
8004
|
return _failed_from_api_error(
|
|
7874
8005
|
"SCHEMA_APPLY_FAILED",
|
|
7875
8006
|
api_error,
|
|
@@ -7993,13 +8124,13 @@ class AiBuilderFacade:
|
|
|
7993
8124
|
"request_id": None,
|
|
7994
8125
|
"suggested_next_call": None,
|
|
7995
8126
|
"noop": False,
|
|
7996
|
-
"warnings":
|
|
8127
|
+
"warnings": code_block_normalization_warnings,
|
|
7997
8128
|
"verification": {
|
|
7998
8129
|
"fields_verified": False,
|
|
7999
8130
|
"package_attached": None,
|
|
8000
8131
|
"app_visuals_verified": True,
|
|
8001
8132
|
"app_base_verified": True,
|
|
8002
|
-
"
|
|
8133
|
+
"form_layout_verified": not layout_requested,
|
|
8003
8134
|
"relation_target_metadata_verified": relation_target_metadata_verified,
|
|
8004
8135
|
},
|
|
8005
8136
|
"app_key": target.app_key,
|
|
@@ -8026,6 +8157,12 @@ class AiBuilderFacade:
|
|
|
8026
8157
|
"package_attached": None,
|
|
8027
8158
|
}
|
|
8028
8159
|
response["details"]["relation_field_count"] = relation_field_count
|
|
8160
|
+
if layout_requested:
|
|
8161
|
+
response["details"]["form_layout_diff"] = {
|
|
8162
|
+
"mode": layout_mode.value,
|
|
8163
|
+
"changed": layout_write_requested,
|
|
8164
|
+
"requested_section_count": len(requested_layout_sections),
|
|
8165
|
+
}
|
|
8029
8166
|
if normalized_code_block_fields:
|
|
8030
8167
|
response["normalized_code_block_output_assignment"] = True
|
|
8031
8168
|
response["normalized_code_block_fields"] = normalized_code_block_fields
|
|
@@ -8048,6 +8185,11 @@ class AiBuilderFacade:
|
|
|
8048
8185
|
after_fields=cast(list[dict[str, Any]], verified["schema"]["fields"]),
|
|
8049
8186
|
)
|
|
8050
8187
|
verification_ok = all(name in verified_field_names for name in added + updated) and all(name not in verified_field_names for name in removed)
|
|
8188
|
+
if layout_requested:
|
|
8189
|
+
verified_layout = cast(dict[str, Any], verified.get("layout") or {"root_rows": [], "sections": []})
|
|
8190
|
+
layout_verified = _layouts_equal(verified_layout, applied_layout) or _layouts_semantically_equal(verified_layout, applied_layout)
|
|
8191
|
+
response["verification"]["form_layout_verified"] = layout_verified
|
|
8192
|
+
verification_ok = verification_ok and layout_verified
|
|
8051
8193
|
data_display_verification = _verify_data_display_readback(
|
|
8052
8194
|
form_settings=verified.get("form_settings"),
|
|
8053
8195
|
selection=data_display_selection,
|
|
@@ -8091,7 +8233,6 @@ class AiBuilderFacade:
|
|
|
8091
8233
|
response["verification"]["package_attached"] = package_attached
|
|
8092
8234
|
response["verification"]["app_visuals_verified"] = app_base_verified
|
|
8093
8235
|
response["verification"]["app_base_verified"] = app_base_verified
|
|
8094
|
-
response["verification"]["relation_field_limit_verified"] = relation_limit_verified
|
|
8095
8236
|
response["verified"] = verification_ok and app_base_verified
|
|
8096
8237
|
response["tag_ids_after"] = tag_ids_after
|
|
8097
8238
|
response["package_attached"] = package_attached
|
|
@@ -10304,6 +10445,7 @@ class AiBuilderFacade:
|
|
|
10304
10445
|
)
|
|
10305
10446
|
if existing is None:
|
|
10306
10447
|
temp_chart_id = str(patch.chart_id or f"mcp_{uuid4().hex[:16]}")
|
|
10448
|
+
chart_id = temp_chart_id
|
|
10307
10449
|
create_payload = {
|
|
10308
10450
|
"chartId": temp_chart_id,
|
|
10309
10451
|
"chartName": patch.name,
|
|
@@ -12439,7 +12581,8 @@ def _match_mapping_operator_to_judge_type(operator: Any, *, reason_path: str) ->
|
|
|
12439
12581
|
"le": JUDGE_LESS_OR_EQUAL,
|
|
12440
12582
|
"<=": JUDGE_LESS_OR_EQUAL,
|
|
12441
12583
|
"less_or_equal": JUDGE_LESS_OR_EQUAL,
|
|
12442
|
-
"contains":
|
|
12584
|
+
"contains": JUDGE_INCLUDE,
|
|
12585
|
+
"include": JUDGE_INCLUDE,
|
|
12443
12586
|
"like": JUDGE_FUZZY_MATCH,
|
|
12444
12587
|
"fuzzy": JUDGE_FUZZY_MATCH,
|
|
12445
12588
|
"fuzzy_match": JUDGE_FUZZY_MATCH,
|
|
@@ -13693,7 +13836,18 @@ def _build_qingbi_chart_field_lookup(
|
|
|
13693
13836
|
field_lookup: dict[str, dict[str, Any]],
|
|
13694
13837
|
) -> dict[str, Any]:
|
|
13695
13838
|
by_selector: dict[str, list[dict[str, Any]]] = {}
|
|
13696
|
-
form_by_que_id = field_lookup.get("by_que_id") or {}
|
|
13839
|
+
form_by_que_id = dict(field_lookup.get("by_que_id") or {})
|
|
13840
|
+
if not form_by_que_id:
|
|
13841
|
+
for bucket_name in ("by_name", "by_field_id"):
|
|
13842
|
+
bucket = field_lookup.get(bucket_name) or {}
|
|
13843
|
+
if not isinstance(bucket, dict):
|
|
13844
|
+
continue
|
|
13845
|
+
for form_field in bucket.values():
|
|
13846
|
+
if not isinstance(form_field, dict):
|
|
13847
|
+
continue
|
|
13848
|
+
que_id = _coerce_any_int(form_field.get("que_id"))
|
|
13849
|
+
if que_id is not None:
|
|
13850
|
+
form_by_que_id.setdefault(que_id, form_field)
|
|
13697
13851
|
|
|
13698
13852
|
def add_selector(key: Any, field: dict[str, Any]) -> None:
|
|
13699
13853
|
normalized = str(key or "").strip()
|
|
@@ -14082,37 +14236,80 @@ def _build_public_metric_fields(
|
|
|
14082
14236
|
metrics: list[dict[str, Any]] = []
|
|
14083
14237
|
for selector in selectors:
|
|
14084
14238
|
qingbi_field = _resolve_qingbi_chart_field(selector, chart_field_lookup=chart_field_lookup, chart_type=chart_type, role="metric")
|
|
14085
|
-
|
|
14086
|
-
|
|
14087
|
-
|
|
14239
|
+
metrics.append(_public_qingbi_metric_field(qingbi_field, aggregate=normalized_aggregate))
|
|
14240
|
+
return metrics or [_default_public_total_metric()]
|
|
14241
|
+
|
|
14242
|
+
|
|
14243
|
+
def _public_qingbi_metric_field(qingbi_field: dict[str, Any], *, aggregate: str) -> dict[str, Any]:
|
|
14244
|
+
field_id = _chart_field_id(qingbi_field)
|
|
14245
|
+
if field_id == _QINGBI_TOTAL_FIELD_ID:
|
|
14246
|
+
return deepcopy(qingbi_field)
|
|
14247
|
+
form_field = qingbi_field.get("_public_form_field") if isinstance(qingbi_field.get("_public_form_field"), dict) else {}
|
|
14248
|
+
normalized_aggregate = str(aggregate or "sum").strip().lower()
|
|
14249
|
+
aggre_type = {"sum": "sum", "avg": "avg", "average": "avg", "max": "max", "min": "min"}.get(normalized_aggregate, "sum")
|
|
14250
|
+
return {
|
|
14251
|
+
"fieldId": field_id,
|
|
14252
|
+
"fieldName": qingbi_field.get("fieldName") or form_field.get("name") or field_id,
|
|
14253
|
+
"fieldType": qingbi_field.get("fieldType") or _qingbi_field_type_from_public_field(str(form_field.get("type") or "")),
|
|
14254
|
+
"orderType": "default",
|
|
14255
|
+
"alignType": "left",
|
|
14256
|
+
"dateFormat": "yyyy-MM-dd",
|
|
14257
|
+
"numberFormat": "default",
|
|
14258
|
+
"numberConfig": {"format": "splitter", "unit": "DEFAULT", "prefix": "", "suffix": "", "digit": None},
|
|
14259
|
+
"digit": None,
|
|
14260
|
+
"aggreType": aggre_type,
|
|
14261
|
+
"orderPriority": None,
|
|
14262
|
+
"width": None,
|
|
14263
|
+
"verticalAlign": "middle",
|
|
14264
|
+
"formula": qingbi_field.get("formula"),
|
|
14265
|
+
"fieldSource": qingbi_field.get("fieldSource") or "default",
|
|
14266
|
+
"status": qingbi_field.get("status"),
|
|
14267
|
+
"supId": qingbi_field.get("supId"),
|
|
14268
|
+
"beingTable": bool(qingbi_field.get("beingTable", False)),
|
|
14269
|
+
"returnType": qingbi_field.get("returnType"),
|
|
14270
|
+
"biFormulaType": qingbi_field.get("biFormulaType"),
|
|
14271
|
+
"aggreFieldId": qingbi_field.get("aggreFieldId"),
|
|
14272
|
+
}
|
|
14273
|
+
|
|
14274
|
+
|
|
14275
|
+
def _build_public_semantic_metric_fields(
|
|
14276
|
+
metrics: list[ChartMetricPatch],
|
|
14277
|
+
*,
|
|
14278
|
+
app_key: str,
|
|
14279
|
+
field_lookup: dict[str, dict[str, Any]],
|
|
14280
|
+
chart_field_lookup: dict[str, Any],
|
|
14281
|
+
qingbi_fields_by_id: dict[str, dict[str, Any]],
|
|
14282
|
+
chart_type: str = "chart",
|
|
14283
|
+
) -> list[dict[str, Any]]:
|
|
14284
|
+
if not metrics:
|
|
14285
|
+
return [_default_public_total_metric()]
|
|
14286
|
+
selected_metrics: list[dict[str, Any]] = []
|
|
14287
|
+
for metric in metrics:
|
|
14288
|
+
op = str(metric.op or "count").strip().lower()
|
|
14289
|
+
field_name = str(metric.field_name or "").strip()
|
|
14290
|
+
if op == "count":
|
|
14291
|
+
if field_name:
|
|
14292
|
+
_raise_chart_rule(
|
|
14293
|
+
rule_code="CHART_COUNT_FIELD_UNSUPPORTED",
|
|
14294
|
+
chart_type=chart_type,
|
|
14295
|
+
message="count metric currently supports count(*) only",
|
|
14296
|
+
expected='Use metric: "count(*)" or {"op": "count"} for record count.',
|
|
14297
|
+
actual={"metric": metric.model_dump(mode="json")},
|
|
14298
|
+
next_action='Use count(*) for count cards; use sum(field), avg(field), max(field), or min(field) for field aggregation.',
|
|
14299
|
+
)
|
|
14300
|
+
selected_metrics.append(_default_public_total_metric())
|
|
14088
14301
|
continue
|
|
14089
|
-
|
|
14090
|
-
|
|
14091
|
-
|
|
14092
|
-
|
|
14093
|
-
|
|
14094
|
-
"fieldType": qingbi_field.get("fieldType") or _qingbi_field_type_from_public_field(str(form_field.get("type") or "")),
|
|
14095
|
-
"orderType": "default",
|
|
14096
|
-
"alignType": "left",
|
|
14097
|
-
"dateFormat": "yyyy-MM-dd",
|
|
14098
|
-
"numberFormat": "default",
|
|
14099
|
-
"numberConfig": {"format": "splitter", "unit": "DEFAULT", "prefix": "", "suffix": "", "digit": None},
|
|
14100
|
-
"digit": None,
|
|
14101
|
-
"aggreType": {"sum": "sum", "avg": "avg", "average": "avg", "max": "max", "min": "min", "count": "sum", "distinct_count": "sum"}.get(normalized_aggregate, "sum"),
|
|
14102
|
-
"orderPriority": None,
|
|
14103
|
-
"width": None,
|
|
14104
|
-
"verticalAlign": "middle",
|
|
14105
|
-
"formula": qingbi_field.get("formula"),
|
|
14106
|
-
"fieldSource": qingbi_field.get("fieldSource") or "default",
|
|
14107
|
-
"status": qingbi_field.get("status"),
|
|
14108
|
-
"supId": qingbi_field.get("supId"),
|
|
14109
|
-
"beingTable": bool(qingbi_field.get("beingTable", False)),
|
|
14110
|
-
"returnType": qingbi_field.get("returnType"),
|
|
14111
|
-
"biFormulaType": qingbi_field.get("biFormulaType"),
|
|
14112
|
-
"aggreFieldId": qingbi_field.get("aggreFieldId"),
|
|
14113
|
-
}
|
|
14302
|
+
qingbi_field = _resolve_qingbi_chart_field(
|
|
14303
|
+
field_name,
|
|
14304
|
+
chart_field_lookup=chart_field_lookup,
|
|
14305
|
+
chart_type=chart_type,
|
|
14306
|
+
role="metric",
|
|
14114
14307
|
)
|
|
14115
|
-
|
|
14308
|
+
metric_payload = _public_qingbi_metric_field(qingbi_field, aggregate=op)
|
|
14309
|
+
if metric.alias:
|
|
14310
|
+
metric_payload["fieldName"] = metric.alias
|
|
14311
|
+
selected_metrics.append(metric_payload)
|
|
14312
|
+
return selected_metrics or [_default_public_total_metric()]
|
|
14116
14313
|
|
|
14117
14314
|
|
|
14118
14315
|
def _split_axis_metric_fields(metrics: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
@@ -14134,73 +14331,6 @@ def _two_gauge_metric_fields(metrics: list[dict[str, Any]], *, requested_metric_
|
|
|
14134
14331
|
return [deepcopy(metrics[0]), deepcopy(metrics[1])]
|
|
14135
14332
|
|
|
14136
14333
|
|
|
14137
|
-
def _qingbi_filter_judge_type(operator: str) -> str:
|
|
14138
|
-
return {
|
|
14139
|
-
ViewFilterOperator.eq.value: "equal",
|
|
14140
|
-
ViewFilterOperator.neq.value: "notEqual",
|
|
14141
|
-
ViewFilterOperator.gte.value: "greaterOrEqual",
|
|
14142
|
-
ViewFilterOperator.lte.value: "lessOrEqual",
|
|
14143
|
-
ViewFilterOperator.in_.value: "anyMatch",
|
|
14144
|
-
ViewFilterOperator.contains.value: "contains",
|
|
14145
|
-
ViewFilterOperator.is_empty.value: "isNull",
|
|
14146
|
-
ViewFilterOperator.not_empty.value: "isNotNull",
|
|
14147
|
-
}.get(operator, "equal")
|
|
14148
|
-
|
|
14149
|
-
|
|
14150
|
-
def _qingbi_option_label_lookup(*, form_field: dict[str, Any], qingbi_field: dict[str, Any]) -> dict[str, str]:
|
|
14151
|
-
labels: dict[str, str] = {}
|
|
14152
|
-
|
|
14153
|
-
def add_option(raw_option: Any) -> None:
|
|
14154
|
-
if isinstance(raw_option, dict):
|
|
14155
|
-
label = raw_option.get("value")
|
|
14156
|
-
if label is None:
|
|
14157
|
-
label = raw_option.get("label") or raw_option.get("name") or raw_option.get("title") or raw_option.get("optValue")
|
|
14158
|
-
label_text = str(label).strip() if label is not None else ""
|
|
14159
|
-
for key in ("id", "optionId", "option_id", "optId", "value", "label", "name", "title", "optValue"):
|
|
14160
|
-
raw_key = raw_option.get(key)
|
|
14161
|
-
if raw_key is not None and str(raw_key).strip() and label_text:
|
|
14162
|
-
labels[str(raw_key).strip()] = label_text
|
|
14163
|
-
elif raw_option is not None:
|
|
14164
|
-
label_text = str(raw_option).strip()
|
|
14165
|
-
if label_text:
|
|
14166
|
-
labels[label_text] = label_text
|
|
14167
|
-
|
|
14168
|
-
for option in form_field.get("option_details") or []:
|
|
14169
|
-
add_option(option)
|
|
14170
|
-
for option in form_field.get("options") or []:
|
|
14171
|
-
add_option(option)
|
|
14172
|
-
for option in qingbi_field.get("options") or qingbi_field.get("option_details") or []:
|
|
14173
|
-
add_option(option)
|
|
14174
|
-
return labels
|
|
14175
|
-
|
|
14176
|
-
|
|
14177
|
-
def _qingbi_filter_text_values(values: list[Any], *, form_field: dict[str, Any], qingbi_field: dict[str, Any]) -> list[str]:
|
|
14178
|
-
labels = _qingbi_option_label_lookup(form_field=form_field, qingbi_field=qingbi_field)
|
|
14179
|
-
normalized: list[str] = []
|
|
14180
|
-
for value in values:
|
|
14181
|
-
if isinstance(value, dict):
|
|
14182
|
-
raw_label = value.get("value") or value.get("label") or value.get("name") or value.get("title") or value.get("optValue")
|
|
14183
|
-
if raw_label is not None and str(raw_label).strip():
|
|
14184
|
-
normalized.append(str(raw_label).strip())
|
|
14185
|
-
continue
|
|
14186
|
-
raw_id = value.get("id") or value.get("optionId") or value.get("option_id") or value.get("optId")
|
|
14187
|
-
raw_text = str(raw_id).strip() if raw_id is not None else ""
|
|
14188
|
-
else:
|
|
14189
|
-
raw_text = str(value).strip()
|
|
14190
|
-
if not raw_text:
|
|
14191
|
-
continue
|
|
14192
|
-
normalized.append(labels.get(raw_text, raw_text))
|
|
14193
|
-
return normalized
|
|
14194
|
-
|
|
14195
|
-
|
|
14196
|
-
def _qingbi_filter_judge_value(values: list[str]) -> Any:
|
|
14197
|
-
if not values:
|
|
14198
|
-
return ""
|
|
14199
|
-
if len(values) == 1:
|
|
14200
|
-
return values[0]
|
|
14201
|
-
return values
|
|
14202
|
-
|
|
14203
|
-
|
|
14204
14334
|
def _build_public_chart_filter_matrix(
|
|
14205
14335
|
rules: list[Any],
|
|
14206
14336
|
*,
|
|
@@ -14213,6 +14343,16 @@ def _build_public_chart_filter_matrix(
|
|
|
14213
14343
|
if not rules:
|
|
14214
14344
|
return []
|
|
14215
14345
|
group: list[dict[str, Any]] = []
|
|
14346
|
+
judge_map = {
|
|
14347
|
+
ViewFilterOperator.eq.value: "equal",
|
|
14348
|
+
ViewFilterOperator.neq.value: "unequal",
|
|
14349
|
+
ViewFilterOperator.gte.value: "greaterOrEqual",
|
|
14350
|
+
ViewFilterOperator.lte.value: "lessOrEqual",
|
|
14351
|
+
ViewFilterOperator.in_.value: "anyMatch",
|
|
14352
|
+
ViewFilterOperator.contains.value: "include",
|
|
14353
|
+
ViewFilterOperator.is_empty.value: "isNull",
|
|
14354
|
+
ViewFilterOperator.not_empty.value: "notNull",
|
|
14355
|
+
}
|
|
14216
14356
|
for rule in rules:
|
|
14217
14357
|
qingbi_field = _resolve_qingbi_chart_field(
|
|
14218
14358
|
getattr(rule, "field_name", None),
|
|
@@ -14224,21 +14364,301 @@ def _build_public_chart_filter_matrix(
|
|
|
14224
14364
|
form_field = qingbi_field.get("_public_form_field") if isinstance(qingbi_field.get("_public_form_field"), dict) else {}
|
|
14225
14365
|
operator = str(getattr(rule, "operator", ViewFilterOperator.eq.value).value if hasattr(getattr(rule, "operator", None), "value") else getattr(rule, "operator", ViewFilterOperator.eq.value))
|
|
14226
14366
|
values = list(getattr(rule, "values", []) or [])
|
|
14227
|
-
text_values = _qingbi_filter_text_values(values, form_field=form_field, qingbi_field=qingbi_field)
|
|
14228
14367
|
group.append(
|
|
14229
14368
|
{
|
|
14230
14369
|
"fieldId": field_id,
|
|
14231
14370
|
"fieldName": qingbi_field.get("fieldName") or form_field.get("name") or field_id,
|
|
14232
14371
|
"fieldType": qingbi_field.get("fieldType") or _qingbi_field_type_from_public_field(str(form_field.get("type") or "")),
|
|
14233
|
-
"judgeType":
|
|
14234
|
-
"judgeValue":
|
|
14235
|
-
|
|
14372
|
+
"judgeType": judge_map.get(operator, "equal"),
|
|
14373
|
+
"judgeValue": _build_qingbi_chart_filter_judge_value(
|
|
14374
|
+
operator=operator,
|
|
14375
|
+
values=values,
|
|
14376
|
+
form_field=form_field,
|
|
14377
|
+
chart_type=chart_type,
|
|
14378
|
+
),
|
|
14379
|
+
"judgeValueDetailList": [],
|
|
14236
14380
|
"matchType": 1,
|
|
14237
14381
|
}
|
|
14238
14382
|
)
|
|
14239
14383
|
return [group] if group else []
|
|
14240
14384
|
|
|
14241
14385
|
|
|
14386
|
+
def _build_qingbi_chart_filter_judge_value(
|
|
14387
|
+
*,
|
|
14388
|
+
operator: str,
|
|
14389
|
+
values: list[Any],
|
|
14390
|
+
form_field: dict[str, Any],
|
|
14391
|
+
chart_type: str,
|
|
14392
|
+
) -> str | None:
|
|
14393
|
+
if operator in {ViewFilterOperator.is_empty.value, ViewFilterOperator.not_empty.value}:
|
|
14394
|
+
return None
|
|
14395
|
+
if operator == ViewFilterOperator.in_.value:
|
|
14396
|
+
return "<&&>".join(
|
|
14397
|
+
_qingbi_chart_filter_value_to_text(value=value, form_field=form_field, chart_type=chart_type) for value in values
|
|
14398
|
+
)
|
|
14399
|
+
if not values:
|
|
14400
|
+
return ""
|
|
14401
|
+
return _qingbi_chart_filter_value_to_text(value=values[0], form_field=form_field, chart_type=chart_type)
|
|
14402
|
+
|
|
14403
|
+
|
|
14404
|
+
def _qingbi_chart_filter_value_to_text(*, value: Any, form_field: dict[str, Any], chart_type: str) -> str:
|
|
14405
|
+
option_details = [
|
|
14406
|
+
item
|
|
14407
|
+
for item in (form_field.get("option_details") or [])
|
|
14408
|
+
if isinstance(item, dict) and item.get("id") is not None and item.get("value") is not None
|
|
14409
|
+
]
|
|
14410
|
+
if not option_details:
|
|
14411
|
+
if isinstance(value, dict):
|
|
14412
|
+
for key in ("value", "label", "name", "title"):
|
|
14413
|
+
raw = str(value.get(key) or "").strip()
|
|
14414
|
+
if raw:
|
|
14415
|
+
return raw
|
|
14416
|
+
if value.get("id") is not None:
|
|
14417
|
+
return str(value.get("id"))
|
|
14418
|
+
return _stringify_condition_value(value)
|
|
14419
|
+
|
|
14420
|
+
option_by_value = {str(item.get("value") or "").strip(): item for item in option_details if str(item.get("value") or "").strip()}
|
|
14421
|
+
option_by_id = {str(item.get("id")): item for item in option_details if item.get("id") is not None}
|
|
14422
|
+
|
|
14423
|
+
def resolve(raw: Any) -> str | None:
|
|
14424
|
+
text = _stringify_condition_value(raw).strip()
|
|
14425
|
+
if not text:
|
|
14426
|
+
return None
|
|
14427
|
+
matched = option_by_value.get(text) or option_by_id.get(text)
|
|
14428
|
+
if not matched:
|
|
14429
|
+
return None
|
|
14430
|
+
return str(matched.get("value") or "").strip()
|
|
14431
|
+
|
|
14432
|
+
def reject(raw: Any) -> NoReturn:
|
|
14433
|
+
allowed = [
|
|
14434
|
+
{"id": str(item.get("id")), "value": str(item.get("value"))}
|
|
14435
|
+
for item in option_details
|
|
14436
|
+
]
|
|
14437
|
+
_raise_chart_rule(
|
|
14438
|
+
rule_code="CHART_FILTER_OPTION_VALUE_UNSUPPORTED",
|
|
14439
|
+
chart_type=chart_type,
|
|
14440
|
+
message="chart filter value is not in the field option list",
|
|
14441
|
+
expected="Use an existing option label or option id from the target field schema.",
|
|
14442
|
+
actual={
|
|
14443
|
+
"field_name": form_field.get("name"),
|
|
14444
|
+
"field_id": form_field.get("que_id"),
|
|
14445
|
+
"value": raw,
|
|
14446
|
+
"allowed_values": allowed,
|
|
14447
|
+
},
|
|
14448
|
+
offending_fields=[{"field_name": form_field.get("name"), "field_id": form_field.get("que_id"), "options": allowed}],
|
|
14449
|
+
next_action="Read schema first, then pass one of the allowed option labels or option ids.",
|
|
14450
|
+
)
|
|
14451
|
+
|
|
14452
|
+
if isinstance(value, dict):
|
|
14453
|
+
for key in ("value", "label", "name", "title"):
|
|
14454
|
+
if value.get(key) is not None:
|
|
14455
|
+
resolved = resolve(value.get(key))
|
|
14456
|
+
if resolved is not None:
|
|
14457
|
+
return resolved
|
|
14458
|
+
reject(value.get(key))
|
|
14459
|
+
if value.get("id") is not None:
|
|
14460
|
+
resolved = resolve(value.get("id"))
|
|
14461
|
+
if resolved is not None:
|
|
14462
|
+
return resolved
|
|
14463
|
+
reject(value.get("id"))
|
|
14464
|
+
reject(value)
|
|
14465
|
+
if isinstance(value, int):
|
|
14466
|
+
resolved = resolve(value)
|
|
14467
|
+
if resolved is not None:
|
|
14468
|
+
return resolved
|
|
14469
|
+
reject(value)
|
|
14470
|
+
resolved = resolve(value)
|
|
14471
|
+
if resolved is not None:
|
|
14472
|
+
return resolved
|
|
14473
|
+
reject(value)
|
|
14474
|
+
|
|
14475
|
+
|
|
14476
|
+
def _chart_field_names_by_id_from_public_fields(*, app_key: str, fields: list[dict[str, Any]]) -> dict[str, str]:
|
|
14477
|
+
field_name_by_id: dict[str, str] = {}
|
|
14478
|
+
for field in fields:
|
|
14479
|
+
if not isinstance(field, dict):
|
|
14480
|
+
continue
|
|
14481
|
+
name = str(field.get("name") or "").strip()
|
|
14482
|
+
if not name:
|
|
14483
|
+
continue
|
|
14484
|
+
que_id = field.get("que_id")
|
|
14485
|
+
field_id = str(field.get("field_id") or "").strip()
|
|
14486
|
+
for raw_key in (
|
|
14487
|
+
que_id,
|
|
14488
|
+
field_id,
|
|
14489
|
+
f"{app_key}:{que_id}" if que_id is not None else None,
|
|
14490
|
+
f"{app_key}:{field_id}" if field_id else None,
|
|
14491
|
+
):
|
|
14492
|
+
key = str(raw_key or "").strip()
|
|
14493
|
+
if key:
|
|
14494
|
+
field_name_by_id.setdefault(key, name)
|
|
14495
|
+
return field_name_by_id
|
|
14496
|
+
|
|
14497
|
+
|
|
14498
|
+
def _public_chart_filter_groups_from_qingbi_config(
|
|
14499
|
+
config: dict[str, Any],
|
|
14500
|
+
*,
|
|
14501
|
+
field_name_by_id: dict[str, str] | None = None,
|
|
14502
|
+
) -> list[list[dict[str, Any]]]:
|
|
14503
|
+
groups: list[list[dict[str, Any]]] = []
|
|
14504
|
+
raw_groups = config.get("beforeAggregationFilterMatrix")
|
|
14505
|
+
if not isinstance(raw_groups, list):
|
|
14506
|
+
return groups
|
|
14507
|
+
resolved_field_name_by_id = field_name_by_id or {}
|
|
14508
|
+
for raw_group in raw_groups:
|
|
14509
|
+
if not isinstance(raw_group, list):
|
|
14510
|
+
continue
|
|
14511
|
+
group: list[dict[str, Any]] = []
|
|
14512
|
+
for raw_rule in raw_group:
|
|
14513
|
+
if not isinstance(raw_rule, dict):
|
|
14514
|
+
continue
|
|
14515
|
+
operator = _public_chart_filter_operator_from_judge_type(raw_rule.get("judgeType"))
|
|
14516
|
+
field_id = raw_rule.get("fieldId") or raw_rule.get("field_id")
|
|
14517
|
+
field_id_text = _stringify_condition_value(field_id).strip() if field_id is not None else ""
|
|
14518
|
+
raw_field_name = (
|
|
14519
|
+
raw_rule.get("fieldName")
|
|
14520
|
+
or raw_rule.get("field_name")
|
|
14521
|
+
or raw_rule.get("queTitle")
|
|
14522
|
+
or raw_rule.get("title")
|
|
14523
|
+
)
|
|
14524
|
+
raw_field_name_text = _stringify_condition_value(raw_field_name).strip()
|
|
14525
|
+
field_name = (
|
|
14526
|
+
resolved_field_name_by_id.get(raw_field_name_text)
|
|
14527
|
+
or resolved_field_name_by_id.get(field_id_text)
|
|
14528
|
+
or raw_field_name_text
|
|
14529
|
+
or field_id_text
|
|
14530
|
+
)
|
|
14531
|
+
public_rule: dict[str, Any] = {
|
|
14532
|
+
"field_name": field_name,
|
|
14533
|
+
"operator": operator,
|
|
14534
|
+
}
|
|
14535
|
+
if field_id is not None:
|
|
14536
|
+
public_rule["field_id"] = field_id_text
|
|
14537
|
+
values = _public_chart_filter_values_from_rule(raw_rule, operator=operator)
|
|
14538
|
+
if values:
|
|
14539
|
+
public_rule["values"] = values
|
|
14540
|
+
group.append(public_rule)
|
|
14541
|
+
if group:
|
|
14542
|
+
groups.append(group)
|
|
14543
|
+
return groups
|
|
14544
|
+
|
|
14545
|
+
|
|
14546
|
+
def _public_chart_group_by_from_qingbi_config(config: dict[str, Any]) -> list[str]:
|
|
14547
|
+
fields: list[dict[str, Any]] = []
|
|
14548
|
+
for key in ("selectedDimensions", "xDimensions", "yDimensions", "selectedTime"):
|
|
14549
|
+
fields.extend(_chart_fields(config, key))
|
|
14550
|
+
group_by: list[str] = []
|
|
14551
|
+
seen: set[str] = set()
|
|
14552
|
+
for field in fields:
|
|
14553
|
+
name = _stringify_condition_value(
|
|
14554
|
+
field.get("fieldName")
|
|
14555
|
+
or field.get("field_name")
|
|
14556
|
+
or field.get("queTitle")
|
|
14557
|
+
or field.get("title")
|
|
14558
|
+
or field.get("fieldId")
|
|
14559
|
+
or field.get("field_id")
|
|
14560
|
+
).strip()
|
|
14561
|
+
if not name or name in seen:
|
|
14562
|
+
continue
|
|
14563
|
+
seen.add(name)
|
|
14564
|
+
group_by.append(name)
|
|
14565
|
+
return group_by
|
|
14566
|
+
|
|
14567
|
+
|
|
14568
|
+
def _public_chart_metrics_from_qingbi_config(config: dict[str, Any]) -> list[dict[str, Any]]:
|
|
14569
|
+
fields: list[dict[str, Any]] = []
|
|
14570
|
+
for key in ("selectedMetrics", "xMetrics", "yMetrics", "leftMetrics", "rightMetrics"):
|
|
14571
|
+
fields.extend(_chart_fields(config, key))
|
|
14572
|
+
metrics: list[dict[str, Any]] = []
|
|
14573
|
+
seen: set[tuple[str, str]] = set()
|
|
14574
|
+
for field in fields:
|
|
14575
|
+
field_id = _chart_field_id(field)
|
|
14576
|
+
if field_id == _QINGBI_TOTAL_FIELD_ID:
|
|
14577
|
+
metric = {"op": "count", "expr": "count(*)"}
|
|
14578
|
+
else:
|
|
14579
|
+
op = str(field.get("aggreType") or field.get("aggregate") or "sum").strip().lower()
|
|
14580
|
+
if op == "average":
|
|
14581
|
+
op = "avg"
|
|
14582
|
+
field_name = _stringify_condition_value(
|
|
14583
|
+
field.get("fieldName")
|
|
14584
|
+
or field.get("field_name")
|
|
14585
|
+
or field.get("queTitle")
|
|
14586
|
+
or field.get("title")
|
|
14587
|
+
or field_id
|
|
14588
|
+
).strip()
|
|
14589
|
+
metric = {"op": op or "sum", "field_name": field_name}
|
|
14590
|
+
if field_id:
|
|
14591
|
+
metric["field_id"] = field_id
|
|
14592
|
+
metric["expr"] = f"{metric['op']}({field_name})" if field_name else metric["op"]
|
|
14593
|
+
identity = (str(metric.get("op") or ""), str(metric.get("field_id") or metric.get("field_name") or metric.get("expr") or ""))
|
|
14594
|
+
if identity in seen:
|
|
14595
|
+
continue
|
|
14596
|
+
seen.add(identity)
|
|
14597
|
+
metrics.append(metric)
|
|
14598
|
+
return metrics
|
|
14599
|
+
|
|
14600
|
+
|
|
14601
|
+
def _public_chart_filter_operator_from_judge_type(judge_type: Any) -> str:
|
|
14602
|
+
normalized = _stringify_condition_value(judge_type).strip()
|
|
14603
|
+
mapping = {
|
|
14604
|
+
"equal": "eq",
|
|
14605
|
+
"equals": "eq",
|
|
14606
|
+
"=": "eq",
|
|
14607
|
+
"unequal": "neq",
|
|
14608
|
+
"notEqual": "neq",
|
|
14609
|
+
"!=": "neq",
|
|
14610
|
+
"anyMatch": "in",
|
|
14611
|
+
"oneOf": "in",
|
|
14612
|
+
"include": "contains",
|
|
14613
|
+
"contains": "contains",
|
|
14614
|
+
"greaterOrEqual": "gte",
|
|
14615
|
+
"gte": "gte",
|
|
14616
|
+
"lessOrEqual": "lte",
|
|
14617
|
+
"lte": "lte",
|
|
14618
|
+
"isNull": "is_empty",
|
|
14619
|
+
"empty": "is_empty",
|
|
14620
|
+
"notNull": "not_empty",
|
|
14621
|
+
"not_empty": "not_empty",
|
|
14622
|
+
}
|
|
14623
|
+
if normalized in mapping:
|
|
14624
|
+
return mapping[normalized]
|
|
14625
|
+
numeric = _coerce_nonnegative_int(judge_type)
|
|
14626
|
+
if numeric is not None:
|
|
14627
|
+
return _public_view_filter_operator_from_judge_type(numeric)
|
|
14628
|
+
return f"judge_type:{judge_type}"
|
|
14629
|
+
|
|
14630
|
+
|
|
14631
|
+
def _public_chart_filter_values_from_rule(rule: dict[str, Any], *, operator: str) -> list[str]:
|
|
14632
|
+
if operator in {"is_empty", "not_empty"}:
|
|
14633
|
+
return []
|
|
14634
|
+
raw_value = rule.get("judgeValue")
|
|
14635
|
+
if raw_value is None and "judgeValues" in rule:
|
|
14636
|
+
raw_value = rule.get("judgeValues")
|
|
14637
|
+
values: list[str] = []
|
|
14638
|
+
if isinstance(raw_value, list):
|
|
14639
|
+
values = [_stringify_condition_value(value).strip() for value in raw_value]
|
|
14640
|
+
elif isinstance(raw_value, str) and "<&&>" in raw_value:
|
|
14641
|
+
values = [value.strip() for value in raw_value.split("<&&>")]
|
|
14642
|
+
elif raw_value is not None:
|
|
14643
|
+
values = [_stringify_condition_value(raw_value).strip()]
|
|
14644
|
+
values = [value for value in values if value]
|
|
14645
|
+
if values:
|
|
14646
|
+
return values
|
|
14647
|
+
for detail_key in ("judgeValueDetailList", "judgeValueDetails"):
|
|
14648
|
+
details = rule.get(detail_key)
|
|
14649
|
+
if not isinstance(details, list):
|
|
14650
|
+
continue
|
|
14651
|
+
for detail in details:
|
|
14652
|
+
if not isinstance(detail, dict):
|
|
14653
|
+
continue
|
|
14654
|
+
for value_key in ("value", "label", "name", "title", "dataValue", "id"):
|
|
14655
|
+
value = _stringify_condition_value(detail.get(value_key)).strip()
|
|
14656
|
+
if value:
|
|
14657
|
+
values.append(value)
|
|
14658
|
+
break
|
|
14659
|
+
return values
|
|
14660
|
+
|
|
14661
|
+
|
|
14242
14662
|
def _build_public_chart_config_payload(
|
|
14243
14663
|
*,
|
|
14244
14664
|
patch: ChartUpsertPatch,
|
|
@@ -14249,9 +14669,14 @@ def _build_public_chart_config_payload(
|
|
|
14249
14669
|
) -> dict[str, Any]:
|
|
14250
14670
|
config = deepcopy(patch.config)
|
|
14251
14671
|
explicit_fields = set(getattr(patch, "model_fields_set", set()) or set())
|
|
14252
|
-
|
|
14672
|
+
semantic_dimension_fields = bool({"dimension_field_ids", "group_by", "rows", "columns"} & explicit_fields)
|
|
14673
|
+
semantic_metric_fields = bool(
|
|
14674
|
+
{"indicator_field_ids", "metric", "metrics", "x_metric", "y_metric", "left_metric", "right_metric", "value_metric", "target_metric"}
|
|
14675
|
+
& explicit_fields
|
|
14676
|
+
)
|
|
14677
|
+
if semantic_dimension_fields:
|
|
14253
14678
|
config.pop("selectedDimensions", None)
|
|
14254
|
-
if
|
|
14679
|
+
if semantic_metric_fields:
|
|
14255
14680
|
config.pop("selectedMetrics", None)
|
|
14256
14681
|
if "filters" in explicit_fields:
|
|
14257
14682
|
config.pop("beforeAggregationFilterMatrix", None)
|
|
@@ -14277,8 +14702,8 @@ def _build_public_chart_config_payload(
|
|
|
14277
14702
|
)
|
|
14278
14703
|
query_condition_field_ids.append(_chart_field_id(field))
|
|
14279
14704
|
backend_chart_type = _map_public_chart_type_to_backend(patch.chart_type)
|
|
14280
|
-
if backend_chart_type == "gauge" and not patch.indicator_field_ids and "selectedMetrics" not in config:
|
|
14281
|
-
raise ValueError("gauge charts require at least one
|
|
14705
|
+
if backend_chart_type == "gauge" and not patch.indicator_field_ids and not patch.metrics and "selectedMetrics" not in config:
|
|
14706
|
+
raise ValueError("gauge charts require at least one metric; pass value_metric or metric and the CLI will pair it with 数据总量")
|
|
14282
14707
|
selected_dimensions = _build_public_dimension_fields(
|
|
14283
14708
|
patch.dimension_field_ids,
|
|
14284
14709
|
app_key=app_key,
|
|
@@ -14287,15 +14712,25 @@ def _build_public_chart_config_payload(
|
|
|
14287
14712
|
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
14288
14713
|
chart_type=patch.chart_type.value,
|
|
14289
14714
|
)
|
|
14290
|
-
|
|
14291
|
-
|
|
14292
|
-
|
|
14293
|
-
|
|
14294
|
-
|
|
14295
|
-
|
|
14296
|
-
|
|
14297
|
-
|
|
14298
|
-
|
|
14715
|
+
if patch.metrics:
|
|
14716
|
+
selected_metrics = _build_public_semantic_metric_fields(
|
|
14717
|
+
patch.metrics,
|
|
14718
|
+
app_key=app_key,
|
|
14719
|
+
field_lookup=field_lookup,
|
|
14720
|
+
chart_field_lookup=chart_field_lookup,
|
|
14721
|
+
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
14722
|
+
chart_type=patch.chart_type.value,
|
|
14723
|
+
)
|
|
14724
|
+
else:
|
|
14725
|
+
selected_metrics = _build_public_metric_fields(
|
|
14726
|
+
patch.indicator_field_ids,
|
|
14727
|
+
app_key=app_key,
|
|
14728
|
+
field_lookup=field_lookup,
|
|
14729
|
+
chart_field_lookup=chart_field_lookup,
|
|
14730
|
+
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
14731
|
+
aggregate=aggregate,
|
|
14732
|
+
chart_type=patch.chart_type.value,
|
|
14733
|
+
)
|
|
14299
14734
|
payload: dict[str, Any] = {
|
|
14300
14735
|
"chartName": patch.name,
|
|
14301
14736
|
"chartType": backend_chart_type,
|
|
@@ -14320,7 +14755,15 @@ def _build_public_chart_config_payload(
|
|
|
14320
14755
|
if backend_chart_type == "summary":
|
|
14321
14756
|
payload.pop("selectedDimensions", None)
|
|
14322
14757
|
payload.setdefault("xDimensions", deepcopy(selected_dimensions))
|
|
14323
|
-
|
|
14758
|
+
y_dimensions = _build_public_dimension_fields(
|
|
14759
|
+
patch.columns,
|
|
14760
|
+
app_key=app_key,
|
|
14761
|
+
field_lookup=field_lookup,
|
|
14762
|
+
chart_field_lookup=chart_field_lookup,
|
|
14763
|
+
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
14764
|
+
chart_type=patch.chart_type.value,
|
|
14765
|
+
)
|
|
14766
|
+
payload.setdefault("yDimensions", y_dimensions)
|
|
14324
14767
|
elif backend_chart_type == "scatter":
|
|
14325
14768
|
x_metrics, y_metrics = _split_axis_metric_fields(selected_metrics)
|
|
14326
14769
|
payload.pop("selectedMetrics", None)
|
|
@@ -14354,7 +14797,26 @@ def _build_public_chart_config_payload(
|
|
|
14354
14797
|
|
|
14355
14798
|
def _chart_patch_updates_chart_config(patch: ChartUpsertPatch) -> bool:
|
|
14356
14799
|
explicit_fields = set(getattr(patch, "model_fields_set", set()) or set())
|
|
14357
|
-
return bool(
|
|
14800
|
+
return bool(
|
|
14801
|
+
{
|
|
14802
|
+
"dimension_field_ids",
|
|
14803
|
+
"indicator_field_ids",
|
|
14804
|
+
"group_by",
|
|
14805
|
+
"rows",
|
|
14806
|
+
"columns",
|
|
14807
|
+
"metric",
|
|
14808
|
+
"metrics",
|
|
14809
|
+
"x_metric",
|
|
14810
|
+
"y_metric",
|
|
14811
|
+
"left_metric",
|
|
14812
|
+
"right_metric",
|
|
14813
|
+
"value_metric",
|
|
14814
|
+
"target_metric",
|
|
14815
|
+
"filters",
|
|
14816
|
+
"config",
|
|
14817
|
+
}
|
|
14818
|
+
& explicit_fields
|
|
14819
|
+
)
|
|
14358
14820
|
|
|
14359
14821
|
|
|
14360
14822
|
def _chart_patch_dataset_source_type(patch: ChartUpsertPatch) -> str:
|
|
@@ -21871,7 +22333,7 @@ def _translate_flow_condition_rule(*, field: dict[str, Any], rule: dict[str, Any
|
|
|
21871
22333
|
base["judgeType"] = JUDGE_INCLUDE_ANY if field_type in INCLUDE_ANY_FLOW_FIELD_TYPES else JUDGE_EQUAL_ANY
|
|
21872
22334
|
base["judgeValues"] = [_stringify_condition_value(value) for value in values]
|
|
21873
22335
|
elif operator == "contains":
|
|
21874
|
-
base["judgeType"] =
|
|
22336
|
+
base["judgeType"] = JUDGE_INCLUDE
|
|
21875
22337
|
base["judgeValues"] = [_stringify_condition_value(values[0])]
|
|
21876
22338
|
elif operator == "gte":
|
|
21877
22339
|
base["judgeType"] = JUDGE_GREATER_OR_EQUAL
|
|
@@ -23638,7 +24100,7 @@ def _translate_view_filter_rule(*, field: dict[str, Any], rule: dict[str, Any])
|
|
|
23638
24100
|
payload["judgeType"] = JUDGE_INCLUDE_ANY if field_type in INCLUDE_ANY_FLOW_FIELD_TYPES else JUDGE_EQUAL_ANY
|
|
23639
24101
|
payload["judgeValues"] = judge_values
|
|
23640
24102
|
elif operator == "contains":
|
|
23641
|
-
payload["judgeType"] =
|
|
24103
|
+
payload["judgeType"] = JUDGE_INCLUDE
|
|
23642
24104
|
payload["judgeValues"] = judge_values[:1] if judge_values else []
|
|
23643
24105
|
elif operator == "gte":
|
|
23644
24106
|
payload["judgeType"] = JUDGE_GREATER_OR_EQUAL
|