@qingflow-tech/qingflow-app-user-mcp 1.0.8 → 1.0.9
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 +2 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/builder_facade/models.py +34 -2
- package/src/qingflow_mcp/builder_facade/service.py +253 -56
- package/src/qingflow_mcp/server_app_builder.py +2 -1
- package/src/qingflow_mcp/tools/ai_builder_tools.py +10 -3
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @qingflow-tech/qingflow-app-user-mcp@1.0.
|
|
6
|
+
npm install @qingflow-tech/qingflow-app-user-mcp@1.0.9
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @qingflow-tech/qingflow-app-user-mcp@1.0.
|
|
12
|
+
npx -y -p @qingflow-tech/qingflow-app-user-mcp@1.0.9 qingflow-app-user-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -142,10 +142,34 @@ class PublicButtonPlacement(str, Enum):
|
|
|
142
142
|
|
|
143
143
|
class PublicChartType(str, Enum):
|
|
144
144
|
target = "target"
|
|
145
|
+
indicator = "indicator"
|
|
146
|
+
summary = "summary"
|
|
145
147
|
pie = "pie"
|
|
146
148
|
bar = "bar"
|
|
149
|
+
columnar = "columnar"
|
|
147
150
|
line = "line"
|
|
148
151
|
table = "table"
|
|
152
|
+
detail = "detail"
|
|
153
|
+
area = "area"
|
|
154
|
+
stacked_area = "stacked_area"
|
|
155
|
+
pct_stack_area = "pct_stack_area"
|
|
156
|
+
funnel = "funnel"
|
|
157
|
+
waterfall = "waterfall"
|
|
158
|
+
gauge = "gauge"
|
|
159
|
+
heatmap = "heatmap"
|
|
160
|
+
histogram = "histogram"
|
|
161
|
+
treemap = "treemap"
|
|
162
|
+
radar = "radar"
|
|
163
|
+
stacked_bar = "stacked_bar"
|
|
164
|
+
pct_stack_bar = "pct_stack_bar"
|
|
165
|
+
stacked_column = "stacked_column"
|
|
166
|
+
pct_stack_col = "pct_stack_col"
|
|
167
|
+
scatter = "scatter"
|
|
168
|
+
ring = "ring"
|
|
169
|
+
rose = "rose"
|
|
170
|
+
dualaxes = "dualaxes"
|
|
171
|
+
map = "map"
|
|
172
|
+
timeline = "timeline"
|
|
149
173
|
|
|
150
174
|
|
|
151
175
|
class LayoutApplyMode(str, Enum):
|
|
@@ -1576,8 +1600,8 @@ class AssociatedResourcesApplyRequest(StrictModel):
|
|
|
1576
1600
|
app_key: str
|
|
1577
1601
|
upsert_resources: list[AssociatedResourceUpsertPatch] = Field(default_factory=list)
|
|
1578
1602
|
patch_resources: list[AssociatedResourcePartialPatch] = Field(default_factory=list)
|
|
1579
|
-
remove_associated_item_ids: list[
|
|
1580
|
-
reorder_associated_item_ids: list[
|
|
1603
|
+
remove_associated_item_ids: list[Any] = Field(default_factory=list)
|
|
1604
|
+
reorder_associated_item_ids: list[Any] = Field(default_factory=list)
|
|
1581
1605
|
view_configs: list[AssociatedResourceViewConfigPatch] = Field(default_factory=list)
|
|
1582
1606
|
|
|
1583
1607
|
@model_validator(mode="before")
|
|
@@ -1775,10 +1799,18 @@ class ChartUpsertPatch(StrictModel):
|
|
|
1775
1799
|
normalized = raw_type.strip().lower()
|
|
1776
1800
|
aliases = {
|
|
1777
1801
|
"targetchart": PublicChartType.target.value,
|
|
1802
|
+
"indicatorchart": PublicChartType.indicator.value,
|
|
1803
|
+
"summarychart": PublicChartType.summary.value,
|
|
1778
1804
|
"piechart": PublicChartType.pie.value,
|
|
1779
1805
|
"barchart": PublicChartType.bar.value,
|
|
1806
|
+
"columnchart": PublicChartType.columnar.value,
|
|
1807
|
+
"columnarchart": PublicChartType.columnar.value,
|
|
1780
1808
|
"linechart": PublicChartType.line.value,
|
|
1781
1809
|
"tablechart": PublicChartType.table.value,
|
|
1810
|
+
"detailchart": PublicChartType.detail.value,
|
|
1811
|
+
"percent_stacked_column": PublicChartType.pct_stack_col.value,
|
|
1812
|
+
"percent_stacked_bar": PublicChartType.pct_stack_bar.value,
|
|
1813
|
+
"percent_stacked_area": PublicChartType.pct_stack_area.value,
|
|
1782
1814
|
}
|
|
1783
1815
|
if normalized in aliases:
|
|
1784
1816
|
payload["chart_type"] = aliases[normalized]
|
|
@@ -3672,6 +3672,7 @@ class AiBuilderFacade:
|
|
|
3672
3672
|
client_key_to_id: dict[str, int] = {}
|
|
3673
3673
|
used_client_keys: set[str] = set()
|
|
3674
3674
|
force_update_resource_ids: set[int] = set()
|
|
3675
|
+
resolved_associated_item_refs: dict[str, list[int]] = {}
|
|
3675
3676
|
|
|
3676
3677
|
upsert_resources = list(request.upsert_resources)
|
|
3677
3678
|
if request.patch_resources:
|
|
@@ -3778,34 +3779,35 @@ class AiBuilderFacade:
|
|
|
3778
3779
|
else:
|
|
3779
3780
|
upsert_ops.append({"operation": "create", "associated_item_id": None, "patch": patch, "index": index})
|
|
3780
3781
|
|
|
3781
|
-
remove_ids = [
|
|
3782
|
-
item_id
|
|
3783
|
-
for item_id in (_coerce_positive_int(raw_id) for raw_id in request.remove_associated_item_ids)
|
|
3784
|
-
if item_id is not None
|
|
3785
|
-
]
|
|
3782
|
+
remove_ids: list[int] = []
|
|
3786
3783
|
for raw_id in request.remove_associated_item_ids:
|
|
3787
|
-
item_id =
|
|
3784
|
+
item_id, issue = _resolve_associated_resource_selector(
|
|
3785
|
+
raw_id,
|
|
3786
|
+
existing_resources=existing_resources,
|
|
3787
|
+
existing_by_id=existing_by_id,
|
|
3788
|
+
reason_path="remove_associated_item_ids",
|
|
3789
|
+
)
|
|
3788
3790
|
if item_id is None:
|
|
3789
|
-
blocking_issues.append({"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND", "reason_path": "remove_associated_item_ids", "received": raw_id})
|
|
3791
|
+
blocking_issues.append(issue or {"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND", "reason_path": "remove_associated_item_ids", "received": raw_id})
|
|
3790
3792
|
continue
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
elif item_id in touched_ids:
|
|
3793
|
+
remove_ids.append(item_id)
|
|
3794
|
+
if item_id in touched_ids:
|
|
3794
3795
|
blocking_issues.append(_duplicate_associated_resource_issue("remove_associated_item_ids", item_id))
|
|
3795
3796
|
else:
|
|
3796
3797
|
touched_ids.add(item_id)
|
|
3797
3798
|
|
|
3798
|
-
reorder_ids = [
|
|
3799
|
-
item_id
|
|
3800
|
-
for item_id in (_coerce_positive_int(raw_id) for raw_id in request.reorder_associated_item_ids)
|
|
3801
|
-
if item_id is not None
|
|
3802
|
-
]
|
|
3799
|
+
reorder_ids: list[int] = []
|
|
3803
3800
|
for raw_id in request.reorder_associated_item_ids:
|
|
3804
|
-
item_id =
|
|
3801
|
+
item_id, issue = _resolve_associated_resource_selector(
|
|
3802
|
+
raw_id,
|
|
3803
|
+
existing_resources=existing_resources,
|
|
3804
|
+
existing_by_id=existing_by_id,
|
|
3805
|
+
reason_path="reorder_associated_item_ids",
|
|
3806
|
+
)
|
|
3805
3807
|
if item_id is None:
|
|
3806
|
-
blocking_issues.append({"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND", "reason_path": "reorder_associated_item_ids", "received": raw_id})
|
|
3807
|
-
|
|
3808
|
-
|
|
3808
|
+
blocking_issues.append(issue or {"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND", "reason_path": "reorder_associated_item_ids", "received": raw_id})
|
|
3809
|
+
continue
|
|
3810
|
+
reorder_ids.append(item_id)
|
|
3809
3811
|
|
|
3810
3812
|
for index, view_config in enumerate(request.view_configs):
|
|
3811
3813
|
refs = [str(ref or "").strip() for ref in view_config.associated_item_refs if str(ref or "").strip()]
|
|
@@ -3819,21 +3821,26 @@ class AiBuilderFacade:
|
|
|
3819
3821
|
"message": "associated_item_refs must reference client_key values from upsert_resources in the same apply call",
|
|
3820
3822
|
}
|
|
3821
3823
|
)
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
{
|
|
3830
|
-
"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND",
|
|
3831
|
-
"reason_path": f"view_configs[{index}].associated_item_ids",
|
|
3832
|
-
"invalid_associated_item_ids": invalid_ids,
|
|
3833
|
-
"available_associated_item_ids": sorted(existing_by_id),
|
|
3834
|
-
"message": "view_configs.associated_item_ids must use app-level associated_item_id values from app_get",
|
|
3835
|
-
}
|
|
3824
|
+
resolved_ids: list[int] = []
|
|
3825
|
+
for raw_id in view_config.associated_item_ids:
|
|
3826
|
+
item_id, issue = _resolve_associated_resource_selector(
|
|
3827
|
+
raw_id,
|
|
3828
|
+
existing_resources=existing_resources,
|
|
3829
|
+
existing_by_id=existing_by_id,
|
|
3830
|
+
reason_path=f"view_configs[{index}].associated_item_ids",
|
|
3836
3831
|
)
|
|
3832
|
+
if item_id is None:
|
|
3833
|
+
blocking_issues.append(
|
|
3834
|
+
issue
|
|
3835
|
+
or {
|
|
3836
|
+
"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND",
|
|
3837
|
+
"reason_path": f"view_configs[{index}].associated_item_ids",
|
|
3838
|
+
"received": raw_id,
|
|
3839
|
+
}
|
|
3840
|
+
)
|
|
3841
|
+
continue
|
|
3842
|
+
resolved_ids.append(item_id)
|
|
3843
|
+
resolved_associated_item_refs[f"view_configs[{index}].associated_item_ids"] = resolved_ids
|
|
3837
3844
|
raw_limit_type = str(view_config.limit_type or ("all" if view_config.visible else "")).strip().lower()
|
|
3838
3845
|
if view_config.visible and raw_limit_type and raw_limit_type not in {"all", "select"}:
|
|
3839
3846
|
blocking_issues.append(
|
|
@@ -3851,6 +3858,8 @@ class AiBuilderFacade:
|
|
|
3851
3858
|
for issue in resource_match_issues
|
|
3852
3859
|
]
|
|
3853
3860
|
)
|
|
3861
|
+
if resolved_associated_item_refs:
|
|
3862
|
+
normalized_args["resolved_associated_item_refs"] = deepcopy(resolved_associated_item_refs)
|
|
3854
3863
|
|
|
3855
3864
|
if blocking_issues:
|
|
3856
3865
|
return finalize(
|
|
@@ -4008,12 +4017,13 @@ class AiBuilderFacade:
|
|
|
4008
4017
|
resources_after = []
|
|
4009
4018
|
|
|
4010
4019
|
for index, view_config in enumerate(request.view_configs):
|
|
4020
|
+
resolved_view_config_ids = resolved_associated_item_refs.get(f"view_configs[{index}].associated_item_ids", [])
|
|
4011
4021
|
selected_ids = [
|
|
4012
4022
|
item_id
|
|
4013
4023
|
for item_id in (
|
|
4014
4024
|
_coerce_positive_int(raw_id)
|
|
4015
4025
|
for raw_id in [
|
|
4016
|
-
*
|
|
4026
|
+
*resolved_view_config_ids,
|
|
4017
4027
|
*[client_key_to_id.get(str(ref or "").strip()) for ref in view_config.associated_item_refs],
|
|
4018
4028
|
]
|
|
4019
4029
|
)
|
|
@@ -4115,6 +4125,7 @@ class AiBuilderFacade:
|
|
|
4115
4125
|
str(index): _summarize_compiled_match_rules(rules)
|
|
4116
4126
|
for index, rules in compiled_resource_match_rules.items()
|
|
4117
4127
|
},
|
|
4128
|
+
"resolved_associated_item_refs": resolved_associated_item_refs,
|
|
4118
4129
|
},
|
|
4119
4130
|
"request_id": None,
|
|
4120
4131
|
"suggested_next_call": None if verified else {"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
@@ -9657,6 +9668,13 @@ class AiBuilderFacade:
|
|
|
9657
9668
|
|
|
9658
9669
|
for patch in upsert_charts:
|
|
9659
9670
|
try:
|
|
9671
|
+
dataset_source = _chart_patch_dataset_source_type(patch)
|
|
9672
|
+
if dataset_source:
|
|
9673
|
+
raise ValueError(
|
|
9674
|
+
"app_charts_apply only creates or edits app-source QingBI reports with dataSourceType=qingflow; "
|
|
9675
|
+
f"dataset report source '{dataset_source}' is not supported for creation/update yet. "
|
|
9676
|
+
"Create the dataset report in QingBI first, then attach it with app_associated_resources_apply using report_source='dataset'."
|
|
9677
|
+
)
|
|
9660
9678
|
config_update_requested = _chart_patch_updates_chart_config(patch)
|
|
9661
9679
|
chart_visible_auth = (
|
|
9662
9680
|
self._compile_visibility_to_chart_visible_auth(profile=profile, visibility=patch.visibility)
|
|
@@ -9680,6 +9698,13 @@ class AiBuilderFacade:
|
|
|
9680
9698
|
existing_name = str((existing or {}).get("chartName") or "").strip()
|
|
9681
9699
|
existing_type = _normalize_backend_chart_type((existing or {}).get("chartType"))
|
|
9682
9700
|
target_type = _map_public_chart_type_to_backend(patch.chart_type)
|
|
9701
|
+
existing_source_type = _chart_item_dataset_source_type(existing or {})
|
|
9702
|
+
if existing_source_type:
|
|
9703
|
+
raise ValueError(
|
|
9704
|
+
"app_charts_apply only creates or edits app-source QingBI reports with dataSourceType=qingflow; "
|
|
9705
|
+
f"existing chart '{chart_id or patch.name}' uses dataset report source '{existing_source_type}' and is not supported for update yet. "
|
|
9706
|
+
"Update it in QingBI directly, then attach the existing report with app_associated_resources_apply using report_source='dataset'."
|
|
9707
|
+
)
|
|
9683
9708
|
if existing is None:
|
|
9684
9709
|
temp_chart_id = str(patch.chart_id or f"mcp_{uuid4().hex[:16]}")
|
|
9685
9710
|
create_payload = {
|
|
@@ -12337,13 +12362,16 @@ def _bi_field_id_for_field(*, app_key: str, field: dict[str, Any], qingbi_fields
|
|
|
12337
12362
|
|
|
12338
12363
|
|
|
12339
12364
|
def _map_public_chart_type_to_backend(chart_type: PublicChartType) -> str:
|
|
12340
|
-
|
|
12365
|
+
aliases = {
|
|
12341
12366
|
PublicChartType.target: "indicator",
|
|
12367
|
+
PublicChartType.indicator: "indicator",
|
|
12342
12368
|
PublicChartType.pie: "pie",
|
|
12343
12369
|
PublicChartType.bar: "bar",
|
|
12344
12370
|
PublicChartType.line: "line",
|
|
12345
12371
|
PublicChartType.table: "detail",
|
|
12346
|
-
|
|
12372
|
+
PublicChartType.detail: "detail",
|
|
12373
|
+
}
|
|
12374
|
+
return aliases.get(chart_type, chart_type.value)
|
|
12347
12375
|
|
|
12348
12376
|
|
|
12349
12377
|
_CHART_PARTIAL_PATCH_KEY_ALIASES = {
|
|
@@ -12579,6 +12607,25 @@ def _build_public_metric_fields(
|
|
|
12579
12607
|
return metrics or [_default_public_total_metric()]
|
|
12580
12608
|
|
|
12581
12609
|
|
|
12610
|
+
def _split_axis_metric_fields(metrics: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
12611
|
+
if not metrics:
|
|
12612
|
+
default_metric = _default_public_total_metric()
|
|
12613
|
+
return [deepcopy(default_metric)], [deepcopy(default_metric)]
|
|
12614
|
+
if len(metrics) == 1:
|
|
12615
|
+
return [deepcopy(metrics[0])], [deepcopy(metrics[0])]
|
|
12616
|
+
return [deepcopy(metrics[0])], [deepcopy(metrics[1])]
|
|
12617
|
+
|
|
12618
|
+
|
|
12619
|
+
def _two_gauge_metric_fields(metrics: list[dict[str, Any]], *, requested_metric_count: int) -> list[dict[str, Any]]:
|
|
12620
|
+
if not metrics:
|
|
12621
|
+
return []
|
|
12622
|
+
if len(metrics) == 1:
|
|
12623
|
+
if requested_metric_count <= 0:
|
|
12624
|
+
return [deepcopy(metrics[0])]
|
|
12625
|
+
return [deepcopy(metrics[0]), _default_public_total_metric()]
|
|
12626
|
+
return [deepcopy(metrics[0]), deepcopy(metrics[1])]
|
|
12627
|
+
|
|
12628
|
+
|
|
12582
12629
|
def _build_public_chart_filter_matrix(
|
|
12583
12630
|
rules: list[Any],
|
|
12584
12631
|
*,
|
|
@@ -12647,23 +12694,28 @@ def _build_public_chart_config_payload(
|
|
|
12647
12694
|
for selector in list(config.pop("query_condition_field_ids", []) or []):
|
|
12648
12695
|
field = _resolve_public_field(selector, field_lookup=field_lookup)
|
|
12649
12696
|
query_condition_field_ids.append(_bi_field_id_for_field(app_key=app_key, field=field, qingbi_fields_by_id=qingbi_fields_by_id))
|
|
12697
|
+
backend_chart_type = _map_public_chart_type_to_backend(patch.chart_type)
|
|
12698
|
+
if backend_chart_type == "gauge" and not patch.indicator_field_ids and "selectedMetrics" not in config:
|
|
12699
|
+
raise ValueError("gauge charts require at least one indicator_field_ids value; pass one metric and the CLI will pair it with 数据总量")
|
|
12700
|
+
selected_dimensions = _build_public_dimension_fields(
|
|
12701
|
+
patch.dimension_field_ids,
|
|
12702
|
+
app_key=app_key,
|
|
12703
|
+
field_lookup=field_lookup,
|
|
12704
|
+
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
12705
|
+
)
|
|
12706
|
+
selected_metrics = _build_public_metric_fields(
|
|
12707
|
+
patch.indicator_field_ids,
|
|
12708
|
+
app_key=app_key,
|
|
12709
|
+
field_lookup=field_lookup,
|
|
12710
|
+
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
12711
|
+
aggregate=aggregate,
|
|
12712
|
+
)
|
|
12650
12713
|
payload: dict[str, Any] = {
|
|
12651
12714
|
"chartName": patch.name,
|
|
12652
|
-
"chartType":
|
|
12715
|
+
"chartType": backend_chart_type,
|
|
12653
12716
|
"dataSource": {"dataSourceId": app_key, "dataSourceType": "qingflow"},
|
|
12654
|
-
"selectedDimensions":
|
|
12655
|
-
|
|
12656
|
-
app_key=app_key,
|
|
12657
|
-
field_lookup=field_lookup,
|
|
12658
|
-
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
12659
|
-
),
|
|
12660
|
-
"selectedMetrics": _build_public_metric_fields(
|
|
12661
|
-
patch.indicator_field_ids,
|
|
12662
|
-
app_key=app_key,
|
|
12663
|
-
field_lookup=field_lookup,
|
|
12664
|
-
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
12665
|
-
aggregate=aggregate,
|
|
12666
|
-
),
|
|
12717
|
+
"selectedDimensions": selected_dimensions,
|
|
12718
|
+
"selectedMetrics": selected_metrics,
|
|
12667
12719
|
"beforeAggregationFilterMatrix": before_filters,
|
|
12668
12720
|
"afterAggregationFilterMatrix": after_filters,
|
|
12669
12721
|
"chartStyleConfigs": deepcopy(config.pop("chartStyleConfigs", [])),
|
|
@@ -12679,6 +12731,23 @@ def _build_public_chart_config_payload(
|
|
|
12679
12731
|
"queryConditionStatus": bool(config.pop("queryConditionStatus", bool(query_condition_field_ids))),
|
|
12680
12732
|
"queryConditionExact": bool(config.pop("queryConditionExact", False)),
|
|
12681
12733
|
}
|
|
12734
|
+
if backend_chart_type == "summary":
|
|
12735
|
+
payload.pop("selectedDimensions", None)
|
|
12736
|
+
payload.setdefault("xDimensions", deepcopy(selected_dimensions))
|
|
12737
|
+
payload.setdefault("yDimensions", [])
|
|
12738
|
+
elif backend_chart_type == "scatter":
|
|
12739
|
+
x_metrics, y_metrics = _split_axis_metric_fields(selected_metrics)
|
|
12740
|
+
payload.pop("selectedMetrics", None)
|
|
12741
|
+
payload.setdefault("xMetrics", x_metrics)
|
|
12742
|
+
payload.setdefault("yMetrics", y_metrics)
|
|
12743
|
+
elif backend_chart_type == "dualaxes":
|
|
12744
|
+
left_metrics, right_metrics = _split_axis_metric_fields(selected_metrics)
|
|
12745
|
+
payload.pop("selectedMetrics", None)
|
|
12746
|
+
payload.setdefault("leftMetrics", left_metrics)
|
|
12747
|
+
payload.setdefault("rightMetrics", right_metrics)
|
|
12748
|
+
elif backend_chart_type == "gauge":
|
|
12749
|
+
payload["selectedDimensions"] = []
|
|
12750
|
+
payload["selectedMetrics"] = _two_gauge_metric_fields(selected_metrics, requested_metric_count=len(patch.indicator_field_ids or []))
|
|
12682
12751
|
for key in (
|
|
12683
12752
|
"selectedTime",
|
|
12684
12753
|
"xDimensions",
|
|
@@ -12701,6 +12770,46 @@ def _chart_patch_updates_chart_config(patch: ChartUpsertPatch) -> bool:
|
|
|
12701
12770
|
return bool({"dimension_field_ids", "indicator_field_ids", "filters", "config"} & explicit_fields)
|
|
12702
12771
|
|
|
12703
12772
|
|
|
12773
|
+
def _chart_patch_dataset_source_type(patch: ChartUpsertPatch) -> str:
|
|
12774
|
+
config = patch.config if isinstance(patch.config, dict) else {}
|
|
12775
|
+
candidates = [
|
|
12776
|
+
config.get("dataSourceType"),
|
|
12777
|
+
config.get("data_source_type"),
|
|
12778
|
+
]
|
|
12779
|
+
data_source = config.get("dataSource")
|
|
12780
|
+
if isinstance(data_source, dict):
|
|
12781
|
+
candidates.extend([data_source.get("dataSourceType"), data_source.get("data_source_type"), data_source.get("type")])
|
|
12782
|
+
data_source = config.get("data_source")
|
|
12783
|
+
if isinstance(data_source, dict):
|
|
12784
|
+
candidates.extend([data_source.get("dataSourceType"), data_source.get("data_source_type"), data_source.get("type")])
|
|
12785
|
+
for candidate in candidates:
|
|
12786
|
+
normalized = str(candidate or "").strip().lower()
|
|
12787
|
+
if not normalized:
|
|
12788
|
+
continue
|
|
12789
|
+
if normalized not in {"qingflow", "app"}:
|
|
12790
|
+
return normalized
|
|
12791
|
+
return ""
|
|
12792
|
+
|
|
12793
|
+
|
|
12794
|
+
def _chart_item_dataset_source_type(item: dict[str, Any]) -> str:
|
|
12795
|
+
candidates = [
|
|
12796
|
+
item.get("dataSourceType"),
|
|
12797
|
+
item.get("data_source_type"),
|
|
12798
|
+
item.get("sourceType"),
|
|
12799
|
+
item.get("source_type"),
|
|
12800
|
+
]
|
|
12801
|
+
data_source = item.get("dataSource")
|
|
12802
|
+
if isinstance(data_source, dict):
|
|
12803
|
+
candidates.extend([data_source.get("dataSourceType"), data_source.get("data_source_type"), data_source.get("type")])
|
|
12804
|
+
for candidate in candidates:
|
|
12805
|
+
normalized = str(candidate or "").strip().lower()
|
|
12806
|
+
if not normalized:
|
|
12807
|
+
continue
|
|
12808
|
+
if normalized not in {"qingflow", "app", "bi_qingflow"}:
|
|
12809
|
+
return normalized
|
|
12810
|
+
return ""
|
|
12811
|
+
|
|
12812
|
+
|
|
12704
12813
|
def _build_public_portal_base_payload(
|
|
12705
12814
|
*,
|
|
12706
12815
|
dash_name: str,
|
|
@@ -12771,16 +12880,33 @@ def _normalize_backend_chart_type(value: Any) -> str:
|
|
|
12771
12880
|
"12": "dualaxes",
|
|
12772
12881
|
"13": "map",
|
|
12773
12882
|
"14": "timeline",
|
|
12883
|
+
"15": "area",
|
|
12884
|
+
"16": "stacked_column",
|
|
12885
|
+
"17": "stacked_bar",
|
|
12886
|
+
"18": "rose",
|
|
12887
|
+
"19": "stacked_area",
|
|
12888
|
+
"20": "pct_stack_col",
|
|
12889
|
+
"21": "pct_stack_bar",
|
|
12890
|
+
"22": "pct_stack_area",
|
|
12891
|
+
"23": "waterfall",
|
|
12892
|
+
"24": "gauge",
|
|
12893
|
+
"25": "heatmap",
|
|
12894
|
+
"26": "histogram",
|
|
12895
|
+
"27": "treemap",
|
|
12896
|
+
}
|
|
12897
|
+
aliases = {
|
|
12898
|
+
"percent_stacked_column": "pct_stack_col",
|
|
12899
|
+
"percent_stacked_bar": "pct_stack_bar",
|
|
12900
|
+
"percent_stacked_area": "pct_stack_area",
|
|
12774
12901
|
}
|
|
12775
|
-
|
|
12902
|
+
normalized = by_code.get(raw, raw.lower())
|
|
12903
|
+
return aliases.get(normalized, normalized)
|
|
12776
12904
|
|
|
12777
12905
|
|
|
12778
12906
|
def _public_chart_type_from_backend(value: Any) -> str:
|
|
12779
12907
|
normalized = _normalize_backend_chart_type(value)
|
|
12780
12908
|
return {
|
|
12781
12909
|
"indicator": PublicChartType.target.value,
|
|
12782
|
-
"summary": PublicChartType.target.value,
|
|
12783
|
-
"columnar": PublicChartType.bar.value,
|
|
12784
12910
|
"detail": PublicChartType.table.value,
|
|
12785
12911
|
}.get(normalized, normalized)
|
|
12786
12912
|
|
|
@@ -20129,7 +20255,7 @@ def _build_view_associated_resources_payload(
|
|
|
20129
20255
|
"missing_fields": [],
|
|
20130
20256
|
"received": raw_item_id,
|
|
20131
20257
|
"message": "associated_item_id must be an app-level associated resource id from app_get.associated_resources",
|
|
20132
|
-
"next_action": "
|
|
20258
|
+
"next_action": "prefer app_associated_resources_apply for associated report/view display; it can resolve associated_item_id, chart_id/chart_key, or view_key",
|
|
20133
20259
|
}
|
|
20134
20260
|
]
|
|
20135
20261
|
item_ids.append(item_id)
|
|
@@ -20143,7 +20269,7 @@ def _build_view_associated_resources_payload(
|
|
|
20143
20269
|
"invalid_associated_item_ids": missing_ids,
|
|
20144
20270
|
"available_associated_item_ids": sorted(available_by_id),
|
|
20145
20271
|
"message": "associated_resource references ids that are not in the app-level associated resource pool",
|
|
20146
|
-
"next_action": "
|
|
20272
|
+
"next_action": "prefer app_associated_resources_apply for associated report/view display; it can resolve associated_item_id, chart_id/chart_key, or view_key",
|
|
20147
20273
|
}
|
|
20148
20274
|
]
|
|
20149
20275
|
payload = {
|
|
@@ -20561,6 +20687,77 @@ def _associated_resource_upsert_payload_from_existing_item(
|
|
|
20561
20687
|
return _compact_dict(payload)
|
|
20562
20688
|
|
|
20563
20689
|
|
|
20690
|
+
def _resolve_associated_resource_selector(
|
|
20691
|
+
selector: Any,
|
|
20692
|
+
*,
|
|
20693
|
+
existing_resources: list[dict[str, Any]],
|
|
20694
|
+
existing_by_id: dict[int, dict[str, Any]],
|
|
20695
|
+
reason_path: str,
|
|
20696
|
+
) -> tuple[int | None, dict[str, Any] | None]:
|
|
20697
|
+
item_id = _coerce_positive_int(selector)
|
|
20698
|
+
if item_id is not None:
|
|
20699
|
+
if item_id in existing_by_id:
|
|
20700
|
+
return item_id, None
|
|
20701
|
+
raw = str(selector or "").strip()
|
|
20702
|
+
if not raw:
|
|
20703
|
+
return None, {
|
|
20704
|
+
"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND",
|
|
20705
|
+
"reason_path": reason_path,
|
|
20706
|
+
"received": selector,
|
|
20707
|
+
"available_associated_item_ids": sorted(existing_by_id),
|
|
20708
|
+
"message": "associated resource selector cannot be empty",
|
|
20709
|
+
}
|
|
20710
|
+
matches: list[dict[str, Any]] = []
|
|
20711
|
+
for resource in existing_resources:
|
|
20712
|
+
if not isinstance(resource, dict):
|
|
20713
|
+
continue
|
|
20714
|
+
candidates = [
|
|
20715
|
+
resource.get("chart_id"),
|
|
20716
|
+
resource.get("chart_key"),
|
|
20717
|
+
resource.get("view_key"),
|
|
20718
|
+
resource.get("associated_item_id"),
|
|
20719
|
+
]
|
|
20720
|
+
if raw in {str(candidate).strip() for candidate in candidates if candidate not in {None, ""}}:
|
|
20721
|
+
matches.append(resource)
|
|
20722
|
+
if len(matches) == 1:
|
|
20723
|
+
resolved_id = _coerce_positive_int(matches[0].get("associated_item_id"))
|
|
20724
|
+
if resolved_id is not None:
|
|
20725
|
+
return resolved_id, None
|
|
20726
|
+
if len(matches) > 1:
|
|
20727
|
+
return None, {
|
|
20728
|
+
"error_code": "AMBIGUOUS_ASSOCIATED_RESOURCE",
|
|
20729
|
+
"reason_path": reason_path,
|
|
20730
|
+
"received": selector,
|
|
20731
|
+
"candidate_associated_item_ids": [
|
|
20732
|
+
item_id
|
|
20733
|
+
for item_id in (_coerce_positive_int(item.get("associated_item_id")) for item in matches)
|
|
20734
|
+
if item_id is not None
|
|
20735
|
+
],
|
|
20736
|
+
"message": "selector matches multiple associated resources; pass associated_item_id",
|
|
20737
|
+
}
|
|
20738
|
+
return None, {
|
|
20739
|
+
"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND",
|
|
20740
|
+
"reason_path": reason_path,
|
|
20741
|
+
"received": selector,
|
|
20742
|
+
"available_associated_item_ids": sorted(existing_by_id),
|
|
20743
|
+
"available_chart_ids": sorted(
|
|
20744
|
+
{
|
|
20745
|
+
str(resource.get("chart_id") or resource.get("chart_key") or "").strip()
|
|
20746
|
+
for resource in existing_resources
|
|
20747
|
+
if str(resource.get("chart_id") or resource.get("chart_key") or "").strip()
|
|
20748
|
+
}
|
|
20749
|
+
),
|
|
20750
|
+
"available_view_keys": sorted(
|
|
20751
|
+
{
|
|
20752
|
+
str(resource.get("view_key") or "").strip()
|
|
20753
|
+
for resource in existing_resources
|
|
20754
|
+
if str(resource.get("view_key") or "").strip()
|
|
20755
|
+
}
|
|
20756
|
+
),
|
|
20757
|
+
"message": "selector must be an associated_item_id, QingBI chart_id/chart_key, or Qingflow view_key from the app-level associated resource pool",
|
|
20758
|
+
}
|
|
20759
|
+
|
|
20760
|
+
|
|
20564
20761
|
def _associated_resource_not_found_issue(reason_path: str, associated_item_id: int, existing_by_id: dict[int, dict[str, Any]]) -> dict[str, Any]:
|
|
20565
20762
|
return {
|
|
20566
20763
|
"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND",
|
|
@@ -20568,7 +20765,7 @@ def _associated_resource_not_found_issue(reason_path: str, associated_item_id: i
|
|
|
20568
20765
|
"associated_item_id": associated_item_id,
|
|
20569
20766
|
"available_associated_item_ids": sorted(existing_by_id),
|
|
20570
20767
|
"message": "associated_item_id is not in the app-level associated resource pool",
|
|
20571
|
-
"next_action": "call app_get and use associated_resources[].associated_item_id
|
|
20768
|
+
"next_action": "call app_get and use associated_resources[].associated_item_id, chart_id/chart_key, or view_key",
|
|
20572
20769
|
}
|
|
20573
20770
|
|
|
20574
20771
|
|
|
@@ -43,8 +43,9 @@ def build_builder_server() -> FastMCP:
|
|
|
43
43
|
"For app_schema_apply, configure data title and data cover directly in field JSON with as_data_title=true and as_data_cover=true; data title is required and exactly one field may be marked, while data cover is optional and must be a top-level attachment field. "
|
|
44
44
|
"For app_views_apply, keep fixed saved filters in filters and configure the frontend query panel separately with query_conditions; query_conditions.rows is a matrix of field names compiled to backend queryCondition queIds. "
|
|
45
45
|
"For custom button body create/update/delete and view placement, use app_custom_buttons_apply. For addData buttons, prefer trigger_add_data_config.target_app_key + field_mappings/default_values; do not ask agents to write raw que_relation unless maintaining a legacy config. field_mappings.source_field accepts source schema fields and supported system fields: 数据ID/row_record_id/apply_id/_id means current record id (-17), 编号/record_number means visible record number (0). To fill a target relation with the current record, map {'source_field': '数据ID', 'target_field': '目标引用字段'}; default_values is only for static constants. View button bindings merge by default and merge-mode view_configs must include buttons; use view_configs[].mode=replace or explicit buttons=[] only when clearing/replacing existing bindings is intended. Builder view_key arguments are raw keys from app_get.views[].view_key and must not be prefixed with custom:. "
|
|
46
|
+
"For BI reports, keep report-body development separate from Qingflow in-app display: use app_charts_apply to create, update, remove, or reorder app-source QingBI chart bodies/configs with dataSourceType=qingflow; dataset BI reports are not created or edited by app_charts_apply yet and should be created in QingBI first, then attached with app_associated_resources_apply using report_source=dataset. "
|
|
46
47
|
"For associated views/reports, use app_associated_resources_apply. Use match_mappings for filtering associated resources: dynamic current-record conditions use source_field, static conditions use value. match_mappings also supports 数据ID(-17) and 编号(0). Do not ask agents to write raw match_rules unless preserving a legacy backend config. "
|
|
47
|
-
"For associated reports/views, use app_associated_resources_apply for both the app-level associated_resources pool and per-view display config; associated_item_id is the app-level form_asos_chart.id,
|
|
48
|
+
"For associated reports/views, use app_associated_resources_apply for both the app-level associated_resources pool and per-view display config; associated_item_id is the app-level form_asos_chart.id, and view_configs/remove/reorder may also pass an existing resource's chart_id/chart_key/view_key because the tool resolves those to the internal id. Before creating an associated resource, read app_get.associated_resources and reuse an existing matching target_app_key + view_key/chart_key through patch_resources; client_key only works inside one apply call and is not persisted. Do not ask agents to pass backend raw sourceType: views infer the internal Qingflow view source, reports default to BI app reports, and dataset reports use report_source=dataset. "
|
|
48
49
|
"For code_block fields with output bindings, always use qf_output assignment rather than const/let qf_output, and use app_repair_code_blocks when an existing form hangs because output-bound fields stay loading. "
|
|
49
50
|
"Use package_apply to manage package metadata, visibility, grouping, and ordering, and app_publish_verify for explicit final publish verification. "
|
|
50
51
|
"For workflow edits, keep the public builder surface on stable linear flows only: start/approve/fill/copy/webhook/end. Branch and condition nodes are intentionally disabled because the backend workflow route is not front-end stable for those node types. Declare node assignees and editable fields explicitly. "
|
|
@@ -2976,6 +2976,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2976
2976
|
"graphType": "graph_type",
|
|
2977
2977
|
"targetAppKey": "target_app_key",
|
|
2978
2978
|
"chartKey": "chart_key",
|
|
2979
|
+
"chartId": "chart_key",
|
|
2979
2980
|
"viewKey": "view_key",
|
|
2980
2981
|
"viewgraphKey": "view_key",
|
|
2981
2982
|
"reportSource": "report_source",
|
|
@@ -2990,12 +2991,14 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2990
2991
|
"view_configs[].limit_type": ["all", "select"],
|
|
2991
2992
|
},
|
|
2992
2993
|
"execution_notes": [
|
|
2994
|
+
"this tool manages Qingflow in-app associated report/view display; it does not create or edit QingBI report bodies/configs",
|
|
2995
|
+
"create or edit app-source BI report bodies first with app_charts_apply, then attach the resulting chart_id here with graph_type=chart; dataset BI reports can only be attached when they already exist",
|
|
2993
2996
|
"this is the default associated report/view path; it manages both the app-level associated resource pool and per-view display config",
|
|
2994
2997
|
"use patch_resources for partial parameter replacement on existing associated resources; the tool reads the current resource including backend-required raw fields, merges patch_resources[].set/unset, then submits the backend full-save payload internally",
|
|
2995
|
-
"associated_item_id is form_asos_chart.id from app_get.associated_resources[].associated_item_id;
|
|
2998
|
+
"associated_item_id is form_asos_chart.id from app_get.associated_resources[].associated_item_id; view_configs/remove/reorder may also pass an existing associated resource's chart_id/chart_key/view_key and the tool resolves it to the internal id",
|
|
2996
2999
|
"before creating an associated resource, read app_get.associated_resources and reuse an existing item with patch_resources when target_app_key + view_key/chart_key already matches; repeated upsert_resources without associated_item_id can create duplicate associated items",
|
|
2997
3000
|
"graph_type=view uses view_key and internally compiles to the Qingflow view source; graph_type=chart uses chart_key and defaults to report_source=app",
|
|
2998
|
-
"report_source=app maps to BI_QINGFLOW; report_source=dataset maps to BI_DATASET; do not pass raw backend sourceType",
|
|
3001
|
+
"report_source=app maps to BI_QINGFLOW; report_source=dataset maps to BI_DATASET for associating an existing dataset report; do not pass raw backend sourceType",
|
|
2999
3002
|
"use match_mappings for associated view/report filtering; dynamic conditions use source_field and static conditions use value",
|
|
3000
3003
|
"match_mappings.source_field accepts source schema fields plus system fields 数据ID(-17) and 编号(0); match_mappings compiles to backend matchRules",
|
|
3001
3004
|
"do not write raw match_rules unless preserving a legacy backend config; match_mappings and match_rules are mutually exclusive",
|
|
@@ -3673,7 +3676,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3673
3676
|
"can_edit_form covers form/schema routes only and does not imply app base-info writes",
|
|
3674
3677
|
"returns normalized app visibility when backend auth is readable",
|
|
3675
3678
|
"custom_buttons[].button_id is the id required by app_custom_buttons_apply view_configs[].buttons[].button_ref",
|
|
3676
|
-
"associated_resources[].associated_item_id is the id
|
|
3679
|
+
"associated_resources[].associated_item_id is the internal id; app_associated_resources_apply.view_configs/remove/reorder may also pass an existing resource's chart_id/chart_key/view_key",
|
|
3677
3680
|
],
|
|
3678
3681
|
"minimal_example": {
|
|
3679
3682
|
"profile": "default",
|
|
@@ -3797,6 +3800,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3797
3800
|
**deepcopy(_VISIBILITY_ALLOWED_VALUES),
|
|
3798
3801
|
},
|
|
3799
3802
|
"execution_notes": [
|
|
3803
|
+
"this tool manages QingBI report bodies/configs; it does not attach reports to Qingflow app associated-resource display",
|
|
3804
|
+
"app_charts_apply creates/updates app-source QingBI reports only; generated payloads use dataSourceType=qingflow",
|
|
3805
|
+
"dataset BI reports are not created or edited by this tool yet; create them in QingBI first, then attach the existing report with app_associated_resources_apply report_source=dataset",
|
|
3806
|
+
"after creating or updating an app-source report body, use app_associated_resources_apply when the report should appear inside a Qingflow app/view",
|
|
3800
3807
|
"app_charts_apply is immediate-live and does not publish",
|
|
3801
3808
|
"use patch_charts for partial parameter replacement on existing charts; the tool reads current chart base/config, merges patch_charts[].set/unset, then submits full QingBI base/config payloads internally",
|
|
3802
3809
|
"chart matching precedence is chart_id first, then exact unique chart name",
|