@qingflow-tech/qingflow-app-user-mcp 1.0.5 → 1.0.7

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.
@@ -123,30 +123,22 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
123
123
  button_catalog = button_subparsers.add_parser("catalog", help="读取按钮样式目录")
124
124
  button_catalog.set_defaults(handler=_handle_button_catalog, format_hint="builder_summary")
125
125
 
126
- button_list = button_subparsers.add_parser("list", help="列出自定义按钮")
127
- button_list.add_argument("--app-key", required=True)
128
- button_list.set_defaults(handler=_handle_button_list, format_hint="builder_summary")
129
-
130
- button_get = button_subparsers.add_parser("get", help="读取自定义按钮")
131
- button_get.add_argument("--app-key", required=True)
132
- button_get.add_argument("--button-id", type=int, required=True)
133
- button_get.set_defaults(handler=_handle_button_get, format_hint="builder_summary")
134
-
135
- button_create = button_subparsers.add_parser("create", help="创建自定义按钮")
136
- button_create.add_argument("--app-key", required=True)
137
- button_create.add_argument("--payload-file", required=True)
138
- button_create.set_defaults(handler=_handle_button_create, format_hint="builder_summary")
139
-
140
- button_update = button_subparsers.add_parser("update", help="更新自定义按钮")
141
- button_update.add_argument("--app-key", required=True)
142
- button_update.add_argument("--button-id", type=int, required=True)
143
- button_update.add_argument("--payload-file", required=True)
144
- button_update.set_defaults(handler=_handle_button_update, format_hint="builder_summary")
145
-
146
- button_delete = button_subparsers.add_parser("delete", help="删除自定义按钮")
147
- button_delete.add_argument("--app-key", required=True)
148
- button_delete.add_argument("--button-id", type=int, required=True)
149
- button_delete.set_defaults(handler=_handle_button_delete, format_hint="builder_summary")
126
+ button_apply = button_subparsers.add_parser("apply", help="声明式创建、更新或删除自定义按钮")
127
+ button_apply.add_argument("--app-key", required=True)
128
+ button_apply.add_argument("--upsert-buttons-file")
129
+ button_apply.add_argument("--remove-buttons-file")
130
+ button_apply.add_argument("--view-configs-file")
131
+ button_apply.set_defaults(handler=_handle_button_apply, format_hint="builder_summary")
132
+
133
+ associated_resource = builder_subparsers.add_parser("associated-resource", aliases=["associated-resources"], help="关联视图/报表")
134
+ associated_resource_subparsers = associated_resource.add_subparsers(dest="builder_associated_resource_command", required=True)
135
+ associated_resource_apply = associated_resource_subparsers.add_parser("apply", help="声明式管理应用关联资源池和视图展示配置")
136
+ associated_resource_apply.add_argument("--app-key", required=True)
137
+ associated_resource_apply.add_argument("--upsert-resources-file")
138
+ associated_resource_apply.add_argument("--remove-associated-item-ids-file")
139
+ associated_resource_apply.add_argument("--reorder-associated-item-ids-file")
140
+ associated_resource_apply.add_argument("--view-configs-file")
141
+ associated_resource_apply.set_defaults(handler=_handle_associated_resource_apply, format_hint="builder_summary")
150
142
 
151
143
  portal = builder_subparsers.add_parser("portal", help="门户")
152
144
  portal_subparsers = portal.add_subparsers(dest="builder_portal_command", required=True)
@@ -185,8 +177,8 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
185
177
  schema_apply_apply.add_argument("--visibility-file")
186
178
  schema_apply_apply.add_argument("--create-if-missing", action="store_true")
187
179
  schema_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
188
- schema_apply_apply.add_argument("--add-fields-file")
189
- schema_apply_apply.add_argument("--update-fields-file")
180
+ schema_apply_apply.add_argument("--add-fields-file", help="字段 JSON 数组;字段可用 as_data_title/as_data_cover 标记数据标题/封面")
181
+ schema_apply_apply.add_argument("--update-fields-file", help="字段更新 JSON 数组;set 内可用 as_data_title/as_data_cover 标记数据标题/封面")
190
182
  schema_apply_apply.add_argument("--remove-fields-file")
191
183
  schema_apply_apply.set_defaults(handler=_handle_schema_apply, format_hint="builder_summary")
192
184
 
