@josephyan/qingflow-app-user-mcp 0.2.0-beta.83 → 0.2.0-beta.85
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 +159 -32
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.
|
|
6
|
+
npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.85
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.
|
|
12
|
+
npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.85 qingflow-app-user-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -450,6 +450,10 @@ class AiBuilderFacade:
|
|
|
450
450
|
"http_status": detail_read_error.http_status,
|
|
451
451
|
}
|
|
452
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)
|
|
453
457
|
return {
|
|
454
458
|
"status": "success",
|
|
455
459
|
"error_code": None,
|
|
@@ -469,13 +473,13 @@ class AiBuilderFacade:
|
|
|
469
473
|
"package_name": str(source.get("tagName") or base.get("tagName") or "").strip() or None,
|
|
470
474
|
"icon": str(source.get("tagIcon") or base.get("tagIcon") or "").strip() or None,
|
|
471
475
|
"publish_status": source.get("publishStatus") if source.get("publishStatus") is not None else base.get("publishStatus"),
|
|
472
|
-
"item_count":
|
|
476
|
+
"item_count": item_count,
|
|
473
477
|
"add_app_status": base.get("addAppStatus") if base.get("addAppStatus") is not None else summary.get("addAppStatus"),
|
|
474
478
|
"edit_app_status": base.get("editAppStatus") if base.get("editAppStatus") is not None else summary.get("editAppStatus"),
|
|
475
479
|
"del_app_status": base.get("delAppStatus") if base.get("delAppStatus") is not None else summary.get("delAppStatus"),
|
|
476
480
|
"edit_tag_status": base.get("editTagStatus") if base.get("editTagStatus") is not None else summary.get("editTagStatus"),
|
|
477
481
|
"visibility": _public_visibility_from_member_auth(base.get("auth") or source.get("auth")),
|
|
478
|
-
"items":
|
|
482
|
+
"items": public_items,
|
|
479
483
|
}
|
|
480
484
|
|
|
481
485
|
def package_apply(
|
|
@@ -844,15 +848,18 @@ class AiBuilderFacade:
|
|
|
844
848
|
allow_detach: bool,
|
|
845
849
|
normalized_args: JSONObject,
|
|
846
850
|
) -> JSONObject:
|
|
847
|
-
|
|
851
|
+
current_detail_result: JSONObject | None = None
|
|
852
|
+
current_base_result: JSONObject | None = None
|
|
853
|
+
detail_api_error: QingflowApiError | None = None
|
|
848
854
|
try:
|
|
849
|
-
|
|
855
|
+
current_detail_result = self.packages.package_get(profile=profile, tag_id=package_id, include_raw=True)
|
|
850
856
|
except (QingflowApiError, RuntimeError) as detail_error:
|
|
851
857
|
detail_api_error = _coerce_api_error(detail_error)
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
858
|
+
try:
|
|
859
|
+
current_base_result = self.packages.package_get_base(profile=profile, tag_id=package_id, include_raw=True)
|
|
860
|
+
except (QingflowApiError, RuntimeError) as base_error:
|
|
861
|
+
api_error = _coerce_api_error(base_error)
|
|
862
|
+
if current_detail_result is None:
|
|
856
863
|
return _failed_from_api_error(
|
|
857
864
|
"PACKAGE_LAYOUT_READ_FAILED",
|
|
858
865
|
api_error,
|
|
@@ -863,8 +870,20 @@ class AiBuilderFacade:
|
|
|
863
870
|
},
|
|
864
871
|
suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "package_id": package_id}},
|
|
865
872
|
)
|
|
866
|
-
|
|
867
|
-
|
|
873
|
+
|
|
874
|
+
detail_raw = (
|
|
875
|
+
current_detail_result.get("result")
|
|
876
|
+
if isinstance(current_detail_result, dict) and isinstance(current_detail_result.get("result"), dict)
|
|
877
|
+
else {}
|
|
878
|
+
)
|
|
879
|
+
base_raw = (
|
|
880
|
+
current_base_result.get("result")
|
|
881
|
+
if isinstance(current_base_result, dict) and isinstance(current_base_result.get("result"), dict)
|
|
882
|
+
else {}
|
|
883
|
+
)
|
|
884
|
+
detail_tag_items = detail_raw.get("tagItems") if isinstance(detail_raw.get("tagItems"), list) else None
|
|
885
|
+
base_tag_items = base_raw.get("tagItems") if isinstance(base_raw.get("tagItems"), list) else None
|
|
886
|
+
raw_tag_items = detail_tag_items if detail_tag_items else base_tag_items
|
|
868
887
|
if not isinstance(raw_tag_items, list):
|
|
869
888
|
return _failed(
|
|
870
889
|
"PACKAGE_LAYOUT_UNREADABLE",
|
|
@@ -875,8 +894,19 @@ class AiBuilderFacade:
|
|
|
875
894
|
)
|
|
876
895
|
current_items = raw_tag_items
|
|
877
896
|
|
|
897
|
+
current_group_specs = _collect_backend_package_group_specs(current_items)
|
|
898
|
+
normalized_items, group_resolution_issues = _align_public_package_group_ids(items, current_group_specs=current_group_specs)
|
|
899
|
+
if group_resolution_issues:
|
|
900
|
+
return _failed(
|
|
901
|
+
"PACKAGE_GROUP_AMBIGUOUS",
|
|
902
|
+
"items contains group names that match multiple existing package groups; pass explicit group_id to disambiguate",
|
|
903
|
+
normalized_args=normalized_args,
|
|
904
|
+
details={"package_id": package_id, "issues": group_resolution_issues},
|
|
905
|
+
suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "package_id": package_id}},
|
|
906
|
+
)
|
|
907
|
+
|
|
878
908
|
current_resources = _flatten_package_resource_identities(current_items, public=False)
|
|
879
|
-
desired_resources = _flatten_package_resource_identities(
|
|
909
|
+
desired_resources = _flatten_package_resource_identities(normalized_items, public=True)
|
|
880
910
|
missing_resources = sorted(current_resources - desired_resources)
|
|
881
911
|
if missing_resources and not allow_detach:
|
|
882
912
|
return _failed(
|
|
@@ -887,7 +917,7 @@ class AiBuilderFacade:
|
|
|
887
917
|
suggested_next_call=None,
|
|
888
918
|
)
|
|
889
919
|
|
|
890
|
-
duplicate_resources = _find_duplicate_package_resources(
|
|
920
|
+
duplicate_resources = _find_duplicate_package_resources(normalized_items)
|
|
891
921
|
if duplicate_resources:
|
|
892
922
|
return _failed(
|
|
893
923
|
"PACKAGE_LAYOUT_DUPLICATE_ITEM",
|
|
@@ -897,8 +927,8 @@ class AiBuilderFacade:
|
|
|
897
927
|
suggested_next_call=None,
|
|
898
928
|
)
|
|
899
929
|
|
|
900
|
-
current_groups =
|
|
901
|
-
desired_groups = _collect_public_package_group_specs(
|
|
930
|
+
current_groups = {int(spec["group_id"]): str(spec["name"] or "").strip() for spec in current_group_specs}
|
|
931
|
+
desired_groups = _collect_public_package_group_specs(normalized_items)
|
|
902
932
|
desired_group_ids = {
|
|
903
933
|
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
934
|
}
|
|
@@ -907,7 +937,7 @@ class AiBuilderFacade:
|
|
|
907
937
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
908
938
|
needs_group_create = any(_coerce_positive_int(group.get("group_id")) is None for group in desired_groups)
|
|
909
939
|
needs_group_delete = bool(deleted_group_ids)
|
|
910
|
-
needs_edit_app = bool(
|
|
940
|
+
needs_edit_app = bool(normalized_items)
|
|
911
941
|
for required_permission in (
|
|
912
942
|
(["add_app"] if needs_group_create else [])
|
|
913
943
|
+ (["edit_app"] if needs_edit_app else [])
|
|
@@ -975,7 +1005,7 @@ class AiBuilderFacade:
|
|
|
975
1005
|
group_ids_by_path[path] = group_id
|
|
976
1006
|
|
|
977
1007
|
try:
|
|
978
|
-
backend_items = _backend_package_items_from_public_items(
|
|
1008
|
+
backend_items = _backend_package_items_from_public_items(normalized_items, group_ids_by_path)
|
|
979
1009
|
except ValueError as error:
|
|
980
1010
|
return _failed(
|
|
981
1011
|
"PACKAGE_LAYOUT_INVALID",
|
|
@@ -984,8 +1014,6 @@ class AiBuilderFacade:
|
|
|
984
1014
|
details={"package_id": package_id},
|
|
985
1015
|
suggested_next_call=None,
|
|
986
1016
|
)
|
|
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
1017
|
|
|
990
1018
|
try:
|
|
991
1019
|
sort_result = self.packages.package_sort_items(profile=profile, tag_id=package_id, tag_items=backend_items)
|
|
@@ -12419,22 +12447,36 @@ def _find_duplicate_package_resources(items: Any) -> list[tuple[str, str]]:
|
|
|
12419
12447
|
|
|
12420
12448
|
|
|
12421
12449
|
def _collect_backend_package_groups(tag_items: Any) -> dict[int, str]:
|
|
12422
|
-
|
|
12450
|
+
return {
|
|
12451
|
+
int(spec["group_id"]): str(spec["name"] or "").strip()
|
|
12452
|
+
for spec in _collect_backend_package_group_specs(tag_items)
|
|
12453
|
+
if _coerce_positive_int(spec.get("group_id")) is not None
|
|
12454
|
+
}
|
|
12423
12455
|
|
|
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
12456
|
|
|
12437
|
-
|
|
12457
|
+
def _collect_backend_package_group_specs(tag_items: Any, *, path: tuple[int, ...] = ()) -> list[JSONObject]:
|
|
12458
|
+
if not isinstance(tag_items, list):
|
|
12459
|
+
return []
|
|
12460
|
+
groups: list[JSONObject] = []
|
|
12461
|
+
for index, item in enumerate(tag_items):
|
|
12462
|
+
if not isinstance(item, dict):
|
|
12463
|
+
continue
|
|
12464
|
+
item_type = _coerce_positive_int(item.get("itemType"))
|
|
12465
|
+
child_path = (*path, index)
|
|
12466
|
+
if item_type == 3:
|
|
12467
|
+
group_id = _coerce_positive_int(item.get("groupId"))
|
|
12468
|
+
if group_id is None:
|
|
12469
|
+
continue
|
|
12470
|
+
child_items = item.get("subItems") if isinstance(item.get("subItems"), list) else []
|
|
12471
|
+
groups.append(
|
|
12472
|
+
{
|
|
12473
|
+
"path": list(child_path),
|
|
12474
|
+
"group_id": group_id,
|
|
12475
|
+
"name": str(item.get("title") or item.get("groupName") or "").strip(),
|
|
12476
|
+
"resource_signature": _package_resource_signature(child_items, public=False),
|
|
12477
|
+
}
|
|
12478
|
+
)
|
|
12479
|
+
groups.extend(_collect_backend_package_group_specs(child_items, path=child_path))
|
|
12438
12480
|
return groups
|
|
12439
12481
|
|
|
12440
12482
|
|
|
@@ -12459,6 +12501,91 @@ def _collect_public_package_group_specs(items: Any, *, path: tuple[int, ...] = (
|
|
|
12459
12501
|
return groups
|
|
12460
12502
|
|
|
12461
12503
|
|
|
12504
|
+
def _align_public_package_group_ids(
|
|
12505
|
+
items: list[dict[str, Any]],
|
|
12506
|
+
*,
|
|
12507
|
+
current_group_specs: list[JSONObject],
|
|
12508
|
+
) -> tuple[list[dict[str, Any]], list[JSONObject]]:
|
|
12509
|
+
normalized_items = deepcopy(items)
|
|
12510
|
+
used_group_ids: set[int] = set()
|
|
12511
|
+
issues: list[JSONObject] = []
|
|
12512
|
+
|
|
12513
|
+
def walk(nodes: list[dict[str, Any]], *, path: tuple[int, ...] = ()) -> None:
|
|
12514
|
+
for index, node in enumerate(nodes):
|
|
12515
|
+
if not isinstance(node, dict):
|
|
12516
|
+
continue
|
|
12517
|
+
item_type = str(node.get("type") or "").strip().lower()
|
|
12518
|
+
child_path = (*path, index)
|
|
12519
|
+
child_items = node.get("items") if isinstance(node.get("items"), list) else []
|
|
12520
|
+
if item_type == "group" or isinstance(node.get("items"), list):
|
|
12521
|
+
explicit_group_id = _coerce_positive_int(node.get("group_id") or node.get("groupId"))
|
|
12522
|
+
if explicit_group_id is not None:
|
|
12523
|
+
node["group_id"] = explicit_group_id
|
|
12524
|
+
used_group_ids.add(explicit_group_id)
|
|
12525
|
+
else:
|
|
12526
|
+
group_name = str(node.get("name") or node.get("title") or node.get("group_name") or "").strip()
|
|
12527
|
+
matched_group, ambiguity = _match_existing_package_group(
|
|
12528
|
+
group_name=group_name,
|
|
12529
|
+
child_items=child_items,
|
|
12530
|
+
path=child_path,
|
|
12531
|
+
current_group_specs=current_group_specs,
|
|
12532
|
+
used_group_ids=used_group_ids,
|
|
12533
|
+
)
|
|
12534
|
+
if ambiguity is not None:
|
|
12535
|
+
issues.append(ambiguity)
|
|
12536
|
+
elif matched_group is not None:
|
|
12537
|
+
matched_group_id = _coerce_positive_int(matched_group.get("group_id"))
|
|
12538
|
+
if matched_group_id is not None:
|
|
12539
|
+
node["group_id"] = matched_group_id
|
|
12540
|
+
used_group_ids.add(matched_group_id)
|
|
12541
|
+
walk(child_items, path=child_path)
|
|
12542
|
+
|
|
12543
|
+
walk(normalized_items)
|
|
12544
|
+
return normalized_items, issues
|
|
12545
|
+
|
|
12546
|
+
|
|
12547
|
+
def _match_existing_package_group(
|
|
12548
|
+
*,
|
|
12549
|
+
group_name: str,
|
|
12550
|
+
child_items: list[dict[str, Any]],
|
|
12551
|
+
path: tuple[int, ...],
|
|
12552
|
+
current_group_specs: list[JSONObject],
|
|
12553
|
+
used_group_ids: set[int],
|
|
12554
|
+
) -> tuple[JSONObject | None, JSONObject | None]:
|
|
12555
|
+
desired_signature = _package_resource_signature(child_items, public=True)
|
|
12556
|
+
candidates = [
|
|
12557
|
+
spec
|
|
12558
|
+
for spec in current_group_specs
|
|
12559
|
+
if _coerce_positive_int(spec.get("group_id")) is not None
|
|
12560
|
+
and int(spec["group_id"]) not in used_group_ids
|
|
12561
|
+
and str(spec.get("name") or "").strip() == group_name
|
|
12562
|
+
]
|
|
12563
|
+
exact_matches = [spec for spec in candidates if spec.get("resource_signature") == desired_signature]
|
|
12564
|
+
if len(exact_matches) == 1:
|
|
12565
|
+
return exact_matches[0], None
|
|
12566
|
+
path_matches = [spec for spec in candidates if tuple(spec.get("path") or ()) == path]
|
|
12567
|
+
if len(path_matches) == 1:
|
|
12568
|
+
return path_matches[0], None
|
|
12569
|
+
if len(candidates) == 1:
|
|
12570
|
+
return candidates[0], None
|
|
12571
|
+
ambiguous_matches = exact_matches if len(exact_matches) > 1 else candidates if len(candidates) > 1 else []
|
|
12572
|
+
if ambiguous_matches:
|
|
12573
|
+
return None, {
|
|
12574
|
+
"path": list(path),
|
|
12575
|
+
"name": group_name,
|
|
12576
|
+
"candidate_group_ids": [
|
|
12577
|
+
int(spec["group_id"])
|
|
12578
|
+
for spec in ambiguous_matches
|
|
12579
|
+
if _coerce_positive_int(spec.get("group_id")) is not None
|
|
12580
|
+
],
|
|
12581
|
+
}
|
|
12582
|
+
return None, None
|
|
12583
|
+
|
|
12584
|
+
|
|
12585
|
+
def _package_resource_signature(items: Any, *, public: bool) -> tuple[tuple[str, str], ...]:
|
|
12586
|
+
return tuple(sorted(_flatten_package_resource_identities(items, public=public)))
|
|
12587
|
+
|
|
12588
|
+
|
|
12462
12589
|
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
12590
|
backend_items: list[JSONObject] = []
|
|
12464
12591
|
for index, item in enumerate(items):
|