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

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.
@@ -126,6 +126,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
126
126
  button_apply = button_subparsers.add_parser("apply", help="声明式创建、更新或删除自定义按钮")
127
127
  button_apply.add_argument("--app-key", required=True)
128
128
  button_apply.add_argument("--upsert-buttons-file")
129
+ button_apply.add_argument("--patch-buttons-file")
129
130
  button_apply.add_argument("--remove-buttons-file")
130
131
  button_apply.add_argument("--view-configs-file")
131
132
  button_apply.set_defaults(handler=_handle_button_apply, format_hint="builder_summary")
@@ -135,6 +136,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
135
136
  associated_resource_apply = associated_resource_subparsers.add_parser("apply", help="声明式管理应用关联资源池和视图展示配置")
136
137
  associated_resource_apply.add_argument("--app-key", required=True)
137
138
  associated_resource_apply.add_argument("--upsert-resources-file")
139
+ associated_resource_apply.add_argument("--patch-resources-file")
138
140
  associated_resource_apply.add_argument("--remove-associated-item-ids-file")
139
141
  associated_resource_apply.add_argument("--reorder-associated-item-ids-file")
140
142
  associated_resource_apply.add_argument("--view-configs-file")
@@ -197,6 +199,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
197
199
  views_apply_apply.add_argument("--app-key", required=True)
198
200
  views_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
199
201
  views_apply_apply.add_argument("--upsert-views-file")
202
+ views_apply_apply.add_argument("--patch-views-file")
200
203
  views_apply_apply.add_argument("--remove-views-file")
201
204
  views_apply_apply.set_defaults(handler=_handle_views_apply, format_hint="builder_summary")
202
205
 
@@ -215,6 +218,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
215
218
  charts_apply_apply = charts_apply_subparsers.add_parser("apply", help="执行报表变更")
216
219
  charts_apply_apply.add_argument("--app-key", required=True)
217
220
  charts_apply_apply.add_argument("--upsert-file")
221
+ charts_apply_apply.add_argument("--patch-file")
218
222
  charts_apply_apply.add_argument("--remove-chart-ids-file")
219
223
  charts_apply_apply.add_argument("--reorder-chart-ids-file")
220
224
  charts_apply_apply.set_defaults(handler=_handle_charts_apply, format_hint="builder_summary")
@@ -366,6 +370,7 @@ def _handle_button_apply(args: argparse.Namespace, context: CliContext) -> dict:
366
370
  profile=args.profile,
367
371
  app_key=args.app_key,
368
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"),
369
374
  remove_buttons=load_list_arg(args.remove_buttons_file, option_name="--remove-buttons-file"),
370
375
  view_configs=load_list_arg(args.view_configs_file, option_name="--view-configs-file"),
371
376
  )
@@ -376,6 +381,7 @@ def _handle_associated_resource_apply(args: argparse.Namespace, context: CliCont
376
381
  profile=args.profile,
377
382
  app_key=args.app_key,
378
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"),
379
385
  remove_associated_item_ids=load_list_arg(args.remove_associated_item_ids_file, option_name="--remove-associated-item-ids-file"),
380
386
  reorder_associated_item_ids=load_list_arg(args.reorder_associated_item_ids_file, option_name="--reorder-associated-item-ids-file"),
381
387
  view_configs=load_list_arg(args.view_configs_file, option_name="--view-configs-file"),
@@ -494,6 +500,7 @@ def _handle_views_apply(args: argparse.Namespace, context: CliContext) -> dict:
494
500
  app_key=args.app_key,
495
501
  publish=bool(args.publish),
496
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"),
497
504
  remove_views=load_list_arg(args.remove_views_file, option_name="--remove-views-file"),
498
505
  )
499
506
 
@@ -514,6 +521,7 @@ def _handle_charts_apply(args: argparse.Namespace, context: CliContext) -> dict:
514
521
  profile=args.profile,
515
522
  app_key=args.app_key,
516
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"),
517
525
  remove_chart_ids=load_list_arg(args.remove_chart_ids_file, option_name="--remove-chart-ids-file"),
