@qingflow-tech/qingflow-app-builder-mcp 1.0.4 → 1.0.6
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.
- package/README.md +2 -2
- package/package.json +1 -1
- package/pyproject.toml +2 -1
- package/src/qingflow_mcp/backend_client.py +55 -1
- package/src/qingflow_mcp/cli/commands/record.py +63 -6
- package/src/qingflow_mcp/cli/formatters.py +101 -1
- package/src/qingflow_mcp/public_surface.py +2 -1
- package/src/qingflow_mcp/response_trim.py +235 -10
- package/src/qingflow_mcp/server.py +19 -12
- package/src/qingflow_mcp/server_app_user.py +30 -13
- package/src/qingflow_mcp/tools/record_tools.py +13425 -8817
|
@@ -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
|
|
|
@@ -65,7 +72,10 @@ def trim_success_response(tool_name: str | None, payload: dict[str, Any]) -> dic
|
|
|
65
72
|
if not isinstance(payload, dict):
|
|
66
73
|
return payload
|
|
67
74
|
trimmed = deepcopy(payload)
|
|
68
|
-
|
|
75
|
+
drop_keys = COMMON_SUCCESS_DROP_TOP
|
|
76
|
+
if tool_name == "user:record_get":
|
|
77
|
+
drop_keys = COMMON_SUCCESS_DROP_TOP - {"output_profile"}
|
|
78
|
+
_drop_top_keys(trimmed, drop_keys)
|
|
69
79
|
transformer = SUCCESS_POLICY_BY_TOOL.get(tool_name or "")
|
|
70
80
|
if transformer is not None:
|
|
71
81
|
transformer(trimmed)
|
|
@@ -137,7 +147,7 @@ def _looks_like_failure_payload(payload: dict[str, Any]) -> bool:
|
|
|
137
147
|
if payload.get("ok") is False:
|
|
138
148
|
return True
|
|
139
149
|
status = str(payload.get("status") or "").lower()
|
|
140
|
-
return status in {"failed", "blocked"
|
|
150
|
+
return status in {"failed", "blocked"}
|
|
141
151
|
|
|
142
152
|
|
|
143
153
|
def _trim_returned_failure(payload: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -365,6 +375,9 @@ def _trim_record_write(payload: JSONObject) -> None:
|
|
|
365
375
|
data = payload.get("data")
|
|
366
376
|
if not isinstance(data, dict):
|
|
367
377
|
return
|
|
378
|
+
if payload.get("mode") == "batch" or data.get("mode") == "batch":
|
|
379
|
+
_trim_record_write_batch(payload, data)
|
|
380
|
+
return
|
|
368
381
|
data.pop("debug", None)
|
|
369
382
|
data.pop("normalized_payload", None)
|
|
370
383
|
data.pop("human_review", None)
|
|
@@ -394,7 +407,48 @@ def _trim_record_write(payload: JSONObject) -> None:
|
|
|
394
407
|
data.pop(key, None)
|
|
395
408
|
|
|
396
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
|
+
|
|
397
448
|
def _trim_record_get(payload: JSONObject) -> None:
|
|
449
|
+
if payload.get("fields") is not None or payload.get("semantic_context") is not None:
|
|
450
|
+
_trim_detail_context_record_get(payload)
|
|
451
|
+
return
|
|
398
452
|
data = payload.get("data")
|
|
399
453
|
if not isinstance(data, dict):
|
|
400
454
|
return
|
|
@@ -417,22 +471,142 @@ def _trim_record_get(payload: JSONObject) -> None:
|
|
|
417
471
|
payload["data"] = compact
|
|
418
472
|
|
|
419
473
|
|
|
474
|
+
def _trim_detail_context_record_get(payload: JSONObject) -> None:
|
|
475
|
+
_trim_item_list(
|
|
476
|
+
payload,
|
|
477
|
+
"fields",
|
|
478
|
+
allowed=("field_id", "title", "type", "value", "display_value", "requested_focus", "asset_ids", "file_asset_ids"),
|
|
479
|
+
)
|
|
480
|
+
references = payload.get("references")
|
|
481
|
+
if isinstance(references, list):
|
|
482
|
+
compact_refs: list[JSONObject] = []
|
|
483
|
+
for item in references:
|
|
484
|
+
if not isinstance(item, dict):
|
|
485
|
+
continue
|
|
486
|
+
compact = _pick(
|
|
487
|
+
item,
|
|
488
|
+
(
|
|
489
|
+
"field_id",
|
|
490
|
+
"field_title",
|
|
491
|
+
"target_app_key",
|
|
492
|
+
"target_record_id",
|
|
493
|
+
"target_title",
|
|
494
|
+
"target_detail_completeness",
|
|
495
|
+
"self_reference",
|
|
496
|
+
),
|
|
497
|
+
)
|
|
498
|
+
target_fields = item.get("target_fields") if isinstance(item.get("target_fields"), list) else []
|
|
499
|
+
if target_fields:
|
|
500
|
+
compact["target_fields"] = [
|
|
501
|
+
_pick(field, ("field_id", "title", "type", "value", "display_value", "asset_ids", "file_asset_ids"))
|
|
502
|
+
for field in target_fields
|
|
503
|
+
if isinstance(field, dict)
|
|
504
|
+
]
|
|
505
|
+
compact_refs.append(compact)
|
|
506
|
+
payload["references"] = compact_refs
|
|
507
|
+
media_assets = payload.get("media_assets")
|
|
508
|
+
if isinstance(media_assets, dict):
|
|
509
|
+
compact_media = _pick(media_assets, ("status", "local_dir", "warnings"))
|
|
510
|
+
items = media_assets.get("items") if isinstance(media_assets.get("items"), list) else []
|
|
511
|
+
compact_media["items"] = [
|
|
512
|
+
_pick(
|
|
513
|
+
item,
|
|
514
|
+
(
|
|
515
|
+
"asset_id",
|
|
516
|
+
"kind",
|
|
517
|
+
"source",
|
|
518
|
+
"field_id",
|
|
519
|
+
"field_title",
|
|
520
|
+
"local_path",
|
|
521
|
+
"access_status",
|
|
522
|
+
"readable_by_agent",
|
|
523
|
+
"mime_type",
|
|
524
|
+
"size_bytes",
|
|
525
|
+
"download_strategy",
|
|
526
|
+
"storage_auth_type",
|
|
527
|
+
"storage_cookie_prefix",
|
|
528
|
+
"redirected",
|
|
529
|
+
),
|
|
530
|
+
)
|
|
531
|
+
for item in items
|
|
532
|
+
if isinstance(item, dict)
|
|
533
|
+
]
|
|
534
|
+
payload["media_assets"] = compact_media
|
|
535
|
+
file_assets = payload.get("file_assets")
|
|
536
|
+
if isinstance(file_assets, dict):
|
|
537
|
+
compact_files = _pick(file_assets, ("status", "local_dir", "warnings"))
|
|
538
|
+
items = file_assets.get("items") if isinstance(file_assets.get("items"), list) else []
|
|
539
|
+
compact_files["items"] = [
|
|
540
|
+
_pick(
|
|
541
|
+
item,
|
|
542
|
+
(
|
|
543
|
+
"file_asset_id",
|
|
544
|
+
"media_asset_id",
|
|
545
|
+
"kind",
|
|
546
|
+
"source",
|
|
547
|
+
"field_id",
|
|
548
|
+
"field_title",
|
|
549
|
+
"file_name",
|
|
550
|
+
"local_path",
|
|
551
|
+
"access_status",
|
|
552
|
+
"readable_by_agent",
|
|
553
|
+
"mime_type",
|
|
554
|
+
"size_bytes",
|
|
555
|
+
"download_strategy",
|
|
556
|
+
"storage_auth_type",
|
|
557
|
+
"storage_cookie_prefix",
|
|
558
|
+
"redirected",
|
|
559
|
+
"extraction",
|
|
560
|
+
),
|
|
561
|
+
)
|
|
562
|
+
for item in items
|
|
563
|
+
if isinstance(item, dict)
|
|
564
|
+
]
|
|
565
|
+
payload["file_assets"] = compact_files
|
|
566
|
+
for key in ("data_logs", "workflow_logs"):
|
|
567
|
+
node = payload.get(key)
|
|
568
|
+
if not isinstance(node, dict):
|
|
569
|
+
continue
|
|
570
|
+
compact_node = _pick(
|
|
571
|
+
node,
|
|
572
|
+
(
|
|
573
|
+
"status",
|
|
574
|
+
"visible",
|
|
575
|
+
"reason",
|
|
576
|
+
"page",
|
|
577
|
+
"page_size",
|
|
578
|
+
"items_loaded",
|
|
579
|
+
"reported_total",
|
|
580
|
+
"has_more",
|
|
581
|
+
"complete",
|
|
582
|
+
"items",
|
|
583
|
+
),
|
|
584
|
+
)
|
|
585
|
+
payload[key] = compact_node
|
|
586
|
+
associated_resources = payload.get("associated_resources")
|
|
587
|
+
if isinstance(associated_resources, list):
|
|
588
|
+
payload["associated_resources"] = [
|
|
589
|
+
_pick(item, ("type", "name", "app_key", "app_name", "view_key", "chart_key", "view_type", "data_access"))
|
|
590
|
+
for item in associated_resources
|
|
591
|
+
if isinstance(item, dict)
|
|
592
|
+
]
|
|
593
|
+
_drop_deep_keys(payload, {"raw", "debug"})
|
|
594
|
+
|
|
595
|
+
|
|
420
596
|
def _trim_record_list(payload: JSONObject) -> None:
|
|
421
597
|
data = payload.get("data")
|
|
422
598
|
if not isinstance(data, dict):
|
|
423
599
|
return
|
|
424
600
|
pagination = data.get("pagination") if isinstance(data.get("pagination"), dict) else {}
|
|
425
|
-
returned_items = pagination.get("returned_items")
|
|
426
|
-
result_amount = pagination.get("result_amount")
|
|
427
|
-
|
|
428
|
-
truncated
|
|
429
|
-
if isinstance(result_amount, int) and isinstance(returned_items, int):
|
|
601
|
+
returned_items = pagination.get("returned_count", pagination.get("returned_items"))
|
|
602
|
+
result_amount = pagination.get("total_count", pagination.get("result_amount"))
|
|
603
|
+
truncated = bool(pagination.get("truncated"))
|
|
604
|
+
if not truncated and isinstance(result_amount, int) and isinstance(returned_items, int):
|
|
430
605
|
truncated = result_amount > returned_items
|
|
431
606
|
compact_pagination = {
|
|
432
607
|
"loaded": True,
|
|
433
|
-
"
|
|
434
|
-
"
|
|
435
|
-
"reported_total": result_amount,
|
|
608
|
+
"returned_count": returned_items,
|
|
609
|
+
"total_count": result_amount,
|
|
436
610
|
"truncated": truncated,
|
|
437
611
|
}
|
|
438
612
|
selection = data.get("selection") if isinstance(data.get("selection"), dict) else {}
|
|
@@ -445,6 +619,43 @@ def _trim_record_list(payload: JSONObject) -> None:
|
|
|
445
619
|
if view:
|
|
446
620
|
compact["view"] = _pick(view, ("view_id", "name"))
|
|
447
621
|
payload["data"] = compact
|
|
622
|
+
lookup = payload.get("lookup") if isinstance(payload.get("lookup"), dict) else {}
|
|
623
|
+
if lookup:
|
|
624
|
+
payload["lookup"] = {
|
|
625
|
+
"mode": lookup.get("mode"),
|
|
626
|
+
"query": lookup.get("query"),
|
|
627
|
+
"total_count": lookup.get("total_count", lookup.get("reported_total")),
|
|
628
|
+
"returned_count": lookup.get("returned_count"),
|
|
629
|
+
"truncated": lookup.get("truncated"),
|
|
630
|
+
"confidence": lookup.get("confidence"),
|
|
631
|
+
"next_action": lookup.get("next_action"),
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def _trim_record_access(payload: JSONObject) -> None:
|
|
636
|
+
compact: dict[str, Any] = {}
|
|
637
|
+
for key in (
|
|
638
|
+
"ok",
|
|
639
|
+
"status",
|
|
640
|
+
"app_key",
|
|
641
|
+
"view_id",
|
|
642
|
+
"format",
|
|
643
|
+
"local_dir",
|
|
644
|
+
"row_count",
|
|
645
|
+
"complete",
|
|
646
|
+
"truncated",
|
|
647
|
+
"safe_for_final_conclusion",
|
|
648
|
+
"files",
|
|
649
|
+
"fields",
|
|
650
|
+
"warnings",
|
|
651
|
+
"verification",
|
|
652
|
+
"scope",
|
|
653
|
+
):
|
|
654
|
+
value = payload.get(key)
|
|
655
|
+
if value is not None:
|
|
656
|
+
compact[key] = value
|
|
657
|
+
payload.clear()
|
|
658
|
+
payload.update(compact)
|
|
448
659
|
|
|
449
660
|
|
|
450
661
|
def _trim_record_analyze(payload: JSONObject) -> None:
|
|
@@ -567,6 +778,19 @@ def _compact_schema_field(item: Any, *, template_map: dict[str, Any] | None) ->
|
|
|
567
778
|
compact["required"] = bool(item.get("required"))
|
|
568
779
|
if template_map is not None and isinstance(title, str) and title in template_map:
|
|
569
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
|
|
570
794
|
candidate_hint = item.get("candidate_hint")
|
|
571
795
|
if isinstance(candidate_hint, dict):
|
|
572
796
|
compact["candidate_hint"] = candidate_hint
|
|
@@ -783,6 +1007,7 @@ _register_policy(
|
|
|
783
1007
|
_register_policy((USER_DOMAIN,), ("record_insert", "record_update"), _trim_record_write)
|
|
784
1008
|
_register_policy((USER_DOMAIN,), ("record_get",), _trim_record_get)
|
|
785
1009
|
_register_policy((USER_DOMAIN,), ("record_list",), _trim_record_list)
|
|
1010
|
+
_register_policy((USER_DOMAIN,), ("record_access",), _trim_record_access)
|
|
786
1011
|
_register_policy((USER_DOMAIN,), ("record_analyze",), _trim_record_analyze)
|
|
787
1012
|
_register_policy((USER_DOMAIN,), ("record_code_block_run",), _trim_code_block_run)
|
|
788
1013
|
_register_policy((USER_DOMAIN,), ("task_list",), _trim_task_list)
|
|
@@ -50,7 +50,7 @@ All resource tools operate with the logged-in user's Qingflow permissions.
|
|
|
50
50
|
|
|
51
51
|
If `app_key` is unknown, use `app_list` or `app_search` first.
|
|
52
52
|
If the app is known but the data range is not, use `app_get` first and choose from `accessible_views`.
|
|
53
|
-
If an accessible view has `analysis_supported=false`, do not use it for `
|
|
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.
|
|
55
55
|
|
|
56
56
|
## Schema-First Rule
|
|
@@ -58,7 +58,7 @@ If an accessible view has `analysis_supported=false`, do not use it for `record_
|
|
|
58
58
|
Call `record_insert_schema_get` before `record_insert`.
|
|
59
59
|
Call `record_update_schema_get` before `record_update`.
|
|
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 `
|
|
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`.
|
|
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.
|
|
@@ -68,7 +68,8 @@ Call `record_import_schema_get` when the import field mapping is unclear before
|
|
|
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
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`.
|
|
71
|
-
`record_browse_schema_get(view_id=...)` returns
|
|
71
|
+
`record_browse_schema_get(view_id=...)` returns the same readable fields shown in the selected Qingflow table view header.
|
|
72
|
+
`record_access.fields` / CSV columns and `record_list.columns / where / order_by / query_fields` use that exact same view schema.
|
|
72
73
|
`record_code_block_schema_get` returns code-block-ready schema for exact code block field selection.
|
|
73
74
|
`record_import_schema_get` returns import-ready column metadata.
|
|
74
75
|
|
|
@@ -78,16 +79,17 @@ Call `record_import_schema_get` when the import field mapping is unclear before
|
|
|
78
79
|
|
|
79
80
|
## Analytics Path
|
|
80
81
|
|
|
81
|
-
`app_get -> record_browse_schema_get(view_id=...) ->
|
|
82
|
+
`app_get -> record_browse_schema_get(view_id=...) -> record_access -> Python`
|
|
82
83
|
|
|
83
84
|
Prefer `view_id` entries from `accessible_views` where `analysis_supported=true`.
|
|
84
85
|
|
|
86
|
+
Use `record_access` to write local CSV shard files for analysis, then use Python to compute counts, rankings, ratios, trends, and final conclusions.
|
|
87
|
+
|
|
85
88
|
Use this DSL shape:
|
|
86
89
|
|
|
87
|
-
- `
|
|
88
|
-
- `
|
|
89
|
-
- `
|
|
90
|
-
- `sort`: `{{by, order}}`
|
|
90
|
+
- `columns`: `[{{field_id}}]`
|
|
91
|
+
- `where`: `[{{field_id, op, value}}]`
|
|
92
|
+
- `order_by`: `[{{field_id, direction}}]`
|
|
91
93
|
|
|
92
94
|
Important key rules:
|
|
93
95
|
|
|
@@ -102,17 +104,18 @@ Analysis answers must include concrete numbers. When applicable, include percent
|
|
|
102
104
|
## Record CRUD Path
|
|
103
105
|
|
|
104
106
|
`app_get -> record_browse_schema_get(view_id=...) -> record_list / record_get`
|
|
105
|
-
`record_insert_schema_get -> record_insert`
|
|
107
|
+
`record_insert_schema_get -> record_insert(items)`
|
|
106
108
|
`record_update_schema_get -> record_update`
|
|
107
109
|
`record_list / record_get -> record_delete`
|
|
108
110
|
`record_code_block_schema_get -> record_code_block_run`
|
|
109
111
|
|
|
110
112
|
- Use `columns` as `[{{field_id}}]`
|
|
113
|
+
- Use `record_list(query=..., query_fields=[{{field_id}}])` for fuzzy single-record lookup, then follow `lookup.next_action`; `query_fields` is search scope and `columns` is display shape.
|
|
111
114
|
- Use `where` items as `{{field_id, op, value}}`
|
|
112
115
|
- Use `order_by` items as `{{field_id, direction}}`
|
|
113
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
|
|
114
117
|
|
|
115
|
-
- `record_insert`
|
|
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.
|
|
116
119
|
- `record_update` uses a field-title keyed `fields` map and internally selects the first accessible view that can execute the current payload.
|
|
117
120
|
- For insert, `runtime_linked_required_fields` means required-but-not-directly-writable fields that are usually supplied by runtime linkage or upstream context.
|
|
118
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.
|
|
@@ -121,10 +124,14 @@ Analysis answers must include concrete numbers. When applicable, include percent
|
|
|
121
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.
|
|
122
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.
|
|
123
126
|
- `record_delete` deletes by `record_id` or `record_ids`.
|
|
124
|
-
-
|
|
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
|
+
- 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
|
+
- `record_get.columns` are focus hints only; they do not project the detail fields. Read facts from top-level `fields[]`.
|
|
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.
|
|
125
131
|
|
|
126
132
|
- Read relation targets from `record_insert_schema_get` / `record_update_schema_get` relation metadata before preparing relation writes.
|
|
127
|
-
- Member and
|
|
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`.
|
|
128
135
|
- If explicit candidate browsing is needed for default-all member or department fields, prefer those field candidate tools instead of starting with `directory_*`.
|
|
129
136
|
|
|
130
137
|
## Code Block Path
|
|
@@ -33,7 +33,7 @@ def build_user_server() -> FastMCP:
|
|
|
33
33
|
|
|
34
34
|
If `app_key` is unknown, use `app_list` or `app_search` first.
|
|
35
35
|
If the app is known but the data range is not, use `app_get` first and choose from `accessible_views`.
|
|
36
|
-
If an accessible view has `analysis_supported=false`, do not use it for `
|
|
36
|
+
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.
|
|
37
37
|
`view_get(view_id=...)` also returns `export_capability`; it only means there is a supported export route, not that export permission has been verified.
|
|
38
38
|
|
|
39
39
|
## Shared Helper
|
|
@@ -49,7 +49,7 @@ If an accessible view has `analysis_supported=false`, do not use it for `record_
|
|
|
49
49
|
Call `record_insert_schema_get` before `record_insert`.
|
|
50
50
|
Call `record_update_schema_get` before `record_update`.
|
|
51
51
|
Call `record_code_block_schema_get` before `record_code_block_run`.
|
|
52
|
-
Call `app_get` first when the data range is unclear, then use `record_browse_schema_get(view_id=...)` before `
|
|
52
|
+
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`.
|
|
53
53
|
Call `record_import_schema_get` when the import field mapping is unclear before template download or verify.
|
|
54
54
|
|
|
55
55
|
- All `field_id` values must come from the schema response.
|
|
@@ -60,7 +60,9 @@ Call `record_import_schema_get` when the import field mapping is unclear before
|
|
|
60
60
|
`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`.
|
|
61
61
|
Inside `optional_fields`, any field with `may_become_required=true` is still writable, but may become required when linked visibility or option-driven runtime rules activate.
|
|
62
62
|
`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`.
|
|
63
|
-
`record_browse_schema_get(view_id=...)` returns
|
|
63
|
+
`record_browse_schema_get(view_id=...)` returns the same readable fields shown in the selected Qingflow table view header.
|
|
64
|
+
`record_access.fields` / CSV columns and `record_list.columns / where / order_by / query_fields` use that exact same view schema; a missing field means it is not readable in that view.
|
|
65
|
+
`searchQueIds` is a backend full-text search scope, not an output-column/projection mechanism.
|
|
64
66
|
`record_code_block_schema_get` returns code-block-ready schema for exact code block field selection.
|
|
65
67
|
`record_import_schema_get` returns import-ready column metadata.
|
|
66
68
|
|
|
@@ -70,16 +72,19 @@ Inside `optional_fields`, any field with `may_become_required=true` is still wri
|
|
|
70
72
|
|
|
71
73
|
## Analytics Path
|
|
72
74
|
|
|
73
|
-
`app_get -> record_browse_schema_get(view_id=...) ->
|
|
75
|
+
`app_get -> record_browse_schema_get(view_id=...) -> record_access -> Python`
|
|
74
76
|
|
|
75
77
|
Prefer `view_id` entries from `accessible_views` where `analysis_supported=true`.
|
|
76
78
|
|
|
77
|
-
Use
|
|
79
|
+
Use `record_access` to write local CSV shard files, then use Python to compute counts, rankings, ratios, trends, and final conclusions. `record_access` does not return bulk `items`; read `files[].local_path`. CSV columns are readable and field-id anchored, such as `项目状态__field_343283094`, and `fields[]` is the compact metadata source.
|
|
80
|
+
For analysis-style tasks, prefer an explicit time range or business filter. If `record_access.status == "needs_scope"`, do not treat it as a failure; ask for a time/business scope or retry with a user-provided period using `scope.suggested_time_fields` / `scope.recommended_where_examples`. If `record_access.status == "partial"`, read the returned files only as a limited subset and do not give a final full-population conclusion.
|
|
81
|
+
Use `chart_get` only when the user provides a report URL / chart_id or explicitly asks to read an existing report. Do not use QingBI as the default analysis route.
|
|
78
82
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
- `
|
|
82
|
-
- `
|
|
83
|
+
Use this data-access DSL shape:
|
|
84
|
+
|
|
85
|
+
- `columns`: `[{{field_id}}]`
|
|
86
|
+
- `where`: `[{{field_id, op, value}}]`
|
|
87
|
+
- `order_by`: `[{{field_id, direction}}]`
|
|
83
88
|
|
|
84
89
|
Important key rules:
|
|
85
90
|
|
|
@@ -89,12 +94,16 @@ Important key rules:
|
|
|
89
94
|
- Do **not** use `aggregation`
|
|
90
95
|
- Do **not** use `operator`
|
|
91
96
|
|
|
97
|
+
`record_list` is for browsing and sample checks, not final analysis conclusions.
|
|
98
|
+
For fuzzy single-record lookup, use `record_list(query=..., query_fields=[{{field_id}}])` to find candidates, read `lookup.next_action`, and only call `record_get` after one candidate is clear.
|
|
99
|
+
`record_list.query_fields` maps to backend full-text search scope (`searchQueIds`); `record_list.columns` only controls displayed fields.
|
|
100
|
+
|
|
92
101
|
Analysis answers must include concrete numbers. When applicable, include percentages based on the returned totals.
|
|
93
102
|
|
|
94
103
|
## Record CRUD Path
|
|
95
104
|
|
|
96
105
|
`app_get -> record_browse_schema_get(view_id=...) -> record_list / record_get`
|
|
97
|
-
`record_insert_schema_get -> record_insert`
|
|
106
|
+
`record_insert_schema_get -> record_insert(items)`
|
|
98
107
|
`record_update_schema_get -> record_update`
|
|
99
108
|
`record_list / record_get -> record_delete`
|
|
100
109
|
`record_code_block_schema_get -> record_code_block_run`
|
|
@@ -102,11 +111,12 @@ Analysis answers must include concrete numbers. When applicable, include percent
|
|
|
102
111
|
`portal_get -> view_get -> record_list`
|
|
103
112
|
|
|
104
113
|
- Use `columns` as `[{{field_id}}]`
|
|
114
|
+
- Use `query` plus optional `query_fields` when the user provides fuzzy record-identifying text
|
|
105
115
|
- Use `where` items as `{{field_id, op, value}}`
|
|
106
116
|
- Use `order_by` items as `{{field_id, direction}}`
|
|
107
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
|
|
108
118
|
|
|
109
|
-
- `record_insert`
|
|
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.
|
|
110
120
|
- `record_update` uses a field-title keyed `fields` map and internally selects the first accessible view that can execute the current payload.
|
|
111
121
|
- For insert, `runtime_linked_required_fields` means required-but-not-directly-writable fields that are usually supplied by runtime linkage or upstream context.
|
|
112
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.
|
|
@@ -115,10 +125,14 @@ Analysis answers must include concrete numbers. When applicable, include percent
|
|
|
115
125
|
- `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.
|
|
116
126
|
- `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.
|
|
117
127
|
- `record_delete` deletes by `record_id` or `record_ids`.
|
|
118
|
-
-
|
|
128
|
+
- `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`.
|
|
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.
|
|
130
|
+
- `record_get.columns` are focus hints only; they do not project the detail fields. Read facts from top-level `fields[]`.
|
|
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.
|
|
119
132
|
|
|
120
133
|
- Read relation targets from `record_insert_schema_get` / `record_update_schema_get` relation metadata before preparing relation writes.
|
|
121
|
-
- Member and
|
|
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`.
|
|
122
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.
|
|
123
137
|
- If explicit candidate browsing is needed for default-all member or department fields, prefer those field candidate tools instead of starting with `directory_*`.
|
|
124
138
|
|
|
@@ -146,9 +160,12 @@ Use `record_code_block_run` when the user wants to execute a form code-block fie
|
|
|
146
160
|
|
|
147
161
|
## Export Path
|
|
148
162
|
|
|
163
|
+
Use export only when the user explicitly asks to export/download/generate an Excel or export file.
|
|
164
|
+
|
|
149
165
|
`view_get -> record_export_start -> record_export_status_get -> record_export_get`
|
|
150
166
|
|
|
151
167
|
- `record_export_direct` is the one-shot path that starts export, waits, downloads locally, and still returns remote download links.
|
|
168
|
+
- Do not use `record_export_direct` as the default analysis path; use `record_access -> Python` instead.
|
|
152
169
|
- Export v1 supports record views only and follows the same public `view_id` semantics as `record_list`.
|
|
153
170
|
- `record_export_start` / `record_export_direct` support frontend-like row selection:
|
|
154
171
|
- omit `record_ids` to export all rows in the selected view
|