@josephyan/qingflow-cli 1.0.10 → 1.1.1

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.
Files changed (65) hide show
  1. package/README.md +3 -3
  2. package/npm/bin/qingflow.mjs +32 -1
  3. package/npm/lib/runtime.mjs +43 -2
  4. package/package.json +1 -1
  5. package/pyproject.toml +2 -1
  6. package/skills/qingflow-cli/SKILL.md +440 -0
  7. package/skills/qingflow-cli/manifest.yaml +10 -0
  8. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +94 -0
  9. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md +485 -0
  10. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md +237 -0
  11. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_MATCH_RULES.md +137 -0
  12. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md +263 -0
  13. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md +304 -0
  14. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_WORKSPACE_ICONS.md +41 -0
  15. package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +139 -0
  16. package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +84 -0
  17. package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +129 -0
  18. package/skills/qingflow-cli/reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +195 -0
  19. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +159 -0
  20. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +20 -0
  21. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md +176 -0
  22. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +163 -0
  23. package/skills/qingflow-cli/reference/QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md +107 -0
  24. package/skills/qingflow-cli/reference/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md +151 -0
  25. package/skills/qingflow-cli/reference/_batch_schema_complex.json +18 -0
  26. package/skills/qingflow-cli/reference/_batch_schema_scalar.json +17 -0
  27. package/skills/qingflow-cli/reference/charts_remove.example.json +1 -0
  28. package/skills/qingflow-cli/reference/charts_reorder.example.json +1 -0
  29. package/skills/qingflow-cli/reference/charts_upsert_bar.example.json +8 -0
  30. package/skills/qingflow-cli/reference/charts_upsert_dashboard_starter.example.json +37 -0
  31. package/skills/qingflow-cli/reference/charts_upsert_minimal.example.json +13 -0
  32. package/skills/qingflow-cli/reference/portal_sections_all_types.example.json +131 -0
  33. package/skills/qingflow-cli/reference/portal_sections_five_types.example.json +126 -0
  34. package/skills/qingflow-cli/reference/portal_sections_standard_workbench.example.json +128 -0
  35. package/skills/qingflow-cli/reference/schema_add_fields_minimal.example.json +7 -0
  36. package/skills/qingflow-cli/reference/schema_apply_add_fields_all_types.json +78 -0
  37. package/skills/qingflow-cli/reference/views_upsert_table_minimal.example.json +7 -0
  38. package/skills/qingflow-cli/scripts/builder-package-from-app-list.py +140 -0
  39. package/skills/qingflow-cli/scripts/find-app-by-keyword.py +132 -0
  40. package/skills/qingflow-cli/scripts/validate_qingflow_output_files.py +87 -0
  41. package/src/qingflow_mcp/__init__.py +1 -1
  42. package/src/qingflow_mcp/builder_facade/models.py +532 -48
  43. package/src/qingflow_mcp/builder_facade/service.py +9194 -2384
  44. package/src/qingflow_mcp/builder_facade/workflow_spec.py +111 -0
  45. package/src/qingflow_mcp/cli/commands/app.py +3 -16
  46. package/src/qingflow_mcp/cli/commands/builder.py +354 -56
  47. package/src/qingflow_mcp/cli/commands/record.py +89 -4
  48. package/src/qingflow_mcp/cli/formatters.py +53 -15
  49. package/src/qingflow_mcp/cli/main.py +204 -3
  50. package/src/qingflow_mcp/public_surface.py +11 -8
  51. package/src/qingflow_mcp/response_trim.py +185 -46
  52. package/src/qingflow_mcp/server.py +18 -15
  53. package/src/qingflow_mcp/server_app_builder.py +108 -30
  54. package/src/qingflow_mcp/server_app_user.py +20 -21
  55. package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
  56. package/src/qingflow_mcp/solution/compiler/icon_utils.py +294 -0
  57. package/src/qingflow_mcp/solution/executor.py +3 -133
  58. package/src/qingflow_mcp/tools/ai_builder_tools.py +2617 -440
  59. package/src/qingflow_mcp/tools/app_tools.py +53 -8
  60. package/src/qingflow_mcp/tools/package_tools.py +16 -2
  61. package/src/qingflow_mcp/tools/record_tools.py +3408 -599
  62. package/src/qingflow_mcp/tools/resource_read_tools.py +3 -0
  63. package/src/qingflow_mcp/tools/solution_tools.py +30 -2
  64. package/src/qingflow_mcp/tools/workflow_tools.py +3 -31
  65. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +0 -173
