@josephyan/qingflow-app-user-mcp 1.1.21 → 1.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/builder_facade/models.py +0 -1
- package/src/qingflow_mcp/builder_facade/service.py +1581 -487
- package/src/qingflow_mcp/cli/commands/builder.py +7 -19
- package/src/qingflow_mcp/server_app_builder.py +8 -13
- package/src/qingflow_mcp/tools/ai_builder_tools.py +719 -371
|
@@ -108,6 +108,7 @@ from .models import (
|
|
|
108
108
|
|
|
109
109
|
BUILDER_PORTAL_LIST_DETAIL_VERIFY_LIMIT = 50
|
|
110
110
|
CHART_APPLY_RECOMMENDED_UPSERT_BATCH_SIZE = 8
|
|
111
|
+
CUSTOM_BUTTON_VIEW_CONFIG_READBACK_RETRY_SECONDS = 0.4
|
|
111
112
|
|
|
112
113
|
|
|
113
114
|
QUESTION_TYPE_TO_FIELD_TYPE: dict[int, str] = {
|
|
@@ -293,6 +294,12 @@ class AiBuilderFacade:
|
|
|
293
294
|
def _with_backend_context(self, profile: str, handler):
|
|
294
295
|
return self.apps._run(profile, lambda _session_profile, context: handler(context))
|
|
295
296
|
|
|
297
|
+
def _read_app_base_raw_direct(self, *, profile: str, app_key: str) -> JSONObject:
|
|
298
|
+
return self._with_backend_context(
|
|
299
|
+
profile,
|
|
300
|
+
lambda context: self.apps.backend.request("GET", context, f"/app/{app_key}/baseInfo"),
|
|
301
|
+
)
|
|
302
|
+
|
|
296
303
|
def flow_get_schema(self, *, profile: str, schema_version: str | None = None) -> JSONObject:
|
|
297
304
|
normalized_args = {"schema_version": schema_version or workflow_spec_api.DEFAULT_SCHEMA_VERSION}
|
|
298
305
|
try:
|
|
@@ -406,22 +413,50 @@ class AiBuilderFacade:
|
|
|
406
413
|
"idempotency_key": idempotency_key,
|
|
407
414
|
"schema_version": schema_version or workflow_spec_api.DEFAULT_SCHEMA_VERSION,
|
|
408
415
|
}
|
|
416
|
+
apply_started_at = time.perf_counter()
|
|
417
|
+
permission_ms = 0
|
|
418
|
+
flow_spec_apply_ms = 0
|
|
419
|
+
flow_spec_verify_ms = 0
|
|
420
|
+
publish_ms = 0
|
|
409
421
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
422
|
+
def finalize(response: JSONObject) -> JSONObject:
|
|
423
|
+
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
424
|
+
|
|
425
|
+
def attach_duration(response: JSONObject) -> JSONObject:
|
|
426
|
+
_merge_duration_breakdown(
|
|
427
|
+
response,
|
|
428
|
+
permission_ms=permission_ms,
|
|
429
|
+
flow_spec_apply_ms=flow_spec_apply_ms,
|
|
430
|
+
flow_spec_verify_ms=flow_spec_verify_ms,
|
|
431
|
+
publish_ms=publish_ms,
|
|
432
|
+
total_ms=_duration_ms(apply_started_at),
|
|
433
|
+
)
|
|
434
|
+
return response
|
|
435
|
+
|
|
436
|
+
def finish(response: JSONObject) -> JSONObject:
|
|
437
|
+
return finalize(attach_duration(response))
|
|
438
|
+
|
|
439
|
+
def finish_with_publish(response: JSONObject) -> JSONObject:
|
|
440
|
+
nonlocal publish_ms
|
|
441
|
+
publish_started_at = time.perf_counter()
|
|
442
|
+
published_response = self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
443
|
+
publish_ms += _duration_ms(publish_started_at)
|
|
444
|
+
return finish(published_response)
|
|
445
|
+
|
|
446
|
+
permission_started_at = time.perf_counter()
|
|
410
447
|
permission_outcome = self._guard_app_permission(
|
|
411
448
|
profile=profile,
|
|
412
449
|
app_key=app_key,
|
|
413
450
|
required_permission="data_manage",
|
|
414
451
|
normalized_args=normalized_args,
|
|
415
452
|
)
|
|
453
|
+
permission_ms += _duration_ms(permission_started_at)
|
|
416
454
|
if permission_outcome.block is not None:
|
|
417
|
-
return permission_outcome.block
|
|
455
|
+
return finish(permission_outcome.block)
|
|
418
456
|
permission_outcomes.append(permission_outcome)
|
|
419
457
|
|
|
420
|
-
def finalize(response: JSONObject) -> JSONObject:
|
|
421
|
-
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
422
|
-
|
|
423
458
|
if not isinstance(spec, dict) or not spec:
|
|
424
|
-
return
|
|
459
|
+
return finish(
|
|
425
460
|
_failed(
|
|
426
461
|
"FLOW_SPEC_REQUIRED",
|
|
427
462
|
"spec must be a non-empty WorkflowSpecDTO object",
|
|
@@ -436,13 +471,15 @@ class AiBuilderFacade:
|
|
|
436
471
|
"spec": spec,
|
|
437
472
|
}
|
|
438
473
|
try:
|
|
474
|
+
flow_spec_apply_started_at = time.perf_counter()
|
|
439
475
|
apply_result = self._with_backend_context(
|
|
440
476
|
profile,
|
|
441
477
|
lambda context: workflow_spec_api.post_apply(self.apps.backend, context, apply_body=apply_body),
|
|
442
478
|
)
|
|
443
479
|
except (QingflowApiError, RuntimeError) as error:
|
|
480
|
+
flow_spec_apply_ms += _duration_ms(flow_spec_apply_started_at)
|
|
444
481
|
api_error = _coerce_api_error(error)
|
|
445
|
-
return
|
|
482
|
+
return finish(
|
|
446
483
|
_failed_from_api_error(
|
|
447
484
|
"FLOW_SPEC_APPLY_FAILED",
|
|
448
485
|
api_error,
|
|
@@ -451,15 +488,18 @@ class AiBuilderFacade:
|
|
|
451
488
|
suggested_next_call={"tool_name": "app_flow_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
452
489
|
)
|
|
453
490
|
)
|
|
491
|
+
flow_spec_apply_ms += _duration_ms(flow_spec_apply_started_at)
|
|
454
492
|
if not isinstance(apply_result, dict):
|
|
455
493
|
apply_result = {"result": apply_result}
|
|
494
|
+
flow_spec_verify_started_at = time.perf_counter()
|
|
456
495
|
verification, verify_warnings, verified = workflow_spec_api.verify_apply_response(
|
|
457
496
|
input_spec=spec,
|
|
458
497
|
apply_result=apply_result,
|
|
459
498
|
)
|
|
499
|
+
flow_spec_verify_ms += _duration_ms(flow_spec_verify_started_at)
|
|
460
500
|
backend_status = str(apply_result.get("status") or "SUCCESS").upper()
|
|
461
501
|
if backend_status not in {"SUCCESS", "OK"}:
|
|
462
|
-
return
|
|
502
|
+
return finish(
|
|
463
503
|
_failed(
|
|
464
504
|
"FLOW_SPEC_APPLY_FAILED",
|
|
465
505
|
str(apply_result.get("message") or "workflow spec apply failed"),
|
|
@@ -495,7 +535,7 @@ class AiBuilderFacade:
|
|
|
495
535
|
"write_succeeded": True,
|
|
496
536
|
"safe_to_retry": False,
|
|
497
537
|
}
|
|
498
|
-
return
|
|
538
|
+
return finish_with_publish(response)
|
|
499
539
|
|
|
500
540
|
def package_resolve(self, *, profile: str, package_name: str) -> JSONObject:
|
|
501
541
|
requested = str(package_name or "").strip()
|
|
@@ -796,7 +836,6 @@ class AiBuilderFacade:
|
|
|
796
836
|
profile: str,
|
|
797
837
|
package_id: int | None = None,
|
|
798
838
|
package_name: str | None = None,
|
|
799
|
-
create_if_missing: bool = False,
|
|
800
839
|
icon: str | None = None,
|
|
801
840
|
color: str | None = None,
|
|
802
841
|
visibility: VisibilityPatch | None = None,
|
|
@@ -807,7 +846,6 @@ class AiBuilderFacade:
|
|
|
807
846
|
normalized_args: JSONObject = {
|
|
808
847
|
"package_id": package_id,
|
|
809
848
|
**({"package_name": requested_name} if requested_name else {}),
|
|
810
|
-
"create_if_missing": bool(create_if_missing),
|
|
811
849
|
**({"icon": icon} if icon else {}),
|
|
812
850
|
**({"color": color} if color else {}),
|
|
813
851
|
**({"visibility": visibility.model_dump(mode="json")} if visibility is not None else {}),
|
|
@@ -819,39 +857,119 @@ class AiBuilderFacade:
|
|
|
819
857
|
create_result: JSONObject | None = None
|
|
820
858
|
update_result: JSONObject | None = None
|
|
821
859
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
860
|
+
duration_started_at = time.perf_counter()
|
|
861
|
+
duration_breakdown_ms: dict[str, int] = {
|
|
862
|
+
"package_create_ms": 0,
|
|
863
|
+
"package_update_ms": 0,
|
|
864
|
+
"package_items_ms": 0,
|
|
865
|
+
"package_readback_ms": 0,
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
def finish_duration(response: JSONObject) -> JSONObject:
|
|
869
|
+
duration = dict(duration_breakdown_ms)
|
|
870
|
+
duration["total_ms"] = int((time.perf_counter() - duration_started_at) * 1000)
|
|
871
|
+
existing_duration = response.get("duration_breakdown_ms")
|
|
872
|
+
if isinstance(existing_duration, dict):
|
|
873
|
+
duration.update({str(key): value for key, value in existing_duration.items()})
|
|
874
|
+
duration["total_ms"] = int((time.perf_counter() - duration_started_at) * 1000)
|
|
875
|
+
response["duration_breakdown_ms"] = duration
|
|
876
|
+
return response
|
|
822
877
|
|
|
823
878
|
if effective_package_id is None:
|
|
824
|
-
if not create_if_missing:
|
|
825
|
-
return _failed(
|
|
826
|
-
"PACKAGE_ID_REQUIRED",
|
|
827
|
-
"package_id is required unless create_if_missing=true",
|
|
828
|
-
normalized_args=normalized_args,
|
|
829
|
-
suggested_next_call=None,
|
|
830
|
-
)
|
|
831
879
|
if not requested_name:
|
|
832
880
|
return _failed(
|
|
833
881
|
"PACKAGE_NAME_REQUIRED",
|
|
834
|
-
"package_name is required when
|
|
882
|
+
"package_name is required when creating a package without package_id",
|
|
835
883
|
normalized_args=normalized_args,
|
|
836
884
|
suggested_next_call=None,
|
|
837
885
|
)
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
886
|
+
create_started_at = time.perf_counter()
|
|
887
|
+
try:
|
|
888
|
+
desired_tag_icon = encode_workspace_icon_with_defaults(
|
|
889
|
+
icon=icon,
|
|
890
|
+
color=color,
|
|
891
|
+
title=requested_name,
|
|
892
|
+
fallback_icon_name="files-folder",
|
|
893
|
+
)
|
|
894
|
+
desired_auth = self._compile_visibility_to_member_auth(profile=profile, visibility=visibility)
|
|
895
|
+
created_payload = self.packages.package_create(
|
|
896
|
+
profile=profile,
|
|
897
|
+
payload={"tagName": requested_name, "tagIcon": desired_tag_icon, "auth": desired_auth},
|
|
898
|
+
)
|
|
899
|
+
except VisibilityResolutionError as error:
|
|
900
|
+
duration_breakdown_ms["package_create_ms"] += int((time.perf_counter() - create_started_at) * 1000)
|
|
901
|
+
return finish_duration(
|
|
902
|
+
_failed(
|
|
903
|
+
error.error_code,
|
|
904
|
+
error.message,
|
|
905
|
+
normalized_args=normalized_args,
|
|
906
|
+
details=error.details,
|
|
907
|
+
suggested_next_call=None,
|
|
908
|
+
)
|
|
909
|
+
)
|
|
910
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
911
|
+
duration_breakdown_ms["package_create_ms"] += int((time.perf_counter() - create_started_at) * 1000)
|
|
912
|
+
api_error = _coerce_api_error(error)
|
|
913
|
+
return finish_duration(
|
|
914
|
+
_failed_from_api_error(
|
|
915
|
+
"PACKAGE_CREATE_FAILED",
|
|
916
|
+
api_error,
|
|
917
|
+
normalized_args=normalized_args,
|
|
918
|
+
details={"package_name": requested_name},
|
|
919
|
+
suggested_next_call={
|
|
920
|
+
"tool_name": "package_apply",
|
|
921
|
+
"arguments": {
|
|
922
|
+
"profile": profile,
|
|
923
|
+
"package_name": requested_name,
|
|
924
|
+
**({"icon": icon} if icon else {}),
|
|
925
|
+
**({"color": color} if color else {}),
|
|
926
|
+
},
|
|
927
|
+
},
|
|
928
|
+
)
|
|
929
|
+
)
|
|
930
|
+
duration_breakdown_ms["package_create_ms"] += int((time.perf_counter() - create_started_at) * 1000)
|
|
931
|
+
result = created_payload.get("result") if isinstance(created_payload.get("result"), dict) else {}
|
|
932
|
+
tag_id = _coerce_positive_int(result.get("tagId"))
|
|
933
|
+
tag_name = str(result.get("tagName") or requested_name).strip() or requested_name
|
|
934
|
+
tag_icon = str(result.get("tagIcon") or desired_tag_icon or "").strip() or None
|
|
935
|
+
verified = tag_id is not None
|
|
936
|
+
create_result = {
|
|
937
|
+
"status": "success" if verified else "partial_success",
|
|
938
|
+
"error_code": None if verified else "PACKAGE_CREATE_UNVERIFIED",
|
|
939
|
+
"recoverable": not verified,
|
|
940
|
+
"message": "created package" if verified else "created package but could not verify package_id",
|
|
941
|
+
"normalized_args": normalized_args,
|
|
942
|
+
"missing_fields": [],
|
|
943
|
+
"allowed_values": {},
|
|
944
|
+
"details": {"direct_submit": True},
|
|
945
|
+
"request_id": created_payload.get("request_id") if isinstance(created_payload, dict) else None,
|
|
946
|
+
"suggested_next_call": None
|
|
947
|
+
if verified
|
|
948
|
+
else {"tool_name": "package_get", "arguments": {"profile": profile, "package_id": tag_id}},
|
|
949
|
+
"noop": False,
|
|
950
|
+
"warnings": [],
|
|
951
|
+
"verification": {"tag_id_verified": verified, "direct_submit": True},
|
|
952
|
+
"verified": verified,
|
|
953
|
+
"tag_id": tag_id,
|
|
954
|
+
"tag_name": tag_name,
|
|
955
|
+
"tag_icon": tag_icon,
|
|
956
|
+
"visibility": _public_visibility_from_member_auth(desired_auth),
|
|
957
|
+
"write_executed": True,
|
|
958
|
+
"write_succeeded": True,
|
|
959
|
+
"safe_to_retry": False,
|
|
960
|
+
}
|
|
845
961
|
if create_result.get("status") not in {"success", "partial_success"}:
|
|
846
|
-
return _publicize_package_apply_failure(create_result, profile=profile, normalized_args=normalized_args)
|
|
962
|
+
return finish_duration(_publicize_package_apply_failure(create_result, profile=profile, normalized_args=normalized_args))
|
|
847
963
|
effective_package_id = _coerce_positive_int(create_result.get("tag_id") or create_result.get("package_id"))
|
|
848
964
|
if effective_package_id is None:
|
|
849
|
-
return
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
965
|
+
return finish_duration(
|
|
966
|
+
_failed(
|
|
967
|
+
"PACKAGE_CREATE_UNVERIFIED",
|
|
968
|
+
"created package but could not verify package_id",
|
|
969
|
+
normalized_args=normalized_args,
|
|
970
|
+
details={"create_result": create_result},
|
|
971
|
+
suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "package_id": package_id}},
|
|
972
|
+
)
|
|
855
973
|
)
|
|
856
974
|
normalized_args["package_id"] = effective_package_id
|
|
857
975
|
created = True
|
|
@@ -867,6 +985,7 @@ class AiBuilderFacade:
|
|
|
867
985
|
if edit_tag_outcome.block is not None:
|
|
868
986
|
return edit_tag_outcome.block
|
|
869
987
|
permission_outcomes.append(edit_tag_outcome)
|
|
988
|
+
update_started_at = time.perf_counter()
|
|
870
989
|
update_result = self.package_update(
|
|
871
990
|
profile=profile,
|
|
872
991
|
tag_id=effective_package_id,
|
|
@@ -875,18 +994,22 @@ class AiBuilderFacade:
|
|
|
875
994
|
color=color,
|
|
876
995
|
visibility=visibility,
|
|
877
996
|
)
|
|
997
|
+
duration_breakdown_ms["package_update_ms"] += int((time.perf_counter() - update_started_at) * 1000)
|
|
878
998
|
if update_result.get("status") not in {"success", "partial_success"}:
|
|
879
|
-
return _publicize_package_apply_failure(update_result, profile=profile, normalized_args=normalized_args)
|
|
999
|
+
return finish_duration(_publicize_package_apply_failure(update_result, profile=profile, normalized_args=normalized_args))
|
|
880
1000
|
|
|
881
1001
|
layout_result: JSONObject | None = None
|
|
882
1002
|
if items is not None:
|
|
883
1003
|
if not isinstance(items, list):
|
|
884
|
-
return
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1004
|
+
return finish_duration(
|
|
1005
|
+
_failed(
|
|
1006
|
+
"PACKAGE_ITEMS_INVALID",
|
|
1007
|
+
"items must be a list",
|
|
1008
|
+
normalized_args=normalized_args,
|
|
1009
|
+
suggested_next_call=None,
|
|
1010
|
+
)
|
|
889
1011
|
)
|
|
1012
|
+
items_started_at = time.perf_counter()
|
|
890
1013
|
layout_result = self._apply_package_items(
|
|
891
1014
|
profile=profile,
|
|
892
1015
|
package_id=effective_package_id,
|
|
@@ -894,6 +1017,7 @@ class AiBuilderFacade:
|
|
|
894
1017
|
allow_detach=allow_detach,
|
|
895
1018
|
normalized_args=normalized_args,
|
|
896
1019
|
)
|
|
1020
|
+
duration_breakdown_ms["package_items_ms"] += int((time.perf_counter() - items_started_at) * 1000)
|
|
897
1021
|
if layout_result.get("status") not in {"success", "partial_success"}:
|
|
898
1022
|
prior_package_write_executed = bool(
|
|
899
1023
|
created
|
|
@@ -930,8 +1054,8 @@ class AiBuilderFacade:
|
|
|
930
1054
|
)
|
|
931
1055
|
partial["package_id"] = effective_package_id
|
|
932
1056
|
partial["layout_failed"] = True
|
|
933
|
-
return _apply_permission_outcomes(partial, *permission_outcomes)
|
|
934
|
-
return _apply_permission_outcomes(layout_result, *permission_outcomes)
|
|
1057
|
+
return finish_duration(_apply_permission_outcomes(partial, *permission_outcomes))
|
|
1058
|
+
return finish_duration(_apply_permission_outcomes(layout_result, *permission_outcomes))
|
|
935
1059
|
|
|
936
1060
|
write_executed = bool(
|
|
937
1061
|
created
|
|
@@ -948,10 +1072,120 @@ class AiBuilderFacade:
|
|
|
948
1072
|
and not bool(layout_result.get("noop"))
|
|
949
1073
|
)
|
|
950
1074
|
)
|
|
1075
|
+
if items is None and (created or (metadata_requested and isinstance(update_result, dict))):
|
|
1076
|
+
metadata_result = create_result if created else update_result
|
|
1077
|
+
metadata_verified = bool(metadata_result.get("verified")) if isinstance(metadata_result, dict) else False
|
|
1078
|
+
metadata_verification = metadata_result.get("verification") if isinstance(metadata_result, dict) and isinstance(metadata_result.get("verification"), dict) else {}
|
|
1079
|
+
response_verification: JSONObject = {
|
|
1080
|
+
"package_exists": True,
|
|
1081
|
+
"package_created": created,
|
|
1082
|
+
"layout_applied": False,
|
|
1083
|
+
"metadata_verified": metadata_verified,
|
|
1084
|
+
**deepcopy(metadata_verification),
|
|
1085
|
+
}
|
|
1086
|
+
response: JSONObject = {
|
|
1087
|
+
"status": "success" if metadata_verified else "partial_success",
|
|
1088
|
+
"error_code": None if metadata_verified else (metadata_result.get("error_code") if isinstance(metadata_result, dict) else "PACKAGE_READBACK_PENDING"),
|
|
1089
|
+
"recoverable": not metadata_verified,
|
|
1090
|
+
"message": "applied package" if metadata_verified else "applied package with unverified readback",
|
|
1091
|
+
"normalized_args": normalized_args,
|
|
1092
|
+
"missing_fields": [],
|
|
1093
|
+
"allowed_values": {},
|
|
1094
|
+
"details": {
|
|
1095
|
+
"metadata_result": metadata_result,
|
|
1096
|
+
"final_readback_skipped": True,
|
|
1097
|
+
"skip_reason": "metadata_result_already_verified" if metadata_verified else "metadata_result_is_partial",
|
|
1098
|
+
},
|
|
1099
|
+
"request_id": metadata_result.get("request_id") if isinstance(metadata_result, dict) else None,
|
|
1100
|
+
"suggested_next_call": None
|
|
1101
|
+
if metadata_verified
|
|
1102
|
+
else {"tool_name": "package_get", "arguments": {"profile": profile, "package_id": effective_package_id}},
|
|
1103
|
+
"noop": False,
|
|
1104
|
+
"warnings": deepcopy(metadata_result.get("warnings") or []) if isinstance(metadata_result, dict) and isinstance(metadata_result.get("warnings"), list) else [],
|
|
1105
|
+
"verification": response_verification,
|
|
1106
|
+
"verified": metadata_verified,
|
|
1107
|
+
"write_executed": write_executed,
|
|
1108
|
+
"write_succeeded": write_executed,
|
|
1109
|
+
"safe_to_retry": not write_executed,
|
|
1110
|
+
}
|
|
1111
|
+
if isinstance(metadata_result, dict):
|
|
1112
|
+
response.update(
|
|
1113
|
+
{
|
|
1114
|
+
key: deepcopy(value)
|
|
1115
|
+
for key, value in metadata_result.items()
|
|
1116
|
+
if key
|
|
1117
|
+
not in {
|
|
1118
|
+
"status",
|
|
1119
|
+
"error_code",
|
|
1120
|
+
"recoverable",
|
|
1121
|
+
"message",
|
|
1122
|
+
"normalized_args",
|
|
1123
|
+
"missing_fields",
|
|
1124
|
+
"allowed_values",
|
|
1125
|
+
"details",
|
|
1126
|
+
"request_id",
|
|
1127
|
+
"suggested_next_call",
|
|
1128
|
+
"noop",
|
|
1129
|
+
"warnings",
|
|
1130
|
+
"verification",
|
|
1131
|
+
"verified",
|
|
1132
|
+
"write_executed",
|
|
1133
|
+
"write_succeeded",
|
|
1134
|
+
"safe_to_retry",
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
)
|
|
1138
|
+
return finish_duration(_apply_permission_outcomes(response, *permission_outcomes))
|
|
1139
|
+
|
|
1140
|
+
layout_readback_skippable = (
|
|
1141
|
+
items is not None
|
|
1142
|
+
and not created
|
|
1143
|
+
and not metadata_requested
|
|
1144
|
+
and isinstance(layout_result, dict)
|
|
1145
|
+
and bool(layout_result.get("verified"))
|
|
1146
|
+
and isinstance(layout_result.get("details"), dict)
|
|
1147
|
+
and bool(layout_result["details"].get("current_read_skipped"))
|
|
1148
|
+
)
|
|
1149
|
+
if layout_readback_skippable:
|
|
1150
|
+
response: JSONObject = {
|
|
1151
|
+
"status": "success",
|
|
1152
|
+
"error_code": None,
|
|
1153
|
+
"recoverable": False,
|
|
1154
|
+
"message": "applied package",
|
|
1155
|
+
"normalized_args": normalized_args,
|
|
1156
|
+
"missing_fields": [],
|
|
1157
|
+
"allowed_values": {},
|
|
1158
|
+
"details": {
|
|
1159
|
+
"layout_result": layout_result,
|
|
1160
|
+
"final_readback_skipped": True,
|
|
1161
|
+
"skip_reason": "layout_result_already_verified",
|
|
1162
|
+
},
|
|
1163
|
+
"request_id": layout_result.get("request_id") if isinstance(layout_result.get("request_id"), str) else None,
|
|
1164
|
+
"suggested_next_call": None,
|
|
1165
|
+
"noop": False,
|
|
1166
|
+
"warnings": deepcopy(layout_result.get("warnings") or []) if isinstance(layout_result.get("warnings"), list) else [],
|
|
1167
|
+
"verification": {
|
|
1168
|
+
"package_exists": True,
|
|
1169
|
+
"package_created": False,
|
|
1170
|
+
"layout_applied": True,
|
|
1171
|
+
"layout_verified": True,
|
|
1172
|
+
"metadata_verified": True,
|
|
1173
|
+
"final_readback_skipped": True,
|
|
1174
|
+
},
|
|
1175
|
+
"verified": True,
|
|
1176
|
+
"package_id": effective_package_id,
|
|
1177
|
+
"write_executed": write_executed,
|
|
1178
|
+
"write_succeeded": write_executed,
|
|
1179
|
+
"safe_to_retry": not write_executed,
|
|
1180
|
+
}
|
|
1181
|
+
return finish_duration(_apply_permission_outcomes(response, *permission_outcomes))
|
|
1182
|
+
|
|
1183
|
+
readback_started_at = time.perf_counter()
|
|
951
1184
|
verification = self.package_get(profile=profile, package_id=effective_package_id)
|
|
1185
|
+
duration_breakdown_ms["package_readback_ms"] += int((time.perf_counter() - readback_started_at) * 1000)
|
|
952
1186
|
if verification.get("status") != "success":
|
|
953
1187
|
if write_executed:
|
|
954
|
-
return _apply_permission_outcomes(
|
|
1188
|
+
return finish_duration(_apply_permission_outcomes(
|
|
955
1189
|
_post_write_readback_pending_result(
|
|
956
1190
|
error_code="PACKAGE_READBACK_PENDING",
|
|
957
1191
|
message="applied package; final package readback is unavailable",
|
|
@@ -960,8 +1194,8 @@ class AiBuilderFacade:
|
|
|
960
1194
|
suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "package_id": effective_package_id}},
|
|
961
1195
|
),
|
|
962
1196
|
*permission_outcomes,
|
|
963
|
-
)
|
|
964
|
-
return _apply_permission_outcomes(verification, *permission_outcomes)
|
|
1197
|
+
))
|
|
1198
|
+
return finish_duration(_apply_permission_outcomes(verification, *permission_outcomes))
|
|
965
1199
|
expected_visibility = None
|
|
966
1200
|
if visibility is not None:
|
|
967
1201
|
try:
|
|
@@ -1052,7 +1286,7 @@ class AiBuilderFacade:
|
|
|
1052
1286
|
}
|
|
1053
1287
|
},
|
|
1054
1288
|
}
|
|
1055
|
-
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
1289
|
+
return finish_duration(_apply_permission_outcomes(response, *permission_outcomes))
|
|
1056
1290
|
|
|
1057
1291
|
def package_update(
|
|
1058
1292
|
self,
|
|
@@ -1313,6 +1547,72 @@ class AiBuilderFacade:
|
|
|
1313
1547
|
allow_detach: bool,
|
|
1314
1548
|
normalized_args: JSONObject,
|
|
1315
1549
|
) -> JSONObject:
|
|
1550
|
+
duplicate_resources = _find_duplicate_package_resources(items)
|
|
1551
|
+
if duplicate_resources:
|
|
1552
|
+
return _failed(
|
|
1553
|
+
"PACKAGE_LAYOUT_DUPLICATE_ITEM",
|
|
1554
|
+
"items contains duplicate apps or portals",
|
|
1555
|
+
normalized_args=normalized_args,
|
|
1556
|
+
details={"duplicates": [{"type": item[0], "id": item[1]} for item in duplicate_resources]},
|
|
1557
|
+
suggested_next_call=None,
|
|
1558
|
+
)
|
|
1559
|
+
|
|
1560
|
+
desired_groups = _collect_public_package_group_specs(items)
|
|
1561
|
+
if allow_detach and not desired_groups:
|
|
1562
|
+
permission_outcome = self._guard_package_permission(
|
|
1563
|
+
profile=profile,
|
|
1564
|
+
tag_id=package_id,
|
|
1565
|
+
required_permission="edit_app",
|
|
1566
|
+
normalized_args=normalized_args,
|
|
1567
|
+
)
|
|
1568
|
+
if permission_outcome.block is not None:
|
|
1569
|
+
return permission_outcome.block
|
|
1570
|
+
try:
|
|
1571
|
+
backend_items = _backend_package_items_from_public_items(items, {})
|
|
1572
|
+
except ValueError as error:
|
|
1573
|
+
return _failed(
|
|
1574
|
+
"PACKAGE_LAYOUT_INVALID",
|
|
1575
|
+
str(error),
|
|
1576
|
+
normalized_args=normalized_args,
|
|
1577
|
+
details={"package_id": package_id},
|
|
1578
|
+
suggested_next_call=None,
|
|
1579
|
+
)
|
|
1580
|
+
try:
|
|
1581
|
+
sort_result = self.packages.package_sort_items(profile=profile, tag_id=package_id, tag_items=backend_items)
|
|
1582
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1583
|
+
api_error = _coerce_api_error(error)
|
|
1584
|
+
return _failed_from_api_error(
|
|
1585
|
+
"PACKAGE_LAYOUT_SORT_FAILED",
|
|
1586
|
+
api_error,
|
|
1587
|
+
normalized_args=normalized_args,
|
|
1588
|
+
details={"package_id": package_id, "current_read_skipped": True},
|
|
1589
|
+
suggested_next_call=None,
|
|
1590
|
+
)
|
|
1591
|
+
return _apply_permission_outcomes(
|
|
1592
|
+
{
|
|
1593
|
+
"status": "success",
|
|
1594
|
+
"error_code": None,
|
|
1595
|
+
"recoverable": False,
|
|
1596
|
+
"message": "applied package items",
|
|
1597
|
+
"normalized_args": normalized_args,
|
|
1598
|
+
"missing_fields": [],
|
|
1599
|
+
"allowed_values": {},
|
|
1600
|
+
"details": {
|
|
1601
|
+
"group_operations": [],
|
|
1602
|
+
"sort_result": sort_result,
|
|
1603
|
+
"current_read_skipped": True,
|
|
1604
|
+
},
|
|
1605
|
+
"request_id": sort_result.get("request_id") if isinstance(sort_result, dict) else None,
|
|
1606
|
+
"suggested_next_call": None,
|
|
1607
|
+
"noop": False,
|
|
1608
|
+
"warnings": [],
|
|
1609
|
+
"verification": {"layout_applied": True, "current_read_skipped": True},
|
|
1610
|
+
"verified": True,
|
|
1611
|
+
"package_id": package_id,
|
|
1612
|
+
},
|
|
1613
|
+
permission_outcome,
|
|
1614
|
+
)
|
|
1615
|
+
|
|
1316
1616
|
current_detail_result: JSONObject | None = None
|
|
1317
1617
|
current_base_result: JSONObject | None = None
|
|
1318
1618
|
detail_api_error: QingflowApiError | None = None
|
|
@@ -1388,16 +1688,6 @@ class AiBuilderFacade:
|
|
|
1388
1688
|
suggested_next_call=None,
|
|
1389
1689
|
)
|
|
1390
1690
|
|
|
1391
|
-
duplicate_resources = _find_duplicate_package_resources(normalized_items)
|
|
1392
|
-
if duplicate_resources:
|
|
1393
|
-
return _failed(
|
|
1394
|
-
"PACKAGE_LAYOUT_DUPLICATE_ITEM",
|
|
1395
|
-
"items contains duplicate apps or portals",
|
|
1396
|
-
normalized_args=normalized_args,
|
|
1397
|
-
details={"duplicates": [{"type": item[0], "id": item[1]} for item in duplicate_resources]},
|
|
1398
|
-
suggested_next_call=None,
|
|
1399
|
-
)
|
|
1400
|
-
|
|
1401
1691
|
current_groups = {int(spec["group_id"]): str(spec["name"] or "").strip() for spec in current_group_specs}
|
|
1402
1692
|
desired_groups = _collect_public_package_group_specs(normalized_items)
|
|
1403
1693
|
desired_group_ids = {
|
|
@@ -3158,47 +3448,107 @@ class AiBuilderFacade:
|
|
|
3158
3448
|
def app_custom_buttons_apply(self, *, profile: str, request: CustomButtonsApplyRequest) -> JSONObject:
|
|
3159
3449
|
normalized_args = request.model_dump(mode="json", exclude_none=True, exclude_defaults=True)
|
|
3160
3450
|
app_key = request.app_key
|
|
3451
|
+
apply_started_at = time.perf_counter()
|
|
3452
|
+
permission_ms = 0
|
|
3453
|
+
button_inventory_ms = 0
|
|
3454
|
+
app_name_read_ms = 0
|
|
3455
|
+
button_patch_hydration_ms = 0
|
|
3456
|
+
add_data_compile_ms = 0
|
|
3457
|
+
edit_context_ms = 0
|
|
3458
|
+
button_write_ms = 0
|
|
3459
|
+
button_readback_ms = 0
|
|
3460
|
+
view_config_ms = 0
|
|
3461
|
+
publish_ms = 0
|
|
3161
3462
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
3162
3463
|
button_write_intent = bool(request.upsert_buttons or request.patch_buttons or request.remove_buttons)
|
|
3163
3464
|
if button_write_intent:
|
|
3465
|
+
permission_started_at = time.perf_counter()
|
|
3164
3466
|
permission_outcome = self._guard_app_permission(
|
|
3165
3467
|
profile=profile,
|
|
3166
3468
|
app_key=app_key,
|
|
3167
3469
|
required_permission="edit_app",
|
|
3168
3470
|
normalized_args=normalized_args,
|
|
3169
3471
|
)
|
|
3472
|
+
permission_ms += _duration_ms(permission_started_at)
|
|
3170
3473
|
if permission_outcome.block is not None:
|
|
3474
|
+
_merge_duration_breakdown(
|
|
3475
|
+
permission_outcome.block,
|
|
3476
|
+
permission_ms=permission_ms,
|
|
3477
|
+
total_ms=_duration_ms(apply_started_at),
|
|
3478
|
+
)
|
|
3171
3479
|
return permission_outcome.block
|
|
3172
3480
|
permission_outcomes.append(permission_outcome)
|
|
3173
3481
|
if request.view_configs:
|
|
3482
|
+
permission_started_at = time.perf_counter()
|
|
3174
3483
|
permission_outcome = self._guard_app_permission(
|
|
3175
3484
|
profile=profile,
|
|
3176
3485
|
app_key=app_key,
|
|
3177
3486
|
required_permission="view_manage",
|
|
3178
3487
|
normalized_args=normalized_args,
|
|
3179
3488
|
)
|
|
3489
|
+
permission_ms += _duration_ms(permission_started_at)
|
|
3180
3490
|
if permission_outcome.block is not None:
|
|
3491
|
+
_merge_duration_breakdown(
|
|
3492
|
+
permission_outcome.block,
|
|
3493
|
+
permission_ms=permission_ms,
|
|
3494
|
+
total_ms=_duration_ms(apply_started_at),
|
|
3495
|
+
)
|
|
3181
3496
|
return permission_outcome.block
|
|
3182
3497
|
permission_outcomes.append(permission_outcome)
|
|
3183
3498
|
|
|
3184
3499
|
def finalize(response: JSONObject) -> JSONObject:
|
|
3185
|
-
|
|
3500
|
+
response = _apply_permission_outcomes(response, *permission_outcomes)
|
|
3501
|
+
_merge_duration_breakdown(
|
|
3502
|
+
response,
|
|
3503
|
+
permission_ms=permission_ms,
|
|
3504
|
+
button_inventory_ms=button_inventory_ms,
|
|
3505
|
+
app_name_read_ms=app_name_read_ms,
|
|
3506
|
+
button_patch_hydration_ms=button_patch_hydration_ms,
|
|
3507
|
+
add_data_compile_ms=add_data_compile_ms,
|
|
3508
|
+
edit_context_ms=edit_context_ms,
|
|
3509
|
+
button_write_ms=button_write_ms,
|
|
3510
|
+
button_readback_ms=button_readback_ms,
|
|
3511
|
+
view_config_ms=view_config_ms,
|
|
3512
|
+
publish_ms=publish_ms,
|
|
3513
|
+
total_ms=_duration_ms(apply_started_at),
|
|
3514
|
+
)
|
|
3515
|
+
return response
|
|
3186
3516
|
|
|
3517
|
+
same_call_client_keys = {
|
|
3518
|
+
str(patch.client_key or "").strip()
|
|
3519
|
+
for patch in request.upsert_buttons
|
|
3520
|
+
if str(patch.client_key or "").strip()
|
|
3521
|
+
}
|
|
3522
|
+
all_upserts_are_named_creates = bool(request.upsert_buttons) and all(
|
|
3523
|
+
str(patch.client_key or "").strip()
|
|
3524
|
+
and _coerce_positive_int(patch.button_id) is None
|
|
3525
|
+
for patch in request.upsert_buttons
|
|
3526
|
+
)
|
|
3187
3527
|
view_config_refs = [
|
|
3188
3528
|
binding.button_ref
|
|
3189
3529
|
for config in request.view_configs
|
|
3190
3530
|
for binding in config.buttons
|
|
3191
3531
|
]
|
|
3192
|
-
|
|
3193
|
-
_coerce_positive_int(ref) is None
|
|
3532
|
+
view_refs_need_inventory = any(
|
|
3533
|
+
_coerce_positive_int(ref) is None and str(ref or "").strip() not in same_call_client_keys
|
|
3534
|
+
for ref in view_config_refs
|
|
3535
|
+
)
|
|
3536
|
+
needs_button_inventory = (
|
|
3537
|
+
bool(request.patch_buttons)
|
|
3538
|
+
or bool(request.remove_buttons)
|
|
3539
|
+
or any(_coerce_positive_int(patch.button_id) is not None for patch in request.upsert_buttons)
|
|
3540
|
+
or (bool(request.upsert_buttons) and not all_upserts_are_named_creates)
|
|
3541
|
+
or view_refs_need_inventory
|
|
3194
3542
|
)
|
|
3195
3543
|
try:
|
|
3544
|
+
button_inventory_started_at = time.perf_counter()
|
|
3196
3545
|
existing_buttons = (
|
|
3197
3546
|
self._load_custom_buttons_for_builder(profile=profile, app_key=app_key)
|
|
3198
3547
|
if needs_button_inventory
|
|
3199
3548
|
else []
|
|
3200
3549
|
)
|
|
3201
3550
|
except (QingflowApiError, RuntimeError) as error:
|
|
3551
|
+
button_inventory_ms += _duration_ms(button_inventory_started_at)
|
|
3202
3552
|
api_error = _coerce_api_error(error)
|
|
3203
3553
|
return finalize(_failed_from_api_error(
|
|
3204
3554
|
"CUSTOM_BUTTON_LIST_FAILED",
|
|
@@ -3207,7 +3557,10 @@ class AiBuilderFacade:
|
|
|
3207
3557
|
details=_with_state_read_blocked_details({"app_key": app_key}, resource="custom_buttons", error=api_error),
|
|
3208
3558
|
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3209
3559
|
))
|
|
3560
|
+
button_inventory_ms += _duration_ms(button_inventory_started_at)
|
|
3561
|
+
app_name_started_at = time.perf_counter()
|
|
3210
3562
|
app_name = self._read_app_name_for_builder_output(profile=profile, app_key=app_key)
|
|
3563
|
+
app_name_read_ms += _duration_ms(app_name_started_at)
|
|
3211
3564
|
|
|
3212
3565
|
existing_by_id = {
|
|
3213
3566
|
button_id: item
|
|
@@ -3222,6 +3575,7 @@ class AiBuilderFacade:
|
|
|
3222
3575
|
|
|
3223
3576
|
upsert_buttons = list(request.upsert_buttons)
|
|
3224
3577
|
if request.patch_buttons:
|
|
3578
|
+
button_patch_started_at = time.perf_counter()
|
|
3225
3579
|
expanded_buttons, patch_issues, patch_results = self._expand_custom_button_partial_patches(
|
|
3226
3580
|
profile=profile,
|
|
3227
3581
|
app_key=app_key,
|
|
@@ -3229,6 +3583,7 @@ class AiBuilderFacade:
|
|
|
3229
3583
|
existing_by_text=existing_by_text,
|
|
3230
3584
|
patch_buttons=request.patch_buttons,
|
|
3231
3585
|
)
|
|
3586
|
+
button_patch_hydration_ms += _duration_ms(button_patch_started_at)
|
|
3232
3587
|
if patch_issues:
|
|
3233
3588
|
return finalize(
|
|
3234
3589
|
_failed(
|
|
@@ -3246,11 +3601,13 @@ class AiBuilderFacade:
|
|
|
3246
3601
|
]
|
|
3247
3602
|
normalized_args["patch_results"] = patch_results
|
|
3248
3603
|
|
|
3604
|
+
add_data_compile_started_at = time.perf_counter()
|
|
3249
3605
|
compiled_add_data_configs, add_data_issues = self._compile_custom_button_semantic_add_data_configs(
|
|
3250
3606
|
profile=profile,
|
|
3251
3607
|
app_key=app_key,
|
|
3252
3608
|
patches=upsert_buttons,
|
|
3253
3609
|
)
|
|
3610
|
+
add_data_compile_ms += _duration_ms(add_data_compile_started_at)
|
|
3254
3611
|
upsert_ops: list[dict[str, Any]] = []
|
|
3255
3612
|
remove_ops: list[dict[str, Any]] = []
|
|
3256
3613
|
blocking_issues: list[dict[str, Any]] = []
|
|
@@ -3433,12 +3790,14 @@ class AiBuilderFacade:
|
|
|
3433
3790
|
|
|
3434
3791
|
edit_version_no = None
|
|
3435
3792
|
if button_write_intent:
|
|
3793
|
+
edit_context_started_at = time.perf_counter()
|
|
3436
3794
|
edit_version_no, edit_context_error = self._ensure_app_edit_context(
|
|
3437
3795
|
profile=profile,
|
|
3438
3796
|
app_key=app_key,
|
|
3439
3797
|
normalized_args=normalized_args,
|
|
3440
3798
|
failure_code="CUSTOM_BUTTON_APPLY_FAILED",
|
|
3441
3799
|
)
|
|
3800
|
+
edit_context_ms += _duration_ms(edit_context_started_at)
|
|
3442
3801
|
if edit_context_error is not None:
|
|
3443
3802
|
return finalize(edit_context_error)
|
|
3444
3803
|
|
|
@@ -3450,6 +3809,7 @@ class AiBuilderFacade:
|
|
|
3450
3809
|
client_key_map: dict[str, int] = {}
|
|
3451
3810
|
write_executed = False
|
|
3452
3811
|
|
|
3812
|
+
button_write_started_at = time.perf_counter()
|
|
3453
3813
|
for op in upsert_ops:
|
|
3454
3814
|
patch = cast(CustomButtonUpsertPatch, op["patch"])
|
|
3455
3815
|
patch_payload = _serialize_custom_button_payload(patch, add_data_config_override=compiled_add_data_configs.get(int(op["index"])))
|
|
@@ -3460,7 +3820,9 @@ class AiBuilderFacade:
|
|
|
3460
3820
|
button_id = _extract_custom_button_id(result.get("result"))
|
|
3461
3821
|
if button_id is None:
|
|
3462
3822
|
try:
|
|
3823
|
+
button_readback_started_at = time.perf_counter()
|
|
3463
3824
|
readback_buttons = self._load_custom_buttons_for_builder(profile=profile, app_key=app_key)
|
|
3825
|
+
button_readback_ms += _duration_ms(button_readback_started_at)
|
|
3464
3826
|
matches = [
|
|
3465
3827
|
item
|
|
3466
3828
|
for item in readback_buttons
|
|
@@ -3470,6 +3832,7 @@ class AiBuilderFacade:
|
|
|
3470
3832
|
if len(matches) == 1:
|
|
3471
3833
|
button_id = _coerce_positive_int(matches[0].get("button_id"))
|
|
3472
3834
|
except (QingflowApiError, RuntimeError) as error:
|
|
3835
|
+
button_readback_ms += _duration_ms(button_readback_started_at)
|
|
3473
3836
|
readback_errors.append(
|
|
3474
3837
|
{
|
|
3475
3838
|
"resource": "custom_buttons",
|
|
@@ -3525,7 +3888,9 @@ class AiBuilderFacade:
|
|
|
3525
3888
|
try:
|
|
3526
3889
|
write_executed = True
|
|
3527
3890
|
self.buttons.custom_button_delete(profile=profile, app_key=app_key, button_id=button_id)
|
|
3891
|
+
button_readback_started_at = time.perf_counter()
|
|
3528
3892
|
delete_readback = self._verify_custom_button_deleted_by_id(profile=profile, app_key=app_key, button_id=button_id)
|
|
3893
|
+
button_readback_ms += _duration_ms(button_readback_started_at)
|
|
3529
3894
|
removed.append(
|
|
3530
3895
|
{
|
|
3531
3896
|
"index": op["index"],
|
|
@@ -3565,13 +3930,41 @@ class AiBuilderFacade:
|
|
|
3565
3930
|
}
|
|
3566
3931
|
)
|
|
3567
3932
|
|
|
3568
|
-
|
|
3933
|
+
button_write_ms += _duration_ms(button_write_started_at)
|
|
3934
|
+
created_ids = [
|
|
3935
|
+
int(item["button_id"])
|
|
3936
|
+
for item in created
|
|
3937
|
+
if _coerce_positive_int(item.get("button_id")) is not None
|
|
3938
|
+
]
|
|
3939
|
+
updated_ids = [
|
|
3940
|
+
int(item["button_id"])
|
|
3941
|
+
for item in updated
|
|
3942
|
+
if _coerce_positive_int(item.get("button_id")) is not None
|
|
3943
|
+
]
|
|
3944
|
+
removed_ids = [
|
|
3945
|
+
int(item["button_id"])
|
|
3946
|
+
for item in removed
|
|
3947
|
+
if _coerce_positive_int(item.get("button_id")) is not None
|
|
3948
|
+
]
|
|
3949
|
+
same_call_view_binding_verifies_buttons = bool(request.view_configs) and not needs_button_inventory
|
|
3950
|
+
final_readback_skipped = (
|
|
3951
|
+
bool(created or updated)
|
|
3952
|
+
and (not request.view_configs or same_call_view_binding_verifies_buttons)
|
|
3953
|
+
and not removed
|
|
3954
|
+
and not failed
|
|
3955
|
+
and not readback_errors
|
|
3956
|
+
and all(_coerce_positive_int(item.get("button_id")) is not None for item in [*created, *updated])
|
|
3957
|
+
)
|
|
3958
|
+
needs_button_list_readback = bool(created or updated or (request.view_configs and needs_button_inventory)) and not final_readback_skipped
|
|
3569
3959
|
readback_buttons: list[dict[str, Any]] = []
|
|
3570
3960
|
readback_failed = False
|
|
3571
3961
|
if needs_button_list_readback:
|
|
3572
3962
|
try:
|
|
3963
|
+
button_readback_started_at = time.perf_counter()
|
|
3573
3964
|
readback_buttons = self._load_custom_buttons_for_builder(profile=profile, app_key=app_key)
|
|
3965
|
+
button_readback_ms += _duration_ms(button_readback_started_at)
|
|
3574
3966
|
except (QingflowApiError, RuntimeError) as error:
|
|
3967
|
+
button_readback_ms += _duration_ms(button_readback_started_at)
|
|
3575
3968
|
readback_errors.append(
|
|
3576
3969
|
{
|
|
3577
3970
|
"resource": "custom_buttons",
|
|
@@ -3585,26 +3978,14 @@ class AiBuilderFacade:
|
|
|
3585
3978
|
for item in readback_buttons
|
|
3586
3979
|
if (button_id := _coerce_positive_int(item.get("button_id"))) is not None
|
|
3587
3980
|
}
|
|
3588
|
-
created_ids = [
|
|
3589
|
-
int(item["button_id"])
|
|
3590
|
-
for item in created
|
|
3591
|
-
if _coerce_positive_int(item.get("button_id")) is not None
|
|
3592
|
-
]
|
|
3593
|
-
updated_ids = [
|
|
3594
|
-
int(item["button_id"])
|
|
3595
|
-
for item in updated
|
|
3596
|
-
if _coerce_positive_int(item.get("button_id")) is not None
|
|
3597
|
-
]
|
|
3598
|
-
removed_ids = [
|
|
3599
|
-
int(item["button_id"])
|
|
3600
|
-
for item in removed
|
|
3601
|
-
if _coerce_positive_int(item.get("button_id")) is not None
|
|
3602
|
-
]
|
|
3603
3981
|
removed_verified = all(str(item.get("readback_status") or "") == "deleted" for item in removed)
|
|
3604
3982
|
remove_readback_pending = any(str(item.get("readback_status") or "") != "deleted" for item in removed)
|
|
3983
|
+
created_verified = final_readback_skipped or (not readback_failed and all(button_id in readback_ids for button_id in created_ids))
|
|
3984
|
+
updated_verified = final_readback_skipped or (not readback_failed and all(button_id in readback_ids for button_id in updated_ids))
|
|
3605
3985
|
verified = (
|
|
3606
|
-
not readback_failed
|
|
3607
|
-
and
|
|
3986
|
+
(final_readback_skipped or not readback_failed)
|
|
3987
|
+
and created_verified
|
|
3988
|
+
and updated_verified
|
|
3608
3989
|
and removed_verified
|
|
3609
3990
|
and not failed
|
|
3610
3991
|
and all(_coerce_positive_int(item.get("button_id")) is not None for item in created)
|
|
@@ -3616,7 +3997,9 @@ class AiBuilderFacade:
|
|
|
3616
3997
|
view_config_verified = True
|
|
3617
3998
|
view_config_write_executed = False
|
|
3618
3999
|
view_config_write_succeeded = False
|
|
4000
|
+
created_verified_by_view_binding = False
|
|
3619
4001
|
if request.view_configs:
|
|
4002
|
+
view_config_started_at = time.perf_counter()
|
|
3620
4003
|
view_config_result = self._apply_custom_button_view_configs(
|
|
3621
4004
|
profile=profile,
|
|
3622
4005
|
app_key=app_key,
|
|
@@ -3628,6 +4011,7 @@ class AiBuilderFacade:
|
|
|
3628
4011
|
updated_ids=updated_ids,
|
|
3629
4012
|
removed_ids=removed_ids,
|
|
3630
4013
|
)
|
|
4014
|
+
view_config_ms += _duration_ms(view_config_started_at)
|
|
3631
4015
|
view_config_results = list(view_config_result.get("view_configs") or [])
|
|
3632
4016
|
view_config_failed = list(view_config_result.get("failed") or [])
|
|
3633
4017
|
view_config_warnings = list(view_config_result.get("warnings") or [])
|
|
@@ -3640,6 +4024,38 @@ class AiBuilderFacade:
|
|
|
3640
4024
|
if view_config_warnings:
|
|
3641
4025
|
if not isinstance(view_config_warnings, list):
|
|
3642
4026
|
view_config_warnings = []
|
|
4027
|
+
if not created_verified and created_ids and view_config_verified:
|
|
4028
|
+
bound_custom_button_ids = {
|
|
4029
|
+
button_id
|
|
4030
|
+
for view_config in view_config_results
|
|
4031
|
+
if isinstance(view_config, dict)
|
|
4032
|
+
for button in (view_config.get("actual_buttons") or [])
|
|
4033
|
+
if isinstance(button, dict)
|
|
4034
|
+
and str(button.get("button_type") or "").strip().upper() == "CUSTOM"
|
|
4035
|
+
and (button_id := _coerce_positive_int(button.get("button_id"))) is not None
|
|
4036
|
+
}
|
|
4037
|
+
expected_custom_button_ids = {
|
|
4038
|
+
button_id
|
|
4039
|
+
for view_config in view_config_results
|
|
4040
|
+
if isinstance(view_config, dict) and bool(view_config.get("verified_by_write_ack"))
|
|
4041
|
+
for button in (view_config.get("expected_buttons") or [])
|
|
4042
|
+
if isinstance(button, dict)
|
|
4043
|
+
and str(button.get("button_type") or "").strip().upper() == "CUSTOM"
|
|
4044
|
+
and (button_id := _coerce_positive_int(button.get("button_id"))) is not None
|
|
4045
|
+
}
|
|
4046
|
+
created_verified_by_view_binding = all(
|
|
4047
|
+
button_id in bound_custom_button_ids or button_id in expected_custom_button_ids
|
|
4048
|
+
for button_id in created_ids
|
|
4049
|
+
)
|
|
4050
|
+
if created_verified_by_view_binding:
|
|
4051
|
+
created_verified = True
|
|
4052
|
+
custom_buttons_verified = (
|
|
4053
|
+
(final_readback_skipped or not readback_failed)
|
|
4054
|
+
and updated_verified
|
|
4055
|
+
and removed_verified
|
|
4056
|
+
and not failed
|
|
4057
|
+
and all(_coerce_positive_int(item.get("button_id")) is not None for item in created)
|
|
4058
|
+
)
|
|
3643
4059
|
verified = custom_buttons_verified and view_config_verified
|
|
3644
4060
|
write_succeeded = bool(created or updated or removed or view_config_write_succeeded)
|
|
3645
4061
|
status = "success" if verified else ("partial_success" if write_succeeded else "failed")
|
|
@@ -3693,6 +4109,10 @@ class AiBuilderFacade:
|
|
|
3693
4109
|
"edit_version_no": edit_version_no,
|
|
3694
4110
|
"button_ids_by_client_key": client_key_map,
|
|
3695
4111
|
"readback_failed": readback_failed,
|
|
4112
|
+
"initial_inventory_read_skipped": not needs_button_inventory,
|
|
4113
|
+
"final_readback_skipped": final_readback_skipped,
|
|
4114
|
+
"same_call_view_binding_verifies_buttons": same_call_view_binding_verifies_buttons,
|
|
4115
|
+
"created_verified_by_view_binding": created_verified_by_view_binding,
|
|
3696
4116
|
**({"readback_errors": readback_errors} if readback_errors else {}),
|
|
3697
4117
|
"compiled_match_rules": {
|
|
3698
4118
|
str(index): _summarize_compiled_match_rules(config.get("que_relation") or [])
|
|
@@ -3705,9 +4125,10 @@ class AiBuilderFacade:
|
|
|
3705
4125
|
"warnings": warnings,
|
|
3706
4126
|
"verification": {
|
|
3707
4127
|
"custom_buttons_verified": custom_buttons_verified,
|
|
3708
|
-
"readback_loaded": not readback_failed,
|
|
3709
|
-
"
|
|
3710
|
-
"
|
|
4128
|
+
"readback_loaded": False if final_readback_skipped else not readback_failed,
|
|
4129
|
+
"final_readback_skipped": final_readback_skipped,
|
|
4130
|
+
"created_verified": created_verified,
|
|
4131
|
+
"updated_verified": updated_verified,
|
|
3711
4132
|
"removed_verified": removed_verified,
|
|
3712
4133
|
"remove_readback_pending": remove_readback_pending,
|
|
3713
4134
|
"removed_readback_results": deepcopy(removed),
|
|
@@ -3727,14 +4148,17 @@ class AiBuilderFacade:
|
|
|
3727
4148
|
"write_succeeded": write_succeeded,
|
|
3728
4149
|
"safe_to_retry": not write_executed,
|
|
3729
4150
|
}
|
|
4151
|
+
publish_started_at = time.perf_counter()
|
|
4152
|
+
published_response = self._append_publish_result(
|
|
4153
|
+
profile=profile,
|
|
4154
|
+
app_key=app_key,
|
|
4155
|
+
publish=bool(write_succeeded and button_write_intent),
|
|
4156
|
+
response=response,
|
|
4157
|
+
edit_version_no=edit_version_no,
|
|
4158
|
+
)
|
|
4159
|
+
publish_ms += _duration_ms(publish_started_at)
|
|
3730
4160
|
return finalize(
|
|
3731
|
-
|
|
3732
|
-
profile=profile,
|
|
3733
|
-
app_key=app_key,
|
|
3734
|
-
publish=bool(write_succeeded and button_write_intent),
|
|
3735
|
-
response=response,
|
|
3736
|
-
edit_version_no=edit_version_no,
|
|
3737
|
-
)
|
|
4161
|
+
published_response
|
|
3738
4162
|
)
|
|
3739
4163
|
|
|
3740
4164
|
def app_custom_button_list(self, *, profile: str, app_key: str) -> JSONObject:
|
|
@@ -4360,6 +4784,18 @@ class AiBuilderFacade:
|
|
|
4360
4784
|
def app_associated_resources_apply(self, *, profile: str, request: AssociatedResourcesApplyRequest) -> JSONObject:
|
|
4361
4785
|
normalized_args = request.model_dump(mode="json", exclude_none=True)
|
|
4362
4786
|
app_key = request.app_key
|
|
4787
|
+
apply_started_at = time.perf_counter()
|
|
4788
|
+
permission_ms = 0
|
|
4789
|
+
resource_inventory_ms = 0
|
|
4790
|
+
app_name_read_ms = 0
|
|
4791
|
+
resource_patch_hydration_ms = 0
|
|
4792
|
+
match_mapping_compile_ms = 0
|
|
4793
|
+
edit_context_ms = 0
|
|
4794
|
+
resource_write_ms = 0
|
|
4795
|
+
resource_readback_ms = 0
|
|
4796
|
+
view_config_ms = 0
|
|
4797
|
+
publish_ms = 0
|
|
4798
|
+
post_publish_verify_ms = 0
|
|
4363
4799
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
4364
4800
|
resource_write_intent = bool(
|
|
4365
4801
|
request.upsert_resources
|
|
@@ -4368,32 +4804,97 @@ class AiBuilderFacade:
|
|
|
4368
4804
|
or request.reorder_associated_item_ids
|
|
4369
4805
|
)
|
|
4370
4806
|
if resource_write_intent:
|
|
4807
|
+
permission_started_at = time.perf_counter()
|
|
4371
4808
|
permission_outcome = self._guard_app_permission(
|
|
4372
4809
|
profile=profile,
|
|
4373
4810
|
app_key=app_key,
|
|
4374
4811
|
required_permission="edit_app",
|
|
4375
4812
|
normalized_args=normalized_args,
|
|
4376
4813
|
)
|
|
4814
|
+
permission_ms += _duration_ms(permission_started_at)
|
|
4377
4815
|
if permission_outcome.block is not None:
|
|
4816
|
+
_merge_duration_breakdown(
|
|
4817
|
+
permission_outcome.block,
|
|
4818
|
+
permission_ms=permission_ms,
|
|
4819
|
+
total_ms=_duration_ms(apply_started_at),
|
|
4820
|
+
)
|
|
4378
4821
|
return permission_outcome.block
|
|
4379
4822
|
permission_outcomes.append(permission_outcome)
|
|
4380
4823
|
if request.view_configs:
|
|
4824
|
+
permission_started_at = time.perf_counter()
|
|
4381
4825
|
permission_outcome = self._guard_app_permission(
|
|
4382
4826
|
profile=profile,
|
|
4383
4827
|
app_key=app_key,
|
|
4384
4828
|
required_permission="view_manage",
|
|
4385
4829
|
normalized_args=normalized_args,
|
|
4386
4830
|
)
|
|
4831
|
+
permission_ms += _duration_ms(permission_started_at)
|
|
4387
4832
|
if permission_outcome.block is not None:
|
|
4833
|
+
_merge_duration_breakdown(
|
|
4834
|
+
permission_outcome.block,
|
|
4835
|
+
permission_ms=permission_ms,
|
|
4836
|
+
total_ms=_duration_ms(apply_started_at),
|
|
4837
|
+
)
|
|
4388
4838
|
return permission_outcome.block
|
|
4389
4839
|
permission_outcomes.append(permission_outcome)
|
|
4390
4840
|
|
|
4391
4841
|
def finalize(response: JSONObject) -> JSONObject:
|
|
4392
|
-
|
|
4842
|
+
response = _apply_permission_outcomes(response, *permission_outcomes)
|
|
4843
|
+
_merge_duration_breakdown(
|
|
4844
|
+
response,
|
|
4845
|
+
permission_ms=permission_ms,
|
|
4846
|
+
resource_inventory_ms=resource_inventory_ms,
|
|
4847
|
+
app_name_read_ms=app_name_read_ms,
|
|
4848
|
+
resource_patch_hydration_ms=resource_patch_hydration_ms,
|
|
4849
|
+
match_mapping_compile_ms=match_mapping_compile_ms,
|
|
4850
|
+
edit_context_ms=edit_context_ms,
|
|
4851
|
+
resource_write_ms=resource_write_ms,
|
|
4852
|
+
resource_readback_ms=resource_readback_ms,
|
|
4853
|
+
view_config_ms=view_config_ms,
|
|
4854
|
+
publish_ms=publish_ms,
|
|
4855
|
+
post_publish_verify_ms=post_publish_verify_ms,
|
|
4856
|
+
total_ms=_duration_ms(apply_started_at),
|
|
4857
|
+
)
|
|
4858
|
+
return response
|
|
4393
4859
|
|
|
4860
|
+
same_call_resource_client_keys = {
|
|
4861
|
+
str(patch.client_key or "").strip()
|
|
4862
|
+
for patch in request.upsert_resources
|
|
4863
|
+
if str(patch.client_key or "").strip()
|
|
4864
|
+
}
|
|
4865
|
+
all_resource_upserts_are_named_creates = bool(request.upsert_resources) and all(
|
|
4866
|
+
str(patch.client_key or "").strip()
|
|
4867
|
+
and _coerce_positive_int(patch.associated_item_id) is None
|
|
4868
|
+
for patch in request.upsert_resources
|
|
4869
|
+
)
|
|
4870
|
+
view_config_refs_need_inventory = any(
|
|
4871
|
+
str(ref or "").strip() not in same_call_resource_client_keys
|
|
4872
|
+
for config in request.view_configs
|
|
4873
|
+
for ref in config.associated_item_refs
|
|
4874
|
+
if str(ref or "").strip()
|
|
4875
|
+
)
|
|
4876
|
+
view_config_ids_need_inventory = any(
|
|
4877
|
+
bool(config.associated_item_ids)
|
|
4878
|
+
for config in request.view_configs
|
|
4879
|
+
)
|
|
4880
|
+
needs_resource_inventory = (
|
|
4881
|
+
bool(request.patch_resources)
|
|
4882
|
+
or bool(request.remove_associated_item_ids)
|
|
4883
|
+
or bool(request.reorder_associated_item_ids)
|
|
4884
|
+
or any(_coerce_positive_int(patch.associated_item_id) is not None for patch in request.upsert_resources)
|
|
4885
|
+
or (bool(request.upsert_resources) and not all_resource_upserts_are_named_creates)
|
|
4886
|
+
or view_config_refs_need_inventory
|
|
4887
|
+
or view_config_ids_need_inventory
|
|
4888
|
+
)
|
|
4394
4889
|
try:
|
|
4395
|
-
|
|
4890
|
+
resource_inventory_started_at = time.perf_counter()
|
|
4891
|
+
existing_resources = (
|
|
4892
|
+
self._load_associated_resources_for_builder(profile=profile, app_key=app_key, include_raw=True)
|
|
4893
|
+
if needs_resource_inventory
|
|
4894
|
+
else []
|
|
4895
|
+
)
|
|
4396
4896
|
except (QingflowApiError, RuntimeError) as error:
|
|
4897
|
+
resource_inventory_ms += _duration_ms(resource_inventory_started_at)
|
|
4397
4898
|
api_error = _coerce_api_error(error)
|
|
4398
4899
|
return finalize(_failed_from_api_error(
|
|
4399
4900
|
"ASSOCIATED_RESOURCES_READ_FAILED",
|
|
@@ -4402,7 +4903,10 @@ class AiBuilderFacade:
|
|
|
4402
4903
|
details=_with_state_read_blocked_details({"app_key": app_key}, resource="associated_resources", error=api_error),
|
|
4403
4904
|
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
4404
4905
|
))
|
|
4906
|
+
resource_inventory_ms += _duration_ms(resource_inventory_started_at)
|
|
4907
|
+
app_name_started_at = time.perf_counter()
|
|
4405
4908
|
app_name = self._read_app_name_for_builder_output(profile=profile, app_key=app_key)
|
|
4909
|
+
app_name_read_ms += _duration_ms(app_name_started_at)
|
|
4406
4910
|
|
|
4407
4911
|
existing_by_id = _associated_resource_index(existing_resources)
|
|
4408
4912
|
blocking_issues: list[dict[str, Any]] = []
|
|
@@ -4415,10 +4919,12 @@ class AiBuilderFacade:
|
|
|
4415
4919
|
|
|
4416
4920
|
upsert_resources = list(request.upsert_resources)
|
|
4417
4921
|
if request.patch_resources:
|
|
4922
|
+
resource_patch_started_at = time.perf_counter()
|
|
4418
4923
|
expanded_resources, patch_issues, patch_results = self._expand_associated_resource_partial_patches(
|
|
4419
4924
|
existing_by_id=existing_by_id,
|
|
4420
4925
|
patch_resources=request.patch_resources,
|
|
4421
4926
|
)
|
|
4927
|
+
resource_patch_hydration_ms += _duration_ms(resource_patch_started_at)
|
|
4422
4928
|
if patch_issues:
|
|
4423
4929
|
return finalize(
|
|
4424
4930
|
_failed(
|
|
@@ -4433,11 +4939,13 @@ class AiBuilderFacade:
|
|
|
4433
4939
|
normalized_args["upsert_resources"] = [patch.model_dump(mode="json") for patch in upsert_resources]
|
|
4434
4940
|
normalized_args["patch_results"] = patch_results
|
|
4435
4941
|
|
|
4942
|
+
match_mapping_compile_started_at = time.perf_counter()
|
|
4436
4943
|
compiled_resource_match_rules, resource_match_issues = self._compile_associated_resource_semantic_match_mappings(
|
|
4437
4944
|
profile=profile,
|
|
4438
4945
|
app_key=app_key,
|
|
4439
4946
|
patches=upsert_resources,
|
|
4440
4947
|
)
|
|
4948
|
+
match_mapping_compile_ms += _duration_ms(match_mapping_compile_started_at)
|
|
4441
4949
|
normalized_args["compiled_match_rules"] = {
|
|
4442
4950
|
str(index): _summarize_compiled_match_rules(rules)
|
|
4443
4951
|
for index, rules in compiled_resource_match_rules.items()
|
|
@@ -4651,12 +5159,14 @@ class AiBuilderFacade:
|
|
|
4651
5159
|
|
|
4652
5160
|
edit_version_no = None
|
|
4653
5161
|
if resource_write_intent:
|
|
5162
|
+
edit_context_started_at = time.perf_counter()
|
|
4654
5163
|
edit_version_no, edit_context_error = self._ensure_app_edit_context(
|
|
4655
5164
|
profile=profile,
|
|
4656
5165
|
app_key=app_key,
|
|
4657
5166
|
normalized_args=normalized_args,
|
|
4658
5167
|
failure_code="ASSOCIATED_RESOURCES_APPLY_FAILED",
|
|
4659
5168
|
)
|
|
5169
|
+
edit_context_ms += _duration_ms(edit_context_started_at)
|
|
4660
5170
|
if edit_context_error is not None:
|
|
4661
5171
|
return finalize(edit_context_error)
|
|
4662
5172
|
|
|
@@ -4671,6 +5181,7 @@ class AiBuilderFacade:
|
|
|
4671
5181
|
verification_errors: list[JSONObject] = []
|
|
4672
5182
|
write_executed = False
|
|
4673
5183
|
|
|
5184
|
+
resource_write_started_at = time.perf_counter()
|
|
4674
5185
|
for op in upsert_ops:
|
|
4675
5186
|
patch = cast(AssociatedResourceUpsertPatch, op["patch"])
|
|
4676
5187
|
try:
|
|
@@ -4688,26 +5199,30 @@ class AiBuilderFacade:
|
|
|
4688
5199
|
match_rules_override=compiled_resource_match_rules.get(int(op["index"])),
|
|
4689
5200
|
)
|
|
4690
5201
|
created_id = _extract_associated_resource_id_from_result(create_result)
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
5202
|
+
if created_id is None:
|
|
5203
|
+
try:
|
|
5204
|
+
resource_readback_started_at = time.perf_counter()
|
|
5205
|
+
readback_resources = self._load_associated_resources_for_builder(profile=profile, app_key=app_key)
|
|
5206
|
+
resource_readback_ms += _duration_ms(resource_readback_started_at)
|
|
5207
|
+
matches = [
|
|
5208
|
+
item
|
|
5209
|
+
for item in readback_resources
|
|
5210
|
+
if _associated_resource_matches_patch(item, patch)
|
|
5211
|
+
and _coerce_positive_int(item.get("associated_item_id")) is not None
|
|
5212
|
+
]
|
|
5213
|
+
readback_id = _coerce_positive_int(matches[0].get("associated_item_id")) if len(matches) == 1 else None
|
|
5214
|
+
if readback_id is not None:
|
|
5215
|
+
created_id = readback_id
|
|
5216
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
5217
|
+
resource_readback_ms += _duration_ms(resource_readback_started_at)
|
|
5218
|
+
readback_errors.append(
|
|
5219
|
+
{
|
|
5220
|
+
"resource": "associated_resources",
|
|
5221
|
+
"phase": "create_id_lookup",
|
|
5222
|
+
"index": op["index"],
|
|
5223
|
+
"transport_error": _transport_error_payload(_coerce_api_error(error)),
|
|
5224
|
+
}
|
|
5225
|
+
)
|
|
4711
5226
|
created.append(_associated_resource_result_entry("create", op["index"], patch, associated_item_id=created_id))
|
|
4712
5227
|
if created_id is not None and patch.client_key:
|
|
4713
5228
|
client_key_to_id[str(patch.client_key)] = created_id
|
|
@@ -4765,15 +5280,19 @@ class AiBuilderFacade:
|
|
|
4765
5280
|
except (QingflowApiError, RuntimeError) as error:
|
|
4766
5281
|
api_error = _coerce_api_error(error)
|
|
4767
5282
|
failed.append({"operation": "reorder", "associated_item_ids": reorder_ids, "status": "failed", "error_code": "ASSOCIATED_RESOURCE_REORDER_FAILED", "message": api_error.message, "transport_error": _transport_error_payload(api_error)})
|
|
5283
|
+
resource_write_ms += _duration_ms(resource_write_started_at)
|
|
4768
5284
|
|
|
4769
5285
|
resources_after: list[dict[str, Any]] = []
|
|
4770
5286
|
resources_after_loaded = False
|
|
4771
5287
|
resources_after_readback_failed = False
|
|
4772
5288
|
if request.view_configs:
|
|
4773
5289
|
try:
|
|
5290
|
+
resource_readback_started_at = time.perf_counter()
|
|
4774
5291
|
resources_after = self._load_associated_resources_for_builder(profile=profile, app_key=app_key)
|
|
5292
|
+
resource_readback_ms += _duration_ms(resource_readback_started_at)
|
|
4775
5293
|
resources_after_loaded = True
|
|
4776
5294
|
except (QingflowApiError, RuntimeError) as error:
|
|
5295
|
+
resource_readback_ms += _duration_ms(resource_readback_started_at)
|
|
4777
5296
|
readback_errors.append({"resource": "associated_resources", "phase": "pre_view_config", "transport_error": _transport_error_payload(_coerce_api_error(error))})
|
|
4778
5297
|
resources_after = []
|
|
4779
5298
|
resources_after_readback_failed = True
|
|
@@ -4819,6 +5338,7 @@ class AiBuilderFacade:
|
|
|
4819
5338
|
failed.append({"operation": "view_config", "index": index, "view_key": view_config.view_key, "view_name": view_name, "status": "failed", "issues": issues})
|
|
4820
5339
|
continue
|
|
4821
5340
|
try:
|
|
5341
|
+
view_config_started_at = time.perf_counter()
|
|
4822
5342
|
write_executed = True
|
|
4823
5343
|
self._update_view_associated_resources_config(profile=profile, view_key=view_config.view_key, associated_resources_payload=view_payload)
|
|
4824
5344
|
try:
|
|
@@ -4848,16 +5368,35 @@ class AiBuilderFacade:
|
|
|
4848
5368
|
actual_config = {}
|
|
4849
5369
|
verified_config = False
|
|
4850
5370
|
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})
|
|
5371
|
+
view_config_ms += _duration_ms(view_config_started_at)
|
|
4851
5372
|
except (QingflowApiError, RuntimeError) as error:
|
|
5373
|
+
view_config_ms += _duration_ms(view_config_started_at)
|
|
4852
5374
|
api_error = _coerce_api_error(error)
|
|
4853
5375
|
failed.append({"operation": "view_config", "index": index, "view_key": view_config.view_key, "view_name": view_name, "status": "failed", "error_code": "VIEW_ASSOCIATED_RESOURCES_WRITE_FAILED", "message": api_error.message, "transport_error": _transport_error_payload(api_error)})
|
|
4854
5376
|
|
|
5377
|
+
created_ids = [int(item["associated_item_id"]) for item in created if _coerce_positive_int(item.get("associated_item_id")) is not None]
|
|
5378
|
+
updated_ids = [int(item["associated_item_id"]) for item in updated if _coerce_positive_int(item.get("associated_item_id")) is not None]
|
|
5379
|
+
unchanged_ids = [int(item["associated_item_id"]) for item in unchanged if _coerce_positive_int(item.get("associated_item_id")) is not None]
|
|
5380
|
+
final_readback_skipped = (
|
|
5381
|
+
bool(created or updated)
|
|
5382
|
+
and not request.view_configs
|
|
5383
|
+
and not removed
|
|
5384
|
+
and not reordered
|
|
5385
|
+
and not failed
|
|
5386
|
+
and not readback_errors
|
|
5387
|
+
and all(_coerce_positive_int(item.get("associated_item_id")) is not None for item in [*created, *updated])
|
|
5388
|
+
)
|
|
4855
5389
|
final_resources: list[dict[str, Any]] = resources_after if resources_after_loaded else []
|
|
4856
5390
|
readback_failed = resources_after_readback_failed
|
|
4857
|
-
if
|
|
5391
|
+
if final_readback_skipped:
|
|
5392
|
+
final_resources = []
|
|
5393
|
+
elif not resources_after_loaded and not resources_after_readback_failed:
|
|
4858
5394
|
try:
|
|
5395
|
+
resource_readback_started_at = time.perf_counter()
|
|
4859
5396
|
final_resources = self._load_associated_resources_for_builder(profile=profile, app_key=app_key)
|
|
5397
|
+
resource_readback_ms += _duration_ms(resource_readback_started_at)
|
|
4860
5398
|
except (QingflowApiError, RuntimeError) as error:
|
|
5399
|
+
resource_readback_ms += _duration_ms(resource_readback_started_at)
|
|
4861
5400
|
readback_errors.append({"resource": "associated_resources", "phase": "final_pool", "transport_error": _transport_error_payload(_coerce_api_error(error))})
|
|
4862
5401
|
readback_failed = True
|
|
4863
5402
|
final_by_id = _associated_resource_index(final_resources)
|
|
@@ -4867,15 +5406,16 @@ class AiBuilderFacade:
|
|
|
4867
5406
|
resources=final_resources,
|
|
4868
5407
|
readback_failed=readback_failed,
|
|
4869
5408
|
)
|
|
4870
|
-
created_ids = [int(item["associated_item_id"]) for item in created if _coerce_positive_int(item.get("associated_item_id")) is not None]
|
|
4871
|
-
updated_ids = [int(item["associated_item_id"]) for item in updated if _coerce_positive_int(item.get("associated_item_id")) is not None]
|
|
4872
|
-
unchanged_ids = [int(item["associated_item_id"]) for item in unchanged if _coerce_positive_int(item.get("associated_item_id")) is not None]
|
|
4873
5409
|
removed_ids = [int(item["associated_item_id"]) for item in removed if _coerce_positive_int(item.get("associated_item_id")) is not None]
|
|
4874
5410
|
removed_verified = all(str(item.get("readback_status") or "") == "deleted" for item in removed)
|
|
4875
5411
|
remove_readback_pending = any(str(item.get("readback_status") or "") != "deleted" for item in removed)
|
|
4876
|
-
|
|
5412
|
+
pool_items_verified = final_readback_skipped or (
|
|
4877
5413
|
not readback_failed
|
|
4878
5414
|
and all(item_id in final_by_id for item_id in created_ids + updated_ids + unchanged_ids)
|
|
5415
|
+
)
|
|
5416
|
+
pool_verified = (
|
|
5417
|
+
(final_readback_skipped or not readback_failed)
|
|
5418
|
+
and pool_items_verified
|
|
4879
5419
|
and removed_verified
|
|
4880
5420
|
and not failed
|
|
4881
5421
|
and all(_coerce_positive_int(item.get("associated_item_id")) is not None for item in created)
|
|
@@ -4944,6 +5484,8 @@ class AiBuilderFacade:
|
|
|
4944
5484
|
"edit_version_no": edit_version_no,
|
|
4945
5485
|
"associated_item_ids_by_client_key": client_key_to_id,
|
|
4946
5486
|
"readback_failed": readback_failed,
|
|
5487
|
+
"initial_inventory_read_skipped": not needs_resource_inventory,
|
|
5488
|
+
"final_readback_skipped": final_readback_skipped,
|
|
4947
5489
|
**({"readback_errors": readback_errors} if readback_errors else {}),
|
|
4948
5490
|
**({"verification_errors": verification_errors} if verification_errors else {}),
|
|
4949
5491
|
"compiled_match_rules": {
|
|
@@ -4959,7 +5501,8 @@ class AiBuilderFacade:
|
|
|
4959
5501
|
"verification": {
|
|
4960
5502
|
"associated_resources_verified": pool_verified,
|
|
4961
5503
|
"associated_resource_view_configs_verified": view_configs_verified,
|
|
4962
|
-
"readback_loaded": not readback_failed,
|
|
5504
|
+
"readback_loaded": False if final_readback_skipped else not readback_failed,
|
|
5505
|
+
"final_readback_skipped": final_readback_skipped,
|
|
4963
5506
|
"removed_verified": removed_verified,
|
|
4964
5507
|
"remove_readback_pending": remove_readback_pending,
|
|
4965
5508
|
"removed_readback_results": deepcopy(removed),
|
|
@@ -4981,6 +5524,7 @@ class AiBuilderFacade:
|
|
|
4981
5524
|
"safe_to_retry": not write_executed,
|
|
4982
5525
|
"associated_resources": final_resources,
|
|
4983
5526
|
}
|
|
5527
|
+
publish_started_at = time.perf_counter()
|
|
4984
5528
|
response = self._append_publish_result(
|
|
4985
5529
|
profile=profile,
|
|
4986
5530
|
app_key=app_key,
|
|
@@ -4988,7 +5532,9 @@ class AiBuilderFacade:
|
|
|
4988
5532
|
response=response,
|
|
4989
5533
|
edit_version_no=edit_version_no,
|
|
4990
5534
|
)
|
|
5535
|
+
publish_ms += _duration_ms(publish_started_at)
|
|
4991
5536
|
if response.get("published") and view_config_results:
|
|
5537
|
+
post_publish_started_at = time.perf_counter()
|
|
4992
5538
|
try:
|
|
4993
5539
|
post_publish_resources = self._load_associated_resources_for_builder(profile=profile, app_key=app_key)
|
|
4994
5540
|
except (QingflowApiError, RuntimeError) as error:
|
|
@@ -5097,6 +5643,7 @@ class AiBuilderFacade:
|
|
|
5097
5643
|
response["recoverable"] = False
|
|
5098
5644
|
response["message"] = "applied associated resources patch"
|
|
5099
5645
|
response["suggested_next_call"] = None
|
|
5646
|
+
post_publish_verify_ms += _duration_ms(post_publish_started_at)
|
|
5100
5647
|
return finalize(response)
|
|
5101
5648
|
|
|
5102
5649
|
def _resolve_app_matches_in_visible_apps(
|
|
@@ -6153,32 +6700,52 @@ class AiBuilderFacade:
|
|
|
6153
6700
|
idempotency_key: str | None = None,
|
|
6154
6701
|
schema_version: str | None = None,
|
|
6155
6702
|
) -> JSONObject:
|
|
6703
|
+
patch_started_at = time.perf_counter()
|
|
6704
|
+
flow_patch_read_ms = 0
|
|
6705
|
+
flow_patch_compile_ms = 0
|
|
6706
|
+
flow_patch_apply_ms = 0
|
|
6707
|
+
|
|
6708
|
+
def attach_patch_duration(response: JSONObject) -> JSONObject:
|
|
6709
|
+
_merge_duration_breakdown(
|
|
6710
|
+
response,
|
|
6711
|
+
flow_patch_read_ms=flow_patch_read_ms,
|
|
6712
|
+
flow_patch_compile_ms=flow_patch_compile_ms,
|
|
6713
|
+
flow_patch_apply_ms=flow_patch_apply_ms,
|
|
6714
|
+
total_ms=_duration_ms(patch_started_at),
|
|
6715
|
+
)
|
|
6716
|
+
return response
|
|
6717
|
+
|
|
6156
6718
|
try:
|
|
6719
|
+
flow_patch_read_started_at = time.perf_counter()
|
|
6157
6720
|
current = self.flow_get(profile=profile, app_key=app_key)
|
|
6158
6721
|
except (QingflowApiError, RuntimeError) as error:
|
|
6722
|
+
flow_patch_read_ms += _duration_ms(flow_patch_read_started_at)
|
|
6159
6723
|
api_error = _coerce_api_error(error)
|
|
6160
|
-
return _failed_from_api_error(
|
|
6724
|
+
return attach_patch_duration(_failed_from_api_error(
|
|
6161
6725
|
"FLOW_READ_FAILED",
|
|
6162
6726
|
api_error,
|
|
6163
6727
|
normalized_args={"app_key": app_key},
|
|
6164
6728
|
details={"app_key": app_key},
|
|
6165
6729
|
suggested_next_call={"tool_name": "app_flow_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
6166
|
-
)
|
|
6730
|
+
))
|
|
6731
|
+
flow_patch_read_ms += _duration_ms(flow_patch_read_started_at)
|
|
6167
6732
|
if str(current.get("status") or "").strip().lower() != "success":
|
|
6168
|
-
return _failed(
|
|
6733
|
+
return attach_patch_duration(_failed(
|
|
6169
6734
|
"FLOW_READ_FAILED",
|
|
6170
6735
|
current.get("message") or "failed to read current flow",
|
|
6171
6736
|
normalized_args={"app_key": app_key},
|
|
6172
6737
|
suggested_next_call={"tool_name": "app_flow_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
6173
|
-
)
|
|
6738
|
+
))
|
|
6739
|
+
flow_patch_compile_started_at = time.perf_counter()
|
|
6174
6740
|
spec = deepcopy(current.get("spec") or {})
|
|
6175
6741
|
if not isinstance(spec, dict) or not spec:
|
|
6176
|
-
|
|
6742
|
+
flow_patch_compile_ms += _duration_ms(flow_patch_compile_started_at)
|
|
6743
|
+
return attach_patch_duration(_failed(
|
|
6177
6744
|
"FLOW_PATCH_FAILED",
|
|
6178
6745
|
"current flow has no spec; use full app_flow_apply with spec first",
|
|
6179
6746
|
normalized_args={"app_key": app_key, "patch_nodes": patch_nodes},
|
|
6180
6747
|
suggested_next_call={"tool_name": "app_flow_apply", "arguments": {"profile": profile, "app_key": app_key}},
|
|
6181
|
-
)
|
|
6748
|
+
))
|
|
6182
6749
|
nodes = spec.get("nodes") if isinstance(spec.get("nodes"), list) else []
|
|
6183
6750
|
node_by_id: dict[str, dict[str, Any]] = {}
|
|
6184
6751
|
for node in nodes:
|
|
@@ -6205,30 +6772,35 @@ class AiBuilderFacade:
|
|
|
6205
6772
|
for key in unset_payload:
|
|
6206
6773
|
target.pop(str(key), None)
|
|
6207
6774
|
if invalid_items:
|
|
6208
|
-
|
|
6775
|
+
flow_patch_compile_ms += _duration_ms(flow_patch_compile_started_at)
|
|
6776
|
+
return attach_patch_duration(_failed(
|
|
6209
6777
|
"FLOW_PATCH_INVALID",
|
|
6210
6778
|
"patch_nodes[] contains invalid items",
|
|
6211
6779
|
normalized_args={"app_key": app_key, "invalid_items": invalid_items},
|
|
6212
6780
|
suggested_next_call={"tool_name": "app_flow_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
6213
|
-
)
|
|
6781
|
+
))
|
|
6214
6782
|
if not_found:
|
|
6215
|
-
|
|
6783
|
+
flow_patch_compile_ms += _duration_ms(flow_patch_compile_started_at)
|
|
6784
|
+
return attach_patch_duration(_failed(
|
|
6216
6785
|
"FLOW_NODE_NOT_FOUND",
|
|
6217
6786
|
f"patch_nodes[] referenced node id(s) not found in current flow: {not_found}",
|
|
6218
6787
|
normalized_args={"app_key": app_key, "not_found_ids": not_found},
|
|
6219
6788
|
suggested_next_call={"tool_name": "app_flow_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
6220
|
-
)
|
|
6789
|
+
))
|
|
6221
6790
|
edge_container = spec.get("edges") if isinstance(spec.get("edges"), dict) else {}
|
|
6222
6791
|
edges = edge_container.get("edges") if isinstance(edge_container, dict) else None
|
|
6223
6792
|
transitions = spec.get("transitions") if isinstance(spec.get("transitions"), list) else None
|
|
6224
6793
|
if not edges and not transitions:
|
|
6225
|
-
|
|
6794
|
+
flow_patch_compile_ms += _duration_ms(flow_patch_compile_started_at)
|
|
6795
|
+
return attach_patch_duration(_failed(
|
|
6226
6796
|
"FLOW_PATCH_EMPTY_GRAPH_UNSUPPORTED",
|
|
6227
6797
|
"current flow spec has no edges; patch_nodes cannot safely save this backend shape",
|
|
6228
6798
|
normalized_args={"app_key": app_key, "patch_nodes": patch_nodes},
|
|
6229
6799
|
suggested_next_call={"tool_name": "app_flow_apply", "arguments": {"profile": profile, "app_key": app_key}},
|
|
6230
|
-
)
|
|
6231
|
-
|
|
6800
|
+
))
|
|
6801
|
+
flow_patch_compile_ms += _duration_ms(flow_patch_compile_started_at)
|
|
6802
|
+
flow_patch_apply_started_at = time.perf_counter()
|
|
6803
|
+
result = self.flow_apply(
|
|
6232
6804
|
profile=profile,
|
|
6233
6805
|
app_key=app_key,
|
|
6234
6806
|
spec=spec,
|
|
@@ -6236,6 +6808,8 @@ class AiBuilderFacade:
|
|
|
6236
6808
|
idempotency_key=idempotency_key,
|
|
6237
6809
|
schema_version=schema_version,
|
|
6238
6810
|
)
|
|
6811
|
+
flow_patch_apply_ms += _duration_ms(flow_patch_apply_started_at)
|
|
6812
|
+
return attach_patch_duration(result)
|
|
6239
6813
|
|
|
6240
6814
|
def _components_to_sections_payload(self, components: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
6241
6815
|
result: list[dict[str, Any]] = []
|
|
@@ -7474,19 +8048,35 @@ class AiBuilderFacade:
|
|
|
7474
8048
|
all_verified = False
|
|
7475
8049
|
continue
|
|
7476
8050
|
try:
|
|
7477
|
-
verify_response = self.views.view_get_config(profile=profile, viewgraph_key=view_key)
|
|
7478
|
-
verify_config = verify_response.get("result") if isinstance(verify_response.get("result"), dict) else {}
|
|
7479
8051
|
expected_summary = _normalize_expected_view_buttons_for_compare(
|
|
7480
8052
|
effective_merged_dtos,
|
|
7481
8053
|
custom_button_details_by_id=button_inventory,
|
|
7482
8054
|
)
|
|
7483
|
-
actual_summary =
|
|
7484
|
-
comparison =
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
8055
|
+
actual_summary: list[dict[str, Any]] = []
|
|
8056
|
+
comparison: dict[str, Any] = {"verified": False, "custom_button_readback_pending": False}
|
|
8057
|
+
readback_retry_count = 0
|
|
8058
|
+
while True:
|
|
8059
|
+
verify_response = self.views.view_get_config(profile=profile, viewgraph_key=view_key)
|
|
8060
|
+
verify_config = verify_response.get("result") if isinstance(verify_response.get("result"), dict) else {}
|
|
8061
|
+
actual_summary = _normalize_view_buttons_for_compare(verify_config)
|
|
8062
|
+
comparison = _compare_view_button_summaries(
|
|
8063
|
+
expected=expected_summary,
|
|
8064
|
+
actual=actual_summary,
|
|
8065
|
+
pending_custom_button_ids=set(created_ids),
|
|
8066
|
+
)
|
|
8067
|
+
if not comparison.get("custom_button_readback_pending") or readback_retry_count >= 1:
|
|
8068
|
+
break
|
|
8069
|
+
readback_retry_count += 1
|
|
8070
|
+
time.sleep(CUSTOM_BUTTON_VIEW_CONFIG_READBACK_RETRY_SECONDS)
|
|
8071
|
+
pending_custom_button_readback = bool(comparison.get("custom_button_readback_pending"))
|
|
8072
|
+
accepted_with_delayed_custom_button_readback = (
|
|
8073
|
+
pending_custom_button_readback
|
|
8074
|
+
and write_succeeded
|
|
8075
|
+
and unsupported_list_issue is None
|
|
7488
8076
|
)
|
|
7489
|
-
verified =
|
|
8077
|
+
verified = (
|
|
8078
|
+
bool(comparison.get("verified")) or accepted_with_delayed_custom_button_readback
|
|
8079
|
+
) and unsupported_list_issue is None
|
|
7490
8080
|
if not verified:
|
|
7491
8081
|
all_verified = False
|
|
7492
8082
|
results.append(
|
|
@@ -7501,12 +8091,14 @@ class AiBuilderFacade:
|
|
|
7501
8091
|
"view_buttons_verified": verified,
|
|
7502
8092
|
"supported_buttons_verified": bool(comparison.get("verified")) if unsupported_list_issue is not None else None,
|
|
7503
8093
|
"unsupported_placements": ["list"] if unsupported_list_issue is not None else [],
|
|
7504
|
-
"custom_button_readback_pending":
|
|
8094
|
+
"custom_button_readback_pending": pending_custom_button_readback,
|
|
8095
|
+
"verified_by_write_ack": accepted_with_delayed_custom_button_readback,
|
|
8096
|
+
"readback_retry_count": readback_retry_count,
|
|
7505
8097
|
"expected_buttons": expected_summary,
|
|
7506
8098
|
"actual_buttons": actual_summary,
|
|
7507
8099
|
}
|
|
7508
8100
|
)
|
|
7509
|
-
if
|
|
8101
|
+
if pending_custom_button_readback and not accepted_with_delayed_custom_button_readback:
|
|
7510
8102
|
warnings.append(_warning("VIEW_CUSTOM_BUTTON_READBACK_PENDING", "view config write landed but custom button readback is delayed"))
|
|
7511
8103
|
except (QingflowApiError, RuntimeError) as error:
|
|
7512
8104
|
api_error = _coerce_api_error(error)
|
|
@@ -7959,7 +8551,6 @@ class AiBuilderFacade:
|
|
|
7959
8551
|
app_key=request.app_key,
|
|
7960
8552
|
app_name=request.app_name,
|
|
7961
8553
|
package_tag_id=request.package_tag_id,
|
|
7962
|
-
create_if_missing=request.create_if_missing,
|
|
7963
8554
|
)
|
|
7964
8555
|
if target.get("status") == "failed":
|
|
7965
8556
|
target.setdefault("normalized_args", normalized_args)
|
|
@@ -8231,7 +8822,6 @@ class AiBuilderFacade:
|
|
|
8231
8822
|
"arguments": {
|
|
8232
8823
|
"profile": profile,
|
|
8233
8824
|
"app_key": request.app_key,
|
|
8234
|
-
"create_if_missing": False,
|
|
8235
8825
|
"add_fields": [{"name": "状态", "type": "select", "options": ["草稿", "进行中", "已完成"], "required": True}],
|
|
8236
8826
|
"update_fields": [],
|
|
8237
8827
|
"remove_fields": [],
|
|
@@ -8581,7 +9171,6 @@ class AiBuilderFacade:
|
|
|
8581
9171
|
icon: str | None = None,
|
|
8582
9172
|
color: str | None = None,
|
|
8583
9173
|
visibility: VisibilityPatch | None = None,
|
|
8584
|
-
create_if_missing: bool = False,
|
|
8585
9174
|
publish: bool = True,
|
|
8586
9175
|
add_fields: list[FieldPatch],
|
|
8587
9176
|
update_fields: list[FieldUpdatePatch],
|
|
@@ -8604,7 +9193,6 @@ class AiBuilderFacade:
|
|
|
8604
9193
|
"icon": icon,
|
|
8605
9194
|
"color": color,
|
|
8606
9195
|
"visibility": visibility.model_dump(mode="json") if visibility is not None else None,
|
|
8607
|
-
"create_if_missing": create_if_missing,
|
|
8608
9196
|
"publish": publish,
|
|
8609
9197
|
"add_fields": [patch.model_dump(mode="json") for patch in add_fields],
|
|
8610
9198
|
"update_fields": [patch.model_dump(mode="json") for patch in update_fields],
|
|
@@ -8629,25 +9217,30 @@ class AiBuilderFacade:
|
|
|
8629
9217
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
8630
9218
|
requested_inline_layout = bool(requested_inline_layout_sections)
|
|
8631
9219
|
requested_field_changes = bool(add_fields or update_fields or remove_fields or requested_inline_layout)
|
|
8632
|
-
resolved: JSONObject
|
|
8633
|
-
if app_key:
|
|
8634
|
-
resolved = self.app_resolve(profile=profile, app_key=app_key)
|
|
8635
|
-
elif app_name:
|
|
8636
|
-
resolved = self.app_resolve(profile=profile, app_name=app_name, package_tag_id=package_tag_id)
|
|
8637
|
-
else:
|
|
8638
|
-
return _failed("APP_NAME_REQUIRED", "app_name or app_key is required", normalized_args=normalized_args, suggested_next_call=None)
|
|
8639
9220
|
|
|
8640
9221
|
def finalize(response: JSONObject) -> JSONObject:
|
|
8641
9222
|
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
8642
9223
|
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
|
|
8647
|
-
|
|
9224
|
+
resolved: JSONObject
|
|
9225
|
+
if app_key:
|
|
9226
|
+
resolved = self.app_resolve(profile=profile, app_key=app_key)
|
|
9227
|
+
else:
|
|
9228
|
+
requested_app_name_for_create = str(app_name or "").strip()
|
|
8648
9229
|
permission_tag_id = _coerce_positive_int(package_tag_id)
|
|
9230
|
+
if not requested_app_name_for_create:
|
|
9231
|
+
return _failed(
|
|
9232
|
+
"APP_NAME_REQUIRED",
|
|
9233
|
+
"app_name is required when creating an app without app_key",
|
|
9234
|
+
normalized_args=normalized_args,
|
|
9235
|
+
suggested_next_call=None,
|
|
9236
|
+
)
|
|
8649
9237
|
if permission_tag_id is None:
|
|
8650
|
-
|
|
9238
|
+
return _failed(
|
|
9239
|
+
"PACKAGE_ID_REQUIRED",
|
|
9240
|
+
"package_id is required when creating an app without app_key",
|
|
9241
|
+
normalized_args=normalized_args,
|
|
9242
|
+
suggested_next_call=None,
|
|
9243
|
+
)
|
|
8651
9244
|
add_permission_outcome = self._guard_package_permission(
|
|
8652
9245
|
profile=profile,
|
|
8653
9246
|
tag_id=permission_tag_id,
|
|
@@ -8659,8 +9252,8 @@ class AiBuilderFacade:
|
|
|
8659
9252
|
permission_outcomes.append(add_permission_outcome)
|
|
8660
9253
|
resolved = self._create_target_app_shell(
|
|
8661
9254
|
profile=profile,
|
|
8662
|
-
app_name=
|
|
8663
|
-
package_tag_id=
|
|
9255
|
+
app_name=requested_app_name_for_create,
|
|
9256
|
+
package_tag_id=permission_tag_id,
|
|
8664
9257
|
icon=icon,
|
|
8665
9258
|
color=color,
|
|
8666
9259
|
auth=desired_auth,
|
|
@@ -8673,6 +9266,11 @@ class AiBuilderFacade:
|
|
|
8673
9266
|
if not isinstance(resolved.get("normalized_args"), dict) or not resolved.get("normalized_args"):
|
|
8674
9267
|
resolved["normalized_args"] = normalized_args
|
|
8675
9268
|
return finalize(resolved)
|
|
9269
|
+
|
|
9270
|
+
if resolved.get("status") == "failed":
|
|
9271
|
+
if not isinstance(resolved.get("normalized_args"), dict) or not resolved.get("normalized_args"):
|
|
9272
|
+
resolved["normalized_args"] = normalized_args
|
|
9273
|
+
return finalize(resolved)
|
|
8676
9274
|
resolved_outcome = _permission_outcome_from_result(resolved)
|
|
8677
9275
|
if resolved_outcome is not None:
|
|
8678
9276
|
permission_outcomes.append(resolved_outcome)
|
|
@@ -9611,71 +10209,113 @@ class AiBuilderFacade:
|
|
|
9611
10209
|
"sections": requested_sections,
|
|
9612
10210
|
"publish": publish,
|
|
9613
10211
|
}
|
|
10212
|
+
apply_started_at = time.perf_counter()
|
|
10213
|
+
permission_ms = 0
|
|
10214
|
+
schema_read_ms = 0
|
|
10215
|
+
layout_compile_ms = 0
|
|
10216
|
+
edit_version_ms = 0
|
|
10217
|
+
layout_write_ms = 0
|
|
10218
|
+
layout_readback_ms = 0
|
|
10219
|
+
publish_ms = 0
|
|
9614
10220
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
10221
|
+
def finalize(response: JSONObject) -> JSONObject:
|
|
10222
|
+
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
10223
|
+
|
|
10224
|
+
def attach_duration(response: JSONObject) -> JSONObject:
|
|
10225
|
+
_merge_duration_breakdown(
|
|
10226
|
+
response,
|
|
10227
|
+
permission_ms=permission_ms,
|
|
10228
|
+
schema_read_ms=schema_read_ms,
|
|
10229
|
+
layout_compile_ms=layout_compile_ms,
|
|
10230
|
+
edit_version_ms=edit_version_ms,
|
|
10231
|
+
layout_write_ms=layout_write_ms,
|
|
10232
|
+
layout_readback_ms=layout_readback_ms,
|
|
10233
|
+
publish_ms=publish_ms,
|
|
10234
|
+
total_ms=_duration_ms(apply_started_at),
|
|
10235
|
+
)
|
|
10236
|
+
return response
|
|
10237
|
+
|
|
10238
|
+
def finish(response: JSONObject) -> JSONObject:
|
|
10239
|
+
return finalize(attach_duration(response))
|
|
10240
|
+
|
|
10241
|
+
def finish_with_publish(response: JSONObject) -> JSONObject:
|
|
10242
|
+
nonlocal publish_ms
|
|
10243
|
+
publish_started_at = time.perf_counter()
|
|
10244
|
+
published_response = self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
10245
|
+
publish_ms += _duration_ms(publish_started_at)
|
|
10246
|
+
return finish(published_response)
|
|
10247
|
+
|
|
10248
|
+
permission_started_at = time.perf_counter()
|
|
9615
10249
|
permission_outcome = self._guard_app_permission(
|
|
9616
10250
|
profile=profile,
|
|
9617
10251
|
app_key=app_key,
|
|
9618
10252
|
required_permission="edit_app",
|
|
9619
10253
|
normalized_args=normalized_args,
|
|
9620
10254
|
)
|
|
10255
|
+
permission_ms += _duration_ms(permission_started_at)
|
|
9621
10256
|
if permission_outcome.block is not None:
|
|
9622
|
-
return permission_outcome.block
|
|
10257
|
+
return finish(permission_outcome.block)
|
|
9623
10258
|
permission_outcomes.append(permission_outcome)
|
|
9624
10259
|
|
|
9625
|
-
def finalize(response: JSONObject) -> JSONObject:
|
|
9626
|
-
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
9627
|
-
|
|
9628
10260
|
try:
|
|
10261
|
+
schema_read_started_at = time.perf_counter()
|
|
9629
10262
|
schema_result, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
9630
10263
|
except (QingflowApiError, RuntimeError) as error:
|
|
10264
|
+
schema_read_ms += _duration_ms(schema_read_started_at)
|
|
9631
10265
|
api_error = _coerce_api_error(error)
|
|
9632
|
-
return
|
|
10266
|
+
return finish(_failed_from_api_error(
|
|
9633
10267
|
"LAYOUT_READ_FAILED",
|
|
9634
10268
|
api_error,
|
|
9635
10269
|
normalized_args=normalized_args,
|
|
9636
10270
|
details=_with_state_read_blocked_details({"app_key": app_key}, resource="schema", error=api_error),
|
|
9637
10271
|
suggested_next_call={"tool_name": "app_get_layout", "arguments": {"profile": profile, "app_key": app_key}},
|
|
9638
10272
|
))
|
|
10273
|
+
schema_read_ms += _duration_ms(schema_read_started_at)
|
|
10274
|
+
layout_compile_started_at = time.perf_counter()
|
|
9639
10275
|
app_name = str(schema_result.get("formTitle") or schema_result.get("title") or schema_result.get("appName") or app_key).strip() or app_key
|
|
9640
10276
|
parsed = _parse_schema(schema_result)
|
|
9641
10277
|
current_fields = parsed["fields"]
|
|
9642
10278
|
requested_sections, missing_selectors = _resolve_layout_sections_to_names(requested_sections, current_fields)
|
|
9643
10279
|
normalized_args["sections"] = requested_sections
|
|
9644
10280
|
if missing_selectors:
|
|
9645
|
-
|
|
10281
|
+
layout_compile_ms += _duration_ms(layout_compile_started_at)
|
|
10282
|
+
return finish(_failed(
|
|
9646
10283
|
"UNKNOWN_LAYOUT_FIELD",
|
|
9647
10284
|
"layout references unknown field selectors",
|
|
9648
10285
|
normalized_args=normalized_args,
|
|
9649
10286
|
details={"unknown_selectors": missing_selectors},
|
|
9650
10287
|
missing_fields=[str(item) for item in missing_selectors],
|
|
9651
10288
|
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
9652
|
-
)
|
|
10289
|
+
))
|
|
9653
10290
|
fields_by_name = {field["name"]: field for field in current_fields}
|
|
9654
10291
|
seen: list[str] = []
|
|
9655
10292
|
for section in requested_sections:
|
|
9656
10293
|
for row in section.get("rows", []):
|
|
9657
10294
|
for field_name in row:
|
|
9658
10295
|
if field_name not in fields_by_name:
|
|
9659
|
-
|
|
10296
|
+
layout_compile_ms += _duration_ms(layout_compile_started_at)
|
|
10297
|
+
return finish(_failed(
|
|
9660
10298
|
"UNKNOWN_LAYOUT_FIELD",
|
|
9661
10299
|
f"layout references unknown field '{field_name}'",
|
|
9662
10300
|
normalized_args=normalized_args,
|
|
9663
10301
|
details={"field_name": field_name},
|
|
9664
10302
|
suggested_next_call={"tool_name": "app_get_layout", "arguments": {"profile": profile, "app_key": app_key}},
|
|
9665
|
-
)
|
|
10303
|
+
))
|
|
9666
10304
|
if field_name in seen:
|
|
9667
|
-
|
|
10305
|
+
layout_compile_ms += _duration_ms(layout_compile_started_at)
|
|
10306
|
+
return finish(_failed(
|
|
9668
10307
|
"DUPLICATE_LAYOUT_FIELD",
|
|
9669
10308
|
f"layout references field '{field_name}' more than once",
|
|
9670
10309
|
normalized_args=normalized_args,
|
|
9671
10310
|
details={"field_name": field_name},
|
|
9672
10311
|
suggested_next_call={"tool_name": "app_layout_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
9673
|
-
)
|
|
10312
|
+
))
|
|
9674
10313
|
seen.append(field_name)
|
|
9675
10314
|
expected = {field["name"] for field in current_fields}
|
|
9676
10315
|
if mode == LayoutApplyMode.replace and set(seen) != expected:
|
|
9677
10316
|
missing = sorted(expected.difference(seen))
|
|
9678
|
-
|
|
10317
|
+
layout_compile_ms += _duration_ms(layout_compile_started_at)
|
|
10318
|
+
return finish(_failed(
|
|
9679
10319
|
"INCOMPLETE_LAYOUT",
|
|
9680
10320
|
"layout must reference every current field exactly once in replace mode",
|
|
9681
10321
|
normalized_args=normalized_args,
|
|
@@ -9692,7 +10332,7 @@ class AiBuilderFacade:
|
|
|
9692
10332
|
"tool_name": "app_layout_apply",
|
|
9693
10333
|
"arguments": {"profile": profile, "app_key": app_key, "mode": "merge", "sections": requested_sections},
|
|
9694
10334
|
},
|
|
9695
|
-
)
|
|
10335
|
+
))
|
|
9696
10336
|
merged = _merge_layout(
|
|
9697
10337
|
current_layout=parsed["layout"],
|
|
9698
10338
|
requested_sections=requested_sections,
|
|
@@ -9704,6 +10344,7 @@ class AiBuilderFacade:
|
|
|
9704
10344
|
else merged["layout"]
|
|
9705
10345
|
)
|
|
9706
10346
|
if _layouts_equal(parsed["layout"], target_layout):
|
|
10347
|
+
layout_compile_ms += _duration_ms(layout_compile_started_at)
|
|
9707
10348
|
response = {
|
|
9708
10349
|
"status": "success",
|
|
9709
10350
|
"error_code": None,
|
|
@@ -9732,21 +10373,26 @@ class AiBuilderFacade:
|
|
|
9732
10373
|
"write_succeeded": False,
|
|
9733
10374
|
"safe_to_retry": True,
|
|
9734
10375
|
}
|
|
9735
|
-
return
|
|
10376
|
+
return finish_with_publish(response)
|
|
9736
10377
|
payload = _build_form_payload_from_existing_schema(
|
|
9737
10378
|
current_schema=schema_result,
|
|
9738
10379
|
layout=target_layout,
|
|
9739
10380
|
)
|
|
10381
|
+
layout_compile_ms += _duration_ms(layout_compile_started_at)
|
|
10382
|
+
edit_version_started_at = time.perf_counter()
|
|
9740
10383
|
payload["editVersionNo"] = self._resolve_form_edit_version(
|
|
9741
10384
|
profile=profile,
|
|
9742
10385
|
app_key=app_key,
|
|
9743
10386
|
current_schema=schema_result,
|
|
9744
10387
|
)
|
|
10388
|
+
edit_version_ms += _duration_ms(edit_version_started_at)
|
|
9745
10389
|
applied_layout = target_layout
|
|
9746
10390
|
fallback_applied = None
|
|
9747
10391
|
try:
|
|
10392
|
+
layout_write_started_at = time.perf_counter()
|
|
9748
10393
|
self.apps.app_update_form_schema(profile=profile, app_key=app_key, payload=payload)
|
|
9749
10394
|
except (QingflowApiError, RuntimeError) as error:
|
|
10395
|
+
layout_write_ms += _duration_ms(layout_write_started_at)
|
|
9750
10396
|
api_error = _coerce_api_error(error)
|
|
9751
10397
|
if backend_code_int(api_error) == 400 and target_layout.get("sections"):
|
|
9752
10398
|
flattened_layout = _flatten_layout_sections(target_layout)
|
|
@@ -9760,12 +10406,15 @@ class AiBuilderFacade:
|
|
|
9760
10406
|
current_schema=schema_result,
|
|
9761
10407
|
)
|
|
9762
10408
|
try:
|
|
10409
|
+
fallback_write_started_at = time.perf_counter()
|
|
9763
10410
|
self.apps.app_update_form_schema(profile=profile, app_key=app_key, payload=fallback_payload)
|
|
10411
|
+
layout_write_ms += _duration_ms(fallback_write_started_at)
|
|
9764
10412
|
applied_layout = flattened_layout
|
|
9765
10413
|
fallback_applied = "flatten_sections"
|
|
9766
10414
|
except (QingflowApiError, RuntimeError) as fallback_error:
|
|
10415
|
+
layout_write_ms += _duration_ms(fallback_write_started_at)
|
|
9767
10416
|
api_fallback_error = _coerce_api_error(fallback_error)
|
|
9768
|
-
return
|
|
10417
|
+
return finish(_failed_from_api_error(
|
|
9769
10418
|
"LAYOUT_APPLY_FAILED",
|
|
9770
10419
|
api_fallback_error,
|
|
9771
10420
|
normalized_args=normalized_args,
|
|
@@ -9780,7 +10429,7 @@ class AiBuilderFacade:
|
|
|
9780
10429
|
suggested_next_call={"tool_name": "app_layout_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
9781
10430
|
))
|
|
9782
10431
|
else:
|
|
9783
|
-
return
|
|
10432
|
+
return finish(_failed_from_api_error(
|
|
9784
10433
|
"LAYOUT_APPLY_FAILED",
|
|
9785
10434
|
api_error,
|
|
9786
10435
|
normalized_args=normalized_args,
|
|
@@ -9792,9 +10441,13 @@ class AiBuilderFacade:
|
|
|
9792
10441
|
},
|
|
9793
10442
|
suggested_next_call={"tool_name": "app_layout_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
9794
10443
|
))
|
|
10444
|
+
else:
|
|
10445
|
+
layout_write_ms += _duration_ms(layout_write_started_at)
|
|
9795
10446
|
try:
|
|
10447
|
+
layout_readback_started_at = time.perf_counter()
|
|
9796
10448
|
verified_schema, _verified_schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
9797
10449
|
except (QingflowApiError, RuntimeError) as error:
|
|
10450
|
+
layout_readback_ms += _duration_ms(layout_readback_started_at)
|
|
9798
10451
|
api_error = _coerce_api_error(error)
|
|
9799
10452
|
response = {
|
|
9800
10453
|
"status": "partial_success",
|
|
@@ -9834,7 +10487,8 @@ class AiBuilderFacade:
|
|
|
9834
10487
|
"write_succeeded": True,
|
|
9835
10488
|
"safe_to_retry": False,
|
|
9836
10489
|
}
|
|
9837
|
-
return
|
|
10490
|
+
return finish_with_publish(response)
|
|
10491
|
+
layout_readback_ms += _duration_ms(layout_readback_started_at)
|
|
9838
10492
|
verified_layout = _parse_schema(verified_schema)["layout"]
|
|
9839
10493
|
layout_verified = _layouts_equal(verified_layout, applied_layout) or _layouts_semantically_equal(verified_layout, applied_layout)
|
|
9840
10494
|
raw_layout_has_content = _schema_has_layout_content(verified_schema)
|
|
@@ -9848,24 +10502,21 @@ class AiBuilderFacade:
|
|
|
9848
10502
|
warnings.append(_warning("LAYOUT_SUMMARY_UNVERIFIED", "layout summary is incomplete relative to raw schema readback"))
|
|
9849
10503
|
if fallback_applied is not None and layout_verified and layout_summary_verified:
|
|
9850
10504
|
warnings.append(_warning("LAYOUT_FALLBACK_APPLIED", "layout readback normalized sectioned layout into flat layout while preserving field placement"))
|
|
10505
|
+
result_verified = layout_verified
|
|
9851
10506
|
response = {
|
|
9852
|
-
"status": "success" if
|
|
10507
|
+
"status": "success" if result_verified else "partial_success",
|
|
9853
10508
|
"error_code": (
|
|
9854
10509
|
None
|
|
9855
|
-
if
|
|
9856
|
-
else "LAYOUT_SUMMARY_UNVERIFIED"
|
|
9857
|
-
if not layout_summary_verified
|
|
10510
|
+
if result_verified
|
|
9858
10511
|
else "LAYOUT_READBACK_MISMATCH"
|
|
9859
10512
|
),
|
|
9860
|
-
"recoverable": not
|
|
10513
|
+
"recoverable": not result_verified,
|
|
9861
10514
|
"message": (
|
|
9862
10515
|
"applied app layout with flattened section fallback"
|
|
9863
10516
|
if layout_verified and layout_summary_verified and fallback_applied is not None
|
|
9864
10517
|
else
|
|
9865
10518
|
"applied app layout"
|
|
9866
|
-
if layout_verified
|
|
9867
|
-
else "applied app layout; raw layout verified but compact summary is incomplete"
|
|
9868
|
-
if layout_verified and not layout_summary_verified
|
|
10519
|
+
if layout_verified
|
|
9869
10520
|
else "applied app layout with flattened section fallback"
|
|
9870
10521
|
if fallback_applied is not None
|
|
9871
10522
|
else "applied app layout; readback did not fully match the requested layout"
|
|
@@ -9888,12 +10539,12 @@ class AiBuilderFacade:
|
|
|
9888
10539
|
"auto_added_fields": merged["auto_added_fields"] if mode == LayoutApplyMode.merge else [],
|
|
9889
10540
|
"fallback_applied": fallback_applied,
|
|
9890
10541
|
},
|
|
9891
|
-
"verified":
|
|
10542
|
+
"verified": result_verified,
|
|
9892
10543
|
"write_executed": True,
|
|
9893
10544
|
"write_succeeded": True,
|
|
9894
10545
|
"safe_to_retry": False,
|
|
9895
10546
|
}
|
|
9896
|
-
return
|
|
10547
|
+
return finish_with_publish(response)
|
|
9897
10548
|
|
|
9898
10549
|
def app_flow_apply(
|
|
9899
10550
|
self,
|
|
@@ -9912,22 +10563,54 @@ class AiBuilderFacade:
|
|
|
9912
10563
|
"transitions": transitions,
|
|
9913
10564
|
"publish": publish,
|
|
9914
10565
|
}
|
|
10566
|
+
apply_started_at = time.perf_counter()
|
|
10567
|
+
permission_ms = 0
|
|
10568
|
+
flow_state_read_ms = 0
|
|
10569
|
+
flow_compile_ms = 0
|
|
10570
|
+
flow_write_ms = 0
|
|
10571
|
+
flow_readback_ms = 0
|
|
10572
|
+
publish_ms = 0
|
|
9915
10573
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
10574
|
+
def finalize(response: JSONObject) -> JSONObject:
|
|
10575
|
+
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
10576
|
+
|
|
10577
|
+
def attach_duration(response: JSONObject) -> JSONObject:
|
|
10578
|
+
_merge_duration_breakdown(
|
|
10579
|
+
response,
|
|
10580
|
+
permission_ms=permission_ms,
|
|
10581
|
+
flow_state_read_ms=flow_state_read_ms,
|
|
10582
|
+
flow_compile_ms=flow_compile_ms,
|
|
10583
|
+
flow_write_ms=flow_write_ms,
|
|
10584
|
+
flow_readback_ms=flow_readback_ms,
|
|
10585
|
+
publish_ms=publish_ms,
|
|
10586
|
+
total_ms=_duration_ms(apply_started_at),
|
|
10587
|
+
)
|
|
10588
|
+
return response
|
|
10589
|
+
|
|
10590
|
+
def finish(response: JSONObject) -> JSONObject:
|
|
10591
|
+
return finalize(attach_duration(response))
|
|
10592
|
+
|
|
10593
|
+
def finish_with_publish(response: JSONObject) -> JSONObject:
|
|
10594
|
+
nonlocal publish_ms
|
|
10595
|
+
publish_started_at = time.perf_counter()
|
|
10596
|
+
published_response = self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
10597
|
+
publish_ms += _duration_ms(publish_started_at)
|
|
10598
|
+
return finish(published_response)
|
|
10599
|
+
|
|
10600
|
+
permission_started_at = time.perf_counter()
|
|
9916
10601
|
permission_outcome = self._guard_app_permission(
|
|
9917
10602
|
profile=profile,
|
|
9918
10603
|
app_key=app_key,
|
|
9919
10604
|
required_permission="edit_app",
|
|
9920
10605
|
normalized_args=normalized_args,
|
|
9921
10606
|
)
|
|
10607
|
+
permission_ms += _duration_ms(permission_started_at)
|
|
9922
10608
|
if permission_outcome.block is not None:
|
|
9923
|
-
return permission_outcome.block
|
|
10609
|
+
return finish(permission_outcome.block)
|
|
9924
10610
|
permission_outcomes.append(permission_outcome)
|
|
9925
10611
|
|
|
9926
|
-
def finalize(response: JSONObject) -> JSONObject:
|
|
9927
|
-
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
9928
|
-
|
|
9929
10612
|
if mode != "replace":
|
|
9930
|
-
return
|
|
10613
|
+
return finish(_failed(
|
|
9931
10614
|
"UNSUPPORTED_FLOW_MODE",
|
|
9932
10615
|
"only mode='replace' is supported",
|
|
9933
10616
|
normalized_args=normalized_args,
|
|
@@ -9935,17 +10618,21 @@ class AiBuilderFacade:
|
|
|
9935
10618
|
suggested_next_call={"tool_name": "app_flow_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
9936
10619
|
))
|
|
9937
10620
|
try:
|
|
10621
|
+
flow_state_read_started_at = time.perf_counter()
|
|
9938
10622
|
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
9939
10623
|
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
9940
10624
|
except (QingflowApiError, RuntimeError) as error:
|
|
10625
|
+
flow_state_read_ms += _duration_ms(flow_state_read_started_at)
|
|
9941
10626
|
api_error = _coerce_api_error(error)
|
|
9942
|
-
return
|
|
10627
|
+
return finish(_failed_from_api_error(
|
|
9943
10628
|
"FLOW_READ_FAILED",
|
|
9944
10629
|
api_error,
|
|
9945
10630
|
normalized_args=normalized_args,
|
|
9946
10631
|
details=_with_state_read_blocked_details({"app_key": app_key}, resource="workflow", error=api_error),
|
|
9947
10632
|
suggested_next_call={"tool_name": "app_get_flow", "arguments": {"profile": profile, "app_key": app_key}},
|
|
9948
10633
|
))
|
|
10634
|
+
flow_state_read_ms += _duration_ms(flow_state_read_started_at)
|
|
10635
|
+
flow_compile_started_at = time.perf_counter()
|
|
9949
10636
|
app_name = str(base.get("formTitle") or base.get("title") or base.get("appName") or app_key).strip() or app_key
|
|
9950
10637
|
entity = _entity_spec_from_app(base_info=base, schema=schema, views=None)
|
|
9951
10638
|
current_fields = _parse_schema(schema)["fields"]
|
|
@@ -9954,7 +10641,8 @@ class AiBuilderFacade:
|
|
|
9954
10641
|
unsupported_nodes = self._unsupported_public_flow_nodes(nodes=public_nodes)
|
|
9955
10642
|
normalized_args["nodes"] = public_nodes
|
|
9956
10643
|
if unsupported_nodes:
|
|
9957
|
-
|
|
10644
|
+
flow_compile_ms += _duration_ms(flow_compile_started_at)
|
|
10645
|
+
return finish(_failed(
|
|
9958
10646
|
"FLOW_NODE_TYPE_UNSUPPORTED",
|
|
9959
10647
|
"app_flow_apply currently supports only linear workflows; branch and condition nodes are disabled because the backend workflow route is not front-end stable for these node types.",
|
|
9960
10648
|
normalized_args=normalized_args,
|
|
@@ -9964,7 +10652,7 @@ class AiBuilderFacade:
|
|
|
9964
10652
|
"disabled_node_types": sorted(DISABLED_PUBLIC_FLOW_NODE_TYPES),
|
|
9965
10653
|
},
|
|
9966
10654
|
suggested_next_call={"tool_name": "builder_tool_contract", "arguments": {"tool_name": "app_flow_apply"}},
|
|
9967
|
-
)
|
|
10655
|
+
))
|
|
9968
10656
|
if resolution_issues:
|
|
9969
10657
|
first_issue = resolution_issues[0]
|
|
9970
10658
|
suggested_call = None
|
|
@@ -9974,13 +10662,14 @@ class AiBuilderFacade:
|
|
|
9974
10662
|
suggested_call = {"tool_name": "member_search", "arguments": {"profile": profile, "query": first_issue.get("value") or ""}}
|
|
9975
10663
|
elif first_issue.get("kind") in {"editable_fields", "condition_fields"}:
|
|
9976
10664
|
suggested_call = {"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}}
|
|
9977
|
-
|
|
10665
|
+
flow_compile_ms += _duration_ms(flow_compile_started_at)
|
|
10666
|
+
return finish(_failed(
|
|
9978
10667
|
first_issue.get("error_code") or "FLOW_ASSIGNEE_UNRESOLVED",
|
|
9979
10668
|
"workflow contains unresolved assignees or field permissions",
|
|
9980
10669
|
normalized_args=normalized_args,
|
|
9981
10670
|
details={"issues": resolution_issues},
|
|
9982
10671
|
suggested_next_call=suggested_call,
|
|
9983
|
-
)
|
|
10672
|
+
))
|
|
9984
10673
|
assignee_required_nodes = [
|
|
9985
10674
|
node.get("id")
|
|
9986
10675
|
for node in normalized_nodes
|
|
@@ -9991,19 +10680,21 @@ class AiBuilderFacade:
|
|
|
9991
10680
|
)
|
|
9992
10681
|
]
|
|
9993
10682
|
if assignee_required_nodes:
|
|
9994
|
-
|
|
10683
|
+
flow_compile_ms += _duration_ms(flow_compile_started_at)
|
|
10684
|
+
return finish(_failed(
|
|
9995
10685
|
"FLOW_ASSIGNEE_REQUIRED",
|
|
9996
10686
|
"workflow approval/fill/copy nodes must declare at least one role or member assignee",
|
|
9997
10687
|
normalized_args=normalized_args,
|
|
9998
10688
|
details={"node_ids": assignee_required_nodes, "policy": "prefer role assignees; explicit members are also supported"},
|
|
9999
10689
|
suggested_next_call={"tool_name": "role_search", "arguments": {"profile": profile, "keyword": ""}},
|
|
10000
|
-
)
|
|
10690
|
+
))
|
|
10001
10691
|
workflow_spec = _build_public_workflow_spec(nodes=normalized_nodes, transitions=transitions)
|
|
10002
10692
|
if workflow_spec.get("status") == "failed":
|
|
10003
10693
|
workflow_spec["normalized_args"] = normalized_args
|
|
10004
10694
|
workflow_spec.setdefault("request_id", None)
|
|
10005
10695
|
workflow_spec["suggested_next_call"] = {"tool_name": "app_flow_apply", "arguments": {"profile": profile, **normalized_args}}
|
|
10006
|
-
|
|
10696
|
+
flow_compile_ms += _duration_ms(flow_compile_started_at)
|
|
10697
|
+
return finish(workflow_spec)
|
|
10007
10698
|
desired_node_count = len([node for node in normalized_nodes if node.get("type") != "end"])
|
|
10008
10699
|
build_id = f"facade-flow-{uuid4().hex[:12]}"
|
|
10009
10700
|
previous_build_home = os.getenv("QINGFLOW_MCP_BUILD_HOME")
|
|
@@ -10028,6 +10719,8 @@ class AiBuilderFacade:
|
|
|
10028
10719
|
"entities": [{"entity_id": entity["entity_id"], "workflow": workflow_spec["workflow"]}],
|
|
10029
10720
|
}
|
|
10030
10721
|
assembly.set_stage_spec("app_flow", flow_stage_spec)
|
|
10722
|
+
flow_compile_ms += _duration_ms(flow_compile_started_at)
|
|
10723
|
+
flow_write_started_at = time.perf_counter()
|
|
10031
10724
|
stage = self.solutions.solution_build_flow(
|
|
10032
10725
|
profile=profile,
|
|
10033
10726
|
mode="apply",
|
|
@@ -10037,6 +10730,7 @@ class AiBuilderFacade:
|
|
|
10037
10730
|
run_label=None,
|
|
10038
10731
|
repair_patch={},
|
|
10039
10732
|
)
|
|
10733
|
+
flow_write_ms += _duration_ms(flow_write_started_at)
|
|
10040
10734
|
finally:
|
|
10041
10735
|
if previous_build_home is None:
|
|
10042
10736
|
os.environ.pop("QINGFLOW_MCP_BUILD_HOME", None)
|
|
@@ -10058,8 +10752,10 @@ class AiBuilderFacade:
|
|
|
10058
10752
|
suggested_next_call["tool_name"] = "app_flow_apply"
|
|
10059
10753
|
suggested_next_call["arguments"] = arguments
|
|
10060
10754
|
failed["suggested_next_call"] = suggested_next_call
|
|
10061
|
-
return
|
|
10755
|
+
return finish(failed)
|
|
10756
|
+
flow_readback_started_at = time.perf_counter()
|
|
10062
10757
|
verified_nodes, verified_nodes_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
10758
|
+
flow_readback_ms += _duration_ms(flow_readback_started_at)
|
|
10063
10759
|
workflow_structure_verified = bool(verified_nodes) and _workflow_nodes_semantically_equal(
|
|
10064
10760
|
current_workflow=verified_nodes,
|
|
10065
10761
|
requested_nodes=normalized_nodes,
|
|
@@ -10121,7 +10817,7 @@ class AiBuilderFacade:
|
|
|
10121
10817
|
"write_succeeded": True,
|
|
10122
10818
|
"safe_to_retry": False,
|
|
10123
10819
|
}
|
|
10124
|
-
return
|
|
10820
|
+
return finish_with_publish(response)
|
|
10125
10821
|
|
|
10126
10822
|
def _extract_view_action_button_patch_intents(
|
|
10127
10823
|
self,
|
|
@@ -10547,12 +11243,65 @@ class AiBuilderFacade:
|
|
|
10547
11243
|
"remove_views": list(remove_views),
|
|
10548
11244
|
"publish": publish,
|
|
10549
11245
|
}
|
|
11246
|
+
apply_started_at = time.perf_counter()
|
|
11247
|
+
permission_ms = 0
|
|
11248
|
+
view_state_read_ms = 0
|
|
11249
|
+
custom_button_inventory_ms = 0
|
|
11250
|
+
associated_resource_inventory_ms = 0
|
|
11251
|
+
view_mutation_ms = 0
|
|
11252
|
+
action_buttons_ms = 0
|
|
11253
|
+
view_readback_ms = 0
|
|
11254
|
+
view_verification_ms = 0
|
|
11255
|
+
publish_ms = 0
|
|
11256
|
+
publish_already_satisfied_by_action_buttons = False
|
|
11257
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
11258
|
+
|
|
11259
|
+
def finalize(response: JSONObject) -> JSONObject:
|
|
11260
|
+
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
11261
|
+
|
|
11262
|
+
def attach_duration(response: JSONObject) -> JSONObject:
|
|
11263
|
+
_merge_duration_breakdown(
|
|
11264
|
+
response,
|
|
11265
|
+
permission_ms=permission_ms,
|
|
11266
|
+
view_state_read_ms=view_state_read_ms,
|
|
11267
|
+
custom_button_inventory_ms=custom_button_inventory_ms,
|
|
11268
|
+
associated_resource_inventory_ms=associated_resource_inventory_ms,
|
|
11269
|
+
view_mutation_ms=view_mutation_ms,
|
|
11270
|
+
action_buttons_ms=action_buttons_ms,
|
|
11271
|
+
view_readback_ms=view_readback_ms,
|
|
11272
|
+
view_verification_ms=view_verification_ms,
|
|
11273
|
+
publish_ms=publish_ms,
|
|
11274
|
+
total_ms=_duration_ms(apply_started_at),
|
|
11275
|
+
)
|
|
11276
|
+
return response
|
|
11277
|
+
|
|
11278
|
+
def finish(response: JSONObject) -> JSONObject:
|
|
11279
|
+
return finalize(attach_duration(response))
|
|
11280
|
+
|
|
11281
|
+
def finish_with_publish(response: JSONObject) -> JSONObject:
|
|
11282
|
+
nonlocal publish_ms
|
|
11283
|
+
if publish and publish_already_satisfied_by_action_buttons and not bool(response.get("noop")):
|
|
11284
|
+
verification = response.get("verification")
|
|
11285
|
+
if not isinstance(verification, dict):
|
|
11286
|
+
verification = {}
|
|
11287
|
+
response["verification"] = verification
|
|
11288
|
+
response["publish_requested"] = True
|
|
11289
|
+
response["publish_skipped"] = True
|
|
11290
|
+
response["publish_skip_reason"] = "already_published_by_action_buttons"
|
|
11291
|
+
response["published"] = True
|
|
11292
|
+
verification["published"] = True
|
|
11293
|
+
return finish(response)
|
|
11294
|
+
publish_started_at = time.perf_counter()
|
|
11295
|
+
published_response = self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
11296
|
+
publish_ms += _duration_ms(publish_started_at)
|
|
11297
|
+
return finish(published_response)
|
|
11298
|
+
|
|
10550
11299
|
has_action_button_intent = any(patch.action_buttons is not None for patch in upsert_views) or any(
|
|
10551
11300
|
any(key in (patch.set or {}) for key in ("action_buttons", "actionButtons"))
|
|
10552
11301
|
for patch in patch_views
|
|
10553
11302
|
)
|
|
10554
11303
|
if has_action_button_intent and not publish:
|
|
10555
|
-
return _failed(
|
|
11304
|
+
return finish(_failed(
|
|
10556
11305
|
"VIEW_ACTION_BUTTONS_REQUIRE_PUBLISH",
|
|
10557
11306
|
"app_views_apply action_buttons require publish=true because the underlying custom button writer may publish after successful button writes",
|
|
10558
11307
|
normalized_args=normalized_args,
|
|
@@ -10562,7 +11311,7 @@ class AiBuilderFacade:
|
|
|
10562
11311
|
"reason": "action_buttons are compiled through app_custom_buttons_apply",
|
|
10563
11312
|
},
|
|
10564
11313
|
suggested_next_call={"tool_name": "app_views_apply", "arguments": {"profile": profile, **{**normalized_args, "publish": True}}},
|
|
10565
|
-
)
|
|
11314
|
+
))
|
|
10566
11315
|
if not upsert_views and not patch_views and not remove_views:
|
|
10567
11316
|
response = {
|
|
10568
11317
|
"status": "success",
|
|
@@ -10590,10 +11339,10 @@ class AiBuilderFacade:
|
|
|
10590
11339
|
"write_succeeded": False,
|
|
10591
11340
|
"safe_to_retry": True,
|
|
10592
11341
|
}
|
|
10593
|
-
return
|
|
10594
|
-
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
11342
|
+
return finish_with_publish(response)
|
|
10595
11343
|
app_permission_summary: JSONObject | None = None
|
|
10596
11344
|
app_permission_error: QingflowApiError | None = None
|
|
11345
|
+
permission_started_at = time.perf_counter()
|
|
10597
11346
|
try:
|
|
10598
11347
|
app_permission_summary = self._read_app_permission_summary(profile=profile, app_key=app_key)
|
|
10599
11348
|
except (QingflowApiError, RuntimeError) as error:
|
|
@@ -10637,26 +11386,27 @@ class AiBuilderFacade:
|
|
|
10637
11386
|
normalized_args=normalized_args,
|
|
10638
11387
|
permission_summary=app_permission_summary,
|
|
10639
11388
|
)
|
|
11389
|
+
permission_ms += _duration_ms(permission_started_at)
|
|
10640
11390
|
if permission_outcome.block is not None:
|
|
10641
|
-
return permission_outcome.block
|
|
11391
|
+
return finish(permission_outcome.block)
|
|
10642
11392
|
permission_outcomes.append(permission_outcome)
|
|
10643
11393
|
|
|
10644
|
-
def finalize(response: JSONObject) -> JSONObject:
|
|
10645
|
-
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
10646
|
-
|
|
10647
11394
|
try:
|
|
11395
|
+
view_state_read_started_at = time.perf_counter()
|
|
10648
11396
|
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
10649
11397
|
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
10650
11398
|
existing_views, _views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=False)
|
|
10651
11399
|
except (QingflowApiError, RuntimeError) as error:
|
|
11400
|
+
view_state_read_ms += _duration_ms(view_state_read_started_at)
|
|
10652
11401
|
api_error = _coerce_api_error(error)
|
|
10653
|
-
return
|
|
11402
|
+
return finish(_failed_from_api_error(
|
|
10654
11403
|
"VIEWS_READ_FAILED",
|
|
10655
11404
|
api_error,
|
|
10656
11405
|
normalized_args=normalized_args,
|
|
10657
11406
|
details=_with_state_read_blocked_details({"app_key": app_key}, resource="views", error=api_error),
|
|
10658
11407
|
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
10659
11408
|
))
|
|
11409
|
+
view_state_read_ms += _duration_ms(view_state_read_started_at)
|
|
10660
11410
|
app_name = str(base.get("formTitle") or base.get("title") or base.get("appName") or "").strip() or None
|
|
10661
11411
|
existing_views = existing_views or []
|
|
10662
11412
|
existing_by_key: dict[str, dict[str, Any]] = {}
|
|
@@ -10680,7 +11430,7 @@ class AiBuilderFacade:
|
|
|
10680
11430
|
if action_button_patch_results:
|
|
10681
11431
|
normalized_args["action_button_patch_results"] = action_button_patch_results
|
|
10682
11432
|
if action_button_patch_issues:
|
|
10683
|
-
return
|
|
11433
|
+
return finish(
|
|
10684
11434
|
_failed(
|
|
10685
11435
|
"VIEW_ACTION_BUTTON_PATCH_FAILED",
|
|
10686
11436
|
"one or more patch_views action_buttons entries could not be resolved; no write was executed",
|
|
@@ -10705,6 +11455,7 @@ class AiBuilderFacade:
|
|
|
10705
11455
|
transport_error=_transport_error_payload(app_permission_error),
|
|
10706
11456
|
)
|
|
10707
11457
|
else:
|
|
11458
|
+
create_permission_started_at = time.perf_counter()
|
|
10708
11459
|
create_permission_outcome = self._guard_app_permission(
|
|
10709
11460
|
profile=profile,
|
|
10710
11461
|
app_key=app_key,
|
|
@@ -10712,13 +11463,14 @@ class AiBuilderFacade:
|
|
|
10712
11463
|
normalized_args=normalized_args,
|
|
10713
11464
|
permission_summary=app_permission_summary,
|
|
10714
11465
|
)
|
|
11466
|
+
permission_ms += _duration_ms(create_permission_started_at)
|
|
10715
11467
|
if create_permission_outcome.block is not None:
|
|
10716
11468
|
details = create_permission_outcome.block.get("details")
|
|
10717
11469
|
if isinstance(details, dict):
|
|
10718
11470
|
details["operation"] = "view_create"
|
|
10719
11471
|
details["view_names"] = creating_view_names
|
|
10720
11472
|
details["also_required_permission"] = "view_manage"
|
|
10721
|
-
return create_permission_outcome.block
|
|
11473
|
+
return finish(create_permission_outcome.block)
|
|
10722
11474
|
permission_outcomes.append(create_permission_outcome)
|
|
10723
11475
|
parsed_schema = _parse_schema(schema)
|
|
10724
11476
|
field_names = {field["name"] for field in parsed_schema["fields"]}
|
|
@@ -10757,6 +11509,7 @@ class AiBuilderFacade:
|
|
|
10757
11509
|
custom_button_details_by_id: dict[int, dict[str, Any]] = {}
|
|
10758
11510
|
if requires_custom_button_validation:
|
|
10759
11511
|
try:
|
|
11512
|
+
custom_button_inventory_started_at = time.perf_counter()
|
|
10760
11513
|
button_listing = self.buttons.custom_button_list(
|
|
10761
11514
|
profile=profile,
|
|
10762
11515
|
app_key=app_key,
|
|
@@ -10764,8 +11517,9 @@ class AiBuilderFacade:
|
|
|
10764
11517
|
include_raw=False,
|
|
10765
11518
|
)
|
|
10766
11519
|
except (QingflowApiError, RuntimeError) as error:
|
|
11520
|
+
custom_button_inventory_ms += _duration_ms(custom_button_inventory_started_at)
|
|
10767
11521
|
api_error = _coerce_api_error(error)
|
|
10768
|
-
return
|
|
11522
|
+
return finish(
|
|
10769
11523
|
_failed_from_api_error(
|
|
10770
11524
|
"CUSTOM_BUTTON_LIST_FAILED",
|
|
10771
11525
|
api_error,
|
|
@@ -10774,6 +11528,7 @@ class AiBuilderFacade:
|
|
|
10774
11528
|
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
10775
11529
|
)
|
|
10776
11530
|
)
|
|
11531
|
+
custom_button_inventory_ms += _duration_ms(custom_button_inventory_started_at)
|
|
10777
11532
|
valid_custom_button_ids = {
|
|
10778
11533
|
button_id
|
|
10779
11534
|
for item in (button_listing.get("items") or [])
|
|
@@ -10787,6 +11542,7 @@ class AiBuilderFacade:
|
|
|
10787
11542
|
}
|
|
10788
11543
|
for button_id in sorted(referenced_custom_button_ids):
|
|
10789
11544
|
try:
|
|
11545
|
+
custom_button_detail_started_at = time.perf_counter()
|
|
10790
11546
|
detail = self.buttons.custom_button_get(
|
|
10791
11547
|
profile=profile,
|
|
10792
11548
|
app_key=app_key,
|
|
@@ -10794,7 +11550,9 @@ class AiBuilderFacade:
|
|
|
10794
11550
|
being_draft=True,
|
|
10795
11551
|
include_raw=False,
|
|
10796
11552
|
)
|
|
11553
|
+
custom_button_inventory_ms += _duration_ms(custom_button_detail_started_at)
|
|
10797
11554
|
except (QingflowApiError, RuntimeError) as error:
|
|
11555
|
+
custom_button_inventory_ms += _duration_ms(custom_button_detail_started_at)
|
|
10798
11556
|
api_error = _coerce_api_error(error)
|
|
10799
11557
|
if _is_optional_builder_lookup_error(api_error):
|
|
10800
11558
|
continue
|
|
@@ -10813,7 +11571,7 @@ class AiBuilderFacade:
|
|
|
10813
11571
|
},
|
|
10814
11572
|
)
|
|
10815
11573
|
failed.update({"write_executed": False, "write_succeeded": False, "safe_to_retry": True})
|
|
10816
|
-
return
|
|
11574
|
+
return finish(failed)
|
|
10817
11575
|
detail_result = detail.get("result")
|
|
10818
11576
|
if isinstance(detail_result, dict):
|
|
10819
11577
|
custom_button_details_by_id[button_id] = _normalize_custom_button_detail(detail_result)
|
|
@@ -10821,10 +11579,12 @@ class AiBuilderFacade:
|
|
|
10821
11579
|
associated_resources: list[dict[str, Any]] = []
|
|
10822
11580
|
if requires_associated_resource_validation:
|
|
10823
11581
|
try:
|
|
11582
|
+
associated_resource_inventory_started_at = time.perf_counter()
|
|
10824
11583
|
associated_resources = self._load_associated_resources_for_builder(profile=profile, app_key=app_key)
|
|
10825
11584
|
except (QingflowApiError, RuntimeError) as error:
|
|
11585
|
+
associated_resource_inventory_ms += _duration_ms(associated_resource_inventory_started_at)
|
|
10826
11586
|
api_error = _coerce_api_error(error)
|
|
10827
|
-
return
|
|
11587
|
+
return finish(
|
|
10828
11588
|
_failed_from_api_error(
|
|
10829
11589
|
"ASSOCIATED_RESOURCES_READ_FAILED",
|
|
10830
11590
|
api_error,
|
|
@@ -10833,6 +11593,7 @@ class AiBuilderFacade:
|
|
|
10833
11593
|
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
10834
11594
|
)
|
|
10835
11595
|
)
|
|
11596
|
+
associated_resource_inventory_ms += _duration_ms(associated_resource_inventory_started_at)
|
|
10836
11597
|
removed: list[str] = []
|
|
10837
11598
|
removed_keys: set[str] = set()
|
|
10838
11599
|
view_results: list[dict[str, Any]] = []
|
|
@@ -10853,6 +11614,7 @@ class AiBuilderFacade:
|
|
|
10853
11614
|
}
|
|
10854
11615
|
)
|
|
10855
11616
|
|
|
11617
|
+
view_mutation_started_at = time.perf_counter()
|
|
10856
11618
|
for selector in remove_views:
|
|
10857
11619
|
selector_text = str(selector or "").strip()
|
|
10858
11620
|
if not selector_text:
|
|
@@ -11489,6 +12251,7 @@ class AiBuilderFacade:
|
|
|
11489
12251
|
failed_views.append(failure_entry)
|
|
11490
12252
|
view_results.append(failure_entry)
|
|
11491
12253
|
continue
|
|
12254
|
+
view_mutation_ms += _duration_ms(view_mutation_started_at)
|
|
11492
12255
|
successful_action_view_keys = {
|
|
11493
12256
|
str(item.get("view_key") or "").strip()
|
|
11494
12257
|
for item in view_results
|
|
@@ -11510,11 +12273,13 @@ class AiBuilderFacade:
|
|
|
11510
12273
|
runnable_action_button_intents = [
|
|
11511
12274
|
intent for intent in action_button_intents if id(intent) not in skipped_action_button_intent_ids
|
|
11512
12275
|
]
|
|
12276
|
+
action_buttons_started_at = time.perf_counter()
|
|
11513
12277
|
action_buttons_result = self._apply_view_action_buttons(
|
|
11514
12278
|
profile=profile,
|
|
11515
12279
|
app_key=app_key,
|
|
11516
12280
|
intents=runnable_action_button_intents,
|
|
11517
12281
|
)
|
|
12282
|
+
action_buttons_ms += _duration_ms(action_buttons_started_at)
|
|
11518
12283
|
if skipped_action_button_intents:
|
|
11519
12284
|
action_buttons_result.setdefault("skipped_due_to_view_write_failure", [])
|
|
11520
12285
|
if isinstance(action_buttons_result["skipped_due_to_view_write_failure"], list):
|
|
@@ -11530,6 +12295,11 @@ class AiBuilderFacade:
|
|
|
11530
12295
|
)
|
|
11531
12296
|
action_button_write_executed = bool(action_buttons_result.get("write_executed"))
|
|
11532
12297
|
action_button_write_succeeded = bool(action_buttons_result.get("write_succeeded"))
|
|
12298
|
+
publish_already_satisfied_by_action_buttons = bool(
|
|
12299
|
+
publish
|
|
12300
|
+
and action_buttons_result.get("publish_requested") is True
|
|
12301
|
+
and action_buttons_result.get("published") is True
|
|
12302
|
+
)
|
|
11533
12303
|
action_buttons_verification = action_buttons_result.get("verification") if isinstance(action_buttons_result.get("verification"), dict) else {}
|
|
11534
12304
|
action_buttons_verified = (
|
|
11535
12305
|
bool(action_buttons_result.get("verified", True))
|
|
@@ -11544,16 +12314,19 @@ class AiBuilderFacade:
|
|
|
11544
12314
|
verified_views_unavailable = False
|
|
11545
12315
|
if needs_view_list_readback:
|
|
11546
12316
|
try:
|
|
12317
|
+
view_readback_started_at = time.perf_counter()
|
|
11547
12318
|
verified_view_result, verified_views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
11548
12319
|
except (QingflowApiError, RuntimeError) as error:
|
|
12320
|
+
view_readback_ms += _duration_ms(view_readback_started_at)
|
|
11549
12321
|
api_error = _coerce_api_error(error)
|
|
11550
|
-
return
|
|
12322
|
+
return finish(_failed_from_api_error(
|
|
11551
12323
|
"VIEWS_READ_FAILED",
|
|
11552
12324
|
api_error,
|
|
11553
12325
|
normalized_args=normalized_args,
|
|
11554
12326
|
details=_with_state_read_blocked_details({"app_key": app_key}, resource="views", error=api_error),
|
|
11555
12327
|
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
11556
12328
|
))
|
|
12329
|
+
view_readback_ms += _duration_ms(view_readback_started_at)
|
|
11557
12330
|
verified_names = {
|
|
11558
12331
|
_extract_view_name(item)
|
|
11559
12332
|
for item in (verified_view_result or [])
|
|
@@ -11583,6 +12356,22 @@ class AiBuilderFacade:
|
|
|
11583
12356
|
button_mismatches: list[dict[str, Any]] = []
|
|
11584
12357
|
custom_button_readback_pending = False
|
|
11585
12358
|
custom_button_readback_pending_entries: list[dict[str, Any]] = []
|
|
12359
|
+
view_config_readback_cache: dict[str, tuple[dict[str, Any], QingflowApiError | None]] = {}
|
|
12360
|
+
|
|
12361
|
+
def read_view_config_for_verification(view_key: str) -> tuple[dict[str, Any], QingflowApiError | None]:
|
|
12362
|
+
cached = view_config_readback_cache.get(view_key)
|
|
12363
|
+
if cached is not None:
|
|
12364
|
+
return cached
|
|
12365
|
+
try:
|
|
12366
|
+
config_response = self.views.view_get_config(profile=profile, viewgraph_key=view_key)
|
|
12367
|
+
config_result = (config_response.get("result") or {}) if isinstance(config_response.get("result"), dict) else {}
|
|
12368
|
+
cached = (config_result, None)
|
|
12369
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
12370
|
+
cached = ({}, _coerce_api_error(error))
|
|
12371
|
+
view_config_readback_cache[view_key] = cached
|
|
12372
|
+
return cached
|
|
12373
|
+
|
|
12374
|
+
view_verification_started_at = time.perf_counter()
|
|
11586
12375
|
for item in view_results:
|
|
11587
12376
|
status = str(item.get("status") or "")
|
|
11588
12377
|
name = str(item.get("name") or "")
|
|
@@ -11629,8 +12418,9 @@ class AiBuilderFacade:
|
|
|
11629
12418
|
verification_by_view.append(verification_entry)
|
|
11630
12419
|
continue
|
|
11631
12420
|
try:
|
|
11632
|
-
|
|
11633
|
-
|
|
12421
|
+
config_result, config_error = read_view_config_for_verification(verification_key)
|
|
12422
|
+
if config_error is not None:
|
|
12423
|
+
raise config_error
|
|
11634
12424
|
actual_filters = _normalize_view_filter_groups_for_compare(config_result.get("viewgraphLimit"))
|
|
11635
12425
|
expected_filter_summary = _normalize_view_filter_groups_for_compare(expected_filters)
|
|
11636
12426
|
expected_data_scope = "CUSTOM" if expected_filter_summary else "ALL"
|
|
@@ -11706,8 +12496,9 @@ class AiBuilderFacade:
|
|
|
11706
12496
|
verification_by_view.append(verification_entry)
|
|
11707
12497
|
continue
|
|
11708
12498
|
try:
|
|
11709
|
-
|
|
11710
|
-
|
|
12499
|
+
config_result, config_error = read_view_config_for_verification(verification_key)
|
|
12500
|
+
if config_error is not None:
|
|
12501
|
+
raise config_error
|
|
11711
12502
|
actual_query_conditions = _normalize_view_query_conditions_for_compare(config_result)
|
|
11712
12503
|
query_conditions_verified = actual_query_conditions == expected_query_conditions
|
|
11713
12504
|
verification_entry["query_conditions_verified"] = query_conditions_verified
|
|
@@ -11754,8 +12545,9 @@ class AiBuilderFacade:
|
|
|
11754
12545
|
verification_by_view.append(verification_entry)
|
|
11755
12546
|
continue
|
|
11756
12547
|
try:
|
|
11757
|
-
|
|
11758
|
-
|
|
12548
|
+
config_result, config_error = read_view_config_for_verification(verification_key)
|
|
12549
|
+
if config_error is not None:
|
|
12550
|
+
raise config_error
|
|
11759
12551
|
actual_associated_resources = _extract_view_associated_resources_config(
|
|
11760
12552
|
config_result,
|
|
11761
12553
|
available_resources=associated_resources,
|
|
@@ -11808,8 +12600,9 @@ class AiBuilderFacade:
|
|
|
11808
12600
|
verification_by_view.append(verification_entry)
|
|
11809
12601
|
continue
|
|
11810
12602
|
try:
|
|
11811
|
-
|
|
11812
|
-
|
|
12603
|
+
config_result, config_error = read_view_config_for_verification(verification_key)
|
|
12604
|
+
if config_error is not None:
|
|
12605
|
+
raise config_error
|
|
11813
12606
|
actual_buttons = _normalize_view_buttons_for_compare(config_result)
|
|
11814
12607
|
button_comparison = _compare_view_button_summaries(
|
|
11815
12608
|
expected=expected_buttons,
|
|
@@ -11893,6 +12686,7 @@ class AiBuilderFacade:
|
|
|
11893
12686
|
"error_code": item.get("error_code"),
|
|
11894
12687
|
}
|
|
11895
12688
|
)
|
|
12689
|
+
view_verification_ms += _duration_ms(view_verification_started_at)
|
|
11896
12690
|
removed_delete_results = [
|
|
11897
12691
|
item
|
|
11898
12692
|
for item in view_results
|
|
@@ -11992,7 +12786,7 @@ class AiBuilderFacade:
|
|
|
11992
12786
|
"write_succeeded": bool(created or updated or removed or action_button_write_succeeded),
|
|
11993
12787
|
"safe_to_retry": not bool(created or updated or removed or action_button_write_executed),
|
|
11994
12788
|
}
|
|
11995
|
-
return
|
|
12789
|
+
return finish_with_publish(response)
|
|
11996
12790
|
warnings: list[dict[str, Any]] = []
|
|
11997
12791
|
if filter_readback_pending or filter_mismatches:
|
|
11998
12792
|
warnings.append(_warning("VIEW_FILTERS_UNVERIFIED", "view definitions were applied, but saved filter behavior is not fully verified"))
|
|
@@ -12092,7 +12886,7 @@ class AiBuilderFacade:
|
|
|
12092
12886
|
"write_succeeded": bool(created or updated or removed or action_button_write_succeeded),
|
|
12093
12887
|
"safe_to_retry": not bool(created or updated or removed or action_button_write_executed),
|
|
12094
12888
|
}
|
|
12095
|
-
return
|
|
12889
|
+
return finish_with_publish(response)
|
|
12096
12890
|
|
|
12097
12891
|
def app_publish_verify(
|
|
12098
12892
|
self,
|
|
@@ -12101,87 +12895,132 @@ class AiBuilderFacade:
|
|
|
12101
12895
|
app_key: str,
|
|
12102
12896
|
expected_package_tag_id: int | None = None,
|
|
12103
12897
|
) -> JSONObject:
|
|
12898
|
+
apply_started_at = time.perf_counter()
|
|
12899
|
+
initial_base_read_ms = 0
|
|
12900
|
+
initial_views_read_ms = 0
|
|
12901
|
+
edit_version_ms = 0
|
|
12902
|
+
publish_ms = 0
|
|
12903
|
+
base_readback_ms = 0
|
|
12904
|
+
views_readback_ms = 0
|
|
12104
12905
|
normalized_args = {"app_key": app_key, "expected_package_tag_id": expected_package_tag_id}
|
|
12906
|
+
|
|
12907
|
+
def finish(response: JSONObject) -> JSONObject:
|
|
12908
|
+
_merge_duration_breakdown(
|
|
12909
|
+
response,
|
|
12910
|
+
initial_base_read_ms=initial_base_read_ms,
|
|
12911
|
+
initial_views_read_ms=initial_views_read_ms,
|
|
12912
|
+
edit_version_ms=edit_version_ms,
|
|
12913
|
+
publish_ms=publish_ms,
|
|
12914
|
+
base_readback_ms=base_readback_ms,
|
|
12915
|
+
views_readback_ms=views_readback_ms,
|
|
12916
|
+
total_ms=_duration_ms(apply_started_at),
|
|
12917
|
+
)
|
|
12918
|
+
return response
|
|
12919
|
+
|
|
12920
|
+
initial_base_read_started_at = time.perf_counter()
|
|
12105
12921
|
try:
|
|
12106
12922
|
base_before = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
12107
12923
|
except (QingflowApiError, RuntimeError) as error:
|
|
12924
|
+
initial_base_read_ms += _duration_ms(initial_base_read_started_at)
|
|
12108
12925
|
api_error = _coerce_api_error(error)
|
|
12109
|
-
return
|
|
12110
|
-
|
|
12111
|
-
|
|
12112
|
-
|
|
12113
|
-
|
|
12114
|
-
|
|
12926
|
+
return finish(
|
|
12927
|
+
_failed_from_api_error(
|
|
12928
|
+
"APP_READ_FAILED",
|
|
12929
|
+
api_error,
|
|
12930
|
+
normalized_args=normalized_args,
|
|
12931
|
+
details={"app_key": app_key},
|
|
12932
|
+
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
12933
|
+
)
|
|
12115
12934
|
)
|
|
12935
|
+
initial_base_read_ms += _duration_ms(initial_base_read_started_at)
|
|
12116
12936
|
tag_ids_before = _coerce_int_list(base_before.get("tagIds"))
|
|
12117
12937
|
app_name_before = str(base_before.get("formTitle") or base_before.get("title") or base_before.get("appName") or "").strip() or None
|
|
12118
12938
|
already_published = bool(base_before.get("appPublishStatus") in {1, 2})
|
|
12119
12939
|
package_already_attached = None if not expected_package_tag_id else expected_package_tag_id in tag_ids_before
|
|
12940
|
+
initial_views_read_started_at = time.perf_counter()
|
|
12120
12941
|
try:
|
|
12121
12942
|
views_before, views_before_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
12122
12943
|
except (QingflowApiError, RuntimeError) as error:
|
|
12944
|
+
initial_views_read_ms += _duration_ms(initial_views_read_started_at)
|
|
12123
12945
|
api_error = _coerce_api_error(error)
|
|
12124
|
-
return
|
|
12125
|
-
|
|
12126
|
-
|
|
12127
|
-
|
|
12128
|
-
|
|
12129
|
-
|
|
12946
|
+
return finish(
|
|
12947
|
+
_failed_from_api_error(
|
|
12948
|
+
"VIEWS_READ_FAILED",
|
|
12949
|
+
api_error,
|
|
12950
|
+
normalized_args=normalized_args,
|
|
12951
|
+
details={"app_key": app_key},
|
|
12952
|
+
suggested_next_call={"tool_name": "app_get_views", "arguments": {"profile": profile, "app_key": app_key}},
|
|
12953
|
+
)
|
|
12130
12954
|
)
|
|
12955
|
+
initial_views_read_ms += _duration_ms(initial_views_read_started_at)
|
|
12131
12956
|
views_before = views_before or []
|
|
12132
12957
|
if already_published and package_already_attached is not False and isinstance(views_before, list) and not views_before_unavailable:
|
|
12133
|
-
return
|
|
12134
|
-
|
|
12135
|
-
|
|
12136
|
-
|
|
12137
|
-
|
|
12138
|
-
|
|
12139
|
-
|
|
12140
|
-
|
|
12141
|
-
|
|
12142
|
-
|
|
12143
|
-
|
|
12144
|
-
|
|
12145
|
-
|
|
12146
|
-
|
|
12147
|
-
|
|
12148
|
-
|
|
12149
|
-
|
|
12150
|
-
|
|
12151
|
-
|
|
12152
|
-
|
|
12153
|
-
|
|
12154
|
-
|
|
12155
|
-
|
|
12156
|
-
|
|
12157
|
-
|
|
12958
|
+
return finish(
|
|
12959
|
+
{
|
|
12960
|
+
"status": "success",
|
|
12961
|
+
"error_code": None,
|
|
12962
|
+
"recoverable": False,
|
|
12963
|
+
"message": "app already published and verified",
|
|
12964
|
+
"normalized_args": normalized_args,
|
|
12965
|
+
"missing_fields": [],
|
|
12966
|
+
"allowed_values": {},
|
|
12967
|
+
"details": {},
|
|
12968
|
+
"request_id": None,
|
|
12969
|
+
"suggested_next_call": None,
|
|
12970
|
+
"noop": True,
|
|
12971
|
+
"warnings": [],
|
|
12972
|
+
"verification": {"published": True, "package_attached": package_already_attached, "views_ok": True},
|
|
12973
|
+
"app_key": app_key,
|
|
12974
|
+
"app_name": app_name_before,
|
|
12975
|
+
"published": True,
|
|
12976
|
+
"package_attached": package_already_attached,
|
|
12977
|
+
"tag_ids_after": tag_ids_before,
|
|
12978
|
+
"views_ok": True,
|
|
12979
|
+
"verified": True,
|
|
12980
|
+
"write_executed": False,
|
|
12981
|
+
"write_succeeded": False,
|
|
12982
|
+
"safe_to_retry": True,
|
|
12983
|
+
}
|
|
12984
|
+
)
|
|
12985
|
+
edit_version_started_at = time.perf_counter()
|
|
12158
12986
|
try:
|
|
12159
12987
|
version_result = self.apps.app_get_edit_version_no(profile=profile, app_key=app_key).get("result") or {}
|
|
12160
12988
|
except (QingflowApiError, RuntimeError) as error:
|
|
12989
|
+
edit_version_ms += _duration_ms(edit_version_started_at)
|
|
12161
12990
|
api_error = _coerce_api_error(error)
|
|
12162
|
-
return
|
|
12163
|
-
|
|
12164
|
-
|
|
12165
|
-
|
|
12166
|
-
|
|
12167
|
-
|
|
12991
|
+
return finish(
|
|
12992
|
+
_failed_from_api_error(
|
|
12993
|
+
"PUBLISH_PRECHECK_FAILED",
|
|
12994
|
+
api_error,
|
|
12995
|
+
normalized_args=normalized_args,
|
|
12996
|
+
details={"app_key": app_key, "phase": "prepare_publish_edit_version"},
|
|
12997
|
+
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
12998
|
+
)
|
|
12168
12999
|
)
|
|
13000
|
+
edit_version_ms += _duration_ms(edit_version_started_at)
|
|
12169
13001
|
edit_version_no = _coerce_positive_int(version_result.get("editVersionNo") or version_result.get("versionNo")) or 1
|
|
13002
|
+
publish_started_at = time.perf_counter()
|
|
12170
13003
|
try:
|
|
12171
13004
|
self.apps.app_publish(profile=profile, app_key=app_key, payload={"editVersionNo": edit_version_no})
|
|
12172
13005
|
self.apps.app_edit_finished(profile=profile, app_key=app_key, payload={"editVersionNo": edit_version_no})
|
|
12173
13006
|
except (QingflowApiError, RuntimeError) as error:
|
|
13007
|
+
publish_ms += _duration_ms(publish_started_at)
|
|
12174
13008
|
api_error = _coerce_api_error(error)
|
|
12175
|
-
return
|
|
12176
|
-
|
|
12177
|
-
|
|
12178
|
-
|
|
12179
|
-
|
|
12180
|
-
|
|
13009
|
+
return finish(
|
|
13010
|
+
_failed_from_api_error(
|
|
13011
|
+
"PUBLISH_FAILED",
|
|
13012
|
+
api_error,
|
|
13013
|
+
normalized_args=normalized_args,
|
|
13014
|
+
details={"app_key": app_key, "edit_version_no": edit_version_no},
|
|
13015
|
+
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
13016
|
+
)
|
|
12181
13017
|
)
|
|
13018
|
+
publish_ms += _duration_ms(publish_started_at)
|
|
13019
|
+
base_readback_started_at = time.perf_counter()
|
|
12182
13020
|
try:
|
|
12183
13021
|
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
12184
13022
|
except (QingflowApiError, RuntimeError) as error:
|
|
13023
|
+
base_readback_ms += _duration_ms(base_readback_started_at)
|
|
12185
13024
|
api_error = _coerce_api_error(error)
|
|
12186
13025
|
result = _post_write_readback_pending_result(
|
|
12187
13026
|
error_code="PUBLISH_READBACK_PENDING",
|
|
@@ -12194,13 +13033,16 @@ class AiBuilderFacade:
|
|
|
12194
13033
|
http_status=None if api_error.http_status == 404 else api_error.http_status,
|
|
12195
13034
|
)
|
|
12196
13035
|
result.update({"app_key": app_key, "published": None, "verified": False})
|
|
12197
|
-
return result
|
|
13036
|
+
return finish(result)
|
|
13037
|
+
base_readback_ms += _duration_ms(base_readback_started_at)
|
|
12198
13038
|
tag_ids_after = _coerce_int_list(base.get("tagIds"))
|
|
12199
13039
|
app_name_after = str(base.get("formTitle") or base.get("title") or base.get("appName") or app_name_before or "").strip() or None
|
|
12200
13040
|
package_attached = None if not expected_package_tag_id else expected_package_tag_id in tag_ids_after
|
|
13041
|
+
views_readback_started_at = time.perf_counter()
|
|
12201
13042
|
try:
|
|
12202
13043
|
views, views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
12203
13044
|
except (QingflowApiError, RuntimeError) as error:
|
|
13045
|
+
views_readback_ms += _duration_ms(views_readback_started_at)
|
|
12204
13046
|
api_error = _coerce_api_error(error)
|
|
12205
13047
|
result = _post_write_readback_pending_result(
|
|
12206
13048
|
error_code="VIEWS_READBACK_PENDING",
|
|
@@ -12213,7 +13055,8 @@ class AiBuilderFacade:
|
|
|
12213
13055
|
http_status=None if api_error.http_status == 404 else api_error.http_status,
|
|
12214
13056
|
)
|
|
12215
13057
|
result.update({"app_key": app_key, "app_name": app_name_after, "published": bool(base.get("appPublishStatus") in {1, 2}), "verified": False})
|
|
12216
|
-
return result
|
|
13058
|
+
return finish(result)
|
|
13059
|
+
views_readback_ms += _duration_ms(views_readback_started_at)
|
|
12217
13060
|
views = views or []
|
|
12218
13061
|
views_ok = isinstance(views, list) and not views_unavailable
|
|
12219
13062
|
verified = bool(base.get("appPublishStatus") in {1, 2}) and (package_attached is not False) and views_ok
|
|
@@ -12222,36 +13065,38 @@ class AiBuilderFacade:
|
|
|
12222
13065
|
views_unavailable=views_unavailable,
|
|
12223
13066
|
verified=verified,
|
|
12224
13067
|
)
|
|
12225
|
-
return
|
|
12226
|
-
|
|
12227
|
-
|
|
12228
|
-
|
|
12229
|
-
|
|
12230
|
-
|
|
12231
|
-
|
|
12232
|
-
|
|
12233
|
-
|
|
12234
|
-
|
|
12235
|
-
|
|
12236
|
-
|
|
12237
|
-
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
|
|
12242
|
-
|
|
12243
|
-
|
|
12244
|
-
|
|
12245
|
-
|
|
12246
|
-
|
|
12247
|
-
|
|
12248
|
-
|
|
12249
|
-
|
|
12250
|
-
|
|
12251
|
-
|
|
12252
|
-
|
|
12253
|
-
|
|
12254
|
-
|
|
13068
|
+
return finish(
|
|
13069
|
+
{
|
|
13070
|
+
"status": "success" if verified else "partial_success",
|
|
13071
|
+
"error_code": None if not views_unavailable else "VIEWS_READBACK_PENDING",
|
|
13072
|
+
"recoverable": bool(views_unavailable),
|
|
13073
|
+
"message": "published and verified app" if not views_unavailable else "published app; views readback pending",
|
|
13074
|
+
"normalized_args": normalized_args,
|
|
13075
|
+
"missing_fields": [],
|
|
13076
|
+
"allowed_values": {},
|
|
13077
|
+
"details": {},
|
|
13078
|
+
"request_id": None,
|
|
13079
|
+
"suggested_next_call": None
|
|
13080
|
+
if package_attached is not False
|
|
13081
|
+
else {
|
|
13082
|
+
"tool_name": "package_attach_app",
|
|
13083
|
+
"arguments": {"profile": profile, "tag_id": expected_package_tag_id, "app_key": app_key},
|
|
13084
|
+
},
|
|
13085
|
+
"noop": False,
|
|
13086
|
+
"warnings": warnings,
|
|
13087
|
+
"verification": {"published": bool(base.get("appPublishStatus") in {1, 2}), "package_attached": package_attached, "views_ok": views_ok, "views_read_unavailable": views_unavailable},
|
|
13088
|
+
"app_key": app_key,
|
|
13089
|
+
"app_name": app_name_after,
|
|
13090
|
+
"published": bool(base.get("appPublishStatus") in {1, 2}),
|
|
13091
|
+
"package_attached": package_attached,
|
|
13092
|
+
"tag_ids_after": tag_ids_after,
|
|
13093
|
+
"views_ok": views_ok,
|
|
13094
|
+
"verified": verified,
|
|
13095
|
+
"write_executed": True,
|
|
13096
|
+
"write_succeeded": True,
|
|
13097
|
+
"safe_to_retry": False,
|
|
13098
|
+
}
|
|
13099
|
+
)
|
|
12255
13100
|
|
|
12256
13101
|
def _expand_chart_partial_patches(
|
|
12257
13102
|
self,
|
|
@@ -12535,6 +13380,12 @@ class AiBuilderFacade:
|
|
|
12535
13380
|
}
|
|
12536
13381
|
|
|
12537
13382
|
def chart_apply(self, *, profile: str, request: ChartApplyRequest) -> JSONObject:
|
|
13383
|
+
apply_started_at = time.perf_counter()
|
|
13384
|
+
chart_inventory_ms: int | None = None
|
|
13385
|
+
chart_upsert_ms: int | None = None
|
|
13386
|
+
chart_delete_ms: int | None = None
|
|
13387
|
+
chart_reorder_ms: int | None = None
|
|
13388
|
+
chart_readback_ms: int | None = None
|
|
12538
13389
|
normalized_args = request.model_dump(mode="json")
|
|
12539
13390
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
12540
13391
|
app_result = self.app_resolve(profile=profile, app_key=request.app_key)
|
|
@@ -12556,21 +13407,51 @@ class AiBuilderFacade:
|
|
|
12556
13407
|
permission_outcomes.append(permission_outcome)
|
|
12557
13408
|
|
|
12558
13409
|
def finalize(response: JSONObject) -> JSONObject:
|
|
13410
|
+
_merge_duration_breakdown(
|
|
13411
|
+
response,
|
|
13412
|
+
chart_inventory_ms=chart_inventory_ms,
|
|
13413
|
+
chart_upsert_ms=chart_upsert_ms,
|
|
13414
|
+
chart_delete_ms=chart_delete_ms,
|
|
13415
|
+
chart_reorder_ms=chart_reorder_ms,
|
|
13416
|
+
chart_readback_ms=chart_readback_ms,
|
|
13417
|
+
total_ms=_duration_ms(apply_started_at),
|
|
13418
|
+
)
|
|
12559
13419
|
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
12560
13420
|
|
|
12561
13421
|
fields: list[dict[str, Any]] = []
|
|
12562
13422
|
qingbi_fields: list[Any] = []
|
|
12563
13423
|
existing_chart_items: list[Any] = []
|
|
12564
13424
|
existing_chart_list_source: str | None = None
|
|
12565
|
-
|
|
13425
|
+
upsert_charts = list(request.upsert_charts)
|
|
13426
|
+
direct_create_upserts = [
|
|
13427
|
+
patch for patch in upsert_charts if not str(patch.chart_id or "").strip()
|
|
13428
|
+
]
|
|
13429
|
+
explicit_target_upserts = [
|
|
13430
|
+
patch for patch in upsert_charts if str(patch.chart_id or "").strip()
|
|
13431
|
+
]
|
|
13432
|
+
pure_direct_create = bool(direct_create_upserts) and len(direct_create_upserts) == len(upsert_charts) and not (
|
|
13433
|
+
request.patch_charts or request.remove_chart_ids or request.reorder_chart_ids
|
|
13434
|
+
)
|
|
13435
|
+
chart_inventory_skipped = False
|
|
13436
|
+
chart_final_readback_skipped = False
|
|
13437
|
+
needs_schema_and_qingbi_fields = bool(upsert_charts or request.patch_charts)
|
|
13438
|
+
needs_existing_chart_inventory = bool(explicit_target_upserts or request.patch_charts or request.reorder_chart_ids)
|
|
13439
|
+
needs_chart_inventory = bool(needs_schema_and_qingbi_fields or needs_existing_chart_inventory)
|
|
12566
13440
|
if needs_chart_inventory:
|
|
13441
|
+
chart_inventory_started_at = time.perf_counter()
|
|
12567
13442
|
try:
|
|
12568
|
-
|
|
12569
|
-
|
|
12570
|
-
|
|
12571
|
-
|
|
12572
|
-
|
|
13443
|
+
if needs_schema_and_qingbi_fields:
|
|
13444
|
+
schema_state = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
13445
|
+
parsed = schema_state.get("parsed") if isinstance(schema_state.get("parsed"), dict) else {}
|
|
13446
|
+
fields = parsed.get("fields") if isinstance(parsed.get("fields"), list) else []
|
|
13447
|
+
qingbi_fields = self.charts.qingbi_report_list_fields(profile=profile, app_key=app_key).get("items") or []
|
|
13448
|
+
if needs_existing_chart_inventory:
|
|
13449
|
+
existing_chart_items, existing_chart_list_source = self._load_chart_list_for_builder(profile=profile, app_key=app_key)
|
|
13450
|
+
elif direct_create_upserts:
|
|
13451
|
+
chart_inventory_skipped = True
|
|
13452
|
+
existing_chart_list_source = "skipped_for_direct_create"
|
|
12573
13453
|
except (QingflowApiError, RuntimeError) as error:
|
|
13454
|
+
chart_inventory_ms = _duration_ms(chart_inventory_started_at)
|
|
12574
13455
|
api_error = _coerce_api_error(error)
|
|
12575
13456
|
return finalize(_failed_from_api_error(
|
|
12576
13457
|
"CHART_APPLY_FAILED",
|
|
@@ -12579,6 +13460,7 @@ class AiBuilderFacade:
|
|
|
12579
13460
|
details=_with_state_read_blocked_details({"app_key": app_key}, resource="chart", error=api_error),
|
|
12580
13461
|
suggested_next_call={"tool_name": "app_charts_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
12581
13462
|
))
|
|
13463
|
+
chart_inventory_ms = _duration_ms(chart_inventory_started_at)
|
|
12582
13464
|
|
|
12583
13465
|
field_lookup = _build_public_field_lookup(fields)
|
|
12584
13466
|
qingbi_fields_by_id = {
|
|
@@ -12605,7 +13487,6 @@ class AiBuilderFacade:
|
|
|
12605
13487
|
continue
|
|
12606
13488
|
existing_by_name.setdefault(item_name, []).append(deepcopy(item))
|
|
12607
13489
|
|
|
12608
|
-
upsert_charts = list(request.upsert_charts)
|
|
12609
13490
|
if request.patch_charts:
|
|
12610
13491
|
expanded_charts, patch_issues, patch_results = self._expand_chart_partial_patches(
|
|
12611
13492
|
profile=profile,
|
|
@@ -12628,67 +13509,37 @@ class AiBuilderFacade:
|
|
|
12628
13509
|
normalized_args["upsert_charts"] = [patch.model_dump(mode="json") for patch in upsert_charts]
|
|
12629
13510
|
normalized_args["patch_results"] = patch_results
|
|
12630
13511
|
|
|
12631
|
-
|
|
12632
|
-
|
|
12633
|
-
|
|
12634
|
-
|
|
12635
|
-
|
|
12636
|
-
|
|
12637
|
-
|
|
12638
|
-
|
|
12639
|
-
|
|
12640
|
-
|
|
12641
|
-
|
|
12642
|
-
|
|
12643
|
-
f"upsert_charts contains {len(upsert_charts)} items; split into batches of "
|
|
12644
|
-
f"{CHART_APPLY_RECOMMENDED_UPSERT_BATCH_SIZE} or fewer before writing"
|
|
12645
|
-
),
|
|
12646
|
-
"normalized_args": normalized_args,
|
|
12647
|
-
"missing_fields": [],
|
|
12648
|
-
"allowed_values": {"chart.chart_type": [member.value for member in PublicChartType], "chart.filter.operator": [member.value for member in ViewFilterOperator]},
|
|
12649
|
-
"details": batching_context,
|
|
12650
|
-
"request_id": None,
|
|
12651
|
-
"suggested_next_call": {"tool_name": "app_charts_apply", "arguments": {"profile": profile, **first_batch}},
|
|
12652
|
-
"backend_code": None,
|
|
12653
|
-
"http_status": None,
|
|
12654
|
-
"noop": False,
|
|
12655
|
-
"warnings": [
|
|
12656
|
-
_warning(
|
|
12657
|
-
"CHART_UPSERT_BATCH_SIZE_BLOCKED",
|
|
12658
|
-
"large chart upsert batches are blocked before write to avoid timeout and unknown write state",
|
|
12659
|
-
upsert_count=len(upsert_charts),
|
|
12660
|
-
max_upsert_count=CHART_APPLY_RECOMMENDED_UPSERT_BATCH_SIZE,
|
|
12661
|
-
)
|
|
12662
|
-
],
|
|
12663
|
-
"verification": {
|
|
12664
|
-
"charts_verified": False,
|
|
12665
|
-
"readback_unavailable": False,
|
|
12666
|
-
"readback_before_retry": False,
|
|
12667
|
-
"chart_delete_readback_results": [],
|
|
12668
|
-
"chart_order_verified": True,
|
|
12669
|
-
"chart_list_source": existing_chart_list_source,
|
|
12670
|
-
},
|
|
12671
|
-
"app_key": app_key,
|
|
12672
|
-
"app_name": app_name,
|
|
12673
|
-
"chart_results": [],
|
|
12674
|
-
"verified": False,
|
|
12675
|
-
"write_executed": False,
|
|
12676
|
-
"write_succeeded": False,
|
|
12677
|
-
"safe_to_retry": True,
|
|
12678
|
-
"next_action": "split_upsert_charts_and_retry",
|
|
12679
|
-
})
|
|
13512
|
+
large_upsert_warnings = (
|
|
13513
|
+
[
|
|
13514
|
+
_warning(
|
|
13515
|
+
"CHART_UPSERT_BATCH_SIZE_RECOMMENDED",
|
|
13516
|
+
"large chart upsert batch accepted; monitor duration_breakdown_ms and readback before retrying",
|
|
13517
|
+
upsert_count=len(upsert_charts),
|
|
13518
|
+
recommended_upsert_count=CHART_APPLY_RECOMMENDED_UPSERT_BATCH_SIZE,
|
|
13519
|
+
)
|
|
13520
|
+
]
|
|
13521
|
+
if len(upsert_charts) > CHART_APPLY_RECOMMENDED_UPSERT_BATCH_SIZE
|
|
13522
|
+
else []
|
|
13523
|
+
)
|
|
12680
13524
|
|
|
12681
13525
|
chart_results: list[dict[str, Any]] = []
|
|
12682
13526
|
created_ids: list[str] = []
|
|
12683
13527
|
updated_ids: list[str] = []
|
|
12684
13528
|
removed_ids: list[str] = []
|
|
12685
13529
|
failed_items: list[dict[str, Any]] = []
|
|
13530
|
+
skipped_after_timeout_count = 0
|
|
13531
|
+
stopped_after_timeout = False
|
|
12686
13532
|
delete_readback_issues: list[dict[str, Any]] = []
|
|
13533
|
+
write_attempted = False
|
|
13534
|
+
created_ids_from_create_response: list[str] = []
|
|
13535
|
+
create_id_readback_fallback_used = False
|
|
12687
13536
|
|
|
12688
|
-
|
|
13537
|
+
chart_upsert_started_at = time.perf_counter()
|
|
13538
|
+
for patch_index, patch in enumerate(upsert_charts):
|
|
12689
13539
|
chart_id = ""
|
|
12690
13540
|
target_type = ""
|
|
12691
13541
|
config_payload: dict[str, Any] | None = None
|
|
13542
|
+
patch_write_attempted = False
|
|
12692
13543
|
try:
|
|
12693
13544
|
dataset_source = _chart_patch_dataset_source_type(patch)
|
|
12694
13545
|
if dataset_source:
|
|
@@ -12708,7 +13559,7 @@ class AiBuilderFacade:
|
|
|
12708
13559
|
existing = existing_by_id.get(str(patch.chart_id))
|
|
12709
13560
|
if existing is None:
|
|
12710
13561
|
raise ValueError(f"chart_id '{patch.chart_id}' was not found under app '{app_key}'")
|
|
12711
|
-
if existing is None:
|
|
13562
|
+
if existing is None and needs_existing_chart_inventory:
|
|
12712
13563
|
name_matches = list(existing_by_name.get(patch.name) or [])
|
|
12713
13564
|
if len(name_matches) > 1:
|
|
12714
13565
|
raise ValueError(
|
|
@@ -12737,6 +13588,7 @@ class AiBuilderFacade:
|
|
|
12737
13588
|
)
|
|
12738
13589
|
if existing is None:
|
|
12739
13590
|
temp_chart_id = str(patch.chart_id or f"mcp_{uuid4().hex[:16]}")
|
|
13591
|
+
chart_id = temp_chart_id
|
|
12740
13592
|
create_payload = {
|
|
12741
13593
|
"chartId": temp_chart_id,
|
|
12742
13594
|
"chartName": patch.name,
|
|
@@ -12750,9 +13602,13 @@ class AiBuilderFacade:
|
|
|
12750
13602
|
"editAuthType": "ws",
|
|
12751
13603
|
"editAuthIncludeSubDept": True,
|
|
12752
13604
|
}
|
|
13605
|
+
write_attempted = True
|
|
13606
|
+
patch_write_attempted = True
|
|
12753
13607
|
create_result = self.charts.qingbi_report_create(profile=profile, payload=create_payload).get("result") or {}
|
|
12754
13608
|
created_chart_id = _extract_chart_identifier(create_result or {})
|
|
13609
|
+
created_id_from_response = bool(created_chart_id)
|
|
12755
13610
|
if not created_chart_id:
|
|
13611
|
+
create_id_readback_fallback_used = True
|
|
12756
13612
|
try:
|
|
12757
13613
|
refreshed_items, _ = self._load_chart_list_for_builder(profile=profile, app_key=app_key)
|
|
12758
13614
|
refreshed_matches = _find_charts_by_name(
|
|
@@ -12774,6 +13630,8 @@ class AiBuilderFacade:
|
|
|
12774
13630
|
)
|
|
12775
13631
|
chart_id = created_chart_id
|
|
12776
13632
|
created_ids.append(chart_id)
|
|
13633
|
+
if created_id_from_response:
|
|
13634
|
+
created_ids_from_create_response.append(chart_id)
|
|
12777
13635
|
created_chart = {
|
|
12778
13636
|
"chartId": chart_id,
|
|
12779
13637
|
"chartName": patch.name,
|
|
@@ -12783,6 +13641,8 @@ class AiBuilderFacade:
|
|
|
12783
13641
|
existing_by_name.setdefault(patch.name, []).append(deepcopy(created_chart))
|
|
12784
13642
|
elif existing_name != patch.name or existing_type != target_type or isinstance(chart_visible_auth, dict):
|
|
12785
13643
|
base_info = self.charts.qingbi_report_get_base(profile=profile, chart_id=chart_id).get("result") or {}
|
|
13644
|
+
write_attempted = True
|
|
13645
|
+
patch_write_attempted = True
|
|
12786
13646
|
self.charts.qingbi_report_update_base(
|
|
12787
13647
|
profile=profile,
|
|
12788
13648
|
chart_id=chart_id,
|
|
@@ -12821,11 +13681,15 @@ class AiBuilderFacade:
|
|
|
12821
13681
|
|
|
12822
13682
|
config_updated = False
|
|
12823
13683
|
if existing is None or config_update_requested:
|
|
13684
|
+
write_attempted = True
|
|
13685
|
+
patch_write_attempted = True
|
|
12824
13686
|
self.charts.qingbi_report_update_config(profile=profile, chart_id=chart_id, payload=config_payload or {})
|
|
12825
13687
|
config_updated = True
|
|
12826
13688
|
if existing is not None and chart_id not in updated_ids and config_updated:
|
|
12827
13689
|
updated_ids.append(chart_id)
|
|
12828
13690
|
if patch.question_config:
|
|
13691
|
+
write_attempted = True
|
|
13692
|
+
patch_write_attempted = True
|
|
12829
13693
|
self._request_backend(
|
|
12830
13694
|
profile=profile,
|
|
12831
13695
|
method="POST",
|
|
@@ -12833,6 +13697,8 @@ class AiBuilderFacade:
|
|
|
12833
13697
|
json_body=patch.question_config,
|
|
12834
13698
|
)
|
|
12835
13699
|
if patch.user_config:
|
|
13700
|
+
write_attempted = True
|
|
13701
|
+
patch_write_attempted = True
|
|
12836
13702
|
self._request_backend(
|
|
12837
13703
|
profile=profile,
|
|
12838
13704
|
method="POST",
|
|
@@ -12872,9 +13738,30 @@ class AiBuilderFacade:
|
|
|
12872
13738
|
}
|
|
12873
13739
|
failed_items.append(failure)
|
|
12874
13740
|
chart_results.append(failure)
|
|
13741
|
+
if api_error is not None and patch_write_attempted and _is_uncertain_write_transport_error(api_error):
|
|
13742
|
+
stopped_after_timeout = True
|
|
13743
|
+
remaining_upserts = upsert_charts[patch_index + 1 :]
|
|
13744
|
+
skipped_after_timeout_count = len(remaining_upserts)
|
|
13745
|
+
for remaining_patch in remaining_upserts:
|
|
13746
|
+
skipped = {
|
|
13747
|
+
"chart_id": str(remaining_patch.chart_id or ""),
|
|
13748
|
+
"name": remaining_patch.name,
|
|
13749
|
+
"chart_type": remaining_patch.chart_type.value,
|
|
13750
|
+
"status": "skipped",
|
|
13751
|
+
"error_code": "CHART_APPLY_SKIPPED_AFTER_TIMEOUT",
|
|
13752
|
+
"message": "skipped because a previous chart write timed out with uncertain backend state; read back charts before retrying remaining items",
|
|
13753
|
+
"depends_on_failed_chart": patch.name,
|
|
13754
|
+
"readback_before_retry": True,
|
|
13755
|
+
}
|
|
13756
|
+
failed_items.append(skipped)
|
|
13757
|
+
chart_results.append(skipped)
|
|
13758
|
+
break
|
|
13759
|
+
chart_upsert_ms = _duration_ms(chart_upsert_started_at)
|
|
12875
13760
|
|
|
13761
|
+
chart_delete_started_at = time.perf_counter()
|
|
12876
13762
|
for chart_id in request.remove_chart_ids:
|
|
12877
13763
|
try:
|
|
13764
|
+
write_attempted = True
|
|
12878
13765
|
self.charts.qingbi_report_delete(profile=profile, chart_id=chart_id)
|
|
12879
13766
|
removed_ids.append(chart_id)
|
|
12880
13767
|
delete_result = self._verify_chart_deleted_by_id(profile=profile, chart_id=chart_id)
|
|
@@ -12896,8 +13783,10 @@ class AiBuilderFacade:
|
|
|
12896
13783
|
}
|
|
12897
13784
|
failed_items.append(failure)
|
|
12898
13785
|
chart_results.append(failure)
|
|
13786
|
+
chart_delete_ms = _duration_ms(chart_delete_started_at)
|
|
12899
13787
|
|
|
12900
13788
|
reordered = False
|
|
13789
|
+
chart_reorder_started_at = time.perf_counter()
|
|
12901
13790
|
if request.reorder_chart_ids:
|
|
12902
13791
|
try:
|
|
12903
13792
|
current_order = [
|
|
@@ -12907,6 +13796,7 @@ class AiBuilderFacade:
|
|
|
12907
13796
|
]
|
|
12908
13797
|
desired_display_order = list(request.reorder_chart_ids) + [chart_id for chart_id in current_order if chart_id not in request.reorder_chart_ids]
|
|
12909
13798
|
backend_reorder_ids = list(reversed(desired_display_order))
|
|
13799
|
+
write_attempted = True
|
|
12910
13800
|
self.charts.qingbi_report_reorder(profile=profile, app_key=app_key, chart_ids=backend_reorder_ids)
|
|
12911
13801
|
reordered = True
|
|
12912
13802
|
except (QingflowApiError, RuntimeError) as error:
|
|
@@ -12921,17 +13811,34 @@ class AiBuilderFacade:
|
|
|
12921
13811
|
}
|
|
12922
13812
|
failed_items.append(failure)
|
|
12923
13813
|
chart_results.append(failure)
|
|
13814
|
+
chart_reorder_ms = _duration_ms(chart_reorder_started_at)
|
|
12924
13815
|
|
|
12925
13816
|
noop = not created_ids and not updated_ids and not removed_ids and not reordered and not failed_items
|
|
12926
|
-
|
|
12927
|
-
|
|
12928
|
-
|
|
13817
|
+
confirmed_write = bool(created_ids or updated_ids or removed_ids or reordered)
|
|
13818
|
+
write_executed = bool(confirmed_write or write_attempted)
|
|
13819
|
+
write_succeeded = confirmed_write
|
|
13820
|
+
chart_final_readback_skipped = bool(
|
|
13821
|
+
pure_direct_create
|
|
13822
|
+
and created_ids
|
|
13823
|
+
and not updated_ids
|
|
13824
|
+
and not removed_ids
|
|
13825
|
+
and not reordered
|
|
13826
|
+
and not failed_items
|
|
13827
|
+
and not create_id_readback_fallback_used
|
|
13828
|
+
and len(created_ids_from_create_response) == len(created_ids) == len(direct_create_upserts)
|
|
13829
|
+
)
|
|
13830
|
+
needs_list_readback = bool(created_ids or updated_ids or reordered) and not chart_final_readback_skipped
|
|
12929
13831
|
delete_readback_unavailable = any(item.get("readback_status") == "unavailable" for item in delete_readback_issues)
|
|
12930
13832
|
deletes_verified = not delete_readback_issues
|
|
12931
13833
|
readback_unavailable = False
|
|
12932
13834
|
readback_error: QingflowApiError | None = None
|
|
12933
13835
|
readback_list_source: str | None = existing_chart_list_source
|
|
12934
|
-
if
|
|
13836
|
+
if chart_final_readback_skipped:
|
|
13837
|
+
verified = True
|
|
13838
|
+
chart_readback_ms = 0
|
|
13839
|
+
readback_list_source = "skipped_for_direct_create"
|
|
13840
|
+
elif needs_list_readback:
|
|
13841
|
+
chart_readback_started_at = time.perf_counter()
|
|
12935
13842
|
try:
|
|
12936
13843
|
readback_items, readback_list_source = self._load_chart_list_for_builder(profile=profile, app_key=app_key)
|
|
12937
13844
|
readback_ids = {
|
|
@@ -12954,8 +13861,10 @@ class AiBuilderFacade:
|
|
|
12954
13861
|
verified = False
|
|
12955
13862
|
readback_unavailable = True
|
|
12956
13863
|
readback_list_source = None
|
|
13864
|
+
chart_readback_ms = _duration_ms(chart_readback_started_at)
|
|
12957
13865
|
else:
|
|
12958
13866
|
verified = True
|
|
13867
|
+
chart_readback_ms = 0
|
|
12959
13868
|
verified = verified and deletes_verified
|
|
12960
13869
|
any_readback_unavailable = readback_unavailable or delete_readback_unavailable
|
|
12961
13870
|
|
|
@@ -12983,6 +13892,10 @@ class AiBuilderFacade:
|
|
|
12983
13892
|
"allowed_values": {"chart.chart_type": [member.value for member in PublicChartType], "chart.filter.operator": [member.value for member in ViewFilterOperator]},
|
|
12984
13893
|
"details": {
|
|
12985
13894
|
"per_chart_results": chart_results,
|
|
13895
|
+
"stopped_after_timeout": stopped_after_timeout,
|
|
13896
|
+
"skipped_after_timeout_count": skipped_after_timeout_count,
|
|
13897
|
+
"inventory_readback_skipped": chart_inventory_skipped,
|
|
13898
|
+
"final_readback_skipped": chart_final_readback_skipped,
|
|
12986
13899
|
**retry_context,
|
|
12987
13900
|
**({"readback_error": _transport_error_payload(readback_error)} if readback_error is not None else {}),
|
|
12988
13901
|
},
|
|
@@ -12991,7 +13904,7 @@ class AiBuilderFacade:
|
|
|
12991
13904
|
"backend_code": failed_items[0].get("backend_code") or (readback_error.backend_code if readback_error is not None else None),
|
|
12992
13905
|
"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),
|
|
12993
13906
|
"noop": noop,
|
|
12994
|
-
"warnings": _chart_apply_warnings(
|
|
13907
|
+
"warnings": large_upsert_warnings + _chart_apply_warnings(
|
|
12995
13908
|
failed_items=failed_items,
|
|
12996
13909
|
readback_unavailable=readback_unavailable,
|
|
12997
13910
|
verified=False if failed_items else verified,
|
|
@@ -13004,6 +13917,8 @@ class AiBuilderFacade:
|
|
|
13004
13917
|
"chart_delete_readback_results": [deepcopy(item) for item in chart_results if item.get("operation") == "delete"],
|
|
13005
13918
|
"chart_order_verified": False if request.reorder_chart_ids else True,
|
|
13006
13919
|
"chart_list_source": readback_list_source or existing_chart_list_source,
|
|
13920
|
+
"inventory_readback_skipped": chart_inventory_skipped,
|
|
13921
|
+
"final_readback_skipped": chart_final_readback_skipped,
|
|
13007
13922
|
},
|
|
13008
13923
|
"app_key": app_key,
|
|
13009
13924
|
"app_name": app_name,
|
|
@@ -13031,13 +13946,18 @@ class AiBuilderFacade:
|
|
|
13031
13946
|
"normalized_args": normalized_args,
|
|
13032
13947
|
"missing_fields": [],
|
|
13033
13948
|
"allowed_values": {"chart.chart_type": [member.value for member in PublicChartType], "chart.filter.operator": [member.value for member in ViewFilterOperator]},
|
|
13034
|
-
"details": {
|
|
13949
|
+
"details": {
|
|
13950
|
+
"inventory_readback_skipped": chart_inventory_skipped,
|
|
13951
|
+
"final_readback_skipped": chart_final_readback_skipped,
|
|
13952
|
+
**({"readback_skip_reason": "direct_create_response_ids"} if chart_final_readback_skipped else {}),
|
|
13953
|
+
**({"readback_error": _transport_error_payload(readback_error)} if readback_error is not None else {}),
|
|
13954
|
+
},
|
|
13035
13955
|
"request_id": readback_error.request_id if readback_error is not None else None,
|
|
13036
13956
|
"suggested_next_call": None if result_verified else pending_suggestion,
|
|
13037
13957
|
"backend_code": readback_error.backend_code if readback_error is not None else None,
|
|
13038
13958
|
"http_status": None if readback_error is None or readback_error.http_status == 404 else readback_error.http_status,
|
|
13039
13959
|
"noop": noop,
|
|
13040
|
-
"warnings": _chart_apply_warnings(
|
|
13960
|
+
"warnings": large_upsert_warnings + _chart_apply_warnings(
|
|
13041
13961
|
failed_items=[],
|
|
13042
13962
|
readback_unavailable=False if noop else readback_unavailable,
|
|
13043
13963
|
verified=result_verified,
|
|
@@ -13049,6 +13969,8 @@ class AiBuilderFacade:
|
|
|
13049
13969
|
"chart_delete_readback_results": [deepcopy(item) for item in chart_results if item.get("operation") == "delete"],
|
|
13050
13970
|
"chart_order_verified": (readback_list_source == "sorted" and result_verified) if request.reorder_chart_ids else True,
|
|
13051
13971
|
"chart_list_source": existing_chart_list_source if noop else readback_list_source,
|
|
13972
|
+
"inventory_readback_skipped": chart_inventory_skipped,
|
|
13973
|
+
"final_readback_skipped": chart_final_readback_skipped,
|
|
13052
13974
|
},
|
|
13053
13975
|
"app_key": app_key,
|
|
13054
13976
|
"app_name": app_name,
|
|
@@ -13060,6 +13982,15 @@ class AiBuilderFacade:
|
|
|
13060
13982
|
})
|
|
13061
13983
|
|
|
13062
13984
|
def portal_apply(self, *, profile: str, request: PortalApplyRequest) -> JSONObject:
|
|
13985
|
+
apply_started_at = time.perf_counter()
|
|
13986
|
+
portal_initial_read_ms: int | None = None
|
|
13987
|
+
portal_create_ms: int | None = None
|
|
13988
|
+
portal_component_build_ms: int | None = None
|
|
13989
|
+
portal_update_ms: int | None = None
|
|
13990
|
+
portal_base_info_update_ms: int | None = None
|
|
13991
|
+
portal_draft_readback_ms: int | None = None
|
|
13992
|
+
portal_publish_ms: int | None = None
|
|
13993
|
+
portal_live_readback_ms: int | None = None
|
|
13063
13994
|
normalized_args = request.model_dump(mode="json")
|
|
13064
13995
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
13065
13996
|
dash_key = str(request.dash_key or "").strip()
|
|
@@ -13089,8 +14020,10 @@ class AiBuilderFacade:
|
|
|
13089
14020
|
suggested_next_call=None,
|
|
13090
14021
|
)
|
|
13091
14022
|
try:
|
|
14023
|
+
portal_initial_read_started_at = time.perf_counter()
|
|
13092
14024
|
base_payload = self.portals.portal_get(profile=profile, dash_key=dash_key, being_draft=True).get("result") if dash_key else {}
|
|
13093
14025
|
except (QingflowApiError, RuntimeError) as error:
|
|
14026
|
+
portal_initial_read_ms = _duration_ms(portal_initial_read_started_at)
|
|
13094
14027
|
api_error = _coerce_api_error(error)
|
|
13095
14028
|
return _failed(
|
|
13096
14029
|
"PORTAL_APPLY_FAILED",
|
|
@@ -13102,6 +14035,7 @@ class AiBuilderFacade:
|
|
|
13102
14035
|
backend_code=api_error.backend_code,
|
|
13103
14036
|
http_status=None if api_error.http_status == 404 else api_error.http_status,
|
|
13104
14037
|
)
|
|
14038
|
+
portal_initial_read_ms = _duration_ms(portal_initial_read_started_at) if dash_key else 0
|
|
13105
14039
|
if not isinstance(base_payload, dict):
|
|
13106
14040
|
base_payload = {}
|
|
13107
14041
|
if not creating:
|
|
@@ -13116,6 +14050,18 @@ class AiBuilderFacade:
|
|
|
13116
14050
|
permission_outcomes.append(portal_permission_outcome)
|
|
13117
14051
|
|
|
13118
14052
|
def finalize(response: JSONObject) -> JSONObject:
|
|
14053
|
+
_merge_duration_breakdown(
|
|
14054
|
+
response,
|
|
14055
|
+
portal_initial_read_ms=portal_initial_read_ms,
|
|
14056
|
+
portal_create_ms=portal_create_ms,
|
|
14057
|
+
portal_component_build_ms=portal_component_build_ms,
|
|
14058
|
+
portal_update_ms=portal_update_ms,
|
|
14059
|
+
portal_base_info_update_ms=portal_base_info_update_ms,
|
|
14060
|
+
portal_draft_readback_ms=portal_draft_readback_ms,
|
|
14061
|
+
portal_publish_ms=portal_publish_ms,
|
|
14062
|
+
portal_live_readback_ms=portal_live_readback_ms,
|
|
14063
|
+
total_ms=_duration_ms(apply_started_at),
|
|
14064
|
+
)
|
|
13119
14065
|
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
13120
14066
|
target_package_tag_id = request.package_tag_id
|
|
13121
14067
|
if target_package_tag_id is None:
|
|
@@ -13175,7 +14121,9 @@ class AiBuilderFacade:
|
|
|
13175
14121
|
config=request.config,
|
|
13176
14122
|
base_payload=None,
|
|
13177
14123
|
)
|
|
14124
|
+
portal_create_started_at = time.perf_counter()
|
|
13178
14125
|
create_result = self.portals.portal_create(profile=profile, payload=create_payload)
|
|
14126
|
+
portal_create_ms = _duration_ms(portal_create_started_at)
|
|
13179
14127
|
write_executed = True
|
|
13180
14128
|
created = create_result.get("result") if isinstance(create_result.get("result"), dict) else {}
|
|
13181
14129
|
dash_key = str(created.get("dashKey") or "")
|
|
@@ -13208,36 +14156,61 @@ class AiBuilderFacade:
|
|
|
13208
14156
|
base_payload=base_payload,
|
|
13209
14157
|
)
|
|
13210
14158
|
if sections_requested:
|
|
14159
|
+
portal_component_build_started_at = time.perf_counter()
|
|
13211
14160
|
component_payload, layout_metadata, inline_chart_results = self._build_portal_components_from_sections(
|
|
13212
14161
|
profile=profile,
|
|
13213
14162
|
sections=request.sections,
|
|
13214
14163
|
layout_preset=request.layout_preset,
|
|
13215
14164
|
inline_chart_results=inline_chart_results,
|
|
13216
14165
|
)
|
|
14166
|
+
portal_component_build_ms = _duration_ms(portal_component_build_started_at)
|
|
13217
14167
|
layout_diagnostics = _portal_layout_diagnostics(request.sections, component_payload, layout_metadata=layout_metadata)
|
|
13218
14168
|
update_payload["components"] = component_payload
|
|
14169
|
+
portal_update_started_at = time.perf_counter()
|
|
13219
14170
|
self.portals.portal_update(profile=profile, dash_key=dash_key, payload=update_payload)
|
|
14171
|
+
portal_update_ms = _duration_ms(portal_update_started_at)
|
|
13220
14172
|
write_executed = True
|
|
13221
|
-
|
|
13222
|
-
|
|
13223
|
-
|
|
13224
|
-
|
|
13225
|
-
|
|
13226
|
-
|
|
13227
|
-
|
|
13228
|
-
|
|
13229
|
-
|
|
13230
|
-
|
|
13231
|
-
|
|
14173
|
+
else:
|
|
14174
|
+
portal_component_build_ms = 0
|
|
14175
|
+
portal_update_ms = 0
|
|
14176
|
+
base_info_update_requested = bool(
|
|
14177
|
+
(not creating and not sections_requested)
|
|
14178
|
+
or request.dash_name is not None
|
|
14179
|
+
or request.icon is not None
|
|
14180
|
+
or request.color is not None
|
|
14181
|
+
or request.visibility is not None
|
|
14182
|
+
or request.auth is not None
|
|
14183
|
+
or request.package_tag_id is not None
|
|
14184
|
+
)
|
|
14185
|
+
if base_info_update_requested:
|
|
14186
|
+
portal_base_info_update_started_at = time.perf_counter()
|
|
14187
|
+
self.portals.portal_update_base_info(
|
|
14188
|
+
profile=profile,
|
|
14189
|
+
dash_key=dash_key,
|
|
14190
|
+
payload={
|
|
14191
|
+
"dashName": update_payload.get("dashName"),
|
|
14192
|
+
"dashIcon": update_payload.get("dashIcon"),
|
|
14193
|
+
"auth": deepcopy(update_payload.get("auth")),
|
|
14194
|
+
"tags": deepcopy(update_payload.get("tags") or []),
|
|
14195
|
+
},
|
|
14196
|
+
)
|
|
14197
|
+
portal_base_info_update_ms = _duration_ms(portal_base_info_update_started_at)
|
|
14198
|
+
write_executed = True
|
|
14199
|
+
else:
|
|
14200
|
+
portal_base_info_update_ms = 0
|
|
13232
14201
|
try:
|
|
14202
|
+
portal_draft_readback_started_at = time.perf_counter()
|
|
13233
14203
|
draft_result = self.portals.portal_get(profile=profile, dash_key=dash_key, being_draft=True).get("result") or {}
|
|
13234
14204
|
except (QingflowApiError, RuntimeError) as read_error:
|
|
14205
|
+
portal_draft_readback_ms = _duration_ms(portal_draft_readback_started_at)
|
|
13235
14206
|
api_read_error = _coerce_api_error(read_error)
|
|
13236
14207
|
draft_readback_error = {
|
|
13237
14208
|
"phase": "portal_draft_readback",
|
|
13238
14209
|
"transport_error": _transport_error_payload(api_read_error),
|
|
13239
14210
|
}
|
|
13240
14211
|
draft_result = {}
|
|
14212
|
+
else:
|
|
14213
|
+
portal_draft_readback_ms = _duration_ms(portal_draft_readback_started_at)
|
|
13241
14214
|
except (QingflowApiError, RuntimeError, ValueError) as error:
|
|
13242
14215
|
api_error = _coerce_api_error(error) if not isinstance(error, ValueError) else None
|
|
13243
14216
|
inline_chart_write_executed = any(bool(item.get("write_executed")) for item in inline_chart_results)
|
|
@@ -13313,9 +14286,12 @@ class AiBuilderFacade:
|
|
|
13313
14286
|
publish_error: JSONObject | None = None
|
|
13314
14287
|
if request.publish:
|
|
13315
14288
|
try:
|
|
14289
|
+
portal_publish_started_at = time.perf_counter()
|
|
13316
14290
|
self.portals.portal_publish(profile=profile, dash_key=dash_key)
|
|
14291
|
+
portal_publish_ms = _duration_ms(portal_publish_started_at)
|
|
13317
14292
|
published = True
|
|
13318
14293
|
except (QingflowApiError, RuntimeError) as error:
|
|
14294
|
+
portal_publish_ms = _duration_ms(portal_publish_started_at)
|
|
13319
14295
|
api_error = _coerce_api_error(error)
|
|
13320
14296
|
publish_failed = True
|
|
13321
14297
|
publish_error = {
|
|
@@ -13324,13 +14300,20 @@ class AiBuilderFacade:
|
|
|
13324
14300
|
}
|
|
13325
14301
|
if published:
|
|
13326
14302
|
try:
|
|
14303
|
+
portal_live_readback_started_at = time.perf_counter()
|
|
13327
14304
|
live_result = self.portals.portal_get(profile=profile, dash_key=dash_key, being_draft=False).get("result") or {}
|
|
13328
14305
|
except (QingflowApiError, RuntimeError) as read_error:
|
|
14306
|
+
portal_live_readback_ms = _duration_ms(portal_live_readback_started_at)
|
|
13329
14307
|
api_read_error = _coerce_api_error(read_error)
|
|
13330
14308
|
live_readback_error = {
|
|
13331
14309
|
"phase": "portal_live_readback",
|
|
13332
14310
|
"transport_error": _transport_error_payload(api_read_error),
|
|
13333
14311
|
}
|
|
14312
|
+
else:
|
|
14313
|
+
portal_live_readback_ms = _duration_ms(portal_live_readback_started_at)
|
|
14314
|
+
else:
|
|
14315
|
+
portal_publish_ms = 0
|
|
14316
|
+
portal_live_readback_ms = 0
|
|
13334
14317
|
|
|
13335
14318
|
draft_components = draft_result.get("components") if isinstance(draft_result, dict) else None
|
|
13336
14319
|
expected_count = len(request.sections) if sections_requested else None
|
|
@@ -13469,55 +14452,79 @@ class AiBuilderFacade:
|
|
|
13469
14452
|
})
|
|
13470
14453
|
|
|
13471
14454
|
def portal_delete(self, *, profile: str, dash_key: str) -> JSONObject:
|
|
14455
|
+
apply_started_at = time.perf_counter()
|
|
14456
|
+
portal_delete_ms = 0
|
|
14457
|
+
portal_delete_readback_ms = 0
|
|
13472
14458
|
normalized_args = {"dash_key": dash_key}
|
|
14459
|
+
|
|
14460
|
+
def finish(response: JSONObject) -> JSONObject:
|
|
14461
|
+
_merge_duration_breakdown(
|
|
14462
|
+
response,
|
|
14463
|
+
portal_delete_ms=portal_delete_ms,
|
|
14464
|
+
portal_delete_readback_ms=portal_delete_readback_ms,
|
|
14465
|
+
total_ms=_duration_ms(apply_started_at),
|
|
14466
|
+
)
|
|
14467
|
+
return response
|
|
14468
|
+
|
|
13473
14469
|
dash_key = str(dash_key or "").strip()
|
|
13474
14470
|
if not dash_key:
|
|
13475
|
-
return
|
|
13476
|
-
|
|
13477
|
-
|
|
13478
|
-
|
|
13479
|
-
|
|
13480
|
-
|
|
14471
|
+
return finish(
|
|
14472
|
+
_failed(
|
|
14473
|
+
"PORTAL_DASH_KEY_REQUIRED",
|
|
14474
|
+
"dash_key is required when deleting a portal",
|
|
14475
|
+
normalized_args=normalized_args,
|
|
14476
|
+
missing_fields=["dash_key"],
|
|
14477
|
+
suggested_next_call={"tool_name": "portal_delete", "arguments": {"profile": profile, "dash_key": "DASH_KEY"}},
|
|
14478
|
+
)
|
|
13481
14479
|
)
|
|
14480
|
+
portal_delete_started_at = time.perf_counter()
|
|
13482
14481
|
try:
|
|
13483
14482
|
self.portals.portal_delete(profile=profile, dash_key=dash_key)
|
|
13484
14483
|
except (QingflowApiError, RuntimeError) as error:
|
|
14484
|
+
portal_delete_ms += _duration_ms(portal_delete_started_at)
|
|
13485
14485
|
api_error = _coerce_api_error(error)
|
|
13486
|
-
return
|
|
13487
|
-
|
|
13488
|
-
|
|
13489
|
-
|
|
13490
|
-
|
|
13491
|
-
|
|
14486
|
+
return finish(
|
|
14487
|
+
_failed_from_api_error(
|
|
14488
|
+
"PORTAL_DELETE_FAILED",
|
|
14489
|
+
api_error,
|
|
14490
|
+
normalized_args=normalized_args,
|
|
14491
|
+
details={"dash_key": dash_key},
|
|
14492
|
+
suggested_next_call={"tool_name": "portal_delete", "arguments": {"profile": profile, **normalized_args}},
|
|
14493
|
+
)
|
|
13492
14494
|
)
|
|
14495
|
+
portal_delete_ms += _duration_ms(portal_delete_started_at)
|
|
13493
14496
|
|
|
14497
|
+
portal_delete_readback_started_at = time.perf_counter()
|
|
13494
14498
|
delete_readback = self._verify_portal_deleted_by_key(profile=profile, dash_key=dash_key)
|
|
14499
|
+
portal_delete_readback_ms += _duration_ms(portal_delete_readback_started_at)
|
|
13495
14500
|
verified = delete_readback.get("readback_status") == "deleted"
|
|
13496
|
-
return
|
|
13497
|
-
|
|
13498
|
-
|
|
13499
|
-
|
|
13500
|
-
|
|
13501
|
-
|
|
13502
|
-
|
|
13503
|
-
|
|
13504
|
-
|
|
13505
|
-
|
|
13506
|
-
|
|
13507
|
-
|
|
13508
|
-
|
|
13509
|
-
|
|
13510
|
-
|
|
13511
|
-
|
|
13512
|
-
|
|
13513
|
-
|
|
13514
|
-
|
|
13515
|
-
|
|
13516
|
-
|
|
13517
|
-
|
|
13518
|
-
|
|
13519
|
-
|
|
13520
|
-
|
|
14501
|
+
return finish(
|
|
14502
|
+
{
|
|
14503
|
+
"status": "success" if verified else "partial_success",
|
|
14504
|
+
"error_code": None if verified else delete_readback.get("error_code") or "PORTAL_DELETE_READBACK_PENDING",
|
|
14505
|
+
"recoverable": not verified,
|
|
14506
|
+
"message": "deleted portal" if verified else "portal delete completed; readback pending",
|
|
14507
|
+
"normalized_args": normalized_args,
|
|
14508
|
+
"missing_fields": [],
|
|
14509
|
+
"allowed_values": {},
|
|
14510
|
+
"details": {} if verified else {"delete_readback": delete_readback},
|
|
14511
|
+
"request_id": delete_readback.get("request_id"),
|
|
14512
|
+
"backend_code": delete_readback.get("backend_code"),
|
|
14513
|
+
"http_status": delete_readback.get("http_status"),
|
|
14514
|
+
"suggested_next_call": None if verified else {"tool_name": "portal_get", "arguments": {"profile": profile, "dash_key": dash_key}},
|
|
14515
|
+
"noop": False,
|
|
14516
|
+
"warnings": [] if verified else [_warning("PORTAL_DELETE_READBACK_PENDING", "portal delete was sent, but deletion readback is not fully verified")],
|
|
14517
|
+
"verification": {"portal_deleted": verified, "delete_readback": delete_readback},
|
|
14518
|
+
"verified": verified,
|
|
14519
|
+
"dash_key": dash_key,
|
|
14520
|
+
"deleted": verified,
|
|
14521
|
+
"delete_executed": True,
|
|
14522
|
+
"readback_status": delete_readback.get("readback_status"),
|
|
14523
|
+
"safe_to_retry_delete": False,
|
|
14524
|
+
"write_executed": True,
|
|
14525
|
+
"safe_to_retry": False,
|
|
14526
|
+
}
|
|
14527
|
+
)
|
|
13521
14528
|
|
|
13522
14529
|
def _verify_portal_deleted_by_key(self, *, profile: str, dash_key: str) -> JSONObject:
|
|
13523
14530
|
try:
|
|
@@ -13920,36 +14927,30 @@ class AiBuilderFacade:
|
|
|
13920
14927
|
app_key: str,
|
|
13921
14928
|
app_name: str,
|
|
13922
14929
|
package_tag_id: int | None,
|
|
13923
|
-
create_if_missing: bool,
|
|
13924
14930
|
) -> JSONObject:
|
|
13925
14931
|
if app_key:
|
|
13926
14932
|
return self.app_resolve(profile=profile, app_key=app_key)
|
|
13927
14933
|
if app_name:
|
|
13928
|
-
|
|
13929
|
-
|
|
13930
|
-
|
|
13931
|
-
|
|
13932
|
-
|
|
13933
|
-
|
|
14934
|
+
return {
|
|
14935
|
+
"status": "success",
|
|
14936
|
+
"error_code": None,
|
|
14937
|
+
"recoverable": False,
|
|
14938
|
+
"message": "target app would be created",
|
|
14939
|
+
"normalized_args": {},
|
|
14940
|
+
"missing_fields": [],
|
|
14941
|
+
"allowed_values": {},
|
|
14942
|
+
"details": {},
|
|
14943
|
+
"request_id": None,
|
|
14944
|
+
"suggested_next_call": None,
|
|
14945
|
+
"noop": False,
|
|
14946
|
+
"verification": {},
|
|
14947
|
+
"app_key": "",
|
|
14948
|
+
"app_name": app_name,
|
|
14949
|
+
"tag_ids": [package_tag_id] if package_tag_id else [],
|
|
14950
|
+
"would_create": True,
|
|
14951
|
+
}
|
|
14952
|
+
else:
|
|
13934
14953
|
return _failed("APP_NAME_REQUIRED", "app_name or app_key is required", suggested_next_call=None)
|
|
13935
|
-
return {
|
|
13936
|
-
"status": "success",
|
|
13937
|
-
"error_code": None,
|
|
13938
|
-
"recoverable": False,
|
|
13939
|
-
"message": "target app would be created",
|
|
13940
|
-
"normalized_args": {},
|
|
13941
|
-
"missing_fields": [],
|
|
13942
|
-
"allowed_values": {},
|
|
13943
|
-
"details": {},
|
|
13944
|
-
"request_id": None,
|
|
13945
|
-
"suggested_next_call": None,
|
|
13946
|
-
"noop": False,
|
|
13947
|
-
"verification": {},
|
|
13948
|
-
"app_key": "",
|
|
13949
|
-
"app_name": app_name or "未命名应用",
|
|
13950
|
-
"tag_ids": [package_tag_id] if package_tag_id else [],
|
|
13951
|
-
"would_create": True,
|
|
13952
|
-
}
|
|
13953
14954
|
|
|
13954
14955
|
def _create_target_app_shell(
|
|
13955
14956
|
self,
|
|
@@ -14018,7 +15019,7 @@ class AiBuilderFacade:
|
|
|
14018
15019
|
if not new_app_key:
|
|
14019
15020
|
return _failed("APP_CREATE_FAILED", "failed to create app shell", details={"result": result}, suggested_next_call=None)
|
|
14020
15021
|
try:
|
|
14021
|
-
base = self.
|
|
15022
|
+
base = self._read_app_base_raw_direct(profile=profile, app_key=new_app_key)
|
|
14022
15023
|
except (QingflowApiError, RuntimeError) as error:
|
|
14023
15024
|
api_error = _coerce_api_error(error)
|
|
14024
15025
|
if api_error.http_status != 404:
|
|
@@ -14211,12 +15212,104 @@ class AiBuilderFacade:
|
|
|
14211
15212
|
resolved_components: list[dict[str, Any]] = []
|
|
14212
15213
|
layout_metadata: list[dict[str, Any]] = []
|
|
14213
15214
|
inline_chart_results = inline_chart_results if inline_chart_results is not None else []
|
|
15215
|
+
inline_chart_resolutions: dict[int, dict[str, Any]] = {}
|
|
15216
|
+
inline_chart_result_by_section: dict[int, dict[str, Any]] = {}
|
|
15217
|
+
inline_chart_failures: list[str] = []
|
|
15218
|
+
inline_chart_batches: dict[str, list[tuple[int, PortalSectionPatch, ChartUpsertPatch]]] = {}
|
|
15219
|
+
for section_index, section in enumerate(sections):
|
|
15220
|
+
if section.source_type != "chart" or section.chart is None:
|
|
15221
|
+
continue
|
|
15222
|
+
inline_chart_payload = section.chart.model_dump(mode="json", exclude={"app_key"})
|
|
15223
|
+
inline_chart_batches.setdefault(section.chart.app_key, []).append(
|
|
15224
|
+
(section_index, section, ChartUpsertPatch.model_validate(inline_chart_payload))
|
|
15225
|
+
)
|
|
15226
|
+
for app_key, batch_entries in inline_chart_batches.items():
|
|
15227
|
+
chart_request = ChartApplyRequest(
|
|
15228
|
+
app_key=app_key,
|
|
15229
|
+
upsert_charts=[patch for _, _, patch in batch_entries],
|
|
15230
|
+
patch_charts=[],
|
|
15231
|
+
remove_chart_ids=[],
|
|
15232
|
+
reorder_chart_ids=[],
|
|
15233
|
+
)
|
|
15234
|
+
chart_apply = self.chart_apply(profile=profile, request=chart_request)
|
|
15235
|
+
chart_items = chart_apply.get("chart_results") if isinstance(chart_apply.get("chart_results"), list) else []
|
|
15236
|
+
matched_chart_item_indexes: set[int] = set()
|
|
15237
|
+
for batch_index, (section_index, section, patch) in enumerate(batch_entries):
|
|
15238
|
+
result_item: dict[str, Any] | None = None
|
|
15239
|
+
if (
|
|
15240
|
+
batch_index < len(chart_items)
|
|
15241
|
+
and batch_index not in matched_chart_item_indexes
|
|
15242
|
+
and isinstance(chart_items[batch_index], dict)
|
|
15243
|
+
and str(chart_items[batch_index].get("name") or "").strip() == patch.name
|
|
15244
|
+
):
|
|
15245
|
+
result_item = chart_items[batch_index]
|
|
15246
|
+
matched_chart_item_indexes.add(batch_index)
|
|
15247
|
+
if result_item is None:
|
|
15248
|
+
for item_index, item in enumerate(chart_items):
|
|
15249
|
+
if item_index in matched_chart_item_indexes or not isinstance(item, dict):
|
|
15250
|
+
continue
|
|
15251
|
+
if str(item.get("name") or "").strip() == patch.name:
|
|
15252
|
+
result_item = item
|
|
15253
|
+
matched_chart_item_indexes.add(item_index)
|
|
15254
|
+
break
|
|
15255
|
+
if (
|
|
15256
|
+
result_item is None
|
|
15257
|
+
and batch_index < len(chart_items)
|
|
15258
|
+
and batch_index not in matched_chart_item_indexes
|
|
15259
|
+
and isinstance(chart_items[batch_index], dict)
|
|
15260
|
+
):
|
|
15261
|
+
result_item = chart_items[batch_index]
|
|
15262
|
+
matched_chart_item_indexes.add(batch_index)
|
|
15263
|
+
successful_chart = (
|
|
15264
|
+
result_item
|
|
15265
|
+
if isinstance(result_item, dict)
|
|
15266
|
+
and str(result_item.get("status") or "") in {"created", "updated"}
|
|
15267
|
+
and str(result_item.get("chart_id") or "").strip()
|
|
15268
|
+
else None
|
|
15269
|
+
)
|
|
15270
|
+
inline_chart_result_by_section[section_index] = {
|
|
15271
|
+
"title": section.title,
|
|
15272
|
+
"app_key": app_key,
|
|
15273
|
+
"chart_name": section.chart.name,
|
|
15274
|
+
"chart_type": section.chart.chart_type.value,
|
|
15275
|
+
"status": str(successful_chart.get("status") or "") if isinstance(successful_chart, dict) else "failed",
|
|
15276
|
+
"chart_id": str(successful_chart.get("chart_id") or "") if isinstance(successful_chart, dict) else "",
|
|
15277
|
+
"chart_apply_status": chart_apply.get("status"),
|
|
15278
|
+
"write_executed": bool(chart_apply.get("write_executed")),
|
|
15279
|
+
"batch_size": len(batch_entries),
|
|
15280
|
+
"batch_index": batch_index,
|
|
15281
|
+
**(
|
|
15282
|
+
{"duration_breakdown_ms": deepcopy(chart_apply.get("duration_breakdown_ms"))}
|
|
15283
|
+
if isinstance(chart_apply.get("duration_breakdown_ms"), dict)
|
|
15284
|
+
else {}
|
|
15285
|
+
),
|
|
15286
|
+
**({"error_code": chart_apply.get("error_code")} if chart_apply.get("error_code") else {}),
|
|
15287
|
+
**({"message": chart_apply.get("message")} if chart_apply.get("message") else {}),
|
|
15288
|
+
}
|
|
15289
|
+
if not isinstance(successful_chart, dict):
|
|
15290
|
+
inline_chart_failures.append(
|
|
15291
|
+
f"inline chart '{section.chart.name}' could not be created or updated for portal section '{section.title}'"
|
|
15292
|
+
)
|
|
15293
|
+
continue
|
|
15294
|
+
inline_chart_resolutions[section_index] = {
|
|
15295
|
+
"chart_id": str(successful_chart.get("chart_id") or "").strip(),
|
|
15296
|
+
"chart_name": str(successful_chart.get("name") or section.chart.name).strip(),
|
|
15297
|
+
"app_key": app_key,
|
|
15298
|
+
"chart_type": section.chart.chart_type.value,
|
|
15299
|
+
}
|
|
15300
|
+
if inline_chart_result_by_section:
|
|
15301
|
+
inline_chart_results.extend(
|
|
15302
|
+
deepcopy(inline_chart_result_by_section[section_index])
|
|
15303
|
+
for section_index in sorted(inline_chart_result_by_section)
|
|
15304
|
+
)
|
|
15305
|
+
if inline_chart_failures:
|
|
15306
|
+
raise ValueError(inline_chart_failures[0])
|
|
14214
15307
|
pc_x = 0
|
|
14215
15308
|
pc_y = 0
|
|
14216
15309
|
pc_row_height = 0
|
|
14217
15310
|
mobile_y = 0
|
|
14218
15311
|
app_title_cache: dict[str, str] = {}
|
|
14219
|
-
for section in sections:
|
|
15312
|
+
for section_index, section in enumerate(sections):
|
|
14220
15313
|
position_payload: dict[str, Any]
|
|
14221
15314
|
if section.position is None:
|
|
14222
15315
|
position_payload, pc_x, pc_y, pc_row_height, mobile_y = _portal_component_position_public(
|
|
@@ -14238,46 +15331,9 @@ class AiBuilderFacade:
|
|
|
14238
15331
|
component: dict[str, Any]
|
|
14239
15332
|
if section.source_type == "chart":
|
|
14240
15333
|
if section.chart is not None:
|
|
14241
|
-
|
|
14242
|
-
|
|
14243
|
-
app_key=section.chart.app_key,
|
|
14244
|
-
upsert_charts=[ChartUpsertPatch.model_validate(inline_chart_payload)],
|
|
14245
|
-
patch_charts=[],
|
|
14246
|
-
remove_chart_ids=[],
|
|
14247
|
-
reorder_chart_ids=[],
|
|
14248
|
-
)
|
|
14249
|
-
chart_apply = self.chart_apply(profile=profile, request=chart_request)
|
|
14250
|
-
chart_items = chart_apply.get("chart_results") if isinstance(chart_apply.get("chart_results"), list) else []
|
|
14251
|
-
successful_chart = next(
|
|
14252
|
-
(
|
|
14253
|
-
item
|
|
14254
|
-
for item in chart_items
|
|
14255
|
-
if isinstance(item, dict)
|
|
14256
|
-
and str(item.get("status") or "") in {"created", "updated"}
|
|
14257
|
-
and str(item.get("chart_id") or "").strip()
|
|
14258
|
-
),
|
|
14259
|
-
None,
|
|
14260
|
-
)
|
|
14261
|
-
inline_chart_results.append({
|
|
14262
|
-
"title": section.title,
|
|
14263
|
-
"app_key": section.chart.app_key,
|
|
14264
|
-
"chart_name": section.chart.name,
|
|
14265
|
-
"chart_type": section.chart.chart_type.value,
|
|
14266
|
-
"status": str(successful_chart.get("status") or "") if isinstance(successful_chart, dict) else "failed",
|
|
14267
|
-
"chart_id": str(successful_chart.get("chart_id") or "") if isinstance(successful_chart, dict) else "",
|
|
14268
|
-
"chart_apply_status": chart_apply.get("status"),
|
|
14269
|
-
"write_executed": bool(chart_apply.get("write_executed")),
|
|
14270
|
-
**({"error_code": chart_apply.get("error_code")} if chart_apply.get("error_code") else {}),
|
|
14271
|
-
**({"message": chart_apply.get("message")} if chart_apply.get("message") else {}),
|
|
14272
|
-
})
|
|
14273
|
-
if not isinstance(successful_chart, dict):
|
|
15334
|
+
resolved_chart = inline_chart_resolutions.get(section_index)
|
|
15335
|
+
if not isinstance(resolved_chart, dict):
|
|
14274
15336
|
raise ValueError(f"inline chart '{section.chart.name}' could not be created or updated for portal section '{section.title}'")
|
|
14275
|
-
resolved_chart = {
|
|
14276
|
-
"chart_id": str(successful_chart.get("chart_id") or "").strip(),
|
|
14277
|
-
"chart_name": str(successful_chart.get("name") or section.chart.name).strip(),
|
|
14278
|
-
"app_key": section.chart.app_key,
|
|
14279
|
-
"chart_type": section.chart.chart_type.value,
|
|
14280
|
-
}
|
|
14281
15337
|
else:
|
|
14282
15338
|
resolved_chart = _resolve_chart_reference(
|
|
14283
15339
|
charts=self.charts,
|
|
@@ -18907,7 +19963,11 @@ def _verify_relation_readback_by_name(
|
|
|
18907
19963
|
for item in cast(list[Any], actual.get("visible_fields") or [])
|
|
18908
19964
|
if isinstance(item, dict) and str(item.get("name") or "").strip()
|
|
18909
19965
|
]
|
|
18910
|
-
if
|
|
19966
|
+
if not _relation_visible_field_names_match(
|
|
19967
|
+
expected_visible_names=expected_visible_names,
|
|
19968
|
+
actual_visible_names=actual_visible_names,
|
|
19969
|
+
display_field_name=expected_display_name,
|
|
19970
|
+
):
|
|
18911
19971
|
return False
|
|
18912
19972
|
return True
|
|
18913
19973
|
|
|
@@ -18925,6 +19985,36 @@ def _relation_field_names(values: object) -> list[str]:
|
|
|
18925
19985
|
return names
|
|
18926
19986
|
|
|
18927
19987
|
|
|
19988
|
+
def _relation_visible_field_names_match(
|
|
19989
|
+
*,
|
|
19990
|
+
expected_visible_names: list[str],
|
|
19991
|
+
actual_visible_names: list[str],
|
|
19992
|
+
display_field_name: str | None = None,
|
|
19993
|
+
) -> bool:
|
|
19994
|
+
if expected_visible_names == actual_visible_names:
|
|
19995
|
+
return True
|
|
19996
|
+
|
|
19997
|
+
def dedupe(values: list[str]) -> list[str]:
|
|
19998
|
+
result: list[str] = []
|
|
19999
|
+
for value in values:
|
|
20000
|
+
normalized = str(value or "").strip()
|
|
20001
|
+
if normalized and normalized not in result:
|
|
20002
|
+
result.append(normalized)
|
|
20003
|
+
return result
|
|
20004
|
+
|
|
20005
|
+
expected = dedupe(expected_visible_names)
|
|
20006
|
+
actual = dedupe(actual_visible_names)
|
|
20007
|
+
if expected == actual:
|
|
20008
|
+
return True
|
|
20009
|
+
if set(expected) != set(actual):
|
|
20010
|
+
return False
|
|
20011
|
+
display = str(display_field_name or "").strip()
|
|
20012
|
+
if display and actual and actual[0] == display and display in expected:
|
|
20013
|
+
expected_display_first = [display, *[name for name in expected if name != display]]
|
|
20014
|
+
return actual == expected_display_first
|
|
20015
|
+
return False
|
|
20016
|
+
|
|
20017
|
+
|
|
18928
20018
|
def _relation_field_public_selector(value: object) -> dict[str, Any] | None:
|
|
18929
20019
|
if not isinstance(value, dict):
|
|
18930
20020
|
return None
|
|
@@ -18983,13 +20073,18 @@ def _schema_relation_readback_matrix(
|
|
|
18983
20073
|
actual_display = _relation_field_public_selector((actual or {}).get("display_field"))
|
|
18984
20074
|
expected_visible_names = _relation_field_names((expected or degraded or {}).get("visible_fields"))
|
|
18985
20075
|
actual_visible_names = _relation_field_names((actual or {}).get("visible_fields"))
|
|
20076
|
+
display_name = str((expected_display or {}).get("name") or "").strip() or None
|
|
18986
20077
|
|
|
18987
20078
|
checks = {
|
|
18988
20079
|
"field_exists": isinstance(actual, dict),
|
|
18989
20080
|
"target_app_key": expected_target == actual_target,
|
|
18990
20081
|
"relation_mode": expected_mode == actual_mode,
|
|
18991
20082
|
"display_field": (expected_display or {}).get("name") == (actual_display or {}).get("name"),
|
|
18992
|
-
"visible_fields":
|
|
20083
|
+
"visible_fields": _relation_visible_field_names_match(
|
|
20084
|
+
expected_visible_names=expected_visible_names,
|
|
20085
|
+
actual_visible_names=actual_visible_names,
|
|
20086
|
+
display_field_name=display_name,
|
|
20087
|
+
),
|
|
18993
20088
|
}
|
|
18994
20089
|
readback_verified = all(checks.values())
|
|
18995
20090
|
if not isinstance(actual, dict):
|
|
@@ -23802,8 +24897,8 @@ def _build_form_payload_for_edit_fields(
|
|
|
23802
24897
|
parsed_section_id = _coerce_positive_int(section.get("section_id"))
|
|
23803
24898
|
if parsed_section_id is not None:
|
|
23804
24899
|
wrapper["sectionId"] = parsed_section_id
|
|
23805
|
-
elif template is None and
|
|
23806
|
-
wrapper
|
|
24900
|
+
elif template is None and _coerce_positive_int(wrapper.get("sectionId")) is None:
|
|
24901
|
+
wrapper.pop("sectionId", None)
|
|
23807
24902
|
wrapper["innerQuestions"] = inner_rows
|
|
23808
24903
|
form_rows.append([wrapper])
|
|
23809
24904
|
|
|
@@ -27678,7 +28773,6 @@ def _normalize_flow_stage_failure(stage: JSONObject, *, profile: str, app_key: s
|
|
|
27678
28773
|
"arguments": {
|
|
27679
28774
|
"profile": profile,
|
|
27680
28775
|
"app_key": app_key,
|
|
27681
|
-
"create_if_missing": False,
|
|
27682
28776
|
"add_fields": [
|
|
27683
28777
|
{
|
|
27684
28778
|
"name": "状态",
|