518
526
  reorder_chart_ids=load_list_arg(args.reorder_chart_ids_file, option_name="--reorder-chart-ids-file"),
519
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=[])
@@ -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,
@@ -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),
@@ -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:
@@ -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:
@@ -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
 
@@ -39,10 +39,13 @@ 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
+ "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. "
42
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. "
43
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 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. "
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 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
+ "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. "
48
+ "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, and view_configs/remove/reorder may also pass an existing resource's chart_id/chart_key/view_key because the tool resolves those to the internal id. 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. "
46
49
  "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. "
47
50
  "Use package_apply to manage package metadata, visibility, grouping, and ordering, and app_publish_verify for explicit final publish verification. "
48
51
  "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. "
@@ -303,6 +306,7 @@ def build_builder_server() -> FastMCP:
303
306
  profile: str = DEFAULT_PROFILE,
304
307
  app_key: str = "",
305
308
  upsert_buttons: list[dict] | None = None,
309
+ patch_buttons: list[dict] | None = None,
306
310
  remove_buttons: list[dict] | None = None,
307
311
  view_configs: list[dict] | None = None,
308
312
  ) -> dict:
@@ -310,6 +314,7 @@ def build_builder_server() -> FastMCP:
310
314
  profile=profile,
311
315
  app_key=app_key,
312
316
  upsert_buttons=upsert_buttons or [],
317
+ patch_buttons=patch_buttons or [],
313
318
  remove_buttons=remove_buttons or [],
314
319
  view_configs=view_configs or [],
315
320
  )
@@ -319,6 +324,7 @@ def build_builder_server() -> FastMCP:
319
324
  profile: str = DEFAULT_PROFILE,
320
325
  app_key: str = "",
321
326
  upsert_resources: list[dict] | None = None,
327
+ patch_resources: list[dict] | None = None,
322
328
  remove_associated_item_ids: list[int] | None = None,
323
329
  reorder_associated_item_ids: list[int] | None = None,
324
330
  view_configs: list[dict] | None = None,
@@ -327,6 +333,7 @@ def build_builder_server() -> FastMCP:
327
333
  profile=profile,
328
334
  app_key=app_key,
329
335
  upsert_resources=upsert_resources or [],
336
+ patch_resources=patch_resources or [],
330
337
  remove_associated_item_ids=remove_associated_item_ids or [],
331
338
  reorder_associated_item_ids=reorder_associated_item_ids or [],
332
339
  view_configs=view_configs or [],
@@ -466,6 +473,7 @@ def build_builder_server() -> FastMCP:
466
473
  app_key: str = "",
467
474
  publish: bool = True,
468
475
  upsert_views: list[dict] | None = None,
476
+ patch_views: list[dict] | None = None,
469
477
  remove_views: list[str] | None = None,
470
478
  ) -> dict:
471
479
  return ai_builder.app_views_apply(
@@ -473,6 +481,7 @@ def build_builder_server() -> FastMCP:
473
481
  app_key=app_key,
474
482
  publish=publish,
475
483
  upsert_views=upsert_views or [],
484
+ patch_views=patch_views or [],
476
485
  remove_views=remove_views or [],
477
486
  )
478
487
 
@@ -481,6 +490,7 @@ def build_builder_server() -> FastMCP:
481
490
  profile: str = DEFAULT_PROFILE,
482
491
  app_key: str = "",
483
492
  upsert_charts: list[dict] | None = None,
493
+ patch_charts: list[dict] | None = None,
484
494
  remove_chart_ids: list[str] | None = None,
485
495
  reorder_chart_ids: list[str] | None = None,
486
496
  ) -> dict:
@@ -488,6 +498,7 @@ def build_builder_server() -> FastMCP:
488
498
  profile=profile,
489
499
  app_key=app_key,
490
500
  upsert_charts=upsert_charts or [],
501
+ patch_charts=patch_charts or [],
491
502
  remove_chart_ids=remove_chart_ids or [],
492
503
  reorder_chart_ids=reorder_chart_ids or [],
493
504
  )
@@ -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_*`.