@@ -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", "user:record_delete"} 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
 
@@ -66,8 +73,10 @@ def trim_success_response(tool_name: str | None, payload: dict[str, Any]) -> dic
66
73
  return payload
67
74
  trimmed = deepcopy(payload)
68
75
  drop_keys = COMMON_SUCCESS_DROP_TOP
69
- if tool_name == "user:record_get":
76
+ if tool_name in {"user:record_get", "user:record_logs_get"}:
70
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"}
71
80
  _drop_top_keys(trimmed, drop_keys)
72
81
  transformer = SUCCESS_POLICY_BY_TOOL.get(tool_name or "")
73
82
  if transformer is not None:
@@ -84,7 +93,12 @@ def trim_error_response(payload: dict[str, Any]) -> dict[str, Any]:
84
93
  _drop_deep_keys(trimmed.get("details"), {"request_route", "base_url", "normalized_args", "suggested_next_call", "transport", "response", "body", "raw"})
85
94
  details = trimmed.get("details")
86
95
  if isinstance(details, dict):
96
+ preserved = {}
97
+ for key in ("blocking_issues", "compiled_match_rules"):
98
+ if key in details:
99
+ preserved[key] = details.get(key)
87
100
  compact_details = _compact_scalar_dict(details)
101
+ compact_details.update(preserved)
88
102
  if compact_details:
89
103
  trimmed["details"] = compact_details
90
104
  else:
@@ -140,7 +154,7 @@ def _looks_like_failure_payload(payload: dict[str, Any]) -> bool:
140
154
  if payload.get("ok") is False:
141
155
  return True
142
156
  status = str(payload.get("status") or "").lower()
143
- return status in {"failed", "blocked", "verification_failed"}
157
+ return status in {"failed", "blocked"}
144
158
 
145
159
 
146
160
  def _trim_returned_failure(payload: dict[str, Any]) -> dict[str, Any]:
@@ -149,7 +163,12 @@ def _trim_returned_failure(payload: dict[str, Any]) -> dict[str, Any]:
149
163
  _drop_deep_keys(trimmed.get("details"), {"request_route", "base_url", "normalized_args", "suggested_next_call", "transport", "response", "body", "raw"})
150
164
  details = trimmed.get("details")
151
165
  if isinstance(details, dict):
166
+ preserved = {}
167
+ for key in ("blocking_issues", "compiled_match_rules"):
168
+ if key in details:
169
+ preserved[key] = details.get(key)
152
170
  compact_details = _compact_scalar_dict(details)
171
+ compact_details.update(preserved)
153
172
  if compact_details:
154
173
  trimmed["details"] = compact_details
155
174
  else:
@@ -275,7 +294,7 @@ def _trim_workspace_get(payload: JSONObject) -> None:
275
294
  )
276
295
 
277
296
 
278
- def _trim_app_search_like(payload: JSONObject) -> None:
297
+ def _trim_app_list_like(payload: JSONObject) -> None:
279
298
  payload.pop("apps", None)
280
299
  _trim_item_list(payload, "items", allowed=("app_key", "app_name", "package_name"))
281
300
 
@@ -368,10 +387,17 @@ def _trim_record_write(payload: JSONObject) -> None:
368
387
  data = payload.get("data")
