@qingflow-tech/qingflow-app-builder-mcp 1.0.6 → 1.0.8

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,24 @@ 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("--patch-buttons-file")
130
+ button_apply.add_argument("--remove-buttons-file")
131
+ button_apply.add_argument("--view-configs-file")
132
+ button_apply.set_defaults(handler=_handle_button_apply, format_hint="builder_summary")
133
+
134
+ associated_resource = builder_subparsers.add_parser("associated-resource", aliases=["associated-resources"], help="关联视图/报表")
135
+ associated_resource_subparsers = associated_resource.add_subparsers(dest="builder_associated_resource_command", required=True)
136
+ associated_resource_apply = associated_resource_subparsers.add_parser("apply", help="声明式管理应用关联资源池和视图展示配置")
137
+ associated_resource_apply.add_argument("--app-key", required=True)
138
+ associated_resource_apply.add_argument("--upsert-resources-file")
139
+ associated_resource_apply.add_argument("--patch-resources-file")
140
+ associated_resource_apply.add_argument("--remove-associated-item-ids-file")
141
+ associated_resource_apply.add_argument("--reorder-associated-item-ids-file")
142
+ associated_resource_apply.add_argument("--view-configs-file")
143
+ associated_resource_apply.set_defaults(handler=_handle_associated_resource_apply, format_hint="builder_summary")
150
144
 
151
145
  portal = builder_subparsers.add_parser("portal", help="门户")
152
146
  portal_subparsers = portal.add_subparsers(dest="builder_portal_command", required=True)
@@ -185,8 +179,8 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
185
179
  schema_apply_apply.add_argument("--visibility-file")
186
180
  schema_apply_apply.add_argument("--create-if-missing", action="store_true")
187
181
  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")
182
+ schema_apply_apply.add_argument("--add-fields-file", help="字段 JSON 数组;字段可用 as_data_title/as_data_cover 标记数据标题/封面")
183
+ schema_apply_apply.add_argument("--update-fields-file", help="字段更新 JSON 数组;set 内可用 as_data_title/as_data_cover 标记数据标题/封面")
190
184
  schema_apply_apply.add_argument("--remove-fields-file")
191
185
  schema_apply_apply.set_defaults(handler=_handle_schema_apply, format_hint="builder_summary")
192
186
 
@@ -205,6 +199,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
205
199
  views_apply_apply.add_argument("--app-key", required=True)
206
200
  views_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
207
201
  views_apply_apply.add_argument("--upsert-views-file")
202
+ views_apply_apply.add_argument("--patch-views-file")
208
203
  views_apply_apply.add_argument("--remove-views-file")
209
204
  views_apply_apply.set_defaults(handler=_handle_views_apply, format_hint="builder_summary")
210
205
 
@@ -223,6 +218,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
223
218
  charts_apply_apply = charts_apply_subparsers.add_parser("apply", help="执行报表变更")
224
219
  charts_apply_apply.add_argument("--app-key", required=True)
225
220
  charts_apply_apply.add_argument("--upsert-file")
221
+ charts_apply_apply.add_argument("--patch-file")
226
222
  charts_apply_apply.add_argument("--remove-chart-ids-file")
227
223
  charts_apply_apply.add_argument("--reorder-chart-ids-file")
228
224
  charts_apply_apply.set_defaults(handler=_handle_charts_apply, format_hint="builder_summary")
@@ -369,6 +365,29 @@ def _handle_button_catalog(args: argparse.Namespace, context: CliContext) -> dic
369
365
  return context.builder.button_style_catalog_get(profile=args.profile)
370
366
 
371
367
 
