@qingflow-tech/qingflow-app-builder-mcp 1.0.9 → 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-app-builder/SKILL.md +8 -0
- package/skills/qingflow-app-builder/references/create-app.md +39 -2
- package/skills/qingflow-app-builder/references/tool-selection.md +2 -2
- package/skills/qingflow-app-builder/references/update-views.md +2 -1
- package/src/qingflow_mcp/builder_facade/models.py +2 -0
- package/src/qingflow_mcp/builder_facade/service.py +223 -39
- 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 +27 -2
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1189 -29
- package/src/qingflow_mcp/tools/record_tools.py +200 -6
|
@@ -70,6 +70,19 @@ def _normalize_builder_view_key(value: str) -> str:
|
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
PUBLIC_STABLE_FLOW_NODE_TYPES = ["start", "approve", "fill", "copy", "webhook", "end"]
|
|
73
|
+
BUILDER_APPLY_SCHEMA_VERSION = "builder.apply.v1"
|
|
74
|
+
BUILDER_APPLY_TOOL_NAMES = {
|
|
75
|
+
"package_apply",
|
|
76
|
+
"app_schema_apply",
|
|
77
|
+
"app_layout_apply",
|
|
78
|
+
"app_flow_apply",
|
|
79
|
+
"app_views_apply",
|
|
80
|
+
"app_custom_buttons_apply",
|
|
81
|
+
"app_associated_resources_apply",
|
|
82
|
+
"app_charts_apply",
|
|
83
|
+
"portal_apply",
|
|
84
|
+
"app_publish_verify",
|
|
85
|
+
}
|
|
73
86
|
|
|
74
87
|
|
|
75
88
|
class AiBuilderTools(ToolBase):
|
|
@@ -340,7 +353,32 @@ class AiBuilderTools(ToolBase):
|
|
|
340
353
|
add_fields: list[JSONObject] | None = None,
|
|
341
354
|
update_fields: list[JSONObject] | None = None,
|
|
342
355
|
remove_fields: list[JSONObject] | None = None,
|
|
356
|
+
apps: list[JSONObject] | None = None,
|
|
343
357
|
) -> JSONObject:
|
|
358
|
+
if apps:
|
|
359
|
+
if app_key or app_name or app_title or add_fields or update_fields or remove_fields:
|
|
360
|
+
return _config_failure(
|
|
361
|
+
tool_name="app_schema_apply",
|
|
362
|
+
message="app_schema_apply multi-app mode accepts package_id/create_if_missing plus apps only.",
|
|
363
|
+
fix_hint="Use `apps` for batch mode, or use the single-app arguments without `apps`.",
|
|
364
|
+
)
|
|
365
|
+
if package_id is None:
|
|
366
|
+
return _config_failure(
|
|
367
|
+
tool_name="app_schema_apply",
|
|
368
|
+
message="app_schema_apply multi-app mode requires package_id.",
|
|
369
|
+
fix_hint="Pass package_id and apps[].app_name for new apps, or apps[].app_key for existing apps.",
|
|
370
|
+
)
|
|
371
|
+
return self.app_schema_apply(
|
|
372
|
+
profile=profile,
|
|
373
|
+
package_id=package_id,
|
|
374
|
+
visibility=visibility,
|
|
375
|
+
create_if_missing=create_if_missing,
|
|
376
|
+
publish=publish,
|
|
377
|
+
apps=apps,
|
|
378
|
+
add_fields=[],
|
|
379
|
+
update_fields=[],
|
|
380
|
+
remove_fields=[],
|
|
381
|
+
)
|
|
344
382
|
has_app_key = bool((app_key or "").strip())
|
|
345
383
|
has_app_name = bool((app_name or "").strip())
|
|
346
384
|
has_app_title = bool((app_title or "").strip())
|
|
@@ -372,6 +410,7 @@ class AiBuilderTools(ToolBase):
|
|
|
372
410
|
add_fields=add_fields or [],
|
|
373
411
|
update_fields=update_fields or [],
|
|
374
412
|
remove_fields=remove_fields or [],
|
|
413
|
+
apps=[],
|
|
375
414
|
)
|
|
376
415
|
|
|
377
416
|
@mcp.tool()
|
|
@@ -550,6 +589,7 @@ class AiBuilderTools(ToolBase):
|
|
|
550
589
|
"verification": {},
|
|
551
590
|
"verified": False,
|
|
552
591
|
}
|
|
592
|
+
contract = _builder_contract_with_apply_output(lookup_name, contract)
|
|
553
593
|
return {
|
|
554
594
|
"status": "success",
|
|
555
595
|
"error_code": None,
|
|
@@ -647,7 +687,10 @@ class AiBuilderTools(ToolBase):
|
|
|
647
687
|
try:
|
|
648
688
|
visibility_patch = VisibilityPatch.model_validate(visibility)
|
|
649
689
|
except ValidationError as exc:
|
|
650
|
-
return
|
|
690
|
+
return _attach_builder_apply_envelope(
|
|
691
|
+
"package_apply",
|
|
692
|
+
_visibility_validation_failure(str(exc), tool_name="package_apply", exc=exc),
|
|
693
|
+
)
|
|
651
694
|
normalized_args = {
|
|
652
695
|
"package_id": package_id,
|
|
653
696
|
**({"package_name": package_name} if str(package_name or "").strip() else {}),
|
|
@@ -658,7 +701,7 @@ class AiBuilderTools(ToolBase):
|
|
|
658
701
|
**({"items": deepcopy(items)} if items is not None else {}),
|
|
659
702
|
"allow_detach": bool(allow_detach),
|
|
660
703
|
}
|
|
661
|
-
|
|
704
|
+
result = _publicize_package_fields(_safe_tool_call(
|
|
662
705
|
lambda: self._facade.package_apply(
|
|
663
706
|
profile=profile,
|
|
664
707
|
package_id=package_id,
|
|
@@ -674,6 +717,7 @@ class AiBuilderTools(ToolBase):
|
|
|
674
717
|
normalized_args=normalized_args,
|
|
675
718
|
suggested_next_call={"tool_name": "package_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
676
719
|
))
|
|
720
|
+
return _attach_builder_apply_envelope("package_apply", result)
|
|
677
721
|
|
|
678
722
|
@tool_cn_name("分组更新")
|
|
679
723
|
def package_update(
|
|
@@ -914,7 +958,7 @@ class AiBuilderTools(ToolBase):
|
|
|
914
958
|
try:
|
|
915
959
|
request = CustomButtonsApplyRequest.model_validate(raw_request)
|
|
916
960
|
except ValidationError as exc:
|
|
917
|
-
return _validation_failure(
|
|
961
|
+
return _attach_builder_apply_envelope("app_custom_buttons_apply", _validation_failure(
|
|
918
962
|
str(exc),
|
|
919
963
|
tool_name="app_custom_buttons_apply",
|
|
920
964
|
exc=exc,
|
|
@@ -936,14 +980,14 @@ class AiBuilderTools(ToolBase):
|
|
|
936
980
|
"view_configs": [],
|
|
937
981
|
},
|
|
938
982
|
},
|
|
939
|
-
)
|
|
983
|
+
))
|
|
940
984
|
normalized_args = request.model_dump(mode="json")
|
|
941
|
-
return _safe_tool_call(
|
|
985
|
+
return _attach_builder_apply_envelope("app_custom_buttons_apply", _safe_tool_call(
|
|
942
986
|
lambda: self._facade.app_custom_buttons_apply(profile=profile, request=request),
|
|
943
987
|
error_code="CUSTOM_BUTTONS_APPLY_FAILED",
|
|
944
988
|
normalized_args=normalized_args,
|
|
945
989
|
suggested_next_call={"tool_name": "app_custom_buttons_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
946
|
-
)
|
|
990
|
+
))
|
|
947
991
|
|
|
948
992
|
@tool_cn_name("应用关联资源声明式应用")
|
|
949
993
|
def app_associated_resources_apply(
|
|
@@ -969,7 +1013,7 @@ class AiBuilderTools(ToolBase):
|
|
|
969
1013
|
try:
|
|
970
1014
|
request = AssociatedResourcesApplyRequest.model_validate(raw_request)
|
|
971
1015
|
except ValidationError as exc:
|
|
972
|
-
return _validation_failure(
|
|
1016
|
+
return _attach_builder_apply_envelope("app_associated_resources_apply", _validation_failure(
|
|
973
1017
|
str(exc),
|
|
974
1018
|
tool_name="app_associated_resources_apply",
|
|
975
1019
|
exc=exc,
|
|
@@ -996,14 +1040,14 @@ class AiBuilderTools(ToolBase):
|
|
|
996
1040
|
],
|
|
997
1041
|
},
|
|
998
1042
|
},
|
|
999
|
-
)
|
|
1043
|
+
))
|
|
1000
1044
|
normalized_args = request.model_dump(mode="json", exclude_none=True)
|
|
1001
|
-
return _safe_tool_call(
|
|
1045
|
+
return _attach_builder_apply_envelope("app_associated_resources_apply", _safe_tool_call(
|
|
1002
1046
|
lambda: self._facade.app_associated_resources_apply(profile=profile, request=request),
|
|
1003
1047
|
error_code="ASSOCIATED_RESOURCES_APPLY_FAILED",
|
|
1004
1048
|
normalized_args=normalized_args,
|
|
1005
1049
|
suggested_next_call={"tool_name": "app_associated_resources_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
1006
|
-
)
|
|
1050
|
+
))
|
|
1007
1051
|
|
|
1008
1052
|
@tool_cn_name("应用按钮列表")
|
|
1009
1053
|
def app_custom_button_list(self, *, profile: str, app_key: str) -> JSONObject:
|
|
@@ -1534,8 +1578,19 @@ class AiBuilderTools(ToolBase):
|
|
|
1534
1578
|
add_fields: list[JSONObject],
|
|
1535
1579
|
update_fields: list[JSONObject],
|
|
1536
1580
|
remove_fields: list[JSONObject],
|
|
1581
|
+
apps: list[JSONObject] | None = None,
|
|
1537
1582
|
) -> JSONObject:
|
|
1538
1583
|
"""执行应用相关逻辑。"""
|
|
1584
|
+
if apps:
|
|
1585
|
+
result = self._app_schema_apply_multi(
|
|
1586
|
+
profile=profile,
|
|
1587
|
+
package_id=package_id,
|
|
1588
|
+
visibility=visibility,
|
|
1589
|
+
create_if_missing=create_if_missing,
|
|
1590
|
+
publish=publish,
|
|
1591
|
+
apps=apps,
|
|
1592
|
+
)
|
|
1593
|
+
return _attach_builder_apply_envelope("app_schema_apply", result)
|
|
1539
1594
|
result = self._app_schema_apply_once(
|
|
1540
1595
|
profile=profile,
|
|
1541
1596
|
app_key=app_key,
|
|
@@ -1551,7 +1606,7 @@ class AiBuilderTools(ToolBase):
|
|
|
1551
1606
|
update_fields=update_fields,
|
|
1552
1607
|
remove_fields=remove_fields,
|
|
1553
1608
|
)
|
|
1554
|
-
|
|
1609
|
+
result = self._retry_after_self_lock_release(
|
|
1555
1610
|
profile=profile,
|
|
1556
1611
|
result=result,
|
|
1557
1612
|
retry_call=lambda: self._app_schema_apply_once(
|
|
@@ -1570,6 +1625,229 @@ class AiBuilderTools(ToolBase):
|
|
|
1570
1625
|
remove_fields=remove_fields,
|
|
1571
1626
|
),
|
|
1572
1627
|
)
|
|
1628
|
+
return _attach_builder_apply_envelope("app_schema_apply", result)
|
|
1629
|
+
|
|
1630
|
+
def _app_schema_apply_multi(
|
|
1631
|
+
self,
|
|
1632
|
+
*,
|
|
1633
|
+
profile: str,
|
|
1634
|
+
package_id: int | None,
|
|
1635
|
+
visibility: JSONObject | None,
|
|
1636
|
+
create_if_missing: bool,
|
|
1637
|
+
publish: bool,
|
|
1638
|
+
apps: list[JSONObject],
|
|
1639
|
+
) -> JSONObject:
|
|
1640
|
+
normalized_args: JSONObject = {
|
|
1641
|
+
"package_id": package_id,
|
|
1642
|
+
"create_if_missing": create_if_missing,
|
|
1643
|
+
"publish": publish,
|
|
1644
|
+
"apps": deepcopy(apps),
|
|
1645
|
+
}
|
|
1646
|
+
if visibility is not None:
|
|
1647
|
+
normalized_args["visibility"] = deepcopy(visibility)
|
|
1648
|
+
if package_id is None:
|
|
1649
|
+
return _config_failure(
|
|
1650
|
+
tool_name="app_schema_apply",
|
|
1651
|
+
message="app_schema_apply multi-app mode requires package_id.",
|
|
1652
|
+
fix_hint="Pass package_id and apps[].app_name for new apps, or apps[].app_key for existing apps.",
|
|
1653
|
+
)
|
|
1654
|
+
if not apps:
|
|
1655
|
+
return _config_failure(
|
|
1656
|
+
tool_name="app_schema_apply",
|
|
1657
|
+
message="app_schema_apply multi-app mode requires non-empty apps.",
|
|
1658
|
+
fix_hint="Pass apps as a non-empty list of app schema items.",
|
|
1659
|
+
)
|
|
1660
|
+
|
|
1661
|
+
client_key_to_app_key: dict[str, str] = {}
|
|
1662
|
+
created_app_keys: list[str] = []
|
|
1663
|
+
results: list[JSONObject] = []
|
|
1664
|
+
any_write_executed = False
|
|
1665
|
+
client_keys: set[str] = set()
|
|
1666
|
+
|
|
1667
|
+
for index, raw_item in enumerate(apps):
|
|
1668
|
+
if not isinstance(raw_item, dict):
|
|
1669
|
+
results.append(_multi_app_item_failure(index, raw_item, "INVALID_APP_ITEM", "apps[] items must be objects"))
|
|
1670
|
+
continue
|
|
1671
|
+
item = deepcopy(raw_item)
|
|
1672
|
+
client_key = str(item.get("client_key") or item.get("clientKey") or "").strip()
|
|
1673
|
+
app_name = str(item.get("app_name") or item.get("appTitle") or item.get("app_title") or item.get("title") or "").strip()
|
|
1674
|
+
app_key = str(item.get("app_key") or item.get("appKey") or "").strip()
|
|
1675
|
+
if client_key:
|
|
1676
|
+
if client_key in client_keys:
|
|
1677
|
+
results.append(_multi_app_item_failure(index, item, "DUPLICATE_CLIENT_KEY", f"duplicate client_key '{client_key}'"))
|
|
1678
|
+
continue
|
|
1679
|
+
client_keys.add(client_key)
|
|
1680
|
+
if not app_key and not app_name:
|
|
1681
|
+
results.append(_multi_app_item_failure(index, item, "APP_SELECTOR_REQUIRED", "apps[] requires app_key or app_name"))
|
|
1682
|
+
continue
|
|
1683
|
+
|
|
1684
|
+
initial_add_fields, deferred_add_fields = _split_multi_app_initial_add_fields(item, is_new_app=not bool(app_key))
|
|
1685
|
+
item["_deferred_add_fields"] = deferred_add_fields
|
|
1686
|
+
shell = self._app_schema_apply_once(
|
|
1687
|
+
profile=profile,
|
|
1688
|
+
app_key=app_key,
|
|
1689
|
+
package_id=package_id if not app_key else None,
|
|
1690
|
+
app_name=app_name,
|
|
1691
|
+
app_title="",
|
|
1692
|
+
icon=str(item.get("icon") or ""),
|
|
1693
|
+
color=str(item.get("color") or ""),
|
|
1694
|
+
visibility=item.get("visibility", visibility),
|
|
1695
|
+
create_if_missing=create_if_missing and not app_key,
|
|
1696
|
+
publish=publish and not deferred_add_fields,
|
|
1697
|
+
add_fields=initial_add_fields,
|
|
1698
|
+
update_fields=[],
|
|
1699
|
+
remove_fields=[],
|
|
1700
|
+
)
|
|
1701
|
+
public_shell = _publicize_package_fields(shell)
|
|
1702
|
+
resolved_key = str(public_shell.get("app_key") or "").strip()
|
|
1703
|
+
if public_shell.get("status") not in {"success", "partial_success"} or not resolved_key:
|
|
1704
|
+
results.append({
|
|
1705
|
+
"index": index,
|
|
1706
|
+
"row_number": index + 1,
|
|
1707
|
+
"client_key": client_key or None,
|
|
1708
|
+
"app_name": app_name or None,
|
|
1709
|
+
"app_key": resolved_key or app_key or None,
|
|
1710
|
+
"status": "failed",
|
|
1711
|
+
"stage": "resolve_or_create_shell",
|
|
1712
|
+
"error_code": public_shell.get("error_code") or "APP_SHELL_APPLY_FAILED",
|
|
1713
|
+
"message": public_shell.get("message") or "app shell resolve/create failed",
|
|
1714
|
+
"safe_to_retry": not any_write_executed,
|
|
1715
|
+
})
|
|
1716
|
+
continue
|
|
1717
|
+
if bool(public_shell.get("created")):
|
|
1718
|
+
created_app_keys.append(resolved_key)
|
|
1719
|
+
if _schema_apply_result_has_write(public_shell):
|
|
1720
|
+
any_write_executed = True
|
|
1721
|
+
if client_key:
|
|
1722
|
+
client_key_to_app_key[client_key] = resolved_key
|
|
1723
|
+
results.append({
|
|
1724
|
+
"index": index,
|
|
1725
|
+
"row_number": index + 1,
|
|
1726
|
+
"client_key": client_key or None,
|
|
1727
|
+
"app_name": str(public_shell.get("app_name_after") or public_shell.get("app_name") or app_name or "").strip() or None,
|
|
1728
|
+
"app_key": resolved_key,
|
|
1729
|
+
"status": "shell_ready",
|
|
1730
|
+
"created": bool(public_shell.get("created")),
|
|
1731
|
+
"shell_result": public_shell,
|
|
1732
|
+
"shell_field_diff": public_shell.get("field_diff") or {},
|
|
1733
|
+
"shell_field_diff_details": public_shell.get("field_diff_details") or {},
|
|
1734
|
+
"deferred_add_fields": deferred_add_fields,
|
|
1735
|
+
})
|
|
1736
|
+
|
|
1737
|
+
final_items: list[JSONObject] = []
|
|
1738
|
+
for index, raw_item in enumerate(apps):
|
|
1739
|
+
existing = next((item for item in results if item.get("index") == index), None)
|
|
1740
|
+
if not existing or existing.get("status") != "shell_ready":
|
|
1741
|
+
if existing:
|
|
1742
|
+
final_items.append(existing)
|
|
1743
|
+
continue
|
|
1744
|
+
item = deepcopy(raw_item)
|
|
1745
|
+
app_key = str(existing.get("app_key") or "").strip()
|
|
1746
|
+
try:
|
|
1747
|
+
compiled_item = _compile_multi_app_schema_item_refs(item, client_key_to_app_key)
|
|
1748
|
+
except ValueError as error:
|
|
1749
|
+
final_items.append({
|
|
1750
|
+
**{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
|
|
1751
|
+
"status": "failed",
|
|
1752
|
+
"stage": "compile_relation_refs",
|
|
1753
|
+
"error_code": "TARGET_APP_REF_NOT_FOUND",
|
|
1754
|
+
"message": str(error),
|
|
1755
|
+
"safe_to_retry": False,
|
|
1756
|
+
})
|
|
1757
|
+
any_write_executed = True
|
|
1758
|
+
continue
|
|
1759
|
+
|
|
1760
|
+
deferred_add_fields = (
|
|
1761
|
+
_compiled_multi_app_deferred_add_fields(compiled_item, existing)
|
|
1762
|
+
if bool(existing.get("created"))
|
|
1763
|
+
else list(compiled_item.get("add_fields") or [])
|
|
1764
|
+
)
|
|
1765
|
+
update_fields = list(compiled_item.get("update_fields") or [])
|
|
1766
|
+
remove_fields = list(compiled_item.get("remove_fields") or [])
|
|
1767
|
+
if bool(existing.get("created")) and not deferred_add_fields and not update_fields and not remove_fields:
|
|
1768
|
+
shell_result = existing.get("shell_result") if isinstance(existing.get("shell_result"), dict) else {}
|
|
1769
|
+
item_status = shell_result.get("status") if shell_result.get("status") in {"success", "partial_success"} else "failed"
|
|
1770
|
+
shell_field_diff = existing.get("shell_field_diff") if isinstance(existing.get("shell_field_diff"), dict) else {}
|
|
1771
|
+
shell_field_diff_details = existing.get("shell_field_diff_details") if isinstance(existing.get("shell_field_diff_details"), dict) else {}
|
|
1772
|
+
final_items.append({
|
|
1773
|
+
**{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
|
|
1774
|
+
"status": item_status,
|
|
1775
|
+
"stage": "schema_apply",
|
|
1776
|
+
"field_diff": shell_field_diff,
|
|
1777
|
+
"field_diff_details": shell_field_diff_details,
|
|
1778
|
+
"shell_field_diff": shell_field_diff,
|
|
1779
|
+
"shell_field_diff_details": shell_field_diff_details,
|
|
1780
|
+
"published": bool(shell_result.get("published")),
|
|
1781
|
+
"verified": bool(shell_result.get("verified")),
|
|
1782
|
+
"error_code": shell_result.get("error_code"),
|
|
1783
|
+
"message": shell_result.get("message"),
|
|
1784
|
+
"safe_to_retry": False,
|
|
1785
|
+
})
|
|
1786
|
+
continue
|
|
1787
|
+
|
|
1788
|
+
field_result = self._app_schema_apply_once(
|
|
1789
|
+
profile=profile,
|
|
1790
|
+
app_key=app_key,
|
|
1791
|
+
package_id=None,
|
|
1792
|
+
app_name=str(compiled_item.get("app_name") or compiled_item.get("appTitle") or compiled_item.get("app_title") or ""),
|
|
1793
|
+
app_title="",
|
|
1794
|
+
icon=str(compiled_item.get("icon") or ""),
|
|
1795
|
+
color=str(compiled_item.get("color") or ""),
|
|
1796
|
+
visibility=compiled_item.get("visibility"),
|
|
1797
|
+
create_if_missing=False,
|
|
1798
|
+
publish=publish,
|
|
1799
|
+
add_fields=deferred_add_fields,
|
|
1800
|
+
update_fields=update_fields,
|
|
1801
|
+
remove_fields=remove_fields,
|
|
1802
|
+
)
|
|
1803
|
+
public_result = _publicize_package_fields(field_result)
|
|
1804
|
+
if _schema_apply_result_has_write(public_result):
|
|
1805
|
+
any_write_executed = True
|
|
1806
|
+
item_status = public_result.get("status") if public_result.get("status") in {"success", "partial_success"} else "failed"
|
|
1807
|
+
shell_field_diff = existing.get("shell_field_diff") if isinstance(existing.get("shell_field_diff"), dict) else {}
|
|
1808
|
+
shell_field_diff_details = existing.get("shell_field_diff_details") if isinstance(existing.get("shell_field_diff_details"), dict) else {}
|
|
1809
|
+
field_diff = _merge_schema_field_diffs(shell_field_diff, public_result.get("field_diff") or {})
|
|
1810
|
+
field_diff_details = _merge_schema_field_diffs(shell_field_diff_details, public_result.get("field_diff_details") or {})
|
|
1811
|
+
final_items.append({
|
|
1812
|
+
**{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
|
|
1813
|
+
"status": item_status,
|
|
1814
|
+
"stage": "schema_apply",
|
|
1815
|
+
"field_diff": field_diff,
|
|
1816
|
+
"field_diff_details": field_diff_details,
|
|
1817
|
+
"shell_field_diff": shell_field_diff,
|
|
1818
|
+
"shell_field_diff_details": shell_field_diff_details,
|
|
1819
|
+
"published": bool(public_result.get("published")),
|
|
1820
|
+
"verified": bool(public_result.get("verified")),
|
|
1821
|
+
"error_code": public_result.get("error_code"),
|
|
1822
|
+
"message": public_result.get("message"),
|
|
1823
|
+
"safe_to_retry": False,
|
|
1824
|
+
})
|
|
1825
|
+
|
|
1826
|
+
succeeded = sum(1 for item in final_items if item.get("status") in {"success", "partial_success"})
|
|
1827
|
+
failed = len(final_items) - succeeded
|
|
1828
|
+
overall_status = "success" if failed == 0 else ("partial_success" if succeeded > 0 or any_write_executed else "failed")
|
|
1829
|
+
return {
|
|
1830
|
+
"status": overall_status,
|
|
1831
|
+
"mode": "multi_app",
|
|
1832
|
+
"total": len(apps),
|
|
1833
|
+
"succeeded": succeeded,
|
|
1834
|
+
"failed": failed,
|
|
1835
|
+
"created_app_keys": created_app_keys,
|
|
1836
|
+
"write_executed": any_write_executed,
|
|
1837
|
+
"safe_to_retry": not any_write_executed,
|
|
1838
|
+
"package_id": package_id,
|
|
1839
|
+
"publish_requested": publish,
|
|
1840
|
+
"apps": final_items,
|
|
1841
|
+
"normalized_args": normalized_args,
|
|
1842
|
+
"verification": {
|
|
1843
|
+
"all_apps_succeeded": failed == 0,
|
|
1844
|
+
"created_app_count": len(created_app_keys),
|
|
1845
|
+
},
|
|
1846
|
+
"request_id": None,
|
|
1847
|
+
"error_code": None if overall_status != "failed" else "MULTI_APP_SCHEMA_APPLY_FAILED",
|
|
1848
|
+
"recoverable": overall_status != "success",
|
|
1849
|
+
"message": "multi-app schema apply completed" if overall_status != "failed" else "multi-app schema apply failed",
|
|
1850
|
+
}
|
|
1573
1851
|
|
|
1574
1852
|
def _app_schema_apply_once(
|
|
1575
1853
|
self,
|
|
@@ -1685,7 +1963,7 @@ class AiBuilderTools(ToolBase):
|
|
|
1685
1963
|
publish=publish,
|
|
1686
1964
|
sections=sections,
|
|
1687
1965
|
)
|
|
1688
|
-
|
|
1966
|
+
result = self._retry_after_self_lock_release(
|
|
1689
1967
|
profile=profile,
|
|
1690
1968
|
result=result,
|
|
1691
1969
|
retry_call=lambda: self._app_layout_apply_once(
|
|
@@ -1696,6 +1974,7 @@ class AiBuilderTools(ToolBase):
|
|
|
1696
1974
|
sections=sections,
|
|
1697
1975
|
),
|
|
1698
1976
|
)
|
|
1977
|
+
return _attach_builder_apply_envelope("app_layout_apply", result)
|
|
1699
1978
|
|
|
1700
1979
|
def _app_layout_apply_once(self, *, profile: str, app_key: str, mode: str = "merge", publish: bool = True, sections: list[JSONObject]) -> JSONObject:
|
|
1701
1980
|
"""执行内部辅助逻辑。"""
|
|
@@ -1775,7 +2054,7 @@ class AiBuilderTools(ToolBase):
|
|
|
1775
2054
|
nodes=nodes,
|
|
1776
2055
|
transitions=transitions,
|
|
1777
2056
|
)
|
|
1778
|
-
|
|
2057
|
+
result = self._retry_after_self_lock_release(
|
|
1779
2058
|
profile=profile,
|
|
1780
2059
|
result=result,
|
|
1781
2060
|
retry_call=lambda: self._app_flow_apply_once(
|
|
@@ -1787,6 +2066,7 @@ class AiBuilderTools(ToolBase):
|
|
|
1787
2066
|
transitions=transitions,
|
|
1788
2067
|
),
|
|
1789
2068
|
)
|
|
2069
|
+
return _attach_builder_apply_envelope("app_flow_apply", result)
|
|
1790
2070
|
|
|
1791
2071
|
def _app_flow_apply_once(
|
|
1792
2072
|
self,
|
|
@@ -1886,7 +2166,7 @@ class AiBuilderTools(ToolBase):
|
|
|
1886
2166
|
patch_views=patch_views or [],
|
|
1887
2167
|
remove_views=remove_views,
|
|
1888
2168
|
)
|
|
1889
|
-
|
|
2169
|
+
result = self._retry_after_self_lock_release(
|
|
1890
2170
|
profile=profile,
|
|
1891
2171
|
result=result,
|
|
1892
2172
|
retry_call=lambda: self._app_views_apply_once(
|
|
@@ -1898,6 +2178,7 @@ class AiBuilderTools(ToolBase):
|
|
|
1898
2178
|
patch_views=patch_views or [],
|
|
1899
2179
|
),
|
|
1900
2180
|
)
|
|
2181
|
+
return _attach_builder_apply_envelope("app_views_apply", result)
|
|
1901
2182
|
|
|
1902
2183
|
def _app_views_apply_once(
|
|
1903
2184
|
self,
|
|
@@ -2053,7 +2334,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2053
2334
|
}
|
|
2054
2335
|
)
|
|
2055
2336
|
except ValidationError as exc:
|
|
2056
|
-
return _visibility_validation_failure(
|
|
2337
|
+
return _attach_builder_apply_envelope("app_charts_apply", _visibility_validation_failure(
|
|
2057
2338
|
str(exc),
|
|
2058
2339
|
tool_name="app_charts_apply",
|
|
2059
2340
|
exc=exc,
|
|
@@ -2067,14 +2348,14 @@ class AiBuilderTools(ToolBase):
|
|
|
2067
2348
|
"reorder_chart_ids": [],
|
|
2068
2349
|
},
|
|
2069
2350
|
},
|
|
2070
|
-
)
|
|
2351
|
+
))
|
|
2071
2352
|
normalized_args = request.model_dump(mode="json")
|
|
2072
|
-
return _safe_tool_call(
|
|
2353
|
+
return _attach_builder_apply_envelope("app_charts_apply", _safe_tool_call(
|
|
2073
2354
|
lambda: self._facade.chart_apply(profile=profile, request=request),
|
|
2074
2355
|
error_code="CHART_APPLY_FAILED",
|
|
2075
2356
|
normalized_args=normalized_args,
|
|
2076
2357
|
suggested_next_call={"tool_name": "app_charts_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
2077
|
-
)
|
|
2358
|
+
))
|
|
2078
2359
|
|
|
2079
2360
|
@tool_cn_name("门户配置应用")
|
|
2080
2361
|
def portal_apply(
|
|
@@ -2113,7 +2394,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2113
2394
|
}
|
|
2114
2395
|
)
|
|
2115
2396
|
except ValidationError as exc:
|
|
2116
|
-
return _visibility_validation_failure(
|
|
2397
|
+
return _attach_builder_apply_envelope("portal_apply", _visibility_validation_failure(
|
|
2117
2398
|
str(exc),
|
|
2118
2399
|
tool_name="portal_apply",
|
|
2119
2400
|
exc=exc,
|
|
@@ -2133,15 +2414,16 @@ class AiBuilderTools(ToolBase):
|
|
|
2133
2414
|
],
|
|
2134
2415
|
},
|
|
2135
2416
|
},
|
|
2136
|
-
)
|
|
2417
|
+
))
|
|
2137
2418
|
normalized_args = request.model_dump(mode="json")
|
|
2138
2419
|
normalized_args["package_id"] = normalized_args.pop("package_tag_id", package_id)
|
|
2139
|
-
|
|
2420
|
+
result = _publicize_package_fields(_safe_tool_call(
|
|
2140
2421
|
lambda: self._facade.portal_apply(profile=profile, request=request),
|
|
2141
2422
|
error_code="PORTAL_APPLY_FAILED",
|
|
2142
2423
|
normalized_args=normalized_args,
|
|
2143
2424
|
suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
2144
2425
|
))
|
|
2426
|
+
return _attach_builder_apply_envelope("portal_apply", result)
|
|
2145
2427
|
|
|
2146
2428
|
@tool_cn_name("应用发布校验")
|
|
2147
2429
|
def app_publish_verify(
|
|
@@ -2159,7 +2441,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2159
2441
|
normalized_args=normalized_args,
|
|
2160
2442
|
suggested_next_call={"tool_name": "app_publish_verify", "arguments": {"profile": profile, **normalized_args}},
|
|
2161
2443
|
))
|
|
2162
|
-
|
|
2444
|
+
result = _publicize_package_fields(self._retry_after_self_lock_release(
|
|
2163
2445
|
profile=profile,
|
|
2164
2446
|
result=result,
|
|
2165
2447
|
retry_call=lambda: self._facade.app_publish_verify(
|
|
@@ -2168,6 +2450,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2168
2450
|
expected_package_tag_id=expected_package_id,
|
|
2169
2451
|
),
|
|
2170
2452
|
))
|
|
2453
|
+
return _attach_builder_apply_envelope("app_publish_verify", result)
|
|
2171
2454
|
|
|
2172
2455
|
def _retry_after_self_lock_release(self, *, profile: str, result: JSONObject, retry_call) -> JSONObject:
|
|
2173
2456
|
"""执行内部辅助逻辑。"""
|
|
@@ -2286,6 +2569,128 @@ class AiBuilderTools(ToolBase):
|
|
|
2286
2569
|
return rewritten
|
|
2287
2570
|
|
|
2288
2571
|
|
|
2572
|
+
def _multi_app_item_failure(index: int, item: object, error_code: str, message: str) -> JSONObject:
|
|
2573
|
+
app_name = None
|
|
2574
|
+
client_key = None
|
|
2575
|
+
app_key = None
|
|
2576
|
+
if isinstance(item, dict):
|
|
2577
|
+
app_name = str(item.get("app_name") or item.get("appTitle") or item.get("app_title") or item.get("title") or "").strip() or None
|
|
2578
|
+
client_key = str(item.get("client_key") or item.get("clientKey") or "").strip() or None
|
|
2579
|
+
app_key = str(item.get("app_key") or item.get("appKey") or "").strip() or None
|
|
2580
|
+
return {
|
|
2581
|
+
"index": index,
|
|
2582
|
+
"row_number": index + 1,
|
|
2583
|
+
"client_key": client_key,
|
|
2584
|
+
"app_name": app_name,
|
|
2585
|
+
"app_key": app_key,
|
|
2586
|
+
"status": "failed",
|
|
2587
|
+
"stage": "validate_item",
|
|
2588
|
+
"error_code": error_code,
|
|
2589
|
+
"message": message,
|
|
2590
|
+
"safe_to_retry": True,
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
|
|
2594
|
+
def _compile_multi_app_schema_item_refs(item: JSONObject, client_key_to_app_key: dict[str, str]) -> JSONObject:
|
|
2595
|
+
compiled = deepcopy(item)
|
|
2596
|
+
|
|
2597
|
+
def visit(value):
|
|
2598
|
+
if isinstance(value, list):
|
|
2599
|
+
return [visit(entry) for entry in value]
|
|
2600
|
+
if not isinstance(value, dict):
|
|
2601
|
+
return value
|
|
2602
|
+
payload = {key: visit(entry) for key, entry in value.items()}
|
|
2603
|
+
ref = (
|
|
2604
|
+
payload.pop("target_app_ref", None)
|
|
2605
|
+
or payload.pop("targetAppRef", None)
|
|
2606
|
+
or payload.pop("target_app_client_key", None)
|
|
2607
|
+
or payload.pop("targetAppClientKey", None)
|
|
2608
|
+
)
|
|
2609
|
+
if ref is not None:
|
|
2610
|
+
ref_key = str(ref or "").strip()
|
|
2611
|
+
target_app_key = client_key_to_app_key.get(ref_key)
|
|
2612
|
+
if not target_app_key:
|
|
2613
|
+
raise ValueError(f"target_app_ref '{ref_key}' did not match any apps[].client_key")
|
|
2614
|
+
payload["target_app_key"] = target_app_key
|
|
2615
|
+
return payload
|
|
2616
|
+
|
|
2617
|
+
return visit(compiled)
|
|
2618
|
+
|
|
2619
|
+
|
|
2620
|
+
def _split_multi_app_initial_add_fields(item: JSONObject, *, is_new_app: bool) -> tuple[list[JSONObject], list[JSONObject]]:
|
|
2621
|
+
add_fields = _multi_app_list_value(item, "add_fields", "addFields")
|
|
2622
|
+
if not is_new_app:
|
|
2623
|
+
return [], add_fields
|
|
2624
|
+
initial: list[JSONObject] = []
|
|
2625
|
+
deferred: list[JSONObject] = []
|
|
2626
|
+
for field in add_fields:
|
|
2627
|
+
if _contains_multi_app_target_ref(field):
|
|
2628
|
+
deferred.append(field)
|
|
2629
|
+
else:
|
|
2630
|
+
initial.append(field)
|
|
2631
|
+
return initial, deferred
|
|
2632
|
+
|
|
2633
|
+
|
|
2634
|
+
def _compiled_multi_app_deferred_add_fields(compiled_item: JSONObject, existing_result: JSONObject) -> list[JSONObject]:
|
|
2635
|
+
deferred = existing_result.get("deferred_add_fields")
|
|
2636
|
+
if not isinstance(deferred, list):
|
|
2637
|
+
return list(compiled_item.get("add_fields") or [])
|
|
2638
|
+
deferred_names = {str(item.get("name") or item.get("title") or item.get("label") or "").strip() for item in deferred if isinstance(item, dict)}
|
|
2639
|
+
if not deferred_names:
|
|
2640
|
+
return []
|
|
2641
|
+
return [
|
|
2642
|
+
deepcopy(field)
|
|
2643
|
+
for field in list(compiled_item.get("add_fields") or [])
|
|
2644
|
+
if isinstance(field, dict)
|
|
2645
|
+
and str(field.get("name") or field.get("title") or field.get("label") or "").strip() in deferred_names
|
|
2646
|
+
]
|
|
2647
|
+
|
|
2648
|
+
|
|
2649
|
+
def _multi_app_list_value(item: JSONObject, *keys: str) -> list[JSONObject]:
|
|
2650
|
+
for key in keys:
|
|
2651
|
+
value = item.get(key)
|
|
2652
|
+
if isinstance(value, list):
|
|
2653
|
+
return [deepcopy(entry) for entry in value if isinstance(entry, dict)]
|
|
2654
|
+
return []
|
|
2655
|
+
|
|
2656
|
+
|
|
2657
|
+
def _contains_multi_app_target_ref(value: object) -> bool:
|
|
2658
|
+
if isinstance(value, list):
|
|
2659
|
+
return any(_contains_multi_app_target_ref(item) for item in value)
|
|
2660
|
+
if not isinstance(value, dict):
|
|
2661
|
+
return False
|
|
2662
|
+
for key, entry in value.items():
|
|
2663
|
+
if key in {"target_app_ref", "targetAppRef", "target_app_client_key", "targetAppClientKey"}:
|
|
2664
|
+
return True
|
|
2665
|
+
if _contains_multi_app_target_ref(entry):
|
|
2666
|
+
return True
|
|
2667
|
+
return False
|
|
2668
|
+
|
|
2669
|
+
|
|
2670
|
+
def _merge_schema_field_diffs(*diffs: object) -> JSONObject:
|
|
2671
|
+
merged: JSONObject = {"added": [], "updated": [], "removed": []}
|
|
2672
|
+
for diff in diffs:
|
|
2673
|
+
if not isinstance(diff, dict):
|
|
2674
|
+
continue
|
|
2675
|
+
for key in ("added", "updated", "removed"):
|
|
2676
|
+
values = diff.get(key)
|
|
2677
|
+
if not isinstance(values, list):
|
|
2678
|
+
continue
|
|
2679
|
+
for value in values:
|
|
2680
|
+
if value not in merged[key]:
|
|
2681
|
+
merged[key].append(value)
|
|
2682
|
+
return merged
|
|
2683
|
+
|
|
2684
|
+
|
|
2685
|
+
def _schema_apply_result_has_write(result: JSONObject) -> bool:
|
|
2686
|
+
if bool(result.get("created")) or bool(result.get("published")) or bool(result.get("app_base_updated")):
|
|
2687
|
+
return True
|
|
2688
|
+
field_diff = result.get("field_diff")
|
|
2689
|
+
if isinstance(field_diff, dict):
|
|
2690
|
+
return any(bool(field_diff.get(key)) for key in ("added", "updated", "removed"))
|
|
2691
|
+
return False
|
|
2692
|
+
|
|
2693
|
+
|
|
2289
2694
|
def _validation_failure(
|
|
2290
2695
|
detail: str,
|
|
2291
2696
|
*,
|
|
@@ -2448,6 +2853,661 @@ def _publicize_package_fields(value):
|
|
|
2448
2853
|
return public
|
|
2449
2854
|
|
|
2450
2855
|
|
|
2856
|
+
def _builder_contract_with_apply_output(tool_name: str, contract: JSONObject) -> JSONObject:
|
|
2857
|
+
public = deepcopy(contract)
|
|
2858
|
+
if tool_name not in BUILDER_APPLY_TOOL_NAMES:
|
|
2859
|
+
return public
|
|
2860
|
+
notes = public.setdefault("execution_notes", [])
|
|
2861
|
+
if isinstance(notes, list):
|
|
2862
|
+
note = "apply/write output includes schema_version, operation, summary, and resources[]; UI and agents should read resources[].id/key/name first and use legacy fields only for compatibility/debugging"
|
|
2863
|
+
if note not in notes:
|
|
2864
|
+
notes.append(note)
|
|
2865
|
+
public["output_contract"] = {
|
|
2866
|
+
"schema_version": BUILDER_APPLY_SCHEMA_VERSION,
|
|
2867
|
+
"preferred_ui_fields": ["operation", "summary", "resources"],
|
|
2868
|
+
"resource_fields": ["resource_type", "operation", "status", "id", "key", "name", "ids", "parent", "error_code", "message"],
|
|
2869
|
+
"legacy_fields_preserved": True,
|
|
2870
|
+
}
|
|
2871
|
+
return public
|
|
2872
|
+
|
|
2873
|
+
|
|
2874
|
+
def _attach_builder_apply_envelope(tool_name: str, payload: JSONObject) -> JSONObject:
|
|
2875
|
+
if not isinstance(payload, dict):
|
|
2876
|
+
return payload
|
|
2877
|
+
resources = _builder_apply_resources(tool_name, payload)
|
|
2878
|
+
payload["schema_version"] = BUILDER_APPLY_SCHEMA_VERSION
|
|
2879
|
+
payload["operation"] = tool_name
|
|
2880
|
+
payload["resources"] = resources
|
|
2881
|
+
payload["summary"] = _builder_apply_summary(payload, resources)
|
|
2882
|
+
return payload
|
|
2883
|
+
|
|
2884
|
+
|
|
2885
|
+
def _builder_apply_summary(payload: JSONObject, resources: list[JSONObject]) -> JSONObject:
|
|
2886
|
+
status = str(payload.get("status") or "")
|
|
2887
|
+
failed = sum(1 for item in resources if str(item.get("status") or "") == "failed")
|
|
2888
|
+
created = sum(1 for item in resources if str(item.get("operation") or "") == "created" and str(item.get("status") or "") != "failed")
|
|
2889
|
+
removed = sum(1 for item in resources if str(item.get("operation") or "") == "removed" and str(item.get("status") or "") != "failed")
|
|
2890
|
+
updated = sum(
|
|
2891
|
+
1
|
|
2892
|
+
for item in resources
|
|
2893
|
+
if str(item.get("status") or "") != "failed"
|
|
2894
|
+
and str(item.get("operation") or "") in {"updated", "layout_updated", "workflow_updated", "verified", "published"}
|
|
2895
|
+
)
|
|
2896
|
+
published_value = payload.get("published")
|
|
2897
|
+
if published_value is None:
|
|
2898
|
+
publish_requested = payload.get("publish_requested")
|
|
2899
|
+
published_value = bool(publish_requested) and status in {"success", "partial_success"}
|
|
2900
|
+
verified_value = payload.get("verified")
|
|
2901
|
+
if verified_value is None:
|
|
2902
|
+
verification = payload.get("verification")
|
|
2903
|
+
verified_value = status == "success" and failed == 0 and _builder_verification_truthy(verification)
|
|
2904
|
+
summary: JSONObject = {
|
|
2905
|
+
"total": len(resources),
|
|
2906
|
+
"created": created,
|
|
2907
|
+
"updated": updated,
|
|
2908
|
+
"removed": removed,
|
|
2909
|
+
"failed": failed,
|
|
2910
|
+
"published": bool(published_value),
|
|
2911
|
+
"verified": bool(verified_value),
|
|
2912
|
+
}
|
|
2913
|
+
if "write_executed" in payload:
|
|
2914
|
+
summary["write_executed"] = bool(payload.get("write_executed"))
|
|
2915
|
+
if "safe_to_retry" in payload:
|
|
2916
|
+
summary["safe_to_retry"] = bool(payload.get("safe_to_retry"))
|
|
2917
|
+
return summary
|
|
2918
|
+
|
|
2919
|
+
|
|
2920
|
+
def _builder_verification_truthy(value: object) -> bool:
|
|
2921
|
+
if value is None:
|
|
2922
|
+
return True
|
|
2923
|
+
if isinstance(value, bool):
|
|
2924
|
+
return value
|
|
2925
|
+
if isinstance(value, dict):
|
|
2926
|
+
booleans = [item for item in value.values() if isinstance(item, bool)]
|
|
2927
|
+
return all(booleans) if booleans else True
|
|
2928
|
+
return True
|
|
2929
|
+
|
|
2930
|
+
|
|
2931
|
+
def _builder_apply_resources(tool_name: str, payload: JSONObject) -> list[JSONObject]:
|
|
2932
|
+
resources: list[JSONObject]
|
|
2933
|
+
if tool_name == "package_apply":
|
|
2934
|
+
resources = _builder_package_resources(payload)
|
|
2935
|
+
elif tool_name == "app_schema_apply":
|
|
2936
|
+
resources = _builder_schema_resources(payload)
|
|
2937
|
+
elif tool_name == "app_layout_apply":
|
|
2938
|
+
resources = [_builder_app_resource(payload, operation="layout_updated")]
|
|
2939
|
+
elif tool_name == "app_flow_apply":
|
|
2940
|
+
resources = [_builder_app_resource(payload, operation="workflow_updated")]
|
|
2941
|
+
elif tool_name == "app_views_apply":
|
|
2942
|
+
resources = _builder_view_resources(payload)
|
|
2943
|
+
elif tool_name == "app_charts_apply":
|
|
2944
|
+
resources = _builder_chart_resources(payload)
|
|
2945
|
+
elif tool_name == "portal_apply":
|
|
2946
|
+
resources = _builder_portal_resources(payload)
|
|
2947
|
+
elif tool_name == "app_custom_buttons_apply":
|
|
2948
|
+
resources = _builder_button_resources(payload)
|
|
2949
|
+
elif tool_name == "app_associated_resources_apply":
|
|
2950
|
+
resources = _builder_associated_resource_resources(payload)
|
|
2951
|
+
elif tool_name == "app_publish_verify":
|
|
2952
|
+
resources = [_builder_app_resource(payload, operation="verified")]
|
|
2953
|
+
else:
|
|
2954
|
+
resources = []
|
|
2955
|
+
if not resources and _builder_status(payload, "") == "failed" and _builder_apply_tool_is_app_scoped(tool_name):
|
|
2956
|
+
app_key = _builder_payload_app_key(payload)
|
|
2957
|
+
if app_key not in (None, ""):
|
|
2958
|
+
resources = [_builder_app_resource(payload, operation="failed")]
|
|
2959
|
+
return resources
|
|
2960
|
+
|
|
2961
|
+
|
|
2962
|
+
def _builder_apply_tool_is_app_scoped(tool_name: str) -> bool:
|
|
2963
|
+
return tool_name in {
|
|
2964
|
+
"app_schema_apply",
|
|
2965
|
+
"app_layout_apply",
|
|
2966
|
+
"app_flow_apply",
|
|
2967
|
+
"app_views_apply",
|
|
2968
|
+
"app_custom_buttons_apply",
|
|
2969
|
+
"app_associated_resources_apply",
|
|
2970
|
+
"app_charts_apply",
|
|
2971
|
+
"app_publish_verify",
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
|
|
2975
|
+
def _builder_status(payload_or_item: JSONObject, fallback: str = "success") -> str:
|
|
2976
|
+
status = str(payload_or_item.get("status") or fallback or "success")
|
|
2977
|
+
return status
|
|
2978
|
+
|
|
2979
|
+
|
|
2980
|
+
def _builder_operation(value: object, fallback: str = "updated") -> str:
|
|
2981
|
+
raw = str(value or fallback or "updated").strip()
|
|
2982
|
+
mapping = {
|
|
2983
|
+
"create": "created",
|
|
2984
|
+
"created": "created",
|
|
2985
|
+
"add": "created",
|
|
2986
|
+
"update": "updated",
|
|
2987
|
+
"updated": "updated",
|
|
2988
|
+
"patch": "updated",
|
|
2989
|
+
"remove": "removed",
|
|
2990
|
+
"removed": "removed",
|
|
2991
|
+
"delete": "removed",
|
|
2992
|
+
"deleted": "removed",
|
|
2993
|
+
"unchanged": "unchanged",
|
|
2994
|
+
"failed": "failed",
|
|
2995
|
+
}
|
|
2996
|
+
return mapping.get(raw, raw)
|
|
2997
|
+
|
|
2998
|
+
|
|
2999
|
+
def _builder_parent(resource_type: str, *, key: object = None, name: object = None, id_value: object = None) -> JSONObject:
|
|
3000
|
+
return {
|
|
3001
|
+
"resource_type": resource_type,
|
|
3002
|
+
"id": id_value,
|
|
3003
|
+
"key": str(key) if key not in (None, "") else None,
|
|
3004
|
+
"name": str(name) if name not in (None, "") else None,
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
|
|
3008
|
+
def _builder_app_parent(payload: JSONObject) -> JSONObject | None:
|
|
3009
|
+
app_key = payload.get("app_key") or payload.get("appKey")
|
|
3010
|
+
app_name = payload.get("app_name_after") or payload.get("app_name") or payload.get("appTitle") or payload.get("app_title")
|
|
3011
|
+
if app_key in (None, "") and app_name in (None, ""):
|
|
3012
|
+
return None
|
|
3013
|
+
return _builder_parent("app", key=app_key, name=app_name)
|
|
3014
|
+
|
|
3015
|
+
|
|
3016
|
+
def _builder_resource(
|
|
3017
|
+
*,
|
|
3018
|
+
resource_type: str,
|
|
3019
|
+
operation: str,
|
|
3020
|
+
status: str,
|
|
3021
|
+
id_value: object = None,
|
|
3022
|
+
key: object = None,
|
|
3023
|
+
name: object = None,
|
|
3024
|
+
ids: JSONObject | None = None,
|
|
3025
|
+
parent: JSONObject | None = None,
|
|
3026
|
+
error_code: object = None,
|
|
3027
|
+
message: object = None,
|
|
3028
|
+
) -> JSONObject:
|
|
3029
|
+
return {
|
|
3030
|
+
"resource_type": resource_type,
|
|
3031
|
+
"operation": operation,
|
|
3032
|
+
"status": status,
|
|
3033
|
+
"id": id_value,
|
|
3034
|
+
"key": str(key) if key not in (None, "") else None,
|
|
3035
|
+
"name": str(name) if name not in (None, "") else None,
|
|
3036
|
+
"ids": ids or {},
|
|
3037
|
+
"parent": parent,
|
|
3038
|
+
"error_code": str(error_code) if error_code not in (None, "") else None,
|
|
3039
|
+
"message": str(message) if message not in (None, "") else None,
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
|
|
3043
|
+
def _builder_app_resource(payload: JSONObject, *, operation: str) -> JSONObject:
|
|
3044
|
+
status = _builder_status(payload, "success")
|
|
3045
|
+
if status == "failed":
|
|
3046
|
+
operation = "failed"
|
|
3047
|
+
app_key = _builder_payload_app_key(payload)
|
|
3048
|
+
app_name = _builder_payload_app_name(payload)
|
|
3049
|
+
return _builder_resource(
|
|
3050
|
+
resource_type="app",
|
|
3051
|
+
operation=operation,
|
|
3052
|
+
status=status,
|
|
3053
|
+
key=app_key,
|
|
3054
|
+
name=app_name,
|
|
3055
|
+
ids={"app_key": app_key} if app_key not in (None, "") else {},
|
|
3056
|
+
error_code=payload.get("error_code"),
|
|
3057
|
+
message=payload.get("message") if status == "failed" else None,
|
|
3058
|
+
)
|
|
3059
|
+
|
|
3060
|
+
|
|
3061
|
+
def _builder_payload_app_key(payload: JSONObject) -> object:
|
|
3062
|
+
return _builder_payload_identity_value(payload, ("app_key", "appKey"))
|
|
3063
|
+
|
|
3064
|
+
|
|
3065
|
+
def _builder_payload_app_name(payload: JSONObject) -> object:
|
|
3066
|
+
return _builder_payload_identity_value(payload, ("app_name_after", "app_name", "appName", "appTitle", "app_title", "name", "title"))
|
|
3067
|
+
|
|
3068
|
+
|
|
3069
|
+
def _builder_payload_identity_value(payload: JSONObject, keys: tuple[str, ...]) -> object:
|
|
3070
|
+
for key in keys:
|
|
3071
|
+
value = payload.get(key)
|
|
3072
|
+
if value not in (None, ""):
|
|
3073
|
+
return value
|
|
3074
|
+
for container_key in ("normalized_args", "canonical_arguments"):
|
|
3075
|
+
container = payload.get(container_key)
|
|
3076
|
+
if isinstance(container, dict):
|
|
3077
|
+
for key in keys:
|
|
3078
|
+
value = container.get(key)
|
|
3079
|
+
if value not in (None, ""):
|
|
3080
|
+
return value
|
|
3081
|
+
details = payload.get("details")
|
|
3082
|
+
if isinstance(details, dict):
|
|
3083
|
+
for container_key in ("normalized_args", "canonical_arguments"):
|
|
3084
|
+
container = details.get(container_key)
|
|
3085
|
+
if isinstance(container, dict):
|
|
3086
|
+
for key in keys:
|
|
3087
|
+
value = container.get(key)
|
|
3088
|
+
if value not in (None, ""):
|
|
3089
|
+
return value
|
|
3090
|
+
return None
|
|
3091
|
+
|
|
3092
|
+
|
|
3093
|
+
def _builder_package_resources(payload: JSONObject) -> list[JSONObject]:
|
|
3094
|
+
package_id = payload.get("package_id") or payload.get("id")
|
|
3095
|
+
package_name = payload.get("package_name") or payload.get("name")
|
|
3096
|
+
status = _builder_status(payload, "success")
|
|
3097
|
+
operation = "failed" if status == "failed" else ("created" if bool(payload.get("created")) else "updated")
|
|
3098
|
+
return [
|
|
3099
|
+
_builder_resource(
|
|
3100
|
+
resource_type="package",
|
|
3101
|
+
operation=operation,
|
|
3102
|
+
status=status,
|
|
3103
|
+
id_value=package_id,
|
|
3104
|
+
key=str(package_id) if package_id not in (None, "") else None,
|
|
3105
|
+
name=package_name,
|
|
3106
|
+
ids={"package_id": package_id} if package_id not in (None, "") else {},
|
|
3107
|
+
error_code=payload.get("error_code"),
|
|
3108
|
+
message=payload.get("message") if status == "failed" else None,
|
|
3109
|
+
)
|
|
3110
|
+
]
|
|
3111
|
+
|
|
3112
|
+
|
|
3113
|
+
def _builder_schema_resources(payload: JSONObject) -> list[JSONObject]:
|
|
3114
|
+
if payload.get("mode") == "multi_app" and isinstance(payload.get("apps"), list):
|
|
3115
|
+
resources: list[JSONObject] = []
|
|
3116
|
+
package_id = payload.get("package_id")
|
|
3117
|
+
package_parent = _builder_parent("package", id_value=package_id, key=package_id) if package_id not in (None, "") else None
|
|
3118
|
+
for item in payload.get("apps") or []:
|
|
3119
|
+
if not isinstance(item, dict):
|
|
3120
|
+
continue
|
|
3121
|
+
status = _builder_status(item, "success")
|
|
3122
|
+
operation = "failed" if status == "failed" else ("created" if bool(item.get("created")) else "updated")
|
|
3123
|
+
parent = _builder_parent("app", key=item.get("app_key"), name=item.get("app_name"))
|
|
3124
|
+
resources.append(
|
|
3125
|
+
_builder_resource(
|
|
3126
|
+
resource_type="app",
|
|
3127
|
+
operation=operation,
|
|
3128
|
+
status=status,
|
|
3129
|
+
key=item.get("app_key"),
|
|
3130
|
+
name=item.get("app_name"),
|
|
3131
|
+
ids={
|
|
3132
|
+
**({"app_key": item.get("app_key")} if item.get("app_key") else {}),
|
|
3133
|
+
**({"package_id": package_id} if package_id not in (None, "") else {}),
|
|
3134
|
+
},
|
|
3135
|
+
parent=package_parent,
|
|
3136
|
+
error_code=item.get("error_code"),
|
|
3137
|
+
message=item.get("message") if status == "failed" else None,
|
|
3138
|
+
)
|
|
3139
|
+
)
|
|
3140
|
+
resources.extend(_builder_field_resources(item.get("field_diff_details") or item.get("field_diff"), parent=parent))
|
|
3141
|
+
return resources
|
|
3142
|
+
|
|
3143
|
+
status = _builder_status(payload, "success")
|
|
3144
|
+
operation = "failed" if status == "failed" else ("created" if bool(payload.get("created")) else "updated")
|
|
3145
|
+
app_key = payload.get("app_key")
|
|
3146
|
+
app_name = payload.get("app_name_after") or payload.get("app_name")
|
|
3147
|
+
parent = _builder_parent("app", key=app_key, name=app_name)
|
|
3148
|
+
resources = [
|
|
3149
|
+
_builder_resource(
|
|
3150
|
+
resource_type="app",
|
|
3151
|
+
operation=operation,
|
|
3152
|
+
status=status,
|
|
3153
|
+
key=app_key,
|
|
3154
|
+
name=app_name,
|
|
3155
|
+
ids={"app_key": app_key} if app_key else {},
|
|
3156
|
+
error_code=payload.get("error_code"),
|
|
3157
|
+
message=payload.get("message") if status == "failed" else None,
|
|
3158
|
+
)
|
|
3159
|
+
]
|
|
3160
|
+
resources.extend(_builder_field_resources(payload.get("field_diff_details") or payload.get("field_diff"), parent=parent))
|
|
3161
|
+
return resources
|
|
3162
|
+
|
|
3163
|
+
|
|
3164
|
+
def _builder_field_resources(field_diff: object, *, parent: JSONObject | None) -> list[JSONObject]:
|
|
3165
|
+
if not isinstance(field_diff, dict):
|
|
3166
|
+
return []
|
|
3167
|
+
resources: list[JSONObject] = []
|
|
3168
|
+
for key, operation in (("added", "created"), ("updated", "updated"), ("removed", "removed")):
|
|
3169
|
+
for field in field_diff.get(key) or []:
|
|
3170
|
+
if isinstance(field, dict):
|
|
3171
|
+
name = field.get("name") or field.get("title") or field.get("field_name") or field.get("queTitle")
|
|
3172
|
+
field_id = field.get("field_id") or field.get("queId")
|
|
3173
|
+
que_id = field.get("que_id") or field.get("queId")
|
|
3174
|
+
else:
|
|
3175
|
+
name = field
|
|
3176
|
+
field_id = None
|
|
3177
|
+
que_id = None
|
|
3178
|
+
resource_id = que_id if que_id not in (None, "") else field_id
|
|
3179
|
+
resources.append(
|
|
3180
|
+
_builder_resource(
|
|
3181
|
+
resource_type="field",
|
|
3182
|
+
operation=operation,
|
|
3183
|
+
status="success",
|
|
3184
|
+
id_value=resource_id,
|
|
3185
|
+
key=field_id if field_id not in (None, "") else name,
|
|
3186
|
+
name=name,
|
|
3187
|
+
ids={
|
|
3188
|
+
**({"field_id": field_id} if field_id not in (None, "") else {}),
|
|
3189
|
+
**({"que_id": que_id} if que_id not in (None, "") else {}),
|
|
3190
|
+
**({"app_key": parent.get("key")} if isinstance(parent, dict) and parent.get("key") else {}),
|
|
3191
|
+
},
|
|
3192
|
+
parent=parent,
|
|
3193
|
+
)
|
|
3194
|
+
)
|
|
3195
|
+
return resources
|
|
3196
|
+
|
|
3197
|
+
|
|
3198
|
+
def _builder_view_resources(payload: JSONObject) -> list[JSONObject]:
|
|
3199
|
+
diff = payload.get("views_diff") if isinstance(payload.get("views_diff"), dict) else {}
|
|
3200
|
+
verification_by_name = _builder_view_verification_by_name(payload)
|
|
3201
|
+
parent = _builder_app_parent(payload)
|
|
3202
|
+
resources: list[JSONObject] = []
|
|
3203
|
+
for key, operation in (("created", "created"), ("updated", "updated"), ("removed", "removed")):
|
|
3204
|
+
for item in diff.get(key) or []:
|
|
3205
|
+
name, view_key, status, error_code, message = _builder_view_identity(item, verification_by_name)
|
|
3206
|
+
resources.append(
|
|
3207
|
+
_builder_resource(
|
|
3208
|
+
resource_type="view",
|
|
3209
|
+
operation=operation,
|
|
3210
|
+
status=status,
|
|
3211
|
+
key=view_key,
|
|
3212
|
+
name=name,
|
|
3213
|
+
ids={
|
|
3214
|
+
**({"view_key": view_key} if view_key else {}),
|
|
3215
|
+
**({"app_key": payload.get("app_key")} if payload.get("app_key") else {}),
|
|
3216
|
+
},
|
|
3217
|
+
parent=parent,
|
|
3218
|
+
error_code=error_code,
|
|
3219
|
+
message=message,
|
|
3220
|
+
)
|
|
3221
|
+
)
|
|
3222
|
+
for item in diff.get("failed") or []:
|
|
3223
|
+
name, view_key, _status, error_code, message = _builder_view_identity(item, verification_by_name)
|
|
3224
|
+
resources.append(
|
|
3225
|
+
_builder_resource(
|
|
3226
|
+
resource_type="view",
|
|
3227
|
+
operation="failed",
|
|
3228
|
+
status="failed",
|
|
3229
|
+
key=view_key,
|
|
3230
|
+
name=name,
|
|
3231
|
+
ids={
|
|
3232
|
+
**({"view_key": view_key} if view_key else {}),
|
|
3233
|
+
**({"app_key": payload.get("app_key")} if payload.get("app_key") else {}),
|
|
3234
|
+
},
|
|
3235
|
+
parent=parent,
|
|
3236
|
+
error_code=error_code,
|
|
3237
|
+
message=message,
|
|
3238
|
+
)
|
|
3239
|
+
)
|
|
3240
|
+
return resources
|
|
3241
|
+
|
|
3242
|
+
|
|
3243
|
+
def _builder_view_verification_by_name(payload: JSONObject) -> dict[str, JSONObject]:
|
|
3244
|
+
verification = payload.get("verification")
|
|
3245
|
+
by_view = verification.get("by_view") if isinstance(verification, dict) else None
|
|
3246
|
+
result: dict[str, JSONObject] = {}
|
|
3247
|
+
if isinstance(by_view, list):
|
|
3248
|
+
for item in by_view:
|
|
3249
|
+
if isinstance(item, dict):
|
|
3250
|
+
name = str(item.get("name") or "").strip()
|
|
3251
|
+
if name:
|
|
3252
|
+
result[name] = item
|
|
3253
|
+
return result
|
|
3254
|
+
|
|
3255
|
+
|
|
3256
|
+
def _builder_view_identity(item: object, verification_by_name: dict[str, JSONObject]) -> tuple[str | None, str | None, str, object, object]:
|
|
3257
|
+
if isinstance(item, dict):
|
|
3258
|
+
name = item.get("name") or item.get("view_name") or item.get("viewName")
|
|
3259
|
+
view_key = item.get("view_key") or item.get("viewKey")
|
|
3260
|
+
status = str(item.get("status") or "success")
|
|
3261
|
+
error_code = item.get("error_code")
|
|
3262
|
+
message = item.get("message")
|
|
3263
|
+
else:
|
|
3264
|
+
name = str(item) if item not in (None, "") else None
|
|
3265
|
+
view_key = None
|
|
3266
|
+
status = "success"
|
|
3267
|
+
error_code = None
|
|
3268
|
+
message = None
|
|
3269
|
+
if name and not view_key:
|
|
3270
|
+
verification = verification_by_name.get(str(name))
|
|
3271
|
+
if isinstance(verification, dict):
|
|
3272
|
+
view_key = verification.get("view_key") or verification.get("viewKey")
|
|
3273
|
+
if not view_key:
|
|
3274
|
+
matching = verification.get("matching_view_keys")
|
|
3275
|
+
if isinstance(matching, list) and matching:
|
|
3276
|
+
view_key = matching[0]
|
|
3277
|
+
return (
|
|
3278
|
+
str(name) if name not in (None, "") else None,
|
|
3279
|
+
str(view_key) if view_key not in (None, "") else None,
|
|
3280
|
+
status,
|
|
3281
|
+
error_code,
|
|
3282
|
+
message,
|
|
3283
|
+
)
|
|
3284
|
+
|
|
3285
|
+
|
|
3286
|
+
def _builder_chart_resources(payload: JSONObject) -> list[JSONObject]:
|
|
3287
|
+
parent = _builder_app_parent(payload)
|
|
3288
|
+
resources: list[JSONObject] = []
|
|
3289
|
+
for item in payload.get("chart_results") or []:
|
|
3290
|
+
if not isinstance(item, dict):
|
|
3291
|
+
continue
|
|
3292
|
+
status = str(item.get("status") or "success")
|
|
3293
|
+
operation = _builder_operation(status, fallback="updated")
|
|
3294
|
+
if status == "failed":
|
|
3295
|
+
operation = "failed"
|
|
3296
|
+
chart_id = item.get("chart_id") or item.get("chartId")
|
|
3297
|
+
chart_key = item.get("chart_key") or item.get("chartKey")
|
|
3298
|
+
resources.append(
|
|
3299
|
+
_builder_resource(
|
|
3300
|
+
resource_type="chart",
|
|
3301
|
+
operation=operation,
|
|
3302
|
+
status="failed" if status == "failed" else "success",
|
|
3303
|
+
id_value=chart_id,
|
|
3304
|
+
key=chart_key or chart_id,
|
|
3305
|
+
name=item.get("name") or item.get("chart_name") or item.get("chartName"),
|
|
3306
|
+
ids={
|
|
3307
|
+
**({"chart_id": chart_id} if chart_id not in (None, "") else {}),
|
|
3308
|
+
**({"chart_key": chart_key} if chart_key else {}),
|
|
3309
|
+
**({"app_key": payload.get("app_key")} if payload.get("app_key") else {}),
|
|
3310
|
+
**({"chart_type": item.get("chart_type")} if item.get("chart_type") else {}),
|
|
3311
|
+
},
|
|
3312
|
+
parent=parent,
|
|
3313
|
+
error_code=item.get("error_code"),
|
|
3314
|
+
message=item.get("message") if status == "failed" else None,
|
|
3315
|
+
)
|
|
3316
|
+
)
|
|
3317
|
+
return resources
|
|
3318
|
+
|
|
3319
|
+
|
|
3320
|
+
def _builder_portal_resources(payload: JSONObject) -> list[JSONObject]:
|
|
3321
|
+
status = _builder_status(payload, "success")
|
|
3322
|
+
draft_result = payload.get("draft_result") if isinstance(payload.get("draft_result"), dict) else {}
|
|
3323
|
+
live_result = payload.get("live_result") if isinstance(payload.get("live_result"), dict) else {}
|
|
3324
|
+
normalized_args = payload.get("normalized_args") if isinstance(payload.get("normalized_args"), dict) else {}
|
|
3325
|
+
dash_key = (
|
|
3326
|
+
payload.get("dash_key")
|
|
3327
|
+
or payload.get("dashKey")
|
|
3328
|
+
or draft_result.get("dashKey")
|
|
3329
|
+
or draft_result.get("dash_key")
|
|
3330
|
+
or live_result.get("dashKey")
|
|
3331
|
+
or live_result.get("dash_key")
|
|
3332
|
+
)
|
|
3333
|
+
dash_name = (
|
|
3334
|
+
payload.get("dash_name")
|
|
3335
|
+
or payload.get("dashName")
|
|
3336
|
+
or payload.get("name")
|
|
3337
|
+
or draft_result.get("dashName")
|
|
3338
|
+
or draft_result.get("dash_name")
|
|
3339
|
+
or draft_result.get("name")
|
|
3340
|
+
or live_result.get("dashName")
|
|
3341
|
+
or live_result.get("dash_name")
|
|
3342
|
+
or live_result.get("name")
|
|
3343
|
+
or normalized_args.get("dash_name")
|
|
3344
|
+
or normalized_args.get("dashName")
|
|
3345
|
+
)
|
|
3346
|
+
package_id = payload.get("package_id") or normalized_args.get("package_id") or normalized_args.get("package_tag_id")
|
|
3347
|
+
if package_id in (None, "") and isinstance(draft_result.get("tags"), list) and draft_result.get("tags"):
|
|
3348
|
+
first_tag = draft_result.get("tags")[0]
|
|
3349
|
+
if isinstance(first_tag, dict):
|
|
3350
|
+
package_id = first_tag.get("tagId") or first_tag.get("tag_id") or first_tag.get("id")
|
|
3351
|
+
operation = "failed" if status == "failed" else ("created" if bool(payload.get("created")) else "updated")
|
|
3352
|
+
parent = None
|
|
3353
|
+
if package_id:
|
|
3354
|
+
parent = _builder_parent("package", id_value=package_id, key=package_id)
|
|
3355
|
+
return [
|
|
3356
|
+
_builder_resource(
|
|
3357
|
+
resource_type="portal",
|
|
3358
|
+
operation=operation,
|
|
3359
|
+
status=status,
|
|
3360
|
+
key=dash_key,
|
|
3361
|
+
name=dash_name,
|
|
3362
|
+
ids={
|
|
3363
|
+
**({"dash_key": dash_key} if dash_key else {}),
|
|
3364
|
+
**({"package_id": package_id} if package_id else {}),
|
|
3365
|
+
},
|
|
3366
|
+
parent=parent,
|
|
3367
|
+
error_code=payload.get("error_code"),
|
|
3368
|
+
message=payload.get("message") if status == "failed" else None,
|
|
3369
|
+
)
|
|
3370
|
+
]
|
|
3371
|
+
|
|
3372
|
+
|
|
3373
|
+
def _builder_button_resources(payload: JSONObject) -> list[JSONObject]:
|
|
3374
|
+
parent = _builder_app_parent(payload)
|
|
3375
|
+
resources: list[JSONObject] = []
|
|
3376
|
+
for key, operation in (("created", "created"), ("updated", "updated"), ("removed", "removed"), ("failed", "failed")):
|
|
3377
|
+
for item in payload.get(key) or []:
|
|
3378
|
+
if not isinstance(item, dict):
|
|
3379
|
+
continue
|
|
3380
|
+
status = "failed" if operation == "failed" or item.get("status") == "failed" else str(item.get("status") or "success")
|
|
3381
|
+
button_id = item.get("button_id") or item.get("buttonId")
|
|
3382
|
+
resources.append(
|
|
3383
|
+
_builder_resource(
|
|
3384
|
+
resource_type="button",
|
|
3385
|
+
operation=operation if operation != "failed" else "failed",
|
|
3386
|
+
status=status,
|
|
3387
|
+
id_value=button_id,
|
|
3388
|
+
key=button_id,
|
|
3389
|
+
name=item.get("button_text") or item.get("buttonText"),
|
|
3390
|
+
ids={
|
|
3391
|
+
**({"button_id": button_id} if button_id not in (None, "") else {}),
|
|
3392
|
+
**({"app_key": payload.get("app_key")} if payload.get("app_key") else {}),
|
|
3393
|
+
},
|
|
3394
|
+
parent=parent,
|
|
3395
|
+
error_code=item.get("error_code"),
|
|
3396
|
+
message=item.get("message") if status == "failed" else None,
|
|
3397
|
+
)
|
|
3398
|
+
)
|
|
3399
|
+
for item in payload.get("view_configs") or []:
|
|
3400
|
+
if isinstance(item, dict):
|
|
3401
|
+
status = str(item.get("status") or "success")
|
|
3402
|
+
view_key = item.get("view_key") or item.get("viewKey")
|
|
3403
|
+
resources.append(
|
|
3404
|
+
_builder_resource(
|
|
3405
|
+
resource_type="button_binding",
|
|
3406
|
+
operation="updated" if status != "failed" else "failed",
|
|
3407
|
+
status=status,
|
|
3408
|
+
key=view_key,
|
|
3409
|
+
name=item.get("view_name") or item.get("viewName") or view_key,
|
|
3410
|
+
ids={
|
|
3411
|
+
**({"view_key": view_key} if view_key else {}),
|
|
3412
|
+
**({"app_key": payload.get("app_key")} if payload.get("app_key") else {}),
|
|
3413
|
+
},
|
|
3414
|
+
parent=parent,
|
|
3415
|
+
error_code=item.get("error_code"),
|
|
3416
|
+
message=item.get("message") if status == "failed" else None,
|
|
3417
|
+
)
|
|
3418
|
+
)
|
|
3419
|
+
return resources
|
|
3420
|
+
|
|
3421
|
+
|
|
3422
|
+
def _builder_associated_resource_resources(payload: JSONObject) -> list[JSONObject]:
|
|
3423
|
+
parent = _builder_app_parent(payload)
|
|
3424
|
+
readback_by_id = _builder_associated_resource_readback_by_id(payload)
|
|
3425
|
+
resources: list[JSONObject] = []
|
|
3426
|
+
for key, operation in (
|
|
3427
|
+
("created", "created"),
|
|
3428
|
+
("updated", "updated"),
|
|
3429
|
+
("unchanged", "unchanged"),
|
|
3430
|
+
("removed", "removed"),
|
|
3431
|
+
("failed", "failed"),
|
|
3432
|
+
):
|
|
3433
|
+
for item in payload.get(key) or []:
|
|
3434
|
+
if not isinstance(item, dict):
|
|
3435
|
+
continue
|
|
3436
|
+
status = "failed" if operation == "failed" or item.get("status") == "failed" else str(item.get("status") or "success")
|
|
3437
|
+
associated_item_id = item.get("associated_item_id") or item.get("associatedItemId")
|
|
3438
|
+
readback = readback_by_id.get(str(associated_item_id)) if associated_item_id not in (None, "") else None
|
|
3439
|
+
view_key = item.get("view_key") or item.get("viewKey") or (readback or {}).get("view_key") or (readback or {}).get("viewKey")
|
|
3440
|
+
chart_key = item.get("chart_key") or item.get("chartKey") or (readback or {}).get("chart_key") or (readback or {}).get("chartKey")
|
|
3441
|
+
target_app_key = item.get("target_app_key") or (readback or {}).get("target_app_key")
|
|
3442
|
+
name = item.get("name") or item.get("resource_name") or (readback or {}).get("name") or view_key or chart_key
|
|
3443
|
+
resources.append(
|
|
3444
|
+
_builder_resource(
|
|
3445
|
+
resource_type="associated_resource",
|
|
3446
|
+
operation=operation if operation != "failed" else "failed",
|
|
3447
|
+
status=status,
|
|
3448
|
+
id_value=associated_item_id,
|
|
3449
|
+
key=view_key or chart_key or associated_item_id,
|
|
3450
|
+
name=name,
|
|
3451
|
+
ids={
|
|
3452
|
+
**({"associated_item_id": associated_item_id} if associated_item_id not in (None, "") else {}),
|
|
3453
|
+
**({"app_key": payload.get("app_key")} if payload.get("app_key") else {}),
|
|
3454
|
+
**({"target_app_key": target_app_key} if target_app_key else {}),
|
|
3455
|
+
**({"view_key": view_key} if view_key else {}),
|
|
3456
|
+
**({"chart_key": chart_key} if chart_key else {}),
|
|
3457
|
+
},
|
|
3458
|
+
parent=parent,
|
|
3459
|
+
error_code=item.get("error_code"),
|
|
3460
|
+
message=item.get("message") if status == "failed" else None,
|
|
3461
|
+
)
|
|
3462
|
+
)
|
|
3463
|
+
for item in payload.get("view_configs") or []:
|
|
3464
|
+
if isinstance(item, dict):
|
|
3465
|
+
status = str(item.get("status") or "success")
|
|
3466
|
+
view_key = item.get("view_key") or item.get("viewKey")
|
|
3467
|
+
resources.append(
|
|
3468
|
+
_builder_resource(
|
|
3469
|
+
resource_type="associated_resource_binding",
|
|
3470
|
+
operation="updated" if status != "failed" else "failed",
|
|
3471
|
+
status=status,
|
|
3472
|
+
key=view_key,
|
|
3473
|
+
name=item.get("view_name") or item.get("viewName") or view_key,
|
|
3474
|
+
ids={
|
|
3475
|
+
**({"view_key": view_key} if view_key else {}),
|
|
3476
|
+
**({"app_key": payload.get("app_key")} if payload.get("app_key") else {}),
|
|
3477
|
+
},
|
|
3478
|
+
parent=parent,
|
|
3479
|
+
error_code=item.get("error_code"),
|
|
3480
|
+
message=item.get("message") if status == "failed" else None,
|
|
3481
|
+
)
|
|
3482
|
+
)
|
|
3483
|
+
return resources
|
|
3484
|
+
|
|
3485
|
+
|
|
3486
|
+
def _builder_associated_resource_readback_by_id(payload: JSONObject) -> dict[str, JSONObject]:
|
|
3487
|
+
result: dict[str, JSONObject] = {}
|
|
3488
|
+
|
|
3489
|
+
def collect(items: object) -> None:
|
|
3490
|
+
if not isinstance(items, list):
|
|
3491
|
+
return
|
|
3492
|
+
for item in items:
|
|
3493
|
+
if not isinstance(item, dict):
|
|
3494
|
+
continue
|
|
3495
|
+
associated_item_id = item.get("associated_item_id") or item.get("associatedItemId")
|
|
3496
|
+
if associated_item_id in (None, ""):
|
|
3497
|
+
continue
|
|
3498
|
+
result[str(associated_item_id)] = item
|
|
3499
|
+
|
|
3500
|
+
collect(payload.get("associated_resources"))
|
|
3501
|
+
for config in payload.get("view_configs") or []:
|
|
3502
|
+
if not isinstance(config, dict):
|
|
3503
|
+
continue
|
|
3504
|
+
for key in ("actual", "expected"):
|
|
3505
|
+
value = config.get(key)
|
|
3506
|
+
if isinstance(value, dict):
|
|
3507
|
+
collect(value.get("items"))
|
|
3508
|
+
return result
|
|
3509
|
+
|
|
3510
|
+
|
|
2451
3511
|
def _coerce_api_error(error: Exception) -> QingflowApiError:
|
|
2452
3512
|
if isinstance(error, QingflowApiError):
|
|
2453
3513
|
return error
|
|
@@ -3105,11 +4165,40 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3105
4165
|
},
|
|
3106
4166
|
},
|
|
3107
4167
|
"app_schema_apply": {
|
|
3108
|
-
"allowed_keys": [
|
|
4168
|
+
"allowed_keys": [
|
|
4169
|
+
"app_key",
|
|
4170
|
+
"package_id",
|
|
4171
|
+
"app_name",
|
|
4172
|
+
"icon",
|
|
4173
|
+
"color",
|
|
4174
|
+
"visibility",
|
|
4175
|
+
"create_if_missing",
|
|
4176
|
+
"publish",
|
|
4177
|
+
"add_fields",
|
|
4178
|
+
"update_fields",
|
|
4179
|
+
"remove_fields",
|
|
4180
|
+
"apps",
|
|
4181
|
+
"apps[].client_key",
|
|
4182
|
+
"apps[].app_key",
|
|
4183
|
+
"apps[].app_name",
|
|
4184
|
+
"apps[].icon",
|
|
4185
|
+
"apps[].color",
|
|
4186
|
+
"apps[].visibility",
|
|
4187
|
+
"apps[].add_fields",
|
|
4188
|
+
"apps[].update_fields",
|
|
4189
|
+
"apps[].remove_fields",
|
|
4190
|
+
"apps[].add_fields[].target_app_ref",
|
|
4191
|
+
],
|
|
3109
4192
|
"aliases": {
|
|
3110
4193
|
"app_title": "app_name",
|
|
3111
4194
|
"title": "app_name",
|
|
3112
4195
|
"packageId": "package_id",
|
|
4196
|
+
"apps[].clientKey": "apps[].client_key",
|
|
4197
|
+
"apps[].appKey": "apps[].app_key",
|
|
4198
|
+
"apps[].appName": "apps[].app_name",
|
|
4199
|
+
"apps[].appTitle": "apps[].app_name",
|
|
4200
|
+
"field.targetAppRef": "field.target_app_ref",
|
|
4201
|
+
"field.targetAppClientKey": "field.target_app_ref",
|
|
3113
4202
|
"field.title": "field.name",
|
|
3114
4203
|
"field.label": "field.name",
|
|
3115
4204
|
"field.fields": "field.subfields",
|
|
@@ -3146,6 +4235,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3146
4235
|
"use exactly one resource mode",
|
|
3147
4236
|
"edit mode: app_key, optional app_name to rename the existing app",
|
|
3148
4237
|
"create mode: package_id + app_name + create_if_missing=true",
|
|
4238
|
+
"multi-app mode: pass package_id + create_if_missing + apps[]; do not mix apps with top-level app_key/app_name/add_fields/update_fields/remove_fields",
|
|
4239
|
+
"multi-app relation fields may use target_app_ref to point at another apps[].client_key; the tool creates/resolves app shells first and compiles it to target_app_key",
|
|
4240
|
+
"multi-app mode is not transactional; read created_app_keys and apps[].status before retrying, and retry only failed app items",
|
|
3149
4241
|
"create mode defaults new app visibility to workspace/not when visibility is omitted; edit mode preserves current visibility when omitted",
|
|
3150
4242
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
3151
4243
|
"update_fields is the field-level partial update path; it reads current form schema and preserves untouched field config",
|
|
@@ -3183,6 +4275,36 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3183
4275
|
"update_fields": [],
|
|
3184
4276
|
"remove_fields": [],
|
|
3185
4277
|
},
|
|
4278
|
+
"multi_app_example": {
|
|
4279
|
+
"profile": "default",
|
|
4280
|
+
"package_id": 1001,
|
|
4281
|
+
"create_if_missing": True,
|
|
4282
|
+
"publish": True,
|
|
4283
|
+
"apps": [
|
|
4284
|
+
{
|
|
4285
|
+
"client_key": "employee",
|
|
4286
|
+
"app_name": "员工花名册",
|
|
4287
|
+
"add_fields": [
|
|
4288
|
+
{"name": "员工名称", "type": "text", "as_data_title": True},
|
|
4289
|
+
{"name": "员工照片", "type": "attachment", "as_data_cover": True},
|
|
4290
|
+
],
|
|
4291
|
+
},
|
|
4292
|
+
{
|
|
4293
|
+
"client_key": "worklog",
|
|
4294
|
+
"app_name": "工时表",
|
|
4295
|
+
"add_fields": [
|
|
4296
|
+
{"name": "工时标题", "type": "text", "as_data_title": True},
|
|
4297
|
+
{
|
|
4298
|
+
"name": "关联员工",
|
|
4299
|
+
"type": "relation",
|
|
4300
|
+
"target_app_ref": "employee",
|
|
4301
|
+
"display_field": {"name": "员工名称"},
|
|
4302
|
+
"visible_fields": [{"name": "员工名称"}],
|
|
4303
|
+
},
|
|
4304
|
+
],
|
|
4305
|
+
},
|
|
4306
|
+
],
|
|
4307
|
+
},
|
|
3186
4308
|
"rename_example": {
|
|
3187
4309
|
"profile": "default",
|
|
3188
4310
|
"app_key": "APP_PROJECT",
|
|
@@ -3451,7 +4573,25 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3451
4573
|
},
|
|
3452
4574
|
},
|
|
3453
4575
|
"app_views_plan": {
|
|
3454
|
-
"allowed_keys": [
|
|
4576
|
+
"allowed_keys": [
|
|
4577
|
+
"app_key",
|
|
4578
|
+
"upsert_views",
|
|
4579
|
+
"patch_views",
|
|
4580
|
+
"remove_views",
|
|
4581
|
+
"preset",
|
|
4582
|
+
"upsert_views[].view_key",
|
|
4583
|
+
"upsert_views[].name",
|
|
4584
|
+
"upsert_views[].type",
|
|
4585
|
+
"upsert_views[].columns",
|
|
4586
|
+
"upsert_views[].filters",
|
|
4587
|
+
"upsert_views[].buttons",
|
|
4588
|
+
"upsert_views[].visibility",
|
|
4589
|
+
"upsert_views[].query_conditions",
|
|
4590
|
+
"patch_views[].view_key",
|
|
4591
|
+
"patch_views[].name",
|
|
4592
|
+
"patch_views[].set",
|
|
4593
|
+
"patch_views[].unset",
|
|
4594
|
+
],
|
|
3455
4595
|
"aliases": {
|
|
3456
4596
|
"fields": "columns",
|
|
3457
4597
|
"column_names": "columns",
|
|
@@ -3483,7 +4623,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3483
4623
|
"view.type": [member.value for member in PublicViewType],
|
|
3484
4624
|
"view.filter.operator": [member.value for member in ViewFilterOperator],
|
|
3485
4625
|
"view.buttons.button_type": ["SYSTEM", "CUSTOM"],
|
|
3486
|
-
"view.buttons.config_type": ["TOP", "DETAIL"],
|
|
4626
|
+
"view.buttons.config_type": ["TOP", "DETAIL", "INSIDE"],
|
|
3487
4627
|
**deepcopy(_VISIBILITY_ALLOWED_VALUES),
|
|
3488
4628
|
},
|
|
3489
4629
|
"execution_notes": [
|
|
@@ -3491,7 +4631,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3491
4631
|
"filters are saved fixed filters that apply when the view opens; query_conditions configure the frontend query panel and only apply after a user enters query values",
|
|
3492
4632
|
"upsert_views[].query_conditions.rows is a layout matrix of field names; it is compiled to backend queryCondition queIds",
|
|
3493
4633
|
"use patch_views for partial parameter replacement on existing views; the tool reads current config, merges patch_views[].set/unset, then submits the backend full-save payload internally",
|
|
3494
|
-
"
|
|
4634
|
+
"new views created by app_views_apply default associated report/view display to visible with limit_type=all; existing views preserve their current associated display unless patch_views/upsert_views explicitly changes associated_resources",
|
|
4635
|
+
"associated report/view resource pool and per-view selected resources are configured through app_associated_resources_apply; app_views_apply only keeps legacy associated_resources input compatible",
|
|
3495
4636
|
"for multi-value operators such as in, pass values as a list; value may also be used as an alias when it already contains a list",
|
|
3496
4637
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
3497
4638
|
],
|
|
@@ -3554,7 +4695,25 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3554
4695
|
},
|
|
3555
4696
|
},
|
|
3556
4697
|
"app_views_apply": {
|
|
3557
|
-
"allowed_keys": [
|
|
4698
|
+
"allowed_keys": [
|
|
4699
|
+
"app_key",
|
|
4700
|
+
"publish",
|
|
4701
|
+
"upsert_views",
|
|
4702
|
+
"patch_views",
|
|
4703
|
+
"remove_views",
|
|
4704
|
+
"upsert_views[].view_key",
|
|
4705
|
+
"upsert_views[].name",
|
|
4706
|
+
"upsert_views[].type",
|
|
4707
|
+
"upsert_views[].columns",
|
|
4708
|
+
"upsert_views[].filters",
|
|
4709
|
+
"upsert_views[].buttons",
|
|
4710
|
+
"upsert_views[].visibility",
|
|
4711
|
+
"upsert_views[].query_conditions",
|
|
4712
|
+
"patch_views[].view_key",
|
|
4713
|
+
"patch_views[].name",
|
|
4714
|
+
"patch_views[].set",
|
|
4715
|
+
"patch_views[].unset",
|
|
4716
|
+
],
|
|
3558
4717
|
"aliases": {
|
|
3559
4718
|
"fields": "columns",
|
|
3560
4719
|
"column_names": "columns",
|
|
@@ -3585,7 +4744,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3585
4744
|
"view.type": [member.value for member in PublicViewType],
|
|
3586
4745
|
"view.filter.operator": [member.value for member in ViewFilterOperator],
|
|
3587
4746
|
"view.buttons.button_type": ["SYSTEM", "CUSTOM"],
|
|
3588
|
-
"view.buttons.config_type": ["TOP", "DETAIL"],
|
|
4747
|
+
"view.buttons.config_type": ["TOP", "DETAIL", "INSIDE"],
|
|
3589
4748
|
**deepcopy(_VISIBILITY_ALLOWED_VALUES),
|
|
3590
4749
|
},
|
|
3591
4750
|
"execution_notes": [
|
|
@@ -3598,7 +4757,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3598
4757
|
"filters are saved fixed filters that apply when the view opens; query_conditions configure the frontend query panel and only apply after a user enters query values",
|
|
3599
4758
|
"upsert_views[].query_conditions.rows is a layout matrix of field names; it is compiled to backend queryCondition queIds",
|
|
3600
4759
|
"use patch_views for partial parameter replacement on existing views; the public update mode is patch even though the backend save is still a full view payload",
|
|
3601
|
-
"
|
|
4760
|
+
"new views created by app_views_apply default associated report/view display to visible with limit_type=all; existing views preserve their current associated display unless patch_views/upsert_views explicitly changes associated_resources",
|
|
4761
|
+
"associated report/view resource pool and per-view selected resources are configured through app_associated_resources_apply; app_views_apply keeps legacy associated_resources input compatible but it is no longer the recommended public contract",
|
|
3602
4762
|
"for multi-value operators such as in, pass values as a list; value may also be used as an alias when it already contains a list",
|
|
3603
4763
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
3604
4764
|
],
|