369
388
  if not isinstance(data, dict):
370
389
  return
390
+ if payload.get("mode") == "batch" or data.get("mode") == "batch":
391
+ _trim_record_write_batch(payload, data)
392
+ return
371
393
  data.pop("debug", None)
372
394
  data.pop("normalized_payload", None)
373
395
  data.pop("human_review", None)
374
396
  data.pop("action", None)
397
+ for key in ("update_route", "tried_routes"):
398
+ value = payload.get(key)
399
+ if value not in (None, [], {}, ""):
400
+ data[key] = value
375
401
  resource = _compact_record_resource(data.get("resource"))
376
402
  if resource:
377
403
  data["resource"] = resource
@@ -397,6 +423,44 @@ def _trim_record_write(payload: JSONObject) -> None:
397
423
  data.pop(key, None)
398
424
 
399
425
 
426
+ def _trim_record_write_batch(payload: JSONObject, data: JSONObject) -> None:
427
+ data.pop("items", None)
428
+ data.pop("debug", None)
429
+ for key in ("summary", "app_key", "mode"):
430
+ if data.get(key) in (None, [], {}, ""):
431
+ data.pop(key, None)
432
+ items = payload.get("items")
433
+ if isinstance(items, list):
434
+ payload["items"] = [
435
+ _pick(
436
+ item,
437
+ (
438
+ "index",
439
+ "row_number",
440
+ "status",
441
+ "record_id",
442
+ "apply_id",
443
+ "write_executed",
444
+ "verification_status",
445
+ "safe_to_retry",
446
+ "update_route",
447
+ "tried_routes",
448
+ "failed_fields",
449
+ "confirmation_requests",
450
+ "blockers",
451
+ "error",
452
+ "warnings",
453
+ "resource",
454
+ "verification",
455
+ ),
456
+ )
457
+ for item in items
458
+ if isinstance(item, dict)
459
+ ]
460
+ if payload.get("items") in (None, [], {}, ""):
461
+ payload.pop("items", None)
462
+
463
+
400
464
  def _trim_record_get(payload: JSONObject) -> None:
401
465
  if payload.get("fields") is not None or payload.get("semantic_context") is not None:
402
466
  _trim_detail_context_record_get(payload)
@@ -427,7 +491,7 @@ def _trim_detail_context_record_get(payload: JSONObject) -> None:
427
491
  _trim_item_list(
428
492
  payload,
429
493
  "fields",
430
- allowed=("field_id", "title", "type", "value", "display_value", "requested_focus", "asset_ids"),
494
+ allowed=("field_id", "title", "type", "value", "display_value", "requested_focus", "asset_ids", "file_asset_ids"),
431
495
  )
432
496
  references = payload.get("references")
433
497
  if isinstance(references, list):
@@ -450,7 +514,7 @@ def _trim_detail_context_record_get(payload: JSONObject) -> None:
450
514
  target_fields = item.get("target_fields") if isinstance(item.get("target_fields"), list) else []
451
515
  if target_fields:
452
516
  compact["target_fields"] = [
453
- _pick(field, ("field_id", "title", "type", "value", "display_value", "asset_ids"))
517
+ _pick(field, ("field_id", "title", "type", "value", "display_value", "asset_ids", "file_asset_ids"))
454
518
  for field in target_fields
455
519
  if isinstance(field, dict)
456
520
  ]
@@ -484,6 +548,37 @@ def _trim_detail_context_record_get(payload: JSONObject) -> None:
484
548
  if isinstance(item, dict)
485
549
  ]
486
550
  payload["media_assets"] = compact_media