368
+ def _handle_button_apply(args: argparse.Namespace, context: CliContext) -> dict:
369
+ return context.builder.app_custom_buttons_apply(
370
+ profile=args.profile,
371
+ app_key=args.app_key,
372
+ upsert_buttons=load_list_arg(args.upsert_buttons_file, option_name="--upsert-buttons-file"),
373
+ patch_buttons=load_list_arg(args.patch_buttons_file, option_name="--patch-buttons-file"),
374
+ remove_buttons=load_list_arg(args.remove_buttons_file, option_name="--remove-buttons-file"),
375
+ view_configs=load_list_arg(args.view_configs_file, option_name="--view-configs-file"),
376
+ )
377
+
378
+
379
+ def _handle_associated_resource_apply(args: argparse.Namespace, context: CliContext) -> dict:
380
+ return context.builder.app_associated_resources_apply(
381
+ profile=args.profile,
382
+ app_key=args.app_key,
383
+ upsert_resources=load_list_arg(args.upsert_resources_file, option_name="--upsert-resources-file"),
384
+ patch_resources=load_list_arg(args.patch_resources_file, option_name="--patch-resources-file"),
385
+ remove_associated_item_ids=load_list_arg(args.remove_associated_item_ids_file, option_name="--remove-associated-item-ids-file"),
386
+ reorder_associated_item_ids=load_list_arg(args.reorder_associated_item_ids_file, option_name="--reorder-associated-item-ids-file"),
387
+ view_configs=load_list_arg(args.view_configs_file, option_name="--view-configs-file"),
388
+ )
389
+
390
+
372
391
  def _handle_button_get(args: argparse.Namespace, context: CliContext) -> dict:
373
392
  return context.builder.app_custom_button_get(profile=args.profile, app_key=args.app_key, button_id=args.button_id)
374
393
 
@@ -481,6 +500,7 @@ def _handle_views_apply(args: argparse.Namespace, context: CliContext) -> dict:
481
500
  app_key=args.app_key,
482
501
  publish=bool(args.publish),
483
502
  upsert_views=load_list_arg(args.upsert_views_file, option_name="--upsert-views-file"),
503
+ patch_views=load_list_arg(args.patch_views_file, option_name="--patch-views-file"),
484
504
  remove_views=load_list_arg(args.remove_views_file, option_name="--remove-views-file"),
485
505
  )
486
506
 
@@ -501,6 +521,7 @@ def _handle_charts_apply(args: argparse.Namespace, context: CliContext) -> dict:
501
521
  profile=args.profile,
502
522
  app_key=args.app_key,
503
523
  upsert_charts=load_list_arg(args.upsert_file, option_name="--upsert-file"),
524
+ patch_charts=load_list_arg(args.patch_file, option_name="--patch-file"),
504
525
  remove_chart_ids=load_list_arg(args.remove_chart_ids_file, option_name="--remove-chart-ids-file"),
505
526
  reorder_chart_ids=load_list_arg(args.reorder_chart_ids_file, option_name="--reorder-chart-ids-file"),
506
527
  )
@@ -13,7 +13,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
13
13
  record_subparsers = parser.add_subparsers(
14
14
  dest="record_command",
15
15
  required=True,
16
- metavar="{schema,list,access,get,insert,update,delete,code-block-run}",
16
+ metavar="{schema,list,access,get,insert,update,delete,member-candidates,department-candidates,code-block-run}",
17
17
  )
18
18
 
19
19
  schema = record_subparsers.add_parser("schema", help="读取记录相关表结构")
@@ -47,6 +47,28 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
47
47
  schema_code_block.add_argument("--app-key", required=True)
48
48
  schema_code_block.set_defaults(handler=_handle_schema_code_block, format_hint="")
49
49
 
50
+ member_candidates = record_subparsers.add_parser("member-candidates", help="读取成员字段候选项")
51
+ member_candidates.add_argument("--app-key", required=True)
52
+ member_candidates.add_argument("--field-id", type=int, required=True)
53
+ member_candidates.add_argument("--keyword", default="")
54
+ member_candidates.add_argument("--page-num", type=int, default=1)
55
+ member_candidates.add_argument("--page-size", type=int, default=20)
56
+ member_candidates.add_argument("--record-id")
57
+ member_candidates.add_argument("--workflow-node-id", type=int)
58
+ member_candidates.add_argument("--fields-file")
59
+ member_candidates.set_defaults(handler=_handle_member_candidates, format_hint="")
60
+
61
+ department_candidates = record_subparsers.add_parser("department-candidates", help="读取部门字段候选项")
62
+ department_candidates.add_argument("--app-key", required=True)
63
+ department_candidates.add_argument("--field-id", type=int, required=True)
64
+ department_candidates.add_argument("--keyword", default="")
65
+ department_candidates.add_argument("--page-num", type=int, default=1)
66
+ department_candidates.add_argument("--page-size", type=int, default=20)
67
+ department_candidates.add_argument("--record-id")
68
+ department_candidates.add_argument("--workflow-node-id", type=int)
69
+ department_candidates.add_argument("--fields-file")
70
+ department_candidates.set_defaults(handler=_handle_department_candidates, format_hint="")
71
+
50
72
  list_parser = record_subparsers.add_parser("list", help="列出记录")
