@josephyan/qingflow-cli 1.1.2 → 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/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md +6 -0
- package/src/qingflow_mcp/builder_facade/models.py +82 -0
- package/src/qingflow_mcp/builder_facade/service.py +143 -17
- package/src/qingflow_mcp/cli/commands/builder.py +90 -8
- package/src/qingflow_mcp/tools/ai_builder_tools.py +50 -5
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
|
@@ -428,6 +428,7 @@ qingflow builder button apply \
|
|
|
428
428
|
默认使用 `qingflow builder associated-resource apply` 管理应用级关联资源池与视图展示配置。
|
|
429
429
|
|
|
430
430
|
- 权限分层:`upsert_resources` / `patch_resources` / 删除 / 排序应用级关联资源池走 **EditAppAuth**;`view_configs` 修改某个视图里的关联资源展示配置,还需要视图配置侧的 **ViewManagementAuth**(未开启高级应用权限时回落到 **DataManageAuth**)。
|
|
431
|
+
- 多应用批量配置时可用 `--apps-file`,文件为 JSON 数组,每项写 `{ "app_key": "...", "upsert_resources": [...], "patch_resources": [...], "remove_associated_item_ids": [...], "reorder_associated_item_ids": [...], "view_configs": [...] }`;不要和单应用的 `--app-key` / `--*-file` 混用。
|
|
431
432
|
|
|
432
433
|
```bash
|
|
433
434
|
qingflow builder associated-resource apply \
|
|
@@ -436,6 +437,11 @@ qingflow builder associated-resource apply \
|
|
|
436
437
|
--view-configs-file tmp/associated_view_configs.json
|
|
437
438
|
```
|
|
438
439
|
|
|
440
|
+
```bash
|
|
441
|
+
qingflow builder associated-resource apply \
|
|
442
|
+
--apps-file tmp/associated_resource_apps.json
|
|
443
|
+
```
|
|
444
|
+
|
|
439
445
|
- Shell 退出码为 0、重定向成功或 `echo OK` 只表示命令执行完,不表示业务成功。必须读取输出 JSON,检查顶层 `status`、`error_code`、`warnings`、`blocking_issues`;`status: failed`、`partial_success`、`ASSOCIATED_RESOURCES_APPLY_BLOCKED` 都不能当作完成。
|
|
440
446
|
- 关联资源要生效必须同时处理两层:应用级资源池(`upsert_resources` / `patch_resources`)和视图详情展示绑定(`view_configs`)。只传 `--upsert-resources-file` 往往只是在资源池准备资源,不会让按钮或视图详情里看到关联资源。
|
|
441
447
|
- `builder views apply` 新建视图会默认打开详情页关联查看(展示全部应用级关联资源);若只需要默认展示全部资源,创建视图时不用额外写 `view_configs`。需要指定部分资源、关闭展示或配置匹配筛选时,仍必须使用本工具的 `view_configs` / `match_mappings`。
|
|
@@ -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,
|
|
@@ -5486,6 +5514,9 @@ class AiBuilderFacade:
|
|
|
5486
5514
|
result = []
|
|
5487
5515
|
for c in components:
|
|
5488
5516
|
item = {k: v for k, v in c.items() if k != "order"}
|
|
5517
|
+
position = item.get("position")
|
|
5518
|
+
if isinstance(position, dict):
|
|
5519
|
+
item["position"] = _portal_component_position_for_request(position)
|
|
5489
5520
|
result.append(item)
|
|
5490
5521
|
return result
|
|
5491
5522
|
|
|
@@ -13830,6 +13861,73 @@ def _two_gauge_metric_fields(metrics: list[dict[str, Any]], *, requested_metric_
|
|
|
13830
13861
|
return [deepcopy(metrics[0]), deepcopy(metrics[1])]
|
|
13831
13862
|
|
|
13832
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
|
+
|
|
13833
13931
|
def _build_public_chart_filter_matrix(
|
|
13834
13932
|
rules: list[Any],
|
|
13835
13933
|
*,
|
|
@@ -13842,16 +13940,6 @@ def _build_public_chart_filter_matrix(
|
|
|
13842
13940
|
if not rules:
|
|
13843
13941
|
return []
|
|
13844
13942
|
group: list[dict[str, Any]] = []
|
|
13845
|
-
judge_map = {
|
|
13846
|
-
ViewFilterOperator.eq.value: 0,
|
|
13847
|
-
ViewFilterOperator.neq.value: 1,
|
|
13848
|
-
ViewFilterOperator.gte.value: 5,
|
|
13849
|
-
ViewFilterOperator.lte.value: 7,
|
|
13850
|
-
ViewFilterOperator.in_.value: 9,
|
|
13851
|
-
ViewFilterOperator.contains.value: 19,
|
|
13852
|
-
ViewFilterOperator.is_empty.value: 15,
|
|
13853
|
-
ViewFilterOperator.not_empty.value: 16,
|
|
13854
|
-
}
|
|
13855
13943
|
for rule in rules:
|
|
13856
13944
|
qingbi_field = _resolve_qingbi_chart_field(
|
|
13857
13945
|
getattr(rule, "field_name", None),
|
|
@@ -13863,13 +13951,15 @@ def _build_public_chart_filter_matrix(
|
|
|
13863
13951
|
form_field = qingbi_field.get("_public_form_field") if isinstance(qingbi_field.get("_public_form_field"), dict) else {}
|
|
13864
13952
|
operator = str(getattr(rule, "operator", ViewFilterOperator.eq.value).value if hasattr(getattr(rule, "operator", None), "value") else getattr(rule, "operator", ViewFilterOperator.eq.value))
|
|
13865
13953
|
values = list(getattr(rule, "values", []) or [])
|
|
13954
|
+
text_values = _qingbi_filter_text_values(values, form_field=form_field, qingbi_field=qingbi_field)
|
|
13866
13955
|
group.append(
|
|
13867
13956
|
{
|
|
13868
13957
|
"fieldId": field_id,
|
|
13869
13958
|
"fieldName": qingbi_field.get("fieldName") or form_field.get("name") or field_id,
|
|
13870
13959
|
"fieldType": qingbi_field.get("fieldType") or _qingbi_field_type_from_public_field(str(form_field.get("type") or "")),
|
|
13871
|
-
"judgeType":
|
|
13872
|
-
"
|
|
13960
|
+
"judgeType": _qingbi_filter_judge_type(operator),
|
|
13961
|
+
"judgeValue": _qingbi_filter_judge_value(text_values),
|
|
13962
|
+
"judgeValues": text_values,
|
|
13873
13963
|
"matchType": 1,
|
|
13874
13964
|
}
|
|
13875
13965
|
)
|
|
@@ -14180,6 +14270,27 @@ def _portal_position_payload(position: Any, *, inferred_mobile_y: int = 0) -> di
|
|
|
14180
14270
|
}
|
|
14181
14271
|
|
|
14182
14272
|
|
|
14273
|
+
def _portal_component_position_for_request(position: dict[str, Any]) -> dict[str, Any]:
|
|
14274
|
+
"""Normalize portal_get component positions into PortalSectionPatch input shape."""
|
|
14275
|
+
payload = deepcopy(position)
|
|
14276
|
+
if isinstance(payload.get("pc"), dict) or isinstance(payload.get("mobile"), dict):
|
|
14277
|
+
return payload
|
|
14278
|
+
pc_keys = {"x", "y", "cols", "rows"}
|
|
14279
|
+
if any(key in payload for key in pc_keys):
|
|
14280
|
+
pc = {
|
|
14281
|
+
"x": int(payload.get("x") or 0),
|
|
14282
|
+
"y": int(payload.get("y") or 0),
|
|
14283
|
+
"cols": int(payload.get("cols") or payload.get("w") or 12),
|
|
14284
|
+
"rows": int(payload.get("rows") or payload.get("h") or 8),
|
|
14285
|
+
}
|
|
14286
|
+
normalized: dict[str, Any] = {"pc": pc}
|
|
14287
|
+
mobile = payload.get("mobile")
|
|
14288
|
+
if isinstance(mobile, dict):
|
|
14289
|
+
normalized["mobile"] = mobile
|
|
14290
|
+
return normalized
|
|
14291
|
+
return payload
|
|
14292
|
+
|
|
14293
|
+
|
|
14183
14294
|
def _portal_component_position_public(
|
|
14184
14295
|
source_type: Any,
|
|
14185
14296
|
*,
|
|
@@ -14261,15 +14372,26 @@ def _portal_layout_diagnostics(sections: list[PortalSectionPatch], components: l
|
|
|
14261
14372
|
pc_positions.append(pc)
|
|
14262
14373
|
section = sections[index] if index < len(sections) else None
|
|
14263
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 ""
|
|
14264
14376
|
title = str(getattr(section, "title", "") or "").strip() if section is not None else None
|
|
14265
14377
|
cols = int(pc.get("cols") or 0)
|
|
14266
14378
|
rows = int(pc.get("rows") or 0)
|
|
14267
|
-
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):
|
|
14268
14389
|
warnings.append(_warning(
|
|
14269
14390
|
"PORTAL_CHART_CARD_TOO_SMALL",
|
|
14270
|
-
"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",
|
|
14271
14392
|
section_index=index,
|
|
14272
14393
|
title=title,
|
|
14394
|
+
role=role or None,
|
|
14273
14395
|
pc=deepcopy(pc),
|
|
14274
14396
|
))
|
|
14275
14397
|
if section is not None and section.position is not None and not bool(getattr(section.position, "mobile_provided", False)):
|
|
@@ -21059,6 +21181,9 @@ def _normalize_portal_components(components: Any) -> list[dict[str, Any]]:
|
|
|
21059
21181
|
"chart_key": chart_key,
|
|
21060
21182
|
"chart_name": str(config.get("chartComponentTitle") or title or "").strip() or None,
|
|
21061
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"
|
|
21062
21187
|
section_config = _portal_section_config_for_roundtrip(config, source_type="chart")
|
|
21063
21188
|
if section_config:
|
|
21064
21189
|
summary["config"] = section_config
|
|
@@ -21794,10 +21919,11 @@ _BATCH_READ_ENVELOPE_KEYS = {
|
|
|
21794
21919
|
|
|
21795
21920
|
|
|
21796
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()
|
|
21797
21923
|
item: dict[str, Any] = {
|
|
21798
21924
|
"app_key": app_key,
|
|
21799
21925
|
"status": result.get("status"),
|
|
21800
|
-
"verified": bool(result.get("verified",
|
|
21926
|
+
"verified": bool(result.get("verified", status.lower() == "success")),
|
|
21801
21927
|
}
|
|
21802
21928
|
for key, value in result.items():
|
|
21803
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)
|
|
@@ -166,12 +216,13 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
166
216
|
associated_resource_get.set_defaults(handler=_handle_associated_resource_get, format_hint="builder_summary")
|
|
167
217
|
|
|
168
218
|
associated_resource_apply = associated_resource_subparsers.add_parser("apply", help="声明式管理应用关联资源池和视图展示配置")
|
|
169
|
-
associated_resource_apply.add_argument("--app-key",
|
|
219
|
+
associated_resource_apply.add_argument("--app-key", default="")
|
|
170
220
|
associated_resource_apply.add_argument("--upsert-resources-file")
|
|
171
221
|
associated_resource_apply.add_argument("--patch-resources-file")
|
|
172
222
|
associated_resource_apply.add_argument("--remove-associated-item-ids-file")
|
|
173
223
|
associated_resource_apply.add_argument("--reorder-associated-item-ids-file")
|
|
174
224
|
associated_resource_apply.add_argument("--view-configs-file")
|
|
225
|
+
associated_resource_apply.add_argument("--apps-file", help="多应用批量关联资源 JSON 数组,每项含 app_key + upsert_resources/patch_resources/remove_associated_item_ids/reorder_associated_item_ids/view_configs")
|
|
175
226
|
associated_resource_apply.set_defaults(handler=_handle_associated_resource_apply, format_hint="builder_summary", force_json_output=True)
|
|
176
227
|
|
|
177
228
|
portal = builder_subparsers.add_parser("portal", help="门户")
|
|
@@ -214,7 +265,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
214
265
|
schema_apply_apply.add_argument("--visibility-file")
|
|
215
266
|
schema_apply_apply.add_argument("--create-if-missing", action="store_true")
|
|
216
267
|
schema_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
|
|
217
|
-
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")
|
|
218
269
|
schema_apply_apply.add_argument("--add-fields-file", help="字段 JSON 数组;字段可用 as_data_title/as_data_cover 标记数据标题/封面")
|
|
219
270
|
schema_apply_apply.add_argument("--update-fields-file", help="字段更新 JSON 数组;set 内可用 as_data_title/as_data_cover 标记数据标题/封面")
|
|
220
271
|
schema_apply_apply.add_argument("--remove-fields-file")
|
|
@@ -456,6 +507,36 @@ def _handle_button_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
456
507
|
|
|
457
508
|
|
|
458
509
|
def _handle_associated_resource_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
510
|
+
apps = load_list_arg(getattr(args, "apps_file", None), option_name="--apps-file")
|
|
511
|
+
if apps:
|
|
512
|
+
single_app_args = [
|
|
513
|
+
a for a in [
|
|
514
|
+
"--app-key" if args.app_key else None,
|
|
515
|
+
"--upsert-resources-file" if getattr(args, "upsert_resources_file", None) else None,
|
|
516
|
+
"--patch-resources-file" if getattr(args, "patch_resources_file", None) else None,
|
|
517
|
+
"--remove-associated-item-ids-file" if getattr(args, "remove_associated_item_ids_file", None) else None,
|
|
518
|
+
"--reorder-associated-item-ids-file" if getattr(args, "reorder_associated_item_ids_file", None) else None,
|
|
519
|
+
"--view-configs-file" if getattr(args, "view_configs_file", None) else None,
|
|
520
|
+
] if a
|
|
521
|
+
]
|
|
522
|
+
if single_app_args:
|
|
523
|
+
raise_config_error(
|
|
524
|
+
f"associated-resource apply --apps-file cannot be combined with {', '.join(single_app_args)}.",
|
|
525
|
+
fix_hint="Use --apps-file for batch mode (each item contains app_key + per-app params), or remove --apps-file for single-app mode.",
|
|
526
|
+
)
|
|
527
|
+
if apps:
|
|
528
|
+
return context.builder.app_associated_resources_apply(
|
|
529
|
+
profile=args.profile,
|
|
530
|
+
app_key="",
|
|
531
|
+
upsert_resources=[],
|
|
532
|
+
patch_resources=[],
|
|
533
|
+
remove_associated_item_ids=[],
|
|
534
|
+
reorder_associated_item_ids=[],
|
|
535
|
+
view_configs=[],
|
|
536
|
+
apps=apps,
|
|
537
|
+
)
|
|
538
|
+
if not args.app_key:
|
|
539
|
+
raise_config_error("associated-resource apply requires --app-key or --apps-file", fix_hint="Pass --app-key APP_KEY for single-app mode, or --apps-file for batch mode.")
|
|
459
540
|
return context.builder.app_associated_resources_apply(
|
|
460
541
|
profile=args.profile,
|
|
461
542
|
app_key=args.app_key,
|
|
@@ -524,6 +605,7 @@ def _handle_app_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
524
605
|
return context.builder.app_get_associated_resources(profile=args.profile, app_key=app_key)
|
|
525
606
|
if app_keys:
|
|
526
607
|
batch_handlers = {
|
|
608
|
+
"summary": context.builder.app_get,
|
|
527
609
|
"fields": context.builder.app_get_fields,
|
|
528
610
|
"layout": context.builder.app_get_layout,
|
|
529
611
|
"views": context.builder.app_get_views,
|
|
@@ -531,7 +613,7 @@ def _handle_app_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
531
613
|
"charts": context.builder.app_get_charts,
|
|
532
614
|
}
|
|
533
615
|
if section not in batch_handlers:
|
|
534
|
-
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")
|
|
535
617
|
return batch_handlers[section](profile=args.profile, app_keys=app_keys)
|
|
536
618
|
handlers = {
|
|
537
619
|
"summary": context.builder.app_get,
|
|
@@ -570,26 +652,26 @@ def _handle_chart_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
570
652
|
|
|
571
653
|
|
|
572
654
|
def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
573
|
-
apps =
|
|
655
|
+
apps, apps_package_id = _load_schema_apps_payload(args.apps_file, cli_package_id=args.package_id)
|
|
574
656
|
if args.apps_file:
|
|
575
657
|
if not apps:
|
|
576
658
|
raise_config_error(
|
|
577
659
|
"schema apply multi-app mode requires a non-empty --apps-file.",
|
|
578
|
-
fix_hint=
|
|
660
|
+
fix_hint='Pass {"package_id": 123, "apps": [{"client_key": "main", "app_name": "主表", "icon": "database", "color": "blue", "add_fields": []}]}',
|
|
579
661
|
)
|
|
580
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:
|
|
581
663
|
raise_config_error(
|
|
582
664
|
"schema apply multi-app mode accepts --package-id/--create-if-missing plus --apps-file only.",
|
|
583
665
|
fix_hint="Use `--apps-file` for batch mode, or remove `--apps-file` and use the single-app arguments.",
|
|
584
666
|
)
|
|
585
|
-
if
|
|
667
|
+
if apps_package_id is None:
|
|
586
668
|
raise_config_error(
|
|
587
669
|
"schema apply multi-app mode requires --package-id.",
|
|
588
|
-
fix_hint="Pass `--package-id
|
|
670
|
+
fix_hint="Pass root `--package-id`, or include package_id in the --apps-file object.",
|
|
589
671
|
)
|
|
590
672
|
return context.builder.app_schema_apply(
|
|
591
673
|
profile=args.profile,
|
|
592
|
-
package_id=
|
|
674
|
+
package_id=apps_package_id,
|
|
593
675
|
visibility=load_object_arg(args.visibility_file, option_name="--visibility-file"),
|
|
594
676
|
create_if_missing=bool(args.create_if_missing),
|
|
595
677
|
publish=bool(args.publish),
|
|
@@ -284,6 +284,7 @@ class AiBuilderTools(ToolBase):
|
|
|
284
284
|
remove_associated_item_ids: list[int] | None = None,
|
|
285
285
|
reorder_associated_item_ids: list[int] | None = None,
|
|
286
286
|
view_configs: list[JSONObject] | None = None,
|
|
287
|
+
apps: list[JSONObject] | None = None,
|
|
287
288
|
) -> JSONObject:
|
|
288
289
|
return self.app_associated_resources_apply(
|
|
289
290
|
profile=profile,
|
|
@@ -293,10 +294,13 @@ class AiBuilderTools(ToolBase):
|
|
|
293
294
|
remove_associated_item_ids=remove_associated_item_ids or [],
|
|
294
295
|
reorder_associated_item_ids=reorder_associated_item_ids or [],
|
|
295
296
|
view_configs=view_configs or [],
|
|
297
|
+
apps=apps,
|
|
296
298
|
)
|
|
297
299
|
|
|
298
300
|
@mcp.tool()
|
|
299
|
-
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")
|
|
300
304
|
return self.app_get(profile=profile, app_key=app_key)
|
|
301
305
|
|
|
302
306
|
@mcp.tool()
|
|
@@ -1120,8 +1124,24 @@ class AiBuilderTools(ToolBase):
|
|
|
1120
1124
|
reorder_associated_item_ids: list[int],
|
|
1121
1125
|
view_configs: list[JSONObject],
|
|
1122
1126
|
patch_resources: list[JSONObject] | None = None,
|
|
1127
|
+
apps: list[JSONObject] | None = None,
|
|
1123
1128
|
) -> JSONObject:
|
|
1124
1129
|
"""执行应用关联资源 apply 逻辑。"""
|
|
1130
|
+
if apps:
|
|
1131
|
+
return self._facade._batch_write_apps(
|
|
1132
|
+
profile=profile,
|
|
1133
|
+
apps=apps,
|
|
1134
|
+
single_writer=lambda profile, app_key, **kw: self.app_associated_resources_apply(
|
|
1135
|
+
profile=profile,
|
|
1136
|
+
app_key=app_key,
|
|
1137
|
+
upsert_resources=kw.get("upsert_resources", []),
|
|
1138
|
+
patch_resources=kw.get("patch_resources", []),
|
|
1139
|
+
remove_associated_item_ids=kw.get("remove_associated_item_ids", []),
|
|
1140
|
+
reorder_associated_item_ids=kw.get("reorder_associated_item_ids", []),
|
|
1141
|
+
view_configs=kw.get("view_configs", []),
|
|
1142
|
+
),
|
|
1143
|
+
tool_name="app_associated_resources_apply",
|
|
1144
|
+
)
|
|
1125
1145
|
raw_request = {
|
|
1126
1146
|
"app_key": app_key,
|
|
1127
1147
|
"upsert_resources": upsert_resources,
|
|
@@ -1284,8 +1304,12 @@ class AiBuilderTools(ToolBase):
|
|
|
1284
1304
|
))
|
|
1285
1305
|
|
|
1286
1306
|
@tool_cn_name("应用详情查询")
|
|
1287
|
-
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:
|
|
1288
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
|
+
)
|
|
1289
1313
|
normalized_args = {"app_key": app_key}
|
|
1290
1314
|
return _publicize_package_fields(_safe_tool_call(
|
|
1291
1315
|
lambda: self._facade.app_get(profile=profile, app_key=app_key),
|
|
@@ -4458,6 +4482,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4458
4482
|
"app_associated_resources_apply": {
|
|
4459
4483
|
"allowed_keys": [
|
|
4460
4484
|
"app_key",
|
|
4485
|
+
"apps",
|
|
4461
4486
|
"upsert_resources",
|
|
4462
4487
|
"patch_resources",
|
|
4463
4488
|
"remove_associated_item_ids",
|
|
@@ -4527,6 +4552,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4527
4552
|
"if an associated resource delete returns readback_status=unavailable or still_exists, treat the result as readback pending and do not blindly repeat the delete",
|
|
4528
4553
|
"this tool publishes after at least one write succeeds; there is no draft-only mode",
|
|
4529
4554
|
"visible=false hides the associated-resource area without clearing previous selected ids; visible=true with limit_type=all shows the whole app-level pool",
|
|
4555
|
+
"accepts apps[] for multi-app batch; each item is {app_key, upsert_resources?, patch_resources?, remove_associated_item_ids?, reorder_associated_item_ids?, view_configs?}",
|
|
4530
4556
|
],
|
|
4531
4557
|
"minimal_example": {
|
|
4532
4558
|
"profile": "default",
|
|
@@ -4557,6 +4583,22 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4557
4583
|
}
|
|
4558
4584
|
],
|
|
4559
4585
|
},
|
|
4586
|
+
"batch_example": {
|
|
4587
|
+
"profile": "default",
|
|
4588
|
+
"apps": [
|
|
4589
|
+
{
|
|
4590
|
+
"app_key": "APP_1",
|
|
4591
|
+
"upsert_resources": [
|
|
4592
|
+
{"client_key": "orders_view", "graph_type": "view", "target_app_key": "ORDER_APP", "view_key": "ORDER_VIEW"}
|
|
4593
|
+
],
|
|
4594
|
+
"view_configs": [{"view_key": "MAIN_VIEW", "limit_type": "select", "associated_item_refs": ["orders_view"]}],
|
|
4595
|
+
},
|
|
4596
|
+
{
|
|
4597
|
+
"app_key": "APP_2",
|
|
4598
|
+
"view_configs": [{"view_key": "MAIN_VIEW", "visible": True, "limit_type": "all"}],
|
|
4599
|
+
},
|
|
4600
|
+
],
|
|
4601
|
+
},
|
|
4560
4602
|
},
|
|
4561
4603
|
"app_schema_plan": {
|
|
4562
4604
|
"allowed_keys": ["app_key", "package_id", "app_name", "icon", "color", "visibility", "create_if_missing", "add_fields", "update_fields", "remove_fields"],
|
|
@@ -5287,7 +5329,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5287
5329
|
},
|
|
5288
5330
|
},
|
|
5289
5331
|
"app_get": {
|
|
5290
|
-
"allowed_keys": ["app_key"],
|
|
5332
|
+
"allowed_keys": ["app_key", "app_keys"],
|
|
5291
5333
|
"aliases": {},
|
|
5292
5334
|
"allowed_values": {},
|
|
5293
5335
|
"execution_notes": [
|
|
@@ -5299,6 +5341,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5299
5341
|
"returns normalized app visibility when backend auth is readable",
|
|
5300
5342
|
"custom_buttons[].button_id is the id required by app_custom_buttons_apply view_configs[].buttons[].button_ref",
|
|
5301
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",
|
|
5302
5345
|
],
|
|
5303
5346
|
"minimal_example": {
|
|
5304
5347
|
"profile": "default",
|
|
@@ -5529,7 +5572,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5529
5572
|
"viewRef": "view_ref",
|
|
5530
5573
|
"dashStyleConfigBO": "dash_style_config",
|
|
5531
5574
|
},
|
|
5532
|
-
"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"],
|
|
5533
5576
|
"section_aliases": {
|
|
5534
5577
|
"sourceType": "source_type",
|
|
5535
5578
|
"chartRef": "chart_ref",
|
|
@@ -5538,6 +5581,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5538
5581
|
},
|
|
5539
5582
|
"allowed_values": {
|
|
5540
5583
|
"section.source_type": ["chart", "view", "grid", "filter", "text", "link"],
|
|
5584
|
+
"section.role": ["metric"],
|
|
5541
5585
|
"workspace_icon.icon_names": list(WORKSPACE_ICON_NAMES),
|
|
5542
5586
|
"workspace_icon.icon_colors": list(WORKSPACE_ICON_COLORS),
|
|
5543
5587
|
"workspace_icon.generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
|
|
@@ -5552,7 +5596,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5552
5596
|
"call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
|
|
5553
5597
|
"portal_apply uses replace semantics for sections",
|
|
5554
5598
|
"when editing an existing portal, sections may be omitted to update only base info such as visibility, icon, or package",
|
|
5555
|
-
"use patch_sections[] for
|
|
5599
|
+
"use patch_sections[] for section-level patch updates without replacing all sections; each item needs one selector (chart_ref with chart_id/chart_key/chart_name, view_ref with view_key/view_name, or order as 0-based index) plus set/unset",
|
|
5556
5600
|
"when sections[] is supplied without patch_sections[], it uses replace semantics for all sections",
|
|
5557
5601
|
"remove a section by omitting it from the sections list (replace mode) or by unset in patch_sections (patch mode)",
|
|
5558
5602
|
"package_id is required when creating a new portal",
|
|
@@ -5560,6 +5604,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5560
5604
|
"chart_ref resolves by chart_id/chart_key first, then exact unique chart_name",
|
|
5561
5605
|
"view_ref resolves by view_key first, then exact unique view_name",
|
|
5562
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",
|
|
5563
5608
|
"if unsure about layout, omit position or use layout_preset=auto/dashboard_2col/dashboard_3col",
|
|
5564
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",
|
|
5565
5610
|
"x=0/6 with cols=6 only occupies the left half of the pc portal and triggers PORTAL_LAYOUT_HALF_WIDTH",
|