551
+ file_assets = payload.get("file_assets")
552
+ if isinstance(file_assets, dict):
553
+ compact_files = _pick(file_assets, ("status", "local_dir", "warnings"))
554
+ items = file_assets.get("items") if isinstance(file_assets.get("items"), list) else []
555
+ compact_files["items"] = [
556
+ _pick(
557
+ item,
558
+ (
559
+ "file_asset_id",
560
+ "media_asset_id",
561
+ "kind",
562
+ "source",
563
+ "field_id",
564
+ "field_title",
565
+ "file_name",
566
+ "local_path",
567
+ "access_status",
568
+ "readable_by_agent",
569
+ "mime_type",
570
+ "size_bytes",
571
+ "download_strategy",
572
+ "storage_auth_type",
573
+ "storage_cookie_prefix",
574
+ "redirected",
575
+ "extraction",
576
+ ),
577
+ )
578
+ for item in items
579
+ if isinstance(item, dict)
580
+ ]
581
+ payload["file_assets"] = compact_files
487
582
  for key in ("data_logs", "workflow_logs"):
488
583
  node = payload.get(key)
489
584
  if not isinstance(node, dict):
@@ -507,7 +602,7 @@ def _trim_detail_context_record_get(payload: JSONObject) -> None:
507
602
  associated_resources = payload.get("associated_resources")
508
603
  if isinstance(associated_resources, list):
509
604
  payload["associated_resources"] = [
510
- _pick(item, ("type", "name", "app_key", "app_name", "view_key", "chart_key", "view_type", "data_access"))
605
+ _pick(item, ("type", "resource_type", "name", "app_key", "app_name", "view_key", "chart_key", "view_type", "report_source", "data_access"))
511
606
  for item in associated_resources
512
607
  if isinstance(item, dict)
513
608
  ]
@@ -519,17 +614,15 @@ def _trim_record_list(payload: JSONObject) -> None:
519
614
  if not isinstance(data, dict):
520
615
  return
521
616
  pagination = data.get("pagination") if isinstance(data.get("pagination"), dict) else {}
522
- returned_items = pagination.get("returned_items")
523
- result_amount = pagination.get("result_amount")
524
- limit = pagination.get("limit")
525
- truncated = False
526
- if isinstance(result_amount, int) and isinstance(returned_items, int):
617
+ returned_items = pagination.get("returned_count", pagination.get("returned_items"))
618
+ result_amount = pagination.get("total_count", pagination.get("result_amount"))
619
+ truncated = bool(pagination.get("truncated"))
620
+ if not truncated and isinstance(result_amount, int) and isinstance(returned_items, int):
527
621
  truncated = result_amount > returned_items
528
622
  compact_pagination = {
529
623
  "loaded": True,
530
- "page_size": limit,
531
- "fetched_pages": 1,
532
- "reported_total": result_amount,
624
+ "returned_count": returned_items,
625
+ "total_count": result_amount,
533
626
  "truncated": truncated,
534
627
  }
535
628
  selection = data.get("selection") if isinstance(data.get("selection"), dict) else {}
@@ -544,33 +637,14 @@ def _trim_record_list(payload: JSONObject) -> None:
544
637
  payload["data"] = compact
545
638
  lookup = payload.get("lookup") if isinstance(payload.get("lookup"), dict) else {}
546
639
  if lookup:
547
- candidates = lookup.get("candidates") if isinstance(lookup.get("candidates"), list) else []
548
640
  payload["lookup"] = {
549
641
  "mode": lookup.get("mode"),
550
642
  "query": lookup.get("query"),
551
- "reported_total": lookup.get("reported_total"),
552
- "returned_candidates": lookup.get("returned_candidates"),
643
+ "total_count": lookup.get("total_count", lookup.get("reported_total")),
644
+ "returned_count": lookup.get("returned_count"),
645
+ "truncated": lookup.get("truncated"),
553
646
  "confidence": lookup.get("confidence"),
554
647
  "next_action": lookup.get("next_action"),
555
- "candidates": [
556
- {
557
- "record_id": candidate.get("record_id"),
558
- "title": candidate.get("title"),
559
- "score": candidate.get("score"),
560
- "matched_fields": [
561
- _pick(match, ("title", "value", "match_type", "score"))
562
- for match in (candidate.get("matched_fields") if isinstance(candidate.get("matched_fields"), list) else [])
563
- if isinstance(match, dict)
564
- ],
565
- "display_fields": [
566
- _pick(field, ("title", "value"))
567
- for field in (candidate.get("display_fields") if isinstance(candidate.get("display_fields"), list) else [])
568
- if isinstance(field, dict)
569
- ],
570
- }
571
- for candidate in candidates[:5]
572
- if isinstance(candidate, dict)
573
- ],
574
648
  }
