@qingflow-tech/qingflow-app-builder-mcp 1.0.11 → 1.0.12
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 +6 -3
- package/docs/local-agent-install.md +54 -3
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-skills.mjs +5 -0
- package/npm/lib/runtime.mjs +304 -13
- package/npm/scripts/postinstall.mjs +1 -5
- package/package.json +3 -2
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +12 -12
- package/skills/qingflow-app-builder/references/create-app.md +3 -3
- package/skills/qingflow-app-builder/references/environments.md +1 -1
- package/skills/qingflow-app-builder/references/gotchas.md +1 -1
- package/skills/qingflow-app-builder/references/public-surface-sync.md +75 -0
- package/skills/qingflow-app-builder/references/tool-selection.md +6 -5
- package/skills/qingflow-app-builder/references/update-views.md +1 -1
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +3 -3
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +1 -1
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +1 -1
- package/src/qingflow_mcp/__main__.py +6 -2
- package/src/qingflow_mcp/builder_facade/service.py +1488 -288
- package/src/qingflow_mcp/cli/commands/builder.py +2 -2
- package/src/qingflow_mcp/cli/commands/exports.py +2 -2
- package/src/qingflow_mcp/cli/commands/imports.py +1 -1
- package/src/qingflow_mcp/cli/commands/record.py +39 -11
- package/src/qingflow_mcp/cli/context.py +0 -3
- package/src/qingflow_mcp/cli/formatters.py +206 -7
- package/src/qingflow_mcp/cli/main.py +47 -3
- package/src/qingflow_mcp/errors.py +43 -2
- package/src/qingflow_mcp/public_surface.py +21 -15
- package/src/qingflow_mcp/response_trim.py +68 -13
- package/src/qingflow_mcp/server.py +11 -9
- package/src/qingflow_mcp/server_app_builder.py +3 -2
- package/src/qingflow_mcp/server_app_user.py +15 -13
- package/src/qingflow_mcp/solution/executor.py +112 -15
- package/src/qingflow_mcp/tools/ai_builder_tools.py +36 -11
- package/src/qingflow_mcp/tools/app_tools.py +184 -43
- package/src/qingflow_mcp/tools/approval_tools.py +196 -34
- package/src/qingflow_mcp/tools/auth_tools.py +92 -16
- package/src/qingflow_mcp/tools/code_block_tools.py +296 -39
- package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
- package/src/qingflow_mcp/tools/directory_tools.py +236 -72
- package/src/qingflow_mcp/tools/export_tools.py +230 -33
- package/src/qingflow_mcp/tools/file_tools.py +7 -3
- package/src/qingflow_mcp/tools/import_tools.py +293 -40
- package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
- package/src/qingflow_mcp/tools/package_tools.py +118 -6
- package/src/qingflow_mcp/tools/portal_tools.py +39 -3
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
- package/src/qingflow_mcp/tools/record_tools.py +1042 -338
- package/src/qingflow_mcp/tools/resource_read_tools.py +188 -39
- package/src/qingflow_mcp/tools/role_tools.py +80 -9
- package/src/qingflow_mcp/tools/solution_tools.py +57 -15
- package/src/qingflow_mcp/tools/task_context_tools.py +569 -119
- package/src/qingflow_mcp/tools/task_tools.py +113 -29
- package/src/qingflow_mcp/tools/view_tools.py +106 -3
- package/src/qingflow_mcp/tools/workflow_tools.py +17 -1
- package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
|
@@ -14,7 +14,7 @@ from urllib.parse import quote_plus, unquote_plus
|
|
|
14
14
|
from uuid import uuid4
|
|
15
15
|
|
|
16
16
|
from ..backend_client import BackendRequestContext
|
|
17
|
-
from ..errors import QingflowApiError
|
|
17
|
+
from ..errors import QingflowApiError, backend_code_int, backend_code_value_int, is_auth_like_error
|
|
18
18
|
from ..json_types import JSONObject
|
|
19
19
|
from ..list_type_labels import RECORD_LIST_TYPE_LABELS, SYSTEM_VIEW_ID_TO_LIST_TYPE
|
|
20
20
|
from ..solution.build_assembly_store import BuildAssemblyStore, default_artifacts, default_manifest
|
|
@@ -374,7 +374,18 @@ class AiBuilderFacade:
|
|
|
374
374
|
if existing.get("error_code") == "AMBIGUOUS_PACKAGE":
|
|
375
375
|
return existing
|
|
376
376
|
if existing.get("error_code") == "PACKAGE_RESOLVE_FAILED":
|
|
377
|
-
if existing.get("
|
|
377
|
+
existing_details = existing.get("details") if isinstance(existing.get("details"), dict) else {}
|
|
378
|
+
existing_transport_error = (
|
|
379
|
+
existing_details.get("transport_error") if isinstance(existing_details.get("transport_error"), dict) else {}
|
|
380
|
+
)
|
|
381
|
+
existing_error = QingflowApiError(
|
|
382
|
+
category=str(existing_transport_error.get("category") or ""),
|
|
383
|
+
message=str(existing.get("message") or ""),
|
|
384
|
+
backend_code=existing.get("backend_code"),
|
|
385
|
+
http_status=existing.get("http_status"),
|
|
386
|
+
request_id=existing.get("request_id"),
|
|
387
|
+
)
|
|
388
|
+
if is_auth_like_error(existing_error) or backend_code_value_int(existing.get("backend_code")) not in {40002, 40027}:
|
|
378
389
|
return existing
|
|
379
390
|
lookup_permission_blocked = {
|
|
380
391
|
"backend_code": existing.get("backend_code"),
|
|
@@ -468,7 +479,17 @@ class AiBuilderFacade:
|
|
|
468
479
|
try:
|
|
469
480
|
detail_result = self.packages.package_get(profile=profile, tag_id=effective_package_id, include_raw=True)
|
|
470
481
|
except (QingflowApiError, RuntimeError) as error:
|
|
471
|
-
|
|
482
|
+
api_error = _coerce_api_error(error)
|
|
483
|
+
if _is_optional_builder_lookup_error(api_error):
|
|
484
|
+
detail_read_error = api_error
|
|
485
|
+
else:
|
|
486
|
+
return _failed_from_api_error(
|
|
487
|
+
"PACKAGE_GET_FAILED",
|
|
488
|
+
api_error,
|
|
489
|
+
normalized_args=normalized_args,
|
|
490
|
+
details={"package_id": effective_package_id},
|
|
491
|
+
suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "package_id": effective_package_id}},
|
|
492
|
+
)
|
|
472
493
|
|
|
473
494
|
detail = detail_result.get("result") if isinstance(detail_result, dict) and isinstance(detail_result.get("result"), dict) else {}
|
|
474
495
|
base = base_result.get("result") if isinstance(base_result.get("result"), dict) else {}
|
|
@@ -476,14 +497,17 @@ class AiBuilderFacade:
|
|
|
476
497
|
source = detail if detail else base
|
|
477
498
|
layout_tag_items = _select_package_layout_tag_items(detail=detail, base=base)
|
|
478
499
|
warnings: list[JSONObject] = []
|
|
500
|
+
if isinstance(detail_result, dict) and isinstance(detail_result.get("warnings"), list):
|
|
501
|
+
warnings.extend(deepcopy(item) for item in detail_result.get("warnings", []) if isinstance(item, dict))
|
|
479
502
|
if detail_read_error is not None:
|
|
480
503
|
warnings.append(
|
|
481
|
-
|
|
482
|
-
"
|
|
483
|
-
"
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
504
|
+
_warning(
|
|
505
|
+
"PACKAGE_DETAIL_READ_DEGRADED",
|
|
506
|
+
"package_get used baseInfo because the package detail endpoint was not readable",
|
|
507
|
+
backend_code=detail_read_error.backend_code,
|
|
508
|
+
http_status=detail_read_error.http_status,
|
|
509
|
+
request_id=detail_read_error.request_id,
|
|
510
|
+
)
|
|
487
511
|
)
|
|
488
512
|
public_items = _public_package_items_from_tag_items(layout_tag_items)
|
|
489
513
|
item_count = summary.get("itemCount")
|
|
@@ -624,8 +648,34 @@ class AiBuilderFacade:
|
|
|
624
648
|
if layout_result.get("status") not in {"success", "partial_success"}:
|
|
625
649
|
return _apply_permission_outcomes(layout_result, *permission_outcomes)
|
|
626
650
|
|
|
651
|
+
write_executed = bool(
|
|
652
|
+
created
|
|
653
|
+
or (
|
|
654
|
+
metadata_requested
|
|
655
|
+
and isinstance(update_result, dict)
|
|
656
|
+
and update_result.get("status") in {"success", "partial_success"}
|
|
657
|
+
and not bool(update_result.get("noop"))
|
|
658
|
+
)
|
|
659
|
+
or (
|
|
660
|
+
items is not None
|
|
661
|
+
and isinstance(layout_result, dict)
|
|
662
|
+
and layout_result.get("status") in {"success", "partial_success"}
|
|
663
|
+
and not bool(layout_result.get("noop"))
|
|
664
|
+
)
|
|
665
|
+
)
|
|
627
666
|
verification = self.package_get(profile=profile, package_id=effective_package_id)
|
|
628
667
|
if verification.get("status") != "success":
|
|
668
|
+
if write_executed:
|
|
669
|
+
return _apply_permission_outcomes(
|
|
670
|
+
_post_write_readback_pending_result(
|
|
671
|
+
error_code="PACKAGE_READBACK_PENDING",
|
|
672
|
+
message="applied package; final package readback is unavailable",
|
|
673
|
+
normalized_args=normalized_args,
|
|
674
|
+
details={"package_id": effective_package_id, "verification_result": verification},
|
|
675
|
+
suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "package_id": effective_package_id}},
|
|
676
|
+
),
|
|
677
|
+
*permission_outcomes,
|
|
678
|
+
)
|
|
629
679
|
return _apply_permission_outcomes(verification, *permission_outcomes)
|
|
630
680
|
expected_visibility = None
|
|
631
681
|
if visibility is not None:
|
|
@@ -643,6 +693,11 @@ class AiBuilderFacade:
|
|
|
643
693
|
layout_verified = True
|
|
644
694
|
if items is not None and layout_result is not None:
|
|
645
695
|
layout_verified = bool(layout_result.get("verified"))
|
|
696
|
+
layout_error_code = (
|
|
697
|
+
str(layout_result.get("error_code") or "").strip()
|
|
698
|
+
if isinstance(layout_result, dict) and layout_result.get("error_code")
|
|
699
|
+
else None
|
|
700
|
+
)
|
|
646
701
|
response_verification: JSONObject = {
|
|
647
702
|
"package_exists": True,
|
|
648
703
|
"package_created": created,
|
|
@@ -660,21 +715,36 @@ class AiBuilderFacade:
|
|
|
660
715
|
if key in update_verification:
|
|
661
716
|
response_verification[key] = deepcopy(update_verification.get(key))
|
|
662
717
|
response_verified = metadata_verified and layout_verified and response_verification.get("visibility_verified") is not False
|
|
718
|
+
response_warnings = []
|
|
719
|
+
if isinstance(layout_result, dict) and isinstance(layout_result.get("warnings"), list):
|
|
720
|
+
response_warnings.extend(deepcopy(layout_result.get("warnings") or []))
|
|
663
721
|
response: JSONObject = {
|
|
664
722
|
"status": "success" if response_verified else "partial_success",
|
|
665
|
-
"error_code": None,
|
|
723
|
+
"error_code": None if response_verified else layout_error_code,
|
|
666
724
|
"recoverable": False,
|
|
667
725
|
"message": "applied package" if response_verified else "applied package with unverified readback",
|
|
668
726
|
"normalized_args": normalized_args,
|
|
669
727
|
"missing_fields": [],
|
|
670
728
|
"allowed_values": {},
|
|
671
|
-
"details": {
|
|
729
|
+
"details": {
|
|
730
|
+
**({"layout_result": layout_result} if layout_result is not None else {}),
|
|
731
|
+
**(
|
|
732
|
+
{"layout_write_error": layout_result.get("details", {}).get("write_error")}
|
|
733
|
+
if isinstance(layout_result, dict)
|
|
734
|
+
and isinstance(layout_result.get("details"), dict)
|
|
735
|
+
and isinstance(layout_result.get("details", {}).get("write_error"), dict)
|
|
736
|
+
else {}
|
|
737
|
+
),
|
|
738
|
+
},
|
|
672
739
|
"request_id": None,
|
|
673
740
|
"suggested_next_call": None,
|
|
674
741
|
"noop": not (created or metadata_requested or items is not None),
|
|
675
|
-
"warnings":
|
|
742
|
+
"warnings": response_warnings,
|
|
676
743
|
"verification": response_verification,
|
|
677
744
|
"verified": response_verified,
|
|
745
|
+
"write_executed": write_executed,
|
|
746
|
+
"write_succeeded": write_executed,
|
|
747
|
+
"safe_to_retry": not write_executed,
|
|
678
748
|
**{
|
|
679
749
|
key: deepcopy(value)
|
|
680
750
|
for key, value in verification.items()
|
|
@@ -719,7 +789,6 @@ class AiBuilderFacade:
|
|
|
719
789
|
if _coerce_positive_int(tag_id) is None:
|
|
720
790
|
return _failed("TAG_ID_REQUIRED", "tag_id must be positive", normalized_args=normalized_args, suggested_next_call=None)
|
|
721
791
|
try:
|
|
722
|
-
current = self.packages.package_get(profile=profile, tag_id=tag_id, include_raw=True)
|
|
723
792
|
current_base = self.packages.package_get_base(profile=profile, tag_id=tag_id, include_raw=True)
|
|
724
793
|
except (QingflowApiError, RuntimeError) as error:
|
|
725
794
|
api_error = _coerce_api_error(error)
|
|
@@ -730,8 +799,37 @@ class AiBuilderFacade:
|
|
|
730
799
|
details={"tag_id": tag_id},
|
|
731
800
|
suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "tag_id": tag_id}},
|
|
732
801
|
)
|
|
733
|
-
|
|
802
|
+
current: JSONObject | None = None
|
|
803
|
+
detail_read_error: QingflowApiError | None = None
|
|
804
|
+
try:
|
|
805
|
+
current = self.packages.package_get(profile=profile, tag_id=tag_id, include_raw=True)
|
|
806
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
807
|
+
api_error = _coerce_api_error(error)
|
|
808
|
+
if _is_optional_builder_lookup_error(api_error):
|
|
809
|
+
detail_read_error = api_error
|
|
810
|
+
else:
|
|
811
|
+
return _failed_from_api_error(
|
|
812
|
+
"PACKAGE_UPDATE_FAILED",
|
|
813
|
+
api_error,
|
|
814
|
+
normalized_args=normalized_args,
|
|
815
|
+
details={"tag_id": tag_id},
|
|
816
|
+
suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "tag_id": tag_id}},
|
|
817
|
+
)
|
|
818
|
+
raw_current = current.get("result") if isinstance(current, dict) and isinstance(current.get("result"), dict) else {}
|
|
734
819
|
raw_current_base = current_base.get("result") if isinstance(current_base.get("result"), dict) else {}
|
|
820
|
+
warnings: list[JSONObject] = []
|
|
821
|
+
if isinstance(current, dict) and isinstance(current.get("warnings"), list):
|
|
822
|
+
warnings.extend(deepcopy(item) for item in current.get("warnings", []) if isinstance(item, dict))
|
|
823
|
+
if detail_read_error is not None:
|
|
824
|
+
warnings.append(
|
|
825
|
+
_warning(
|
|
826
|
+
"PACKAGE_DETAIL_READ_DEGRADED",
|
|
827
|
+
"package_update used baseInfo because the package detail endpoint was not readable",
|
|
828
|
+
backend_code=detail_read_error.backend_code,
|
|
829
|
+
http_status=detail_read_error.http_status,
|
|
830
|
+
request_id=detail_read_error.request_id,
|
|
831
|
+
)
|
|
832
|
+
)
|
|
735
833
|
current_name = str(raw_current.get("tagName") or raw_current_base.get("tagName") or "").strip() or None
|
|
736
834
|
desired_name = str(package_name or current_name or "").strip() or current_name or "未命名应用包"
|
|
737
835
|
desired_icon = encode_workspace_icon_with_defaults(
|
|
@@ -772,7 +870,13 @@ class AiBuilderFacade:
|
|
|
772
870
|
)
|
|
773
871
|
verification = self.package_get(profile=profile, package_id=tag_id)
|
|
774
872
|
if verification.get("status") != "success":
|
|
775
|
-
return
|
|
873
|
+
return _post_write_readback_pending_result(
|
|
874
|
+
error_code="PACKAGE_UPDATE_READBACK_PENDING",
|
|
875
|
+
message="updated package; package readback is unavailable",
|
|
876
|
+
normalized_args=normalized_args,
|
|
877
|
+
details={"tag_id": tag_id, "package_id": tag_id, "verification_result": verification},
|
|
878
|
+
suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "package_id": tag_id}},
|
|
879
|
+
)
|
|
776
880
|
package_name_verified = str(verification.get("package_name") or "").strip() == desired_name
|
|
777
881
|
package_icon_verified = str(verification.get("icon") or "").strip() == desired_icon
|
|
778
882
|
visibility_verified = _visibility_matches_expected(
|
|
@@ -792,7 +896,7 @@ class AiBuilderFacade:
|
|
|
792
896
|
"request_id": update_result.get("request_id") if isinstance(update_result, dict) else None,
|
|
793
897
|
"suggested_next_call": None if verified else {"tool_name": "package_get", "arguments": {"profile": profile, "package_id": tag_id}},
|
|
794
898
|
"noop": False,
|
|
795
|
-
"warnings":
|
|
899
|
+
"warnings": warnings,
|
|
796
900
|
"verification": {
|
|
797
901
|
"package_exists": True,
|
|
798
902
|
"package_name_verified": package_name_verified,
|
|
@@ -800,6 +904,8 @@ class AiBuilderFacade:
|
|
|
800
904
|
"visibility_verified": visibility_verified,
|
|
801
905
|
},
|
|
802
906
|
"verified": verified,
|
|
907
|
+
"write_executed": True,
|
|
908
|
+
"safe_to_retry": False,
|
|
803
909
|
**{
|
|
804
910
|
key: deepcopy(value)
|
|
805
911
|
for key, value in verification.items()
|
|
@@ -876,6 +982,8 @@ class AiBuilderFacade:
|
|
|
876
982
|
"suggested_next_call": None,
|
|
877
983
|
"noop": False,
|
|
878
984
|
"verification": {},
|
|
985
|
+
"write_executed": True,
|
|
986
|
+
"safe_to_retry": False,
|
|
879
987
|
}
|
|
880
988
|
|
|
881
989
|
def package_list(self, *, profile: str, trial_status: str = "all", query: str = "") -> JSONObject:
|
|
@@ -927,6 +1035,14 @@ class AiBuilderFacade:
|
|
|
927
1035
|
current_detail_result = self.packages.package_get(profile=profile, tag_id=package_id, include_raw=True)
|
|
928
1036
|
except (QingflowApiError, RuntimeError) as detail_error:
|
|
929
1037
|
detail_api_error = _coerce_api_error(detail_error)
|
|
1038
|
+
if not _is_optional_builder_lookup_error(detail_api_error):
|
|
1039
|
+
return _failed_from_api_error(
|
|
1040
|
+
"PACKAGE_LAYOUT_READ_FAILED",
|
|
1041
|
+
detail_api_error,
|
|
1042
|
+
normalized_args=normalized_args,
|
|
1043
|
+
details={"package_id": package_id},
|
|
1044
|
+
suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "package_id": package_id}},
|
|
1045
|
+
)
|
|
930
1046
|
try:
|
|
931
1047
|
current_base_result = self.packages.package_get_base(profile=profile, tag_id=package_id, include_raw=True)
|
|
932
1048
|
except (QingflowApiError, RuntimeError) as base_error:
|
|
@@ -1007,7 +1123,9 @@ class AiBuilderFacade:
|
|
|
1007
1123
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
1008
1124
|
needs_group_create = any(_coerce_positive_int(group.get("group_id")) is None for group in desired_groups)
|
|
1009
1125
|
needs_group_delete = bool(deleted_group_ids)
|
|
1010
|
-
|
|
1126
|
+
# sortGroupUnderPackage is always called for a layout apply and is guarded by
|
|
1127
|
+
# backend MoveGroupAuth, which maps to package editAppStatus.
|
|
1128
|
+
needs_edit_app = True
|
|
1011
1129
|
for required_permission in (
|
|
1012
1130
|
(["add_app"] if needs_group_create else [])
|
|
1013
1131
|
+ (["edit_app"] if needs_edit_app else [])
|
|
@@ -1105,13 +1223,47 @@ class AiBuilderFacade:
|
|
|
1105
1223
|
except (QingflowApiError, RuntimeError) as error:
|
|
1106
1224
|
api_error = _coerce_api_error(error)
|
|
1107
1225
|
return _apply_permission_outcomes(
|
|
1108
|
-
|
|
1109
|
-
"
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1226
|
+
{
|
|
1227
|
+
"status": "partial_success",
|
|
1228
|
+
"error_code": "PACKAGE_APPLY_PARTIAL",
|
|
1229
|
+
"recoverable": True,
|
|
1230
|
+
"message": "package layout sort was applied, but a later group delete failed",
|
|
1231
|
+
"normalized_args": normalized_args,
|
|
1232
|
+
"missing_fields": [],
|
|
1233
|
+
"allowed_values": {},
|
|
1234
|
+
"details": {
|
|
1235
|
+
"package_id": package_id,
|
|
1236
|
+
"group_id": group_id,
|
|
1237
|
+
"group_operations": group_operations,
|
|
1238
|
+
"sort_result": sort_result,
|
|
1239
|
+
"write_error": {
|
|
1240
|
+
"message": api_error.message,
|
|
1241
|
+
"transport_error": _transport_error_payload(api_error),
|
|
1242
|
+
},
|
|
1243
|
+
},
|
|
1244
|
+
"request_id": api_error.request_id,
|
|
1245
|
+
"backend_code": api_error.backend_code,
|
|
1246
|
+
"http_status": None if api_error.http_status == 404 else api_error.http_status,
|
|
1247
|
+
"suggested_next_call": None,
|
|
1248
|
+
"noop": False,
|
|
1249
|
+
"warnings": [
|
|
1250
|
+
_warning(
|
|
1251
|
+
"PACKAGE_WRITE_INCOMPLETE_AFTER_PARTIAL_WRITE",
|
|
1252
|
+
"package layout write was partially applied before a later write step failed; do not blindly repeat the same package apply",
|
|
1253
|
+
**_transport_error_payload(api_error),
|
|
1254
|
+
)
|
|
1255
|
+
],
|
|
1256
|
+
"verification": {
|
|
1257
|
+
"layout_applied": True,
|
|
1258
|
+
"write_incomplete": True,
|
|
1259
|
+
"metadata_unverified": True,
|
|
1260
|
+
},
|
|
1261
|
+
"verified": False,
|
|
1262
|
+
"package_id": package_id,
|
|
1263
|
+
"write_executed": True,
|
|
1264
|
+
"write_succeeded": True,
|
|
1265
|
+
"safe_to_retry": False,
|
|
1266
|
+
},
|
|
1115
1267
|
*permission_outcomes,
|
|
1116
1268
|
)
|
|
1117
1269
|
group_operations.append({"action": "delete", "group_id": group_id})
|
|
@@ -1166,6 +1318,25 @@ class AiBuilderFacade:
|
|
|
1166
1318
|
details={"query": requested},
|
|
1167
1319
|
suggested_next_call={"tool_name": "member_search", "arguments": {"profile": profile, **normalized_args}},
|
|
1168
1320
|
)
|
|
1321
|
+
if listed.get("status") == "failed" and listed.get("error_code") == "CONTACT_DIRECTORY_PERMISSION_DENIED":
|
|
1322
|
+
return _failed(
|
|
1323
|
+
"CONTACT_DIRECTORY_PERMISSION_DENIED",
|
|
1324
|
+
str(listed.get("message") or "Contact-directory management data is not readable for the current user."),
|
|
1325
|
+
normalized_args=normalized_args,
|
|
1326
|
+
details={
|
|
1327
|
+
"query": requested,
|
|
1328
|
+
"permission_boundary": "contact_directory",
|
|
1329
|
+
"fix_hint": (
|
|
1330
|
+
"This builder member lookup uses the contact-directory management route. "
|
|
1331
|
+
"For record member/department field candidates, use record_member_candidates "
|
|
1332
|
+
"or record_department_candidates instead."
|
|
1333
|
+
),
|
|
1334
|
+
},
|
|
1335
|
+
suggested_next_call=None,
|
|
1336
|
+
request_id=listed.get("request_id") if isinstance(listed.get("request_id"), str) else None,
|
|
1337
|
+
backend_code=listed.get("backend_code"),
|
|
1338
|
+
http_status=listed.get("http_status"),
|
|
1339
|
+
)
|
|
1169
1340
|
items = []
|
|
1170
1341
|
for item in _extract_directory_items(listed):
|
|
1171
1342
|
uid = _coerce_positive_int(item.get("uid") or item.get("id"))
|
|
@@ -1211,6 +1382,23 @@ class AiBuilderFacade:
|
|
|
1211
1382
|
details={"keyword": requested},
|
|
1212
1383
|
suggested_next_call={"tool_name": "role_search", "arguments": {"profile": profile, **normalized_args}},
|
|
1213
1384
|
)
|
|
1385
|
+
if listed.get("status") == "failed":
|
|
1386
|
+
permission_boundary = "contact_role" if listed.get("error_code") == "CONTACT_ROLE_PERMISSION_DENIED" else None
|
|
1387
|
+
return _failed(
|
|
1388
|
+
str(listed.get("error_code") or "ROLE_SEARCH_FAILED"),
|
|
1389
|
+
str(listed.get("message") or "role search failed"),
|
|
1390
|
+
normalized_args=normalized_args,
|
|
1391
|
+
details={
|
|
1392
|
+
"keyword": requested,
|
|
1393
|
+
"permission_boundary": permission_boundary,
|
|
1394
|
+
},
|
|
1395
|
+
suggested_next_call=None
|
|
1396
|
+
if permission_boundary
|
|
1397
|
+
else {"tool_name": "role_search", "arguments": {"profile": profile, **normalized_args}},
|
|
1398
|
+
backend_code=listed.get("backend_code"),
|
|
1399
|
+
request_id=listed.get("request_id"),
|
|
1400
|
+
http_status=listed.get("http_status"),
|
|
1401
|
+
)
|
|
1214
1402
|
page = listed.get("page") if isinstance(listed.get("page"), dict) else {}
|
|
1215
1403
|
raw_items = page.get("list") if isinstance(page.get("list"), list) else []
|
|
1216
1404
|
items = []
|
|
@@ -1283,6 +1471,8 @@ class AiBuilderFacade:
|
|
|
1283
1471
|
"role_id": exact[0]["role_id"],
|
|
1284
1472
|
"role_name": exact[0]["role_name"],
|
|
1285
1473
|
"role_icon": exact[0].get("role_icon"),
|
|
1474
|
+
"write_executed": False,
|
|
1475
|
+
"safe_to_retry": True,
|
|
1286
1476
|
}
|
|
1287
1477
|
if len(exact) > 1:
|
|
1288
1478
|
return _failed(
|
|
@@ -1343,6 +1533,8 @@ class AiBuilderFacade:
|
|
|
1343
1533
|
"role_id": role_id,
|
|
1344
1534
|
"role_name": requested_name,
|
|
1345
1535
|
"role_icon": normalized_args["role_icon"],
|
|
1536
|
+
"write_executed": True,
|
|
1537
|
+
"safe_to_retry": False,
|
|
1346
1538
|
}
|
|
1347
1539
|
|
|
1348
1540
|
def _resolve_role_references(
|
|
@@ -1373,6 +1565,18 @@ class AiBuilderFacade:
|
|
|
1373
1565
|
if not requested:
|
|
1374
1566
|
continue
|
|
1375
1567
|
matches_result = self.role_search(profile=profile, keyword=requested, page_num=1, page_size=50)
|
|
1568
|
+
if matches_result.get("status") != "success":
|
|
1569
|
+
issues.append(
|
|
1570
|
+
{
|
|
1571
|
+
"kind": "role",
|
|
1572
|
+
"value": requested,
|
|
1573
|
+
"error_code": matches_result.get("error_code") or "ROLE_SEARCH_FAILED",
|
|
1574
|
+
"message": matches_result.get("message"),
|
|
1575
|
+
"backend_code": matches_result.get("backend_code"),
|
|
1576
|
+
"request_id": matches_result.get("request_id"),
|
|
1577
|
+
}
|
|
1578
|
+
)
|
|
1579
|
+
continue
|
|
1376
1580
|
items = matches_result.get("items", []) if matches_result.get("status") == "success" else []
|
|
1377
1581
|
exact = [item for item in items if isinstance(item, dict) and item.get("role_name") == requested]
|
|
1378
1582
|
if len(exact) != 1:
|
|
@@ -1434,6 +1638,18 @@ class AiBuilderFacade:
|
|
|
1434
1638
|
if not requested:
|
|
1435
1639
|
continue
|
|
1436
1640
|
matches = self.member_search(profile=profile, query=requested, page_num=1, page_size=50, contain_disable=False)
|
|
1641
|
+
if matches.get("status") != "success":
|
|
1642
|
+
issues.append(
|
|
1643
|
+
{
|
|
1644
|
+
"kind": "member_email",
|
|
1645
|
+
"value": requested,
|
|
1646
|
+
"error_code": matches.get("error_code") or "MEMBER_SEARCH_FAILED",
|
|
1647
|
+
"message": matches.get("message"),
|
|
1648
|
+
"backend_code": matches.get("backend_code"),
|
|
1649
|
+
"request_id": matches.get("request_id"),
|
|
1650
|
+
}
|
|
1651
|
+
)
|
|
1652
|
+
continue
|
|
1437
1653
|
items = matches.get("items", []) if matches.get("status") == "success" else []
|
|
1438
1654
|
exact = [item for item in items if isinstance(item, dict) and str(item.get("email") or "").strip().lower() == requested.lower()]
|
|
1439
1655
|
if len(exact) != 1:
|
|
@@ -1453,6 +1669,18 @@ class AiBuilderFacade:
|
|
|
1453
1669
|
if not requested:
|
|
1454
1670
|
continue
|
|
1455
1671
|
matches = self.member_search(profile=profile, query=requested, page_num=1, page_size=50, contain_disable=False)
|
|
1672
|
+
if matches.get("status") != "success":
|
|
1673
|
+
issues.append(
|
|
1674
|
+
{
|
|
1675
|
+
"kind": "member_name",
|
|
1676
|
+
"value": requested,
|
|
1677
|
+
"error_code": matches.get("error_code") or "MEMBER_SEARCH_FAILED",
|
|
1678
|
+
"message": matches.get("message"),
|
|
1679
|
+
"backend_code": matches.get("backend_code"),
|
|
1680
|
+
"request_id": matches.get("request_id"),
|
|
1681
|
+
}
|
|
1682
|
+
)
|
|
1683
|
+
continue
|
|
1456
1684
|
items = matches.get("items", []) if matches.get("status") == "success" else []
|
|
1457
1685
|
exact = [item for item in items if isinstance(item, dict) and str(item.get("name") or "").strip() == requested]
|
|
1458
1686
|
if len(exact) != 1:
|
|
@@ -1481,23 +1709,6 @@ class AiBuilderFacade:
|
|
|
1481
1709
|
seen_ids: set[int] = set()
|
|
1482
1710
|
if not dept_ids and not dept_names:
|
|
1483
1711
|
return {"department_entries": resolved, "issues": issues}
|
|
1484
|
-
listed = self.directory.directory_list_all_departments(
|
|
1485
|
-
profile=profile,
|
|
1486
|
-
parent_dept_id=None,
|
|
1487
|
-
max_depth=20,
|
|
1488
|
-
max_items=5000,
|
|
1489
|
-
)
|
|
1490
|
-
items = _extract_directory_items(listed)
|
|
1491
|
-
by_id: dict[int, dict[str, Any]] = {}
|
|
1492
|
-
by_name: dict[str, list[dict[str, Any]]] = {}
|
|
1493
|
-
for item in items:
|
|
1494
|
-
dept_id = _coerce_positive_int(item.get("deptId") or item.get("id"))
|
|
1495
|
-
if dept_id is None:
|
|
1496
|
-
continue
|
|
1497
|
-
by_id[dept_id] = item
|
|
1498
|
-
dept_name = str(item.get("deptName") or item.get("departName") or item.get("name") or "").strip()
|
|
1499
|
-
if dept_name:
|
|
1500
|
-
by_name.setdefault(dept_name, []).append(item)
|
|
1501
1712
|
|
|
1502
1713
|
def add_department(item: dict[str, Any], *, fallback_name: str | None = None) -> None:
|
|
1503
1714
|
dept_id = _coerce_positive_int(item.get("deptId") or item.get("id"))
|
|
@@ -1517,7 +1728,40 @@ class AiBuilderFacade:
|
|
|
1517
1728
|
normalized = _coerce_positive_int(dept_id)
|
|
1518
1729
|
if normalized is None:
|
|
1519
1730
|
continue
|
|
1520
|
-
add_department(
|
|
1731
|
+
add_department({"deptId": normalized}, fallback_name=str(normalized))
|
|
1732
|
+
|
|
1733
|
+
if not dept_names:
|
|
1734
|
+
return {"department_entries": resolved, "issues": issues}
|
|
1735
|
+
|
|
1736
|
+
listed = self.directory.directory_list_all_departments(
|
|
1737
|
+
profile=profile,
|
|
1738
|
+
parent_dept_id=None,
|
|
1739
|
+
max_depth=20,
|
|
1740
|
+
max_items=5000,
|
|
1741
|
+
)
|
|
1742
|
+
if listed.get("status") == "failed" and listed.get("error_code") == "CONTACT_DIRECTORY_PERMISSION_DENIED":
|
|
1743
|
+
for dept_name in dept_names:
|
|
1744
|
+
requested = str(dept_name or "").strip()
|
|
1745
|
+
if not requested:
|
|
1746
|
+
continue
|
|
1747
|
+
issues.append(
|
|
1748
|
+
{
|
|
1749
|
+
"kind": "department_name",
|
|
1750
|
+
"value": requested,
|
|
1751
|
+
"error_code": "CONTACT_DIRECTORY_PERMISSION_DENIED",
|
|
1752
|
+
"message": "department names require contact-directory lookup; pass dept_ids or grant directory access",
|
|
1753
|
+
"backend_code": listed.get("backend_code"),
|
|
1754
|
+
"request_id": listed.get("request_id"),
|
|
1755
|
+
}
|
|
1756
|
+
)
|
|
1757
|
+
return {"department_entries": resolved, "issues": issues}
|
|
1758
|
+
|
|
1759
|
+
items = _extract_directory_items(listed)
|
|
1760
|
+
by_name: dict[str, list[dict[str, Any]]] = {}
|
|
1761
|
+
for item in items:
|
|
1762
|
+
dept_name = str(item.get("deptName") or item.get("departName") or item.get("name") or "").strip()
|
|
1763
|
+
if dept_name:
|
|
1764
|
+
by_name.setdefault(dept_name, []).append(item)
|
|
1521
1765
|
|
|
1522
1766
|
for dept_name in dept_names:
|
|
1523
1767
|
requested = str(dept_name or "").strip()
|
|
@@ -1584,6 +1828,18 @@ class AiBuilderFacade:
|
|
|
1584
1828
|
page_size=100,
|
|
1585
1829
|
simple=True,
|
|
1586
1830
|
)
|
|
1831
|
+
if listed.get("status") == "failed":
|
|
1832
|
+
issues.append(
|
|
1833
|
+
{
|
|
1834
|
+
"kind": "external_member_email",
|
|
1835
|
+
"value": requested,
|
|
1836
|
+
"error_code": listed.get("error_code") or "EXTERNAL_MEMBER_SEARCH_FAILED",
|
|
1837
|
+
"message": listed.get("message"),
|
|
1838
|
+
"backend_code": listed.get("backend_code"),
|
|
1839
|
+
"request_id": listed.get("request_id"),
|
|
1840
|
+
}
|
|
1841
|
+
)
|
|
1842
|
+
continue
|
|
1587
1843
|
items = _extract_directory_items(listed)
|
|
1588
1844
|
exact = [
|
|
1589
1845
|
item
|
|
@@ -1618,6 +1874,9 @@ class AiBuilderFacade:
|
|
|
1618
1874
|
elif error_code.endswith("_NOT_FOUND"):
|
|
1619
1875
|
public_code = "VISIBILITY_SUBJECT_NOT_FOUND"
|
|
1620
1876
|
message = f"{kind} '{requested_value}' was not found in the visibility directory"
|
|
1877
|
+
elif "PERMISSION_DENIED" in error_code or error_code.endswith("_FAILED"):
|
|
1878
|
+
public_code = "VISIBILITY_SUBJECT_LOOKUP_FAILED"
|
|
1879
|
+
message = f"{kind} '{requested_value}' could not be resolved because the visibility directory lookup failed"
|
|
1621
1880
|
else:
|
|
1622
1881
|
public_code = "VISIBILITY_SUBJECT_UNSUPPORTED"
|
|
1623
1882
|
message = f"{kind} visibility selector is unsupported"
|
|
@@ -2033,6 +2292,9 @@ class AiBuilderFacade:
|
|
|
2033
2292
|
"tag_id": tag_id,
|
|
2034
2293
|
"tag_ids_after": tag_ids_after,
|
|
2035
2294
|
"attached": attached,
|
|
2295
|
+
"write_executed": not already_attached,
|
|
2296
|
+
"write_succeeded": not already_attached or attached,
|
|
2297
|
+
"safe_to_retry": bool(already_attached),
|
|
2036
2298
|
}
|
|
2037
2299
|
if verification_error is not None:
|
|
2038
2300
|
response["details"]["verification_error"] = _transport_error_payload(verification_error)
|
|
@@ -2145,6 +2407,8 @@ class AiBuilderFacade:
|
|
|
2145
2407
|
"verification": {"released": True},
|
|
2146
2408
|
"app_key": app_key,
|
|
2147
2409
|
"released": True,
|
|
2410
|
+
"write_executed": True,
|
|
2411
|
+
"safe_to_retry": False,
|
|
2148
2412
|
}
|
|
2149
2413
|
|
|
2150
2414
|
def app_resolve(
|
|
@@ -2225,11 +2489,20 @@ class AiBuilderFacade:
|
|
|
2225
2489
|
if not requested:
|
|
2226
2490
|
return _failed("APP_NAME_REQUIRED", "app_name or app_key is required", suggested_next_call=None)
|
|
2227
2491
|
if package_tag_id is not None and package_tag_id > 0:
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2492
|
+
try:
|
|
2493
|
+
package_matches = self._resolve_app_matches_in_package(
|
|
2494
|
+
profile=profile,
|
|
2495
|
+
app_name=requested,
|
|
2496
|
+
package_tag_id=package_tag_id,
|
|
2497
|
+
)
|
|
2498
|
+
except (QingflowApiError, RuntimeError) as exc:
|
|
2499
|
+
api_error = _coerce_api_error(exc)
|
|
2500
|
+
return _failed_from_api_error(
|
|
2501
|
+
"APP_RESOLVE_FAILED",
|
|
2502
|
+
api_error,
|
|
2503
|
+
details={"app_name": requested, "package_tag_id": package_tag_id, "match_scope": "package"},
|
|
2504
|
+
suggested_next_call=None,
|
|
2505
|
+
)
|
|
2233
2506
|
if len(package_matches) == 1:
|
|
2234
2507
|
match = package_matches[0]
|
|
2235
2508
|
return {
|
|
@@ -2254,12 +2527,50 @@ class AiBuilderFacade:
|
|
|
2254
2527
|
details={"app_name": requested, "package_tag_id": package_tag_id, "matches": package_matches},
|
|
2255
2528
|
suggested_next_call=None,
|
|
2256
2529
|
)
|
|
2530
|
+
try:
|
|
2531
|
+
visible_matches = self._resolve_app_matches_in_visible_apps(
|
|
2532
|
+
profile=profile,
|
|
2533
|
+
app_name=requested,
|
|
2534
|
+
package_tag_id=package_tag_id,
|
|
2535
|
+
)
|
|
2536
|
+
except (QingflowApiError, RuntimeError) as exc:
|
|
2537
|
+
api_error = _coerce_api_error(exc)
|
|
2538
|
+
return _failed_from_api_error(
|
|
2539
|
+
"APP_RESOLVE_FAILED",
|
|
2540
|
+
api_error,
|
|
2541
|
+
details={"app_name": requested, "package_tag_id": package_tag_id, "match_scope": "visible_apps"},
|
|
2542
|
+
suggested_next_call=None,
|
|
2543
|
+
)
|
|
2544
|
+
if len(visible_matches) == 1:
|
|
2545
|
+
match = visible_matches[0]
|
|
2546
|
+
return {
|
|
2547
|
+
"status": "success",
|
|
2548
|
+
"error_code": None,
|
|
2549
|
+
"recoverable": False,
|
|
2550
|
+
"message": "resolved app",
|
|
2551
|
+
"normalized_args": {"app_name": requested, "package_tag_id": package_tag_id},
|
|
2552
|
+
"missing_fields": [],
|
|
2553
|
+
"allowed_values": {},
|
|
2554
|
+
"details": {"match_scope": "visible_apps"},
|
|
2555
|
+
"request_id": None,
|
|
2556
|
+
"suggested_next_call": None,
|
|
2557
|
+
"noop": False,
|
|
2558
|
+
"verification": {},
|
|
2559
|
+
**match,
|
|
2560
|
+
}
|
|
2561
|
+
if len(visible_matches) > 1:
|
|
2562
|
+
return _failed(
|
|
2563
|
+
"AMBIGUOUS_APP",
|
|
2564
|
+
f"multiple apps matched '{requested}'",
|
|
2565
|
+
details={"app_name": requested, "package_tag_id": package_tag_id, "matches": visible_matches},
|
|
2566
|
+
suggested_next_call=None,
|
|
2567
|
+
)
|
|
2257
2568
|
search_error: QingflowApiError | None = None
|
|
2258
2569
|
try:
|
|
2259
2570
|
search = self.apps.app_search(profile=profile, keyword=requested, page_num=1, page_size=200)
|
|
2260
2571
|
except (QingflowApiError, RuntimeError) as exc:
|
|
2261
2572
|
api_error = _coerce_api_error(exc)
|
|
2262
|
-
if package_tag_id is None or package_tag_id <= 0 or api_error
|
|
2573
|
+
if is_auth_like_error(api_error) or package_tag_id is None or package_tag_id <= 0 or backend_code_int(api_error) not in {40002, 40027}:
|
|
2263
2574
|
return _failed_from_api_error(
|
|
2264
2575
|
"APP_NOT_FOUND" if api_error.http_status == 404 else "APP_RESOLVE_FAILED",
|
|
2265
2576
|
api_error,
|
|
@@ -2268,6 +2579,7 @@ class AiBuilderFacade:
|
|
|
2268
2579
|
)
|
|
2269
2580
|
search = {}
|
|
2270
2581
|
search_error = api_error
|
|
2582
|
+
search_permission_blocked = _search_permission_blocked_from_warnings(search) if isinstance(search, dict) else None
|
|
2271
2583
|
apps = search.get("apps") if isinstance(search.get("apps"), list) else []
|
|
2272
2584
|
matches = []
|
|
2273
2585
|
for item in apps:
|
|
@@ -2286,8 +2598,16 @@ class AiBuilderFacade:
|
|
|
2286
2598
|
if package_tag_id is not None and package_tag_id > 0:
|
|
2287
2599
|
try:
|
|
2288
2600
|
base = self.apps.app_get_base(profile=profile, app_key=candidate_key, include_raw=True)
|
|
2289
|
-
except (QingflowApiError, RuntimeError):
|
|
2290
|
-
|
|
2601
|
+
except (QingflowApiError, RuntimeError) as exc:
|
|
2602
|
+
api_error = _coerce_api_error(exc)
|
|
2603
|
+
if _is_optional_builder_lookup_error(api_error):
|
|
2604
|
+
continue
|
|
2605
|
+
return _failed_from_api_error(
|
|
2606
|
+
"APP_RESOLVE_FAILED",
|
|
2607
|
+
api_error,
|
|
2608
|
+
details={"app_name": requested, "candidate_app_key": candidate_key, "package_tag_id": package_tag_id},
|
|
2609
|
+
suggested_next_call=None,
|
|
2610
|
+
)
|
|
2291
2611
|
result = base.get("result") if isinstance(base.get("result"), dict) else {}
|
|
2292
2612
|
resolved_tag_ids = _coerce_int_list(result.get("tagIds"))
|
|
2293
2613
|
if resolved_tag_ids:
|
|
@@ -2301,12 +2621,7 @@ class AiBuilderFacade:
|
|
|
2301
2621
|
"tag_ids": tag_ids,
|
|
2302
2622
|
}
|
|
2303
2623
|
)
|
|
2304
|
-
if not matches and
|
|
2305
|
-
visible_matches = self._resolve_app_matches_in_visible_apps(
|
|
2306
|
-
profile=profile,
|
|
2307
|
-
app_name=requested,
|
|
2308
|
-
package_tag_id=package_tag_id,
|
|
2309
|
-
)
|
|
2624
|
+
if not matches and search_error is not None:
|
|
2310
2625
|
if len(visible_matches) == 1:
|
|
2311
2626
|
match = visible_matches[0]
|
|
2312
2627
|
return {
|
|
@@ -2366,6 +2681,14 @@ class AiBuilderFacade:
|
|
|
2366
2681
|
if search_error is not None
|
|
2367
2682
|
else {}
|
|
2368
2683
|
),
|
|
2684
|
+
**(
|
|
2685
|
+
{
|
|
2686
|
+
"search_permission_blocked": search_permission_blocked,
|
|
2687
|
+
"match_scope": "visible_apps_fallback",
|
|
2688
|
+
}
|
|
2689
|
+
if search_permission_blocked is not None
|
|
2690
|
+
else {}
|
|
2691
|
+
),
|
|
2369
2692
|
},
|
|
2370
2693
|
suggested_next_call=None,
|
|
2371
2694
|
)
|
|
@@ -2551,21 +2874,45 @@ class AiBuilderFacade:
|
|
|
2551
2874
|
normalized_args = request.model_dump(mode="json")
|
|
2552
2875
|
app_key = request.app_key
|
|
2553
2876
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2877
|
+
button_write_intent = bool(request.upsert_buttons or request.patch_buttons or request.remove_buttons)
|
|
2878
|
+
if button_write_intent:
|
|
2879
|
+
permission_outcome = self._guard_app_permission(
|
|
2880
|
+
profile=profile,
|
|
2881
|
+
app_key=app_key,
|
|
2882
|
+
required_permission="edit_app",
|
|
2883
|
+
normalized_args=normalized_args,
|
|
2884
|
+
)
|
|
2885
|
+
if permission_outcome.block is not None:
|
|
2886
|
+
return permission_outcome.block
|
|
2887
|
+
permission_outcomes.append(permission_outcome)
|
|
2888
|
+
if request.view_configs:
|
|
2889
|
+
permission_outcome = self._guard_app_permission(
|
|
2890
|
+
profile=profile,
|
|
2891
|
+
app_key=app_key,
|
|
2892
|
+
required_permission="view_manage",
|
|
2893
|
+
normalized_args=normalized_args,
|
|
2894
|
+
)
|
|
2895
|
+
if permission_outcome.block is not None:
|
|
2896
|
+
return permission_outcome.block
|
|
2897
|
+
permission_outcomes.append(permission_outcome)
|
|
2563
2898
|
|
|
2564
2899
|
def finalize(response: JSONObject) -> JSONObject:
|
|
2565
2900
|
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
2566
2901
|
|
|
2902
|
+
view_config_refs = [
|
|
2903
|
+
binding.button_ref
|
|
2904
|
+
for config in request.view_configs
|
|
2905
|
+
for binding in config.buttons
|
|
2906
|
+
]
|
|
2907
|
+
needs_button_inventory = button_write_intent or any(
|
|
2908
|
+
_coerce_positive_int(ref) is None for ref in view_config_refs
|
|
2909
|
+
)
|
|
2567
2910
|
try:
|
|
2568
|
-
existing_buttons =
|
|
2911
|
+
existing_buttons = (
|
|
2912
|
+
self._load_custom_buttons_for_builder(profile=profile, app_key=app_key)
|
|
2913
|
+
if needs_button_inventory
|
|
2914
|
+
else []
|
|
2915
|
+
)
|
|
2569
2916
|
except (QingflowApiError, RuntimeError) as error:
|
|
2570
2917
|
api_error = _coerce_api_error(error)
|
|
2571
2918
|
return finalize(_failed_from_api_error(
|
|
@@ -2796,19 +3143,22 @@ class AiBuilderFacade:
|
|
|
2796
3143
|
)
|
|
2797
3144
|
)
|
|
2798
3145
|
|
|
2799
|
-
edit_version_no
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
3146
|
+
edit_version_no = None
|
|
3147
|
+
if button_write_intent:
|
|
3148
|
+
edit_version_no, edit_context_error = self._ensure_app_edit_context(
|
|
3149
|
+
profile=profile,
|
|
3150
|
+
app_key=app_key,
|
|
3151
|
+
normalized_args=normalized_args,
|
|
3152
|
+
failure_code="CUSTOM_BUTTON_APPLY_FAILED",
|
|
3153
|
+
)
|
|
3154
|
+
if edit_context_error is not None:
|
|
3155
|
+
return finalize(edit_context_error)
|
|
2807
3156
|
|
|
2808
3157
|
created: list[dict[str, Any]] = []
|
|
2809
3158
|
updated: list[dict[str, Any]] = []
|
|
2810
3159
|
removed: list[dict[str, Any]] = []
|
|
2811
3160
|
failed: list[dict[str, Any]] = []
|
|
3161
|
+
readback_errors: list[JSONObject] = []
|
|
2812
3162
|
client_key_map: dict[str, int] = {}
|
|
2813
3163
|
write_executed = False
|
|
2814
3164
|
|
|
@@ -2831,7 +3181,15 @@ class AiBuilderFacade:
|
|
|
2831
3181
|
]
|
|
2832
3182
|
if len(matches) == 1:
|
|
2833
3183
|
button_id = _coerce_positive_int(matches[0].get("button_id"))
|
|
2834
|
-
except (QingflowApiError, RuntimeError):
|
|
3184
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
3185
|
+
readback_errors.append(
|
|
3186
|
+
{
|
|
3187
|
+
"resource": "custom_buttons",
|
|
3188
|
+
"phase": "create_id_lookup",
|
|
3189
|
+
"index": op["index"],
|
|
3190
|
+
"transport_error": _transport_error_payload(_coerce_api_error(error)),
|
|
3191
|
+
}
|
|
3192
|
+
)
|
|
2835
3193
|
button_id = None
|
|
2836
3194
|
entry = {
|
|
2837
3195
|
"index": op["index"],
|
|
@@ -2919,13 +3277,20 @@ class AiBuilderFacade:
|
|
|
2919
3277
|
}
|
|
2920
3278
|
)
|
|
2921
3279
|
|
|
2922
|
-
needs_button_list_readback = bool(created or updated or request.view_configs)
|
|
3280
|
+
needs_button_list_readback = bool(created or updated or (request.view_configs and needs_button_inventory))
|
|
2923
3281
|
readback_buttons: list[dict[str, Any]] = []
|
|
2924
3282
|
readback_failed = False
|
|
2925
3283
|
if needs_button_list_readback:
|
|
2926
3284
|
try:
|
|
2927
3285
|
readback_buttons = self._load_custom_buttons_for_builder(profile=profile, app_key=app_key)
|
|
2928
|
-
except (QingflowApiError, RuntimeError):
|
|
3286
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
3287
|
+
readback_errors.append(
|
|
3288
|
+
{
|
|
3289
|
+
"resource": "custom_buttons",
|
|
3290
|
+
"phase": "final_list",
|
|
3291
|
+
"transport_error": _transport_error_payload(_coerce_api_error(error)),
|
|
3292
|
+
}
|
|
3293
|
+
)
|
|
2929
3294
|
readback_failed = True
|
|
2930
3295
|
readback_ids = {
|
|
2931
3296
|
button_id
|
|
@@ -3018,6 +3383,13 @@ class AiBuilderFacade:
|
|
|
3018
3383
|
"custom button delete was sent, but deletion readback is not fully verified; do not blindly repeat delete",
|
|
3019
3384
|
)
|
|
3020
3385
|
)
|
|
3386
|
+
if readback_errors:
|
|
3387
|
+
warnings.append(
|
|
3388
|
+
_warning(
|
|
3389
|
+
"CUSTOM_BUTTON_READBACK_UNAVAILABLE",
|
|
3390
|
+
"custom button write was executed but readback is unavailable",
|
|
3391
|
+
)
|
|
3392
|
+
)
|
|
3021
3393
|
response = {
|
|
3022
3394
|
"status": status,
|
|
3023
3395
|
"error_code": error_code,
|
|
@@ -3033,6 +3405,7 @@ class AiBuilderFacade:
|
|
|
3033
3405
|
"edit_version_no": edit_version_no,
|
|
3034
3406
|
"button_ids_by_client_key": client_key_map,
|
|
3035
3407
|
"readback_failed": readback_failed,
|
|
3408
|
+
**({"readback_errors": readback_errors} if readback_errors else {}),
|
|
3036
3409
|
"compiled_match_rules": {
|
|
3037
3410
|
str(index): _summarize_compiled_match_rules(config.get("que_relation") or [])
|
|
3038
3411
|
for index, config in compiled_add_data_configs.items()
|
|
@@ -3066,7 +3439,15 @@ class AiBuilderFacade:
|
|
|
3066
3439
|
"write_succeeded": write_succeeded,
|
|
3067
3440
|
"safe_to_retry": not write_executed,
|
|
3068
3441
|
}
|
|
3069
|
-
return finalize(
|
|
3442
|
+
return finalize(
|
|
3443
|
+
self._append_publish_result(
|
|
3444
|
+
profile=profile,
|
|
3445
|
+
app_key=app_key,
|
|
3446
|
+
publish=bool(write_succeeded and button_write_intent),
|
|
3447
|
+
response=response,
|
|
3448
|
+
edit_version_no=edit_version_no,
|
|
3449
|
+
)
|
|
3450
|
+
)
|
|
3070
3451
|
|
|
3071
3452
|
def app_custom_button_list(self, *, profile: str, app_key: str) -> JSONObject:
|
|
3072
3453
|
normalized_args = {"app_key": app_key}
|
|
@@ -3235,6 +3616,9 @@ class AiBuilderFacade:
|
|
|
3235
3616
|
"verified": False,
|
|
3236
3617
|
"app_key": app_key,
|
|
3237
3618
|
"button_id": button_id,
|
|
3619
|
+
"write_executed": True,
|
|
3620
|
+
"write_succeeded": True,
|
|
3621
|
+
"safe_to_retry": False,
|
|
3238
3622
|
}
|
|
3239
3623
|
if _is_permission_restricted_api_error(api_error):
|
|
3240
3624
|
response = _apply_permission_outcomes(
|
|
@@ -3348,6 +3732,9 @@ class AiBuilderFacade:
|
|
|
3348
3732
|
"verified": False,
|
|
3349
3733
|
"app_key": app_key,
|
|
3350
3734
|
"button_id": button_id,
|
|
3735
|
+
"write_executed": True,
|
|
3736
|
+
"write_succeeded": True,
|
|
3737
|
+
"safe_to_retry": False,
|
|
3351
3738
|
}
|
|
3352
3739
|
if _is_permission_restricted_api_error(api_error):
|
|
3353
3740
|
response = _apply_permission_outcomes(
|
|
@@ -3686,15 +4073,32 @@ class AiBuilderFacade:
|
|
|
3686
4073
|
normalized_args = request.model_dump(mode="json", exclude_none=True)
|
|
3687
4074
|
app_key = request.app_key
|
|
3688
4075
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
4076
|
+
resource_write_intent = bool(
|
|
4077
|
+
request.upsert_resources
|
|
4078
|
+
or request.patch_resources
|
|
4079
|
+
or request.remove_associated_item_ids
|
|
4080
|
+
or request.reorder_associated_item_ids
|
|
3694
4081
|
)
|
|
3695
|
-
if
|
|
3696
|
-
|
|
3697
|
-
|
|
4082
|
+
if resource_write_intent:
|
|
4083
|
+
permission_outcome = self._guard_app_permission(
|
|
4084
|
+
profile=profile,
|
|
4085
|
+
app_key=app_key,
|
|
4086
|
+
required_permission="edit_app",
|
|
4087
|
+
normalized_args=normalized_args,
|
|
4088
|
+
)
|
|
4089
|
+
if permission_outcome.block is not None:
|
|
4090
|
+
return permission_outcome.block
|
|
4091
|
+
permission_outcomes.append(permission_outcome)
|
|
4092
|
+
if request.view_configs:
|
|
4093
|
+
permission_outcome = self._guard_app_permission(
|
|
4094
|
+
profile=profile,
|
|
4095
|
+
app_key=app_key,
|
|
4096
|
+
required_permission="view_manage",
|
|
4097
|
+
normalized_args=normalized_args,
|
|
4098
|
+
)
|
|
4099
|
+
if permission_outcome.block is not None:
|
|
4100
|
+
return permission_outcome.block
|
|
4101
|
+
permission_outcomes.append(permission_outcome)
|
|
3698
4102
|
|
|
3699
4103
|
def finalize(response: JSONObject) -> JSONObject:
|
|
3700
4104
|
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
@@ -3957,14 +4361,16 @@ class AiBuilderFacade:
|
|
|
3957
4361
|
}
|
|
3958
4362
|
return finalize(response)
|
|
3959
4363
|
|
|
3960
|
-
edit_version_no
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
4364
|
+
edit_version_no = None
|
|
4365
|
+
if resource_write_intent:
|
|
4366
|
+
edit_version_no, edit_context_error = self._ensure_app_edit_context(
|
|
4367
|
+
profile=profile,
|
|
4368
|
+
app_key=app_key,
|
|
4369
|
+
normalized_args=normalized_args,
|
|
4370
|
+
failure_code="ASSOCIATED_RESOURCES_APPLY_FAILED",
|
|
4371
|
+
)
|
|
4372
|
+
if edit_context_error is not None:
|
|
4373
|
+
return finalize(edit_context_error)
|
|
3968
4374
|
|
|
3969
4375
|
created: list[dict[str, Any]] = []
|
|
3970
4376
|
updated: list[dict[str, Any]] = []
|
|
@@ -3973,6 +4379,8 @@ class AiBuilderFacade:
|
|
|
3973
4379
|
reordered: list[int] = []
|
|
3974
4380
|
view_config_results: list[dict[str, Any]] = []
|
|
3975
4381
|
failed: list[dict[str, Any]] = []
|
|
4382
|
+
readback_errors: list[JSONObject] = []
|
|
4383
|
+
verification_errors: list[JSONObject] = []
|
|
3976
4384
|
write_executed = False
|
|
3977
4385
|
|
|
3978
4386
|
for op in upsert_ops:
|
|
@@ -3985,20 +4393,33 @@ class AiBuilderFacade:
|
|
|
3985
4393
|
client_key_to_id[str(patch.client_key)] = item_id
|
|
3986
4394
|
elif op["operation"] == "create":
|
|
3987
4395
|
write_executed = True
|
|
3988
|
-
self._associated_resource_create(
|
|
4396
|
+
create_result = self._associated_resource_create(
|
|
3989
4397
|
profile=profile,
|
|
3990
4398
|
app_key=app_key,
|
|
3991
4399
|
patch=patch,
|
|
3992
4400
|
match_rules_override=compiled_resource_match_rules.get(int(op["index"])),
|
|
3993
4401
|
)
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4402
|
+
created_id = _extract_associated_resource_id_from_result(create_result)
|
|
4403
|
+
try:
|
|
4404
|
+
readback_resources = self._load_associated_resources_for_builder(profile=profile, app_key=app_key)
|
|
4405
|
+
matches = [
|
|
4406
|
+
item
|
|
4407
|
+
for item in readback_resources
|
|
4408
|
+
if _associated_resource_matches_patch(item, patch)
|
|
4409
|
+
and _coerce_positive_int(item.get("associated_item_id")) is not None
|
|
4410
|
+
]
|
|
4411
|
+
readback_id = _coerce_positive_int(matches[0].get("associated_item_id")) if len(matches) == 1 else None
|
|
4412
|
+
if readback_id is not None:
|
|
4413
|
+
created_id = readback_id
|
|
4414
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
4415
|
+
readback_errors.append(
|
|
4416
|
+
{
|
|
4417
|
+
"resource": "associated_resources",
|
|
4418
|
+
"phase": "create_id_lookup",
|
|
4419
|
+
"index": op["index"],
|
|
4420
|
+
"transport_error": _transport_error_payload(_coerce_api_error(error)),
|
|
4421
|
+
}
|
|
4422
|
+
)
|
|
4002
4423
|
created.append(_associated_resource_result_entry("create", op["index"], patch, associated_item_id=created_id))
|
|
4003
4424
|
if created_id is not None and patch.client_key:
|
|
4004
4425
|
client_key_to_id[str(patch.client_key)] = created_id
|
|
@@ -4064,7 +4485,8 @@ class AiBuilderFacade:
|
|
|
4064
4485
|
try:
|
|
4065
4486
|
resources_after = self._load_associated_resources_for_builder(profile=profile, app_key=app_key)
|
|
4066
4487
|
resources_after_loaded = True
|
|
4067
|
-
except (QingflowApiError, RuntimeError):
|
|
4488
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
4489
|
+
readback_errors.append({"resource": "associated_resources", "phase": "pre_view_config", "transport_error": _transport_error_payload(_coerce_api_error(error))})
|
|
4068
4490
|
resources_after = []
|
|
4069
4491
|
resources_after_readback_failed = True
|
|
4070
4492
|
|
|
@@ -4126,7 +4548,15 @@ class AiBuilderFacade:
|
|
|
4126
4548
|
available_resources=refreshed_resources,
|
|
4127
4549
|
)
|
|
4128
4550
|
verified_config = _associated_resources_config_matches(expected_config, actual_config)
|
|
4129
|
-
except (QingflowApiError, RuntimeError):
|
|
4551
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
4552
|
+
verification_errors.append(
|
|
4553
|
+
{
|
|
4554
|
+
"resource": "view_associated_resources_config",
|
|
4555
|
+
"phase": "view_config_readback",
|
|
4556
|
+
"view_key": view_config.view_key,
|
|
4557
|
+
"transport_error": _transport_error_payload(_coerce_api_error(error)),
|
|
4558
|
+
}
|
|
4559
|
+
)
|
|
4130
4560
|
actual_config = {}
|
|
4131
4561
|
verified_config = False
|
|
4132
4562
|
view_config_results.append({"index": index, "view_key": view_config.view_key, "view_name": view_name, "status": "success" if verified_config else "partial_success", "associated_resources_verified": verified_config, "expected": expected_config, "actual": actual_config})
|
|
@@ -4139,7 +4569,8 @@ class AiBuilderFacade:
|
|
|
4139
4569
|
if not resources_after_loaded and not resources_after_readback_failed:
|
|
4140
4570
|
try:
|
|
4141
4571
|
final_resources = self._load_associated_resources_for_builder(profile=profile, app_key=app_key)
|
|
4142
|
-
except (QingflowApiError, RuntimeError):
|
|
4572
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
4573
|
+
readback_errors.append({"resource": "associated_resources", "phase": "final_pool", "transport_error": _transport_error_payload(_coerce_api_error(error))})
|
|
4143
4574
|
readback_failed = True
|
|
4144
4575
|
final_by_id = _associated_resource_index(final_resources)
|
|
4145
4576
|
if removed:
|
|
@@ -4199,6 +4630,20 @@ class AiBuilderFacade:
|
|
|
4199
4630
|
"associated resource delete was sent, but deletion readback is not fully verified; do not blindly repeat delete",
|
|
4200
4631
|
)
|
|
4201
4632
|
)
|
|
4633
|
+
if readback_errors:
|
|
4634
|
+
warnings.append(
|
|
4635
|
+
_warning(
|
|
4636
|
+
"ASSOCIATED_RESOURCES_READBACK_UNAVAILABLE",
|
|
4637
|
+
"associated resource write was executed but readback is unavailable",
|
|
4638
|
+
)
|
|
4639
|
+
)
|
|
4640
|
+
if verification_errors:
|
|
4641
|
+
warnings.append(
|
|
4642
|
+
_warning(
|
|
4643
|
+
"ASSOCIATED_RESOURCE_VIEW_CONFIG_VERIFICATION_UNAVAILABLE",
|
|
4644
|
+
"associated resource view config write was executed but verification readback is unavailable",
|
|
4645
|
+
)
|
|
4646
|
+
)
|
|
4202
4647
|
response = {
|
|
4203
4648
|
"status": status,
|
|
4204
4649
|
"error_code": error_code,
|
|
@@ -4211,6 +4656,8 @@ class AiBuilderFacade:
|
|
|
4211
4656
|
"edit_version_no": edit_version_no,
|
|
4212
4657
|
"associated_item_ids_by_client_key": client_key_to_id,
|
|
4213
4658
|
"readback_failed": readback_failed,
|
|
4659
|
+
**({"readback_errors": readback_errors} if readback_errors else {}),
|
|
4660
|
+
**({"verification_errors": verification_errors} if verification_errors else {}),
|
|
4214
4661
|
"compiled_match_rules": {
|
|
4215
4662
|
str(index): _summarize_compiled_match_rules(rules)
|
|
4216
4663
|
for index, rules in compiled_resource_match_rules.items()
|
|
@@ -4246,11 +4693,39 @@ class AiBuilderFacade:
|
|
|
4246
4693
|
"safe_to_retry": not write_executed,
|
|
4247
4694
|
"associated_resources": final_resources,
|
|
4248
4695
|
}
|
|
4249
|
-
response = self._append_publish_result(
|
|
4696
|
+
response = self._append_publish_result(
|
|
4697
|
+
profile=profile,
|
|
4698
|
+
app_key=app_key,
|
|
4699
|
+
publish=bool(write_succeeded and resource_write_intent),
|
|
4700
|
+
response=response,
|
|
4701
|
+
edit_version_no=edit_version_no,
|
|
4702
|
+
)
|
|
4250
4703
|
if response.get("published") and view_config_results:
|
|
4251
4704
|
try:
|
|
4252
4705
|
post_publish_resources = self._load_associated_resources_for_builder(profile=profile, app_key=app_key)
|
|
4253
|
-
except (QingflowApiError, RuntimeError):
|
|
4706
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
4707
|
+
details = response.get("details") if isinstance(response.get("details"), dict) else {}
|
|
4708
|
+
response["details"] = details
|
|
4709
|
+
post_publish_readback_errors = details.get("readback_errors") if isinstance(details.get("readback_errors"), list) else []
|
|
4710
|
+
post_publish_readback_errors.append(
|
|
4711
|
+
{
|
|
4712
|
+
"resource": "associated_resources",
|
|
4713
|
+
"phase": "post_publish_pool",
|
|
4714
|
+
"transport_error": _transport_error_payload(_coerce_api_error(error)),
|
|
4715
|
+
}
|
|
4716
|
+
)
|
|
4717
|
+
details["readback_errors"] = post_publish_readback_errors
|
|
4718
|
+
warnings = response.get("warnings")
|
|
4719
|
+
if not isinstance(warnings, list):
|
|
4720
|
+
warnings = []
|
|
4721
|
+
response["warnings"] = warnings
|
|
4722
|
+
if not any(isinstance(warning, dict) and warning.get("code") == "ASSOCIATED_RESOURCES_READBACK_UNAVAILABLE" for warning in warnings):
|
|
4723
|
+
warnings.append(
|
|
4724
|
+
_warning(
|
|
4725
|
+
"ASSOCIATED_RESOURCES_READBACK_UNAVAILABLE",
|
|
4726
|
+
"associated resource write was executed but readback is unavailable",
|
|
4727
|
+
)
|
|
4728
|
+
)
|
|
4254
4729
|
post_publish_resources = final_resources
|
|
4255
4730
|
if post_publish_resources:
|
|
4256
4731
|
response["associated_resources"] = post_publish_resources
|
|
@@ -4268,7 +4743,30 @@ class AiBuilderFacade:
|
|
|
4268
4743
|
config if isinstance(config, dict) else {},
|
|
4269
4744
|
available_resources=post_publish_resources,
|
|
4270
4745
|
)
|
|
4271
|
-
except (QingflowApiError, RuntimeError):
|
|
4746
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
4747
|
+
details = response.get("details") if isinstance(response.get("details"), dict) else {}
|
|
4748
|
+
response["details"] = details
|
|
4749
|
+
post_publish_verification_errors = details.get("verification_errors") if isinstance(details.get("verification_errors"), list) else []
|
|
4750
|
+
post_publish_verification_errors.append(
|
|
4751
|
+
{
|
|
4752
|
+
"resource": "view_associated_resources_config",
|
|
4753
|
+
"phase": "post_publish_view_config_readback",
|
|
4754
|
+
"view_key": view_key,
|
|
4755
|
+
"transport_error": _transport_error_payload(_coerce_api_error(error)),
|
|
4756
|
+
}
|
|
4757
|
+
)
|
|
4758
|
+
details["verification_errors"] = post_publish_verification_errors
|
|
4759
|
+
warnings = response.get("warnings")
|
|
4760
|
+
if not isinstance(warnings, list):
|
|
4761
|
+
warnings = []
|
|
4762
|
+
response["warnings"] = warnings
|
|
4763
|
+
if not any(isinstance(warning, dict) and warning.get("code") == "ASSOCIATED_RESOURCE_VIEW_CONFIG_VERIFICATION_UNAVAILABLE" for warning in warnings):
|
|
4764
|
+
warnings.append(
|
|
4765
|
+
_warning(
|
|
4766
|
+
"ASSOCIATED_RESOURCE_VIEW_CONFIG_VERIFICATION_UNAVAILABLE",
|
|
4767
|
+
"associated resource view config write was executed but verification readback is unavailable",
|
|
4768
|
+
)
|
|
4769
|
+
)
|
|
4272
4770
|
continue
|
|
4273
4771
|
if _associated_resources_config_matches(expected_config, actual_config):
|
|
4274
4772
|
result["status"] = "success"
|
|
@@ -4318,11 +4816,14 @@ class AiBuilderFacade:
|
|
|
4318
4816
|
*,
|
|
4319
4817
|
profile: str,
|
|
4320
4818
|
app_name: str,
|
|
4321
|
-
package_tag_id: int,
|
|
4819
|
+
package_tag_id: int | None,
|
|
4322
4820
|
) -> list[JSONObject]:
|
|
4323
4821
|
try:
|
|
4324
4822
|
listing = self.apps.app_list(profile=profile, ship_auth=False)
|
|
4325
|
-
except (QingflowApiError, RuntimeError):
|
|
4823
|
+
except (QingflowApiError, RuntimeError) as exc:
|
|
4824
|
+
api_error = _coerce_api_error(exc)
|
|
4825
|
+
if not _is_optional_builder_lookup_error(api_error):
|
|
4826
|
+
raise
|
|
4326
4827
|
return []
|
|
4327
4828
|
items = listing.get("items") if isinstance(listing.get("items"), list) else []
|
|
4328
4829
|
matches: list[JSONObject] = []
|
|
@@ -4340,7 +4841,7 @@ class AiBuilderFacade:
|
|
|
4340
4841
|
tag_id = _coerce_positive_int(item.get("tag_id"))
|
|
4341
4842
|
if tag_id is not None and tag_id not in tag_ids:
|
|
4342
4843
|
tag_ids.append(tag_id)
|
|
4343
|
-
if package_tag_id not in tag_ids:
|
|
4844
|
+
if package_tag_id is not None and package_tag_id > 0 and package_tag_id not in tag_ids:
|
|
4344
4845
|
continue
|
|
4345
4846
|
seen_app_keys.add(candidate_key)
|
|
4346
4847
|
matches.append({"app_key": candidate_key, "app_name": title, "tag_ids": tag_ids})
|
|
@@ -4355,7 +4856,10 @@ class AiBuilderFacade:
|
|
|
4355
4856
|
) -> list[JSONObject]:
|
|
4356
4857
|
try:
|
|
4357
4858
|
package_result = self.packages.package_get(profile=profile, tag_id=package_tag_id, include_raw=True)
|
|
4358
|
-
except (QingflowApiError, RuntimeError):
|
|
4859
|
+
except (QingflowApiError, RuntimeError) as exc:
|
|
4860
|
+
api_error = _coerce_api_error(exc)
|
|
4861
|
+
if not _is_optional_builder_lookup_error(api_error):
|
|
4862
|
+
raise
|
|
4359
4863
|
return []
|
|
4360
4864
|
raw_package = package_result.get("result") if isinstance(package_result.get("result"), dict) else {}
|
|
4361
4865
|
tag_items = raw_package.get("tagItems") if isinstance(raw_package.get("tagItems"), list) else []
|
|
@@ -4394,23 +4898,15 @@ class AiBuilderFacade:
|
|
|
4394
4898
|
"tag_ids": _coerce_int_list(base.get("tagIds")),
|
|
4395
4899
|
"can_edit_app": _coerce_optional_bool(base.get("editItemStatus")),
|
|
4396
4900
|
"can_manage_data": _coerce_optional_bool(base.get("dataManageStatus")),
|
|
4901
|
+
"can_manage_views": (
|
|
4902
|
+
_coerce_optional_bool(base.get("beingViewManageStatus"))
|
|
4903
|
+
if "beingViewManageStatus" in base
|
|
4904
|
+
else _coerce_optional_bool(base.get("dataManageStatus"))
|
|
4905
|
+
),
|
|
4397
4906
|
"can_delete_app": _coerce_optional_bool(base.get("deleteItemStatus")),
|
|
4398
4907
|
"can_copy_app": _coerce_optional_bool(base.get("copyAppStatus")),
|
|
4399
4908
|
}
|
|
4400
4909
|
|
|
4401
|
-
def _derive_can_edit_app_base(self, *, profile: str, permission_summary: JSONObject) -> bool:
|
|
4402
|
-
if permission_summary.get("can_edit_app") is not True:
|
|
4403
|
-
return False
|
|
4404
|
-
tag_ids = _coerce_int_list(permission_summary.get("tag_ids"))
|
|
4405
|
-
for tag_id in tag_ids:
|
|
4406
|
-
try:
|
|
4407
|
-
package_permission = self._read_package_permission_summary(profile=profile, tag_id=tag_id)
|
|
4408
|
-
except (QingflowApiError, RuntimeError):
|
|
4409
|
-
return False
|
|
4410
|
-
if package_permission.get("can_edit_tag") is not True:
|
|
4411
|
-
return False
|
|
4412
|
-
return True
|
|
4413
|
-
|
|
4414
4910
|
def _read_portal_permission_summary(self, *, dash_key: str, portal_result: dict[str, Any]) -> JSONObject:
|
|
4415
4911
|
tag_ids = _coerce_int_list(portal_result.get("tagIds"))
|
|
4416
4912
|
if not tag_ids:
|
|
@@ -4435,42 +4931,45 @@ class AiBuilderFacade:
|
|
|
4435
4931
|
app_key: str,
|
|
4436
4932
|
required_permission: str,
|
|
4437
4933
|
normalized_args: JSONObject,
|
|
4934
|
+
permission_summary: JSONObject | None = None,
|
|
4438
4935
|
) -> PermissionCheckOutcome:
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
"
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4936
|
+
if permission_summary is None:
|
|
4937
|
+
try:
|
|
4938
|
+
permission_summary = self._read_app_permission_summary(profile=profile, app_key=app_key)
|
|
4939
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
4940
|
+
api_error = _coerce_api_error(error)
|
|
4941
|
+
if _is_permission_restricted_api_error(api_error):
|
|
4942
|
+
return _permission_skip_outcome(
|
|
4943
|
+
scope="app",
|
|
4944
|
+
target={"app_key": app_key},
|
|
4945
|
+
required_permission=required_permission,
|
|
4946
|
+
transport_error=_transport_error_payload(api_error),
|
|
4947
|
+
)
|
|
4948
|
+
return PermissionCheckOutcome(
|
|
4949
|
+
block=_failed(
|
|
4950
|
+
"APP_PERMISSION_UNVERIFIED",
|
|
4951
|
+
"could not confirm current user's builder permissions for this app",
|
|
4952
|
+
normalized_args=normalized_args,
|
|
4953
|
+
details={
|
|
4954
|
+
"app_key": app_key,
|
|
4955
|
+
"required_permission": required_permission,
|
|
4956
|
+
"permission_read_error": {
|
|
4957
|
+
"message": api_error.message,
|
|
4958
|
+
"http_status": api_error.http_status,
|
|
4959
|
+
"backend_code": api_error.backend_code,
|
|
4960
|
+
"category": api_error.category,
|
|
4961
|
+
},
|
|
4463
4962
|
},
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4963
|
+
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
4964
|
+
request_id=api_error.request_id,
|
|
4965
|
+
backend_code=api_error.backend_code,
|
|
4966
|
+
http_status=None if api_error.http_status == 404 else api_error.http_status,
|
|
4967
|
+
)
|
|
4469
4968
|
)
|
|
4470
|
-
)
|
|
4471
4969
|
permission_key = {
|
|
4472
4970
|
"edit_app": "can_edit_app",
|
|
4473
4971
|
"data_manage": "can_manage_data",
|
|
4972
|
+
"view_manage": "can_manage_views",
|
|
4474
4973
|
}.get(required_permission)
|
|
4475
4974
|
if permission_key is None:
|
|
4476
4975
|
return PermissionCheckOutcome()
|
|
@@ -4484,12 +4983,15 @@ class AiBuilderFacade:
|
|
|
4484
4983
|
)
|
|
4485
4984
|
if permission_value is not False:
|
|
4486
4985
|
return PermissionCheckOutcome()
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
"current user does not have builder edit-app permission on this app"
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4986
|
+
if required_permission == "edit_app":
|
|
4987
|
+
error_code = "EDIT_APP_UNAUTHORIZED"
|
|
4988
|
+
message = "current user does not have builder edit-app permission on this app"
|
|
4989
|
+
elif required_permission == "view_manage":
|
|
4990
|
+
error_code = "VIEW_MANAGE_UNAUTHORIZED"
|
|
4991
|
+
message = "current user does not have view-management permission on this app"
|
|
4992
|
+
else:
|
|
4993
|
+
error_code = "DATA_MANAGE_UNAUTHORIZED"
|
|
4994
|
+
message = "current user does not have data-management permission on this app"
|
|
4493
4995
|
return PermissionCheckOutcome(
|
|
4494
4996
|
block=_failed(
|
|
4495
4997
|
error_code,
|
|
@@ -4652,38 +5154,78 @@ class AiBuilderFacade:
|
|
|
4652
5154
|
details={"app_key": app_key},
|
|
4653
5155
|
suggested_next_call={"tool_name": "app_resolve", "arguments": {"profile": profile, "app_key": app_key}},
|
|
4654
5156
|
)
|
|
4655
|
-
views, views_unavailable = self._load_views_result(
|
|
5157
|
+
views, views_unavailable, views_read_error = self._load_views_result(
|
|
4656
5158
|
profile=profile,
|
|
4657
5159
|
app_key=app_key,
|
|
4658
5160
|
tolerate_404=True,
|
|
4659
5161
|
tolerate_permission_restricted=True,
|
|
5162
|
+
include_error=True,
|
|
4660
5163
|
)
|
|
4661
5164
|
view_summaries = _summarize_views(views)
|
|
5165
|
+
readback_errors: list[JSONObject] = []
|
|
4662
5166
|
charts_unavailable = False
|
|
4663
5167
|
try:
|
|
4664
5168
|
chart_items, _chart_source = self._load_chart_list_for_builder(profile=profile, app_key=app_key)
|
|
4665
5169
|
chart_summaries = _summarize_charts(chart_items)
|
|
4666
|
-
except (QingflowApiError, RuntimeError):
|
|
5170
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
4667
5171
|
charts_unavailable = True
|
|
4668
5172
|
chart_summaries = []
|
|
5173
|
+
readback_errors.append(
|
|
5174
|
+
{
|
|
5175
|
+
"resource": "charts",
|
|
5176
|
+
"phase": "summary",
|
|
5177
|
+
"transport_error": _transport_error_payload(_coerce_api_error(error)),
|
|
5178
|
+
}
|
|
5179
|
+
)
|
|
4669
5180
|
associated_resources_unavailable = False
|
|
4670
5181
|
try:
|
|
4671
5182
|
associated_resources = self._load_associated_resources_for_builder(profile=profile, app_key=app_key)
|
|
4672
|
-
except (QingflowApiError, RuntimeError):
|
|
5183
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
4673
5184
|
associated_resources_unavailable = True
|
|
4674
5185
|
associated_resources = []
|
|
5186
|
+
readback_errors.append(
|
|
5187
|
+
{
|
|
5188
|
+
"resource": "associated_resources",
|
|
5189
|
+
"phase": "summary",
|
|
5190
|
+
"transport_error": _transport_error_payload(_coerce_api_error(error)),
|
|
5191
|
+
}
|
|
5192
|
+
)
|
|
4675
5193
|
custom_buttons_unavailable = False
|
|
4676
5194
|
try:
|
|
4677
5195
|
custom_buttons = self._load_custom_buttons_for_builder(profile=profile, app_key=app_key)
|
|
4678
|
-
except (QingflowApiError, RuntimeError):
|
|
5196
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
4679
5197
|
custom_buttons_unavailable = True
|
|
4680
5198
|
custom_buttons = []
|
|
4681
|
-
|
|
5199
|
+
readback_errors.append(
|
|
5200
|
+
{
|
|
5201
|
+
"resource": "custom_buttons",
|
|
5202
|
+
"phase": "summary",
|
|
5203
|
+
"transport_error": _transport_error_payload(_coerce_api_error(error)),
|
|
5204
|
+
}
|
|
5205
|
+
)
|
|
5206
|
+
workflow, workflow_unavailable, workflow_read_error = self._load_workflow_result(
|
|
4682
5207
|
profile=profile,
|
|
4683
5208
|
app_key=app_key,
|
|
4684
5209
|
tolerate_404=True,
|
|
4685
5210
|
tolerate_permission_restricted=True,
|
|
5211
|
+
include_error=True,
|
|
4686
5212
|
)
|
|
5213
|
+
if views_read_error is not None:
|
|
5214
|
+
readback_errors.append(
|
|
5215
|
+
{
|
|
5216
|
+
"resource": "views",
|
|
5217
|
+
"phase": "summary",
|
|
5218
|
+
"transport_error": _transport_error_payload(views_read_error),
|
|
5219
|
+
}
|
|
5220
|
+
)
|
|
5221
|
+
if workflow_read_error is not None:
|
|
5222
|
+
readback_errors.append(
|
|
5223
|
+
{
|
|
5224
|
+
"resource": "workflow",
|
|
5225
|
+
"phase": "summary",
|
|
5226
|
+
"transport_error": _transport_error_payload(workflow_read_error),
|
|
5227
|
+
}
|
|
5228
|
+
)
|
|
4687
5229
|
verification_hints = _build_verification_hints(
|
|
4688
5230
|
tag_ids=_coerce_int_list(base_result.get("tagIds")),
|
|
4689
5231
|
fields=parsed["fields"],
|
|
@@ -4751,7 +5293,7 @@ class AiBuilderFacade:
|
|
|
4751
5293
|
"normalized_args": {"app_key": app_key},
|
|
4752
5294
|
"missing_fields": [],
|
|
4753
5295
|
"allowed_values": {},
|
|
4754
|
-
"details": {},
|
|
5296
|
+
"details": {"readback_errors": readback_errors} if readback_errors else {},
|
|
4755
5297
|
"request_id": None,
|
|
4756
5298
|
"suggested_next_call": None,
|
|
4757
5299
|
"noop": False,
|
|
@@ -4780,13 +5322,54 @@ class AiBuilderFacade:
|
|
|
4780
5322
|
if not result.get("suggested_next_call"):
|
|
4781
5323
|
result["suggested_next_call"] = {"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}}
|
|
4782
5324
|
return result
|
|
4783
|
-
|
|
5325
|
+
try:
|
|
5326
|
+
permission_summary = self._read_app_permission_summary(profile=profile, app_key=app_key)
|
|
5327
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
5328
|
+
api_error = _coerce_api_error(error)
|
|
5329
|
+
if not _is_permission_restricted_api_error(api_error):
|
|
5330
|
+
raise
|
|
5331
|
+
result["message"] = "read app config summary; editability unverified"
|
|
5332
|
+
result["editability"] = {
|
|
5333
|
+
"can_edit_app_base": None,
|
|
5334
|
+
"can_edit_form": None,
|
|
5335
|
+
"can_edit_flow": None,
|
|
5336
|
+
"can_edit_views": None,
|
|
5337
|
+
"can_edit_charts": None,
|
|
5338
|
+
}
|
|
5339
|
+
details = result.get("details") if isinstance(result.get("details"), dict) else {}
|
|
5340
|
+
permission_read_errors = details.get("permission_read_errors") if isinstance(details.get("permission_read_errors"), list) else []
|
|
5341
|
+
permission_read_errors.append(
|
|
5342
|
+
{
|
|
5343
|
+
"resource": "app_permission",
|
|
5344
|
+
"app_key": app_key,
|
|
5345
|
+
"required_permission": None,
|
|
5346
|
+
"transport_error": _transport_error_payload(api_error),
|
|
5347
|
+
}
|
|
5348
|
+
)
|
|
5349
|
+
details["permission_read_errors"] = permission_read_errors
|
|
5350
|
+
details["permission_check_skipped"] = True
|
|
5351
|
+
result["details"] = details
|
|
5352
|
+
warnings = result.get("warnings") if isinstance(result.get("warnings"), list) else []
|
|
5353
|
+
warnings.append(
|
|
5354
|
+
_warning(
|
|
5355
|
+
"APP_EDITABILITY_UNAVAILABLE",
|
|
5356
|
+
"could not confirm current user's builder permissions for this app; app summary remains available",
|
|
5357
|
+
app_key=app_key,
|
|
5358
|
+
**_transport_error_payload(api_error),
|
|
5359
|
+
)
|
|
5360
|
+
)
|
|
5361
|
+
result["warnings"] = warnings
|
|
5362
|
+
verification = result.get("verification") if isinstance(result.get("verification"), dict) else {}
|
|
5363
|
+
verification["editability_unavailable"] = True
|
|
5364
|
+
result["verification"] = verification
|
|
5365
|
+
result["verified"] = False
|
|
5366
|
+
return result
|
|
4784
5367
|
result["message"] = "read app config summary"
|
|
4785
5368
|
result["editability"] = {
|
|
4786
|
-
"can_edit_app_base":
|
|
5369
|
+
"can_edit_app_base": permission_summary.get("can_edit_app"),
|
|
4787
5370
|
"can_edit_form": permission_summary.get("can_edit_app"),
|
|
4788
|
-
"can_edit_flow": permission_summary.get("
|
|
4789
|
-
"can_edit_views": permission_summary.get("
|
|
5371
|
+
"can_edit_flow": permission_summary.get("can_edit_app"),
|
|
5372
|
+
"can_edit_views": permission_summary.get("can_manage_views"),
|
|
4790
5373
|
"can_edit_charts": permission_summary.get("can_manage_data"),
|
|
4791
5374
|
}
|
|
4792
5375
|
return result
|
|
@@ -4994,6 +5577,8 @@ class AiBuilderFacade:
|
|
|
4994
5577
|
"would_update": bool(update_fields),
|
|
4995
5578
|
},
|
|
4996
5579
|
"verified": True,
|
|
5580
|
+
"write_executed": False,
|
|
5581
|
+
"safe_to_retry": True,
|
|
4997
5582
|
"app_key": app_key,
|
|
4998
5583
|
"apply": False,
|
|
4999
5584
|
"repair_plan": plans,
|
|
@@ -5020,6 +5605,8 @@ class AiBuilderFacade:
|
|
|
5020
5605
|
"applied": False,
|
|
5021
5606
|
},
|
|
5022
5607
|
"verified": True,
|
|
5608
|
+
"write_executed": False,
|
|
5609
|
+
"safe_to_retry": True,
|
|
5023
5610
|
"app_key": app_key,
|
|
5024
5611
|
"apply": True,
|
|
5025
5612
|
"repair_plan": plans,
|
|
@@ -5037,6 +5624,7 @@ class AiBuilderFacade:
|
|
|
5037
5624
|
)
|
|
5038
5625
|
if apply_result.get("status") == "failed":
|
|
5039
5626
|
return apply_result
|
|
5627
|
+
verification_error: JSONObject | None = None
|
|
5040
5628
|
try:
|
|
5041
5629
|
reread = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
5042
5630
|
verified_fields = cast(list[dict[str, Any]], reread["parsed"].get("fields") or [])
|
|
@@ -5057,17 +5645,36 @@ class AiBuilderFacade:
|
|
|
5057
5645
|
if plan["would_update"]:
|
|
5058
5646
|
plan["applied"] = True
|
|
5059
5647
|
applied_fields.append(plan["field_name"])
|
|
5060
|
-
except (QingflowApiError, RuntimeError):
|
|
5061
|
-
|
|
5648
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
5649
|
+
verification_error = _transport_error_payload(_coerce_api_error(error))
|
|
5062
5650
|
apply_result["message"] = "repaired code block fields"
|
|
5063
5651
|
apply_result["apply"] = True
|
|
5064
5652
|
apply_result["repair_plan"] = plans
|
|
5065
5653
|
apply_result["applied_fields"] = applied_fields
|
|
5654
|
+
if verification_error is None:
|
|
5655
|
+
existing_details = apply_result.get("details") if isinstance(apply_result.get("details"), dict) else {}
|
|
5656
|
+
existing_verification_error = existing_details.get("verification_error") if isinstance(existing_details, dict) else None
|
|
5657
|
+
if isinstance(existing_verification_error, dict):
|
|
5658
|
+
verification_error = deepcopy(existing_verification_error)
|
|
5659
|
+
if verification_error is not None:
|
|
5660
|
+
details = apply_result.get("details") if isinstance(apply_result.get("details"), dict) else {}
|
|
5661
|
+
details = deepcopy(details)
|
|
5662
|
+
details["code_block_repair_verification_error"] = verification_error
|
|
5663
|
+
apply_result["details"] = details
|
|
5664
|
+
warnings = apply_result.get("warnings") if isinstance(apply_result.get("warnings"), list) else []
|
|
5665
|
+
apply_result["warnings"] = [
|
|
5666
|
+
*warnings,
|
|
5667
|
+
_warning(
|
|
5668
|
+
"CODE_BLOCK_REPAIR_VERIFICATION_UNAVAILABLE",
|
|
5669
|
+
"code block repair write was executed but post-write verification readback is unavailable",
|
|
5670
|
+
),
|
|
5671
|
+
]
|
|
5066
5672
|
apply_result["verification"] = {
|
|
5067
5673
|
**(apply_result.get("verification") if isinstance(apply_result.get("verification"), dict) else {}),
|
|
5068
5674
|
"code_block_fields_scanned": len(plans),
|
|
5069
5675
|
"would_update": bool(update_fields),
|
|
5070
5676
|
"applied": bool(applied_fields),
|
|
5677
|
+
**({"code_block_repair_verification_unavailable": True} if verification_error is not None else {}),
|
|
5071
5678
|
}
|
|
5072
5679
|
return apply_result
|
|
5073
5680
|
|
|
@@ -5120,6 +5727,17 @@ class AiBuilderFacade:
|
|
|
5120
5727
|
)
|
|
5121
5728
|
except (QingflowApiError, RuntimeError) as error:
|
|
5122
5729
|
api_error = _coerce_api_error(error)
|
|
5730
|
+
if is_auth_like_error(api_error) or (
|
|
5731
|
+
backend_code_int(api_error) not in {40002, 40027, 404}
|
|
5732
|
+
and api_error.http_status != 404
|
|
5733
|
+
):
|
|
5734
|
+
return _failed_from_api_error(
|
|
5735
|
+
"APP_GET_FIELDS_FAILED",
|
|
5736
|
+
api_error,
|
|
5737
|
+
normalized_args={"app_key": app_key},
|
|
5738
|
+
details={"app_key": app_key, "resource": "qingbi_fields"},
|
|
5739
|
+
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
5740
|
+
)
|
|
5123
5741
|
warnings.append(
|
|
5124
5742
|
_warning(
|
|
5125
5743
|
"QINGBI_FIELDS_READ_FAILED",
|
|
@@ -5388,13 +6006,23 @@ class AiBuilderFacade:
|
|
|
5388
6006
|
continue
|
|
5389
6007
|
try:
|
|
5390
6008
|
portal_result = self.portals.portal_get(profile=profile, dash_key=dash_key, being_draft=True).get("result") or {}
|
|
5391
|
-
except (QingflowApiError, RuntimeError):
|
|
6009
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
6010
|
+
api_error = _coerce_api_error(error)
|
|
6011
|
+
if not _is_optional_builder_lookup_error(api_error):
|
|
6012
|
+
return _failed_from_api_error(
|
|
6013
|
+
"PORTAL_LIST_FAILED",
|
|
6014
|
+
api_error,
|
|
6015
|
+
normalized_args={},
|
|
6016
|
+
details={"dash_key": dash_key, "resource": "portal_detail"},
|
|
6017
|
+
suggested_next_call={"tool_name": "portal_get", "arguments": {"profile": profile, "dash_key": dash_key}},
|
|
6018
|
+
)
|
|
5392
6019
|
permission_verified = False
|
|
5393
6020
|
warnings.append(
|
|
5394
6021
|
_warning(
|
|
5395
6022
|
"PORTAL_PERMISSION_READ_UNAVAILABLE",
|
|
5396
6023
|
f"builder portal_list skipped `{dash_key}` because portal detail readback was unavailable during permission verification",
|
|
5397
6024
|
dash_key=dash_key,
|
|
6025
|
+
**_transport_error_payload(api_error),
|
|
5398
6026
|
)
|
|
5399
6027
|
)
|
|
5400
6028
|
continue
|
|
@@ -5446,8 +6074,10 @@ class AiBuilderFacade:
|
|
|
5446
6074
|
sorted_items = self.charts.qingbi_report_list_sorted(profile=profile, app_key=app_key, page_num=1, page_size=500).get("items") or []
|
|
5447
6075
|
if isinstance(sorted_items, list):
|
|
5448
6076
|
return sorted_items, "sorted"
|
|
5449
|
-
except (QingflowApiError, RuntimeError):
|
|
5450
|
-
|
|
6077
|
+
except (QingflowApiError, RuntimeError) as exc:
|
|
6078
|
+
api_error = _coerce_api_error(exc)
|
|
6079
|
+
if not _is_optional_builder_lookup_error(api_error):
|
|
6080
|
+
raise
|
|
5451
6081
|
fallback_items = self.charts.qingbi_report_list(profile=profile, app_key=app_key).get("items") or []
|
|
5452
6082
|
return list(fallback_items) if isinstance(fallback_items, list) else [], "fallback"
|
|
5453
6083
|
|
|
@@ -5890,6 +6520,7 @@ class AiBuilderFacade:
|
|
|
5890
6520
|
if button_id is not None:
|
|
5891
6521
|
button_inventory[button_id] = item
|
|
5892
6522
|
valid_custom_button_ids = (set(button_inventory) | set(created_ids) | set(updated_ids)) - set(removed_ids)
|
|
6523
|
+
allow_unverified_numeric_button_ids = not bool(valid_custom_button_ids)
|
|
5893
6524
|
write_executed = False
|
|
5894
6525
|
write_succeeded = False
|
|
5895
6526
|
all_verified = True
|
|
@@ -5941,6 +6572,7 @@ class AiBuilderFacade:
|
|
|
5941
6572
|
button_inventory=button_inventory,
|
|
5942
6573
|
valid_custom_button_ids=valid_custom_button_ids,
|
|
5943
6574
|
reason_path=f"view_configs[{config_index}].buttons[{button_index}].button_ref",
|
|
6575
|
+
allow_unverified_numeric_id=allow_unverified_numeric_button_ids,
|
|
5944
6576
|
)
|
|
5945
6577
|
if ref_issue:
|
|
5946
6578
|
config_issues.append(ref_issue)
|
|
@@ -5952,6 +6584,7 @@ class AiBuilderFacade:
|
|
|
5952
6584
|
binding=view_binding,
|
|
5953
6585
|
current_fields_by_name=current_fields_by_name,
|
|
5954
6586
|
valid_custom_button_ids=valid_custom_button_ids,
|
|
6587
|
+
allow_unverified_custom_button_id=allow_unverified_numeric_button_ids,
|
|
5955
6588
|
)
|
|
5956
6589
|
if binding_issues:
|
|
5957
6590
|
config_issues.extend(binding_issues)
|
|
@@ -6249,6 +6882,7 @@ class AiBuilderFacade:
|
|
|
6249
6882
|
)
|
|
6250
6883
|
|
|
6251
6884
|
warnings: list[dict[str, Any]] = []
|
|
6885
|
+
readback_errors: list[JSONObject] = []
|
|
6252
6886
|
verification = {
|
|
6253
6887
|
"view_exists": True,
|
|
6254
6888
|
"base_info_verified": True,
|
|
@@ -6259,39 +6893,133 @@ class AiBuilderFacade:
|
|
|
6259
6893
|
|
|
6260
6894
|
base_info: dict[str, Any] = {}
|
|
6261
6895
|
try:
|
|
6262
|
-
|
|
6896
|
+
base_info_response = self.views.view_get_base_info(profile=profile, viewgraph_key=view_key, passcode=None)
|
|
6897
|
+
base_info_payload = base_info_response.get("result") or {}
|
|
6263
6898
|
if isinstance(base_info_payload, dict):
|
|
6264
6899
|
base_info = deepcopy(base_info_payload)
|
|
6265
|
-
|
|
6900
|
+
base_info_verification = (
|
|
6901
|
+
base_info_response.get("verification")
|
|
6902
|
+
if isinstance(base_info_response.get("verification"), dict)
|
|
6903
|
+
else {}
|
|
6904
|
+
)
|
|
6905
|
+
if base_info_verification.get("base_info_verified") is False:
|
|
6906
|
+
verification["base_info_verified"] = False
|
|
6907
|
+
for warning in base_info_response.get("warnings") or []:
|
|
6908
|
+
if not isinstance(warning, dict):
|
|
6909
|
+
continue
|
|
6910
|
+
warnings.append(deepcopy(warning))
|
|
6911
|
+
readback_errors.append(
|
|
6912
|
+
{
|
|
6913
|
+
"resource": "view_base_info",
|
|
6914
|
+
"phase": "view_get",
|
|
6915
|
+
"view_key": view_key,
|
|
6916
|
+
"transport_error": {
|
|
6917
|
+
"http_status": warning.get("http_status"),
|
|
6918
|
+
"backend_code": warning.get("backend_code"),
|
|
6919
|
+
"category": warning.get("category"),
|
|
6920
|
+
"request_id": warning.get("request_id"),
|
|
6921
|
+
},
|
|
6922
|
+
}
|
|
6923
|
+
)
|
|
6924
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
6266
6925
|
verification["base_info_verified"] = False
|
|
6267
|
-
|
|
6926
|
+
api_error = _coerce_api_error(error)
|
|
6927
|
+
if not _is_optional_builder_lookup_error(api_error):
|
|
6928
|
+
return _failed_from_api_error(
|
|
6929
|
+
"VIEW_GET_FAILED",
|
|
6930
|
+
api_error,
|
|
6931
|
+
normalized_args={"view_key": view_key},
|
|
6932
|
+
details={"view_key": view_key, "resource": "view_base_info"},
|
|
6933
|
+
suggested_next_call={"tool_name": "view_get", "arguments": {"profile": profile, "view_key": view_key}},
|
|
6934
|
+
)
|
|
6935
|
+
readback_errors.append(
|
|
6936
|
+
{
|
|
6937
|
+
"resource": "view_base_info",
|
|
6938
|
+
"phase": "view_get",
|
|
6939
|
+
"view_key": view_key,
|
|
6940
|
+
"transport_error": _transport_error_payload(api_error),
|
|
6941
|
+
}
|
|
6942
|
+
)
|
|
6943
|
+
warnings.append(_warning("VIEW_BASE_INFO_UNAVAILABLE", "view base info readback is unavailable", **_transport_error_payload(api_error)))
|
|
6268
6944
|
|
|
6269
6945
|
questions: list[dict[str, Any]] = []
|
|
6270
6946
|
try:
|
|
6271
6947
|
questions_payload = self.views.view_list_questions(profile=profile, viewgraph_key=view_key).get("result") or []
|
|
6272
6948
|
if isinstance(questions_payload, list):
|
|
6273
6949
|
questions = [deepcopy(item) for item in questions_payload if isinstance(item, dict)]
|
|
6274
|
-
except (QingflowApiError, RuntimeError):
|
|
6950
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
6275
6951
|
verification["questions_verified"] = False
|
|
6276
|
-
|
|
6952
|
+
api_error = _coerce_api_error(error)
|
|
6953
|
+
if not _is_optional_builder_lookup_error(api_error):
|
|
6954
|
+
return _failed_from_api_error(
|
|
6955
|
+
"VIEW_GET_FAILED",
|
|
6956
|
+
api_error,
|
|
6957
|
+
normalized_args={"view_key": view_key},
|
|
6958
|
+
details={"view_key": view_key, "resource": "view_questions"},
|
|
6959
|
+
suggested_next_call={"tool_name": "view_get", "arguments": {"profile": profile, "view_key": view_key}},
|
|
6960
|
+
)
|
|
6961
|
+
readback_errors.append(
|
|
6962
|
+
{
|
|
6963
|
+
"resource": "view_questions",
|
|
6964
|
+
"phase": "view_get",
|
|
6965
|
+
"view_key": view_key,
|
|
6966
|
+
"transport_error": _transport_error_payload(api_error),
|
|
6967
|
+
}
|
|
6968
|
+
)
|
|
6969
|
+
warnings.append(_warning("VIEW_QUESTIONS_UNAVAILABLE", "view question list readback is unavailable", **_transport_error_payload(api_error)))
|
|
6277
6970
|
|
|
6278
6971
|
associations: list[dict[str, Any]] = []
|
|
6279
6972
|
try:
|
|
6280
6973
|
associations_payload = self.views.view_list_associations(profile=profile, viewgraph_key=view_key).get("result") or []
|
|
6281
6974
|
if isinstance(associations_payload, list):
|
|
6282
6975
|
associations = [deepcopy(item) for item in associations_payload if isinstance(item, dict)]
|
|
6283
|
-
except (QingflowApiError, RuntimeError):
|
|
6976
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
6284
6977
|
verification["associations_verified"] = False
|
|
6285
|
-
|
|
6978
|
+
api_error = _coerce_api_error(error)
|
|
6979
|
+
if not _is_optional_builder_lookup_error(api_error):
|
|
6980
|
+
return _failed_from_api_error(
|
|
6981
|
+
"VIEW_GET_FAILED",
|
|
6982
|
+
api_error,
|
|
6983
|
+
normalized_args={"view_key": view_key},
|
|
6984
|
+
details={"view_key": view_key, "resource": "view_associations"},
|
|
6985
|
+
suggested_next_call={"tool_name": "view_get", "arguments": {"profile": profile, "view_key": view_key}},
|
|
6986
|
+
)
|
|
6987
|
+
readback_errors.append(
|
|
6988
|
+
{
|
|
6989
|
+
"resource": "view_associations",
|
|
6990
|
+
"phase": "view_get",
|
|
6991
|
+
"view_key": view_key,
|
|
6992
|
+
"transport_error": _transport_error_payload(api_error),
|
|
6993
|
+
}
|
|
6994
|
+
)
|
|
6995
|
+
warnings.append(_warning("VIEW_ASSOCIATIONS_UNAVAILABLE", "view association list readback is unavailable", **_transport_error_payload(api_error)))
|
|
6286
6996
|
|
|
6287
6997
|
app_key = str(_first_present(config, "appKey", "formKey") or _first_present(base_info, "appKey", "formKey") or "").strip()
|
|
6288
6998
|
associated_resources: list[dict[str, Any]] = []
|
|
6289
6999
|
if app_key:
|
|
6290
7000
|
try:
|
|
6291
7001
|
associated_resources = self._load_associated_resources_for_builder(profile=profile, app_key=app_key)
|
|
6292
|
-
except (QingflowApiError, RuntimeError):
|
|
7002
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
6293
7003
|
verification["associated_resources_verified"] = False
|
|
6294
|
-
|
|
7004
|
+
api_error = _coerce_api_error(error)
|
|
7005
|
+
if not _is_optional_builder_lookup_error(api_error):
|
|
7006
|
+
return _failed_from_api_error(
|
|
7007
|
+
"VIEW_GET_FAILED",
|
|
7008
|
+
api_error,
|
|
7009
|
+
normalized_args={"view_key": view_key},
|
|
7010
|
+
details={"view_key": view_key, "app_key": app_key, "resource": "associated_resources"},
|
|
7011
|
+
suggested_next_call={"tool_name": "view_get", "arguments": {"profile": profile, "view_key": view_key}},
|
|
7012
|
+
)
|
|
7013
|
+
readback_errors.append(
|
|
7014
|
+
{
|
|
7015
|
+
"resource": "associated_resources",
|
|
7016
|
+
"phase": "view_get",
|
|
7017
|
+
"view_key": view_key,
|
|
7018
|
+
"app_key": app_key,
|
|
7019
|
+
"transport_error": _transport_error_payload(api_error),
|
|
7020
|
+
}
|
|
7021
|
+
)
|
|
7022
|
+
warnings.append(_warning("VIEW_ASSOCIATED_RESOURCES_UNAVAILABLE", "view associated resource pool readback is unavailable", **_transport_error_payload(api_error)))
|
|
6295
7023
|
associated_resources_config = _extract_view_associated_resources_config(
|
|
6296
7024
|
config if isinstance(config, dict) else {},
|
|
6297
7025
|
available_resources=associated_resources,
|
|
@@ -6324,7 +7052,7 @@ class AiBuilderFacade:
|
|
|
6324
7052
|
"normalized_args": {"view_key": view_key},
|
|
6325
7053
|
"missing_fields": [],
|
|
6326
7054
|
"allowed_values": {},
|
|
6327
|
-
"details": {},
|
|
7055
|
+
"details": {"readback_errors": readback_errors} if readback_errors else {},
|
|
6328
7056
|
"request_id": None,
|
|
6329
7057
|
"suggested_next_call": None,
|
|
6330
7058
|
"noop": False,
|
|
@@ -6361,15 +7089,34 @@ class AiBuilderFacade:
|
|
|
6361
7089
|
)
|
|
6362
7090
|
|
|
6363
7091
|
try:
|
|
6364
|
-
|
|
7092
|
+
config_response = self.charts.qingbi_report_get_config(profile=profile, chart_id=chart_id)
|
|
7093
|
+
config = config_response.get("result") or {}
|
|
7094
|
+
config_warnings = config_response.get("warnings") if isinstance(config_response.get("warnings"), list) else []
|
|
7095
|
+
warnings.extend(item for item in config_warnings if isinstance(item, dict))
|
|
7096
|
+
config_verification = (
|
|
7097
|
+
config_response.get("verification") if isinstance(config_response.get("verification"), dict) else {}
|
|
7098
|
+
)
|
|
7099
|
+
if config_verification:
|
|
7100
|
+
verification.update(config_verification)
|
|
6365
7101
|
except (QingflowApiError, RuntimeError) as error:
|
|
7102
|
+
api_error = _coerce_api_error(error)
|
|
7103
|
+
if not _is_optional_builder_lookup_error(api_error) and backend_code_int(api_error) != 81007:
|
|
7104
|
+
return _failed_from_api_error(
|
|
7105
|
+
"CHART_GET_FAILED",
|
|
7106
|
+
api_error,
|
|
7107
|
+
normalized_args={"chart_id": chart_id},
|
|
7108
|
+
details={"chart_id": chart_id, "resource": "chart_config"},
|
|
7109
|
+
suggested_next_call={"tool_name": "chart_get", "arguments": {"profile": profile, "chart_id": chart_id}},
|
|
7110
|
+
)
|
|
6366
7111
|
fallback_config: dict[str, Any] | None = None
|
|
7112
|
+
fallback_api_error: QingflowApiError | None = None
|
|
6367
7113
|
try:
|
|
6368
7114
|
data_fallback = self.charts.qingbi_report_get_data(profile=profile, chart_id=chart_id, payload={}).get("result") or {}
|
|
6369
7115
|
config_from_data = data_fallback.get("config") if isinstance(data_fallback, dict) else None
|
|
6370
7116
|
if isinstance(config_from_data, dict):
|
|
6371
7117
|
fallback_config = deepcopy(config_from_data)
|
|
6372
|
-
except (QingflowApiError, RuntimeError):
|
|
7118
|
+
except (QingflowApiError, RuntimeError) as fallback_error:
|
|
7119
|
+
fallback_api_error = _coerce_api_error(fallback_error)
|
|
6373
7120
|
fallback_config = None
|
|
6374
7121
|
if isinstance(fallback_config, dict):
|
|
6375
7122
|
config = fallback_config
|
|
@@ -6380,12 +7127,17 @@ class AiBuilderFacade:
|
|
|
6380
7127
|
)
|
|
6381
7128
|
)
|
|
6382
7129
|
else:
|
|
6383
|
-
|
|
7130
|
+
details: JSONObject = {
|
|
7131
|
+
"chart_id": chart_id,
|
|
7132
|
+
"config_error": _transport_error_payload(api_error),
|
|
7133
|
+
}
|
|
7134
|
+
if fallback_api_error is not None:
|
|
7135
|
+
details["data_fallback_error"] = _transport_error_payload(fallback_api_error)
|
|
6384
7136
|
return _failed_from_api_error(
|
|
6385
7137
|
"CHART_GET_FAILED",
|
|
6386
7138
|
api_error,
|
|
6387
7139
|
normalized_args={"chart_id": chart_id},
|
|
6388
|
-
details=
|
|
7140
|
+
details=details,
|
|
6389
7141
|
suggested_next_call={"tool_name": "chart_get", "arguments": {"profile": profile, "chart_id": chart_id}},
|
|
6390
7142
|
)
|
|
6391
7143
|
|
|
@@ -7097,16 +7849,6 @@ class AiBuilderFacade:
|
|
|
7097
7849
|
if add_permission_outcome.block is not None:
|
|
7098
7850
|
return add_permission_outcome.block
|
|
7099
7851
|
permission_outcomes.append(add_permission_outcome)
|
|
7100
|
-
if requested_field_changes:
|
|
7101
|
-
edit_permission_outcome = self._guard_package_permission(
|
|
7102
|
-
profile=profile,
|
|
7103
|
-
tag_id=permission_tag_id,
|
|
7104
|
-
required_permission="edit_app",
|
|
7105
|
-
normalized_args=normalized_args,
|
|
7106
|
-
)
|
|
7107
|
-
if edit_permission_outcome.block is not None:
|
|
7108
|
-
return edit_permission_outcome.block
|
|
7109
|
-
permission_outcomes.append(edit_permission_outcome)
|
|
7110
7852
|
resolved = self._create_target_app_shell(
|
|
7111
7853
|
profile=profile,
|
|
7112
7854
|
app_name=app_name,
|
|
@@ -7206,6 +7948,9 @@ class AiBuilderFacade:
|
|
|
7206
7948
|
"created": True,
|
|
7207
7949
|
"field_diff": {"added": [], "updated": [], "removed": []},
|
|
7208
7950
|
"verified": True,
|
|
7951
|
+
"write_executed": True,
|
|
7952
|
+
"write_succeeded": True,
|
|
7953
|
+
"safe_to_retry": False,
|
|
7209
7954
|
"tag_ids_after": list(target.tag_ids),
|
|
7210
7955
|
"package_attached": None if package_tag_id is None else package_tag_id in target.tag_ids,
|
|
7211
7956
|
"publish_requested": False,
|
|
@@ -7434,7 +8179,34 @@ class AiBuilderFacade:
|
|
|
7434
8179
|
)
|
|
7435
8180
|
|
|
7436
8181
|
if not added and not updated and not removed and not normalized_code_block_fields and not data_display_selection.has_any and not bool(resolved.get("created")):
|
|
7437
|
-
|
|
8182
|
+
try:
|
|
8183
|
+
base_info = self.apps.app_get_base(profile=profile, app_key=target.app_key, include_raw=True).get("result") or {}
|
|
8184
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
8185
|
+
api_error = _coerce_api_error(error)
|
|
8186
|
+
if bool(visual_result.get("updated")):
|
|
8187
|
+
return finalize(_post_write_readback_pending_result(
|
|
8188
|
+
error_code="APP_BASE_READBACK_PENDING",
|
|
8189
|
+
message="updated app base metadata; app base readback is unavailable",
|
|
8190
|
+
normalized_args=normalized_args,
|
|
8191
|
+
details={
|
|
8192
|
+
"app_key": target.app_key,
|
|
8193
|
+
"verification_error": _transport_error_payload(api_error),
|
|
8194
|
+
"app_name_after": effective_app_name,
|
|
8195
|
+
"app_base_updated": True,
|
|
8196
|
+
"field_diff": {"added": [], "updated": [], "removed": []},
|
|
8197
|
+
},
|
|
8198
|
+
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
8199
|
+
request_id=api_error.request_id,
|
|
8200
|
+
backend_code=api_error.backend_code,
|
|
8201
|
+
http_status=None if api_error.http_status == 404 else api_error.http_status,
|
|
8202
|
+
))
|
|
8203
|
+
return finalize(_failed_from_api_error(
|
|
8204
|
+
"APP_BASE_READBACK_FAILED",
|
|
8205
|
+
api_error,
|
|
8206
|
+
normalized_args=normalized_args,
|
|
8207
|
+
details=_with_state_read_blocked_details({"app_key": target.app_key}, resource="app_base", error=api_error),
|
|
8208
|
+
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
8209
|
+
))
|
|
7438
8210
|
tag_ids_after = _coerce_int_list(base_info.get("tagIds"))
|
|
7439
8211
|
package_attached = None if package_tag_id is None else package_tag_id in tag_ids_after
|
|
7440
8212
|
actual_app_name = str(base_info.get("formTitle") or effective_app_name).strip() or effective_app_name
|
|
@@ -7472,6 +8244,9 @@ class AiBuilderFacade:
|
|
|
7472
8244
|
"created": False,
|
|
7473
8245
|
"field_diff": {"added": [], "updated": [], "removed": []},
|
|
7474
8246
|
"verified": verified,
|
|
8247
|
+
"write_executed": bool(visual_result.get("updated")),
|
|
8248
|
+
"write_succeeded": bool(visual_result.get("updated")),
|
|
8249
|
+
"safe_to_retry": not bool(visual_result.get("updated")),
|
|
7475
8250
|
"tag_ids_after": tag_ids_after,
|
|
7476
8251
|
"package_attached": package_attached,
|
|
7477
8252
|
}
|
|
@@ -7509,7 +8284,7 @@ class AiBuilderFacade:
|
|
|
7509
8284
|
self.apps.app_update_form_schema(profile=profile, app_key=target.app_key, payload=payload)
|
|
7510
8285
|
except (QingflowApiError, RuntimeError) as error:
|
|
7511
8286
|
api_error = _coerce_api_error(error)
|
|
7512
|
-
if api_error
|
|
8287
|
+
if backend_code_int(api_error) == 49614:
|
|
7513
8288
|
return _failed(
|
|
7514
8289
|
"MULTIPLE_RELATION_FIELDS_UNSUPPORTED",
|
|
7515
8290
|
"backend currently rejects apps with more than one relation field; keep one real relation field and use text/reference summary fields for additional cross-object links.",
|
|
@@ -7681,6 +8456,9 @@ class AiBuilderFacade:
|
|
|
7681
8456
|
after_fields=current_fields,
|
|
7682
8457
|
),
|
|
7683
8458
|
"verified": False,
|
|
8459
|
+
"write_executed": True,
|
|
8460
|
+
"write_succeeded": True,
|
|
8461
|
+
"safe_to_retry": False,
|
|
7684
8462
|
"tag_ids_after": [],
|
|
7685
8463
|
"package_attached": None,
|
|
7686
8464
|
}
|
|
@@ -7786,8 +8564,7 @@ class AiBuilderFacade:
|
|
|
7786
8564
|
response["details"] = details
|
|
7787
8565
|
details["verification_error"] = {
|
|
7788
8566
|
"message": verification_error.message,
|
|
7789
|
-
|
|
7790
|
-
"backend_code": verification_error.backend_code,
|
|
8567
|
+
**_transport_error_payload(verification_error),
|
|
7791
8568
|
}
|
|
7792
8569
|
return finalize(response)
|
|
7793
8570
|
|
|
@@ -7924,6 +8701,9 @@ class AiBuilderFacade:
|
|
|
7924
8701
|
"fallback_applied": None,
|
|
7925
8702
|
},
|
|
7926
8703
|
"verified": True,
|
|
8704
|
+
"write_executed": False,
|
|
8705
|
+
"write_succeeded": False,
|
|
8706
|
+
"safe_to_retry": True,
|
|
7927
8707
|
}
|
|
7928
8708
|
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response))
|
|
7929
8709
|
payload = _build_form_payload_from_existing_schema(
|
|
@@ -7941,7 +8721,7 @@ class AiBuilderFacade:
|
|
|
7941
8721
|
self.apps.app_update_form_schema(profile=profile, app_key=app_key, payload=payload)
|
|
7942
8722
|
except (QingflowApiError, RuntimeError) as error:
|
|
7943
8723
|
api_error = _coerce_api_error(error)
|
|
7944
|
-
if api_error
|
|
8724
|
+
if backend_code_int(api_error) == 400 and target_layout.get("sections"):
|
|
7945
8725
|
flattened_layout = _flatten_layout_sections(target_layout)
|
|
7946
8726
|
fallback_payload = _build_form_payload_from_existing_schema(
|
|
7947
8727
|
current_schema=schema_result,
|
|
@@ -7997,11 +8777,21 @@ class AiBuilderFacade:
|
|
|
7997
8777
|
"normalized_args": normalized_args,
|
|
7998
8778
|
"missing_fields": [],
|
|
7999
8779
|
"allowed_values": {"modes": ["merge", "replace"]},
|
|
8000
|
-
"details": {},
|
|
8001
|
-
"request_id":
|
|
8780
|
+
"details": {"readback_error": _transport_error_payload(api_error)},
|
|
8781
|
+
"request_id": api_error.request_id,
|
|
8782
|
+
"backend_code": api_error.backend_code,
|
|
8783
|
+
"http_status": None if api_error.http_status == 404 else api_error.http_status,
|
|
8002
8784
|
"suggested_next_call": {"tool_name": "app_get_layout", "arguments": {"profile": profile, "app_key": app_key}},
|
|
8003
8785
|
"noop": False,
|
|
8004
|
-
"warnings": [
|
|
8786
|
+
"warnings": [
|
|
8787
|
+
_warning(
|
|
8788
|
+
"READBACK_UNAVAILABLE_AFTER_WRITE",
|
|
8789
|
+
"write was executed but layout readback is unavailable",
|
|
8790
|
+
backend_code=api_error.backend_code,
|
|
8791
|
+
http_status=None if api_error.http_status == 404 else api_error.http_status,
|
|
8792
|
+
request_id=api_error.request_id,
|
|
8793
|
+
)
|
|
8794
|
+
],
|
|
8005
8795
|
"verification": {"layout_verified": False, "layout_summary_verified": False, "layout_read_unavailable": True},
|
|
8006
8796
|
"app_key": app_key,
|
|
8007
8797
|
"app_name": app_name,
|
|
@@ -8013,8 +8803,10 @@ class AiBuilderFacade:
|
|
|
8013
8803
|
"fallback_applied": fallback_applied,
|
|
8014
8804
|
},
|
|
8015
8805
|
"verified": False,
|
|
8806
|
+
"write_executed": True,
|
|
8807
|
+
"write_succeeded": True,
|
|
8808
|
+
"safe_to_retry": False,
|
|
8016
8809
|
}
|
|
8017
|
-
response["request_id"] = api_error.request_id
|
|
8018
8810
|
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response))
|
|
8019
8811
|
verified_layout = _parse_schema(verified_schema)["layout"]
|
|
8020
8812
|
layout_verified = _layouts_equal(verified_layout, applied_layout) or _layouts_semantically_equal(verified_layout, applied_layout)
|
|
@@ -8070,6 +8862,9 @@ class AiBuilderFacade:
|
|
|
8070
8862
|
"fallback_applied": fallback_applied,
|
|
8071
8863
|
},
|
|
8072
8864
|
"verified": layout_verified,
|
|
8865
|
+
"write_executed": True,
|
|
8866
|
+
"write_succeeded": True,
|
|
8867
|
+
"safe_to_retry": False,
|
|
8073
8868
|
}
|
|
8074
8869
|
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response))
|
|
8075
8870
|
|
|
@@ -8094,7 +8889,7 @@ class AiBuilderFacade:
|
|
|
8094
8889
|
permission_outcome = self._guard_app_permission(
|
|
8095
8890
|
profile=profile,
|
|
8096
8891
|
app_key=app_key,
|
|
8097
|
-
required_permission="
|
|
8892
|
+
required_permission="edit_app",
|
|
8098
8893
|
normalized_args=normalized_args,
|
|
8099
8894
|
)
|
|
8100
8895
|
if permission_outcome.block is not None:
|
|
@@ -8295,6 +9090,9 @@ class AiBuilderFacade:
|
|
|
8295
9090
|
"app_name": app_name,
|
|
8296
9091
|
"flow_diff": {"mode": "replace", "node_count": desired_node_count},
|
|
8297
9092
|
"verified": workflow_verified,
|
|
9093
|
+
"write_executed": True,
|
|
9094
|
+
"write_succeeded": True,
|
|
9095
|
+
"safe_to_retry": False,
|
|
8298
9096
|
}
|
|
8299
9097
|
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response))
|
|
8300
9098
|
|
|
@@ -8339,15 +9137,57 @@ class AiBuilderFacade:
|
|
|
8339
9137
|
"app_key": app_key,
|
|
8340
9138
|
"views_diff": {"created": [], "updated": [], "removed": []},
|
|
8341
9139
|
"verified": True,
|
|
9140
|
+
"write_executed": False,
|
|
9141
|
+
"write_succeeded": False,
|
|
9142
|
+
"safe_to_retry": True,
|
|
8342
9143
|
}
|
|
8343
9144
|
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
8344
9145
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
9146
|
+
app_permission_summary: JSONObject | None = None
|
|
9147
|
+
app_permission_error: QingflowApiError | None = None
|
|
9148
|
+
try:
|
|
9149
|
+
app_permission_summary = self._read_app_permission_summary(profile=profile, app_key=app_key)
|
|
9150
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
9151
|
+
app_permission_error = _coerce_api_error(error)
|
|
9152
|
+
app_permission_summary = None
|
|
9153
|
+
if app_permission_error is not None:
|
|
9154
|
+
if _is_permission_restricted_api_error(app_permission_error):
|
|
9155
|
+
permission_outcome = _permission_skip_outcome(
|
|
9156
|
+
scope="app",
|
|
9157
|
+
target={"app_key": app_key},
|
|
9158
|
+
required_permission="view_manage",
|
|
9159
|
+
transport_error=_transport_error_payload(app_permission_error),
|
|
9160
|
+
)
|
|
9161
|
+
else:
|
|
9162
|
+
permission_outcome = PermissionCheckOutcome(
|
|
9163
|
+
block=_failed(
|
|
9164
|
+
"APP_PERMISSION_UNVERIFIED",
|
|
9165
|
+
"could not confirm current user's builder permissions for this app",
|
|
9166
|
+
normalized_args=normalized_args,
|
|
9167
|
+
details={
|
|
9168
|
+
"app_key": app_key,
|
|
9169
|
+
"required_permission": "view_manage",
|
|
9170
|
+
"permission_read_error": {
|
|
9171
|
+
"message": app_permission_error.message,
|
|
9172
|
+
"http_status": app_permission_error.http_status,
|
|
9173
|
+
"backend_code": app_permission_error.backend_code,
|
|
9174
|
+
"category": app_permission_error.category,
|
|
9175
|
+
},
|
|
9176
|
+
},
|
|
9177
|
+
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
9178
|
+
request_id=app_permission_error.request_id,
|
|
9179
|
+
backend_code=app_permission_error.backend_code,
|
|
9180
|
+
http_status=None if app_permission_error.http_status == 404 else app_permission_error.http_status,
|
|
9181
|
+
)
|
|
9182
|
+
)
|
|
9183
|
+
else:
|
|
9184
|
+
permission_outcome = self._guard_app_permission(
|
|
9185
|
+
profile=profile,
|
|
9186
|
+
app_key=app_key,
|
|
9187
|
+
required_permission="view_manage",
|
|
9188
|
+
normalized_args=normalized_args,
|
|
9189
|
+
permission_summary=app_permission_summary,
|
|
9190
|
+
)
|
|
8351
9191
|
if permission_outcome.block is not None:
|
|
8352
9192
|
return permission_outcome.block
|
|
8353
9193
|
permission_outcomes.append(permission_outcome)
|
|
@@ -8380,6 +9220,35 @@ class AiBuilderFacade:
|
|
|
8380
9220
|
if name and key:
|
|
8381
9221
|
existing_by_key[key] = view
|
|
8382
9222
|
existing_by_name.setdefault(name, []).append(view)
|
|
9223
|
+
creating_view_names = [
|
|
9224
|
+
patch.name
|
|
9225
|
+
for patch in upsert_views
|
|
9226
|
+
if not patch.view_key and not existing_by_name.get(patch.name)
|
|
9227
|
+
]
|
|
9228
|
+
if creating_view_names:
|
|
9229
|
+
if app_permission_error is not None and _is_permission_restricted_api_error(app_permission_error):
|
|
9230
|
+
create_permission_outcome = _permission_skip_outcome(
|
|
9231
|
+
scope="app",
|
|
9232
|
+
target={"app_key": app_key},
|
|
9233
|
+
required_permission="data_manage",
|
|
9234
|
+
transport_error=_transport_error_payload(app_permission_error),
|
|
9235
|
+
)
|
|
9236
|
+
else:
|
|
9237
|
+
create_permission_outcome = self._guard_app_permission(
|
|
9238
|
+
profile=profile,
|
|
9239
|
+
app_key=app_key,
|
|
9240
|
+
required_permission="data_manage",
|
|
9241
|
+
normalized_args=normalized_args,
|
|
9242
|
+
permission_summary=app_permission_summary,
|
|
9243
|
+
)
|
|
9244
|
+
if create_permission_outcome.block is not None:
|
|
9245
|
+
details = create_permission_outcome.block.get("details")
|
|
9246
|
+
if isinstance(details, dict):
|
|
9247
|
+
details["operation"] = "view_create"
|
|
9248
|
+
details["view_names"] = creating_view_names
|
|
9249
|
+
details["also_required_permission"] = "view_manage"
|
|
9250
|
+
return create_permission_outcome.block
|
|
9251
|
+
permission_outcomes.append(create_permission_outcome)
|
|
8383
9252
|
parsed_schema = _parse_schema(schema)
|
|
8384
9253
|
field_names = {field["name"] for field in parsed_schema["fields"]}
|
|
8385
9254
|
if patch_views:
|
|
@@ -8454,8 +9323,26 @@ class AiBuilderFacade:
|
|
|
8454
9323
|
being_draft=True,
|
|
8455
9324
|
include_raw=False,
|
|
8456
9325
|
)
|
|
8457
|
-
except (QingflowApiError, RuntimeError):
|
|
8458
|
-
|
|
9326
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
9327
|
+
api_error = _coerce_api_error(error)
|
|
9328
|
+
if _is_optional_builder_lookup_error(api_error):
|
|
9329
|
+
continue
|
|
9330
|
+
failed = _failed_from_api_error(
|
|
9331
|
+
"CUSTOM_BUTTON_DETAIL_READ_FAILED",
|
|
9332
|
+
api_error,
|
|
9333
|
+
normalized_args=normalized_args,
|
|
9334
|
+
details=_with_state_read_blocked_details(
|
|
9335
|
+
{"app_key": app_key, "button_id": button_id},
|
|
9336
|
+
resource="custom_button",
|
|
9337
|
+
error=api_error,
|
|
9338
|
+
),
|
|
9339
|
+
suggested_next_call={
|
|
9340
|
+
"tool_name": "app_custom_button_get",
|
|
9341
|
+
"arguments": {"profile": profile, "app_key": app_key, "button_id": button_id},
|
|
9342
|
+
},
|
|
9343
|
+
)
|
|
9344
|
+
failed.update({"write_executed": False, "write_succeeded": False, "safe_to_retry": True})
|
|
9345
|
+
return finalize(failed)
|
|
8459
9346
|
detail_result = detail.get("result")
|
|
8460
9347
|
if isinstance(detail_result, dict):
|
|
8461
9348
|
custom_button_details_by_id[button_id] = _normalize_custom_button_detail(detail_result)
|
|
@@ -8930,7 +9817,7 @@ class AiBuilderFacade:
|
|
|
8930
9817
|
except (QingflowApiError, RuntimeError) as error:
|
|
8931
9818
|
api_error = _coerce_api_error(error)
|
|
8932
9819
|
should_retry_minimal = operation_phase != "default_view_apply_config_sync" and (
|
|
8933
|
-
api_error
|
|
9820
|
+
backend_code_int(api_error) == 48104
|
|
8934
9821
|
or (patch.type.value == "table" and bool(patch.filters) and api_error.http_status is not None and api_error.http_status >= 500)
|
|
8935
9822
|
)
|
|
8936
9823
|
if should_retry_minimal:
|
|
@@ -9526,6 +10413,9 @@ class AiBuilderFacade:
|
|
|
9526
10413
|
"app_name": app_name,
|
|
9527
10414
|
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": failed_views},
|
|
9528
10415
|
"verified": verified and view_filters_verified and view_query_conditions_verified and view_associated_resources_verified and view_buttons_verified,
|
|
10416
|
+
"write_executed": bool(created or updated or removed),
|
|
10417
|
+
"write_succeeded": bool(created or updated or removed),
|
|
10418
|
+
"safe_to_retry": not bool(created or updated or removed),
|
|
9529
10419
|
}
|
|
9530
10420
|
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response))
|
|
9531
10421
|
warnings: list[dict[str, Any]] = []
|
|
@@ -9607,6 +10497,9 @@ class AiBuilderFacade:
|
|
|
9607
10497
|
"app_name": app_name,
|
|
9608
10498
|
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": []},
|
|
9609
10499
|
"verified": all_verified,
|
|
10500
|
+
"write_executed": bool(created or updated or removed),
|
|
10501
|
+
"write_succeeded": bool(created or updated or removed),
|
|
10502
|
+
"safe_to_retry": not bool(created or updated or removed),
|
|
9610
10503
|
}
|
|
9611
10504
|
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response))
|
|
9612
10505
|
|
|
@@ -9667,8 +10560,21 @@ class AiBuilderFacade:
|
|
|
9667
10560
|
"tag_ids_after": tag_ids_before,
|
|
9668
10561
|
"views_ok": True,
|
|
9669
10562
|
"verified": True,
|
|
10563
|
+
"write_executed": False,
|
|
10564
|
+
"write_succeeded": False,
|
|
10565
|
+
"safe_to_retry": True,
|
|
9670
10566
|
}
|
|
9671
|
-
|
|
10567
|
+
try:
|
|
10568
|
+
version_result = self.apps.app_get_edit_version_no(profile=profile, app_key=app_key).get("result") or {}
|
|
10569
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
10570
|
+
api_error = _coerce_api_error(error)
|
|
10571
|
+
return _failed_from_api_error(
|
|
10572
|
+
"PUBLISH_PRECHECK_FAILED",
|
|
10573
|
+
api_error,
|
|
10574
|
+
normalized_args=normalized_args,
|
|
10575
|
+
details={"app_key": app_key, "phase": "prepare_publish_edit_version"},
|
|
10576
|
+
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
10577
|
+
)
|
|
9672
10578
|
edit_version_no = _coerce_positive_int(version_result.get("editVersionNo") or version_result.get("versionNo")) or 1
|
|
9673
10579
|
try:
|
|
9674
10580
|
self.apps.app_publish(profile=profile, app_key=app_key, payload={"editVersionNo": edit_version_no})
|
|
@@ -9686,13 +10592,18 @@ class AiBuilderFacade:
|
|
|
9686
10592
|
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
9687
10593
|
except (QingflowApiError, RuntimeError) as error:
|
|
9688
10594
|
api_error = _coerce_api_error(error)
|
|
9689
|
-
|
|
9690
|
-
"
|
|
9691
|
-
|
|
10595
|
+
result = _post_write_readback_pending_result(
|
|
10596
|
+
error_code="PUBLISH_READBACK_PENDING",
|
|
10597
|
+
message="published app; app base readback is unavailable",
|
|
9692
10598
|
normalized_args=normalized_args,
|
|
9693
|
-
details={"app_key": app_key},
|
|
10599
|
+
details={"app_key": app_key, "edit_version_no": edit_version_no, "readback_error": _transport_error_payload(api_error)},
|
|
9694
10600
|
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
10601
|
+
request_id=api_error.request_id,
|
|
10602
|
+
backend_code=api_error.backend_code,
|
|
10603
|
+
http_status=None if api_error.http_status == 404 else api_error.http_status,
|
|
9695
10604
|
)
|
|
10605
|
+
result.update({"app_key": app_key, "published": None, "verified": False})
|
|
10606
|
+
return result
|
|
9696
10607
|
tag_ids_after = _coerce_int_list(base.get("tagIds"))
|
|
9697
10608
|
app_name_after = str(base.get("formTitle") or base.get("title") or base.get("appName") or app_name_before or "").strip() or None
|
|
9698
10609
|
package_attached = None if not expected_package_tag_id else expected_package_tag_id in tag_ids_after
|
|
@@ -9700,13 +10611,18 @@ class AiBuilderFacade:
|
|
|
9700
10611
|
views, views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
9701
10612
|
except (QingflowApiError, RuntimeError) as error:
|
|
9702
10613
|
api_error = _coerce_api_error(error)
|
|
9703
|
-
|
|
9704
|
-
"
|
|
9705
|
-
|
|
10614
|
+
result = _post_write_readback_pending_result(
|
|
10615
|
+
error_code="VIEWS_READBACK_PENDING",
|
|
10616
|
+
message="published app; views readback is unavailable",
|
|
9706
10617
|
normalized_args=normalized_args,
|
|
9707
|
-
details={"app_key": app_key},
|
|
10618
|
+
details={"app_key": app_key, "edit_version_no": edit_version_no, "readback_error": _transport_error_payload(api_error)},
|
|
9708
10619
|
suggested_next_call={"tool_name": "app_get_views", "arguments": {"profile": profile, "app_key": app_key}},
|
|
10620
|
+
request_id=api_error.request_id,
|
|
10621
|
+
backend_code=api_error.backend_code,
|
|
10622
|
+
http_status=None if api_error.http_status == 404 else api_error.http_status,
|
|
9709
10623
|
)
|
|
10624
|
+
result.update({"app_key": app_key, "app_name": app_name_after, "published": bool(base.get("appPublishStatus") in {1, 2}), "verified": False})
|
|
10625
|
+
return result
|
|
9710
10626
|
views = views or []
|
|
9711
10627
|
views_ok = isinstance(views, list) and not views_unavailable
|
|
9712
10628
|
verified = bool(base.get("appPublishStatus") in {1, 2}) and (package_attached is not False) and views_ok
|
|
@@ -9741,6 +10657,9 @@ class AiBuilderFacade:
|
|
|
9741
10657
|
"tag_ids_after": tag_ids_after,
|
|
9742
10658
|
"views_ok": views_ok,
|
|
9743
10659
|
"verified": verified,
|
|
10660
|
+
"write_executed": True,
|
|
10661
|
+
"write_succeeded": True,
|
|
10662
|
+
"safe_to_retry": False,
|
|
9744
10663
|
}
|
|
9745
10664
|
|
|
9746
10665
|
def _expand_chart_partial_patches(
|
|
@@ -9904,7 +10823,15 @@ class AiBuilderFacade:
|
|
|
9904
10823
|
|
|
9905
10824
|
def _verify_custom_button_deleted_by_id(self, *, profile: str, app_key: str, button_id: int) -> JSONObject:
|
|
9906
10825
|
try:
|
|
9907
|
-
|
|
10826
|
+
def runner(_: Any, context: BackendRequestContext) -> object:
|
|
10827
|
+
return self.buttons.backend.request(
|
|
10828
|
+
"GET",
|
|
10829
|
+
context,
|
|
10830
|
+
f"/app/{app_key}/customButton/{button_id}",
|
|
10831
|
+
params={"beingDraft": True},
|
|
10832
|
+
)
|
|
10833
|
+
|
|
10834
|
+
self.buttons._run(profile, runner)
|
|
9908
10835
|
except (QingflowApiError, RuntimeError) as error:
|
|
9909
10836
|
api_error = _coerce_api_error(error)
|
|
9910
10837
|
if _delete_readback_is_not_found(api_error):
|
|
@@ -10185,18 +11112,21 @@ class AiBuilderFacade:
|
|
|
10185
11112
|
create_result = self.charts.qingbi_report_create(profile=profile, payload=create_payload).get("result") or {}
|
|
10186
11113
|
created_chart_id = _extract_chart_identifier(create_result or {})
|
|
10187
11114
|
if not created_chart_id:
|
|
10188
|
-
|
|
10189
|
-
|
|
10190
|
-
|
|
10191
|
-
|
|
10192
|
-
|
|
10193
|
-
|
|
10194
|
-
if len(refreshed_matches) == 1:
|
|
10195
|
-
created_chart_id = _extract_chart_identifier(refreshed_matches[0])
|
|
10196
|
-
elif len(refreshed_matches) > 1:
|
|
10197
|
-
raise ValueError(
|
|
10198
|
-
f"created chart '{patch.name}' could not be uniquely resolved from readback; supply chart_id on the next update"
|
|
11115
|
+
try:
|
|
11116
|
+
refreshed_items, _ = self._load_chart_list_for_builder(profile=profile, app_key=app_key)
|
|
11117
|
+
refreshed_matches = _find_charts_by_name(
|
|
11118
|
+
refreshed_items,
|
|
11119
|
+
chart_name=patch.name,
|
|
11120
|
+
chart_type=target_type,
|
|
10199
11121
|
)
|
|
11122
|
+
if len(refreshed_matches) == 1:
|
|
11123
|
+
created_chart_id = _extract_chart_identifier(refreshed_matches[0])
|
|
11124
|
+
elif len(refreshed_matches) > 1:
|
|
11125
|
+
raise ValueError(
|
|
11126
|
+
f"created chart '{patch.name}' could not be uniquely resolved from readback; supply chart_id on the next update"
|
|
11127
|
+
)
|
|
11128
|
+
except (QingflowApiError, RuntimeError):
|
|
11129
|
+
created_chart_id = temp_chart_id
|
|
10200
11130
|
if not created_chart_id:
|
|
10201
11131
|
raise ValueError(
|
|
10202
11132
|
f"created chart '{patch.name}' did not return a real chart_id and could not be confirmed from readback"
|
|
@@ -10352,10 +11282,13 @@ class AiBuilderFacade:
|
|
|
10352
11282
|
chart_results.append(failure)
|
|
10353
11283
|
|
|
10354
11284
|
noop = not created_ids and not updated_ids and not removed_ids and not reordered and not failed_items
|
|
11285
|
+
write_executed = bool(created_ids or updated_ids or removed_ids or reordered)
|
|
11286
|
+
write_succeeded = write_executed
|
|
10355
11287
|
needs_list_readback = bool(created_ids or updated_ids or reordered)
|
|
10356
11288
|
delete_readback_unavailable = any(item.get("readback_status") == "unavailable" for item in delete_readback_issues)
|
|
10357
11289
|
deletes_verified = not delete_readback_issues
|
|
10358
11290
|
readback_unavailable = False
|
|
11291
|
+
readback_error: QingflowApiError | None = None
|
|
10359
11292
|
readback_list_source: str | None = existing_chart_list_source
|
|
10360
11293
|
if needs_list_readback:
|
|
10361
11294
|
try:
|
|
@@ -10375,7 +11308,8 @@ class AiBuilderFacade:
|
|
|
10375
11308
|
requested_existing = [chart_id for chart_id in request.reorder_chart_ids if chart_id in ordered_readback]
|
|
10376
11309
|
verified = verified and readback_list_source == "sorted" and ordered_readback[: len(requested_existing)] == requested_existing
|
|
10377
11310
|
readback_unavailable = False
|
|
10378
|
-
except (QingflowApiError, RuntimeError):
|
|
11311
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
11312
|
+
readback_error = _coerce_api_error(error)
|
|
10379
11313
|
verified = False
|
|
10380
11314
|
readback_unavailable = True
|
|
10381
11315
|
readback_list_source = None
|
|
@@ -10394,11 +11328,14 @@ class AiBuilderFacade:
|
|
|
10394
11328
|
"normalized_args": normalized_args,
|
|
10395
11329
|
"missing_fields": [],
|
|
10396
11330
|
"allowed_values": {"chart.chart_type": [member.value for member in PublicChartType], "chart.filter.operator": [member.value for member in ViewFilterOperator]},
|
|
10397
|
-
"details": {
|
|
10398
|
-
|
|
11331
|
+
"details": {
|
|
11332
|
+
"per_chart_results": chart_results,
|
|
11333
|
+
**({"readback_error": _transport_error_payload(readback_error)} if readback_error is not None else {}),
|
|
11334
|
+
},
|
|
11335
|
+
"request_id": failed_items[0].get("request_id") or (readback_error.request_id if readback_error is not None else None),
|
|
10399
11336
|
"suggested_next_call": {"tool_name": "app_charts_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
10400
|
-
"backend_code": failed_items[0].get("backend_code"),
|
|
10401
|
-
"http_status": failed_items[0].get("http_status"),
|
|
11337
|
+
"backend_code": failed_items[0].get("backend_code") or (readback_error.backend_code if readback_error is not None else None),
|
|
11338
|
+
"http_status": failed_items[0].get("http_status") or (None if readback_error is None or readback_error.http_status == 404 else readback_error.http_status),
|
|
10402
11339
|
"noop": noop,
|
|
10403
11340
|
"warnings": _chart_apply_warnings(
|
|
10404
11341
|
failed_items=failed_items,
|
|
@@ -10417,6 +11354,9 @@ class AiBuilderFacade:
|
|
|
10417
11354
|
"app_name": app_name,
|
|
10418
11355
|
"chart_results": chart_results,
|
|
10419
11356
|
"verified": False if failed_items else verified,
|
|
11357
|
+
"write_executed": write_executed,
|
|
11358
|
+
"write_succeeded": write_succeeded,
|
|
11359
|
+
"safe_to_retry": not write_executed,
|
|
10420
11360
|
})
|
|
10421
11361
|
result_verified = verified or noop
|
|
10422
11362
|
pending_delete = bool(delete_readback_issues)
|
|
@@ -10435,9 +11375,11 @@ class AiBuilderFacade:
|
|
|
10435
11375
|
"normalized_args": normalized_args,
|
|
10436
11376
|
"missing_fields": [],
|
|
10437
11377
|
"allowed_values": {"chart.chart_type": [member.value for member in PublicChartType], "chart.filter.operator": [member.value for member in ViewFilterOperator]},
|
|
10438
|
-
"details": {},
|
|
10439
|
-
"request_id": None,
|
|
11378
|
+
"details": {"readback_error": _transport_error_payload(readback_error)} if readback_error is not None else {},
|
|
11379
|
+
"request_id": readback_error.request_id if readback_error is not None else None,
|
|
10440
11380
|
"suggested_next_call": None if result_verified else pending_suggestion,
|
|
11381
|
+
"backend_code": readback_error.backend_code if readback_error is not None else None,
|
|
11382
|
+
"http_status": None if readback_error is None or readback_error.http_status == 404 else readback_error.http_status,
|
|
10441
11383
|
"noop": noop,
|
|
10442
11384
|
"warnings": _chart_apply_warnings(
|
|
10443
11385
|
failed_items=[],
|
|
@@ -10456,6 +11398,9 @@ class AiBuilderFacade:
|
|
|
10456
11398
|
"app_name": app_name,
|
|
10457
11399
|
"chart_results": chart_results,
|
|
10458
11400
|
"verified": result_verified,
|
|
11401
|
+
"write_executed": write_executed,
|
|
11402
|
+
"write_succeeded": write_succeeded,
|
|
11403
|
+
"safe_to_retry": not write_executed,
|
|
10459
11404
|
})
|
|
10460
11405
|
|
|
10461
11406
|
def portal_apply(self, *, profile: str, request: PortalApplyRequest) -> JSONObject:
|
|
@@ -10536,15 +11481,6 @@ class AiBuilderFacade:
|
|
|
10536
11481
|
if package_add_outcome.block is not None:
|
|
10537
11482
|
return package_add_outcome.block
|
|
10538
11483
|
permission_outcomes.append(package_add_outcome)
|
|
10539
|
-
package_edit_outcome = self._guard_package_permission(
|
|
10540
|
-
profile=profile,
|
|
10541
|
-
tag_id=target_package_tag_id,
|
|
10542
|
-
required_permission="edit_app",
|
|
10543
|
-
normalized_args=normalized_args,
|
|
10544
|
-
)
|
|
10545
|
-
if package_edit_outcome.block is not None:
|
|
10546
|
-
return package_edit_outcome.block
|
|
10547
|
-
permission_outcomes.append(package_edit_outcome)
|
|
10548
11484
|
if not sections_requested:
|
|
10549
11485
|
unsupported_base_only_keys: list[str] = []
|
|
10550
11486
|
if request.hide_copyright is not None:
|
|
@@ -10564,6 +11500,10 @@ class AiBuilderFacade:
|
|
|
10564
11500
|
},
|
|
10565
11501
|
suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
10566
11502
|
)
|
|
11503
|
+
write_executed = False
|
|
11504
|
+
draft_readback_error: JSONObject | None = None
|
|
11505
|
+
live_readback_error: JSONObject | None = None
|
|
11506
|
+
update_payload: dict[str, Any] = {}
|
|
10567
11507
|
try:
|
|
10568
11508
|
layout_diagnostics: dict[str, Any] = _empty_portal_layout_diagnostics()
|
|
10569
11509
|
if creating:
|
|
@@ -10579,6 +11519,7 @@ class AiBuilderFacade:
|
|
|
10579
11519
|
base_payload=None,
|
|
10580
11520
|
)
|
|
10581
11521
|
create_result = self.portals.portal_create(profile=profile, payload=create_payload)
|
|
11522
|
+
write_executed = True
|
|
10582
11523
|
created = create_result.get("result") if isinstance(create_result.get("result"), dict) else {}
|
|
10583
11524
|
dash_key = str(created.get("dashKey") or "")
|
|
10584
11525
|
if not dash_key:
|
|
@@ -10589,7 +11530,15 @@ class AiBuilderFacade:
|
|
|
10589
11530
|
details={"create_result": created},
|
|
10590
11531
|
suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
10591
11532
|
)
|
|
10592
|
-
|
|
11533
|
+
try:
|
|
11534
|
+
base_payload = self.portals.portal_get(profile=profile, dash_key=dash_key, being_draft=True).get("result") or {}
|
|
11535
|
+
except (QingflowApiError, RuntimeError) as read_error:
|
|
11536
|
+
api_read_error = _coerce_api_error(read_error)
|
|
11537
|
+
draft_readback_error = {
|
|
11538
|
+
"phase": "created_portal_draft_readback",
|
|
11539
|
+
"transport_error": _transport_error_payload(api_read_error),
|
|
11540
|
+
}
|
|
11541
|
+
base_payload = {}
|
|
10593
11542
|
update_payload = _build_public_portal_base_payload(
|
|
10594
11543
|
dash_name=request.dash_name or str(base_payload.get("dashName") or "").strip() or "未命名门户",
|
|
10595
11544
|
package_tag_id=target_package_tag_id,
|
|
@@ -10610,6 +11559,7 @@ class AiBuilderFacade:
|
|
|
10610
11559
|
layout_diagnostics = _portal_layout_diagnostics(request.sections, component_payload)
|
|
10611
11560
|
update_payload["components"] = component_payload
|
|
10612
11561
|
self.portals.portal_update(profile=profile, dash_key=dash_key, payload=update_payload)
|
|
11562
|
+
write_executed = True
|
|
10613
11563
|
self.portals.portal_update_base_info(
|
|
10614
11564
|
profile=profile,
|
|
10615
11565
|
dash_key=dash_key,
|
|
@@ -10620,9 +11570,70 @@ class AiBuilderFacade:
|
|
|
10620
11570
|
"tags": deepcopy(update_payload.get("tags") or []),
|
|
10621
11571
|
},
|
|
10622
11572
|
)
|
|
10623
|
-
|
|
11573
|
+
write_executed = True
|
|
11574
|
+
try:
|
|
11575
|
+
draft_result = self.portals.portal_get(profile=profile, dash_key=dash_key, being_draft=True).get("result") or {}
|
|
11576
|
+
except (QingflowApiError, RuntimeError) as read_error:
|
|
11577
|
+
api_read_error = _coerce_api_error(read_error)
|
|
11578
|
+
draft_readback_error = {
|
|
11579
|
+
"phase": "portal_draft_readback",
|
|
11580
|
+
"transport_error": _transport_error_payload(api_read_error),
|
|
11581
|
+
}
|
|
11582
|
+
draft_result = {}
|
|
10624
11583
|
except (QingflowApiError, RuntimeError, ValueError) as error:
|
|
10625
11584
|
api_error = _coerce_api_error(error) if not isinstance(error, ValueError) else None
|
|
11585
|
+
if write_executed:
|
|
11586
|
+
transport_error = _transport_error_payload(api_error) if api_error is not None else None
|
|
11587
|
+
warning = _warning(
|
|
11588
|
+
"PORTAL_WRITE_INCOMPLETE_AFTER_PARTIAL_WRITE",
|
|
11589
|
+
"one or more portal write steps executed before a later write step failed",
|
|
11590
|
+
)
|
|
11591
|
+
if transport_error is not None:
|
|
11592
|
+
for key in ("backend_code", "http_status", "request_id"):
|
|
11593
|
+
if transport_error.get(key) is not None:
|
|
11594
|
+
warning[key] = transport_error.get(key)
|
|
11595
|
+
return finalize({
|
|
11596
|
+
"status": "partial_success",
|
|
11597
|
+
"error_code": "PORTAL_APPLY_PARTIAL",
|
|
11598
|
+
"recoverable": True,
|
|
11599
|
+
"message": "some portal write steps executed; a later portal write step failed",
|
|
11600
|
+
"normalized_args": normalized_args,
|
|
11601
|
+
"missing_fields": [],
|
|
11602
|
+
"allowed_values": {"section.source_type": ["chart", "view", "grid", "filter", "text", "link"]},
|
|
11603
|
+
"details": {
|
|
11604
|
+
"dash_key": dash_key or None,
|
|
11605
|
+
"write_error": (
|
|
11606
|
+
{"message": api_error.message, "transport_error": transport_error}
|
|
11607
|
+
if api_error is not None
|
|
11608
|
+
else {"message": str(error)}
|
|
11609
|
+
),
|
|
11610
|
+
},
|
|
11611
|
+
"request_id": api_error.request_id if api_error else None,
|
|
11612
|
+
"backend_code": api_error.backend_code if api_error else None,
|
|
11613
|
+
"http_status": None if api_error is None or api_error.http_status == 404 else api_error.http_status,
|
|
11614
|
+
"suggested_next_call": {"tool_name": "portal_get", "arguments": {"profile": profile, "dash_key": dash_key}},
|
|
11615
|
+
"noop": False,
|
|
11616
|
+
"warnings": [warning],
|
|
11617
|
+
"verification": {
|
|
11618
|
+
"draft_verified": False,
|
|
11619
|
+
"draft_metadata_verified": False,
|
|
11620
|
+
"live_verified": None,
|
|
11621
|
+
"live_metadata_verified": None,
|
|
11622
|
+
"published": False,
|
|
11623
|
+
"publish_failed": False,
|
|
11624
|
+
"write_incomplete": True,
|
|
11625
|
+
"readback_unavailable": False,
|
|
11626
|
+
"metadata_unverified": True,
|
|
11627
|
+
},
|
|
11628
|
+
"dash_key": dash_key,
|
|
11629
|
+
"dash_name": update_payload.get("dashName") if isinstance(update_payload, dict) else None,
|
|
11630
|
+
"package_id": target_package_tag_id,
|
|
11631
|
+
"created": creating,
|
|
11632
|
+
"published": False,
|
|
11633
|
+
"verified": False,
|
|
11634
|
+
"write_executed": True,
|
|
11635
|
+
"safe_to_retry": False,
|
|
11636
|
+
})
|
|
10626
11637
|
return _failed(
|
|
10627
11638
|
"PORTAL_APPLY_FAILED",
|
|
10628
11639
|
_public_error_message("PORTAL_APPLY_FAILED", api_error) if api_error else str(error),
|
|
@@ -10637,19 +11648,35 @@ class AiBuilderFacade:
|
|
|
10637
11648
|
live_result: dict[str, Any] | None = None
|
|
10638
11649
|
published = False
|
|
10639
11650
|
publish_failed = False
|
|
11651
|
+
publish_error: JSONObject | None = None
|
|
10640
11652
|
if request.publish:
|
|
10641
11653
|
try:
|
|
10642
11654
|
self.portals.portal_publish(profile=profile, dash_key=dash_key)
|
|
10643
11655
|
published = True
|
|
10644
|
-
|
|
10645
|
-
|
|
11656
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
11657
|
+
api_error = _coerce_api_error(error)
|
|
10646
11658
|
publish_failed = True
|
|
11659
|
+
publish_error = {
|
|
11660
|
+
"message": api_error.message,
|
|
11661
|
+
"transport_error": _transport_error_payload(api_error),
|
|
11662
|
+
}
|
|
11663
|
+
if published:
|
|
11664
|
+
try:
|
|
11665
|
+
live_result = self.portals.portal_get(profile=profile, dash_key=dash_key, being_draft=False).get("result") or {}
|
|
11666
|
+
except (QingflowApiError, RuntimeError) as read_error:
|
|
11667
|
+
api_read_error = _coerce_api_error(read_error)
|
|
11668
|
+
live_readback_error = {
|
|
11669
|
+
"phase": "portal_live_readback",
|
|
11670
|
+
"transport_error": _transport_error_payload(api_read_error),
|
|
11671
|
+
}
|
|
10647
11672
|
|
|
10648
11673
|
draft_components = draft_result.get("components") if isinstance(draft_result, dict) else None
|
|
10649
11674
|
expected_count = len(request.sections) if sections_requested else None
|
|
10650
11675
|
draft_verified = isinstance(draft_result, dict) and (
|
|
10651
11676
|
expected_count is None or (isinstance(draft_components, list) and len(draft_components) == expected_count)
|
|
10652
11677
|
)
|
|
11678
|
+
if draft_readback_error is not None:
|
|
11679
|
+
draft_verified = False
|
|
10653
11680
|
draft_meta_verified, draft_meta_mismatches = _verify_portal_readback(
|
|
10654
11681
|
actual=draft_result,
|
|
10655
11682
|
expected_payload=update_payload,
|
|
@@ -10677,6 +11704,8 @@ class AiBuilderFacade:
|
|
|
10677
11704
|
)
|
|
10678
11705
|
)
|
|
10679
11706
|
)
|
|
11707
|
+
if live_readback_error is not None:
|
|
11708
|
+
live_verified = False
|
|
10680
11709
|
live_meta_verified, live_meta_mismatches = _verify_portal_readback(
|
|
10681
11710
|
actual=live_result,
|
|
10682
11711
|
expected_payload=update_payload,
|
|
@@ -10710,6 +11739,23 @@ class AiBuilderFacade:
|
|
|
10710
11739
|
live_meta_verified=live_meta_verified,
|
|
10711
11740
|
publish_requested=request.publish,
|
|
10712
11741
|
)
|
|
11742
|
+
details_payload = {
|
|
11743
|
+
"verification_mismatches": {
|
|
11744
|
+
"draft": draft_meta_mismatches,
|
|
11745
|
+
"live": live_meta_mismatches,
|
|
11746
|
+
},
|
|
11747
|
+
**({"publish_error": publish_error} if publish_error is not None else {}),
|
|
11748
|
+
**({"draft_readback_error": draft_readback_error} if draft_readback_error is not None else {}),
|
|
11749
|
+
**({"live_readback_error": live_readback_error} if live_readback_error is not None else {}),
|
|
11750
|
+
}
|
|
11751
|
+
readback_transport_error = _readback_transport_error_from_details(details_payload)
|
|
11752
|
+
if draft_readback_error is not None or live_readback_error is not None:
|
|
11753
|
+
warning = _warning("READBACK_UNAVAILABLE_AFTER_WRITE", "write was executed but portal readback is unavailable")
|
|
11754
|
+
if readback_transport_error is not None:
|
|
11755
|
+
for key in ("backend_code", "http_status", "request_id"):
|
|
11756
|
+
if readback_transport_error.get(key) is not None:
|
|
11757
|
+
warning[key] = readback_transport_error.get(key)
|
|
11758
|
+
warnings.append(warning)
|
|
10713
11759
|
warnings.extend(_portal_layout_warning_items(layout_diagnostics))
|
|
10714
11760
|
return finalize({
|
|
10715
11761
|
"status": status,
|
|
@@ -10727,13 +11773,10 @@ class AiBuilderFacade:
|
|
|
10727
11773
|
"normalized_args": normalized_args,
|
|
10728
11774
|
"missing_fields": [],
|
|
10729
11775
|
"allowed_values": {"section.source_type": ["chart", "view", "grid", "filter", "text", "link"]},
|
|
10730
|
-
"details":
|
|
10731
|
-
|
|
10732
|
-
|
|
10733
|
-
|
|
10734
|
-
}
|
|
10735
|
-
},
|
|
10736
|
-
"request_id": None,
|
|
11776
|
+
"details": details_payload,
|
|
11777
|
+
"request_id": (readback_transport_error or {}).get("request_id"),
|
|
11778
|
+
"backend_code": (readback_transport_error or {}).get("backend_code"),
|
|
11779
|
+
"http_status": (readback_transport_error or {}).get("http_status"),
|
|
10737
11780
|
"suggested_next_call": None if verified else {"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
10738
11781
|
"noop": False,
|
|
10739
11782
|
"warnings": warnings,
|
|
@@ -10745,6 +11788,8 @@ class AiBuilderFacade:
|
|
|
10745
11788
|
"live_metadata_verified": live_meta_verified,
|
|
10746
11789
|
"published": published,
|
|
10747
11790
|
"publish_failed": publish_failed,
|
|
11791
|
+
"readback_unavailable": draft_readback_error is not None or live_readback_error is not None,
|
|
11792
|
+
"metadata_unverified": draft_readback_error is not None or live_readback_error is not None,
|
|
10748
11793
|
},
|
|
10749
11794
|
"dash_key": dash_key,
|
|
10750
11795
|
"dash_name": update_payload.get("dashName"),
|
|
@@ -10752,6 +11797,8 @@ class AiBuilderFacade:
|
|
|
10752
11797
|
"created": creating,
|
|
10753
11798
|
"published": published,
|
|
10754
11799
|
"verified": verified,
|
|
11800
|
+
"write_executed": write_executed or published,
|
|
11801
|
+
"safe_to_retry": not (write_executed or published),
|
|
10755
11802
|
"draft_result": draft_result,
|
|
10756
11803
|
"live_result": live_result,
|
|
10757
11804
|
})
|
|
@@ -10759,7 +11806,17 @@ class AiBuilderFacade:
|
|
|
10759
11806
|
def _publish_current_edit_version(self, *, profile: str, app_key: str, edit_version_no: int | None = None) -> JSONObject:
|
|
10760
11807
|
normalized_args = {"app_key": app_key}
|
|
10761
11808
|
if edit_version_no is None:
|
|
10762
|
-
|
|
11809
|
+
try:
|
|
11810
|
+
version_result = self.apps.app_get_edit_version_no(profile=profile, app_key=app_key).get("result") or {}
|
|
11811
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
11812
|
+
api_error = _coerce_api_error(error)
|
|
11813
|
+
return _failed_from_api_error(
|
|
11814
|
+
"PUBLISH_PRECHECK_FAILED",
|
|
11815
|
+
api_error,
|
|
11816
|
+
normalized_args=normalized_args,
|
|
11817
|
+
details={"app_key": app_key, "phase": "prepare_publish_edit_version"},
|
|
11818
|
+
suggested_next_call={"tool_name": "app_publish_verify", "arguments": {"profile": profile, "app_key": app_key}},
|
|
11819
|
+
)
|
|
10763
11820
|
edit_version_no = _coerce_positive_int(version_result.get("editVersionNo") or version_result.get("versionNo")) or 1
|
|
10764
11821
|
try:
|
|
10765
11822
|
self.apps.app_publish(profile=profile, app_key=app_key, payload={"editVersionNo": edit_version_no})
|
|
@@ -10893,6 +11950,7 @@ class AiBuilderFacade:
|
|
|
10893
11950
|
response["status"] = "partial_success"
|
|
10894
11951
|
response["error_code"] = response.get("error_code") or publish_result.get("error_code")
|
|
10895
11952
|
response["recoverable"] = True
|
|
11953
|
+
response["verified"] = False
|
|
10896
11954
|
response["message"] = f"{response.get('message') or 'apply succeeded'}; publish failed"
|
|
10897
11955
|
if not response.get("suggested_next_call"):
|
|
10898
11956
|
response["suggested_next_call"] = publish_result.get("suggested_next_call")
|
|
@@ -10981,7 +12039,8 @@ class AiBuilderFacade:
|
|
|
10981
12039
|
app_key: str,
|
|
10982
12040
|
tolerate_404: bool,
|
|
10983
12041
|
tolerate_permission_restricted: bool = False,
|
|
10984
|
-
|
|
12042
|
+
include_error: bool = False,
|
|
12043
|
+
) -> tuple[Any, bool] | tuple[Any, bool, QingflowApiError | None]:
|
|
10985
12044
|
try:
|
|
10986
12045
|
views = self.views.view_list_flat(profile=profile, app_key=app_key)
|
|
10987
12046
|
except (QingflowApiError, RuntimeError) as error:
|
|
@@ -11003,18 +12062,19 @@ class AiBuilderFacade:
|
|
|
11003
12062
|
)
|
|
11004
12063
|
)
|
|
11005
12064
|
):
|
|
11006
|
-
return [], True
|
|
12065
|
+
return ([], True, legacy_api_error) if include_error else ([], True)
|
|
11007
12066
|
raise
|
|
11008
12067
|
legacy_result = legacy_views.get("result")
|
|
11009
12068
|
if _is_view_collection_shape(legacy_result):
|
|
11010
|
-
|
|
12069
|
+
result = _normalize_view_collection(legacy_result)
|
|
12070
|
+
return (result, False, api_error) if include_error else (result, False)
|
|
11011
12071
|
if tolerate_404:
|
|
11012
|
-
return [], True
|
|
12072
|
+
return ([], True, api_error) if include_error else ([], True)
|
|
11013
12073
|
raise error
|
|
11014
12074
|
raise
|
|
11015
12075
|
normalized_views = _normalize_view_collection(views.get("result"))
|
|
11016
12076
|
if normalized_views:
|
|
11017
|
-
return normalized_views, False
|
|
12077
|
+
return (normalized_views, False, None) if include_error else (normalized_views, False)
|
|
11018
12078
|
try:
|
|
11019
12079
|
legacy_views = self.views.view_list(profile=profile, app_key=app_key)
|
|
11020
12080
|
except (QingflowApiError, RuntimeError) as legacy_error:
|
|
@@ -11029,11 +12089,12 @@ class AiBuilderFacade:
|
|
|
11029
12089
|
)
|
|
11030
12090
|
)
|
|
11031
12091
|
):
|
|
11032
|
-
return normalized_views, False
|
|
12092
|
+
return (normalized_views, False, legacy_api_error) if include_error else (normalized_views, False)
|
|
11033
12093
|
raise
|
|
11034
12094
|
legacy_result = legacy_views.get("result")
|
|
11035
12095
|
legacy_normalized = _normalize_view_collection(legacy_result)
|
|
11036
|
-
|
|
12096
|
+
result = legacy_normalized or normalized_views
|
|
12097
|
+
return (result, False, None) if include_error else (result, False)
|
|
11037
12098
|
|
|
11038
12099
|
def _load_workflow_result(
|
|
11039
12100
|
self,
|
|
@@ -11042,7 +12103,8 @@ class AiBuilderFacade:
|
|
|
11042
12103
|
app_key: str,
|
|
11043
12104
|
tolerate_404: bool,
|
|
11044
12105
|
tolerate_permission_restricted: bool = False,
|
|
11045
|
-
|
|
12106
|
+
include_error: bool = False,
|
|
12107
|
+
) -> tuple[Any, bool] | tuple[Any, bool, QingflowApiError | None]:
|
|
11046
12108
|
try:
|
|
11047
12109
|
workflow = self.workflows.workflow_list_nodes(profile=profile, app_key=app_key)
|
|
11048
12110
|
except (QingflowApiError, RuntimeError) as error:
|
|
@@ -11051,9 +12113,10 @@ class AiBuilderFacade:
|
|
|
11051
12113
|
api_error.http_status == 404
|
|
11052
12114
|
or (tolerate_permission_restricted and _is_permission_restricted_api_error(api_error))
|
|
11053
12115
|
):
|
|
11054
|
-
return [], True
|
|
12116
|
+
return ([], True, api_error) if include_error else ([], True)
|
|
11055
12117
|
raise
|
|
11056
|
-
|
|
12118
|
+
result = workflow.get("result")
|
|
12119
|
+
return (result, False, None) if include_error else (result, False)
|
|
11057
12120
|
|
|
11058
12121
|
def _load_app_state(self, *, profile: str, app_key: str) -> dict[str, Any]:
|
|
11059
12122
|
state = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
@@ -12206,10 +13269,11 @@ def _resolve_custom_button_view_button_ref(
|
|
|
12206
13269
|
button_inventory: dict[int, dict[str, Any]],
|
|
12207
13270
|
valid_custom_button_ids: set[int],
|
|
12208
13271
|
reason_path: str,
|
|
13272
|
+
allow_unverified_numeric_id: bool = False,
|
|
12209
13273
|
) -> tuple[int | None, dict[str, Any] | None]:
|
|
12210
13274
|
explicit_id = _coerce_positive_int(button_ref)
|
|
12211
13275
|
if explicit_id is not None:
|
|
12212
|
-
if explicit_id in valid_custom_button_ids:
|
|
13276
|
+
if explicit_id in valid_custom_button_ids or allow_unverified_numeric_id:
|
|
12213
13277
|
return explicit_id, None
|
|
12214
13278
|
return None, {
|
|
12215
13279
|
"error_code": "UNKNOWN_CUSTOM_BUTTON",
|
|
@@ -12486,11 +13550,16 @@ def _failed_from_api_error(
|
|
|
12486
13550
|
suggested_next_call: JSONObject | None = None,
|
|
12487
13551
|
recoverable: bool = True,
|
|
12488
13552
|
) -> JSONObject:
|
|
12489
|
-
|
|
13553
|
+
if is_auth_like_error(error):
|
|
13554
|
+
effective_error_code = "AUTH_REQUIRED"
|
|
13555
|
+
elif backend_code_int(error) == 40074:
|
|
13556
|
+
effective_error_code = "APP_EDIT_LOCKED"
|
|
13557
|
+
else:
|
|
13558
|
+
effective_error_code = error_code
|
|
12490
13559
|
public_message = _public_error_message(effective_error_code, error)
|
|
12491
13560
|
public_http_status = None if error.http_status == 404 else error.http_status
|
|
12492
13561
|
merged_details = dict(details or {})
|
|
12493
|
-
if error
|
|
13562
|
+
if backend_code_int(error) == 40074:
|
|
12494
13563
|
owner = _extract_edit_lock_owner(error.message)
|
|
12495
13564
|
merged_details.setdefault("lock_owner_name", owner.get("lock_owner_name"))
|
|
12496
13565
|
merged_details.setdefault("lock_owner_email", owner.get("lock_owner_email"))
|
|
@@ -12532,6 +13601,91 @@ def _failed_from_api_error(
|
|
|
12532
13601
|
)
|
|
12533
13602
|
|
|
12534
13603
|
|
|
13604
|
+
def _post_write_readback_pending_result(
|
|
13605
|
+
*,
|
|
13606
|
+
error_code: str,
|
|
13607
|
+
message: str,
|
|
13608
|
+
normalized_args: JSONObject | None = None,
|
|
13609
|
+
details: JSONObject | None = None,
|
|
13610
|
+
suggested_next_call: JSONObject | None = None,
|
|
13611
|
+
request_id: str | None = None,
|
|
13612
|
+
backend_code: Any = None,
|
|
13613
|
+
http_status: int | None = None,
|
|
13614
|
+
) -> JSONObject:
|
|
13615
|
+
effective_details = details or {}
|
|
13616
|
+
transport_error = _readback_transport_error_from_details(effective_details)
|
|
13617
|
+
effective_backend_code = backend_code if backend_code is not None else (transport_error or {}).get("backend_code")
|
|
13618
|
+
effective_http_status = http_status if http_status is not None else (transport_error or {}).get("http_status")
|
|
13619
|
+
effective_request_id = request_id if request_id is not None else (transport_error or {}).get("request_id")
|
|
13620
|
+
warning = _warning("READBACK_UNAVAILABLE_AFTER_WRITE", "write was executed but post-write readback is unavailable")
|
|
13621
|
+
for key, value in (
|
|
13622
|
+
("backend_code", effective_backend_code),
|
|
13623
|
+
("http_status", effective_http_status),
|
|
13624
|
+
("request_id", effective_request_id),
|
|
13625
|
+
):
|
|
13626
|
+
if value is not None:
|
|
13627
|
+
warning[key] = value
|
|
13628
|
+
return {
|
|
13629
|
+
"status": "partial_success",
|
|
13630
|
+
"error_code": error_code,
|
|
13631
|
+
"recoverable": True,
|
|
13632
|
+
"message": message,
|
|
13633
|
+
"normalized_args": normalized_args or {},
|
|
13634
|
+
"missing_fields": [],
|
|
13635
|
+
"allowed_values": {},
|
|
13636
|
+
"details": effective_details,
|
|
13637
|
+
"suggested_next_call": suggested_next_call,
|
|
13638
|
+
"request_id": effective_request_id,
|
|
13639
|
+
"backend_code": effective_backend_code,
|
|
13640
|
+
"http_status": effective_http_status,
|
|
13641
|
+
"noop": False,
|
|
13642
|
+
"warnings": [warning],
|
|
13643
|
+
"verification": {
|
|
13644
|
+
"readback_unavailable": True,
|
|
13645
|
+
"metadata_unverified": True,
|
|
13646
|
+
},
|
|
13647
|
+
"verified": False,
|
|
13648
|
+
"write_executed": True,
|
|
13649
|
+
"write_succeeded": True,
|
|
13650
|
+
"safe_to_retry": False,
|
|
13651
|
+
}
|
|
13652
|
+
|
|
13653
|
+
|
|
13654
|
+
def _readback_transport_error_from_details(details: JSONObject) -> JSONObject | None:
|
|
13655
|
+
direct = details.get("transport_error")
|
|
13656
|
+
if isinstance(direct, dict):
|
|
13657
|
+
return direct
|
|
13658
|
+
for key in (
|
|
13659
|
+
"readback_error",
|
|
13660
|
+
"verification_error",
|
|
13661
|
+
"draft_readback_error",
|
|
13662
|
+
"live_readback_error",
|
|
13663
|
+
"state_read_blocked",
|
|
13664
|
+
):
|
|
13665
|
+
value = details.get(key)
|
|
13666
|
+
if isinstance(value, dict) and isinstance(value.get("transport_error"), dict):
|
|
13667
|
+
return value.get("transport_error")
|
|
13668
|
+
verification_result = details.get("verification_result")
|
|
13669
|
+
if isinstance(verification_result, dict):
|
|
13670
|
+
verification_details = verification_result.get("details")
|
|
13671
|
+
if isinstance(verification_details, dict):
|
|
13672
|
+
nested = verification_details.get("transport_error")
|
|
13673
|
+
if isinstance(nested, dict):
|
|
13674
|
+
payload = dict(nested)
|
|
13675
|
+
for key in ("backend_code", "http_status", "request_id", "category"):
|
|
13676
|
+
if payload.get(key) is None and verification_result.get(key) is not None:
|
|
13677
|
+
payload[key] = verification_result.get(key)
|
|
13678
|
+
return payload
|
|
13679
|
+
payload = {
|
|
13680
|
+
key: verification_result.get(key)
|
|
13681
|
+
for key in ("backend_code", "http_status", "request_id", "category")
|
|
13682
|
+
if verification_result.get(key) is not None
|
|
13683
|
+
}
|
|
13684
|
+
if payload:
|
|
13685
|
+
return payload
|
|
13686
|
+
return None
|
|
13687
|
+
|
|
13688
|
+
|
|
12535
13689
|
def _transport_error_payload(error: QingflowApiError) -> JSONObject:
|
|
12536
13690
|
return {
|
|
12537
13691
|
"http_status": error.http_status,
|
|
@@ -12542,7 +13696,30 @@ def _transport_error_payload(error: QingflowApiError) -> JSONObject:
|
|
|
12542
13696
|
|
|
12543
13697
|
|
|
12544
13698
|
def _is_permission_restricted_api_error(error: QingflowApiError) -> bool:
|
|
12545
|
-
|
|
13699
|
+
if is_auth_like_error(error):
|
|
13700
|
+
return False
|
|
13701
|
+
return backend_code_int(error) in {40002, 40027}
|
|
13702
|
+
|
|
13703
|
+
|
|
13704
|
+
def _is_optional_builder_lookup_error(error: QingflowApiError) -> bool:
|
|
13705
|
+
if is_auth_like_error(error):
|
|
13706
|
+
return False
|
|
13707
|
+
return backend_code_int(error) in {40002, 40027, 404} or error.http_status == 404
|
|
13708
|
+
|
|
13709
|
+
|
|
13710
|
+
def _search_permission_blocked_from_warnings(payload: JSONObject) -> JSONObject | None:
|
|
13711
|
+
warnings = payload.get("warnings")
|
|
13712
|
+
if not isinstance(warnings, list):
|
|
13713
|
+
return None
|
|
13714
|
+
for item in warnings:
|
|
13715
|
+
if not isinstance(item, dict) or item.get("code") != "APP_SEARCH_FALLBACK_VISIBLE_APPS":
|
|
13716
|
+
continue
|
|
13717
|
+
return {
|
|
13718
|
+
"backend_code": item.get("backend_code"),
|
|
13719
|
+
"http_status": item.get("http_status"),
|
|
13720
|
+
"request_id": item.get("request_id"),
|
|
13721
|
+
}
|
|
13722
|
+
return None
|
|
12546
13723
|
|
|
12547
13724
|
|
|
12548
13725
|
def _append_response_detail(details: JSONObject, *, key: str, value: Any) -> None:
|
|
@@ -12743,7 +13920,7 @@ def _coerce_api_error(error: Exception) -> QingflowApiError:
|
|
|
12743
13920
|
|
|
12744
13921
|
|
|
12745
13922
|
def _public_error_message(error_code: str, error: QingflowApiError) -> str:
|
|
12746
|
-
if error
|
|
13923
|
+
if backend_code_int(error) == 40074 or error_code == "APP_EDIT_LOCKED":
|
|
12747
13924
|
owner = _extract_edit_lock_owner(error.message)
|
|
12748
13925
|
owner_label = owner.get("lock_owner_email") or owner.get("lock_owner_name")
|
|
12749
13926
|
if owner_label:
|
|
@@ -12777,7 +13954,9 @@ def _chart_delete_readback_is_not_found(error: QingflowApiError) -> bool:
|
|
|
12777
13954
|
|
|
12778
13955
|
|
|
12779
13956
|
def _delete_readback_is_not_found(error: QingflowApiError) -> bool:
|
|
12780
|
-
|
|
13957
|
+
if is_auth_like_error(error):
|
|
13958
|
+
return False
|
|
13959
|
+
backend_code = backend_code_int(error)
|
|
12781
13960
|
if error.http_status == 404 or backend_code in {404, 40038, 81007}:
|
|
12782
13961
|
return True
|
|
12783
13962
|
message = str(error.message or "").lower()
|
|
@@ -13619,7 +14798,7 @@ def _explain_chart_backend_validation_error(
|
|
|
13619
14798
|
chart_type: str,
|
|
13620
14799
|
payload: dict[str, Any] | None,
|
|
13621
14800
|
) -> dict[str, Any] | None:
|
|
13622
|
-
backend_code = api_error
|
|
14801
|
+
backend_code = backend_code_int(api_error)
|
|
13623
14802
|
if backend_code not in {81002, 81005}:
|
|
13624
14803
|
return None
|
|
13625
14804
|
chart_type = str(chart_type or (payload or {}).get("chartType") or "").strip().lower()
|
|
@@ -14902,7 +16081,9 @@ def _department_scope_equal(left: Any, right: Any) -> bool:
|
|
|
14902
16081
|
|
|
14903
16082
|
|
|
14904
16083
|
def _is_relation_target_metadata_read_restricted_api_error(error: QingflowApiError) -> bool:
|
|
14905
|
-
|
|
16084
|
+
if is_auth_like_error(error):
|
|
16085
|
+
return False
|
|
16086
|
+
return backend_code_int(error) in {40002, 40027, 40161}
|
|
14906
16087
|
|
|
14907
16088
|
|
|
14908
16089
|
def _relation_target_field_matches(left: dict[str, Any], right: dict[str, Any]) -> bool:
|
|
@@ -20781,8 +21962,13 @@ def _serialize_view_button_binding(
|
|
|
20781
21962
|
binding: ViewButtonBindingPatch,
|
|
20782
21963
|
current_fields_by_name: dict[str, dict[str, Any]],
|
|
20783
21964
|
valid_custom_button_ids: set[int],
|
|
21965
|
+
allow_unverified_custom_button_id: bool = False,
|
|
20784
21966
|
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
20785
|
-
if
|
|
21967
|
+
if (
|
|
21968
|
+
binding.button_type == PublicViewButtonType.custom
|
|
21969
|
+
and binding.button_id not in valid_custom_button_ids
|
|
21970
|
+
and not allow_unverified_custom_button_id
|
|
21971
|
+
):
|
|
20786
21972
|
return {}, [
|
|
20787
21973
|
{
|
|
20788
21974
|
"error_code": "UNKNOWN_CUSTOM_BUTTON",
|
|
@@ -22087,6 +23273,20 @@ def _associated_resource_patch_has_match_config(patch: AssociatedResourceUpsertP
|
|
|
22087
23273
|
return bool(patch.match_mappings) or bool(patch.match_rules) or "match_mappings" in fields_set
|
|
22088
23274
|
|
|
22089
23275
|
|
|
23276
|
+
def _extract_associated_resource_id_from_result(result: Any) -> int | None:
|
|
23277
|
+
if isinstance(result, dict):
|
|
23278
|
+
for key in ("associated_item_id", "associatedItemId", "asosChartId", "id"):
|
|
23279
|
+
item_id = _coerce_positive_int(result.get(key))
|
|
23280
|
+
if item_id is not None:
|
|
23281
|
+
return item_id
|
|
23282
|
+
nested = result.get("result") or result.get("data")
|
|
23283
|
+
if nested is not None and nested is not result:
|
|
23284
|
+
return _extract_associated_resource_id_from_result(nested)
|
|
23285
|
+
if isinstance(result, list) and len(result) == 1:
|
|
23286
|
+
return _extract_associated_resource_id_from_result(result[0])
|
|
23287
|
+
return None
|
|
23288
|
+
|
|
23289
|
+
|
|
22090
23290
|
def _serialize_associated_resource_match_rules(match_rules: list[Any]) -> list[list[dict[str, Any]]]:
|
|
22091
23291
|
if not match_rules:
|
|
22092
23292
|
return []
|