@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
|
@@ -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
|
|
@@ -2976,6 +4036,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2976
4036
|
"graphType": "graph_type",
|
|
2977
4037
|
"targetAppKey": "target_app_key",
|
|
2978
4038
|
"chartKey": "chart_key",
|
|
4039
|
+
"chartId": "chart_key",
|
|
2979
4040
|
"viewKey": "view_key",
|
|
2980
4041
|
"viewgraphKey": "view_key",
|
|
2981
4042
|
"reportSource": "report_source",
|
|
@@ -2990,12 +4051,14 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2990
4051
|
"view_configs[].limit_type": ["all", "select"],
|
|
2991
4052
|
},
|
|
2992
4053
|
"execution_notes": [
|
|
4054
|
+
"this tool manages Qingflow in-app associated report/view display; it does not create or edit QingBI report bodies/configs",
|
|
4055
|
+
"create or edit app-source BI report bodies first with app_charts_apply, then attach the resulting chart_id here with graph_type=chart; dataset BI reports can only be attached when they already exist",
|
|
2993
4056
|
"this is the default associated report/view path; it manages both the app-level associated resource pool and per-view display config",
|
|
2994
4057
|
"use patch_resources for partial parameter replacement on existing associated resources; the tool reads the current resource including backend-required raw fields, merges patch_resources[].set/unset, then submits the backend full-save payload internally",
|
|
2995
|
-
"associated_item_id is form_asos_chart.id from app_get.associated_resources[].associated_item_id;
|
|
4058
|
+
"associated_item_id is form_asos_chart.id from app_get.associated_resources[].associated_item_id; view_configs/remove/reorder may also pass an existing associated resource's chart_id/chart_key/view_key and the tool resolves it to the internal id",
|
|
2996
4059
|
"before creating an associated resource, read app_get.associated_resources and reuse an existing item with patch_resources when target_app_key + view_key/chart_key already matches; repeated upsert_resources without associated_item_id can create duplicate associated items",
|
|
2997
4060
|
"graph_type=view uses view_key and internally compiles to the Qingflow view source; graph_type=chart uses chart_key and defaults to report_source=app",
|
|
2998
|
-
"report_source=app maps to BI_QINGFLOW; report_source=dataset maps to BI_DATASET; do not pass raw backend sourceType",
|
|
4061
|
+
"report_source=app maps to BI_QINGFLOW; report_source=dataset maps to BI_DATASET for associating an existing dataset report; do not pass raw backend sourceType",
|
|
2999
4062
|
"use match_mappings for associated view/report filtering; dynamic conditions use source_field and static conditions use value",
|
|
3000
4063
|
"match_mappings.source_field accepts source schema fields plus system fields 数据ID(-17) and 编号(0); match_mappings compiles to backend matchRules",
|
|
3001
4064
|
"do not write raw match_rules unless preserving a legacy backend config; match_mappings and match_rules are mutually exclusive",
|
|
@@ -3102,11 +4165,40 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3102
4165
|
},
|
|
3103
4166
|
},
|
|
3104
4167
|
"app_schema_apply": {
|
|
3105
|
-
"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
|
+
],
|
|
3106
4192
|
"aliases": {
|
|
3107
4193
|
"app_title": "app_name",
|
|
3108
4194
|
"title": "app_name",
|
|
3109
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",
|
|
3110
4202
|
"field.title": "field.name",
|
|
3111
4203
|
"field.label": "field.name",
|
|
3112
4204
|
"field.fields": "field.subfields",
|
|
@@ -3143,6 +4235,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3143
4235
|
"use exactly one resource mode",
|
|
3144
4236
|
"edit mode: app_key, optional app_name to rename the existing app",
|
|
3145
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",
|
|
3146
4241
|
"create mode defaults new app visibility to workspace/not when visibility is omitted; edit mode preserves current visibility when omitted",
|
|
3147
4242
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
3148
4243
|
"update_fields is the field-level partial update path; it reads current form schema and preserves untouched field config",
|
|
@@ -3180,6 +4275,36 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3180
4275
|
"update_fields": [],
|
|
3181
4276
|
"remove_fields": [],
|
|
3182
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
|
+
},
|
|
3183
4308
|
"rename_example": {
|
|
3184
4309
|
"profile": "default",
|
|
3185
4310
|
"app_key": "APP_PROJECT",
|
|
@@ -3448,7 +4573,25 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3448
4573
|
},
|
|
3449
4574
|
},
|
|
3450
4575
|
"app_views_plan": {
|
|
3451
|
-
"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
|
+
],
|
|
3452
4595
|
"aliases": {
|
|
3453
4596
|
"fields": "columns",
|
|
3454
4597
|
"column_names": "columns",
|
|
@@ -3480,7 +4623,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3480
4623
|
"view.type": [member.value for member in PublicViewType],
|
|
3481
4624
|
"view.filter.operator": [member.value for member in ViewFilterOperator],
|
|
3482
4625
|
"view.buttons.button_type": ["SYSTEM", "CUSTOM"],
|
|
3483
|
-
"view.buttons.config_type": ["TOP", "DETAIL"],
|
|
4626
|
+
"view.buttons.config_type": ["TOP", "DETAIL", "INSIDE"],
|
|
3484
4627
|
**deepcopy(_VISIBILITY_ALLOWED_VALUES),
|
|
3485
4628
|
},
|
|
3486
4629
|
"execution_notes": [
|
|
@@ -3488,7 +4631,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3488
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",
|
|
3489
4632
|
"upsert_views[].query_conditions.rows is a layout matrix of field names; it is compiled to backend queryCondition queIds",
|
|
3490
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",
|
|
3491
|
-
"
|
|
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",
|
|
3492
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",
|
|
3493
4637
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
3494
4638
|
],
|
|
@@ -3551,7 +4695,25 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3551
4695
|
},
|
|
3552
4696
|
},
|
|
3553
4697
|
"app_views_apply": {
|
|
3554
|
-
"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
|
+
],
|
|
3555
4717
|
"aliases": {
|
|
3556
4718
|
"fields": "columns",
|
|
3557
4719
|
"column_names": "columns",
|
|
@@ -3582,7 +4744,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3582
4744
|
"view.type": [member.value for member in PublicViewType],
|
|
3583
4745
|
"view.filter.operator": [member.value for member in ViewFilterOperator],
|
|
3584
4746
|
"view.buttons.button_type": ["SYSTEM", "CUSTOM"],
|
|
3585
|
-
"view.buttons.config_type": ["TOP", "DETAIL"],
|
|
4747
|
+
"view.buttons.config_type": ["TOP", "DETAIL", "INSIDE"],
|
|
3586
4748
|
**deepcopy(_VISIBILITY_ALLOWED_VALUES),
|
|
3587
4749
|
},
|
|
3588
4750
|
"execution_notes": [
|
|
@@ -3595,7 +4757,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3595
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",
|
|
3596
4758
|
"upsert_views[].query_conditions.rows is a layout matrix of field names; it is compiled to backend queryCondition queIds",
|
|
3597
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",
|
|
3598
|
-
"
|
|
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",
|
|
3599
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",
|
|
3600
4763
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
3601
4764
|
],
|
|
@@ -3673,7 +4836,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3673
4836
|
"can_edit_form covers form/schema routes only and does not imply app base-info writes",
|
|
3674
4837
|
"returns normalized app visibility when backend auth is readable",
|
|
3675
4838
|
"custom_buttons[].button_id is the id required by app_custom_buttons_apply view_configs[].buttons[].button_ref",
|
|
3676
|
-
"associated_resources[].associated_item_id is the id
|
|
4839
|
+
"associated_resources[].associated_item_id is the internal id; app_associated_resources_apply.view_configs/remove/reorder may also pass an existing resource's chart_id/chart_key/view_key",
|
|
3677
4840
|
],
|
|
3678
4841
|
"minimal_example": {
|
|
3679
4842
|
"profile": "default",
|
|
@@ -3797,6 +4960,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3797
4960
|
**deepcopy(_VISIBILITY_ALLOWED_VALUES),
|
|
3798
4961
|
},
|
|
3799
4962
|
"execution_notes": [
|
|
4963
|
+
"this tool manages QingBI report bodies/configs; it does not attach reports to Qingflow app associated-resource display",
|
|
4964
|
+
"app_charts_apply creates/updates app-source QingBI reports only; generated payloads use dataSourceType=qingflow",
|
|
4965
|
+
"dataset BI reports are not created or edited by this tool yet; create them in QingBI first, then attach the existing report with app_associated_resources_apply report_source=dataset",
|
|
4966
|
+
"after creating or updating an app-source report body, use app_associated_resources_apply when the report should appear inside a Qingflow app/view",
|
|
3800
4967
|
"app_charts_apply is immediate-live and does not publish",
|
|
3801
4968
|
"use patch_charts for partial parameter replacement on existing charts; the tool reads current chart base/config, merges patch_charts[].set/unset, then submits full QingBI base/config payloads internally",
|
|
3802
4969
|
"chart matching precedence is chart_id first, then exact unique chart name",
|