@josephyan/qingflow-cli 1.1.3 → 1.1.5

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.
Files changed (154) hide show
  1. package/README.md +7 -3
  2. package/docs/local-agent-install.md +57 -6
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-skills.mjs +5 -0
  5. package/npm/bin/qingflow.mjs +1 -34
  6. package/npm/lib/runtime.mjs +21 -101
  7. package/npm/scripts/postinstall.mjs +1 -10
  8. package/package.json +3 -2
  9. package/pyproject.toml +1 -1
  10. package/skills/qingflow-cli/SKILL.md +58 -44
  11. package/skills/qingflow-cli/manifest.yaml +1 -1
  12. package/skills/qingflow-cli/reference/00-INDEX.md +35 -0
  13. package/skills/qingflow-cli/reference/builder/10-build-single-app.md +38 -0
  14. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +39 -0
  15. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md → builder/30-schema-fields.md} +52 -10
  16. package/skills/qingflow-cli/reference/builder/40-layout.md +52 -0
  17. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md → builder/50-views.md} +39 -15
  18. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md → builder/60-charts.md} +36 -13
  19. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md → builder/70-portal.md} +36 -13
  20. package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +41 -0
  21. package/skills/qingflow-cli/reference/builder/90-workflow.md +34 -0
  22. package/skills/qingflow-cli/reference/builder/99-publish-verify.md +46 -0
  23. package/skills/qingflow-cli/reference/builder/README.md +41 -0
  24. package/skills/qingflow-cli/reference/builder/code-integrations/README.md +130 -0
  25. package/skills/qingflow-cli/reference/builder/code-integrations/code-block.md +66 -0
  26. package/skills/qingflow-cli/reference/builder/code-integrations/q-linker.md +77 -0
  27. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md → builder/reference/app-delivery-sop.md} +26 -16
  28. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/README.md +293 -0
  29. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-complete-system.md +809 -0
  30. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-single-app.md +830 -0
  31. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/complete-system-development-guide.md +123 -0
  32. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/create-app.md +182 -0
  33. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/environments.md +63 -0
  34. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/flow-actors-and-permissions.md +142 -0
  35. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/gotchas.md +108 -0
  36. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/match-rules.md +114 -0
  37. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/public-surface-sync.md +75 -0
  38. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/single-app-development-guide.md +58 -0
  39. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/solution-playbooks.md +52 -0
  40. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/tool-selection.md +107 -0
  41. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-flow.md +7 -0
  42. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-layout.md +7 -0
  43. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-schema.md +7 -0
  44. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-views.md +7 -0
  45. package/skills/qingflow-cli/reference/builder/workflow/01-overview.md +45 -0
  46. package/skills/qingflow-cli/reference/builder/workflow/02-update-mode.md +53 -0
  47. package/skills/qingflow-cli/reference/builder/workflow/03-flow-patterns.md +57 -0
  48. package/skills/qingflow-cli/reference/builder/workflow/04-stage1-business-modeling.md +131 -0
  49. package/skills/qingflow-cli/reference/builder/workflow/05-stage2-members-roles.md +29 -0
  50. package/skills/qingflow-cli/reference/builder/workflow/06-stage3-build-spec.md +165 -0
  51. package/skills/qingflow-cli/reference/builder/workflow/07-stage4-validate-spec.md +33 -0
  52. package/skills/qingflow-cli/reference/builder/workflow/08-stage5-apply-verify.md +51 -0
  53. package/skills/qingflow-cli/reference/builder/workflow/09-stage6-summary.md +88 -0
  54. package/skills/qingflow-cli/reference/builder/workflow/10-node-config-reference.md +93 -0
  55. package/skills/qingflow-cli/reference/builder/workflow/11-troubleshooting.md +15 -0
  56. package/skills/qingflow-cli/reference/builder/workflow/README.md +88 -0
  57. package/skills/qingflow-cli/reference/builder/workflow/workflow-schema.json +1754 -0
  58. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ADMIN_CHEATSHEET.md → core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md} +3 -3
  59. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md → core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md} +6 -6
  60. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_EXPLORATION_REPORT.md → core/QINGFLOW_CLI_EXPLORATION_REPORT.md} +2 -2
  61. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_FIELD_DATA_TYPES.md → core/QINGFLOW_CLI_FIELD_DATA_TYPES.md} +11 -11
  62. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_MEMBER_CHEATSHEET.md → core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md} +4 -4
  63. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md → core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md} +4 -4
  64. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md} +3 -3
  65. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_DELETE_WORKFLOW.md +31 -0
  66. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md} +4 -4
  67. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md} +7 -7
  68. package/skills/qingflow-cli/reference/record/analysis/README.md +130 -0
  69. package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +91 -0
  70. package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +112 -0
  71. package/skills/qingflow-cli/reference/record/analysis/business-context.md +74 -0
  72. package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +69 -0
  73. package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +106 -0
  74. package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +172 -0
  75. package/skills/qingflow-cli/reference/record/analysis/report-format.md +76 -0
  76. package/skills/qingflow-cli/reference/record/insert/README.md +75 -0
  77. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md → task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md} +5 -5
  78. package/skills/qingflow-cli/reference/task/ops/README.md +131 -0
  79. package/skills/qingflow-cli/reference/task/ops/environments.md +43 -0
  80. package/skills/qingflow-cli/reference/task/ops/workflow-usage.md +26 -0
  81. package/skills/qingflow-cli/scripts/validate_system_build_summary.py +124 -0
  82. package/skills/qingflow-cli/scripts/workflow/diff_flow_spec.py +275 -0
  83. package/skills/qingflow-cli/scripts/workflow/validate_flow_spec.py +605 -0
  84. package/skills/qingflow-mcp-setup/SKILL.md +115 -0
  85. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  86. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  87. package/skills/qingflow-mcp-setup/references/environments.md +62 -0
  88. package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
  89. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  90. package/src/qingflow_mcp/__init__.py +1 -1
  91. package/src/qingflow_mcp/__main__.py +6 -2
  92. package/src/qingflow_mcp/builder_facade/models.py +287 -25
  93. package/src/qingflow_mcp/builder_facade/service.py +4195 -856
  94. package/src/qingflow_mcp/cli/commands/builder.py +316 -247
  95. package/src/qingflow_mcp/cli/commands/chart.py +1 -1
  96. package/src/qingflow_mcp/cli/commands/common.py +12 -3
  97. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  98. package/src/qingflow_mcp/cli/commands/imports.py +3 -3
  99. package/src/qingflow_mcp/cli/commands/portal.py +2 -2
  100. package/src/qingflow_mcp/cli/commands/record.py +101 -27
  101. package/src/qingflow_mcp/cli/commands/task.py +28 -47
  102. package/src/qingflow_mcp/cli/commands/view.py +1 -1
  103. package/src/qingflow_mcp/cli/context.py +0 -3
  104. package/src/qingflow_mcp/cli/formatters.py +784 -16
  105. package/src/qingflow_mcp/cli/main.py +117 -33
  106. package/src/qingflow_mcp/errors.py +43 -2
  107. package/src/qingflow_mcp/public_surface.py +26 -17
  108. package/src/qingflow_mcp/response_trim.py +81 -17
  109. package/src/qingflow_mcp/server.py +14 -12
  110. package/src/qingflow_mcp/server_app_builder.py +65 -21
  111. package/src/qingflow_mcp/server_app_user.py +22 -16
  112. package/src/qingflow_mcp/session_store.py +11 -7
  113. package/src/qingflow_mcp/solution/compiler/__init__.py +3 -1
  114. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  115. package/src/qingflow_mcp/solution/executor.py +245 -18
  116. package/src/qingflow_mcp/tools/ai_builder_tools.py +1782 -399
  117. package/src/qingflow_mcp/tools/app_tools.py +184 -43
  118. package/src/qingflow_mcp/tools/approval_tools.py +197 -35
  119. package/src/qingflow_mcp/tools/auth_tools.py +92 -16
  120. package/src/qingflow_mcp/tools/code_block_tools.py +298 -40
  121. package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
  122. package/src/qingflow_mcp/tools/directory_tools.py +236 -72
  123. package/src/qingflow_mcp/tools/export_tools.py +244 -34
  124. package/src/qingflow_mcp/tools/feedback_tools.py +9 -0
  125. package/src/qingflow_mcp/tools/file_tools.py +9 -3
  126. package/src/qingflow_mcp/tools/import_tools.py +336 -49
  127. package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
  128. package/src/qingflow_mcp/tools/package_tools.py +118 -6
  129. package/src/qingflow_mcp/tools/portal_tools.py +39 -3
  130. package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
  131. package/src/qingflow_mcp/tools/record_tools.py +1141 -356
  132. package/src/qingflow_mcp/tools/resource_read_tools.py +188 -39
  133. package/src/qingflow_mcp/tools/role_tools.py +80 -9
  134. package/src/qingflow_mcp/tools/solution_tools.py +59 -45
  135. package/src/qingflow_mcp/tools/task_context_tools.py +662 -158
  136. package/src/qingflow_mcp/tools/task_tools.py +113 -29
  137. package/src/qingflow_mcp/tools/view_tools.py +106 -3
  138. package/src/qingflow_mcp/tools/workflow_tools.py +48 -4
  139. package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
  140. /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_MATCH_RULES.md → builder/reference/match-rules.md} +0 -0
  141. /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_WORKSPACE_ICONS.md → builder/reference/workspace-icons.md} +0 -0
  142. /package/skills/qingflow-cli/reference/{charts_remove.example.json → examples/charts/charts_remove.example.json} +0 -0
  143. /package/skills/qingflow-cli/reference/{charts_reorder.example.json → examples/charts/charts_reorder.example.json} +0 -0
  144. /package/skills/qingflow-cli/reference/{charts_upsert_bar.example.json → examples/charts/charts_upsert_bar.example.json} +0 -0
  145. /package/skills/qingflow-cli/reference/{charts_upsert_dashboard_starter.example.json → examples/charts/charts_upsert_dashboard_starter.example.json} +0 -0
  146. /package/skills/qingflow-cli/reference/{charts_upsert_minimal.example.json → examples/charts/charts_upsert_minimal.example.json} +0 -0
  147. /package/skills/qingflow-cli/reference/{portal_sections_all_types.example.json → examples/portal/portal_sections_all_types.example.json} +0 -0
  148. /package/skills/qingflow-cli/reference/{portal_sections_five_types.example.json → examples/portal/portal_sections_five_types.example.json} +0 -0
  149. /package/skills/qingflow-cli/reference/{portal_sections_standard_workbench.example.json → examples/portal/portal_sections_standard_workbench.example.json} +0 -0
  150. /package/skills/qingflow-cli/reference/{_batch_schema_complex.json → examples/schema/_batch_schema_complex.json} +0 -0
  151. /package/skills/qingflow-cli/reference/{_batch_schema_scalar.json → examples/schema/_batch_schema_scalar.json} +0 -0
  152. /package/skills/qingflow-cli/reference/{schema_add_fields_minimal.example.json → examples/schema/schema_add_fields_minimal.example.json} +0 -0
  153. /package/skills/qingflow-cli/reference/{schema_apply_add_fields_all_types.json → examples/schema/schema_apply_add_fields_all_types.json} +0 -0
  154. /package/skills/qingflow-cli/reference/{views_upsert_table_minimal.example.json → examples/views/views_upsert_table_minimal.example.json} +0 -0
@@ -15,17 +15,22 @@ from ..builder_facade.button_style_catalog import (
15
15
  )
16
16
  from ..public_surface import public_builder_contract_tool_names
17
17
  from ..config import DEFAULT_PROFILE
18
- from ..errors import QingflowApiError
18
+ from ..errors import QingflowApiError, backend_code_int
19
19
  from ..json_types import JSONObject
20
20
  from ..builder_facade.models import (
21
21
  AssociatedResourcesApplyRequest,
22
22
  ChartApplyRequest,
23
23
  CustomButtonsApplyRequest,
24
24
  CustomButtonPatch,
25
+ FIELD_TYPE_ALIASES,
25
26
  FIELD_TYPE_ID_ALIASES,
26
27
  FieldPatch,
27
28
  FieldRemovePatch,
28
29
  FieldUpdatePatch,
30
+ FlowPreset,
31
+ FlowNodePatch,
32
+ FlowPlanRequest,
33
+ FlowTransitionPatch,
29
34
  LayoutApplyMode,
30
35
  LayoutPlanRequest,
31
36
  LayoutPreset,
@@ -65,6 +70,7 @@ from .qingbi_report_tools import QingbiReportTools
65
70
  from .role_tools import RoleTools
66
71
  from .solution_tools import SolutionTools
67
72
  from .view_tools import ViewTools
73
+ from .workflow_tools import WorkflowTools
68
74
 
69
75
 
70
76
  def _normalize_builder_view_key(value: str) -> str:
@@ -74,6 +80,32 @@ def _normalize_builder_view_key(value: str) -> str:
74
80
  return raw
75
81
 
76
82
 
83
+ def _payload_get(payload: JSONObject, *keys: str, default: Any = None) -> Any:
84
+ for key in keys:
85
+ if key in payload:
86
+ return payload.get(key)
87
+ return default
88
+
89
+
90
+ def _payload_list(payload: JSONObject, *keys: str, default: list | None = None) -> list:
91
+ value = _payload_get(payload, *keys, default=default if default is not None else [])
92
+ return value if isinstance(value, list) else []
93
+
94
+
95
+ def _payload_bool(payload: JSONObject, *keys: str, default: bool = True) -> bool:
96
+ value = _payload_get(payload, *keys, default=default)
97
+ if isinstance(value, bool):
98
+ return value
99
+ if isinstance(value, str):
100
+ normalized = value.strip().lower()
101
+ if normalized in {"1", "true", "yes", "y", "on"}:
102
+ return True
103
+ if normalized in {"0", "false", "no", "n", "off"}:
104
+ return False
105
+ return bool(value)
106
+
107
+
108
+ PUBLIC_STABLE_FLOW_NODE_TYPES = ["start", "approve", "fill", "copy", "webhook", "end"]
77
109
  BUILDER_APPLY_SCHEMA_VERSION = "builder.apply.v1"
78
110
  BUILDER_APPLY_TOOL_NAMES = {
79
111
  "package_apply",
@@ -85,6 +117,7 @@ BUILDER_APPLY_TOOL_NAMES = {
85
117
  "app_associated_resources_apply",
86
118
  "app_charts_apply",
87
119
  "portal_apply",
120
+ "portal_delete",
88
121
  "app_publish_verify",
89
122
  }
90
123
 
@@ -107,6 +140,7 @@ class AiBuilderTools(ToolBase):
107
140
  buttons=CustomButtonTools(sessions, backend),
108
141
  packages=PackageTools(sessions, backend),
109
142
  views=ViewTools(sessions, backend),
143
+ workflows=WorkflowTools(sessions, backend),
110
144
  portals=PortalTools(sessions, backend),
111
145
  charts=QingbiReportTools(sessions, backend),
112
146
  roles=RoleTools(sessions, backend),
@@ -114,6 +148,165 @@ class AiBuilderTools(ToolBase):
114
148
  solutions=SolutionTools(sessions, backend),
115
149
  )
116
150
 
151
+ def _apply_app_batch(self, *, tool_name: str, profile: str, apps: list[JSONObject], apply_one) -> JSONObject:
152
+ if not isinstance(apps, list) or not apps:
153
+ return _attach_builder_apply_envelope(tool_name, _config_failure(
154
+ tool_name=tool_name,
155
+ message=f"{tool_name} batch mode requires non-empty apps[].",
156
+ fix_hint="Pass apps as a JSON array; each item must include app_key plus that resource's normal payload.",
157
+ details={"expected_shape": {"apps": [{"app_key": "APP_KEY"}]}},
158
+ ))
159
+
160
+ app_results: list[JSONObject] = []
161
+ errors: list[JSONObject] = []
162
+ for index, item in enumerate(apps):
163
+ if not isinstance(item, dict):
164
+ error = {
165
+ "index": index,
166
+ "status": "failed",
167
+ "error_code": "APPS_FILE_ITEM_INVALID",
168
+ "message": "apps[] item must be an object",
169
+ "reason_path": f"apps[{index}]",
170
+ }
171
+ errors.append(error)
172
+ app_results.append(error)
173
+ continue
174
+ app_key = str(item.get("app_key") or item.get("appKey") or "").strip()
175
+ if not app_key:
176
+ error = {
177
+ "index": index,
178
+ "status": "failed",
179
+ "error_code": "APPS_FILE_APP_KEY_REQUIRED",
180
+ "message": "apps[] item requires app_key",
181
+ "reason_path": f"apps[{index}].app_key",
182
+ }
183
+ errors.append(error)
184
+ app_results.append(error)
185
+ continue
186
+ try:
187
+ result = apply_one(index, item, app_key)
188
+ except (QingflowApiError, RuntimeError) as error:
189
+ api_error = _coerce_api_error(error)
190
+ result = {
191
+ "status": "failed",
192
+ "error_code": f"{tool_name.upper()}_BATCH_ITEM_FAILED",
193
+ "recoverable": True,
194
+ "message": _public_error_message(f"{tool_name.upper()}_BATCH_ITEM_FAILED", api_error),
195
+ "app_key": app_key,
196
+ "normalized_args": {"app_key": app_key},
197
+ "details": {"transport_error": _transport_error_payload(api_error)},
198
+ "request_id": api_error.request_id,
199
+ "backend_code": api_error.backend_code,
200
+ "http_status": None if api_error.http_status == 404 else api_error.http_status,
201
+ }
202
+ status = str(result.get("status") or "success") if isinstance(result, dict) else "failed"
203
+ wrapped: JSONObject = {
204
+ "index": index,
205
+ "app_key": app_key,
206
+ "status": status,
207
+ "result": result if isinstance(result, dict) else {"status": "failed", "message": str(result)},
208
+ }
209
+ if status == "failed":
210
+ error = {
211
+ "index": index,
212
+ "app_key": app_key,
213
+ "status": status,
214
+ "error_code": wrapped["result"].get("error_code"),
215
+ "message": wrapped["result"].get("message"),
216
+ }
217
+ errors.append(error)
218
+ wrapped["error"] = error
219
+ app_results.append(wrapped)
220
+
221
+ succeeded = sum(1 for item in app_results if str(item.get("status") or "") in {"success", "partial_success"})
222
+ failed = len(app_results) - succeeded
223
+ status = "success" if failed == 0 else "failed" if succeeded == 0 else "partial_success"
224
+ write_executed = any(
225
+ isinstance(item.get("result"), dict) and bool(item["result"].get("write_executed"))
226
+ for item in app_results
227
+ )
228
+ payload: JSONObject = {
229
+ "status": status,
230
+ "error_code": None if status == "success" else f"{tool_name.upper()}_BATCH_PARTIAL" if succeeded else f"{tool_name.upper()}_BATCH_FAILED",
231
+ "recoverable": failed > 0,
232
+ "message": (
233
+ f"applied {tool_name} to {succeeded}/{len(app_results)} apps"
234
+ if status != "success"
235
+ else f"applied {tool_name} to {succeeded} apps"
236
+ ),
237
+ "normalized_args": {"apps": apps},
238
+ "apps": app_results,
239
+ "errors": errors,
240
+ "verification": {
241
+ "batch_verified": failed == 0,
242
+ "succeeded": succeeded,
243
+ "failed": failed,
244
+ },
245
+ "write_executed": write_executed,
246
+ "write_succeeded": write_executed and failed == 0,
247
+ "safe_to_retry": not write_executed,
248
+ }
249
+ return _attach_builder_apply_envelope(tool_name, payload)
250
+
251
+ def _read_app_batch(self, *, tool_name: str, profile: str, app_keys: list[str], read_one) -> JSONObject:
252
+ normalized_keys = [str(item).strip() for item in app_keys if str(item).strip()]
253
+ if not normalized_keys:
254
+ return _config_failure(
255
+ tool_name=tool_name,
256
+ message=f"{tool_name} batch mode requires non-empty app_keys[].",
257
+ fix_hint="Pass app_keys as a non-empty list of app keys.",
258
+ details={"expected_shape": {"app_keys": ["APP_A", "APP_B"]}},
259
+ )
260
+ apps: list[JSONObject] = []
261
+ errors: list[JSONObject] = []
262
+ for index, app_key in enumerate(normalized_keys):
263
+ try:
264
+ result = read_one(app_key)
265
+ except (QingflowApiError, RuntimeError) as error:
266
+ api_error = _coerce_api_error(error)
267
+ result = {
268
+ "status": "failed",
269
+ "error_code": f"{tool_name.upper()}_BATCH_ITEM_FAILED",
270
+ "recoverable": True,
271
+ "message": _public_error_message(f"{tool_name.upper()}_BATCH_ITEM_FAILED", api_error),
272
+ "app_key": app_key,
273
+ "details": {"transport_error": _transport_error_payload(api_error)},
274
+ "request_id": api_error.request_id,
275
+ "backend_code": api_error.backend_code,
276
+ "http_status": None if api_error.http_status == 404 else api_error.http_status,
277
+ }
278
+ status = str(result.get("status") or "success") if isinstance(result, dict) else "failed"
279
+ app_item: JSONObject = {
280
+ "index": index,
281
+ "app_key": app_key,
282
+ "status": status,
283
+ "data": result,
284
+ }
285
+ if status == "failed":
286
+ error = {
287
+ "index": index,
288
+ "app_key": app_key,
289
+ "status": "failed",
290
+ "error_code": result.get("error_code") if isinstance(result, dict) else None,
291
+ "message": result.get("message") if isinstance(result, dict) else str(result),
292
+ }
293
+ app_item["error"] = error
294
+ errors.append(error)
295
+ apps.append(app_item)
296
+ succeeded = sum(1 for item in apps if str(item.get("status") or "") in {"success", "partial_success"})
297
+ failed = len(apps) - succeeded
298
+ return {
299
+ "status": "success" if failed == 0 else "failed" if succeeded == 0 else "partial_success",
300
+ "error_code": None if failed == 0 else f"{tool_name.upper()}_BATCH_PARTIAL" if succeeded else f"{tool_name.upper()}_BATCH_FAILED",
301
+ "recoverable": failed > 0,
302
+ "message": f"read {tool_name} for {succeeded}/{len(apps)} apps" if failed else f"read {tool_name} for {succeeded} apps",
303
+ "normalized_args": {"app_keys": normalized_keys},
304
+ "app_keys": normalized_keys,
305
+ "apps": apps,
306
+ "errors": errors,
307
+ "verification": {"batch_verified": failed == 0, "succeeded": succeeded, "failed": failed},
308
+ }
309
+
117
310
  def register(self, mcp) -> None:
118
311
  """注册当前工具到 MCP 服务。"""
119
312
  @mcp.tool()
@@ -138,8 +331,11 @@ class AiBuilderTools(ToolBase):
138
331
  package_id: int | None = None,
139
332
  package_name: str | None = None,
140
333
  create_if_missing: bool = False,
141
- icon: str | None = None,
334
+ icon: str | JSONObject | None = None,
142
335
  color: str | None = None,
336
+ icon_name: str | None = None,
337
+ icon_color: str | None = None,
338
+ icon_config: JSONObject | None = None,
143
339
  visibility: JSONObject | None = None,
144
340
  items: list[dict] | None = None,
145
341
  allow_detach: bool = False,
@@ -151,6 +347,9 @@ class AiBuilderTools(ToolBase):
151
347
  create_if_missing=create_if_missing,
152
348
  icon=icon,
153
349
  color=color,
350
+ icon_name=icon_name,
351
+ icon_color=icon_color,
352
+ icon_config=icon_config,
154
353
  visibility=visibility,
155
354
  items=items or None,
156
355
  allow_detach=allow_detach,
@@ -298,14 +497,12 @@ class AiBuilderTools(ToolBase):
298
497
  )
