@qingflow-tech/qingflow-app-user-mcp 1.0.8 → 1.0.10
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-record-update/SKILL.md +2 -0
- package/src/qingflow_mcp/builder_facade/models.py +36 -2
- package/src/qingflow_mcp/builder_facade/service.py +476 -95
- package/src/qingflow_mcp/cli/commands/builder.py +40 -11
- package/src/qingflow_mcp/cli/main.py +204 -3
- package/src/qingflow_mcp/response_trim.py +15 -10
- package/src/qingflow_mcp/server_app_builder.py +29 -3
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1199 -32
- package/src/qingflow_mcp/tools/record_tools.py +200 -6
|
@@ -2564,6 +2564,7 @@ class AiBuilderFacade:
|
|
|
2564
2564
|
details=_with_state_read_blocked_details({"app_key": app_key}, resource="custom_buttons", error=api_error),
|
|
2565
2565
|
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
2566
2566
|
))
|
|
2567
|
+
app_name = self._read_app_name_for_builder_output(profile=profile, app_key=app_key)
|
|
2567
2568
|
|
|
2568
2569
|
existing_by_id = {
|
|
2569
2570
|
button_id: item
|
|
@@ -3013,6 +3014,7 @@ class AiBuilderFacade:
|
|
|
3013
3014
|
},
|
|
3014
3015
|
"verified": verified,
|
|
3015
3016
|
"app_key": app_key,
|
|
3017
|
+
"app_name": app_name,
|
|
3016
3018
|
"mode": "apply",
|
|
3017
3019
|
"created": created,
|
|
3018
3020
|
"updated": updated,
|
|
@@ -3663,6 +3665,7 @@ class AiBuilderFacade:
|
|
|
3663
3665
|
details=_with_state_read_blocked_details({"app_key": app_key}, resource="associated_resources", error=api_error),
|
|
3664
3666
|
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3665
3667
|
))
|
|
3668
|
+
app_name = self._read_app_name_for_builder_output(profile=profile, app_key=app_key)
|
|
3666
3669
|
|
|
3667
3670
|
existing_by_id = _associated_resource_index(existing_resources)
|
|
3668
3671
|
blocking_issues: list[dict[str, Any]] = []
|
|
@@ -3671,7 +3674,7 @@ class AiBuilderFacade:
|
|
|
3671
3674
|
client_key_to_patch: dict[str, AssociatedResourceUpsertPatch] = {}
|
|
3672
3675
|
client_key_to_id: dict[str, int] = {}
|
|
3673
3676
|
used_client_keys: set[str] = set()
|
|
3674
|
-
|
|
3677
|
+
resolved_associated_item_refs: dict[str, list[int]] = {}
|
|
3675
3678
|
|
|
3676
3679
|
upsert_resources = list(request.upsert_resources)
|
|
3677
3680
|
if request.patch_resources:
|
|
@@ -3690,11 +3693,6 @@ class AiBuilderFacade:
|
|
|
3690
3693
|
)
|
|
3691
3694
|
)
|
|
3692
3695
|
upsert_resources.extend(expanded_resources)
|
|
3693
|
-
force_update_resource_ids.update(
|
|
3694
|
-
item_id
|
|
3695
|
-
for item_id in (_coerce_positive_int(patch.associated_item_id) for patch in expanded_resources)
|
|
3696
|
-
if item_id is not None
|
|
3697
|
-
)
|
|
3698
3696
|
normalized_args["upsert_resources"] = [patch.model_dump(mode="json") for patch in upsert_resources]
|
|
3699
3697
|
normalized_args["patch_results"] = patch_results
|
|
3700
3698
|
|
|
@@ -3732,14 +3730,9 @@ class AiBuilderFacade:
|
|
|
3732
3730
|
touched_ids.add(associated_item_id)
|
|
3733
3731
|
if client_key:
|
|
3734
3732
|
client_key_to_id[client_key] = associated_item_id
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
or _associated_resource_patch_has_match_config(patch)
|
|
3739
|
-
else "unchanged"
|
|
3740
|
-
if _associated_resource_matches_patch(existing_by_id[associated_item_id], patch)
|
|
3741
|
-
else "update"
|
|
3742
|
-
)
|
|
3733
|
+
has_match_config = _associated_resource_patch_has_match_config(patch)
|
|
3734
|
+
identity_matches = _associated_resource_matches_patch(existing_by_id[associated_item_id], patch)
|
|
3735
|
+
operation = "update" if has_match_config or not identity_matches else "unchanged"
|
|
3743
3736
|
upsert_ops.append({"operation": operation, "associated_item_id": associated_item_id, "patch": patch, "index": index})
|
|
3744
3737
|
continue
|
|
3745
3738
|
matches = [
|
|
@@ -3778,34 +3771,35 @@ class AiBuilderFacade:
|
|
|
3778
3771
|
else:
|
|
3779
3772
|
upsert_ops.append({"operation": "create", "associated_item_id": None, "patch": patch, "index": index})
|
|
3780
3773
|
|
|
3781
|
-
remove_ids = [
|
|
3782
|
-
item_id
|
|
3783
|
-
for item_id in (_coerce_positive_int(raw_id) for raw_id in request.remove_associated_item_ids)
|
|
3784
|
-
if item_id is not None
|
|
3785
|
-
]
|
|
3774
|
+
remove_ids: list[int] = []
|
|
3786
3775
|
for raw_id in request.remove_associated_item_ids:
|
|
3787
|
-
item_id =
|
|
3776
|
+
item_id, issue = _resolve_associated_resource_selector(
|
|
3777
|
+
raw_id,
|
|
3778
|
+
existing_resources=existing_resources,
|
|
3779
|
+
existing_by_id=existing_by_id,
|
|
3780
|
+
reason_path="remove_associated_item_ids",
|
|
3781
|
+
)
|
|
3788
3782
|
if item_id is None:
|
|
3789
|
-
blocking_issues.append({"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND", "reason_path": "remove_associated_item_ids", "received": raw_id})
|
|
3783
|
+
blocking_issues.append(issue or {"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND", "reason_path": "remove_associated_item_ids", "received": raw_id})
|
|
3790
3784
|
continue
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
elif item_id in touched_ids:
|
|
3785
|
+
remove_ids.append(item_id)
|
|
3786
|
+
if item_id in touched_ids:
|
|
3794
3787
|
blocking_issues.append(_duplicate_associated_resource_issue("remove_associated_item_ids", item_id))
|
|
3795
3788
|
else:
|
|
3796
3789
|
touched_ids.add(item_id)
|
|
3797
3790
|
|
|
3798
|
-
reorder_ids = [
|
|
3799
|
-
item_id
|
|
3800
|
-
for item_id in (_coerce_positive_int(raw_id) for raw_id in request.reorder_associated_item_ids)
|
|
3801
|
-
if item_id is not None
|
|
3802
|
-
]
|
|
3791
|
+
reorder_ids: list[int] = []
|
|
3803
3792
|
for raw_id in request.reorder_associated_item_ids:
|
|
3804
|
-
item_id =
|
|
3793
|
+
item_id, issue = _resolve_associated_resource_selector(
|
|
3794
|
+
raw_id,
|
|
3795
|
+
existing_resources=existing_resources,
|
|
3796
|
+
existing_by_id=existing_by_id,
|
|
3797
|
+
reason_path="reorder_associated_item_ids",
|
|
3798
|
+
)
|
|
3805
3799
|
if item_id is None:
|
|
3806
|
-
blocking_issues.append({"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND", "reason_path": "reorder_associated_item_ids", "received": raw_id})
|
|
3807
|
-
|
|
3808
|
-
|
|
3800
|
+
blocking_issues.append(issue or {"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND", "reason_path": "reorder_associated_item_ids", "received": raw_id})
|
|
3801
|
+
continue
|
|
3802
|
+
reorder_ids.append(item_id)
|
|
3809
3803
|
|
|
3810
3804
|
for index, view_config in enumerate(request.view_configs):
|
|
3811
3805
|
refs = [str(ref or "").strip() for ref in view_config.associated_item_refs if str(ref or "").strip()]
|
|
@@ -3819,21 +3813,26 @@ class AiBuilderFacade:
|
|
|
3819
3813
|
"message": "associated_item_refs must reference client_key values from upsert_resources in the same apply call",
|
|
3820
3814
|
}
|
|
3821
3815
|
)
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
{
|
|
3830
|
-
"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND",
|
|
3831
|
-
"reason_path": f"view_configs[{index}].associated_item_ids",
|
|
3832
|
-
"invalid_associated_item_ids": invalid_ids,
|
|
3833
|
-
"available_associated_item_ids": sorted(existing_by_id),
|
|
3834
|
-
"message": "view_configs.associated_item_ids must use app-level associated_item_id values from app_get",
|
|
3835
|
-
}
|
|
3816
|
+
resolved_ids: list[int] = []
|
|
3817
|
+
for raw_id in view_config.associated_item_ids:
|
|
3818
|
+
item_id, issue = _resolve_associated_resource_selector(
|
|
3819
|
+
raw_id,
|
|
3820
|
+
existing_resources=existing_resources,
|
|
3821
|
+
existing_by_id=existing_by_id,
|
|
3822
|
+
reason_path=f"view_configs[{index}].associated_item_ids",
|
|
3836
3823
|
)
|
|
3824
|
+
if item_id is None:
|
|
3825
|
+
blocking_issues.append(
|
|
3826
|
+
issue
|
|
3827
|
+
or {
|
|
3828
|
+
"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND",
|
|
3829
|
+
"reason_path": f"view_configs[{index}].associated_item_ids",
|
|
3830
|
+
"received": raw_id,
|
|
3831
|
+
}
|
|
3832
|
+
)
|
|
3833
|
+
continue
|
|
3834
|
+
resolved_ids.append(item_id)
|
|
3835
|
+
resolved_associated_item_refs[f"view_configs[{index}].associated_item_ids"] = resolved_ids
|
|
3837
3836
|
raw_limit_type = str(view_config.limit_type or ("all" if view_config.visible else "")).strip().lower()
|
|
3838
3837
|
if view_config.visible and raw_limit_type and raw_limit_type not in {"all", "select"}:
|
|
3839
3838
|
blocking_issues.append(
|
|
@@ -3851,6 +3850,8 @@ class AiBuilderFacade:
|
|
|
3851
3850
|
for issue in resource_match_issues
|
|
3852
3851
|
]
|
|
3853
3852
|
)
|
|
3853
|
+
if resolved_associated_item_refs:
|
|
3854
|
+
normalized_args["resolved_associated_item_refs"] = deepcopy(resolved_associated_item_refs)
|
|
3854
3855
|
|
|
3855
3856
|
if blocking_issues:
|
|
3856
3857
|
return finalize(
|
|
@@ -4008,12 +4009,33 @@ class AiBuilderFacade:
|
|
|
4008
4009
|
resources_after = []
|
|
4009
4010
|
|
|
4010
4011
|
for index, view_config in enumerate(request.view_configs):
|
|
4012
|
+
resolved_view_config_ids = resolved_associated_item_refs.get(f"view_configs[{index}].associated_item_ids", [])
|
|
4013
|
+
view_name = str(view_config.view_key or "").strip()
|
|
4014
|
+
missing_ref_ids = [
|
|
4015
|
+
str(ref or "").strip()
|
|
4016
|
+
for ref in view_config.associated_item_refs
|
|
4017
|
+
if str(ref or "").strip() and str(ref or "").strip() not in client_key_to_id
|
|
4018
|
+
]
|
|
4019
|
+
if missing_ref_ids:
|
|
4020
|
+
failed.append(
|
|
4021
|
+
{
|
|
4022
|
+
"operation": "view_config",
|
|
4023
|
+
"index": index,
|
|
4024
|
+
"view_key": view_config.view_key,
|
|
4025
|
+
"view_name": view_name,
|
|
4026
|
+
"status": "failed",
|
|
4027
|
+
"error_code": "ASSOCIATED_RESOURCE_REF_UNRESOLVED",
|
|
4028
|
+
"missing_refs": missing_ref_ids,
|
|
4029
|
+
"message": "view_config references associated resources that were not successfully created or resolved; skipped view config write to avoid clearing existing associations",
|
|
4030
|
+
}
|
|
4031
|
+
)
|
|
4032
|
+
continue
|
|
4011
4033
|
selected_ids = [
|
|
4012
4034
|
item_id
|
|
4013
4035
|
for item_id in (
|
|
4014
4036
|
_coerce_positive_int(raw_id)
|
|
4015
4037
|
for raw_id in [
|
|
4016
|
-
*
|
|
4038
|
+
*resolved_view_config_ids,
|
|
4017
4039
|
*[client_key_to_id.get(str(ref or "").strip()) for ref in view_config.associated_item_refs],
|
|
4018
4040
|
]
|
|
4019
4041
|
)
|
|
@@ -4024,19 +4046,21 @@ class AiBuilderFacade:
|
|
|
4024
4046
|
available_resources=resources_after,
|
|
4025
4047
|
)
|
|
4026
4048
|
if issues:
|
|
4027
|
-
failed.append({"operation": "view_config", "index": index, "view_key": view_config.view_key, "status": "failed", "issues": issues})
|
|
4049
|
+
failed.append({"operation": "view_config", "index": index, "view_key": view_config.view_key, "view_name": view_name, "status": "failed", "issues": issues})
|
|
4028
4050
|
continue
|
|
4029
4051
|
try:
|
|
4030
4052
|
write_executed = True
|
|
4031
4053
|
self._update_view_associated_resources_config(profile=profile, view_key=view_config.view_key, associated_resources_payload=view_payload)
|
|
4032
4054
|
try:
|
|
4033
4055
|
config = self.views.view_get_config(profile=profile, viewgraph_key=view_config.view_key).get("result") or {}
|
|
4056
|
+
view_name = _extract_view_name(config if isinstance(config, dict) else {}) or view_name
|
|
4034
4057
|
actual_config = _extract_view_associated_resources_config(config if isinstance(config, dict) else {}, available_resources=resources_after)
|
|
4035
4058
|
verified_config = expected_config is not None and _associated_resources_config_matches(expected_config, actual_config)
|
|
4036
4059
|
if not verified_config and expected_config is not None:
|
|
4037
4060
|
self._update_view_associated_resources_config(profile=profile, view_key=view_config.view_key, associated_resources_payload=view_payload)
|
|
4038
4061
|
refreshed_resources = self._load_associated_resources_for_builder(profile=profile, app_key=app_key)
|
|
4039
4062
|
refreshed_config = self.views.view_get_config(profile=profile, viewgraph_key=view_config.view_key).get("result") or {}
|
|
4063
|
+
view_name = _extract_view_name(refreshed_config if isinstance(refreshed_config, dict) else {}) or view_name
|
|
4040
4064
|
actual_config = _extract_view_associated_resources_config(
|
|
4041
4065
|
refreshed_config if isinstance(refreshed_config, dict) else {},
|
|
4042
4066
|
available_resources=refreshed_resources,
|
|
@@ -4045,10 +4069,10 @@ class AiBuilderFacade:
|
|
|
4045
4069
|
except (QingflowApiError, RuntimeError):
|
|
4046
4070
|
actual_config = {}
|
|
4047
4071
|
verified_config = False
|
|
4048
|
-
view_config_results.append({"index": index, "view_key": view_config.view_key, "status": "success" if verified_config else "partial_success", "associated_resources_verified": verified_config, "expected": expected_config, "actual": actual_config})
|
|
4072
|
+
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})
|
|
4049
4073
|
except (QingflowApiError, RuntimeError) as error:
|
|
4050
4074
|
api_error = _coerce_api_error(error)
|
|
4051
|
-
failed.append({"operation": "view_config", "index": index, "view_key": view_config.view_key, "status": "failed", "error_code": "VIEW_ASSOCIATED_RESOURCES_WRITE_FAILED", "message": api_error.message, "transport_error": _transport_error_payload(api_error)})
|
|
4075
|
+
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)})
|
|
4052
4076
|
|
|
4053
4077
|
final_resources: list[dict[str, Any]] = []
|
|
4054
4078
|
readback_failed = False
|
|
@@ -4115,6 +4139,7 @@ class AiBuilderFacade:
|
|
|
4115
4139
|
str(index): _summarize_compiled_match_rules(rules)
|
|
4116
4140
|
for index, rules in compiled_resource_match_rules.items()
|
|
4117
4141
|
},
|
|
4142
|
+
"resolved_associated_item_refs": resolved_associated_item_refs,
|
|
4118
4143
|
},
|
|
4119
4144
|
"request_id": None,
|
|
4120
4145
|
"suggested_next_call": None if verified else {"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
@@ -4123,6 +4148,7 @@ class AiBuilderFacade:
|
|
|
4123
4148
|
"verification": {"associated_resources_verified": pool_verified, "associated_resource_view_configs_verified": view_configs_verified, "readback_loaded": not readback_failed},
|
|
4124
4149
|
"verified": verified,
|
|
4125
4150
|
"app_key": app_key,
|
|
4151
|
+
"app_name": app_name,
|
|
4126
4152
|
"mode": "apply",
|
|
4127
4153
|
"created": created,
|
|
4128
4154
|
"updated": updated,
|
|
@@ -4601,9 +4627,18 @@ class AiBuilderFacade:
|
|
|
4601
4627
|
"associated_resources": len(associated_resources),
|
|
4602
4628
|
"custom_buttons": len(custom_buttons),
|
|
4603
4629
|
}
|
|
4630
|
+
app_name = str(
|
|
4631
|
+
base_result.get("formTitle")
|
|
4632
|
+
or base_result.get("title")
|
|
4633
|
+
or base_result.get("appName")
|
|
4634
|
+
or base_result.get("name")
|
|
4635
|
+
or app_key
|
|
4636
|
+
).strip() or app_key
|
|
4604
4637
|
response = AppReadSummaryResponse(
|
|
4605
4638
|
app_key=app_key,
|
|
4606
|
-
|
|
4639
|
+
app_name=app_name,
|
|
4640
|
+
name=app_name,
|
|
4641
|
+
title=app_name,
|
|
4607
4642
|
app_icon=str(base_result.get("appIcon") or "").strip() or None,
|
|
4608
4643
|
visibility=_public_visibility_from_member_auth(base_result.get("auth")),
|
|
4609
4644
|
tag_ids=_coerce_int_list(base_result.get("tagIds")),
|
|
@@ -5430,7 +5465,7 @@ class AiBuilderFacade:
|
|
|
5430
5465
|
for index, patch, config in semantic_patches:
|
|
5431
5466
|
config_payload = config.model_dump(mode="json", exclude_none=True)
|
|
5432
5467
|
reason_base = f"upsert_buttons[{index}].trigger_add_data_config"
|
|
5433
|
-
if config.
|
|
5468
|
+
if "que_relation" in config.model_fields_set:
|
|
5434
5469
|
issues.append(
|
|
5435
5470
|
{
|
|
5436
5471
|
"error_code": "MIXED_CUSTOM_BUTTON_MAPPING_MODES",
|
|
@@ -5734,11 +5769,12 @@ class AiBuilderFacade:
|
|
|
5734
5769
|
)
|
|
5735
5770
|
return {"verified": False, "write_executed": False, "write_succeeded": False, "view_configs": results, "failed": failed, "warnings": warnings}
|
|
5736
5771
|
|
|
5737
|
-
|
|
5738
|
-
_extract_view_key(view)
|
|
5772
|
+
existing_views_by_key = {
|
|
5773
|
+
_extract_view_key(view): view
|
|
5739
5774
|
for view in (existing_views if isinstance(existing_views, list) else [])
|
|
5740
5775
|
if isinstance(view, dict) and _extract_view_key(view)
|
|
5741
5776
|
}
|
|
5777
|
+
view_keys = set(existing_views_by_key)
|
|
5742
5778
|
button_inventory: dict[int, dict[str, Any]] = {}
|
|
5743
5779
|
for item in [*existing_buttons, *readback_buttons]:
|
|
5744
5780
|
if not isinstance(item, dict):
|
|
@@ -5770,6 +5806,7 @@ class AiBuilderFacade:
|
|
|
5770
5806
|
try:
|
|
5771
5807
|
current_response = self.views.view_get_config(profile=profile, viewgraph_key=view_key)
|
|
5772
5808
|
current_config = current_response.get("result") if isinstance(current_response.get("result"), dict) else {}
|
|
5809
|
+
view_name = _extract_view_name(current_config) or _extract_view_name(existing_views_by_key.get(view_key) or {}) or view_key
|
|
5773
5810
|
except (QingflowApiError, RuntimeError) as error:
|
|
5774
5811
|
api_error = _coerce_api_error(error)
|
|
5775
5812
|
issue = {
|
|
@@ -5778,6 +5815,7 @@ class AiBuilderFacade:
|
|
|
5778
5815
|
"status": "failed",
|
|
5779
5816
|
"error_code": "VIEW_CONFIG_READ_FAILED",
|
|
5780
5817
|
"view_key": view_key,
|
|
5818
|
+
"view_name": _extract_view_name(existing_views_by_key.get(view_key) or {}) or view_key,
|
|
5781
5819
|
"message": api_error.message,
|
|
5782
5820
|
"transport_error": _transport_error_payload(api_error),
|
|
5783
5821
|
}
|
|
@@ -5868,6 +5906,7 @@ class AiBuilderFacade:
|
|
|
5868
5906
|
"status": "failed",
|
|
5869
5907
|
"error_code": "INSIDE_BUTTON_BACKEND_UNSUPPORTED",
|
|
5870
5908
|
"view_key": view_key,
|
|
5909
|
+
"view_name": view_name,
|
|
5871
5910
|
"message": "backend rejected inside/list-button placement; header/detail placements were retried without inside buttons",
|
|
5872
5911
|
"backend_message": api_error.message,
|
|
5873
5912
|
"transport_error": _transport_error_payload(api_error),
|
|
@@ -5880,9 +5919,10 @@ class AiBuilderFacade:
|
|
|
5880
5919
|
"index": config_index,
|
|
5881
5920
|
"operation": "view_config",
|
|
5882
5921
|
"status": "failed",
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5922
|
+
"error_code": "VIEW_BUTTON_CONFIG_WRITE_FAILED",
|
|
5923
|
+
"view_key": view_key,
|
|
5924
|
+
"view_name": view_name,
|
|
5925
|
+
"message": fallback_api_error.message,
|
|
5886
5926
|
"transport_error": _transport_error_payload(fallback_api_error),
|
|
5887
5927
|
"initial_error": _transport_error_payload(api_error),
|
|
5888
5928
|
}
|
|
@@ -5897,6 +5937,7 @@ class AiBuilderFacade:
|
|
|
5897
5937
|
"status": "failed",
|
|
5898
5938
|
"error_code": "INSIDE_BUTTON_BACKEND_UNSUPPORTED",
|
|
5899
5939
|
"view_key": view_key,
|
|
5940
|
+
"view_name": view_name,
|
|
5900
5941
|
"message": "backend rejected inside/list-button placement",
|
|
5901
5942
|
"backend_message": api_error.message,
|
|
5902
5943
|
"transport_error": _transport_error_payload(api_error),
|
|
@@ -5913,6 +5954,7 @@ class AiBuilderFacade:
|
|
|
5913
5954
|
"status": "failed",
|
|
5914
5955
|
"error_code": "VIEW_BUTTON_CONFIG_WRITE_FAILED",
|
|
5915
5956
|
"view_key": view_key,
|
|
5957
|
+
"view_name": view_name,
|
|
5916
5958
|
"message": api_error.message,
|
|
5917
5959
|
"transport_error": _transport_error_payload(api_error),
|
|
5918
5960
|
}
|
|
@@ -5942,6 +5984,7 @@ class AiBuilderFacade:
|
|
|
5942
5984
|
"operation": "view_config",
|
|
5943
5985
|
"status": "success" if verified else ("partial_success" if unsupported_list_issue is not None else "unverified"),
|
|
5944
5986
|
"view_key": view_key,
|
|
5987
|
+
"view_name": view_name,
|
|
5945
5988
|
"mode": "replace" if replace_existing else "merge",
|
|
5946
5989
|
"buttons_configured": len(new_dtos),
|
|
5947
5990
|
"view_buttons_verified": verified,
|
|
@@ -5964,6 +6007,7 @@ class AiBuilderFacade:
|
|
|
5964
6007
|
"operation": "view_config",
|
|
5965
6008
|
"status": "unverified",
|
|
5966
6009
|
"view_key": view_key,
|
|
6010
|
+
"view_name": view_name,
|
|
5967
6011
|
"mode": "replace" if replace_existing else "merge",
|
|
5968
6012
|
"buttons_configured": len(new_dtos),
|
|
5969
6013
|
"view_buttons_verified": None,
|
|
@@ -7044,6 +7088,7 @@ class AiBuilderFacade:
|
|
|
7044
7088
|
},
|
|
7045
7089
|
"app_key": target.app_key,
|
|
7046
7090
|
"app_icon": str(resolved.get("app_icon") or visual_result.get("app_icon") or "").strip() or None,
|
|
7091
|
+
"app_name": str(visual_result.get("app_name_after") or target.app_name),
|
|
7047
7092
|
"app_name_before": str(visual_result.get("app_name_before") or target.app_name),
|
|
7048
7093
|
"app_name_after": str(visual_result.get("app_name_after") or target.app_name),
|
|
7049
7094
|
"app_base_updated": bool(visual_result.get("updated")),
|
|
@@ -7284,7 +7329,8 @@ class AiBuilderFacade:
|
|
|
7284
7329
|
actual_app_name = str(base_info.get("formTitle") or effective_app_name).strip() or effective_app_name
|
|
7285
7330
|
actual_app_icon = str(base_info.get("appIcon") or visual_result.get("app_icon") or "").strip() or None
|
|
7286
7331
|
expected_app_icon = str(visual_result.get("app_icon") or "").strip() or None
|
|
7287
|
-
|
|
7332
|
+
app_icon_verified = True if expected_app_icon is None else actual_app_icon == expected_app_icon
|
|
7333
|
+
app_base_verified = actual_app_name == effective_app_name and app_icon_verified
|
|
7288
7334
|
verified = app_base_verified and relation_target_metadata_verified
|
|
7289
7335
|
response = {
|
|
7290
7336
|
"status": "success" if verified else "partial_success",
|
|
@@ -7308,6 +7354,7 @@ class AiBuilderFacade:
|
|
|
7308
7354
|
},
|
|
7309
7355
|
"app_key": target.app_key,
|
|
7310
7356
|
"app_icon": str(visual_result.get("app_icon") or "").strip() or None,
|
|
7357
|
+
"app_name": effective_app_name,
|
|
7311
7358
|
"app_name_before": str(visual_result.get("app_name_before") or target.app_name),
|
|
7312
7359
|
"app_name_after": effective_app_name,
|
|
7313
7360
|
"app_base_updated": bool(visual_result.get("updated")),
|
|
@@ -7505,6 +7552,7 @@ class AiBuilderFacade:
|
|
|
7505
7552
|
},
|
|
7506
7553
|
"app_key": target.app_key,
|
|
7507
7554
|
"app_icon": str(visual_result.get("app_icon") or resolved.get("app_icon") or "").strip() or None,
|
|
7555
|
+
"app_name": effective_app_name,
|
|
7508
7556
|
"app_name_before": str(visual_result.get("app_name_before") or target.app_name),
|
|
7509
7557
|
"app_name_after": effective_app_name,
|
|
7510
7558
|
"app_base_updated": bool(visual_result.get("updated")),
|
|
@@ -7514,6 +7562,13 @@ class AiBuilderFacade:
|
|
|
7514
7562
|
"updated": updated,
|
|
7515
7563
|
"removed": removed,
|
|
7516
7564
|
},
|
|
7565
|
+
"field_diff_details": _schema_field_diff_details(
|
|
7566
|
+
added=added,
|
|
7567
|
+
updated=updated,
|
|
7568
|
+
removed=removed,
|
|
7569
|
+
before_fields=original_fields,
|
|
7570
|
+
after_fields=current_fields,
|
|
7571
|
+
),
|
|
7517
7572
|
"verified": False,
|
|
7518
7573
|
"tag_ids_after": [],
|
|
7519
7574
|
"package_attached": None,
|
|
@@ -7533,6 +7588,13 @@ class AiBuilderFacade:
|
|
|
7533
7588
|
try:
|
|
7534
7589
|
verified = self.app_read(profile=profile, app_key=target.app_key, include_raw=False)
|
|
7535
7590
|
verified_field_names = {field["name"] for field in verified["schema"]["fields"]}
|
|
7591
|
+
response["field_diff_details"] = _schema_field_diff_details(
|
|
7592
|
+
added=added,
|
|
7593
|
+
updated=updated,
|
|
7594
|
+
removed=removed,
|
|
7595
|
+
before_fields=original_fields,
|
|
7596
|
+
after_fields=cast(list[dict[str, Any]], verified["schema"]["fields"]),
|
|
7597
|
+
)
|
|
7536
7598
|
verification_ok = all(name in verified_field_names for name in added + updated) and all(name not in verified_field_names for name in removed)
|
|
7537
7599
|
data_display_verification = _verify_data_display_readback(
|
|
7538
7600
|
form_settings=verified.get("form_settings"),
|
|
@@ -7564,7 +7626,8 @@ class AiBuilderFacade:
|
|
|
7564
7626
|
actual_app_name = str(base_info.get("formTitle") or effective_app_name).strip() or effective_app_name
|
|
7565
7627
|
actual_app_icon = str(base_info.get("appIcon") or visual_result.get("app_icon") or "").strip() or None
|
|
7566
7628
|
expected_app_icon = str(visual_result.get("app_icon") or "").strip() or None
|
|
7567
|
-
|
|
7629
|
+
app_icon_verified = True if expected_app_icon is None else actual_app_icon == expected_app_icon
|
|
7630
|
+
app_base_verified = actual_app_name == effective_app_name and app_icon_verified
|
|
7568
7631
|
except (QingflowApiError, RuntimeError) as error:
|
|
7569
7632
|
base_error = _coerce_api_error(error)
|
|
7570
7633
|
if verification_error is None:
|
|
@@ -7658,6 +7721,7 @@ class AiBuilderFacade:
|
|
|
7658
7721
|
details=_with_state_read_blocked_details({"app_key": app_key}, resource="schema", error=api_error),
|
|
7659
7722
|
suggested_next_call={"tool_name": "app_get_layout", "arguments": {"profile": profile, "app_key": app_key}},
|
|
7660
7723
|
))
|
|
7724
|
+
app_name = str(schema_result.get("formTitle") or schema_result.get("title") or schema_result.get("appName") or app_key).strip() or app_key
|
|
7661
7725
|
parsed = _parse_schema(schema_result)
|
|
7662
7726
|
current_fields = parsed["fields"]
|
|
7663
7727
|
requested_sections, missing_selectors = _resolve_layout_sections_to_names(requested_sections, current_fields)
|
|
@@ -7740,6 +7804,7 @@ class AiBuilderFacade:
|
|
|
7740
7804
|
"warnings": [],
|
|
7741
7805
|
"verification": {"layout_verified": True, "layout_summary_verified": True},
|
|
7742
7806
|
"app_key": app_key,
|
|
7807
|
+
"app_name": app_name,
|
|
7743
7808
|
"layout_diff": {
|
|
7744
7809
|
"mode": mode.value,
|
|
7745
7810
|
"replaced": mode == LayoutApplyMode.replace,
|
|
@@ -7828,6 +7893,7 @@ class AiBuilderFacade:
|
|
|
7828
7893
|
"warnings": [],
|
|
7829
7894
|
"verification": {"layout_verified": False, "layout_summary_verified": False, "layout_read_unavailable": True},
|
|
7830
7895
|
"app_key": app_key,
|
|
7896
|
+
"app_name": app_name,
|
|
7831
7897
|
"layout_diff": {
|
|
7832
7898
|
"mode": mode.value,
|
|
7833
7899
|
"replaced": mode == LayoutApplyMode.replace,
|
|
@@ -7884,6 +7950,7 @@ class AiBuilderFacade:
|
|
|
7884
7950
|
"warnings": warnings,
|
|
7885
7951
|
"verification": {"layout_verified": layout_verified, "layout_summary_verified": layout_summary_verified},
|
|
7886
7952
|
"app_key": app_key,
|
|
7953
|
+
"app_name": app_name,
|
|
7887
7954
|
"layout_diff": {
|
|
7888
7955
|
"mode": mode.value,
|
|
7889
7956
|
"replaced": mode == LayoutApplyMode.replace,
|
|
@@ -7946,6 +8013,7 @@ class AiBuilderFacade:
|
|
|
7946
8013
|
details=_with_state_read_blocked_details({"app_key": app_key}, resource="workflow", error=api_error),
|
|
7947
8014
|
suggested_next_call={"tool_name": "app_get_flow", "arguments": {"profile": profile, "app_key": app_key}},
|
|
7948
8015
|
))
|
|
8016
|
+
app_name = str(base.get("formTitle") or base.get("title") or base.get("appName") or app_key).strip() or app_key
|
|
7949
8017
|
entity = _entity_spec_from_app(base_info=base, schema=schema, views=None)
|
|
7950
8018
|
current_fields = _parse_schema(schema)["fields"]
|
|
7951
8019
|
normalized_nodes, resolution_issues = self._normalize_flow_nodes(profile=profile, current_fields=current_fields, nodes=nodes)
|
|
@@ -8113,6 +8181,7 @@ class AiBuilderFacade:
|
|
|
8113
8181
|
"workflow_read_unavailable": verified_nodes_unavailable,
|
|
8114
8182
|
},
|
|
8115
8183
|
"app_key": app_key,
|
|
8184
|
+
"app_name": app_name,
|
|
8116
8185
|
"flow_diff": {"mode": "replace", "node_count": desired_node_count},
|
|
8117
8186
|
"verified": workflow_verified,
|
|
8118
8187
|
}
|
|
@@ -8188,6 +8257,7 @@ class AiBuilderFacade:
|
|
|
8188
8257
|
details=_with_state_read_blocked_details({"app_key": app_key}, resource="views", error=api_error),
|
|
8189
8258
|
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
8190
8259
|
))
|
|
8260
|
+
app_name = str(base.get("formTitle") or base.get("title") or base.get("appName") or "").strip() or None
|
|
8191
8261
|
existing_views = existing_views or []
|
|
8192
8262
|
existing_by_key: dict[str, dict[str, Any]] = {}
|
|
8193
8263
|
existing_by_name: dict[str, list[dict[str, Any]]] = {}
|
|
@@ -8295,9 +8365,15 @@ class AiBuilderFacade:
|
|
|
8295
8365
|
)
|
|
8296
8366
|
)
|
|
8297
8367
|
removed: list[str] = []
|
|
8368
|
+
removed_keys: set[str] = set()
|
|
8298
8369
|
view_results: list[dict[str, Any]] = []
|
|
8299
|
-
|
|
8300
|
-
|
|
8370
|
+
failed_views: list[dict[str, Any]] = []
|
|
8371
|
+
for selector in remove_views:
|
|
8372
|
+
selector_text = str(selector or "").strip()
|
|
8373
|
+
if not selector_text:
|
|
8374
|
+
continue
|
|
8375
|
+
key_match = existing_by_key.get(selector_text)
|
|
8376
|
+
matches = [key_match] if isinstance(key_match, dict) else existing_by_name.get(selector_text, [])
|
|
8301
8377
|
if len(matches) > 1:
|
|
8302
8378
|
return _failed(
|
|
8303
8379
|
"AMBIGUOUS_VIEW",
|
|
@@ -8305,7 +8381,7 @@ class AiBuilderFacade:
|
|
|
8305
8381
|
normalized_args=normalized_args,
|
|
8306
8382
|
details={
|
|
8307
8383
|
"app_key": app_key,
|
|
8308
|
-
"view_name":
|
|
8384
|
+
"view_name": selector_text,
|
|
8309
8385
|
"matches": [
|
|
8310
8386
|
{"name": _extract_view_name(view), "view_key": _extract_view_key(view), "type": _normalize_view_type_name(view.get("viewgraphType") or view.get("type"))}
|
|
8311
8387
|
for view in matches
|
|
@@ -8313,21 +8389,36 @@ class AiBuilderFacade:
|
|
|
8313
8389
|
},
|
|
8314
8390
|
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
8315
8391
|
)
|
|
8392
|
+
if not matches:
|
|
8393
|
+
failed_view = {
|
|
8394
|
+
"name": selector_text,
|
|
8395
|
+
"view_key": selector_text,
|
|
8396
|
+
"type": None,
|
|
8397
|
+
"status": "failed",
|
|
8398
|
+
"error_code": "VIEW_NOT_FOUND",
|
|
8399
|
+
"message": "remove_views item did not match an existing view name or view_key",
|
|
8400
|
+
}
|
|
8401
|
+
failed_views.append(failed_view)
|
|
8402
|
+
view_results.append(deepcopy(failed_view))
|
|
8403
|
+
continue
|
|
8316
8404
|
if len(matches) == 1:
|
|
8317
8405
|
key = _extract_view_key(matches[0])
|
|
8406
|
+
removed_name = _extract_view_name(matches[0]) or selector_text
|
|
8318
8407
|
self.views.view_delete(profile=profile, viewgraph_key=key)
|
|
8319
|
-
removed.append(
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8408
|
+
removed.append(removed_name)
|
|
8409
|
+
if key:
|
|
8410
|
+
removed_keys.add(key)
|
|
8411
|
+
existing_by_key.pop(key, None)
|
|
8412
|
+
existing_by_name.pop(removed_name, None)
|
|
8413
|
+
view_results.append({"name": removed_name, "view_key": key, "type": None, "status": "removed"})
|
|
8323
8414
|
created: list[str] = []
|
|
8324
8415
|
updated: list[str] = []
|
|
8325
|
-
failed_views: list[dict[str, Any]] = []
|
|
8326
8416
|
existing_view_list = [
|
|
8327
8417
|
view
|
|
8328
8418
|
for view in (existing_views if isinstance(existing_views, list) else [])
|
|
8329
8419
|
if isinstance(view, dict)
|
|
8330
|
-
and _extract_view_name(view) not in
|
|
8420
|
+
and _extract_view_name(view) not in removed
|
|
8421
|
+
and _extract_view_key(view) not in removed_keys
|
|
8331
8422
|
]
|
|
8332
8423
|
for ordinal, patch in enumerate(upsert_views, start=1):
|
|
8333
8424
|
apply_columns = _resolve_view_visible_field_names(patch)
|
|
@@ -8540,6 +8631,11 @@ class AiBuilderFacade:
|
|
|
8540
8631
|
if not existing_key and query_condition_payload_for_apply is None:
|
|
8541
8632
|
query_condition_payload_for_apply = _empty_view_query_conditions_payload()
|
|
8542
8633
|
expected_query_conditions_for_verify = _normalize_view_query_conditions_for_compare(query_condition_payload_for_apply)
|
|
8634
|
+
if not existing_key and associated_resources_payload_for_apply is None:
|
|
8635
|
+
associated_resources_payload_for_apply, _, _ = _build_view_associated_resources_payload(
|
|
8636
|
+
associated_resources={"visible": True, "limit_type": "all", "associated_item_ids": []},
|
|
8637
|
+
available_resources=associated_resources,
|
|
8638
|
+
)
|
|
8543
8639
|
try:
|
|
8544
8640
|
view_auth_override = (
|
|
8545
8641
|
self._compile_visibility_to_member_auth(profile=profile, visibility=patch.visibility)
|
|
@@ -9149,9 +9245,11 @@ class AiBuilderFacade:
|
|
|
9149
9245
|
verification_by_view.append(
|
|
9150
9246
|
{
|
|
9151
9247
|
"name": name,
|
|
9248
|
+
"view_key": item.get("view_key"),
|
|
9152
9249
|
"type": item.get("type"),
|
|
9153
9250
|
"status": "removed",
|
|
9154
|
-
"present_in_readback": None if verified_views_unavailable else name
|
|
9251
|
+
"present_in_readback": None if verified_views_unavailable else name in verified_names,
|
|
9252
|
+
"removed_verified": None if verified_views_unavailable else name not in verified_names,
|
|
9155
9253
|
}
|
|
9156
9254
|
)
|
|
9157
9255
|
else:
|
|
@@ -9172,7 +9270,7 @@ class AiBuilderFacade:
|
|
|
9172
9270
|
view_filters_verified = verified and not filter_readback_pending and not filter_mismatches
|
|
9173
9271
|
view_query_conditions_verified = verified and not query_condition_readback_pending and not query_condition_mismatches
|
|
9174
9272
|
view_associated_resources_verified = verified and not associated_resource_readback_pending and not associated_resource_mismatches
|
|
9175
|
-
view_buttons_verified = verified and not button_readback_pending and not button_mismatches
|
|
9273
|
+
view_buttons_verified = verified and not button_readback_pending and not button_mismatches and not custom_button_readback_pending
|
|
9176
9274
|
noop = not created and not updated and not removed
|
|
9177
9275
|
if failed_views:
|
|
9178
9276
|
successful_changes = bool(created or updated or removed)
|
|
@@ -9241,6 +9339,7 @@ class AiBuilderFacade:
|
|
|
9241
9339
|
"custom_button_readback_pending_entries": deepcopy(custom_button_readback_pending_entries),
|
|
9242
9340
|
},
|
|
9243
9341
|
"app_key": app_key,
|
|
9342
|
+
"app_name": app_name,
|
|
9244
9343
|
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": failed_views},
|
|
9245
9344
|
"verified": verified and view_filters_verified and view_query_conditions_verified and view_associated_resources_verified and view_buttons_verified,
|
|
9246
9345
|
}
|
|
@@ -9313,6 +9412,7 @@ class AiBuilderFacade:
|
|
|
9313
9412
|
"by_view": verification_by_view,
|
|
9314
9413
|
},
|
|
9315
9414
|
"app_key": app_key,
|
|
9415
|
+
"app_name": app_name,
|
|
9316
9416
|
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": []},
|
|
9317
9417
|
"verified": all_verified,
|
|
9318
9418
|
}
|
|
@@ -9338,6 +9438,7 @@ class AiBuilderFacade:
|
|
|
9338
9438
|
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
9339
9439
|
)
|
|
9340
9440
|
tag_ids_before = _coerce_int_list(base_before.get("tagIds"))
|
|
9441
|
+
app_name_before = str(base_before.get("formTitle") or base_before.get("title") or base_before.get("appName") or "").strip() or None
|
|
9341
9442
|
already_published = bool(base_before.get("appPublishStatus") in {1, 2})
|
|
9342
9443
|
package_already_attached = None if not expected_package_tag_id else expected_package_tag_id in tag_ids_before
|
|
9343
9444
|
try:
|
|
@@ -9368,6 +9469,7 @@ class AiBuilderFacade:
|
|
|
9368
9469
|
"warnings": [],
|
|
9369
9470
|
"verification": {"published": True, "package_attached": package_already_attached, "views_ok": True},
|
|
9370
9471
|
"app_key": app_key,
|
|
9472
|
+
"app_name": app_name_before,
|
|
9371
9473
|
"published": True,
|
|
9372
9474
|
"package_attached": package_already_attached,
|
|
9373
9475
|
"tag_ids_after": tag_ids_before,
|
|
@@ -9400,6 +9502,7 @@ class AiBuilderFacade:
|
|
|
9400
9502
|
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
|
|
9401
9503
|
)
|
|
9402
9504
|
tag_ids_after = _coerce_int_list(base.get("tagIds"))
|
|
9505
|
+
app_name_after = str(base.get("formTitle") or base.get("title") or base.get("appName") or app_name_before or "").strip() or None
|
|
9403
9506
|
package_attached = None if not expected_package_tag_id else expected_package_tag_id in tag_ids_after
|
|
9404
9507
|
try:
|
|
9405
9508
|
views, views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
@@ -9440,6 +9543,7 @@ class AiBuilderFacade:
|
|
|
9440
9543
|
"warnings": warnings,
|
|
9441
9544
|
"verification": {"published": bool(base.get("appPublishStatus") in {1, 2}), "package_attached": package_attached, "views_ok": views_ok, "views_read_unavailable": views_unavailable},
|
|
9442
9545
|
"app_key": app_key,
|
|
9546
|
+
"app_name": app_name_after,
|
|
9443
9547
|
"published": bool(base.get("appPublishStatus") in {1, 2}),
|
|
9444
9548
|
"package_attached": package_attached,
|
|
9445
9549
|
"tag_ids_after": tag_ids_after,
|
|
@@ -9577,6 +9681,7 @@ class AiBuilderFacade:
|
|
|
9577
9681
|
if resolved_outcome is not None:
|
|
9578
9682
|
permission_outcomes.append(resolved_outcome)
|
|
9579
9683
|
app_key = str(app_result.get("app_key") or request.app_key)
|
|
9684
|
+
app_name = str(app_result.get("app_name") or "").strip() or None
|
|
9580
9685
|
permission_outcome = self._guard_app_permission(
|
|
9581
9686
|
profile=profile,
|
|
9582
9687
|
app_key=app_key,
|
|
@@ -9657,6 +9762,13 @@ class AiBuilderFacade:
|
|
|
9657
9762
|
|
|
9658
9763
|
for patch in upsert_charts:
|
|
9659
9764
|
try:
|
|
9765
|
+
dataset_source = _chart_patch_dataset_source_type(patch)
|
|
9766
|
+
if dataset_source:
|
|
9767
|
+
raise ValueError(
|
|
9768
|
+
"app_charts_apply only creates or edits app-source QingBI reports with dataSourceType=qingflow; "
|
|
9769
|
+
f"dataset report source '{dataset_source}' is not supported for creation/update yet. "
|
|
9770
|
+
"Create the dataset report in QingBI first, then attach it with app_associated_resources_apply using report_source='dataset'."
|
|
9771
|
+
)
|
|
9660
9772
|
config_update_requested = _chart_patch_updates_chart_config(patch)
|
|
9661
9773
|
chart_visible_auth = (
|
|
9662
9774
|
self._compile_visibility_to_chart_visible_auth(profile=profile, visibility=patch.visibility)
|
|
@@ -9680,6 +9792,13 @@ class AiBuilderFacade:
|
|
|
9680
9792
|
existing_name = str((existing or {}).get("chartName") or "").strip()
|
|
9681
9793
|
existing_type = _normalize_backend_chart_type((existing or {}).get("chartType"))
|
|
9682
9794
|
target_type = _map_public_chart_type_to_backend(patch.chart_type)
|
|
9795
|
+
existing_source_type = _chart_item_dataset_source_type(existing or {})
|
|
9796
|
+
if existing_source_type:
|
|
9797
|
+
raise ValueError(
|
|
9798
|
+
"app_charts_apply only creates or edits app-source QingBI reports with dataSourceType=qingflow; "
|
|
9799
|
+
f"existing chart '{chart_id or patch.name}' uses dataset report source '{existing_source_type}' and is not supported for update yet. "
|
|
9800
|
+
"Update it in QingBI directly, then attach the existing report with app_associated_resources_apply using report_source='dataset'."
|
|
9801
|
+
)
|
|
9683
9802
|
if existing is None:
|
|
9684
9803
|
temp_chart_id = str(patch.chart_id or f"mcp_{uuid4().hex[:16]}")
|
|
9685
9804
|
create_payload = {
|
|
@@ -9908,6 +10027,7 @@ class AiBuilderFacade:
|
|
|
9908
10027
|
"chart_list_source": readback_list_source or existing_chart_list_source,
|
|
9909
10028
|
},
|
|
9910
10029
|
"app_key": app_key,
|
|
10030
|
+
"app_name": app_name,
|
|
9911
10031
|
"chart_results": chart_results,
|
|
9912
10032
|
"verified": False if failed_items else verified,
|
|
9913
10033
|
})
|
|
@@ -9936,6 +10056,7 @@ class AiBuilderFacade:
|
|
|
9936
10056
|
"chart_list_source": existing_chart_list_source if noop else readback_list_source,
|
|
9937
10057
|
},
|
|
9938
10058
|
"app_key": app_key,
|
|
10059
|
+
"app_name": app_name,
|
|
9939
10060
|
"chart_results": chart_results,
|
|
9940
10061
|
"verified": result_verified,
|
|
9941
10062
|
})
|
|
@@ -10221,6 +10342,9 @@ class AiBuilderFacade:
|
|
|
10221
10342
|
"publish_failed": publish_failed,
|
|
10222
10343
|
},
|
|
10223
10344
|
"dash_key": dash_key,
|
|
10345
|
+
"dash_name": update_payload.get("dashName"),
|
|
10346
|
+
"package_id": target_package_tag_id,
|
|
10347
|
+
"created": creating,
|
|
10224
10348
|
"published": published,
|
|
10225
10349
|
"verified": verified,
|
|
10226
10350
|
"draft_result": draft_result,
|
|
@@ -10313,6 +10437,49 @@ class AiBuilderFacade:
|
|
|
10313
10437
|
verification["published"] = False
|
|
10314
10438
|
return response
|
|
10315
10439
|
publish_result = self._publish_current_edit_version(profile=profile, app_key=app_key, edit_version_no=edit_version_no)
|
|
10440
|
+
if publish_result.get("status") == "failed" and publish_result.get("error_code") == "APP_EDIT_LOCKED":
|
|
10441
|
+
details = response.get("details")
|
|
10442
|
+
if not isinstance(details, dict):
|
|
10443
|
+
details = {}
|
|
10444
|
+
response["details"] = details
|
|
10445
|
+
suggested = publish_result.get("suggested_next_call")
|
|
10446
|
+
release_args = suggested.get("arguments") if isinstance(suggested, dict) else {}
|
|
10447
|
+
if isinstance(release_args, dict):
|
|
10448
|
+
release_result = self.app_release_edit_lock_if_mine(
|
|
10449
|
+
profile=profile,
|
|
10450
|
+
app_key=app_key,
|
|
10451
|
+
lock_owner_email=str(release_args.get("lock_owner_email") or ""),
|
|
10452
|
+
lock_owner_name=str(release_args.get("lock_owner_name") or ""),
|
|
10453
|
+
)
|
|
10454
|
+
details["edit_lock_release_result"] = release_result
|
|
10455
|
+
if release_result.get("status") == "success":
|
|
10456
|
+
retry_result = self._publish_current_edit_version(profile=profile, app_key=app_key, edit_version_no=None)
|
|
10457
|
+
details["publish_retry_after_edit_lock_release"] = retry_result
|
|
10458
|
+
publish_result = retry_result
|
|
10459
|
+
response["retried_after_edit_lock_release"] = True
|
|
10460
|
+
response["edit_lock_released"] = True
|
|
10461
|
+
elif release_result.get("error_code") == "EDIT_LOCK_OWNER_UNKNOWN" and edit_version_no is not None:
|
|
10462
|
+
try:
|
|
10463
|
+
self.apps.app_edit_finished(profile=profile, app_key=app_key, payload={"editVersionNo": edit_version_no})
|
|
10464
|
+
release_result = {
|
|
10465
|
+
"status": "success",
|
|
10466
|
+
"error_code": None,
|
|
10467
|
+
"recoverable": False,
|
|
10468
|
+
"message": "released current apply edit context before publish retry",
|
|
10469
|
+
"details": {"app_key": app_key, "edit_version_no": edit_version_no, "release_strategy": "current_apply_edit_context"},
|
|
10470
|
+
"verification": {"released": True},
|
|
10471
|
+
"app_key": app_key,
|
|
10472
|
+
"verified": True,
|
|
10473
|
+
}
|
|
10474
|
+
details["edit_lock_release_result"] = release_result
|
|
10475
|
+
retry_result = self._publish_current_edit_version(profile=profile, app_key=app_key, edit_version_no=None)
|
|
10476
|
+
details["publish_retry_after_edit_lock_release"] = retry_result
|
|
10477
|
+
publish_result = retry_result
|
|
10478
|
+
response["retried_after_edit_lock_release"] = True
|
|
10479
|
+
response["edit_lock_released"] = True
|
|
10480
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
10481
|
+
api_error = _coerce_api_error(error)
|
|
10482
|
+
details["current_apply_edit_context_release_error"] = _transport_error_payload(api_error)
|
|
10316
10483
|
response["publish_result"] = publish_result
|
|
10317
10484
|
response["published"] = bool(publish_result.get("published"))
|
|
10318
10485
|
verification["published"] = bool(publish_result.get("published"))
|
|
@@ -10337,6 +10504,14 @@ class AiBuilderFacade:
|
|
|
10337
10504
|
"schema_source": schema_source,
|
|
10338
10505
|
}
|
|
10339
10506
|
|
|
10507
|
+
def _read_app_name_for_builder_output(self, *, profile: str, app_key: str) -> str | None:
|
|
10508
|
+
try:
|
|
10509
|
+
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
10510
|
+
except (QingflowApiError, RuntimeError):
|
|
10511
|
+
return None
|
|
10512
|
+
app_name = str(base.get("formTitle") or base.get("title") or base.get("appName") or "").strip()
|
|
10513
|
+
return app_name or None
|
|
10514
|
+
|
|
10340
10515
|
def _sync_system_view_apply_config(
|
|
10341
10516
|
self,
|
|
10342
10517
|
*,
|
|
@@ -12192,6 +12367,8 @@ def _extract_edit_lock_owner(message: str) -> JSONObject:
|
|
|
12192
12367
|
patterns = [
|
|
12193
12368
|
r"应用已被\s*(?P<name>[^((]+?)\s*[((](?P<email>[^))]+)[))]\s*编辑",
|
|
12194
12369
|
r"edited by\s*(?P<name>[^<(]+?)\s*<(?P<email>[^>]+)>",
|
|
12370
|
+
r"active editor\s+(?P<email>[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,})",
|
|
12371
|
+
r"(?P<email>[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,})",
|
|
12195
12372
|
]
|
|
12196
12373
|
for pattern in patterns:
|
|
12197
12374
|
match = re.search(pattern, text)
|
|
@@ -12337,13 +12514,16 @@ def _bi_field_id_for_field(*, app_key: str, field: dict[str, Any], qingbi_fields
|
|
|
12337
12514
|
|
|
12338
12515
|
|
|
12339
12516
|
def _map_public_chart_type_to_backend(chart_type: PublicChartType) -> str:
|
|
12340
|
-
|
|
12517
|
+
aliases = {
|
|
12341
12518
|
PublicChartType.target: "indicator",
|
|
12519
|
+
PublicChartType.indicator: "indicator",
|
|
12342
12520
|
PublicChartType.pie: "pie",
|
|
12343
12521
|
PublicChartType.bar: "bar",
|
|
12344
12522
|
PublicChartType.line: "line",
|
|
12345
12523
|
PublicChartType.table: "detail",
|
|
12346
|
-
|
|
12524
|
+
PublicChartType.detail: "detail",
|
|
12525
|
+
}
|
|
12526
|
+
return aliases.get(chart_type, chart_type.value)
|
|
12347
12527
|
|
|
12348
12528
|
|
|
12349
12529
|
_CHART_PARTIAL_PATCH_KEY_ALIASES = {
|
|
@@ -12579,6 +12759,25 @@ def _build_public_metric_fields(
|
|
|
12579
12759
|
return metrics or [_default_public_total_metric()]
|
|
12580
12760
|
|
|
12581
12761
|
|
|
12762
|
+
def _split_axis_metric_fields(metrics: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
12763
|
+
if not metrics:
|
|
12764
|
+
default_metric = _default_public_total_metric()
|
|
12765
|
+
return [deepcopy(default_metric)], [deepcopy(default_metric)]
|
|
12766
|
+
if len(metrics) == 1:
|
|
12767
|
+
return [deepcopy(metrics[0])], [deepcopy(metrics[0])]
|
|
12768
|
+
return [deepcopy(metrics[0])], [deepcopy(metrics[1])]
|
|
12769
|
+
|
|
12770
|
+
|
|
12771
|
+
def _two_gauge_metric_fields(metrics: list[dict[str, Any]], *, requested_metric_count: int) -> list[dict[str, Any]]:
|
|
12772
|
+
if not metrics:
|
|
12773
|
+
return []
|
|
12774
|
+
if len(metrics) == 1:
|
|
12775
|
+
if requested_metric_count <= 0:
|
|
12776
|
+
return [deepcopy(metrics[0])]
|
|
12777
|
+
return [deepcopy(metrics[0]), _default_public_total_metric()]
|
|
12778
|
+
return [deepcopy(metrics[0]), deepcopy(metrics[1])]
|
|
12779
|
+
|
|
12780
|
+
|
|
12582
12781
|
def _build_public_chart_filter_matrix(
|
|
12583
12782
|
rules: list[Any],
|
|
12584
12783
|
*,
|
|
@@ -12647,23 +12846,28 @@ def _build_public_chart_config_payload(
|
|
|
12647
12846
|
for selector in list(config.pop("query_condition_field_ids", []) or []):
|
|
12648
12847
|
field = _resolve_public_field(selector, field_lookup=field_lookup)
|
|
12649
12848
|
query_condition_field_ids.append(_bi_field_id_for_field(app_key=app_key, field=field, qingbi_fields_by_id=qingbi_fields_by_id))
|
|
12849
|
+
backend_chart_type = _map_public_chart_type_to_backend(patch.chart_type)
|
|
12850
|
+
if backend_chart_type == "gauge" and not patch.indicator_field_ids and "selectedMetrics" not in config:
|
|
12851
|
+
raise ValueError("gauge charts require at least one indicator_field_ids value; pass one metric and the CLI will pair it with 数据总量")
|
|
12852
|
+
selected_dimensions = _build_public_dimension_fields(
|
|
12853
|
+
patch.dimension_field_ids,
|
|
12854
|
+
app_key=app_key,
|
|
12855
|
+
field_lookup=field_lookup,
|
|
12856
|
+
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
12857
|
+
)
|
|
12858
|
+
selected_metrics = _build_public_metric_fields(
|
|
12859
|
+
patch.indicator_field_ids,
|
|
12860
|
+
app_key=app_key,
|
|
12861
|
+
field_lookup=field_lookup,
|
|
12862
|
+
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
12863
|
+
aggregate=aggregate,
|
|
12864
|
+
)
|
|
12650
12865
|
payload: dict[str, Any] = {
|
|
12651
12866
|
"chartName": patch.name,
|
|
12652
|
-
"chartType":
|
|
12867
|
+
"chartType": backend_chart_type,
|
|
12653
12868
|
"dataSource": {"dataSourceId": app_key, "dataSourceType": "qingflow"},
|
|
12654
|
-
"selectedDimensions":
|
|
12655
|
-
|
|
12656
|
-
app_key=app_key,
|
|
12657
|
-
field_lookup=field_lookup,
|
|
12658
|
-
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
12659
|
-
),
|
|
12660
|
-
"selectedMetrics": _build_public_metric_fields(
|
|
12661
|
-
patch.indicator_field_ids,
|
|
12662
|
-
app_key=app_key,
|
|
12663
|
-
field_lookup=field_lookup,
|
|
12664
|
-
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
12665
|
-
aggregate=aggregate,
|
|
12666
|
-
),
|
|
12869
|
+
"selectedDimensions": selected_dimensions,
|
|
12870
|
+
"selectedMetrics": selected_metrics,
|
|
12667
12871
|
"beforeAggregationFilterMatrix": before_filters,
|
|
12668
12872
|
"afterAggregationFilterMatrix": after_filters,
|
|
12669
12873
|
"chartStyleConfigs": deepcopy(config.pop("chartStyleConfigs", [])),
|
|
@@ -12679,6 +12883,23 @@ def _build_public_chart_config_payload(
|
|
|
12679
12883
|
"queryConditionStatus": bool(config.pop("queryConditionStatus", bool(query_condition_field_ids))),
|
|
12680
12884
|
"queryConditionExact": bool(config.pop("queryConditionExact", False)),
|
|
12681
12885
|
}
|
|
12886
|
+
if backend_chart_type == "summary":
|
|
12887
|
+
payload.pop("selectedDimensions", None)
|
|
12888
|
+
payload.setdefault("xDimensions", deepcopy(selected_dimensions))
|
|
12889
|
+
payload.setdefault("yDimensions", [])
|
|
12890
|
+
elif backend_chart_type == "scatter":
|
|
12891
|
+
x_metrics, y_metrics = _split_axis_metric_fields(selected_metrics)
|
|
12892
|
+
payload.pop("selectedMetrics", None)
|
|
12893
|
+
payload.setdefault("xMetrics", x_metrics)
|
|
12894
|
+
payload.setdefault("yMetrics", y_metrics)
|
|
12895
|
+
elif backend_chart_type == "dualaxes":
|
|
12896
|
+
left_metrics, right_metrics = _split_axis_metric_fields(selected_metrics)
|
|
12897
|
+
payload.pop("selectedMetrics", None)
|
|
12898
|
+
payload.setdefault("leftMetrics", left_metrics)
|
|
12899
|
+
payload.setdefault("rightMetrics", right_metrics)
|
|
12900
|
+
elif backend_chart_type == "gauge":
|
|
12901
|
+
payload["selectedDimensions"] = []
|
|
12902
|
+
payload["selectedMetrics"] = _two_gauge_metric_fields(selected_metrics, requested_metric_count=len(patch.indicator_field_ids or []))
|
|
12682
12903
|
for key in (
|
|
12683
12904
|
"selectedTime",
|
|
12684
12905
|
"xDimensions",
|
|
@@ -12701,6 +12922,46 @@ def _chart_patch_updates_chart_config(patch: ChartUpsertPatch) -> bool:
|
|
|
12701
12922
|
return bool({"dimension_field_ids", "indicator_field_ids", "filters", "config"} & explicit_fields)
|
|
12702
12923
|
|
|
12703
12924
|
|
|
12925
|
+
def _chart_patch_dataset_source_type(patch: ChartUpsertPatch) -> str:
|
|
12926
|
+
config = patch.config if isinstance(patch.config, dict) else {}
|
|
12927
|
+
candidates = [
|
|
12928
|
+
config.get("dataSourceType"),
|
|
12929
|
+
config.get("data_source_type"),
|
|
12930
|
+
]
|
|
12931
|
+
data_source = config.get("dataSource")
|
|
12932
|
+
if isinstance(data_source, dict):
|
|
12933
|
+
candidates.extend([data_source.get("dataSourceType"), data_source.get("data_source_type"), data_source.get("type")])
|
|
12934
|
+
data_source = config.get("data_source")
|
|
12935
|
+
if isinstance(data_source, dict):
|
|
12936
|
+
candidates.extend([data_source.get("dataSourceType"), data_source.get("data_source_type"), data_source.get("type")])
|
|
12937
|
+
for candidate in candidates:
|
|
12938
|
+
normalized = str(candidate or "").strip().lower()
|
|
12939
|
+
if not normalized:
|
|
12940
|
+
continue
|
|
12941
|
+
if normalized not in {"qingflow", "app"}:
|
|
12942
|
+
return normalized
|
|
12943
|
+
return ""
|
|
12944
|
+
|
|
12945
|
+
|
|
12946
|
+
def _chart_item_dataset_source_type(item: dict[str, Any]) -> str:
|
|
12947
|
+
candidates = [
|
|
12948
|
+
item.get("dataSourceType"),
|
|
12949
|
+
item.get("data_source_type"),
|
|
12950
|
+
item.get("sourceType"),
|
|
12951
|
+
item.get("source_type"),
|
|
12952
|
+
]
|
|
12953
|
+
data_source = item.get("dataSource")
|
|
12954
|
+
if isinstance(data_source, dict):
|
|
12955
|
+
candidates.extend([data_source.get("dataSourceType"), data_source.get("data_source_type"), data_source.get("type")])
|
|
12956
|
+
for candidate in candidates:
|
|
12957
|
+
normalized = str(candidate or "").strip().lower()
|
|
12958
|
+
if not normalized:
|
|
12959
|
+
continue
|
|
12960
|
+
if normalized not in {"qingflow", "app", "bi_qingflow"}:
|
|
12961
|
+
return normalized
|
|
12962
|
+
return ""
|
|
12963
|
+
|
|
12964
|
+
|
|
12704
12965
|
def _build_public_portal_base_payload(
|
|
12705
12966
|
*,
|
|
12706
12967
|
dash_name: str,
|
|
@@ -12771,16 +13032,33 @@ def _normalize_backend_chart_type(value: Any) -> str:
|
|
|
12771
13032
|
"12": "dualaxes",
|
|
12772
13033
|
"13": "map",
|
|
12773
13034
|
"14": "timeline",
|
|
13035
|
+
"15": "area",
|
|
13036
|
+
"16": "stacked_column",
|
|
13037
|
+
"17": "stacked_bar",
|
|
13038
|
+
"18": "rose",
|
|
13039
|
+
"19": "stacked_area",
|
|
13040
|
+
"20": "pct_stack_col",
|
|
13041
|
+
"21": "pct_stack_bar",
|
|
13042
|
+
"22": "pct_stack_area",
|
|
13043
|
+
"23": "waterfall",
|
|
13044
|
+
"24": "gauge",
|
|
13045
|
+
"25": "heatmap",
|
|
13046
|
+
"26": "histogram",
|
|
13047
|
+
"27": "treemap",
|
|
12774
13048
|
}
|
|
12775
|
-
|
|
13049
|
+
aliases = {
|
|
13050
|
+
"percent_stacked_column": "pct_stack_col",
|
|
13051
|
+
"percent_stacked_bar": "pct_stack_bar",
|
|
13052
|
+
"percent_stacked_area": "pct_stack_area",
|
|
13053
|
+
}
|
|
13054
|
+
normalized = by_code.get(raw, raw.lower())
|
|
13055
|
+
return aliases.get(normalized, normalized)
|
|
12776
13056
|
|
|
12777
13057
|
|
|
12778
13058
|
def _public_chart_type_from_backend(value: Any) -> str:
|
|
12779
13059
|
normalized = _normalize_backend_chart_type(value)
|
|
12780
13060
|
return {
|
|
12781
13061
|
"indicator": PublicChartType.target.value,
|
|
12782
|
-
"summary": PublicChartType.target.value,
|
|
12783
|
-
"columnar": PublicChartType.bar.value,
|
|
12784
13062
|
"detail": PublicChartType.table.value,
|
|
12785
13063
|
}.get(normalized, normalized)
|
|
12786
13064
|
|
|
@@ -14067,6 +14345,38 @@ def _parse_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
|
|
14067
14345
|
return parsed
|
|
14068
14346
|
|
|
14069
14347
|
|
|
14348
|
+
def _schema_field_identity(field: dict[str, Any] | None, *, fallback_name: str | None = None) -> dict[str, Any]:
|
|
14349
|
+
field = field or {}
|
|
14350
|
+
name = str(field.get("name") or fallback_name or "").strip() or None
|
|
14351
|
+
field_id = str(field.get("field_id") or "").strip() or None
|
|
14352
|
+
que_id = _coerce_positive_int(field.get("que_id") or field.get("queId"))
|
|
14353
|
+
return {
|
|
14354
|
+
"name": name,
|
|
14355
|
+
"field_id": field_id,
|
|
14356
|
+
"que_id": que_id,
|
|
14357
|
+
}
|
|
14358
|
+
|
|
14359
|
+
|
|
14360
|
+
def _schema_field_diff_details(
|
|
14361
|
+
*,
|
|
14362
|
+
added: list[str],
|
|
14363
|
+
updated: list[str],
|
|
14364
|
+
removed: list[str],
|
|
14365
|
+
before_fields: list[dict[str, Any]],
|
|
14366
|
+
after_fields: list[dict[str, Any]],
|
|
14367
|
+
) -> dict[str, list[dict[str, Any]]]:
|
|
14368
|
+
before_by_name = {str(field.get("name") or ""): field for field in before_fields if str(field.get("name") or "").strip()}
|
|
14369
|
+
after_by_name = {str(field.get("name") or ""): field for field in after_fields if str(field.get("name") or "").strip()}
|
|
14370
|
+
return {
|
|
14371
|
+
"added": [_schema_field_identity(after_by_name.get(name), fallback_name=name) for name in added],
|
|
14372
|
+
"updated": [
|
|
14373
|
+
_schema_field_identity(after_by_name.get(name) or before_by_name.get(name), fallback_name=name)
|
|
14374
|
+
for name in updated
|
|
14375
|
+
],
|
|
14376
|
+
"removed": [_schema_field_identity(before_by_name.get(name), fallback_name=name) for name in removed],
|
|
14377
|
+
}
|
|
14378
|
+
|
|
14379
|
+
|
|
14070
14380
|
def _resolve_layout_sections_to_names(
|
|
14071
14381
|
requested_sections: list[dict[str, Any]],
|
|
14072
14382
|
fields: list[dict[str, Any]],
|
|
@@ -19234,7 +19544,7 @@ def _compare_view_button_summaries(
|
|
|
19234
19544
|
expected_without_pending = [item for index, item in enumerate(expected) if index not in pending_indexes]
|
|
19235
19545
|
if _sorted_view_button_compare_signatures(actual) == _sorted_view_button_compare_signatures(expected_without_pending):
|
|
19236
19546
|
return {
|
|
19237
|
-
"verified":
|
|
19547
|
+
"verified": False,
|
|
19238
19548
|
"custom_button_readback_pending": True,
|
|
19239
19549
|
"pending_custom_buttons": deepcopy(pending_custom_buttons),
|
|
19240
19550
|
}
|
|
@@ -19245,7 +19555,7 @@ def _compare_view_button_summaries(
|
|
|
19245
19555
|
and _sorted_view_button_compare_signatures(actual_other) == _sorted_view_button_compare_signatures(expected_other)
|
|
19246
19556
|
)
|
|
19247
19557
|
return {
|
|
19248
|
-
"verified":
|
|
19558
|
+
"verified": False,
|
|
19249
19559
|
"custom_button_readback_pending": custom_button_readback_pending,
|
|
19250
19560
|
"pending_custom_buttons": deepcopy(expected_custom) if custom_button_readback_pending else [],
|
|
19251
19561
|
}
|
|
@@ -20129,7 +20439,7 @@ def _build_view_associated_resources_payload(
|
|
|
20129
20439
|
"missing_fields": [],
|
|
20130
20440
|
"received": raw_item_id,
|
|
20131
20441
|
"message": "associated_item_id must be an app-level associated resource id from app_get.associated_resources",
|
|
20132
|
-
"next_action": "
|
|
20442
|
+
"next_action": "prefer app_associated_resources_apply for associated report/view display; it can resolve associated_item_id, chart_id/chart_key, or view_key",
|
|
20133
20443
|
}
|
|
20134
20444
|
]
|
|
20135
20445
|
item_ids.append(item_id)
|
|
@@ -20143,7 +20453,7 @@ def _build_view_associated_resources_payload(
|
|
|
20143
20453
|
"invalid_associated_item_ids": missing_ids,
|
|
20144
20454
|
"available_associated_item_ids": sorted(available_by_id),
|
|
20145
20455
|
"message": "associated_resource references ids that are not in the app-level associated resource pool",
|
|
20146
|
-
"next_action": "
|
|
20456
|
+
"next_action": "prefer app_associated_resources_apply for associated report/view display; it can resolve associated_item_id, chart_id/chart_key, or view_key",
|
|
20147
20457
|
}
|
|
20148
20458
|
]
|
|
20149
20459
|
payload = {
|
|
@@ -20561,6 +20871,77 @@ def _associated_resource_upsert_payload_from_existing_item(
|
|
|
20561
20871
|
return _compact_dict(payload)
|
|
20562
20872
|
|
|
20563
20873
|
|
|
20874
|
+
def _resolve_associated_resource_selector(
|
|
20875
|
+
selector: Any,
|
|
20876
|
+
*,
|
|
20877
|
+
existing_resources: list[dict[str, Any]],
|
|
20878
|
+
existing_by_id: dict[int, dict[str, Any]],
|
|
20879
|
+
reason_path: str,
|
|
20880
|
+
) -> tuple[int | None, dict[str, Any] | None]:
|
|
20881
|
+
item_id = _coerce_positive_int(selector)
|
|
20882
|
+
if item_id is not None:
|
|
20883
|
+
if item_id in existing_by_id:
|
|
20884
|
+
return item_id, None
|
|
20885
|
+
raw = str(selector or "").strip()
|
|
20886
|
+
if not raw:
|
|
20887
|
+
return None, {
|
|
20888
|
+
"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND",
|
|
20889
|
+
"reason_path": reason_path,
|
|
20890
|
+
"received": selector,
|
|
20891
|
+
"available_associated_item_ids": sorted(existing_by_id),
|
|
20892
|
+
"message": "associated resource selector cannot be empty",
|
|
20893
|
+
}
|
|
20894
|
+
matches: list[dict[str, Any]] = []
|
|
20895
|
+
for resource in existing_resources:
|
|
20896
|
+
if not isinstance(resource, dict):
|
|
20897
|
+
continue
|
|
20898
|
+
candidates = [
|
|
20899
|
+
resource.get("chart_id"),
|
|
20900
|
+
resource.get("chart_key"),
|
|
20901
|
+
resource.get("view_key"),
|
|
20902
|
+
resource.get("associated_item_id"),
|
|
20903
|
+
]
|
|
20904
|
+
if raw in {str(candidate).strip() for candidate in candidates if candidate not in {None, ""}}:
|
|
20905
|
+
matches.append(resource)
|
|
20906
|
+
if len(matches) == 1:
|
|
20907
|
+
resolved_id = _coerce_positive_int(matches[0].get("associated_item_id"))
|
|
20908
|
+
if resolved_id is not None:
|
|
20909
|
+
return resolved_id, None
|
|
20910
|
+
if len(matches) > 1:
|
|
20911
|
+
return None, {
|
|
20912
|
+
"error_code": "AMBIGUOUS_ASSOCIATED_RESOURCE",
|
|
20913
|
+
"reason_path": reason_path,
|
|
20914
|
+
"received": selector,
|
|
20915
|
+
"candidate_associated_item_ids": [
|
|
20916
|
+
item_id
|
|
20917
|
+
for item_id in (_coerce_positive_int(item.get("associated_item_id")) for item in matches)
|
|
20918
|
+
if item_id is not None
|
|
20919
|
+
],
|
|
20920
|
+
"message": "selector matches multiple associated resources; pass associated_item_id",
|
|
20921
|
+
}
|
|
20922
|
+
return None, {
|
|
20923
|
+
"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND",
|
|
20924
|
+
"reason_path": reason_path,
|
|
20925
|
+
"received": selector,
|
|
20926
|
+
"available_associated_item_ids": sorted(existing_by_id),
|
|
20927
|
+
"available_chart_ids": sorted(
|
|
20928
|
+
{
|
|
20929
|
+
str(resource.get("chart_id") or resource.get("chart_key") or "").strip()
|
|
20930
|
+
for resource in existing_resources
|
|
20931
|
+
if str(resource.get("chart_id") or resource.get("chart_key") or "").strip()
|
|
20932
|
+
}
|
|
20933
|
+
),
|
|
20934
|
+
"available_view_keys": sorted(
|
|
20935
|
+
{
|
|
20936
|
+
str(resource.get("view_key") or "").strip()
|
|
20937
|
+
for resource in existing_resources
|
|
20938
|
+
if str(resource.get("view_key") or "").strip()
|
|
20939
|
+
}
|
|
20940
|
+
),
|
|
20941
|
+
"message": "selector must be an associated_item_id, QingBI chart_id/chart_key, or Qingflow view_key from the app-level associated resource pool",
|
|
20942
|
+
}
|
|
20943
|
+
|
|
20944
|
+
|
|
20564
20945
|
def _associated_resource_not_found_issue(reason_path: str, associated_item_id: int, existing_by_id: dict[int, dict[str, Any]]) -> dict[str, Any]:
|
|
20565
20946
|
return {
|
|
20566
20947
|
"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND",
|
|
@@ -20568,7 +20949,7 @@ def _associated_resource_not_found_issue(reason_path: str, associated_item_id: i
|
|
|
20568
20949
|
"associated_item_id": associated_item_id,
|
|
20569
20950
|
"available_associated_item_ids": sorted(existing_by_id),
|
|
20570
20951
|
"message": "associated_item_id is not in the app-level associated resource pool",
|
|
20571
|
-
"next_action": "call app_get and use associated_resources[].associated_item_id
|
|
20952
|
+
"next_action": "call app_get and use associated_resources[].associated_item_id, chart_id/chart_key, or view_key",
|
|
20572
20953
|
}
|
|
20573
20954
|
|
|
20574
20955
|
|