51
73
  list_parser.add_argument("--app-key", required=True)
52
74
  list_parser.add_argument("--column", dest="columns", action="append", type=int, default=[])
@@ -82,7 +104,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
82
104
 
83
105
  insert = record_subparsers.add_parser("insert", help="新增记录")
84
106
  insert.add_argument("--app-key", required=True)
85
- insert.add_argument("--fields-file")
107
+ insert.add_argument("--fields-file", help=argparse.SUPPRESS)
86
108
  insert.add_argument("--items-file")
87
109
  insert.add_argument("--verify-write", action=argparse.BooleanOptionalAction, default=True)
88
110
  insert.set_defaults(handler=_handle_insert, format_hint="")
@@ -207,6 +229,38 @@ def _handle_schema_code_block(args: argparse.Namespace, context: CliContext) ->
207
229
  return context.code_block.record_code_block_schema_get_public(profile=args.profile, app_key=args.app_key)
208
230
 
209
231
 
232
+ def _candidate_context_fields(args: argparse.Namespace) -> dict[str, Any]:
233
+ return load_object_arg(args.fields_file, option_name="--fields-file") or {}
234
+
235
+
236
+ def _handle_member_candidates(args: argparse.Namespace, context: CliContext) -> dict:
237
+ return context.record.record_member_candidates(
238
+ profile=args.profile,
239
+ app_key=args.app_key,
240
+ field_id=args.field_id,
241
+ record_id=args.record_id,
242
+ workflow_node_id=args.workflow_node_id,
243
+ fields=_candidate_context_fields(args),
244
+ keyword=args.keyword,
245
+ page_num=args.page_num,
246
+ page_size=args.page_size,
247
+ )
248
+
249
+
250
+ def _handle_department_candidates(args: argparse.Namespace, context: CliContext) -> dict:
251
+ return context.record.record_department_candidates(
252
+ profile=args.profile,
253
+ app_key=args.app_key,
254
+ field_id=args.field_id,
255
+ record_id=args.record_id,
256
+ workflow_node_id=args.workflow_node_id,
257
+ fields=_candidate_context_fields(args),
258
+ keyword=args.keyword,
259
+ page_num=args.page_num,
260
+ page_size=args.page_size,
261
+ )
262
+
263
+
210
264
  def _validate_public_view_selector(
211
265
  *,
212
266
  view_id: str | None,
@@ -289,8 +343,8 @@ def _handle_insert(args: argparse.Namespace, context: CliContext) -> dict:
289
343
  )
290
344
  if not args.fields_file:
291
345
  raise_config_error(
292
- "record insert requires --items-file or --fields-file.",
293
- fix_hint="Prefer `record insert --app-key APP_KEY --items-file ITEMS.json`; use --fields-file only for legacy single inserts.",
346
+ "record insert requires --items-file.",
347
+ fix_hint="Use `record insert --app-key APP_KEY --items-file ITEMS.json`; a single insert is one item in the JSON array.",
294
348
  )
295
349
  return context.record.record_insert_public(
296
350
  profile=args.profile,
@@ -78,8 +78,8 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
78
78
  ("record_code_block_schema_get_public",),
79
79
  ("record", "schema", "code-block"),
80
80
  ),
81
- PublicToolSpec(USER_DOMAIN, "record_member_candidates", ("record_member_candidates",), cli_public=False),
82
- PublicToolSpec(USER_DOMAIN, "record_department_candidates", ("record_department_candidates",), cli_public=False),
81
+ PublicToolSpec(USER_DOMAIN, "record_member_candidates", ("record_member_candidates",), ("record", "member-candidates")),
82
+ PublicToolSpec(USER_DOMAIN, "record_department_candidates", ("record_department_candidates",), ("record", "department-candidates")),
83
83
  PublicToolSpec(USER_DOMAIN, "record_analyze", ("record_analyze",), ("record", "analyze"), mcp_public=False),
84
84
  PublicToolSpec(USER_DOMAIN, "record_list", ("record_list",), ("record", "list"), cli_show_effective_context=True),
85
85
  PublicToolSpec(USER_DOMAIN, "record_access", ("record_access",), ("record", "access"), cli_show_effective_context=True),
@@ -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),
@@ -91,7 +91,12 @@ def trim_error_response(payload: dict[str, Any]) -> dict[str, Any]:
91
91
  _drop_deep_keys(trimmed.get("details"), {"request_route", "base_url", "normalized_args", "suggested_next_call", "transport", "response", "body", "raw"})
