@josephyan/qingflow-cli 1.1.4 → 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 +282 -102
  93. package/src/qingflow_mcp/builder_facade/service.py +4166 -929
  94. package/src/qingflow_mcp/cli/commands/builder.py +316 -298
  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 +1780 -406
  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,
@@ -299,15 +498,11 @@ class AiBuilderTools(ToolBase):
299
498
 
300
499
  @mcp.tool()
301
500
  def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
302
- if app_keys:
303
- return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get, data_key="summary", tool_name="app_get")
304
- return self.app_get(profile=profile, app_key=app_key)
501
+ return self.app_get(profile=profile, app_key=app_key, app_keys=app_keys)
305
502
 
306
503
  @mcp.tool()
307
504
  def app_get_fields(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
308
- if app_keys:
309
- 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")
310
- 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)
311
506
 
312
507
  @mcp.tool()
313
508
  def app_repair_code_blocks(
@@ -320,39 +515,27 @@ class AiBuilderTools(ToolBase):
320
515
 
321
516
  @mcp.tool()
322
517
  def app_get_layout(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
323
- if app_keys:
324
- 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")
325
- 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)
326
519
 
327
520
  @mcp.tool()
328
521
  def app_get_views(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
329
- if app_keys:
330
- 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")
331
- 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)
332
523
 
333
524
  @mcp.tool()
334
525
  def app_get_flow(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
335
- if app_keys:
336
- 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")
337
- 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)
338
527
 
339
528
  @mcp.tool()
340
529
  def app_get_charts(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
341
- if app_keys:
342
- 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")
343
- 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)
344
531
 
345
532
  @mcp.tool()
346
533
  def app_get_buttons(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
347
- if app_keys:
348
- 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")
349
- 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)
350
535
 
351
536
  @mcp.tool()
352
537
  def app_get_associated_resources(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
353
- if app_keys:
354
- 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")
355
- 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)
356
539
 
357
540
  @mcp.tool()
358
541
  def portal_list(profile: str = DEFAULT_PROFILE) -> JSONObject:
@@ -384,8 +567,11 @@ class AiBuilderTools(ToolBase):
384
567
  package_id: int | None = None,
385
568
  app_name: str = "",
386
569
  app_title: str = "",
387
- icon: str = "",
570
+ icon: str | JSONObject = "",
388
571
  color: str = "",
572
+ icon_name: str | None = None,
573
+ icon_color: str | None = None,
574
+ icon_config: JSONObject | None = None,
389
575
  visibility: JSONObject | None = None,
390
576
  create_if_missing: bool = False,
391
577
  publish: bool = True,
@@ -394,7 +580,7 @@ class AiBuilderTools(ToolBase):
394
580
  remove_fields: list[JSONObject] | None = None,
395
581
  apps: list[JSONObject] | None = None,
396
582
  ) -> JSONObject:
397
- if apps:
583
+ if apps is not None:
398
584
  if app_key or app_name or app_title or add_fields or update_fields or remove_fields:
399
585
  return _config_failure(
400
586
  tool_name="app_schema_apply",
@@ -443,6 +629,9 @@ class AiBuilderTools(ToolBase):
443
629
  app_title=app_title,
444
630
  icon=icon,
445
631
  color=color,
632
+ icon_name=icon_name,
633
+ icon_color=icon_color,
634
+ icon_config=icon_config,
446
635
  visibility=visibility,
447
636
  create_if_missing=create_if_missing,
448
637
  publish=publish,
@@ -469,7 +658,7 @@ class AiBuilderTools(ToolBase):
469
658
  app_key: str = "",
470
659
  version_id: str = "",
471
660
  ) -> JSONObject:
472
- 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)
473
662
 
474
663
  @mcp.tool()
475
664
  def app_flow_get_schema(profile: str = DEFAULT_PROFILE, schema_version: str = "") -> JSONObject:
@@ -479,7 +668,10 @@ class AiBuilderTools(ToolBase):
479
668
  def app_flow_apply(
480
669
  profile: str = DEFAULT_PROFILE,
481
670
  app_key: str = "",
671
+ mode: str = "replace",
482
672
  publish: bool = True,
673
+ nodes: list[JSONObject] | None = None,
674
+ transitions: list[JSONObject] | None = None,
483
675
  spec: JSONObject | None = None,
484
676
  idempotency_key: str = "",
485
677
  schema_version: str = "",
@@ -488,8 +680,11 @@ class AiBuilderTools(ToolBase):
488
680
  return self.app_flow_apply(
489
681
  profile=profile,
490
682
  app_key=app_key,
683
+ mode=mode,
491
684
  publish=publish,
492
- spec=spec or {},
685
+ nodes=nodes or [],
686
+ transitions=transitions or [],
687
+ spec=spec,
493
688
  idempotency_key=idempotency_key or None,
494
689
  schema_version=schema_version or None,
495
690
  patch_nodes=patch_nodes,
@@ -595,6 +790,13 @@ class AiBuilderTools(ToolBase):
595
790
  patch_sections=patch_sections,
596
791
  )
597
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
+
598
800
  @mcp.tool()
599
801
  def app_publish_verify(
600
802
  profile: str = DEFAULT_PROFILE,
@@ -602,17 +804,10 @@ class AiBuilderTools(ToolBase):
602
804
  app_keys: list[str] | None = None,
603
805
  expected_package_id: int | None = None,
604
806
  ) -> JSONObject:
605
- if app_keys:
606
- return self._facade._batch_read_app_keys(
607
- profile=profile,
608
- app_keys=app_keys,
609
- single_reader=lambda profile, app_key: self.app_publish_verify(profile=profile, app_key=app_key, expected_package_id=expected_package_id),
610
- data_key="verification",
611
- tool_name="app_publish_verify",
612
- )
613
807
  return self.app_publish_verify(
614
808
  profile=profile,
615
809
  app_key=app_key,
810
+ app_keys=app_keys,
616
811
  expected_package_id=expected_package_id,
617
812
  )
618
813
 
@@ -661,15 +856,16 @@ class AiBuilderTools(ToolBase):
661
856
  "allowed_values": {"tool_name": public_tool_names},
662
857
  "details": {"reason_path": "tool_name"},
663
858
  "suggested_next_call": None,
664
- "request_id": None,
665
- "backend_code": None,
666
- "http_status": None,
667
- "noop": False,
668
- "warnings": [],
669
- "verification": {},
670
- "verified": False,
671
- }
859
+ "request_id": None,
860
+ "backend_code": None,
861
+ "http_status": None,
862
+ "noop": False,
863
+ "warnings": [],
864
+ "verification": {},
865
+ "verified": False,
866
+ }
672
867
  contract = _builder_contract_with_apply_output(lookup_name, contract)
868
+ contract_summary = _builder_tool_contract_summary(lookup_name, contract)
673
869
  return {
674
870
  "status": "success",
675
871
  "error_code": None,
@@ -688,6 +884,8 @@ class AiBuilderTools(ToolBase):
688
884
  "verification": {},
689
885
  "verified": True,
690
886
  "tool_name": requested,
887
+ "summary": contract_summary,
888
+ "json_paths": contract_summary["json_paths"],
691
889
  "contract": contract,
692
890
  }
693
891
 
@@ -710,6 +908,12 @@ class AiBuilderTools(ToolBase):
710
908
  "color_count": len(catalog["icon_colors"]),
711
909
  "warnings": [],
712
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
+ },
713
917
  }
714
918
 
715
919
  @tool_cn_name("分组创建")
@@ -776,13 +980,23 @@ class AiBuilderTools(ToolBase):
776
980
  package_id: int | None = None,
777
981
  package_name: str | None = None,
778
982
  create_if_missing: bool = False,
779
- icon: str | None = None,
983
+ icon: str | JSONObject | None = None,
780
984
  color: str | None = None,
985
+ icon_name: str | None = None,
986
+ icon_color: str | None = None,
987
+ icon_config: JSONObject | None = None,
781
988
  visibility: JSONObject | None = None,
782
989
  items: list[dict] | None = None,
783
990
  allow_detach: bool = False,
784
991
  ) -> JSONObject:
785
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
+ )
786
1000
  visibility_patch = None
787
1001
  if visibility is not None:
788
1002
  try:
@@ -905,16 +1119,23 @@ class AiBuilderTools(ToolBase):
905
1119
  contain_disable: bool = False,
906
1120
  ) -> JSONObject:
907
1121
  """执行工具方法逻辑。"""
1122
+ normalized_query = str(query or "").strip()
908
1123
  normalized_args = {
909
- "query": query,
1124
+ "query": normalized_query,
910
1125
  "page_num": page_num,
911
1126
  "page_size": page_size,
912
1127
  "contain_disable": contain_disable,
913
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
+ )
914
1135
  return _safe_tool_call(
915
1136
  lambda: self._facade.member_search(
916
1137
  profile=profile,
917
- query=query,
1138
+ query=normalized_query,
918
1139
  page_num=page_num,
919
1140
  page_size=page_size,
920
1141
  contain_disable=contain_disable,
@@ -927,9 +1148,16 @@ class AiBuilderTools(ToolBase):
927
1148
  @tool_cn_name("角色检索")
928
1149
  def role_search(self, *, profile: str, keyword: str, page_num: int = 1, page_size: int = 20) -> JSONObject:
929
1150
  """执行角色相关逻辑。"""
930
- 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
+ )
931
1159
  return _safe_tool_call(
932
- 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),
933
1161
  error_code="ROLE_SEARCH_FAILED",
934
1162
  normalized_args=normalized_args,
935
1163
  suggested_next_call={"tool_name": "role_search", "arguments": {"profile": profile, **normalized_args}},
@@ -1058,19 +1286,20 @@ class AiBuilderTools(ToolBase):
1058
1286
  apps: list[JSONObject] | None = None,
1059
1287
  ) -> JSONObject:
