@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.
- package/README.md +3 -3
- package/npm/bin/qingflow.mjs +32 -1
- package/npm/lib/runtime.mjs +43 -2
- package/package.json +1 -1
- package/pyproject.toml +2 -1
- package/skills/qingflow-cli/SKILL.md +440 -0
- package/skills/qingflow-cli/manifest.yaml +10 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +94 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md +485 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md +237 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_MATCH_RULES.md +137 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md +263 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md +304 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_WORKSPACE_ICONS.md +41 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +139 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +84 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +129 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +195 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +159 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +20 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md +176 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +163 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md +107 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md +151 -0
- package/skills/qingflow-cli/reference/_batch_schema_complex.json +18 -0
- package/skills/qingflow-cli/reference/_batch_schema_scalar.json +17 -0
- package/skills/qingflow-cli/reference/charts_remove.example.json +1 -0
- package/skills/qingflow-cli/reference/charts_reorder.example.json +1 -0
- package/skills/qingflow-cli/reference/charts_upsert_bar.example.json +8 -0
- package/skills/qingflow-cli/reference/charts_upsert_dashboard_starter.example.json +37 -0
- package/skills/qingflow-cli/reference/charts_upsert_minimal.example.json +13 -0
- package/skills/qingflow-cli/reference/portal_sections_all_types.example.json +131 -0
- package/skills/qingflow-cli/reference/portal_sections_five_types.example.json +126 -0
- package/skills/qingflow-cli/reference/portal_sections_standard_workbench.example.json +128 -0
- package/skills/qingflow-cli/reference/schema_add_fields_minimal.example.json +7 -0
- package/skills/qingflow-cli/reference/schema_apply_add_fields_all_types.json +78 -0
- package/skills/qingflow-cli/reference/views_upsert_table_minimal.example.json +7 -0
- package/skills/qingflow-cli/scripts/builder-package-from-app-list.py +140 -0
- package/skills/qingflow-cli/scripts/find-app-by-keyword.py +132 -0
- package/skills/qingflow-cli/scripts/validate_qingflow_output_files.py +87 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/builder_facade/models.py +532 -48
- package/src/qingflow_mcp/builder_facade/service.py +9194 -2384
- package/src/qingflow_mcp/builder_facade/workflow_spec.py +111 -0
- package/src/qingflow_mcp/cli/commands/app.py +3 -16
- package/src/qingflow_mcp/cli/commands/builder.py +354 -56
- package/src/qingflow_mcp/cli/commands/record.py +89 -4
- package/src/qingflow_mcp/cli/formatters.py +53 -15
- package/src/qingflow_mcp/cli/main.py +204 -3
- package/src/qingflow_mcp/public_surface.py +11 -8
- package/src/qingflow_mcp/response_trim.py +185 -46
- package/src/qingflow_mcp/server.py +18 -15
- package/src/qingflow_mcp/server_app_builder.py +108 -30
- package/src/qingflow_mcp/server_app_user.py +20 -21
- package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +294 -0
- package/src/qingflow_mcp/solution/executor.py +3 -133
- package/src/qingflow_mcp/tools/ai_builder_tools.py +2617 -440
- package/src/qingflow_mcp/tools/app_tools.py +53 -8
- package/src/qingflow_mcp/tools/package_tools.py +16 -2
- package/src/qingflow_mcp/tools/record_tools.py +3408 -599
- package/src/qingflow_mcp/tools/resource_read_tools.py +3 -0
- package/src/qingflow_mcp/tools/solution_tools.py +30 -2
- package/src/qingflow_mcp/tools/workflow_tools.py +3 -31
- 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
|
|
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"
|
|
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
|
|
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
|
-
|
|
525
|
-
truncated
|
|
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
|
-
"
|
|
531
|
-
"
|
|
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
|
-
"
|
|
552
|
-
"
|
|
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
|
|
650
|
-
if isinstance(
|
|
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
|
|
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",
|
|
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
|
-
"
|
|
990
|
-
"
|
|
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`
|
|
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
|
-
|
|
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 `
|
|
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 `
|
|
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`
|
|
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`
|
|
119
|
-
- `record_update` uses a field-title keyed `fields` map
|
|
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
|
|
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
|
|
128
|
-
-
|
|
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
|
|
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
|
|
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
|