@josephyan/qingflow-app-builder-mcp 1.1.20 → 1.1.22
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/skills/qingflow-app-builder/SKILL.md +1 -1
- package/skills/qingflow-app-builder/references/complete-system-development-guide.md +2 -2
- package/skills/qingflow-app-builder/references/create-app.md +3 -6
- package/skills/qingflow-app-builder/references/tool-selection.md +4 -4
- package/src/qingflow_mcp/builder_facade/models.py +0 -1
- package/src/qingflow_mcp/builder_facade/service.py +1730 -488
- 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 +761 -368
|
@@ -9,6 +9,7 @@ import random
|
|
|
9
9
|
import re
|
|
10
10
|
import string
|
|
11
11
|
import tempfile
|
|
12
|
+
import time
|
|
12
13
|
from typing import Any, NoReturn, cast
|
|
13
14
|
from urllib.parse import quote_plus, unquote_plus
|
|
14
15
|
from uuid import uuid4
|
|
@@ -107,6 +108,7 @@ from .models import (
|
|
|
107
108
|
|
|
108
109
|
BUILDER_PORTAL_LIST_DETAIL_VERIFY_LIMIT = 50
|
|
109
110
|
CHART_APPLY_RECOMMENDED_UPSERT_BATCH_SIZE = 8
|
|
111
|
+
CUSTOM_BUTTON_VIEW_CONFIG_READBACK_RETRY_SECONDS = 0.4
|
|
110
112
|
|
|
111
113
|
|
|
112
114
|
QUESTION_TYPE_TO_FIELD_TYPE: dict[int, str] = {
|
|
@@ -292,6 +294,12 @@ class AiBuilderFacade:
|
|
|
292
294
|
def _with_backend_context(self, profile: str, handler):
|
|
293
295
|
return self.apps._run(profile, lambda _session_profile, context: handler(context))
|
|
294
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
|
+
|
|
295
303
|
def flow_get_schema(self, *, profile: str, schema_version: str | None = None) -> JSONObject:
|
|
296
304
|
normalized_args = {"schema_version": schema_version or workflow_spec_api.DEFAULT_SCHEMA_VERSION}
|
|
297
305
|
try:
|
|
@@ -405,22 +413,50 @@ class AiBuilderFacade:
|
|
|
405
413
|
"idempotency_key": idempotency_key,
|
|
406
414
|
"schema_version": schema_version or workflow_spec_api.DEFAULT_SCHEMA_VERSION,
|
|
407
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
|
|
408
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()
|
|
409
447
|
permission_outcome = self._guard_app_permission(
|
|
410
448
|
profile=profile,
|
|
411
449
|
app_key=app_key,
|
|
412
450
|
required_permission="data_manage",
|
|
413
451
|
normalized_args=normalized_args,
|
|
414
452
|
)
|
|
453
|
+
permission_ms += _duration_ms(permission_started_at)
|
|
415
454
|
if permission_outcome.block is not None:
|
|
416
|
-
return permission_outcome.block
|
|
455
|
+
return finish(permission_outcome.block)
|
|
417
456
|
permission_outcomes.append(permission_outcome)
|
|
418
457
|
|
|
419
|
-
def finalize(response: JSONObject) -> JSONObject:
|
|
420
|
-
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
421
|
-
|
|
422
458
|
if not isinstance(spec, dict) or not spec:
|
|
423
|
-
return
|
|
459
|
+
return finish(
|
|
424
460
|
_failed(
|
|
425
461
|
"FLOW_SPEC_REQUIRED",
|
|
426
462
|
"spec must be a non-empty WorkflowSpecDTO object",
|
|
@@ -435,13 +471,15 @@ class AiBuilderFacade:
|
|
|
435
471
|
"spec": spec,
|
|
436
472
|
}
|
|
437
473
|
try:
|
|
474
|
+
flow_spec_apply_started_at = time.perf_counter()
|
|
438
475
|
apply_result = self._with_backend_context(
|
|
439
476
|
profile,
|
|
440
477
|
lambda context: workflow_spec_api.post_apply(self.apps.backend, context, apply_body=apply_body),
|
|
441
478
|
)
|
|
442
479
|
except (QingflowApiError, RuntimeError) as error:
|
|
480
|
+
flow_spec_apply_ms += _duration_ms(flow_spec_apply_started_at)
|
|
443
481
|
api_error = _coerce_api_error(error)
|
|
444
|
-
return
|
|
482
|
+
return finish(
|
|
445
483
|
_failed_from_api_error(
|
|
446
484
|
"FLOW_SPEC_APPLY_FAILED",
|
|
447
485
|
api_error,
|
|
@@ -450,15 +488,18 @@ class AiBuilderFacade:
|
|
|
450
488
|
suggested_next_call={"tool_name": "app_flow_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
451
489
|
)
|
|
452
490
|
)
|
|
491
|
+
flow_spec_apply_ms += _duration_ms(flow_spec_apply_started_at)
|
|
453
492
|
if not isinstance(apply_result, dict):
|
|
454
493
|
apply_result = {"result": apply_result}
|
|
494
|
+
flow_spec_verify_started_at = time.perf_counter()
|
|
455
495
|
verification, verify_warnings, verified = workflow_spec_api.verify_apply_response(
|
|
456
496
|
input_spec=spec,
|
|
457
497
|
apply_result=apply_result,
|
|
458
498
|
)
|
|
499
|
+
flow_spec_verify_ms += _duration_ms(flow_spec_verify_started_at)
|
|
459
500
|
backend_status = str(apply_result.get("status") or "SUCCESS").upper()
|
|
460
501
|
if backend_status not in {"SUCCESS", "OK"}:
|
|
461
|
-
return
|
|
502
|
+
return finish(
|
|
462
503
|
_failed(
|
|
463
504
|
"FLOW_SPEC_APPLY_FAILED",
|
|
464
505
|
str(apply_result.get("message") or "workflow spec apply failed"),
|
|
@@ -494,7 +535,7 @@ class AiBuilderFacade:
|
|
|
494
535
|
"write_succeeded": True,
|
|
495
536
|
"safe_to_retry": False,
|
|
496
537
|
}
|
|
497
|
-
return
|
|
538
|
+
return finish_with_publish(response)
|
|
498
539
|
|
|
499
540
|
def package_resolve(self, *, profile: str, package_name: str) -> JSONObject:
|
|
500
541
|
requested = str(package_name or "").strip()
|
|
@@ -795,7 +836,6 @@ class AiBuilderFacade:
|
|
|
795
836
|
profile: str,
|
|
796
837
|
package_id: int | None = None,
|
|
797
838
|
package_name: str | None = None,
|
|
798
|
-
create_if_missing: bool = False,
|
|
799
839
|
icon: str | None = None,
|
|
800
840
|
color: str | None = None,
|
|
801
841
|
visibility: VisibilityPatch | None = None,
|
|
@@ -806,7 +846,6 @@ class AiBuilderFacade:
|
|
|
806
846
|
normalized_args: JSONObject = {
|
|
807
847
|
"package_id": package_id,
|
|
808
848
|
**({"package_name": requested_name} if requested_name else {}),
|
|
809
|
-
"create_if_missing": bool(create_if_missing),
|
|
810
849
|
**({"icon": icon} if icon else {}),
|
|
811
850
|
**({"color": color} if color else {}),
|
|
812
851
|
**({"visibility": visibility.model_dump(mode="json")} if visibility is not None else {}),
|
|
@@ -818,39 +857,119 @@ class AiBuilderFacade:
|
|
|
818
857
|
create_result: JSONObject | None = None
|
|
819
858
|
update_result: JSONObject | None = None
|
|
820
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
|
|
821
877
|
|
|
822
878
|
if effective_package_id is None:
|
|
823
|
-
if not create_if_missing:
|
|
824
|
-
return _failed(
|
|
825
|
-
"PACKAGE_ID_REQUIRED",
|
|
826
|
-
"package_id is required unless create_if_missing=true",
|
|
827
|
-
normalized_args=normalized_args,
|
|
828
|
-
suggested_next_call=None,
|
|
829
|
-
)
|
|
830
879
|
if not requested_name:
|
|
831
880
|
return _failed(
|
|
832
881
|
"PACKAGE_NAME_REQUIRED",
|
|
833
|
-
"package_name is required when
|
|
882
|
+
"package_name is required when creating a package without package_id",
|
|
834
883
|
normalized_args=normalized_args,
|
|
835
884
|
suggested_next_call=None,
|
|
836
885
|
)
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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
|
+
}
|
|
844
961
|
if create_result.get("status") not in {"success", "partial_success"}:
|
|
845
|
-
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))
|
|
846
963
|
effective_package_id = _coerce_positive_int(create_result.get("tag_id") or create_result.get("package_id"))
|
|
847
964
|
if effective_package_id is None:
|
|
848
|
-
return
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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
|
+
)
|
|
854
973
|
)
|
|
855
974
|
normalized_args["package_id"] = effective_package_id
|
|
856
975
|
created = True
|
|
@@ -866,6 +985,7 @@ class AiBuilderFacade:
|
|
|
866
985
|
if edit_tag_outcome.block is not None:
|
|
867
986
|
return edit_tag_outcome.block
|
|
868
987
|
permission_outcomes.append(edit_tag_outcome)
|
|
988
|
+
update_started_at = time.perf_counter()
|
|
869
989
|
update_result = self.package_update(
|
|
870
990
|
profile=profile,
|
|
871
991
|
tag_id=effective_package_id,
|
|
@@ -874,18 +994,22 @@ class AiBuilderFacade:
|
|
|
874
994
|
color=color,
|
|
875
995
|
visibility=visibility,
|
|
876
996
|
)
|
|
997
|
+
duration_breakdown_ms["package_update_ms"] += int((time.perf_counter() - update_started_at) * 1000)
|
|
877
998
|
if update_result.get("status") not in {"success", "partial_success"}:
|
|
878
|
-
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))
|
|
879
1000
|
|
|
880
1001
|
layout_result: JSONObject | None = None
|
|
881
1002
|
if items is not None:
|
|
882
1003
|
if not isinstance(items, list):
|
|
883
|
-
return
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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
|
+
)
|
|
888
1011
|
)
|
|
1012
|
+
items_started_at = time.perf_counter()
|
|
889
1013
|
layout_result = self._apply_package_items(
|
|
890
1014
|
profile=profile,
|
|
891
1015
|
package_id=effective_package_id,
|
|
@@ -893,6 +1017,7 @@ class AiBuilderFacade:
|
|
|
893
1017
|
allow_detach=allow_detach,
|
|
894
1018
|
normalized_args=normalized_args,
|
|
895
1019
|
)
|
|
1020
|
+
duration_breakdown_ms["package_items_ms"] += int((time.perf_counter() - items_started_at) * 1000)
|
|
896
1021
|
if layout_result.get("status") not in {"success", "partial_success"}:
|
|
897
1022
|
prior_package_write_executed = bool(
|
|
898
1023
|
created
|
|
@@ -929,8 +1054,8 @@ class AiBuilderFacade:
|
|
|
929
1054
|
)
|
|
930
1055
|
partial["package_id"] = effective_package_id
|
|
931
1056
|
partial["layout_failed"] = True
|
|
932
|
-
return _apply_permission_outcomes(partial, *permission_outcomes)
|
|
933
|
-
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))
|
|
934
1059
|
|
|
935
1060
|
write_executed = bool(
|
|
936
1061
|
created
|
|
@@ -947,10 +1072,120 @@ class AiBuilderFacade:
|
|
|
947
1072
|
and not bool(layout_result.get("noop"))
|
|
948
1073
|
)
|
|
949
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()
|
|
950
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)
|
|
951
1186
|
if verification.get("status") != "success":
|
|
952
1187
|
if write_executed:
|
|
953
|
-
return _apply_permission_outcomes(
|
|
1188
|
+
return finish_duration(_apply_permission_outcomes(
|
|
954
1189
|
_post_write_readback_pending_result(
|
|
955
1190
|
error_code="PACKAGE_READBACK_PENDING",
|
|
956
1191
|
message="applied package; final package readback is unavailable",
|
|
@@ -959,8 +1194,8 @@ class AiBuilderFacade:
|
|
|
959
1194
|
suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "package_id": effective_package_id}},
|
|
960
1195
|
),
|
|
961
1196
|
*permission_outcomes,
|
|
962
|
-
)
|
|
963
|
-
return _apply_permission_outcomes(verification, *permission_outcomes)
|
|
1197
|
+
))
|
|
1198
|
+
return finish_duration(_apply_permission_outcomes(verification, *permission_outcomes))
|
|
964
1199
|
expected_visibility = None
|
|
965
1200
|
if visibility is not None:
|
|
966
1201
|
try:
|
|
@@ -1051,7 +1286,7 @@ class AiBuilderFacade:
|
|
|
1051
1286
|
}
|
|
1052
1287
|
},
|
|
1053
1288
|
}
|
|
1054
|
-
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
1289
|
+
return finish_duration(_apply_permission_outcomes(response, *permission_outcomes))
|
|
1055
1290
|
|
|
1056
1291
|
def package_update(
|
|
1057
1292
|
self,
|
|
@@ -1312,6 +1547,72 @@ class AiBuilderFacade:
|
|
|
1312
1547
|
allow_detach: bool,
|
|
1313
1548
|
normalized_args: JSONObject,
|
|
1314
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
|
+
|
|
1315
1616
|
current_detail_result: JSONObject | None = None
|
|
1316
1617
|
current_base_result: JSONObject | None = None
|
|
1317
1618
|
detail_api_error: QingflowApiError | None = None
|
|
@@ -1387,16 +1688,6 @@ class AiBuilderFacade:
|
|
|
1387
1688
|
suggested_next_call=None,
|
|
1388
1689
|
)
|
|
1389
1690
|
|
|
1390
|
-
duplicate_resources = _find_duplicate_package_resources(normalized_items)
|
|
1391
|
-
if duplicate_resources:
|
|
1392
|
-
return _failed(
|
|
1393
|
-
"PACKAGE_LAYOUT_DUPLICATE_ITEM",
|
|
1394
|
-
"items contains duplicate apps or portals",
|
|
1395
|
-
normalized_args=normalized_args,
|
|
1396
|
-
details={"duplicates": [{"type": item[0], "id": item[1]} for item in duplicate_resources]},
|
|
1397
|
-
suggested_next_call=None,
|
|
1398
|
-
)
|
|
1399
|
-
|
|
1400
1691
|
current_groups = {int(spec["group_id"]): str(spec["name"] or "").strip() for spec in current_group_specs}
|
|
1401
1692
|
desired_groups = _collect_public_package_group_specs(normalized_items)
|
|
1402
1693
|
desired_group_ids = {
|
|
@@ -3157,47 +3448,107 @@ class AiBuilderFacade:
|
|
|
3157
3448
|
def app_custom_buttons_apply(self, *, profile: str, request: CustomButtonsApplyRequest) -> JSONObject:
|
|
3158
3449
|
normalized_args = request.model_dump(mode="json", exclude_none=True, exclude_defaults=True)
|
|
3159
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
|
|
3160
3462
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
3161
3463
|
button_write_intent = bool(request.upsert_buttons or request.patch_buttons or request.remove_buttons)
|
|
3162
3464
|
if button_write_intent:
|
|
3465
|
+
permission_started_at = time.perf_counter()
|
|
3163
3466
|
permission_outcome = self._guard_app_permission(
|
|
3164
3467
|
profile=profile,
|
|
3165
3468
|
app_key=app_key,
|
|
3166
3469
|
required_permission="edit_app",
|
|
3167
3470
|
normalized_args=normalized_args,
|
|
3168
3471
|
)
|
|
3472
|
+
permission_ms += _duration_ms(permission_started_at)
|
|
3169
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
|
+
)
|
|
3170
3479
|
return permission_outcome.block
|
|
3171
3480
|
permission_outcomes.append(permission_outcome)
|
|
3172
3481
|
if request.view_configs:
|
|
3482
|
+
permission_started_at = time.perf_counter()
|
|
3173
3483
|
permission_outcome = self._guard_app_permission(
|
|
3174
3484
|
profile=profile,
|
|
3175
3485
|
app_key=app_key,
|
|
3176
3486
|
required_permission="view_manage",
|
|
3177
3487
|
normalized_args=normalized_args,
|
|
3178
3488
|
)
|
|
3489
|
+
permission_ms += _duration_ms(permission_started_at)
|
|
3179
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
|
+
)
|
|
3180
3496
|
return permission_outcome.block
|
|
3181
3497
|
permission_outcomes.append(permission_outcome)
|
|
3182
3498
|
|
|
3183
3499
|
def finalize(response: JSONObject) -> JSONObject:
|
|
3184
|
-
|
|
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
|
|
3185
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
|
+
)
|
|
3186
3527
|
view_config_refs = [
|
|
3187
3528
|
binding.button_ref
|
|
3188
3529
|
for config in request.view_configs
|
|
3189
3530
|
for binding in config.buttons
|
|
3190
3531
|
]
|
|
3191
|
-
|
|
3192
|
-
_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
|
|
3193
3542
|
)
|
|
3194
3543
|
try:
|
|
3544
|
+
button_inventory_started_at = time.perf_counter()
|
|
3195
3545
|
existing_buttons = (
|
|
3196
3546
|
self._load_custom_buttons_for_builder(profile=profile, app_key=app_key)
|
|
3197
3547
|
if needs_button_inventory
|
|
3198
3548
|
else []
|
|
3199
3549
|
)
|
|
3200
3550
|
except (QingflowApiError, RuntimeError) as error:
|
|
3551
|
+
button_inventory_ms += _duration_ms(button_inventory_started_at)
|
|
3201
3552
|
api_error = _coerce_api_error(error)
|
|
3202
3553
|
return finalize(_failed_from_api_error(
|
|
3203
3554
|
"CUSTOM_BUTTON_LIST_FAILED",
|
|
@@ -3206,7 +3557,10 @@ class AiBuilderFacade:
|
|
|
3206
3557
|
details=_with_state_read_blocked_details({"app_key": app_key}, resource="custom_buttons", error=api_error),
|
|
3207
3558
|
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3208
3559
|
))
|
|
3560
|
+
button_inventory_ms += _duration_ms(button_inventory_started_at)
|
|
3561
|
+
app_name_started_at = time.perf_counter()
|
|
3209
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)
|
|
3210
3564
|
|
|
3211
3565
|
existing_by_id = {
|
|
3212
3566
|
button_id: item
|
|
@@ -3221,6 +3575,7 @@ class AiBuilderFacade:
|
|
|
3221
3575
|
|
|
3222
3576
|
upsert_buttons = list(request.upsert_buttons)
|
|
3223
3577
|
if request.patch_buttons:
|
|
3578
|
+
button_patch_started_at = time.perf_counter()
|
|
3224
3579
|
expanded_buttons, patch_issues, patch_results = self._expand_custom_button_partial_patches(
|
|
3225
3580
|
profile=profile,
|
|
3226
3581
|
app_key=app_key,
|
|
@@ -3228,6 +3583,7 @@ class AiBuilderFacade:
|
|
|
3228
3583
|
existing_by_text=existing_by_text,
|
|
3229
3584
|
patch_buttons=request.patch_buttons,
|
|
3230
3585
|
)
|
|
3586
|
+
button_patch_hydration_ms += _duration_ms(button_patch_started_at)
|
|
3231
3587
|
if patch_issues:
|
|
3232
3588
|
return finalize(
|
|
3233
3589
|
_failed(
|
|
@@ -3245,11 +3601,13 @@ class AiBuilderFacade:
|
|
|
3245
3601
|
]
|
|
3246
3602
|
normalized_args["patch_results"] = patch_results
|
|
3247
3603
|
|
|
3604
|
+
add_data_compile_started_at = time.perf_counter()
|
|
3248
3605
|
compiled_add_data_configs, add_data_issues = self._compile_custom_button_semantic_add_data_configs(
|
|
3249
3606
|
profile=profile,
|
|
3250
3607
|
app_key=app_key,
|
|
3251
3608
|
patches=upsert_buttons,
|
|
3252
3609
|
)
|
|
3610
|
+
add_data_compile_ms += _duration_ms(add_data_compile_started_at)
|
|
3253
3611
|
upsert_ops: list[dict[str, Any]] = []
|
|
3254
3612
|
remove_ops: list[dict[str, Any]] = []
|
|
3255
3613
|
blocking_issues: list[dict[str, Any]] = []
|
|
@@ -3432,12 +3790,14 @@ class AiBuilderFacade:
|
|
|
3432
3790
|
|
|
3433
3791
|
edit_version_no = None
|
|
3434
3792
|
if button_write_intent:
|
|
3793
|
+
edit_context_started_at = time.perf_counter()
|
|
3435
3794
|
edit_version_no, edit_context_error = self._ensure_app_edit_context(
|
|
3436
3795
|
profile=profile,
|
|
3437
3796
|
app_key=app_key,
|
|
3438
3797
|
normalized_args=normalized_args,
|
|
3439
3798
|
failure_code="CUSTOM_BUTTON_APPLY_FAILED",
|
|
3440
3799
|
)
|
|
3800
|
+
edit_context_ms += _duration_ms(edit_context_started_at)
|
|
3441
3801
|
if edit_context_error is not None:
|
|
3442
3802
|
return finalize(edit_context_error)
|
|
3443
3803
|
|
|
@@ -3449,6 +3809,7 @@ class AiBuilderFacade:
|
|
|
3449
3809
|
client_key_map: dict[str, int] = {}
|
|
3450
3810
|
write_executed = False
|
|
3451
3811
|
|
|
3812
|
+
button_write_started_at = time.perf_counter()
|
|
3452
3813
|
for op in upsert_ops:
|
|
3453
3814
|
patch = cast(CustomButtonUpsertPatch, op["patch"])
|
|
3454
3815
|
patch_payload = _serialize_custom_button_payload(patch, add_data_config_override=compiled_add_data_configs.get(int(op["index"])))
|
|
@@ -3459,7 +3820,9 @@ class AiBuilderFacade:
|
|
|
3459
3820
|
button_id = _extract_custom_button_id(result.get("result"))
|
|
3460
3821
|
if button_id is None:
|
|
3461
3822
|
try:
|
|
3823
|
+
button_readback_started_at = time.perf_counter()
|
|
3462
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)
|
|
3463
3826
|
matches = [
|
|
3464
3827
|
item
|
|
3465
3828
|
for item in readback_buttons
|
|
@@ -3469,6 +3832,7 @@ class AiBuilderFacade:
|
|
|
3469
3832
|
if len(matches) == 1:
|
|
3470
3833
|
button_id = _coerce_positive_int(matches[0].get("button_id"))
|
|
3471
3834
|
except (QingflowApiError, RuntimeError) as error:
|
|
3835
|
+
button_readback_ms += _duration_ms(button_readback_started_at)
|
|
3472
3836
|
readback_errors.append(
|
|
3473
3837
|
{
|
|
3474
3838
|
"resource": "custom_buttons",
|
|
@@ -3524,7 +3888,9 @@ class AiBuilderFacade:
|
|
|
3524
3888
|
try:
|
|
3525
3889
|
write_executed = True
|
|
3526
3890
|
self.buttons.custom_button_delete(profile=profile, app_key=app_key, button_id=button_id)
|
|
3891
|
+
button_readback_started_at = time.perf_counter()
|
|
3527
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)
|
|
3528
3894
|
removed.append(
|
|
3529
3895
|
{
|
|
3530
3896
|
"index": op["index"],
|
|
@@ -3564,13 +3930,41 @@ class AiBuilderFacade:
|
|
|
3564
3930
|
}
|
|
3565
3931
|
)
|
|
3566
3932
|
|
|
3567
|
-
|
|
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
|
|
3568
3959
|
readback_buttons: list[dict[str, Any]] = []
|
|
3569
3960
|
readback_failed = False
|
|
3570
3961
|
if needs_button_list_readback:
|
|
3571
3962
|
try:
|
|
3963
|
+
button_readback_started_at = time.perf_counter()
|
|
3572
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)
|
|
3573
3966
|
except (QingflowApiError, RuntimeError) as error:
|
|
3967
|
+
button_readback_ms += _duration_ms(button_readback_started_at)
|
|
3574
3968
|
readback_errors.append(
|
|
3575
3969
|
{
|
|
3576
3970
|
"resource": "custom_buttons",
|
|
@@ -3584,26 +3978,14 @@ class AiBuilderFacade:
|
|
|
3584
3978
|
for item in readback_buttons
|
|
3585
3979
|
if (button_id := _coerce_positive_int(item.get("button_id"))) is not None
|
|
3586
3980
|
}
|
|
3587
|
-
created_ids = [
|
|
3588
|
-
int(item["button_id"])
|
|
3589
|
-
for item in created
|
|
3590
|
-
if _coerce_positive_int(item.get("button_id")) is not None
|
|
3591
|
-
]
|
|
3592
|
-
updated_ids = [
|
|
3593
|
-
int(item["button_id"])
|
|
3594
|
-
for item in updated
|
|
3595
|
-
if _coerce_positive_int(item.get("button_id")) is not None
|
|
3596
|
-
]
|
|
3597
|
-
removed_ids = [
|
|
3598
|
-
int(item["button_id"])
|
|
3599
|
-
for item in removed
|
|
3600
|
-
if _coerce_positive_int(item.get("button_id")) is not None
|
|
3601
|
-
]
|
|
3602
3981
|
removed_verified = all(str(item.get("readback_status") or "") == "deleted" for item in removed)
|
|
3603
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))
|
|
3604
3985
|
verified = (
|
|
3605
|
-
not readback_failed
|
|
3606
|
-
and
|
|
3986
|
+
(final_readback_skipped or not readback_failed)
|
|
3987
|
+
and created_verified
|
|
3988
|
+
and updated_verified
|
|
3607
3989
|
and removed_verified
|
|
3608
3990
|
and not failed
|
|
3609
3991
|
and all(_coerce_positive_int(item.get("button_id")) is not None for item in created)
|
|
@@ -3615,7 +3997,9 @@ class AiBuilderFacade:
|
|
|
3615
3997
|
view_config_verified = True
|
|
3616
3998
|
view_config_write_executed = False
|
|
3617
3999
|
view_config_write_succeeded = False
|
|
4000
|
+
created_verified_by_view_binding = False
|
|
3618
4001
|
if request.view_configs:
|
|
4002
|
+
view_config_started_at = time.perf_counter()
|
|
3619
4003
|
view_config_result = self._apply_custom_button_view_configs(
|
|
3620
4004
|
profile=profile,
|
|
3621
4005
|
app_key=app_key,
|
|
@@ -3627,6 +4011,7 @@ class AiBuilderFacade:
|
|
|
3627
4011
|
updated_ids=updated_ids,
|
|
3628
4012
|
removed_ids=removed_ids,
|
|
3629
4013
|
)
|
|
4014
|
+
view_config_ms += _duration_ms(view_config_started_at)
|
|
3630
4015
|
view_config_results = list(view_config_result.get("view_configs") or [])
|
|
3631
4016
|
view_config_failed = list(view_config_result.get("failed") or [])
|
|
3632
4017
|
view_config_warnings = list(view_config_result.get("warnings") or [])
|
|
@@ -3639,6 +4024,38 @@ class AiBuilderFacade:
|
|
|
3639
4024
|
if view_config_warnings:
|
|
3640
4025
|
if not isinstance(view_config_warnings, list):
|
|
3641
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
|
+
)
|
|
3642
4059
|
verified = custom_buttons_verified and view_config_verified
|
|
3643
4060
|
write_succeeded = bool(created or updated or removed or view_config_write_succeeded)
|
|
3644
4061
|
status = "success" if verified else ("partial_success" if write_succeeded else "failed")
|
|
@@ -3692,6 +4109,10 @@ class AiBuilderFacade:
|
|
|
3692
4109
|
"edit_version_no": edit_version_no,
|
|
3693
4110
|
"button_ids_by_client_key": client_key_map,
|
|
3694
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,
|
|
3695
4116
|
**({"readback_errors": readback_errors} if readback_errors else {}),
|
|
3696
4117
|
"compiled_match_rules": {
|
|
3697
4118
|
str(index): _summarize_compiled_match_rules(config.get("que_relation") or [])
|
|
@@ -3704,9 +4125,10 @@ class AiBuilderFacade:
|
|
|
3704
4125
|
"warnings": warnings,
|
|
3705
4126
|
"verification": {
|
|
3706
4127
|
"custom_buttons_verified": custom_buttons_verified,
|
|
3707
|
-
"readback_loaded": not readback_failed,
|
|
3708
|
-
"
|
|
3709
|
-
"
|
|
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,
|
|
3710
4132
|
"removed_verified": removed_verified,
|
|
3711
4133
|
"remove_readback_pending": remove_readback_pending,
|
|
3712
4134
|
"removed_readback_results": deepcopy(removed),
|
|
@@ -3726,14 +4148,17 @@ class AiBuilderFacade:
|
|
|
3726
4148
|
"write_succeeded": write_succeeded,
|
|
3727
4149
|
"safe_to_retry": not write_executed,
|
|
3728
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)
|
|
3729
4160
|
return finalize(
|
|
3730
|
-
|
|
3731
|
-
profile=profile,
|
|
3732
|
-
app_key=app_key,
|
|
3733
|
-
publish=bool(write_succeeded and button_write_intent),
|
|
3734
|
-
response=response,
|
|
3735
|
-
edit_version_no=edit_version_no,
|
|
3736
|
-
)
|
|
4161
|
+
published_response
|
|
3737
4162
|
)
|
|
3738
4163
|
|
|
3739
4164
|
def app_custom_button_list(self, *, profile: str, app_key: str) -> JSONObject:
|
|
@@ -4359,6 +4784,18 @@ class AiBuilderFacade:
|
|
|
4359
4784
|
def app_associated_resources_apply(self, *, profile: str, request: AssociatedResourcesApplyRequest) -> JSONObject:
|
|
4360
4785
|
normalized_args = request.model_dump(mode="json", exclude_none=True)
|
|
4361
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
|
|
4362
4799
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
4363
4800
|
resource_write_intent = bool(
|
|
4364
4801
|
request.upsert_resources
|
|
@@ -4367,32 +4804,97 @@ class AiBuilderFacade:
|
|
|
4367
4804
|
or request.reorder_associated_item_ids
|
|
4368
4805
|
)
|
|
4369
4806
|
if resource_write_intent:
|
|
4807
|
+
permission_started_at = time.perf_counter()
|
|
4370
4808
|
permission_outcome = self._guard_app_permission(
|
|
4371
4809
|
profile=profile,
|
|
4372
4810
|
app_key=app_key,
|
|
4373
4811
|
required_permission="edit_app",
|
|
4374
4812
|
normalized_args=normalized_args,
|
|
4375
4813
|
)
|
|
4814
|
+
permission_ms += _duration_ms(permission_started_at)
|
|
4376
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
|
+
)
|
|
4377
4821
|
return permission_outcome.block
|
|
4378
4822
|
permission_outcomes.append(permission_outcome)
|
|
4379
4823
|
if request.view_configs:
|
|
4824
|
+
permission_started_at = time.perf_counter()
|
|
4380
4825
|
permission_outcome = self._guard_app_permission(
|
|
4381
4826
|
profile=profile,
|
|
4382
4827
|
app_key=app_key,
|
|
4383
4828
|
required_permission="view_manage",
|
|
4384
4829
|
normalized_args=normalized_args,
|
|
4385
4830
|
)
|
|
4831
|
+
permission_ms += _duration_ms(permission_started_at)
|
|
4386
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
|
+
)
|
|
4387
4838
|
return permission_outcome.block
|
|
4388
4839
|
permission_outcomes.append(permission_outcome)
|
|
4389
4840
|
|
|
4390
4841
|
def finalize(response: JSONObject) -> JSONObject:
|
|
4391
|
-
|
|
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
|
|
4392
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
|
+
)
|
|
4393
4889
|
try:
|
|
4394
|
-
|
|
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
|
+
)
|
|
4395
4896
|
except (QingflowApiError, RuntimeError) as error:
|
|
4897
|
+
resource_inventory_ms += _duration_ms(resource_inventory_started_at)
|
|
4396
4898
|
api_error = _coerce_api_error(error)
|
|
4397
4899
|
return finalize(_failed_from_api_error(
|
|
4398
4900
|
"ASSOCIATED_RESOURCES_READ_FAILED",
|
|
@@ -4401,7 +4903,10 @@ class AiBuilderFacade:
|
|
|
4401
4903
|
details=_with_state_read_blocked_details({"app_key": app_key}, resource="associated_resources", error=api_error),
|
|
4402
4904
|
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
4403
4905
|
))
|
|
4906
|
+
resource_inventory_ms += _duration_ms(resource_inventory_started_at)
|
|
4907
|
+
app_name_started_at = time.perf_counter()
|
|
4404
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)
|
|
4405
4910
|
|
|
4406
4911
|
existing_by_id = _associated_resource_index(existing_resources)
|
|
4407
4912
|
blocking_issues: list[dict[str, Any]] = []
|
|
@@ -4414,10 +4919,12 @@ class AiBuilderFacade:
|
|
|
4414
4919
|
|
|
4415
4920
|
upsert_resources = list(request.upsert_resources)
|
|
4416
4921
|
if request.patch_resources:
|
|
4922
|
+
resource_patch_started_at = time.perf_counter()
|
|
4417
4923
|
expanded_resources, patch_issues, patch_results = self._expand_associated_resource_partial_patches(
|
|
4418
4924
|
existing_by_id=existing_by_id,
|
|
4419
4925
|
patch_resources=request.patch_resources,
|
|
4420
4926
|
)
|
|
4927
|
+
resource_patch_hydration_ms += _duration_ms(resource_patch_started_at)
|
|
4421
4928
|
if patch_issues:
|
|
4422
4929
|
return finalize(
|
|
4423
4930
|
_failed(
|
|
@@ -4432,11 +4939,13 @@ class AiBuilderFacade:
|
|
|
4432
4939
|
normalized_args["upsert_resources"] = [patch.model_dump(mode="json") for patch in upsert_resources]
|
|
4433
4940
|
normalized_args["patch_results"] = patch_results
|
|
4434
4941
|
|
|
4942
|
+
match_mapping_compile_started_at = time.perf_counter()
|
|
4435
4943
|
compiled_resource_match_rules, resource_match_issues = self._compile_associated_resource_semantic_match_mappings(
|
|
4436
4944
|
profile=profile,
|
|
4437
4945
|
app_key=app_key,
|
|
4438
4946
|
patches=upsert_resources,
|
|
4439
4947
|
)
|
|
4948
|
+
match_mapping_compile_ms += _duration_ms(match_mapping_compile_started_at)
|
|
4440
4949
|
normalized_args["compiled_match_rules"] = {
|
|
4441
4950
|
str(index): _summarize_compiled_match_rules(rules)
|
|
4442
4951
|
for index, rules in compiled_resource_match_rules.items()
|
|
@@ -4650,12 +5159,14 @@ class AiBuilderFacade:
|
|
|
4650
5159
|
|
|
4651
5160
|
edit_version_no = None
|
|
4652
5161
|
if resource_write_intent:
|
|
5162
|
+
edit_context_started_at = time.perf_counter()
|
|
4653
5163
|
edit_version_no, edit_context_error = self._ensure_app_edit_context(
|
|
4654
5164
|
profile=profile,
|
|
4655
5165
|
app_key=app_key,
|
|
4656
5166
|
normalized_args=normalized_args,
|
|
4657
5167
|
failure_code="ASSOCIATED_RESOURCES_APPLY_FAILED",
|
|
4658
5168
|
)
|
|
5169
|
+
edit_context_ms += _duration_ms(edit_context_started_at)
|
|
4659
5170
|
if edit_context_error is not None:
|
|
4660
5171
|
return finalize(edit_context_error)
|
|
4661
5172
|
|
|
@@ -4670,6 +5181,7 @@ class AiBuilderFacade:
|
|
|
4670
5181
|
verification_errors: list[JSONObject] = []
|
|
4671
5182
|
write_executed = False
|
|
4672
5183
|
|
|
5184
|
+
resource_write_started_at = time.perf_counter()
|
|
4673
5185
|
for op in upsert_ops:
|
|
4674
5186
|
patch = cast(AssociatedResourceUpsertPatch, op["patch"])
|
|
4675
5187
|
try:
|
|
@@ -4687,26 +5199,30 @@ class AiBuilderFacade:
|
|
|
4687
5199
|
match_rules_override=compiled_resource_match_rules.get(int(op["index"])),
|
|
4688
5200
|
)
|
|
4689
5201
|
created_id = _extract_associated_resource_id_from_result(create_result)
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
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
|
+
)
|
|
4710
5226
|
created.append(_associated_resource_result_entry("create", op["index"], patch, associated_item_id=created_id))
|
|
4711
5227
|
if created_id is not None and patch.client_key:
|
|
4712
5228
|
client_key_to_id[str(patch.client_key)] = created_id
|
|
@@ -4764,15 +5280,19 @@ class AiBuilderFacade:
|
|
|
4764
5280
|
except (QingflowApiError, RuntimeError) as error:
|
|
4765
5281
|
api_error = _coerce_api_error(error)
|
|
4766
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)
|
|
4767
5284
|
|
|
4768
5285
|
resources_after: list[dict[str, Any]] = []
|
|
4769
5286
|
resources_after_loaded = False
|
|
4770
5287
|
resources_after_readback_failed = False
|
|
4771
5288
|
if request.view_configs:
|
|
4772
5289
|
try:
|
|
5290
|
+
resource_readback_started_at = time.perf_counter()
|
|
4773
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)
|
|
4774
5293
|
resources_after_loaded = True
|
|
4775
5294
|
except (QingflowApiError, RuntimeError) as error:
|
|
5295
|
+
resource_readback_ms += _duration_ms(resource_readback_started_at)
|
|
4776
5296
|
readback_errors.append({"resource": "associated_resources", "phase": "pre_view_config", "transport_error": _transport_error_payload(_coerce_api_error(error))})
|
|
4777
5297
|
resources_after = []
|
|
4778
5298
|
resources_after_readback_failed = True
|
|
@@ -4818,6 +5338,7 @@ class AiBuilderFacade:
|
|
|
4818
5338
|
failed.append({"operation": "view_config", "index": index, "view_key": view_config.view_key, "view_name": view_name, "status": "failed", "issues": issues})
|
|
4819
5339
|
continue
|
|
4820
5340
|
try:
|
|
5341
|
+
view_config_started_at = time.perf_counter()
|
|
4821
5342
|
write_executed = True
|
|
4822
5343
|
self._update_view_associated_resources_config(profile=profile, view_key=view_config.view_key, associated_resources_payload=view_payload)
|
|
4823
5344
|
try:
|
|
@@ -4847,16 +5368,35 @@ class AiBuilderFacade:
|
|
|
4847
5368
|
actual_config = {}
|
|
4848
5369
|
verified_config = False
|
|
4849
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)
|
|
4850
5372
|
except (QingflowApiError, RuntimeError) as error:
|
|
5373
|
+
view_config_ms += _duration_ms(view_config_started_at)
|
|
4851
5374
|
api_error = _coerce_api_error(error)
|
|
4852
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)})
|
|
4853
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
|
+
)
|
|
4854
5389
|
final_resources: list[dict[str, Any]] = resources_after if resources_after_loaded else []
|
|
4855
5390
|
readback_failed = resources_after_readback_failed
|
|
4856
|
-
if
|
|
5391
|
+
if final_readback_skipped:
|
|
5392
|
+
final_resources = []
|
|
5393
|
+
elif not resources_after_loaded and not resources_after_readback_failed:
|
|
4857
5394
|
try:
|
|
5395
|
+
resource_readback_started_at = time.perf_counter()
|
|
4858
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)
|
|
4859
5398
|
except (QingflowApiError, RuntimeError) as error:
|
|
5399
|
+
resource_readback_ms += _duration_ms(resource_readback_started_at)
|
|
4860
5400
|
readback_errors.append({"resource": "associated_resources", "phase": "final_pool", "transport_error": _transport_error_payload(_coerce_api_error(error))})
|
|
4861
5401
|
readback_failed = True
|
|
4862
5402
|
final_by_id = _associated_resource_index(final_resources)
|
|
@@ -4866,15 +5406,16 @@ class AiBuilderFacade:
|
|
|
4866
5406
|
resources=final_resources,
|
|
4867
5407
|
readback_failed=readback_failed,
|
|
4868
5408
|
)
|
|
4869
|
-
created_ids = [int(item["associated_item_id"]) for item in created if _coerce_positive_int(item.get("associated_item_id")) is not None]
|
|
4870
|
-
updated_ids = [int(item["associated_item_id"]) for item in updated if _coerce_positive_int(item.get("associated_item_id")) is not None]
|
|
4871
|
-
unchanged_ids = [int(item["associated_item_id"]) for item in unchanged if _coerce_positive_int(item.get("associated_item_id")) is not None]
|
|
4872
5409
|
removed_ids = [int(item["associated_item_id"]) for item in removed if _coerce_positive_int(item.get("associated_item_id")) is not None]
|
|
4873
5410
|
removed_verified = all(str(item.get("readback_status") or "") == "deleted" for item in removed)
|
|
4874
5411
|
remove_readback_pending = any(str(item.get("readback_status") or "") != "deleted" for item in removed)
|
|
4875
|
-
|
|
5412
|
+
pool_items_verified = final_readback_skipped or (
|
|
4876
5413
|
not readback_failed
|
|
4877
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
|
|
4878
5419
|
and removed_verified
|
|
4879
5420
|
and not failed
|
|
4880
5421
|
and all(_coerce_positive_int(item.get("associated_item_id")) is not None for item in created)
|
|
@@ -4943,6 +5484,8 @@ class AiBuilderFacade:
|
|
|
4943
5484
|
"edit_version_no": edit_version_no,
|
|
4944
5485
|
"associated_item_ids_by_client_key": client_key_to_id,
|
|
4945
5486
|
"readback_failed": readback_failed,
|
|
5487
|
+
"initial_inventory_read_skipped": not needs_resource_inventory,
|
|
5488
|
+
"final_readback_skipped": final_readback_skipped,
|
|
4946
5489
|
**({"readback_errors": readback_errors} if readback_errors else {}),
|
|
4947
5490
|
**({"verification_errors": verification_errors} if verification_errors else {}),
|
|
4948
5491
|
"compiled_match_rules": {
|
|
@@ -4958,7 +5501,8 @@ class AiBuilderFacade:
|
|
|
4958
5501
|
"verification": {
|
|
4959
5502
|
"associated_resources_verified": pool_verified,
|
|
4960
5503
|
"associated_resource_view_configs_verified": view_configs_verified,
|
|
4961
|
-
"readback_loaded": not readback_failed,
|
|
5504
|
+
"readback_loaded": False if final_readback_skipped else not readback_failed,
|
|
5505
|
+
"final_readback_skipped": final_readback_skipped,
|
|
4962
5506
|
"removed_verified": removed_verified,
|
|
4963
5507
|
"remove_readback_pending": remove_readback_pending,
|
|
4964
5508
|
"removed_readback_results": deepcopy(removed),
|
|
@@ -4980,6 +5524,7 @@ class AiBuilderFacade:
|
|
|
4980
5524
|
"safe_to_retry": not write_executed,
|
|
4981
5525
|
"associated_resources": final_resources,
|
|
4982
5526
|
}
|
|
5527
|
+
publish_started_at = time.perf_counter()
|
|
4983
5528
|
response = self._append_publish_result(
|
|
4984
5529
|
profile=profile,
|
|
4985
5530
|
app_key=app_key,
|
|
@@ -4987,7 +5532,9 @@ class AiBuilderFacade:
|
|
|
4987
5532
|
response=response,
|
|
4988
5533
|
edit_version_no=edit_version_no,
|
|
4989
5534
|
)
|
|
5535
|
+
publish_ms += _duration_ms(publish_started_at)
|
|
4990
5536
|
if response.get("published") and view_config_results:
|
|
5537
|
+
post_publish_started_at = time.perf_counter()
|
|
4991
5538
|
try:
|
|
4992
5539
|
post_publish_resources = self._load_associated_resources_for_builder(profile=profile, app_key=app_key)
|
|
4993
5540
|
except (QingflowApiError, RuntimeError) as error:
|
|
@@ -5096,6 +5643,7 @@ class AiBuilderFacade:
|
|
|
5096
5643
|
response["recoverable"] = False
|
|
5097
5644
|
response["message"] = "applied associated resources patch"
|
|
5098
5645
|
response["suggested_next_call"] = None
|
|
5646
|
+
post_publish_verify_ms += _duration_ms(post_publish_started_at)
|
|
5099
5647
|
return finalize(response)
|
|
5100
5648
|
|
|
5101
5649
|
def _resolve_app_matches_in_visible_apps(
|
|
@@ -6152,32 +6700,52 @@ class AiBuilderFacade:
|
|
|
6152
6700
|
idempotency_key: str | None = None,
|
|
6153
6701
|
schema_version: str | None = None,
|
|
6154
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
|
+
|
|
6155
6718
|
try:
|
|
6719
|
+
flow_patch_read_started_at = time.perf_counter()
|
|
6156
6720
|
current = self.flow_get(profile=profile, app_key=app_key)
|
|
6157
6721
|
except (QingflowApiError, RuntimeError) as error:
|
|
6722
|
+
flow_patch_read_ms += _duration_ms(flow_patch_read_started_at)
|
|
6158
6723
|
api_error = _coerce_api_error(error)
|
|
6159
|
-
return _failed_from_api_error(
|
|
6724
|
+
return attach_patch_duration(_failed_from_api_error(
|
|
6160
6725
|
"FLOW_READ_FAILED",
|
|
6161
6726
|
api_error,
|
|
6162
6727
|
normalized_args={"app_key": app_key},
|
|
6163
6728
|
details={"app_key": app_key},
|
|
6164
6729
|
suggested_next_call={"tool_name": "app_flow_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
6165
|
-
)
|
|
6730
|
+
))
|
|
6731
|
+
flow_patch_read_ms += _duration_ms(flow_patch_read_started_at)
|
|
6166
6732
|
if str(current.get("status") or "").strip().lower() != "success":
|
|
6167
|
-
return _failed(
|
|
6733
|
+
return attach_patch_duration(_failed(
|
|
6168
6734
|
"FLOW_READ_FAILED",
|
|
6169
6735
|
current.get("message") or "failed to read current flow",
|
|
6170
6736
|
normalized_args={"app_key": app_key},
|
|
6171
6737
|
suggested_next_call={"tool_name": "app_flow_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
6172
|
-
)
|
|
6738
|
+
))
|
|
6739
|
+
flow_patch_compile_started_at = time.perf_counter()
|
|
6173
6740
|
spec = deepcopy(current.get("spec") or {})
|
|
6174
6741
|
if not isinstance(spec, dict) or not spec:
|
|
6175
|
-
|
|
6742
|
+
flow_patch_compile_ms += _duration_ms(flow_patch_compile_started_at)
|
|
6743
|
+
return attach_patch_duration(_failed(
|
|
6176
6744
|
"FLOW_PATCH_FAILED",
|
|
6177
6745
|
"current flow has no spec; use full app_flow_apply with spec first",
|
|
6178
6746
|
normalized_args={"app_key": app_key, "patch_nodes": patch_nodes},
|
|
6179
6747
|
suggested_next_call={"tool_name": "app_flow_apply", "arguments": {"profile": profile, "app_key": app_key}},
|
|
6180
|
-
)
|
|
6748
|
+
))
|
|
6181
6749
|
nodes = spec.get("nodes") if isinstance(spec.get("nodes"), list) else []
|
|
6182
6750
|
node_by_id: dict[str, dict[str, Any]] = {}
|
|
6183
6751
|
for node in nodes:
|
|
@@ -6204,30 +6772,35 @@ class AiBuilderFacade:
|
|
|
6204
6772
|
for key in unset_payload:
|
|
6205
6773
|
target.pop(str(key), None)
|
|
6206
6774
|
if invalid_items:
|
|
6207
|
-
|
|
6775
|
+
flow_patch_compile_ms += _duration_ms(flow_patch_compile_started_at)
|
|
6776
|
+
return attach_patch_duration(_failed(
|
|
6208
6777
|
"FLOW_PATCH_INVALID",
|
|
6209
6778
|
"patch_nodes[] contains invalid items",
|
|
6210
6779
|
normalized_args={"app_key": app_key, "invalid_items": invalid_items},
|
|
6211
6780
|
suggested_next_call={"tool_name": "app_flow_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
6212
|
-
)
|
|
6781
|
+
))
|
|
6213
6782
|
if not_found:
|
|
6214
|
-
|
|
6783
|
+
flow_patch_compile_ms += _duration_ms(flow_patch_compile_started_at)
|
|
6784
|
+
return attach_patch_duration(_failed(
|
|
6215
6785
|
"FLOW_NODE_NOT_FOUND",
|
|
6216
6786
|
f"patch_nodes[] referenced node id(s) not found in current flow: {not_found}",
|
|
6217
6787
|
normalized_args={"app_key": app_key, "not_found_ids": not_found},
|
|
6218
6788
|
suggested_next_call={"tool_name": "app_flow_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
6219
|
-
)
|
|
6789
|
+
))
|
|
6220
6790
|
edge_container = spec.get("edges") if isinstance(spec.get("edges"), dict) else {}
|
|
6221
6791
|
edges = edge_container.get("edges") if isinstance(edge_container, dict) else None
|
|
6222
6792
|
transitions = spec.get("transitions") if isinstance(spec.get("transitions"), list) else None
|
|
6223
6793
|
if not edges and not transitions:
|
|
6224
|
-
|
|
6794
|
+
flow_patch_compile_ms += _duration_ms(flow_patch_compile_started_at)
|
|
6795
|
+
return attach_patch_duration(_failed(
|
|
6225
6796
|
"FLOW_PATCH_EMPTY_GRAPH_UNSUPPORTED",
|
|
6226
6797
|
"current flow spec has no edges; patch_nodes cannot safely save this backend shape",
|
|
6227
6798
|
normalized_args={"app_key": app_key, "patch_nodes": patch_nodes},
|
|
6228
6799
|
suggested_next_call={"tool_name": "app_flow_apply", "arguments": {"profile": profile, "app_key": app_key}},
|
|
6229
|
-
)
|
|
6230
|
-
|
|
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(
|
|
6231
6804
|
profile=profile,
|
|
6232
6805
|
app_key=app_key,
|
|
6233
6806
|
spec=spec,
|
|
@@ -6235,6 +6808,8 @@ class AiBuilderFacade:
|
|
|
6235
6808
|
idempotency_key=idempotency_key,
|
|
6236
6809
|
schema_version=schema_version,
|
|
6237
6810
|
)
|
|
6811
|
+
flow_patch_apply_ms += _duration_ms(flow_patch_apply_started_at)
|
|
6812
|
+
return attach_patch_duration(result)
|
|
6238
6813
|
|
|
6239
6814
|
def _components_to_sections_payload(self, components: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
6240
6815
|
result: list[dict[str, Any]] = []
|
|
@@ -7473,19 +8048,35 @@ class AiBuilderFacade:
|
|
|
7473
8048
|
all_verified = False
|
|
7474
8049
|
continue
|
|
7475
8050
|
try:
|
|
7476
|
-
verify_response = self.views.view_get_config(profile=profile, viewgraph_key=view_key)
|
|
7477
|
-
verify_config = verify_response.get("result") if isinstance(verify_response.get("result"), dict) else {}
|
|
7478
8051
|
expected_summary = _normalize_expected_view_buttons_for_compare(
|
|
7479
8052
|
effective_merged_dtos,
|
|
7480
8053
|
custom_button_details_by_id=button_inventory,
|
|
7481
8054
|
)
|
|
7482
|
-
actual_summary =
|
|
7483
|
-
comparison =
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
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
|
|
7487
8076
|
)
|
|
7488
|
-
verified =
|
|
8077
|
+
verified = (
|
|
8078
|
+
bool(comparison.get("verified")) or accepted_with_delayed_custom_button_readback
|
|
8079
|
+
) and unsupported_list_issue is None
|
|
7489
8080
|
if not verified:
|
|
7490
8081
|
all_verified = False
|
|
7491
8082
|
results.append(
|
|
@@ -7500,12 +8091,14 @@ class AiBuilderFacade:
|
|
|
7500
8091
|
"view_buttons_verified": verified,
|
|
7501
8092
|
"supported_buttons_verified": bool(comparison.get("verified")) if unsupported_list_issue is not None else None,
|
|
7502
8093
|
"unsupported_placements": ["list"] if unsupported_list_issue is not None else [],
|
|
7503
|
-
"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,
|
|
7504
8097
|
"expected_buttons": expected_summary,
|
|
7505
8098
|
"actual_buttons": actual_summary,
|
|
7506
8099
|
}
|
|
7507
8100
|
)
|
|
7508
|
-
if
|
|
8101
|
+
if pending_custom_button_readback and not accepted_with_delayed_custom_button_readback:
|
|
7509
8102
|
warnings.append(_warning("VIEW_CUSTOM_BUTTON_READBACK_PENDING", "view config write landed but custom button readback is delayed"))
|
|
7510
8103
|
except (QingflowApiError, RuntimeError) as error:
|
|
7511
8104
|
api_error = _coerce_api_error(error)
|
|
@@ -7958,7 +8551,6 @@ class AiBuilderFacade:
|
|
|
7958
8551
|
app_key=request.app_key,
|
|
7959
8552
|
app_name=request.app_name,
|
|
7960
8553
|
package_tag_id=request.package_tag_id,
|
|
7961
|
-
create_if_missing=request.create_if_missing,
|
|
7962
8554
|
)
|
|
7963
8555
|
if target.get("status") == "failed":
|
|
7964
8556
|
target.setdefault("normalized_args", normalized_args)
|
|
@@ -8230,7 +8822,6 @@ class AiBuilderFacade:
|
|
|
8230
8822
|
"arguments": {
|
|
8231
8823
|
"profile": profile,
|
|
8232
8824
|
"app_key": request.app_key,
|
|
8233
|
-
"create_if_missing": False,
|
|
8234
8825
|
"add_fields": [{"name": "状态", "type": "select", "options": ["草稿", "进行中", "已完成"], "required": True}],
|
|
8235
8826
|
"update_fields": [],
|
|
8236
8827
|
"remove_fields": [],
|
|
@@ -8580,12 +9171,21 @@ class AiBuilderFacade:
|
|
|
8580
9171
|
icon: str | None = None,
|
|
8581
9172
|
color: str | None = None,
|
|
8582
9173
|
visibility: VisibilityPatch | None = None,
|
|
8583
|
-
create_if_missing: bool = False,
|
|
8584
9174
|
publish: bool = True,
|
|
8585
9175
|
add_fields: list[FieldPatch],
|
|
8586
9176
|
update_fields: list[FieldUpdatePatch],
|
|
8587
9177
|
remove_fields: list[FieldRemovePatch],
|
|
9178
|
+
inline_form_layout_sections: list[LayoutSectionPatch] | None = None,
|
|
8588
9179
|
) -> JSONObject:
|
|
9180
|
+
apply_started_at = time.perf_counter()
|
|
9181
|
+
schema_write_ms: int | None = None
|
|
9182
|
+
inline_layout_ms: int | None = None
|
|
9183
|
+
publish_ms: int | None = None
|
|
9184
|
+
readback_ms: int | None = None
|
|
9185
|
+
requested_inline_layout_sections = [
|
|
9186
|
+
section.model_dump(mode="json", exclude_none=True)
|
|
9187
|
+
for section in (inline_form_layout_sections or [])
|
|
9188
|
+
]
|
|
8589
9189
|
normalized_args = {
|
|
8590
9190
|
"app_key": app_key,
|
|
8591
9191
|
"package_tag_id": package_tag_id,
|
|
@@ -8593,12 +9193,13 @@ class AiBuilderFacade:
|
|
|
8593
9193
|
"icon": icon,
|
|
8594
9194
|
"color": color,
|
|
8595
9195
|
"visibility": visibility.model_dump(mode="json") if visibility is not None else None,
|
|
8596
|
-
"create_if_missing": create_if_missing,
|
|
8597
9196
|
"publish": publish,
|
|
8598
9197
|
"add_fields": [patch.model_dump(mode="json") for patch in add_fields],
|
|
8599
9198
|
"update_fields": [patch.model_dump(mode="json") for patch in update_fields],
|
|
8600
9199
|
"remove_fields": [patch.model_dump(mode="json") for patch in remove_fields],
|
|
8601
9200
|
}
|
|
9201
|
+
if requested_inline_layout_sections:
|
|
9202
|
+
normalized_args["inline_form_layout_sections"] = requested_inline_layout_sections
|
|
8602
9203
|
try:
|
|
8603
9204
|
desired_auth = (
|
|
8604
9205
|
self._compile_visibility_to_member_auth(profile=profile, visibility=visibility)
|
|
@@ -8614,26 +9215,32 @@ class AiBuilderFacade:
|
|
|
8614
9215
|
suggested_next_call=None,
|
|
8615
9216
|
)
|
|
8616
9217
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
if app_key:
|
|
8620
|
-
resolved = self.app_resolve(profile=profile, app_key=app_key)
|
|
8621
|
-
elif app_name:
|
|
8622
|
-
resolved = self.app_resolve(profile=profile, app_name=app_name, package_tag_id=package_tag_id)
|
|
8623
|
-
else:
|
|
8624
|
-
return _failed("APP_NAME_REQUIRED", "app_name or app_key is required", normalized_args=normalized_args, suggested_next_call=None)
|
|
9218
|
+
requested_inline_layout = bool(requested_inline_layout_sections)
|
|
9219
|
+
requested_field_changes = bool(add_fields or update_fields or remove_fields or requested_inline_layout)
|
|
8625
9220
|
|
|
8626
9221
|
def finalize(response: JSONObject) -> JSONObject:
|
|
8627
9222
|
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
8628
9223
|
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
|
|
8633
|
-
|
|
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()
|
|
8634
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
|
+
)
|
|
8635
9237
|
if permission_tag_id is None:
|
|
8636
|
-
|
|
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
|
+
)
|
|
8637
9244
|
add_permission_outcome = self._guard_package_permission(
|
|
8638
9245
|
profile=profile,
|
|
8639
9246
|
tag_id=permission_tag_id,
|
|
@@ -8645,8 +9252,8 @@ class AiBuilderFacade:
|
|
|
8645
9252
|
permission_outcomes.append(add_permission_outcome)
|
|
8646
9253
|
resolved = self._create_target_app_shell(
|
|
8647
9254
|
profile=profile,
|
|
8648
|
-
app_name=
|
|
8649
|
-
package_tag_id=
|
|
9255
|
+
app_name=requested_app_name_for_create,
|
|
9256
|
+
package_tag_id=permission_tag_id,
|
|
8650
9257
|
icon=icon,
|
|
8651
9258
|
color=color,
|
|
8652
9259
|
auth=desired_auth,
|
|
@@ -8659,6 +9266,11 @@ class AiBuilderFacade:
|
|
|
8659
9266
|
if not isinstance(resolved.get("normalized_args"), dict) or not resolved.get("normalized_args"):
|
|
8660
9267
|
resolved["normalized_args"] = normalized_args
|
|
8661
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)
|
|
8662
9274
|
resolved_outcome = _permission_outcome_from_result(resolved)
|
|
8663
9275
|
if resolved_outcome is not None:
|
|
8664
9276
|
permission_outcomes.append(resolved_outcome)
|
|
@@ -8950,6 +9562,59 @@ class AiBuilderFacade:
|
|
|
8950
9562
|
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
8951
9563
|
)
|
|
8952
9564
|
|
|
9565
|
+
inline_form_layout_target: dict[str, Any] | None = None
|
|
9566
|
+
inline_form_layout_auto_added_fields: list[str] = []
|
|
9567
|
+
inline_form_layout_changed = False
|
|
9568
|
+
if requested_inline_layout:
|
|
9569
|
+
inline_layout_started_at = time.perf_counter()
|
|
9570
|
+
resolved_inline_sections, missing_selectors = _resolve_layout_sections_to_names(
|
|
9571
|
+
requested_inline_layout_sections,
|
|
9572
|
+
current_fields,
|
|
9573
|
+
)
|
|
9574
|
+
normalized_args["inline_form_layout_sections"] = resolved_inline_sections
|
|
9575
|
+
if missing_selectors:
|
|
9576
|
+
return _failed(
|
|
9577
|
+
"UNKNOWN_LAYOUT_FIELD",
|
|
9578
|
+
"inline form layout references unknown field selectors",
|
|
9579
|
+
normalized_args=normalized_args,
|
|
9580
|
+
details={"unknown_selectors": missing_selectors},
|
|
9581
|
+
missing_fields=[str(item) for item in missing_selectors],
|
|
9582
|
+
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
9583
|
+
)
|
|
9584
|
+
fields_by_name = {field["name"]: field for field in current_fields}
|
|
9585
|
+
seen_layout_fields: list[str] = []
|
|
9586
|
+
for section in resolved_inline_sections:
|
|
9587
|
+
for row in section.get("rows", []):
|
|
9588
|
+
for field_name in row:
|
|
9589
|
+
if field_name not in fields_by_name:
|
|
9590
|
+
return _failed(
|
|
9591
|
+
"UNKNOWN_LAYOUT_FIELD",
|
|
9592
|
+
f"inline form layout references unknown field '{field_name}'",
|
|
9593
|
+
normalized_args=normalized_args,
|
|
9594
|
+
details={"field_name": field_name},
|
|
9595
|
+
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
9596
|
+
)
|
|
9597
|
+
if field_name in seen_layout_fields:
|
|
9598
|
+
return _failed(
|
|
9599
|
+
"DUPLICATE_LAYOUT_FIELD",
|
|
9600
|
+
f"inline form layout references field '{field_name}' more than once",
|
|
9601
|
+
normalized_args=normalized_args,
|
|
9602
|
+
details={"field_name": field_name},
|
|
9603
|
+
suggested_next_call={"tool_name": "app_schema_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
9604
|
+
)
|
|
9605
|
+
seen_layout_fields.append(field_name)
|
|
9606
|
+
merged_inline_layout = _merge_layout(
|
|
9607
|
+
current_layout=layout,
|
|
9608
|
+
requested_sections=resolved_inline_sections,
|
|
9609
|
+
all_field_names=[field["name"] for field in current_fields],
|
|
9610
|
+
)
|
|
9611
|
+
inline_form_layout_auto_added_fields = list(merged_inline_layout.get("auto_added_fields") or [])
|
|
9612
|
+
inline_form_layout_target = cast(dict[str, Any], merged_inline_layout["layout"])
|
|
9613
|
+
inline_form_layout_changed = not _layouts_equal(layout, inline_form_layout_target)
|
|
9614
|
+
if inline_form_layout_changed:
|
|
9615
|
+
layout = inline_form_layout_target
|
|
9616
|
+
inline_layout_ms = _duration_ms(inline_layout_started_at)
|
|
9617
|
+
|
|
8953
9618
|
schema_write_requested = bool(
|
|
8954
9619
|
added
|
|
8955
9620
|
or updated
|
|
@@ -8957,6 +9622,7 @@ class AiBuilderFacade:
|
|
|
8957
9622
|
or normalized_code_block_fields
|
|
8958
9623
|
or data_display_selection.has_any
|
|
8959
9624
|
or bool(resolved.get("created"))
|
|
9625
|
+
or inline_form_layout_changed
|
|
8960
9626
|
)
|
|
8961
9627
|
if schema_write_requested:
|
|
8962
9628
|
try:
|
|
@@ -9006,7 +9672,8 @@ class AiBuilderFacade:
|
|
|
9006
9672
|
else []
|
|
9007
9673
|
)
|
|
9008
9674
|
|
|
9009
|
-
if not
|
|
9675
|
+
if not schema_write_requested:
|
|
9676
|
+
readback_started_at = time.perf_counter()
|
|
9010
9677
|
try:
|
|
9011
9678
|
base_info = self.apps.app_get_base(profile=profile, app_key=target.app_key, include_raw=True).get("result") or {}
|
|
9012
9679
|
except (QingflowApiError, RuntimeError) as error:
|
|
@@ -9042,6 +9709,7 @@ class AiBuilderFacade:
|
|
|
9042
9709
|
expected_app_icon = str(visual_result.get("app_icon") or "").strip() or None
|
|
9043
9710
|
app_icon_verified = True if expected_app_icon is None else actual_app_icon == expected_app_icon
|
|
9044
9711
|
app_base_verified = actual_app_name == effective_app_name and app_icon_verified
|
|
9712
|
+
readback_ms = _duration_ms(readback_started_at)
|
|
9045
9713
|
verified = app_base_verified and relation_target_metadata_verified
|
|
9046
9714
|
response = {
|
|
9047
9715
|
"status": "success" if verified else "partial_success",
|
|
@@ -9079,11 +9747,34 @@ class AiBuilderFacade:
|
|
|
9079
9747
|
"package_attached": package_attached,
|
|
9080
9748
|
}
|
|
9081
9749
|
response["details"]["relation_field_count"] = relation_field_count
|
|
9750
|
+
if requested_inline_layout:
|
|
9751
|
+
response["inline_form_layout"] = {
|
|
9752
|
+
"requested": True,
|
|
9753
|
+
"section_count": len(requested_inline_layout_sections),
|
|
9754
|
+
"app_key": target.app_key,
|
|
9755
|
+
"applied": True,
|
|
9756
|
+
"applied_in_schema": True,
|
|
9757
|
+
"layout_status": "success",
|
|
9758
|
+
"message": "layout already matched requested state",
|
|
9759
|
+
"auto_added_fields": inline_form_layout_auto_added_fields,
|
|
9760
|
+
}
|
|
9761
|
+
response["verification"]["inline_form_layout_verified"] = True
|
|
9082
9762
|
if normalized_code_block_fields:
|
|
9083
9763
|
response["normalized_code_block_output_assignment"] = True
|
|
9084
9764
|
response["normalized_code_block_fields"] = normalized_code_block_fields
|
|
9085
9765
|
response = _apply_permission_outcomes(response, relation_permission_outcome)
|
|
9086
|
-
|
|
9766
|
+
publish_started_at = time.perf_counter()
|
|
9767
|
+
response = self._append_publish_result(profile=profile, app_key=target.app_key, publish=publish, response=response)
|
|
9768
|
+
publish_ms = _duration_ms(publish_started_at)
|
|
9769
|
+
_merge_duration_breakdown(
|
|
9770
|
+
response,
|
|
9771
|
+
schema_write_ms=0,
|
|
9772
|
+
inline_layout_ms=inline_layout_ms,
|
|
9773
|
+
publish_ms=publish_ms,
|
|
9774
|
+
readback_ms=readback_ms,
|
|
9775
|
+
total_ms=_duration_ms(apply_started_at),
|
|
9776
|
+
)
|
|
9777
|
+
return finalize(response)
|
|
9087
9778
|
|
|
9088
9779
|
payload = (
|
|
9089
9780
|
_build_form_payload_from_fields(
|
|
@@ -9109,7 +9800,9 @@ class AiBuilderFacade:
|
|
|
9109
9800
|
current_schema=schema_result,
|
|
9110
9801
|
)
|
|
9111
9802
|
try:
|
|
9803
|
+
schema_write_started_at = time.perf_counter()
|
|
9112
9804
|
self.apps.app_update_form_schema(profile=profile, app_key=target.app_key, payload=payload)
|
|
9805
|
+
schema_write_ms = _duration_ms(schema_write_started_at)
|
|
9113
9806
|
except (QingflowApiError, RuntimeError) as error:
|
|
9114
9807
|
api_error = _coerce_api_error(error)
|
|
9115
9808
|
if backend_code_int(api_error) == 49614:
|
|
@@ -9271,7 +9964,9 @@ class AiBuilderFacade:
|
|
|
9271
9964
|
current_schema=rebound_schema,
|
|
9272
9965
|
)
|
|
9273
9966
|
try:
|
|
9967
|
+
schema_write_started_at = time.perf_counter()
|
|
9274
9968
|
self.apps.app_update_form_schema(profile=profile, app_key=target.app_key, payload=rebound_payload)
|
|
9969
|
+
schema_write_ms = (schema_write_ms or 0) + _duration_ms(schema_write_started_at)
|
|
9275
9970
|
except (QingflowApiError, RuntimeError) as error:
|
|
9276
9971
|
api_error = _coerce_api_error(error)
|
|
9277
9972
|
return _failed_from_api_error(
|
|
@@ -9334,6 +10029,22 @@ class AiBuilderFacade:
|
|
|
9334
10029
|
"package_attached": None,
|
|
9335
10030
|
}
|
|
9336
10031
|
response["details"]["relation_field_count"] = relation_field_count
|
|
10032
|
+
if requested_inline_layout:
|
|
10033
|
+
response["inline_form_layout"] = {
|
|
10034
|
+
"requested": True,
|
|
10035
|
+
"section_count": len(requested_inline_layout_sections),
|
|
10036
|
+
"app_key": target.app_key,
|
|
10037
|
+
"applied": True,
|
|
10038
|
+
"applied_in_schema": True,
|
|
10039
|
+
"layout_status": "success",
|
|
10040
|
+
"message": (
|
|
10041
|
+
"applied inline form layout inside schema write"
|
|
10042
|
+
if inline_form_layout_changed
|
|
10043
|
+
else "layout already matched requested state"
|
|
10044
|
+
),
|
|
10045
|
+
"auto_added_fields": inline_form_layout_auto_added_fields,
|
|
10046
|
+
}
|
|
10047
|
+
response["verification"]["inline_form_layout_verified"] = False
|
|
9337
10048
|
if normalized_code_block_fields:
|
|
9338
10049
|
response["normalized_code_block_output_assignment"] = True
|
|
9339
10050
|
response["normalized_code_block_fields"] = normalized_code_block_fields
|
|
@@ -9342,11 +10053,14 @@ class AiBuilderFacade:
|
|
|
9342
10053
|
if schema_readback_delayed_error is not None:
|
|
9343
10054
|
response["details"]["schema_readback_delayed_error"] = schema_readback_delayed_error
|
|
9344
10055
|
response = _apply_permission_outcomes(response, relation_permission_outcome)
|
|
10056
|
+
publish_started_at = time.perf_counter()
|
|
9345
10057
|
response = self._append_publish_result(profile=profile, app_key=target.app_key, publish=publish, response=response)
|
|
10058
|
+
publish_ms = _duration_ms(publish_started_at)
|
|
9346
10059
|
verification_ok = False
|
|
9347
10060
|
tag_ids_after: list[int] = []
|
|
9348
10061
|
package_attached: bool | None = None
|
|
9349
10062
|
verification_error: QingflowApiError | None = None
|
|
10063
|
+
readback_started_at = time.perf_counter()
|
|
9350
10064
|
try:
|
|
9351
10065
|
verified = self.app_read(profile=profile, app_key=target.app_key, include_raw=False)
|
|
9352
10066
|
verified_field_names = {field["name"] for field in verified["schema"]["fields"]}
|
|
@@ -9380,6 +10094,14 @@ class AiBuilderFacade:
|
|
|
9380
10094
|
response["verification"].update(data_display_verification)
|
|
9381
10095
|
if data_display_verification.get("data_display_config_verified") is False:
|
|
9382
10096
|
verification_ok = False
|
|
10097
|
+
if requested_inline_layout and inline_form_layout_target is not None:
|
|
10098
|
+
verified_layout = verified.get("layout") if isinstance(verified.get("layout"), dict) else {}
|
|
10099
|
+
inline_layout_verified = _layouts_equal(verified_layout, inline_form_layout_target) or _layouts_semantically_equal(
|
|
10100
|
+
verified_layout,
|
|
10101
|
+
inline_form_layout_target,
|
|
10102
|
+
)
|
|
10103
|
+
response["verification"]["inline_form_layout_verified"] = inline_layout_verified
|
|
10104
|
+
verification_ok = verification_ok and inline_layout_verified
|
|
9383
10105
|
if relation_degraded_expectations:
|
|
9384
10106
|
relation_verified_fields = cast(list[dict[str, Any]], verified["schema"]["fields"])
|
|
9385
10107
|
try:
|
|
@@ -9412,6 +10134,7 @@ class AiBuilderFacade:
|
|
|
9412
10134
|
tag_ids_after = []
|
|
9413
10135
|
package_attached = None if package_tag_id is None else False
|
|
9414
10136
|
app_base_verified = False
|
|
10137
|
+
readback_ms = _duration_ms(readback_started_at)
|
|
9415
10138
|
response["verification"]["fields_verified"] = verification_ok
|
|
9416
10139
|
response["verification"]["package_attached"] = package_attached
|
|
9417
10140
|
response["verification"]["app_visuals_verified"] = app_base_verified
|
|
@@ -9460,6 +10183,14 @@ class AiBuilderFacade:
|
|
|
9460
10183
|
"message": verification_error.message,
|
|
9461
10184
|
**_transport_error_payload(verification_error),
|
|
9462
10185
|
}
|
|
10186
|
+
_merge_duration_breakdown(
|
|
10187
|
+
response,
|
|
10188
|
+
schema_write_ms=schema_write_ms,
|
|
10189
|
+
inline_layout_ms=inline_layout_ms,
|
|
10190
|
+
publish_ms=publish_ms,
|
|
10191
|
+
readback_ms=readback_ms,
|
|
10192
|
+
total_ms=_duration_ms(apply_started_at),
|
|
10193
|
+
)
|
|
9463
10194
|
return finalize(response)
|
|
9464
10195
|
|
|
9465
10196
|
def app_layout_apply(
|
|
@@ -9478,71 +10209,113 @@ class AiBuilderFacade:
|
|
|
9478
10209
|
"sections": requested_sections,
|
|
9479
10210
|
"publish": publish,
|
|
9480
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
|
|
9481
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()
|
|
9482
10249
|
permission_outcome = self._guard_app_permission(
|
|
9483
10250
|
profile=profile,
|
|
9484
10251
|
app_key=app_key,
|
|
9485
10252
|
required_permission="edit_app",
|
|
9486
10253
|
normalized_args=normalized_args,
|
|
9487
10254
|
)
|
|
10255
|
+
permission_ms += _duration_ms(permission_started_at)
|
|
9488
10256
|
if permission_outcome.block is not None:
|
|
9489
|
-
return permission_outcome.block
|
|
10257
|
+
return finish(permission_outcome.block)
|
|
9490
10258
|
permission_outcomes.append(permission_outcome)
|
|
9491
10259
|
|
|
9492
|
-
def finalize(response: JSONObject) -> JSONObject:
|
|
9493
|
-
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
9494
|
-
|
|
9495
10260
|
try:
|
|
10261
|
+
schema_read_started_at = time.perf_counter()
|
|
9496
10262
|
schema_result, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
9497
10263
|
except (QingflowApiError, RuntimeError) as error:
|
|
10264
|
+
schema_read_ms += _duration_ms(schema_read_started_at)
|
|
9498
10265
|
api_error = _coerce_api_error(error)
|
|
9499
|
-
return
|
|
10266
|
+
return finish(_failed_from_api_error(
|
|
9500
10267
|
"LAYOUT_READ_FAILED",
|
|
9501
10268
|
api_error,
|
|
9502
10269
|
normalized_args=normalized_args,
|
|
9503
10270
|
details=_with_state_read_blocked_details({"app_key": app_key}, resource="schema", error=api_error),
|
|
9504
10271
|
suggested_next_call={"tool_name": "app_get_layout", "arguments": {"profile": profile, "app_key": app_key}},
|
|
9505
10272
|
))
|
|
10273
|
+
schema_read_ms += _duration_ms(schema_read_started_at)
|
|
10274
|
+
layout_compile_started_at = time.perf_counter()
|
|
9506
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
|
|
9507
10276
|
parsed = _parse_schema(schema_result)
|
|
9508
10277
|
current_fields = parsed["fields"]
|
|
9509
10278
|
requested_sections, missing_selectors = _resolve_layout_sections_to_names(requested_sections, current_fields)
|
|
9510
10279
|
normalized_args["sections"] = requested_sections
|
|
9511
10280
|
if missing_selectors:
|
|
9512
|
-
|
|
10281
|
+
layout_compile_ms += _duration_ms(layout_compile_started_at)
|
|
10282
|
+
return finish(_failed(
|
|
9513
10283
|
"UNKNOWN_LAYOUT_FIELD",
|
|
9514
10284
|
"layout references unknown field selectors",
|
|
9515
10285
|
normalized_args=normalized_args,
|
|
9516
10286
|
details={"unknown_selectors": missing_selectors},
|
|
9517
10287
|
missing_fields=[str(item) for item in missing_selectors],
|
|
9518
10288
|
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
9519
|
-
)
|
|
10289
|
+
))
|
|
9520
10290
|
fields_by_name = {field["name"]: field for field in current_fields}
|
|
9521
10291
|
seen: list[str] = []
|
|
9522
10292
|
for section in requested_sections:
|
|
9523
10293
|
for row in section.get("rows", []):
|
|
9524
10294
|
for field_name in row:
|
|
9525
10295
|
if field_name not in fields_by_name:
|
|
9526
|
-
|
|
10296
|
+
layout_compile_ms += _duration_ms(layout_compile_started_at)
|
|
10297
|
+
return finish(_failed(
|
|
9527
10298
|
"UNKNOWN_LAYOUT_FIELD",
|
|
9528
10299
|
f"layout references unknown field '{field_name}'",
|
|
9529
10300
|
normalized_args=normalized_args,
|
|
9530
10301
|
details={"field_name": field_name},
|
|
9531
10302
|
suggested_next_call={"tool_name": "app_get_layout", "arguments": {"profile": profile, "app_key": app_key}},
|
|
9532
|
-
)
|
|
10303
|
+
))
|
|
9533
10304
|
if field_name in seen:
|
|
9534
|
-
|
|
10305
|
+
layout_compile_ms += _duration_ms(layout_compile_started_at)
|
|
10306
|
+
return finish(_failed(
|
|
9535
10307
|
"DUPLICATE_LAYOUT_FIELD",
|
|
9536
10308
|
f"layout references field '{field_name}' more than once",
|
|
9537
10309
|
normalized_args=normalized_args,
|
|
9538
10310
|
details={"field_name": field_name},
|
|
9539
10311
|
suggested_next_call={"tool_name": "app_layout_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
9540
|
-
)
|
|
10312
|
+
))
|
|
9541
10313
|
seen.append(field_name)
|
|
9542
10314
|
expected = {field["name"] for field in current_fields}
|
|
9543
10315
|
if mode == LayoutApplyMode.replace and set(seen) != expected:
|
|
9544
10316
|
missing = sorted(expected.difference(seen))
|
|
9545
|
-
|
|
10317
|
+
layout_compile_ms += _duration_ms(layout_compile_started_at)
|
|
10318
|
+
return finish(_failed(
|
|
9546
10319
|
"INCOMPLETE_LAYOUT",
|
|
9547
10320
|
"layout must reference every current field exactly once in replace mode",
|
|
9548
10321
|
normalized_args=normalized_args,
|
|
@@ -9559,7 +10332,7 @@ class AiBuilderFacade:
|
|
|
9559
10332
|
"tool_name": "app_layout_apply",
|
|
9560
10333
|
"arguments": {"profile": profile, "app_key": app_key, "mode": "merge", "sections": requested_sections},
|
|
9561
10334
|
},
|
|
9562
|
-
)
|
|
10335
|
+
))
|
|
9563
10336
|
merged = _merge_layout(
|
|
9564
10337
|
current_layout=parsed["layout"],
|
|
9565
10338
|
requested_sections=requested_sections,
|
|
@@ -9571,6 +10344,7 @@ class AiBuilderFacade:
|
|
|
9571
10344
|
else merged["layout"]
|
|
9572
10345
|
)
|
|
9573
10346
|
if _layouts_equal(parsed["layout"], target_layout):
|
|
10347
|
+
layout_compile_ms += _duration_ms(layout_compile_started_at)
|
|
9574
10348
|
response = {
|
|
9575
10349
|
"status": "success",
|
|
9576
10350
|
"error_code": None,
|
|
@@ -9599,21 +10373,26 @@ class AiBuilderFacade:
|
|
|
9599
10373
|
"write_succeeded": False,
|
|
9600
10374
|
"safe_to_retry": True,
|
|
9601
10375
|
}
|
|
9602
|
-
return
|
|
10376
|
+
return finish_with_publish(response)
|
|
9603
10377
|
payload = _build_form_payload_from_existing_schema(
|
|
9604
10378
|
current_schema=schema_result,
|
|
9605
10379
|
layout=target_layout,
|
|
9606
10380
|
)
|
|
10381
|
+
layout_compile_ms += _duration_ms(layout_compile_started_at)
|
|
10382
|
+
edit_version_started_at = time.perf_counter()
|
|
9607
10383
|
payload["editVersionNo"] = self._resolve_form_edit_version(
|
|
9608
10384
|
profile=profile,
|
|
9609
10385
|
app_key=app_key,
|
|
9610
10386
|
current_schema=schema_result,
|
|
9611
10387
|
)
|
|
10388
|
+
edit_version_ms += _duration_ms(edit_version_started_at)
|
|
9612
10389
|
applied_layout = target_layout
|
|
9613
10390
|
fallback_applied = None
|
|
9614
10391
|
try:
|
|
10392
|
+
layout_write_started_at = time.perf_counter()
|
|
9615
10393
|
self.apps.app_update_form_schema(profile=profile, app_key=app_key, payload=payload)
|
|
9616
10394
|
except (QingflowApiError, RuntimeError) as error:
|
|
10395
|
+
layout_write_ms += _duration_ms(layout_write_started_at)
|
|
9617
10396
|
api_error = _coerce_api_error(error)
|
|
9618
10397
|
if backend_code_int(api_error) == 400 and target_layout.get("sections"):
|
|
9619
10398
|
flattened_layout = _flatten_layout_sections(target_layout)
|
|
@@ -9627,12 +10406,15 @@ class AiBuilderFacade:
|
|
|
9627
10406
|
current_schema=schema_result,
|
|
9628
10407
|
)
|
|
9629
10408
|
try:
|
|
10409
|
+
fallback_write_started_at = time.perf_counter()
|
|
9630
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)
|
|
9631
10412
|
applied_layout = flattened_layout
|
|
9632
10413
|
fallback_applied = "flatten_sections"
|
|
9633
10414
|
except (QingflowApiError, RuntimeError) as fallback_error:
|
|
10415
|
+
layout_write_ms += _duration_ms(fallback_write_started_at)
|
|
9634
10416
|
api_fallback_error = _coerce_api_error(fallback_error)
|
|
9635
|
-
return
|
|
10417
|
+
return finish(_failed_from_api_error(
|
|
9636
10418
|
"LAYOUT_APPLY_FAILED",
|
|
9637
10419
|
api_fallback_error,
|
|
9638
10420
|
normalized_args=normalized_args,
|
|
@@ -9647,7 +10429,7 @@ class AiBuilderFacade:
|
|
|
9647
10429
|
suggested_next_call={"tool_name": "app_layout_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
9648
10430
|
))
|
|
9649
10431
|
else:
|
|
9650
|
-
return
|
|
10432
|
+
return finish(_failed_from_api_error(
|
|
9651
10433
|
"LAYOUT_APPLY_FAILED",
|
|
9652
10434
|
api_error,
|
|
9653
10435
|
normalized_args=normalized_args,
|
|
@@ -9659,9 +10441,13 @@ class AiBuilderFacade:
|
|
|
9659
10441
|
},
|
|
9660
10442
|
suggested_next_call={"tool_name": "app_layout_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
9661
10443
|
))
|
|
10444
|
+
else:
|
|
10445
|
+
layout_write_ms += _duration_ms(layout_write_started_at)
|
|
9662
10446
|
try:
|
|
10447
|
+
layout_readback_started_at = time.perf_counter()
|
|
9663
10448
|
verified_schema, _verified_schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
9664
10449
|
except (QingflowApiError, RuntimeError) as error:
|
|
10450
|
+
layout_readback_ms += _duration_ms(layout_readback_started_at)
|
|
9665
10451
|
api_error = _coerce_api_error(error)
|
|
9666
10452
|
response = {
|
|
9667
10453
|
"status": "partial_success",
|
|
@@ -9701,7 +10487,8 @@ class AiBuilderFacade:
|
|
|
9701
10487
|
"write_succeeded": True,
|
|
9702
10488
|
"safe_to_retry": False,
|
|
9703
10489
|
}
|
|
9704
|
-
return
|
|
10490
|
+
return finish_with_publish(response)
|
|
10491
|
+
layout_readback_ms += _duration_ms(layout_readback_started_at)
|
|
9705
10492
|
verified_layout = _parse_schema(verified_schema)["layout"]
|
|
9706
10493
|
layout_verified = _layouts_equal(verified_layout, applied_layout) or _layouts_semantically_equal(verified_layout, applied_layout)
|
|
9707
10494
|
raw_layout_has_content = _schema_has_layout_content(verified_schema)
|
|
@@ -9715,24 +10502,21 @@ class AiBuilderFacade:
|
|
|
9715
10502
|
warnings.append(_warning("LAYOUT_SUMMARY_UNVERIFIED", "layout summary is incomplete relative to raw schema readback"))
|
|
9716
10503
|
if fallback_applied is not None and layout_verified and layout_summary_verified:
|
|
9717
10504
|
warnings.append(_warning("LAYOUT_FALLBACK_APPLIED", "layout readback normalized sectioned layout into flat layout while preserving field placement"))
|
|
10505
|
+
result_verified = layout_verified
|
|
9718
10506
|
response = {
|
|
9719
|
-
"status": "success" if
|
|
10507
|
+
"status": "success" if result_verified else "partial_success",
|
|
9720
10508
|
"error_code": (
|
|
9721
10509
|
None
|
|
9722
|
-
if
|
|
9723
|
-
else "LAYOUT_SUMMARY_UNVERIFIED"
|
|
9724
|
-
if not layout_summary_verified
|
|
10510
|
+
if result_verified
|
|
9725
10511
|
else "LAYOUT_READBACK_MISMATCH"
|
|
9726
10512
|
),
|
|
9727
|
-
"recoverable": not
|
|
10513
|
+
"recoverable": not result_verified,
|
|
9728
10514
|
"message": (
|
|
9729
10515
|
"applied app layout with flattened section fallback"
|
|
9730
10516
|
if layout_verified and layout_summary_verified and fallback_applied is not None
|
|
9731
10517
|
else
|
|
9732
10518
|
"applied app layout"
|
|
9733
|
-
if layout_verified
|
|
9734
|
-
else "applied app layout; raw layout verified but compact summary is incomplete"
|
|
9735
|
-
if layout_verified and not layout_summary_verified
|
|
10519
|
+
if layout_verified
|
|
9736
10520
|
else "applied app layout with flattened section fallback"
|
|
9737
10521
|
if fallback_applied is not None
|
|
9738
10522
|
else "applied app layout; readback did not fully match the requested layout"
|
|
@@ -9755,12 +10539,12 @@ class AiBuilderFacade:
|
|
|
9755
10539
|
"auto_added_fields": merged["auto_added_fields"] if mode == LayoutApplyMode.merge else [],
|
|
9756
10540
|
"fallback_applied": fallback_applied,
|
|
9757
10541
|
},
|
|
9758
|
-
"verified":
|
|
10542
|
+
"verified": result_verified,
|
|
9759
10543
|
"write_executed": True,
|
|
9760
10544
|
"write_succeeded": True,
|
|
9761
10545
|
"safe_to_retry": False,
|
|
9762
10546
|
}
|
|
9763
|
-
return
|
|
10547
|
+
return finish_with_publish(response)
|
|
9764
10548
|
|
|
9765
10549
|
def app_flow_apply(
|
|
9766
10550
|
self,
|
|
@@ -9779,22 +10563,54 @@ class AiBuilderFacade:
|
|
|
9779
10563
|
"transitions": transitions,
|
|
9780
10564
|
"publish": publish,
|
|
9781
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
|
|
9782
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()
|
|
9783
10601
|
permission_outcome = self._guard_app_permission(
|
|
9784
10602
|
profile=profile,
|
|
9785
10603
|
app_key=app_key,
|
|
9786
10604
|
required_permission="edit_app",
|
|
9787
10605
|
normalized_args=normalized_args,
|
|
9788
10606
|
)
|
|
10607
|
+
permission_ms += _duration_ms(permission_started_at)
|
|
9789
10608
|
if permission_outcome.block is not None:
|
|
9790
|
-
return permission_outcome.block
|
|
10609
|
+
return finish(permission_outcome.block)
|
|
9791
10610
|
permission_outcomes.append(permission_outcome)
|
|
9792
10611
|
|
|
9793
|
-
def finalize(response: JSONObject) -> JSONObject:
|
|
9794
|
-
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
9795
|
-
|
|
9796
10612
|
if mode != "replace":
|
|
9797
|
-
return
|
|
10613
|
+
return finish(_failed(
|
|
9798
10614
|
"UNSUPPORTED_FLOW_MODE",
|
|
9799
10615
|
"only mode='replace' is supported",
|
|
9800
10616
|
normalized_args=normalized_args,
|
|
@@ -9802,17 +10618,21 @@ class AiBuilderFacade:
|
|
|
9802
10618
|
suggested_next_call={"tool_name": "app_flow_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
9803
10619
|
))
|
|
9804
10620
|
try:
|
|
10621
|
+
flow_state_read_started_at = time.perf_counter()
|
|
9805
10622
|
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
9806
10623
|
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
9807
10624
|
except (QingflowApiError, RuntimeError) as error:
|
|
10625
|
+
flow_state_read_ms += _duration_ms(flow_state_read_started_at)
|
|
9808
10626
|
api_error = _coerce_api_error(error)
|
|
9809
|
-
return
|
|
10627
|
+
return finish(_failed_from_api_error(
|
|
9810
10628
|
"FLOW_READ_FAILED",
|
|
9811
10629
|
api_error,
|
|
9812
10630
|
normalized_args=normalized_args,
|
|
9813
10631
|
details=_with_state_read_blocked_details({"app_key": app_key}, resource="workflow", error=api_error),
|
|
9814
10632
|
suggested_next_call={"tool_name": "app_get_flow", "arguments": {"profile": profile, "app_key": app_key}},
|
|
9815
10633
|
))
|
|
10634
|
+
flow_state_read_ms += _duration_ms(flow_state_read_started_at)
|
|
10635
|
+
flow_compile_started_at = time.perf_counter()
|
|
9816
10636
|
app_name = str(base.get("formTitle") or base.get("title") or base.get("appName") or app_key).strip() or app_key
|
|
9817
10637
|
entity = _entity_spec_from_app(base_info=base, schema=schema, views=None)
|
|
9818
10638
|
current_fields = _parse_schema(schema)["fields"]
|
|
@@ -9821,7 +10641,8 @@ class AiBuilderFacade:
|
|
|
9821
10641
|
unsupported_nodes = self._unsupported_public_flow_nodes(nodes=public_nodes)
|
|
9822
10642
|
normalized_args["nodes"] = public_nodes
|
|
9823
10643
|
if unsupported_nodes:
|
|
9824
|
-
|
|
10644
|
+
flow_compile_ms += _duration_ms(flow_compile_started_at)
|
|
10645
|
+
return finish(_failed(
|
|
9825
10646
|
"FLOW_NODE_TYPE_UNSUPPORTED",
|
|
9826
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.",
|
|
9827
10648
|
normalized_args=normalized_args,
|
|
@@ -9831,7 +10652,7 @@ class AiBuilderFacade:
|
|
|
9831
10652
|
"disabled_node_types": sorted(DISABLED_PUBLIC_FLOW_NODE_TYPES),
|
|
9832
10653
|
},
|
|
9833
10654
|
suggested_next_call={"tool_name": "builder_tool_contract", "arguments": {"tool_name": "app_flow_apply"}},
|
|
9834
|
-
)
|
|
10655
|
+
))
|
|
9835
10656
|
if resolution_issues:
|
|
9836
10657
|
first_issue = resolution_issues[0]
|
|
9837
10658
|
suggested_call = None
|
|
@@ -9841,13 +10662,14 @@ class AiBuilderFacade:
|
|
|
9841
10662
|
suggested_call = {"tool_name": "member_search", "arguments": {"profile": profile, "query": first_issue.get("value") or ""}}
|
|
9842
10663
|
elif first_issue.get("kind") in {"editable_fields", "condition_fields"}:
|
|
9843
10664
|
suggested_call = {"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}}
|
|
9844
|
-
|
|
10665
|
+
flow_compile_ms += _duration_ms(flow_compile_started_at)
|
|
10666
|
+
return finish(_failed(
|
|
9845
10667
|
first_issue.get("error_code") or "FLOW_ASSIGNEE_UNRESOLVED",
|
|
9846
10668
|
"workflow contains unresolved assignees or field permissions",
|
|
9847
10669
|
normalized_args=normalized_args,
|
|
9848
10670
|
details={"issues": resolution_issues},
|
|
9849
10671
|
suggested_next_call=suggested_call,
|
|
9850
|
-
)
|
|
10672
|
+
))
|
|
9851
10673
|
assignee_required_nodes = [
|
|
9852
10674
|
node.get("id")
|
|
9853
10675
|
for node in normalized_nodes
|
|
@@ -9858,19 +10680,21 @@ class AiBuilderFacade:
|
|
|
9858
10680
|
)
|
|
9859
10681
|
]
|
|
9860
10682
|
if assignee_required_nodes:
|
|
9861
|
-
|
|
10683
|
+
flow_compile_ms += _duration_ms(flow_compile_started_at)
|
|
10684
|
+
return finish(_failed(
|
|
9862
10685
|
"FLOW_ASSIGNEE_REQUIRED",
|
|
9863
10686
|
"workflow approval/fill/copy nodes must declare at least one role or member assignee",
|
|
9864
10687
|
normalized_args=normalized_args,
|
|
9865
10688
|
details={"node_ids": assignee_required_nodes, "policy": "prefer role assignees; explicit members are also supported"},
|
|
9866
10689
|
suggested_next_call={"tool_name": "role_search", "arguments": {"profile": profile, "keyword": ""}},
|
|
9867
|
-
)
|
|
10690
|
+
))
|
|
9868
10691
|
workflow_spec = _build_public_workflow_spec(nodes=normalized_nodes, transitions=transitions)
|
|
9869
10692
|
if workflow_spec.get("status") == "failed":
|
|
9870
10693
|
workflow_spec["normalized_args"] = normalized_args
|
|
9871
10694
|
workflow_spec.setdefault("request_id", None)
|
|
9872
10695
|
workflow_spec["suggested_next_call"] = {"tool_name": "app_flow_apply", "arguments": {"profile": profile, **normalized_args}}
|
|
9873
|
-
|
|
10696
|
+
flow_compile_ms += _duration_ms(flow_compile_started_at)
|
|
10697
|
+
return finish(workflow_spec)
|
|
9874
10698
|
desired_node_count = len([node for node in normalized_nodes if node.get("type") != "end"])
|
|
9875
10699
|
build_id = f"facade-flow-{uuid4().hex[:12]}"
|
|
9876
10700
|
previous_build_home = os.getenv("QINGFLOW_MCP_BUILD_HOME")
|
|
@@ -9895,6 +10719,8 @@ class AiBuilderFacade:
|
|
|
9895
10719
|
"entities": [{"entity_id": entity["entity_id"], "workflow": workflow_spec["workflow"]}],
|
|
9896
10720
|
}
|
|
9897
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()
|
|
9898
10724
|
stage = self.solutions.solution_build_flow(
|
|
9899
10725
|
profile=profile,
|
|
9900
10726
|
mode="apply",
|
|
@@ -9904,6 +10730,7 @@ class AiBuilderFacade:
|
|
|
9904
10730
|
run_label=None,
|
|
9905
10731
|
repair_patch={},
|
|
9906
10732
|
)
|
|
10733
|
+
flow_write_ms += _duration_ms(flow_write_started_at)
|
|
9907
10734
|
finally:
|
|
9908
10735
|
if previous_build_home is None:
|
|
9909
10736
|
os.environ.pop("QINGFLOW_MCP_BUILD_HOME", None)
|
|
@@ -9925,8 +10752,10 @@ class AiBuilderFacade:
|
|
|
9925
10752
|
suggested_next_call["tool_name"] = "app_flow_apply"
|
|
9926
10753
|
suggested_next_call["arguments"] = arguments
|
|
9927
10754
|
failed["suggested_next_call"] = suggested_next_call
|
|
9928
|
-
return
|
|
10755
|
+
return finish(failed)
|
|
10756
|
+
flow_readback_started_at = time.perf_counter()
|
|
9929
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)
|
|
9930
10759
|
workflow_structure_verified = bool(verified_nodes) and _workflow_nodes_semantically_equal(
|
|
9931
10760
|
current_workflow=verified_nodes,
|
|
9932
10761
|
requested_nodes=normalized_nodes,
|
|
@@ -9988,7 +10817,7 @@ class AiBuilderFacade:
|
|
|
9988
10817
|
"write_succeeded": True,
|
|
9989
10818
|
"safe_to_retry": False,
|
|
9990
10819
|
}
|
|
9991
|
-
return
|
|
10820
|
+
return finish_with_publish(response)
|
|
9992
10821
|
|
|
9993
10822
|
def _extract_view_action_button_patch_intents(
|
|
9994
10823
|
self,
|
|
@@ -10414,12 +11243,65 @@ class AiBuilderFacade:
|
|
|
10414
11243
|
"remove_views": list(remove_views),
|
|
10415
11244
|
"publish": publish,
|
|
10416
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
|
+
|
|
10417
11299
|
has_action_button_intent = any(patch.action_buttons is not None for patch in upsert_views) or any(
|
|
10418
11300
|
any(key in (patch.set or {}) for key in ("action_buttons", "actionButtons"))
|
|
10419
11301
|
for patch in patch_views
|
|
10420
11302
|
)
|
|
10421
11303
|
if has_action_button_intent and not publish:
|
|
10422
|
-
return _failed(
|
|
11304
|
+
return finish(_failed(
|
|
10423
11305
|
"VIEW_ACTION_BUTTONS_REQUIRE_PUBLISH",
|
|
10424
11306
|
"app_views_apply action_buttons require publish=true because the underlying custom button writer may publish after successful button writes",
|
|
10425
11307
|
normalized_args=normalized_args,
|
|
@@ -10429,7 +11311,7 @@ class AiBuilderFacade:
|
|
|
10429
11311
|
"reason": "action_buttons are compiled through app_custom_buttons_apply",
|
|
10430
11312
|
},
|
|
10431
11313
|
suggested_next_call={"tool_name": "app_views_apply", "arguments": {"profile": profile, **{**normalized_args, "publish": True}}},
|
|
10432
|
-
)
|
|
11314
|
+
))
|
|
10433
11315
|
if not upsert_views and not patch_views and not remove_views:
|
|
10434
11316
|
response = {
|
|
10435
11317
|
"status": "success",
|
|
@@ -10457,10 +11339,10 @@ class AiBuilderFacade:
|
|
|
10457
11339
|
"write_succeeded": False,
|
|
10458
11340
|
"safe_to_retry": True,
|
|
10459
11341
|
}
|
|
10460
|
-
return
|
|
10461
|
-
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
11342
|
+
return finish_with_publish(response)
|
|
10462
11343
|
app_permission_summary: JSONObject | None = None
|
|
10463
11344
|
app_permission_error: QingflowApiError | None = None
|
|
11345
|
+
permission_started_at = time.perf_counter()
|
|
10464
11346
|
try:
|
|
10465
11347
|
app_permission_summary = self._read_app_permission_summary(profile=profile, app_key=app_key)
|
|
10466
11348
|
except (QingflowApiError, RuntimeError) as error:
|
|
@@ -10504,26 +11386,27 @@ class AiBuilderFacade:
|
|
|
10504
11386
|
normalized_args=normalized_args,
|
|
10505
11387
|
permission_summary=app_permission_summary,
|
|
10506
11388
|
)
|
|
11389
|
+
permission_ms += _duration_ms(permission_started_at)
|
|
10507
11390
|
if permission_outcome.block is not None:
|
|
10508
|
-
return permission_outcome.block
|
|
11391
|
+
return finish(permission_outcome.block)
|
|
10509
11392
|
permission_outcomes.append(permission_outcome)
|
|
10510
11393
|
|
|
10511
|
-
def finalize(response: JSONObject) -> JSONObject:
|
|
10512
|
-
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
10513
|
-
|
|
10514
11394
|
try:
|
|
11395
|
+
view_state_read_started_at = time.perf_counter()
|
|
10515
11396
|
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
10516
11397
|
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
10517
11398
|
existing_views, _views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=False)
|
|
10518
11399
|
except (QingflowApiError, RuntimeError) as error:
|
|
11400
|
+
view_state_read_ms += _duration_ms(view_state_read_started_at)
|
|
10519
11401
|
api_error = _coerce_api_error(error)
|
|
10520
|
-
return
|
|
11402
|
+
return finish(_failed_from_api_error(
|
|
10521
11403
|
"VIEWS_READ_FAILED",
|
|
10522
11404
|
api_error,
|
|
10523
11405
|
normalized_args=normalized_args,
|
|
10524
11406
|
details=_with_state_read_blocked_details({"app_key": app_key}, resource="views", error=api_error),
|
|
10525
11407
|
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
10526
11408
|
))
|
|
11409
|
+
view_state_read_ms += _duration_ms(view_state_read_started_at)
|
|
10527
11410
|
app_name = str(base.get("formTitle") or base.get("title") or base.get("appName") or "").strip() or None
|
|
10528
11411
|
existing_views = existing_views or []
|
|
10529
11412
|
existing_by_key: dict[str, dict[str, Any]] = {}
|
|
@@ -10547,7 +11430,7 @@ class AiBuilderFacade:
|
|
|
10547
11430
|
if action_button_patch_results:
|
|
10548
11431
|
normalized_args["action_button_patch_results"] = action_button_patch_results
|
|
10549
11432
|
if action_button_patch_issues:
|
|
10550
|
-
return
|
|
11433
|
+
return finish(
|
|
10551
11434
|
_failed(
|
|
10552
11435
|
"VIEW_ACTION_BUTTON_PATCH_FAILED",
|
|
10553
11436
|
"one or more patch_views action_buttons entries could not be resolved; no write was executed",
|
|
@@ -10572,6 +11455,7 @@ class AiBuilderFacade:
|
|
|
10572
11455
|
transport_error=_transport_error_payload(app_permission_error),
|
|
10573
11456
|
)
|
|
10574
11457
|
else:
|
|
11458
|
+
create_permission_started_at = time.perf_counter()
|
|
10575
11459
|
create_permission_outcome = self._guard_app_permission(
|
|
10576
11460
|
profile=profile,
|
|
10577
11461
|
app_key=app_key,
|
|
@@ -10579,13 +11463,14 @@ class AiBuilderFacade:
|
|
|
10579
11463
|
normalized_args=normalized_args,
|
|
10580
11464
|
permission_summary=app_permission_summary,
|
|
10581
11465
|
)
|
|
11466
|
+
permission_ms += _duration_ms(create_permission_started_at)
|
|
10582
11467
|
if create_permission_outcome.block is not None:
|
|
10583
11468
|
details = create_permission_outcome.block.get("details")
|
|
10584
11469
|
if isinstance(details, dict):
|
|
10585
11470
|
details["operation"] = "view_create"
|
|
10586
11471
|
details["view_names"] = creating_view_names
|
|
10587
11472
|
details["also_required_permission"] = "view_manage"
|
|
10588
|
-
return create_permission_outcome.block
|
|
11473
|
+
return finish(create_permission_outcome.block)
|
|
10589
11474
|
permission_outcomes.append(create_permission_outcome)
|
|
10590
11475
|
parsed_schema = _parse_schema(schema)
|
|
10591
11476
|
field_names = {field["name"] for field in parsed_schema["fields"]}
|
|
@@ -10624,6 +11509,7 @@ class AiBuilderFacade:
|
|
|
10624
11509
|
custom_button_details_by_id: dict[int, dict[str, Any]] = {}
|
|
10625
11510
|
if requires_custom_button_validation:
|
|
10626
11511
|
try:
|
|
11512
|
+
custom_button_inventory_started_at = time.perf_counter()
|
|
10627
11513
|
button_listing = self.buttons.custom_button_list(
|
|
10628
11514
|
profile=profile,
|
|
10629
11515
|
app_key=app_key,
|
|
@@ -10631,8 +11517,9 @@ class AiBuilderFacade:
|
|
|
10631
11517
|
include_raw=False,
|
|
10632
11518
|
)
|
|
10633
11519
|
except (QingflowApiError, RuntimeError) as error:
|
|
11520
|
+
custom_button_inventory_ms += _duration_ms(custom_button_inventory_started_at)
|
|
10634
11521
|
api_error = _coerce_api_error(error)
|
|
10635
|
-
return
|
|
11522
|
+
return finish(
|
|
10636
11523
|
_failed_from_api_error(
|
|
10637
11524
|
"CUSTOM_BUTTON_LIST_FAILED",
|
|
10638
11525
|
api_error,
|
|
@@ -10641,6 +11528,7 @@ class AiBuilderFacade:
|
|
|
10641
11528
|
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
10642
11529
|
)
|
|
10643
11530
|
)
|
|
11531
|
+
custom_button_inventory_ms += _duration_ms(custom_button_inventory_started_at)
|
|
10644
11532
|
valid_custom_button_ids = {
|
|
10645
11533
|
button_id
|
|
10646
11534
|
for item in (button_listing.get("items") or [])
|
|
@@ -10654,6 +11542,7 @@ class AiBuilderFacade:
|
|
|
10654
11542
|
}
|
|
10655
11543
|
for button_id in sorted(referenced_custom_button_ids):
|
|
10656
11544
|
try:
|
|
11545
|
+
custom_button_detail_started_at = time.perf_counter()
|
|
10657
11546
|
detail = self.buttons.custom_button_get(
|
|
10658
11547
|
profile=profile,
|
|
10659
11548
|
app_key=app_key,
|
|
@@ -10661,7 +11550,9 @@ class AiBuilderFacade:
|
|
|
10661
11550
|
being_draft=True,
|
|
10662
11551
|
include_raw=False,
|
|
10663
11552
|
)
|
|
11553
|
+
custom_button_inventory_ms += _duration_ms(custom_button_detail_started_at)
|
|
10664
11554
|
except (QingflowApiError, RuntimeError) as error:
|
|
11555
|
+
custom_button_inventory_ms += _duration_ms(custom_button_detail_started_at)
|
|
10665
11556
|
api_error = _coerce_api_error(error)
|
|
10666
11557
|
if _is_optional_builder_lookup_error(api_error):
|
|
10667
11558
|
continue
|
|
@@ -10680,7 +11571,7 @@ class AiBuilderFacade:
|
|
|
10680
11571
|
},
|
|
10681
11572
|
)
|
|
10682
11573
|
failed.update({"write_executed": False, "write_succeeded": False, "safe_to_retry": True})
|
|
10683
|
-
return
|
|
11574
|
+
return finish(failed)
|
|
10684
11575
|
detail_result = detail.get("result")
|
|
10685
11576
|
if isinstance(detail_result, dict):
|
|
10686
11577
|
custom_button_details_by_id[button_id] = _normalize_custom_button_detail(detail_result)
|
|
@@ -10688,10 +11579,12 @@ class AiBuilderFacade:
|
|
|
10688
11579
|
associated_resources: list[dict[str, Any]] = []
|
|
10689
11580
|
if requires_associated_resource_validation:
|
|
10690
11581
|
try:
|
|
11582
|
+
associated_resource_inventory_started_at = time.perf_counter()
|
|
10691
11583
|
associated_resources = self._load_associated_resources_for_builder(profile=profile, app_key=app_key)
|
|
10692
11584
|
except (QingflowApiError, RuntimeError) as error:
|
|
11585
|
+
associated_resource_inventory_ms += _duration_ms(associated_resource_inventory_started_at)
|
|
10693
11586
|
api_error = _coerce_api_error(error)
|
|
10694
|
-
return
|
|
11587
|
+
return finish(
|
|
10695
11588
|
_failed_from_api_error(
|
|
10696
11589
|
"ASSOCIATED_RESOURCES_READ_FAILED",
|
|
10697
11590
|
api_error,
|
|
@@ -10700,6 +11593,7 @@ class AiBuilderFacade:
|
|
|
10700
11593
|
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
10701
11594
|
)
|
|
10702
11595
|
)
|
|
11596
|
+
associated_resource_inventory_ms += _duration_ms(associated_resource_inventory_started_at)
|
|
10703
11597
|
removed: list[str] = []
|
|
10704
11598
|
removed_keys: set[str] = set()
|
|
10705
11599
|
view_results: list[dict[str, Any]] = []
|
|
@@ -10720,6 +11614,7 @@ class AiBuilderFacade:
|
|
|
10720
11614
|
}
|
|
10721
11615
|
)
|
|
10722
11616
|
|
|
11617
|
+
view_mutation_started_at = time.perf_counter()
|
|
10723
11618
|
for selector in remove_views:
|
|
10724
11619
|
selector_text = str(selector or "").strip()
|
|
10725
11620
|
if not selector_text:
|
|
@@ -11356,6 +12251,7 @@ class AiBuilderFacade:
|
|
|
11356
12251
|
failed_views.append(failure_entry)
|
|
11357
12252
|
view_results.append(failure_entry)
|
|
11358
12253
|
continue
|
|
12254
|
+
view_mutation_ms += _duration_ms(view_mutation_started_at)
|
|
11359
12255
|
successful_action_view_keys = {
|
|
11360
12256
|
str(item.get("view_key") or "").strip()
|
|
11361
12257
|
for item in view_results
|
|
@@ -11377,11 +12273,13 @@ class AiBuilderFacade:
|
|
|
11377
12273
|
runnable_action_button_intents = [
|
|
11378
12274
|
intent for intent in action_button_intents if id(intent) not in skipped_action_button_intent_ids
|
|
11379
12275
|
]
|
|
12276
|
+
action_buttons_started_at = time.perf_counter()
|
|
11380
12277
|
action_buttons_result = self._apply_view_action_buttons(
|
|
11381
12278
|
profile=profile,
|
|
11382
12279
|
app_key=app_key,
|
|
11383
12280
|
intents=runnable_action_button_intents,
|
|
11384
12281
|
)
|
|
12282
|
+
action_buttons_ms += _duration_ms(action_buttons_started_at)
|
|
11385
12283
|
if skipped_action_button_intents:
|
|
11386
12284
|
action_buttons_result.setdefault("skipped_due_to_view_write_failure", [])
|
|
11387
12285
|
if isinstance(action_buttons_result["skipped_due_to_view_write_failure"], list):
|
|
@@ -11397,6 +12295,11 @@ class AiBuilderFacade:
|
|
|
11397
12295
|
)
|
|
11398
12296
|
action_button_write_executed = bool(action_buttons_result.get("write_executed"))
|
|
11399
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
|
+
)
|
|
11400
12303
|
action_buttons_verification = action_buttons_result.get("verification") if isinstance(action_buttons_result.get("verification"), dict) else {}
|
|
11401
12304
|
action_buttons_verified = (
|
|
11402
12305
|
bool(action_buttons_result.get("verified", True))
|
|
@@ -11411,16 +12314,19 @@ class AiBuilderFacade:
|
|
|
11411
12314
|
verified_views_unavailable = False
|
|
11412
12315
|
if needs_view_list_readback:
|
|
11413
12316
|
try:
|
|
12317
|
+
view_readback_started_at = time.perf_counter()
|
|
11414
12318
|
verified_view_result, verified_views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
11415
12319
|
except (QingflowApiError, RuntimeError) as error:
|
|
12320
|
+
view_readback_ms += _duration_ms(view_readback_started_at)
|
|
11416
12321
|
api_error = _coerce_api_error(error)
|
|
11417
|
-
return
|
|
12322
|
+
return finish(_failed_from_api_error(
|
|
11418
12323
|
"VIEWS_READ_FAILED",
|
|
11419
12324
|
api_error,
|
|
11420
12325
|
normalized_args=normalized_args,
|
|
11421
12326
|
details=_with_state_read_blocked_details({"app_key": app_key}, resource="views", error=api_error),
|
|
11422
12327
|
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
11423
12328
|
))
|
|
12329
|
+
view_readback_ms += _duration_ms(view_readback_started_at)
|
|
11424
12330
|
verified_names = {
|
|
11425
12331
|
_extract_view_name(item)
|
|
11426
12332
|
for item in (verified_view_result or [])
|
|
@@ -11450,6 +12356,22 @@ class AiBuilderFacade:
|
|
|
11450
12356
|
button_mismatches: list[dict[str, Any]] = []
|
|
11451
12357
|
custom_button_readback_pending = False
|
|
11452
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()
|
|
11453
12375
|
for item in view_results:
|
|
11454
12376
|
status = str(item.get("status") or "")
|
|
11455
12377
|
name = str(item.get("name") or "")
|
|
@@ -11496,8 +12418,9 @@ class AiBuilderFacade:
|
|
|
11496
12418
|
verification_by_view.append(verification_entry)
|
|
11497
12419
|
continue
|
|
11498
12420
|
try:
|
|
11499
|
-
|
|
11500
|
-
|
|
12421
|
+
config_result, config_error = read_view_config_for_verification(verification_key)
|
|
12422
|
+
if config_error is not None:
|
|
12423
|
+
raise config_error
|
|
11501
12424
|
actual_filters = _normalize_view_filter_groups_for_compare(config_result.get("viewgraphLimit"))
|
|
11502
12425
|
expected_filter_summary = _normalize_view_filter_groups_for_compare(expected_filters)
|
|
11503
12426
|
expected_data_scope = "CUSTOM" if expected_filter_summary else "ALL"
|
|
@@ -11573,8 +12496,9 @@ class AiBuilderFacade:
|
|
|
11573
12496
|
verification_by_view.append(verification_entry)
|
|
11574
12497
|
continue
|
|
11575
12498
|
try:
|
|
11576
|
-
|
|
11577
|
-
|
|
12499
|
+
config_result, config_error = read_view_config_for_verification(verification_key)
|
|
12500
|
+
if config_error is not None:
|
|
12501
|
+
raise config_error
|
|
11578
12502
|
actual_query_conditions = _normalize_view_query_conditions_for_compare(config_result)
|
|
11579
12503
|
query_conditions_verified = actual_query_conditions == expected_query_conditions
|
|
11580
12504
|
verification_entry["query_conditions_verified"] = query_conditions_verified
|
|
@@ -11621,8 +12545,9 @@ class AiBuilderFacade:
|
|
|
11621
12545
|
verification_by_view.append(verification_entry)
|
|
11622
12546
|
continue
|
|
11623
12547
|
try:
|
|
11624
|
-
|
|
11625
|
-
|
|
12548
|
+
config_result, config_error = read_view_config_for_verification(verification_key)
|
|
12549
|
+
if config_error is not None:
|
|
12550
|
+
raise config_error
|
|
11626
12551
|
actual_associated_resources = _extract_view_associated_resources_config(
|
|
11627
12552
|
config_result,
|
|
11628
12553
|
available_resources=associated_resources,
|
|
@@ -11675,8 +12600,9 @@ class AiBuilderFacade:
|
|
|
11675
12600
|
verification_by_view.append(verification_entry)
|
|
11676
12601
|
continue
|
|
11677
12602
|
try:
|
|
11678
|
-
|
|
11679
|
-
|
|
12603
|
+
config_result, config_error = read_view_config_for_verification(verification_key)
|
|
12604
|
+
if config_error is not None:
|
|
12605
|
+
raise config_error
|
|
11680
12606
|
actual_buttons = _normalize_view_buttons_for_compare(config_result)
|
|
11681
12607
|
button_comparison = _compare_view_button_summaries(
|
|
11682
12608
|
expected=expected_buttons,
|
|
@@ -11760,6 +12686,7 @@ class AiBuilderFacade:
|
|
|
11760
12686
|
"error_code": item.get("error_code"),
|
|
11761
12687
|
}
|
|
11762
12688
|
)
|
|
12689
|
+
view_verification_ms += _duration_ms(view_verification_started_at)
|
|
11763
12690
|
removed_delete_results = [
|
|
11764
12691
|
item
|
|
11765
12692
|
for item in view_results
|
|
@@ -11859,7 +12786,7 @@ class AiBuilderFacade:
|
|
|
11859
12786
|
"write_succeeded": bool(created or updated or removed or action_button_write_succeeded),
|
|
11860
12787
|
"safe_to_retry": not bool(created or updated or removed or action_button_write_executed),
|
|
11861
12788
|
}
|
|
11862
|
-
return
|
|
12789
|
+
return finish_with_publish(response)
|
|
11863
12790
|
warnings: list[dict[str, Any]] = []
|
|
11864
12791
|
if filter_readback_pending or filter_mismatches:
|
|
11865
12792
|
warnings.append(_warning("VIEW_FILTERS_UNVERIFIED", "view definitions were applied, but saved filter behavior is not fully verified"))
|
|
@@ -11959,7 +12886,7 @@ class AiBuilderFacade:
|
|
|
11959
12886
|
"write_succeeded": bool(created or updated or removed or action_button_write_succeeded),
|
|
11960
12887
|
"safe_to_retry": not bool(created or updated or removed or action_button_write_executed),
|
|
11961
12888
|
}
|
|
11962
|
-
return
|
|
12889
|
+
return finish_with_publish(response)
|
|
11963
12890
|
|
|
11964
12891
|
def app_publish_verify(
|
|
11965
12892
|
self,
|
|
@@ -11968,87 +12895,132 @@ class AiBuilderFacade:
|
|
|
11968
12895
|
app_key: str,
|
|
11969
12896
|
expected_package_tag_id: int | None = None,
|
|
11970
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
|
|
11971
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()
|
|
11972
12921
|
try:
|
|
11973
12922
|
base_before = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
11974
12923
|
except (QingflowApiError, RuntimeError) as error:
|
|
12924
|
+
initial_base_read_ms += _duration_ms(initial_base_read_started_at)
|
|
11975
12925
|
api_error = _coerce_api_error(error)
|
|
11976
|
-
return
|
|
11977
|
-
|
|
11978
|
-
|
|
11979
|
-
|
|
11980
|
-
|
|
11981
|
-
|
|
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
|
+
)
|
|
11982
12934
|
)
|
|
12935
|
+
initial_base_read_ms += _duration_ms(initial_base_read_started_at)
|
|
11983
12936
|
tag_ids_before = _coerce_int_list(base_before.get("tagIds"))
|
|
11984
12937
|
app_name_before = str(base_before.get("formTitle") or base_before.get("title") or base_before.get("appName") or "").strip() or None
|
|
11985
12938
|
already_published = bool(base_before.get("appPublishStatus") in {1, 2})
|
|
11986
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()
|
|
11987
12941
|
try:
|
|
11988
12942
|
views_before, views_before_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
11989
12943
|
except (QingflowApiError, RuntimeError) as error:
|
|
12944
|
+
initial_views_read_ms += _duration_ms(initial_views_read_started_at)
|
|
11990
12945
|
api_error = _coerce_api_error(error)
|
|
11991
|
-
return
|
|
11992
|
-
|
|
11993
|
-
|
|
11994
|
-
|
|
11995
|
-
|
|
11996
|
-
|
|
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
|
+
)
|
|
11997
12954
|
)
|
|
12955
|
+
initial_views_read_ms += _duration_ms(initial_views_read_started_at)
|
|
11998
12956
|
views_before = views_before or []
|
|
11999
12957
|
if already_published and package_already_attached is not False and isinstance(views_before, list) and not views_before_unavailable:
|
|
12000
|
-
return
|
|
12001
|
-
|
|
12002
|
-
|
|
12003
|
-
|
|
12004
|
-
|
|
12005
|
-
|
|
12006
|
-
|
|
12007
|
-
|
|
12008
|
-
|
|
12009
|
-
|
|
12010
|
-
|
|
12011
|
-
|
|
12012
|
-
|
|
12013
|
-
|
|
12014
|
-
|
|
12015
|
-
|
|
12016
|
-
|
|
12017
|
-
|
|
12018
|
-
|
|
12019
|
-
|
|
12020
|
-
|
|
12021
|
-
|
|
12022
|
-
|
|
12023
|
-
|
|
12024
|
-
|
|
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()
|
|
12025
12986
|
try:
|
|
12026
12987
|
version_result = self.apps.app_get_edit_version_no(profile=profile, app_key=app_key).get("result") or {}
|
|
12027
12988
|
except (QingflowApiError, RuntimeError) as error:
|
|
12989
|
+
edit_version_ms += _duration_ms(edit_version_started_at)
|
|
12028
12990
|
api_error = _coerce_api_error(error)
|
|
12029
|
-
return
|
|
12030
|
-
|
|
12031
|
-
|
|
12032
|
-
|
|
12033
|
-
|
|
12034
|
-
|
|
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
|
+
)
|
|
12035
12999
|
)
|
|
13000
|
+
edit_version_ms += _duration_ms(edit_version_started_at)
|
|
12036
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()
|
|
12037
13003
|
try:
|
|
12038
13004
|
self.apps.app_publish(profile=profile, app_key=app_key, payload={"editVersionNo": edit_version_no})
|
|
12039
13005
|
self.apps.app_edit_finished(profile=profile, app_key=app_key, payload={"editVersionNo": edit_version_no})
|
|
12040
13006
|
except (QingflowApiError, RuntimeError) as error:
|
|
13007
|
+
publish_ms += _duration_ms(publish_started_at)
|
|
12041
13008
|
api_error = _coerce_api_error(error)
|
|
12042
|
-
return
|
|
12043
|
-
|
|
12044
|
-
|
|
12045
|
-
|
|
12046
|
-
|
|
12047
|
-
|
|
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
|
+
)
|
|
12048
13017
|
)
|
|
13018
|
+
publish_ms += _duration_ms(publish_started_at)
|
|
13019
|
+
base_readback_started_at = time.perf_counter()
|
|
12049
13020
|
try:
|
|
12050
13021
|
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
12051
13022
|
except (QingflowApiError, RuntimeError) as error:
|
|
13023
|
+
base_readback_ms += _duration_ms(base_readback_started_at)
|
|
12052
13024
|
api_error = _coerce_api_error(error)
|
|
12053
13025
|
result = _post_write_readback_pending_result(
|
|
12054
13026
|
error_code="PUBLISH_READBACK_PENDING",
|
|
@@ -12061,13 +13033,16 @@ class AiBuilderFacade:
|
|
|
12061
13033
|
http_status=None if api_error.http_status == 404 else api_error.http_status,
|
|
12062
13034
|
)
|
|
12063
13035
|
result.update({"app_key": app_key, "published": None, "verified": False})
|
|
12064
|
-
return result
|
|
13036
|
+
return finish(result)
|
|
13037
|
+
base_readback_ms += _duration_ms(base_readback_started_at)
|
|
12065
13038
|
tag_ids_after = _coerce_int_list(base.get("tagIds"))
|
|
12066
13039
|
app_name_after = str(base.get("formTitle") or base.get("title") or base.get("appName") or app_name_before or "").strip() or None
|
|
12067
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()
|
|
12068
13042
|
try:
|
|
12069
13043
|
views, views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
12070
13044
|
except (QingflowApiError, RuntimeError) as error:
|
|
13045
|
+
views_readback_ms += _duration_ms(views_readback_started_at)
|
|
12071
13046
|
api_error = _coerce_api_error(error)
|
|
12072
13047
|
result = _post_write_readback_pending_result(
|
|
12073
13048
|
error_code="VIEWS_READBACK_PENDING",
|
|
@@ -12080,7 +13055,8 @@ class AiBuilderFacade:
|
|
|
12080
13055
|
http_status=None if api_error.http_status == 404 else api_error.http_status,
|
|
12081
13056
|
)
|
|
12082
13057
|
result.update({"app_key": app_key, "app_name": app_name_after, "published": bool(base.get("appPublishStatus") in {1, 2}), "verified": False})
|
|
12083
|
-
return result
|
|
13058
|
+
return finish(result)
|
|
13059
|
+
views_readback_ms += _duration_ms(views_readback_started_at)
|
|
12084
13060
|
views = views or []
|
|
12085
13061
|
views_ok = isinstance(views, list) and not views_unavailable
|
|
12086
13062
|
verified = bool(base.get("appPublishStatus") in {1, 2}) and (package_attached is not False) and views_ok
|
|
@@ -12089,36 +13065,38 @@ class AiBuilderFacade:
|
|
|
12089
13065
|
views_unavailable=views_unavailable,
|
|
12090
13066
|
verified=verified,
|
|
12091
13067
|
)
|
|
12092
|
-
return
|
|
12093
|
-
|
|
12094
|
-
|
|
12095
|
-
|
|
12096
|
-
|
|
12097
|
-
|
|
12098
|
-
|
|
12099
|
-
|
|
12100
|
-
|
|
12101
|
-
|
|
12102
|
-
|
|
12103
|
-
|
|
12104
|
-
|
|
12105
|
-
|
|
12106
|
-
|
|
12107
|
-
|
|
12108
|
-
|
|
12109
|
-
|
|
12110
|
-
|
|
12111
|
-
|
|
12112
|
-
|
|
12113
|
-
|
|
12114
|
-
|
|
12115
|
-
|
|
12116
|
-
|
|
12117
|
-
|
|
12118
|
-
|
|
12119
|
-
|
|
12120
|
-
|
|
12121
|
-
|
|
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
|
+
)
|
|
12122
13100
|
|
|
12123
13101
|
def _expand_chart_partial_patches(
|
|
12124
13102
|
self,
|
|
@@ -12402,6 +13380,12 @@ class AiBuilderFacade:
|
|
|
12402
13380
|
}
|
|
12403
13381
|
|
|
12404
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
|
|
12405
13389
|
normalized_args = request.model_dump(mode="json")
|
|
12406
13390
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
12407
13391
|
app_result = self.app_resolve(profile=profile, app_key=request.app_key)
|
|
@@ -12423,21 +13407,51 @@ class AiBuilderFacade:
|
|
|
12423
13407
|
permission_outcomes.append(permission_outcome)
|
|
12424
13408
|
|
|
12425
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
|
+
)
|
|
12426
13419
|
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
12427
13420
|
|
|
12428
13421
|
fields: list[dict[str, Any]] = []
|
|
12429
13422
|
qingbi_fields: list[Any] = []
|
|
12430
13423
|
existing_chart_items: list[Any] = []
|
|
12431
13424
|
existing_chart_list_source: str | None = None
|
|
12432
|
-
|
|
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)
|
|
12433
13440
|
if needs_chart_inventory:
|
|
13441
|
+
chart_inventory_started_at = time.perf_counter()
|
|
12434
13442
|
try:
|
|
12435
|
-
|
|
12436
|
-
|
|
12437
|
-
|
|
12438
|
-
|
|
12439
|
-
|
|
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"
|
|
12440
13453
|
except (QingflowApiError, RuntimeError) as error:
|
|
13454
|
+
chart_inventory_ms = _duration_ms(chart_inventory_started_at)
|
|
12441
13455
|
api_error = _coerce_api_error(error)
|
|
12442
13456
|
return finalize(_failed_from_api_error(
|
|
12443
13457
|
"CHART_APPLY_FAILED",
|
|
@@ -12446,6 +13460,7 @@ class AiBuilderFacade:
|
|
|
12446
13460
|
details=_with_state_read_blocked_details({"app_key": app_key}, resource="chart", error=api_error),
|
|
12447
13461
|
suggested_next_call={"tool_name": "app_charts_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
12448
13462
|
))
|
|
13463
|
+
chart_inventory_ms = _duration_ms(chart_inventory_started_at)
|
|
12449
13464
|
|
|
12450
13465
|
field_lookup = _build_public_field_lookup(fields)
|
|
12451
13466
|
qingbi_fields_by_id = {
|
|
@@ -12472,7 +13487,6 @@ class AiBuilderFacade:
|
|
|
12472
13487
|
continue
|
|
12473
13488
|
existing_by_name.setdefault(item_name, []).append(deepcopy(item))
|
|
12474
13489
|
|
|
12475
|
-
upsert_charts = list(request.upsert_charts)
|
|
12476
13490
|
if request.patch_charts:
|
|
12477
13491
|
expanded_charts, patch_issues, patch_results = self._expand_chart_partial_patches(
|
|
12478
13492
|
profile=profile,
|
|
@@ -12495,67 +13509,37 @@ class AiBuilderFacade:
|
|
|
12495
13509
|
normalized_args["upsert_charts"] = [patch.model_dump(mode="json") for patch in upsert_charts]
|
|
12496
13510
|
normalized_args["patch_results"] = patch_results
|
|
12497
13511
|
|
|
12498
|
-
|
|
12499
|
-
|
|
12500
|
-
|
|
12501
|
-
|
|
12502
|
-
|
|
12503
|
-
|
|
12504
|
-
|
|
12505
|
-
|
|
12506
|
-
|
|
12507
|
-
|
|
12508
|
-
|
|
12509
|
-
|
|
12510
|
-
f"upsert_charts contains {len(upsert_charts)} items; split into batches of "
|
|
12511
|
-
f"{CHART_APPLY_RECOMMENDED_UPSERT_BATCH_SIZE} or fewer before writing"
|
|
12512
|
-
),
|
|
12513
|
-
"normalized_args": normalized_args,
|
|
12514
|
-
"missing_fields": [],
|
|
12515
|
-
"allowed_values": {"chart.chart_type": [member.value for member in PublicChartType], "chart.filter.operator": [member.value for member in ViewFilterOperator]},
|
|
12516
|
-
"details": batching_context,
|
|
12517
|
-
"request_id": None,
|
|
12518
|
-
"suggested_next_call": {"tool_name": "app_charts_apply", "arguments": {"profile": profile, **first_batch}},
|
|
12519
|
-
"backend_code": None,
|
|
12520
|
-
"http_status": None,
|
|
12521
|
-
"noop": False,
|
|
12522
|
-
"warnings": [
|
|
12523
|
-
_warning(
|
|
12524
|
-
"CHART_UPSERT_BATCH_SIZE_BLOCKED",
|
|
12525
|
-
"large chart upsert batches are blocked before write to avoid timeout and unknown write state",
|
|
12526
|
-
upsert_count=len(upsert_charts),
|
|
12527
|
-
max_upsert_count=CHART_APPLY_RECOMMENDED_UPSERT_BATCH_SIZE,
|
|
12528
|
-
)
|
|
12529
|
-
],
|
|
12530
|
-
"verification": {
|
|
12531
|
-
"charts_verified": False,
|
|
12532
|
-
"readback_unavailable": False,
|
|
12533
|
-
"readback_before_retry": False,
|
|
12534
|
-
"chart_delete_readback_results": [],
|
|
12535
|
-
"chart_order_verified": True,
|
|
12536
|
-
"chart_list_source": existing_chart_list_source,
|
|
12537
|
-
},
|
|
12538
|
-
"app_key": app_key,
|
|
12539
|
-
"app_name": app_name,
|
|
12540
|
-
"chart_results": [],
|
|
12541
|
-
"verified": False,
|
|
12542
|
-
"write_executed": False,
|
|
12543
|
-
"write_succeeded": False,
|
|
12544
|
-
"safe_to_retry": True,
|
|
12545
|
-
"next_action": "split_upsert_charts_and_retry",
|
|
12546
|
-
})
|
|
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
|
+
)
|
|
12547
13524
|
|
|
12548
13525
|
chart_results: list[dict[str, Any]] = []
|
|
12549
13526
|
created_ids: list[str] = []
|
|
12550
13527
|
updated_ids: list[str] = []
|
|
12551
13528
|
removed_ids: list[str] = []
|
|
12552
13529
|
failed_items: list[dict[str, Any]] = []
|
|
13530
|
+
skipped_after_timeout_count = 0
|
|
13531
|
+
stopped_after_timeout = False
|
|
12553
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
|
|
12554
13536
|
|
|
12555
|
-
|
|
13537
|
+
chart_upsert_started_at = time.perf_counter()
|
|
13538
|
+
for patch_index, patch in enumerate(upsert_charts):
|
|
12556
13539
|
chart_id = ""
|
|
12557
13540
|
target_type = ""
|
|
12558
13541
|
config_payload: dict[str, Any] | None = None
|
|
13542
|
+
patch_write_attempted = False
|
|
12559
13543
|
try:
|
|
12560
13544
|
dataset_source = _chart_patch_dataset_source_type(patch)
|
|
12561
13545
|
if dataset_source:
|
|
@@ -12575,7 +13559,7 @@ class AiBuilderFacade:
|
|
|
12575
13559
|
existing = existing_by_id.get(str(patch.chart_id))
|
|
12576
13560
|
if existing is None:
|
|
12577
13561
|
raise ValueError(f"chart_id '{patch.chart_id}' was not found under app '{app_key}'")
|
|
12578
|
-
if existing is None:
|
|
13562
|
+
if existing is None and needs_existing_chart_inventory:
|
|
12579
13563
|
name_matches = list(existing_by_name.get(patch.name) or [])
|
|
12580
13564
|
if len(name_matches) > 1:
|
|
12581
13565
|
raise ValueError(
|
|
@@ -12604,6 +13588,7 @@ class AiBuilderFacade:
|
|
|
12604
13588
|
)
|
|
12605
13589
|
if existing is None:
|
|
12606
13590
|
temp_chart_id = str(patch.chart_id or f"mcp_{uuid4().hex[:16]}")
|
|
13591
|
+
chart_id = temp_chart_id
|
|
12607
13592
|
create_payload = {
|
|
12608
13593
|
"chartId": temp_chart_id,
|
|
12609
13594
|
"chartName": patch.name,
|
|
@@ -12617,9 +13602,13 @@ class AiBuilderFacade:
|
|
|
12617
13602
|
"editAuthType": "ws",
|
|
12618
13603
|
"editAuthIncludeSubDept": True,
|
|
12619
13604
|
}
|
|
13605
|
+
write_attempted = True
|
|
13606
|
+
patch_write_attempted = True
|
|
12620
13607
|
create_result = self.charts.qingbi_report_create(profile=profile, payload=create_payload).get("result") or {}
|
|
12621
13608
|
created_chart_id = _extract_chart_identifier(create_result or {})
|
|
13609
|
+
created_id_from_response = bool(created_chart_id)
|
|
12622
13610
|
if not created_chart_id:
|
|
13611
|
+
create_id_readback_fallback_used = True
|
|
12623
13612
|
try:
|
|
12624
13613
|
refreshed_items, _ = self._load_chart_list_for_builder(profile=profile, app_key=app_key)
|
|
12625
13614
|
refreshed_matches = _find_charts_by_name(
|
|
@@ -12641,6 +13630,8 @@ class AiBuilderFacade:
|
|
|
12641
13630
|
)
|
|
12642
13631
|
chart_id = created_chart_id
|
|
12643
13632
|
created_ids.append(chart_id)
|
|
13633
|
+
if created_id_from_response:
|
|
13634
|
+
created_ids_from_create_response.append(chart_id)
|
|
12644
13635
|
created_chart = {
|
|
12645
13636
|
"chartId": chart_id,
|
|
12646
13637
|
"chartName": patch.name,
|
|
@@ -12650,6 +13641,8 @@ class AiBuilderFacade:
|
|
|
12650
13641
|
existing_by_name.setdefault(patch.name, []).append(deepcopy(created_chart))
|
|
12651
13642
|
elif existing_name != patch.name or existing_type != target_type or isinstance(chart_visible_auth, dict):
|
|
12652
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
|
|
12653
13646
|
self.charts.qingbi_report_update_base(
|
|
12654
13647
|
profile=profile,
|
|
12655
13648
|
chart_id=chart_id,
|
|
@@ -12688,11 +13681,15 @@ class AiBuilderFacade:
|
|
|
12688
13681
|
|
|
12689
13682
|
config_updated = False
|
|
12690
13683
|
if existing is None or config_update_requested:
|
|
13684
|
+
write_attempted = True
|
|
13685
|
+
patch_write_attempted = True
|
|
12691
13686
|
self.charts.qingbi_report_update_config(profile=profile, chart_id=chart_id, payload=config_payload or {})
|
|
12692
13687
|
config_updated = True
|
|
12693
13688
|
if existing is not None and chart_id not in updated_ids and config_updated:
|
|
12694
13689
|
updated_ids.append(chart_id)
|
|
12695
13690
|
if patch.question_config:
|
|
13691
|
+
write_attempted = True
|
|
13692
|
+
patch_write_attempted = True
|
|
12696
13693
|
self._request_backend(
|
|
12697
13694
|
profile=profile,
|
|
12698
13695
|
method="POST",
|
|
@@ -12700,6 +13697,8 @@ class AiBuilderFacade:
|
|
|
12700
13697
|
json_body=patch.question_config,
|
|
12701
13698
|
)
|
|
12702
13699
|
if patch.user_config:
|
|
13700
|
+
write_attempted = True
|
|
13701
|
+
patch_write_attempted = True
|
|
12703
13702
|
self._request_backend(
|
|
12704
13703
|
profile=profile,
|
|
12705
13704
|
method="POST",
|
|
@@ -12739,9 +13738,30 @@ class AiBuilderFacade:
|
|
|
12739
13738
|
}
|
|
12740
13739
|
failed_items.append(failure)
|
|
12741
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)
|
|
12742
13760
|
|
|
13761
|
+
chart_delete_started_at = time.perf_counter()
|
|
12743
13762
|
for chart_id in request.remove_chart_ids:
|
|
12744
13763
|
try:
|
|
13764
|
+
write_attempted = True
|
|
12745
13765
|
self.charts.qingbi_report_delete(profile=profile, chart_id=chart_id)
|
|
12746
13766
|
removed_ids.append(chart_id)
|
|
12747
13767
|
delete_result = self._verify_chart_deleted_by_id(profile=profile, chart_id=chart_id)
|
|
@@ -12763,8 +13783,10 @@ class AiBuilderFacade:
|
|
|
12763
13783
|
}
|
|
12764
13784
|
failed_items.append(failure)
|
|
12765
13785
|
chart_results.append(failure)
|
|
13786
|
+
chart_delete_ms = _duration_ms(chart_delete_started_at)
|
|
12766
13787
|
|
|
12767
13788
|
reordered = False
|
|
13789
|
+
chart_reorder_started_at = time.perf_counter()
|
|
12768
13790
|
if request.reorder_chart_ids:
|
|
12769
13791
|
try:
|
|
12770
13792
|
current_order = [
|
|
@@ -12774,6 +13796,7 @@ class AiBuilderFacade:
|
|
|
12774
13796
|
]
|
|
12775
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]
|
|
12776
13798
|
backend_reorder_ids = list(reversed(desired_display_order))
|
|
13799
|
+
write_attempted = True
|
|
12777
13800
|
self.charts.qingbi_report_reorder(profile=profile, app_key=app_key, chart_ids=backend_reorder_ids)
|
|
12778
13801
|
reordered = True
|
|
12779
13802
|
except (QingflowApiError, RuntimeError) as error:
|
|
@@ -12788,17 +13811,34 @@ class AiBuilderFacade:
|
|
|
12788
13811
|
}
|
|
12789
13812
|
failed_items.append(failure)
|
|
12790
13813
|
chart_results.append(failure)
|
|
13814
|
+
chart_reorder_ms = _duration_ms(chart_reorder_started_at)
|
|
12791
13815
|
|
|
12792
13816
|
noop = not created_ids and not updated_ids and not removed_ids and not reordered and not failed_items
|
|
12793
|
-
|
|
12794
|
-
|
|
12795
|
-
|
|
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
|
|
12796
13831
|
delete_readback_unavailable = any(item.get("readback_status") == "unavailable" for item in delete_readback_issues)
|
|
12797
13832
|
deletes_verified = not delete_readback_issues
|
|
12798
13833
|
readback_unavailable = False
|
|
12799
13834
|
readback_error: QingflowApiError | None = None
|
|
12800
13835
|
readback_list_source: str | None = existing_chart_list_source
|
|
12801
|
-
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()
|
|
12802
13842
|
try:
|
|
12803
13843
|
readback_items, readback_list_source = self._load_chart_list_for_builder(profile=profile, app_key=app_key)
|
|
12804
13844
|
readback_ids = {
|
|
@@ -12821,8 +13861,10 @@ class AiBuilderFacade:
|
|
|
12821
13861
|
verified = False
|
|
12822
13862
|
readback_unavailable = True
|
|
12823
13863
|
readback_list_source = None
|
|
13864
|
+
chart_readback_ms = _duration_ms(chart_readback_started_at)
|
|
12824
13865
|
else:
|
|
12825
13866
|
verified = True
|
|
13867
|
+
chart_readback_ms = 0
|
|
12826
13868
|
verified = verified and deletes_verified
|
|
12827
13869
|
any_readback_unavailable = readback_unavailable or delete_readback_unavailable
|
|
12828
13870
|
|
|
@@ -12850,6 +13892,10 @@ class AiBuilderFacade:
|
|
|
12850
13892
|
"allowed_values": {"chart.chart_type": [member.value for member in PublicChartType], "chart.filter.operator": [member.value for member in ViewFilterOperator]},
|
|
12851
13893
|
"details": {
|
|
12852
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,
|
|
12853
13899
|
**retry_context,
|
|
12854
13900
|
**({"readback_error": _transport_error_payload(readback_error)} if readback_error is not None else {}),
|
|
12855
13901
|
},
|
|
@@ -12858,7 +13904,7 @@ class AiBuilderFacade:
|
|
|
12858
13904
|
"backend_code": failed_items[0].get("backend_code") or (readback_error.backend_code if readback_error is not None else None),
|
|
12859
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),
|
|
12860
13906
|
"noop": noop,
|
|
12861
|
-
"warnings": _chart_apply_warnings(
|
|
13907
|
+
"warnings": large_upsert_warnings + _chart_apply_warnings(
|
|
12862
13908
|
failed_items=failed_items,
|
|
12863
13909
|
readback_unavailable=readback_unavailable,
|
|
12864
13910
|
verified=False if failed_items else verified,
|
|
@@ -12871,6 +13917,8 @@ class AiBuilderFacade:
|
|
|
12871
13917
|
"chart_delete_readback_results": [deepcopy(item) for item in chart_results if item.get("operation") == "delete"],
|
|
12872
13918
|
"chart_order_verified": False if request.reorder_chart_ids else True,
|
|
12873
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,
|
|
12874
13922
|
},
|
|
12875
13923
|
"app_key": app_key,
|
|
12876
13924
|
"app_name": app_name,
|
|
@@ -12898,13 +13946,18 @@ class AiBuilderFacade:
|
|
|
12898
13946
|
"normalized_args": normalized_args,
|
|
12899
13947
|
"missing_fields": [],
|
|
12900
13948
|
"allowed_values": {"chart.chart_type": [member.value for member in PublicChartType], "chart.filter.operator": [member.value for member in ViewFilterOperator]},
|
|
12901
|
-
"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
|
+
},
|
|
12902
13955
|
"request_id": readback_error.request_id if readback_error is not None else None,
|
|
12903
13956
|
"suggested_next_call": None if result_verified else pending_suggestion,
|
|
12904
13957
|
"backend_code": readback_error.backend_code if readback_error is not None else None,
|
|
12905
13958
|
"http_status": None if readback_error is None or readback_error.http_status == 404 else readback_error.http_status,
|
|
12906
13959
|
"noop": noop,
|
|
12907
|
-
"warnings": _chart_apply_warnings(
|
|
13960
|
+
"warnings": large_upsert_warnings + _chart_apply_warnings(
|
|
12908
13961
|
failed_items=[],
|
|
12909
13962
|
readback_unavailable=False if noop else readback_unavailable,
|
|
12910
13963
|
verified=result_verified,
|
|
@@ -12916,6 +13969,8 @@ class AiBuilderFacade:
|
|
|
12916
13969
|
"chart_delete_readback_results": [deepcopy(item) for item in chart_results if item.get("operation") == "delete"],
|
|
12917
13970
|
"chart_order_verified": (readback_list_source == "sorted" and result_verified) if request.reorder_chart_ids else True,
|
|
12918
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,
|
|
12919
13974
|
},
|
|
12920
13975
|
"app_key": app_key,
|
|
12921
13976
|
"app_name": app_name,
|
|
@@ -12927,6 +13982,15 @@ class AiBuilderFacade:
|
|
|
12927
13982
|
})
|
|
12928
13983
|
|
|
12929
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
|
|
12930
13994
|
normalized_args = request.model_dump(mode="json")
|
|
12931
13995
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
12932
13996
|
dash_key = str(request.dash_key or "").strip()
|
|
@@ -12956,8 +14020,10 @@ class AiBuilderFacade:
|
|
|
12956
14020
|
suggested_next_call=None,
|
|
12957
14021
|
)
|
|
12958
14022
|
try:
|
|
14023
|
+
portal_initial_read_started_at = time.perf_counter()
|
|
12959
14024
|
base_payload = self.portals.portal_get(profile=profile, dash_key=dash_key, being_draft=True).get("result") if dash_key else {}
|
|
12960
14025
|
except (QingflowApiError, RuntimeError) as error:
|
|
14026
|
+
portal_initial_read_ms = _duration_ms(portal_initial_read_started_at)
|
|
12961
14027
|
api_error = _coerce_api_error(error)
|
|
12962
14028
|
return _failed(
|
|
12963
14029
|
"PORTAL_APPLY_FAILED",
|
|
@@ -12969,6 +14035,7 @@ class AiBuilderFacade:
|
|
|
12969
14035
|
backend_code=api_error.backend_code,
|
|
12970
14036
|
http_status=None if api_error.http_status == 404 else api_error.http_status,
|
|
12971
14037
|
)
|
|
14038
|
+
portal_initial_read_ms = _duration_ms(portal_initial_read_started_at) if dash_key else 0
|
|
12972
14039
|
if not isinstance(base_payload, dict):
|
|
12973
14040
|
base_payload = {}
|
|
12974
14041
|
if not creating:
|
|
@@ -12983,6 +14050,18 @@ class AiBuilderFacade:
|
|
|
12983
14050
|
permission_outcomes.append(portal_permission_outcome)
|
|
12984
14051
|
|
|
12985
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
|
+
)
|
|
12986
14065
|
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
12987
14066
|
target_package_tag_id = request.package_tag_id
|
|
12988
14067
|
if target_package_tag_id is None:
|
|
@@ -13042,7 +14121,9 @@ class AiBuilderFacade:
|
|
|
13042
14121
|
config=request.config,
|
|
13043
14122
|
base_payload=None,
|
|
13044
14123
|
)
|
|
14124
|
+
portal_create_started_at = time.perf_counter()
|
|
13045
14125
|
create_result = self.portals.portal_create(profile=profile, payload=create_payload)
|
|
14126
|
+
portal_create_ms = _duration_ms(portal_create_started_at)
|
|
13046
14127
|
write_executed = True
|
|
13047
14128
|
created = create_result.get("result") if isinstance(create_result.get("result"), dict) else {}
|
|
13048
14129
|
dash_key = str(created.get("dashKey") or "")
|
|
@@ -13075,36 +14156,61 @@ class AiBuilderFacade:
|
|
|
13075
14156
|
base_payload=base_payload,
|
|
13076
14157
|
)
|
|
13077
14158
|
if sections_requested:
|
|
14159
|
+
portal_component_build_started_at = time.perf_counter()
|
|
13078
14160
|
component_payload, layout_metadata, inline_chart_results = self._build_portal_components_from_sections(
|
|
13079
14161
|
profile=profile,
|
|
13080
14162
|
sections=request.sections,
|
|
13081
14163
|
layout_preset=request.layout_preset,
|
|
13082
14164
|
inline_chart_results=inline_chart_results,
|
|
13083
14165
|
)
|
|
14166
|
+
portal_component_build_ms = _duration_ms(portal_component_build_started_at)
|
|
13084
14167
|
layout_diagnostics = _portal_layout_diagnostics(request.sections, component_payload, layout_metadata=layout_metadata)
|
|
13085
14168
|
update_payload["components"] = component_payload
|
|
14169
|
+
portal_update_started_at = time.perf_counter()
|
|
13086
14170
|
self.portals.portal_update(profile=profile, dash_key=dash_key, payload=update_payload)
|
|
14171
|
+
portal_update_ms = _duration_ms(portal_update_started_at)
|
|
13087
14172
|
write_executed = True
|
|
13088
|
-
|
|
13089
|
-
|
|
13090
|
-
|
|
13091
|
-
|
|
13092
|
-
|
|
13093
|
-
|
|
13094
|
-
|
|
13095
|
-
|
|
13096
|
-
|
|
13097
|
-
|
|
13098
|
-
|
|
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
|
|
13099
14201
|
try:
|
|
14202
|
+
portal_draft_readback_started_at = time.perf_counter()
|
|
13100
14203
|
draft_result = self.portals.portal_get(profile=profile, dash_key=dash_key, being_draft=True).get("result") or {}
|
|
13101
14204
|
except (QingflowApiError, RuntimeError) as read_error:
|
|
14205
|
+
portal_draft_readback_ms = _duration_ms(portal_draft_readback_started_at)
|
|
13102
14206
|
api_read_error = _coerce_api_error(read_error)
|
|
13103
14207
|
draft_readback_error = {
|
|
13104
14208
|
"phase": "portal_draft_readback",
|
|
13105
14209
|
"transport_error": _transport_error_payload(api_read_error),
|
|
13106
14210
|
}
|
|
13107
14211
|
draft_result = {}
|
|
14212
|
+
else:
|
|
14213
|
+
portal_draft_readback_ms = _duration_ms(portal_draft_readback_started_at)
|
|
13108
14214
|
except (QingflowApiError, RuntimeError, ValueError) as error:
|
|
13109
14215
|
api_error = _coerce_api_error(error) if not isinstance(error, ValueError) else None
|
|
13110
14216
|
inline_chart_write_executed = any(bool(item.get("write_executed")) for item in inline_chart_results)
|
|
@@ -13180,9 +14286,12 @@ class AiBuilderFacade:
|
|
|
13180
14286
|
publish_error: JSONObject | None = None
|
|
13181
14287
|
if request.publish:
|
|
13182
14288
|
try:
|
|
14289
|
+
portal_publish_started_at = time.perf_counter()
|
|
13183
14290
|
self.portals.portal_publish(profile=profile, dash_key=dash_key)
|
|
14291
|
+
portal_publish_ms = _duration_ms(portal_publish_started_at)
|
|
13184
14292
|
published = True
|
|
13185
14293
|
except (QingflowApiError, RuntimeError) as error:
|
|
14294
|
+
portal_publish_ms = _duration_ms(portal_publish_started_at)
|
|
13186
14295
|
api_error = _coerce_api_error(error)
|
|
13187
14296
|
publish_failed = True
|
|
13188
14297
|
publish_error = {
|
|
@@ -13191,13 +14300,20 @@ class AiBuilderFacade:
|
|
|
13191
14300
|
}
|
|
13192
14301
|
if published:
|
|
13193
14302
|
try:
|
|
14303
|
+
portal_live_readback_started_at = time.perf_counter()
|
|
13194
14304
|
live_result = self.portals.portal_get(profile=profile, dash_key=dash_key, being_draft=False).get("result") or {}
|
|
13195
14305
|
except (QingflowApiError, RuntimeError) as read_error:
|
|
14306
|
+
portal_live_readback_ms = _duration_ms(portal_live_readback_started_at)
|
|
13196
14307
|
api_read_error = _coerce_api_error(read_error)
|
|
13197
14308
|
live_readback_error = {
|
|
13198
14309
|
"phase": "portal_live_readback",
|
|
13199
14310
|
"transport_error": _transport_error_payload(api_read_error),
|
|
13200
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
|
|
13201
14317
|
|
|
13202
14318
|
draft_components = draft_result.get("components") if isinstance(draft_result, dict) else None
|
|
13203
14319
|
expected_count = len(request.sections) if sections_requested else None
|
|
@@ -13336,55 +14452,79 @@ class AiBuilderFacade:
|
|
|
13336
14452
|
})
|
|
13337
14453
|
|
|
13338
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
|
|
13339
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
|
+
|
|
13340
14469
|
dash_key = str(dash_key or "").strip()
|
|
13341
14470
|
if not dash_key:
|
|
13342
|
-
return
|
|
13343
|
-
|
|
13344
|
-
|
|
13345
|
-
|
|
13346
|
-
|
|
13347
|
-
|
|
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
|
+
)
|
|
13348
14479
|
)
|
|
14480
|
+
portal_delete_started_at = time.perf_counter()
|
|
13349
14481
|
try:
|
|
13350
14482
|
self.portals.portal_delete(profile=profile, dash_key=dash_key)
|
|
13351
14483
|
except (QingflowApiError, RuntimeError) as error:
|
|
14484
|
+
portal_delete_ms += _duration_ms(portal_delete_started_at)
|
|
13352
14485
|
api_error = _coerce_api_error(error)
|
|
13353
|
-
return
|
|
13354
|
-
|
|
13355
|
-
|
|
13356
|
-
|
|
13357
|
-
|
|
13358
|
-
|
|
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
|
+
)
|
|
13359
14494
|
)
|
|
14495
|
+
portal_delete_ms += _duration_ms(portal_delete_started_at)
|
|
13360
14496
|
|
|
14497
|
+
portal_delete_readback_started_at = time.perf_counter()
|
|
13361
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)
|
|
13362
14500
|
verified = delete_readback.get("readback_status") == "deleted"
|
|
13363
|
-
return
|
|
13364
|
-
|
|
13365
|
-
|
|
13366
|
-
|
|
13367
|
-
|
|
13368
|
-
|
|
13369
|
-
|
|
13370
|
-
|
|
13371
|
-
|
|
13372
|
-
|
|
13373
|
-
|
|
13374
|
-
|
|
13375
|
-
|
|
13376
|
-
|
|
13377
|
-
|
|
13378
|
-
|
|
13379
|
-
|
|
13380
|
-
|
|
13381
|
-
|
|
13382
|
-
|
|
13383
|
-
|
|
13384
|
-
|
|
13385
|
-
|
|
13386
|
-
|
|
13387
|
-
|
|
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
|
+
)
|
|
13388
14528
|
|
|
13389
14529
|
def _verify_portal_deleted_by_key(self, *, profile: str, dash_key: str) -> JSONObject:
|
|
13390
14530
|
try:
|
|
@@ -13787,36 +14927,30 @@ class AiBuilderFacade:
|
|
|
13787
14927
|
app_key: str,
|
|
13788
14928
|
app_name: str,
|
|
13789
14929
|
package_tag_id: int | None,
|
|
13790
|
-
create_if_missing: bool,
|
|
13791
14930
|
) -> JSONObject:
|
|
13792
14931
|
if app_key:
|
|
13793
14932
|
return self.app_resolve(profile=profile, app_key=app_key)
|
|
13794
14933
|
if app_name:
|
|
13795
|
-
|
|
13796
|
-
|
|
13797
|
-
|
|
13798
|
-
|
|
13799
|
-
|
|
13800
|
-
|
|
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:
|
|
13801
14953
|
return _failed("APP_NAME_REQUIRED", "app_name or app_key is required", suggested_next_call=None)
|
|
13802
|
-
return {
|
|
13803
|
-
"status": "success",
|
|
13804
|
-
"error_code": None,
|
|
13805
|
-
"recoverable": False,
|
|
13806
|
-
"message": "target app would be created",
|
|
13807
|
-
"normalized_args": {},
|
|
13808
|
-
"missing_fields": [],
|
|
13809
|
-
"allowed_values": {},
|
|
13810
|
-
"details": {},
|
|
13811
|
-
"request_id": None,
|
|
13812
|
-
"suggested_next_call": None,
|
|
13813
|
-
"noop": False,
|
|
13814
|
-
"verification": {},
|
|
13815
|
-
"app_key": "",
|
|
13816
|
-
"app_name": app_name or "未命名应用",
|
|
13817
|
-
"tag_ids": [package_tag_id] if package_tag_id else [],
|
|
13818
|
-
"would_create": True,
|
|
13819
|
-
}
|
|
13820
14954
|
|
|
13821
14955
|
def _create_target_app_shell(
|
|
13822
14956
|
self,
|
|
@@ -13885,7 +15019,7 @@ class AiBuilderFacade:
|
|
|
13885
15019
|
if not new_app_key:
|
|
13886
15020
|
return _failed("APP_CREATE_FAILED", "failed to create app shell", details={"result": result}, suggested_next_call=None)
|
|
13887
15021
|
try:
|
|
13888
|
-
base = self.
|
|
15022
|
+
base = self._read_app_base_raw_direct(profile=profile, app_key=new_app_key)
|
|
13889
15023
|
except (QingflowApiError, RuntimeError) as error:
|
|
13890
15024
|
api_error = _coerce_api_error(error)
|
|
13891
15025
|
if api_error.http_status != 404:
|
|
@@ -14078,12 +15212,104 @@ class AiBuilderFacade:
|
|
|
14078
15212
|
resolved_components: list[dict[str, Any]] = []
|
|
14079
15213
|
layout_metadata: list[dict[str, Any]] = []
|
|
14080
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])
|
|
14081
15307
|
pc_x = 0
|
|
14082
15308
|
pc_y = 0
|
|
14083
15309
|
pc_row_height = 0
|
|
14084
15310
|
mobile_y = 0
|
|
14085
15311
|
app_title_cache: dict[str, str] = {}
|
|
14086
|
-
for section in sections:
|
|
15312
|
+
for section_index, section in enumerate(sections):
|
|
14087
15313
|
position_payload: dict[str, Any]
|
|
14088
15314
|
if section.position is None:
|
|
14089
15315
|
position_payload, pc_x, pc_y, pc_row_height, mobile_y = _portal_component_position_public(
|
|
@@ -14105,46 +15331,9 @@ class AiBuilderFacade:
|
|
|
14105
15331
|
component: dict[str, Any]
|
|
14106
15332
|
if section.source_type == "chart":
|
|
14107
15333
|
if section.chart is not None:
|
|
14108
|
-
|
|
14109
|
-
|
|
14110
|
-
app_key=section.chart.app_key,
|
|
14111
|
-
upsert_charts=[ChartUpsertPatch.model_validate(inline_chart_payload)],
|
|
14112
|
-
patch_charts=[],
|
|
14113
|
-
remove_chart_ids=[],
|
|
14114
|
-
reorder_chart_ids=[],
|
|
14115
|
-
)
|
|
14116
|
-
chart_apply = self.chart_apply(profile=profile, request=chart_request)
|
|
14117
|
-
chart_items = chart_apply.get("chart_results") if isinstance(chart_apply.get("chart_results"), list) else []
|
|
14118
|
-
successful_chart = next(
|
|
14119
|
-
(
|
|
14120
|
-
item
|
|
14121
|
-
for item in chart_items
|
|
14122
|
-
if isinstance(item, dict)
|
|
14123
|
-
and str(item.get("status") or "") in {"created", "updated"}
|
|
14124
|
-
and str(item.get("chart_id") or "").strip()
|
|
14125
|
-
),
|
|
14126
|
-
None,
|
|
14127
|
-
)
|
|
14128
|
-
inline_chart_results.append({
|
|
14129
|
-
"title": section.title,
|
|
14130
|
-
"app_key": section.chart.app_key,
|
|
14131
|
-
"chart_name": section.chart.name,
|
|
14132
|
-
"chart_type": section.chart.chart_type.value,
|
|
14133
|
-
"status": str(successful_chart.get("status") or "") if isinstance(successful_chart, dict) else "failed",
|
|
14134
|
-
"chart_id": str(successful_chart.get("chart_id") or "") if isinstance(successful_chart, dict) else "",
|
|
14135
|
-
"chart_apply_status": chart_apply.get("status"),
|
|
14136
|
-
"write_executed": bool(chart_apply.get("write_executed")),
|
|
14137
|
-
**({"error_code": chart_apply.get("error_code")} if chart_apply.get("error_code") else {}),
|
|
14138
|
-
**({"message": chart_apply.get("message")} if chart_apply.get("message") else {}),
|
|
14139
|
-
})
|
|
14140
|
-
if not isinstance(successful_chart, dict):
|
|
15334
|
+
resolved_chart = inline_chart_resolutions.get(section_index)
|
|
15335
|
+
if not isinstance(resolved_chart, dict):
|
|
14141
15336
|
raise ValueError(f"inline chart '{section.chart.name}' could not be created or updated for portal section '{section.title}'")
|
|
14142
|
-
resolved_chart = {
|
|
14143
|
-
"chart_id": str(successful_chart.get("chart_id") or "").strip(),
|
|
14144
|
-
"chart_name": str(successful_chart.get("name") or section.chart.name).strip(),
|
|
14145
|
-
"app_key": section.chart.app_key,
|
|
14146
|
-
"chart_type": section.chart.chart_type.value,
|
|
14147
|
-
}
|
|
14148
15337
|
else:
|
|
14149
15338
|
resolved_chart = _resolve_chart_reference(
|
|
14150
15339
|
charts=self.charts,
|
|
@@ -18774,7 +19963,11 @@ def _verify_relation_readback_by_name(
|
|
|
18774
19963
|
for item in cast(list[Any], actual.get("visible_fields") or [])
|
|
18775
19964
|
if isinstance(item, dict) and str(item.get("name") or "").strip()
|
|
18776
19965
|
]
|
|
18777
|
-
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
|
+
):
|
|
18778
19971
|
return False
|
|
18779
19972
|
return True
|
|
18780
19973
|
|
|
@@ -18792,6 +19985,36 @@ def _relation_field_names(values: object) -> list[str]:
|
|
|
18792
19985
|
return names
|
|
18793
19986
|
|
|
18794
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
|
+
|
|
18795
20018
|
def _relation_field_public_selector(value: object) -> dict[str, Any] | None:
|
|
18796
20019
|
if not isinstance(value, dict):
|
|
18797
20020
|
return None
|
|
@@ -18850,13 +20073,18 @@ def _schema_relation_readback_matrix(
|
|
|
18850
20073
|
actual_display = _relation_field_public_selector((actual or {}).get("display_field"))
|
|
18851
20074
|
expected_visible_names = _relation_field_names((expected or degraded or {}).get("visible_fields"))
|
|
18852
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
|
|
18853
20077
|
|
|
18854
20078
|
checks = {
|
|
18855
20079
|
"field_exists": isinstance(actual, dict),
|
|
18856
20080
|
"target_app_key": expected_target == actual_target,
|
|
18857
20081
|
"relation_mode": expected_mode == actual_mode,
|
|
18858
20082
|
"display_field": (expected_display or {}).get("name") == (actual_display or {}).get("name"),
|
|
18859
|
-
"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
|
+
),
|
|
18860
20088
|
}
|
|
18861
20089
|
readback_verified = all(checks.values())
|
|
18862
20090
|
if not isinstance(actual, dict):
|
|
@@ -21636,6 +22864,21 @@ def _layouts_semantically_equal(left: dict[str, Any], right: dict[str, Any]) ->
|
|
|
21636
22864
|
return _layout_field_sequence(left) == _layout_field_sequence(right)
|
|
21637
22865
|
|
|
21638
22866
|
|
|
22867
|
+
def _duration_ms(started_at: float) -> int:
|
|
22868
|
+
return max(0, int((time.perf_counter() - started_at) * 1000))
|
|
22869
|
+
|
|
22870
|
+
|
|
22871
|
+
def _merge_duration_breakdown(response: JSONObject, **items: int | None) -> None:
|
|
22872
|
+
breakdown = response.get("duration_breakdown_ms")
|
|
22873
|
+
if not isinstance(breakdown, dict):
|
|
22874
|
+
breakdown = {}
|
|
22875
|
+
response["duration_breakdown_ms"] = breakdown
|
|
22876
|
+
for key, value in items.items():
|
|
22877
|
+
if value is None:
|
|
22878
|
+
continue
|
|
22879
|
+
breakdown[key] = int(value)
|
|
22880
|
+
|
|
22881
|
+
|
|
21639
22882
|
def _find_field_section_id(layout: dict[str, Any], field_name: str) -> str | None:
|
|
21640
22883
|
for section in layout.get("sections", []) or []:
|
|
21641
22884
|
for row in section.get("rows", []) or []:
|
|
@@ -27530,7 +28773,6 @@ def _normalize_flow_stage_failure(stage: JSONObject, *, profile: str, app_key: s
|
|
|
27530
28773
|
"arguments": {
|
|
27531
28774
|
"profile": profile,
|
|
27532
28775
|
"app_key": app_key,
|
|
27533
|
-
"create_if_missing": False,
|
|
27534
28776
|
"add_fields": [
|
|
27535
28777
|
{
|
|
27536
28778
|
"name": "状态",
|