1060
1288
  """执行应用按钮 apply 逻辑。"""
1061
- if apps:
1062
- 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",
1063
1292
  profile=profile,
1064
1293
  apps=apps,
1065
- 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(
1066
1295
  profile=profile,
1067
- app_key=app_key,
1068
- upsert_buttons=kw.get("upsert_buttons", []),
1069
- patch_buttons=kw.get("patch_buttons", []),
1070
- remove_buttons=kw.get("remove_buttons", []),
1071
- 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,
1072
1302
  ),
1073
- tool_name="app_custom_buttons_apply",
1074
1303
  )
1075
1304
  raw_request = {
1076
1305
  "app_key": app_key,
@@ -1127,20 +1356,31 @@ class AiBuilderTools(ToolBase):
1127
1356
  apps: list[JSONObject] | None = None,
1128
1357
  ) -> JSONObject:
1129
1358
  """执行应用关联资源 apply 逻辑。"""
1130
- if apps:
1131
- 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",
1132
1362
  profile=profile,
1133
1363
  apps=apps,
1134
- 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(
1135
1365
  profile=profile,
1136
- app_key=app_key,
1137
- upsert_resources=kw.get("upsert_resources", []),
1138
- patch_resources=kw.get("patch_resources", []),
1139
- remove_associated_item_ids=kw.get("remove_associated_item_ids", []),
1140
- reorder_associated_item_ids=kw.get("reorder_associated_item_ids", []),
1141
- 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,
1142
1383
  ),
1143
- tool_name="app_associated_resources_apply",
1144
1384
  )
1145
1385
  raw_request = {
1146
1386
  "app_key": app_key,
@@ -1304,11 +1544,14 @@ class AiBuilderTools(ToolBase):
1304
1544
  ))
1305
1545
 
1306
1546
  @tool_cn_name("应用详情查询")
1307
- def app_get(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
1547
+ def app_get(self, *, profile: str, app_key: str, app_keys: list[str] | None = None) -> JSONObject:
1308
1548
  """执行应用相关逻辑。"""
1309
- if app_keys:
1310
- return _publicize_package_fields(
1311
- self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get, data_key="summary", tool_name="app_get")
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),
1312
1555
  )
1313
1556
  normalized_args = {"app_key": app_key}
1314
1557
  return _publicize_package_fields(_safe_tool_call(
@@ -1330,10 +1573,15 @@ class AiBuilderTools(ToolBase):
1330
1573
  )
1331
1574
 
1332
1575
  @tool_cn_name("应用字段详情查询")
1333
- 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:
1334
1577
  """执行应用相关逻辑。"""
1335
- if app_keys:
1336
- 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
+ )
1337
1585
  normalized_args = {"app_key": app_key}
1338
1586
  return _safe_tool_call(
1339
1587
  lambda: self._facade.app_get_fields(profile=profile, app_key=app_key),
@@ -1372,10 +1620,15 @@ class AiBuilderTools(ToolBase):
1372
1620
  )
1373
1621
 
1374
1622
  @tool_cn_name("应用布局详情查询")
1375
- 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:
1376
1624
  """执行应用相关逻辑。"""
1377
- if app_keys:
1378
- 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
+ )
1379
1632
  normalized_args = {"app_key": app_key}
1380
1633
  return _safe_tool_call(
1381
1634
  lambda: self._facade.app_get_layout(profile=profile, app_key=app_key),
@@ -1396,10 +1649,15 @@ class AiBuilderTools(ToolBase):
1396
1649
  )
1397
1650
 
1398
1651
  @tool_cn_name("应用视图详情查询")
1399
- 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:
1400
1653
  """执行应用相关逻辑。"""
1401
- if app_keys:
1402
- 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
+ )
1403
1661
  normalized_args = {"app_key": app_key}
1404
1662
  return _safe_tool_call(
1405
1663
  lambda: self._facade.app_get_views(profile=profile, app_key=app_key),
@@ -1408,27 +1666,53 @@ class AiBuilderTools(ToolBase):
1408
1666
  suggested_next_call={"tool_name": "app_get_views", "arguments": {"profile": profile, "app_key": app_key}},
1409
1667
  )
1410
1668
 
1411
- @tool_cn_name("Workflow Spec Schema")
1412
- def app_flow_get_schema(self, *, profile: str, schema_version: str | None = None) -> JSONObject:
1413
- 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}
1414
1673
  return _safe_tool_call(
1415
- lambda: self._facade.flow_get_schema(profile=profile, schema_version=schema_version),
1416
- 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",
1417
1676
  normalized_args=normalized_args,
1418
- 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}},
1419
1678
  )
1420
1679
 
1421
- @tool_cn_name("Workflow Spec 读取")
1422
- 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:
1423
1682
  """执行应用相关逻辑。"""
1424
- if app_keys:
1425
- 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:
1426
1700
  normalized_args = {"app_key": app_key, "version_id": version_id}
1427
1701
  return _safe_tool_call(
1428
1702
  lambda: self._facade.flow_get(profile=profile, app_key=app_key, version_id=version_id),
1429
- error_code="APP_GET_FLOW_FAILED",
1703
+ error_code="APP_FLOW_GET_FAILED",
1430
1704
  normalized_args=normalized_args,
1431
- 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}},
1432
1716
  )
1433
1717
 
1434
1718
  @tool_cn_name("应用图表摘要读取")
@@ -1443,10 +1727,15 @@ class AiBuilderTools(ToolBase):
1443
1727
  )
1444
1728
 
1445
1729
  @tool_cn_name("应用图表详情查询")
1446
- 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:
1447
1731
  """执行应用相关逻辑。"""
1448
- if app_keys:
1449
- 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
+ )
1450
1739
  normalized_args = {"app_key": app_key}