299
498
 
300
499
  @mcp.tool()
301
- def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
302
- return self.app_get(profile=profile, app_key=app_key)
500
+ def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
501
+ return self.app_get(profile=profile, app_key=app_key, app_keys=app_keys)
303
502
 
304
503
  @mcp.tool()
305
504
  def app_get_fields(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
306
- if app_keys:
307
- return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_fields, data_key="fields", tool_name="app_get_fields")
308
- return self.app_get_fields(profile=profile, app_key=app_key)
505
+ return self.app_get_fields(profile=profile, app_key=app_key, app_keys=app_keys)
309
506
 
310
507
  @mcp.tool()
311
508
  def app_repair_code_blocks(
@@ -318,39 +515,27 @@ class AiBuilderTools(ToolBase):
318
515
 
319
516
  @mcp.tool()
320
517
  def app_get_layout(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
321
- if app_keys:
322
- return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_layout, data_key="sections", tool_name="app_get_layout")
323
- return self.app_get_layout(profile=profile, app_key=app_key)
518
+ return self.app_get_layout(profile=profile, app_key=app_key, app_keys=app_keys)
324
519
 
325
520
  @mcp.tool()
326
521
  def app_get_views(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
327
- if app_keys:
328
- return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_views, data_key="views", tool_name="app_get_views")
329
- return self.app_get_views(profile=profile, app_key=app_key)
522
+ return self.app_get_views(profile=profile, app_key=app_key, app_keys=app_keys)
330
523
 
331
524
  @mcp.tool()
332
525
  def app_get_flow(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
333
- if app_keys:
334
- return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_flow, data_key="spec", tool_name="app_get_flow")
335
- return self.app_get_flow(profile=profile, app_key=app_key)
526
+ return self.app_get_flow(profile=profile, app_key=app_key, app_keys=app_keys)
336
527
 
337
528
  @mcp.tool()
338
529
  def app_get_charts(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
339
- if app_keys:
340
- return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_charts, data_key="charts", tool_name="app_get_charts")
341
- return self.app_get_charts(profile=profile, app_key=app_key)
530
+ return self.app_get_charts(profile=profile, app_key=app_key, app_keys=app_keys)
342
531
 
343
532
  @mcp.tool()
344
533
  def app_get_buttons(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
345
- if app_keys:
346
- return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_buttons, data_key="buttons", tool_name="app_get_buttons")
347
- return self._facade.app_get_buttons(profile=profile, app_key=app_key)
534
+ return self.app_get_buttons(profile=profile, app_key=app_key, app_keys=app_keys)
348
535
 
349
536
  @mcp.tool()
350
537
  def app_get_associated_resources(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
351
- if app_keys:
352
- return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_associated_resources, data_key="associated_resources", tool_name="app_get_associated_resources")
353
- return self._facade.app_get_associated_resources(profile=profile, app_key=app_key)
538
+ return self.app_get_associated_resources(profile=profile, app_key=app_key, app_keys=app_keys)
354
539
 
355
540
  @mcp.tool()
356
541
  def portal_list(profile: str = DEFAULT_PROFILE) -> JSONObject:
@@ -382,8 +567,11 @@ class AiBuilderTools(ToolBase):
382
567
  package_id: int | None = None,
383
568
  app_name: str = "",
384
569
  app_title: str = "",
385
- icon: str = "",
570
+ icon: str | JSONObject = "",
386
571
  color: str = "",
572
+ icon_name: str | None = None,
573
+ icon_color: str | None = None,
574
+ icon_config: JSONObject | None = None,
387
575
  visibility: JSONObject | None = None,
388
576
  create_if_missing: bool = False,
389
577
  publish: bool = True,
@@ -392,7 +580,7 @@ class AiBuilderTools(ToolBase):
392
580
  remove_fields: list[JSONObject] | None = None,
393
581
  apps: list[JSONObject] | None = None,
394
582
  ) -> JSONObject:
395
- if apps:
583
+ if apps is not None:
396
584
  if app_key or app_name or app_title or add_fields or update_fields or remove_fields:
397
585
  return _config_failure(
398
586
  tool_name="app_schema_apply",
@@ -441,6 +629,9 @@ class AiBuilderTools(ToolBase):
441
629
  app_title=app_title,
442
630
  icon=icon,
443
631
  color=color,
632
+ icon_name=icon_name,
633
+ icon_color=icon_color,
634
+ icon_config=icon_config,
444
635
  visibility=visibility,
445
636
  create_if_missing=create_if_missing,
446
637
  publish=publish,
@@ -467,7 +658,7 @@ class AiBuilderTools(ToolBase):
467
658
  app_key: str = "",
468
659
  version_id: str = "",
469
660
  ) -> JSONObject:
470
- return self.app_get_flow(profile=profile, app_key=app_key, version_id=version_id or None)
661
+ return self.app_flow_get(profile=profile, app_key=app_key, version_id=version_id or None)
471
662
 
472
663
  @mcp.tool()
473
664
  def app_flow_get_schema(profile: str = DEFAULT_PROFILE, schema_version: str = "") -> JSONObject:
@@ -477,7 +668,10 @@ class AiBuilderTools(ToolBase):
477
668
  def app_flow_apply(
478
669
  profile: str = DEFAULT_PROFILE,
479
670
  app_key: str = "",
671
+ mode: str = "replace",
480
672
  publish: bool = True,
673
+ nodes: list[JSONObject] | None = None,
674
+ transitions: list[JSONObject] | None = None,
481
675
  spec: JSONObject | None = None,
482
676
  idempotency_key: str = "",
483
677
  schema_version: str = "",
@@ -486,8 +680,11 @@ class AiBuilderTools(ToolBase):
486
680
  return self.app_flow_apply(
487
681
  profile=profile,
488
682
  app_key=app_key,
683
+ mode=mode,
489
684
  publish=publish,
490
- spec=spec or {},
685
+ nodes=nodes or [],
686
+ transitions=transitions or [],
687
+ spec=spec,
491
688
  idempotency_key=idempotency_key or None,
492
689
  schema_version=schema_version or None,
493
690
  patch_nodes=patch_nodes,
@@ -593,6 +790,13 @@ class AiBuilderTools(ToolBase):
593
790
  patch_sections=patch_sections,
594
791
  )
595
792
 
793
+ @mcp.tool(description=self._high_risk_tool_description(operation="delete", target="portal"))
794
+ def portal_delete(
795
+ profile: str = DEFAULT_PROFILE,
796
+ dash_key: str = "",
797
+ ) -> JSONObject:
798
+ return self.portal_delete(profile=profile, dash_key=dash_key)
799
+
596
800
  @mcp.tool()
597
801
  def app_publish_verify(
598
802
  profile: str = DEFAULT_PROFILE,
@@ -600,17 +804,10 @@ class AiBuilderTools(ToolBase):
600
804
  app_keys: list[str] | None = None,
601
805
  expected_package_id: int | None = None,
602
806
  ) -> JSONObject:
603
- if app_keys:
604
- return self._facade._batch_read_app_keys(
605
- profile=profile,
606
- app_keys=app_keys,
607
- single_reader=lambda profile, app_key: self.app_publish_verify(profile=profile, app_key=app_key, expected_package_id=expected_package_id),
608
- data_key="verification",
609
- tool_name="app_publish_verify",
610
- )
611
807
  return self.app_publish_verify(
612
808
  profile=profile,
613
809
  app_key=app_key,
810
+ app_keys=app_keys,
614
811
  expected_package_id=expected_package_id,
615
812
  )
616
813
 
@@ -659,15 +856,16 @@ class AiBuilderTools(ToolBase):
659
856
  "allowed_values": {"tool_name": public_tool_names},
660
857
  "details": {"reason_path": "tool_name"},
661
858
  "suggested_next_call": None,
662
- "request_id": None,
663
- "backend_code": None,
664
- "http_status": None,
665
- "noop": False,
666
- "warnings": [],
667
- "verification": {},
668
- "verified": False,
669
- }
859
+ "request_id": None,
860
+ "backend_code": None,
861
+ "http_status": None,
862
+ "noop": False,
863
+ "warnings": [],
864
+ "verification": {},
865
+ "verified": False,
866
+ }
670
867
  contract = _builder_contract_with_apply_output(lookup_name, contract)
868
+ contract_summary = _builder_tool_contract_summary(lookup_name, contract)
671
869
  return {
672
870
  "status": "success",
673
871
  "error_code": None,
@@ -686,6 +884,8 @@ class AiBuilderTools(ToolBase):
686
884
  "verification": {},
687
885
  "verified": True,
688
886
  "tool_name": requested,
887
+ "summary": contract_summary,
888
+ "json_paths": contract_summary["json_paths"],
689
889
  "contract": contract,
690
890
  }
691
891
 
@@ -708,6 +908,12 @@ class AiBuilderTools(ToolBase):
708
908
  "color_count": len(catalog["icon_colors"]),
709
909
  "warnings": [],
710
910
  "verification": {"source": "backend AiBuildConstant ICON_NAMES/ICON_COLORS"},
911
+ "json_paths": {
912
+ "icon_names": "$.icon_names",
913
+ "icon_colors": "$.icon_colors",
914
+ "generic_icon_names": "$.generic_icon_names",
915
+ "common_examples": "$.common_examples",
916
+ },
711
917
  }
712
918
 
713
919
  @tool_cn_name("分组创建")
@@ -774,13 +980,23 @@ class AiBuilderTools(ToolBase):
774
980
  package_id: int | None = None,
775
981
  package_name: str | None = None,
776
982
  create_if_missing: bool = False,
777
- icon: str | None = None,
983
+ icon: str | JSONObject | None = None,
778
984
  color: str | None = None,
985
+ icon_name: str | None = None,
986
+ icon_color: str | None = None,
987
+ icon_config: JSONObject | None = None,
779
988
  visibility: JSONObject | None = None,
780
989
  items: list[dict] | None = None,
781
990
  allow_detach: bool = False,
782
991
  ) -> JSONObject:
783
992
  """执行分组与包相关逻辑。"""
993
+ icon, color = _normalize_builder_icon_args(
994
+ icon=icon,
995
+ color=color,
996
+ icon_name=icon_name,
997
+ icon_color=icon_color,
998
+ icon_config=icon_config,
999
+ )
784
1000
  visibility_patch = None
785
1001
  if visibility is not None:
786
1002
  try:
@@ -903,16 +1119,23 @@ class AiBuilderTools(ToolBase):
903
1119
  contain_disable: bool = False,
904
1120
  ) -> JSONObject:
905
1121
  """执行工具方法逻辑。"""
1122
+ normalized_query = str(query or "").strip()
906
1123
  normalized_args = {
907
- "query": query,
1124
+ "query": normalized_query,
908
1125
  "page_num": page_num,
909
1126
  "page_size": page_size,
910
1127
  "contain_disable": contain_disable,
911
1128
  }
