@qingflow-tech/qingflow-app-builder-mcp 1.0.40 → 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.
@@ -36,6 +36,7 @@ def _is_executed_nonfatal_result(result: dict[str, Any]) -> bool:
36
36
  status = str(result.get("status") or "").lower()
37
37
  executed = bool(
38
38
  result.get("write_executed")
39
+ or result.get("write_may_have_succeeded")
39
40
  or result.get("delete_executed")
40
41
  or result.get("action_executed")
41
42
  or result.get("export_executed")
@@ -423,6 +423,7 @@ def _is_executed_nonfatal_result(result: dict[str, Any]) -> bool:
423
423
  status = str(result.get("status") or "").lower()
424
424
  executed = bool(
425
425
  result.get("write_executed")
426
+ or result.get("write_may_have_succeeded")
426
427
  or result.get("delete_executed")
427
428
  or result.get("action_executed")
428
429
  or result.get("export_executed")
@@ -441,6 +442,7 @@ def _has_readback_unavailable_verification(result: dict[str, Any]) -> bool:
441
442
  "readback_pending",
442
443
  "metadata_unverified",
443
444
  "views_read_unavailable",
445
+ "readback_before_retry",
444
446
  )
445
447
  )
446
448
 
@@ -169,6 +169,7 @@ def _is_executed_nonfatal_payload(payload: dict[str, Any]) -> bool:
169
169
  status = str(payload.get("status") or "").lower()
170
170
  executed = bool(
171
171
  payload.get("write_executed")
172
+ or payload.get("write_may_have_succeeded")
172
173
  or payload.get("delete_executed")
173
174
  or payload.get("action_executed")
174
175
  or payload.get("export_executed")
@@ -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": "failed",
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
- "safe_to_retry": not shell_write_executed and not any_write_executed,
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 = len(final_items) - succeeded
2063
- overall_status = "success" if failed == 0 else ("partial_success" if succeeded > 0 or any_write_executed else "failed")
2064
- return {
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": "multi-app schema apply completed" if overall_status != "failed" else "multi-app schema apply failed",
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
- "profile": profile,
2616
- "app_key": app_key,
2617
- "upsert_charts": [{"name": "销售总量", "chart_type": "target", "indicator_field_ids": []}],
2618
- "remove_chart_ids": [],
2619
- "reorder_chart_ids": [],
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 == "failed" else None,
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": ["app_key", "upsert_charts", "patch_charts", "remove_chart_ids", "reorder_chart_ids", "upsert_charts[].visibility", "patch_charts[].chart_id", "patch_charts[].name", "patch_charts[].set", "patch_charts[].unset"],
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", "indicator_field_ids": [], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
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",