@josephyan/qingflow-cli 0.2.0-beta.82 → 0.2.0-beta.84
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 +779 -20
- 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,59 @@ 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
|
+
)
|
|
453
|
+
public_items = _public_package_items_from_tag_items(source.get("tagItems") or base.get("tagItems"))
|
|
454
|
+
item_count = summary.get("itemCount")
|
|
455
|
+
if not isinstance(item_count, int) or item_count < 0 or (item_count == 0 and public_items):
|
|
456
|
+
item_count = len(public_items)
|
|
428
457
|
return {
|
|
429
458
|
"status": "success",
|
|
430
459
|
"error_code": None,
|
|
431
460
|
"recoverable": False,
|
|
432
|
-
"message": "read package
|
|
461
|
+
"message": "read package",
|
|
433
462
|
"normalized_args": normalized_args,
|
|
434
463
|
"missing_fields": [],
|
|
435
464
|
"allowed_values": {},
|
|
@@ -437,21 +466,183 @@ class AiBuilderFacade:
|
|
|
437
466
|
"request_id": None,
|
|
438
467
|
"suggested_next_call": None,
|
|
439
468
|
"noop": False,
|
|
440
|
-
"warnings":
|
|
469
|
+
"warnings": warnings,
|
|
441
470
|
"verification": {"package_exists": True},
|
|
442
471
|
"verified": True,
|
|
443
|
-
"
|
|
444
|
-
"
|
|
445
|
-
"
|
|
446
|
-
"publish_status":
|
|
447
|
-
"item_count":
|
|
448
|
-
"item_preview": deepcopy(summary.get("itemPreview") or []),
|
|
472
|
+
"package_id": _coerce_positive_int(source.get("tagId") or base.get("tagId")) or effective_package_id,
|
|
473
|
+
"package_name": str(source.get("tagName") or base.get("tagName") or "").strip() or None,
|
|
474
|
+
"icon": str(source.get("tagIcon") or base.get("tagIcon") or "").strip() or None,
|
|
475
|
+
"publish_status": source.get("publishStatus") if source.get("publishStatus") is not None else base.get("publishStatus"),
|
|
476
|
+
"item_count": item_count,
|
|
449
477
|
"add_app_status": base.get("addAppStatus") if base.get("addAppStatus") is not None else summary.get("addAppStatus"),
|
|
450
478
|
"edit_app_status": base.get("editAppStatus") if base.get("editAppStatus") is not None else summary.get("editAppStatus"),
|
|
451
479
|
"del_app_status": base.get("delAppStatus") if base.get("delAppStatus") is not None else summary.get("delAppStatus"),
|
|
452
480
|
"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
|
|
481
|
+
"visibility": _public_visibility_from_member_auth(base.get("auth") or source.get("auth")),
|
|
482
|
+
"items": public_items,
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
def package_apply(
|
|
486
|
+
self,
|
|
487
|
+
*,
|
|
488
|
+
profile: str,
|
|
489
|
+
package_id: int | None = None,
|
|
490
|
+
package_name: str | None = None,
|
|
491
|
+
create_if_missing: bool = False,
|
|
492
|
+
icon: str | None = None,
|
|
493
|
+
color: str | None = None,
|
|
494
|
+
visibility: VisibilityPatch | None = None,
|
|
495
|
+
items: list[dict[str, Any]] | None = None,
|
|
496
|
+
allow_detach: bool = False,
|
|
497
|
+
) -> JSONObject:
|
|
498
|
+
requested_name = str(package_name or "").strip()
|
|
499
|
+
normalized_args: JSONObject = {
|
|
500
|
+
"package_id": package_id,
|
|
501
|
+
**({"package_name": requested_name} if requested_name else {}),
|
|
502
|
+
"create_if_missing": bool(create_if_missing),
|
|
503
|
+
**({"icon": icon} if icon else {}),
|
|
504
|
+
**({"color": color} if color else {}),
|
|
505
|
+
**({"visibility": visibility.model_dump(mode="json")} if visibility is not None else {}),
|
|
506
|
+
**({"items": deepcopy(items)} if items is not None else {}),
|
|
507
|
+
"allow_detach": bool(allow_detach),
|
|
508
|
+
}
|
|
509
|
+
effective_package_id = _coerce_positive_int(package_id)
|
|
510
|
+
created = False
|
|
511
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
512
|
+
|
|
513
|
+
if effective_package_id is None:
|
|
514
|
+
if not create_if_missing:
|
|
515
|
+
return _failed(
|
|
516
|
+
"PACKAGE_ID_REQUIRED",
|
|
517
|
+
"package_id is required unless create_if_missing=true",
|
|
518
|
+
normalized_args=normalized_args,
|
|
519
|
+
suggested_next_call=None,
|
|
520
|
+
)
|
|
521
|
+
if not requested_name:
|
|
522
|
+
return _failed(
|
|
523
|
+
"PACKAGE_NAME_REQUIRED",
|
|
524
|
+
"package_name is required when create_if_missing=true",
|
|
525
|
+
normalized_args=normalized_args,
|
|
526
|
+
suggested_next_call=None,
|
|
527
|
+
)
|
|
528
|
+
create_result = self.package_create(
|
|
529
|
+
profile=profile,
|
|
530
|
+
package_name=requested_name,
|
|
531
|
+
icon=icon,
|
|
532
|
+
color=color,
|
|
533
|
+
visibility=visibility,
|
|
534
|
+
)
|
|
535
|
+
if create_result.get("status") not in {"success", "partial_success"}:
|
|
536
|
+
return _publicize_package_apply_failure(create_result, profile=profile, normalized_args=normalized_args)
|
|
537
|
+
effective_package_id = _coerce_positive_int(create_result.get("tag_id") or create_result.get("package_id"))
|
|
538
|
+
if effective_package_id is None:
|
|
539
|
+
return _failed(
|
|
540
|
+
"PACKAGE_CREATE_UNVERIFIED",
|
|
541
|
+
"created package but could not verify package_id",
|
|
542
|
+
normalized_args=normalized_args,
|
|
543
|
+
details={"create_result": create_result},
|
|
544
|
+
suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "package_id": package_id}},
|
|
545
|
+
)
|
|
546
|
+
normalized_args["package_id"] = effective_package_id
|
|
547
|
+
created = True
|
|
548
|
+
|
|
549
|
+
metadata_requested = bool(requested_name or icon or color or visibility is not None)
|
|
550
|
+
if metadata_requested and not created:
|
|
551
|
+
edit_tag_outcome = self._guard_package_permission(
|
|
552
|
+
profile=profile,
|
|
553
|
+
tag_id=effective_package_id,
|
|
554
|
+
required_permission="edit_tag",
|
|
555
|
+
normalized_args=normalized_args,
|
|
556
|
+
)
|
|
557
|
+
if edit_tag_outcome.block is not None:
|
|
558
|
+
return edit_tag_outcome.block
|
|
559
|
+
permission_outcomes.append(edit_tag_outcome)
|
|
560
|
+
update_result = self.package_update(
|
|
561
|
+
profile=profile,
|
|
562
|
+
tag_id=effective_package_id,
|
|
563
|
+
package_name=requested_name or None,
|
|
564
|
+
icon=icon,
|
|
565
|
+
color=color,
|
|
566
|
+
visibility=visibility,
|
|
567
|
+
)
|
|
568
|
+
if update_result.get("status") not in {"success", "partial_success"}:
|
|
569
|
+
return _publicize_package_apply_failure(update_result, profile=profile, normalized_args=normalized_args)
|
|
570
|
+
|
|
571
|
+
layout_result: JSONObject | None = None
|
|
572
|
+
if items is not None:
|
|
573
|
+
if not isinstance(items, list):
|
|
574
|
+
return _failed(
|
|
575
|
+
"PACKAGE_ITEMS_INVALID",
|
|
576
|
+
"items must be a list",
|
|
577
|
+
normalized_args=normalized_args,
|
|
578
|
+
suggested_next_call=None,
|
|
579
|
+
)
|
|
580
|
+
layout_result = self._apply_package_items(
|
|
581
|
+
profile=profile,
|
|
582
|
+
package_id=effective_package_id,
|
|
583
|
+
items=items,
|
|
584
|
+
allow_detach=allow_detach,
|
|
585
|
+
normalized_args=normalized_args,
|
|
586
|
+
)
|
|
587
|
+
if layout_result.get("status") not in {"success", "partial_success"}:
|
|
588
|
+
return _apply_permission_outcomes(layout_result, *permission_outcomes)
|
|
589
|
+
|
|
590
|
+
verification = self.package_get(profile=profile, package_id=effective_package_id)
|
|
591
|
+
if verification.get("status") != "success":
|
|
592
|
+
return _apply_permission_outcomes(verification, *permission_outcomes)
|
|
593
|
+
expected_visibility = None
|
|
594
|
+
if visibility is not None:
|
|
595
|
+
try:
|
|
596
|
+
expected_visibility = _public_visibility_from_member_auth(
|
|
597
|
+
self._compile_visibility_to_member_auth(profile=profile, visibility=visibility)
|
|
598
|
+
)
|
|
599
|
+
except VisibilityResolutionError:
|
|
600
|
+
expected_visibility = None
|
|
601
|
+
response: JSONObject = {
|
|
602
|
+
"status": "success",
|
|
603
|
+
"error_code": None,
|
|
604
|
+
"recoverable": False,
|
|
605
|
+
"message": "applied package",
|
|
606
|
+
"normalized_args": normalized_args,
|
|
607
|
+
"missing_fields": [],
|
|
608
|
+
"allowed_values": {},
|
|
609
|
+
"details": {"layout_result": layout_result} if layout_result is not None else {},
|
|
610
|
+
"request_id": None,
|
|
611
|
+
"suggested_next_call": None,
|
|
612
|
+
"noop": not (created or metadata_requested or items is not None),
|
|
613
|
+
"warnings": [],
|
|
614
|
+
"verification": {
|
|
615
|
+
"package_exists": True,
|
|
616
|
+
"package_created": created,
|
|
617
|
+
"layout_applied": items is not None,
|
|
618
|
+
"visibility_verified": None
|
|
619
|
+
if expected_visibility is None
|
|
620
|
+
else _visibility_matches_expected(verification.get("visibility"), expected_visibility),
|
|
621
|
+
},
|
|
622
|
+
"verified": True,
|
|
623
|
+
**{
|
|
624
|
+
key: deepcopy(value)
|
|
625
|
+
for key, value in verification.items()
|
|
626
|
+
if key
|
|
627
|
+
not in {
|
|
628
|
+
"status",
|
|
629
|
+
"error_code",
|
|
630
|
+
"recoverable",
|
|
631
|
+
"message",
|
|
632
|
+
"normalized_args",
|
|
633
|
+
"missing_fields",
|
|
634
|
+
"allowed_values",
|
|
635
|
+
"details",
|
|
636
|
+
"request_id",
|
|
637
|
+
"suggested_next_call",
|
|
638
|
+
"noop",
|
|
639
|
+
"warnings",
|
|
640
|
+
"verification",
|
|
641
|
+
"verified",
|
|
642
|
+
}
|
|
643
|
+
},
|
|
454
644
|
}
|
|
645
|
+
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
455
646
|
|
|
456
647
|
def package_update(
|
|
457
648
|
self,
|
|
@@ -524,7 +715,7 @@ class AiBuilderFacade:
|
|
|
524
715
|
details={"tag_id": tag_id},
|
|
525
716
|
suggested_next_call={"tool_name": "package_update", "arguments": {"profile": profile, **normalized_args}},
|
|
526
717
|
)
|
|
527
|
-
verification = self.package_get(profile=profile,
|
|
718
|
+
verification = self.package_get(profile=profile, package_id=tag_id)
|
|
528
719
|
if verification.get("status") != "success":
|
|
529
720
|
return verification
|
|
530
721
|
return {
|
|
@@ -648,6 +839,217 @@ class AiBuilderFacade:
|
|
|
648
839
|
"retried": bool(listed.get("retried", False)),
|
|
649
840
|
}
|
|
650
841
|
|
|
842
|
+
def _apply_package_items(
|
|
843
|
+
self,
|
|
844
|
+
*,
|
|
845
|
+
profile: str,
|
|
846
|
+
package_id: int,
|
|
847
|
+
items: list[dict[str, Any]],
|
|
848
|
+
allow_detach: bool,
|
|
849
|
+
normalized_args: JSONObject,
|
|
850
|
+
) -> JSONObject:
|
|
851
|
+
current_result: JSONObject | None = None
|
|
852
|
+
try:
|
|
853
|
+
current_result = self.packages.package_get(profile=profile, tag_id=package_id, include_raw=True)
|
|
854
|
+
except (QingflowApiError, RuntimeError) as detail_error:
|
|
855
|
+
detail_api_error = _coerce_api_error(detail_error)
|
|
856
|
+
try:
|
|
857
|
+
current_result = self.packages.package_get_base(profile=profile, tag_id=package_id, include_raw=True)
|
|
858
|
+
except (QingflowApiError, RuntimeError) as base_error:
|
|
859
|
+
api_error = _coerce_api_error(base_error)
|
|
860
|
+
return _failed_from_api_error(
|
|
861
|
+
"PACKAGE_LAYOUT_READ_FAILED",
|
|
862
|
+
api_error,
|
|
863
|
+
normalized_args=normalized_args,
|
|
864
|
+
details={
|
|
865
|
+
"package_id": package_id,
|
|
866
|
+
"detail_read_error": _transport_error_payload(detail_api_error),
|
|
867
|
+
},
|
|
868
|
+
suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "package_id": package_id}},
|
|
869
|
+
)
|
|
870
|
+
current_raw = current_result.get("result") if isinstance(current_result.get("result"), dict) else {}
|
|
871
|
+
raw_tag_items = current_raw.get("tagItems")
|
|
872
|
+
if not isinstance(raw_tag_items, list):
|
|
873
|
+
return _failed(
|
|
874
|
+
"PACKAGE_LAYOUT_UNREADABLE",
|
|
875
|
+
"package items could not be read safely",
|
|
876
|
+
normalized_args=normalized_args,
|
|
877
|
+
details={"package_id": package_id},
|
|
878
|
+
suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "package_id": package_id}},
|
|
879
|
+
)
|
|
880
|
+
current_items = raw_tag_items
|
|
881
|
+
|
|
882
|
+
current_group_specs = _collect_backend_package_group_specs(current_items)
|
|
883
|
+
normalized_items, group_resolution_issues = _align_public_package_group_ids(items, current_group_specs=current_group_specs)
|
|
884
|
+
if group_resolution_issues:
|
|
885
|
+
return _failed(
|
|
886
|
+
"PACKAGE_GROUP_AMBIGUOUS",
|
|
887
|
+
"items contains group names that match multiple existing package groups; pass explicit group_id to disambiguate",
|
|
888
|
+
normalized_args=normalized_args,
|
|
889
|
+
details={"package_id": package_id, "issues": group_resolution_issues},
|
|
890
|
+
suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "package_id": package_id}},
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
current_resources = _flatten_package_resource_identities(current_items, public=False)
|
|
894
|
+
desired_resources = _flatten_package_resource_identities(normalized_items, public=True)
|
|
895
|
+
missing_resources = sorted(current_resources - desired_resources)
|
|
896
|
+
if missing_resources and not allow_detach:
|
|
897
|
+
return _failed(
|
|
898
|
+
"PACKAGE_LAYOUT_ITEMS_MISSING",
|
|
899
|
+
"items omits existing apps or portals; pass allow_detach=true to remove them",
|
|
900
|
+
normalized_args=normalized_args,
|
|
901
|
+
details={"package_id": package_id, "missing_items": [{"type": item[0], "id": item[1]} for item in missing_resources]},
|
|
902
|
+
suggested_next_call=None,
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
duplicate_resources = _find_duplicate_package_resources(normalized_items)
|
|
906
|
+
if duplicate_resources:
|
|
907
|
+
return _failed(
|
|
908
|
+
"PACKAGE_LAYOUT_DUPLICATE_ITEM",
|
|
909
|
+
"items contains duplicate apps or portals",
|
|
910
|
+
normalized_args=normalized_args,
|
|
911
|
+
details={"duplicates": [{"type": item[0], "id": item[1]} for item in duplicate_resources]},
|
|
912
|
+
suggested_next_call=None,
|
|
913
|
+
)
|
|
914
|
+
|
|
915
|
+
current_groups = {int(spec["group_id"]): str(spec["name"] or "").strip() for spec in current_group_specs}
|
|
916
|
+
desired_groups = _collect_public_package_group_specs(normalized_items)
|
|
917
|
+
desired_group_ids = {
|
|
918
|
+
group_id for group_id in (_coerce_positive_int(group.get("group_id")) for group in desired_groups) if group_id is not None
|
|
919
|
+
}
|
|
920
|
+
deleted_group_ids = sorted(set(current_groups) - desired_group_ids)
|
|
921
|
+
|
|
922
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
923
|
+
needs_group_create = any(_coerce_positive_int(group.get("group_id")) is None for group in desired_groups)
|
|
924
|
+
needs_group_delete = bool(deleted_group_ids)
|
|
925
|
+
needs_edit_app = bool(normalized_items)
|
|
926
|
+
for required_permission in (
|
|
927
|
+
(["add_app"] if needs_group_create else [])
|
|
928
|
+
+ (["edit_app"] if needs_edit_app else [])
|
|
929
|
+
+ (["delete_app"] if needs_group_delete else [])
|
|
930
|
+
):
|
|
931
|
+
outcome = self._guard_package_permission(
|
|
932
|
+
profile=profile,
|
|
933
|
+
tag_id=package_id,
|
|
934
|
+
required_permission=required_permission,
|
|
935
|
+
normalized_args=normalized_args,
|
|
936
|
+
)
|
|
937
|
+
if outcome.block is not None:
|
|
938
|
+
return outcome.block
|
|
939
|
+
permission_outcomes.append(outcome)
|
|
940
|
+
|
|
941
|
+
group_ids_by_path: dict[tuple[int, ...], int] = {}
|
|
942
|
+
group_operations: list[JSONObject] = []
|
|
943
|
+
for group in desired_groups:
|
|
944
|
+
path = tuple(group.get("path") or ())
|
|
945
|
+
group_id = _coerce_positive_int(group.get("group_id"))
|
|
946
|
+
group_name = str(group.get("name") or "").strip()
|
|
947
|
+
if not group_name:
|
|
948
|
+
return _failed(
|
|
949
|
+
"PACKAGE_GROUP_NAME_REQUIRED",
|
|
950
|
+
"group items require name",
|
|
951
|
+
normalized_args=normalized_args,
|
|
952
|
+
details={"path": list(path)},
|
|
953
|
+
suggested_next_call=None,
|
|
954
|
+
)
|
|
955
|
+
if group_id is None:
|
|
956
|
+
try:
|
|
957
|
+
created = self.packages.package_group_create(profile=profile, tag_id=package_id, group_name=group_name)
|
|
958
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
959
|
+
api_error = _coerce_api_error(error)
|
|
960
|
+
return _failed_from_api_error(
|
|
961
|
+
"PACKAGE_GROUP_CREATE_FAILED",
|
|
962
|
+
api_error,
|
|
963
|
+
normalized_args=normalized_args,
|
|
964
|
+
details={"package_id": package_id, "group_name": group_name},
|
|
965
|
+
suggested_next_call=None,
|
|
966
|
+
)
|
|
967
|
+
group_id = _extract_package_group_id(created)
|
|
968
|
+
if group_id is None:
|
|
969
|
+
return _failed(
|
|
970
|
+
"PACKAGE_GROUP_CREATE_UNVERIFIED",
|
|
971
|
+
"created package group but could not read group_id",
|
|
972
|
+
normalized_args=normalized_args,
|
|
973
|
+
details={"package_id": package_id, "group_name": group_name, "create_result": created},
|
|
974
|
+
suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "package_id": package_id}},
|
|
975
|
+
)
|
|
976
|
+
group_operations.append({"action": "create", "group_id": group_id, "name": group_name})
|
|
977
|
+
elif current_groups.get(group_id) != group_name:
|
|
978
|
+
try:
|
|
979
|
+
self.packages.package_group_update(profile=profile, tag_id=package_id, group_id=group_id, group_name=group_name)
|
|
980
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
981
|
+
api_error = _coerce_api_error(error)
|
|
982
|
+
return _failed_from_api_error(
|
|
983
|
+
"PACKAGE_GROUP_UPDATE_FAILED",
|
|
984
|
+
api_error,
|
|
985
|
+
normalized_args=normalized_args,
|
|
986
|
+
details={"package_id": package_id, "group_id": group_id, "group_name": group_name},
|
|
987
|
+
suggested_next_call=None,
|
|
988
|
+
)
|
|
989
|
+
group_operations.append({"action": "update", "group_id": group_id, "name": group_name})
|
|
990
|
+
group_ids_by_path[path] = group_id
|
|
991
|
+
|
|
992
|
+
try:
|
|
993
|
+
backend_items = _backend_package_items_from_public_items(normalized_items, group_ids_by_path)
|
|
994
|
+
except ValueError as error:
|
|
995
|
+
return _failed(
|
|
996
|
+
"PACKAGE_LAYOUT_INVALID",
|
|
997
|
+
str(error),
|
|
998
|
+
normalized_args=normalized_args,
|
|
999
|
+
details={"package_id": package_id},
|
|
1000
|
+
suggested_next_call=None,
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
try:
|
|
1004
|
+
sort_result = self.packages.package_sort_items(profile=profile, tag_id=package_id, tag_items=backend_items)
|
|
1005
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1006
|
+
api_error = _coerce_api_error(error)
|
|
1007
|
+
return _failed_from_api_error(
|
|
1008
|
+
"PACKAGE_LAYOUT_SORT_FAILED",
|
|
1009
|
+
api_error,
|
|
1010
|
+
normalized_args=normalized_args,
|
|
1011
|
+
details={"package_id": package_id},
|
|
1012
|
+
suggested_next_call=None,
|
|
1013
|
+
)
|
|
1014
|
+
|
|
1015
|
+
for group_id in deleted_group_ids:
|
|
1016
|
+
try:
|
|
1017
|
+
self.packages.package_group_delete(profile=profile, tag_id=package_id, group_id=group_id)
|
|
1018
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1019
|
+
api_error = _coerce_api_error(error)
|
|
1020
|
+
return _apply_permission_outcomes(
|
|
1021
|
+
_failed_from_api_error(
|
|
1022
|
+
"PACKAGE_GROUP_DELETE_FAILED",
|
|
1023
|
+
api_error,
|
|
1024
|
+
normalized_args=normalized_args,
|
|
1025
|
+
details={"package_id": package_id, "group_id": group_id},
|
|
1026
|
+
suggested_next_call=None,
|
|
1027
|
+
),
|
|
1028
|
+
*permission_outcomes,
|
|
1029
|
+
)
|
|
1030
|
+
group_operations.append({"action": "delete", "group_id": group_id})
|
|
1031
|
+
|
|
1032
|
+
return _apply_permission_outcomes(
|
|
1033
|
+
{
|
|
1034
|
+
"status": "success",
|
|
1035
|
+
"error_code": None,
|
|
1036
|
+
"recoverable": False,
|
|
1037
|
+
"message": "applied package items",
|
|
1038
|
+
"normalized_args": normalized_args,
|
|
1039
|
+
"missing_fields": [],
|
|
1040
|
+
"allowed_values": {},
|
|
1041
|
+
"details": {"group_operations": group_operations, "sort_result": sort_result},
|
|
1042
|
+
"request_id": sort_result.get("request_id") if isinstance(sort_result, dict) else None,
|
|
1043
|
+
"suggested_next_call": None,
|
|
1044
|
+
"noop": False,
|
|
1045
|
+
"warnings": [],
|
|
1046
|
+
"verification": {"layout_applied": True},
|
|
1047
|
+
"verified": True,
|
|
1048
|
+
"package_id": package_id,
|
|
1049
|
+
},
|
|
1050
|
+
*permission_outcomes,
|
|
1051
|
+
)
|
|
1052
|
+
|
|
651
1053
|
def member_search(self, *, profile: str, query: str, page_num: int = 1, page_size: int = 20, contain_disable: bool = False) -> JSONObject:
|
|
652
1054
|
requested = str(query or "").strip()
|
|
653
1055
|
normalized_args = {
|
|
@@ -2505,6 +2907,11 @@ class AiBuilderFacade:
|
|
|
2505
2907
|
"PACKAGE_EDIT_APP_UNAUTHORIZED",
|
|
2506
2908
|
"current user does not have package edit-app permission on this package",
|
|
2507
2909
|
),
|
|
2910
|
+
"delete_app": (
|
|
2911
|
+
"can_delete_app",
|
|
2912
|
+
"PACKAGE_DELETE_APP_UNAUTHORIZED",
|
|
2913
|
+
"current user does not have package delete-app permission on this package",
|
|
2914
|
+
),
|
|
2508
2915
|
"edit_tag": (
|
|
2509
2916
|
"can_edit_tag",
|
|
2510
2917
|
"PACKAGE_EDIT_TAG_UNAUTHORIZED",
|
|
@@ -11904,6 +12311,358 @@ def _tag_items_include_app(tag_items: Any, app_key: str) -> bool:
|
|
|
11904
12311
|
return any(str(item.get("appKey") or "") == app_key for item in tag_items if isinstance(item, dict))
|
|
11905
12312
|
|
|
11906
12313
|
|
|
12314
|
+
def _public_package_items_from_tag_items(tag_items: Any) -> list[JSONObject]:
|
|
12315
|
+
if not isinstance(tag_items, list):
|
|
12316
|
+
return []
|
|
12317
|
+
public_items: list[JSONObject] = []
|
|
12318
|
+
for item in tag_items:
|
|
12319
|
+
if not isinstance(item, dict):
|
|
12320
|
+
continue
|
|
12321
|
+
item_type = _coerce_positive_int(item.get("itemType"))
|
|
12322
|
+
if item_type == 1:
|
|
12323
|
+
app_key = str(item.get("appKey") or "").strip()
|
|
12324
|
+
if not app_key:
|
|
12325
|
+
continue
|
|
12326
|
+
public_items.append(
|
|
12327
|
+
_compact_dict(
|
|
12328
|
+
{
|
|
12329
|
+
"type": "app",
|
|
12330
|
+
"app_key": app_key,
|
|
12331
|
+
"title": str(item.get("title") or item.get("formTitle") or "").strip() or None,
|
|
12332
|
+
"form_id": item.get("formId"),
|
|
12333
|
+
}
|
|
12334
|
+
)
|
|
12335
|
+
)
|
|
12336
|
+
continue
|
|
12337
|
+
if item_type == 2:
|
|
12338
|
+
dash_key = str(item.get("dashKey") or item.get("pageKey") or "").strip()
|
|
12339
|
+
if not dash_key:
|
|
12340
|
+
continue
|
|
12341
|
+
public_items.append(
|
|
12342
|
+
_compact_dict(
|
|
12343
|
+
{
|
|
12344
|
+
"type": "portal",
|
|
12345
|
+
"dash_key": dash_key,
|
|
12346
|
+
"title": str(item.get("title") or item.get("dashName") or "").strip() or None,
|
|
12347
|
+
}
|
|
12348
|
+
)
|
|
12349
|
+
)
|
|
12350
|
+
continue
|
|
12351
|
+
if item_type == 3:
|
|
12352
|
+
group_id = _coerce_positive_int(item.get("groupId"))
|
|
12353
|
+
public_items.append(
|
|
12354
|
+
_compact_dict(
|
|
12355
|
+
{
|
|
12356
|
+
"type": "group",
|
|
12357
|
+
"group_id": group_id,
|
|
12358
|
+
"name": str(item.get("title") or item.get("groupName") or "").strip() or None,
|
|
12359
|
+
"items": _public_package_items_from_tag_items(item.get("subItems")),
|
|
12360
|
+
}
|
|
12361
|
+
)
|
|
12362
|
+
)
|
|
12363
|
+
return public_items
|
|
12364
|
+
|
|
12365
|
+
|
|
12366
|
+
def _flatten_package_resource_identities(items: Any, *, public: bool) -> set[tuple[str, str]]:
|
|
12367
|
+
flattened: set[tuple[str, str]] = set()
|
|
12368
|
+
|
|
12369
|
+
def walk(value: Any) -> None:
|
|
12370
|
+
if isinstance(value, list):
|
|
12371
|
+
for child in value:
|
|
12372
|
+
walk(child)
|
|
12373
|
+
return
|
|
12374
|
+
if not isinstance(value, dict):
|
|
12375
|
+
return
|
|
12376
|
+
if public:
|
|
12377
|
+
item_type = str(value.get("type") or "").strip().lower()
|
|
12378
|
+
if item_type == "app" or value.get("app_key") or value.get("appKey"):
|
|
12379
|
+
app_key = str(value.get("app_key") or value.get("appKey") or "").strip()
|
|
12380
|
+
if app_key:
|
|
12381
|
+
flattened.add(("app", app_key))
|
|
12382
|
+
elif item_type == "portal" or value.get("dash_key") or value.get("dashKey"):
|
|
12383
|
+
dash_key = str(value.get("dash_key") or value.get("dashKey") or "").strip()
|
|
12384
|
+
if dash_key:
|
|
12385
|
+
flattened.add(("portal", dash_key))
|
|
12386
|
+
walk(value.get("items"))
|
|
12387
|
+
return
|
|
12388
|
+
item_type = _coerce_positive_int(value.get("itemType"))
|
|
12389
|
+
if item_type == 1:
|
|
12390
|
+
app_key = str(value.get("appKey") or "").strip()
|
|
12391
|
+
if app_key:
|
|
12392
|
+
flattened.add(("app", app_key))
|
|
12393
|
+
elif item_type == 2:
|
|
12394
|
+
dash_key = str(value.get("dashKey") or value.get("pageKey") or "").strip()
|
|
12395
|
+
if dash_key:
|
|
12396
|
+
flattened.add(("portal", dash_key))
|
|
12397
|
+
walk(value.get("subItems"))
|
|
12398
|
+
|
|
12399
|
+
walk(items)
|
|
12400
|
+
return flattened
|
|
12401
|
+
|
|
12402
|
+
|
|
12403
|
+
def _find_duplicate_package_resources(items: Any) -> list[tuple[str, str]]:
|
|
12404
|
+
seen: set[tuple[str, str]] = set()
|
|
12405
|
+
duplicates: set[tuple[str, str]] = set()
|
|
12406
|
+
|
|
12407
|
+
def walk(value: Any) -> None:
|
|
12408
|
+
if isinstance(value, list):
|
|
12409
|
+
for child in value:
|
|
12410
|
+
walk(child)
|
|
12411
|
+
return
|
|
12412
|
+
if not isinstance(value, dict):
|
|
12413
|
+
return
|
|
12414
|
+
item_type = str(value.get("type") or "").strip().lower()
|
|
12415
|
+
identity: tuple[str, str] | None = None
|
|
12416
|
+
if item_type == "app" or value.get("app_key") or value.get("appKey"):
|
|
12417
|
+
app_key = str(value.get("app_key") or value.get("appKey") or "").strip()
|
|
12418
|
+
if app_key:
|
|
12419
|
+
identity = ("app", app_key)
|
|
12420
|
+
elif item_type == "portal" or value.get("dash_key") or value.get("dashKey"):
|
|
12421
|
+
dash_key = str(value.get("dash_key") or value.get("dashKey") or "").strip()
|
|
12422
|
+
if dash_key:
|
|
12423
|
+
identity = ("portal", dash_key)
|
|
12424
|
+
if identity is not None:
|
|
12425
|
+
if identity in seen:
|
|
12426
|
+
duplicates.add(identity)
|
|
12427
|
+
seen.add(identity)
|
|
12428
|
+
walk(value.get("items"))
|
|
12429
|
+
|
|
12430
|
+
walk(items)
|
|
12431
|
+
return sorted(duplicates)
|
|
12432
|
+
|
|
12433
|
+
|
|
12434
|
+
def _collect_backend_package_groups(tag_items: Any) -> dict[int, str]:
|
|
12435
|
+
return {
|
|
12436
|
+
int(spec["group_id"]): str(spec["name"] or "").strip()
|
|
12437
|
+
for spec in _collect_backend_package_group_specs(tag_items)
|
|
12438
|
+
if _coerce_positive_int(spec.get("group_id")) is not None
|
|
12439
|
+
}
|
|
12440
|
+
|
|
12441
|
+
|
|
12442
|
+
def _collect_backend_package_group_specs(tag_items: Any, *, path: tuple[int, ...] = ()) -> list[JSONObject]:
|
|
12443
|
+
if not isinstance(tag_items, list):
|
|
12444
|
+
return []
|
|
12445
|
+
groups: list[JSONObject] = []
|
|
12446
|
+
for index, item in enumerate(tag_items):
|
|
12447
|
+
if not isinstance(item, dict):
|
|
12448
|
+
continue
|
|
12449
|
+
item_type = _coerce_positive_int(item.get("itemType"))
|
|
12450
|
+
child_path = (*path, index)
|
|
12451
|
+
if item_type == 3:
|
|
12452
|
+
group_id = _coerce_positive_int(item.get("groupId"))
|
|
12453
|
+
if group_id is None:
|
|
12454
|
+
continue
|
|
12455
|
+
child_items = item.get("subItems") if isinstance(item.get("subItems"), list) else []
|
|
12456
|
+
groups.append(
|
|
12457
|
+
{
|
|
12458
|
+
"path": list(child_path),
|
|
12459
|
+
"group_id": group_id,
|
|
12460
|
+
"name": str(item.get("title") or item.get("groupName") or "").strip(),
|
|
12461
|
+
"resource_signature": _package_resource_signature(child_items, public=False),
|
|
12462
|
+
}
|
|
12463
|
+
)
|
|
12464
|
+
groups.extend(_collect_backend_package_group_specs(child_items, path=child_path))
|
|
12465
|
+
return groups
|
|
12466
|
+
|
|
12467
|
+
|
|
12468
|
+
def _collect_public_package_group_specs(items: Any, *, path: tuple[int, ...] = ()) -> list[JSONObject]:
|
|
12469
|
+
if not isinstance(items, list):
|
|
12470
|
+
return []
|
|
12471
|
+
groups: list[JSONObject] = []
|
|
12472
|
+
for index, item in enumerate(items):
|
|
12473
|
+
if not isinstance(item, dict):
|
|
12474
|
+
continue
|
|
12475
|
+
item_type = str(item.get("type") or "").strip().lower()
|
|
12476
|
+
child_path = (*path, index)
|
|
12477
|
+
if item_type == "group" or isinstance(item.get("items"), list):
|
|
12478
|
+
groups.append(
|
|
12479
|
+
{
|
|
12480
|
+
"path": list(child_path),
|
|
12481
|
+
"group_id": _coerce_positive_int(item.get("group_id") or item.get("groupId")),
|
|
12482
|
+
"name": str(item.get("name") or item.get("title") or item.get("group_name") or "").strip(),
|
|
12483
|
+
}
|
|
12484
|
+
)
|
|
12485
|
+
groups.extend(_collect_public_package_group_specs(item.get("items"), path=child_path))
|
|
12486
|
+
return groups
|
|
12487
|
+
|
|
12488
|
+
|
|
12489
|
+
def _align_public_package_group_ids(
|
|
12490
|
+
items: list[dict[str, Any]],
|
|
12491
|
+
*,
|
|
12492
|
+
current_group_specs: list[JSONObject],
|
|
12493
|
+
) -> tuple[list[dict[str, Any]], list[JSONObject]]:
|
|
12494
|
+
normalized_items = deepcopy(items)
|
|
12495
|
+
used_group_ids: set[int] = set()
|
|
12496
|
+
issues: list[JSONObject] = []
|
|
12497
|
+
|
|
12498
|
+
def walk(nodes: list[dict[str, Any]], *, path: tuple[int, ...] = ()) -> None:
|
|
12499
|
+
for index, node in enumerate(nodes):
|
|
12500
|
+
if not isinstance(node, dict):
|
|
12501
|
+
continue
|
|
12502
|
+
item_type = str(node.get("type") or "").strip().lower()
|
|
12503
|
+
child_path = (*path, index)
|
|
12504
|
+
child_items = node.get("items") if isinstance(node.get("items"), list) else []
|
|
12505
|
+
if item_type == "group" or isinstance(node.get("items"), list):
|
|
12506
|
+
explicit_group_id = _coerce_positive_int(node.get("group_id") or node.get("groupId"))
|
|
12507
|
+
if explicit_group_id is not None:
|
|
12508
|
+
node["group_id"] = explicit_group_id
|
|
12509
|
+
used_group_ids.add(explicit_group_id)
|
|
12510
|
+
else:
|
|
12511
|
+
group_name = str(node.get("name") or node.get("title") or node.get("group_name") or "").strip()
|
|
12512
|
+
matched_group, ambiguity = _match_existing_package_group(
|
|
12513
|
+
group_name=group_name,
|
|
12514
|
+
child_items=child_items,
|
|
12515
|
+
path=child_path,
|
|
12516
|
+
current_group_specs=current_group_specs,
|
|
12517
|
+
used_group_ids=used_group_ids,
|
|
12518
|
+
)
|
|
12519
|
+
if ambiguity is not None:
|
|
12520
|
+
issues.append(ambiguity)
|
|
12521
|
+
elif matched_group is not None:
|
|
12522
|
+
matched_group_id = _coerce_positive_int(matched_group.get("group_id"))
|
|
12523
|
+
if matched_group_id is not None:
|
|
12524
|
+
node["group_id"] = matched_group_id
|
|
12525
|
+
used_group_ids.add(matched_group_id)
|
|
12526
|
+
walk(child_items, path=child_path)
|
|
12527
|
+
|
|
12528
|
+
walk(normalized_items)
|
|
12529
|
+
return normalized_items, issues
|
|
12530
|
+
|
|
12531
|
+
|
|
12532
|
+
def _match_existing_package_group(
|
|
12533
|
+
*,
|
|
12534
|
+
group_name: str,
|
|
12535
|
+
child_items: list[dict[str, Any]],
|
|
12536
|
+
path: tuple[int, ...],
|
|
12537
|
+
current_group_specs: list[JSONObject],
|
|
12538
|
+
used_group_ids: set[int],
|
|
12539
|
+
) -> tuple[JSONObject | None, JSONObject | None]:
|
|
12540
|
+
desired_signature = _package_resource_signature(child_items, public=True)
|
|
12541
|
+
candidates = [
|
|
12542
|
+
spec
|
|
12543
|
+
for spec in current_group_specs
|
|
12544
|
+
if _coerce_positive_int(spec.get("group_id")) is not None
|
|
12545
|
+
and int(spec["group_id"]) not in used_group_ids
|
|
12546
|
+
and str(spec.get("name") or "").strip() == group_name
|
|
12547
|
+
]
|
|
12548
|
+
exact_matches = [spec for spec in candidates if spec.get("resource_signature") == desired_signature]
|
|
12549
|
+
if len(exact_matches) == 1:
|
|
12550
|
+
return exact_matches[0], None
|
|
12551
|
+
path_matches = [spec for spec in candidates if tuple(spec.get("path") or ()) == path]
|
|
12552
|
+
if len(path_matches) == 1:
|
|
12553
|
+
return path_matches[0], None
|
|
12554
|
+
if len(candidates) == 1:
|
|
12555
|
+
return candidates[0], None
|
|
12556
|
+
ambiguous_matches = exact_matches if len(exact_matches) > 1 else candidates if len(candidates) > 1 else []
|
|
12557
|
+
if ambiguous_matches:
|
|
12558
|
+
return None, {
|
|
12559
|
+
"path": list(path),
|
|
12560
|
+
"name": group_name,
|
|
12561
|
+
"candidate_group_ids": [
|
|
12562
|
+
int(spec["group_id"])
|
|
12563
|
+
for spec in ambiguous_matches
|
|
12564
|
+
if _coerce_positive_int(spec.get("group_id")) is not None
|
|
12565
|
+
],
|
|
12566
|
+
}
|
|
12567
|
+
return None, None
|
|
12568
|
+
|
|
12569
|
+
|
|
12570
|
+
def _package_resource_signature(items: Any, *, public: bool) -> tuple[tuple[str, str], ...]:
|
|
12571
|
+
return tuple(sorted(_flatten_package_resource_identities(items, public=public)))
|
|
12572
|
+
|
|
12573
|
+
|
|
12574
|
+
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]:
|
|
12575
|
+
backend_items: list[JSONObject] = []
|
|
12576
|
+
for index, item in enumerate(items):
|
|
12577
|
+
if not isinstance(item, dict):
|
|
12578
|
+
raise ValueError(f"items[{index}] must be an object")
|
|
12579
|
+
item_type = str(item.get("type") or "").strip().lower()
|
|
12580
|
+
child_path = (*path, index)
|
|
12581
|
+
if item_type == "app" or item.get("app_key") or item.get("appKey"):
|
|
12582
|
+
app_key = str(item.get("app_key") or item.get("appKey") or "").strip()
|
|
12583
|
+
if not app_key:
|
|
12584
|
+
raise ValueError(f"items[{index}].app_key is required")
|
|
12585
|
+
backend_items.append(
|
|
12586
|
+
_compact_dict(
|
|
12587
|
+
{
|
|
12588
|
+
"itemType": 1,
|
|
12589
|
+
"appKey": app_key,
|
|
12590
|
+
"title": str(item.get("title") or item.get("name") or "").strip() or None,
|
|
12591
|
+
"formId": item.get("form_id") or item.get("formId"),
|
|
12592
|
+
}
|
|
12593
|
+
)
|
|
12594
|
+
)
|
|
12595
|
+
continue
|
|
12596
|
+
if item_type == "portal" or item.get("dash_key") or item.get("dashKey"):
|
|
12597
|
+
dash_key = str(item.get("dash_key") or item.get("dashKey") or "").strip()
|
|
12598
|
+
if not dash_key:
|
|
12599
|
+
raise ValueError(f"items[{index}].dash_key is required")
|
|
12600
|
+
backend_items.append(
|
|
12601
|
+
_compact_dict(
|
|
12602
|
+
{
|
|
12603
|
+
"itemType": 2,
|
|
12604
|
+
"dashKey": dash_key,
|
|
12605
|
+
"title": str(item.get("title") or item.get("name") or "").strip() or None,
|
|
12606
|
+
}
|
|
12607
|
+
)
|
|
12608
|
+
)
|
|
12609
|
+
continue
|
|
12610
|
+
if item_type == "group" or isinstance(item.get("items"), list):
|
|
12611
|
+
group_id = group_ids_by_path.get(child_path) or _coerce_positive_int(item.get("group_id") or item.get("groupId"))
|
|
12612
|
+
if group_id is None:
|
|
12613
|
+
raise ValueError(f"items[{index}].group_id is required after group creation")
|
|
12614
|
+
group_name = str(item.get("name") or item.get("title") or item.get("group_name") or "").strip()
|
|
12615
|
+
if not group_name:
|
|
12616
|
+
raise ValueError(f"items[{index}].name is required")
|
|
12617
|
+
child_items = item.get("items") if isinstance(item.get("items"), list) else []
|
|
12618
|
+
backend_items.append(
|
|
12619
|
+
{
|
|
12620
|
+
"itemType": 3,
|
|
12621
|
+
"groupId": group_id,
|
|
12622
|
+
"title": group_name,
|
|
12623
|
+
"subItems": _backend_package_items_from_public_items(child_items, group_ids_by_path, path=child_path),
|
|
12624
|
+
}
|
|
12625
|
+
)
|
|
12626
|
+
continue
|
|
12627
|
+
raise ValueError(f"items[{index}].type must be app, portal, or group")
|
|
12628
|
+
return backend_items
|
|
12629
|
+
|
|
12630
|
+
|
|
12631
|
+
def _extract_package_group_id(value: Any) -> int | None:
|
|
12632
|
+
if isinstance(value, dict):
|
|
12633
|
+
direct = _coerce_positive_int(value.get("groupId") or value.get("group_id") or value.get("id"))
|
|
12634
|
+
if direct is not None:
|
|
12635
|
+
return direct
|
|
12636
|
+
for nested_key in ("result", "data", "group"):
|
|
12637
|
+
nested = value.get(nested_key)
|
|
12638
|
+
nested_id = _extract_package_group_id(nested)
|
|
12639
|
+
if nested_id is not None:
|
|
12640
|
+
return nested_id
|
|
12641
|
+
if isinstance(value, list):
|
|
12642
|
+
for item in value:
|
|
12643
|
+
nested_id = _extract_package_group_id(item)
|
|
12644
|
+
if nested_id is not None:
|
|
12645
|
+
return nested_id
|
|
12646
|
+
return None
|
|
12647
|
+
|
|
12648
|
+
|
|
12649
|
+
def _publicize_package_apply_failure(result: JSONObject, *, profile: str, normalized_args: JSONObject) -> JSONObject:
|
|
12650
|
+
public_result = deepcopy(result)
|
|
12651
|
+
public_result["normalized_args"] = deepcopy(normalized_args)
|
|
12652
|
+
suggested = public_result.get("suggested_next_call")
|
|
12653
|
+
if isinstance(suggested, dict):
|
|
12654
|
+
public_result["suggested_next_call"] = {
|
|
12655
|
+
"tool_name": "package_apply",
|
|
12656
|
+
"arguments": {"profile": profile, **deepcopy(normalized_args)},
|
|
12657
|
+
}
|
|
12658
|
+
for key in ("tag_id", "tag_name", "tag_icon"):
|
|
12659
|
+
public_result.pop(key, None)
|
|
12660
|
+
details = public_result.get("details")
|
|
12661
|
+
if isinstance(details, dict) and "tag_id" in details:
|
|
12662
|
+
details["package_id"] = details.pop("tag_id")
|
|
12663
|
+
return public_result
|
|
12664
|
+
|
|
12665
|
+
|
|
11907
12666
|
def _verify_package_attachment(packages: PackageTools, *, profile: str, tag_id: int, app_key: str, attempts: int = 2) -> JSONObject:
|
|
11908
12667
|
last_result: JSONObject = {"result": {}}
|
|
11909
12668
|
for _ in range(max(attempts, 1)):
|