@@ -369,6 +361,27 @@ def _handle_button_catalog(args: argparse.Namespace, context: CliContext) -> dic
369
361
  return context.builder.button_style_catalog_get(profile=args.profile)
370
362
 
371
363
 
364
+ def _handle_button_apply(args: argparse.Namespace, context: CliContext) -> dict:
365
+ return context.builder.app_custom_buttons_apply(
366
+ profile=args.profile,
367
+ app_key=args.app_key,
368
+ upsert_buttons=load_list_arg(args.upsert_buttons_file, option_name="--upsert-buttons-file"),
369
+ remove_buttons=load_list_arg(args.remove_buttons_file, option_name="--remove-buttons-file"),
370
+ view_configs=load_list_arg(args.view_configs_file, option_name="--view-configs-file"),
371
+ )
372
+
373
+
374
+ def _handle_associated_resource_apply(args: argparse.Namespace, context: CliContext) -> dict:
375
+ return context.builder.app_associated_resources_apply(
376
+ profile=args.profile,
377
+ app_key=args.app_key,
378
+ upsert_resources=load_list_arg(args.upsert_resources_file, option_name="--upsert-resources-file"),
379
+ remove_associated_item_ids=load_list_arg(args.remove_associated_item_ids_file, option_name="--remove-associated-item-ids-file"),
380
+ reorder_associated_item_ids=load_list_arg(args.reorder_associated_item_ids_file, option_name="--reorder-associated-item-ids-file"),
381
+ view_configs=load_list_arg(args.view_configs_file, option_name="--view-configs-file"),
382
+ )
383
+
384
+
372
385
  def _handle_button_get(args: argparse.Namespace, context: CliContext) -> dict:
373
386
  return context.builder.app_custom_button_get(profile=args.profile, app_key=args.app_key, button_id=args.button_id)
374
387
 
@@ -82,7 +82,8 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
82
82
 
83
83
  insert = record_subparsers.add_parser("insert", help="新增记录")
84
84
  insert.add_argument("--app-key", required=True)
85
- insert.add_argument("--fields-file", required=True)
85
+ insert.add_argument("--fields-file", help=argparse.SUPPRESS)
86
+ insert.add_argument("--items-file")
86
87
  insert.add_argument("--verify-write", action=argparse.BooleanOptionalAction, default=True)
87
88
  insert.set_defaults(handler=_handle_insert, format_hint="")
88
89
 
@@ -274,6 +275,23 @@ def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
274
275
 
275
276
 
276
277
  def _handle_insert(args: argparse.Namespace, context: CliContext) -> dict:
