@josephyan/qingflow-cli 0.2.0-beta.82 → 0.2.0-beta.83
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 +1 -1
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/builder_facade/service.py +666 -19
- package/src/qingflow_mcp/cli/commands/builder.py +36 -95
- package/src/qingflow_mcp/public_surface.py +1 -5
- package/src/qingflow_mcp/response_trim.py +2 -4
- package/src/qingflow_mcp/server_app_builder.py +41 -61
- package/src/qingflow_mcp/tools/ai_builder_tools.py +219 -195
- package/src/qingflow_mcp/tools/package_tools.py +49 -0
|
@@ -406,30 +406,55 @@ class AiBuilderFacade:
|
|
|
406
406
|
"visibility": _public_visibility_from_member_auth(desired_auth),
|
|
407
407
|
}
|
|
408
408
|
|
|
409
|
-
def package_get(self, *, profile: str, tag_id: int) -> JSONObject:
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
409
|
+
def package_get(self, *, profile: str, package_id: int | None = None, tag_id: int | None = None) -> JSONObject:
|
|
410
|
+
effective_package_id = _coerce_positive_int(package_id if package_id is not None else tag_id)
|
|
411
|
+
normalized_args = {"package_id": effective_package_id or package_id or tag_id}
|
|
412
|
+
if effective_package_id is None:
|
|
413
|
+
return _failed(
|
|
414
|
+
"PACKAGE_ID_REQUIRED",
|
|
415
|
+
"package_id must be positive",
|
|
416
|
+
normalized_args=normalized_args,
|
|
417
|
+
suggested_next_call=None,
|
|
418
|
+
)
|
|
419
|
+
base_result: JSONObject
|
|
413
420
|
try:
|
|
414
|
-
|
|
415
|
-
base_result = self.packages.package_get_base(profile=profile, tag_id=tag_id, include_raw=True)
|
|
421
|
+
base_result = self.packages.package_get_base(profile=profile, tag_id=effective_package_id, include_raw=True)
|
|
416
422
|
except (QingflowApiError, RuntimeError) as error:
|
|
417
423
|
api_error = _coerce_api_error(error)
|
|
418
424
|
return _failed_from_api_error(
|
|
419
425
|
"PACKAGE_GET_FAILED",
|
|
420
426
|
api_error,
|
|
421
427
|
normalized_args=normalized_args,
|
|
422
|
-
details={"
|
|
423
|
-
suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "
|
|
428
|
+
details={"package_id": effective_package_id},
|
|
429
|
+
suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "package_id": effective_package_id}},
|
|
424
430
|
)
|
|
425
|
-
|
|
431
|
+
|
|
432
|
+
detail_result: JSONObject | None = None
|
|
433
|
+
detail_read_error: QingflowApiError | None = None
|
|
434
|
+
try:
|
|
435
|
+
detail_result = self.packages.package_get(profile=profile, tag_id=effective_package_id, include_raw=True)
|
|
436
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
437
|
+
detail_read_error = _coerce_api_error(error)
|
|
438
|
+
|
|
439
|
+
detail = detail_result.get("result") if isinstance(detail_result, dict) and isinstance(detail_result.get("result"), dict) else {}
|
|
426
440
|
base = base_result.get("result") if isinstance(base_result.get("result"), dict) else {}
|
|
427
|
-
summary = detail_result.get("summary") if isinstance(detail_result.get("summary"), dict) else {}
|
|
441
|
+
summary = detail_result.get("summary") if isinstance(detail_result, dict) and isinstance(detail_result.get("summary"), dict) else {}
|
|
442
|
+
source = detail if detail else base
|
|
443
|
+
warnings: list[JSONObject] = []
|
|
444
|
+
if detail_read_error is not None:
|
|
445
|
+
warnings.append(
|
|
446
|
+
{
|
|
447
|
+
"code": "PACKAGE_DETAIL_READ_DEGRADED",
|
|
448
|
+
"message": "package_get used baseInfo because the package detail endpoint was not readable",
|
|
449
|
+
"backend_code": detail_read_error.backend_code,
|
|
450
|
+
"http_status": detail_read_error.http_status,
|
|
451
|
+
}
|
|
452
|
+
)
|
|
428
453
|
return {
|
|
429
454
|
"status": "success",
|
|
430
455
|
"error_code": None,
|
|
431
456
|
"recoverable": False,
|
|
432
|
-
"message": "read package
|
|
457
|
+
"message": "read package",
|
|
433
458
|
"normalized_args": normalized_args,
|
|
434
459
|
"missing_fields": [],
|
|
435
460
|
"allowed_values": {},
|
|
@@ -437,22 +462,184 @@ class AiBuilderFacade:
|
|
|
437
462
|
"request_id": None,
|
|
438
463
|
"suggested_next_call": None,
|
|
439
464
|
"noop": False,
|
|
440
|
-
"warnings":
|
|
465
|
+
"warnings": warnings,
|
|
441
466
|
"verification": {"package_exists": True},
|
|
442
467
|
"verified": True,
|
|
443
|
-
"
|
|
444
|
-
"
|
|
445
|
-
"
|
|
446
|
-
"publish_status":
|
|
468
|
+
"package_id": _coerce_positive_int(source.get("tagId") or base.get("tagId")) or effective_package_id,
|
|
469
|
+
"package_name": str(source.get("tagName") or base.get("tagName") or "").strip() or None,
|
|
470
|
+
"icon": str(source.get("tagIcon") or base.get("tagIcon") or "").strip() or None,
|
|
471
|
+
"publish_status": source.get("publishStatus") if source.get("publishStatus") is not None else base.get("publishStatus"),
|
|
447
472
|
"item_count": summary.get("itemCount"),
|
|
448
|
-
"item_preview": deepcopy(summary.get("itemPreview") or []),
|
|
449
473
|
"add_app_status": base.get("addAppStatus") if base.get("addAppStatus") is not None else summary.get("addAppStatus"),
|
|
450
474
|
"edit_app_status": base.get("editAppStatus") if base.get("editAppStatus") is not None else summary.get("editAppStatus"),
|
|
451
475
|
"del_app_status": base.get("delAppStatus") if base.get("delAppStatus") is not None else summary.get("delAppStatus"),
|
|
452
476
|
"edit_tag_status": base.get("editTagStatus") if base.get("editTagStatus") is not None else summary.get("editTagStatus"),
|
|
453
|
-
"visibility": _public_visibility_from_member_auth(base.get("auth") or
|
|
477
|
+
"visibility": _public_visibility_from_member_auth(base.get("auth") or source.get("auth")),
|
|
478
|
+
"items": _public_package_items_from_tag_items(source.get("tagItems") or base.get("tagItems")),
|
|
454
479
|
}
|
|
455
480
|
|
|
481
|
+
def package_apply(
|
|
482
|
+
self,
|
|
483
|
+
*,
|
|
484
|
+
profile: str,
|
|
485
|
+
package_id: int | None = None,
|
|
486
|
+
package_name: str | None = None,
|
|
487
|
+
create_if_missing: bool = False,
|
|
488
|
+
icon: str | None = None,
|
|
489
|
+
color: str | None = None,
|
|
490
|
+
visibility: VisibilityPatch | None = None,
|
|
491
|
+
items: list[dict[str, Any]] | None = None,
|
|
492
|
+
allow_detach: bool = False,
|
|
493
|
+
) -> JSONObject:
|
|
494
|
+
requested_name = str(package_name or "").strip()
|
|
495
|
+
normalized_args: JSONObject = {
|
|
496
|
+
"package_id": package_id,
|
|
497
|
+
**({"package_name": requested_name} if requested_name else {}),
|
|
498
|
+
"create_if_missing": bool(create_if_missing),
|
|
499
|
+
**({"icon": icon} if icon else {}),
|
|
500
|
+
**({"color": color} if color else {}),
|
|
501
|
+
**({"visibility": visibility.model_dump(mode="json")} if visibility is not None else {}),
|
|
502
|
+
**({"items": deepcopy(items)} if items is not None else {}),
|
|
503
|
+
"allow_detach": bool(allow_detach),
|
|
504
|
+
}
|
|
505
|
+
effective_package_id = _coerce_positive_int(package_id)
|
|
506
|
+
created = False
|
|
507
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
508
|
+
|
|
509
|
+
if effective_package_id is None:
|
|
510
|
+
if not create_if_missing:
|
|
511
|
+
return _failed(
|
|
512
|
+
"PACKAGE_ID_REQUIRED",
|
|
513
|
+
"package_id is required unless create_if_missing=true",
|
|
514
|
+
normalized_args=normalized_args,
|
|
515
|
+
suggested_next_call=None,
|
|
516
|
+
)
|
|
517
|
+
if not requested_name:
|
|
518
|
+
return _failed(
|
|
519
|
+
"PACKAGE_NAME_REQUIRED",
|
|
520
|
+
"package_name is required when create_if_missing=true",
|
|
521
|
+
normalized_args=normalized_args,
|
|
522
|
+
suggested_next_call=None,
|
|
523
|
+
)
|
|
524
|
+
create_result = self.package_create(
|
|
525
|
+
profile=profile,
|
|
526
|
+
package_name=requested_name,
|
|
527
|
+
icon=icon,
|
|
528
|
+
color=color,
|
|
529
|
+
visibility=visibility,
|
|
530
|
+
)
|
|
531
|
+
if create_result.get("status") not in {"success", "partial_success"}:
|
|
532
|
+
return _publicize_package_apply_failure(create_result, profile=profile, normalized_args=normalized_args)
|
|
533
|
+
effective_package_id = _coerce_positive_int(create_result.get("tag_id") or create_result.get("package_id"))
|
|
534
|
+
if effective_package_id is None:
|
|
535
|
+
return _failed(
|
|
536
|
+
"PACKAGE_CREATE_UNVERIFIED",
|
|
537
|
+
"created package but could not verify package_id",
|
|
538
|
+
normalized_args=normalized_args,
|
|
539
|
+
details={"create_result": create_result},
|
|
540
|
+
suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "package_id": package_id}},
|
|
541
|
+
)
|
|
542
|
+
normalized_args["package_id"] = effective_package_id
|
|
543
|
+
created = True
|
|
544
|
+
|
|
545
|
+
metadata_requested = bool(requested_name or icon or color or visibility is not None)
|
|
546
|
+
if metadata_requested and not created:
|
|
547
|
+
edit_tag_outcome = self._guard_package_permission(
|
|
548
|
+
profile=profile,
|
|
549
|
+
tag_id=effective_package_id,
|
|
550
|
+
required_permission="edit_tag",
|
|
551
|
+
normalized_args=normalized_args,
|
|
552
|
+
)
|
|
553
|
+
if edit_tag_outcome.block is not None:
|
|
554
|
+
return edit_tag_outcome.block
|
|
555
|
+
permission_outcomes.append(edit_tag_outcome)
|
|
556
|
+
update_result = self.package_update(
|
|
557
|
+
profile=profile,
|
|
558
|
+
tag_id=effective_package_id,
|
|
559
|
+
package_name=requested_name or None,
|
|
560
|
+
icon=icon,
|
|
561
|
+
color=color,
|
|
562
|
+
visibility=visibility,
|
|
563
|
+
)
|
|
564
|
+
if update_result.get("status") not in {"success", "partial_success"}:
|
|
565
|
+
return _publicize_package_apply_failure(update_result, profile=profile, normalized_args=normalized_args)
|
|
566
|
+
|
|
567
|
+
layout_result: JSONObject | None = None
|
|
568
|
+
if items is not None:
|
|
569
|
+
if not isinstance(items, list):
|
|
570
|
+
return _failed(
|
|
571
|
+
"PACKAGE_ITEMS_INVALID",
|
|
572
|
+
"items must be a list",
|
|
573
|
+
normalized_args=normalized_args,
|
|
574
|
+
suggested_next_call=None,
|
|
575
|
+
)
|
|
576
|
+
layout_result = self._apply_package_items(
|
|
577
|
+
profile=profile,
|
|
578
|
+
package_id=effective_package_id,
|
|
579
|
+
items=items,
|
|
580
|
+
allow_detach=allow_detach,
|
|
581
|
+
normalized_args=normalized_args,
|
|
582
|
+
)
|
|
583
|
+
if layout_result.get("status") not in {"success", "partial_success"}:
|
|
584
|
+
return _apply_permission_outcomes(layout_result, *permission_outcomes)
|
|
585
|
+
|
|
586
|
+
verification = self.package_get(profile=profile, package_id=effective_package_id)
|
|
587
|
+
if verification.get("status") != "success":
|
|
588
|
+
return _apply_permission_outcomes(verification, *permission_outcomes)
|
|
589
|
+
expected_visibility = None
|
|
590
|
+
if visibility is not None:
|
|
591
|
+
try:
|
|
592
|
+
expected_visibility = _public_visibility_from_member_auth(
|
|
593
|
+
self._compile_visibility_to_member_auth(profile=profile, visibility=visibility)
|
|
594
|
+
)
|
|
595
|
+
except VisibilityResolutionError:
|
|
596
|
+
expected_visibility = None
|
|
597
|
+
response: JSONObject = {
|
|
598
|
+
"status": "success",
|
|
599
|
+
"error_code": None,
|
|
600
|
+
"recoverable": False,
|
|
601
|
+
"message": "applied package",
|
|
602
|
+
"normalized_args": normalized_args,
|
|
603
|
+
"missing_fields": [],
|
|
604
|
+
"allowed_values": {},
|
|
605
|
+
"details": {"layout_result": layout_result} if layout_result is not None else {},
|
|
606
|
+
"request_id": None,
|
|
607
|
+
"suggested_next_call": None,
|
|
608
|
+
"noop": not (created or metadata_requested or items is not None),
|
|
609
|
+
"warnings": [],
|
|
610
|
+
"verification": {
|
|
611
|
+
"package_exists": True,
|
|
612
|
+
"package_created": created,
|
|
613
|
+
"layout_applied": items is not None,
|
|
614
|
+
"visibility_verified": None
|
|
615
|
+
if expected_visibility is None
|
|
616
|
+
else _visibility_matches_expected(verification.get("visibility"), expected_visibility),
|
|
617
|
+
},
|
|
618
|
+
"verified": True,
|
|
619
|
+
**{
|
|
620
|
+
key: deepcopy(value)
|
|
621
|
+
for key, value in verification.items()
|
|
622
|
+
if key
|
|
623
|
+
not in {
|
|
624
|
+
"status",
|
|
625
|
+
"error_code",
|
|
626
|
+
"recoverable",
|
|
627
|
+
"message",
|
|
628
|
+
"normalized_args",
|
|
629
|
+
"missing_fields",
|
|
630
|
+
"allowed_values",
|
|
631
|
+
"details",
|
|
632
|
+
"request_id",
|
|
633
|
+
"suggested_next_call",
|
|
634
|
+
"noop",
|
|
635
|
+
"warnings",
|
|
636
|
+
"verification",
|
|
637
|
+
"verified",
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
}
|
|
641
|
+
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
642
|
+
|
|
456
643
|
def package_update(
|
|
457
644
|
self,
|
|
458
645
|
*,
|
|
@@ -524,7 +711,7 @@ class AiBuilderFacade:
|
|
|
524
711
|
details={"tag_id": tag_id},
|
|
525
712
|
suggested_next_call={"tool_name": "package_update", "arguments": {"profile": profile, **normalized_args}},
|
|
526
713
|
)
|
|
527
|
-
verification = self.package_get(profile=profile,
|
|
714
|
+
verification = self.package_get(profile=profile, package_id=tag_id)
|
|
528
715
|
if verification.get("status") != "success":
|
|
529
716
|
return verification
|
|
530
717
|
return {
|
|
@@ -648,6 +835,208 @@ class AiBuilderFacade:
|
|
|
648
835
|
"retried": bool(listed.get("retried", False)),
|
|
649
836
|
}
|
|
650
837
|
|
|
838
|
+
def _apply_package_items(
|
|
839
|
+
self,
|
|
840
|
+
*,
|
|
841
|
+
profile: str,
|
|
842
|
+
package_id: int,
|
|
843
|
+
items: list[dict[str, Any]],
|
|
844
|
+
allow_detach: bool,
|
|
845
|
+
normalized_args: JSONObject,
|
|
846
|
+
) -> JSONObject:
|
|
847
|
+
current_result: JSONObject | None = None
|
|
848
|
+
try:
|
|
849
|
+
current_result = self.packages.package_get(profile=profile, tag_id=package_id, include_raw=True)
|
|
850
|
+
except (QingflowApiError, RuntimeError) as detail_error:
|
|
851
|
+
detail_api_error = _coerce_api_error(detail_error)
|
|
852
|
+
try:
|
|
853
|
+
current_result = self.packages.package_get_base(profile=profile, tag_id=package_id, include_raw=True)
|
|
854
|
+
except (QingflowApiError, RuntimeError) as base_error:
|
|
855
|
+
api_error = _coerce_api_error(base_error)
|
|
856
|
+
return _failed_from_api_error(
|
|
857
|
+
"PACKAGE_LAYOUT_READ_FAILED",
|
|
858
|
+
api_error,
|
|
859
|
+
normalized_args=normalized_args,
|
|
860
|
+
details={
|
|
861
|
+
"package_id": package_id,
|
|
862
|
+
"detail_read_error": _transport_error_payload(detail_api_error),
|
|
863
|
+
},
|
|
864
|
+
suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "package_id": package_id}},
|
|
865
|
+
)
|
|
866
|
+
current_raw = current_result.get("result") if isinstance(current_result.get("result"), dict) else {}
|
|
867
|
+
raw_tag_items = current_raw.get("tagItems")
|
|
868
|
+
if not isinstance(raw_tag_items, list):
|
|
869
|
+
return _failed(
|
|
870
|
+
"PACKAGE_LAYOUT_UNREADABLE",
|
|
871
|
+
"package items could not be read safely",
|
|
872
|
+
normalized_args=normalized_args,
|
|
873
|
+
details={"package_id": package_id},
|
|
874
|
+
suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "package_id": package_id}},
|
|
875
|
+
)
|
|
876
|
+
current_items = raw_tag_items
|
|
877
|
+
|
|
878
|
+
current_resources = _flatten_package_resource_identities(current_items, public=False)
|
|
879
|
+
desired_resources = _flatten_package_resource_identities(items, public=True)
|
|
880
|
+
missing_resources = sorted(current_resources - desired_resources)
|
|
881
|
+
if missing_resources and not allow_detach:
|
|
882
|
+
return _failed(
|
|
883
|
+
"PACKAGE_LAYOUT_ITEMS_MISSING",
|
|
884
|
+
"items omits existing apps or portals; pass allow_detach=true to remove them",
|
|
885
|
+
normalized_args=normalized_args,
|
|
886
|
+
details={"package_id": package_id, "missing_items": [{"type": item[0], "id": item[1]} for item in missing_resources]},
|
|
887
|
+
suggested_next_call=None,
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
duplicate_resources = _find_duplicate_package_resources(items)
|
|
891
|
+
if duplicate_resources:
|
|
892
|
+
return _failed(
|
|
893
|
+
"PACKAGE_LAYOUT_DUPLICATE_ITEM",
|
|
894
|
+
"items contains duplicate apps or portals",
|
|
895
|
+
normalized_args=normalized_args,
|
|
896
|
+
details={"duplicates": [{"type": item[0], "id": item[1]} for item in duplicate_resources]},
|
|
897
|
+
suggested_next_call=None,
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
current_groups = _collect_backend_package_groups(current_items)
|
|
901
|
+
desired_groups = _collect_public_package_group_specs(items)
|
|
902
|
+
desired_group_ids = {
|
|
903
|
+
group_id for group_id in (_coerce_positive_int(group.get("group_id")) for group in desired_groups) if group_id is not None
|
|
904
|
+
}
|
|
905
|
+
deleted_group_ids = sorted(set(current_groups) - desired_group_ids)
|
|
906
|
+
|
|
907
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
908
|
+
needs_group_create = any(_coerce_positive_int(group.get("group_id")) is None for group in desired_groups)
|
|
909
|
+
needs_group_delete = bool(deleted_group_ids)
|
|
910
|
+
needs_edit_app = bool(items)
|
|
911
|
+
for required_permission in (
|
|
912
|
+
(["add_app"] if needs_group_create else [])
|
|
913
|
+
+ (["edit_app"] if needs_edit_app else [])
|
|
914
|
+
+ (["delete_app"] if needs_group_delete else [])
|
|
915
|
+
):
|
|
916
|
+
outcome = self._guard_package_permission(
|
|
917
|
+
profile=profile,
|
|
918
|
+
tag_id=package_id,
|
|
919
|
+
required_permission=required_permission,
|
|
920
|
+
normalized_args=normalized_args,
|
|
921
|
+
)
|
|
922
|
+
if outcome.block is not None:
|
|
923
|
+
return outcome.block
|
|
924
|
+
permission_outcomes.append(outcome)
|
|
925
|
+
|
|
926
|
+
group_ids_by_path: dict[tuple[int, ...], int] = {}
|
|
927
|
+
group_operations: list[JSONObject] = []
|
|
928
|
+
for group in desired_groups:
|
|
929
|
+
path = tuple(group.get("path") or ())
|
|
930
|
+
group_id = _coerce_positive_int(group.get("group_id"))
|
|
931
|
+
group_name = str(group.get("name") or "").strip()
|
|
932
|
+
if not group_name:
|
|
933
|
+
return _failed(
|
|
934
|
+
"PACKAGE_GROUP_NAME_REQUIRED",
|
|
935
|
+
"group items require name",
|
|
936
|
+
normalized_args=normalized_args,
|
|
937
|
+
details={"path": list(path)},
|
|
938
|
+
suggested_next_call=None,
|
|
939
|
+
)
|
|
940
|
+
if group_id is None:
|
|
941
|
+
try:
|
|
942
|
+
created = self.packages.package_group_create(profile=profile, tag_id=package_id, group_name=group_name)
|
|
943
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
944
|
+
api_error = _coerce_api_error(error)
|
|
945
|
+
return _failed_from_api_error(
|
|
946
|
+
"PACKAGE_GROUP_CREATE_FAILED",
|
|
947
|
+
api_error,
|
|
948
|
+
normalized_args=normalized_args,
|
|
949
|
+
details={"package_id": package_id, "group_name": group_name},
|
|
950
|
+
suggested_next_call=None,
|
|
951
|
+
)
|
|
952
|
+
group_id = _extract_package_group_id(created)
|
|
953
|
+
if group_id is None:
|
|
954
|
+
return _failed(
|
|
955
|
+
"PACKAGE_GROUP_CREATE_UNVERIFIED",
|
|
956
|
+
"created package group but could not read group_id",
|
|
957
|
+
normalized_args=normalized_args,
|
|
958
|
+
details={"package_id": package_id, "group_name": group_name, "create_result": created},
|
|
959
|
+
suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "package_id": package_id}},
|
|
960
|
+
)
|
|
961
|
+
group_operations.append({"action": "create", "group_id": group_id, "name": group_name})
|
|
962
|
+
elif current_groups.get(group_id) != group_name:
|
|
963
|
+
try:
|
|
964
|
+
self.packages.package_group_update(profile=profile, tag_id=package_id, group_id=group_id, group_name=group_name)
|
|
965
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
966
|
+
api_error = _coerce_api_error(error)
|
|
967
|
+
return _failed_from_api_error(
|
|
968
|
+
"PACKAGE_GROUP_UPDATE_FAILED",
|
|
969
|
+
api_error,
|
|
970
|
+
normalized_args=normalized_args,
|
|
971
|
+
details={"package_id": package_id, "group_id": group_id, "group_name": group_name},
|
|
972
|
+
suggested_next_call=None,
|
|
973
|
+
)
|
|
974
|
+
group_operations.append({"action": "update", "group_id": group_id, "name": group_name})
|
|
975
|
+
group_ids_by_path[path] = group_id
|
|
976
|
+
|
|
977
|
+
try:
|
|
978
|
+
backend_items = _backend_package_items_from_public_items(items, group_ids_by_path)
|
|
979
|
+
except ValueError as error:
|
|
980
|
+
return _failed(
|
|
981
|
+
"PACKAGE_LAYOUT_INVALID",
|
|
982
|
+
str(error),
|
|
983
|
+
normalized_args=normalized_args,
|
|
984
|
+
details={"package_id": package_id},
|
|
985
|
+
suggested_next_call=None,
|
|
986
|
+
)
|
|
987
|
+
for group_id in deleted_group_ids:
|
|
988
|
+
backend_items.append({"itemType": 3, "groupId": group_id, "title": current_groups.get(group_id) or "", "subItems": []})
|
|
989
|
+
|
|
990
|
+
try:
|
|
991
|
+
sort_result = self.packages.package_sort_items(profile=profile, tag_id=package_id, tag_items=backend_items)
|
|
992
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
993
|
+
api_error = _coerce_api_error(error)
|
|
994
|
+
return _failed_from_api_error(
|
|
995
|
+
"PACKAGE_LAYOUT_SORT_FAILED",
|
|
996
|
+
api_error,
|
|
997
|
+
normalized_args=normalized_args,
|
|
998
|
+
details={"package_id": package_id},
|
|
999
|
+
suggested_next_call=None,
|
|
1000
|
+
)
|
|
1001
|
+
|
|
1002
|
+
for group_id in deleted_group_ids:
|
|
1003
|
+
try:
|
|
1004
|
+
self.packages.package_group_delete(profile=profile, tag_id=package_id, group_id=group_id)
|
|
1005
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1006
|
+
api_error = _coerce_api_error(error)
|
|
1007
|
+
return _apply_permission_outcomes(
|
|
1008
|
+
_failed_from_api_error(
|
|
1009
|
+
"PACKAGE_GROUP_DELETE_FAILED",
|
|
1010
|
+
api_error,
|
|
1011
|
+
normalized_args=normalized_args,
|
|
1012
|
+
details={"package_id": package_id, "group_id": group_id},
|
|
1013
|
+
suggested_next_call=None,
|
|
1014
|
+
),
|
|
1015
|
+
*permission_outcomes,
|
|
1016
|
+
)
|
|
1017
|
+
group_operations.append({"action": "delete", "group_id": group_id})
|
|
1018
|
+
|
|
1019
|
+
return _apply_permission_outcomes(
|
|
1020
|
+
{
|
|
1021
|
+
"status": "success",
|
|
1022
|
+
"error_code": None,
|
|
1023
|
+
"recoverable": False,
|
|
1024
|
+
"message": "applied package items",
|
|
1025
|
+
"normalized_args": normalized_args,
|
|
1026
|
+
"missing_fields": [],
|
|
1027
|
+
"allowed_values": {},
|
|
1028
|
+
"details": {"group_operations": group_operations, "sort_result": sort_result},
|
|
1029
|
+
"request_id": sort_result.get("request_id") if isinstance(sort_result, dict) else None,
|
|
1030
|
+
"suggested_next_call": None,
|
|
1031
|
+
"noop": False,
|
|
1032
|
+
"warnings": [],
|
|
1033
|
+
"verification": {"layout_applied": True},
|
|
1034
|
+
"verified": True,
|
|
1035
|
+
"package_id": package_id,
|
|
1036
|
+
},
|
|
1037
|
+
*permission_outcomes,
|
|
1038
|
+
)
|
|
1039
|
+
|
|
651
1040
|
def member_search(self, *, profile: str, query: str, page_num: int = 1, page_size: int = 20, contain_disable: bool = False) -> JSONObject:
|
|
652
1041
|
requested = str(query or "").strip()
|
|
653
1042
|
normalized_args = {
|
|
@@ -2505,6 +2894,11 @@ class AiBuilderFacade:
|
|
|
2505
2894
|
"PACKAGE_EDIT_APP_UNAUTHORIZED",
|
|
2506
2895
|
"current user does not have package edit-app permission on this package",
|
|
2507
2896
|
),
|
|
2897
|
+
"delete_app": (
|
|
2898
|
+
"can_delete_app",
|
|
2899
|
+
"PACKAGE_DELETE_APP_UNAUTHORIZED",
|
|
2900
|
+
"current user does not have package delete-app permission on this package",
|
|
2901
|
+
),
|
|
2508
2902
|
"edit_tag": (
|
|
2509
2903
|
"can_edit_tag",
|
|
2510
2904
|
"PACKAGE_EDIT_TAG_UNAUTHORIZED",
|
|
@@ -11904,6 +12298,259 @@ def _tag_items_include_app(tag_items: Any, app_key: str) -> bool:
|
|
|
11904
12298
|
return any(str(item.get("appKey") or "") == app_key for item in tag_items if isinstance(item, dict))
|
|
11905
12299
|
|
|
11906
12300
|
|
|
12301
|
+
def _public_package_items_from_tag_items(tag_items: Any) -> list[JSONObject]:
|
|
12302
|
+
if not isinstance(tag_items, list):
|
|
12303
|
+
return []
|
|
12304
|
+
public_items: list[JSONObject] = []
|
|
12305
|
+
for item in tag_items:
|
|
12306
|
+
if not isinstance(item, dict):
|
|
12307
|
+
continue
|
|
12308
|
+
item_type = _coerce_positive_int(item.get("itemType"))
|
|
12309
|
+
if item_type == 1:
|
|
12310
|
+
app_key = str(item.get("appKey") or "").strip()
|
|
12311
|
+
if not app_key:
|
|
12312
|
+
continue
|
|
12313
|
+
public_items.append(
|
|
12314
|
+
_compact_dict(
|
|
12315
|
+
{
|
|
12316
|
+
"type": "app",
|
|
12317
|
+
"app_key": app_key,
|
|
12318
|
+
"title": str(item.get("title") or item.get("formTitle") or "").strip() or None,
|
|
12319
|
+
"form_id": item.get("formId"),
|
|
12320
|
+
}
|
|
12321
|
+
)
|
|
12322
|
+
)
|
|
12323
|
+
continue
|
|
12324
|
+
if item_type == 2:
|
|
12325
|
+
dash_key = str(item.get("dashKey") or item.get("pageKey") or "").strip()
|
|
12326
|
+
if not dash_key:
|
|
12327
|
+
continue
|
|
12328
|
+
public_items.append(
|
|
12329
|
+
_compact_dict(
|
|
12330
|
+
{
|
|
12331
|
+
"type": "portal",
|
|
12332
|
+
"dash_key": dash_key,
|
|
12333
|
+
"title": str(item.get("title") or item.get("dashName") or "").strip() or None,
|
|
12334
|
+
}
|
|
12335
|
+
)
|
|
12336
|
+
)
|
|
12337
|
+
continue
|
|
12338
|
+
if item_type == 3:
|
|
12339
|
+
group_id = _coerce_positive_int(item.get("groupId"))
|
|
12340
|
+
public_items.append(
|
|
12341
|
+
_compact_dict(
|
|
12342
|
+
{
|
|
12343
|
+
"type": "group",
|
|
12344
|
+
"group_id": group_id,
|
|
12345
|
+
"name": str(item.get("title") or item.get("groupName") or "").strip() or None,
|
|
12346
|
+
"items": _public_package_items_from_tag_items(item.get("subItems")),
|
|
12347
|
+
}
|
|
12348
|
+
)
|
|
12349
|
+
)
|
|
12350
|
+
return public_items
|
|
12351
|
+
|
|
12352
|
+
|
|
12353
|
+
def _flatten_package_resource_identities(items: Any, *, public: bool) -> set[tuple[str, str]]:
|
|
12354
|
+
flattened: set[tuple[str, str]] = set()
|
|
12355
|
+
|
|
12356
|
+
def walk(value: Any) -> None:
|
|
12357
|
+
if isinstance(value, list):
|
|
12358
|
+
for child in value:
|
|
12359
|
+
walk(child)
|
|
12360
|
+
return
|
|
12361
|
+
if not isinstance(value, dict):
|
|
12362
|
+
return
|
|
12363
|
+
if public:
|
|
12364
|
+
item_type = str(value.get("type") or "").strip().lower()
|
|
12365
|
+
if item_type == "app" or value.get("app_key") or value.get("appKey"):
|
|
12366
|
+
app_key = str(value.get("app_key") or value.get("appKey") or "").strip()
|
|
12367
|
+
if app_key:
|
|
12368
|
+
flattened.add(("app", app_key))
|
|
12369
|
+
elif item_type == "portal" or value.get("dash_key") or value.get("dashKey"):
|
|
12370
|
+
dash_key = str(value.get("dash_key") or value.get("dashKey") or "").strip()
|
|
12371
|
+
if dash_key:
|
|
12372
|
+
flattened.add(("portal", dash_key))
|
|
12373
|
+
walk(value.get("items"))
|
|
12374
|
+
return
|
|
12375
|
+
item_type = _coerce_positive_int(value.get("itemType"))
|
|
12376
|
+
if item_type == 1:
|
|
12377
|
+
app_key = str(value.get("appKey") or "").strip()
|
|
12378
|
+
if app_key:
|
|
12379
|
+
flattened.add(("app", app_key))
|
|
12380
|
+
elif item_type == 2:
|
|
12381
|
+
dash_key = str(value.get("dashKey") or value.get("pageKey") or "").strip()
|
|
12382
|
+
if dash_key:
|
|
12383
|
+
flattened.add(("portal", dash_key))
|
|
12384
|
+
walk(value.get("subItems"))
|
|
12385
|
+
|
|
12386
|
+
walk(items)
|
|
12387
|
+
return flattened
|
|
12388
|
+
|
|
12389
|
+
|
|
12390
|
+
def _find_duplicate_package_resources(items: Any) -> list[tuple[str, str]]:
|
|
12391
|
+
seen: set[tuple[str, str]] = set()
|
|
12392
|
+
duplicates: set[tuple[str, str]] = set()
|
|
12393
|
+
|
|
12394
|
+
def walk(value: Any) -> None:
|
|
12395
|
+
if isinstance(value, list):
|
|
12396
|
+
for child in value:
|
|
12397
|
+
walk(child)
|
|
12398
|
+
return
|
|
12399
|
+
if not isinstance(value, dict):
|
|
12400
|
+
return
|
|
12401
|
+
item_type = str(value.get("type") or "").strip().lower()
|
|
12402
|
+
identity: tuple[str, str] | None = None
|
|
12403
|
+
if item_type == "app" or value.get("app_key") or value.get("appKey"):
|
|
12404
|
+
app_key = str(value.get("app_key") or value.get("appKey") or "").strip()
|
|
12405
|
+
if app_key:
|
|
12406
|
+
identity = ("app", app_key)
|
|
12407
|
+
elif item_type == "portal" or value.get("dash_key") or value.get("dashKey"):
|
|
12408
|
+
dash_key = str(value.get("dash_key") or value.get("dashKey") or "").strip()
|
|
12409
|
+
if dash_key:
|
|
12410
|
+
identity = ("portal", dash_key)
|
|
12411
|
+
if identity is not None:
|
|
12412
|
+
if identity in seen:
|
|
12413
|
+
duplicates.add(identity)
|
|
12414
|
+
seen.add(identity)
|
|
12415
|
+
walk(value.get("items"))
|
|
12416
|
+
|
|
12417
|
+
walk(items)
|
|
12418
|
+
return sorted(duplicates)
|
|
12419
|
+
|
|
12420
|
+
|
|
12421
|
+
def _collect_backend_package_groups(tag_items: Any) -> dict[int, str]:
|
|
12422
|
+
groups: dict[int, str] = {}
|
|
12423
|
+
|
|
12424
|
+
def walk(value: Any) -> None:
|
|
12425
|
+
if isinstance(value, list):
|
|
12426
|
+
for child in value:
|
|
12427
|
+
walk(child)
|
|
12428
|
+
return
|
|
12429
|
+
if not isinstance(value, dict):
|
|
12430
|
+
return
|
|
12431
|
+
if _coerce_positive_int(value.get("itemType")) == 3:
|
|
12432
|
+
group_id = _coerce_positive_int(value.get("groupId"))
|
|
12433
|
+
if group_id is not None:
|
|
12434
|
+
groups[group_id] = str(value.get("title") or value.get("groupName") or "").strip()
|
|
12435
|
+
walk(value.get("subItems"))
|
|
12436
|
+
|
|
12437
|
+
walk(tag_items)
|
|
12438
|
+
return groups
|
|
12439
|
+
|
|
12440
|
+
|
|
12441
|
+
def _collect_public_package_group_specs(items: Any, *, path: tuple[int, ...] = ()) -> list[JSONObject]:
|
|
12442
|
+
if not isinstance(items, list):
|
|
12443
|
+
return []
|
|
12444
|
+
groups: list[JSONObject] = []
|
|
12445
|
+
for index, item in enumerate(items):
|
|
12446
|
+
if not isinstance(item, dict):
|
|
12447
|
+
continue
|
|
12448
|
+
item_type = str(item.get("type") or "").strip().lower()
|
|
12449
|
+
child_path = (*path, index)
|
|
12450
|
+
if item_type == "group" or isinstance(item.get("items"), list):
|
|
12451
|
+
groups.append(
|
|
12452
|
+
{
|
|
12453
|
+
"path": list(child_path),
|
|
12454
|
+
"group_id": _coerce_positive_int(item.get("group_id") or item.get("groupId")),
|
|
12455
|
+
"name": str(item.get("name") or item.get("title") or item.get("group_name") or "").strip(),
|
|
12456
|
+
}
|
|
12457
|
+
)
|
|
12458
|
+
groups.extend(_collect_public_package_group_specs(item.get("items"), path=child_path))
|
|
12459
|
+
return groups
|
|
12460
|
+
|
|
12461
|
+
|
|
12462
|
+
def _backend_package_items_from_public_items(items: list[dict[str, Any]], group_ids_by_path: dict[tuple[int, ...], int], *, path: tuple[int, ...] = ()) -> list[JSONObject]:
|
|
12463
|
+
backend_items: list[JSONObject] = []
|
|
12464
|
+
for index, item in enumerate(items):
|
|
12465
|
+
if not isinstance(item, dict):
|
|
12466
|
+
raise ValueError(f"items[{index}] must be an object")
|
|
12467
|
+
item_type = str(item.get("type") or "").strip().lower()
|
|
12468
|
+
child_path = (*path, index)
|
|
12469
|
+
if item_type == "app" or item.get("app_key") or item.get("appKey"):
|
|
12470
|
+
app_key = str(item.get("app_key") or item.get("appKey") or "").strip()
|
|
12471
|
+
if not app_key:
|
|
12472
|
+
raise ValueError(f"items[{index}].app_key is required")
|
|
12473
|
+
backend_items.append(
|
|
12474
|
+
_compact_dict(
|
|
12475
|
+
{
|
|
12476
|
+
"itemType": 1,
|
|
12477
|
+
"appKey": app_key,
|
|
12478
|
+
"title": str(item.get("title") or item.get("name") or "").strip() or None,
|
|
12479
|
+
"formId": item.get("form_id") or item.get("formId"),
|
|
12480
|
+
}
|
|
12481
|
+
)
|
|
12482
|
+
)
|
|
12483
|
+
continue
|
|
12484
|
+
if item_type == "portal" or item.get("dash_key") or item.get("dashKey"):
|
|
12485
|
+
dash_key = str(item.get("dash_key") or item.get("dashKey") or "").strip()
|
|
12486
|
+
if not dash_key:
|
|
12487
|
+
raise ValueError(f"items[{index}].dash_key is required")
|
|
12488
|
+
backend_items.append(
|
|
12489
|
+
_compact_dict(
|
|
12490
|
+
{
|
|
12491
|
+
"itemType": 2,
|
|
12492
|
+
"dashKey": dash_key,
|
|
12493
|
+
"title": str(item.get("title") or item.get("name") or "").strip() or None,
|
|
12494
|
+
}
|
|
12495
|
+
)
|
|
12496
|
+
)
|
|
12497
|
+
continue
|
|
12498
|
+
if item_type == "group" or isinstance(item.get("items"), list):
|
|
12499
|
+
group_id = group_ids_by_path.get(child_path) or _coerce_positive_int(item.get("group_id") or item.get("groupId"))
|
|
12500
|
+
if group_id is None:
|
|
12501
|
+
raise ValueError(f"items[{index}].group_id is required after group creation")
|
|
12502
|
+
group_name = str(item.get("name") or item.get("title") or item.get("group_name") or "").strip()
|
|
12503
|
+
if not group_name:
|
|
12504
|
+
raise ValueError(f"items[{index}].name is required")
|
|
12505
|
+
child_items = item.get("items") if isinstance(item.get("items"), list) else []
|
|
12506
|
+
backend_items.append(
|
|
12507
|
+
{
|
|
12508
|
+
"itemType": 3,
|
|
12509
|
+
"groupId": group_id,
|
|
12510
|
+
"title": group_name,
|
|
12511
|
+
"subItems": _backend_package_items_from_public_items(child_items, group_ids_by_path, path=child_path),
|
|
12512
|
+
}
|
|
12513
|
+
)
|
|
12514
|
+
continue
|
|
12515
|
+
raise ValueError(f"items[{index}].type must be app, portal, or group")
|
|
12516
|
+
return backend_items
|
|
12517
|
+
|
|
12518
|
+
|
|
12519
|
+
def _extract_package_group_id(value: Any) -> int | None:
|
|
12520
|
+
if isinstance(value, dict):
|
|
12521
|
+
direct = _coerce_positive_int(value.get("groupId") or value.get("group_id") or value.get("id"))
|
|
12522
|
+
if direct is not None:
|
|
12523
|
+
return direct
|
|
12524
|
+
for nested_key in ("result", "data", "group"):
|
|
12525
|
+
nested = value.get(nested_key)
|
|
12526
|
+
nested_id = _extract_package_group_id(nested)
|
|
12527
|
+
if nested_id is not None:
|
|
12528
|
+
return nested_id
|
|
12529
|
+
if isinstance(value, list):
|
|
12530
|
+
for item in value:
|
|
12531
|
+
nested_id = _extract_package_group_id(item)
|
|
12532
|
+
if nested_id is not None:
|
|
12533
|
+
return nested_id
|
|
12534
|
+
return None
|
|
12535
|
+
|
|
12536
|
+
|
|
12537
|
+
def _publicize_package_apply_failure(result: JSONObject, *, profile: str, normalized_args: JSONObject) -> JSONObject:
|
|
12538
|
+
public_result = deepcopy(result)
|
|
12539
|
+
public_result["normalized_args"] = deepcopy(normalized_args)
|
|
12540
|
+
suggested = public_result.get("suggested_next_call")
|
|
12541
|
+
if isinstance(suggested, dict):
|
|
12542
|
+
public_result["suggested_next_call"] = {
|
|
12543
|
+
"tool_name": "package_apply",
|
|
12544
|
+
"arguments": {"profile": profile, **deepcopy(normalized_args)},
|
|
12545
|
+
}
|
|
12546
|
+
for key in ("tag_id", "tag_name", "tag_icon"):
|
|
12547
|
+
public_result.pop(key, None)
|
|
12548
|
+
details = public_result.get("details")
|
|
12549
|
+
if isinstance(details, dict) and "tag_id" in details:
|
|
12550
|
+
details["package_id"] = details.pop("tag_id")
|
|
12551
|
+
return public_result
|
|
12552
|
+
|
|
12553
|
+
|
|
11907
12554
|
def _verify_package_attachment(packages: PackageTools, *, profile: str, tag_id: int, app_key: str, attempts: int = 2) -> JSONObject:
|
|
11908
12555
|
last_result: JSONObject = {"result": {}}
|
|
11909
12556
|
for _ in range(max(attempts, 1)):
|