@qingflow-tech/qingflow-app-user-mcp 1.0.40 → 1.0.42
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 -4
- package/docs/local-agent-install.md +4 -4
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-user/SKILL.md +5 -3
- package/skills/qingflow-mcp-setup/SKILL.md +2 -0
- package/skills/qingflow-record-analysis/SKILL.md +3 -1
- package/skills/qingflow-record-delete/SKILL.md +2 -0
- package/skills/qingflow-record-import/SKILL.md +29 -0
- package/skills/qingflow-record-insert/SKILL.md +24 -1
- package/skills/qingflow-record-update/SKILL.md +3 -0
- package/skills/qingflow-task-ops/SKILL.md +2 -0
- package/src/qingflow_mcp/builder_facade/models.py +183 -0
- package/src/qingflow_mcp/builder_facade/service.py +823 -75
- package/src/qingflow_mcp/cli/commands/builder.py +80 -6
- 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 +6 -4
- package/src/qingflow_mcp/tools/ai_builder_tools.py +388 -17
- package/src/qingflow_mcp/tools/record_tools.py +28 -2
- package/skills/qingflow-app-builder/SKILL.md +0 -280
- package/skills/qingflow-app-builder/agents/openai.yaml +0 -4
- package/skills/qingflow-app-builder/references/create-app.md +0 -160
- package/skills/qingflow-app-builder/references/environments.md +0 -63
- package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +0 -123
- package/skills/qingflow-app-builder/references/gotchas.md +0 -107
- package/skills/qingflow-app-builder/references/match-rules.md +0 -129
- package/skills/qingflow-app-builder/references/public-surface-sync.md +0 -75
- package/skills/qingflow-app-builder/references/solution-playbooks.md +0 -52
- package/skills/qingflow-app-builder/references/tool-selection.md +0 -106
- package/skills/qingflow-app-builder/references/update-flow.md +0 -158
- package/skills/qingflow-app-builder/references/update-layout.md +0 -68
- package/skills/qingflow-app-builder/references/update-schema.md +0 -75
- package/skills/qingflow-app-builder/references/update-views.md +0 -286
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +0 -137
- package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +0 -4
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +0 -66
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +0 -77
|
@@ -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,
|
|
@@ -1824,6 +1825,13 @@ class AiBuilderTools(ToolBase):
|
|
|
1824
1825
|
shape_failure = _schema_apps_shape_failure(tool_name="app_schema_apply", apps=apps)
|
|
1825
1826
|
if shape_failure is not None:
|
|
1826
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
|
|
1827
1835
|
icon_errors: list[JSONObject] = []
|
|
1828
1836
|
seen_new_app_icons: dict[str, int] = {}
|
|
1829
1837
|
for index, raw_item in enumerate(apps):
|
|
@@ -1892,6 +1900,9 @@ class AiBuilderTools(ToolBase):
|
|
|
1892
1900
|
created_app_keys: list[str] = []
|
|
1893
1901
|
results: list[JSONObject] = []
|
|
1894
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] = []
|
|
1895
1906
|
client_keys: set[str] = set()
|
|
1896
1907
|
|
|
1897
1908
|
for index, raw_item in enumerate(apps):
|
|
@@ -1931,25 +1942,47 @@ class AiBuilderTools(ToolBase):
|
|
|
1931
1942
|
public_shell = _publicize_package_fields(shell)
|
|
1932
1943
|
resolved_key = str(public_shell.get("app_key") or "").strip()
|
|
1933
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)
|
|
1934
1946
|
if shell_write_executed:
|
|
1935
1947
|
any_write_executed = True
|
|
1948
|
+
if shell_write_may_have_succeeded:
|
|
1949
|
+
any_write_may_have_succeeded = True
|
|
1936
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)
|
|
1937
1962
|
results.append({
|
|
1938
1963
|
"index": index,
|
|
1939
1964
|
"row_number": index + 1,
|
|
1940
1965
|
"client_key": client_key or None,
|
|
1941
1966
|
"app_name": app_name or None,
|
|
1942
1967
|
"app_key": resolved_key or app_key or None,
|
|
1943
|
-
"status":
|
|
1968
|
+
"status": item_status,
|
|
1944
1969
|
"stage": "resolve_or_create_shell",
|
|
1945
1970
|
"error_code": public_shell.get("error_code") or "APP_SHELL_APPLY_FAILED",
|
|
1946
1971
|
"message": public_shell.get("message") or "app shell resolve/create failed",
|
|
1947
1972
|
"write_executed": shell_write_executed,
|
|
1948
|
-
"
|
|
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 {}),
|
|
1949
1978
|
})
|
|
1950
1979
|
continue
|
|
1951
1980
|
if bool(public_shell.get("created")):
|
|
1952
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)
|
|
1953
1986
|
if client_key:
|
|
1954
1987
|
client_key_to_app_key[client_key] = resolved_key
|
|
1955
1988
|
resolved_name = str(public_shell.get("app_name_after") or public_shell.get("app_name") or app_name or "").strip()
|
|
@@ -2016,7 +2049,11 @@ class AiBuilderTools(ToolBase):
|
|
|
2016
2049
|
"verified": bool(shell_result.get("verified")),
|
|
2017
2050
|
"error_code": shell_result.get("error_code"),
|
|
2018
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),
|
|
2019
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 {}),
|
|
2020
2057
|
})
|
|
2021
2058
|
continue
|
|
2022
2059
|
|
|
@@ -2038,11 +2075,18 @@ class AiBuilderTools(ToolBase):
|
|
|
2038
2075
|
public_result = _publicize_package_fields(field_result)
|
|
2039
2076
|
if _schema_apply_result_has_write(public_result):
|
|
2040
2077
|
any_write_executed = True
|
|
2078
|
+
if _schema_apply_result_may_have_write(public_result):
|
|
2079
|
+
any_write_may_have_succeeded = True
|
|
2041
2080
|
item_status = public_result.get("status") if public_result.get("status") in {"success", "partial_success"} else "failed"
|
|
2042
2081
|
shell_field_diff = existing.get("shell_field_diff") if isinstance(existing.get("shell_field_diff"), dict) else {}
|
|
2043
2082
|
shell_field_diff_details = existing.get("shell_field_diff_details") if isinstance(existing.get("shell_field_diff_details"), dict) else {}
|
|
2044
2083
|
field_diff = _merge_schema_field_diffs(shell_field_diff, public_result.get("field_diff") or {})
|
|
2045
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")))
|
|
2046
2090
|
final_items.append({
|
|
2047
2091
|
**{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
|
|
2048
2092
|
"status": item_status,
|
|
@@ -2055,21 +2099,33 @@ class AiBuilderTools(ToolBase):
|
|
|
2055
2099
|
"verified": bool(public_result.get("verified")),
|
|
2056
2100
|
"error_code": public_result.get("error_code"),
|
|
2057
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),
|
|
2058
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 {}),
|
|
2059
2107
|
})
|
|
2060
2108
|
|
|
2109
|
+
pending_readback = sum(1 for item in final_items if item.get("status") == "pending_readback")
|
|
2061
2110
|
succeeded = sum(1 for item in final_items if item.get("status") in {"success", "partial_success"})
|
|
2062
|
-
failed =
|
|
2063
|
-
|
|
2064
|
-
|
|
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 = {
|
|
2065
2120
|
"status": overall_status,
|
|
2066
2121
|
"mode": "multi_app",
|
|
2067
2122
|
"total": len(apps),
|
|
2068
2123
|
"succeeded": succeeded,
|
|
2069
2124
|
"failed": failed,
|
|
2125
|
+
"pending_readback": pending_readback,
|
|
2070
2126
|
"created_app_keys": created_app_keys,
|
|
2071
2127
|
"write_executed": any_write_executed,
|
|
2072
|
-
"safe_to_retry": not any_write_executed,
|
|
2128
|
+
"safe_to_retry": not (any_write_executed or uncertain_write),
|
|
2073
2129
|
"package_id": package_id,
|
|
2074
2130
|
"publish_requested": publish,
|
|
2075
2131
|
"apps": final_items,
|
|
@@ -2077,12 +2133,23 @@ class AiBuilderTools(ToolBase):
|
|
|
2077
2133
|
"verification": {
|
|
2078
2134
|
"all_apps_succeeded": failed == 0,
|
|
2079
2135
|
"created_app_count": len(created_app_keys),
|
|
2136
|
+
"pending_readback_count": pending_readback,
|
|
2080
2137
|
},
|
|
2081
2138
|
"request_id": None,
|
|
2082
2139
|
"error_code": None if overall_status != "failed" else "MULTI_APP_SCHEMA_APPLY_FAILED",
|
|
2083
2140
|
"recoverable": overall_status != "success",
|
|
2084
|
-
"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
|
+
),
|
|
2085
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
|
|
2086
2153
|
|
|
2087
2154
|
def _app_schema_apply_once(
|
|
2088
2155
|
self,
|
|
@@ -2612,12 +2679,12 @@ class AiBuilderTools(ToolBase):
|
|
|
2612
2679
|
suggested_next_call={
|
|
2613
2680
|
"tool_name": "app_charts_apply",
|
|
2614
2681
|
"arguments": {
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
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
|
+
},
|
|
2621
2688
|
},
|
|
2622
2689
|
))
|
|
2623
2690
|
normalized_args = request.model_dump(mode="json")
|
|
@@ -3053,6 +3120,262 @@ def _normalize_schema_apps_argument(*, tool_name: str, package_id: int | None, a
|
|
|
3053
3120
|
return {"package_id": normalized_package_id, "apps": normalized_items, "warnings": warnings}
|
|
3054
3121
|
|
|
3055
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
|
+
|
|
3056
3379
|
def _compile_multi_app_schema_item_refs(
|
|
3057
3380
|
item: JSONObject,
|
|
3058
3381
|
client_key_to_app_key: dict[str, str],
|
|
@@ -3167,6 +3490,15 @@ def _schema_apply_result_has_write(result: JSONObject) -> bool:
|
|
|
3167
3490
|
return False
|
|
3168
3491
|
|
|
3169
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
|
+
|
|
3170
3502
|
def _validation_failure(
|
|
3171
3503
|
detail: str,
|
|
3172
3504
|
*,
|
|
@@ -3641,8 +3973,14 @@ def _builder_apply_summary(payload: JSONObject, resources: list[JSONObject]) ->
|
|
|
3641
3973
|
}
|
|
3642
3974
|
if "write_executed" in payload:
|
|
3643
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"))
|
|
3644
3978
|
if "safe_to_retry" in payload:
|
|
3645
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")
|
|
3646
3984
|
return summary
|
|
3647
3985
|
|
|
3648
3986
|
|
|
@@ -3929,7 +4267,7 @@ def _builder_schema_resources(payload: JSONObject) -> list[JSONObject]:
|
|
|
3929
4267
|
parent=package_parent,
|
|
3930
4268
|
icon_config=icon_config,
|
|
3931
4269
|
error_code=item.get("error_code"),
|
|
3932
|
-
message=item.get("message") if status
|
|
4270
|
+
message=item.get("message") if status in {"failed", "pending_readback"} else None,
|
|
3933
4271
|
)
|
|
3934
4272
|
)
|
|
3935
4273
|
resources.extend(_builder_field_resources(item.get("field_diff_details") or item.get("field_diff"), parent=parent))
|
|
@@ -5132,6 +5470,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5132
5470
|
},
|
|
5133
5471
|
"allowed_values": {
|
|
5134
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())},
|
|
5135
5474
|
"field.relation_mode": [member.value for member in PublicRelationMode],
|
|
5136
5475
|
"field.department_scope.mode": ["all", "custom"],
|
|
5137
5476
|
"field.code_block_binding.outputs.target_field.type": list(INTEGRATION_OUTPUT_TARGET_FIELD_TYPES),
|
|
@@ -5146,6 +5485,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5146
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",
|
|
5147
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",
|
|
5148
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",
|
|
5149
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",
|
|
5150
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",
|
|
5151
5491
|
"multi-app mode is not transactional; read created_app_keys and apps[].status before retrying, and retry only failed app items",
|
|
@@ -5153,6 +5493,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5153
5493
|
"create mode requires explicit icon + color; icon=template is blocked because it is too generic",
|
|
5154
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",
|
|
5155
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",
|
|
5156
5497
|
"single_select and multi_select options accept strings or objects such as {label,value}; builder normalizes them to option labels before writing",
|
|
5157
5498
|
"edit mode preserves existing icon/color when omitted; explicit icon/color values are still validated",
|
|
5158
5499
|
"call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
|
|
@@ -5164,6 +5505,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5164
5505
|
"relation_mode=multiple maps to referenceConfig.optionalDataNum=0",
|
|
5165
5506
|
"relation fields now require both display_field and visible_fields in MCP/CLI payloads",
|
|
5166
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",
|
|
5167
5509
|
"update_fields[].set.subfield_updates is the safe patch path for editing existing subtable child fields without rebuilding the entire subtable",
|
|
5168
5510
|
"subfield_updates only supports safe child overlays: name, required, description, and nested subfield_updates",
|
|
5169
5511
|
"set.subfields remains the full replace/rebuild path for a subtable and is higher risk when hidden relation/reference children exist",
|
|
@@ -5782,6 +6124,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5782
6124
|
"use this before app_schema_apply when you need exact field definitions",
|
|
5783
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",
|
|
5784
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",
|
|
5785
6128
|
"subtable fields include nested subfields using the same compact field shape",
|
|
5786
6129
|
],
|
|
5787
6130
|
"minimal_example": {
|
|
@@ -5877,7 +6220,22 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5877
6220
|
},
|
|
5878
6221
|
},
|
|
5879
6222
|
"app_charts_apply": {
|
|
5880
|
-
"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
|
+
],
|
|
5881
6239
|
"aliases": {
|
|
5882
6240
|
"patchCharts": "patch_charts",
|
|
5883
6241
|
"chart.id": "chart.chart_id",
|
|
@@ -5885,6 +6243,11 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5885
6243
|
"chart.dimension_fields": "chart.dimension_field_ids",
|
|
5886
6244
|
"chart.indicator_fields": "chart.indicator_field_ids",
|
|
5887
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",
|
|
5888
6251
|
"chart.filter.op": "chart.filter.operator",
|
|
5889
6252
|
},
|
|
5890
6253
|
"allowed_values": {
|
|
@@ -5905,6 +6268,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5905
6268
|
"upsert_charts[].visibility compiles to QingBI base visibleAuth only",
|
|
5906
6269
|
"visibility-only updates keep the existing chart config and do not rewrite rawDataConfigDTO.authInfo",
|
|
5907
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",
|
|
5908
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",
|
|
5909
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",
|
|
5910
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",
|
|
@@ -5915,7 +6281,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5915
6281
|
"minimal_example": {
|
|
5916
6282
|
"profile": "default",
|
|
5917
6283
|
"app_key": "APP_KEY",
|
|
5918
|
-
"upsert_charts": [{"name": "数据总量", "chart_type": "target", "
|
|
6284
|
+
"upsert_charts": [{"name": "数据总量", "chart_type": "target", "metric": "count(*)", "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
|
|
5919
6285
|
"patch_charts": [{"chart_id": "CHART_ID", "set": {"name": "数据总量-新版"}}],
|
|
5920
6286
|
"remove_chart_ids": [],
|
|
5921
6287
|
"reorder_chart_ids": [],
|
|
@@ -5962,9 +6328,11 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5962
6328
|
"viewRef": "view_ref",
|
|
5963
6329
|
"dashStyleConfigBO": "dash_style_config",
|
|
5964
6330
|
},
|
|
5965
|
-
"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"],
|
|
5966
6332
|
"section_aliases": {
|
|
5967
6333
|
"sourceType": "source_type",
|
|
6334
|
+
"zone": "role",
|
|
6335
|
+
"sectionRole": "role",
|
|
5968
6336
|
"chartRef": "chart_ref",
|
|
5969
6337
|
"viewRef": "view_ref",
|
|
5970
6338
|
"dashStyleConfigBO": "dash_style_config",
|
|
@@ -5996,6 +6364,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5996
6364
|
"if unsure about layout, omit position or use layout_preset=auto/dashboard_2col/dashboard_3col",
|
|
5997
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",
|
|
5998
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",
|
|
5999
6370
|
"position.pc/mobile is the canonical portal layout shape",
|
|
6000
6371
|
"compat payload accepts name -> dash_name and single pages[0].components -> sections",
|
|
6001
6372
|
"visibility is the canonical public auth shape; auth is kept only as a deprecated compatibility alias",
|