575
649
 
576
650
 
@@ -600,6 +674,49 @@ def _trim_record_access(payload: JSONObject) -> None:
600
674
  payload.update(compact)
601
675
 
602
676
 
677
+ def _trim_record_logs(payload: JSONObject) -> None:
678
+ compact: dict[str, Any] = {}
679
+ for key in (
680
+ "ok",
681
+ "status",
682
+ "output_profile",
683
+ "app",
684
+ "view",
685
+ "record",
686
+ "local_dir",
687
+ "summary_path",
688
+ "warnings",
689
+ "unavailable_context",
690
+ "context_integrity",
691
+ ):
692
+ value = payload.get(key)
693
+ if value is not None:
694
+ compact[key] = value
695
+ for key in ("data_logs", "workflow_logs"):
696
+ node = payload.get(key)
697
+ if isinstance(node, dict):
698
+ compact[key] = _pick(
699
+ node,
700
+ (
701
+ "status",
702
+ "visible",
703
+ "source",
704
+ "reason",
705
+ "complete",
706
+ "items_count",
707
+ "pages_fetched",
708
+ "page_size",
709
+ "reported_total",
710
+ "local_path",
711
+ "preview_items",
712
+ "warnings",
713
+ "stopped_reason",
714
+ ),
715
+ )
716
+ payload.clear()
717
+ payload.update(compact)
718
+
719
+
603
720
  def _trim_record_analyze(payload: JSONObject) -> None:
604
721
  summary: dict[str, Any] = {}
605
722
  completeness = payload.get("completeness")
@@ -646,13 +763,18 @@ def _trim_record_delete(payload: JSONObject) -> None:
646
763
  if not isinstance(data, dict):
647
764
  return
648
765
  resource = data.get("resource")
649
- deleted_ids: list[str] = []
650
- if isinstance(resource, dict):
766
+ deleted_ids = payload.get("deleted_ids") if isinstance(payload.get("deleted_ids"), list) else data.get("deleted_ids")
767
+ failed_ids = payload.get("failed_ids") if isinstance(payload.get("failed_ids"), list) else data.get("failed_ids")
768
+ if not isinstance(deleted_ids, list):
769
+ deleted_ids = []
770
+ if not isinstance(failed_ids, list):
771
+ failed_ids = []
772
+ if not deleted_ids and isinstance(resource, dict):
651
773
  raw_ids = resource.get("record_ids") or resource.get("apply_ids") or resource.get("applyIds")
652
774
  if isinstance(raw_ids, list):
653
775
  deleted_ids = [str(item) for item in raw_ids if item not in (None, "")]
