@qingflow-tech/qingflow-app-user-mcp 1.0.9 → 1.0.10

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.
@@ -84,7 +84,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
84
84
 
85
85
  package_apply = package_subparsers.add_parser("apply", help="创建或更新应用包配置")
86
86
  package_apply.add_argument("--config-file", required=True)
87
- package_apply.set_defaults(handler=_handle_package_apply, format_hint="builder_summary")
87
+ package_apply.set_defaults(handler=_handle_package_apply, format_hint="builder_summary", force_json_output=True)
88
88
 
89
89
  app = builder_subparsers.add_parser("app", help="应用")
90
90
  app_subparsers = app.add_subparsers(dest="builder_app_command", required=True)
@@ -99,7 +99,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
99
99
  app_release_lock.add_argument("--app-key", required=True)
100
100
  app_release_lock.add_argument("--lock-owner-email", required=True)
101
101
  app_release_lock.add_argument("--lock-owner-name", required=True)
102
- app_release_lock.set_defaults(handler=_handle_app_release_edit_lock_if_mine, format_hint="builder_summary")
102
+ app_release_lock.set_defaults(handler=_handle_app_release_edit_lock_if_mine, format_hint="builder_summary", force_json_output=True)
103
103
 
104
104
  app_get = app_subparsers.add_parser("get", help="读取应用配置(字段请使用: builder app get --app-key APP fields)")