1451
1740
  return _safe_tool_call(
1452
1741
  lambda: self._facade.app_get_charts(profile=profile, app_key=app_key),
@@ -1457,9 +1746,13 @@ class AiBuilderTools(ToolBase):
1457
1746
 
1458
1747
  @tool_cn_name("自定义按钮读取")
1459
1748
  def app_get_buttons(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
1460
- """执行按钮相关逻辑。"""
1461
- if app_keys:
1462
- 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
+ )
1463
1756
  normalized_args = {"app_key": app_key}
1464
1757
  return _safe_tool_call(
1465
1758
  lambda: self._facade.app_get_buttons(profile=profile, app_key=app_key),
@@ -1470,9 +1763,13 @@ class AiBuilderTools(ToolBase):
1470
1763
 
1471
1764
  @tool_cn_name("关联资源读取")
1472
1765
  def app_get_associated_resources(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
1473
- """执行关联资源相关逻辑。"""
1474
- if app_keys:
1475
- 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
+ )
1476
1773
  normalized_args = {"app_key": app_key}
1477
1774
  return _safe_tool_call(
1478
1775
  lambda: self._facade.app_get_associated_resources(profile=profile, app_key=app_key),
@@ -1558,6 +1855,22 @@ class AiBuilderTools(ToolBase):
1558
1855
  remove_fields: list[JSONObject],
1559
1856
  ) -> JSONObject:
1560
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
1561
1874
  try:
1562
1875
  request = SchemaPlanRequest.model_validate(
1563
1876
  {
@@ -1651,6 +1964,52 @@ class AiBuilderTools(ToolBase):
1651
1964
  suggested_next_call={"tool_name": "app_layout_plan", "arguments": {"profile": profile, **normalized_request}},
1652
1965
  )
1653
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
+
1654
2013
  @tool_cn_name("应用视图规划")
1655
2014
  def app_views_plan(
1656
2015
  self,
@@ -1662,6 +2021,15 @@ class AiBuilderTools(ToolBase):
1662
2021
  preset: str | None = None,
1663
2022
  ) -> JSONObject:
1664
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
1665
2033
  try:
1666
2034
  request = ViewsPlanRequest.model_validate(
1667
2035
  {
@@ -1703,8 +2071,11 @@ class AiBuilderTools(ToolBase):
1703
2071
  package_id: int | None = None,
1704
2072
  app_name: str = "",
1705
2073
  app_title: str = "",
1706
- icon: str = "",
2074
+ icon: str | JSONObject = "",
1707
2075
  color: str = "",
2076
+ icon_name: str | None = None,
2077
+ icon_color: str | None = None,
2078
+ icon_config: JSONObject | None = None,
1708
2079
  visibility: JSONObject | None = None,
1709
2080
  create_if_missing: bool = False,
1710
2081
  publish: bool = True,
@@ -1714,7 +2085,25 @@ class AiBuilderTools(ToolBase):
1714
2085
  apps: list[JSONObject] | None = None,
1715
2086
  ) -> JSONObject:
1716
2087
  """执行应用相关逻辑。"""
1717
- 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 [])
1718
2107
  result = self._app_schema_apply_multi(
1719
2108
  profile=profile,
1720
2109
  package_id=package_id,
@@ -1723,6 +2112,10 @@ class AiBuilderTools(ToolBase):
1723
2112
  publish=publish,
1724
2113
  apps=apps,
1725
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
1726
2119
  return _attach_builder_apply_envelope("app_schema_apply", result)
1727
2120
  result = self._app_schema_apply_once(
1728
2121
  profile=profile,
@@ -1778,6 +2171,17 @@ class AiBuilderTools(ToolBase):
1778
2171
  }
1779
2172
  if visibility is not None:
1780
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
1781
2185
  if package_id is None:
1782
2186
  return _config_failure(
1783
2187
  tool_name="app_schema_apply",
@@ -1787,9 +2191,20 @@ class AiBuilderTools(ToolBase):
1787
2191
  if not apps:
1788
2192
  return _config_failure(
1789
2193
  tool_name="app_schema_apply",
2194
+ error_code="APPS_FILE_EMPTY",
1790
2195
  message="app_schema_apply multi-app mode requires non-empty apps.",
1791
2196
  fix_hint="Pass apps as a non-empty list of app schema items.",
1792
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
1793
2208
  icon_errors: list[JSONObject] = []
1794
2209
  seen_new_app_icons: dict[str, int] = {}
1795
2210
  for index, raw_item in enumerate(apps):
@@ -1854,9 +2269,13 @@ class AiBuilderTools(ToolBase):
1854
2269
  )
1855
2270
 
1856
2271
  client_key_to_app_key: dict[str, str] = {}
2272
+ app_name_to_app_key: dict[str, str] = {}
1857
2273
  created_app_keys: list[str] = []
1858
2274
  results: list[JSONObject] = []
1859
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] = []
1860
2279
  client_keys: set[str] = set()
1861
2280
 
1862
2281
  for index, raw_item in enumerate(apps):
@@ -1895,31 +2314,58 @@ class AiBuilderTools(ToolBase):
1895
2314
  )
1896
2315
  public_shell = _publicize_package_fields(shell)
1897
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
1898
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)
1899
2335
  results.append({
1900
2336
  "index": index,
1901
2337
  "row_number": index + 1,
1902
2338
  "client_key": client_key or None,
1903
2339
  "app_name": app_name or None,
1904
2340
  "app_key": resolved_key or app_key or None,
1905
- "status": "failed",
2341
+ "status": item_status,
1906
2342
  "stage": "resolve_or_create_shell",
1907
2343
  "error_code": public_shell.get("error_code") or "APP_SHELL_APPLY_FAILED",
1908
2344
  "message": public_shell.get("message") or "app shell resolve/create failed",
1909
- "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 {}),
1910
2351
  })
1911
2352
  continue
1912
2353
  if bool(public_shell.get("created")):
1913
2354
  created_app_keys.append(resolved_key)
1914
- if _schema_apply_result_has_write(public_shell):
1915
- 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)
1916
2359
  if client_key:
1917
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
1918
2364
  results.append({
1919
2365
  "index": index,
1920
2366
  "row_number": index + 1,
1921
2367
  "client_key": client_key or None,
1922
- "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,
1923
2369
  "app_key": resolved_key,
1924
2370
  "status": "shell_ready",
1925
2371
  "created": bool(public_shell.get("created")),
@@ -1939,7 +2385,7 @@ class AiBuilderTools(ToolBase):
1939
2385
  item = deepcopy(raw_item)
1940
2386
  app_key = str(existing.get("app_key") or "").strip()
1941
2387
  try:
1942
- 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)
1943
2389
  except ValueError as error:
1944
2390
  final_items.append({
1945
2391
  **{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
@@ -1976,7 +2422,11 @@ class AiBuilderTools(ToolBase):
1976
2422
  "verified": bool(shell_result.get("verified")),
1977
2423
  "error_code": shell_result.get("error_code"),
1978
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),
1979
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 {}),
1980
2430
  })
1981
2431
  continue
1982
2432
 
@@ -1998,11 +2448,18 @@ class AiBuilderTools(ToolBase):
1998
2448
  public_result = _publicize_package_fields(field_result)
1999
2449
  if _schema_apply_result_has_write(public_result):
2000
2450
  any_write_executed = True
2451
+ if _schema_apply_result_may_have_write(public_result):
2452
+ any_write_may_have_succeeded = True
2001
2453
  item_status = public_result.get("status") if public_result.get("status") in {"success", "partial_success"} else "failed"
2002
2454
  shell_field_diff = existing.get("shell_field_diff") if isinstance(existing.get("shell_field_diff"), dict) else {}
2003
2455
  shell_field_diff_details = existing.get("shell_field_diff_details") if isinstance(existing.get("shell_field_diff_details"), dict) else {}
2004
2456
  field_diff = _merge_schema_field_diffs(shell_field_diff, public_result.get("field_diff") or {})
2005
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")))
2006
2463
  final_items.append({
2007
2464
  **{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
2008
2465
  "status": item_status,
@@ -2015,55 +2472,33 @@ class AiBuilderTools(ToolBase):
2015
2472
  "verified": bool(public_result.get("verified")),
2016
2473
  "error_code": public_result.get("error_code"),
2017
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),
2018
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 {}),
2019
2480
  })
2020
2481
 
2021
- # Inline layout: apply layout for each successful app that included a layout field.
2022
- for index, raw_item in enumerate(apps):
2023
- if not isinstance(raw_item, dict):
2024
- continue
2025
- layout_spec = raw_item.get("layout")
2026
- if not isinstance(layout_spec, dict) or not layout_spec:
2027
- continue
2028
- final_item = next((it for it in final_items if it.get("index") == index), None)
2029
- if not final_item or final_item.get("status") not in {"success", "partial_success"}:
2030
- continue
2031
- resolved_app_key = str(final_item.get("app_key") or "").strip()
2032
- if not resolved_app_key:
2033
- continue
2034
- layout_mode = str(layout_spec.get("mode") or "merge").strip() or "merge"
2035
- layout_sections = layout_spec.get("sections") or []
2036
- try:
2037
- layout_result = self.app_layout_apply(
2038
- profile=profile,
2039
- app_key=resolved_app_key,
2040
- mode=layout_mode,
2041
- publish=publish,
2042
- sections=list(layout_sections),
2043
- )
2044
- layout_ok = layout_result.get("status") in {"success", "partial_success"}
2045
- except Exception as layout_error:
2046
- layout_ok = False
2047
- layout_result = {"status": "failed", "message": str(layout_error)}
2048
- if layout_ok:
2049
- final_item["layout_applied"] = True
2050
- final_item["layout_status"] = layout_result.get("status")
2051
- else:
2052
- final_item["layout_warning"] = layout_result.get("error_code") or "LAYOUT_APPLY_FAILED"
2053
- final_item["layout_message"] = layout_result.get("message")
2054
-
2482
+ pending_readback = sum(1 for item in final_items if item.get("status") == "pending_readback")
2055
2483
  succeeded = sum(1 for item in final_items if item.get("status") in {"success", "partial_success"})
2056
- failed = len(final_items) - succeeded
2057
- overall_status = "success" if failed == 0 else ("partial_success" if succeeded > 0 or any_write_executed else "failed")
2058
- 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 = {
2059
2493
  "status": overall_status,
2060
2494
  "mode": "multi_app",
2061
2495
  "total": len(apps),
2062
2496
  "succeeded": succeeded,
2063
2497
  "failed": failed,
2498
+ "pending_readback": pending_readback,
2064
2499
  "created_app_keys": created_app_keys,
2065
2500
  "write_executed": any_write_executed,
2066
- "safe_to_retry": not any_write_executed,
2501
+ "safe_to_retry": not (any_write_executed or uncertain_write),
2067
2502
  "package_id": package_id,
2068
2503
  "publish_requested": publish,
2069
2504
  "apps": final_items,
@@ -2071,12 +2506,23 @@ class AiBuilderTools(ToolBase):
2071
2506
  "verification": {
2072
2507
  "all_apps_succeeded": failed == 0,
2073
2508
  "created_app_count": len(created_app_keys),
2509
+ "pending_readback_count": pending_readback,
2074
2510
  },
2075
2511
  "request_id": None,
2076
2512
  "error_code": None if overall_status != "failed" else "MULTI_APP_SCHEMA_APPLY_FAILED",
2077
2513
  "recoverable": overall_status != "success",
2078
- "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
+ ),
2079
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
2080
2526
 
2081
2527
  def _app_schema_apply_once(
2082
2528
  self,
@@ -2097,6 +2543,23 @@ class AiBuilderTools(ToolBase):
2097
2543
  ) -> JSONObject:
2098
2544
  """执行内部辅助逻辑。"""