654
- data["deleted_ids"] = deleted_ids
655
- data.setdefault("failed_ids", [])
776
+ data["deleted_ids"] = [str(item) for item in deleted_ids if item not in (None, "")]
777
+ data["failed_ids"] = [str(item) for item in failed_ids if item not in (None, "")]
656
778
  for key in (
657
779
  "resource",
658
780
  "action",
@@ -720,6 +842,19 @@ def _compact_schema_field(item: Any, *, template_map: dict[str, Any] | None) ->
720
842
  compact["required"] = bool(item.get("required"))
721
843
  if template_map is not None and isinstance(title, str) and title in template_map:
722
844
  compact["template"] = template_map.get(title)
845
+ for key in (
846
+ "format_hint",
847
+ "example_value",
848
+ "linkage",
849
+ "may_become_required",
850
+ "activation_sources",
851
+ "requirement_reason",
852
+ "accepts_natural_input",
853
+ "requires_upload",
854
+ ):
855
+ value = item.get(key)
856
+ if value not in (None, [], {}, ""):
857
+ compact[key] = value
723
858
  candidate_hint = item.get("candidate_hint")
724
859
  if isinstance(candidate_hint, dict):
725
860
  compact["candidate_hint"] = candidate_hint
@@ -873,7 +1008,11 @@ def _trim_builder_envelope(payload: JSONObject) -> None:
873
1008
  details = payload.get("details")
874
1009
  if isinstance(details, dict):
875
1010
  _drop_deep_keys(details, {"request_route", "base_url", "normalized_args", "suggested_next_call", "transport", "response", "body", "raw"})
1011
+ preserved = {}
1012
+ if isinstance(details.get("compiled_match_rules"), dict):
1013
+ preserved["compiled_match_rules"] = details.get("compiled_match_rules")
876
1014
  compact = _compact_scalar_dict(details)
1015
+ compact.update(preserved)
877
1016
  if compact:
878
1017
  payload["details"] = compact
879
1018
  else:
@@ -894,7 +1033,7 @@ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_use_credential", "auth_wh
894
1033
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_logout",), _trim_auth_logout)
895
1034
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_list",), _trim_workspace_list)
896
1035
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_get", "workspace_select"), _trim_workspace_get)
897
- _register_policy((USER_DOMAIN,), ("app_list", "app_search"), _trim_app_search_like)
1036
+ _register_policy((USER_DOMAIN,), ("app_list",), _trim_app_list_like)
898
1037
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("app_get",), _trim_app_get)
899
1038
  _register_policy((BUILDER_DOMAIN,), ("app_repair_code_blocks",), _trim_builder_list_like)
