@josephyan/qingflow-app-builder-mcp 1.0.9 → 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 +4 -2
- package/npm/bin/qingflow-app-builder-mcp.mjs +31 -2
- package/npm/lib/runtime.mjs +43 -2
- package/package.json +1 -1
- package/pyproject.toml +2 -1
- package/skills/qingflow-app-builder/SKILL.md +135 -14
- package/skills/qingflow-app-builder/references/build-complete-system.md +428 -0
- package/skills/qingflow-app-builder/references/build-single-app.md +530 -0
- package/skills/qingflow-app-builder/references/create-app.md +43 -3
- package/skills/qingflow-app-builder/references/gotchas.md +47 -3
- package/skills/qingflow-app-builder/references/match-rules.md +114 -0
- package/skills/qingflow-app-builder/references/public-surface-sync.md +75 -0
- package/skills/qingflow-app-builder/references/tool-selection.md +25 -10
- package/skills/qingflow-app-builder/references/update-flow.md +29 -1
- package/skills/qingflow-app-builder/references/update-schema.md +6 -2
- package/skills/qingflow-app-builder/references/update-views.md +135 -6
- package/skills/qingflow-mcp-setup/SKILL.md +111 -0
- package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
- package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
- package/skills/qingflow-mcp-setup/references/environments.md +61 -0
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +31 -0
- package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
- package/skills/qingflow-workflow-builder/SKILL.md +96 -0
- package/skills/qingflow-workflow-builder/manifest.yaml +8 -0
- package/skills/qingflow-workflow-builder/references/01-overview.md +45 -0
- package/skills/qingflow-workflow-builder/references/02-update-mode.md +53 -0
- package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +57 -0
- package/skills/qingflow-workflow-builder/references/04-stage1-business-modeling.md +131 -0
- package/skills/qingflow-workflow-builder/references/05-stage2-members-roles.md +29 -0
- package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +165 -0
- package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +33 -0
- package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +51 -0
- package/skills/qingflow-workflow-builder/references/09-stage6-summary.md +88 -0
- package/skills/qingflow-workflow-builder/references/10-node-config-reference.md +93 -0
- package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +15 -0
- package/skills/qingflow-workflow-builder/scripts/diff_flow_spec.py +275 -0
- package/skills/qingflow-workflow-builder/scripts/validate_flow_spec.py +605 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/backend_client.py +55 -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 +112 -6
- package/src/qingflow_mcp/cli/formatters.py +102 -2
- package/src/qingflow_mcp/cli/main.py +204 -3
- package/src/qingflow_mcp/public_surface.py +12 -9
- package/src/qingflow_mcp/response_trim.py +287 -22
- package/src/qingflow_mcp/server.py +29 -20
- package/src/qingflow_mcp/server_app_builder.py +108 -30
- package/src/qingflow_mcp/server_app_user.py +28 -23
- 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 +14283 -9162
- 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
|
|
|
@@ -65,7 +72,12 @@ 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 in {"user:record_get", "user:record_logs_get"}:
|
|
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"}
|
|
80
|
+
_drop_top_keys(trimmed, drop_keys)
|
|
69
81
|
transformer = SUCCESS_POLICY_BY_TOOL.get(tool_name or "")
|
|
70
82
|
if transformer is not None:
|
|
71
83
|
transformer(trimmed)
|
|
@@ -81,7 +93,12 @@ def trim_error_response(payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
81
93
|
_drop_deep_keys(trimmed.get("details"), {"request_route", "base_url", "normalized_args", "suggested_next_call", "transport", "response", "body", "raw"})
|
|
82
94
|
details = trimmed.get("details")
|
|
83
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)
|
|
84
100
|
compact_details = _compact_scalar_dict(details)
|
|
101
|
+
compact_details.update(preserved)
|
|
85
102
|
if compact_details:
|
|
86
103
|
trimmed["details"] = compact_details
|
|
87
104
|
else:
|
|
@@ -137,7 +154,7 @@ def _looks_like_failure_payload(payload: dict[str, Any]) -> bool:
|
|
|
137
154
|
if payload.get("ok") is False:
|
|
138
155
|
return True
|
|
139
156
|
status = str(payload.get("status") or "").lower()
|
|
140
|
-
return status in {"failed", "blocked"
|
|
157
|
+
return status in {"failed", "blocked"}
|
|
141
158
|
|
|
142
159
|
|
|
143
160
|
def _trim_returned_failure(payload: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -146,7 +163,12 @@ def _trim_returned_failure(payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
146
163
|
_drop_deep_keys(trimmed.get("details"), {"request_route", "base_url", "normalized_args", "suggested_next_call", "transport", "response", "body", "raw"})
|
|
147
164
|
details = trimmed.get("details")
|
|
148
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)
|
|
149
170
|
compact_details = _compact_scalar_dict(details)
|
|
171
|
+
compact_details.update(preserved)
|
|
150
172
|
if compact_details:
|
|
151
173
|
trimmed["details"] = compact_details
|
|
152
174
|
else:
|
|
@@ -272,7 +294,7 @@ def _trim_workspace_get(payload: JSONObject) -> None:
|
|
|
272
294
|
)
|
|
273
295
|
|
|
274
296
|
|
|
275
|
-
def
|
|
297
|
+
def _trim_app_list_like(payload: JSONObject) -> None:
|
|
276
298
|
payload.pop("apps", None)
|
|
277
299
|
_trim_item_list(payload, "items", allowed=("app_key", "app_name", "package_name"))
|
|
278
300
|
|
|
@@ -365,10 +387,17 @@ def _trim_record_write(payload: JSONObject) -> None:
|
|
|
365
387
|
data = payload.get("data")
|
|
366
388
|
if not isinstance(data, dict):
|
|
367
389
|
return
|
|
390
|
+
if payload.get("mode") == "batch" or data.get("mode") == "batch":
|
|
391
|
+
_trim_record_write_batch(payload, data)
|
|
392
|
+
return
|
|
368
393
|
data.pop("debug", None)
|
|
369
394
|
data.pop("normalized_payload", None)
|
|
370
395
|
data.pop("human_review", None)
|
|
371
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
|
|
372
401
|
resource = _compact_record_resource(data.get("resource"))
|
|
373
402
|
if resource:
|
|
374
403
|
data["resource"] = resource
|
|
@@ -394,7 +423,48 @@ def _trim_record_write(payload: JSONObject) -> None:
|
|
|
394
423
|
data.pop(key, None)
|
|
395
424
|
|
|
396
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
|
+
|
|
397
464
|
def _trim_record_get(payload: JSONObject) -> None:
|
|
465
|
+
if payload.get("fields") is not None or payload.get("semantic_context") is not None:
|
|
466
|
+
_trim_detail_context_record_get(payload)
|
|
467
|
+
return
|
|
398
468
|
data = payload.get("data")
|
|
399
469
|
if not isinstance(data, dict):
|
|
400
470
|
return
|
|
@@ -417,22 +487,142 @@ def _trim_record_get(payload: JSONObject) -> None:
|
|
|
417
487
|
payload["data"] = compact
|
|
418
488
|
|
|
419
489
|
|
|
490
|
+
def _trim_detail_context_record_get(payload: JSONObject) -> None:
|
|
491
|
+
_trim_item_list(
|
|
492
|
+
payload,
|
|
493
|
+
"fields",
|
|
494
|
+
allowed=("field_id", "title", "type", "value", "display_value", "requested_focus", "asset_ids", "file_asset_ids"),
|
|
495
|
+
)
|
|
496
|
+
references = payload.get("references")
|
|
497
|
+
if isinstance(references, list):
|
|
498
|
+
compact_refs: list[JSONObject] = []
|
|
499
|
+
for item in references:
|
|
500
|
+
if not isinstance(item, dict):
|
|
501
|
+
continue
|
|
502
|
+
compact = _pick(
|
|
503
|
+
item,
|
|
504
|
+
(
|
|
505
|
+
"field_id",
|
|
506
|
+
"field_title",
|
|
507
|
+
"target_app_key",
|
|
508
|
+
"target_record_id",
|
|
509
|
+
"target_title",
|
|
510
|
+
"target_detail_completeness",
|
|
511
|
+
"self_reference",
|
|
512
|
+
),
|
|
513
|
+
)
|
|
514
|
+
target_fields = item.get("target_fields") if isinstance(item.get("target_fields"), list) else []
|
|
515
|
+
if target_fields:
|
|
516
|
+
compact["target_fields"] = [
|
|
517
|
+
_pick(field, ("field_id", "title", "type", "value", "display_value", "asset_ids", "file_asset_ids"))
|
|
518
|
+
for field in target_fields
|
|
519
|
+
if isinstance(field, dict)
|
|
520
|
+
]
|
|
521
|
+
compact_refs.append(compact)
|
|
522
|
+
payload["references"] = compact_refs
|
|
523
|
+
media_assets = payload.get("media_assets")
|
|
524
|
+
if isinstance(media_assets, dict):
|
|
525
|
+
compact_media = _pick(media_assets, ("status", "local_dir", "warnings"))
|
|
526
|
+
items = media_assets.get("items") if isinstance(media_assets.get("items"), list) else []
|
|
527
|
+
compact_media["items"] = [
|
|
528
|
+
_pick(
|
|
529
|
+
item,
|
|
530
|
+
(
|
|
531
|
+
"asset_id",
|
|
532
|
+
"kind",
|
|
533
|
+
"source",
|
|
534
|
+
"field_id",
|
|
535
|
+
"field_title",
|
|
536
|
+
"local_path",
|
|
537
|
+
"access_status",
|
|
538
|
+
"readable_by_agent",
|
|
539
|
+
"mime_type",
|
|
540
|
+
"size_bytes",
|
|
541
|
+
"download_strategy",
|
|
542
|
+
"storage_auth_type",
|
|
543
|
+
"storage_cookie_prefix",
|
|
544
|
+
"redirected",
|
|
545
|
+
),
|
|
546
|
+
)
|
|
547
|
+
for item in items
|
|
548
|
+
if isinstance(item, dict)
|
|
549
|
+
]
|
|
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
|
|
582
|
+
for key in ("data_logs", "workflow_logs"):
|
|
583
|
+
node = payload.get(key)
|
|
584
|
+
if not isinstance(node, dict):
|
|
585
|
+
continue
|
|
586
|
+
compact_node = _pick(
|
|
587
|
+
node,
|
|
588
|
+
(
|
|
589
|
+
"status",
|
|
590
|
+
"visible",
|
|
591
|
+
"reason",
|
|
592
|
+
"page",
|
|
593
|
+
"page_size",
|
|
594
|
+
"items_loaded",
|
|
595
|
+
"reported_total",
|
|
596
|
+
"has_more",
|
|
597
|
+
"complete",
|
|
598
|
+
"items",
|
|
599
|
+
),
|
|
600
|
+
)
|
|
601
|
+
payload[key] = compact_node
|
|
602
|
+
associated_resources = payload.get("associated_resources")
|
|
603
|
+
if isinstance(associated_resources, list):
|
|
604
|
+
payload["associated_resources"] = [
|
|
605
|
+
_pick(item, ("type", "resource_type", "name", "app_key", "app_name", "view_key", "chart_key", "view_type", "report_source", "data_access"))
|
|
606
|
+
for item in associated_resources
|
|
607
|
+
if isinstance(item, dict)
|
|
608
|
+
]
|
|
609
|
+
_drop_deep_keys(payload, {"raw", "debug"})
|
|
610
|
+
|
|
611
|
+
|
|
420
612
|
def _trim_record_list(payload: JSONObject) -> None:
|
|
421
613
|
data = payload.get("data")
|
|
422
614
|
if not isinstance(data, dict):
|
|
423
615
|
return
|
|
424
616
|
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):
|
|
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):
|
|
430
621
|
truncated = result_amount > returned_items
|
|
431
622
|
compact_pagination = {
|
|
432
623
|
"loaded": True,
|
|
433
|
-
"
|
|
434
|
-
"
|
|
435
|
-
"reported_total": result_amount,
|
|
624
|
+
"returned_count": returned_items,
|
|
625
|
+
"total_count": result_amount,
|
|
436
626
|
"truncated": truncated,
|
|
437
627
|
}
|
|
438
628
|
selection = data.get("selection") if isinstance(data.get("selection"), dict) else {}
|
|
@@ -445,6 +635,17 @@ def _trim_record_list(payload: JSONObject) -> None:
|
|
|
445
635
|
if view:
|
|
446
636
|
compact["view"] = _pick(view, ("view_id", "name"))
|
|
447
637
|
payload["data"] = compact
|
|
638
|
+
lookup = payload.get("lookup") if isinstance(payload.get("lookup"), dict) else {}
|
|
639
|
+
if lookup:
|
|
640
|
+
payload["lookup"] = {
|
|
641
|
+
"mode": lookup.get("mode"),
|
|
642
|
+
"query": lookup.get("query"),
|
|
643
|
+
"total_count": lookup.get("total_count", lookup.get("reported_total")),
|
|
644
|
+
"returned_count": lookup.get("returned_count"),
|
|
645
|
+
"truncated": lookup.get("truncated"),
|
|
646
|
+
"confidence": lookup.get("confidence"),
|
|
647
|
+
"next_action": lookup.get("next_action"),
|
|
648
|
+
}
|
|
448
649
|
|
|
449
650
|
|
|
450
651
|
def _trim_record_access(payload: JSONObject) -> None:
|
|
@@ -461,7 +662,6 @@ def _trim_record_access(payload: JSONObject) -> None:
|
|
|
461
662
|
"truncated",
|
|
462
663
|
"safe_for_final_conclusion",
|
|
463
664
|
"files",
|
|
464
|
-
"metadata_files",
|
|
465
665
|
"fields",
|
|
466
666
|
"warnings",
|
|
467
667
|
"verification",
|
|
@@ -474,6 +674,49 @@ def _trim_record_access(payload: JSONObject) -> None:
|
|
|
474
674
|
payload.update(compact)
|
|
475
675
|
|
|
476
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
|
+
|
|
477
720
|
def _trim_record_analyze(payload: JSONObject) -> None:
|
|
478
721
|
summary: dict[str, Any] = {}
|
|
479
722
|
completeness = payload.get("completeness")
|
|
@@ -520,13 +763,18 @@ def _trim_record_delete(payload: JSONObject) -> None:
|
|
|
520
763
|
if not isinstance(data, dict):
|
|
521
764
|
return
|
|
522
765
|
resource = data.get("resource")
|
|
523
|
-
deleted_ids
|
|
524
|
-
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):
|
|
525
773
|
raw_ids = resource.get("record_ids") or resource.get("apply_ids") or resource.get("applyIds")
|
|
526
774
|
if isinstance(raw_ids, list):
|
|
527
775
|
deleted_ids = [str(item) for item in raw_ids if item not in (None, "")]
|
|
528
|
-
data["deleted_ids"] = deleted_ids
|
|
529
|
-
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, "")]
|
|
530
778
|
for key in (
|
|
531
779
|
"resource",
|
|
532
780
|
"action",
|
|
@@ -594,6 +842,19 @@ def _compact_schema_field(item: Any, *, template_map: dict[str, Any] | None) ->
|
|
|
594
842
|
compact["required"] = bool(item.get("required"))
|
|
595
843
|
if template_map is not None and isinstance(title, str) and title in template_map:
|
|
596
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
|
|
597
858
|
candidate_hint = item.get("candidate_hint")
|
|
598
859
|
if isinstance(candidate_hint, dict):
|
|
599
860
|
compact["candidate_hint"] = candidate_hint
|
|
@@ -747,7 +1008,11 @@ def _trim_builder_envelope(payload: JSONObject) -> None:
|
|
|
747
1008
|
details = payload.get("details")
|
|
748
1009
|
if isinstance(details, dict):
|
|
749
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")
|
|
750
1014
|
compact = _compact_scalar_dict(details)
|
|
1015
|
+
compact.update(preserved)
|
|
751
1016
|
if compact:
|
|
752
1017
|
payload["details"] = compact
|
|
753
1018
|
else:
|
|
@@ -768,7 +1033,7 @@ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_use_credential", "auth_wh
|
|
|
768
1033
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_logout",), _trim_auth_logout)
|
|
769
1034
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_list",), _trim_workspace_list)
|
|
770
1035
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_get", "workspace_select"), _trim_workspace_get)
|
|
771
|
-
_register_policy((USER_DOMAIN,), ("app_list",
|
|
1036
|
+
_register_policy((USER_DOMAIN,), ("app_list",), _trim_app_list_like)
|
|
772
1037
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("app_get",), _trim_app_get)
|
|
773
1038
|
_register_policy((BUILDER_DOMAIN,), ("app_repair_code_blocks",), _trim_builder_list_like)
|
|
774
1039
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("portal_list", "portal_get", "view_get", "chart_get"), _trim_builder_list_like)
|
|
@@ -811,6 +1076,7 @@ _register_policy((USER_DOMAIN,), ("record_insert", "record_update"), _trim_recor
|
|
|
811
1076
|
_register_policy((USER_DOMAIN,), ("record_get",), _trim_record_get)
|
|
812
1077
|
_register_policy((USER_DOMAIN,), ("record_list",), _trim_record_list)
|
|
813
1078
|
_register_policy((USER_DOMAIN,), ("record_access",), _trim_record_access)
|
|
1079
|
+
_register_policy((USER_DOMAIN,), ("record_logs_get",), _trim_record_logs)
|
|
814
1080
|
_register_policy((USER_DOMAIN,), ("record_analyze",), _trim_record_analyze)
|
|
815
1081
|
_register_policy((USER_DOMAIN,), ("record_code_block_run",), _trim_code_block_run)
|
|
816
1082
|
_register_policy((USER_DOMAIN,), ("task_list",), _trim_task_list)
|
|
@@ -851,6 +1117,8 @@ _register_policy(
|
|
|
851
1117
|
(BUILDER_DOMAIN,),
|
|
852
1118
|
(
|
|
853
1119
|
"builder_tool_contract",
|
|
1120
|
+
"workspace_icon_catalog_get",
|
|
1121
|
+
"package_list",
|
|
854
1122
|
"package_get",
|
|
855
1123
|
"package_apply",
|
|
856
1124
|
"solution_install",
|
|
@@ -860,11 +1128,8 @@ _register_policy(
|
|
|
860
1128
|
"app_release_edit_lock_if_mine",
|
|
861
1129
|
"app_resolve",
|
|
862
1130
|
"button_style_catalog_get",
|
|
863
|
-
"
|
|
864
|
-
"
|
|
865
|
-
"app_custom_button_create",
|
|
866
|
-
"app_custom_button_update",
|
|
867
|
-
"app_custom_button_delete",
|
|
1131
|
+
"app_custom_buttons_apply",
|
|
1132
|
+
"app_associated_resources_apply",
|
|
868
1133
|
"app_get_fields",
|
|
869
1134
|
"app_repair_code_blocks",
|
|
870
1135
|
"app_get_layout",
|
|
@@ -48,17 +48,17 @@ 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
|
-
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
|
|
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_list`, `record_get`, 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,8 +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 `
|
|
71
|
-
`record_browse_schema_get(view_id=...)` returns
|
|
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
|
+
`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
|
|
|
@@ -101,30 +103,37 @@ Analysis answers must include concrete numbers. When applicable, include percent
|
|
|
101
103
|
|
|
102
104
|
## Record CRUD Path
|
|
103
105
|
|
|
104
|
-
`app_get -> record_browse_schema_get(view_id=...) -> record_list / record_get`
|
|
105
|
-
`record_insert_schema_get -> record_insert`
|
|
106
|
-
`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.
|
|
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`
|
|
116
|
-
- `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.
|
|
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.
|
|
119
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.
|
|
120
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.
|
|
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
|
-
- `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.
|
|
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
|
+
- 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.
|
|
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.
|
|
125
132
|
|
|
126
133
|
- Read relation targets from `record_insert_schema_get` / `record_update_schema_get` relation metadata before preparing relation writes.
|
|
127
|
-
- 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`.
|
|
128
137
|
- If explicit candidate browsing is needed for default-all member or department fields, prefer those field candidate tools instead of starting with `directory_*`.
|
|
129
138
|
|
|
130
139
|
## Code Block Path
|
|
@@ -177,7 +186,7 @@ Use `record_code_block_run` when the user wants to execute a form code-block fie
|
|
|
177
186
|
- `task_action_execute(task_id=..., action=...)` is also supported; MCP resolves the current todo locator internally before calling the real action route.
|
|
178
187
|
- `task_workflow_log_get(task_id=...)` and `task_associated_report_detail_get(task_id=...)` are also supported for the current todo context.
|
|
179
188
|
- Use `task_associated_report_detail_get` for associated view or report details.
|
|
180
|
-
- 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?)`.
|
|
181
190
|
- Task actions operate on `app_key + record_id + workflow_node_id`, not `task_id`.
|
|
182
191
|
|
|
183
192
|
## Time Handling
|