2099
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
2100
2563
  icon_failure = _validate_workspace_icon_for_builder(
2101
2564
  tool_name="app_schema_apply",
2102
2565
  icon=icon,
@@ -2191,20 +2654,30 @@ class AiBuilderTools(ToolBase):
2191
2654
  return _publicize_package_fields(result)
2192
2655
 
2193
2656
  @tool_cn_name("应用布局应用")
2194
- 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:
2195
2667
  """执行应用相关逻辑。"""
2196
- if apps:
2197
- return self._facade._batch_write_apps(
2668
+ if apps is not None:
2669
+ return self._apply_app_batch(
2670
+ tool_name="app_layout_apply",
2198
2671
  profile=profile,
2199
2672
  apps=apps,
2200
- single_writer=lambda profile, app_key, **kw: self.app_layout_apply(
2673
+ apply_one=lambda _index, item, item_app_key: self.app_layout_apply(
2201
2674
  profile=profile,
2202
- app_key=app_key,
2203
- mode=kw.get("mode", mode),
2204
- publish=kw.get("publish", publish),
2205
- 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,
2206
2680
  ),
2207
- tool_name="app_layout_apply",
2208
2681
  )
2209
2682
  result = self._app_layout_apply_once(
2210
2683
  profile=profile,
@@ -2290,15 +2763,25 @@ class AiBuilderTools(ToolBase):
2290
2763
  *,
2291
2764
  profile: str,
2292
2765
  app_key: str,
2293
- spec: JSONObject,
2766
+ mode: str = "replace",
2294
2767
  publish: bool = True,
2768
+ nodes: list[JSONObject] | None = None,
2769
+ transitions: list[JSONObject] | None = None,
2770
+ spec: JSONObject | None = None,
2295
2771
  idempotency_key: str | None = None,
2296
2772
  schema_version: str | None = None,
2297
2773
  patch_nodes: list[JSONObject] | None = None,
2298
2774
  ) -> JSONObject:
2299
2775
  """执行应用相关逻辑。"""
2300
- if patch_nodes:
2301
- 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(
2302
2785
  lambda: self._facade.flow_patch_nodes(
2303
2786
  profile=profile,
2304
2787
  app_key=app_key,
@@ -2307,50 +2790,132 @@ class AiBuilderTools(ToolBase):
2307
2790
  idempotency_key=idempotency_key,
2308
2791
  schema_version=schema_version,
2309
2792
  ),
2310
- error_code="FLOW_APPLY_FAILED",
2311
- normalized_args={"app_key": app_key, "patch_nodes": patch_nodes, "publish": publish},
2312
- 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
+ }
2313
2881
  )
2314
- return _attach_builder_apply_envelope("app_flow_apply", result)
2315
- if not isinstance(spec, dict) or not spec:
2316
- return _config_failure(
2882
+ except ValidationError as exc:
2883
+ return _validation_failure(
2884
+ str(exc),
2317
2885
  tool_name="app_flow_apply",
2318
- message="app_flow_apply requires a non-empty WorkflowSpecDTO `spec` object.",
2319
- 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
+ },
2320
2898
  )
2321
2899
  normalized_args = {
2322
- "app_key": app_key,
2900
+ "app_key": request.app_key,
2901
+ "mode": request.mode,
2323
2902
  "publish": publish,
2324
- "spec": spec,
2325
- "idempotency_key": idempotency_key,
2326
- "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],
2327
2905
  }
2328
- result = _safe_tool_call(
2329
- lambda: self._facade.flow_apply(
2906
+ return _safe_tool_call(
2907
+ lambda: self._facade.app_flow_apply(
2330
2908
  profile=profile,
2331
- app_key=app_key,
2332
- spec=spec,
2909
+ app_key=request.app_key,
2910
+ mode=request.mode,
2333
2911
  publish=publish,
2334
- idempotency_key=idempotency_key,
2335
- 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],
2336
2914
  ),
2337
2915
  error_code="FLOW_APPLY_FAILED",
2338
2916
  normalized_args=normalized_args,
2339
2917
  suggested_next_call={"tool_name": "app_flow_apply", "arguments": {"profile": profile, **normalized_args}},
2340
2918
  )
2341
- result = self._retry_after_self_lock_release(
2342
- profile=profile,
2343
- result=result,
2344
- retry_call=lambda: self._facade.flow_apply(
2345
- profile=profile,
2346
- app_key=app_key,
2347
- spec=spec,
2348
- publish=publish,
2349
- idempotency_key=idempotency_key,
2350
- schema_version=schema_version,
2351
- ),
2352
- )
2353
- return _attach_builder_apply_envelope("app_flow_apply", result)
2354
2919
 
2355
2920
  @tool_cn_name("应用视图应用")
2356
2921
  def app_views_apply(
@@ -2365,19 +2930,20 @@ class AiBuilderTools(ToolBase):
2365
2930
  apps: list[JSONObject] | None = None,
2366
2931
  ) -> JSONObject:
2367
2932
  """执行应用相关逻辑。"""
2368
- if apps:
2369
- return self._facade._batch_write_apps(
2933
+ if apps is not None:
2934
+ return self._apply_app_batch(
2935
+ tool_name="app_views_apply",
2370
2936
  profile=profile,
2371
2937
  apps=apps,
2372
- single_writer=lambda profile, app_key, **kw: self.app_views_apply(
2938
+ apply_one=lambda _index, item, item_app_key: self.app_views_apply(
2373
2939
  profile=profile,
2374
- app_key=app_key,
2375
- publish=kw.get("publish", publish),
2376
- upsert_views=kw.get("upsert_views", []),
2377
- patch_views=kw.get("patch_views", []),
2378
- 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,
2379
2946
  ),
2380
- tool_name="app_views_apply",
2381
2947
  )
2382
2948
  result = self._app_views_apply_once(
2383
2949
  profile=profile,
@@ -2412,6 +2978,17 @@ class AiBuilderTools(ToolBase):
2412
2978
  remove_views: list[str],
2413
2979
  ) -> JSONObject:
2414
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
2415
2992
  if patch_views:
2416
2993
  try:
2417
2994
  parsed_views = [ViewUpsertPatch.model_validate(item) for item in (upsert_views or [])]
@@ -2486,7 +3063,7 @@ class AiBuilderTools(ToolBase):
2486
3063
  "profile": profile,
2487
3064
  "app_key": str(plan_args.get("app_key") or app_key),
2488
3065
  "publish": publish,
2489
- "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"]}],
2490
3067
  "remove_views": plan_args.get("remove_views") or [],
2491
3068
  },
2492
3069
  },
@@ -2545,19 +3122,20 @@ class AiBuilderTools(ToolBase):
2545
3122
  apps: list[JSONObject] | None = None,
2546
3123
  ) -> JSONObject:
2547
3124
  """执行应用相关逻辑。"""
2548
- if apps:
2549
- return self._facade._batch_write_apps(
3125
+ if apps is not None:
3126
+ return self._apply_app_batch(
3127
+ tool_name="app_charts_apply",
2550
3128
  profile=profile,
2551
3129
  apps=apps,
2552
- single_writer=lambda profile, app_key, **kw: self.app_charts_apply(
3130
+ apply_one=lambda _index, item, item_app_key: self.app_charts_apply(
2553
3131
  profile=profile,
2554
- app_key=app_key,
2555
- upsert_charts=kw.get("upsert_charts", []),
2556
- patch_charts=kw.get("patch_charts", []),
2557
- remove_chart_ids=kw.get("remove_chart_ids", []),
2558
- 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,
2559
3138
  ),
2560
- tool_name="app_charts_apply",
2561
3139
  )
2562
3140
  try:
2563
3141
  request = ChartApplyRequest.model_validate(
@@ -2577,12 +3155,12 @@ class AiBuilderTools(ToolBase):
2577
3155
  suggested_next_call={
2578
3156
  "tool_name": "app_charts_apply",
2579
3157
  "arguments": {
2580
- "profile": profile,
2581
- "app_key": app_key,
2582
- "upsert_charts": [{"name": "销售总量", "chart_type": "target", "indicator_field_ids": []}],
2583
- "remove_chart_ids": [],
2584
- "reorder_chart_ids": [],
2585
- },
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
+ },
2586
3164
  },
2587
3165
  ))
2588
3166
  normalized_args = request.model_dump(mode="json")
@@ -2617,25 +3195,23 @@ class AiBuilderTools(ToolBase):
2617
3195
  patch_sections: list[JSONObject] | None = None,
2618
3196
  ) -> JSONObject:
2619
3197
  """执行门户相关逻辑。"""
2620
- if patch_sections:
2621
- if not dash_key:
2622
- return _attach_builder_apply_envelope("portal_apply", _config_failure(
2623
- tool_name="portal_apply",
2624
- message="patch_sections requires dash_key to identify the portal",
2625
- fix_hint="Provide dash_key from portal_list or portal_get",
2626
- ))
2627
- 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(
2628
3205
  lambda: self._facade.portal_patch_sections(
2629
3206
  profile=profile,
2630
3207
  dash_key=dash_key,
2631
3208
  patch_sections=patch_sections,
2632
3209
  publish=publish,
2633
3210
  ),
2634
- error_code="PORTAL_APPLY_FAILED",
2635
- normalized_args={"dash_key": dash_key, "patch_sections": patch_sections, "publish": publish},
2636
- suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, "dash_key": dash_key, "patch_sections": patch_sections}},
2637
- )
2638
- 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
+ )))
2639
3215
  request_payload: dict[str, Any] = dict(payload) if isinstance(payload, dict) else {}
2640
3216
  if dash_key:
2641
3217
  request_payload["dash_key"] = dash_key
@@ -2712,23 +3288,39 @@ class AiBuilderTools(ToolBase):
2712
3288
  ))
2713
3289
  return _attach_builder_apply_envelope("portal_apply", result)
2714
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
+
2715
3303
  @tool_cn_name("应用发布校验")
2716
3304
  def app_publish_verify(
2717
3305
  self,
2718
3306
  *,
2719
3307
  profile: str,
2720
- app_key: str = "",
3308
+ app_key: str,
2721
3309
  app_keys: list[str] | None = None,
2722
3310
  expected_package_id: int | None = None,
2723
3311
  ) -> JSONObject:
2724
3312
  """执行应用相关逻辑。"""
2725
- if app_keys:
2726
- return self._facade._batch_read_app_keys(
2727
- profile=profile,
2728
- app_keys=app_keys,
2729
- single_reader=lambda profile, app_key: self._facade.app_publish_verify(profile=profile, app_key=app_key, expected_package_tag_id=expected_package_id),
2730
- data_key="verification",
3313
+ if app_keys is not None:
3314
+ return self._apply_app_batch(
2731
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
+ ),
2732
3324
  )