1129
+ if not normalized_query:
1130
+ return _config_failure(
1131
+ tool_name="member_search",
1132
+ message="query is required for member_search; builder member lookup is a contact-directory path, not a record candidate fallback.",
1133
+ fix_hint="For record member/department field ambiguity, use record member-candidates / department-candidates instead.",
1134
+ )
912
1135
  return _safe_tool_call(
913
1136
  lambda: self._facade.member_search(
914
1137
  profile=profile,
915
- query=query,
1138
+ query=normalized_query,
916
1139
  page_num=page_num,
917
1140
  page_size=page_size,
918
1141
  contain_disable=contain_disable,
@@ -925,9 +1148,16 @@ class AiBuilderTools(ToolBase):
925
1148
  @tool_cn_name("角色检索")
926
1149
  def role_search(self, *, profile: str, keyword: str, page_num: int = 1, page_size: int = 20) -> JSONObject:
927
1150
  """执行角色相关逻辑。"""
928
- normalized_args = {"keyword": keyword, "page_num": page_num, "page_size": page_size}
1151
+ normalized_keyword = str(keyword or "").strip()
1152
+ normalized_args = {"keyword": normalized_keyword, "page_num": page_num, "page_size": page_size}
1153
+ if not normalized_keyword:
1154
+ return _config_failure(
1155
+ tool_name="role_search",
1156
+ message="keyword is required for role_search; builder role lookup is a contact-management path, not a record candidate fallback.",
1157
+ fix_hint="For record member/department field ambiguity, use record member-candidates / department-candidates instead.",
1158
+ )
929
1159
  return _safe_tool_call(
930
- lambda: self._facade.role_search(profile=profile, keyword=keyword, page_num=page_num, page_size=page_size),
1160
+ lambda: self._facade.role_search(profile=profile, keyword=normalized_keyword, page_num=page_num, page_size=page_size),
931
1161
  error_code="ROLE_SEARCH_FAILED",
932
1162
  normalized_args=normalized_args,
933
1163
  suggested_next_call={"tool_name": "role_search", "arguments": {"profile": profile, **normalized_args}},
@@ -1056,19 +1286,20 @@ class AiBuilderTools(ToolBase):
1056
1286
  apps: list[JSONObject] | None = None,
1057
1287
  ) -> JSONObject:
1058
1288
  """执行应用按钮 apply 逻辑。"""
1059
- if apps:
1060
- return self._facade._batch_write_apps(
1289
+ if apps is not None:
1290
+ return self._apply_app_batch(
1291
+ tool_name="app_custom_buttons_apply",
1061
1292
  profile=profile,
1062
1293
  apps=apps,
1063
- single_writer=lambda profile, app_key, **kw: self.app_custom_buttons_apply(
1294
+ apply_one=lambda _index, item, item_app_key: self.app_custom_buttons_apply(
1064
1295
  profile=profile,
1065
- app_key=app_key,
1066
- upsert_buttons=kw.get("upsert_buttons", []),
1067
- patch_buttons=kw.get("patch_buttons", []),
1068
- remove_buttons=kw.get("remove_buttons", []),
1069
- view_configs=kw.get("view_configs", []),
1296
+ app_key=item_app_key,
1297
+ upsert_buttons=_payload_list(item, "upsert_buttons", "upsertButtons", "buttons", default=upsert_buttons or []),
1298
+ patch_buttons=_payload_list(item, "patch_buttons", "patchButtons", default=patch_buttons or []),
1299
+ remove_buttons=_payload_list(item, "remove_buttons", "removeButtons", default=remove_buttons or []),
1300
+ view_configs=_payload_list(item, "view_configs", "viewConfigs", default=view_configs or []),
1301
+ apps=None,
1070
1302
  ),
1071
- tool_name="app_custom_buttons_apply",
1072
1303
  )
1073
1304
  raw_request = {
1074
1305
  "app_key": app_key,
@@ -1125,20 +1356,31 @@ class AiBuilderTools(ToolBase):
1125
1356
  apps: list[JSONObject] | None = None,
1126
1357
  ) -> JSONObject:
1127
1358
  """执行应用关联资源 apply 逻辑。"""
1128
- if apps:
1129
- return self._facade._batch_write_apps(
1359
+ if apps is not None:
1360
+ return self._apply_app_batch(
1361
+ tool_name="app_associated_resources_apply",
1130
1362
  profile=profile,
1131
1363
  apps=apps,
1132
- single_writer=lambda profile, app_key, **kw: self.app_associated_resources_apply(
1364
+ apply_one=lambda _index, item, item_app_key: self.app_associated_resources_apply(
1133
1365
  profile=profile,
1134
- app_key=app_key,
1135
- upsert_resources=kw.get("upsert_resources", []),
1136
- patch_resources=kw.get("patch_resources", []),
1137
- remove_associated_item_ids=kw.get("remove_associated_item_ids", []),
1138
- reorder_associated_item_ids=kw.get("reorder_associated_item_ids", []),
1139
- view_configs=kw.get("view_configs", []),
1366
+ app_key=item_app_key,
1367
+ upsert_resources=_payload_list(item, "upsert_resources", "upsertResources", "resources", default=upsert_resources or []),
1368
+ patch_resources=_payload_list(item, "patch_resources", "patchResources", default=patch_resources or []),
1369
+ remove_associated_item_ids=_payload_list(
1370
+ item,
1371
+ "remove_associated_item_ids",
1372
+ "removeAssociatedItemIds",
1373
+ default=remove_associated_item_ids or [],
1374
+ ),
1375
+ reorder_associated_item_ids=_payload_list(
1376
+ item,
1377
+ "reorder_associated_item_ids",
1378
+ "reorderAssociatedItemIds",
1379
+ default=reorder_associated_item_ids or [],
1380
+ ),
1381
+ view_configs=_payload_list(item, "view_configs", "viewConfigs", default=view_configs or []),
1382
+ apps=None,
1140
1383
  ),
1141
- tool_name="app_associated_resources_apply",
1142
1384
  )
1143
1385
  raw_request = {
1144
1386
  "app_key": app_key,
@@ -1302,8 +1544,15 @@ class AiBuilderTools(ToolBase):
1302
1544
  ))
1303
1545
 
1304
1546
  @tool_cn_name("应用详情查询")
1305
- def app_get(self, *, profile: str, app_key: str) -> JSONObject:
1547
+ def app_get(self, *, profile: str, app_key: str, app_keys: list[str] | None = None) -> JSONObject:
1306
1548
  """执行应用相关逻辑。"""
1549
+ if app_keys is not None:
1550
+ return self._read_app_batch(
1551
+ tool_name="app_get",
1552
+ profile=profile,
1553
+ app_keys=app_keys,
1554
+ read_one=lambda item_app_key: self.app_get(profile=profile, app_key=item_app_key, app_keys=None),
1555
+ )
1307
1556
  normalized_args = {"app_key": app_key}
1308
1557
  return _publicize_package_fields(_safe_tool_call(
1309
1558
  lambda: self._facade.app_get(profile=profile, app_key=app_key),
@@ -1324,10 +1573,15 @@ class AiBuilderTools(ToolBase):
1324
1573
  )
1325
1574
 
1326
1575
  @tool_cn_name("应用字段详情查询")
1327
- def app_get_fields(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
1576
+ def app_get_fields(self, *, profile: str, app_key: str, app_keys: list[str] | None = None) -> JSONObject:
1328
1577
  """执行应用相关逻辑。"""
1329
- if app_keys:
1330
- return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_fields, data_key="fields", tool_name="app_get_fields")
1578
+ if app_keys is not None:
1579
+ return self._read_app_batch(
1580
+ tool_name="app_get_fields",
1581
+ profile=profile,
1582
+ app_keys=app_keys,
1583
+ read_one=lambda item_app_key: self.app_get_fields(profile=profile, app_key=item_app_key, app_keys=None),
1584
+ )
1331
1585
  normalized_args = {"app_key": app_key}
1332
1586
  return _safe_tool_call(
1333
1587
  lambda: self._facade.app_get_fields(profile=profile, app_key=app_key),
@@ -1366,10 +1620,15 @@ class AiBuilderTools(ToolBase):
1366
1620
  )
1367
1621
 
1368
1622
  @tool_cn_name("应用布局详情查询")
1369
- def app_get_layout(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
1623
+ def app_get_layout(self, *, profile: str, app_key: str, app_keys: list[str] | None = None) -> JSONObject:
1370
1624
  """执行应用相关逻辑。"""
1371
- if app_keys:
1372
- return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_layout, data_key="sections", tool_name="app_get_layout")
1625
+ if app_keys is not None:
1626
+ return self._read_app_batch(
1627
+ tool_name="app_get_layout",
1628
+ profile=profile,
1629
+ app_keys=app_keys,
1630
+ read_one=lambda item_app_key: self.app_get_layout(profile=profile, app_key=item_app_key, app_keys=None),
1631
+ )
1373
1632
  normalized_args = {"app_key": app_key}
1374
1633
  return _safe_tool_call(
1375
1634
  lambda: self._facade.app_get_layout(profile=profile, app_key=app_key),
@@ -1390,10 +1649,15 @@ class AiBuilderTools(ToolBase):
1390
1649
  )
1391
1650
 
1392
1651
  @tool_cn_name("应用视图详情查询")
1393
- def app_get_views(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
1652
+ def app_get_views(self, *, profile: str, app_key: str, app_keys: list[str] | None = None) -> JSONObject:
1394
1653
  """执行应用相关逻辑。"""
1395
- if app_keys:
1396
- return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_views, data_key="views", tool_name="app_get_views")
1654
+ if app_keys is not None:
1655
+ return self._read_app_batch(
1656
+ tool_name="app_get_views",
1657
+ profile=profile,
1658
+ app_keys=app_keys,
1659
+ read_one=lambda item_app_key: self.app_get_views(profile=profile, app_key=item_app_key, app_keys=None),
1660
+ )
1397
1661
  normalized_args = {"app_key": app_key}
1398
1662
  return _safe_tool_call(
1399
1663
  lambda: self._facade.app_get_views(profile=profile, app_key=app_key),
@@ -1402,27 +1666,53 @@ class AiBuilderTools(ToolBase):
1402
1666
  suggested_next_call={"tool_name": "app_get_views", "arguments": {"profile": profile, "app_key": app_key}},
1403
1667
  )
1404
1668
 
1405
- @tool_cn_name("Workflow Spec Schema")
1406
- def app_flow_get_schema(self, *, profile: str, schema_version: str | None = None) -> JSONObject:
1407
- normalized_args = {"schema_version": schema_version}
1669
+ @tool_cn_name("应用流程摘要读取")
1670
+ def app_read_flow_summary(self, *, profile: str, app_key: str) -> JSONObject:
1671
+ """执行应用相关逻辑。"""
1672
+ normalized_args = {"app_key": app_key}
1408
1673
  return _safe_tool_call(
1409
- lambda: self._facade.flow_get_schema(profile=profile, schema_version=schema_version),
1410
- error_code="FLOW_SPEC_SCHEMA_FAILED",
1674
+ lambda: self._facade.app_read_flow_summary(profile=profile, app_key=app_key),
1675
+ error_code="FLOW_READ_FAILED",
1411
1676
  normalized_args=normalized_args,
1412
- suggested_next_call={"tool_name": "app_flow_get_schema", "arguments": {"profile": profile, **normalized_args}},
1677
+ suggested_next_call={"tool_name": "app_get_flow", "arguments": {"profile": profile, "app_key": app_key}},
1413
1678
  )
1414
1679
 
1415
- @tool_cn_name("Workflow Spec 读取")
1416
- def app_get_flow(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None, version_id: str | None = None) -> JSONObject:
1680
+ @tool_cn_name("应用流程详情查询")
1681
+ def app_get_flow(self, *, profile: str, app_key: str, app_keys: list[str] | None = None) -> JSONObject:
1417
1682
  """执行应用相关逻辑。"""
1418
- if app_keys:
1419
- return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_flow, data_key="spec", tool_name="app_get_flow")
1683
+ if app_keys is not None:
1684
+ return self._read_app_batch(
1685
+ tool_name="app_get_flow",
1686
+ profile=profile,
1687
+ app_keys=app_keys,
1688
+ read_one=lambda item_app_key: self.app_get_flow(profile=profile, app_key=item_app_key, app_keys=None),
1689
+ )
1690
+ normalized_args = {"app_key": app_key}
1691
+ return _safe_tool_call(
1692
+ lambda: self._facade.app_get_flow(profile=profile, app_key=app_key),
1693
+ error_code="APP_GET_FLOW_FAILED",
1694
+ normalized_args=normalized_args,
1695
+ suggested_next_call={"tool_name": "app_get_flow", "arguments": {"profile": profile, "app_key": app_key}},
1696
+ )
1697
+
1698
+ @tool_cn_name("应用流程规格查询")
1699
+ def app_flow_get(self, *, profile: str, app_key: str, version_id: str | None = None) -> JSONObject:
1420
1700
  normalized_args = {"app_key": app_key, "version_id": version_id}
1421
1701
  return _safe_tool_call(
1422
1702
  lambda: self._facade.flow_get(profile=profile, app_key=app_key, version_id=version_id),
1423
- error_code="APP_GET_FLOW_FAILED",
1703
+ error_code="APP_FLOW_GET_FAILED",
1424
1704
  normalized_args=normalized_args,
1425
- suggested_next_call={"tool_name": "app_flow_get", "arguments": {"profile": profile, "app_key": app_key}},
1705
+ suggested_next_call={"tool_name": "app_flow_get", "arguments": {"profile": profile, **normalized_args}},
1706
+ )
1707
+
1708
+ @tool_cn_name("应用流程规格 Schema 查询")
1709
+ def app_flow_get_schema(self, *, profile: str, schema_version: str | None = None) -> JSONObject:
1710
+ normalized_args = {"schema_version": schema_version}
1711
+ return _safe_tool_call(
1712
+ lambda: self._facade.flow_get_schema(profile=profile, schema_version=schema_version),
1713
+ error_code="APP_FLOW_SCHEMA_GET_FAILED",
1714
+ normalized_args=normalized_args,
1715
+ suggested_next_call={"tool_name": "app_flow_get_schema", "arguments": {"profile": profile, **normalized_args}},
1426
1716
  )
1427
1717
 
1428
1718
  @tool_cn_name("应用图表摘要读取")
@@ -1437,10 +1727,15 @@ class AiBuilderTools(ToolBase):
1437
1727
  )
1438
1728
 
1439
1729
  @tool_cn_name("应用图表详情查询")
1440
- def app_get_charts(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
1730
+ def app_get_charts(self, *, profile: str, app_key: str, app_keys: list[str] | None = None) -> JSONObject:
1441
1731
  """执行应用相关逻辑。"""
1442
- if app_keys:
1443
- return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_charts, data_key="charts", tool_name="app_get_charts")
1732
+ if app_keys is not None:
1733
+ return self._read_app_batch(
1734
+ tool_name="app_get_charts",
1735
+ profile=profile,
1736
+ app_keys=app_keys,
1737
+ read_one=lambda item_app_key: self.app_get_charts(profile=profile, app_key=item_app_key, app_keys=None),
1738
+ )
1444
1739
  normalized_args = {"app_key": app_key}
1445
1740
  return _safe_tool_call(
1446
1741
  lambda: self._facade.app_get_charts(profile=profile, app_key=app_key),
@@ -1451,9 +1746,13 @@ class AiBuilderTools(ToolBase):
1451
1746
 
1452
1747
  @tool_cn_name("自定义按钮读取")
1453
1748
  def app_get_buttons(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
1454
- """执行按钮相关逻辑。"""
1455
- if app_keys:
1456
- return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_buttons, data_key="buttons", tool_name="app_get_buttons")
1749
+ if app_keys is not None:
1750
+ return self._read_app_batch(
1751
+ tool_name="app_get_buttons",
1752
+ profile=profile,
1753
+ app_keys=app_keys,
1754
+ read_one=lambda item_app_key: self.app_get_buttons(profile=profile, app_key=item_app_key, app_keys=None),
1755
+ )
1457
1756
  normalized_args = {"app_key": app_key}
1458
1757
  return _safe_tool_call(
1459
1758
  lambda: self._facade.app_get_buttons(profile=profile, app_key=app_key),
@@ -1464,9 +1763,13 @@ class AiBuilderTools(ToolBase):
1464
1763
 
1465
1764
  @tool_cn_name("关联资源读取")
1466
1765
  def app_get_associated_resources(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
1467
- """执行关联资源相关逻辑。"""
1468
- if app_keys:
1469
- return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_associated_resources, data_key="associated_resources", tool_name="app_get_associated_resources")
1766
+ if app_keys is not None:
1767
+ return self._read_app_batch(
1768
+ tool_name="app_get_associated_resources",
1769
+ profile=profile,
1770
+ app_keys=app_keys,
1771
+ read_one=lambda item_app_key: self.app_get_associated_resources(profile=profile, app_key=item_app_key, app_keys=None),
1772
+ )
1470
1773
  normalized_args = {"app_key": app_key}
1471
1774
  return _safe_tool_call(
1472
1775
  lambda: self._facade.app_get_associated_resources(profile=profile, app_key=app_key),
@@ -1552,6 +1855,22 @@ class AiBuilderTools(ToolBase):
1552
1855
  remove_fields: list[JSONObject],
1553
1856
  ) -> JSONObject:
1554
1857
  """执行应用相关逻辑。"""
1858
+ system_field_failure = _reserved_system_field_name_failure(
1859
+ tool_name="app_schema_plan",
1860
+ profile=profile,
1861
+ app_key=app_key,
1862
+ package_id=package_id,
1863
+ app_name=app_name,
1864
+ icon=icon,
1865
+ color=color,
1866
+ visibility=visibility,
1867
+ create_if_missing=create_if_missing,
1868
+ add_fields=add_fields,
1869
+ update_fields=update_fields,
1870
+ remove_fields=remove_fields,
1871
+ )
1872
+ if system_field_failure is not None:
1873
+ return system_field_failure
1555
1874
  try:
1556
1875
  request = SchemaPlanRequest.model_validate(
1557
1876
  {
@@ -1645,6 +1964,52 @@ class AiBuilderTools(ToolBase):
1645
1964
  suggested_next_call={"tool_name": "app_layout_plan", "arguments": {"profile": profile, **normalized_request}},
1646
1965
  )
1647
1966
 
1967
+ @tool_cn_name("应用流程规划")
1968
+ def app_flow_plan(
1969
+ self,
1970
+ *,
1971
+ profile: str,
1972
+ app_key: str,
1973
+ mode: str = "replace",
1974
+ nodes: list[JSONObject] | None = None,
1975
+ transitions: list[JSONObject] | None = None,
1976
+ preset: str | None = None,
1977
+ ) -> JSONObject:
1978
+ """执行应用相关逻辑。"""
1979
+ try:
1980
+ request = FlowPlanRequest.model_validate(
1981
+ {
1982
+ "app_key": app_key,
1983
+ "mode": mode,
1984
+ "nodes": nodes or [],
1985
+ "transitions": transitions or [],
1986
+ "preset": preset,
1987
+ }
1988
+ )
1989
+ except ValidationError as exc:
1990
+ return _validation_failure(
1991
+ str(exc),
1992
+ tool_name="app_flow_plan",
1993
+ exc=exc,
1994
+ suggested_next_call={
1995
+ "tool_name": "app_flow_plan",
1996
+ "arguments": {
1997
+ "profile": profile,
1998
+ "app_key": app_key,
1999
+ "mode": "replace",
2000
+ "preset": "basic_approval",
2001
+ "nodes": [],
2002
+ "transitions": [],
2003
+ },
2004
+ },
2005
+ )
2006
+ return _safe_tool_call(
2007
+ lambda: self._facade.app_flow_plan(profile=profile, request=request),
2008
+ error_code="FLOW_PLAN_FAILED",
2009
+ normalized_args=request.model_dump(mode="json"),
2010
+ suggested_next_call={"tool_name": "app_flow_plan", "arguments": {"profile": profile, **request.model_dump(mode="json")}},
2011
+ )
2012
+
1648
2013
  @tool_cn_name("应用视图规划")
1649
2014
  def app_views_plan(
1650
2015
  self,
@@ -1656,6 +2021,15 @@ class AiBuilderTools(ToolBase):
1656
2021
  preset: str | None = None,
1657
2022
  ) -> JSONObject:
1658
2023
  """执行应用相关逻辑。"""
2024
+ reserved_failure = _reserved_system_view_name_failure(
2025
+ tool_name="app_views_plan",
2026
+ profile=profile,
2027
+ app_key=app_key,
2028
+ upsert_views=upsert_views,
2029
+ remove_views=remove_views,
2030
+ )
2031
+ if reserved_failure is not None:
2032
+ return reserved_failure
1659
2033
  try:
1660
2034
  request = ViewsPlanRequest.model_validate(
1661
2035
  {
@@ -1697,8 +2071,11 @@ class AiBuilderTools(ToolBase):
1697
2071
  package_id: int | None = None,
1698
2072
  app_name: str = "",
1699
2073
  app_title: str = "",
1700
- icon: str = "",
2074
+ icon: str | JSONObject = "",
1701
2075
  color: str = "",
2076
+ icon_name: str | None = None,
2077
+ icon_color: str | None = None,
2078
+ icon_config: JSONObject | None = None,
1702
2079
  visibility: JSONObject | None = None,
1703
2080
  create_if_missing: bool = False,
1704
2081
  publish: bool = True,
@@ -1708,7 +2085,25 @@ class AiBuilderTools(ToolBase):
1708
2085
  apps: list[JSONObject] | None = None,
1709
2086
  ) -> JSONObject:
1710
2087
  """执行应用相关逻辑。"""
1711
- if apps:
2088
+ icon, color = _normalize_builder_icon_args(
2089
+ icon=icon,
2090
+ color=color,
2091
+ icon_name=icon_name,
2092
+ icon_color=icon_color,
2093
+ icon_config=icon_config,
2094
+ )
2095
+ if apps is not None:
2096
+ normalized_apps_payload = _normalize_schema_apps_argument(
2097
+ tool_name="app_schema_apply",
2098
+ package_id=package_id,
2099
+ apps=apps,
2100
+ )
2101
+ failure = normalized_apps_payload.get("failure")
2102
+ if isinstance(failure, dict):
2103
+ return _attach_builder_apply_envelope("app_schema_apply", failure)
2104
+ package_id = normalized_apps_payload.get("package_id") # type: ignore[assignment]
2105
+ apps = normalized_apps_payload.get("apps") # type: ignore[assignment]
2106
+ input_warnings = list(normalized_apps_payload.get("warnings") or [])
1712
2107
  result = self._app_schema_apply_multi(
1713
2108
  profile=profile,
1714
2109
  package_id=package_id,
@@ -1717,6 +2112,10 @@ class AiBuilderTools(ToolBase):
1717
2112
  publish=publish,
1718
2113
  apps=apps,
1719
2114
  )
2115
+ if input_warnings:
2116
+ result_warnings = list(result.get("warnings") or [])
2117
+ result_warnings.extend(deepcopy(input_warnings))
2118
+ result["warnings"] = result_warnings
1720
2119
  return _attach_builder_apply_envelope("app_schema_apply", result)
1721
2120
  result = self._app_schema_apply_once(
1722
2121
  profile=profile,
@@ -1772,6 +2171,17 @@ class AiBuilderTools(ToolBase):
1772
2171
  }
1773
2172
  if visibility is not None:
1774
2173
  normalized_args["visibility"] = deepcopy(visibility)
2174
+ system_field_failure = _reserved_system_field_name_failure_for_apps(
2175
+ tool_name="app_schema_apply",
2176
+ profile=profile,
2177
+ package_id=package_id,
2178
+ visibility=visibility,
2179
+ create_if_missing=create_if_missing,
2180
+ publish=publish,
2181
+ apps=apps,
2182
+ )
2183
+ if system_field_failure is not None:
2184
+ return system_field_failure
1775
2185
  if package_id is None:
1776
2186
  return _config_failure(
1777
2187
  tool_name="app_schema_apply",
@@ -1781,9 +2191,20 @@ class AiBuilderTools(ToolBase):
1781
2191
  if not apps:
1782
2192
  return _config_failure(
1783
2193
  tool_name="app_schema_apply",
2194
+ error_code="APPS_FILE_EMPTY",
1784
2195
  message="app_schema_apply multi-app mode requires non-empty apps.",
1785
2196
  fix_hint="Pass apps as a non-empty list of app schema items.",
1786
2197
  )
2198
+ shape_failure = _schema_apps_shape_failure(tool_name="app_schema_apply", apps=apps)
2199
+ if shape_failure is not None:
2200
+ return shape_failure
2201
+ static_validation_failure = _multi_app_static_validation_failure(
2202
+ tool_name="app_schema_apply",
2203
+ apps=apps,
2204
+ create_if_missing=create_if_missing,
2205
+ )
2206
+ if static_validation_failure is not None:
2207
+ return static_validation_failure
1787
2208
  icon_errors: list[JSONObject] = []
1788
2209
  seen_new_app_icons: dict[str, int] = {}
1789
2210
  for index, raw_item in enumerate(apps):
@@ -1848,9 +2269,13 @@ class AiBuilderTools(ToolBase):
1848
2269
  )
1849
2270
 
1850
2271
  client_key_to_app_key: dict[str, str] = {}
2272
+ app_name_to_app_key: dict[str, str] = {}
1851
2273
  created_app_keys: list[str] = []
1852
2274
  results: list[JSONObject] = []
1853
2275
  any_write_executed = False
2276
+ any_write_may_have_succeeded = False
2277
+ pending_readback_app_keys: list[str] = []
2278
+ pending_readback_app_names: list[str] = []
1854
2279
  client_keys: set[str] = set()
1855
2280
 
1856
2281
  for index, raw_item in enumerate(apps):
@@ -1889,31 +2314,58 @@ class AiBuilderTools(ToolBase):
1889
2314
  )
1890
2315
  public_shell = _publicize_package_fields(shell)
1891
2316
  resolved_key = str(public_shell.get("app_key") or "").strip()
2317
+ shell_write_executed = _schema_apply_result_has_write(public_shell)
2318
+ shell_write_may_have_succeeded = _schema_apply_result_may_have_write(public_shell)
2319
+ if shell_write_executed:
2320
+ any_write_executed = True
2321
+ if shell_write_may_have_succeeded:
2322
+ any_write_may_have_succeeded = True
1892
2323
  if public_shell.get("status") not in {"success", "partial_success"} or not resolved_key:
2324
+ pending_readback = bool(shell_write_may_have_succeeded or shell_write_executed) and (
2325
+ str(public_shell.get("next_action") or "") == "readback_before_retry"
2326
+ or bool((public_shell.get("verification") if isinstance(public_shell.get("verification"), dict) else {}).get("readback_before_retry"))
2327
+ or not resolved_key
2328
+ )
2329
+ item_status = "pending_readback" if pending_readback else "failed"
2330
+ if pending_readback:
2331
+ if resolved_key:
2332
+ pending_readback_app_keys.append(resolved_key)
2333
+ if app_name:
2334
+ pending_readback_app_names.append(app_name)
1893
2335
  results.append({
1894
2336
  "index": index,
1895
2337
  "row_number": index + 1,
1896
2338
  "client_key": client_key or None,
1897
2339
  "app_name": app_name or None,
1898
2340
  "app_key": resolved_key or app_key or None,
1899
- "status": "failed",
2341
+ "status": item_status,
1900
2342
  "stage": "resolve_or_create_shell",
1901
2343
  "error_code": public_shell.get("error_code") or "APP_SHELL_APPLY_FAILED",
1902
2344
  "message": public_shell.get("message") or "app shell resolve/create failed",
1903
- "safe_to_retry": not any_write_executed,
2345
+ "write_executed": shell_write_executed,
2346
+ "write_may_have_succeeded": bool(shell_write_may_have_succeeded or shell_write_executed),
2347
+ "safe_to_retry": False if pending_readback else (not shell_write_executed and not any_write_executed),
2348
+ **({"next_action": "readback_before_retry"} if pending_readback else {}),
2349
+ **({"verification": public_shell.get("verification")} if isinstance(public_shell.get("verification"), dict) else {}),
2350
+ **({"created": True} if pending_readback and create_if_missing and not app_key else {}),
1904
2351
  })
1905
2352
  continue
1906
2353
  if bool(public_shell.get("created")):
1907
2354
  created_app_keys.append(resolved_key)
1908
- if _schema_apply_result_has_write(public_shell):
1909
- any_write_executed = True
2355
+ if shell_write_may_have_succeeded and str(public_shell.get("next_action") or "") == "readback_before_retry":
2356
+ pending_readback_app_keys.append(resolved_key)
2357
+ if app_name:
2358
+ pending_readback_app_names.append(app_name)
1910
2359
  if client_key:
1911
2360
  client_key_to_app_key[client_key] = resolved_key
2361
+ resolved_name = str(public_shell.get("app_name_after") or public_shell.get("app_name") or app_name or "").strip()
2362
+ if resolved_name:
2363
+ app_name_to_app_key[resolved_name] = resolved_key
1912
2364
  results.append({
1913
2365
  "index": index,
1914
2366
  "row_number": index + 1,
1915
2367
  "client_key": client_key or None,
1916
- "app_name": str(public_shell.get("app_name_after") or public_shell.get("app_name") or app_name or "").strip() or None,
2368
+ "app_name": resolved_name or None,
1917
2369
  "app_key": resolved_key,
1918
2370
  "status": "shell_ready",
1919
2371
  "created": bool(public_shell.get("created")),
@@ -1933,7 +2385,7 @@ class AiBuilderTools(ToolBase):
1933
2385
  item = deepcopy(raw_item)
1934
2386
  app_key = str(existing.get("app_key") or "").strip()
1935
2387
  try:
1936
- compiled_item = _compile_multi_app_schema_item_refs(item, client_key_to_app_key)
2388
+ compiled_item = _compile_multi_app_schema_item_refs(item, client_key_to_app_key, app_name_to_app_key)
1937
2389
  except ValueError as error:
1938
2390
  final_items.append({
1939
2391
  **{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
@@ -1970,7 +2422,11 @@ class AiBuilderTools(ToolBase):
1970
2422
  "verified": bool(shell_result.get("verified")),
1971
2423
  "error_code": shell_result.get("error_code"),
1972
2424
  "message": shell_result.get("message"),
2425
+ "write_executed": _schema_apply_result_has_write(shell_result),
2426
+ "write_may_have_succeeded": _schema_apply_result_may_have_write(shell_result),
1973
2427
  "safe_to_retry": False,
2428
+ **({"next_action": shell_result.get("next_action")} if shell_result.get("next_action") else {}),
2429
+ **({"verification": shell_result.get("verification")} if isinstance(shell_result.get("verification"), dict) else {}),
1974
2430
  })
1975
2431
  continue
1976
2432
 
@@ -1992,11 +2448,18 @@ class AiBuilderTools(ToolBase):
1992
2448
  public_result = _publicize_package_fields(field_result)
1993
2449
  if _schema_apply_result_has_write(public_result):
1994
2450
  any_write_executed = True
2451
+ if _schema_apply_result_may_have_write(public_result):
2452
+ any_write_may_have_succeeded = True
1995
2453
  item_status = public_result.get("status") if public_result.get("status") in {"success", "partial_success"} else "failed"
1996
2454
  shell_field_diff = existing.get("shell_field_diff") if isinstance(existing.get("shell_field_diff"), dict) else {}
1997
2455
  shell_field_diff_details = existing.get("shell_field_diff_details") if isinstance(existing.get("shell_field_diff_details"), dict) else {}
1998
2456
  field_diff = _merge_schema_field_diffs(shell_field_diff, public_result.get("field_diff") or {})
1999
2457
  field_diff_details = _merge_schema_field_diffs(shell_field_diff_details, public_result.get("field_diff_details") or {})
2458
+ if _schema_apply_result_may_have_write(public_result) and str(public_result.get("next_action") or "") == "readback_before_retry":
2459
+ if app_key:
2460
+ pending_readback_app_keys.append(app_key)
2461
+ if existing.get("app_name"):
2462
+ pending_readback_app_names.append(str(existing.get("app_name")))
2000
2463
  final_items.append({
2001
2464
  **{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
2002
2465
  "status": item_status,
@@ -2009,55 +2472,33 @@ class AiBuilderTools(ToolBase):
2009
2472
  "verified": bool(public_result.get("verified")),
2010
2473
  "error_code": public_result.get("error_code"),
2011
2474
  "message": public_result.get("message"),
2475
+ "write_executed": _schema_apply_result_has_write(public_result),
2476
+ "write_may_have_succeeded": _schema_apply_result_may_have_write(public_result),
2012
2477
  "safe_to_retry": False,
2478
+ **({"next_action": public_result.get("next_action")} if public_result.get("next_action") else {}),
2479
+ **({"verification": public_result.get("verification")} if isinstance(public_result.get("verification"), dict) else {}),
2013
2480
  })
2014
2481
 
2015
- # Inline layout: apply layout for each successful app that included a layout field.
2016
- for index, raw_item in enumerate(apps):
2017
- if not isinstance(raw_item, dict):
2018
- continue
2019
- layout_spec = raw_item.get("layout")
2020
- if not isinstance(layout_spec, dict) or not layout_spec:
2021
- continue
2022
- final_item = next((it for it in final_items if it.get("index") == index), None)
2023
- if not final_item or final_item.get("status") not in {"success", "partial_success"}:
2024
- continue
2025
- resolved_app_key = str(final_item.get("app_key") or "").strip()
2026
- if not resolved_app_key:
2027
- continue
2028
- layout_mode = str(layout_spec.get("mode") or "merge").strip() or "merge"
2029
- layout_sections = layout_spec.get("sections") or []
2030
- try:
2031
- layout_result = self.app_layout_apply(
2032
- profile=profile,
2033
- app_key=resolved_app_key,
2034
- mode=layout_mode,
2035
- publish=publish,
2036
- sections=list(layout_sections),
2037
- )
2038
- layout_ok = layout_result.get("status") in {"success", "partial_success"}
2039
- except Exception as layout_error:
2040
- layout_ok = False
2041
- layout_result = {"status": "failed", "message": str(layout_error)}
2042
- if layout_ok:
2043
- final_item["layout_applied"] = True
2044
- final_item["layout_status"] = layout_result.get("status")
2045
- else:
2046
- final_item["layout_warning"] = layout_result.get("error_code") or "LAYOUT_APPLY_FAILED"
2047
- final_item["layout_message"] = layout_result.get("message")
2048
-
2482
+ pending_readback = sum(1 for item in final_items if item.get("status") == "pending_readback")
2049
2483
  succeeded = sum(1 for item in final_items if item.get("status") in {"success", "partial_success"})
2050
- failed = len(final_items) - succeeded
2051
- overall_status = "success" if failed == 0 else ("partial_success" if succeeded > 0 or any_write_executed else "failed")
2052
- return {
2484
+ failed = sum(1 for item in final_items if item.get("status") == "failed")
2485
+ partial = sum(1 for item in final_items if item.get("status") == "partial_success")
2486
+ overall_status = (
2487
+ "success"
2488
+ if failed == 0 and pending_readback == 0 and partial == 0
2489
+ else ("partial_success" if succeeded > 0 or pending_readback > 0 or any_write_executed or any_write_may_have_succeeded else "failed")
2490
+ )
2491
+ uncertain_write = any_write_may_have_succeeded or pending_readback > 0
2492
+ response: JSONObject = {
2053
2493
  "status": overall_status,
2054
2494
  "mode": "multi_app",
2055
2495
  "total": len(apps),
2056
2496
  "succeeded": succeeded,
2057
2497
  "failed": failed,
2498
+ "pending_readback": pending_readback,
2058
2499
  "created_app_keys": created_app_keys,
2059
2500
  "write_executed": any_write_executed,
2060
- "safe_to_retry": not any_write_executed,
2501
+ "safe_to_retry": not (any_write_executed or uncertain_write),
2061
2502
  "package_id": package_id,
2062
2503
  "publish_requested": publish,
2063
2504
  "apps": final_items,
@@ -2065,12 +2506,23 @@ class AiBuilderTools(ToolBase):
2065
2506
  "verification": {
2066
2507
  "all_apps_succeeded": failed == 0,
2067
2508
  "created_app_count": len(created_app_keys),
2509
+ "pending_readback_count": pending_readback,
2068
2510
  },
2069
2511
  "request_id": None,
2070
2512
  "error_code": None if overall_status != "failed" else "MULTI_APP_SCHEMA_APPLY_FAILED",
2071
2513
  "recoverable": overall_status != "success",
2072
- "message": "multi-app schema apply completed" if overall_status != "failed" else "multi-app schema apply failed",
2514
+ "message": (
2515
+ "multi-app schema apply needs readback before retry"
2516
+ if pending_readback > 0
2517
+ else ("multi-app schema apply completed" if overall_status != "failed" else "multi-app schema apply failed")
2518
+ ),
2073
2519
  }
2520
+ if uncertain_write:
2521
+ response["write_may_have_succeeded"] = True
2522
+ response["next_action"] = "readback_before_retry"
2523
+ response["pending_readback_app_keys"] = list(dict.fromkeys(pending_readback_app_keys))
2524
+ response["pending_readback_app_names"] = list(dict.fromkeys(pending_readback_app_names))
2525
+ return response
2074
2526
 
2075
2527
  def _app_schema_apply_once(
2076
2528
  self,
@@ -2091,6 +2543,23 @@ class AiBuilderTools(ToolBase):
2091
2543
  ) -> JSONObject:
2092
2544
  """执行内部辅助逻辑。"""
2093
2545
  effective_app_name = app_name or app_title
2546
+ system_field_failure = _reserved_system_field_name_failure(
2547
+ tool_name="app_schema_apply",
2548
+ profile=profile,
2549
+ app_key=app_key,
2550
+ package_id=package_id,
2551
+ app_name=effective_app_name,
2552
+ icon=icon,
2553
+ color=color,
2554
+ visibility=visibility,
2555
+ create_if_missing=create_if_missing,
2556
+ publish=publish,
2557
+ add_fields=add_fields,
2558
+ update_fields=update_fields,
2559
+ remove_fields=remove_fields,
2560
+ )
2561
+ if system_field_failure is not None:
2562
+ return system_field_failure
2094
2563
  icon_failure = _validate_workspace_icon_for_builder(
2095
2564
  tool_name="app_schema_apply",
2096
2565
  icon=icon,
@@ -2185,20 +2654,30 @@ class AiBuilderTools(ToolBase):
2185
2654
  return _publicize_package_fields(result)
2186
2655
 
2187
2656
  @tool_cn_name("应用布局应用")
2188
- def app_layout_apply(self, *, profile: str, app_key: str, mode: str = "merge", publish: bool = True, sections: list[JSONObject], apps: list[JSONObject] | None = None) -> JSONObject:
2657
+ def app_layout_apply(
2658
+ self,
2659
+ *,
2660
+ profile: str,
2661
+ app_key: str,
2662
+ mode: str = "merge",
2663
+ publish: bool = True,
2664
+ sections: list[JSONObject],
2665
+ apps: list[JSONObject] | None = None,
2666
+ ) -> JSONObject:
2189
2667
  """执行应用相关逻辑。"""
2190
- if apps:
2191
- return self._facade._batch_write_apps(
2668
+ if apps is not None:
2669
+ return self._apply_app_batch(
2670
+ tool_name="app_layout_apply",
2192
2671
  profile=profile,
2193
2672
  apps=apps,
2194
- single_writer=lambda profile, app_key, **kw: self.app_layout_apply(
2673
+ apply_one=lambda _index, item, item_app_key: self.app_layout_apply(
2195
2674
  profile=profile,
2196
- app_key=app_key,
2197
- mode=kw.get("mode", mode),
2198
- publish=kw.get("publish", publish),
2199
- sections=kw.get("sections", []),
2675
+ app_key=item_app_key,
2676
+ mode=str(_payload_get(item, "mode", default=mode) or mode),
2677
+ publish=_payload_bool(item, "publish", default=publish),
2678
+ sections=_payload_list(item, "sections", default=sections or []),
2679
+ apps=None,
2200
2680
  ),
2201
- tool_name="app_layout_apply",
2202
2681
  )
2203
2682
  result = self._app_layout_apply_once(
2204
2683
  profile=profile,
@@ -2284,15 +2763,25 @@ class AiBuilderTools(ToolBase):
2284
2763
  *,
2285
2764
  profile: str,
2286
2765
  app_key: str,
2287
- spec: JSONObject,
2766
+ mode: str = "replace",
2288
2767
  publish: bool = True,
2768
+ nodes: list[JSONObject] | None = None,
2769
+ transitions: list[JSONObject] | None = None,
2770
+ spec: JSONObject | None = None,
2289
2771
  idempotency_key: str | None = None,
2290
2772
  schema_version: str | None = None,
2291
2773
  patch_nodes: list[JSONObject] | None = None,
2292
2774
  ) -> JSONObject:
2293
2775
  """执行应用相关逻辑。"""
2294
- if patch_nodes:
2295
- result = _safe_tool_call(
2776
+ if patch_nodes is not None:
2777
+ normalized_args = {
2778
+ "app_key": app_key,
2779
+ "publish": publish,
2780
+ "patch_nodes": patch_nodes,
2781
+ "idempotency_key": idempotency_key,
2782
+ "schema_version": schema_version,
2783
+ }
2784
+ return _attach_builder_apply_envelope("app_flow_apply", _safe_tool_call(
2296
2785
  lambda: self._facade.flow_patch_nodes(
2297
2786
  profile=profile,
2298
2787
  app_key=app_key,
@@ -2301,50 +2790,132 @@ class AiBuilderTools(ToolBase):
2301
2790
  idempotency_key=idempotency_key,
2302
2791
  schema_version=schema_version,
2303
2792
  ),
2304
- error_code="FLOW_APPLY_FAILED",
2305
- normalized_args={"app_key": app_key, "patch_nodes": patch_nodes, "publish": publish},
2306
- suggested_next_call={"tool_name": "app_flow_apply", "arguments": {"profile": profile, "app_key": app_key, "patch_nodes": patch_nodes}},
2793
+ error_code="FLOW_PATCH_FAILED",
2794
+ normalized_args=normalized_args,
2795
+ suggested_next_call={"tool_name": "app_flow_get", "arguments": {"profile": profile, "app_key": app_key}},
2796
+ ))
2797
+ if spec is not None:
2798
+ normalized_args = {
2799
+ "app_key": app_key,
2800
+ "publish": publish,
2801
+ "spec": spec,
2802
+ "idempotency_key": idempotency_key,
2803
+ "schema_version": schema_version,
2804
+ }
2805
+ return _attach_builder_apply_envelope("app_flow_apply", _safe_tool_call(
2806
+ lambda: self._facade.flow_apply(
2807
+ profile=profile,
2808
+ app_key=app_key,
2809
+ spec=spec,
2810
+ publish=publish,
2811
+ idempotency_key=idempotency_key,
2812
+ schema_version=schema_version,
2813
+ ),
2814
+ error_code="FLOW_SPEC_APPLY_FAILED",
2815
+ normalized_args=normalized_args,
2816
+ suggested_next_call={"tool_name": "app_flow_get", "arguments": {"profile": profile, "app_key": app_key}},
2817
+ ))
2818
+ effective_nodes = nodes or []
2819
+ effective_transitions = transitions or []
2820
+ result = self._app_flow_apply_once(
2821
+ profile=profile,
2822
+ app_key=app_key,
2823
+ mode=mode,
2824
+ publish=publish,
2825
+ nodes=effective_nodes,
2826
+ transitions=effective_transitions,
2827
+ )
2828
+ result = self._retry_after_self_lock_release(
2829
+ profile=profile,
2830
+ result=result,
2831
+ retry_call=lambda: self._app_flow_apply_once(
2832
+ profile=profile,
2833
+ app_key=app_key,
2834
+ mode=mode,
2835
+ publish=publish,
2836
+ nodes=effective_nodes,
2837
+ transitions=effective_transitions,
2838
+ ),
2839
+ )
2840
+ return _attach_builder_apply_envelope("app_flow_apply", result)
2841
+
2842
+ def _app_flow_apply_once(
2843
+ self,
2844
+ *,
2845
+ profile: str,
2846
+ app_key: str,
2847
+ mode: str = "replace",
2848
+ publish: bool = True,
2849
+ nodes: list[JSONObject],
2850
+ transitions: list[JSONObject],
2851
+ ) -> JSONObject:
2852
+ """执行内部辅助逻辑。"""
2853
+ plan_result = self._rewrite_plan_result_for_apply(
2854
+ result=self.app_flow_plan(
2855
+ profile=profile,
2856
+ app_key=app_key,
2857
+ mode=mode,
2858
+ nodes=nodes,
2859
+ transitions=transitions,
2860
+ preset=None,
2861
+ ),
2862
+ profile=profile,
2863
+ publish=publish,
2864
+ plan_tool_name="app_flow_plan",
2865
+ apply_tool_name="app_flow_apply",
2866
+ )
2867
+ if not isinstance(plan_result, dict) or plan_result.get("status") != "success":
2868
+ return plan_result
2869
+ plan_args = plan_result.get("normalized_args")
2870
+ if not isinstance(plan_args, dict):
2871
+ plan_args = {}
2872
+ try:
2873
+ request = FlowPlanRequest.model_validate(
2874
+ {
2875
+ "app_key": plan_args.get("app_key") or app_key,
2876
+ "mode": plan_args.get("mode") or mode,
2877
+ "nodes": plan_args.get("nodes") or [],
2878
+ "transitions": plan_args.get("transitions") or [],
2879
+ "preset": None,
2880
+ }
2307
2881
  )
2308
- return _attach_builder_apply_envelope("app_flow_apply", result)
2309
- if not isinstance(spec, dict) or not spec:
2310
- return _config_failure(
2882
+ except ValidationError as exc:
2883
+ return _validation_failure(
2884
+ str(exc),
2311
2885
  tool_name="app_flow_apply",
2312
- message="app_flow_apply requires a non-empty WorkflowSpecDTO `spec` object.",
2313
- fix_hint="Call app_flow_get_schema, then app_flow_get for GET-first baseline, patch spec, and apply.",
2886
+ exc=exc,
2887
+ suggested_next_call={
2888
+ "tool_name": "app_flow_apply",
2889
+ "arguments": {
2890
+ "profile": profile,
2891
+ "app_key": str(plan_args.get("app_key") or app_key),
2892
+ "mode": str(plan_args.get("mode") or "replace"),
2893
+ "publish": publish,
2894
+ "nodes": plan_args.get("nodes") or [{"id": "start", "type": "start", "name": "发起"}],
2895
+ "transitions": plan_args.get("transitions") or [],
2896
+ },
2897
+ },
2314
2898
  )
2315
2899
  normalized_args = {
2316
- "app_key": app_key,
2900
+ "app_key": request.app_key,
2901
+ "mode": request.mode,
2317
2902
  "publish": publish,
2318
- "spec": spec,
2319
- "idempotency_key": idempotency_key,
2320
- "schema_version": schema_version,
2903
+ "nodes": [node.model_dump(mode="json") for node in request.nodes],
2904
+ "transitions": [transition.model_dump(mode="json", by_alias=True) for transition in request.transitions],
2321
2905
  }
2322
- result = _safe_tool_call(
2323
- lambda: self._facade.flow_apply(
2906
+ return _safe_tool_call(
2907
+ lambda: self._facade.app_flow_apply(
2324
2908
  profile=profile,
2325
- app_key=app_key,
2326
- spec=spec,
2909
+ app_key=request.app_key,
2910
+ mode=request.mode,
2327
2911
  publish=publish,
2328
- idempotency_key=idempotency_key,
2329
- schema_version=schema_version,
2912
+ nodes=[node.model_dump(mode="json") for node in request.nodes],
2913
+ transitions=[transition.model_dump(mode="json", by_alias=True) for transition in request.transitions],
2330
2914
  ),
2331
2915
  error_code="FLOW_APPLY_FAILED",
2332
2916
  normalized_args=normalized_args,
2333
2917
  suggested_next_call={"tool_name": "app_flow_apply", "arguments": {"profile": profile, **normalized_args}},
2334
2918
  )
2335
- result = self._retry_after_self_lock_release(
2336
- profile=profile,
2337
- result=result,
2338
- retry_call=lambda: self._facade.flow_apply(
2339
- profile=profile,
2340
- app_key=app_key,
2341
- spec=spec,
2342
- publish=publish,
2343
- idempotency_key=idempotency_key,
2344
- schema_version=schema_version,
2345
- ),
2346
- )
2347
- return _attach_builder_apply_envelope("app_flow_apply", result)
2348
2919
 
2349
2920
  @tool_cn_name("应用视图应用")
2350
2921
  def app_views_apply(
@@ -2359,19 +2930,20 @@ class AiBuilderTools(ToolBase):
2359
2930
  apps: list[JSONObject] | None = None,
2360
2931
  ) -> JSONObject:
2361
2932
  """执行应用相关逻辑。"""
2362
- if apps:
2363
- return self._facade._batch_write_apps(
2933
+ if apps is not None:
2934
+ return self._apply_app_batch(
2935
+ tool_name="app_views_apply",
2364
2936
  profile=profile,
2365
2937
  apps=apps,
2366
- single_writer=lambda profile, app_key, **kw: self.app_views_apply(
2938
+ apply_one=lambda _index, item, item_app_key: self.app_views_apply(
2367
2939
  profile=profile,
2368
- app_key=app_key,
2369
- publish=kw.get("publish", publish),
2370
- upsert_views=kw.get("upsert_views", []),
2371
- patch_views=kw.get("patch_views", []),
2372
- remove_views=kw.get("remove_views", []),
2940
+ app_key=item_app_key,
2941
+ publish=_payload_bool(item, "publish", default=publish),
2942
+ upsert_views=_payload_list(item, "upsert_views", "upsertViews", "views", default=upsert_views or []),
2943
+ patch_views=_payload_list(item, "patch_views", "patchViews", default=patch_views or []),
2944
+ remove_views=_payload_list(item, "remove_views", "removeViews", default=remove_views or []),
2945
+ apps=None,
2373
2946
  ),
2374
- tool_name="app_views_apply",
2375
2947
  )
2376
2948
  result = self._app_views_apply_once(
2377
2949
  profile=profile,
@@ -2406,6 +2978,17 @@ class AiBuilderTools(ToolBase):
2406
2978
  remove_views: list[str],
2407
2979
  ) -> JSONObject:
2408
2980
  """执行内部辅助逻辑。"""
2981
+ reserved_failure = _reserved_system_view_name_failure(
2982
+ tool_name="app_views_apply",
2983
+ profile=profile,
2984
+ app_key=app_key,
2985
+ publish=publish,
2986
+ upsert_views=upsert_views,
2987
+ patch_views=patch_views,
2988
+ remove_views=remove_views,
2989
+ )
2990
+ if reserved_failure is not None:
2991
+ return reserved_failure
2409
2992
  if patch_views:
2410
2993
  try:
2411
2994
  parsed_views = [ViewUpsertPatch.model_validate(item) for item in (upsert_views or [])]
@@ -2480,7 +3063,7 @@ class AiBuilderTools(ToolBase):
2480
3063
  "profile": profile,
2481
3064
  "app_key": str(plan_args.get("app_key") or app_key),
2482
3065
  "publish": publish,
2483
- "upsert_views": plan_args.get("upsert_views") or [{"name": "全部数据", "type": "table", "columns": ["字段A"]}],
3066
+ "upsert_views": plan_args.get("upsert_views") or [{"name": "业务台账视图", "type": "table", "columns": ["字段A"]}],
2484
3067
  "remove_views": plan_args.get("remove_views") or [],
2485
3068
  },
2486
3069
  },
@@ -2539,19 +3122,20 @@ class AiBuilderTools(ToolBase):
2539
3122
  apps: list[JSONObject] | None = None,
2540
3123
  ) -> JSONObject:
2541
3124
  """执行应用相关逻辑。"""
2542
- if apps:
2543
- return self._facade._batch_write_apps(
3125
+ if apps is not None:
3126
+ return self._apply_app_batch(
3127
+ tool_name="app_charts_apply",
2544
3128
  profile=profile,
2545
3129
  apps=apps,
2546
- single_writer=lambda profile, app_key, **kw: self.app_charts_apply(
3130
+ apply_one=lambda _index, item, item_app_key: self.app_charts_apply(
2547
3131
  profile=profile,
2548
- app_key=app_key,
2549
- upsert_charts=kw.get("upsert_charts", []),
2550
- patch_charts=kw.get("patch_charts", []),
2551
- remove_chart_ids=kw.get("remove_chart_ids", []),
2552
- reorder_chart_ids=kw.get("reorder_chart_ids", []),
3132
+ app_key=item_app_key,
3133
+ upsert_charts=_payload_list(item, "upsert_charts", "upsertCharts", "charts", default=upsert_charts or []),
3134
+ patch_charts=_payload_list(item, "patch_charts", "patchCharts", default=patch_charts or []),
3135
+ remove_chart_ids=_payload_list(item, "remove_chart_ids", "removeChartIds", default=remove_chart_ids or []),
3136
+ reorder_chart_ids=_payload_list(item, "reorder_chart_ids", "reorderChartIds", default=reorder_chart_ids or []),
3137
+ apps=None,
2553
3138
  ),
2554
- tool_name="app_charts_apply",
2555
3139
  )
2556
3140
  try:
2557
3141
  request = ChartApplyRequest.model_validate(
@@ -2571,12 +3155,12 @@ class AiBuilderTools(ToolBase):
2571
3155
  suggested_next_call={
2572
3156
  "tool_name": "app_charts_apply",
2573
3157
  "arguments": {
2574
- "profile": profile,
2575
- "app_key": app_key,
2576
- "upsert_charts": [{"name": "销售总量", "chart_type": "target", "indicator_field_ids": []}],
2577
- "remove_chart_ids": [],
2578
- "reorder_chart_ids": [],
2579
- },
3158
+ "profile": profile,
3159
+ "app_key": app_key,
3160
+ "upsert_charts": [{"name": "销售总量", "chart_type": "target", "metric": "count(*)"}],
3161
+ "remove_chart_ids": [],
3162
+ "reorder_chart_ids": [],
3163
+ },
2580
3164
  },
2581
3165
  ))
2582
3166
  normalized_args = request.model_dump(mode="json")
@@ -2611,25 +3195,23 @@ class AiBuilderTools(ToolBase):
2611
3195
  patch_sections: list[JSONObject] | None = None,
2612
3196
  ) -> JSONObject:
2613
3197
  """执行门户相关逻辑。"""
2614
- if patch_sections:
2615
- if not dash_key:
2616
- return _attach_builder_apply_envelope("portal_apply", _config_failure(
2617
- tool_name="portal_apply",
2618
- message="patch_sections requires dash_key to identify the portal",
2619
- fix_hint="Provide dash_key from portal_list or portal_get",
2620
- ))
2621
- result = _safe_tool_call(
3198
+ if patch_sections is not None:
3199
+ normalized_args = {
3200
+ "dash_key": dash_key,
3201
+ "publish": publish,
3202
+ "patch_sections": patch_sections,
3203
+ }
3204
+ return _attach_builder_apply_envelope("portal_apply", _publicize_package_fields(_safe_tool_call(
2622
3205
  lambda: self._facade.portal_patch_sections(
2623
3206
  profile=profile,
2624
3207
  dash_key=dash_key,
2625
3208
  patch_sections=patch_sections,
2626
3209
  publish=publish,
2627
3210
  ),
2628
- error_code="PORTAL_APPLY_FAILED",
2629
- normalized_args={"dash_key": dash_key, "patch_sections": patch_sections, "publish": publish},
2630
- suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, "dash_key": dash_key, "patch_sections": patch_sections}},
2631
- )
2632
- return _attach_builder_apply_envelope("portal_apply", result)
3211
+ error_code="PORTAL_PATCH_FAILED",
3212
+ normalized_args=normalized_args,
3213
+ suggested_next_call={"tool_name": "portal_get", "arguments": {"profile": profile, "dash_key": dash_key}},
3214
+ )))
2633
3215
  request_payload: dict[str, Any] = dict(payload) if isinstance(payload, dict) else {}
2634
3216
  if dash_key:
2635
3217
  request_payload["dash_key"] = dash_key
@@ -2706,23 +3288,39 @@ class AiBuilderTools(ToolBase):
2706
3288
  ))
2707
3289
  return _attach_builder_apply_envelope("portal_apply", result)
2708
3290
 
3291
+ @tool_cn_name("门户删除")
3292
+ def portal_delete(self, *, profile: str, dash_key: str) -> JSONObject:
3293
+ """执行门户删除逻辑。"""
3294
+ normalized_args = {"dash_key": dash_key}
3295
+ result = _publicize_package_fields(_safe_tool_call(
3296
+ lambda: self._facade.portal_delete(profile=profile, dash_key=dash_key),
3297
+ error_code="PORTAL_DELETE_FAILED",
3298
+ normalized_args=normalized_args,
3299
+ suggested_next_call={"tool_name": "portal_delete", "arguments": {"profile": profile, **normalized_args}},
3300
+ ))
3301
+ return _attach_builder_apply_envelope("portal_delete", result)
3302
+
2709
3303
  @tool_cn_name("应用发布校验")
2710
3304
  def app_publish_verify(
2711
3305
  self,
2712
3306
  *,
2713
3307
  profile: str,
2714
- app_key: str = "",
3308
+ app_key: str,
2715
3309
  app_keys: list[str] | None = None,
2716
3310
  expected_package_id: int | None = None,
2717
3311
  ) -> JSONObject:
2718
3312
  """执行应用相关逻辑。"""
2719
- if app_keys:
2720
- return self._facade._batch_read_app_keys(
2721
- profile=profile,
2722
- app_keys=app_keys,
2723
- single_reader=lambda profile, app_key: self._facade.app_publish_verify(profile=profile, app_key=app_key, expected_package_tag_id=expected_package_id),
2724
- data_key="verification",
3313
+ if app_keys is not None:
3314
+ return self._apply_app_batch(
2725
3315
  tool_name="app_publish_verify",
3316
+ profile=profile,
3317
+ apps=[{"app_key": item, "expected_package_id": expected_package_id} for item in app_keys],
3318
+ apply_one=lambda _index, item, item_app_key: self.app_publish_verify(
3319
+ profile=profile,
3320
+ app_key=item_app_key,
3321
+ app_keys=None,
3322
+ expected_package_id=item.get("expected_package_id") or item.get("expectedPackageId") or expected_package_id,
3323
+ ),
2726
3324
  )
2727
3325
  normalized_args = {"app_key": app_key, "expected_package_id": expected_package_id}
2728
3326
  result = _publicize_package_fields(_safe_tool_call(
@@ -2856,33 +3454,442 @@ class AiBuilderTools(ToolBase):
2856
3454
  "tool_name": apply_tool_name,
2857
3455
  "arguments": {"profile": profile, **normalized_args, "publish": publish},
2858
3456
  }
2859
- return rewritten
3457
+ return rewritten
3458
+
3459
+
3460
+ def _multi_app_item_failure(index: int, item: object, error_code: str, message: str) -> JSONObject:
3461
+ app_name = None
3462
+ client_key = None
3463
+ app_key = None
3464
+ if isinstance(item, dict):
3465
+ app_name = str(item.get("app_name") or item.get("appTitle") or item.get("app_title") or item.get("title") or "").strip() or None
3466
+ client_key = str(item.get("client_key") or item.get("clientKey") or "").strip() or None
3467
+ app_key = str(item.get("app_key") or item.get("appKey") or "").strip() or None
3468
+ return {
3469
+ "index": index,
3470
+ "row_number": index + 1,
3471
+ "client_key": client_key,
3472
+ "app_name": app_name,
3473
+ "app_key": app_key,
3474
+ "status": "failed",
3475
+ "stage": "validate_item",
3476
+ "error_code": error_code,
3477
+ "message": message,
3478
+ "safe_to_retry": True,
3479
+ }
3480
+
3481
+
3482
+ def _normalize_builder_icon_args(
3483
+ *,
3484
+ icon: str | JSONObject | None,
3485
+ color: str | None,
3486
+ icon_name: str | None = None,
3487
+ icon_color: str | None = None,
3488
+ icon_config: JSONObject | None = None,
3489
+ ) -> tuple[str | None, str | None]:
3490
+ payloads: list[JSONObject] = []
3491
+ if isinstance(icon_config, dict):
3492
+ payloads.append(icon_config)
3493
+ if isinstance(icon, dict):
3494
+ payloads.append(icon)
3495
+
3496
+ normalized_icon = None if isinstance(icon, dict) else icon
3497
+ normalized_color = color
3498
+ for payload in payloads:
3499
+ normalized_icon = normalized_icon or payload.get("icon") or payload.get("icon_name") or payload.get("iconName") or payload.get("name")
3500
+ normalized_color = normalized_color or payload.get("color") or payload.get("icon_color") or payload.get("iconColor")
3501
+ normalized_icon = normalized_icon or icon_name
3502
+ normalized_color = normalized_color or icon_color
3503
+ return (
3504
+ str(normalized_icon).strip() if normalized_icon is not None else None,
3505
+ str(normalized_color).strip() if normalized_color is not None else None,
3506
+ )
3507
+
3508
+
3509
+ def _normalize_schema_app_item(item: JSONObject) -> JSONObject:
3510
+ normalized = deepcopy(item)
3511
+ icon, color = _normalize_builder_icon_args(
3512
+ icon=normalized.get("icon"),
3513
+ color=normalized.get("color"),
3514
+ icon_name=normalized.get("icon_name") or normalized.get("iconName"),
3515
+ icon_color=normalized.get("icon_color") or normalized.get("iconColor"),
3516
+ icon_config=normalized.get("icon_config") or normalized.get("iconConfig"),
3517
+ )
3518
+ if icon:
3519
+ normalized["icon"] = icon
3520
+ if color:
3521
+ normalized["color"] = color
3522
+ for key in ("icon_name", "iconName", "icon_color", "iconColor", "icon_config", "iconConfig"):
3523
+ normalized.pop(key, None)
3524
+ return normalized
3525
+
3526
+
3527
+ def _schema_apps_expected_shape() -> JSONObject:
3528
+ return {
3529
+ "package_id": 1001,
3530
+ "apps": [
3531
+ {
3532
+ "client_key": "employee",
3533
+ "app_name": "员工花名册",
3534
+ "icon": "business-personalcard",
3535
+ "color": "emerald",
3536
+ "add_fields": [{"name": "员工名称", "type": "text", "as_data_title": True}],
3537
+ }
3538
+ ],
3539
+ }
3540
+
3541
+
3542
+ def _schema_apps_expected_shape_json() -> str:
3543
+ return (
3544
+ '{"package_id":1001,"apps":[{"client_key":"employee","app_name":"员工花名册",'
3545
+ '"icon":"business-personalcard","color":"emerald","add_fields":[{"name":"员工名称","type":"text","as_data_title":true}]}]}'
3546
+ )
3547
+
3548
+
3549
+ def _is_schema_apps_wrapper_item(item: object) -> bool:
3550
+ return isinstance(item, dict) and "apps" in item
3551
+
3552
+
3553
+ def _schema_apps_shape_failure(*, tool_name: str, apps: list[JSONObject]) -> JSONObject | None:
3554
+ for index, item in enumerate(apps):
3555
+ if _is_schema_apps_wrapper_item(item):
3556
+ return _config_failure(
3557
+ tool_name=tool_name,
3558
+ error_code="APPS_FILE_SHAPE_INVALID",
3559
+ message="apps[] items must be app schema items, not package wrapper objects.",
3560
+ fix_hint=(
3561
+ "For MCP pass package_id separately and apps=[{app_name, icon, color, add_fields}]. "
3562
+ 'For CLI use --apps-file with {"package_id":1001,"apps":[...]}. '
3563
+ "Do not pass multiple {package_id, apps} wrapper objects inside apps[]."
3564
+ ),
3565
+ details={
3566
+ "index": index,
3567
+ "row_number": index + 1,
3568
+ "expected_shape": _schema_apps_expected_shape(),
3569
+ "expected_shape_json": _schema_apps_expected_shape_json(),
3570
+ },
3571
+ )
3572
+ return None
3573
+
3574
+
3575
+ def _normalize_schema_apps_argument(*, tool_name: str, package_id: int | None, apps: list[JSONObject]) -> JSONObject:
3576
+ normalized_package_id = package_id
3577
+ normalized_apps: list[JSONObject] = apps
3578
+ warnings: list[JSONObject] = []
3579
+
3580
+ if len(apps) == 1 and _is_schema_apps_wrapper_item(apps[0]):
3581
+ wrapper = apps[0]
3582
+ wrapper_apps = wrapper.get("apps")
3583
+ if not isinstance(wrapper_apps, list):
3584
+ return {
3585
+ "failure": _config_failure(
3586
+ tool_name=tool_name,
3587
+ error_code="APPS_FILE_SHAPE_INVALID",
3588
+ message="apps singleton wrapper requires an apps array.",
3589
+ fix_hint='Use {"package_id":1001,"apps":[...]} in CLI, or pass MCP package_id=1001 and apps=[...].',
3590
+ details={
3591
+ "expected_shape": _schema_apps_expected_shape(),
3592
+ "expected_shape_json": _schema_apps_expected_shape_json(),
3593
+ },
3594
+ )
3595
+ }
3596
+ if normalized_package_id is None and wrapper.get("package_id") is not None:
3597
+ try:
3598
+ normalized_package_id = int(wrapper.get("package_id"))
3599
+ except (TypeError, ValueError):
3600
+ return {
3601
+ "failure": _config_failure(
3602
+ tool_name=tool_name,
3603
+ error_code="APPS_FILE_SHAPE_INVALID",
3604
+ message="apps singleton wrapper package_id must be an integer.",
3605
+ fix_hint="Pass package_id as a number at the top level.",
3606
+ details={
3607
+ "expected_shape": _schema_apps_expected_shape(),
3608
+ "expected_shape_json": _schema_apps_expected_shape_json(),
3609
+ },
3610
+ )
3611
+ }
3612
+ normalized_apps = wrapper_apps
3613
+ warnings.append(
3614
+ {
3615
+ "code": "APPS_FILE_WRAPPER_ARRAY_UNWRAPPED",
3616
+ "message": "apps was a singleton wrapper array; normalized it to package_id + apps.",
3617
+ }
3618
+ )
3619
+ else:
3620
+ failure = _schema_apps_shape_failure(tool_name=tool_name, apps=apps)
3621
+ if failure is not None:
3622
+ return {"failure": failure}
3623
+
3624
+ normalized_items: list[JSONObject] = []
3625
+ for item in normalized_apps:
3626
+ normalized_items.append(_normalize_schema_app_item(item) if isinstance(item, dict) else item)
3627
+ return {"package_id": normalized_package_id, "apps": normalized_items, "warnings": warnings}
3628
+
3629
+
3630
+ def _multi_app_item_app_name(item: JSONObject) -> str:
3631
+ return str(item.get("app_name") or item.get("appName") or item.get("appTitle") or item.get("app_title") or item.get("title") or "").strip()
3632
+
3633
+
3634
+ def _multi_app_item_app_key(item: JSONObject) -> str:
3635
+ return str(item.get("app_key") or item.get("appKey") or "").strip()
3636
+
3637
+
3638
+ def _multi_app_item_client_key(item: JSONObject) -> str:
3639
+ return str(item.get("client_key") or item.get("clientKey") or "").strip()
3640
+
3641
+
3642
+ def _field_name_for_static_validation(field: JSONObject) -> str:
3643
+ return str(field.get("name") or field.get("title") or field.get("label") or "").strip()
3644
+
3645
+
3646
+ def _field_type_for_static_validation(field: JSONObject) -> str:
3647
+ return str(field.get("type") or field.get("type_id") or field.get("typeId") or "").strip()
3648
+
3649
+
3650
+ def _is_data_title_field(field: JSONObject) -> bool:
3651
+ return bool(field.get("as_data_title") or field.get("asDataTitle"))
3652
+
3653
+
3654
+ def _collect_multi_app_target_refs(value: object, *, path: str) -> list[JSONObject]:
3655
+ refs: list[JSONObject] = []
3656
+ if isinstance(value, list):
3657
+ for index, entry in enumerate(value):
3658
+ refs.extend(_collect_multi_app_target_refs(entry, path=f"{path}[{index}]"))
3659
+ return refs
3660
+ if not isinstance(value, dict):
3661
+ return refs
3662
+ for key, entry in value.items():
3663
+ current_path = f"{path}.{key}"
3664
+ if key in {"target_app_ref", "targetAppRef", "target_app_client_key", "targetAppClientKey"}:
3665
+ refs.append({"kind": "target_app_ref", "value": str(entry or "").strip(), "path": current_path})
3666
+ continue
3667
+ if key in {"target_app", "targetApp"}:
3668
+ refs.append({"kind": "target_app", "value": str(entry or "").strip(), "path": current_path})
3669
+ continue
3670
+ refs.extend(_collect_multi_app_target_refs(entry, path=current_path))
3671
+ return refs
3672
+
3673
+
3674
+ def _multi_app_static_validation_failure(
3675
+ *,
3676
+ tool_name: str,
3677
+ apps: list[JSONObject],
3678
+ create_if_missing: bool,
3679
+ ) -> JSONObject | None:
3680
+ issues: list[JSONObject] = []
3681
+ client_key_indexes: dict[str, int] = {}
3682
+ app_name_indexes: dict[str, list[int]] = {}
3683
+
3684
+ for index, item in enumerate(apps):
3685
+ if not isinstance(item, dict):
3686
+ continue
3687
+ app_name = _multi_app_item_app_name(item)
3688
+ client_key = _multi_app_item_client_key(item)
3689
+ if client_key:
3690
+ first_index = client_key_indexes.get(client_key)
3691
+ if first_index is not None:
3692
+ issues.append(
3693
+ {
3694
+ "index": index,
3695
+ "row_number": index + 1,
3696
+ "path": f"apps[{index}].client_key",
3697
+ "error_code": "DUPLICATE_CLIENT_KEY",
3698
+ "message": f"duplicate client_key '{client_key}' also appears at apps[{first_index}]",
3699
+ "fix_hint": "Use a unique stable client_key for each app item, then reference relations through target_app_ref.",
3700
+ "details": {"client_key": client_key, "first_index": first_index, "duplicate_index": index},
3701
+ }
3702
+ )
3703
+ else:
3704
+ client_key_indexes[client_key] = index
3705
+ if app_name:
3706
+ app_name_indexes.setdefault(app_name, []).append(index)
3707
+
3708
+ for app_name, indexes in sorted(app_name_indexes.items()):
3709
+ if len(indexes) <= 1:
3710
+ continue
3711
+ issues.append(
3712
+ {
3713
+ "index": indexes[1],
3714
+ "row_number": indexes[1] + 1,
3715
+ "path": f"apps[{indexes[1]}].app_name",
3716
+ "error_code": "DUPLICATE_APP_NAME",
3717
+ "message": f"duplicate app_name '{app_name}' appears at apps{indexes}",
3718
+ "fix_hint": "Use unique app_name values in one multi-app payload, or use app_key for existing apps.",
3719
+ "details": {"app_name": app_name, "indexes": indexes},
3720
+ }
3721
+ )
3722
+
3723
+ for index, item in enumerate(apps):
3724
+ if not isinstance(item, dict):
3725
+ continue
3726
+ app_key = _multi_app_item_app_key(item)
3727
+ app_name = _multi_app_item_app_name(item)
3728
+ new_app_item = not bool(app_key)
3729
+ if not app_key and not app_name:
3730
+ issues.append(
3731
+ {
3732
+ "index": index,
3733
+ "row_number": index + 1,
3734
+ "path": f"apps[{index}]",
3735
+ "error_code": "APP_SELECTOR_REQUIRED",
3736
+ "message": "apps[] item requires app_key or app_name",
3737
+ "fix_hint": "For new apps pass app_name; for existing apps pass app_key.",
3738
+ }
3739
+ )
3740
+ continue
3741
+ if new_app_item and not create_if_missing:
3742
+ issues.append(
3743
+ {
3744
+ "index": index,
3745
+ "row_number": index + 1,
3746
+ "path": f"apps[{index}]",
3747
+ "error_code": "CREATE_IF_MISSING_REQUIRED",
3748
+ "message": "new multi-app items require create_if_missing=true",
3749
+ "fix_hint": "Set create_if_missing=true, or pass app_key for an existing app.",
3750
+ }
3751
+ )
2860
3752
 
3753
+ add_fields = _multi_app_list_value(item, "add_fields", "addFields")
3754
+ if new_app_item:
3755
+ title_fields: list[tuple[int, JSONObject]] = [
3756
+ (field_index, field)
3757
+ for field_index, field in enumerate(add_fields)
3758
+ if isinstance(field, dict) and _is_data_title_field(field)
3759
+ ]
3760
+ if not title_fields:
3761
+ issues.append(
3762
+ {
3763
+ "index": index,
3764
+ "row_number": index + 1,
3765
+ "path": f"apps[{index}].add_fields",
3766
+ "error_code": "MISSING_DATA_TITLE_FIELD",
3767
+ "message": "new apps must define exactly one top-level data title field",
3768
+ "fix_hint": "Mark one readable top-level field with as_data_title=true, for example {'name':'客户名称','type':'text','as_data_title':true}.",
3769
+ "details": {"suggested_field_names": [_field_name_for_static_validation(field) for field in add_fields[:8] if isinstance(field, dict)]},
3770
+ }
3771
+ )
3772
+ elif len(title_fields) > 1:
3773
+ issues.append(
3774
+ {
3775
+ "index": index,
3776
+ "row_number": index + 1,
3777
+ "path": f"apps[{index}].add_fields",
3778
+ "error_code": "MULTIPLE_DATA_TITLE_FIELDS",
3779
+ "message": "new apps can mark only one top-level field as data title",
3780
+ "fix_hint": "Keep as_data_title=true on exactly one top-level field.",
3781
+ "details": {
3782
+ "fields": [
3783
+ {
3784
+ "field_index": field_index,
3785
+ "name": _field_name_for_static_validation(field),
3786
+ "type": _field_type_for_static_validation(field),
3787
+ }
3788
+ for field_index, field in title_fields
3789
+ ]
3790
+ },
3791
+ }
3792
+ )
3793
+ else:
3794
+ field_index, title_field = title_fields[0]
3795
+ if _field_type_for_static_validation(title_field) in {"subtable", "table"}:
3796
+ issues.append(
3797
+ {
3798
+ "index": index,
3799
+ "row_number": index + 1,
3800
+ "path": f"apps[{index}].add_fields[{field_index}]",
3801
+ "error_code": "INVALID_DATA_TITLE_FIELD",
3802
+ "message": "data title must be a top-level non-subtable field",
3803
+ "fix_hint": "Move as_data_title=true to a normal top-level text/number/date-like field.",
3804
+ "details": {
3805
+ "field_name": _field_name_for_static_validation(title_field),
3806
+ "field_type": _field_type_for_static_validation(title_field),
3807
+ },
3808
+ }
3809
+ )
3810
+ if _contains_multi_app_target_ref(title_field):
3811
+ issues.append(
3812
+ {
3813
+ "index": index,
3814
+ "row_number": index + 1,
3815
+ "path": f"apps[{index}].add_fields[{field_index}]",
3816
+ "error_code": "DATA_TITLE_FIELD_DEFERRED_BY_TARGET_REF",
3817
+ "message": "data title cannot be a relation field that depends on target_app_ref/target_app in the same multi-app call",
3818
+ "fix_hint": "Use a normal title field such as 客户名称/项目名称 as data title; keep relation fields as non-title fields.",
3819
+ "details": {"field_name": _field_name_for_static_validation(title_field)},
3820
+ }
3821
+ )
2861
3822
 
2862
- def _multi_app_item_failure(index: int, item: object, error_code: str, message: str) -> JSONObject:
2863
- app_name = None
2864
- client_key = None
2865
- app_key = None
2866
- if isinstance(item, dict):
2867
- app_name = str(item.get("app_name") or item.get("appTitle") or item.get("app_title") or item.get("title") or "").strip() or None
2868
- client_key = str(item.get("client_key") or item.get("clientKey") or "").strip() or None
2869
- app_key = str(item.get("app_key") or item.get("appKey") or "").strip() or None
2870
- return {
2871
- "index": index,
2872
- "row_number": index + 1,
2873
- "client_key": client_key,
2874
- "app_name": app_name,
2875
- "app_key": app_key,
2876
- "status": "failed",
2877
- "stage": "validate_item",
2878
- "error_code": error_code,
2879
- "message": message,
2880
- "safe_to_retry": True,
2881
- }
3823
+ for ref in _collect_multi_app_target_refs(item, path=f"apps[{index}]"):
3824
+ ref_value = str(ref.get("value") or "").strip()
3825
+ if not ref_value:
3826
+ issues.append(
3827
+ {
3828
+ "index": index,
3829
+ "row_number": index + 1,
3830
+ "path": ref.get("path"),
3831
+ "error_code": "TARGET_APP_REFERENCE_EMPTY",
3832
+ "message": "target app reference cannot be empty",
3833
+ "fix_hint": "Use target_app_ref with another apps[].client_key, or target_app with a unique apps[].app_name.",
3834
+ }
3835
+ )
3836
+ elif ref.get("kind") == "target_app_ref" and ref_value not in client_key_indexes:
3837
+ issues.append(
3838
+ {
3839
+ "index": index,
3840
+ "row_number": index + 1,
3841
+ "path": ref.get("path"),
3842
+ "error_code": "TARGET_APP_REF_UNRESOLVED",
3843
+ "message": f"target_app_ref '{ref_value}' does not match any apps[].client_key in this payload",
3844
+ "fix_hint": "Set target_app_ref to one of apps[].client_key, or use target_app_key for an already-known existing app.",
3845
+ "details": {"target_app_ref": ref_value, "available_client_keys": sorted(client_key_indexes.keys())},
3846
+ }
3847
+ )
3848
+ elif ref.get("kind") == "target_app":
3849
+ matching_indexes = app_name_indexes.get(ref_value, [])
3850
+ if not matching_indexes:
3851
+ issues.append(
3852
+ {
3853
+ "index": index,
3854
+ "row_number": index + 1,
3855
+ "path": ref.get("path"),
3856
+ "error_code": "TARGET_APP_NAME_UNRESOLVED",
3857
+ "message": f"target_app '{ref_value}' does not match any apps[].app_name in this payload",
3858
+ "fix_hint": "Prefer target_app_ref with a stable apps[].client_key, or make target_app match an app_name exactly.",
3859
+ "details": {"target_app": ref_value, "available_app_names": sorted(app_name_indexes.keys())},
3860
+ }
3861
+ )
3862
+ elif len(matching_indexes) > 1:
3863
+ issues.append(
3864
+ {
3865
+ "index": index,
3866
+ "row_number": index + 1,
3867
+ "path": ref.get("path"),
3868
+ "error_code": "TARGET_APP_NAME_AMBIGUOUS",
3869
+ "message": f"target_app '{ref_value}' matches multiple apps[].app_name values",
3870
+ "fix_hint": "Use target_app_ref with a unique apps[].client_key instead of target_app.",
3871
+ "details": {"target_app": ref_value, "matching_indexes": matching_indexes},
3872
+ }
3873
+ )
3874
+
3875
+ if not issues:
3876
+ return None
3877
+ return _config_failure(
3878
+ tool_name=tool_name,
3879
+ error_code="MULTI_APP_STATIC_VALIDATION_FAILED",
3880
+ message="multi-app schema payload has static errors; fix apps[] before writing.",
3881
+ fix_hint="Before creating app shells, ensure each new app has exactly one data title, unique client_key/app_name, and relation refs match apps[].client_key or app_name.",
3882
+ details={"issues": issues, "expected_shape": _schema_apps_expected_shape()},
3883
+ )
2882
3884
 
2883
3885
 
2884
- def _compile_multi_app_schema_item_refs(item: JSONObject, client_key_to_app_key: dict[str, str]) -> JSONObject:
3886
+ def _compile_multi_app_schema_item_refs(
3887
+ item: JSONObject,
3888
+ client_key_to_app_key: dict[str, str],
3889
+ app_name_to_app_key: dict[str, str] | None = None,
3890
+ ) -> JSONObject:
2885
3891
  compiled = deepcopy(item)
3892
+ app_name_to_app_key = app_name_to_app_key or {}
2886
3893
 
2887
3894
  def visit(value):
2888
3895
  if isinstance(value, list):
@@ -2902,6 +3909,13 @@ def _compile_multi_app_schema_item_refs(item: JSONObject, client_key_to_app_key:
2902
3909
  if not target_app_key:
2903
3910
  raise ValueError(f"target_app_ref '{ref_key}' did not match any apps[].client_key")
2904
3911
  payload["target_app_key"] = target_app_key
3912
+ target_app = payload.pop("target_app", None) or payload.pop("targetApp", None)
3913
+ if target_app is not None and not str(payload.get("target_app_key") or "").strip():
3914
+ target_name = str(target_app or "").strip()
3915
+ target_app_key = app_name_to_app_key.get(target_name)
3916
+ if not target_app_key:
3917
+ raise ValueError(f"target_app '{target_name}' did not match any apps[].app_name in the same call")
3918
+ payload["target_app_key"] = target_app_key
2905
3919
  return payload
2906
3920
 
2907
3921
  return visit(compiled)
@@ -2950,7 +3964,7 @@ def _contains_multi_app_target_ref(value: object) -> bool:
2950
3964
  if not isinstance(value, dict):
2951
3965
  return False
2952
3966
  for key, entry in value.items():
2953
- if key in {"target_app_ref", "targetAppRef", "target_app_client_key", "targetAppClientKey"}:
3967
+ if key in {"target_app_ref", "targetAppRef", "target_app_client_key", "targetAppClientKey", "target_app", "targetApp"}:
2954
3968
  return True
2955
3969
  if _contains_multi_app_target_ref(entry):
2956
3970
  return True
@@ -2973,6 +3987,8 @@ def _merge_schema_field_diffs(*diffs: object) -> JSONObject:
2973
3987
 
2974
3988
 
2975
3989
  def _schema_apply_result_has_write(result: JSONObject) -> bool:
3990
+ if "write_executed" in result:
3991
+ return bool(result.get("write_executed"))
2976
3992
  if bool(result.get("created")) or bool(result.get("published")) or bool(result.get("app_base_updated")):
2977
3993
  return True
2978
3994
  field_diff = result.get("field_diff")
@@ -2981,6 +3997,15 @@ def _schema_apply_result_has_write(result: JSONObject) -> bool:
2981
3997
  return False
2982
3998
 
2983
3999
 
4000
+ def _schema_apply_result_may_have_write(result: JSONObject) -> bool:
4001
+ verification = result.get("verification") if isinstance(result.get("verification"), dict) else {}
4002
+ return bool(
4003
+ result.get("write_may_have_succeeded")
4004
+ or str(result.get("next_action") or "") == "readback_before_retry"
4005
+ or verification.get("readback_before_retry")
4006
+ )
4007
+
4008
+
2984
4009
  def _validation_failure(
2985
4010
  detail: str,
2986
4011
  *,
@@ -3061,6 +4086,197 @@ def _visibility_validation_failure(
3061
4086
  return result
3062
4087
 
3063
4088
 
4089
+ _RESERVED_SYSTEM_FIELD_NAMES = {
4090
+ "数据ID",
4091
+ "编号",
4092
+ "申请人",
4093
+ "申请时间",
4094
+ "创建人",
4095
+ "创建时间",
4096
+ "提交人",
4097
+ "提交时间",
4098
+ "更新时间",
4099
+ "更新人",
4100
+ "当前流程状态",
4101
+ "当前处理人",
4102
+ "当前处理节点",
4103
+ "流程标题",
4104
+ }
4105
+
4106
+
4107
+ def _field_patch_name(item: object) -> str:
4108
+ if not isinstance(item, dict):
4109
+ return ""
4110
+ value = item.get("name")
4111
+ if value is None:
4112
+ value = item.get("title")
4113
+ if value is None:
4114
+ value = item.get("field_name")
4115
+ if value is None:
4116
+ value = item.get("fieldName")
4117
+ return str(value or "").strip()
4118
+
4119
+
4120
+ def _reserved_system_field_name_failure(
4121
+ *,
4122
+ tool_name: str,
4123
+ profile: str,
4124
+ app_key: str = "",
4125
+ package_id: int | None = None,
4126
+ app_name: str = "",
4127
+ icon: str | None = None,
4128
+ color: str | None = None,
4129
+ visibility: JSONObject | None = None,
4130
+ create_if_missing: bool = False,
4131
+ publish: bool | None = None,
4132
+ add_fields: list[JSONObject] | None = None,
4133
+ update_fields: list[JSONObject] | None = None,
4134
+ remove_fields: list[JSONObject] | None = None,
4135
+ app_index: int | None = None,
4136
+ ) -> JSONObject | None:
4137
+ for index, item in enumerate(add_fields or []):
4138
+ name = _field_patch_name(item)
4139
+ if name not in _RESERVED_SYSTEM_FIELD_NAMES:
4140
+ continue
4141
+ suggested_add_fields = [field for field in (add_fields or []) if _field_patch_name(field) not in _RESERVED_SYSTEM_FIELD_NAMES]
4142
+ arguments: JSONObject = {
4143
+ "profile": profile,
4144
+ "app_key": app_key,
4145
+ "package_id": package_id,
4146
+ "app_name": app_name,
4147
+ "icon": icon or "",
4148
+ "color": color or "",
4149
+ "visibility": visibility,
4150
+ "create_if_missing": create_if_missing,
4151
+ "add_fields": suggested_add_fields,
4152
+ "update_fields": update_fields or [],
4153
+ "remove_fields": remove_fields or [],
4154
+ }
4155
+ if publish is not None:
4156
+ arguments["publish"] = publish
4157
+ return _config_failure(
4158
+ tool_name=tool_name,
4159
+ error_code="RESERVED_SYSTEM_FIELD_NAME",
4160
+ message=f"add_fields[{index}].name uses built-in system field name '{name}'",
4161
+ fix_hint="Do not create form fields named 数据ID, 编号, 申请人, 申请时间, 创建人, 创建时间, 提交人, 提交时间, 更新时间, 更新人, 当前流程状态, 当前处理人, 当前处理节点, or 流程标题. These fields are provided by Qingflow. Remove them from add_fields; only reference them where the tool explicitly supports system fields, such as button source_field 数据ID.",
4162
+ details={
4163
+ "reserved_field_names": sorted(_RESERVED_SYSTEM_FIELD_NAMES),
4164
+ "blocked_index": index,
4165
+ "blocked_name": name,
4166
+ "app_index": app_index,
4167
+ },
4168
+ suggested_next_call={"tool_name": tool_name, "arguments": arguments},
4169
+ )
4170
+ return None
4171
+
4172
+
4173
+ def _reserved_system_field_name_failure_for_apps(
4174
+ *,
4175
+ tool_name: str,
4176
+ profile: str,
4177
+ package_id: int | None,
4178
+ visibility: JSONObject | None,
4179
+ create_if_missing: bool,
4180
+ publish: bool,
4181
+ apps: list[JSONObject],
4182
+ ) -> JSONObject | None:
4183
+ for index, item in enumerate(apps or []):
4184
+ if not isinstance(item, dict):
4185
+ continue
4186
+ failure = _reserved_system_field_name_failure(
4187
+ tool_name=tool_name,
4188
+ profile=profile,
4189
+ app_key=str(item.get("app_key") or item.get("appKey") or ""),
4190
+ package_id=package_id,
4191
+ app_name=str(item.get("app_name") or item.get("appName") or ""),
4192
+ icon=str(item.get("icon") or ""),
4193
+ color=str(item.get("color") or ""),
4194
+ visibility=item.get("visibility") if isinstance(item.get("visibility"), dict) else visibility,
4195
+ create_if_missing=create_if_missing,
4196
+ publish=publish,
4197
+ add_fields=list(item.get("add_fields") or item.get("addFields") or []),
4198
+ update_fields=list(item.get("update_fields") or item.get("updateFields") or []),
4199
+ remove_fields=list(item.get("remove_fields") or item.get("removeFields") or []),
4200
+ app_index=index,
4201
+ )
4202
+ if failure is not None:
4203
+ suggested = deepcopy(failure.get("suggested_next_call") or {})
4204
+ arguments = suggested.get("arguments")
4205
+ if isinstance(arguments, dict):
4206
+ fixed_apps = deepcopy(apps)
4207
+ if isinstance(fixed_apps[index], dict):
4208
+ fixed_apps[index]["add_fields"] = arguments.get("add_fields") or []
4209
+ arguments = {
4210
+ "profile": profile,
4211
+ "package_id": package_id,
4212
+ "visibility": visibility,
4213
+ "create_if_missing": create_if_missing,
4214
+ "publish": publish,
4215
+ "apps": fixed_apps,
4216
+ }
4217
+ failure["suggested_next_call"] = {"tool_name": tool_name, "arguments": arguments}
4218
+ return failure
4219
+ return None
4220
+
4221
+
4222
+ _RESERVED_SYSTEM_VIEW_NAMES = {
4223
+ "全部数据",
4224
+ "我的数据",
4225
+ "我发起的",
4226
+ "待办",
4227
+ "已办",
4228
+ "抄送",
4229
+ }
4230
+
4231
+
4232
+ def _reserved_system_view_name_failure(
4233
+ *,
4234
+ tool_name: str,
4235
+ profile: str,
4236
+ app_key: str,
4237
+ publish: bool | None = None,
4238
+ upsert_views: list[JSONObject] | None = None,
4239
+ patch_views: list[JSONObject] | None = None,
4240
+ remove_views: list[str] | None = None,
4241
+ ) -> JSONObject | None:
4242
+ for index, item in enumerate(upsert_views or []):
4243
+ if not isinstance(item, dict):
4244
+ continue
4245
+ name = str(item.get("name") or "").strip()
4246
+ view_key = str(item.get("view_key") or item.get("viewKey") or "").strip()
4247
+ if name in _RESERVED_SYSTEM_VIEW_NAMES and not view_key:
4248
+ suggested: JSONObject = {
4249
+ "tool_name": tool_name,
4250
+ "arguments": {
4251
+ "profile": profile,
4252
+ "app_key": app_key,
4253
+ "upsert_views": [
4254
+ {
4255
+ **item,
4256
+ "name": "业务台账视图",
4257
+ }
4258
+ ],
4259
+ "patch_views": patch_views or [],
4260
+ "remove_views": remove_views or [],
4261
+ },
4262
+ }
4263
+ if publish is not None:
4264
+ suggested["arguments"]["publish"] = publish
4265
+ return _config_failure(
4266
+ tool_name=tool_name,
4267
+ error_code="RESERVED_SYSTEM_VIEW_NAME",
4268
+ message=f"upsert_views[{index}].name uses built-in system view name '{name}' without view_key",
4269
+ fix_hint="Do not create business views named 全部数据, 我的数据, 我发起的, 待办, 已办, or 抄送. Use a business-specific view name for new views; to modify a built-in view, pass its existing raw view_key or use patch_views.",
4270
+ details={
4271
+ "reserved_view_names": sorted(_RESERVED_SYSTEM_VIEW_NAMES),
4272
+ "blocked_index": index,
4273
+ "blocked_name": name,
4274
+ },
4275
+ suggested_next_call=suggested,
4276
+ )
4277
+ return None
4278
+
4279
+
3064
4280
  def _config_failure(
3065
4281
  *,
3066
4282
  tool_name: str,
@@ -3069,6 +4285,7 @@ def _config_failure(
3069
4285
  error_code: str = "CONFIG_ERROR",
3070
4286
  details: JSONObject | None = None,
3071
4287
  allowed_values: JSONObject | None = None,
4288
+ suggested_next_call: JSONObject | None = None,
3072
4289
  ) -> JSONObject:
3073
4290
  contract = _BUILDER_TOOL_CONTRACTS.get(tool_name or "")
3074
4291
  public_allowed_values = deepcopy(contract.get("allowed_values", {})) if isinstance(contract, dict) else {}
@@ -3089,7 +4306,7 @@ def _config_failure(
3089
4306
  "missing_fields": [],
3090
4307
  "allowed_values": public_allowed_values,
3091
4308
  "details": public_details,
3092
- "suggested_next_call": None,
4309
+ "suggested_next_call": suggested_next_call,
3093
4310
  "request_id": None,
3094
4311
  "backend_code": None,
3095
4312
  "http_status": None,
@@ -3217,11 +4434,58 @@ def _builder_contract_with_apply_output(tool_name: str, contract: JSONObject) ->
3217
4434
  "schema_version": BUILDER_APPLY_SCHEMA_VERSION,
3218
4435
  "preferred_ui_fields": ["operation", "summary", "resources"],
3219
4436
  "resource_fields": ["resource_type", "operation", "status", "id", "key", "name", "ids", "parent", "icon_config", "error_code", "message"],
4437
+ "json_paths": _builder_apply_json_paths(),
3220
4438
  "legacy_fields_preserved": True,
3221
4439
  }
3222
4440
  return public
3223
4441
 
3224
4442
 
4443
+ def _builder_tool_contract_summary(tool_name: str, contract: JSONObject) -> JSONObject:
4444
+ allowed_values = contract.get("allowed_values") if isinstance(contract, dict) else {}
4445
+ allowed_values_keys = sorted(str(key) for key in allowed_values) if isinstance(allowed_values, dict) else []
4446
+ return {
4447
+ "tool_name": tool_name,
4448
+ "contract_path": "$.contract",
4449
+ "allowed_keys_path": "$.contract.allowed_keys",
4450
+ "allowed_values_path": "$.contract.allowed_values",
4451
+ "allowed_values_key_style": "flat_dotted_keys",
4452
+ "minimal_example_path": "$.contract.minimal_example",
4453
+ "execution_notes_path": "$.contract.execution_notes",
4454
+ "top_level_allowed_values_usage": "empty on successful contract lookup; use $.contract.allowed_values instead",
4455
+ "allowed_values_keys_sample": allowed_values_keys[:12],
4456
+ "json_paths": {
4457
+ "contract": "$.contract",
4458
+ "allowed_keys": "$.contract.allowed_keys",
4459
+ "allowed_values": "$.contract.allowed_values",
4460
+ "minimal_example": "$.contract.minimal_example",
4461
+ "execution_notes": "$.contract.execution_notes",
4462
+ "aliases": "$.contract.aliases",
4463
+ },
4464
+ }
4465
+
4466
+
4467
+ def _builder_apply_json_paths() -> JSONObject:
4468
+ return {
4469
+ "status": "$.status",
4470
+ "schema_version": "$.schema_version",
4471
+ "operation": "$.operation",
4472
+ "summary": "$.summary",
4473
+ "resources": "$.resources",
4474
+ "warnings": "$.warnings",
4475
+ "verification": "$.verification",
4476
+ "details": "$.details",
4477
+ "write_executed": "$.write_executed",
4478
+ "write_may_have_succeeded": "$.write_may_have_succeeded",
4479
+ "safe_to_retry": "$.safe_to_retry",
4480
+ "next_action": "$.next_action",
4481
+ "summary_write_executed": "$.summary.write_executed",
4482
+ "summary_safe_to_retry": "$.summary.safe_to_retry",
4483
+ "resource_keys": "$.resources[].key",
4484
+ "resource_ids": "$.resources[].id",
4485
+ "resource_statuses": "$.resources[].status",
4486
+ }
4487
+
4488
+
3225
4489
  def _attach_builder_apply_envelope(tool_name: str, payload: JSONObject) -> JSONObject:
3226
4490
  if not isinstance(payload, dict):
3227
4491
  return payload
@@ -3230,6 +4494,7 @@ def _attach_builder_apply_envelope(tool_name: str, payload: JSONObject) -> JSONO
3230
4494
  payload["operation"] = tool_name
3231
4495
  payload["resources"] = resources
3232
4496
  payload["summary"] = _builder_apply_summary(payload, resources)
4497
+ payload["json_paths"] = _builder_apply_json_paths()
3233
4498
  return payload
3234
4499
 
3235
4500
 
@@ -3263,8 +4528,14 @@ def _builder_apply_summary(payload: JSONObject, resources: list[JSONObject]) ->
3263
4528
  }
3264
4529
  if "write_executed" in payload:
3265
4530
  summary["write_executed"] = bool(payload.get("write_executed"))
4531
+ if "write_may_have_succeeded" in payload:
4532
+ summary["write_may_have_succeeded"] = bool(payload.get("write_may_have_succeeded"))
3266
4533
  if "safe_to_retry" in payload:
3267
4534
  summary["safe_to_retry"] = bool(payload.get("safe_to_retry"))
4535
+ if payload.get("next_action"):
4536
+ summary["next_action"] = payload.get("next_action")
4537
+ if payload.get("pending_readback") is not None:
4538
+ summary["pending_readback"] = payload.get("pending_readback")
3268
4539
  return summary
3269
4540
 
3270
4541
 
@@ -3285,6 +4556,8 @@ def _builder_apply_resources(tool_name: str, payload: JSONObject) -> list[JSONOb
3285
4556
  resources = _builder_package_resources(payload)
3286
4557
  elif tool_name == "app_schema_apply":
3287
4558
  resources = _builder_schema_resources(payload)
4559
+ elif isinstance(payload.get("apps"), list) and _builder_apply_tool_is_app_scoped(tool_name):
4560
+ resources = _builder_batch_app_resources(tool_name, payload)
3288
4561
  elif tool_name == "app_layout_apply":
3289
4562
  resources = [_builder_app_resource(payload, operation="layout_updated")]
3290
4563
  elif tool_name == "app_flow_apply":
@@ -3295,6 +4568,8 @@ def _builder_apply_resources(tool_name: str, payload: JSONObject) -> list[JSONOb
3295
4568
  resources = _builder_chart_resources(payload)
3296
4569
  elif tool_name == "portal_apply":
3297
4570
  resources = _builder_portal_resources(payload)
4571
+ elif tool_name == "portal_delete":
4572
+ resources = _builder_portal_resources(payload, operation_override="removed")
3298
4573
  elif tool_name == "app_custom_buttons_apply":
3299
4574
  resources = _builder_button_resources(payload)
3300
4575
  elif tool_name == "app_associated_resources_apply":
@@ -3310,6 +4585,38 @@ def _builder_apply_resources(tool_name: str, payload: JSONObject) -> list[JSONOb
3310
4585
  return resources
3311
4586
 
3312
4587
 
4588
+ def _builder_batch_app_resources(tool_name: str, payload: JSONObject) -> list[JSONObject]:
4589
+ resources: list[JSONObject] = []
4590
+ operation_by_tool = {
4591
+ "app_schema_apply": "updated",
4592
+ "app_layout_apply": "layout_updated",
4593
+ "app_flow_apply": "workflow_updated",
4594
+ "app_views_apply": "updated",
4595
+ "app_custom_buttons_apply": "updated",
4596
+ "app_associated_resources_apply": "updated",
4597
+ "app_charts_apply": "updated",
4598
+ "app_publish_verify": "verified",
4599
+ }
4600
+ operation = operation_by_tool.get(tool_name, "updated")
4601
+ for item in payload.get("apps") or []:
4602
+ if not isinstance(item, dict):
4603
+ continue
4604
+ result = item.get("result") if isinstance(item.get("result"), dict) else {}
4605
+ nested_resources = result.get("resources") if isinstance(result, dict) else None
4606
+ if isinstance(nested_resources, list) and nested_resources:
4607
+ resources.extend(resource for resource in nested_resources if isinstance(resource, dict))
4608
+ continue
4609
+ app_payload: JSONObject = {
4610
+ **(result if isinstance(result, dict) else {}),
4611
+ "app_key": item.get("app_key") or (result.get("app_key") if isinstance(result, dict) else None),
4612
+ "status": item.get("status") or (result.get("status") if isinstance(result, dict) else None),
4613
+ "error_code": item.get("error_code") or (result.get("error_code") if isinstance(result, dict) else None),
4614
+ "message": item.get("message") or (result.get("message") if isinstance(result, dict) else None),
4615
+ }
4616
+ resources.append(_builder_app_resource(app_payload, operation=operation))
4617
+ return resources
4618
+
4619
+
3313
4620
  def _builder_apply_tool_is_app_scoped(tool_name: str) -> bool:
3314
4621
  return tool_name in {
3315
4622
  "app_schema_apply",
@@ -3490,7 +4797,15 @@ def _builder_package_resources(payload: JSONObject) -> list[JSONObject]:
3490
4797
  package_id = payload.get("package_id") or payload.get("id")
3491
4798
  package_name = payload.get("package_name") or payload.get("name")
3492
4799
  status = _builder_status(payload, "success")
3493
- operation = "failed" if status == "failed" else ("created" if bool(payload.get("created")) else "updated")
4800
+ operation = (
4801
+ "failed"
4802
+ if status == "failed"
4803
+ else "removed"
4804
+ if bool(payload.get("deleted")) or bool(payload.get("delete_executed"))
4805
+ else "created"
4806
+ if bool(payload.get("created"))
4807
+ else "updated"
4808
+ )
3494
4809
  normalized_args = payload.get("normalized_args") if isinstance(payload.get("normalized_args"), dict) else {}
3495
4810
  icon_config = (
3496
4811
  _builder_container_icon_config(payload, raw_keys=("icon", "tagIcon", "tag_icon"))
@@ -3541,7 +4856,7 @@ def _builder_schema_resources(payload: JSONObject) -> list[JSONObject]:
3541
4856
  parent=package_parent,
3542
4857
  icon_config=icon_config,
3543
4858
  error_code=item.get("error_code"),
3544
- message=item.get("message") if status == "failed" else None,
4859
+ message=item.get("message") if status in {"failed", "pending_readback"} else None,
3545
4860
  )
3546
4861
  )
3547
4862
  resources.extend(_builder_field_resources(item.get("field_diff_details") or item.get("field_diff"), parent=parent))
@@ -3742,7 +5057,7 @@ def _builder_chart_resources(payload: JSONObject) -> list[JSONObject]:
3742
5057
  return resources
3743
5058
 
3744
5059
 
3745
- def _builder_portal_resources(payload: JSONObject) -> list[JSONObject]:
5060
+ def _builder_portal_resources(payload: JSONObject, *, operation_override: str | None = None) -> list[JSONObject]:
3746
5061
  status = _builder_status(payload, "success")
3747
5062
  draft_result = payload.get("draft_result") if isinstance(payload.get("draft_result"), dict) else {}
3748
5063
  live_result = payload.get("live_result") if isinstance(payload.get("live_result"), dict) else {}
@@ -3773,7 +5088,7 @@ def _builder_portal_resources(payload: JSONObject) -> list[JSONObject]:
3773
5088
  first_tag = draft_result.get("tags")[0]
3774
5089
  if isinstance(first_tag, dict):
3775
5090
  package_id = first_tag.get("tagId") or first_tag.get("tag_id") or first_tag.get("id")
3776
- operation = "failed" if status == "failed" else ("created" if bool(payload.get("created")) else "updated")
5091
+ operation = operation_override or ("failed" if status == "failed" else ("created" if bool(payload.get("created")) else "updated"))
3777
5092
  parent = None
3778
5093
  if package_id:
3779
5094
  parent = _builder_parent("package", id_value=package_id, key=package_id)
@@ -3962,7 +5277,7 @@ def _coerce_api_error(error: Exception) -> QingflowApiError:
3962
5277
 
3963
5278
 
3964
5279
  def _public_error_message(error_code: str, error: QingflowApiError) -> str:
3965
- if error.backend_code == 40074 or error_code == "APP_EDIT_LOCKED":
5280
+ if backend_code_int(error) == 40074 or error_code == "APP_EDIT_LOCKED":
3966
5281
  return "app is currently locked by another active editor session"
3967
5282
  if error.http_status != 404:
3968
5283
  return error.message
@@ -4137,13 +5452,29 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4137
5452
  },
4138
5453
  },
4139
5454
  "package_apply": {
4140
- "allowed_keys": ["package_id", "package_name", "create_if_missing", "icon", "color", "visibility", "items", "allow_detach"],
5455
+ "allowed_keys": [
5456
+ "package_id",
5457
+ "package_name",
5458
+ "create_if_missing",
5459
+ "icon",
5460
+ "color",
5461
+ "icon_name",
5462
+ "icon_color",
5463
+ "icon_config",
5464
+ "visibility",
5465
+ "items",
5466
+ "allow_detach",
5467
+ ],
4141
5468
  "aliases": {
4142
5469
  "packageId": "package_id",
4143
5470
  "packageName": "package_name",
4144
5471
  "createIfMissing": "create_if_missing",
4145
5472
  "iconName": "icon",
4146
5473
  "iconColor": "color",
5474
+ "icon_config.name": "icon",
5475
+ "icon_config.icon_name": "icon",
5476
+ "icon_config.color": "color",
5477
+ "icon_config.icon_color": "color",
4147
5478
  "allowDetach": "allow_detach",
4148
5479
  },
4149
5480
  "allowed_values": {
@@ -4155,12 +5486,14 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4155
5486
  "execution_notes": [
4156
5487
  "create or update package metadata, visibility, grouping, and ordering in one call",
4157
5488
  "creating a package requires explicit icon + color; icon=template is blocked because it is too generic",
5489
+ "agent-facing primary icon shape is icon + color; icon_name/icon_color, icon_config, and icon={name,color} are compatibility aliases that normalize to icon/color",
4158
5490
  "updating a package preserves existing icon/color when omitted; explicit icon/color values are still validated",
4159
5491
  "call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
4160
5492
  "metadata keys omitted on update are preserved",
4161
5493
  "package_id maps internally to backend tagId; do not use tag_id in public calls",
4162
5494
  "items is a full package layout tree; omitting existing app/portal items is blocked unless allow_detach=true",
4163
5495
  "item shapes: {type:'app', app_key}, {type:'portal', dash_key}, or {type:'group', group_id?, name, items:[...]}",
5496
+ "layout apply calls backend package ordering (MoveGroupAuth), so it requires package edit_app permission even when items=[] only clears/deletes existing groups",
4164
5497
  *_VISIBILITY_EXECUTION_NOTES,
4165
5498
  ],
4166
5499
  "minimal_example": {
@@ -4303,7 +5636,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4303
5636
  "app_custom_buttons_apply": {
4304
5637
  "allowed_keys": [
4305
5638
  "app_key",
4306
- "apps",
4307
5639
  "upsert_buttons",
4308
5640
  "patch_buttons",
4309
5641
  "remove_buttons",
@@ -4388,6 +5720,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4388
5720
  "field_mappings.source_field accepts source schema fields and supported system fields: 数据ID/row_record_id/apply_id/_id maps to current record id (-17), 编号/record_number maps to visible record number (0)",
4389
5721
  "to fill a target relation field with the current source record, map source_field='数据ID' to the target relation field; default_values is for static constants, not dynamic current-record values",
4390
5722
  "do not write raw que_relation unless maintaining a legacy config; field_mappings/default_values and que_relation are mutually exclusive",
5723
+ "permission split follows backend routes: upsert_buttons/patch_buttons/remove_buttons require EditAppAuth; view_configs also requires ViewManagementAuth (beingViewManageStatus), which falls back to DataManageAuth when advanced app permissions are not enabled",
4391
5724
  "view_configs binds custom buttons into views in the same apply call; button_ref may be a same-call client_key, a button_id, or an exact unique existing button_text",
4392
5725
  "view_configs[].view_key is the raw builder view key from app_get.views[].view_key; do not pass record-data view_id values like custom:VIEW_KEY",
4393
5726
  "view_configs[].buttons is required in merge mode; omitting buttons is blocked to avoid no-op writes and accidental publish",
@@ -4400,7 +5733,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4400
5733
  "if a removed button returns readback_status=unavailable or still_exists, treat the result as readback pending and do not blindly repeat the delete",
4401
5734
  "all operations share one edit context and publish after at least one write succeeds; there is no draft-only mode for this tool",
4402
5735
  "background_color and text_color cannot both be white",
4403
- "accepts apps[] for multi-app batch; each item is {app_key, upsert_buttons?, patch_buttons?, remove_buttons?, view_configs?}",
4404
5736
  ],
4405
5737
  "minimal_example": {
4406
5738
  "profile": "default",
@@ -4476,7 +5808,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4476
5808
  "app_associated_resources_apply": {
4477
5809
  "allowed_keys": [
4478
5810
  "app_key",
4479
- "apps",
4480
5811
  "upsert_resources",
4481
5812
  "patch_resources",
4482
5813
  "remove_associated_item_ids",
@@ -4533,6 +5864,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4533
5864
  "this tool manages Qingflow in-app associated report/view display; it does not create or edit QingBI report bodies/configs",
4534
5865
  "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",
4535
5866
  "this is the default associated report/view path; it manages both the app-level associated resource pool and per-view display config",
5867
+ "permission split follows backend routes: upsert_resources/patch_resources/remove/reorder require EditAppAuth; view_configs require ViewManagementAuth (beingViewManageStatus), which falls back to DataManageAuth when advanced app permissions are not enabled",
4536
5868
  "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",
4537
5869
  "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",
4538
5870
  "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",
@@ -4546,7 +5878,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4546
5878
  "if an associated resource delete returns readback_status=unavailable or still_exists, treat the result as readback pending and do not blindly repeat the delete",
4547
5879
  "this tool publishes after at least one write succeeds; there is no draft-only mode",
4548
5880
  "visible=false hides the associated-resource area without clearing previous selected ids; visible=true with limit_type=all shows the whole app-level pool",
4549
- "accepts apps[] for multi-app batch; each item is {app_key, upsert_resources?, patch_resources?, remove_associated_item_ids?, reorder_associated_item_ids?, view_configs?}",
4550
5881
  ],
4551
5882
  "minimal_example": {
4552
5883
  "profile": "default",
@@ -4577,22 +5908,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4577
5908
  }
4578
5909
  ],
4579
5910
  },
4580
- "batch_example": {
4581
- "profile": "default",
4582
- "apps": [
4583
- {
4584
- "app_key": "APP_1",
4585
- "upsert_resources": [
4586
- {"client_key": "orders_view", "graph_type": "view", "target_app_key": "ORDER_APP", "view_key": "ORDER_VIEW"}
4587
- ],
4588
- "view_configs": [{"view_key": "MAIN_VIEW", "limit_type": "select", "associated_item_refs": ["orders_view"]}],
4589
- },
4590
- {
4591
- "app_key": "APP_2",
4592
- "view_configs": [{"view_key": "MAIN_VIEW", "visible": True, "limit_type": "all"}],
4593
- },
4594
- ],
4595
- },
4596
5911
  },
4597
5912
  "app_schema_plan": {
4598
5913
  "allowed_keys": ["app_key", "package_id", "app_name", "icon", "color", "visibility", "create_if_missing", "add_fields", "update_fields", "remove_fields"],
@@ -4636,6 +5951,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4636
5951
  "create mode may set visibility for the new app; edit mode may update visibility on an existing app",
4637
5952
  "create mode should include explicit non-template icon + color; apply mode enforces this before writing",
4638
5953
  "call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
5954
+ "do not add form fields named 数据ID, 编号, 申请人, 申请时间, 创建人, 创建时间, 提交人, 提交时间, 更新时间, 更新人, 当前流程状态, 当前处理人, 当前处理节点, or 流程标题; these are Qingflow built-in system fields, not fields to create",
4639
5955
  *_VISIBILITY_EXECUTION_NOTES,
4640
5956
  ],
4641
5957
  "minimal_example": {
@@ -4672,8 +5988,12 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4672
5988
  "app_key",
4673
5989
  "package_id",
4674
5990
  "app_name",
5991
+ "app_title",
4675
5992
  "icon",
4676
5993
  "color",
5994
+ "icon_name",
5995
+ "icon_color",
5996
+ "icon_config",
4677
5997
  "visibility",
4678
5998
  "create_if_missing",
4679
5999
  "publish",
@@ -4686,11 +6006,15 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4686
6006
  "apps[].app_name",
4687
6007
  "apps[].icon",
4688
6008
  "apps[].color",
6009
+ "apps[].icon_name",
6010
+ "apps[].icon_color",
6011
+ "apps[].icon_config",
4689
6012
  "apps[].visibility",
4690
6013
  "apps[].add_fields",
4691
6014
  "apps[].update_fields",
4692
6015
  "apps[].remove_fields",
4693
6016
  "apps[].add_fields[].target_app_ref",
6017
+ "apps[].add_fields[].target_app",
4694
6018
  ],
4695
6019
  "aliases": {
4696
6020
  "app_title": "app_name",
@@ -4700,8 +6024,16 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4700
6024
  "apps[].appKey": "apps[].app_key",
4701
6025
  "apps[].appName": "apps[].app_name",
4702
6026
  "apps[].appTitle": "apps[].app_name",
6027
+ "apps[].iconName": "apps[].icon",
6028
+ "apps[].iconColor": "apps[].color",
6029
+ "apps[].icon_config.name": "apps[].icon",
6030
+ "apps[].icon_config.color": "apps[].color",
4703
6031
  "field.targetAppRef": "field.target_app_ref",
4704
6032
  "field.targetAppClientKey": "field.target_app_ref",
6033
+ "field.targetApp": "field.target_app",
6034
+ "field.options[].label": "field.options[]",
6035
+ "field.options[].value": "field.options[]",
6036
+ "field.options[].optValue": "field.options[]",
4705
6037
  "field.title": "field.name",
4706
6038
  "field.label": "field.name",
4707
6039
  "field.fields": "field.subfields",
@@ -4727,6 +6059,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4727
6059
  },
4728
6060
  "allowed_values": {
4729
6061
  "field.type": [member.value for member in PublicFieldType],
6062
+ "field.type_aliases": {alias: field_type.value for alias, field_type in sorted(FIELD_TYPE_ALIASES.items())},
4730
6063
  "field.relation_mode": [member.value for member in PublicRelationMode],
4731
6064
  "field.department_scope.mode": ["all", "custom"],
4732
6065
  "field.code_block_binding.outputs.target_field.type": list(INTEGRATION_OUTPUT_TARGET_FIELD_TYPES),
@@ -4738,14 +6071,22 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4738
6071
  "use exactly one resource mode",
4739
6072
  "edit mode: app_key, optional app_name to rename the existing app",
4740
6073
  "create mode: package_id + app_name + create_if_missing=true",
6074
+ "create mode follows backend CreateAppBean: package add_app permission is checked on the target package; package edit_app is not required for the create precheck",
4741
6075
  "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",
6076
+ "CLI --apps-file primary shape is {package_id, apps:[...]}; raw app arrays and singleton wrapper arrays are compatibility paths, not recommended examples",
6077
+ "multi-app mode preflights static errors before writing: duplicate client_key/app_name, missing data title on new apps, create_if_missing omissions, and unresolved target_app_ref/target_app",
4742
6078
  "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",
6079
+ "multi-app relation fields may also use target_app with another apps[].app_name; prefer target_app_ref/client_key when names may collide",
4743
6080
  "multi-app mode is not transactional; read created_app_keys and apps[].status before retrying, and retry only failed app items",
4744
6081
  "create mode defaults new app visibility to workspace/not when visibility is omitted; edit mode preserves current visibility when omitted",
4745
6082
  "create mode requires explicit icon + color; icon=template is blocked because it is too generic",
6083
+ "agent-facing primary icon shape is icon + color; icon_name/icon_color, icon_config, and icon={name,color} are compatibility aliases that normalize to icon/color",
4746
6084
  "multi-app create mode requires each new app item to include a distinct non-template icon and a valid color",
6085
+ "agent-friendly field type aliases are normalized before writing: multiline/multiline_text/textarea -> long_text; select/single_choice/dropdown -> single_select; multi_choice/multiple_choice/checkbox -> multi_select",
6086
+ "single_select and multi_select options accept strings or objects such as {label,value}; builder normalizes them to option labels before writing",
4747
6087
  "edit mode preserves existing icon/color when omitted; explicit icon/color values are still validated",
4748
6088
  "call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
6089
+ "do not add form fields named 数据ID, 编号, 申请人, 申请时间, 创建人, 创建时间, 提交人, 提交时间, 更新时间, 更新人, 当前流程状态, 当前处理人, 当前处理节点, or 流程标题; these are Qingflow built-in system fields, not fields to create",
4749
6090
  *_VISIBILITY_EXECUTION_NOTES,
4750
6091
  "update_fields is the field-level partial update path; it reads current form schema and preserves untouched field config",
4751
6092
  "multiple relation fields are backend-risky; read verification.relation_field_limit_verified and warnings before declaring the schema stable",
@@ -4753,6 +6094,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4753
6094
  "relation_mode=multiple maps to referenceConfig.optionalDataNum=0",
4754
6095
  "relation fields now require both display_field and visible_fields in MCP/CLI payloads",
4755
6096
  "if relation target metadata lookup is blocked by 40161/40002/40027, explicit display_field.name and visible_fields[].name let builder degrade verification and still continue schema write",
6097
+ "relation write readback returns details.relation_readback_matrix; mismatches include details.relation_repair_plan with a minimal update_fields relation patch and data_impact",
4756
6098
  "update_fields[].set.subfield_updates is the safe patch path for editing existing subtable child fields without rebuilding the entire subtable",
4757
6099
  "subfield_updates only supports safe child overlays: name, required, description, and nested subfield_updates",
4758
6100
  "set.subfields remains the full replace/rebuild path for a subtable and is higher risk when hidden relation/reference children exist",
@@ -4968,7 +6310,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4968
6310
  "preset_example": {"profile": "default", "app_key": "APP_KEY", "mode": "merge", "preset": "balanced", "sections": []},
4969
6311
  },
4970
6312
  "app_layout_apply": {
4971
- "allowed_keys": ["app_key", "mode", "publish", "sections", "apps"],
6313
+ "allowed_keys": ["app_key", "mode", "publish", "sections"],
4972
6314
  "aliases": {"overwrite": "replace", "sectionId": "section_id"},
4973
6315
  "section_allowed_keys": ["type", "paragraph_id", "section_id", "title", "rows"],
4974
6316
  "section_aliases": {
@@ -4985,7 +6327,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4985
6327
  "mode=replace is full layout replacement and should be used only when intentionally rewriting all sections",
4986
6328
  "layout verification is split into layout_verified and layout_summary_verified",
4987
6329
  "LAYOUT_SUMMARY_UNVERIFIED means raw form readback is stronger than the compact summary",
4988
- "accepts apps[] for multi-app batch; each item is {app_key, mode?, sections, publish?}; top-level publish is used as default for items that omit publish",
4989
6330
  ],
4990
6331
  "minimal_section_example": {"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["字段A", "字段B", "字段C", "字段D"]]},
4991
6332
  "minimal_example": {
@@ -4995,73 +6336,95 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4995
6336
  "publish": True,
4996
6337
  "sections": [{"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["项目名称", "项目负责人", "项目阶段", "优先级"]]}],
4997
6338
  },
4998
- "batch_example": {
4999
- "profile": "default",
5000
- "publish": True,
5001
- "apps": [
5002
- {"app_key": "APP_KEY_1", "mode": "merge", "sections": [{"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["字段A", "字段B"]]}]},
5003
- {"app_key": "APP_KEY_2", "mode": "merge", "sections": [{"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["字段C", "字段D"]]}]},
5004
- ],
5005
- },
5006
6339
  },
5007
- "app_flow_get_schema": {
5008
- "allowed_keys": ["schema_version"],
5009
- "aliases": {"schemaVersion": "schema_version"},
5010
- "allowed_values": {},
5011
- "execution_notes": [
5012
- "returns WorkflowSpec JSON Schema from /workflow/spec/schema",
5013
- "call this before authoring a new spec or validating spec shape",
5014
- "worksheet-level approval deduplication toggles (legacy global settings) are not yet part of WorkflowSpec",
5015
- ],
5016
- "minimal_example": {
5017
- "profile": "default",
5018
- "schema_version": "vnext-2026-06",
6340
+ "app_flow_plan": {
6341
+ "allowed_keys": ["app_key", "mode", "nodes", "transitions", "preset"],
6342
+ "aliases": {
6343
+ "overwrite": "replace",
6344
+ "base_preset": "preset",
6345
+ "default_approval": "basic_approval",
6346
+ "node.role_names": "node.assignees.role_names",
6347
+ "node.role_ids": "node.assignees.role_ids",
6348
+ "node.member_names": "node.assignees.member_names",
6349
+ "node.member_emails": "node.assignees.member_emails",
6350
+ "node.member_uids": "node.assignees.member_uids",
6351
+ "node.editable_fields": "node.permissions.editable_fields",
6352
+ "default_approval": "basic_approval",
5019
6353
  },
5020
- },
5021
- "app_flow_get": {
5022
- "allowed_keys": ["app_key", "version_id"],
5023
- "aliases": {"versionId": "version_id"},
5024
- "allowed_values": {},
6354
+ "allowed_values": {
6355
+ "mode": ["replace"],
6356
+ "preset": [member.value for member in FlowPreset],
6357
+ "node.type": PUBLIC_STABLE_FLOW_NODE_TYPES,
6358
+ },
6359
+ "dependency_hints": [
6360
+ "approval-style workflows require an explicit business status select field before app_flow_apply, such as 状态 / 处理状态 / 审批状态 / 工单状态",
6361
+ "approve/fill/copy nodes require at least one assignee",
6362
+ ],
5025
6363
  "execution_notes": [
5026
- "returns current WorkflowSpecDTO for one app via GET /workflow/spec",
5027
- "use GET-first before patching and app_flow_apply",
6364
+ "public flow building is intentionally limited to linear workflows",
6365
+ "branch and condition nodes are disabled because the backend workflow route is not front-end stable for these node types",
6366
+ "do not create platform system workflow fields such as 当前流程状态 / 当前处理人 / 当前处理节点 / 流程标题; use an explicit business status field instead",
5028
6367
  ],
5029
6368
  "minimal_example": {
5030
6369
  "profile": "default",
5031
6370
  "app_key": "APP_KEY",
6371
+ "mode": "replace",
6372
+ "preset": "basic_approval",
6373
+ "nodes": [
6374
+ {
6375
+ "id": "approve_1",
6376
+ "type": "approve",
6377
+ "name": "部门审批",
6378
+ "assignees": {"role_names": ["项目经理"]},
6379
+ "permissions": {"editable_fields": ["状态", "审批意见"]},
6380
+ }
6381
+ ],
6382
+ "transitions": [],
5032
6383
  },
5033
6384
  },
5034
6385
  "app_flow_apply": {
5035
- "allowed_keys": ["app_key", "publish", "spec", "idempotency_key", "schema_version", "patch_nodes"],
6386
+ "allowed_keys": ["app_key", "mode", "publish", "nodes", "transitions"],
5036
6387
  "aliases": {
5037
- "schemaVersion": "schema_version",
5038
- "idempotencyKey": "idempotency_key",
6388
+ "overwrite": "replace",
6389
+ "node.role_names": "node.assignees.role_names",
6390
+ "node.role_ids": "node.assignees.role_ids",
6391
+ "node.member_names": "node.assignees.member_names",
6392
+ "node.member_emails": "node.assignees.member_emails",
6393
+ "node.member_uids": "node.assignees.member_uids",
6394
+ "node.editable_fields": "node.permissions.editable_fields",
6395
+ },
6396
+ "allowed_values": {
6397
+ "mode": ["replace"],
6398
+ "node.type": PUBLIC_STABLE_FLOW_NODE_TYPES,
5039
6399
  },
5040
- "allowed_values": {},
5041
6400
  "dependency_hints": [
5042
- "when using spec: must be a complete WorkflowSpecDTO object (replace-only apply); when using patch_nodes[]: spec is not required",
5043
- "use patch_nodes[] instead of spec when updating only specific nodes; patch_nodes reads current flow, merges set/unset, then writes back — spec is not needed when patch_nodes is supplied",
6401
+ "approval-style workflows require an explicit business status select field before app_flow_apply, such as 状态 / 处理状态 / 审批状态 / 工单状态",
6402
+ "approve/fill/copy nodes require at least one assignee",
5044
6403
  ],
5045
6404
  "execution_notes": [
5046
- "posts to /workflow/spec:apply with appKey, idempotencyKey, schemaVersion, and spec",
5047
- "verification uses appliedSpec, diffSummary, and semanticLint from the apply response",
5048
- "publish=false keeps changes in draft when supported by the backend",
5049
- "patch_nodes[] items are {id, set, unset}; id must match an existing node id from app_get_flow.spec.nodes; FLOW_NODE_NOT_FOUND is returned if id is missing",
6405
+ "public flow building is intentionally limited to linear workflows",
6406
+ "app_flow_apply is replace-only; do not treat node snippets as partial patches",
6407
+ "branch and condition nodes are disabled because the backend workflow route is not front-end stable for these node types",
6408
+ "do not create platform system workflow fields such as 当前流程状态 / 当前处理人 / 当前处理节点 / 流程标题; use an explicit business status field instead",
6409
+ "workflow verification only covers linear node structure in the public tool surface",
5050
6410
  ],
5051
6411
  "minimal_example": {
5052
6412
  "profile": "default",
5053
6413
  "app_key": "APP_KEY",
6414
+ "mode": "replace",
5054
6415
  "publish": True,
5055
- "spec": {
5056
- "nodes": [{"id": "n1", "type": "APPLICANT", "name": "发起"}],
5057
- "transitions": [],
5058
- },
5059
- },
5060
- "patch_nodes_example": {
5061
- "profile": "default",
5062
- "app_key": "APP_KEY",
5063
- "publish": True,
5064
- "patch_nodes": [{"id": "approve_1", "set": {"name": "总监审批", "assignees": {"role_names": ["总监"]}}}],
6416
+ "nodes": [
6417
+ {"id": "start", "type": "start", "name": "发起"},
6418
+ {
6419
+ "id": "approve_1",
6420
+ "type": "approve",
6421
+ "name": "部门审批",
6422
+ "assignees": {"role_names": ["项目经理"]},
6423
+ "permissions": {"editable_fields": ["状态", "审批意见"]},
6424
+ },
6425
+ {"id": "end", "type": "end", "name": "结束"},
6426
+ ],
6427
+ "transitions": [{"from": "start", "to": "approve_1"}, {"from": "approve_1", "to": "end"}],
5065
6428
  },
5066
6429
  },
5067
6430
  "app_views_plan": {
@@ -5119,12 +6482,15 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5119
6482
  **deepcopy(_VISIBILITY_ALLOWED_VALUES),
5120
6483
  },
5121
6484
  "execution_notes": [
6485
+ "creating a new view follows backend createViewgraphConfig and requires both ViewManagementAuth and DataManageAuth; updating/deleting existing views only requires ViewManagementAuth",
5122
6486
  "upsert_views[].visibility may set per-view visibility; omit it to preserve an existing view's auth or default a new view to workspace/not",
5123
6487
  "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",
5124
- "upsert_views[].query_conditions.rows is a layout matrix of field names; it is compiled to backend queryCondition queIds",
6488
+ "upsert_views[].query_conditions.rows is a layout matrix of query-panel supported field names; it is compiled to backend queryCondition queIds",
6489
+ "do not put relation/attachment/subtable/code_block/q_linker/address fields in query_conditions; use filters for fixed filters or app_associated_resources_apply.match_mappings for current-record relation/report matching",
5125
6490
  "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",
5126
6491
  "remove_views accepts a raw view_key or an exact unique view name; after DELETE the tool verifies deletion by single view_key readback, not by a full app view list",
5127
6492
  "deleted views return verification.by_view[].delete_executed, readback_status, and safe_to_retry_delete=false; if readback is pending, do not blindly repeat the delete",
6493
+ "do not create business views named 全部数据, 我的数据, 我发起的, 待办, 已办, or 抄送; these are built-in system/default views. Use business-specific names for new views, and pass the existing raw view_key or patch_views when changing a built-in view",
5128
6494
  "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",
5129
6495
  "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",
5130
6496
  "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",
@@ -5133,7 +6499,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5133
6499
  "minimal_example": {
5134
6500
  "profile": "default",
5135
6501
  "app_key": "APP_KEY",
5136
- "upsert_views": [{"name": "全部数据", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
6502
+ "upsert_views": [{"name": "项目台账视图", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
5137
6503
  "remove_views": [],
5138
6504
  },
5139
6505
  "query_conditions_example": {
@@ -5171,6 +6537,14 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5171
6537
  ],
5172
6538
  "remove_views": [],
5173
6539
  },
6540
+ "query_conditions_field_rules": {
6541
+ "supported_field_types": ["text", "long_text", "number", "amount", "date", "datetime", "single_select", "multi_select", "phone", "email", "boolean", "member", "department"],
6542
+ "unsupported_field_types": ["relation", "attachment", "subtable", "address", "code_block", "q_linker"],
6543
+ "use_instead": {
6544
+ "fixed_filter": "filters",
6545
+ "current_record_related_report_or_view": "app_associated_resources_apply.match_mappings",
6546
+ },
6547
+ },
5174
6548
  "gantt_example": {
5175
6549
  "profile": "default",
5176
6550
  "app_key": "APP_KEY",
@@ -5191,7 +6565,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5191
6565
  "app_views_apply": {
5192
6566
  "allowed_keys": [
5193
6567
  "app_key",
5194
- "apps",
5195
6568
  "publish",
5196
6569
  "upsert_views",
5197
6570
  "patch_views",
@@ -5244,27 +6617,29 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5244
6617
  },
5245
6618
  "execution_notes": [
5246
6619
  "apply may return partial_success when some views land and others fail",
6620
+ "creating a new view follows backend createViewgraphConfig and requires both ViewManagementAuth and DataManageAuth; updating/deleting existing views only requires ViewManagementAuth",
5247
6621
  "when duplicate view names exist, supply view_key to target the exact view",
5248
6622
  "read back app_get after any failed or partial view apply",
5249
6623
  "view existence verification and saved-filter verification are separate; treat filters as unverified until verification.view_filters_verified is true",
5250
6624
  "buttons omitted preserves existing button config; buttons=[] clears all buttons; buttons=[...] replaces the full button config",
5251
6625
  "upsert_views[].visibility may set per-view visibility; omit it to preserve an existing view's auth or default a new view to workspace/not",
5252
6626
  "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",
5253
- "upsert_views[].query_conditions.rows is a layout matrix of field names; it is compiled to backend queryCondition queIds",
6627
+ "upsert_views[].query_conditions.rows is a layout matrix of query-panel supported field names; it is compiled to backend queryCondition queIds",
6628
+ "do not put relation/attachment/subtable/code_block/q_linker/address fields in query_conditions; use filters for fixed filters or app_associated_resources_apply.match_mappings for current-record relation/report matching",
5254
6629
  "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",
5255
6630
  "remove_views accepts a raw view_key or an exact unique view name; after DELETE the tool verifies deletion by single view_key readback, not by a full app view list",
5256
6631
  "deleted views return verification.by_view[].delete_executed, readback_status, and safe_to_retry_delete=false; if readback is pending, do not blindly repeat the delete",
6632
+ "do not create business views named 全部数据, 我的数据, 我发起的, 待办, 已办, or 抄送; these are built-in system/default views. Use business-specific names for new views, and pass the existing raw view_key or patch_views when changing a built-in view",
5257
6633
  "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",
5258
6634
  "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",
5259
6635
  "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",
5260
- "accepts apps[] for multi-app batch; each item is {app_key, upsert_views?, patch_views?, remove_views?, publish?}; top-level publish is used as default for items that omit publish",
5261
6636
  *_VISIBILITY_EXECUTION_NOTES,
5262
6637
  ],
5263
6638
  "minimal_example": {
5264
6639
  "profile": "default",
5265
6640
  "app_key": "APP_KEY",
5266
6641
  "publish": True,
5267
- "upsert_views": [{"name": "全部数据", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
6642
+ "upsert_views": [{"name": "项目台账视图", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
5268
6643
  "remove_views": [],
5269
6644
  },
5270
6645
  "query_conditions_example": {
@@ -5304,6 +6679,14 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5304
6679
  ],
5305
6680
  "remove_views": [],
5306
6681
  },
6682
+ "query_conditions_field_rules": {
6683
+ "supported_field_types": ["text", "long_text", "number", "amount", "date", "datetime", "single_select", "multi_select", "phone", "email", "boolean", "member", "department"],
6684
+ "unsupported_field_types": ["relation", "attachment", "subtable", "address", "code_block", "q_linker"],
6685
+ "use_instead": {
6686
+ "fixed_filter": "filters",
6687
+ "current_record_related_report_or_view": "app_associated_resources_apply.match_mappings",
6688
+ },
6689
+ },
5307
6690
  "gantt_example": {
5308
6691
  "profile": "default",
5309
6692
  "app_key": "APP_KEY",
@@ -5330,8 +6713,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5330
6713
  "returns builder-side app map: base summary, editability, field/view/chart/button counts, compact views, compact charts, custom_buttons, and app-level associated_resources",
5331
6714
  "use this as the default builder discovery read before view_get/chart_get/apply detail work",
5332
6715
  "editability is route-aware builder capability summary, not end-user data visibility",
5333
- "can_edit_app_base covers app base-info writes such as app_name, icon, and visibility",
5334
- "can_edit_form covers form/schema routes only and does not imply app base-info writes",
6716
+ "can_edit_app_base covers app base-info writes such as app_name, icon, and visibility; it follows backend EditAppAuth and does not require package edit_tag",
6717
+ "can_edit_form covers form/schema routes and also follows backend EditAppAuth",
5335
6718
  "returns normalized app visibility when backend auth is readable",
5336
6719
  "custom_buttons[].button_id is the id required by app_custom_buttons_apply view_configs[].buttons[].button_ref",
5337
6720
  "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",
@@ -5342,7 +6725,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5342
6725
  },
5343
6726
  },
5344
6727
  "app_get_fields": {
5345
- "allowed_keys": ["app_key", "app_keys"],
6728
+ "allowed_keys": ["app_key"],
5346
6729
  "aliases": {},
5347
6730
  "allowed_values": {},
5348
6731
  "execution_notes": [
@@ -5350,8 +6733,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5350
6733
  "use this before app_schema_apply when you need exact field definitions",
5351
6734
  "also returns chart_fields from QingBI datasource fields; app_charts_apply field selectors should use chart_fields because record/schema-visible fields and QingBI fields are not the same schema",
5352
6735
  "chart_fields[].field_id supports field_<queId> selectors, while chart_fields[].bi_field_id is the raw QingBI fieldId accepted by report configs",
6736
+ "chart_fields[].chart_apply_examples contains copyable semantic app_charts_apply snippets such as count_by_field, filtered_count, and numeric sum_metric",
5353
6737
  "subtable fields include nested subfields using the same compact field shape",
5354
- "accepts app_keys[] for batch read; batch returns {status, apps[], errors[]} where each apps[] item has {app_key, fields}",
5355
6738
  ],
5356
6739
  "minimal_example": {
5357
6740
  "profile": "default",
@@ -5359,13 +6742,12 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5359
6742
  },
5360
6743
  },
5361
6744
  "app_get_layout": {
5362
- "allowed_keys": ["app_key", "app_keys"],
6745
+ "allowed_keys": ["app_key"],
5363
6746
  "aliases": {},
5364
6747
  "allowed_values": {},
5365
6748
  "execution_notes": [
5366
6749
  "returns compact current layout configuration for one app",
5367
6750
  "use this before app_layout_apply when you need paragraph and row structure",
5368
- "accepts app_keys[] for batch read; batch returns {status, apps[], errors[]} where each apps[] item has {app_key, sections}",
5369
6751
  ],
5370
6752
  "minimal_example": {
5371
6753
  "profile": "default",
@@ -5373,7 +6755,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5373
6755
  },
5374
6756
  },
5375
6757
  "app_get_views": {
5376
- "allowed_keys": ["app_key", "app_keys"],
6758
+ "allowed_keys": ["app_key"],
5377
6759
  "aliases": {},
5378
6760
  "allowed_values": {},
5379
6761
  "execution_notes": [
@@ -5381,7 +6763,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5381
6763
  "compatibility/specialized inventory tool; default builder discovery should start with app_get",
5382
6764
  "use this before app_views_apply only when you need an exact current view inventory beyond app_get",
5383
6765
  "view items include visibility_summary when backend view auth is readable",
5384
- "accepts app_keys[] for batch read; batch returns {status, apps[], errors[]} where each apps[] item has {app_key, views}",
5385
6766
  ],
5386
6767
  "minimal_example": {
5387
6768
  "profile": "default",
@@ -5389,13 +6770,12 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5389
6770
  },
5390
6771
  },
5391
6772
  "app_get_flow": {
5392
- "allowed_keys": ["app_key", "app_keys"],
6773
+ "allowed_keys": ["app_key"],
5393
6774
  "aliases": {},
5394
6775
  "allowed_values": {},
5395
6776
  "execution_notes": [
5396
- "returns WorkflowSpecDTO for one app (alias of app_flow_get)",
5397
- "use app_flow_get_schema then app_flow_get before app_flow_apply",
5398
- "accepts app_keys[] for batch read; batch returns {status, apps[], errors[]} where each apps[] item has {app_key, spec}",
6777
+ "returns workflow configuration summary for one app",
6778
+ "use this before app_flow_apply when you need the current node structure",
5399
6779
  ],
5400
6780
  "minimal_example": {
5401
6781
  "profile": "default",
@@ -5403,7 +6783,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5403
6783
  },
5404
6784
  },
5405
6785
  "app_get_charts": {
5406
- "allowed_keys": ["app_key", "app_keys"],
6786
+ "allowed_keys": ["app_key"],
5407
6787
  "aliases": {},
5408
6788
  "allowed_values": {},
5409
6789
  "execution_notes": [
@@ -5412,37 +6792,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5412
6792
  "use this before app_charts_apply when you need exact current chart_id values beyond the app_get summary",
5413
6793
  "chart summaries do not include full qingbi config payloads",
5414
6794
  "chart items include visibility_summary when QingBI base info is readable",
5415
- "accepts app_keys[] for batch read; batch returns {status, apps[], errors[]} where each apps[] item has {app_key, charts}",
5416
- ],
5417
- "minimal_example": {
5418
- "profile": "default",
5419
- "app_key": "APP_KEY",
5420
- },
5421
- },
5422
- "app_get_buttons": {
5423
- "allowed_keys": ["app_key", "app_keys"],
5424
- "aliases": {},
5425
- "allowed_values": {},
5426
- "execution_notes": [
5427
- "returns custom button list (draft state) for one app",
5428
- "also returns view_configs read from view bindings, so app_custom_buttons_apply view_configs can be patched from the same read result",
5429
- "accepts app_keys[] for batch read; batch returns {status, apps[], errors[]} where each apps[] item preserves the single-read fields such as {app_key, buttons, view_configs, warnings, verification}",
5430
- "use before app_custom_buttons_apply when you need current button_id values",
5431
- ],
5432
- "minimal_example": {
5433
- "profile": "default",
5434
- "app_key": "APP_KEY",
5435
- },
5436
- },
5437
- "app_get_associated_resources": {
5438
- "allowed_keys": ["app_key", "app_keys"],
5439
- "aliases": {},
5440
- "allowed_values": {},
5441
- "execution_notes": [
5442
- "returns associated resource pool (draft state) for one app",
5443
- "also returns view_configs read from view bindings, so app_associated_resources_apply view_configs can be patched from the same read result",
5444
- "accepts app_keys[] for batch read; batch returns {status, apps[], errors[]} where each apps[] item preserves the single-read fields such as {app_key, associated_resources, view_configs, warnings, verification}",
5445
- "use before app_associated_resources_apply when you need current associated_item_id values",
5446
6795
  ],
5447
6796
  "minimal_example": {
5448
6797
  "profile": "default",
@@ -5457,6 +6806,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5457
6806
  "returns builder-configurable portal list items only",
5458
6807
  "use this as the builder portal discovery path before portal_get",
5459
6808
  "results are compact list items, not raw dash payloads",
6809
+ "large workspaces may return an unfiltered discovery list with portal_permissions_verified=false instead of probing every portal detail",
5460
6810
  ],
5461
6811
  "minimal_example": {
5462
6812
  "profile": "default",
@@ -5479,7 +6829,22 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5479
6829
  },
5480
6830
  },
5481
6831
  "app_charts_apply": {
5482
- "allowed_keys": ["app_key", "apps", "upsert_charts", "patch_charts", "remove_chart_ids", "reorder_chart_ids", "upsert_charts[].visibility", "patch_charts[].chart_id", "patch_charts[].name", "patch_charts[].set", "patch_charts[].unset"],
6832
+ "allowed_keys": [
6833
+ "app_key",
6834
+ "upsert_charts",
6835
+ "patch_charts",
6836
+ "remove_chart_ids",
6837
+ "reorder_chart_ids",
6838
+ "upsert_charts[].metric",
6839
+ "upsert_charts[].metrics",
6840
+ "upsert_charts[].group_by",
6841
+ "upsert_charts[].where",
6842
+ "upsert_charts[].visibility",
6843
+ "patch_charts[].chart_id",
6844
+ "patch_charts[].name",
6845
+ "patch_charts[].set",
6846
+ "patch_charts[].unset",
6847
+ ],
5483
6848
  "aliases": {
5484
6849
  "patchCharts": "patch_charts",
5485
6850
  "chart.id": "chart.chart_id",
@@ -5487,6 +6852,11 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5487
6852
  "chart.dimension_fields": "chart.dimension_field_ids",
5488
6853
  "chart.indicator_fields": "chart.indicator_field_ids",
5489
6854
  "chart.metric_field_ids": "chart.indicator_field_ids",
6855
+ "chart.dimensions": "chart.group_by",
6856
+ "chart.groupBy": "chart.group_by",
6857
+ "chart.where": "chart.filters",
6858
+ "chart.metric.operation": "chart.metric.op",
6859
+ "chart.metric.aggregation": "chart.metric.op",
5490
6860
  "chart.filter.op": "chart.filter.operator",
5491
6861
  },
5492
6862
  "allowed_values": {
@@ -5507,18 +6877,20 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5507
6877
  "upsert_charts[].visibility compiles to QingBI base visibleAuth only",
5508
6878
  "visibility-only updates keep the existing chart config and do not rewrite rawDataConfigDTO.authInfo",
5509
6879
  "chart dimension/metric/filter/query fields are resolved from app_get_fields.chart_fields (QingBI datasource fields), not record schema or form-only fields",
6880
+ "preferred chart metric DSL is SQL-like: metric='count(*)', metric='sum(金额)', metrics=['sum(金额)'], group_by=['状态'], where=[{field, op, value}]",
6881
+ "legacy dimension_field_ids/indicator_field_ids/config.aggregate remain supported as advanced compatibility input, but should not be the first choice for agents",
6882
+ "chart_get returns semantic group_by and metrics; raw QingBI config is diagnostic detail",
5510
6883
  "system fields such as 申请人/申请时间/编号 are usable only when they appear in chart_fields; otherwise app_charts_apply returns CHART_FIELD_NOT_IN_QINGBI_SCHEMA",
5511
6884
  "low-frequency chart types have local prevalidation: gauge requires 0 dimensions and 2 non-duplicated metrics; histogram requires at most 1 dimension and exactly 1 plain numeric metric",
5512
6885
  "chart rule failures return chart_results[].diagnostics with rule_code, expected, actual, offending_fields, and next_action; backend 81002/81005 are translated when possible",
5513
6886
  "remove_chart_ids deletes by chart_id and verifies each deleted chart with single chart_id readback; pure delete does not read the full chart list",
5514
6887
  "if delete readback is unavailable or still finds the chart, chart_results[] returns delete_executed=true, readback_status, and safe_to_retry_delete=false; do not blindly repeat delete",
5515
- "accepts apps[] for multi-app batch; each item is {app_key, upsert_charts?, patch_charts?, remove_chart_ids?, reorder_chart_ids?}",
5516
6888
  *_VISIBILITY_EXECUTION_NOTES,
5517
6889
  ],
5518
6890
  "minimal_example": {
5519
6891
  "profile": "default",
5520
6892
  "app_key": "APP_KEY",
5521
- "upsert_charts": [{"name": "数据总量", "chart_type": "target", "indicator_field_ids": [], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
6893
+ "upsert_charts": [{"name": "数据总量", "chart_type": "target", "metric": "count(*)", "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
5522
6894
  "patch_charts": [{"chart_id": "CHART_ID", "set": {"name": "数据总量-新版"}}],
5523
6895
  "remove_chart_ids": [],
5524
6896
  "reorder_chart_ids": [],
@@ -5556,7 +6928,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5556
6928
  },
5557
6929
  },
5558
6930
  "portal_apply": {
5559
- "allowed_keys": ["dash_key", "dash_name", "name", "package_id", "publish", "sections", "patch_sections", "pages", "payload", "layout_preset", "visibility", "auth", "icon", "color", "hide_copyright", "dash_global_config", "config"],
6931
+ "allowed_keys": ["dash_key", "dash_name", "name", "package_id", "publish", "sections", "pages", "payload", "layout_preset", "visibility", "auth", "icon", "color", "hide_copyright", "dash_global_config", "config"],
5560
6932
  "aliases": {
5561
6933
  "packageId": "package_id",
5562
6934
  "name": "dash_name",
@@ -5565,9 +6937,11 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5565
6937
  "viewRef": "view_ref",
5566
6938
  "dashStyleConfigBO": "dash_style_config",
5567
6939
  },
5568
- "section_allowed_keys": ["title", "source_type", "position", "dash_style_config", "config", "chart_ref", "view_ref", "text", "url"],
6940
+ "section_allowed_keys": ["title", "source_type", "role", "position", "dash_style_config", "config", "chart_ref", "view_ref", "text", "url"],
5569
6941
  "section_aliases": {
5570
6942
  "sourceType": "source_type",
6943
+ "zone": "role",
6944
+ "sectionRole": "role",
5571
6945
  "chartRef": "chart_ref",
5572
6946
  "viewRef": "view_ref",
5573
6947
  "dashStyleConfigBO": "dash_style_config",
@@ -5583,22 +6957,25 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5583
6957
  "use exactly one resource mode",
5584
6958
  "update mode: dash_key",
5585
6959
  "create mode: package_id + dash_name",
6960
+ "create mode follows backend DashCtrl.createDash: package add_app permission is checked on the target package; package edit_app is not required for the create precheck",
5586
6961
  "create mode requires explicit icon + color; icon=template is blocked because it is too generic",
5587
6962
  "edit mode preserves existing icon/color when omitted; explicit icon/color values are still validated",
5588
6963
  "call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
5589
6964
  "portal_apply uses replace semantics for sections",
5590
6965
  "when editing an existing portal, sections may be omitted to update only base info such as visibility, icon, or package",
5591
- "use patch_sections[] for section-level patch updates without replacing all sections; each item needs one selector (chart_ref with chart_id/chart_key/chart_name, view_ref with view_key/view_name, or order as 0-based index) plus set/unset",
5592
- "when sections[] is supplied without patch_sections[], it uses replace semantics for all sections",
5593
- "remove a section by omitting it from the sections list (replace mode) or by unset in patch_sections (patch mode)",
6966
+ "portal section-level patch is not exposed; supplying sections means full sections replacement",
6967
+ "remove a section by omitting it from the new sections list",
5594
6968
  "package_id is required when creating a new portal",
5595
6969
  "publish=false only guarantees draft and base-info updates; it does not claim live has changed",
5596
- "chart_ref resolves by chart_id/chart_key first, then exact unique chart_name",
6970
+ "chart_ref resolves by chart_id first, then exact unique chart_name",
5597
6971
  "view_ref resolves by view_key first, then exact unique view_name",
5598
6972
  "pc layout uses a 24-column grid; mobile layout uses a 6-column grid",
5599
6973
  "if unsure about layout, omit position or use layout_preset=auto/dashboard_2col/dashboard_3col",
5600
6974
  "two-column pc layout should use x=0/12 with cols=12; three-column pc layout should use x=0/8/16 with cols=8",
5601
6975
  "x=0/6 with cols=6 only occupies the left half of the pc portal and triggers PORTAL_LAYOUT_HALF_WIDTH",
6976
+ "grid sections must include config.items; an empty grid triggers PORTAL_GRID_ITEMS_EMPTY because the frontend only shows an empty entry container",
6977
+ "metric portal sections may set role=metric; role=metric requires a target/indicator chart and otherwise triggers PORTAL_METRIC_SECTION_CHART_TYPE_MISMATCH",
6978
+ "standard workbench count diagnostics warn when metric cards are not 4-6, BI charts are not 2-3, or business views are not 1-2",
5602
6979
  "position.pc/mobile is the canonical portal layout shape",
5603
6980
  "compat payload accepts name -> dash_name and single pages[0].components -> sections",
5604
6981
  "visibility is the canonical public auth shape; auth is kept only as a deprecated compatibility alias",
@@ -5652,14 +7029,19 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5652
7029
  "mobile": {"x": 0, "y": 0, "cols": 6, "rows": 8},
5653
7030
  },
5654
7031
  },
5655
- "patch_sections_example": {
7032
+ },
7033
+ "portal_delete": {
7034
+ "allowed_keys": ["dash_key"],
7035
+ "aliases": {"dashKey": "dash_key"},
7036
+ "allowed_values": {},
7037
+ "execution_notes": [
7038
+ "deletes one portal by dash_key",
7039
+ "delete results separate DELETE execution from readback verification",
7040
+ "if delete_executed=true and readback_status=unavailable or still_exists, do not blindly repeat delete; confirm later with portal_get or portal_list",
7041
+ ],
7042
+ "minimal_example": {
5656
7043
  "profile": "default",
5657
7044
  "dash_key": "DASH_KEY",
5658
- "publish": True,
5659
- "patch_sections": [
5660
- {"chart_ref": {"chart_id": "CHART_ID"}, "set": {"title": "销售总览-新版"}},
5661
- {"order": 2, "set": {"title": "任务列表-新版"}},
5662
- ],
5663
7045
  },
5664
7046
  },
5665
7047
  "app_publish_verify": {
@@ -5699,6 +7081,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5699
7081
  _PRIVATE_BUILDER_TOOL_CONTRACTS = {
5700
7082
  "app_schema_plan",
5701
7083
  "app_layout_plan",
7084
+ "app_flow_plan",
5702
7085
  "app_views_plan",
5703
7086
  }
5704
7087