@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.
@@ -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 _visibility_validation_failure(str(exc), tool_name="package_apply", exc=exc)
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
- return _publicize_package_fields(_safe_tool_call(
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
- return self._retry_after_self_lock_release(
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
- return self._retry_after_self_lock_release(
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
- return self._retry_after_self_lock_release(
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
- return self._retry_after_self_lock_release(
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
- return _publicize_package_fields(_safe_tool_call(
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
- return _publicize_package_fields(self._retry_after_self_lock_release(
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; it is not chart_id, chart_key, or view_key",
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": ["app_key", "package_id", "app_name", "icon", "color", "visibility", "create_if_missing", "publish", "add_fields", "update_fields", "remove_fields"],
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": ["app_key", "upsert_views", "patch_views", "remove_views", "preset", "upsert_views[].view_key", "upsert_views[].buttons", "upsert_views[].visibility", "upsert_views[].query_conditions", "patch_views[].view_key", "patch_views[].name", "patch_views[].set", "patch_views[].unset"],
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
- "view associated report/view display is now configured through app_associated_resources_apply, not app_views_apply",
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": ["app_key", "publish", "upsert_views", "patch_views", "remove_views", "upsert_views[].view_key", "upsert_views[].buttons", "upsert_views[].visibility", "upsert_views[].query_conditions", "patch_views[].view_key", "patch_views[].name", "patch_views[].set", "patch_views[].unset"],
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
- "view associated report/view display is now configured through app_associated_resources_apply; app_views_apply keeps legacy associated_resources input compatible but it is no longer the recommended public contract",
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 required by app_associated_resources_apply.view_configs.associated_item_ids; do not pass chart_id there",
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",