278
+ if args.items_file:
279
+ if args.fields_file:
280
+ raise_config_error(
281
+ "record insert batch mode does not accept --fields-file.",
282
+ fix_hint="Use `record insert --app-key APP_KEY --items-file ITEMS.json` for batch inserts.",
283
+ )
284
+ return context.record.record_insert_public(
285
+ profile=args.profile,
286
+ app_key=args.app_key,
287
+ items=require_list_arg(args.items_file, option_name="--items-file"),
288
+ verify_write=bool(args.verify_write),
289
+ )
290
+ if not args.fields_file:
291
+ raise_config_error(
292
+ "record insert requires --items-file.",
293
+ fix_hint="Use `record insert --app-key APP_KEY --items-file ITEMS.json`; a single insert is one item in the JSON array.",
294
+ )
277
295
  return context.record.record_insert_public(
278
296
  profile=args.profile,
279
297
  app_key=args.app_key,
@@ -136,11 +136,8 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
136
136
  PublicToolSpec(BUILDER_DOMAIN, "app_release_edit_lock_if_mine", ("app_release_edit_lock_if_mine",), ("builder", "app", "release-edit-lock-if-mine"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
137
137
  PublicToolSpec(BUILDER_DOMAIN, "app_resolve", ("app_resolve",), ("builder", "app", "resolve"), has_contract=True, cli_show_effective_context=True),
138
138
  PublicToolSpec(BUILDER_DOMAIN, "button_style_catalog_get", ("button_style_catalog_get",), ("builder", "button", "catalog"), has_contract=True, cli_show_effective_context=True),
139
- PublicToolSpec(BUILDER_DOMAIN, "app_custom_button_list", ("app_custom_button_list",), ("builder", "button", "list"), has_contract=True, cli_show_effective_context=True),
140
- PublicToolSpec(BUILDER_DOMAIN, "app_custom_button_get", ("app_custom_button_get",), ("builder", "button", "get"), has_contract=True, cli_show_effective_context=True),
141
- PublicToolSpec(BUILDER_DOMAIN, "app_custom_button_create", ("app_custom_button_create",), ("builder", "button", "create"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
142
- PublicToolSpec(BUILDER_DOMAIN, "app_custom_button_update", ("app_custom_button_update",), ("builder", "button", "update"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
143
- PublicToolSpec(BUILDER_DOMAIN, "app_custom_button_delete", ("app_custom_button_delete",), ("builder", "button", "delete"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
139
+ PublicToolSpec(BUILDER_DOMAIN, "app_custom_buttons_apply", ("app_custom_buttons_apply",), ("builder", "button", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
140
+ PublicToolSpec(BUILDER_DOMAIN, "app_associated_resources_apply", ("app_associated_resources_apply",), ("builder", "associated-resource", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
144
141
  PublicToolSpec(BUILDER_DOMAIN, "app_get", ("app_get",), ("builder", "app", "get", "summary"), has_contract=True, cli_show_effective_context=True),
145
142
  PublicToolSpec(BUILDER_DOMAIN, "app_get_fields", ("app_get_fields",), ("builder", "app", "get", "fields"), has_contract=True, cli_show_effective_context=True),
146
143
  PublicToolSpec(BUILDER_DOMAIN, "app_repair_code_blocks", ("app_repair_code_blocks",), ("builder", "app", "repair-code-blocks"), has_contract=True, cli_show_effective_context=True),
@@ -57,6 +57,13 @@ def trim_public_response(tool_name: str | None, payload: dict[str, Any]) -> dict
57
57
  if not isinstance(payload, dict):
58
58
  return payload
59
59
  if _looks_like_failure_payload(payload):
60
+ status = str(payload.get("status") or "").lower()
61
+ if tool_name in {"user:record_insert", "user:record_update"} and status in {
62
+ "blocked",
63
+ "needs_confirmation",
64
+ "partial_success",
65
+ }:
66
+ return trim_success_response(tool_name, payload)
60
67
  return _trim_returned_failure(payload)
61
68
  return trim_success_response(tool_name, payload)
62
69
 
@@ -140,7 +147,7 @@ def _looks_like_failure_payload(payload: dict[str, Any]) -> bool:
140
147
  if payload.get("ok") is False:
141
148
  return True
142
149
  status = str(payload.get("status") or "").lower()
143
- return status in {"failed", "blocked", "verification_failed"}
150
+ return status in {"failed", "blocked"}
144
151
 
145
152
 
146
153
  def _trim_returned_failure(payload: dict[str, Any]) -> dict[str, Any]:
@@ -368,6 +375,9 @@ def _trim_record_write(payload: JSONObject) -> None:
368
375
  data = payload.get("data")
369
376
  if not isinstance(data, dict):
370
377
  return
378
+ if payload.get("mode") == "batch" or data.get("mode") == "batch":
379
+ _trim_record_write_batch(payload, data)
380
+ return
371
381
  data.pop("debug", None)
372
382
  data.pop("normalized_payload", None)
373
383
  data.pop("human_review", None)
@@ -397,6 +407,44 @@ def _trim_record_write(payload: JSONObject) -> None:
397
407
  data.pop(key, None)
398
408
 
399
409
 
410
+ def _trim_record_write_batch(payload: JSONObject, data: JSONObject) -> None:
411
+ data.pop("items", None)
412
+ data.pop("debug", None)
413
+ for key in ("summary", "created_record_ids", "app_key", "mode"):
414
+ if data.get(key) in (None, [], {}, ""):
415
+ data.pop(key, None)
416
+ items = payload.get("items")
417
+ if isinstance(items, list):
418
+ payload["items"] = [
419
+ _pick(
420
+ item,
421
+ (
422
+ "index",
423
+ "row_number",
424
+ "status",
425
+ "record_id",
426
+ "apply_id",
427
+ "write_executed",
428
+ "verification_status",
429
+ "safe_to_retry",
430
+ "failed_fields",
431
+ "confirmation_requests",
432
+ "blockers",
433
+ "error",
434
+ "warnings",
435
+ "resource",
436
+ "verification",
437
+ ),
438
+ )
439
+ for item in items
440
+ if isinstance(item, dict)
441
+ ]
442
+ for key in ("items", "created_record_ids"):
443
+ value = payload.get(key)
444
+ if value in (None, [], {}, ""):
445
+ payload.pop(key, None)
446
+
447
+
400
448
  def _trim_record_get(payload: JSONObject) -> None:
401
449
  if payload.get("fields") is not None or payload.get("semantic_context") is not None:
402
450
  _trim_detail_context_record_get(payload)
@@ -538,7 +586,7 @@ def _trim_detail_context_record_get(payload: JSONObject) -> None:
538
586
  associated_resources = payload.get("associated_resources")
539
587
  if isinstance(associated_resources, list):
540
588
  payload["associated_resources"] = [
541
- _pick(item, ("type", "name", "app_key", "app_name", "view_key", "chart_key", "view_type", "data_access"))
589
+ _pick(item, ("type", "resource_type", "name", "app_key", "app_name", "view_key", "chart_key", "view_type", "report_source", "data_access"))
542
590
  for item in associated_resources
543
591
  if isinstance(item, dict)
544
592
  ]
@@ -730,6 +778,19 @@ def _compact_schema_field(item: Any, *, template_map: dict[str, Any] | None) ->
730
778
  compact["required"] = bool(item.get("required"))
731
779
  if template_map is not None and isinstance(title, str) and title in template_map:
732
780
  compact["template"] = template_map.get(title)
781
+ for key in (
782
+ "format_hint",
783
+ "example_value",
784
+ "linkage",
785
+ "may_become_required",
786
+ "activation_sources",
787
+ "requirement_reason",
788
+ "accepts_natural_input",
789
+ "requires_upload",
790
+ ):
791
+ value = item.get(key)
792
+ if value not in (None, [], {}, ""):
793
+ compact[key] = value
733
794
  candidate_hint = item.get("candidate_hint")
734
795
  if isinstance(candidate_hint, dict):
735
796
  compact["candidate_hint"] = candidate_hint
@@ -996,11 +1057,8 @@ _register_policy(
996
1057
  "app_release_edit_lock_if_mine",
997
1058
  "app_resolve",
998
1059
  "button_style_catalog_get",
999
- "app_custom_button_list",
1000
- "app_custom_button_get",
1001
- "app_custom_button_create",
1002
- "app_custom_button_update",
1003
- "app_custom_button_delete",
1060
+ "app_custom_buttons_apply",
1061
+ "app_associated_resources_apply",
1004
1062
  "app_get_fields",
1005
1063
  "app_repair_code_blocks",
1006
1064
  "app_get_layout",
@@ -104,7 +104,7 @@ Analysis answers must include concrete numbers. When applicable, include percent
104
104
  ## Record CRUD Path
105
105
 
106
106
  `app_get -> record_browse_schema_get(view_id=...) -> record_list / record_get`
107
- `record_insert_schema_get -> record_insert`
107
+ `record_insert_schema_get -> record_insert(items)`
108
108
  `record_update_schema_get -> record_update`
109
109
  `record_list / record_get -> record_delete`
110
110
  `record_code_block_schema_get -> record_code_block_run`
@@ -115,7 +115,7 @@ Analysis answers must include concrete numbers. When applicable, include percent
115
115
  - Use `order_by` items as `{{field_id, direction}}`
116
116
  - Legacy forms such as bare integer `field_id`, `fieldId`, `operator`, `values`, or `order` may still parse, but they are compatibility-only and not the canonical DSL
117
117
 
118
- - `record_insert` uses an applicant-node `fields` map keyed by field title.
118
+ - `record_insert` defaults to an applicant-node `items` array; each item contains a field-title keyed `fields` map. A single insert is one item.
119
119
  - `record_update` uses a field-title keyed `fields` map and internally selects the first accessible view that can execute the current payload.
120
120
  - For insert, `runtime_linked_required_fields` means required-but-not-directly-writable fields that are usually supplied by runtime linkage or upstream context.
121
121
  - For insert, fields marked `may_become_required=true` stay in `optional_fields`; they are still directly writable, but linked visibility or option-driven rules can make them required at runtime.
@@ -130,7 +130,8 @@ Analysis answers must include concrete numbers. When applicable, include percent
130
130
  - When readback shape matters after insert or update, prefer `record_get` for human/detail-page context, or `record_list(..., output_profile="normalized")` for batch-shaped normalized rows.
131
131
 
132
132
  - Read relation targets from `record_insert_schema_get` / `record_update_schema_get` relation metadata before preparing relation writes.
133
- - Member and department fields may be written with natural strings directly on `record_insert` / `record_update`; only fall back to `record_member_candidates` or `record_department_candidates` when the user wants explicit candidate browsing or the write returns ambiguity that needs confirmation.
133
+ - Member, department, and relation fields may be written with natural strings directly on `record_insert` / `record_update`; only fall back to candidate tools when the user wants explicit candidate browsing or the write returns ambiguity that needs confirmation.
134
+ - For batch insert `partial_success`, read `created_record_ids`, failed `items[].row_number`, and `failed_fields`; repair only failed rows and never retry the whole batch after any row has `write_executed=true`.
134
135
  - If explicit candidate browsing is needed for default-all member or department fields, prefer those field candidate tools instead of starting with `directory_*`.
135
136
 
136
137
  ## Code Block Path
@@ -36,9 +36,13 @@ def build_builder_server() -> FastMCP:
36
36
  "Use builder_tool_contract when you need a machine-readable contract, aliases, allowed enums, or a minimal valid example for a public builder tool. "
37
37
  "Use solution_install when the user explicitly wants to install a packaged solution/template by solution_key, optionally copying bundled demo data. "
38
38
  "If creating or updating an app package may be appropriate, use package_apply with explicit user intent; otherwise use package_get and app_resolve to locate resources, "
39
- "app_get/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 configuration reads, "
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
- "then app_schema_apply/app_layout_apply/app_flow_apply/app_views_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, 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, and flow should use publish=false whenever you only want draft/precheck behavior. "
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
+ "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. "
43
+ "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 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. 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. "
45
+ "For associated reports/views, use app_associated_resources_apply for both the app-level associated_resources pool and per-view display config; associated_item_id is the app-level form_asos_chart.id, not chart_id/chart_key. Do not ask agents to pass backend raw sourceType: views infer the internal Qingflow view source, reports default to BI app reports, and dataset reports use report_source=dataset. "
42
46
  "For code_block fields with output bindings, always use qf_output assignment rather than const/let qf_output, and use app_repair_code_blocks when an existing form hangs because output-bound fields stay loading. "
43
47
  "Use package_apply to manage package metadata, visibility, grouping, and ordering, and app_publish_verify for explicit final publish verification. "
44
48
  "For workflow edits, keep the public builder surface on stable linear flows only: start/approve/fill/copy/webhook/end. Branch and condition nodes are intentionally disabled because the backend workflow route is not front-end stable for those node types. Declare node assignees and editable fields explicitly. "
@@ -295,29 +299,38 @@ def build_builder_server() -> FastMCP:
295
299
  return ai_builder.button_style_catalog_get(profile=profile)
296
300
 
297
301
  @server.tool()
298
- def app_custom_button_list(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
299
- return ai_builder.app_custom_button_list(profile=profile, app_key=app_key)
300
-
301
- @server.tool()
302
- def app_custom_button_get(profile: str = DEFAULT_PROFILE, app_key: str = "", button_id: int = 0) -> dict:
303
- return ai_builder.app_custom_button_get(profile=profile, app_key=app_key, button_id=button_id)
304
-
305
- @server.tool()
306
- def app_custom_button_create(profile: str = DEFAULT_PROFILE, app_key: str = "", payload: dict | None = None) -> dict:
307
- return ai_builder.app_custom_button_create(profile=profile, app_key=app_key, payload=payload or {})
308
-
309
- @server.tool()
310
- def app_custom_button_update(
302
+ def app_custom_buttons_apply(
311
303
  profile: str = DEFAULT_PROFILE,
312
304
  app_key: str = "",
313
- button_id: int = 0,
314
- payload: dict | None = None,
305
+ upsert_buttons: list[dict] | None = None,
306
+ remove_buttons: list[dict] | None = None,
307
+ view_configs: list[dict] | None = None,
315
308
  ) -> dict:
316
- return ai_builder.app_custom_button_update(profile=profile, app_key=app_key, button_id=button_id, payload=payload or {})
309
+ return ai_builder.app_custom_buttons_apply(
310
+ profile=profile,
311
+ app_key=app_key,
312
+ upsert_buttons=upsert_buttons or [],
313
+ remove_buttons=remove_buttons or [],
314
+ view_configs=view_configs or [],
315
+ )
317
316
 
318
317
  @server.tool()
319
- def app_custom_button_delete(profile: str = DEFAULT_PROFILE, app_key: str = "", button_id: int = 0) -> dict:
320
- return ai_builder.app_custom_button_delete(profile=profile, app_key=app_key, button_id=button_id)
318
+ def app_associated_resources_apply(
319
+ profile: str = DEFAULT_PROFILE,
320
+ app_key: str = "",
321
+ upsert_resources: list[dict] | None = None,
322
+ remove_associated_item_ids: list[int] | None = None,
323
+ reorder_associated_item_ids: list[int] | None = None,
324
+ view_configs: list[dict] | None = None,
325
+ ) -> dict:
326
+ return ai_builder.app_associated_resources_apply(
327
+ profile=profile,
328
+ app_key=app_key,
329
+ upsert_resources=upsert_resources or [],
330
+ remove_associated_item_ids=remove_associated_item_ids or [],
331
+ reorder_associated_item_ids=reorder_associated_item_ids or [],
332
+ view_configs=view_configs or [],
333
+ )
321
334
 
322
335
  @server.tool()
323
336
  def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
@@ -103,7 +103,7 @@ Analysis answers must include concrete numbers. When applicable, include percent
103
103
  ## Record CRUD Path
104
104
 
105
105
  `app_get -> record_browse_schema_get(view_id=...) -> record_list / record_get`
106
- `record_insert_schema_get -> record_insert`
106
+ `record_insert_schema_get -> record_insert(items)`
107
107
  `record_update_schema_get -> record_update`
108
108
  `record_list / record_get -> record_delete`
109
109
  `record_code_block_schema_get -> record_code_block_run`
@@ -116,7 +116,7 @@ Analysis answers must include concrete numbers. When applicable, include percent
116
116
  - Use `order_by` items as `{{field_id, direction}}`
117
117
  - Legacy forms such as bare integer `field_id`, `fieldId`, `operator`, `values`, or `order` may still parse, but they are compatibility-only and not the canonical DSL
118
118
 
119
- - `record_insert` uses an applicant-node `fields` map keyed by field title.
119
+ - `record_insert` defaults to an applicant-node `items` array; each item contains a field-title keyed `fields` map. A single insert is one item.
120
120
  - `record_update` uses a field-title keyed `fields` map and internally selects the first accessible view that can execute the current payload.
121
121
  - For insert, `runtime_linked_required_fields` means required-but-not-directly-writable fields that are usually supplied by runtime linkage or upstream context.
122
122
  - For insert, fields marked `may_become_required=true` stay in `optional_fields`; they are still directly writable, but linked visibility or option-driven rules can make them required at runtime.
@@ -131,7 +131,8 @@ Analysis answers must include concrete numbers. When applicable, include percent
131
131
  - When readback shape matters after insert or update, prefer `record_get` for human/detail-page context, or `record_list(..., output_profile="normalized")` for batch-shaped normalized rows.
132
132
 
133
133
  - Read relation targets from `record_insert_schema_get` / `record_update_schema_get` relation metadata before preparing relation writes.
134
- - Member and department fields may be written with natural strings directly on `record_insert` / `record_update`; only fall back to `record_member_candidates` or `record_department_candidates` when the user wants explicit candidate browsing or the write returns ambiguity that needs confirmation.
134
+ - Member, department, and relation fields may be written with natural strings directly on `record_insert` / `record_update`; only fall back to candidate tools when the user wants explicit candidate browsing or the write returns ambiguity that needs confirmation.
135
+ - For batch insert `partial_success`, read `created_record_ids`, failed `items[].row_number`, and `failed_fields`; repair only failed rows and never retry the whole batch after any row has `write_executed=true`.
135
136
  - When candidate browsing must match a real update/write scope, pass `record_id`, `workflow_node_id`, and any pending `fields` context to the candidate tool; otherwise the candidate result is only a static applicant-node preview.
136
137
  - If explicit candidate browsing is needed for default-all member or department fields, prefer those field candidate tools instead of starting with `directory_*`.
137
138