@qingflow-tech/qingflow-app-builder-mcp 1.0.39 → 1.0.41
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-app-builder/SKILL.md +18 -7
- package/skills/qingflow-app-builder/references/complete-system-development-guide.md +59 -0
- package/skills/qingflow-app-builder/references/create-app.md +13 -7
- package/skills/qingflow-app-builder/references/gotchas.md +6 -0
- package/skills/qingflow-app-builder/references/single-app-development-guide.md +47 -0
- package/skills/qingflow-app-builder/references/solution-playbooks.md +10 -0
- package/skills/qingflow-app-builder/references/tool-selection.md +2 -2
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +2 -0
- package/src/qingflow_mcp/builder_facade/models.py +183 -0
- package/src/qingflow_mcp/builder_facade/service.py +722 -74
- package/src/qingflow_mcp/cli/commands/builder.py +62 -2
- package/src/qingflow_mcp/cli/commands/common.py +12 -3
- package/src/qingflow_mcp/cli/formatters.py +1 -0
- package/src/qingflow_mcp/cli/main.py +2 -0
- package/src/qingflow_mcp/response_trim.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +515 -22
- package/src/qingflow_mcp/tools/record_tools.py +28 -2
|
@@ -22,6 +22,7 @@ from ..builder_facade.models import (
|
|
|
22
22
|
ChartApplyRequest,
|
|
23
23
|
CustomButtonsApplyRequest,
|
|
24
24
|
CustomButtonPatch,
|
|
25
|
+
FIELD_TYPE_ALIASES,
|
|
25
26
|
FIELD_TYPE_ID_ALIASES,
|
|
26
27
|
FieldPatch,
|
|
27
28
|
FieldRemovePatch,
|
|
@@ -383,7 +384,7 @@ class AiBuilderTools(ToolBase):
|
|
|
383
384
|
remove_fields: list[JSONObject] | None = None,
|
|
384
385
|
apps: list[JSONObject] | None = None,
|
|
385
386
|
) -> JSONObject:
|
|
386
|
-
if apps:
|
|
387
|
+
if apps is not None:
|
|
387
388
|
if app_key or app_name or app_title or add_fields or update_fields or remove_fields:
|
|
388
389
|
return _config_failure(
|
|
389
390
|
tool_name="app_schema_apply",
|
|
@@ -1718,8 +1719,18 @@ class AiBuilderTools(ToolBase):
|
|
|
1718
1719
|
icon_color=icon_color,
|
|
1719
1720
|
icon_config=icon_config,
|
|
1720
1721
|
)
|
|
1721
|
-
if apps:
|
|
1722
|
-
|
|
1722
|
+
if apps is not None:
|
|
1723
|
+
normalized_apps_payload = _normalize_schema_apps_argument(
|
|
1724
|
+
tool_name="app_schema_apply",
|
|
1725
|
+
package_id=package_id,
|
|
1726
|
+
apps=apps,
|
|
1727
|
+
)
|
|
1728
|
+
failure = normalized_apps_payload.get("failure")
|
|
1729
|
+
if isinstance(failure, dict):
|
|
1730
|
+
return _attach_builder_apply_envelope("app_schema_apply", failure)
|
|
1731
|
+
package_id = normalized_apps_payload.get("package_id") # type: ignore[assignment]
|
|
1732
|
+
apps = normalized_apps_payload.get("apps") # type: ignore[assignment]
|
|
1733
|
+
input_warnings = list(normalized_apps_payload.get("warnings") or [])
|
|
1723
1734
|
result = self._app_schema_apply_multi(
|
|
1724
1735
|
profile=profile,
|
|
1725
1736
|
package_id=package_id,
|
|
@@ -1728,6 +1739,10 @@ class AiBuilderTools(ToolBase):
|
|
|
1728
1739
|
publish=publish,
|
|
1729
1740
|
apps=apps,
|
|
1730
1741
|
)
|
|
1742
|
+
if input_warnings:
|
|
1743
|
+
result_warnings = list(result.get("warnings") or [])
|
|
1744
|
+
result_warnings.extend(deepcopy(input_warnings))
|
|
1745
|
+
result["warnings"] = result_warnings
|
|
1731
1746
|
return _attach_builder_apply_envelope("app_schema_apply", result)
|
|
1732
1747
|
result = self._app_schema_apply_once(
|
|
1733
1748
|
profile=profile,
|
|
@@ -1803,9 +1818,20 @@ class AiBuilderTools(ToolBase):
|
|
|
1803
1818
|
if not apps:
|
|
1804
1819
|
return _config_failure(
|
|
1805
1820
|
tool_name="app_schema_apply",
|
|
1821
|
+
error_code="APPS_FILE_EMPTY",
|
|
1806
1822
|
message="app_schema_apply multi-app mode requires non-empty apps.",
|
|
1807
1823
|
fix_hint="Pass apps as a non-empty list of app schema items.",
|
|
1808
1824
|
)
|
|
1825
|
+
shape_failure = _schema_apps_shape_failure(tool_name="app_schema_apply", apps=apps)
|
|
1826
|
+
if shape_failure is not None:
|
|
1827
|
+
return shape_failure
|
|
1828
|
+
static_validation_failure = _multi_app_static_validation_failure(
|
|
1829
|
+
tool_name="app_schema_apply",
|
|
1830
|
+
apps=apps,
|
|
1831
|
+
create_if_missing=create_if_missing,
|
|
1832
|
+
)
|
|
1833
|
+
if static_validation_failure is not None:
|
|
1834
|
+
return static_validation_failure
|
|
1809
1835
|
icon_errors: list[JSONObject] = []
|
|
1810
1836
|
seen_new_app_icons: dict[str, int] = {}
|
|
1811
1837
|
for index, raw_item in enumerate(apps):
|
|
@@ -1874,6 +1900,9 @@ class AiBuilderTools(ToolBase):
|
|
|
1874
1900
|
created_app_keys: list[str] = []
|
|
1875
1901
|
results: list[JSONObject] = []
|
|
1876
1902
|
any_write_executed = False
|
|
1903
|
+
any_write_may_have_succeeded = False
|
|
1904
|
+
pending_readback_app_keys: list[str] = []
|
|
1905
|
+
pending_readback_app_names: list[str] = []
|
|
1877
1906
|
client_keys: set[str] = set()
|
|
1878
1907
|
|
|
1879
1908
|
for index, raw_item in enumerate(apps):
|
|
@@ -1913,25 +1942,47 @@ class AiBuilderTools(ToolBase):
|
|
|
1913
1942
|
public_shell = _publicize_package_fields(shell)
|
|
1914
1943
|
resolved_key = str(public_shell.get("app_key") or "").strip()
|
|
1915
1944
|
shell_write_executed = _schema_apply_result_has_write(public_shell)
|
|
1945
|
+
shell_write_may_have_succeeded = _schema_apply_result_may_have_write(public_shell)
|
|
1916
1946
|
if shell_write_executed:
|
|
1917
1947
|
any_write_executed = True
|
|
1948
|
+
if shell_write_may_have_succeeded:
|
|
1949
|
+
any_write_may_have_succeeded = True
|
|
1918
1950
|
if public_shell.get("status") not in {"success", "partial_success"} or not resolved_key:
|
|
1951
|
+
pending_readback = bool(shell_write_may_have_succeeded or shell_write_executed) and (
|
|
1952
|
+
str(public_shell.get("next_action") or "") == "readback_before_retry"
|
|
1953
|
+
or bool((public_shell.get("verification") if isinstance(public_shell.get("verification"), dict) else {}).get("readback_before_retry"))
|
|
1954
|
+
or not resolved_key
|
|
1955
|
+
)
|
|
1956
|
+
item_status = "pending_readback" if pending_readback else "failed"
|
|
1957
|
+
if pending_readback:
|
|
1958
|
+
if resolved_key:
|
|
1959
|
+
pending_readback_app_keys.append(resolved_key)
|
|
1960
|
+
if app_name:
|
|
1961
|
+
pending_readback_app_names.append(app_name)
|
|
1919
1962
|
results.append({
|
|
1920
1963
|
"index": index,
|
|
1921
1964
|
"row_number": index + 1,
|
|
1922
1965
|
"client_key": client_key or None,
|
|
1923
1966
|
"app_name": app_name or None,
|
|
1924
1967
|
"app_key": resolved_key or app_key or None,
|
|
1925
|
-
"status":
|
|
1968
|
+
"status": item_status,
|
|
1926
1969
|
"stage": "resolve_or_create_shell",
|
|
1927
1970
|
"error_code": public_shell.get("error_code") or "APP_SHELL_APPLY_FAILED",
|
|
1928
1971
|
"message": public_shell.get("message") or "app shell resolve/create failed",
|
|
1929
1972
|
"write_executed": shell_write_executed,
|
|
1930
|
-
"
|
|
1973
|
+
"write_may_have_succeeded": bool(shell_write_may_have_succeeded or shell_write_executed),
|
|
1974
|
+
"safe_to_retry": False if pending_readback else (not shell_write_executed and not any_write_executed),
|
|
1975
|
+
**({"next_action": "readback_before_retry"} if pending_readback else {}),
|
|
1976
|
+
**({"verification": public_shell.get("verification")} if isinstance(public_shell.get("verification"), dict) else {}),
|
|
1977
|
+
**({"created": True} if pending_readback and create_if_missing and not app_key else {}),
|
|
1931
1978
|
})
|
|
1932
1979
|
continue
|
|
1933
1980
|
if bool(public_shell.get("created")):
|
|
1934
1981
|
created_app_keys.append(resolved_key)
|
|
1982
|
+
if shell_write_may_have_succeeded and str(public_shell.get("next_action") or "") == "readback_before_retry":
|
|
1983
|
+
pending_readback_app_keys.append(resolved_key)
|
|
1984
|
+
if app_name:
|
|
1985
|
+
pending_readback_app_names.append(app_name)
|
|
1935
1986
|
if client_key:
|
|
1936
1987
|
client_key_to_app_key[client_key] = resolved_key
|
|
1937
1988
|
resolved_name = str(public_shell.get("app_name_after") or public_shell.get("app_name") or app_name or "").strip()
|
|
@@ -1998,7 +2049,11 @@ class AiBuilderTools(ToolBase):
|
|
|
1998
2049
|
"verified": bool(shell_result.get("verified")),
|
|
1999
2050
|
"error_code": shell_result.get("error_code"),
|
|
2000
2051
|
"message": shell_result.get("message"),
|
|
2052
|
+
"write_executed": _schema_apply_result_has_write(shell_result),
|
|
2053
|
+
"write_may_have_succeeded": _schema_apply_result_may_have_write(shell_result),
|
|
2001
2054
|
"safe_to_retry": False,
|
|
2055
|
+
**({"next_action": shell_result.get("next_action")} if shell_result.get("next_action") else {}),
|
|
2056
|
+
**({"verification": shell_result.get("verification")} if isinstance(shell_result.get("verification"), dict) else {}),
|
|
2002
2057
|
})
|
|
2003
2058
|
continue
|
|
2004
2059
|
|
|
@@ -2020,11 +2075,18 @@ class AiBuilderTools(ToolBase):
|
|
|
2020
2075
|
public_result = _publicize_package_fields(field_result)
|
|
2021
2076
|
if _schema_apply_result_has_write(public_result):
|
|
2022
2077
|
any_write_executed = True
|
|
2078
|
+
if _schema_apply_result_may_have_write(public_result):
|
|
2079
|
+
any_write_may_have_succeeded = True
|
|
2023
2080
|
item_status = public_result.get("status") if public_result.get("status") in {"success", "partial_success"} else "failed"
|
|
2024
2081
|
shell_field_diff = existing.get("shell_field_diff") if isinstance(existing.get("shell_field_diff"), dict) else {}
|
|
2025
2082
|
shell_field_diff_details = existing.get("shell_field_diff_details") if isinstance(existing.get("shell_field_diff_details"), dict) else {}
|
|
2026
2083
|
field_diff = _merge_schema_field_diffs(shell_field_diff, public_result.get("field_diff") or {})
|
|
2027
2084
|
field_diff_details = _merge_schema_field_diffs(shell_field_diff_details, public_result.get("field_diff_details") or {})
|
|
2085
|
+
if _schema_apply_result_may_have_write(public_result) and str(public_result.get("next_action") or "") == "readback_before_retry":
|
|
2086
|
+
if app_key:
|
|
2087
|
+
pending_readback_app_keys.append(app_key)
|
|
2088
|
+
if existing.get("app_name"):
|
|
2089
|
+
pending_readback_app_names.append(str(existing.get("app_name")))
|
|
2028
2090
|
final_items.append({
|
|
2029
2091
|
**{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
|
|
2030
2092
|
"status": item_status,
|
|
@@ -2037,21 +2099,33 @@ class AiBuilderTools(ToolBase):
|
|
|
2037
2099
|
"verified": bool(public_result.get("verified")),
|
|
2038
2100
|
"error_code": public_result.get("error_code"),
|
|
2039
2101
|
"message": public_result.get("message"),
|
|
2102
|
+
"write_executed": _schema_apply_result_has_write(public_result),
|
|
2103
|
+
"write_may_have_succeeded": _schema_apply_result_may_have_write(public_result),
|
|
2040
2104
|
"safe_to_retry": False,
|
|
2105
|
+
**({"next_action": public_result.get("next_action")} if public_result.get("next_action") else {}),
|
|
2106
|
+
**({"verification": public_result.get("verification")} if isinstance(public_result.get("verification"), dict) else {}),
|
|
2041
2107
|
})
|
|
2042
2108
|
|
|
2109
|
+
pending_readback = sum(1 for item in final_items if item.get("status") == "pending_readback")
|
|
2043
2110
|
succeeded = sum(1 for item in final_items if item.get("status") in {"success", "partial_success"})
|
|
2044
|
-
failed =
|
|
2045
|
-
|
|
2046
|
-
|
|
2111
|
+
failed = sum(1 for item in final_items if item.get("status") == "failed")
|
|
2112
|
+
partial = sum(1 for item in final_items if item.get("status") == "partial_success")
|
|
2113
|
+
overall_status = (
|
|
2114
|
+
"success"
|
|
2115
|
+
if failed == 0 and pending_readback == 0 and partial == 0
|
|
2116
|
+
else ("partial_success" if succeeded > 0 or pending_readback > 0 or any_write_executed or any_write_may_have_succeeded else "failed")
|
|
2117
|
+
)
|
|
2118
|
+
uncertain_write = any_write_may_have_succeeded or pending_readback > 0
|
|
2119
|
+
response: JSONObject = {
|
|
2047
2120
|
"status": overall_status,
|
|
2048
2121
|
"mode": "multi_app",
|
|
2049
2122
|
"total": len(apps),
|
|
2050
2123
|
"succeeded": succeeded,
|
|
2051
2124
|
"failed": failed,
|
|
2125
|
+
"pending_readback": pending_readback,
|
|
2052
2126
|
"created_app_keys": created_app_keys,
|
|
2053
2127
|
"write_executed": any_write_executed,
|
|
2054
|
-
"safe_to_retry": not any_write_executed,
|
|
2128
|
+
"safe_to_retry": not (any_write_executed or uncertain_write),
|
|
2055
2129
|
"package_id": package_id,
|
|
2056
2130
|
"publish_requested": publish,
|
|
2057
2131
|
"apps": final_items,
|
|
@@ -2059,12 +2133,23 @@ class AiBuilderTools(ToolBase):
|
|
|
2059
2133
|
"verification": {
|
|
2060
2134
|
"all_apps_succeeded": failed == 0,
|
|
2061
2135
|
"created_app_count": len(created_app_keys),
|
|
2136
|
+
"pending_readback_count": pending_readback,
|
|
2062
2137
|
},
|
|
2063
2138
|
"request_id": None,
|
|
2064
2139
|
"error_code": None if overall_status != "failed" else "MULTI_APP_SCHEMA_APPLY_FAILED",
|
|
2065
2140
|
"recoverable": overall_status != "success",
|
|
2066
|
-
"message":
|
|
2141
|
+
"message": (
|
|
2142
|
+
"multi-app schema apply needs readback before retry"
|
|
2143
|
+
if pending_readback > 0
|
|
2144
|
+
else ("multi-app schema apply completed" if overall_status != "failed" else "multi-app schema apply failed")
|
|
2145
|
+
),
|
|
2067
2146
|
}
|
|
2147
|
+
if uncertain_write:
|
|
2148
|
+
response["write_may_have_succeeded"] = True
|
|
2149
|
+
response["next_action"] = "readback_before_retry"
|
|
2150
|
+
response["pending_readback_app_keys"] = list(dict.fromkeys(pending_readback_app_keys))
|
|
2151
|
+
response["pending_readback_app_names"] = list(dict.fromkeys(pending_readback_app_names))
|
|
2152
|
+
return response
|
|
2068
2153
|
|
|
2069
2154
|
def _app_schema_apply_once(
|
|
2070
2155
|
self,
|
|
@@ -2594,12 +2679,12 @@ class AiBuilderTools(ToolBase):
|
|
|
2594
2679
|
suggested_next_call={
|
|
2595
2680
|
"tool_name": "app_charts_apply",
|
|
2596
2681
|
"arguments": {
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2682
|
+
"profile": profile,
|
|
2683
|
+
"app_key": app_key,
|
|
2684
|
+
"upsert_charts": [{"name": "销售总量", "chart_type": "target", "metric": "count(*)"}],
|
|
2685
|
+
"remove_chart_ids": [],
|
|
2686
|
+
"reorder_chart_ids": [],
|
|
2687
|
+
},
|
|
2603
2688
|
},
|
|
2604
2689
|
))
|
|
2605
2690
|
normalized_args = request.model_dump(mode="json")
|
|
@@ -2932,6 +3017,365 @@ def _normalize_schema_app_item(item: JSONObject) -> JSONObject:
|
|
|
2932
3017
|
return normalized
|
|
2933
3018
|
|
|
2934
3019
|
|
|
3020
|
+
def _schema_apps_expected_shape() -> JSONObject:
|
|
3021
|
+
return {
|
|
3022
|
+
"package_id": 1001,
|
|
3023
|
+
"apps": [
|
|
3024
|
+
{
|
|
3025
|
+
"client_key": "employee",
|
|
3026
|
+
"app_name": "员工花名册",
|
|
3027
|
+
"icon": "business-personalcard",
|
|
3028
|
+
"color": "emerald",
|
|
3029
|
+
"add_fields": [{"name": "员工名称", "type": "text", "as_data_title": True}],
|
|
3030
|
+
}
|
|
3031
|
+
],
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
|
|
3035
|
+
def _schema_apps_expected_shape_json() -> str:
|
|
3036
|
+
return (
|
|
3037
|
+
'{"package_id":1001,"apps":[{"client_key":"employee","app_name":"员工花名册",'
|
|
3038
|
+
'"icon":"business-personalcard","color":"emerald","add_fields":[{"name":"员工名称","type":"text","as_data_title":true}]}]}'
|
|
3039
|
+
)
|
|
3040
|
+
|
|
3041
|
+
|
|
3042
|
+
def _is_schema_apps_wrapper_item(item: object) -> bool:
|
|
3043
|
+
return isinstance(item, dict) and "apps" in item
|
|
3044
|
+
|
|
3045
|
+
|
|
3046
|
+
def _schema_apps_shape_failure(*, tool_name: str, apps: list[JSONObject]) -> JSONObject | None:
|
|
3047
|
+
for index, item in enumerate(apps):
|
|
3048
|
+
if _is_schema_apps_wrapper_item(item):
|
|
3049
|
+
return _config_failure(
|
|
3050
|
+
tool_name=tool_name,
|
|
3051
|
+
error_code="APPS_FILE_SHAPE_INVALID",
|
|
3052
|
+
message="apps[] items must be app schema items, not package wrapper objects.",
|
|
3053
|
+
fix_hint=(
|
|
3054
|
+
"For MCP pass package_id separately and apps=[{app_name, icon, color, add_fields}]. "
|
|
3055
|
+
'For CLI use --apps-file with {"package_id":1001,"apps":[...]}. '
|
|
3056
|
+
"Do not pass multiple {package_id, apps} wrapper objects inside apps[]."
|
|
3057
|
+
),
|
|
3058
|
+
details={
|
|
3059
|
+
"index": index,
|
|
3060
|
+
"row_number": index + 1,
|
|
3061
|
+
"expected_shape": _schema_apps_expected_shape(),
|
|
3062
|
+
"expected_shape_json": _schema_apps_expected_shape_json(),
|
|
3063
|
+
},
|
|
3064
|
+
)
|
|
3065
|
+
return None
|
|
3066
|
+
|
|
3067
|
+
|
|
3068
|
+
def _normalize_schema_apps_argument(*, tool_name: str, package_id: int | None, apps: list[JSONObject]) -> JSONObject:
|
|
3069
|
+
normalized_package_id = package_id
|
|
3070
|
+
normalized_apps: list[JSONObject] = apps
|
|
3071
|
+
warnings: list[JSONObject] = []
|
|
3072
|
+
|
|
3073
|
+
if len(apps) == 1 and _is_schema_apps_wrapper_item(apps[0]):
|
|
3074
|
+
wrapper = apps[0]
|
|
3075
|
+
wrapper_apps = wrapper.get("apps")
|
|
3076
|
+
if not isinstance(wrapper_apps, list):
|
|
3077
|
+
return {
|
|
3078
|
+
"failure": _config_failure(
|
|
3079
|
+
tool_name=tool_name,
|
|
3080
|
+
error_code="APPS_FILE_SHAPE_INVALID",
|
|
3081
|
+
message="apps singleton wrapper requires an apps array.",
|
|
3082
|
+
fix_hint='Use {"package_id":1001,"apps":[...]} in CLI, or pass MCP package_id=1001 and apps=[...].',
|
|
3083
|
+
details={
|
|
3084
|
+
"expected_shape": _schema_apps_expected_shape(),
|
|
3085
|
+
"expected_shape_json": _schema_apps_expected_shape_json(),
|
|
3086
|
+
},
|
|
3087
|
+
)
|
|
3088
|
+
}
|
|
3089
|
+
if normalized_package_id is None and wrapper.get("package_id") is not None:
|
|
3090
|
+
try:
|
|
3091
|
+
normalized_package_id = int(wrapper.get("package_id"))
|
|
3092
|
+
except (TypeError, ValueError):
|
|
3093
|
+
return {
|
|
3094
|
+
"failure": _config_failure(
|
|
3095
|
+
tool_name=tool_name,
|
|
3096
|
+
error_code="APPS_FILE_SHAPE_INVALID",
|
|
3097
|
+
message="apps singleton wrapper package_id must be an integer.",
|
|
3098
|
+
fix_hint="Pass package_id as a number at the top level.",
|
|
3099
|
+
details={
|
|
3100
|
+
"expected_shape": _schema_apps_expected_shape(),
|
|
3101
|
+
"expected_shape_json": _schema_apps_expected_shape_json(),
|
|
3102
|
+
},
|
|
3103
|
+
)
|
|
3104
|
+
}
|
|
3105
|
+
normalized_apps = wrapper_apps
|
|
3106
|
+
warnings.append(
|
|
3107
|
+
{
|
|
3108
|
+
"code": "APPS_FILE_WRAPPER_ARRAY_UNWRAPPED",
|
|
3109
|
+
"message": "apps was a singleton wrapper array; normalized it to package_id + apps.",
|
|
3110
|
+
}
|
|
3111
|
+
)
|
|
3112
|
+
else:
|
|
3113
|
+
failure = _schema_apps_shape_failure(tool_name=tool_name, apps=apps)
|
|
3114
|
+
if failure is not None:
|
|
3115
|
+
return {"failure": failure}
|
|
3116
|
+
|
|
3117
|
+
normalized_items: list[JSONObject] = []
|
|
3118
|
+
for item in normalized_apps:
|
|
3119
|
+
normalized_items.append(_normalize_schema_app_item(item) if isinstance(item, dict) else item)
|
|
3120
|
+
return {"package_id": normalized_package_id, "apps": normalized_items, "warnings": warnings}
|
|
3121
|
+
|
|
3122
|
+
|
|
3123
|
+
def _multi_app_item_app_name(item: JSONObject) -> str:
|
|
3124
|
+
return str(item.get("app_name") or item.get("appName") or item.get("appTitle") or item.get("app_title") or item.get("title") or "").strip()
|
|
3125
|
+
|
|
3126
|
+
|
|
3127
|
+
def _multi_app_item_app_key(item: JSONObject) -> str:
|
|
3128
|
+
return str(item.get("app_key") or item.get("appKey") or "").strip()
|
|
3129
|
+
|
|
3130
|
+
|
|
3131
|
+
def _multi_app_item_client_key(item: JSONObject) -> str:
|
|
3132
|
+
return str(item.get("client_key") or item.get("clientKey") or "").strip()
|
|
3133
|
+
|
|
3134
|
+
|
|
3135
|
+
def _field_name_for_static_validation(field: JSONObject) -> str:
|
|
3136
|
+
return str(field.get("name") or field.get("title") or field.get("label") or "").strip()
|
|
3137
|
+
|
|
3138
|
+
|
|
3139
|
+
def _field_type_for_static_validation(field: JSONObject) -> str:
|
|
3140
|
+
return str(field.get("type") or field.get("type_id") or field.get("typeId") or "").strip()
|
|
3141
|
+
|
|
3142
|
+
|
|
3143
|
+
def _is_data_title_field(field: JSONObject) -> bool:
|
|
3144
|
+
return bool(field.get("as_data_title") or field.get("asDataTitle"))
|
|
3145
|
+
|
|
3146
|
+
|
|
3147
|
+
def _collect_multi_app_target_refs(value: object, *, path: str) -> list[JSONObject]:
|
|
3148
|
+
refs: list[JSONObject] = []
|
|
3149
|
+
if isinstance(value, list):
|
|
3150
|
+
for index, entry in enumerate(value):
|
|
3151
|
+
refs.extend(_collect_multi_app_target_refs(entry, path=f"{path}[{index}]"))
|
|
3152
|
+
return refs
|
|
3153
|
+
if not isinstance(value, dict):
|
|
3154
|
+
return refs
|
|
3155
|
+
for key, entry in value.items():
|
|
3156
|
+
current_path = f"{path}.{key}"
|
|
3157
|
+
if key in {"target_app_ref", "targetAppRef", "target_app_client_key", "targetAppClientKey"}:
|
|
3158
|
+
refs.append({"kind": "target_app_ref", "value": str(entry or "").strip(), "path": current_path})
|
|
3159
|
+
continue
|
|
3160
|
+
if key in {"target_app", "targetApp"}:
|
|
3161
|
+
refs.append({"kind": "target_app", "value": str(entry or "").strip(), "path": current_path})
|
|
3162
|
+
continue
|
|
3163
|
+
refs.extend(_collect_multi_app_target_refs(entry, path=current_path))
|
|
3164
|
+
return refs
|
|
3165
|
+
|
|
3166
|
+
|
|
3167
|
+
def _multi_app_static_validation_failure(
|
|
3168
|
+
*,
|
|
3169
|
+
tool_name: str,
|
|
3170
|
+
apps: list[JSONObject],
|
|
3171
|
+
create_if_missing: bool,
|
|
3172
|
+
) -> JSONObject | None:
|
|
3173
|
+
issues: list[JSONObject] = []
|
|
3174
|
+
client_key_indexes: dict[str, int] = {}
|
|
3175
|
+
app_name_indexes: dict[str, list[int]] = {}
|
|
3176
|
+
|
|
3177
|
+
for index, item in enumerate(apps):
|
|
3178
|
+
if not isinstance(item, dict):
|
|
3179
|
+
continue
|
|
3180
|
+
app_name = _multi_app_item_app_name(item)
|
|
3181
|
+
client_key = _multi_app_item_client_key(item)
|
|
3182
|
+
if client_key:
|
|
3183
|
+
first_index = client_key_indexes.get(client_key)
|
|
3184
|
+
if first_index is not None:
|
|
3185
|
+
issues.append(
|
|
3186
|
+
{
|
|
3187
|
+
"index": index,
|
|
3188
|
+
"row_number": index + 1,
|
|
3189
|
+
"path": f"apps[{index}].client_key",
|
|
3190
|
+
"error_code": "DUPLICATE_CLIENT_KEY",
|
|
3191
|
+
"message": f"duplicate client_key '{client_key}' also appears at apps[{first_index}]",
|
|
3192
|
+
"fix_hint": "Use a unique stable client_key for each app item, then reference relations through target_app_ref.",
|
|
3193
|
+
"details": {"client_key": client_key, "first_index": first_index, "duplicate_index": index},
|
|
3194
|
+
}
|
|
3195
|
+
)
|
|
3196
|
+
else:
|
|
3197
|
+
client_key_indexes[client_key] = index
|
|
3198
|
+
if app_name:
|
|
3199
|
+
app_name_indexes.setdefault(app_name, []).append(index)
|
|
3200
|
+
|
|
3201
|
+
for app_name, indexes in sorted(app_name_indexes.items()):
|
|
3202
|
+
if len(indexes) <= 1:
|
|
3203
|
+
continue
|
|
3204
|
+
issues.append(
|
|
3205
|
+
{
|
|
3206
|
+
"index": indexes[1],
|
|
3207
|
+
"row_number": indexes[1] + 1,
|
|
3208
|
+
"path": f"apps[{indexes[1]}].app_name",
|
|
3209
|
+
"error_code": "DUPLICATE_APP_NAME",
|
|
3210
|
+
"message": f"duplicate app_name '{app_name}' appears at apps{indexes}",
|
|
3211
|
+
"fix_hint": "Use unique app_name values in one multi-app payload, or use app_key for existing apps.",
|
|
3212
|
+
"details": {"app_name": app_name, "indexes": indexes},
|
|
3213
|
+
}
|
|
3214
|
+
)
|
|
3215
|
+
|
|
3216
|
+
for index, item in enumerate(apps):
|
|
3217
|
+
if not isinstance(item, dict):
|
|
3218
|
+
continue
|
|
3219
|
+
app_key = _multi_app_item_app_key(item)
|
|
3220
|
+
app_name = _multi_app_item_app_name(item)
|
|
3221
|
+
new_app_item = not bool(app_key)
|
|
3222
|
+
if not app_key and not app_name:
|
|
3223
|
+
issues.append(
|
|
3224
|
+
{
|
|
3225
|
+
"index": index,
|
|
3226
|
+
"row_number": index + 1,
|
|
3227
|
+
"path": f"apps[{index}]",
|
|
3228
|
+
"error_code": "APP_SELECTOR_REQUIRED",
|
|
3229
|
+
"message": "apps[] item requires app_key or app_name",
|
|
3230
|
+
"fix_hint": "For new apps pass app_name; for existing apps pass app_key.",
|
|
3231
|
+
}
|
|
3232
|
+
)
|
|
3233
|
+
continue
|
|
3234
|
+
if new_app_item and not create_if_missing:
|
|
3235
|
+
issues.append(
|
|
3236
|
+
{
|
|
3237
|
+
"index": index,
|
|
3238
|
+
"row_number": index + 1,
|
|
3239
|
+
"path": f"apps[{index}]",
|
|
3240
|
+
"error_code": "CREATE_IF_MISSING_REQUIRED",
|
|
3241
|
+
"message": "new multi-app items require create_if_missing=true",
|
|
3242
|
+
"fix_hint": "Set create_if_missing=true, or pass app_key for an existing app.",
|
|
3243
|
+
}
|
|
3244
|
+
)
|
|
3245
|
+
|
|
3246
|
+
add_fields = _multi_app_list_value(item, "add_fields", "addFields")
|
|
3247
|
+
if new_app_item:
|
|
3248
|
+
title_fields: list[tuple[int, JSONObject]] = [
|
|
3249
|
+
(field_index, field)
|
|
3250
|
+
for field_index, field in enumerate(add_fields)
|
|
3251
|
+
if isinstance(field, dict) and _is_data_title_field(field)
|
|
3252
|
+
]
|
|
3253
|
+
if not title_fields:
|
|
3254
|
+
issues.append(
|
|
3255
|
+
{
|
|
3256
|
+
"index": index,
|
|
3257
|
+
"row_number": index + 1,
|
|
3258
|
+
"path": f"apps[{index}].add_fields",
|
|
3259
|
+
"error_code": "MISSING_DATA_TITLE_FIELD",
|
|
3260
|
+
"message": "new apps must define exactly one top-level data title field",
|
|
3261
|
+
"fix_hint": "Mark one readable top-level field with as_data_title=true, for example {'name':'客户名称','type':'text','as_data_title':true}.",
|
|
3262
|
+
"details": {"suggested_field_names": [_field_name_for_static_validation(field) for field in add_fields[:8] if isinstance(field, dict)]},
|
|
3263
|
+
}
|
|
3264
|
+
)
|
|
3265
|
+
elif len(title_fields) > 1:
|
|
3266
|
+
issues.append(
|
|
3267
|
+
{
|
|
3268
|
+
"index": index,
|
|
3269
|
+
"row_number": index + 1,
|
|
3270
|
+
"path": f"apps[{index}].add_fields",
|
|
3271
|
+
"error_code": "MULTIPLE_DATA_TITLE_FIELDS",
|
|
3272
|
+
"message": "new apps can mark only one top-level field as data title",
|
|
3273
|
+
"fix_hint": "Keep as_data_title=true on exactly one top-level field.",
|
|
3274
|
+
"details": {
|
|
3275
|
+
"fields": [
|
|
3276
|
+
{
|
|
3277
|
+
"field_index": field_index,
|
|
3278
|
+
"name": _field_name_for_static_validation(field),
|
|
3279
|
+
"type": _field_type_for_static_validation(field),
|
|
3280
|
+
}
|
|
3281
|
+
for field_index, field in title_fields
|
|
3282
|
+
]
|
|
3283
|
+
},
|
|
3284
|
+
}
|
|
3285
|
+
)
|
|
3286
|
+
else:
|
|
3287
|
+
field_index, title_field = title_fields[0]
|
|
3288
|
+
if _field_type_for_static_validation(title_field) in {"subtable", "table"}:
|
|
3289
|
+
issues.append(
|
|
3290
|
+
{
|
|
3291
|
+
"index": index,
|
|
3292
|
+
"row_number": index + 1,
|
|
3293
|
+
"path": f"apps[{index}].add_fields[{field_index}]",
|
|
3294
|
+
"error_code": "INVALID_DATA_TITLE_FIELD",
|
|
3295
|
+
"message": "data title must be a top-level non-subtable field",
|
|
3296
|
+
"fix_hint": "Move as_data_title=true to a normal top-level text/number/date-like field.",
|
|
3297
|
+
"details": {
|
|
3298
|
+
"field_name": _field_name_for_static_validation(title_field),
|
|
3299
|
+
"field_type": _field_type_for_static_validation(title_field),
|
|
3300
|
+
},
|
|
3301
|
+
}
|
|
3302
|
+
)
|
|
3303
|
+
if _contains_multi_app_target_ref(title_field):
|
|
3304
|
+
issues.append(
|
|
3305
|
+
{
|
|
3306
|
+
"index": index,
|
|
3307
|
+
"row_number": index + 1,
|
|
3308
|
+
"path": f"apps[{index}].add_fields[{field_index}]",
|
|
3309
|
+
"error_code": "DATA_TITLE_FIELD_DEFERRED_BY_TARGET_REF",
|
|
3310
|
+
"message": "data title cannot be a relation field that depends on target_app_ref/target_app in the same multi-app call",
|
|
3311
|
+
"fix_hint": "Use a normal title field such as 客户名称/项目名称 as data title; keep relation fields as non-title fields.",
|
|
3312
|
+
"details": {"field_name": _field_name_for_static_validation(title_field)},
|
|
3313
|
+
}
|
|
3314
|
+
)
|
|
3315
|
+
|
|
3316
|
+
for ref in _collect_multi_app_target_refs(item, path=f"apps[{index}]"):
|
|
3317
|
+
ref_value = str(ref.get("value") or "").strip()
|
|
3318
|
+
if not ref_value:
|
|
3319
|
+
issues.append(
|
|
3320
|
+
{
|
|
3321
|
+
"index": index,
|
|
3322
|
+
"row_number": index + 1,
|
|
3323
|
+
"path": ref.get("path"),
|
|
3324
|
+
"error_code": "TARGET_APP_REFERENCE_EMPTY",
|
|
3325
|
+
"message": "target app reference cannot be empty",
|
|
3326
|
+
"fix_hint": "Use target_app_ref with another apps[].client_key, or target_app with a unique apps[].app_name.",
|
|
3327
|
+
}
|
|
3328
|
+
)
|
|
3329
|
+
elif ref.get("kind") == "target_app_ref" and ref_value not in client_key_indexes:
|
|
3330
|
+
issues.append(
|
|
3331
|
+
{
|
|
3332
|
+
"index": index,
|
|
3333
|
+
"row_number": index + 1,
|
|
3334
|
+
"path": ref.get("path"),
|
|
3335
|
+
"error_code": "TARGET_APP_REF_UNRESOLVED",
|
|
3336
|
+
"message": f"target_app_ref '{ref_value}' does not match any apps[].client_key in this payload",
|
|
3337
|
+
"fix_hint": "Set target_app_ref to one of apps[].client_key, or use target_app_key for an already-known existing app.",
|
|
3338
|
+
"details": {"target_app_ref": ref_value, "available_client_keys": sorted(client_key_indexes.keys())},
|
|
3339
|
+
}
|
|
3340
|
+
)
|
|
3341
|
+
elif ref.get("kind") == "target_app":
|
|
3342
|
+
matching_indexes = app_name_indexes.get(ref_value, [])
|
|
3343
|
+
if not matching_indexes:
|
|
3344
|
+
issues.append(
|
|
3345
|
+
{
|
|
3346
|
+
"index": index,
|
|
3347
|
+
"row_number": index + 1,
|
|
3348
|
+
"path": ref.get("path"),
|
|
3349
|
+
"error_code": "TARGET_APP_NAME_UNRESOLVED",
|
|
3350
|
+
"message": f"target_app '{ref_value}' does not match any apps[].app_name in this payload",
|
|
3351
|
+
"fix_hint": "Prefer target_app_ref with a stable apps[].client_key, or make target_app match an app_name exactly.",
|
|
3352
|
+
"details": {"target_app": ref_value, "available_app_names": sorted(app_name_indexes.keys())},
|
|
3353
|
+
}
|
|
3354
|
+
)
|
|
3355
|
+
elif len(matching_indexes) > 1:
|
|
3356
|
+
issues.append(
|
|
3357
|
+
{
|
|
3358
|
+
"index": index,
|
|
3359
|
+
"row_number": index + 1,
|
|
3360
|
+
"path": ref.get("path"),
|
|
3361
|
+
"error_code": "TARGET_APP_NAME_AMBIGUOUS",
|
|
3362
|
+
"message": f"target_app '{ref_value}' matches multiple apps[].app_name values",
|
|
3363
|
+
"fix_hint": "Use target_app_ref with a unique apps[].client_key instead of target_app.",
|
|
3364
|
+
"details": {"target_app": ref_value, "matching_indexes": matching_indexes},
|
|
3365
|
+
}
|
|
3366
|
+
)
|
|
3367
|
+
|
|
3368
|
+
if not issues:
|
|
3369
|
+
return None
|
|
3370
|
+
return _config_failure(
|
|
3371
|
+
tool_name=tool_name,
|
|
3372
|
+
error_code="MULTI_APP_STATIC_VALIDATION_FAILED",
|
|
3373
|
+
message="multi-app schema payload has static errors; fix apps[] before writing.",
|
|
3374
|
+
fix_hint="Before creating app shells, ensure each new app has exactly one data title, unique client_key/app_name, and relation refs match apps[].client_key or app_name.",
|
|
3375
|
+
details={"issues": issues, "expected_shape": _schema_apps_expected_shape()},
|
|
3376
|
+
)
|
|
3377
|
+
|
|
3378
|
+
|
|
2935
3379
|
def _compile_multi_app_schema_item_refs(
|
|
2936
3380
|
item: JSONObject,
|
|
2937
3381
|
client_key_to_app_key: dict[str, str],
|
|
@@ -3046,6 +3490,15 @@ def _schema_apply_result_has_write(result: JSONObject) -> bool:
|
|
|
3046
3490
|
return False
|
|
3047
3491
|
|
|
3048
3492
|
|
|
3493
|
+
def _schema_apply_result_may_have_write(result: JSONObject) -> bool:
|
|
3494
|
+
verification = result.get("verification") if isinstance(result.get("verification"), dict) else {}
|
|
3495
|
+
return bool(
|
|
3496
|
+
result.get("write_may_have_succeeded")
|
|
3497
|
+
or str(result.get("next_action") or "") == "readback_before_retry"
|
|
3498
|
+
or verification.get("readback_before_retry")
|
|
3499
|
+
)
|
|
3500
|
+
|
|
3501
|
+
|
|
3049
3502
|
def _validation_failure(
|
|
3050
3503
|
detail: str,
|
|
3051
3504
|
*,
|
|
@@ -3520,8 +3973,14 @@ def _builder_apply_summary(payload: JSONObject, resources: list[JSONObject]) ->
|
|
|
3520
3973
|
}
|
|
3521
3974
|
if "write_executed" in payload:
|
|
3522
3975
|
summary["write_executed"] = bool(payload.get("write_executed"))
|
|
3976
|
+
if "write_may_have_succeeded" in payload:
|
|
3977
|
+
summary["write_may_have_succeeded"] = bool(payload.get("write_may_have_succeeded"))
|
|
3523
3978
|
if "safe_to_retry" in payload:
|
|
3524
3979
|
summary["safe_to_retry"] = bool(payload.get("safe_to_retry"))
|
|
3980
|
+
if payload.get("next_action"):
|
|
3981
|
+
summary["next_action"] = payload.get("next_action")
|
|
3982
|
+
if payload.get("pending_readback") is not None:
|
|
3983
|
+
summary["pending_readback"] = payload.get("pending_readback")
|
|
3525
3984
|
return summary
|
|
3526
3985
|
|
|
3527
3986
|
|
|
@@ -3808,7 +4267,7 @@ def _builder_schema_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3808
4267
|
parent=package_parent,
|
|
3809
4268
|
icon_config=icon_config,
|
|
3810
4269
|
error_code=item.get("error_code"),
|
|
3811
|
-
message=item.get("message") if status
|
|
4270
|
+
message=item.get("message") if status in {"failed", "pending_readback"} else None,
|
|
3812
4271
|
)
|
|
3813
4272
|
)
|
|
3814
4273
|
resources.extend(_builder_field_resources(item.get("field_diff_details") or item.get("field_diff"), parent=parent))
|
|
@@ -4438,7 +4897,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4438
4897
|
"execution_notes": [
|
|
4439
4898
|
"create or update package metadata, visibility, grouping, and ordering in one call",
|
|
4440
4899
|
"creating a package requires explicit icon + color; icon=template is blocked because it is too generic",
|
|
4441
|
-
"icon
|
|
4900
|
+
"agent-facing primary icon shape is icon + color; icon_name/icon_color, icon_config, and icon={name,color} are compatibility aliases that normalize to icon/color",
|
|
4442
4901
|
"updating a package preserves existing icon/color when omitted; explicit icon/color values are still validated",
|
|
4443
4902
|
"call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
|
|
4444
4903
|
"metadata keys omitted on update are preserved",
|
|
@@ -5011,6 +5470,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5011
5470
|
},
|
|
5012
5471
|
"allowed_values": {
|
|
5013
5472
|
"field.type": [member.value for member in PublicFieldType],
|
|
5473
|
+
"field.type_aliases": {alias: field_type.value for alias, field_type in sorted(FIELD_TYPE_ALIASES.items())},
|
|
5014
5474
|
"field.relation_mode": [member.value for member in PublicRelationMode],
|
|
5015
5475
|
"field.department_scope.mode": ["all", "custom"],
|
|
5016
5476
|
"field.code_block_binding.outputs.target_field.type": list(INTEGRATION_OUTPUT_TARGET_FIELD_TYPES),
|
|
@@ -5024,13 +5484,16 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5024
5484
|
"create mode: package_id + app_name + create_if_missing=true",
|
|
5025
5485
|
"create mode follows backend CreateAppBean: package add_app permission is checked on the target package; package edit_app is not required for the create precheck",
|
|
5026
5486
|
"multi-app mode: pass package_id + create_if_missing + apps[]; do not mix apps with top-level app_key/app_name/add_fields/update_fields/remove_fields",
|
|
5487
|
+
"CLI --apps-file primary shape is {package_id, apps:[...]}; raw app arrays and singleton wrapper arrays are compatibility paths, not recommended examples",
|
|
5488
|
+
"multi-app mode preflights static errors before writing: duplicate client_key/app_name, missing data title on new apps, create_if_missing omissions, and unresolved target_app_ref/target_app",
|
|
5027
5489
|
"multi-app relation fields may use target_app_ref to point at another apps[].client_key; the tool creates/resolves app shells first and compiles it to target_app_key",
|
|
5028
5490
|
"multi-app relation fields may also use target_app with another apps[].app_name; prefer target_app_ref/client_key when names may collide",
|
|
5029
5491
|
"multi-app mode is not transactional; read created_app_keys and apps[].status before retrying, and retry only failed app items",
|
|
5030
5492
|
"create mode defaults new app visibility to workspace/not when visibility is omitted; edit mode preserves current visibility when omitted",
|
|
5031
5493
|
"create mode requires explicit icon + color; icon=template is blocked because it is too generic",
|
|
5032
|
-
"icon
|
|
5494
|
+
"agent-facing primary icon shape is icon + color; icon_name/icon_color, icon_config, and icon={name,color} are compatibility aliases that normalize to icon/color",
|
|
5033
5495
|
"multi-app create mode requires each new app item to include a distinct non-template icon and a valid color",
|
|
5496
|
+
"agent-friendly field type aliases are normalized before writing: multiline/multiline_text/textarea -> long_text; select/single_choice/dropdown -> single_select; multi_choice/multiple_choice/checkbox -> multi_select",
|
|
5034
5497
|
"single_select and multi_select options accept strings or objects such as {label,value}; builder normalizes them to option labels before writing",
|
|
5035
5498
|
"edit mode preserves existing icon/color when omitted; explicit icon/color values are still validated",
|
|
5036
5499
|
"call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
|
|
@@ -5042,6 +5505,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5042
5505
|
"relation_mode=multiple maps to referenceConfig.optionalDataNum=0",
|
|
5043
5506
|
"relation fields now require both display_field and visible_fields in MCP/CLI payloads",
|
|
5044
5507
|
"if relation target metadata lookup is blocked by 40161/40002/40027, explicit display_field.name and visible_fields[].name let builder degrade verification and still continue schema write",
|
|
5508
|
+
"relation write readback returns details.relation_readback_matrix; mismatches include details.relation_repair_plan with a minimal update_fields relation patch and data_impact",
|
|
5045
5509
|
"update_fields[].set.subfield_updates is the safe patch path for editing existing subtable child fields without rebuilding the entire subtable",
|
|
5046
5510
|
"subfield_updates only supports safe child overlays: name, required, description, and nested subfield_updates",
|
|
5047
5511
|
"set.subfields remains the full replace/rebuild path for a subtable and is higher risk when hidden relation/reference children exist",
|
|
@@ -5660,6 +6124,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5660
6124
|
"use this before app_schema_apply when you need exact field definitions",
|
|
5661
6125
|
"also returns chart_fields from QingBI datasource fields; app_charts_apply field selectors should use chart_fields because record/schema-visible fields and QingBI fields are not the same schema",
|
|
5662
6126
|
"chart_fields[].field_id supports field_<queId> selectors, while chart_fields[].bi_field_id is the raw QingBI fieldId accepted by report configs",
|
|
6127
|
+
"chart_fields[].chart_apply_examples contains copyable semantic app_charts_apply snippets such as count_by_field, filtered_count, and numeric sum_metric",
|
|
5663
6128
|
"subtable fields include nested subfields using the same compact field shape",
|
|
5664
6129
|
],
|
|
5665
6130
|
"minimal_example": {
|
|
@@ -5755,7 +6220,22 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5755
6220
|
},
|
|
5756
6221
|
},
|
|
5757
6222
|
"app_charts_apply": {
|
|
5758
|
-
"allowed_keys": [
|
|
6223
|
+
"allowed_keys": [
|
|
6224
|
+
"app_key",
|
|
6225
|
+
"upsert_charts",
|
|
6226
|
+
"patch_charts",
|
|
6227
|
+
"remove_chart_ids",
|
|
6228
|
+
"reorder_chart_ids",
|
|
6229
|
+
"upsert_charts[].metric",
|
|
6230
|
+
"upsert_charts[].metrics",
|
|
6231
|
+
"upsert_charts[].group_by",
|
|
6232
|
+
"upsert_charts[].where",
|
|
6233
|
+
"upsert_charts[].visibility",
|
|
6234
|
+
"patch_charts[].chart_id",
|
|
6235
|
+
"patch_charts[].name",
|
|
6236
|
+
"patch_charts[].set",
|
|
6237
|
+
"patch_charts[].unset",
|
|
6238
|
+
],
|
|
5759
6239
|
"aliases": {
|
|
5760
6240
|
"patchCharts": "patch_charts",
|
|
5761
6241
|
"chart.id": "chart.chart_id",
|
|
@@ -5763,6 +6243,11 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5763
6243
|
"chart.dimension_fields": "chart.dimension_field_ids",
|
|
5764
6244
|
"chart.indicator_fields": "chart.indicator_field_ids",
|
|
5765
6245
|
"chart.metric_field_ids": "chart.indicator_field_ids",
|
|
6246
|
+
"chart.dimensions": "chart.group_by",
|
|
6247
|
+
"chart.groupBy": "chart.group_by",
|
|
6248
|
+
"chart.where": "chart.filters",
|
|
6249
|
+
"chart.metric.operation": "chart.metric.op",
|
|
6250
|
+
"chart.metric.aggregation": "chart.metric.op",
|
|
5766
6251
|
"chart.filter.op": "chart.filter.operator",
|
|
5767
6252
|
},
|
|
5768
6253
|
"allowed_values": {
|
|
@@ -5783,6 +6268,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5783
6268
|
"upsert_charts[].visibility compiles to QingBI base visibleAuth only",
|
|
5784
6269
|
"visibility-only updates keep the existing chart config and do not rewrite rawDataConfigDTO.authInfo",
|
|
5785
6270
|
"chart dimension/metric/filter/query fields are resolved from app_get_fields.chart_fields (QingBI datasource fields), not record schema or form-only fields",
|
|
6271
|
+
"preferred chart metric DSL is SQL-like: metric='count(*)', metric='sum(金额)', metrics=['sum(金额)'], group_by=['状态'], where=[{field, op, value}]",
|
|
6272
|
+
"legacy dimension_field_ids/indicator_field_ids/config.aggregate remain supported as advanced compatibility input, but should not be the first choice for agents",
|
|
6273
|
+
"chart_get returns semantic group_by and metrics; raw QingBI config is diagnostic detail",
|
|
5786
6274
|
"system fields such as 申请人/申请时间/编号 are usable only when they appear in chart_fields; otherwise app_charts_apply returns CHART_FIELD_NOT_IN_QINGBI_SCHEMA",
|
|
5787
6275
|
"low-frequency chart types have local prevalidation: gauge requires 0 dimensions and 2 non-duplicated metrics; histogram requires at most 1 dimension and exactly 1 plain numeric metric",
|
|
5788
6276
|
"chart rule failures return chart_results[].diagnostics with rule_code, expected, actual, offending_fields, and next_action; backend 81002/81005 are translated when possible",
|
|
@@ -5793,7 +6281,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5793
6281
|
"minimal_example": {
|
|
5794
6282
|
"profile": "default",
|
|
5795
6283
|
"app_key": "APP_KEY",
|
|
5796
|
-
"upsert_charts": [{"name": "数据总量", "chart_type": "target", "
|
|
6284
|
+
"upsert_charts": [{"name": "数据总量", "chart_type": "target", "metric": "count(*)", "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
|
|
5797
6285
|
"patch_charts": [{"chart_id": "CHART_ID", "set": {"name": "数据总量-新版"}}],
|
|
5798
6286
|
"remove_chart_ids": [],
|
|
5799
6287
|
"reorder_chart_ids": [],
|
|
@@ -5840,9 +6328,11 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5840
6328
|
"viewRef": "view_ref",
|
|
5841
6329
|
"dashStyleConfigBO": "dash_style_config",
|
|
5842
6330
|
},
|
|
5843
|
-
"section_allowed_keys": ["title", "source_type", "position", "dash_style_config", "config", "chart_ref", "view_ref", "text", "url"],
|
|
6331
|
+
"section_allowed_keys": ["title", "source_type", "role", "position", "dash_style_config", "config", "chart_ref", "view_ref", "text", "url"],
|
|
5844
6332
|
"section_aliases": {
|
|
5845
6333
|
"sourceType": "source_type",
|
|
6334
|
+
"zone": "role",
|
|
6335
|
+
"sectionRole": "role",
|
|
5846
6336
|
"chartRef": "chart_ref",
|
|
5847
6337
|
"viewRef": "view_ref",
|
|
5848
6338
|
"dashStyleConfigBO": "dash_style_config",
|
|
@@ -5874,6 +6364,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5874
6364
|
"if unsure about layout, omit position or use layout_preset=auto/dashboard_2col/dashboard_3col",
|
|
5875
6365
|
"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",
|
|
5876
6366
|
"x=0/6 with cols=6 only occupies the left half of the pc portal and triggers PORTAL_LAYOUT_HALF_WIDTH",
|
|
6367
|
+
"grid sections must include config.items; an empty grid triggers PORTAL_GRID_ITEMS_EMPTY because the frontend only shows an empty entry container",
|
|
6368
|
+
"metric portal sections may set role=metric; role=metric requires a target/indicator chart and otherwise triggers PORTAL_METRIC_SECTION_CHART_TYPE_MISMATCH",
|
|
6369
|
+
"standard workbench count diagnostics warn when metric cards are not 4-6, BI charts are not 2-3, or business views are not 1-2",
|
|
5877
6370
|
"position.pc/mobile is the canonical portal layout shape",
|
|
5878
6371
|
"compat payload accepts name -> dash_name and single pages[0].components -> sections",
|
|
5879
6372
|
"visibility is the canonical public auth shape; auth is kept only as a deprecated compatibility alias",
|