2733
3325
  normalized_args = {"app_key": app_key, "expected_package_id": expected_package_id}
2734
3326
  result = _publicize_package_fields(_safe_tool_call(
@@ -2862,33 +3454,442 @@ class AiBuilderTools(ToolBase):
2862
3454
  "tool_name": apply_tool_name,
2863
3455
  "arguments": {"profile": profile, **normalized_args, "publish": publish},
2864
3456
  }
2865
- 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
+ )
2866
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
+ )
2867
3822
 
2868
- def _multi_app_item_failure(index: int, item: object, error_code: str, message: str) -> JSONObject:
2869
- app_name = None
2870
- client_key = None
2871
- app_key = None
2872
- if isinstance(item, dict):
2873
- app_name = str(item.get("app_name") or item.get("appTitle") or item.get("app_title") or item.get("title") or "").strip() or None
2874
- client_key = str(item.get("client_key") or item.get("clientKey") or "").strip() or None
2875
- app_key = str(item.get("app_key") or item.get("appKey") or "").strip() or None
2876
- return {
2877
- "index": index,
2878
- "row_number": index + 1,
2879
- "client_key": client_key,
2880
- "app_name": app_name,
2881
- "app_key": app_key,
2882
- "status": "failed",
2883
- "stage": "validate_item",
2884
- "error_code": error_code,
2885
- "message": message,
2886
- "safe_to_retry": True,
2887
- }
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
+ )
2888
3884
 
2889
3885
 
2890
- 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:
2891
3891
  compiled = deepcopy(item)
3892
+ app_name_to_app_key = app_name_to_app_key or {}
2892
3893
 
2893
3894
  def visit(value):
2894
3895
  if isinstance(value, list):