900
1039
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("portal_list", "portal_get", "view_get", "chart_get"), _trim_builder_list_like)
@@ -937,6 +1076,7 @@ _register_policy((USER_DOMAIN,), ("record_insert", "record_update"), _trim_recor
937
1076
  _register_policy((USER_DOMAIN,), ("record_get",), _trim_record_get)
938
1077
  _register_policy((USER_DOMAIN,), ("record_list",), _trim_record_list)
939
1078
  _register_policy((USER_DOMAIN,), ("record_access",), _trim_record_access)
1079
+ _register_policy((USER_DOMAIN,), ("record_logs_get",), _trim_record_logs)
940
1080
  _register_policy((USER_DOMAIN,), ("record_analyze",), _trim_record_analyze)
941
1081
  _register_policy((USER_DOMAIN,), ("record_code_block_run",), _trim_code_block_run)
942
1082
  _register_policy((USER_DOMAIN,), ("task_list",), _trim_task_list)
@@ -977,6 +1117,8 @@ _register_policy(
977
1117
  (BUILDER_DOMAIN,),
978
1118
  (
979
1119
  "builder_tool_contract",
1120
+ "workspace_icon_catalog_get",
1121
+ "package_list",
980
1122
  "package_get",
981
1123
  "package_apply",
982
1124
  "solution_install",
@@ -986,11 +1128,8 @@ _register_policy(
986
1128
  "app_release_edit_lock_if_mine",
987
1129
  "app_resolve",
988
1130
  "button_style_catalog_get",
989
- "app_custom_button_list",
990
- "app_custom_button_get",
991
- "app_custom_button_create",
992
- "app_custom_button_update",
993
- "app_custom_button_delete",
1131
+ "app_custom_buttons_apply",
1132
+ "app_associated_resources_apply",
994
1133
  "app_get_fields",
995
1134
  "app_repair_code_blocks",
996
1135
  "app_get_layout",
@@ -48,7 +48,7 @@ All resource tools operate with the logged-in user's Qingflow permissions.
48
48
 
49
49
  ## App Discovery
50
50
 
51
- If `app_key` is unknown, use `app_list` or `app_search` first.
51
+ If `app_key` is unknown, use `app_list` first. Pass `query` to filter visible apps by keyword.
52
52
  If the app is known but the data range is not, use `app_get` first and choose from `accessible_views`.
53
53
  If an accessible view has `analysis_supported=false`, do not use it for `record_access` or `record_list`. `boardView` and `ganttView` are special UI views, not data-access targets.
54
54
  `view_get(view_id=...)` also returns `export_capability`; it only means there is a supported export route, not that export permission has been verified.
@@ -56,9 +56,9 @@ If an accessible view has `analysis_supported=false`, do not use it for `record_
56
56
  ## Schema-First Rule
57
57
 
58
58
  Call `record_insert_schema_get` before `record_insert`.
59
- Call `record_update_schema_get` before `record_update`.
59
+ For simple field changes after the target record is clear, call `record_update` directly. Use `record_update_schema_get` for diagnostics, ambiguous fields, or complex writable-scope inspection.
60
60
  Call `record_code_block_schema_get` before `record_code_block_run`.
61
- Call `app_get` first when the data range is unclear, then use `record_browse_schema_get(view_id=...)` before `record_access`, `record_list`, or `record_get`.
61
+ Call `app_get` first when the data range is unclear, then use `record_browse_schema_get(view_id=...)` before `record_access`, `record_list`, `record_get`, or `record_logs_get`.
62
62
  Call `record_import_schema_get` when the import field mapping is unclear before template download or verify.
63
63
 
64
64
  - All `field_id` values must come from the schema response.
@@ -67,9 +67,9 @@ Call `record_import_schema_get` when the import field mapping is unclear before
67
67
  ## Schema Scope
68
68
 
69
69
  `record_insert_schema_get` returns the current user's insert-ready applicant schema; read `required_fields`, `optional_fields`, `runtime_linked_required_fields`, and `payload_template`.
70
- `record_update_schema_get` returns the current record's overall update-ready writable field set across matched accessible views; read `writable_fields` and `payload_template`.
70
+ `record_update_schema_get` returns the current record's overall update-ready writable field set and route diagnostics across matched accessible views; read `writable_fields`, `payload_template`, `available_update_routes`, and `recommended_update_route`.
71
71
  `record_browse_schema_get(view_id=...)` returns the same readable fields shown in the selected Qingflow table view header.
72
- `record_access.fields` and CSV columns use that exact same view schema.
72
+ `record_access.fields` / CSV columns and `record_list.columns / where / order_by / query_fields` use that exact same view schema.
73
73
  `record_code_block_schema_get` returns code-block-ready schema for exact code block field selection.
74
74
  `record_import_schema_get` returns import-ready column metadata.
75
75
 
@@ -103,9 +103,9 @@ Analysis answers must include concrete numbers. When applicable, include percent
103
103
 
104
104
  ## Record CRUD Path
105
105
 
106
- `app_get -> record_browse_schema_get(view_id=...) -> record_list / record_get`
107
- `record_insert_schema_get -> record_insert`
108
- `record_update_schema_get -> record_update`
106
+ `app_get -> record_browse_schema_get(view_id=...) -> record_list / record_get / record_logs_get`
107
+ `record_insert_schema_get -> record_insert(items)`
108
+ `record_update` for simple updates; `record_update_schema_get -> record_update` when the writable field scope is unclear.
109
109
  `record_list / record_get -> record_delete`
110
110
  `record_code_block_schema_get -> record_code_block_run`
111
111
 
@@ -115,22 +115,25 @@ 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.
119
- - `record_update` uses a field-title keyed `fields` map and internally selects the first accessible view that can execute the current payload.
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
+ - `record_update` uses a field-title keyed `fields` map. It first tries the data-manager direct update route, then falls back to the frontend custom-view detail edit route when the selected view can cover the payload; if a unique current-user todo task for the same record exposes editable fields, it can finally use the workflow save-only route. Read `update_route` and `tried_routes` after execution.
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.
122
122
  - Read field-level `linkage` whenever present on `record_insert_schema_get` or `record_update_schema_get`; it is the static hint for linked visibility, reference-driven auto fill, and formula/default auto-fill behavior.
123
123
  - `linkage.sources` lists upstream field titles that influence the current field; `linkage.affects_fields` lists downstream fields that may change when the current field changes.
124
124
  - `linkage.kind=logic_visibility` means linked visibility or option-driven rules are involved; `linkage.kind=reference_fill` means reference/default matching logic is involved; `linkage.kind=formula_fill` means formula/default auto-fill logic is involved.
125
- - `record_update_schema_get` exposes the overall writable field set for the record, but not every field combination is guaranteed; `record_update` still needs one single matched accessible view that can cover the payload.
125
+ - `record_update_schema_get` exposes the overall writable field set and route candidates for the record, but not every field combination is guaranteed; `record_update` still needs data-manager permission, one single matched custom view that can cover the payload, or one unique editable current-user todo task.
126
126
  - `record_delete` deletes by `record_id` or `record_ids`.
127
- - `record_get` is the single-record frontend detail context tool. It returns detail-page visible fields, one-level relation targets, first-page data/workflow logs, associated views/reports, local readable image assets from rich text/attachments/image fields/subtables/reference targets, unavailable context, and `semantic_context`.
128
- - Read record images from `record_get.media_assets.items[].local_path` when `readable_by_agent=true`; `record_get` follows the frontend storage cookie redirect path for Qingflow attachments, and remote file URLs should not be treated as directly readable.
127
+ - `record_get` is the single-record frontend detail context tool. It returns detail-page visible fields, one-level relation targets, first-page data/workflow logs, associated views/reports, local readable image assets, local downloadable file assets, unavailable context, and `semantic_context`.
128
+ - Use `record_logs_get` only when the user needs the full visible data/workflow log history for a specific record. It writes JSONL files locally and returns file paths plus completeness metadata; do not expect full log arrays in the response.
129
+ - Read record images from `record_get.media_assets.items[].local_path` when `readable_by_agent=true`; read attachments/documents/tables from `record_get.file_assets.items[].local_path` and `extraction.text_path` when present. `record_get` follows the frontend storage cookie redirect path for Qingflow attachments, and remote file URLs should not be treated as directly readable.
129
130
  - `record_get.columns` are focus hints only; they do not project the detail fields. Read facts from top-level `fields[]`.
130
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.
131
132
 
132
133
  - 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.
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; pass `--record-id`, `--workflow-node-id`, and `--fields-file` only when the candidate scope must match an existing runtime context.
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`.
134
137
  - If explicit candidate browsing is needed for default-all member or department fields, prefer those field candidate tools instead of starting with `directory_*`.
135
138
 
136
139
  ## Code Block Path
@@ -183,7 +186,7 @@ Use `record_code_block_run` when the user wants to execute a form code-block fie
183
186
  - `task_action_execute(task_id=..., action=...)` is also supported; MCP resolves the current todo locator internally before calling the real action route.
184
187
  - `task_workflow_log_get(task_id=...)` and `task_associated_report_detail_get(task_id=...)` are also supported for the current todo context.
185
188
  - Use `task_associated_report_detail_get` for associated view or report details.
186
- - Use `task_workflow_log_get` for full workflow log history.
189
+ - Use `task_workflow_log_get` for the current task context workflow log page. For full record-level data/workflow logs, use `record_logs_get(app_key, record_id, view_id?)`.
187
190
  - Task actions operate on `app_key + record_id + workflow_node_id`, not `task_id`.
188
191
 
189
192
  ## Time Handling