@josephyan/qingflow-cli 1.1.3 → 1.1.4
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 +82 -0
- package/src/qingflow_mcp/builder_facade/service.py +119 -17
- package/src/qingflow_mcp/cli/commands/builder.py +58 -7
- package/src/qingflow_mcp/tools/ai_builder_tools.py +13 -4
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @josephyan/qingflow-cli@1.1.
|
|
6
|
+
npm install @josephyan/qingflow-cli@1.1.4
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-cli@1.1.
|
|
12
|
+
npx -y -p @josephyan/qingflow-cli@1.1.4 qingflow
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import re
|
|
3
4
|
from enum import Enum
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
@@ -1762,6 +1763,70 @@ class ChartFilterRulePatch(StrictModel):
|
|
|
1762
1763
|
return self
|
|
1763
1764
|
|
|
1764
1765
|
|
|
1766
|
+
_CHART_METRIC_RE = re.compile(r"^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\((.*)\)\s*$")
|
|
1767
|
+
|
|
1768
|
+
|
|
1769
|
+
def _normalize_chart_string_list(value: Any) -> list[str]:
|
|
1770
|
+
if value is None:
|
|
1771
|
+
return []
|
|
1772
|
+
if isinstance(value, (list, tuple)):
|
|
1773
|
+
return [str(item).strip() for item in value if item is not None and str(item).strip()]
|
|
1774
|
+
raw = str(value).strip()
|
|
1775
|
+
return [raw] if raw else []
|
|
1776
|
+
|
|
1777
|
+
|
|
1778
|
+
def _parse_chart_metric_item(value: Any) -> tuple[list[str], str | None]:
|
|
1779
|
+
if value is None:
|
|
1780
|
+
return [], None
|
|
1781
|
+
if isinstance(value, dict):
|
|
1782
|
+
op = str(value.get("op") or value.get("agg") or value.get("aggregate") or value.get("aggregation") or "").strip().lower()
|
|
1783
|
+
field = value.get("field", value.get("field_name", value.get("name", value.get("selector"))))
|
|
1784
|
+
selectors = _normalize_chart_string_list(field)
|
|
1785
|
+
return selectors, op or ("count" if not selectors else None)
|
|
1786
|
+
raw = str(value).strip()
|
|
1787
|
+
if not raw:
|
|
1788
|
+
return [], None
|
|
1789
|
+
matched = _CHART_METRIC_RE.match(raw)
|
|
1790
|
+
if matched:
|
|
1791
|
+
op = matched.group(1).strip().lower()
|
|
1792
|
+
field = matched.group(2).strip()
|
|
1793
|
+
if field in {"*", ""}:
|
|
1794
|
+
return [], op
|
|
1795
|
+
return [field], op
|
|
1796
|
+
if raw.lower() in {"count", "count(*)"}:
|
|
1797
|
+
return [], "count"
|
|
1798
|
+
return [raw], None
|
|
1799
|
+
|
|
1800
|
+
|
|
1801
|
+
def _apply_semantic_chart_metric_aliases(payload: dict[str, Any]) -> None:
|
|
1802
|
+
raw_metrics: list[Any] = []
|
|
1803
|
+
if "metric" in payload:
|
|
1804
|
+
raw_metrics.append(payload.pop("metric"))
|
|
1805
|
+
if "metrics" in payload:
|
|
1806
|
+
metrics = payload.pop("metrics")
|
|
1807
|
+
raw_metrics.extend(metrics if isinstance(metrics, list) else [metrics])
|
|
1808
|
+
if not raw_metrics:
|
|
1809
|
+
return
|
|
1810
|
+
selectors: list[str] = []
|
|
1811
|
+
aggregates: list[str] = []
|
|
1812
|
+
for metric in raw_metrics:
|
|
1813
|
+
metric_selectors, aggregate = _parse_chart_metric_item(metric)
|
|
1814
|
+
selectors.extend(metric_selectors)
|
|
1815
|
+
if aggregate:
|
|
1816
|
+
aggregates.append(aggregate)
|
|
1817
|
+
if selectors and "indicator_field_ids" not in payload:
|
|
1818
|
+
payload["indicator_field_ids"] = selectors
|
|
1819
|
+
if aggregates:
|
|
1820
|
+
normalized_aggregate = aggregates[0]
|
|
1821
|
+
config = payload.get("config")
|
|
1822
|
+
if not isinstance(config, dict):
|
|
1823
|
+
config = {}
|
|
1824
|
+
else:
|
|
1825
|
+
config = dict(config)
|
|
1826
|
+
config.setdefault("aggregate", normalized_aggregate)
|
|
1827
|
+
payload["config"] = config
|
|
1828
|
+
|
|
1829
|
+
|
|
1765
1830
|
class ChartUpsertPatch(StrictModel):
|
|
1766
1831
|
chart_id: str | None = None
|
|
1767
1832
|
name: str
|
|
@@ -1790,6 +1855,15 @@ class ChartUpsertPatch(StrictModel):
|
|
|
1790
1855
|
payload["indicator_field_ids"] = payload.pop("indicator_fields")
|
|
1791
1856
|
if "metric_field_ids" in payload and "indicator_field_ids" not in payload:
|
|
1792
1857
|
payload["indicator_field_ids"] = payload.pop("metric_field_ids")
|
|
1858
|
+
if "group_by" in payload and "dimension_field_ids" not in payload:
|
|
1859
|
+
payload["dimension_field_ids"] = _normalize_chart_string_list(payload.pop("group_by"))
|
|
1860
|
+
if "groupBy" in payload and "dimension_field_ids" not in payload:
|
|
1861
|
+
payload["dimension_field_ids"] = _normalize_chart_string_list(payload.pop("groupBy"))
|
|
1862
|
+
if "where" in payload and "filters" not in payload:
|
|
1863
|
+
payload["filters"] = payload.pop("where")
|
|
1864
|
+
else:
|
|
1865
|
+
payload.pop("where", None)
|
|
1866
|
+
_apply_semantic_chart_metric_aliases(payload)
|
|
1793
1867
|
raw_type = payload.get("chart_type")
|
|
1794
1868
|
if isinstance(raw_type, str):
|
|
1795
1869
|
normalized = raw_type.strip().lower()
|
|
@@ -1980,6 +2054,7 @@ class PortalViewRefPatch(StrictModel):
|
|
|
1980
2054
|
class PortalSectionPatch(StrictModel):
|
|
1981
2055
|
title: str
|
|
1982
2056
|
source_type: str = Field(validation_alias=AliasChoices("source_type", "sourceType"))
|
|
2057
|
+
role: str | None = None
|
|
1983
2058
|
position: PortalComponentPositionPatch | None = None
|
|
1984
2059
|
dash_style_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("dash_style_config", "dashStyleConfigBO"))
|
|
1985
2060
|
config: dict[str, Any] = Field(default_factory=dict)
|
|
@@ -2010,6 +2085,13 @@ class PortalSectionPatch(StrictModel):
|
|
|
2010
2085
|
supported = {"chart", "view", "grid", "filter", "text", "link"}
|
|
2011
2086
|
if self.source_type not in supported:
|
|
2012
2087
|
raise ValueError(f"unsupported portal source_type '{self.source_type}'")
|
|
2088
|
+
if self.role is not None:
|
|
2089
|
+
normalized_role = str(self.role or "").strip().lower()
|
|
2090
|
+
if normalized_role not in {"metric"}:
|
|
2091
|
+
raise ValueError(f"unsupported portal section role '{self.role}'")
|
|
2092
|
+
self.role = normalized_role
|
|
2093
|
+
if self.source_type != "chart":
|
|
2094
|
+
raise ValueError("portal section role is only supported for chart sections")
|
|
2013
2095
|
if self.source_type == "chart" and self.chart_ref is None:
|
|
2014
2096
|
raise ValueError("chart section requires chart_ref")
|
|
2015
2097
|
if self.source_type == "view" and self.view_ref is None:
|
|
@@ -1184,6 +1184,24 @@ class AiBuilderFacade:
|
|
|
1184
1184
|
suggested_next_call=None,
|
|
1185
1185
|
)
|
|
1186
1186
|
|
|
1187
|
+
if not normalized_items and not current_resources and not current_group_specs:
|
|
1188
|
+
return {
|
|
1189
|
+
"status": "success",
|
|
1190
|
+
"error_code": None,
|
|
1191
|
+
"recoverable": False,
|
|
1192
|
+
"message": "package items already empty; skipped package sort",
|
|
1193
|
+
"normalized_args": normalized_args,
|
|
1194
|
+
"missing_fields": [],
|
|
1195
|
+
"allowed_values": {},
|
|
1196
|
+
"details": {"group_operations": [], "sort_skipped": True},
|
|
1197
|
+
"request_id": None,
|
|
1198
|
+
"suggested_next_call": None,
|
|
1199
|
+
"noop": True,
|
|
1200
|
+
"warnings": [],
|
|
1201
|
+
"verification": {"layout_verified": True, "sort_skipped": True},
|
|
1202
|
+
"verified": True,
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1187
1205
|
duplicate_resources = _find_duplicate_package_resources(normalized_items)
|
|
1188
1206
|
if duplicate_resources:
|
|
1189
1207
|
return _failed(
|
|
@@ -5344,7 +5362,7 @@ class AiBuilderFacade:
|
|
|
5344
5362
|
for app_key in app_keys:
|
|
5345
5363
|
try:
|
|
5346
5364
|
result = single_reader(profile=profile, app_key=app_key)
|
|
5347
|
-
status = result.get("status"
|
|
5365
|
+
status = str(result.get("status") or "").strip().lower()
|
|
5348
5366
|
if status in {"success", "partial_success"}:
|
|
5349
5367
|
app_results.append(_batch_read_app_item(app_key=app_key, result=result, data_key=data_key))
|
|
5350
5368
|
else:
|
|
@@ -5429,7 +5447,7 @@ class AiBuilderFacade:
|
|
|
5429
5447
|
details={"app_key": app_key},
|
|
5430
5448
|
suggested_next_call={"tool_name": "app_flow_apply", "arguments": {"profile": profile, "app_key": app_key}},
|
|
5431
5449
|
)
|
|
5432
|
-
if current.get("status") != "success":
|
|
5450
|
+
if str(current.get("status") or "").strip().lower() != "success":
|
|
5433
5451
|
return _failed(
|
|
5434
5452
|
"FLOW_READ_FAILED",
|
|
5435
5453
|
current.get("message") or "failed to read current flow",
|
|
@@ -5473,6 +5491,16 @@ class AiBuilderFacade:
|
|
|
5473
5491
|
normalized_args={"app_key": app_key, "not_found_ids": not_found},
|
|
5474
5492
|
suggested_next_call={"tool_name": "app_get_flow", "arguments": {"profile": profile, "app_key": app_key}},
|
|
5475
5493
|
)
|
|
5494
|
+
edge_container = spec.get("edges") if isinstance(spec.get("edges"), dict) else {}
|
|
5495
|
+
edges = edge_container.get("edges") if isinstance(edge_container, dict) else None
|
|
5496
|
+
transitions = spec.get("transitions") if isinstance(spec.get("transitions"), list) else None
|
|
5497
|
+
if not edges and not transitions:
|
|
5498
|
+
return _failed(
|
|
5499
|
+
"FLOW_PATCH_EMPTY_GRAPH_UNSUPPORTED",
|
|
5500
|
+
"current flow spec has no edges; patch_nodes cannot safely save this backend shape. Use full app_flow_apply to create a workflow with edges first.",
|
|
5501
|
+
normalized_args={"app_key": app_key, "patch_nodes": patch_nodes},
|
|
5502
|
+
suggested_next_call={"tool_name": "app_flow_apply", "arguments": {"profile": profile, "app_key": app_key}},
|
|
5503
|
+
)
|
|
5476
5504
|
return self.flow_apply(
|
|
5477
5505
|
profile=profile,
|
|
5478
5506
|
app_key=app_key,
|
|
@@ -13833,6 +13861,73 @@ def _two_gauge_metric_fields(metrics: list[dict[str, Any]], *, requested_metric_
|
|
|
13833
13861
|
return [deepcopy(metrics[0]), deepcopy(metrics[1])]
|
|
13834
13862
|
|
|
13835
13863
|
|
|
13864
|
+
def _qingbi_filter_judge_type(operator: str) -> str:
|
|
13865
|
+
return {
|
|
13866
|
+
ViewFilterOperator.eq.value: "equal",
|
|
13867
|
+
ViewFilterOperator.neq.value: "notEqual",
|
|
13868
|
+
ViewFilterOperator.gte.value: "greaterOrEqual",
|
|
13869
|
+
ViewFilterOperator.lte.value: "lessOrEqual",
|
|
13870
|
+
ViewFilterOperator.in_.value: "anyMatch",
|
|
13871
|
+
ViewFilterOperator.contains.value: "contains",
|
|
13872
|
+
ViewFilterOperator.is_empty.value: "isNull",
|
|
13873
|
+
ViewFilterOperator.not_empty.value: "isNotNull",
|
|
13874
|
+
}.get(operator, "equal")
|
|
13875
|
+
|
|
13876
|
+
|
|
13877
|
+
def _qingbi_option_label_lookup(*, form_field: dict[str, Any], qingbi_field: dict[str, Any]) -> dict[str, str]:
|
|
13878
|
+
labels: dict[str, str] = {}
|
|
13879
|
+
|
|
13880
|
+
def add_option(raw_option: Any) -> None:
|
|
13881
|
+
if isinstance(raw_option, dict):
|
|
13882
|
+
label = raw_option.get("value")
|
|
13883
|
+
if label is None:
|
|
13884
|
+
label = raw_option.get("label") or raw_option.get("name") or raw_option.get("title") or raw_option.get("optValue")
|
|
13885
|
+
label_text = str(label).strip() if label is not None else ""
|
|
13886
|
+
for key in ("id", "optionId", "option_id", "optId", "value", "label", "name", "title", "optValue"):
|
|
13887
|
+
raw_key = raw_option.get(key)
|
|
13888
|
+
if raw_key is not None and str(raw_key).strip() and label_text:
|
|
13889
|
+
labels[str(raw_key).strip()] = label_text
|
|
13890
|
+
elif raw_option is not None:
|
|
13891
|
+
label_text = str(raw_option).strip()
|
|
13892
|
+
if label_text:
|
|
13893
|
+
labels[label_text] = label_text
|
|
13894
|
+
|
|
13895
|
+
for option in form_field.get("option_details") or []:
|
|
13896
|
+
add_option(option)
|
|
13897
|
+
for option in form_field.get("options") or []:
|
|
13898
|
+
add_option(option)
|
|
13899
|
+
for option in qingbi_field.get("options") or qingbi_field.get("option_details") or []:
|
|
13900
|
+
add_option(option)
|
|
13901
|
+
return labels
|
|
13902
|
+
|
|
13903
|
+
|
|
13904
|
+
def _qingbi_filter_text_values(values: list[Any], *, form_field: dict[str, Any], qingbi_field: dict[str, Any]) -> list[str]:
|
|
13905
|
+
labels = _qingbi_option_label_lookup(form_field=form_field, qingbi_field=qingbi_field)
|
|
13906
|
+
normalized: list[str] = []
|
|
13907
|
+
for value in values:
|
|
13908
|
+
if isinstance(value, dict):
|
|
13909
|
+
raw_label = value.get("value") or value.get("label") or value.get("name") or value.get("title") or value.get("optValue")
|
|
13910
|
+
if raw_label is not None and str(raw_label).strip():
|
|
13911
|
+
normalized.append(str(raw_label).strip())
|
|
13912
|
+
continue
|
|
13913
|
+
raw_id = value.get("id") or value.get("optionId") or value.get("option_id") or value.get("optId")
|
|
13914
|
+
raw_text = str(raw_id).strip() if raw_id is not None else ""
|
|
13915
|
+
else:
|
|
13916
|
+
raw_text = str(value).strip()
|
|
13917
|
+
if not raw_text:
|
|
13918
|
+
continue
|
|
13919
|
+
normalized.append(labels.get(raw_text, raw_text))
|
|
13920
|
+
return normalized
|
|
13921
|
+
|
|
13922
|
+
|
|
13923
|
+
def _qingbi_filter_judge_value(values: list[str]) -> Any:
|
|
13924
|
+
if not values:
|
|
13925
|
+
return ""
|
|
13926
|
+
if len(values) == 1:
|
|
13927
|
+
return values[0]
|
|
13928
|
+
return values
|
|
13929
|
+
|
|
13930
|
+
|
|
13836
13931
|
def _build_public_chart_filter_matrix(
|
|
13837
13932
|
rules: list[Any],
|
|
13838
13933
|
*,
|
|
@@ -13845,16 +13940,6 @@ def _build_public_chart_filter_matrix(
|
|
|
13845
13940
|
if not rules:
|
|
13846
13941
|
return []
|
|
13847
13942
|
group: list[dict[str, Any]] = []
|
|
13848
|
-
judge_map = {
|
|
13849
|
-
ViewFilterOperator.eq.value: 0,
|
|
13850
|
-
ViewFilterOperator.neq.value: 1,
|
|
13851
|
-
ViewFilterOperator.gte.value: 5,
|
|
13852
|
-
ViewFilterOperator.lte.value: 7,
|
|
13853
|
-
ViewFilterOperator.in_.value: 9,
|
|
13854
|
-
ViewFilterOperator.contains.value: 19,
|
|
13855
|
-
ViewFilterOperator.is_empty.value: 15,
|
|
13856
|
-
ViewFilterOperator.not_empty.value: 16,
|
|
13857
|
-
}
|
|
13858
13943
|
for rule in rules:
|
|
13859
13944
|
qingbi_field = _resolve_qingbi_chart_field(
|
|
13860
13945
|
getattr(rule, "field_name", None),
|
|
@@ -13866,13 +13951,15 @@ def _build_public_chart_filter_matrix(
|
|
|
13866
13951
|
form_field = qingbi_field.get("_public_form_field") if isinstance(qingbi_field.get("_public_form_field"), dict) else {}
|
|
13867
13952
|
operator = str(getattr(rule, "operator", ViewFilterOperator.eq.value).value if hasattr(getattr(rule, "operator", None), "value") else getattr(rule, "operator", ViewFilterOperator.eq.value))
|
|
13868
13953
|
values = list(getattr(rule, "values", []) or [])
|
|
13954
|
+
text_values = _qingbi_filter_text_values(values, form_field=form_field, qingbi_field=qingbi_field)
|
|
13869
13955
|
group.append(
|
|
13870
13956
|
{
|
|
13871
13957
|
"fieldId": field_id,
|
|
13872
13958
|
"fieldName": qingbi_field.get("fieldName") or form_field.get("name") or field_id,
|
|
13873
13959
|
"fieldType": qingbi_field.get("fieldType") or _qingbi_field_type_from_public_field(str(form_field.get("type") or "")),
|
|
13874
|
-
"judgeType":
|
|
13875
|
-
"
|
|
13960
|
+
"judgeType": _qingbi_filter_judge_type(operator),
|
|
13961
|
+
"judgeValue": _qingbi_filter_judge_value(text_values),
|
|
13962
|
+
"judgeValues": text_values,
|
|
13876
13963
|
"matchType": 1,
|
|
13877
13964
|
}
|
|
13878
13965
|
)
|
|
@@ -14285,15 +14372,26 @@ def _portal_layout_diagnostics(sections: list[PortalSectionPatch], components: l
|
|
|
14285
14372
|
pc_positions.append(pc)
|
|
14286
14373
|
section = sections[index] if index < len(sections) else None
|
|
14287
14374
|
source_type = str(getattr(section, "source_type", "") or "").lower() if section is not None else ""
|
|
14375
|
+
role = str(getattr(section, "role", "") or "").lower() if section is not None else ""
|
|
14288
14376
|
title = str(getattr(section, "title", "") or "").strip() if section is not None else None
|
|
14289
14377
|
cols = int(pc.get("cols") or 0)
|
|
14290
14378
|
rows = int(pc.get("rows") or 0)
|
|
14291
|
-
if source_type == "chart" and (cols <
|
|
14379
|
+
if source_type == "chart" and role == "metric" and (cols < 6 or rows < 5):
|
|
14380
|
+
warnings.append(_warning(
|
|
14381
|
+
"PORTAL_CHART_CARD_TOO_SMALL",
|
|
14382
|
+
"metric portal card is too small; use at least pc.cols >= 6 and pc.rows >= 5",
|
|
14383
|
+
section_index=index,
|
|
14384
|
+
title=title,
|
|
14385
|
+
role=role,
|
|
14386
|
+
pc=deepcopy(pc),
|
|
14387
|
+
))
|
|
14388
|
+
elif source_type == "chart" and role != "metric" and (cols < 8 or rows < 7):
|
|
14292
14389
|
warnings.append(_warning(
|
|
14293
14390
|
"PORTAL_CHART_CARD_TOO_SMALL",
|
|
14294
|
-
"chart portal card is too small; use at least pc.cols >= 8 and pc.rows >=
|
|
14391
|
+
"chart portal card is too small; use at least pc.cols >= 8 and pc.rows >= 7",
|
|
14295
14392
|
section_index=index,
|
|
14296
14393
|
title=title,
|
|
14394
|
+
role=role or None,
|
|
14297
14395
|
pc=deepcopy(pc),
|
|
14298
14396
|
))
|
|
14299
14397
|
if section is not None and section.position is not None and not bool(getattr(section.position, "mobile_provided", False)):
|
|
@@ -21083,6 +21181,9 @@ def _normalize_portal_components(components: Any) -> list[dict[str, Any]]:
|
|
|
21083
21181
|
"chart_key": chart_key,
|
|
21084
21182
|
"chart_name": str(config.get("chartComponentTitle") or title or "").strip() or None,
|
|
21085
21183
|
}
|
|
21184
|
+
backend_chart_type = _normalize_backend_chart_type(config.get("biChartType") or config.get("chartType"))
|
|
21185
|
+
if backend_chart_type in {"indicator", PublicChartType.target.value}:
|
|
21186
|
+
summary["role"] = "metric"
|
|
21086
21187
|
section_config = _portal_section_config_for_roundtrip(config, source_type="chart")
|
|
21087
21188
|
if section_config:
|
|
21088
21189
|
summary["config"] = section_config
|
|
@@ -21818,10 +21919,11 @@ _BATCH_READ_ENVELOPE_KEYS = {
|
|
|
21818
21919
|
|
|
21819
21920
|
|
|
21820
21921
|
def _batch_read_app_item(*, app_key: str, result: dict[str, Any], data_key: str) -> dict[str, Any]:
|
|
21922
|
+
status = str(result.get("status") or "").strip()
|
|
21821
21923
|
item: dict[str, Any] = {
|
|
21822
21924
|
"app_key": app_key,
|
|
21823
21925
|
"status": result.get("status"),
|
|
21824
|
-
"verified": bool(result.get("verified",
|
|
21926
|
+
"verified": bool(result.get("verified", status.lower() == "success")),
|
|
21825
21927
|
}
|
|
21826
21928
|
for key, value in result.items():
|
|
21827
21929
|
if key in _BATCH_READ_ENVELOPE_KEYS or key in {"status", "app_key", "verified"}:
|
|
@@ -4,6 +4,7 @@ import argparse
|
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
from ..context import CliContext
|
|
7
|
+
from ..json_io import load_json_value
|
|
7
8
|
from .common import load_list_arg, load_object_arg, raise_config_error, require_list_arg
|
|
8
9
|
|
|
9
10
|
|
|
@@ -15,6 +16,55 @@ def _parse_app_keys_arg(raw: Any) -> list[str] | None:
|
|
|
15
16
|
return keys if keys else None
|
|
16
17
|
|
|
17
18
|
|
|
19
|
+
def _load_schema_apps_payload(path: str | None, *, cli_package_id: int | None) -> tuple[list[Any], int | None]:
|
|
20
|
+
"""Load schema --apps-file.
|
|
21
|
+
|
|
22
|
+
Public docs recommend {"package_id": 123, "apps": [...]}; older payloads used
|
|
23
|
+
a raw apps array, and some agents accidentally wrapped the object in a
|
|
24
|
+
singleton array. Keep all three shapes on the same multi-app path.
|
|
25
|
+
"""
|
|
26
|
+
if not path:
|
|
27
|
+
return [], cli_package_id
|
|
28
|
+
payload = load_json_value(path, option_name="--apps-file")
|
|
29
|
+
package_id = cli_package_id
|
|
30
|
+
if isinstance(payload, dict):
|
|
31
|
+
if package_id is None:
|
|
32
|
+
raw_package_id = payload.get("package_id")
|
|
33
|
+
if isinstance(raw_package_id, int):
|
|
34
|
+
package_id = raw_package_id
|
|
35
|
+
elif raw_package_id is not None and str(raw_package_id).strip().isdigit():
|
|
36
|
+
package_id = int(str(raw_package_id).strip())
|
|
37
|
+
apps = payload.get("apps")
|
|
38
|
+
elif isinstance(payload, list):
|
|
39
|
+
if len(payload) == 1 and isinstance(payload[0], dict) and "apps" in payload[0]:
|
|
40
|
+
wrapper = payload[0]
|
|
41
|
+
if package_id is None:
|
|
42
|
+
raw_package_id = wrapper.get("package_id")
|
|
43
|
+
if isinstance(raw_package_id, int):
|
|
44
|
+
package_id = raw_package_id
|
|
45
|
+
elif raw_package_id is not None and str(raw_package_id).strip().isdigit():
|
|
46
|
+
package_id = int(str(raw_package_id).strip())
|
|
47
|
+
apps = wrapper.get("apps")
|
|
48
|
+
elif any(isinstance(item, dict) and "apps" in item for item in payload):
|
|
49
|
+
raise_config_error(
|
|
50
|
+
"APPS_FILE_SHAPE_INVALID: --apps-file may be {package_id, apps}, a raw apps array, or a singleton wrapper array only.",
|
|
51
|
+
fix_hint='Use {"package_id": 123, "apps": [{"client_key": "main", "app_name": "主表", "icon": "database", "color": "blue", "add_fields": []}]}',
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
apps = payload
|
|
55
|
+
else:
|
|
56
|
+
raise_config_error(
|
|
57
|
+
"APPS_FILE_SHAPE_INVALID: --apps-file must be {package_id, apps} or a JSON array of app items.",
|
|
58
|
+
fix_hint='Use {"package_id": 123, "apps": [{"client_key": "main", "app_name": "主表", "icon": "database", "color": "blue", "add_fields": []}]}',
|
|
59
|
+
)
|
|
60
|
+
if not isinstance(apps, list):
|
|
61
|
+
raise_config_error(
|
|
62
|
+
"APPS_FILE_SHAPE_INVALID: --apps-file apps must be a JSON array.",
|
|
63
|
+
fix_hint='Use {"package_id": 123, "apps": [{"client_key": "main", "app_name": "主表", "icon": "database", "color": "blue", "add_fields": []}]}',
|
|
64
|
+
)
|
|
65
|
+
return apps, package_id
|
|
66
|
+
|
|
67
|
+
|
|
18
68
|
def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
19
69
|
parser = subparsers.add_parser("builder", aliases=["build"], help="稳定 builder 命令")
|
|
20
70
|
builder_subparsers = parser.add_subparsers(dest="builder_command", required=True)
|
|
@@ -215,7 +265,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
215
265
|
schema_apply_apply.add_argument("--visibility-file")
|
|
216
266
|
schema_apply_apply.add_argument("--create-if-missing", action="store_true")
|
|
217
267
|
schema_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
|
|
218
|
-
schema_apply_apply.add_argument("--apps-file", help="多应用 schema JSON
|
|
268
|
+
schema_apply_apply.add_argument("--apps-file", help="多应用 schema JSON;推荐对象 {package_id, apps},兼容 raw apps 数组;支持 relation target_app_ref")
|
|
219
269
|
schema_apply_apply.add_argument("--add-fields-file", help="字段 JSON 数组;字段可用 as_data_title/as_data_cover 标记数据标题/封面")
|
|
220
270
|
schema_apply_apply.add_argument("--update-fields-file", help="字段更新 JSON 数组;set 内可用 as_data_title/as_data_cover 标记数据标题/封面")
|
|
221
271
|
schema_apply_apply.add_argument("--remove-fields-file")
|
|
@@ -555,6 +605,7 @@ def _handle_app_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
555
605
|
return context.builder.app_get_associated_resources(profile=args.profile, app_key=app_key)
|
|
556
606
|
if app_keys:
|
|
557
607
|
batch_handlers = {
|
|
608
|
+
"summary": context.builder.app_get,
|
|
558
609
|
"fields": context.builder.app_get_fields,
|
|
559
610
|
"layout": context.builder.app_get_layout,
|
|
560
611
|
"views": context.builder.app_get_views,
|
|
@@ -562,7 +613,7 @@ def _handle_app_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
562
613
|
"charts": context.builder.app_get_charts,
|
|
563
614
|
}
|
|
564
615
|
if section not in batch_handlers:
|
|
565
|
-
raise_config_error(f"app get --app-keys does not support section '{section}'", fix_hint="Batch reads support: fields, layout, views, flow, charts, buttons, associated-resources")
|
|
616
|
+
raise_config_error(f"app get --app-keys does not support section '{section}'", fix_hint="Batch reads support: summary, fields, layout, views, flow, charts, buttons, associated-resources")
|
|
566
617
|
return batch_handlers[section](profile=args.profile, app_keys=app_keys)
|
|
567
618
|
handlers = {
|
|
568
619
|
"summary": context.builder.app_get,
|
|
@@ -601,26 +652,26 @@ def _handle_chart_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
601
652
|
|
|
602
653
|
|
|
603
654
|
def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
604
|
-
apps =
|
|
655
|
+
apps, apps_package_id = _load_schema_apps_payload(args.apps_file, cli_package_id=args.package_id)
|
|
605
656
|
if args.apps_file:
|
|
606
657
|
if not apps:
|
|
607
658
|
raise_config_error(
|
|
608
659
|
"schema apply multi-app mode requires a non-empty --apps-file.",
|
|
609
|
-
fix_hint=
|
|
660
|
+
fix_hint='Pass {"package_id": 123, "apps": [{"client_key": "main", "app_name": "主表", "icon": "database", "color": "blue", "add_fields": []}]}',
|
|
610
661
|
)
|
|
611
662
|
if args.app_key or args.app_name or args.app_title or args.add_fields_file or args.update_fields_file or args.remove_fields_file:
|
|
612
663
|
raise_config_error(
|
|
613
664
|
"schema apply multi-app mode accepts --package-id/--create-if-missing plus --apps-file only.",
|
|
614
665
|
fix_hint="Use `--apps-file` for batch mode, or remove `--apps-file` and use the single-app arguments.",
|
|
615
666
|
)
|
|
616
|
-
if
|
|
667
|
+
if apps_package_id is None:
|
|
617
668
|
raise_config_error(
|
|
618
669
|
"schema apply multi-app mode requires --package-id.",
|
|
619
|
-
fix_hint="Pass `--package-id
|
|
670
|
+
fix_hint="Pass root `--package-id`, or include package_id in the --apps-file object.",
|
|
620
671
|
)
|
|
621
672
|
return context.builder.app_schema_apply(
|
|
622
673
|
profile=args.profile,
|
|
623
|
-
package_id=
|
|
674
|
+
package_id=apps_package_id,
|
|
624
675
|
visibility=load_object_arg(args.visibility_file, option_name="--visibility-file"),
|
|
625
676
|
create_if_missing=bool(args.create_if_missing),
|
|
626
677
|
publish=bool(args.publish),
|
|
@@ -298,7 +298,9 @@ class AiBuilderTools(ToolBase):
|
|
|
298
298
|
)
|
|
299
299
|
|
|
300
300
|
@mcp.tool()
|
|
301
|
-
def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
|
|
301
|
+
def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
|
|
302
|
+
if app_keys:
|
|
303
|
+
return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get, data_key="summary", tool_name="app_get")
|
|
302
304
|
return self.app_get(profile=profile, app_key=app_key)
|
|
303
305
|
|
|
304
306
|
@mcp.tool()
|
|
@@ -1302,8 +1304,12 @@ class AiBuilderTools(ToolBase):
|
|
|
1302
1304
|
))
|
|
1303
1305
|
|
|
1304
1306
|
@tool_cn_name("应用详情查询")
|
|
1305
|
-
def app_get(self, *, profile: str, app_key: str) -> JSONObject:
|
|
1307
|
+
def app_get(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
|
|
1306
1308
|
"""执行应用相关逻辑。"""
|
|
1309
|
+
if app_keys:
|
|
1310
|
+
return _publicize_package_fields(
|
|
1311
|
+
self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get, data_key="summary", tool_name="app_get")
|
|
1312
|
+
)
|
|
1307
1313
|
normalized_args = {"app_key": app_key}
|
|
1308
1314
|
return _publicize_package_fields(_safe_tool_call(
|
|
1309
1315
|
lambda: self._facade.app_get(profile=profile, app_key=app_key),
|
|
@@ -5323,7 +5329,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5323
5329
|
},
|
|
5324
5330
|
},
|
|
5325
5331
|
"app_get": {
|
|
5326
|
-
"allowed_keys": ["app_key"],
|
|
5332
|
+
"allowed_keys": ["app_key", "app_keys"],
|
|
5327
5333
|
"aliases": {},
|
|
5328
5334
|
"allowed_values": {},
|
|
5329
5335
|
"execution_notes": [
|
|
@@ -5335,6 +5341,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5335
5341
|
"returns normalized app visibility when backend auth is readable",
|
|
5336
5342
|
"custom_buttons[].button_id is the id required by app_custom_buttons_apply view_configs[].buttons[].button_ref",
|
|
5337
5343
|
"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",
|
|
5344
|
+
"accepts app_keys[] for batch summary read; batch returns {status, apps[], errors[]} where each apps[] item preserves the single app_get summary fields",
|
|
5338
5345
|
],
|
|
5339
5346
|
"minimal_example": {
|
|
5340
5347
|
"profile": "default",
|
|
@@ -5565,7 +5572,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5565
5572
|
"viewRef": "view_ref",
|
|
5566
5573
|
"dashStyleConfigBO": "dash_style_config",
|
|
5567
5574
|
},
|
|
5568
|
-
"section_allowed_keys": ["title", "source_type", "position", "dash_style_config", "config", "chart_ref", "view_ref", "text", "url"],
|
|
5575
|
+
"section_allowed_keys": ["title", "source_type", "role", "position", "dash_style_config", "config", "chart_ref", "view_ref", "text", "url"],
|
|
5569
5576
|
"section_aliases": {
|
|
5570
5577
|
"sourceType": "source_type",
|
|
5571
5578
|
"chartRef": "chart_ref",
|
|
@@ -5574,6 +5581,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5574
5581
|
},
|
|
5575
5582
|
"allowed_values": {
|
|
5576
5583
|
"section.source_type": ["chart", "view", "grid", "filter", "text", "link"],
|
|
5584
|
+
"section.role": ["metric"],
|
|
5577
5585
|
"workspace_icon.icon_names": list(WORKSPACE_ICON_NAMES),
|
|
5578
5586
|
"workspace_icon.icon_colors": list(WORKSPACE_ICON_COLORS),
|
|
5579
5587
|
"workspace_icon.generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
|
|
@@ -5596,6 +5604,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5596
5604
|
"chart_ref resolves by chart_id/chart_key first, then exact unique chart_name",
|
|
5597
5605
|
"view_ref resolves by view_key first, then exact unique view_name",
|
|
5598
5606
|
"pc layout uses a 24-column grid; mobile layout uses a 6-column grid",
|
|
5607
|
+
"set section.role=metric only for target/indicator chart cards; metric cards use pc.rows >= 5 and pc.cols >= 6, while ordinary BI charts use pc.rows >= 7 and pc.cols >= 8",
|
|
5599
5608
|
"if unsure about layout, omit position or use layout_preset=auto/dashboard_2col/dashboard_3col",
|
|
5600
5609
|
"two-column pc layout should use x=0/12 with cols=12; three-column pc layout should use x=0/8/16 with cols=8",
|
|
5601
5610
|
"x=0/6 with cols=6 only occupies the left half of the pc portal and triggers PORTAL_LAYOUT_HALF_WIDTH",
|