@@ -2908,6 +3909,13 @@ def _compile_multi_app_schema_item_refs(item: JSONObject, client_key_to_app_key:
2908
3909
  if not target_app_key:
2909
3910
  raise ValueError(f"target_app_ref '{ref_key}' did not match any apps[].client_key")
2910
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
2911
3919
  return payload
2912
3920
 
2913
3921
  return visit(compiled)
@@ -2956,7 +3964,7 @@ def _contains_multi_app_target_ref(value: object) -> bool:
2956
3964
  if not isinstance(value, dict):
2957
3965
  return False
2958
3966
  for key, entry in value.items():
2959
- 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"}:
2960
3968
  return True
2961
3969
  if _contains_multi_app_target_ref(entry):
2962
3970
  return True
@@ -2979,6 +3987,8 @@ def _merge_schema_field_diffs(*diffs: object) -> JSONObject:
2979
3987
 
2980
3988
 
2981
3989
  def _schema_apply_result_has_write(result: JSONObject) -> bool:
3990
+ if "write_executed" in result:
3991
+ return bool(result.get("write_executed"))
2982
3992
  if bool(result.get("created")) or bool(result.get("published")) or bool(result.get("app_base_updated")):
2983
3993
  return True
2984
3994
  field_diff = result.get("field_diff")
@@ -2987,6 +3997,15 @@ def _schema_apply_result_has_write(result: JSONObject) -> bool:
2987
3997
  return False
2988
3998
 
2989
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
+
2990
4009
  def _validation_failure(
2991
4010
  detail: str,
2992
4011
  *,
@@ -3067,6 +4086,197 @@ def _visibility_validation_failure(
3067
4086
  return result
3068
4087
 
3069
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
+
3070
4280
  def _config_failure(
3071
4281
  *,
3072
4282
  tool_name: str,
@@ -3075,6 +4285,7 @@ def _config_failure(
3075
4285
  error_code: str = "CONFIG_ERROR",
3076
4286
  details: JSONObject | None = None,
3077
4287
  allowed_values: JSONObject | None = None,
4288
+ suggested_next_call: JSONObject | None = None,
3078
4289
  ) -> JSONObject:
3079
4290
  contract = _BUILDER_TOOL_CONTRACTS.get(tool_name or "")
3080
4291
  public_allowed_values = deepcopy(contract.get("allowed_values", {})) if isinstance(contract, dict) else {}
@@ -3095,7 +4306,7 @@ def _config_failure(
3095
4306
  "missing_fields": [],
3096
4307
  "allowed_values": public_allowed_values,
3097
4308
  "details": public_details,
3098
- "suggested_next_call": None,
4309
+ "suggested_next_call": suggested_next_call,
3099
4310
  "request_id": None,
3100
4311
  "backend_code": None,
3101
4312
  "http_status": None,
@@ -3223,11 +4434,58 @@ def _builder_contract_with_apply_output(tool_name: str, contract: JSONObject) ->
3223
4434
  "schema_version": BUILDER_APPLY_SCHEMA_VERSION,
3224
4435
  "preferred_ui_fields": ["operation", "summary", "resources"],
3225
4436
  "resource_fields": ["resource_type", "operation", "status", "id", "key", "name", "ids", "parent", "icon_config", "error_code", "message"],
4437
+ "json_paths": _builder_apply_json_paths(),
3226
4438
  "legacy_fields_preserved": True,
3227
4439
  }
3228
4440
  return public
3229
4441
 
3230
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
+
3231
4489
  def _attach_builder_apply_envelope(tool_name: str, payload: JSONObject) -> JSONObject:
3232
4490
  if not isinstance(payload, dict):
3233
4491
  return payload
@@ -3236,6 +4494,7 @@ def _attach_builder_apply_envelope(tool_name: str, payload: JSONObject) -> JSONO
3236
4494
  payload["operation"] = tool_name
3237
4495
  payload["resources"] = resources
3238
4496
  payload["summary"] = _builder_apply_summary(payload, resources)
4497
+ payload["json_paths"] = _builder_apply_json_paths()
3239
4498
  return payload
3240
4499
 
3241
4500
 
@@ -3269,8 +4528,14 @@ def _builder_apply_summary(payload: JSONObject, resources: list[JSONObject]) ->
3269
4528
  }
3270
4529
  if "write_executed" in payload:
3271
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"))
3272
4533
  if "safe_to_retry" in payload:
3273
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")
3274
4539
  return summary
3275
4540
 
3276
4541
 
@@ -3291,6 +4556,8 @@ def _builder_apply_resources(tool_name: str, payload: JSONObject) -> list[JSONOb
3291
4556
  resources = _builder_package_resources(payload)
3292
4557
  elif tool_name == "app_schema_apply":
3293
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)
3294
4561
  elif tool_name == "app_layout_apply":
3295
4562
  resources = [_builder_app_resource(payload, operation="layout_updated")]
3296
4563
  elif tool_name == "app_flow_apply":
@@ -3301,6 +4568,8 @@ def _builder_apply_resources(tool_name: str, payload: JSONObject) -> list[JSONOb
3301
4568
  resources = _builder_chart_resources(payload)
3302
4569
  elif tool_name == "portal_apply":
3303
4570
  resources = _builder_portal_resources(payload)
4571
+ elif tool_name == "portal_delete":
4572
+ resources = _builder_portal_resources(payload, operation_override="removed")
3304
4573
  elif tool_name == "app_custom_buttons_apply":
3305
4574
  resources = _builder_button_resources(payload)
3306
4575
  elif tool_name == "app_associated_resources_apply":
@@ -3316,6 +4585,38 @@ def _builder_apply_resources(tool_name: str, payload: JSONObject) -> list[JSONOb
3316
4585
  return resources
3317
4586
 
3318
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
+
3319
4620
  def _builder_apply_tool_is_app_scoped(tool_name: str) -> bool:
3320
4621
  return tool_name in {
3321
4622
  "app_schema_apply",
@@ -3496,7 +4797,15 @@ def _builder_package_resources(payload: JSONObject) -> list[JSONObject]:
3496
4797
  package_id = payload.get("package_id") or payload.get("id")
3497
4798
  package_name = payload.get("package_name") or payload.get("name")
3498
4799
  status = _builder_status(payload, "success")
3499
- 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
+ )
3500
4809
  normalized_args = payload.get("normalized_args") if isinstance(payload.get("normalized_args"), dict) else {}
3501
4810
  icon_config = (
3502
4811
  _builder_container_icon_config(payload, raw_keys=("icon", "tagIcon", "tag_icon"))
@@ -3547,7 +4856,7 @@ def _builder_schema_resources(payload: JSONObject) -> list[JSONObject]:
3547
4856
  parent=package_parent,
3548
4857
  icon_config=icon_config,
3549
4858
  error_code=item.get("error_code"),
3550
- message=item.get("message") if status == "failed" else None,
4859
+ message=item.get("message") if status in {"failed", "pending_readback"} else None,
3551
4860
  )
3552
4861
  )
3553
4862
  resources.extend(_builder_field_resources(item.get("field_diff_details") or item.get("field_diff"), parent=parent))
@@ -3748,7 +5057,7 @@ def _builder_chart_resources(payload: JSONObject) -> list[JSONObject]:
3748
5057
  return resources
3749
5058
 
3750
5059
 
3751
- def _builder_portal_resources(payload: JSONObject) -> list[JSONObject]:
5060
+ def _builder_portal_resources(payload: JSONObject, *, operation_override: str | None = None) -> list[JSONObject]:
3752
5061
  status = _builder_status(payload, "success")
3753
5062
  draft_result = payload.get("draft_result") if isinstance(payload.get("draft_result"), dict) else {}
3754
5063
  live_result = payload.get("live_result") if isinstance(payload.get("live_result"), dict) else {}
@@ -3779,7 +5088,7 @@ def _builder_portal_resources(payload: JSONObject) -> list[JSONObject]:
3779
5088
  first_tag = draft_result.get("tags")[0]
3780
5089
  if isinstance(first_tag, dict):
3781
5090
  package_id = first_tag.get("tagId") or first_tag.get("tag_id") or first_tag.get("id")
3782
- 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"))
3783
5092
  parent = None
3784
5093
  if package_id:
3785
5094
  parent = _builder_parent("package", id_value=package_id, key=package_id)
@@ -3968,7 +5277,7 @@ def _coerce_api_error(error: Exception) -> QingflowApiError:
3968
5277
 
3969
5278
 
3970
5279
  def _public_error_message(error_code: str, error: QingflowApiError) -> str:
3971
- if error.backend_code == 40074 or error_code == "APP_EDIT_LOCKED":
5280
+ if backend_code_int(error) == 40074 or error_code == "APP_EDIT_LOCKED":
3972
5281
  return "app is currently locked by another active editor session"
3973
5282
  if error.http_status != 404:
3974
5283
  return error.message
@@ -4143,13 +5452,29 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4143
5452
  },
4144
5453
  },
4145
5454
  "package_apply": {
4146
- "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
+ ],
4147
5468
  "aliases": {
4148
5469
  "packageId": "package_id",
4149
5470
  "packageName": "package_name",
4150
5471
  "createIfMissing": "create_if_missing",
4151
5472
  "iconName": "icon",
4152
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",
4153
5478
  "allowDetach": "allow_detach",
4154
5479
  },
4155
5480
  "allowed_values": {
@@ -4161,12 +5486,14 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4161
5486
  "execution_notes": [
4162
5487
  "create or update package metadata, visibility, grouping, and ordering in one call",
4163
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",
4164
5490
  "updating a package preserves existing icon/color when omitted; explicit icon/color values are still validated",
4165
5491
  "call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
4166
5492
  "metadata keys omitted on update are preserved",
4167
5493
  "package_id maps internally to backend tagId; do not use tag_id in public calls",
4168
5494
  "items is a full package layout tree; omitting existing app/portal items is blocked unless allow_detach=true",
4169
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",
4170
5497
  *_VISIBILITY_EXECUTION_NOTES,
4171
5498
  ],
4172
5499
  "minimal_example": {
@@ -4309,7 +5636,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4309
5636
  "app_custom_buttons_apply": {
4310
5637
  "allowed_keys": [
4311
5638
  "app_key",
4312
- "apps",
4313
5639
  "upsert_buttons",
4314
5640
  "patch_buttons",
4315
5641
  "remove_buttons",
@@ -4394,6 +5720,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4394
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)",
4395
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",
4396
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",
4397
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",
4398
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",
4399
5726
  "view_configs[].buttons is required in merge mode; omitting buttons is blocked to avoid no-op writes and accidental publish",
@@ -4406,7 +5733,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4406
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",
4407
5734
  "all operations share one edit context and publish after at least one write succeeds; there is no draft-only mode for this tool",
4408
5735
  "background_color and text_color cannot both be white",
4409
- "accepts apps[] for multi-app batch; each item is {app_key, upsert_buttons?, patch_buttons?, remove_buttons?, view_configs?}",
4410
5736
  ],
4411
5737
  "minimal_example": {
4412
5738
  "profile": "default",
@@ -4482,7 +5808,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4482
5808
  "app_associated_resources_apply": {
4483
5809
  "allowed_keys": [
4484
5810
  "app_key",
4485
- "apps",
4486
5811
  "upsert_resources",
4487
5812
  "patch_resources",
4488
5813
  "remove_associated_item_ids",
@@ -4539,6 +5864,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4539
5864
  "this tool manages Qingflow in-app associated report/view display; it does not create or edit QingBI report bodies/configs",
4540
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",
4541
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",
4542
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",
4543
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",
4544
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",
@@ -4552,7 +5878,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4552
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",
4553
5879
  "this tool publishes after at least one write succeeds; there is no draft-only mode",
4554
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",
4555
- "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?}",
4556
5881
  ],
4557
5882
  "minimal_example": {
4558
5883
  "profile": "default",
@@ -4583,22 +5908,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4583
5908
  }
4584
5909
  ],
4585
5910
  },
4586
- "batch_example": {
4587
- "profile": "default",
4588
- "apps": [
4589
- {
4590
- "app_key": "APP_1",
4591
- "upsert_resources": [
4592
- {"client_key": "orders_view", "graph_type": "view", "target_app_key": "ORDER_APP", "view_key": "ORDER_VIEW"}
4593
- ],
4594
- "view_configs": [{"view_key": "MAIN_VIEW", "limit_type": "select", "associated_item_refs": ["orders_view"]}],
4595
- },
4596
- {
4597
- "app_key": "APP_2",
4598
- "view_configs": [{"view_key": "MAIN_VIEW", "visible": True, "limit_type": "all"}],
4599
- },
4600
- ],
4601
- },
4602
5911
  },
4603
5912
  "app_schema_plan": {
4604
5913
  "allowed_keys": ["app_key", "package_id", "app_name", "icon", "color", "visibility", "create_if_missing", "add_fields", "update_fields", "remove_fields"],
@@ -4642,6 +5951,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4642
5951
  "create mode may set visibility for the new app; edit mode may update visibility on an existing app",
4643
5952
  "create mode should include explicit non-template icon + color; apply mode enforces this before writing",
4644
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",
4645
5955
  *_VISIBILITY_EXECUTION_NOTES,
4646
5956
  ],
4647
5957
  "minimal_example": {
@@ -4678,8 +5988,12 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4678
5988
  "app_key",
4679
5989
  "package_id",
4680
5990
  "app_name",
5991
+ "app_title",
4681
5992
  "icon",
4682
5993
  "color",
5994
+ "icon_name",
5995
+ "icon_color",
5996
+ "icon_config",
4683
5997
  "visibility",
4684
5998
  "create_if_missing",
4685
5999
  "publish",
@@ -4692,11 +6006,15 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4692
6006
  "apps[].app_name",
4693
6007
  "apps[].icon",
4694
6008
  "apps[].color",
6009
+ "apps[].icon_name",
6010
+ "apps[].icon_color",
6011
+ "apps[].icon_config",
4695
6012
  "apps[].visibility",
4696
6013
  "apps[].add_fields",
4697
6014
  "apps[].update_fields",
4698
6015
  "apps[].remove_fields",
4699
6016
  "apps[].add_fields[].target_app_ref",
6017
+ "apps[].add_fields[].target_app",
4700
6018
  ],
4701
6019
  "aliases": {
4702
6020
  "app_title": "app_name",
@@ -4706,8 +6024,16 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4706
6024
  "apps[].appKey": "apps[].app_key",
4707
6025
  "apps[].appName": "apps[].app_name",
4708
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",
4709
6031
  "field.targetAppRef": "field.target_app_ref",
4710
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[]",
4711
6037
  "field.title": "field.name",
4712
6038
  "field.label": "field.name",
4713
6039
  "field.fields": "field.subfields",
@@ -4733,6 +6059,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4733
6059
  },
4734
6060
  "allowed_values": {
4735
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())},
4736
6063
  "field.relation_mode": [member.value for member in PublicRelationMode],
4737
6064
  "field.department_scope.mode": ["all", "custom"],
4738
6065
  "field.code_block_binding.outputs.target_field.type": list(INTEGRATION_OUTPUT_TARGET_FIELD_TYPES),
@@ -4744,14 +6071,22 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4744
6071
  "use exactly one resource mode",
4745
6072
  "edit mode: app_key, optional app_name to rename the existing app",
4746
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",
4747
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",
4748
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",
4749
6080
  "multi-app mode is not transactional; read created_app_keys and apps[].status before retrying, and retry only failed app items",
4750
6081
  "create mode defaults new app visibility to workspace/not when visibility is omitted; edit mode preserves current visibility when omitted",
4751
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",
4752
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",
4753
6087
  "edit mode preserves existing icon/color when omitted; explicit icon/color values are still validated",
4754
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",
4755
6090
  *_VISIBILITY_EXECUTION_NOTES,
4756
6091
  "update_fields is the field-level partial update path; it reads current form schema and preserves untouched field config",
4757
6092
  "multiple relation fields are backend-risky; read verification.relation_field_limit_verified and warnings before declaring the schema stable",
@@ -4759,6 +6094,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4759
6094
  "relation_mode=multiple maps to referenceConfig.optionalDataNum=0",
4760
6095
  "relation fields now require both display_field and visible_fields in MCP/CLI payloads",
4761
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",
4762
6098
  "update_fields[].set.subfield_updates is the safe patch path for editing existing subtable child fields without rebuilding the entire subtable",
4763
6099
  "subfield_updates only supports safe child overlays: name, required, description, and nested subfield_updates",
4764
6100
  "set.subfields remains the full replace/rebuild path for a subtable and is higher risk when hidden relation/reference children exist",
@@ -4974,7 +6310,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4974
6310
  "preset_example": {"profile": "default", "app_key": "APP_KEY", "mode": "merge", "preset": "balanced", "sections": []},
4975
6311
  },
4976
6312
  "app_layout_apply": {
4977
- "allowed_keys": ["app_key", "mode", "publish", "sections", "apps"],
6313
+ "allowed_keys": ["app_key", "mode", "publish", "sections"],
4978
6314
  "aliases": {"overwrite": "replace", "sectionId": "section_id"},
4979
6315
  "section_allowed_keys": ["type", "paragraph_id", "section_id", "title", "rows"],
4980
6316
  "section_aliases": {
@@ -4991,7 +6327,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4991
6327
  "mode=replace is full layout replacement and should be used only when intentionally rewriting all sections",
4992
6328
  "layout verification is split into layout_verified and layout_summary_verified",
4993
6329
  "LAYOUT_SUMMARY_UNVERIFIED means raw form readback is stronger than the compact summary",
4994
- "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",
4995
6330
  ],
4996
6331
  "minimal_section_example": {"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["字段A", "字段B", "字段C", "字段D"]]},
4997
6332
  "minimal_example": {
@@ -5001,73 +6336,95 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5001
6336
  "publish": True,
5002
6337
  "sections": [{"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["项目名称", "项目负责人", "项目阶段", "优先级"]]}],
5003
6338
  },
5004
- "batch_example": {
5005
- "profile": "default",
5006
- "publish": True,
5007
- "apps": [
5008
- {"app_key": "APP_KEY_1", "mode": "merge", "sections": [{"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["字段A", "字段B"]]}]},
5009
- {"app_key": "APP_KEY_2", "mode": "merge", "sections": [{"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["字段C", "字段D"]]}]},
5010
- ],
5011
- },
5012
6339
  },
5013
- "app_flow_get_schema": {
5014
- "allowed_keys": ["schema_version"],
5015
- "aliases": {"schemaVersion": "schema_version"},
5016
- "allowed_values": {},
5017
- "execution_notes": [
5018
- "returns WorkflowSpec JSON Schema from /workflow/spec/schema",
5019
- "call this before authoring a new spec or validating spec shape",
5020
- "worksheet-level approval deduplication toggles (legacy global settings) are not yet part of WorkflowSpec",
5021
- ],
5022
- "minimal_example": {
5023
- "profile": "default",
5024
- "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",
5025
6353
  },
5026
- },
5027
- "app_flow_get": {
5028
- "allowed_keys": ["app_key", "version_id"],
5029
- "aliases": {"versionId": "version_id"},
5030
- "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
+ ],
5031
6363
  "execution_notes": [
5032
- "returns current WorkflowSpecDTO for one app via GET /workflow/spec",
5033
- "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",
5034
6367
  ],
5035
6368
  "minimal_example": {
5036
6369
  "profile": "default",
5037
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": [],
5038
6383
  },
5039
6384
  },
5040
6385
  "app_flow_apply": {
5041
- "allowed_keys": ["app_key", "publish", "spec", "idempotency_key", "schema_version", "patch_nodes"],
6386
+ "allowed_keys": ["app_key", "mode", "publish", "nodes", "transitions"],
5042
6387
  "aliases": {
5043
- "schemaVersion": "schema_version",
5044
- "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,
5045
6399
  },
5046
- "allowed_values": {},
5047
6400
  "dependency_hints": [
5048
- "when using spec: must be a complete WorkflowSpecDTO object (replace-only apply); when using patch_nodes[]: spec is not required",
5049
- "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",
5050
6403
  ],
5051
6404
  "execution_notes": [
5052
- "posts to /workflow/spec:apply with appKey, idempotencyKey, schemaVersion, and spec",
5053
- "verification uses appliedSpec, diffSummary, and semanticLint from the apply response",
5054
- "publish=false keeps changes in draft when supported by the backend",
5055
- "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",
5056
6410
  ],
5057
6411
  "minimal_example": {
5058
6412
  "profile": "default",
5059
6413
  "app_key": "APP_KEY",
6414
+ "mode": "replace",
5060
6415
  "publish": True,
5061
- "spec": {
5062
- "nodes": [{"id": "n1", "type": "APPLICANT", "name": "发起"}],
5063
- "transitions": [],
5064
- },
5065
- },
5066
- "patch_nodes_example": {
5067
- "profile": "default",
5068
- "app_key": "APP_KEY",
5069
- "publish": True,
5070
- "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"}],
5071
6428
  },
5072
6429
  },
5073
6430
  "app_views_plan": {
@@ -5125,12 +6482,15 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5125
6482
  **deepcopy(_VISIBILITY_ALLOWED_VALUES),
5126
6483
  },
5127
6484
  "execution_notes": [
6485
+ "creating a new view follows backend createViewgraphConfig and requires both ViewManagementAuth and DataManageAuth; updating/deleting existing views only requires ViewManagementAuth",
5128
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",
5129
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",
5130
- "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",
5131
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",
5132
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",
5133
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",
5134
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",
5135
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",
5136
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",
@@ -5139,7 +6499,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5139
6499
  "minimal_example": {
5140
6500
  "profile": "default",
5141
6501
  "app_key": "APP_KEY",
5142
- "upsert_views": [{"name": "全部数据", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
6502
+ "upsert_views": [{"name": "项目台账视图", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
5143
6503
  "remove_views": [],
5144
6504
  },
5145
6505
  "query_conditions_example": {
@@ -5177,6 +6537,14 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5177
6537
  ],
5178
6538
  "remove_views": [],
5179
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
+ },
5180
6548
  "gantt_example": {
5181
6549
  "profile": "default",
5182
6550
  "app_key": "APP_KEY",
@@ -5197,7 +6565,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5197
6565
  "app_views_apply": {
5198
6566
  "allowed_keys": [
5199
6567
  "app_key",
5200
- "apps",
5201
6568
  "publish",
5202
6569
  "upsert_views",
5203
6570
  "patch_views",
@@ -5250,27 +6617,29 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5250
6617
  },
5251
6618
  "execution_notes": [
5252
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",
5253
6621
  "when duplicate view names exist, supply view_key to target the exact view",
5254
6622
  "read back app_get after any failed or partial view apply",
5255
6623
  "view existence verification and saved-filter verification are separate; treat filters as unverified until verification.view_filters_verified is true",
5256
6624
  "buttons omitted preserves existing button config; buttons=[] clears all buttons; buttons=[...] replaces the full button config",
5257
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",
5258
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",
5259
- "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",
5260
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",
5261
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",
5262
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",
5263
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",
5264
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",
5265
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",
5266
- "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",
5267
6636
  *_VISIBILITY_EXECUTION_NOTES,
5268
6637
  ],
5269
6638
  "minimal_example": {
5270
6639
  "profile": "default",
5271
6640
  "app_key": "APP_KEY",
5272
6641
  "publish": True,
5273
- "upsert_views": [{"name": "全部数据", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
6642
+ "upsert_views": [{"name": "项目台账视图", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
5274
6643
  "remove_views": [],
5275
6644
  },
5276
6645
  "query_conditions_example": {
@@ -5310,6 +6679,14 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5310
6679
  ],
5311
6680
  "remove_views": [],
5312
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
+ },
5313
6690
  "gantt_example": {
5314
6691
  "profile": "default",
5315
6692
  "app_key": "APP_KEY",
@@ -5329,19 +6706,18 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5329
6706
  },
5330
6707
  },
5331
6708
  "app_get": {
5332
- "allowed_keys": ["app_key", "app_keys"],
6709
+ "allowed_keys": ["app_key"],
5333
6710
  "aliases": {},
5334
6711
  "allowed_values": {},
5335
6712
  "execution_notes": [
5336
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",
5337
6714
  "use this as the default builder discovery read before view_get/chart_get/apply detail work",
5338
6715
  "editability is route-aware builder capability summary, not end-user data visibility",
5339
- "can_edit_app_base covers app base-info writes such as app_name, icon, and visibility",
5340
- "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",
5341
6718
  "returns normalized app visibility when backend auth is readable",
5342
6719
  "custom_buttons[].button_id is the id required by app_custom_buttons_apply view_configs[].buttons[].button_ref",
5343
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",
5344
- "accepts app_keys[] for batch summary read; batch returns {status, apps[], errors[]} where each apps[] item preserves the single app_get summary fields",
5345
6721
  ],
5346
6722
  "minimal_example": {
5347
6723
  "profile": "default",
@@ -5349,7 +6725,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5349
6725
  },
5350
6726
  },
5351
6727
  "app_get_fields": {
5352
- "allowed_keys": ["app_key", "app_keys"],
6728
+ "allowed_keys": ["app_key"],
5353
6729
  "aliases": {},
5354
6730
  "allowed_values": {},
5355
6731
  "execution_notes": [
@@ -5357,8 +6733,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5357
6733
  "use this before app_schema_apply when you need exact field definitions",
5358
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",
5359
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",
5360
6737
  "subtable fields include nested subfields using the same compact field shape",
5361
- "accepts app_keys[] for batch read; batch returns {status, apps[], errors[]} where each apps[] item has {app_key, fields}",
5362
6738
  ],
5363
6739
  "minimal_example": {
5364
6740
  "profile": "default",
@@ -5366,13 +6742,12 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5366
6742
  },
5367
6743
  },
5368
6744
  "app_get_layout": {
5369
- "allowed_keys": ["app_key", "app_keys"],
6745
+ "allowed_keys": ["app_key"],
5370
6746
  "aliases": {},
5371
6747
  "allowed_values": {},
5372
6748
  "execution_notes": [
5373
6749
  "returns compact current layout configuration for one app",
5374
6750
  "use this before app_layout_apply when you need paragraph and row structure",
5375
- "accepts app_keys[] for batch read; batch returns {status, apps[], errors[]} where each apps[] item has {app_key, sections}",
5376
6751
  ],
5377
6752
  "minimal_example": {
5378
6753
  "profile": "default",
@@ -5380,7 +6755,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5380
6755
  },
5381
6756
  },
5382
6757
  "app_get_views": {
5383
- "allowed_keys": ["app_key", "app_keys"],
6758
+ "allowed_keys": ["app_key"],
5384
6759
  "aliases": {},
5385
6760
  "allowed_values": {},
5386
6761
  "execution_notes": [
@@ -5388,7 +6763,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5388
6763
  "compatibility/specialized inventory tool; default builder discovery should start with app_get",
5389
6764
  "use this before app_views_apply only when you need an exact current view inventory beyond app_get",
5390
6765
  "view items include visibility_summary when backend view auth is readable",
5391
- "accepts app_keys[] for batch read; batch returns {status, apps[], errors[]} where each apps[] item has {app_key, views}",
5392
6766
  ],
5393
6767
  "minimal_example": {
5394
6768
  "profile": "default",
@@ -5396,13 +6770,12 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5396
6770
  },
5397
6771
  },
5398
6772
  "app_get_flow": {
5399
- "allowed_keys": ["app_key", "app_keys"],
6773
+ "allowed_keys": ["app_key"],
5400
6774
  "aliases": {},
5401
6775
  "allowed_values": {},
5402
6776
  "execution_notes": [
5403
- "returns WorkflowSpecDTO for one app (alias of app_flow_get)",
5404
- "use app_flow_get_schema then app_flow_get before app_flow_apply",
5405
- "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",
5406
6779
  ],
5407
6780
  "minimal_example": {
5408
6781
  "profile": "default",
@@ -5410,7 +6783,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5410
6783
  },
5411
6784
  },
5412
6785
  "app_get_charts": {
5413
- "allowed_keys": ["app_key", "app_keys"],
6786
+ "allowed_keys": ["app_key"],
5414
6787
  "aliases": {},
5415
6788
  "allowed_values": {},
5416
6789
  "execution_notes": [
@@ -5419,37 +6792,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5419
6792
  "use this before app_charts_apply when you need exact current chart_id values beyond the app_get summary",
5420
6793
  "chart summaries do not include full qingbi config payloads",
5421
6794
  "chart items include visibility_summary when QingBI base info is readable",
5422
- "accepts app_keys[] for batch read; batch returns {status, apps[], errors[]} where each apps[] item has {app_key, charts}",
5423
- ],
5424
- "minimal_example": {
5425
- "profile": "default",
5426
- "app_key": "APP_KEY",
5427
- },
5428
- },
5429
- "app_get_buttons": {
5430
- "allowed_keys": ["app_key", "app_keys"],
5431
- "aliases": {},
5432
- "allowed_values": {},
5433
- "execution_notes": [
5434
- "returns custom button list (draft state) for one app",
5435
- "also returns view_configs read from view bindings, so app_custom_buttons_apply view_configs can be patched from the same read result",
5436
- "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}",
5437
- "use before app_custom_buttons_apply when you need current button_id values",
5438
- ],
5439
- "minimal_example": {
5440
- "profile": "default",
5441
- "app_key": "APP_KEY",
5442
- },
5443
- },
5444
- "app_get_associated_resources": {
5445
- "allowed_keys": ["app_key", "app_keys"],
5446
- "aliases": {},
5447
- "allowed_values": {},
5448
- "execution_notes": [
5449
- "returns associated resource pool (draft state) for one app",
5450
- "also returns view_configs read from view bindings, so app_associated_resources_apply view_configs can be patched from the same read result",
5451
- "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}",
5452
- "use before app_associated_resources_apply when you need current associated_item_id values",
5453
6795
  ],
5454
6796
  "minimal_example": {
5455
6797
  "profile": "default",
@@ -5464,6 +6806,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5464
6806
  "returns builder-configurable portal list items only",
5465
6807
  "use this as the builder portal discovery path before portal_get",
5466
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",
5467
6810
  ],
5468
6811
  "minimal_example": {
5469
6812
  "profile": "default",
@@ -5486,7 +6829,22 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5486
6829
  },
5487
6830
  },
5488
6831
  "app_charts_apply": {
5489
- "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
+ ],
5490
6848
  "aliases": {
5491
6849
  "patchCharts": "patch_charts",
5492
6850
  "chart.id": "chart.chart_id",
@@ -5494,6 +6852,11 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5494
6852
  "chart.dimension_fields": "chart.dimension_field_ids",
5495
6853
  "chart.indicator_fields": "chart.indicator_field_ids",
5496
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",
5497
6860
  "chart.filter.op": "chart.filter.operator",
5498
6861
  },
5499
6862
  "allowed_values": {
@@ -5514,18 +6877,20 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5514
6877
  "upsert_charts[].visibility compiles to QingBI base visibleAuth only",
5515
6878
  "visibility-only updates keep the existing chart config and do not rewrite rawDataConfigDTO.authInfo",
5516
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",
5517
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",
5518
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",
5519
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",
5520
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",
5521
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",
5522
- "accepts apps[] for multi-app batch; each item is {app_key, upsert_charts?, patch_charts?, remove_chart_ids?, reorder_chart_ids?}",
5523
6888
  *_VISIBILITY_EXECUTION_NOTES,
5524
6889
  ],
5525
6890
  "minimal_example": {
5526
6891
  "profile": "default",
5527
6892
  "app_key": "APP_KEY",
5528
- "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)}],
5529
6894
  "patch_charts": [{"chart_id": "CHART_ID", "set": {"name": "数据总量-新版"}}],
5530
6895
  "remove_chart_ids": [],
5531
6896
  "reorder_chart_ids": [],
@@ -5563,7 +6928,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5563
6928
  },
5564
6929
  },
5565
6930
  "portal_apply": {
5566
- "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"],
5567
6932
  "aliases": {
5568
6933
  "packageId": "package_id",
5569
6934
  "name": "dash_name",
@@ -5575,13 +6940,14 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5575
6940
  "section_allowed_keys": ["title", "source_type", "role", "position", "dash_style_config", "config", "chart_ref", "view_ref", "text", "url"],
5576
6941
  "section_aliases": {
5577
6942
  "sourceType": "source_type",
6943
+ "zone": "role",
6944
+ "sectionRole": "role",
5578
6945
  "chartRef": "chart_ref",
5579
6946
  "viewRef": "view_ref",
5580
6947
  "dashStyleConfigBO": "dash_style_config",
5581
6948
  },
5582
6949
  "allowed_values": {
5583
6950
  "section.source_type": ["chart", "view", "grid", "filter", "text", "link"],
5584
- "section.role": ["metric"],
5585
6951
  "workspace_icon.icon_names": list(WORKSPACE_ICON_NAMES),
5586
6952
  "workspace_icon.icon_colors": list(WORKSPACE_ICON_COLORS),
5587
6953
  "workspace_icon.generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
@@ -5591,23 +6957,25 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5591
6957
  "use exactly one resource mode",
5592
6958
  "update mode: dash_key",
5593
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",
5594
6961
  "create mode requires explicit icon + color; icon=template is blocked because it is too generic",
5595
6962
  "edit mode preserves existing icon/color when omitted; explicit icon/color values are still validated",
5596
6963
  "call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
5597
6964
  "portal_apply uses replace semantics for sections",
5598
6965
  "when editing an existing portal, sections may be omitted to update only base info such as visibility, icon, or package",
5599
- "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",
5600
- "when sections[] is supplied without patch_sections[], it uses replace semantics for all sections",
5601
- "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",
5602
6968
  "package_id is required when creating a new portal",
5603
6969
  "publish=false only guarantees draft and base-info updates; it does not claim live has changed",
5604
- "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",
5605
6971
  "view_ref resolves by view_key first, then exact unique view_name",
5606
6972
  "pc layout uses a 24-column grid; mobile layout uses a 6-column grid",
5607
- "set section.role=metric only for target/indicator chart cards; metric cards use pc.rows >= 5 and pc.cols >= 6, while ordinary BI charts use pc.rows >= 7 and pc.cols >= 8",
5608
6973
  "if unsure about layout, omit position or use layout_preset=auto/dashboard_2col/dashboard_3col",
5609
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",
5610
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",
5611
6979
  "position.pc/mobile is the canonical portal layout shape",
5612
6980
  "compat payload accepts name -> dash_name and single pages[0].components -> sections",
5613
6981
  "visibility is the canonical public auth shape; auth is kept only as a deprecated compatibility alias",
@@ -5661,14 +7029,19 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5661
7029
  "mobile": {"x": 0, "y": 0, "cols": 6, "rows": 8},
5662
7030
  },
5663
7031
  },
5664
- "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": {
5665
7043
  "profile": "default",
5666
7044
  "dash_key": "DASH_KEY",
5667
- "publish": True,
5668
- "patch_sections": [
5669
- {"chart_ref": {"chart_id": "CHART_ID"}, "set": {"title": "销售总览-新版"}},
5670
- {"order": 2, "set": {"title": "任务列表-新版"}},
5671
- ],
5672
7045
  },
5673
7046
  },
5674
7047
  "app_publish_verify": {
@@ -5708,6 +7081,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5708
7081
  _PRIVATE_BUILDER_TOOL_CONTRACTS = {
5709
7082
  "app_schema_plan",
5710
7083
  "app_layout_plan",
7084
+ "app_flow_plan",
5711
7085
  "app_views_plan",
5712
7086
  }
5713
7087