@josephyan/qingflow-app-user-mcp 1.1.26 → 1.1.27
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 +612 -152
- 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,
|
|
@@ -5238,6 +5239,20 @@ class AiBuilderFacade:
|
|
|
5238
5239
|
result["message"] = "read app chart config"
|
|
5239
5240
|
return result
|
|
5240
5241
|
|
|
5242
|
+
def _chart_filter_field_names_by_id(
|
|
5243
|
+
self,
|
|
5244
|
+
*,
|
|
5245
|
+
profile: str,
|
|
5246
|
+
app_key: str,
|
|
5247
|
+
) -> tuple[dict[str, str], dict[str, Any] | None]:
|
|
5248
|
+
try:
|
|
5249
|
+
state = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
5250
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
5251
|
+
api_error = _coerce_api_error(error)
|
|
5252
|
+
return {}, _transport_error_payload(api_error)
|
|
5253
|
+
fields = cast(list[dict[str, Any]], state["parsed"]["fields"])
|
|
5254
|
+
return _chart_field_names_by_id_from_public_fields(app_key=app_key, fields=fields), None
|
|
5255
|
+
|
|
5241
5256
|
def app_get_buttons(self, *, profile: str, app_key: str) -> JSONObject:
|
|
5242
5257
|
"""Read custom buttons for a single app."""
|
|
5243
5258
|
self.apps._require_app_key(app_key)
|
|
@@ -5796,6 +5811,8 @@ class AiBuilderFacade:
|
|
|
5796
5811
|
)
|
|
5797
5812
|
charts = _summarize_charts(items)
|
|
5798
5813
|
chart_visibility_read_errors: list[dict[str, Any]] = []
|
|
5814
|
+
chart_config_read_errors: list[dict[str, Any]] = []
|
|
5815
|
+
field_name_by_id, field_name_read_error = self._chart_filter_field_names_by_id(profile=profile, app_key=resolved_app_key)
|
|
5799
5816
|
for chart in charts:
|
|
5800
5817
|
chart_id = str(chart.get("chart_id") or "").strip()
|
|
5801
5818
|
if not chart_id:
|
|
@@ -5818,6 +5835,24 @@ class AiBuilderFacade:
|
|
|
5818
5835
|
base_info.get("visibleAuth") if isinstance(base_info, dict) else None
|
|
5819
5836
|
)
|
|
5820
5837
|
)
|
|
5838
|
+
try:
|
|
5839
|
+
config_response = self.charts.qingbi_report_get_config(profile=profile, chart_id=chart_id)
|
|
5840
|
+
config = config_response.get("result") or {}
|
|
5841
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
5842
|
+
api_error = _coerce_api_error(error)
|
|
5843
|
+
chart_config_read_errors.append(
|
|
5844
|
+
{
|
|
5845
|
+
"chart_id": chart_id,
|
|
5846
|
+
"request_id": api_error.request_id,
|
|
5847
|
+
"http_status": api_error.http_status,
|
|
5848
|
+
"backend_code": api_error.backend_code,
|
|
5849
|
+
}
|
|
5850
|
+
)
|
|
5851
|
+
continue
|
|
5852
|
+
if isinstance(config, dict):
|
|
5853
|
+
chart["group_by"] = _public_chart_group_by_from_qingbi_config(config)
|
|
5854
|
+
chart["metrics"] = _public_chart_metrics_from_qingbi_config(config)
|
|
5855
|
+
chart["filters"] = _public_chart_filter_groups_from_qingbi_config(config, field_name_by_id=field_name_by_id)
|
|
5821
5856
|
response = AppChartsReadResponse(
|
|
5822
5857
|
app_key=resolved_app_key,
|
|
5823
5858
|
charts=charts,
|
|
@@ -5831,7 +5866,11 @@ class AiBuilderFacade:
|
|
|
5831
5866
|
"normalized_args": {"app_key": resolved_app_key},
|
|
5832
5867
|
"missing_fields": [],
|
|
5833
5868
|
"allowed_values": {},
|
|
5834
|
-
"details": {
|
|
5869
|
+
"details": {
|
|
5870
|
+
**({"chart_visibility_read_errors": chart_visibility_read_errors} if chart_visibility_read_errors else {}),
|
|
5871
|
+
**({"chart_config_read_errors": chart_config_read_errors} if chart_config_read_errors else {}),
|
|
5872
|
+
**({"chart_filter_field_name_read_error": field_name_read_error} if field_name_read_error else {}),
|
|
5873
|
+
},
|
|
5835
5874
|
"request_id": None,
|
|
5836
5875
|
"suggested_next_call": None,
|
|
5837
5876
|
"noop": False,
|
|
@@ -5842,12 +5881,24 @@ class AiBuilderFacade:
|
|
|
5842
5881
|
if chart_visibility_read_errors
|
|
5843
5882
|
else []
|
|
5844
5883
|
)
|
|
5884
|
+
+ (
|
|
5885
|
+
[_warning("CHART_CONFIG_READ_PARTIAL", "some chart configs could not be read back; metrics/group_by/filters are incomplete for those charts")]
|
|
5886
|
+
if chart_config_read_errors
|
|
5887
|
+
else []
|
|
5888
|
+
)
|
|
5889
|
+
+ (
|
|
5890
|
+
[_warning("CHART_FILTER_FIELD_NAMES_UNRESOLVED", "chart configs were read, but form fields could not be loaded to resolve filter field names")]
|
|
5891
|
+
if field_name_read_error
|
|
5892
|
+
else []
|
|
5893
|
+
)
|
|
5845
5894
|
),
|
|
5846
5895
|
"verification": {
|
|
5847
5896
|
"app_exists": True,
|
|
5848
5897
|
"chart_order_verified": list_source == "sorted",
|
|
5849
5898
|
"chart_list_source": list_source,
|
|
5850
5899
|
"chart_visibility_readback_complete": not chart_visibility_read_errors,
|
|
5900
|
+
"chart_config_readback_complete": not chart_config_read_errors,
|
|
5901
|
+
"chart_filter_field_names_resolved": field_name_read_error is None,
|
|
5851
5902
|
},
|
|
5852
5903
|
"verified": True,
|
|
5853
5904
|
**response.model_dump(mode="json"),
|
|
@@ -6877,10 +6928,27 @@ class AiBuilderFacade:
|
|
|
6877
6928
|
suggested_next_call={"tool_name": "chart_get", "arguments": {"profile": profile, "chart_id": chart_id}},
|
|
6878
6929
|
)
|
|
6879
6930
|
|
|
6931
|
+
field_name_by_id: dict[str, str] = {}
|
|
6932
|
+
data_source = config.get("dataSource") if isinstance(config.get("dataSource"), dict) else {}
|
|
6933
|
+
data_source_app_key = str(data_source.get("dataSourceId") or config.get("dataSourceId") or "").strip()
|
|
6934
|
+
if data_source_app_key:
|
|
6935
|
+
field_name_by_id, field_name_error = self._chart_filter_field_names_by_id(profile=profile, app_key=data_source_app_key)
|
|
6936
|
+
if field_name_error:
|
|
6937
|
+
warnings.append(
|
|
6938
|
+
_warning(
|
|
6939
|
+
"CHART_FILTER_FIELD_NAMES_UNRESOLVED",
|
|
6940
|
+
"chart config was read, but form fields could not be loaded to resolve filter field names",
|
|
6941
|
+
**field_name_error,
|
|
6942
|
+
)
|
|
6943
|
+
)
|
|
6944
|
+
|
|
6880
6945
|
response = ChartGetResponse(
|
|
6881
6946
|
chart_id=chart_id,
|
|
6882
6947
|
base=deepcopy(base) if isinstance(base, dict) else {},
|
|
6883
6948
|
visibility=_public_visibility_from_chart_visible_auth(base.get("visibleAuth")),
|
|
6949
|
+
filters=_public_chart_filter_groups_from_qingbi_config(config, field_name_by_id=field_name_by_id) if isinstance(config, dict) else [],
|
|
6950
|
+
group_by=_public_chart_group_by_from_qingbi_config(config) if isinstance(config, dict) else [],
|
|
6951
|
+
metrics=_public_chart_metrics_from_qingbi_config(config) if isinstance(config, dict) else [],
|
|
6884
6952
|
config=deepcopy(config) if isinstance(config, dict) else {},
|
|
6885
6953
|
)
|
|
6886
6954
|
return {
|
|
@@ -7380,6 +7448,8 @@ class AiBuilderFacade:
|
|
|
7380
7448
|
add_fields: list[FieldPatch],
|
|
7381
7449
|
update_fields: list[FieldUpdatePatch],
|
|
7382
7450
|
remove_fields: list[FieldRemovePatch],
|
|
7451
|
+
layout_mode: LayoutApplyMode = LayoutApplyMode.merge,
|
|
7452
|
+
layout_sections: list[LayoutSectionPatch] | None = None,
|
|
7383
7453
|
) -> JSONObject:
|
|
7384
7454
|
normalized_args = {
|
|
7385
7455
|
"app_key": app_key,
|
|
@@ -7394,6 +7464,10 @@ class AiBuilderFacade:
|
|
|
7394
7464
|
"update_fields": [patch.model_dump(mode="json") for patch in update_fields],
|
|
7395
7465
|
"remove_fields": [patch.model_dump(mode="json") for patch in remove_fields],
|
|
7396
7466
|
}
|
|
7467
|
+
layout_requested = layout_sections is not None
|
|
7468
|
+
requested_layout_sections = [section.model_dump(mode="json", exclude_none=True) for section in (layout_sections or [])]
|
|
7469
|
+
if layout_requested:
|
|
7470
|
+
normalized_args["form"] = {"mode": layout_mode.value, "sections": deepcopy(requested_layout_sections)}
|
|
7397
7471
|
try:
|
|
7398
7472
|
desired_auth = (
|
|
7399
7473
|
self._compile_visibility_to_member_auth(profile=profile, visibility=visibility)
|
|
@@ -7409,7 +7483,7 @@ class AiBuilderFacade:
|
|
|
7409
7483
|
suggested_next_call=None,
|
|
7410
7484
|
)
|
|
7411
7485
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
7412
|
-
requested_field_changes = bool(add_fields or update_fields or remove_fields)
|
|
7486
|
+
requested_field_changes = bool(add_fields or update_fields or remove_fields or layout_requested)
|
|
7413
7487
|
resolved: JSONObject
|
|
7414
7488
|
if app_key:
|
|
7415
7489
|
resolved = self.app_resolve(profile=profile, app_key=app_key)
|
|
@@ -7533,7 +7607,6 @@ class AiBuilderFacade:
|
|
|
7533
7607
|
"verification": {
|
|
7534
7608
|
"fields_verified": True,
|
|
7535
7609
|
"package_attached": None if package_tag_id is None else package_tag_id in target.tag_ids,
|
|
7536
|
-
"relation_field_limit_verified": True,
|
|
7537
7610
|
"app_visuals_verified": True,
|
|
7538
7611
|
"app_base_verified": True,
|
|
7539
7612
|
"publish_skipped": True,
|
|
@@ -7573,6 +7646,9 @@ class AiBuilderFacade:
|
|
|
7573
7646
|
current_fields = parsed["fields"]
|
|
7574
7647
|
original_fields = deepcopy(current_fields)
|
|
7575
7648
|
layout = parsed["layout"]
|
|
7649
|
+
original_layout = deepcopy(layout)
|
|
7650
|
+
layout_write_requested = False
|
|
7651
|
+
applied_layout = deepcopy(layout)
|
|
7576
7652
|
existing_index = {field["field_id"]: index for index, field in enumerate(current_fields)}
|
|
7577
7653
|
selector_map = _build_selector_map(current_fields)
|
|
7578
7654
|
added: list[str] = []
|
|
@@ -7727,6 +7803,71 @@ class AiBuilderFacade:
|
|
|
7727
7803
|
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
7728
7804
|
)
|
|
7729
7805
|
|
|
7806
|
+
if layout_requested:
|
|
7807
|
+
resolved_sections, missing_selectors = _resolve_layout_sections_to_names(requested_layout_sections, current_fields)
|
|
7808
|
+
normalized_args["form"] = {"mode": layout_mode.value, "sections": deepcopy(resolved_sections)}
|
|
7809
|
+
if missing_selectors:
|
|
7810
|
+
return _failed(
|
|
7811
|
+
"UNKNOWN_LAYOUT_FIELD",
|
|
7812
|
+
"form layout references unknown field selectors",
|
|
7813
|
+
normalized_args=normalized_args,
|
|
7814
|
+
details={"unknown_selectors": missing_selectors, "app_key": target.app_key},
|
|
7815
|
+
missing_fields=[str(item) for item in missing_selectors],
|
|
7816
|
+
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
7817
|
+
)
|
|
7818
|
+
fields_by_name = {field["name"]: field for field in current_fields}
|
|
7819
|
+
seen_layout_fields: list[str] = []
|
|
7820
|
+
for section in resolved_sections:
|
|
7821
|
+
for row in section.get("rows", []):
|
|
7822
|
+
for field_name in row:
|
|
7823
|
+
if field_name not in fields_by_name:
|
|
7824
|
+
return _failed(
|
|
7825
|
+
"UNKNOWN_LAYOUT_FIELD",
|
|
7826
|
+
f"form layout references unknown field '{field_name}'",
|
|
7827
|
+
normalized_args=normalized_args,
|
|
7828
|
+
details={"field_name": field_name, "app_key": target.app_key},
|
|
7829
|
+
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
7830
|
+
)
|
|
7831
|
+
if field_name in seen_layout_fields:
|
|
7832
|
+
return _failed(
|
|
7833
|
+
"DUPLICATE_LAYOUT_FIELD",
|
|
7834
|
+
f"form layout references field '{field_name}' more than once",
|
|
7835
|
+
normalized_args=normalized_args,
|
|
7836
|
+
details={"field_name": field_name, "app_key": target.app_key},
|
|
7837
|
+
suggested_next_call={"tool_name": "app_schema_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
7838
|
+
)
|
|
7839
|
+
seen_layout_fields.append(field_name)
|
|
7840
|
+
expected_layout_fields = {field["name"] for field in current_fields}
|
|
7841
|
+
if layout_mode == LayoutApplyMode.replace and set(seen_layout_fields) != expected_layout_fields:
|
|
7842
|
+
missing = sorted(expected_layout_fields.difference(seen_layout_fields))
|
|
7843
|
+
return _failed(
|
|
7844
|
+
"INCOMPLETE_LAYOUT",
|
|
7845
|
+
"form layout must reference every current field exactly once in replace mode",
|
|
7846
|
+
normalized_args=normalized_args,
|
|
7847
|
+
details={
|
|
7848
|
+
"missing_fields": missing,
|
|
7849
|
+
"auto_fill_preview": _merge_layout(
|
|
7850
|
+
current_layout=layout,
|
|
7851
|
+
requested_sections=resolved_sections,
|
|
7852
|
+
all_field_names=[field["name"] for field in current_fields],
|
|
7853
|
+
)["layout"],
|
|
7854
|
+
},
|
|
7855
|
+
missing_fields=missing,
|
|
7856
|
+
suggested_next_call={"tool_name": "app_schema_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
7857
|
+
)
|
|
7858
|
+
merged_layout = _merge_layout(
|
|
7859
|
+
current_layout=layout,
|
|
7860
|
+
requested_sections=resolved_sections,
|
|
7861
|
+
all_field_names=[field["name"] for field in current_fields],
|
|
7862
|
+
)
|
|
7863
|
+
applied_layout = (
|
|
7864
|
+
{"root_rows": [], "sections": resolved_sections}
|
|
7865
|
+
if layout_mode == LayoutApplyMode.replace
|
|
7866
|
+
else merged_layout["layout"]
|
|
7867
|
+
)
|
|
7868
|
+
layout_write_requested = not _layouts_equal(layout, applied_layout)
|
|
7869
|
+
layout = applied_layout
|
|
7870
|
+
|
|
7730
7871
|
schema_write_requested = bool(
|
|
7731
7872
|
added
|
|
7732
7873
|
or updated
|
|
@@ -7734,6 +7875,7 @@ class AiBuilderFacade:
|
|
|
7734
7875
|
or normalized_code_block_fields
|
|
7735
7876
|
or data_display_selection.has_any
|
|
7736
7877
|
or bool(resolved.get("created"))
|
|
7878
|
+
or layout_write_requested
|
|
7737
7879
|
)
|
|
7738
7880
|
if schema_write_requested:
|
|
7739
7881
|
try:
|
|
@@ -7756,12 +7898,6 @@ class AiBuilderFacade:
|
|
|
7756
7898
|
)
|
|
7757
7899
|
|
|
7758
7900
|
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
7901
|
code_block_normalization_warnings = (
|
|
7766
7902
|
[
|
|
7767
7903
|
_warning(
|
|
@@ -7774,7 +7910,15 @@ class AiBuilderFacade:
|
|
|
7774
7910
|
else []
|
|
7775
7911
|
)
|
|
7776
7912
|
|
|
7777
|
-
if
|
|
7913
|
+
if (
|
|
7914
|
+
not added
|
|
7915
|
+
and not updated
|
|
7916
|
+
and not removed
|
|
7917
|
+
and not normalized_code_block_fields
|
|
7918
|
+
and not data_display_selection.has_any
|
|
7919
|
+
and not bool(resolved.get("created"))
|
|
7920
|
+
and not layout_write_requested
|
|
7921
|
+
):
|
|
7778
7922
|
base_info = self.apps.app_get_base(profile=profile, app_key=target.app_key, include_raw=True).get("result") or {}
|
|
7779
7923
|
tag_ids_after = _coerce_int_list(base_info.get("tagIds"))
|
|
7780
7924
|
package_attached = None if package_tag_id is None else package_tag_id in tag_ids_after
|
|
@@ -7796,10 +7940,10 @@ class AiBuilderFacade:
|
|
|
7796
7940
|
"request_id": None,
|
|
7797
7941
|
"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
7942
|
"noop": not bool(visual_result.get("updated")),
|
|
7799
|
-
"warnings":
|
|
7943
|
+
"warnings": code_block_normalization_warnings,
|
|
7800
7944
|
"verification": {
|
|
7801
7945
|
"fields_verified": True,
|
|
7802
|
-
"
|
|
7946
|
+
"form_layout_verified": True,
|
|
7803
7947
|
"app_visuals_verified": app_base_verified,
|
|
7804
7948
|
"app_base_verified": app_base_verified,
|
|
7805
7949
|
"relation_target_metadata_verified": relation_target_metadata_verified,
|
|
@@ -7817,6 +7961,12 @@ class AiBuilderFacade:
|
|
|
7817
7961
|
"package_attached": package_attached,
|
|
7818
7962
|
}
|
|
7819
7963
|
response["details"]["relation_field_count"] = relation_field_count
|
|
7964
|
+
if layout_requested:
|
|
7965
|
+
response["details"]["form_layout_diff"] = {
|
|
7966
|
+
"mode": layout_mode.value,
|
|
7967
|
+
"changed": False,
|
|
7968
|
+
"requested_section_count": len(requested_layout_sections),
|
|
7969
|
+
}
|
|
7820
7970
|
if normalized_code_block_fields:
|
|
7821
7971
|
response["normalized_code_block_output_assignment"] = True
|
|
7822
7972
|
response["normalized_code_block_fields"] = normalized_code_block_fields
|
|
@@ -7850,26 +8000,6 @@ class AiBuilderFacade:
|
|
|
7850
8000
|
self.apps.app_update_form_schema(profile=profile, app_key=target.app_key, payload=payload)
|
|
7851
8001
|
except (QingflowApiError, RuntimeError) as error:
|
|
7852
8002
|
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
8003
|
return _failed_from_api_error(
|
|
7874
8004
|
"SCHEMA_APPLY_FAILED",
|
|
7875
8005
|
api_error,
|
|
@@ -7993,13 +8123,13 @@ class AiBuilderFacade:
|
|
|
7993
8123
|
"request_id": None,
|
|
7994
8124
|
"suggested_next_call": None,
|
|
7995
8125
|
"noop": False,
|
|
7996
|
-
"warnings":
|
|
8126
|
+
"warnings": code_block_normalization_warnings,
|
|
7997
8127
|
"verification": {
|
|
7998
8128
|
"fields_verified": False,
|
|
7999
8129
|
"package_attached": None,
|
|
8000
8130
|
"app_visuals_verified": True,
|
|
8001
8131
|
"app_base_verified": True,
|
|
8002
|
-
"
|
|
8132
|
+
"form_layout_verified": not layout_requested,
|
|
8003
8133
|
"relation_target_metadata_verified": relation_target_metadata_verified,
|
|
8004
8134
|
},
|
|
8005
8135
|
"app_key": target.app_key,
|
|
@@ -8026,6 +8156,12 @@ class AiBuilderFacade:
|
|
|
8026
8156
|
"package_attached": None,
|
|
8027
8157
|
}
|
|
8028
8158
|
response["details"]["relation_field_count"] = relation_field_count
|
|
8159
|
+
if layout_requested:
|
|
8160
|
+
response["details"]["form_layout_diff"] = {
|
|
8161
|
+
"mode": layout_mode.value,
|
|
8162
|
+
"changed": layout_write_requested,
|
|
8163
|
+
"requested_section_count": len(requested_layout_sections),
|
|
8164
|
+
}
|
|
8029
8165
|
if normalized_code_block_fields:
|
|
8030
8166
|
response["normalized_code_block_output_assignment"] = True
|
|
8031
8167
|
response["normalized_code_block_fields"] = normalized_code_block_fields
|
|
@@ -8048,6 +8184,11 @@ class AiBuilderFacade:
|
|
|
8048
8184
|
after_fields=cast(list[dict[str, Any]], verified["schema"]["fields"]),
|
|
8049
8185
|
)
|
|
8050
8186
|
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)
|
|
8187
|
+
if layout_requested:
|
|
8188
|
+
verified_layout = cast(dict[str, Any], verified.get("layout") or {"root_rows": [], "sections": []})
|
|
8189
|
+
layout_verified = _layouts_equal(verified_layout, applied_layout) or _layouts_semantically_equal(verified_layout, applied_layout)
|
|
8190
|
+
response["verification"]["form_layout_verified"] = layout_verified
|
|
8191
|
+
verification_ok = verification_ok and layout_verified
|
|
8051
8192
|
data_display_verification = _verify_data_display_readback(
|
|
8052
8193
|
form_settings=verified.get("form_settings"),
|
|
8053
8194
|
selection=data_display_selection,
|
|
@@ -8091,7 +8232,6 @@ class AiBuilderFacade:
|
|
|
8091
8232
|
response["verification"]["package_attached"] = package_attached
|
|
8092
8233
|
response["verification"]["app_visuals_verified"] = app_base_verified
|
|
8093
8234
|
response["verification"]["app_base_verified"] = app_base_verified
|
|
8094
|
-
response["verification"]["relation_field_limit_verified"] = relation_limit_verified
|
|
8095
8235
|
response["verified"] = verification_ok and app_base_verified
|
|
8096
8236
|
response["tag_ids_after"] = tag_ids_after
|
|
8097
8237
|
response["package_attached"] = package_attached
|
|
@@ -10304,6 +10444,7 @@ class AiBuilderFacade:
|
|
|
10304
10444
|
)
|
|
10305
10445
|
if existing is None:
|
|
10306
10446
|
temp_chart_id = str(patch.chart_id or f"mcp_{uuid4().hex[:16]}")
|
|
10447
|
+
chart_id = temp_chart_id
|
|
10307
10448
|
create_payload = {
|
|
10308
10449
|
"chartId": temp_chart_id,
|
|
10309
10450
|
"chartName": patch.name,
|
|
@@ -13693,7 +13834,18 @@ def _build_qingbi_chart_field_lookup(
|
|
|
13693
13834
|
field_lookup: dict[str, dict[str, Any]],
|
|
13694
13835
|
) -> dict[str, Any]:
|
|
13695
13836
|
by_selector: dict[str, list[dict[str, Any]]] = {}
|
|
13696
|
-
form_by_que_id = field_lookup.get("by_que_id") or {}
|
|
13837
|
+
form_by_que_id = dict(field_lookup.get("by_que_id") or {})
|
|
13838
|
+
if not form_by_que_id:
|
|
13839
|
+
for bucket_name in ("by_name", "by_field_id"):
|
|
13840
|
+
bucket = field_lookup.get(bucket_name) or {}
|
|
13841
|
+
if not isinstance(bucket, dict):
|
|
13842
|
+
continue
|
|
13843
|
+
for form_field in bucket.values():
|
|
13844
|
+
if not isinstance(form_field, dict):
|
|
13845
|
+
continue
|
|
13846
|
+
que_id = _coerce_any_int(form_field.get("que_id"))
|
|
13847
|
+
if que_id is not None:
|
|
13848
|
+
form_by_que_id.setdefault(que_id, form_field)
|
|
13697
13849
|
|
|
13698
13850
|
def add_selector(key: Any, field: dict[str, Any]) -> None:
|
|
13699
13851
|
normalized = str(key or "").strip()
|
|
@@ -14082,37 +14234,80 @@ def _build_public_metric_fields(
|
|
|
14082
14234
|
metrics: list[dict[str, Any]] = []
|
|
14083
14235
|
for selector in selectors:
|
|
14084
14236
|
qingbi_field = _resolve_qingbi_chart_field(selector, chart_field_lookup=chart_field_lookup, chart_type=chart_type, role="metric")
|
|
14085
|
-
|
|
14086
|
-
|
|
14087
|
-
|
|
14237
|
+
metrics.append(_public_qingbi_metric_field(qingbi_field, aggregate=normalized_aggregate))
|
|
14238
|
+
return metrics or [_default_public_total_metric()]
|
|
14239
|
+
|
|
14240
|
+
|
|
14241
|
+
def _public_qingbi_metric_field(qingbi_field: dict[str, Any], *, aggregate: str) -> dict[str, Any]:
|
|
14242
|
+
field_id = _chart_field_id(qingbi_field)
|
|
14243
|
+
if field_id == _QINGBI_TOTAL_FIELD_ID:
|
|
14244
|
+
return deepcopy(qingbi_field)
|
|
14245
|
+
form_field = qingbi_field.get("_public_form_field") if isinstance(qingbi_field.get("_public_form_field"), dict) else {}
|
|
14246
|
+
normalized_aggregate = str(aggregate or "sum").strip().lower()
|
|
14247
|
+
aggre_type = {"sum": "sum", "avg": "avg", "average": "avg", "max": "max", "min": "min"}.get(normalized_aggregate, "sum")
|
|
14248
|
+
return {
|
|
14249
|
+
"fieldId": field_id,
|
|
14250
|
+
"fieldName": qingbi_field.get("fieldName") or form_field.get("name") or field_id,
|
|
14251
|
+
"fieldType": qingbi_field.get("fieldType") or _qingbi_field_type_from_public_field(str(form_field.get("type") or "")),
|
|
14252
|
+
"orderType": "default",
|
|
14253
|
+
"alignType": "left",
|
|
14254
|
+
"dateFormat": "yyyy-MM-dd",
|
|
14255
|
+
"numberFormat": "default",
|
|
14256
|
+
"numberConfig": {"format": "splitter", "unit": "DEFAULT", "prefix": "", "suffix": "", "digit": None},
|
|
14257
|
+
"digit": None,
|
|
14258
|
+
"aggreType": aggre_type,
|
|
14259
|
+
"orderPriority": None,
|
|
14260
|
+
"width": None,
|
|
14261
|
+
"verticalAlign": "middle",
|
|
14262
|
+
"formula": qingbi_field.get("formula"),
|
|
14263
|
+
"fieldSource": qingbi_field.get("fieldSource") or "default",
|
|
14264
|
+
"status": qingbi_field.get("status"),
|
|
14265
|
+
"supId": qingbi_field.get("supId"),
|
|
14266
|
+
"beingTable": bool(qingbi_field.get("beingTable", False)),
|
|
14267
|
+
"returnType": qingbi_field.get("returnType"),
|
|
14268
|
+
"biFormulaType": qingbi_field.get("biFormulaType"),
|
|
14269
|
+
"aggreFieldId": qingbi_field.get("aggreFieldId"),
|
|
14270
|
+
}
|
|
14271
|
+
|
|
14272
|
+
|
|
14273
|
+
def _build_public_semantic_metric_fields(
|
|
14274
|
+
metrics: list[ChartMetricPatch],
|
|
14275
|
+
*,
|
|
14276
|
+
app_key: str,
|
|
14277
|
+
field_lookup: dict[str, dict[str, Any]],
|
|
14278
|
+
chart_field_lookup: dict[str, Any],
|
|
14279
|
+
qingbi_fields_by_id: dict[str, dict[str, Any]],
|
|
14280
|
+
chart_type: str = "chart",
|
|
14281
|
+
) -> list[dict[str, Any]]:
|
|
14282
|
+
if not metrics:
|
|
14283
|
+
return [_default_public_total_metric()]
|
|
14284
|
+
selected_metrics: list[dict[str, Any]] = []
|
|
14285
|
+
for metric in metrics:
|
|
14286
|
+
op = str(metric.op or "count").strip().lower()
|
|
14287
|
+
field_name = str(metric.field_name or "").strip()
|
|
14288
|
+
if op == "count":
|
|
14289
|
+
if field_name:
|
|
14290
|
+
_raise_chart_rule(
|
|
14291
|
+
rule_code="CHART_COUNT_FIELD_UNSUPPORTED",
|
|
14292
|
+
chart_type=chart_type,
|
|
14293
|
+
message="count metric currently supports count(*) only",
|
|
14294
|
+
expected='Use metric: "count(*)" or {"op": "count"} for record count.',
|
|
14295
|
+
actual={"metric": metric.model_dump(mode="json")},
|
|
14296
|
+
next_action='Use count(*) for count cards; use sum(field), avg(field), max(field), or min(field) for field aggregation.',
|
|
14297
|
+
)
|
|
14298
|
+
selected_metrics.append(_default_public_total_metric())
|
|
14088
14299
|
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
|
-
}
|
|
14300
|
+
qingbi_field = _resolve_qingbi_chart_field(
|
|
14301
|
+
field_name,
|
|
14302
|
+
chart_field_lookup=chart_field_lookup,
|
|
14303
|
+
chart_type=chart_type,
|
|
14304
|
+
role="metric",
|
|
14114
14305
|
)
|
|
14115
|
-
|
|
14306
|
+
metric_payload = _public_qingbi_metric_field(qingbi_field, aggregate=op)
|
|
14307
|
+
if metric.alias:
|
|
14308
|
+
metric_payload["fieldName"] = metric.alias
|
|
14309
|
+
selected_metrics.append(metric_payload)
|
|
14310
|
+
return selected_metrics or [_default_public_total_metric()]
|
|
14116
14311
|
|
|
14117
14312
|
|
|
14118
14313
|
def _split_axis_metric_fields(metrics: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
@@ -14134,73 +14329,6 @@ def _two_gauge_metric_fields(metrics: list[dict[str, Any]], *, requested_metric_
|
|
|
14134
14329
|
return [deepcopy(metrics[0]), deepcopy(metrics[1])]
|
|
14135
14330
|
|
|
14136
14331
|
|
|
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
14332
|
def _build_public_chart_filter_matrix(
|
|
14205
14333
|
rules: list[Any],
|
|
14206
14334
|
*,
|
|
@@ -14213,6 +14341,16 @@ def _build_public_chart_filter_matrix(
|
|
|
14213
14341
|
if not rules:
|
|
14214
14342
|
return []
|
|
14215
14343
|
group: list[dict[str, Any]] = []
|
|
14344
|
+
judge_map = {
|
|
14345
|
+
ViewFilterOperator.eq.value: "equal",
|
|
14346
|
+
ViewFilterOperator.neq.value: "unequal",
|
|
14347
|
+
ViewFilterOperator.gte.value: "greaterOrEqual",
|
|
14348
|
+
ViewFilterOperator.lte.value: "lessOrEqual",
|
|
14349
|
+
ViewFilterOperator.in_.value: "anyMatch",
|
|
14350
|
+
ViewFilterOperator.contains.value: "include",
|
|
14351
|
+
ViewFilterOperator.is_empty.value: "isNull",
|
|
14352
|
+
ViewFilterOperator.not_empty.value: "notNull",
|
|
14353
|
+
}
|
|
14216
14354
|
for rule in rules:
|
|
14217
14355
|
qingbi_field = _resolve_qingbi_chart_field(
|
|
14218
14356
|
getattr(rule, "field_name", None),
|
|
@@ -14224,21 +14362,301 @@ def _build_public_chart_filter_matrix(
|
|
|
14224
14362
|
form_field = qingbi_field.get("_public_form_field") if isinstance(qingbi_field.get("_public_form_field"), dict) else {}
|
|
14225
14363
|
operator = str(getattr(rule, "operator", ViewFilterOperator.eq.value).value if hasattr(getattr(rule, "operator", None), "value") else getattr(rule, "operator", ViewFilterOperator.eq.value))
|
|
14226
14364
|
values = list(getattr(rule, "values", []) or [])
|
|
14227
|
-
text_values = _qingbi_filter_text_values(values, form_field=form_field, qingbi_field=qingbi_field)
|
|
14228
14365
|
group.append(
|
|
14229
14366
|
{
|
|
14230
14367
|
"fieldId": field_id,
|
|
14231
14368
|
"fieldName": qingbi_field.get("fieldName") or form_field.get("name") or field_id,
|
|
14232
14369
|
"fieldType": qingbi_field.get("fieldType") or _qingbi_field_type_from_public_field(str(form_field.get("type") or "")),
|
|
14233
|
-
"judgeType":
|
|
14234
|
-
"judgeValue":
|
|
14235
|
-
|
|
14370
|
+
"judgeType": judge_map.get(operator, "equal"),
|
|
14371
|
+
"judgeValue": _build_qingbi_chart_filter_judge_value(
|
|
14372
|
+
operator=operator,
|
|
14373
|
+
values=values,
|
|
14374
|
+
form_field=form_field,
|
|
14375
|
+
chart_type=chart_type,
|
|
14376
|
+
),
|
|
14377
|
+
"judgeValueDetailList": [],
|
|
14236
14378
|
"matchType": 1,
|
|
14237
14379
|
}
|
|
14238
14380
|
)
|
|
14239
14381
|
return [group] if group else []
|
|
14240
14382
|
|
|
14241
14383
|
|
|
14384
|
+
def _build_qingbi_chart_filter_judge_value(
|
|
14385
|
+
*,
|
|
14386
|
+
operator: str,
|
|
14387
|
+
values: list[Any],
|
|
14388
|
+
form_field: dict[str, Any],
|
|
14389
|
+
chart_type: str,
|
|
14390
|
+
) -> str | None:
|
|
14391
|
+
if operator in {ViewFilterOperator.is_empty.value, ViewFilterOperator.not_empty.value}:
|
|
14392
|
+
return None
|
|
14393
|
+
if operator == ViewFilterOperator.in_.value:
|
|
14394
|
+
return "<&&>".join(
|
|
14395
|
+
_qingbi_chart_filter_value_to_text(value=value, form_field=form_field, chart_type=chart_type) for value in values
|
|
14396
|
+
)
|
|
14397
|
+
if not values:
|
|
14398
|
+
return ""
|
|
14399
|
+
return _qingbi_chart_filter_value_to_text(value=values[0], form_field=form_field, chart_type=chart_type)
|
|
14400
|
+
|
|
14401
|
+
|
|
14402
|
+
def _qingbi_chart_filter_value_to_text(*, value: Any, form_field: dict[str, Any], chart_type: str) -> str:
|
|
14403
|
+
option_details = [
|
|
14404
|
+
item
|
|
14405
|
+
for item in (form_field.get("option_details") or [])
|
|
14406
|
+
if isinstance(item, dict) and item.get("id") is not None and item.get("value") is not None
|
|
14407
|
+
]
|
|
14408
|
+
if not option_details:
|
|
14409
|
+
if isinstance(value, dict):
|
|
14410
|
+
for key in ("value", "label", "name", "title"):
|
|
14411
|
+
raw = str(value.get(key) or "").strip()
|
|
14412
|
+
if raw:
|
|
14413
|
+
return raw
|
|
14414
|
+
if value.get("id") is not None:
|
|
14415
|
+
return str(value.get("id"))
|
|
14416
|
+
return _stringify_condition_value(value)
|
|
14417
|
+
|
|
14418
|
+
option_by_value = {str(item.get("value") or "").strip(): item for item in option_details if str(item.get("value") or "").strip()}
|
|
14419
|
+
option_by_id = {str(item.get("id")): item for item in option_details if item.get("id") is not None}
|
|
14420
|
+
|
|
14421
|
+
def resolve(raw: Any) -> str | None:
|
|
14422
|
+
text = _stringify_condition_value(raw).strip()
|
|
14423
|
+
if not text:
|
|
14424
|
+
return None
|
|
14425
|
+
matched = option_by_value.get(text) or option_by_id.get(text)
|
|
14426
|
+
if not matched:
|
|
14427
|
+
return None
|
|
14428
|
+
return str(matched.get("value") or "").strip()
|
|
14429
|
+
|
|
14430
|
+
def reject(raw: Any) -> NoReturn:
|
|
14431
|
+
allowed = [
|
|
14432
|
+
{"id": str(item.get("id")), "value": str(item.get("value"))}
|
|
14433
|
+
for item in option_details
|
|
14434
|
+
]
|
|
14435
|
+
_raise_chart_rule(
|
|
14436
|
+
rule_code="CHART_FILTER_OPTION_VALUE_UNSUPPORTED",
|
|
14437
|
+
chart_type=chart_type,
|
|
14438
|
+
message="chart filter value is not in the field option list",
|
|
14439
|
+
expected="Use an existing option label or option id from the target field schema.",
|
|
14440
|
+
actual={
|
|
14441
|
+
"field_name": form_field.get("name"),
|
|
14442
|
+
"field_id": form_field.get("que_id"),
|
|
14443
|
+
"value": raw,
|
|
14444
|
+
"allowed_values": allowed,
|
|
14445
|
+
},
|
|
14446
|
+
offending_fields=[{"field_name": form_field.get("name"), "field_id": form_field.get("que_id"), "options": allowed}],
|
|
14447
|
+
next_action="Read schema first, then pass one of the allowed option labels or option ids.",
|
|
14448
|
+
)
|
|
14449
|
+
|
|
14450
|
+
if isinstance(value, dict):
|
|
14451
|
+
for key in ("value", "label", "name", "title"):
|
|
14452
|
+
if value.get(key) is not None:
|
|
14453
|
+
resolved = resolve(value.get(key))
|
|
14454
|
+
if resolved is not None:
|
|
14455
|
+
return resolved
|
|
14456
|
+
reject(value.get(key))
|
|
14457
|
+
if value.get("id") is not None:
|
|
14458
|
+
resolved = resolve(value.get("id"))
|
|
14459
|
+
if resolved is not None:
|
|
14460
|
+
return resolved
|
|
14461
|
+
reject(value.get("id"))
|
|
14462
|
+
reject(value)
|
|
14463
|
+
if isinstance(value, int):
|
|
14464
|
+
resolved = resolve(value)
|
|
14465
|
+
if resolved is not None:
|
|
14466
|
+
return resolved
|
|
14467
|
+
reject(value)
|
|
14468
|
+
resolved = resolve(value)
|
|
14469
|
+
if resolved is not None:
|
|
14470
|
+
return resolved
|
|
14471
|
+
reject(value)
|
|
14472
|
+
|
|
14473
|
+
|
|
14474
|
+
def _chart_field_names_by_id_from_public_fields(*, app_key: str, fields: list[dict[str, Any]]) -> dict[str, str]:
|
|
14475
|
+
field_name_by_id: dict[str, str] = {}
|
|
14476
|
+
for field in fields:
|
|
14477
|
+
if not isinstance(field, dict):
|
|
14478
|
+
continue
|
|
14479
|
+
name = str(field.get("name") or "").strip()
|
|
14480
|
+
if not name:
|
|
14481
|
+
continue
|
|
14482
|
+
que_id = field.get("que_id")
|
|
14483
|
+
field_id = str(field.get("field_id") or "").strip()
|
|
14484
|
+
for raw_key in (
|
|
14485
|
+
que_id,
|
|
14486
|
+
field_id,
|
|
14487
|
+
f"{app_key}:{que_id}" if que_id is not None else None,
|
|
14488
|
+
f"{app_key}:{field_id}" if field_id else None,
|
|
14489
|
+
):
|
|
14490
|
+
key = str(raw_key or "").strip()
|
|
14491
|
+
if key:
|
|
14492
|
+
field_name_by_id.setdefault(key, name)
|
|
14493
|
+
return field_name_by_id
|
|
14494
|
+
|
|
14495
|
+
|
|
14496
|
+
def _public_chart_filter_groups_from_qingbi_config(
|
|
14497
|
+
config: dict[str, Any],
|
|
14498
|
+
*,
|
|
14499
|
+
field_name_by_id: dict[str, str] | None = None,
|
|
14500
|
+
) -> list[list[dict[str, Any]]]:
|
|
14501
|
+
groups: list[list[dict[str, Any]]] = []
|
|
14502
|
+
raw_groups = config.get("beforeAggregationFilterMatrix")
|
|
14503
|
+
if not isinstance(raw_groups, list):
|
|
14504
|
+
return groups
|
|
14505
|
+
resolved_field_name_by_id = field_name_by_id or {}
|
|
14506
|
+
for raw_group in raw_groups:
|
|
14507
|
+
if not isinstance(raw_group, list):
|
|
14508
|
+
continue
|
|
14509
|
+
group: list[dict[str, Any]] = []
|
|
14510
|
+
for raw_rule in raw_group:
|
|
14511
|
+
if not isinstance(raw_rule, dict):
|
|
14512
|
+
continue
|
|
14513
|
+
operator = _public_chart_filter_operator_from_judge_type(raw_rule.get("judgeType"))
|
|
14514
|
+
field_id = raw_rule.get("fieldId") or raw_rule.get("field_id")
|
|
14515
|
+
field_id_text = _stringify_condition_value(field_id).strip() if field_id is not None else ""
|
|
14516
|
+
raw_field_name = (
|
|
14517
|
+
raw_rule.get("fieldName")
|
|
14518
|
+
or raw_rule.get("field_name")
|
|
14519
|
+
or raw_rule.get("queTitle")
|
|
14520
|
+
or raw_rule.get("title")
|
|
14521
|
+
)
|
|
14522
|
+
raw_field_name_text = _stringify_condition_value(raw_field_name).strip()
|
|
14523
|
+
field_name = (
|
|
14524
|
+
resolved_field_name_by_id.get(raw_field_name_text)
|
|
14525
|
+
or resolved_field_name_by_id.get(field_id_text)
|
|
14526
|
+
or raw_field_name_text
|
|
14527
|
+
or field_id_text
|
|
14528
|
+
)
|
|
14529
|
+
public_rule: dict[str, Any] = {
|
|
14530
|
+
"field_name": field_name,
|
|
14531
|
+
"operator": operator,
|
|
14532
|
+
}
|
|
14533
|
+
if field_id is not None:
|
|
14534
|
+
public_rule["field_id"] = field_id_text
|
|
14535
|
+
values = _public_chart_filter_values_from_rule(raw_rule, operator=operator)
|
|
14536
|
+
if values:
|
|
14537
|
+
public_rule["values"] = values
|
|
14538
|
+
group.append(public_rule)
|
|
14539
|
+
if group:
|
|
14540
|
+
groups.append(group)
|
|
14541
|
+
return groups
|
|
14542
|
+
|
|
14543
|
+
|
|
14544
|
+
def _public_chart_group_by_from_qingbi_config(config: dict[str, Any]) -> list[str]:
|
|
14545
|
+
fields: list[dict[str, Any]] = []
|
|
14546
|
+
for key in ("selectedDimensions", "xDimensions", "yDimensions", "selectedTime"):
|
|
14547
|
+
fields.extend(_chart_fields(config, key))
|
|
14548
|
+
group_by: list[str] = []
|
|
14549
|
+
seen: set[str] = set()
|
|
14550
|
+
for field in fields:
|
|
14551
|
+
name = _stringify_condition_value(
|
|
14552
|
+
field.get("fieldName")
|
|
14553
|
+
or field.get("field_name")
|
|
14554
|
+
or field.get("queTitle")
|
|
14555
|
+
or field.get("title")
|
|
14556
|
+
or field.get("fieldId")
|
|
14557
|
+
or field.get("field_id")
|
|
14558
|
+
).strip()
|
|
14559
|
+
if not name or name in seen:
|
|
14560
|
+
continue
|
|
14561
|
+
seen.add(name)
|
|
14562
|
+
group_by.append(name)
|
|
14563
|
+
return group_by
|
|
14564
|
+
|
|
14565
|
+
|
|
14566
|
+
def _public_chart_metrics_from_qingbi_config(config: dict[str, Any]) -> list[dict[str, Any]]:
|
|
14567
|
+
fields: list[dict[str, Any]] = []
|
|
14568
|
+
for key in ("selectedMetrics", "xMetrics", "yMetrics", "leftMetrics", "rightMetrics"):
|
|
14569
|
+
fields.extend(_chart_fields(config, key))
|
|
14570
|
+
metrics: list[dict[str, Any]] = []
|
|
14571
|
+
seen: set[tuple[str, str]] = set()
|
|
14572
|
+
for field in fields:
|
|
14573
|
+
field_id = _chart_field_id(field)
|
|
14574
|
+
if field_id == _QINGBI_TOTAL_FIELD_ID:
|
|
14575
|
+
metric = {"op": "count", "expr": "count(*)"}
|
|
14576
|
+
else:
|
|
14577
|
+
op = str(field.get("aggreType") or field.get("aggregate") or "sum").strip().lower()
|
|
14578
|
+
if op == "average":
|
|
14579
|
+
op = "avg"
|
|
14580
|
+
field_name = _stringify_condition_value(
|
|
14581
|
+
field.get("fieldName")
|
|
14582
|
+
or field.get("field_name")
|
|
14583
|
+
or field.get("queTitle")
|
|
14584
|
+
or field.get("title")
|
|
14585
|
+
or field_id
|
|
14586
|
+
).strip()
|
|
14587
|
+
metric = {"op": op or "sum", "field_name": field_name}
|
|
14588
|
+
if field_id:
|
|
14589
|
+
metric["field_id"] = field_id
|
|
14590
|
+
metric["expr"] = f"{metric['op']}({field_name})" if field_name else metric["op"]
|
|
14591
|
+
identity = (str(metric.get("op") or ""), str(metric.get("field_id") or metric.get("field_name") or metric.get("expr") or ""))
|
|
14592
|
+
if identity in seen:
|
|
14593
|
+
continue
|
|
14594
|
+
seen.add(identity)
|
|
14595
|
+
metrics.append(metric)
|
|
14596
|
+
return metrics
|
|
14597
|
+
|
|
14598
|
+
|
|
14599
|
+
def _public_chart_filter_operator_from_judge_type(judge_type: Any) -> str:
|
|
14600
|
+
normalized = _stringify_condition_value(judge_type).strip()
|
|
14601
|
+
mapping = {
|
|
14602
|
+
"equal": "eq",
|
|
14603
|
+
"equals": "eq",
|
|
14604
|
+
"=": "eq",
|
|
14605
|
+
"unequal": "neq",
|
|
14606
|
+
"notEqual": "neq",
|
|
14607
|
+
"!=": "neq",
|
|
14608
|
+
"anyMatch": "in",
|
|
14609
|
+
"oneOf": "in",
|
|
14610
|
+
"include": "contains",
|
|
14611
|
+
"contains": "contains",
|
|
14612
|
+
"greaterOrEqual": "gte",
|
|
14613
|
+
"gte": "gte",
|
|
14614
|
+
"lessOrEqual": "lte",
|
|
14615
|
+
"lte": "lte",
|
|
14616
|
+
"isNull": "is_empty",
|
|
14617
|
+
"empty": "is_empty",
|
|
14618
|
+
"notNull": "not_empty",
|
|
14619
|
+
"not_empty": "not_empty",
|
|
14620
|
+
}
|
|
14621
|
+
if normalized in mapping:
|
|
14622
|
+
return mapping[normalized]
|
|
14623
|
+
numeric = _coerce_nonnegative_int(judge_type)
|
|
14624
|
+
if numeric is not None:
|
|
14625
|
+
return _public_view_filter_operator_from_judge_type(numeric)
|
|
14626
|
+
return f"judge_type:{judge_type}"
|
|
14627
|
+
|
|
14628
|
+
|
|
14629
|
+
def _public_chart_filter_values_from_rule(rule: dict[str, Any], *, operator: str) -> list[str]:
|
|
14630
|
+
if operator in {"is_empty", "not_empty"}:
|
|
14631
|
+
return []
|
|
14632
|
+
raw_value = rule.get("judgeValue")
|
|
14633
|
+
if raw_value is None and "judgeValues" in rule:
|
|
14634
|
+
raw_value = rule.get("judgeValues")
|
|
14635
|
+
values: list[str] = []
|
|
14636
|
+
if isinstance(raw_value, list):
|
|
14637
|
+
values = [_stringify_condition_value(value).strip() for value in raw_value]
|
|
14638
|
+
elif isinstance(raw_value, str) and "<&&>" in raw_value:
|
|
14639
|
+
values = [value.strip() for value in raw_value.split("<&&>")]
|
|
14640
|
+
elif raw_value is not None:
|
|
14641
|
+
values = [_stringify_condition_value(raw_value).strip()]
|
|
14642
|
+
values = [value for value in values if value]
|
|
14643
|
+
if values:
|
|
14644
|
+
return values
|
|
14645
|
+
for detail_key in ("judgeValueDetailList", "judgeValueDetails"):
|
|
14646
|
+
details = rule.get(detail_key)
|
|
14647
|
+
if not isinstance(details, list):
|
|
14648
|
+
continue
|
|
14649
|
+
for detail in details:
|
|
14650
|
+
if not isinstance(detail, dict):
|
|
14651
|
+
continue
|
|
14652
|
+
for value_key in ("value", "label", "name", "title", "dataValue", "id"):
|
|
14653
|
+
value = _stringify_condition_value(detail.get(value_key)).strip()
|
|
14654
|
+
if value:
|
|
14655
|
+
values.append(value)
|
|
14656
|
+
break
|
|
14657
|
+
return values
|
|
14658
|
+
|
|
14659
|
+
|
|
14242
14660
|
def _build_public_chart_config_payload(
|
|
14243
14661
|
*,
|
|
14244
14662
|
patch: ChartUpsertPatch,
|
|
@@ -14249,9 +14667,14 @@ def _build_public_chart_config_payload(
|
|
|
14249
14667
|
) -> dict[str, Any]:
|
|
14250
14668
|
config = deepcopy(patch.config)
|
|
14251
14669
|
explicit_fields = set(getattr(patch, "model_fields_set", set()) or set())
|
|
14252
|
-
|
|
14670
|
+
semantic_dimension_fields = bool({"dimension_field_ids", "group_by", "rows", "columns"} & explicit_fields)
|
|
14671
|
+
semantic_metric_fields = bool(
|
|
14672
|
+
{"indicator_field_ids", "metric", "metrics", "x_metric", "y_metric", "left_metric", "right_metric", "value_metric", "target_metric"}
|
|
14673
|
+
& explicit_fields
|
|
14674
|
+
)
|
|
14675
|
+
if semantic_dimension_fields:
|
|
14253
14676
|
config.pop("selectedDimensions", None)
|
|
14254
|
-
if
|
|
14677
|
+
if semantic_metric_fields:
|
|
14255
14678
|
config.pop("selectedMetrics", None)
|
|
14256
14679
|
if "filters" in explicit_fields:
|
|
14257
14680
|
config.pop("beforeAggregationFilterMatrix", None)
|
|
@@ -14277,8 +14700,8 @@ def _build_public_chart_config_payload(
|
|
|
14277
14700
|
)
|
|
14278
14701
|
query_condition_field_ids.append(_chart_field_id(field))
|
|
14279
14702
|
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
|
|
14703
|
+
if backend_chart_type == "gauge" and not patch.indicator_field_ids and not patch.metrics and "selectedMetrics" not in config:
|
|
14704
|
+
raise ValueError("gauge charts require at least one metric; pass value_metric or metric and the CLI will pair it with 数据总量")
|
|
14282
14705
|
selected_dimensions = _build_public_dimension_fields(
|
|
14283
14706
|
patch.dimension_field_ids,
|
|
14284
14707
|
app_key=app_key,
|
|
@@ -14287,15 +14710,25 @@ def _build_public_chart_config_payload(
|
|
|
14287
14710
|
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
14288
14711
|
chart_type=patch.chart_type.value,
|
|
14289
14712
|
)
|
|
14290
|
-
|
|
14291
|
-
|
|
14292
|
-
|
|
14293
|
-
|
|
14294
|
-
|
|
14295
|
-
|
|
14296
|
-
|
|
14297
|
-
|
|
14298
|
-
|
|
14713
|
+
if patch.metrics:
|
|
14714
|
+
selected_metrics = _build_public_semantic_metric_fields(
|
|
14715
|
+
patch.metrics,
|
|
14716
|
+
app_key=app_key,
|
|
14717
|
+
field_lookup=field_lookup,
|
|
14718
|
+
chart_field_lookup=chart_field_lookup,
|
|
14719
|
+
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
14720
|
+
chart_type=patch.chart_type.value,
|
|
14721
|
+
)
|
|
14722
|
+
else:
|
|
14723
|
+
selected_metrics = _build_public_metric_fields(
|
|
14724
|
+
patch.indicator_field_ids,
|
|
14725
|
+
app_key=app_key,
|
|
14726
|
+
field_lookup=field_lookup,
|
|
14727
|
+
chart_field_lookup=chart_field_lookup,
|
|
14728
|
+
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
14729
|
+
aggregate=aggregate,
|
|
14730
|
+
chart_type=patch.chart_type.value,
|
|
14731
|
+
)
|
|
14299
14732
|
payload: dict[str, Any] = {
|
|
14300
14733
|
"chartName": patch.name,
|
|
14301
14734
|
"chartType": backend_chart_type,
|
|
@@ -14320,7 +14753,15 @@ def _build_public_chart_config_payload(
|
|
|
14320
14753
|
if backend_chart_type == "summary":
|
|
14321
14754
|
payload.pop("selectedDimensions", None)
|
|
14322
14755
|
payload.setdefault("xDimensions", deepcopy(selected_dimensions))
|
|
14323
|
-
|
|
14756
|
+
y_dimensions = _build_public_dimension_fields(
|
|
14757
|
+
patch.columns,
|
|
14758
|
+
app_key=app_key,
|
|
14759
|
+
field_lookup=field_lookup,
|
|
14760
|
+
chart_field_lookup=chart_field_lookup,
|
|
14761
|
+
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
14762
|
+
chart_type=patch.chart_type.value,
|
|
14763
|
+
)
|
|
14764
|
+
payload.setdefault("yDimensions", y_dimensions)
|
|
14324
14765
|
elif backend_chart_type == "scatter":
|
|
14325
14766
|
x_metrics, y_metrics = _split_axis_metric_fields(selected_metrics)
|
|
14326
14767
|
payload.pop("selectedMetrics", None)
|
|
@@ -14354,7 +14795,26 @@ def _build_public_chart_config_payload(
|
|
|
14354
14795
|
|
|
14355
14796
|
def _chart_patch_updates_chart_config(patch: ChartUpsertPatch) -> bool:
|
|
14356
14797
|
explicit_fields = set(getattr(patch, "model_fields_set", set()) or set())
|
|
14357
|
-
return bool(
|
|
14798
|
+
return bool(
|
|
14799
|
+
{
|
|
14800
|
+
"dimension_field_ids",
|
|
14801
|
+
"indicator_field_ids",
|
|
14802
|
+
"group_by",
|
|
14803
|
+
"rows",
|
|
14804
|
+
"columns",
|
|
14805
|
+
"metric",
|
|
14806
|
+
"metrics",
|
|
14807
|
+
"x_metric",
|
|
14808
|
+
"y_metric",
|
|
14809
|
+
"left_metric",
|
|
14810
|
+
"right_metric",
|
|
14811
|
+
"value_metric",
|
|
14812
|
+
"target_metric",
|
|
14813
|
+
"filters",
|
|
14814
|
+
"config",
|
|
14815
|
+
}
|
|
14816
|
+
& explicit_fields
|
|
14817
|
+
)
|
|
14358
14818
|
|
|
14359
14819
|
|
|
14360
14820
|
def _chart_patch_dataset_source_type(patch: ChartUpsertPatch) -> str:
|