92
92
  details = trimmed.get("details")
93
93
  if isinstance(details, dict):
94
+ preserved = {}
95
+ for key in ("blocking_issues", "compiled_match_rules"):
96
+ if key in details:
97
+ preserved[key] = details.get(key)
94
98
  compact_details = _compact_scalar_dict(details)
99
+ compact_details.update(preserved)
95
100
  if compact_details:
96
101
  trimmed["details"] = compact_details
97
102
  else:
@@ -156,7 +161,12 @@ def _trim_returned_failure(payload: dict[str, Any]) -> dict[str, Any]:
156
161
  _drop_deep_keys(trimmed.get("details"), {"request_route", "base_url", "normalized_args", "suggested_next_call", "transport", "response", "body", "raw"})
157
162
  details = trimmed.get("details")
158
163
  if isinstance(details, dict):
164
+ preserved = {}
165
+ for key in ("blocking_issues", "compiled_match_rules"):
166
+ if key in details:
167
+ preserved[key] = details.get(key)
159
168
  compact_details = _compact_scalar_dict(details)
169
+ compact_details.update(preserved)
160
170
  if compact_details:
161
171
  trimmed["details"] = compact_details
162
172
  else:
@@ -586,7 +596,7 @@ def _trim_detail_context_record_get(payload: JSONObject) -> None:
586
596
  associated_resources = payload.get("associated_resources")
587
597
  if isinstance(associated_resources, list):
588
598
  payload["associated_resources"] = [
589
- _pick(item, ("type", "name", "app_key", "app_name", "view_key", "chart_key", "view_type", "data_access"))
599
+ _pick(item, ("type", "resource_type", "name", "app_key", "app_name", "view_key", "chart_key", "view_type", "report_source", "data_access"))
590
600
  for item in associated_resources
591
601
  if isinstance(item, dict)
592
602
  ]
@@ -944,7 +954,11 @@ def _trim_builder_envelope(payload: JSONObject) -> None:
944
954
  details = payload.get("details")
945
955
  if isinstance(details, dict):
946
956
  _drop_deep_keys(details, {"request_route", "base_url", "normalized_args", "suggested_next_call", "transport", "response", "body", "raw"})
957
+ preserved = {}
958
+ if isinstance(details.get("compiled_match_rules"), dict):
959
+ preserved["compiled_match_rules"] = details.get("compiled_match_rules")
947
960
  compact = _compact_scalar_dict(details)
961
+ compact.update(preserved)
948
962
  if compact:
949
963
  payload["details"] = compact
950
964
  else:
@@ -1057,11 +1071,8 @@ _register_policy(
1057
1071
  "app_release_edit_lock_if_mine",
1058
1072
  "app_resolve",
1059
1073
  "button_style_catalog_get",
1060
- "app_custom_button_list",
1061
- "app_custom_button_get",
1062
- "app_custom_button_create",
1063
- "app_custom_button_update",
1064
- "app_custom_button_delete",
1074
+ "app_custom_buttons_apply",
1075
+ "app_associated_resources_apply",
1065
1076
  "app_get_fields",
1066
1077
  "app_repair_code_blocks",
1067
1078
  "app_get_layout",
@@ -131,6 +131,7 @@ Analysis answers must include concrete numbers. When applicable, include percent
131
131
 
132
132
  - Read relation targets from `record_insert_schema_get` / `record_update_schema_get` relation metadata before preparing relation writes.
133
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
+ - CLI-only agents can use `qingflow record member-candidates --app-key APP_KEY --field-id FIELD_ID --keyword NAME --json` or `qingflow record department-candidates --app-key APP_KEY --field-id FIELD_ID --keyword DEPT --json` for that fallback; pass `--record-id`, `--workflow-node-id`, and `--fields-file` only when the candidate scope must match an existing runtime context.
134
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
  - If explicit candidate browsing is needed for default-all member or department fields, prefer those field candidate tools instead of starting with `directory_*`.
136
137
 
@@ -36,9 +36,15 @@ 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 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. "
45
+ "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
+ "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. "
47
+ "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. Before creating an associated resource, read app_get.associated_resources and reuse an existing matching target_app_key + view_key/chart_key through patch_resources; client_key only works inside one apply call and is not persisted. 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
48
  "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
49
  "Use package_apply to manage package metadata, visibility, grouping, and ordering, and app_publish_verify for explicit final publish verification. "
44
50
  "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 +301,42 @@ def build_builder_server() -> FastMCP:
295
301
  return ai_builder.button_style_catalog_get(profile=profile)
296
302
 
297
303
  @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(
304
+ def app_custom_buttons_apply(
311
305
  profile: str = DEFAULT_PROFILE,
312
306
  app_key: str = "",
313
- button_id: int = 0,
314
- payload: dict | None = None,
307
+ upsert_buttons: list[dict] | None = None,
308
+ patch_buttons: list[dict] | None = None,
309
+ remove_buttons: list[dict] | None = None,
310
+ view_configs: list[dict] | None = None,
315
311
  ) -> dict:
316
- return ai_builder.app_custom_button_update(profile=profile, app_key=app_key, button_id=button_id, payload=payload or {})
312
+ return ai_builder.app_custom_buttons_apply(
313
+ profile=profile,
314
+ app_key=app_key,
315
+ upsert_buttons=upsert_buttons or [],
316
+ patch_buttons=patch_buttons or [],
317
+ remove_buttons=remove_buttons or [],
318
+ view_configs=view_configs or [],
319
+ )
317
320
 
318
321
  @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)
322
+ def app_associated_resources_apply(
323
+ profile: str = DEFAULT_PROFILE,
324
+ app_key: str = "",
325
+ upsert_resources: list[dict] | None = None,
326
+ patch_resources: list[dict] | None = None,
327
+ remove_associated_item_ids: list[int] | None = None,
328
+ reorder_associated_item_ids: list[int] | None = None,
329
+ view_configs: list[dict] | None = None,
330
+ ) -> dict:
331
+ return ai_builder.app_associated_resources_apply(
332
+ profile=profile,
333
+ app_key=app_key,
334
+ upsert_resources=upsert_resources or [],
335
+ patch_resources=patch_resources or [],
336
+ remove_associated_item_ids=remove_associated_item_ids or [],
337
+ reorder_associated_item_ids=reorder_associated_item_ids or [],
338
+ view_configs=view_configs or [],
339
+ )
321
340
 
322
341
  @server.tool()
323
342
  def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
@@ -453,6 +472,7 @@ def build_builder_server() -> FastMCP:
453
472
  app_key: str = "",
454
473
  publish: bool = True,
455
474
  upsert_views: list[dict] | None = None,
475
+ patch_views: list[dict] | None = None,
456
476
  remove_views: list[str] | None = None,
457
477
  ) -> dict:
458
478
  return ai_builder.app_views_apply(
@@ -460,6 +480,7 @@ def build_builder_server() -> FastMCP:
460
480
  app_key=app_key,
461
481
  publish=publish,
462
482
  upsert_views=upsert_views or [],
483
+ patch_views=patch_views or [],
463
484
  remove_views=remove_views or [],
464
485
  )
465
486
 
@@ -468,6 +489,7 @@ def build_builder_server() -> FastMCP:
468
489
  profile: str = DEFAULT_PROFILE,
469
490
  app_key: str = "",
470
491
  upsert_charts: list[dict] | None = None,
492
+ patch_charts: list[dict] | None = None,
471
493
  remove_chart_ids: list[str] | None = None,
472
494
  reorder_chart_ids: list[str] | None = None,
473
495
  ) -> dict:
@@ -475,6 +497,7 @@ def build_builder_server() -> FastMCP:
475
497
  profile=profile,
476
498
  app_key=app_key,
477
499
  upsert_charts=upsert_charts or [],
500
+ patch_charts=patch_charts or [],
478
501
  remove_chart_ids=remove_chart_ids or [],
479
502
  reorder_chart_ids=reorder_chart_ids or [],
480
503
  )
@@ -132,6 +132,7 @@ Analysis answers must include concrete numbers. When applicable, include percent
132
132
 
133
133
  - Read relation targets from `record_insert_schema_get` / `record_update_schema_get` relation metadata before preparing relation writes.
134
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
+ - CLI-only agents can use `qingflow record member-candidates --app-key APP_KEY --field-id FIELD_ID --keyword NAME --json` or `qingflow record department-candidates --app-key APP_KEY --field-id FIELD_ID --keyword DEPT --json` for that fallback.
135
136
  - 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`.
136
137
  - 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.
137
138
  - If explicit candidate browsing is needed for default-all member or department fields, prefer those field candidate tools instead of starting with `directory_*`.