105
105
  app_get.add_argument(
@@ -129,7 +129,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
129
129
  button_apply.add_argument("--patch-buttons-file")
130
130
  button_apply.add_argument("--remove-buttons-file")
131
131
  button_apply.add_argument("--view-configs-file")
132
- button_apply.set_defaults(handler=_handle_button_apply, format_hint="builder_summary")
132
+ button_apply.set_defaults(handler=_handle_button_apply, format_hint="builder_summary", force_json_output=True)
133
133
 
134
134
  associated_resource = builder_subparsers.add_parser("associated-resource", aliases=["associated-resources"], help="关联视图/报表")
135
135
  associated_resource_subparsers = associated_resource.add_subparsers(dest="builder_associated_resource_command", required=True)
@@ -140,7 +140,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
140
140
  associated_resource_apply.add_argument("--remove-associated-item-ids-file")
141
141
  associated_resource_apply.add_argument("--reorder-associated-item-ids-file")
142
142
  associated_resource_apply.add_argument("--view-configs-file")
143
- associated_resource_apply.set_defaults(handler=_handle_associated_resource_apply, format_hint="builder_summary")
143
+ associated_resource_apply.set_defaults(handler=_handle_associated_resource_apply, format_hint="builder_summary", force_json_output=True)
144
144
 
145
145
  portal = builder_subparsers.add_parser("portal", help="门户")
146
146
  portal_subparsers = portal.add_subparsers(dest="builder_portal_command", required=True)
@@ -165,7 +165,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
165
165
  portal_apply.add_argument("--hide-copyright", action=argparse.BooleanOptionalAction, default=None)
166
166
  portal_apply.add_argument("--dash-global-config-file")
167
167
  portal_apply.add_argument("--config-file")
168
- portal_apply.set_defaults(handler=_handle_portal_apply, format_hint="builder_summary")
168
+ portal_apply.set_defaults(handler=_handle_portal_apply, format_hint="builder_summary", force_json_output=True)
169
169
 
170
170
  schema_apply = builder_subparsers.add_parser("schema", help="字段搭建")
171
171
  schema_apply_subparsers = schema_apply.add_subparsers(dest="builder_schema_command", required=True)
@@ -179,10 +179,11 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
179
179
  schema_apply_apply.add_argument("--visibility-file")
180
180
  schema_apply_apply.add_argument("--create-if-missing", action="store_true")
181
181
  schema_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
182
+ schema_apply_apply.add_argument("--apps-file", help="多应用 schema JSON 数组;每项可带 client_key/app_name/add_fields,支持 relation target_app_ref")
182
183
  schema_apply_apply.add_argument("--add-fields-file", help="字段 JSON 数组;字段可用 as_data_title/as_data_cover 标记数据标题/封面")
183
184
  schema_apply_apply.add_argument("--update-fields-file", help="字段更新 JSON 数组;set 内可用 as_data_title/as_data_cover 标记数据标题/封面")
184
185
  schema_apply_apply.add_argument("--remove-fields-file")
185
- schema_apply_apply.set_defaults(handler=_handle_schema_apply, format_hint="builder_summary")
186
+ schema_apply_apply.set_defaults(handler=_handle_schema_apply, format_hint="builder_summary", force_json_output=True)
186
187
 
187
188
  layout_apply = builder_subparsers.add_parser("layout", help="布局")
188
189
  layout_apply_subparsers = layout_apply.add_subparsers(dest="builder_layout_command", required=True)
@@ -191,7 +192,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
191
192
  layout_apply_apply.add_argument("--mode", choices=["merge", "replace"], default="merge")
192
193
  layout_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
193
194
  layout_apply_apply.add_argument("--sections-file", required=True)
194
- layout_apply_apply.set_defaults(handler=_handle_layout_apply, format_hint="builder_summary")
195
+ layout_apply_apply.set_defaults(handler=_handle_layout_apply, format_hint="builder_summary", force_json_output=True)
195
196
 
196
197
  views_apply = builder_subparsers.add_parser("views", help="视图")
197
198
  views_apply_subparsers = views_apply.add_subparsers(dest="builder_views_command", required=True)
@@ -201,7 +202,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
201
202
  views_apply_apply.add_argument("--upsert-views-file")
202
203
  views_apply_apply.add_argument("--patch-views-file")
203
204
  views_apply_apply.add_argument("--remove-views-file")
204
- views_apply_apply.set_defaults(handler=_handle_views_apply, format_hint="builder_summary")
205
+ views_apply_apply.set_defaults(handler=_handle_views_apply, format_hint="builder_summary", force_json_output=True)
205
206
 
206
207
  flow_apply = builder_subparsers.add_parser("flow", help="流程")
207
208
  flow_apply_subparsers = flow_apply.add_subparsers(dest="builder_flow_command", required=True)
@@ -211,7 +212,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
211
212
  flow_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
212
213
  flow_apply_apply.add_argument("--nodes-file", required=True)
213
214
  flow_apply_apply.add_argument("--transitions-file", required=True)
214
- flow_apply_apply.set_defaults(handler=_handle_flow_apply, format_hint="builder_summary")
215
+ flow_apply_apply.set_defaults(handler=_handle_flow_apply, format_hint="builder_summary", force_json_output=True)
215
216
 
216
217
  charts_apply = builder_subparsers.add_parser("charts", help="报表")
217
218
  charts_apply_subparsers = charts_apply.add_subparsers(dest="builder_charts_command", required=True)
@@ -221,14 +222,14 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
221
222
  charts_apply_apply.add_argument("--patch-file")
222
223
  charts_apply_apply.add_argument("--remove-chart-ids-file")
223
224
  charts_apply_apply.add_argument("--reorder-chart-ids-file")
224
- charts_apply_apply.set_defaults(handler=_handle_charts_apply, format_hint="builder_summary")
225
+ charts_apply_apply.set_defaults(handler=_handle_charts_apply, format_hint="builder_summary", force_json_output=True)
225
226
 
226
227
  publish_verify = builder_subparsers.add_parser("publish", help="发布校验")
227
228
  publish_verify_subparsers = publish_verify.add_subparsers(dest="builder_publish_command", required=True)
228
229
  publish_verify_verify = publish_verify_subparsers.add_parser("verify", help="校验应用发布")
229
230
  publish_verify_verify.add_argument("--app-key", required=True)
230
231
  publish_verify_verify.add_argument("--expected-package-id", type=int)
231
- publish_verify_verify.set_defaults(handler=_handle_publish_verify, format_hint="builder_summary")
232
+ publish_verify_verify.set_defaults(handler=_handle_publish_verify, format_hint="builder_summary", force_json_output=True)
232
233
 
233
234
  view = builder_subparsers.add_parser("view", help="视图详情")
234
235
  view_subparsers = view.add_subparsers(dest="builder_view_command", required=True)
@@ -451,6 +452,34 @@ def _handle_chart_get(args: argparse.Namespace, context: CliContext) -> dict:
451
452
 
452
453
 
453
454
  def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
455
+ apps = load_list_arg(args.apps_file, option_name="--apps-file")
456
+ if args.apps_file:
457
+ if not apps:
458
+ raise_config_error(
459
+ "schema apply multi-app mode requires a non-empty --apps-file.",
460
+ fix_hint="Pass a JSON array with at least one app item.",
461
+ )
462
+ if args.app_key or args.app_name or args.app_title or args.add_fields_file or args.update_fields_file or args.remove_fields_file:
463
+ raise_config_error(
464
+ "schema apply multi-app mode accepts --package-id/--create-if-missing plus --apps-file only.",
465
+ fix_hint="Use `--apps-file` for batch mode, or remove `--apps-file` and use the single-app arguments.",
466
+ )
467
+ if args.package_id is None:
468
+ raise_config_error(
469
+ "schema apply multi-app mode requires --package-id.",
470
+ fix_hint="Pass `--package-id` and app names inside --apps-file.",
471
+ )
472
+ return context.builder.app_schema_apply(
473
+ profile=args.profile,
474
+ package_id=args.package_id,
475
+ visibility=load_object_arg(args.visibility_file, option_name="--visibility-file"),
476
+ create_if_missing=bool(args.create_if_missing),
477
+ publish=bool(args.publish),
478
+ apps=apps,
479
+ add_fields=[],
480
+ update_fields=[],
481
+ remove_fields=[],
482
+ )
454
483
  has_app_key = bool((args.app_key or "").strip())
455
484
  has_app_name = bool((args.app_name or "").strip())
456
485
  has_app_title = bool((args.app_title or "").strip())
@@ -8,6 +8,7 @@ from typing import Any, Callable, TextIO
8
8
  from ..errors import QingflowApiError
9
9
  from ..public_surface import cli_public_tool_spec_from_namespace
10
10
  from ..response_trim import resolve_cli_tool_name, trim_error_response, trim_public_response
11
+ from ..tools.ai_builder_tools import _attach_builder_apply_envelope
11
12
  from .context import CliContext, build_cli_context
12
13
  from .formatters import emit_json_result, emit_text_result
13
14
  from .commands import register_all_commands
@@ -16,8 +17,21 @@ from .commands import register_all_commands
16
17
  Handler = Callable[[argparse.Namespace, CliContext], dict[str, Any]]
17
18
 
18
19
 
20
+ class _CliArgumentError(Exception):
21
+ def __init__(self, *, prog: str, message: str, usage: str) -> None:
22
+ super().__init__(message)
23
+ self.prog = prog
24
+ self.message = message
25
+ self.usage = usage
26
+
27
+
28
+ class _QingflowArgumentParser(argparse.ArgumentParser):
29
+ def error(self, message: str) -> None:
30
+ raise _CliArgumentError(prog=self.prog, message=message, usage=self.format_usage())
31
+
32
+
19
33
  def build_parser() -> argparse.ArgumentParser:
20
- parser = argparse.ArgumentParser(prog="qingflow", description="Qingflow CLI")
34
+ parser = _QingflowArgumentParser(prog="qingflow", description="Qingflow CLI")
21
35
  parser.add_argument("--profile", default="default", help="会话 profile,默认 default")
22
36
  parser.add_argument("--json", action="store_true", help="输出 JSON")
23
37
  subparsers = parser.add_subparsers(dest="command", required=True)
@@ -42,6 +56,21 @@ def run(
42
56
  normalized_argv = _normalize_global_args(list(argv) if argv is not None else sys.argv[1:])
43
57
  try:
44
58
  args = parser.parse_args(normalized_argv)
59
+ except _CliArgumentError as exc:
60
+ if _should_force_json_output_argv(normalized_argv):
61
+ payload = {
62
+ "category": "config",
63
+ "status": "failed",
64
+ "error_code": "ARGUMENT_ERROR",
65
+ "message": exc.message,
66
+ "details": {"usage": exc.usage.strip(), "prog": exc.prog},
67
+ }
68
+ payload = _maybe_attach_builder_apply_error_envelope_from_argv(normalized_argv, payload)
69
+ emit_json_result(payload, stream=out)
70
+ return 2
71
+ err.write(exc.usage)
72
+ err.write(f"{exc.prog}: error: {exc.message}\n")
73
+ return 2
45
74
  except SystemExit as exc:
46
75
  return int(exc.code or 0)
47
76
  setattr(args, "_stdin", sys.stdin)
@@ -51,8 +80,10 @@ def run(
51
80
  if handler is None:
52
81
  parser.print_help(out)
53
82
  return 2
54
- context = context_factory()
55
83
  try:
84
+ if _should_force_json_output(args):
85
+ setattr(args, "json", True)
86
+ context = context_factory()
56
87
  if not bool(args.json):
57
88
  _emit_cli_effective_context_notice(args, context, stream=err)
58
89
  result = handler(args, context)
@@ -60,12 +91,15 @@ def run(
60
91
  return int(exc.code or 0)
61
92
  except RuntimeError as exc:
62
93
  payload = trim_error_response(_parse_error_payload(exc))
94
+ payload = _maybe_attach_builder_apply_error_envelope_from_args(args, payload)
63
95
  return _emit_error(payload, json_mode=bool(args.json), stdout=out, stderr=err)
64
96
  except QingflowApiError as exc:
65
97
  payload = trim_error_response(exc.to_dict())
98
+ payload = _maybe_attach_builder_apply_error_envelope_from_args(args, payload)
66
99
  return _emit_error(payload, json_mode=bool(args.json), stdout=out, stderr=err)
67
100
  finally:
68
- context.close()
101
+ if "context" in locals():
102
+ context.close()
69
103
 
70
104
  exit_code = _result_exit_code(result)
71
105
  trimmed_result = trim_public_response(resolve_cli_tool_name(args), result) if isinstance(result, dict) else result
@@ -104,6 +138,173 @@ def _normalize_global_args(argv: list[str]) -> list[str]:
104
138
  return global_args + remaining
105
139
 
106
140
 
141
+ def _should_force_json_output(args: argparse.Namespace) -> bool:
142
+ if bool(getattr(args, "force_json_output", False)):
143
+ return True
144
+ if (
145
+ getattr(args, "command", "") == "builder"
146
+ and getattr(args, "builder_app_command", "") == "repair-code-blocks"
147
+ and bool(getattr(args, "apply", False))
148
+ ):
149
+ return True
150
+ return False
151
+
152
+
153
+ def _should_force_json_output_argv(argv: list[str]) -> bool:
154
+ tokens = _strip_global_args(argv)
155
+ if not tokens or tokens[0] not in {"builder", "build"}:
156
+ return False
157
+ if len(tokens) < 3:
158
+ return False
159
+ section = tokens[1]
160
+ action = tokens[2]
161
+ if section in {"package", "button", "associated-resource", "associated-resources", "portal", "schema", "layout", "views", "flow", "charts"}:
162
+ return action == "apply"
163
+ if section == "publish":
164
+ return action == "verify"
165
+ if section == "app":
166
+ if action == "release-edit-lock-if-mine":
167
+ return True
168
+ if action == "repair-code-blocks":
169
+ return "--apply" in tokens
170
+ return False
171
+
172
+
173
+ def _maybe_attach_builder_apply_error_envelope_from_args(args: argparse.Namespace, payload: dict[str, Any]) -> dict[str, Any]:
174
+ operation = _builder_apply_operation_from_args(args)
175
+ if not operation:
176
+ return payload
177
+ enriched = dict(payload)
178
+ enriched.setdefault("status", "failed")
179
+ enriched.setdefault("ok", False)
180
+ enriched.setdefault("write_executed", False)
181
+ enriched.setdefault("safe_to_retry", False)
182
+ _copy_arg_identity(enriched, args)
183
+ return _attach_builder_apply_envelope(operation, enriched)
184
+
185
+
186
+ def _maybe_attach_builder_apply_error_envelope_from_argv(argv: list[str], payload: dict[str, Any]) -> dict[str, Any]:
187
+ operation = _builder_apply_operation_from_argv(argv)
188
+ if not operation:
189
+ return payload
190
+ enriched = dict(payload)
191
+ enriched.setdefault("status", "failed")
192
+ enriched.setdefault("ok", False)
193
+ enriched.setdefault("write_executed", False)
194
+ enriched.setdefault("safe_to_retry", False)
195
+ _copy_argv_identity(enriched, argv)
196
+ return _attach_builder_apply_envelope(operation, enriched)
197
+
198
+
199
+ def _builder_apply_operation_from_args(args: argparse.Namespace) -> str | None:
200
+ if getattr(args, "command", "") not in {"builder", "build"}:
201
+ return None
202
+ section = str(getattr(args, "builder_command", "") or "")
203
+ if section == "package" and getattr(args, "builder_package_command", "") == "apply":
204
+ return "package_apply"
205
+ if section == "button" and getattr(args, "builder_button_command", "") == "apply":
206
+ return "app_custom_buttons_apply"
207
+ if section in {"associated-resource", "associated-resources"} and getattr(args, "builder_associated_resource_command", "") == "apply":
208
+ return "app_associated_resources_apply"
209
+ if section == "portal" and getattr(args, "builder_portal_command", "") == "apply":
210
+ return "portal_apply"
211
+ if section == "schema" and getattr(args, "builder_schema_command", "") == "apply":
212
+ return "app_schema_apply"
213
+ if section == "layout" and getattr(args, "builder_layout_command", "") == "apply":
214
+ return "app_layout_apply"
215
+ if section == "views" and getattr(args, "builder_views_command", "") == "apply":
216
+ return "app_views_apply"
217
+ if section == "flow" and getattr(args, "builder_flow_command", "") == "apply":
218
+ return "app_flow_apply"
219
+ if section == "charts" and getattr(args, "builder_charts_command", "") == "apply":
220
+ return "app_charts_apply"
221
+ if section == "publish" and getattr(args, "builder_publish_command", "") == "verify":
222
+ return "app_publish_verify"
223
+ return None
224
+
225
+
226
+ def _builder_apply_operation_from_argv(argv: list[str]) -> str | None:
227
+ tokens = _strip_global_args(argv)
228
+ if not tokens or tokens[0] not in {"builder", "build"} or len(tokens) < 3:
229
+ return None
230
+ section = tokens[1]
231
+ action = tokens[2]
232
+ if action != "apply" and not (section == "publish" and action == "verify"):
233
+ return None
234
+ mapping = {
235
+ "package": "package_apply",
236
+ "button": "app_custom_buttons_apply",
237
+ "associated-resource": "app_associated_resources_apply",
238
+ "associated-resources": "app_associated_resources_apply",
239
+ "portal": "portal_apply",
240
+ "schema": "app_schema_apply",
241
+ "layout": "app_layout_apply",
242
+ "views": "app_views_apply",
243
+ "flow": "app_flow_apply",
244
+ "charts": "app_charts_apply",
245
+ "publish": "app_publish_verify",
246
+ }
247
+ return mapping.get(section)
248
+
249
+
250
+ def _copy_arg_identity(payload: dict[str, Any], args: argparse.Namespace) -> None:
251
+ for attr, key in (
252
+ ("app_key", "app_key"),
253
+ ("app_name", "app_name"),
254
+ ("app_title", "app_title"),
255
+ ("package_id", "package_id"),
256
+ ("dash_key", "dash_key"),
257
+ ("dash_name", "dash_name"),
258
+ ):
259
+ value = getattr(args, attr, None)
260
+ if value not in (None, ""):
261
+ payload.setdefault(key, value)
262
+
263
+
264
+ def _copy_argv_identity(payload: dict[str, Any], argv: list[str]) -> None:
265
+ tokens = _strip_global_args(argv)
266
+ option_to_key = {
267
+ "--app-key": "app_key",
268
+ "--app-name": "app_name",
269
+ "--app-title": "app_title",
270
+ "--package-id": "package_id",
271
+ "--dash-key": "dash_key",
272
+ "--dash-name": "dash_name",
273
+ }
274
+ index = 0
275
+ while index < len(tokens):
276
+ token = tokens[index]
277
+ if token in option_to_key and index + 1 < len(tokens):
278
+ payload.setdefault(option_to_key[token], tokens[index + 1])
279
+ index += 2
280
+ continue
281
+ for option, key in option_to_key.items():
282
+ prefix = f"{option}="
283
+ if token.startswith(prefix):
284
+ payload.setdefault(key, token[len(prefix) :])
285
+ break
286
+ index += 1
287
+
288
+
289
+ def _strip_global_args(argv: list[str]) -> list[str]:
290
+ stripped: list[str] = []
291
+ index = 0
292
+ while index < len(argv):
293
+ token = argv[index]
294
+ if token == "--json":
295
+ index += 1
296
+ continue
297
+ if token == "--profile":
298
+ index += 2
299
+ continue
300
+ if token.startswith("--profile="):
301
+ index += 1
302
+ continue
303
+ stripped.append(token)
304
+ index += 1
305
+ return stripped
306
+
307
+
107
308
  def _parse_error_payload(exc: RuntimeError) -> dict[str, Any]:
108
309
  raw = str(exc)
109
310
  try:
@@ -58,7 +58,7 @@ def trim_public_response(tool_name: str | None, payload: dict[str, Any]) -> dict
58
58
  return payload
59
59
  if _looks_like_failure_payload(payload):
60
60
  status = str(payload.get("status") or "").lower()
61
- if tool_name in {"user:record_insert", "user:record_update"} and status in {
61
+ if tool_name in {"user:record_insert", "user:record_update", "user:record_delete"} and status in {
62
62
  "blocked",
63
63
  "needs_confirmation",
64
64
  "partial_success",
@@ -75,6 +75,8 @@ def trim_success_response(tool_name: str | None, payload: dict[str, Any]) -> dic
75
75
  drop_keys = COMMON_SUCCESS_DROP_TOP
76
76
  if tool_name == "user:record_get":
77
77
  drop_keys = COMMON_SUCCESS_DROP_TOP - {"output_profile"}
78
+ if tool_name in {"user:record_insert", "user:record_update", "user:record_delete"} and payload.get("ok") is False:
79
+ drop_keys = drop_keys - {"ok"}
78
80
  _drop_top_keys(trimmed, drop_keys)
79
81
  transformer = SUCCESS_POLICY_BY_TOOL.get(tool_name or "")
80
82
  if transformer is not None:
@@ -420,7 +422,7 @@ def _trim_record_write(payload: JSONObject) -> None:
420
422
  def _trim_record_write_batch(payload: JSONObject, data: JSONObject) -> None:
421
423
  data.pop("items", None)
422
424
  data.pop("debug", None)
423
- for key in ("summary", "created_record_ids", "app_key", "mode"):
425
+ for key in ("summary", "app_key", "mode"):
424
426
  if data.get(key) in (None, [], {}, ""):
425
427
  data.pop(key, None)
426
428
  items = payload.get("items")
@@ -449,10 +451,8 @@ def _trim_record_write_batch(payload: JSONObject, data: JSONObject) -> None:
449
451
  for item in items
450
452
  if isinstance(item, dict)
451
453
  ]
452
- for key in ("items", "created_record_ids"):
453
- value = payload.get(key)
454
- if value in (None, [], {}, ""):
455
- payload.pop(key, None)
454
+ if payload.get("items") in (None, [], {}, ""):
455
+ payload.pop("items", None)
456
456
 
457
457
 
458
458
  def _trim_record_get(payload: JSONObject) -> None:
@@ -714,13 +714,18 @@ def _trim_record_delete(payload: JSONObject) -> None:
714
714
  if not isinstance(data, dict):
715
715
  return
716
716
  resource = data.get("resource")
717
- deleted_ids: list[str] = []
718
- if isinstance(resource, dict):
717
+ deleted_ids = payload.get("deleted_ids") if isinstance(payload.get("deleted_ids"), list) else data.get("deleted_ids")
718
+ failed_ids = payload.get("failed_ids") if isinstance(payload.get("failed_ids"), list) else data.get("failed_ids")
719
+ if not isinstance(deleted_ids, list):
720
+ deleted_ids = []
721
+ if not isinstance(failed_ids, list):
722
+ failed_ids = []
723
+ if not deleted_ids and isinstance(resource, dict):
719
724
  raw_ids = resource.get("record_ids") or resource.get("apply_ids") or resource.get("applyIds")
720
725
  if isinstance(raw_ids, list):
721
726
  deleted_ids = [str(item) for item in raw_ids if item not in (None, "")]
722
- data["deleted_ids"] = deleted_ids
723
- data.setdefault("failed_ids", [])
727
+ data["deleted_ids"] = [str(item) for item in deleted_ids if item not in (None, "")]
728
+ data["failed_ids"] = [str(item) for item in failed_ids if item not in (None, "")]
724
729
  for key in (
725
730
  "resource",
726
731
  "action",
@@ -39,9 +39,10 @@ def build_builder_server() -> FastMCP:
39
39
  "app_get as the default app map read, then app_get_fields/app_repair_code_blocks/app_get_layout/app_get_views/app_get_flow/app_get_charts/portal_list/portal_get/view_get/chart_get for focused configuration reads, "
40
40
  "member_search/role_search/role_create when workflow assignees must come from the directory or role catalog, preferring roles over explicit members unless the user explicitly names members, "
41
41
  "then app_schema_apply/app_layout_apply/app_flow_apply/app_views_apply/app_custom_buttons_apply/app_associated_resources_apply/app_charts_apply/portal_apply to execute normalized patches; these apply tools perform planning, normalization, and dependency checks internally where applicable. Schema/layout/views noop requests skip publish, app_custom_buttons_apply and app_associated_resources_apply publish after at least one write succeeds and expose no draft-only parameter, charts are immediate-live without publish and resolve targets by chart_id first then exact unique chart name, portal updates use replace semantics only when sections are supplied and edit-mode base-info-only updates may omit sections, publish=false only guarantees draft/base-info updates for tools that still expose that parameter, and flow should use publish=false whenever you only want draft/precheck behavior. "
42
+ "Builder apply/write outputs include schema_version, operation, summary, and resources[]; use resources[].id/key/name/ids/parent as the stable UI and agent display entry, and keep legacy fields such as field_diff/views_diff/chart_results only for compatibility or troubleshooting. "
42
43
  "For existing object parameter replacement, prefer patch_views, patch_buttons, patch_resources, and patch_charts with set/unset; the tool reads current config and full-saves internally, while upsert_* is for creation or full target configuration and should not be used as an incomplete partial update. "
43
- "For app_schema_apply, configure data title and data cover directly in field JSON with as_data_title=true and as_data_cover=true; data title is required and exactly one field may be marked, while data cover is optional and must be a top-level attachment field. "
44
- "For app_views_apply, keep fixed saved filters in filters and configure the frontend query panel separately with query_conditions; query_conditions.rows is a matrix of field names compiled to backend queryCondition queIds. "
44
+ "For app_schema_apply, configure data title and data cover directly in field JSON with as_data_title=true and as_data_cover=true; data title is required and exactly one field may be marked, while data cover is optional and must be a top-level attachment field. For multi-app creation, pass apps[]/--apps-file on app_schema_apply; each item may have client_key, and relation fields may use target_app_ref to point at another same-call client_key. "
45
+ "For app_views_apply, keep fixed saved filters in filters and configure the frontend query panel separately with query_conditions; query_conditions.rows is a matrix of field names compiled to backend queryCondition queIds. New views default associated report/view display to visible with limit_type=all; existing views preserve their current associated display unless associated_resources is explicitly patched. "
45
46
  "For custom button body create/update/delete and view placement, use app_custom_buttons_apply. For addData buttons, prefer trigger_add_data_config.target_app_key + field_mappings/default_values; do not ask agents to write raw que_relation unless maintaining a legacy config. field_mappings.source_field accepts source schema fields and supported system fields: 数据ID/row_record_id/apply_id/_id means current record id (-17), 编号/record_number means visible record number (0). To fill a target relation with the current record, map {'source_field': '数据ID', 'target_field': '目标引用字段'}; default_values is only for static constants. View button bindings merge by default and merge-mode view_configs must include buttons; use view_configs[].mode=replace or explicit buttons=[] only when clearing/replacing existing bindings is intended. Builder view_key arguments are raw keys from app_get.views[].view_key and must not be prefixed with custom:. "
46
47
  "For BI reports, keep report-body development separate from Qingflow in-app display: use app_charts_apply to create, update, remove, or reorder app-source QingBI chart bodies/configs with dataSourceType=qingflow; dataset BI reports are not created or edited by app_charts_apply yet and should be created in QingBI first, then attached with app_associated_resources_apply using report_source=dataset. "
47
48
  "For associated views/reports, use app_associated_resources_apply. Use match_mappings for filtering associated resources: dynamic current-record conditions use source_field, static conditions use value. match_mappings also supports 数据ID(-17) and 编号(0). Do not ask agents to write raw match_rules unless preserving a legacy backend config. "
@@ -407,7 +408,30 @@ def build_builder_server() -> FastMCP:
407
408
  add_fields: list[dict] | None = None,
408
409
  update_fields: list[dict] | None = None,
409
410
  remove_fields: list[dict] | None = None,
411
+ apps: list[dict] | None = None,
410
412
  ) -> dict:
413
+ if apps:
414
+ if app_key or app_name or app_title or add_fields or update_fields or remove_fields:
415
+ return _config_failure(
416
+ "app_schema_apply multi-app mode accepts package_id/create_if_missing plus apps only.",
417
+ fix_hint="Use `apps` for batch mode, or use the single-app arguments without `apps`.",
418
+ )
419
+ if package_id is None:
420
+ return _config_failure(
421
+ "app_schema_apply multi-app mode requires package_id.",
422
+ fix_hint="Pass `package_id` and `apps[].app_name` for new apps, or `apps[].app_key` for existing apps.",
423
+ )
424
+ return ai_builder.app_schema_apply(
425
+ profile=profile,
426
+ package_id=package_id,
427
+ visibility=visibility,
428
+ create_if_missing=create_if_missing,
429
+ publish=publish,
430
+ add_fields=[],
431
+ update_fields=[],
432
+ remove_fields=[],
433
+ apps=apps,
434
+ )
411
435
  has_app_key = bool((app_key or "").strip())
412
436
  has_app_name = bool((app_name or "").strip())
413
437
  has_app_title = bool((app_title or "").strip())
@@ -437,6 +461,7 @@ def build_builder_server() -> FastMCP:
437
461
  add_fields=add_fields or [],
438
462
  update_fields=update_fields or [],
439
463
  remove_fields=remove_fields or [],
464
+ apps=[],
440
465
  )
441
466
 
442
467
  @server.tool()