@qingflow-tech/qingflow-cli 1.1.2 → 1.1.34

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 (103) hide show
  1. package/README.md +2 -2
  2. package/docs/local-agent-install.md +10 -0
  3. package/package.json +1 -1
  4. package/pyproject.toml +1 -1
  5. package/skills/qingflow-cli/SKILL.md +61 -47
  6. package/skills/qingflow-cli/manifest.yaml +1 -1
  7. package/skills/qingflow-cli/reference/00-INDEX.md +35 -0
  8. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md +21 -477
  9. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md +44 -290
  10. package/skills/qingflow-cli/reference/QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md +11 -102
  11. package/skills/qingflow-cli/reference/builder/10-build-single-app.md +57 -0
  12. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +84 -0
  13. package/skills/qingflow-cli/reference/builder/30-schema-fields.md +317 -0
  14. package/skills/qingflow-cli/reference/builder/40-layout.md +70 -0
  15. package/skills/qingflow-cli/reference/builder/50-views.md +540 -0
  16. package/skills/qingflow-cli/reference/builder/60-charts.md +328 -0
  17. package/skills/qingflow-cli/reference/builder/70-portal.md +372 -0
  18. package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +148 -0
  19. package/skills/qingflow-cli/reference/builder/90-workflow.md +35 -0
  20. package/skills/qingflow-cli/reference/builder/99-publish-verify.md +64 -0
  21. package/skills/qingflow-cli/reference/builder/README.md +62 -0
  22. package/skills/qingflow-cli/reference/builder/code-integrations/README.md +130 -0
  23. package/skills/qingflow-cli/reference/builder/code-integrations/code-block.md +66 -0
  24. package/skills/qingflow-cli/reference/builder/code-integrations/q-linker.md +77 -0
  25. package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +508 -0
  26. package/skills/qingflow-cli/reference/builder/reference/match-rules.md +229 -0
  27. package/skills/qingflow-cli/reference/builder/reference/workspace-icons.md +89 -0
  28. package/skills/qingflow-cli/reference/builder/workflow/01-overview.md +48 -0
  29. package/skills/qingflow-cli/reference/builder/workflow/02-update-mode.md +63 -0
  30. package/skills/qingflow-cli/reference/builder/workflow/03-flow-patterns.md +59 -0
  31. package/skills/qingflow-cli/reference/builder/workflow/04-stage1-business-modeling.md +131 -0
  32. package/skills/qingflow-cli/reference/builder/workflow/05-stage2-members-roles.md +29 -0
  33. package/skills/qingflow-cli/reference/builder/workflow/06-stage3-build-spec.md +211 -0
  34. package/skills/qingflow-cli/reference/builder/workflow/07-stage4-validate-spec.md +36 -0
  35. package/skills/qingflow-cli/reference/builder/workflow/08-stage5-apply-verify.md +66 -0
  36. package/skills/qingflow-cli/reference/builder/workflow/09-stage6-summary.md +88 -0
  37. package/skills/qingflow-cli/reference/builder/workflow/10-node-config-reference.md +93 -0
  38. package/skills/qingflow-cli/reference/builder/workflow/11-troubleshooting.md +18 -0
  39. package/skills/qingflow-cli/reference/builder/workflow/README.md +88 -0
  40. package/skills/qingflow-cli/reference/builder/workflow/workflow-schema.json +1754 -0
  41. package/skills/qingflow-cli/reference/charts_upsert_dashboard_starter.example.json +81 -0
  42. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +94 -0
  43. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +139 -0
  44. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +84 -0
  45. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md +129 -0
  46. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +195 -0
  47. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +159 -0
  48. package/skills/qingflow-cli/reference/examples/README.md +17 -0
  49. package/skills/qingflow-cli/reference/examples/associated/associated_resources_semantic_full.example.json +50 -0
  50. package/skills/qingflow-cli/reference/examples/buttons/custom_buttons_advanced.example.json +56 -0
  51. package/skills/qingflow-cli/reference/examples/charts/charts_remove.example.json +1 -0
  52. package/skills/qingflow-cli/reference/examples/charts/charts_reorder.example.json +1 -0
  53. package/skills/qingflow-cli/reference/examples/charts/charts_upsert_bar.example.json +8 -0
  54. package/skills/qingflow-cli/reference/examples/charts/charts_upsert_dashboard_starter.example.json +118 -0
  55. package/skills/qingflow-cli/reference/examples/charts/charts_upsert_minimal.example.json +13 -0
  56. package/skills/qingflow-cli/reference/examples/layout/layout_sections_full.example.json +22 -0
  57. package/skills/qingflow-cli/reference/examples/portal/portal_sections_all_types.example.json +145 -0
  58. package/skills/qingflow-cli/reference/examples/portal/portal_sections_five_types.example.json +126 -0
  59. package/skills/qingflow-cli/reference/examples/portal/portal_sections_standard_workbench.example.json +178 -0
  60. package/skills/qingflow-cli/reference/examples/record/record_insert_all_field_types.example.json +35 -0
  61. package/skills/qingflow-cli/reference/examples/schema/_batch_schema_complex.json +18 -0
  62. package/skills/qingflow-cli/reference/examples/schema/_batch_schema_scalar.json +17 -0
  63. package/skills/qingflow-cli/reference/examples/schema/schema_add_fields_minimal.example.json +7 -0
  64. package/skills/qingflow-cli/reference/examples/schema/schema_apply_add_fields_all_types.json +78 -0
  65. package/skills/qingflow-cli/reference/examples/schema/schema_multi_app_form_relation_layout.example.json +166 -0
  66. package/skills/qingflow-cli/reference/examples/schema/schema_single_app_form_standard.example.json +95 -0
  67. package/skills/qingflow-cli/reference/examples/views/views_batch_full.example.json +159 -0
  68. package/skills/qingflow-cli/reference/examples/views/views_upsert_table_minimal.example.json +42 -0
  69. package/skills/qingflow-cli/reference/portal_sections_all_types.example.json +18 -4
  70. package/skills/qingflow-cli/reference/portal_sections_five_types.example.json +3 -3
  71. package/skills/qingflow-cli/reference/portal_sections_standard_workbench.example.json +57 -7
  72. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +22 -0
  73. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_DELETE_WORKFLOW.md +31 -0
  74. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md +176 -0
  75. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +167 -0
  76. package/skills/qingflow-cli/reference/record/analysis/README.md +130 -0
  77. package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +91 -0
  78. package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +112 -0
  79. package/skills/qingflow-cli/reference/record/analysis/business-context.md +74 -0
  80. package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +69 -0
  81. package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +106 -0
  82. package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +172 -0
  83. package/skills/qingflow-cli/reference/record/analysis/report-format.md +76 -0
  84. package/skills/qingflow-cli/reference/record/insert/README.md +125 -0
  85. package/skills/qingflow-cli/reference/schema_apply_add_fields_all_types.json +1 -1
  86. package/skills/qingflow-cli/reference/task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md +153 -0
  87. package/skills/qingflow-cli/reference/task/ops/README.md +134 -0
  88. package/skills/qingflow-cli/reference/task/ops/environments.md +43 -0
  89. package/skills/qingflow-cli/reference/task/ops/workflow-usage.md +29 -0
  90. package/skills/qingflow-cli/reference/views_upsert_table_minimal.example.json +42 -7
  91. package/skills/qingflow-cli/scripts/validate_system_build_summary.py +124 -0
  92. package/skills/qingflow-cli/scripts/workflow/diff_flow_spec.py +275 -0
  93. package/skills/qingflow-cli/scripts/workflow/validate_flow_spec.py +627 -0
  94. package/src/qingflow_mcp/builder_facade/models.py +413 -119
  95. package/src/qingflow_mcp/builder_facade/service.py +4238 -666
  96. package/src/qingflow_mcp/cli/commands/builder.py +232 -75
  97. package/src/qingflow_mcp/cli/main.py +34 -0
  98. package/src/qingflow_mcp/public_surface.py +10 -0
  99. package/src/qingflow_mcp/server_app_builder.py +62 -34
  100. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  101. package/src/qingflow_mcp/solution/executor.py +2 -0
  102. package/src/qingflow_mcp/tools/ai_builder_tools.py +2649 -457
  103. package/src/qingflow_mcp/tools/record_tools.py +23 -11
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from concurrent.futures import ThreadPoolExecutor, as_completed
3
4
  from copy import deepcopy
4
5
  import json
5
6
  import time
@@ -15,7 +16,7 @@ from ..builder_facade.button_style_catalog import (
15
16
  )
16
17
  from ..public_surface import public_builder_contract_tool_names
17
18
  from ..config import DEFAULT_PROFILE
18
- from ..errors import QingflowApiError, backend_code_int
19
+ from ..errors import QingflowApiError, backend_code_int, backend_code_value_int
19
20
  from ..json_types import JSONObject
20
21
  from ..builder_facade.models import (
21
22
  AssociatedResourcesApplyRequest,
@@ -27,6 +28,10 @@ from ..builder_facade.models import (
27
28
  FieldPatch,
28
29
  FieldRemovePatch,
29
30
  FieldUpdatePatch,
31
+ FlowPreset,
32
+ FlowNodePatch,
33
+ FlowPlanRequest,
34
+ FlowTransitionPatch,
30
35
  LayoutApplyMode,
31
36
  LayoutPlanRequest,
32
37
  LayoutPreset,
@@ -45,6 +50,8 @@ from ..builder_facade.models import (
45
50
  ViewUpsertPatch,
46
51
  ViewsPreset,
47
52
  ViewsPlanRequest,
53
+ public_view_partial_payload,
54
+ public_view_upsert_payload,
48
55
  )
49
56
  from ..builder_facade.service import AiBuilderFacade, INTEGRATION_OUTPUT_TARGET_FIELD_TYPES
50
57
  from ..solution.compiler.icon_utils import (
@@ -66,6 +73,7 @@ from .qingbi_report_tools import QingbiReportTools
66
73
  from .role_tools import RoleTools
67
74
  from .solution_tools import SolutionTools
68
75
  from .view_tools import ViewTools
76
+ from .workflow_tools import WorkflowTools
69
77
 
70
78
 
71
79
  def _normalize_builder_view_key(value: str) -> str:
@@ -75,6 +83,52 @@ def _normalize_builder_view_key(value: str) -> str:
75
83
  return raw
76
84
 
77
85
 
86
+ def _payload_get(payload: JSONObject, *keys: str, default: Any = None) -> Any:
87
+ for key in keys:
88
+ if key in payload:
89
+ return payload.get(key)
90
+ return default
91
+
92
+
93
+ def _payload_list(payload: JSONObject, *keys: str, default: list | None = None) -> list:
94
+ value = _payload_get(payload, *keys, default=default if default is not None else [])
95
+ return value if isinstance(value, list) else []
96
+
97
+
98
+ def _payload_bool(payload: JSONObject, *keys: str, default: bool = True) -> bool:
99
+ value = _payload_get(payload, *keys, default=default)
100
+ if isinstance(value, bool):
101
+ return value
102
+ if isinstance(value, str):
103
+ normalized = value.strip().lower()
104
+ if normalized in {"1", "true", "yes", "y", "on"}:
105
+ return True
106
+ if normalized in {"0", "false", "no", "n", "off"}:
107
+ return False
108
+ return bool(value)
109
+
110
+
111
+ def _elapsed_ms(started_at: float) -> int:
112
+ return max(0, int((time.perf_counter() - started_at) * 1000))
113
+
114
+
115
+ def _merge_duration_breakdown(payload: JSONObject, **items: int | None) -> None:
116
+ breakdown = payload.get("duration_breakdown_ms")
117
+ if not isinstance(breakdown, dict):
118
+ breakdown = {}
119
+ payload["duration_breakdown_ms"] = breakdown
120
+ for key, value in items.items():
121
+ if value is None:
122
+ continue
123
+ breakdown[key] = int(value)
124
+
125
+
126
+ def _inline_form_layout_was_handled(payload: JSONObject) -> bool:
127
+ inline_layout = payload.get("inline_form_layout")
128
+ return isinstance(inline_layout, dict) and bool(inline_layout.get("requested"))
129
+
130
+
131
+ PUBLIC_STABLE_FLOW_NODE_TYPES = ["start", "approve", "fill", "copy", "webhook", "end"]
78
132
  BUILDER_APPLY_SCHEMA_VERSION = "builder.apply.v1"
79
133
  BUILDER_APPLY_TOOL_NAMES = {
80
134
  "package_apply",
@@ -89,6 +143,9 @@ BUILDER_APPLY_TOOL_NAMES = {
89
143
  "portal_delete",
90
144
  "app_publish_verify",
91
145
  }
146
+ MULTI_APP_SCHEMA_APPLY_PARALLEL_LIMIT = 10
147
+ VIEW_APPLY_PARALLEL_LIMIT = 20
148
+ PORTAL_INLINE_CHART_MAX_WORKERS = 10
92
149
 
93
150
 
94
151
  class AiBuilderTools(ToolBase):
@@ -109,6 +166,7 @@ class AiBuilderTools(ToolBase):
109
166
  buttons=CustomButtonTools(sessions, backend),
110
167
  packages=PackageTools(sessions, backend),
111
168
  views=ViewTools(sessions, backend),
169
+ workflows=WorkflowTools(sessions, backend),
112
170
  portals=PortalTools(sessions, backend),
113
171
  charts=QingbiReportTools(sessions, backend),
114
172
  roles=RoleTools(sessions, backend),
@@ -116,6 +174,193 @@ class AiBuilderTools(ToolBase):
116
174
  solutions=SolutionTools(sessions, backend),
117
175
  )
118
176
 
177
+ def _apply_app_batch(self, *, tool_name: str, profile: str, apps: list[JSONObject], apply_one) -> JSONObject:
178
+ started_at = time.perf_counter()
179
+ batch_item_apply_ms = 0
180
+ child_total_ms = 0
181
+
182
+ def finish(payload: JSONObject) -> JSONObject:
183
+ _merge_duration_breakdown(
184
+ payload,
185
+ batch_item_apply_ms=batch_item_apply_ms,
186
+ child_total_ms=child_total_ms,
187
+ total_ms=_elapsed_ms(started_at),
188
+ )
189
+ return _attach_builder_apply_envelope(tool_name, payload)
190
+
191
+ if not isinstance(apps, list) or not apps:
192
+ return finish(
193
+ _config_failure(
194
+ tool_name=tool_name,
195
+ message=f"{tool_name} batch mode requires non-empty apps[].",
196
+ fix_hint="Pass apps as a JSON array; each item must include app_key plus that resource's normal payload.",
197
+ details={"expected_shape": {"apps": [{"app_key": "APP_KEY"}]}},
198
+ )
199
+ )
200
+
201
+ app_results: list[JSONObject] = []
202
+ errors: list[JSONObject] = []
203
+ for index, item in enumerate(apps):
204
+ if not isinstance(item, dict):
205
+ error = {
206
+ "index": index,
207
+ "status": "failed",
208
+ "error_code": "APPS_FILE_ITEM_INVALID",
209
+ "message": "apps[] item must be an object",
210
+ "reason_path": f"apps[{index}]",
211
+ }
212
+ errors.append(error)
213
+ app_results.append(error)
214
+ continue
215
+ app_key = str(item.get("app_key") or item.get("appKey") or "").strip()
216
+ if not app_key:
217
+ error = {
218
+ "index": index,
219
+ "status": "failed",
220
+ "error_code": "APPS_FILE_APP_KEY_REQUIRED",
221
+ "message": "apps[] item requires app_key",
222
+ "reason_path": f"apps[{index}].app_key",
223
+ }
224
+ errors.append(error)
225
+ app_results.append(error)
226
+ continue
227
+ try:
228
+ item_started_at = time.perf_counter()
229
+ result = apply_one(index, item, app_key)
230
+ except (QingflowApiError, RuntimeError) as error:
231
+ batch_item_apply_ms += _elapsed_ms(item_started_at)
232
+ api_error = _coerce_api_error(error)
233
+ result = {
234
+ "status": "failed",
235
+ "error_code": f"{tool_name.upper()}_BATCH_ITEM_FAILED",
236
+ "recoverable": True,
237
+ "message": _public_error_message(f"{tool_name.upper()}_BATCH_ITEM_FAILED", api_error),
238
+ "app_key": app_key,
239
+ "normalized_args": {"app_key": app_key},
240
+ "details": {"transport_error": _transport_error_payload(api_error)},
241
+ "request_id": api_error.request_id,
242
+ "backend_code": api_error.backend_code,
243
+ "http_status": None if api_error.http_status == 404 else api_error.http_status,
244
+ }
245
+ else:
246
+ batch_item_apply_ms += _elapsed_ms(item_started_at)
247
+ if isinstance(result, dict):
248
+ child_breakdown = result.get("duration_breakdown_ms")
249
+ child_total = child_breakdown.get("total_ms") if isinstance(child_breakdown, dict) else None
250
+ if isinstance(child_total, (int, float)) and child_total >= 0:
251
+ child_total_ms += int(child_total)
252
+ status = str(result.get("status") or "success") if isinstance(result, dict) else "failed"
253
+ wrapped: JSONObject = {
254
+ "index": index,
255
+ "app_key": app_key,
256
+ "status": status,
257
+ "result": result if isinstance(result, dict) else {"status": "failed", "message": str(result)},
258
+ }
259
+ if status == "failed":
260
+ error = {
261
+ "index": index,
262
+ "app_key": app_key,
263
+ "status": status,
264
+ "error_code": wrapped["result"].get("error_code"),
265
+ "message": wrapped["result"].get("message"),
266
+ }
267
+ errors.append(error)
268
+ wrapped["error"] = error
269
+ app_results.append(wrapped)
270
+
271
+ partial = sum(1 for item in app_results if str(item.get("status") or "") == "partial_success")
272
+ succeeded = sum(1 for item in app_results if str(item.get("status") or "") in {"success", "partial_success"})
273
+ failed = len(app_results) - succeeded
274
+ status = "failed" if succeeded == 0 else "partial_success" if failed > 0 or partial > 0 else "success"
275
+ write_executed = any(
276
+ isinstance(item.get("result"), dict) and bool(item["result"].get("write_executed"))
277
+ for item in app_results
278
+ )
279
+ payload: JSONObject = {
280
+ "status": status,
281
+ "error_code": None if status == "success" else f"{tool_name.upper()}_BATCH_PARTIAL" if succeeded else f"{tool_name.upper()}_BATCH_FAILED",
282
+ "recoverable": failed > 0 or partial > 0,
283
+ "message": (
284
+ f"applied {tool_name} to {succeeded}/{len(app_results)} apps"
285
+ if status != "success"
286
+ else f"applied {tool_name} to {succeeded} apps"
287
+ ),
288
+ "normalized_args": {"apps": apps},
289
+ "apps": app_results,
290
+ "errors": errors,
291
+ "verification": {
292
+ "batch_verified": failed == 0 and partial == 0,
293
+ "succeeded": succeeded,
294
+ "partial": partial,
295
+ "failed": failed,
296
+ },
297
+ "write_executed": write_executed,
298
+ "write_succeeded": write_executed and failed == 0 and partial == 0,
299
+ "safe_to_retry": not write_executed,
300
+ }
301
+ return finish(payload)
302
+
303
+ def _read_app_batch(self, *, tool_name: str, profile: str, app_keys: list[str], read_one) -> JSONObject:
304
+ normalized_keys = [str(item).strip() for item in app_keys if str(item).strip()]
305
+ if not normalized_keys:
306
+ return _config_failure(
307
+ tool_name=tool_name,
308
+ message=f"{tool_name} batch mode requires non-empty app_keys[].",
309
+ fix_hint="Pass app_keys as a non-empty list of app keys.",
310
+ details={"expected_shape": {"app_keys": ["APP_A", "APP_B"]}},
311
+ )
312
+ apps: list[JSONObject] = []
313
+ errors: list[JSONObject] = []
314
+ for index, app_key in enumerate(normalized_keys):
315
+ try:
316
+ result = read_one(app_key)
317
+ except (QingflowApiError, RuntimeError) as error:
318
+ api_error = _coerce_api_error(error)
319
+ result = {
320
+ "status": "failed",
321
+ "error_code": f"{tool_name.upper()}_BATCH_ITEM_FAILED",
322
+ "recoverable": True,
323
+ "message": _public_error_message(f"{tool_name.upper()}_BATCH_ITEM_FAILED", api_error),
324
+ "app_key": app_key,
325
+ "details": {"transport_error": _transport_error_payload(api_error)},
326
+ "request_id": api_error.request_id,
327
+ "backend_code": api_error.backend_code,
328
+ "http_status": None if api_error.http_status == 404 else api_error.http_status,
329
+ }
330
+ status = str(result.get("status") or "success") if isinstance(result, dict) else "failed"
331
+ app_item: JSONObject = {
332
+ "index": index,
333
+ "app_key": app_key,
334
+ "status": status,
335
+ "data": result,
336
+ }
337
+ if status == "failed":
338
+ error = {
339
+ "index": index,
340
+ "app_key": app_key,
341
+ "status": "failed",
342
+ "error_code": result.get("error_code") if isinstance(result, dict) else None,
343
+ "message": result.get("message") if isinstance(result, dict) else str(result),
344
+ }
345
+ app_item["error"] = error
346
+ errors.append(error)
347
+ apps.append(app_item)
348
+ partial = sum(1 for item in apps if str(item.get("status") or "") == "partial_success")
349
+ succeeded = sum(1 for item in apps if str(item.get("status") or "") in {"success", "partial_success"})
350
+ failed = len(apps) - succeeded
351
+ status = "failed" if succeeded == 0 else "partial_success" if failed > 0 or partial > 0 else "success"
352
+ return {
353
+ "status": status,
354
+ "error_code": None if status == "success" else f"{tool_name.upper()}_BATCH_PARTIAL" if succeeded else f"{tool_name.upper()}_BATCH_FAILED",
355
+ "recoverable": failed > 0 or partial > 0,
356
+ "message": f"read {tool_name} for {succeeded}/{len(apps)} apps" if status != "success" else f"read {tool_name} for {succeeded} apps",
357
+ "normalized_args": {"app_keys": normalized_keys},
358
+ "app_keys": normalized_keys,
359
+ "apps": apps,
360
+ "errors": errors,
361
+ "verification": {"batch_verified": failed == 0 and partial == 0, "succeeded": succeeded, "partial": partial, "failed": failed},
362
+ }
363
+
119
364
  def register(self, mcp) -> None:
120
365
  """注册当前工具到 MCP 服务。"""
121
366
  @mcp.tool()
@@ -139,7 +384,6 @@ class AiBuilderTools(ToolBase):
139
384
  profile: str = DEFAULT_PROFILE,
140
385
  package_id: int | None = None,
141
386
  package_name: str | None = None,
142
- create_if_missing: bool = False,
143
387
  icon: str | JSONObject | None = None,
144
388
  color: str | None = None,
145
389
  icon_name: str | None = None,
@@ -153,7 +397,6 @@ class AiBuilderTools(ToolBase):
153
397
  profile=profile,
154
398
  package_id=package_id,
155
399
  package_name=package_name,
156
- create_if_missing=create_if_missing,
157
400
  icon=icon,
158
401
  color=color,
159
402
  icon_name=icon_name,
@@ -271,6 +514,7 @@ class AiBuilderTools(ToolBase):
271
514
  patch_buttons: list[JSONObject] | None = None,
272
515
  remove_buttons: list[JSONObject] | None = None,
273
516
  view_configs: list[JSONObject] | None = None,
517
+ apps: list[JSONObject] | None = None,
274
518
  ) -> JSONObject:
275
519
  return self.app_custom_buttons_apply(
276
520
  profile=profile,
@@ -279,6 +523,7 @@ class AiBuilderTools(ToolBase):
279
523
  patch_buttons=patch_buttons or [],
280
524
  remove_buttons=remove_buttons or [],
281
525
  view_configs=view_configs or [],
526
+ apps=apps,
282
527
  )
283
528
 
284
529
  @mcp.tool()
@@ -290,6 +535,7 @@ class AiBuilderTools(ToolBase):
290
535
  remove_associated_item_ids: list[int] | None = None,
291
536
  reorder_associated_item_ids: list[int] | None = None,
292
537
  view_configs: list[JSONObject] | None = None,
538
+ apps: list[JSONObject] | None = None,
293
539
  ) -> JSONObject:
294
540
  return self.app_associated_resources_apply(
295
541
  profile=profile,
@@ -299,15 +545,16 @@ class AiBuilderTools(ToolBase):
299
545
  remove_associated_item_ids=remove_associated_item_ids or [],
300
546
  reorder_associated_item_ids=reorder_associated_item_ids or [],
301
547
  view_configs=view_configs or [],
548
+ apps=apps,
302
549
  )
303
550
 
304
551
  @mcp.tool()
305
- def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
306
- return self.app_get(profile=profile, app_key=app_key)
552
+ def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
553
+ return self.app_get(profile=profile, app_key=app_key, app_keys=app_keys)
307
554
 
308
555
  @mcp.tool()
309
- def app_get_fields(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
310
- return self.app_get_fields(profile=profile, app_key=app_key)
556
+ def app_get_fields(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
557
+ return self.app_get_fields(profile=profile, app_key=app_key, app_keys=app_keys)
311
558
 
312
559
  @mcp.tool()
313
560
  def app_repair_code_blocks(
@@ -319,20 +566,28 @@ class AiBuilderTools(ToolBase):
319
566
  return self.app_repair_code_blocks(profile=profile, app_key=app_key, field=field, apply=apply)
320
567
 
321
568
  @mcp.tool()
322
- def app_get_layout(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
323
- return self.app_get_layout(profile=profile, app_key=app_key)
569
+ def app_get_layout(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
570
+ return self.app_get_layout(profile=profile, app_key=app_key, app_keys=app_keys)
571
+
572
+ @mcp.tool()
573
+ def app_get_views(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
574
+ return self.app_get_views(profile=profile, app_key=app_key, app_keys=app_keys)
324
575
 
325
576
  @mcp.tool()
326
- def app_get_views(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
327
- return self.app_get_views(profile=profile, app_key=app_key)
577
+ def app_get_flow(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
578
+ return self.app_get_flow(profile=profile, app_key=app_key, app_keys=app_keys)
328
579
 
329
580
  @mcp.tool()
330
- def app_get_flow(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
331
- return self.app_get_flow(profile=profile, app_key=app_key)
581
+ def app_get_charts(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
582
+ return self.app_get_charts(profile=profile, app_key=app_key, app_keys=app_keys)
332
583
 
333
584
  @mcp.tool()
334
- def app_get_charts(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
335
- return self.app_get_charts(profile=profile, app_key=app_key)
585
+ def app_get_buttons(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
586
+ return self.app_get_buttons(profile=profile, app_key=app_key, app_keys=app_keys)
587
+
588
+ @mcp.tool()
589
+ def app_get_associated_resources(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
590
+ return self.app_get_associated_resources(profile=profile, app_key=app_key, app_keys=app_keys)
336
591
 
337
592
  @mcp.tool()
338
593
  def portal_list(profile: str = DEFAULT_PROFILE) -> JSONObject:
@@ -370,19 +625,19 @@ class AiBuilderTools(ToolBase):
370
625
  icon_color: str | None = None,
371
626
  icon_config: JSONObject | None = None,
372
627
  visibility: JSONObject | None = None,
373
- create_if_missing: bool = False,
374
628
  publish: bool = True,
629
+ form: list[JSONObject] | None = None,
375
630
  add_fields: list[JSONObject] | None = None,
376
631
  update_fields: list[JSONObject] | None = None,
377
632
  remove_fields: list[JSONObject] | None = None,
378
633
  apps: list[JSONObject] | None = None,
379
634
  ) -> JSONObject:
380
635
  if apps is not None:
381
- if app_key or app_name or app_title or add_fields or update_fields or remove_fields:
636
+ if app_key or app_name or app_title or form or add_fields or update_fields or remove_fields:
382
637
  return _config_failure(
383
638
  tool_name="app_schema_apply",
384
- message="app_schema_apply multi-app mode accepts package_id/create_if_missing plus apps only.",
385
- fix_hint="Use `apps` for batch mode, or use the single-app arguments without `apps`.",
639
+ message="app_schema_apply multi-app mode accepts package_id plus apps only.",
640
+ fix_hint="Put per-app form in apps[].form when using batch mode.",
386
641
  )
387
642
  if package_id is None:
388
643
  return _config_failure(
@@ -394,9 +649,9 @@ class AiBuilderTools(ToolBase):
394
649
  profile=profile,
395
650
  package_id=package_id,
396
651
  visibility=visibility,
397
- create_if_missing=create_if_missing,
398
652
  publish=publish,
399
653
  apps=apps,
654
+ form=None,
400
655
  add_fields=[],
401
656
  update_fields=[],
402
657
  remove_fields=[],
@@ -406,18 +661,18 @@ class AiBuilderTools(ToolBase):
406
661
  has_app_title = bool((app_title or "").strip())
407
662
  has_package_id = package_id is not None
408
663
  if has_app_key:
409
- if create_if_missing or has_package_id:
664
+ if has_package_id:
410
665
  return _config_failure(
411
666
  tool_name="app_schema_apply",
412
667
  message="app_schema_apply edit mode accepts app_key and optional app_name rename only.",
413
- fix_hint="For existing apps, use `app_key` and optionally `app_name`. For create mode, use `package_id + app_name + create_if_missing=true`.",
668
+ fix_hint="For existing apps, use `app_key` and optionally `app_name`. For create mode, use `package_id + app_name`.",
414
669
  )
415
- elif not (create_if_missing and has_package_id and (has_app_name or has_app_title)):
670
+ elif not (has_package_id and (has_app_name or has_app_title)):
416
671
  return _config_failure(
417
672
  tool_name="app_schema_apply",
418
- message="app_schema_apply create mode requires package_id, app_name, and create_if_missing=true.",
419
- fix_hint="Use `app_key` for existing apps, or pass `package_id + app_name + create_if_missing=true` to create a new app.",
420
- )
673
+ message="app_schema_apply create mode requires package_id and app_name.",
674
+ fix_hint="Use `app_key` for existing apps, or pass `package_id + app_name` to create a new app.",
675
+ )
421
676
  return self.app_schema_apply(
422
677
  profile=profile,
423
678
  app_key=app_key,
@@ -430,12 +685,12 @@ class AiBuilderTools(ToolBase):
430
685
  icon_color=icon_color,
431
686
  icon_config=icon_config,
432
687
  visibility=visibility,
433
- create_if_missing=create_if_missing,
434
688
  publish=publish,
689
+ form=form or [],
435
690
  add_fields=add_fields or [],
436
691
  update_fields=update_fields or [],
437
692
  remove_fields=remove_fields or [],
438
- apps=[],
693
+ apps=None,
439
694
  )
440
695
 
441
696
  @mcp.tool()
@@ -445,8 +700,9 @@ class AiBuilderTools(ToolBase):
445
700
  mode: str = "merge",
446
701
  publish: bool = True,
447
702
  sections: list[JSONObject] | None = None,
703
+ apps: list[JSONObject] | None = None,
448
704
  ) -> JSONObject:
449
- return self.app_layout_apply(profile=profile, app_key=app_key, mode=mode, publish=publish, sections=sections or [])
705
+ return self.app_layout_apply(profile=profile, app_key=app_key, mode=mode, publish=publish, sections=sections or [], apps=apps)
450
706
 
451
707
  @mcp.tool()
452
708
  def app_flow_get(
@@ -454,7 +710,7 @@ class AiBuilderTools(ToolBase):
454
710
  app_key: str = "",
455
711
  version_id: str = "",
456
712
  ) -> JSONObject:
457
- return self.app_get_flow(profile=profile, app_key=app_key, version_id=version_id or None)
713
+ return self.app_flow_get(profile=profile, app_key=app_key, version_id=version_id or None)
458
714
 
459
715
  @mcp.tool()
460
716
  def app_flow_get_schema(profile: str = DEFAULT_PROFILE, schema_version: str = "") -> JSONObject:
@@ -468,14 +724,16 @@ class AiBuilderTools(ToolBase):
468
724
  spec: JSONObject | None = None,
469
725
  idempotency_key: str = "",
470
726
  schema_version: str = "",
727
+ patch_nodes: list[JSONObject] | None = None,
471
728
  ) -> JSONObject:
472
729
  return self.app_flow_apply(
473
730
  profile=profile,
474
731
  app_key=app_key,
475
732
  publish=publish,
476
- spec=spec or {},
733
+ spec=spec,
477
734
  idempotency_key=idempotency_key or None,
478
735
  schema_version=schema_version or None,
736
+ patch_nodes=patch_nodes,
479
737
  )
480
738
 
481
739
  @mcp.tool()
@@ -486,6 +744,7 @@ class AiBuilderTools(ToolBase):
486
744
  upsert_views: list[JSONObject] | None = None,
487
745
  patch_views: list[JSONObject] | None = None,
488
746
  remove_views: list[str] | None = None,
747
+ apps: list[JSONObject] | None = None,
489
748
  ) -> JSONObject:
490
749
  return self.app_views_apply(
491
750
  profile=profile,
@@ -494,6 +753,7 @@ class AiBuilderTools(ToolBase):
494
753
  upsert_views=upsert_views or [],
495
754
  patch_views=patch_views or [],
496
755
  remove_views=remove_views or [],
756
+ apps=apps,
497
757
  )
498
758
 
499
759
  @mcp.tool()
@@ -504,6 +764,7 @@ class AiBuilderTools(ToolBase):
504
764
  patch_charts: list[JSONObject] | None = None,
505
765
  remove_chart_ids: list[str] | None = None,
506
766
  reorder_chart_ids: list[str] | None = None,
767
+ apps: list[JSONObject] | None = None,
507
768
  ) -> JSONObject:
508
769
  return self.app_charts_apply(
509
770
  profile=profile,
@@ -512,6 +773,7 @@ class AiBuilderTools(ToolBase):
512
773
  patch_charts=patch_charts or [],
513
774
  remove_chart_ids=remove_chart_ids or [],
514
775
  reorder_chart_ids=reorder_chart_ids or [],
776
+ apps=apps,
515
777
  )
516
778
 
517
779
  @mcp.tool()
@@ -533,6 +795,7 @@ class AiBuilderTools(ToolBase):
533
795
  dash_global_config: JSONObject | None = None,
534
796
  config: JSONObject | None = None,
535
797
  payload: JSONObject | None = None,
798
+ patch_sections: list[JSONObject] | None = None,
536
799
  ) -> JSONObject:
537
800
  payload = payload if isinstance(payload, dict) else {}
538
801
  has_dash_key = bool((dash_key or "").strip())
@@ -570,6 +833,7 @@ class AiBuilderTools(ToolBase):
570
833
  dash_global_config=dash_global_config,
571
834
  config=config or {},
572
835
  payload=payload,
836
+ patch_sections=patch_sections,
573
837
  )
574
838
 
575
839
  @mcp.tool(description=self._high_risk_tool_description(operation="delete", target="portal"))
@@ -583,11 +847,13 @@ class AiBuilderTools(ToolBase):
583
847
  def app_publish_verify(
584
848
  profile: str = DEFAULT_PROFILE,
585
849
  app_key: str = "",
850
+ app_keys: list[str] | None = None,
586
851
  expected_package_id: int | None = None,
587
852
  ) -> JSONObject:
588
853
  return self.app_publish_verify(
589
854
  profile=profile,
590
855
  app_key=app_key,
856
+ app_keys=app_keys,
591
857
  expected_package_id=expected_package_id,
592
858
  )
593
859
 
@@ -759,7 +1025,6 @@ class AiBuilderTools(ToolBase):
759
1025
  profile: str,
760
1026
  package_id: int | None = None,
761
1027
  package_name: str | None = None,
762
- create_if_missing: bool = False,
763
1028
  icon: str | JSONObject | None = None,
764
1029
  color: str | None = None,
765
1030
  icon_name: str | None = None,
@@ -790,14 +1055,13 @@ class AiBuilderTools(ToolBase):
790
1055
  tool_name="package_apply",
791
1056
  icon=icon,
792
1057
  color=color,
793
- creating=package_id is None and bool(create_if_missing),
1058
+ creating=package_id is None and bool(str(package_name or "").strip()),
794
1059
  )
795
1060
  if icon_failure is not None:
796
1061
  return _attach_builder_apply_envelope("package_apply", icon_failure)
797
1062
  normalized_args = {
798
1063
  "package_id": package_id,
799
1064
  **({"package_name": package_name} if str(package_name or "").strip() else {}),
800
- "create_if_missing": bool(create_if_missing),
801
1065
  **({"icon": icon} if icon else {}),
802
1066
  **({"color": color} if color else {}),
803
1067
  **({"visibility": visibility_patch.model_dump(mode="json")} if visibility_patch is not None else {}),
@@ -809,7 +1073,6 @@ class AiBuilderTools(ToolBase):
809
1073
  profile=profile,
810
1074
  package_id=package_id,
811
1075
  package_name=package_name,
812
- create_if_missing=create_if_missing,
813
1076
  icon=icon,
814
1077
  color=color,
815
1078
  visibility=visibility_patch,
@@ -1063,8 +1326,24 @@ class AiBuilderTools(ToolBase):
1063
1326
  patch_buttons: list[JSONObject] | None = None,
1064
1327
  remove_buttons: list[JSONObject],
1065
1328
  view_configs: list[JSONObject] | None = None,
1329
+ apps: list[JSONObject] | None = None,
1066
1330
  ) -> JSONObject:
1067
1331
  """执行应用按钮 apply 逻辑。"""
1332
+ if apps is not None:
1333
+ return self._apply_app_batch(
1334
+ tool_name="app_custom_buttons_apply",
1335
+ profile=profile,
1336
+ apps=apps,
1337
+ apply_one=lambda _index, item, item_app_key: self.app_custom_buttons_apply(
1338
+ profile=profile,
1339
+ app_key=item_app_key,
1340
+ upsert_buttons=_payload_list(item, "upsert_buttons", "upsertButtons", "buttons", default=upsert_buttons or []),
1341
+ patch_buttons=_payload_list(item, "patch_buttons", "patchButtons", default=patch_buttons or []),
1342
+ remove_buttons=_payload_list(item, "remove_buttons", "removeButtons", default=remove_buttons or []),
1343
+ view_configs=_payload_list(item, "view_configs", "viewConfigs", default=view_configs or []),
1344
+ apps=None,
1345
+ ),
1346
+ )
1068
1347
  raw_request = {
1069
1348
  "app_key": app_key,
1070
1349
  "upsert_buttons": upsert_buttons,
@@ -1117,8 +1396,35 @@ class AiBuilderTools(ToolBase):
1117
1396
  reorder_associated_item_ids: list[int],
1118
1397
  view_configs: list[JSONObject],
1119
1398
  patch_resources: list[JSONObject] | None = None,
1399
+ apps: list[JSONObject] | None = None,
1120
1400
  ) -> JSONObject:
1121
1401
  """执行应用关联资源 apply 逻辑。"""
1402
+ if apps is not None:
1403
+ return self._apply_app_batch(
1404
+ tool_name="app_associated_resources_apply",
1405
+ profile=profile,
1406
+ apps=apps,
1407
+ apply_one=lambda _index, item, item_app_key: self.app_associated_resources_apply(
1408
+ profile=profile,
1409
+ app_key=item_app_key,
1410
+ upsert_resources=_payload_list(item, "upsert_resources", "upsertResources", "resources", default=upsert_resources or []),
1411
+ patch_resources=_payload_list(item, "patch_resources", "patchResources", default=patch_resources or []),
1412
+ remove_associated_item_ids=_payload_list(
1413
+ item,
1414
+ "remove_associated_item_ids",
1415
+ "removeAssociatedItemIds",
1416
+ default=remove_associated_item_ids or [],
1417
+ ),
1418
+ reorder_associated_item_ids=_payload_list(
1419
+ item,
1420
+ "reorder_associated_item_ids",
1421
+ "reorderAssociatedItemIds",
1422
+ default=reorder_associated_item_ids or [],
1423
+ ),
1424
+ view_configs=_payload_list(item, "view_configs", "viewConfigs", default=view_configs or []),
1425
+ apps=None,
1426
+ ),
1427
+ )
1122
1428
  raw_request = {
1123
1429
  "app_key": app_key,
1124
1430
  "upsert_resources": upsert_resources,
@@ -1281,8 +1587,15 @@ class AiBuilderTools(ToolBase):
1281
1587
  ))
1282
1588
 
1283
1589
  @tool_cn_name("应用详情查询")
1284
- def app_get(self, *, profile: str, app_key: str) -> JSONObject:
1590
+ def app_get(self, *, profile: str, app_key: str, app_keys: list[str] | None = None) -> JSONObject:
1285
1591
  """执行应用相关逻辑。"""
1592
+ if app_keys is not None:
1593
+ return self._read_app_batch(
1594
+ tool_name="app_get",
1595
+ profile=profile,
1596
+ app_keys=app_keys,
1597
+ read_one=lambda item_app_key: self.app_get(profile=profile, app_key=item_app_key, app_keys=None),
1598
+ )
1286
1599
  normalized_args = {"app_key": app_key}
1287
1600
  return _publicize_package_fields(_safe_tool_call(
1288
1601
  lambda: self._facade.app_get(profile=profile, app_key=app_key),
@@ -1303,8 +1616,15 @@ class AiBuilderTools(ToolBase):
1303
1616
  )
1304
1617
 
1305
1618
  @tool_cn_name("应用字段详情查询")
1306
- def app_get_fields(self, *, profile: str, app_key: str) -> JSONObject:
1619
+ def app_get_fields(self, *, profile: str, app_key: str, app_keys: list[str] | None = None) -> JSONObject:
1307
1620
  """执行应用相关逻辑。"""
1621
+ if app_keys is not None:
1622
+ return self._read_app_batch(
1623
+ tool_name="app_get_fields",
1624
+ profile=profile,
1625
+ app_keys=app_keys,
1626
+ read_one=lambda item_app_key: self.app_get_fields(profile=profile, app_key=item_app_key, app_keys=None),
1627
+ )
1308
1628
  normalized_args = {"app_key": app_key}
1309
1629
  return _safe_tool_call(
1310
1630
  lambda: self._facade.app_get_fields(profile=profile, app_key=app_key),
@@ -1343,8 +1663,15 @@ class AiBuilderTools(ToolBase):
1343
1663
  )
1344
1664
 
1345
1665
  @tool_cn_name("应用布局详情查询")
1346
- def app_get_layout(self, *, profile: str, app_key: str) -> JSONObject:
1666
+ def app_get_layout(self, *, profile: str, app_key: str, app_keys: list[str] | None = None) -> JSONObject:
1347
1667
  """执行应用相关逻辑。"""
1668
+ if app_keys is not None:
1669
+ return self._read_app_batch(
1670
+ tool_name="app_get_layout",
1671
+ profile=profile,
1672
+ app_keys=app_keys,
1673
+ read_one=lambda item_app_key: self.app_get_layout(profile=profile, app_key=item_app_key, app_keys=None),
1674
+ )
1348
1675
  normalized_args = {"app_key": app_key}
1349
1676
  return _safe_tool_call(
1350
1677
  lambda: self._facade.app_get_layout(profile=profile, app_key=app_key),
@@ -1365,8 +1692,15 @@ class AiBuilderTools(ToolBase):
1365
1692
  )
1366
1693
 
1367
1694
  @tool_cn_name("应用视图详情查询")
1368
- def app_get_views(self, *, profile: str, app_key: str) -> JSONObject:
1695
+ def app_get_views(self, *, profile: str, app_key: str, app_keys: list[str] | None = None) -> JSONObject:
1369
1696
  """执行应用相关逻辑。"""
1697
+ if app_keys is not None:
1698
+ return self._read_app_batch(
1699
+ tool_name="app_get_views",
1700
+ profile=profile,
1701
+ app_keys=app_keys,
1702
+ read_one=lambda item_app_key: self.app_get_views(profile=profile, app_key=item_app_key, app_keys=None),
1703
+ )
1370
1704
  normalized_args = {"app_key": app_key}
1371
1705
  return _safe_tool_call(
1372
1706
  lambda: self._facade.app_get_views(profile=profile, app_key=app_key),
@@ -1375,25 +1709,53 @@ class AiBuilderTools(ToolBase):
1375
1709
  suggested_next_call={"tool_name": "app_get_views", "arguments": {"profile": profile, "app_key": app_key}},
1376
1710
  )
1377
1711
 
1378
- @tool_cn_name("Workflow Spec Schema")
1379
- def app_flow_get_schema(self, *, profile: str, schema_version: str | None = None) -> JSONObject:
1380
- normalized_args = {"schema_version": schema_version}
1712
+ @tool_cn_name("应用流程摘要读取")
1713
+ def app_read_flow_summary(self, *, profile: str, app_key: str) -> JSONObject:
1714
+ """执行应用相关逻辑。"""
1715
+ normalized_args = {"app_key": app_key}
1381
1716
  return _safe_tool_call(
1382
- lambda: self._facade.flow_get_schema(profile=profile, schema_version=schema_version),
1383
- error_code="FLOW_SPEC_SCHEMA_FAILED",
1717
+ lambda: self._facade.app_read_flow_summary(profile=profile, app_key=app_key),
1718
+ error_code="FLOW_READ_FAILED",
1384
1719
  normalized_args=normalized_args,
1385
- suggested_next_call={"tool_name": "app_flow_get_schema", "arguments": {"profile": profile, **normalized_args}},
1720
+ suggested_next_call={"tool_name": "app_get_flow", "arguments": {"profile": profile, "app_key": app_key}},
1386
1721
  )
1387
1722
 
1388
- @tool_cn_name("Workflow Spec 读取")
1389
- def app_get_flow(self, *, profile: str, app_key: str, version_id: str | None = None) -> JSONObject:
1723
+ @tool_cn_name("应用流程详情查询")
1724
+ def app_get_flow(self, *, profile: str, app_key: str, app_keys: list[str] | None = None) -> JSONObject:
1390
1725
  """执行应用相关逻辑。"""
1726
+ if app_keys is not None:
1727
+ return self._read_app_batch(
1728
+ tool_name="app_get_flow",
1729
+ profile=profile,
1730
+ app_keys=app_keys,
1731
+ read_one=lambda item_app_key: self.app_get_flow(profile=profile, app_key=item_app_key, app_keys=None),
1732
+ )
1733
+ normalized_args = {"app_key": app_key}
1734
+ return _safe_tool_call(
1735
+ lambda: self._facade.app_get_flow(profile=profile, app_key=app_key),
1736
+ error_code="APP_GET_FLOW_FAILED",
1737
+ normalized_args=normalized_args,
1738
+ suggested_next_call={"tool_name": "app_get_flow", "arguments": {"profile": profile, "app_key": app_key}},
1739
+ )
1740
+
1741
+ @tool_cn_name("应用流程规格查询")
1742
+ def app_flow_get(self, *, profile: str, app_key: str, version_id: str | None = None) -> JSONObject:
1391
1743
  normalized_args = {"app_key": app_key, "version_id": version_id}
1392
1744
  return _safe_tool_call(
1393
1745
  lambda: self._facade.flow_get(profile=profile, app_key=app_key, version_id=version_id),
1394
- error_code="APP_GET_FLOW_FAILED",
1746
+ error_code="APP_FLOW_GET_FAILED",
1747
+ normalized_args=normalized_args,
1748
+ suggested_next_call={"tool_name": "app_flow_get", "arguments": {"profile": profile, **normalized_args}},
1749
+ )
1750
+
1751
+ @tool_cn_name("应用流程规格 Schema 查询")
1752
+ def app_flow_get_schema(self, *, profile: str, schema_version: str | None = None) -> JSONObject:
1753
+ normalized_args = {"schema_version": schema_version}
1754
+ return _safe_tool_call(
1755
+ lambda: self._facade.flow_get_schema(profile=profile, schema_version=schema_version),
1756
+ error_code="APP_FLOW_SCHEMA_GET_FAILED",
1395
1757
  normalized_args=normalized_args,
1396
- suggested_next_call={"tool_name": "app_flow_get", "arguments": {"profile": profile, "app_key": app_key}},
1758
+ suggested_next_call={"tool_name": "app_flow_get_schema", "arguments": {"profile": profile, **normalized_args}},
1397
1759
  )
1398
1760
 
1399
1761
  @tool_cn_name("应用图表摘要读取")
@@ -1408,8 +1770,15 @@ class AiBuilderTools(ToolBase):
1408
1770
  )
1409
1771
 
1410
1772
  @tool_cn_name("应用图表详情查询")
1411
- def app_get_charts(self, *, profile: str, app_key: str) -> JSONObject:
1773
+ def app_get_charts(self, *, profile: str, app_key: str, app_keys: list[str] | None = None) -> JSONObject:
1412
1774
  """执行应用相关逻辑。"""
1775
+ if app_keys is not None:
1776
+ return self._read_app_batch(
1777
+ tool_name="app_get_charts",
1778
+ profile=profile,
1779
+ app_keys=app_keys,
1780
+ read_one=lambda item_app_key: self.app_get_charts(profile=profile, app_key=item_app_key, app_keys=None),
1781
+ )
1413
1782
  normalized_args = {"app_key": app_key}
1414
1783
  return _safe_tool_call(
1415
1784
  lambda: self._facade.app_get_charts(profile=profile, app_key=app_key),
@@ -1418,6 +1787,40 @@ class AiBuilderTools(ToolBase):
1418
1787
  suggested_next_call={"tool_name": "app_get_charts", "arguments": {"profile": profile, "app_key": app_key}},
1419
1788
  )
1420
1789
 
1790
+ @tool_cn_name("自定义按钮读取")
1791
+ def app_get_buttons(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
1792
+ if app_keys is not None:
1793
+ return self._read_app_batch(
1794
+ tool_name="app_get_buttons",
1795
+ profile=profile,
1796
+ app_keys=app_keys,
1797
+ read_one=lambda item_app_key: self.app_get_buttons(profile=profile, app_key=item_app_key, app_keys=None),
1798
+ )
1799
+ normalized_args = {"app_key": app_key}
1800
+ return _safe_tool_call(
1801
+ lambda: self._facade.app_get_buttons(profile=profile, app_key=app_key),
1802
+ error_code="APP_GET_BUTTONS_FAILED",
1803
+ normalized_args=normalized_args,
1804
+ suggested_next_call={"tool_name": "app_get_buttons", "arguments": {"profile": profile, "app_key": app_key}},
1805
+ )
1806
+
1807
+ @tool_cn_name("关联资源读取")
1808
+ def app_get_associated_resources(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
1809
+ if app_keys is not None:
1810
+ return self._read_app_batch(
1811
+ tool_name="app_get_associated_resources",
1812
+ profile=profile,
1813
+ app_keys=app_keys,
1814
+ read_one=lambda item_app_key: self.app_get_associated_resources(profile=profile, app_key=item_app_key, app_keys=None),
1815
+ )
1816
+ normalized_args = {"app_key": app_key}
1817
+ return _safe_tool_call(
1818
+ lambda: self._facade.app_get_associated_resources(profile=profile, app_key=app_key),
1819
+ error_code="APP_GET_ASSOCIATED_RESOURCES_FAILED",
1820
+ normalized_args=normalized_args,
1821
+ suggested_next_call={"tool_name": "app_get_associated_resources", "arguments": {"profile": profile, "app_key": app_key}},
1822
+ )
1823
+
1421
1824
  @tool_cn_name("门户列表查询")
1422
1825
  def portal_list(self, *, profile: str) -> JSONObject:
1423
1826
  """执行门户相关逻辑。"""
@@ -1489,7 +1892,6 @@ class AiBuilderTools(ToolBase):
1489
1892
  icon: str = "",
1490
1893
  color: str = "",
1491
1894
  visibility: JSONObject | None = None,
1492
- create_if_missing: bool = False,
1493
1895
  add_fields: list[JSONObject],
1494
1896
  update_fields: list[JSONObject],
1495
1897
  remove_fields: list[JSONObject],
@@ -1504,7 +1906,6 @@ class AiBuilderTools(ToolBase):
1504
1906
  icon=icon,
1505
1907
  color=color,
1506
1908
  visibility=visibility,
1507
- create_if_missing=create_if_missing,
1508
1909
  add_fields=add_fields,
1509
1910
  update_fields=update_fields,
1510
1911
  remove_fields=remove_fields,
@@ -1520,7 +1921,6 @@ class AiBuilderTools(ToolBase):
1520
1921
  "icon": icon,
1521
1922
  "color": color,
1522
1923
  "visibility": visibility,
1523
- "create_if_missing": create_if_missing,
1524
1924
  "add_fields": add_fields,
1525
1925
  "update_fields": update_fields,
1526
1926
  "remove_fields": remove_fields,
@@ -1541,7 +1941,6 @@ class AiBuilderTools(ToolBase):
1541
1941
  "icon": icon,
1542
1942
  "color": color,
1543
1943
  "visibility": visibility,
1544
- "create_if_missing": create_if_missing,
1545
1944
  "add_fields": [{"name": "字段名称", "type": "text"}],
1546
1945
  "update_fields": [],
1547
1946
  "remove_fields": [],
@@ -1604,6 +2003,52 @@ class AiBuilderTools(ToolBase):
1604
2003
  suggested_next_call={"tool_name": "app_layout_plan", "arguments": {"profile": profile, **normalized_request}},
1605
2004
  )
1606
2005
 
2006
+ @tool_cn_name("应用流程规划")
2007
+ def app_flow_plan(
2008
+ self,
2009
+ *,
2010
+ profile: str,
2011
+ app_key: str,
2012
+ mode: str = "replace",
2013
+ nodes: list[JSONObject] | None = None,
2014
+ transitions: list[JSONObject] | None = None,
2015
+ preset: str | None = None,
2016
+ ) -> JSONObject:
2017
+ """执行应用相关逻辑。"""
2018
+ try:
2019
+ request = FlowPlanRequest.model_validate(
2020
+ {
2021
+ "app_key": app_key,
2022
+ "mode": mode,
2023
+ "nodes": nodes or [],
2024
+ "transitions": transitions or [],
2025
+ "preset": preset,
2026
+ }
2027
+ )
2028
+ except ValidationError as exc:
2029
+ return _validation_failure(
2030
+ str(exc),
2031
+ tool_name="app_flow_plan",
2032
+ exc=exc,
2033
+ suggested_next_call={
2034
+ "tool_name": "app_flow_plan",
2035
+ "arguments": {
2036
+ "profile": profile,
2037
+ "app_key": app_key,
2038
+ "mode": "replace",
2039
+ "preset": "basic_approval",
2040
+ "nodes": [],
2041
+ "transitions": [],
2042
+ },
2043
+ },
2044
+ )
2045
+ return _safe_tool_call(
2046
+ lambda: self._facade.app_flow_plan(profile=profile, request=request),
2047
+ error_code="FLOW_PLAN_FAILED",
2048
+ normalized_args=request.model_dump(mode="json"),
2049
+ suggested_next_call={"tool_name": "app_flow_plan", "arguments": {"profile": profile, **request.model_dump(mode="json")}},
2050
+ )
2051
+
1607
2052
  @tool_cn_name("应用视图规划")
1608
2053
  def app_views_plan(
1609
2054
  self,
@@ -1611,6 +2056,7 @@ class AiBuilderTools(ToolBase):
1611
2056
  profile: str,
1612
2057
  app_key: str,
1613
2058
  upsert_views: list[JSONObject] | None = None,
2059
+ patch_views: list[JSONObject] | None = None,
1614
2060
  remove_views: list[str] | None = None,
1615
2061
  preset: str | None = None,
1616
2062
  ) -> JSONObject:
@@ -1629,6 +2075,7 @@ class AiBuilderTools(ToolBase):
1629
2075
  {
1630
2076
  "app_key": app_key,
1631
2077
  "upsert_views": upsert_views or [],
2078
+ "patch_views": patch_views or [],
1632
2079
  "remove_views": remove_views or [],
1633
2080
  "preset": preset,
1634
2081
  }
@@ -1645,6 +2092,7 @@ class AiBuilderTools(ToolBase):
1645
2092
  "app_key": app_key,
1646
2093
  "preset": "default_table",
1647
2094
  "upsert_views": [],
2095
+ "patch_views": [],
1648
2096
  "remove_views": [],
1649
2097
  },
1650
2098
  },
@@ -1652,8 +2100,24 @@ class AiBuilderTools(ToolBase):
1652
2100
  return _safe_tool_call(
1653
2101
  lambda: self._facade.app_views_plan(profile=profile, request=request),
1654
2102
  error_code="VIEWS_PLAN_FAILED",
1655
- normalized_args=request.model_dump(mode="json"),
1656
- suggested_next_call={"tool_name": "app_views_plan", "arguments": {"profile": profile, **request.model_dump(mode="json")}},
2103
+ normalized_args={
2104
+ "app_key": request.app_key,
2105
+ "upsert_views": [public_view_upsert_payload(view) for view in request.upsert_views],
2106
+ "patch_views": [public_view_partial_payload(patch) for patch in request.patch_views],
2107
+ "remove_views": list(request.remove_views),
2108
+ "preset": request.preset.value if request.preset is not None else None,
2109
+ },
2110
+ suggested_next_call={
2111
+ "tool_name": "app_views_plan",
2112
+ "arguments": {
2113
+ "profile": profile,
2114
+ "app_key": request.app_key,
2115
+ "upsert_views": [public_view_upsert_payload(view) for view in request.upsert_views],
2116
+ "patch_views": [public_view_partial_payload(patch) for patch in request.patch_views],
2117
+ "remove_views": list(request.remove_views),
2118
+ "preset": request.preset.value if request.preset is not None else None,
2119
+ },
2120
+ },
1657
2121
  )
1658
2122
 
1659
2123
  @tool_cn_name("应用结构应用")
@@ -1671,8 +2135,8 @@ class AiBuilderTools(ToolBase):
1671
2135
  icon_color: str | None = None,
1672
2136
  icon_config: JSONObject | None = None,
1673
2137
  visibility: JSONObject | None = None,
1674
- create_if_missing: bool = False,
1675
2138
  publish: bool = True,
2139
+ form: list[JSONObject] | None = None,
1676
2140
  add_fields: list[JSONObject],
1677
2141
  update_fields: list[JSONObject],
1678
2142
  remove_fields: list[JSONObject],
@@ -1698,11 +2162,25 @@ class AiBuilderTools(ToolBase):
1698
2162
  package_id = normalized_apps_payload.get("package_id") # type: ignore[assignment]
1699
2163
  apps = normalized_apps_payload.get("apps") # type: ignore[assignment]
1700
2164
  input_warnings = list(normalized_apps_payload.get("warnings") or [])
2165
+ normalized_apps: list[JSONObject] = []
2166
+ for index, app_item in enumerate(apps or []):
2167
+ if not isinstance(app_item, dict):
2168
+ normalized_apps.append(app_item)
2169
+ continue
2170
+ form_result = _apply_schema_form_to_payload(
2171
+ tool_name="app_schema_apply",
2172
+ payload=app_item,
2173
+ path=f"apps[{index}]",
2174
+ )
2175
+ failure = form_result.get("failure")
2176
+ if isinstance(failure, dict):
2177
+ return _attach_builder_apply_envelope("app_schema_apply", failure)
2178
+ normalized_apps.append(form_result.get("payload") or app_item)
2179
+ apps = normalized_apps
1701
2180
  result = self._app_schema_apply_multi(
1702
2181
  profile=profile,
1703
2182
  package_id=package_id,
1704
2183
  visibility=visibility,
1705
- create_if_missing=create_if_missing,
1706
2184
  publish=publish,
1707
2185
  apps=apps,
1708
2186
  )
@@ -1711,6 +2189,22 @@ class AiBuilderTools(ToolBase):
1711
2189
  result_warnings.extend(deepcopy(input_warnings))
1712
2190
  result["warnings"] = result_warnings
1713
2191
  return _attach_builder_apply_envelope("app_schema_apply", result)
2192
+ inline_form_layout_sections: list[JSONObject] = []
2193
+ if form:
2194
+ form_payload: JSONObject = {"form": form}
2195
+ if add_fields:
2196
+ form_payload["add_fields"] = add_fields
2197
+ form_result = _apply_schema_form_to_payload(
2198
+ tool_name="app_schema_apply",
2199
+ payload=form_payload,
2200
+ path="schema",
2201
+ )
2202
+ failure = form_result.get("failure")
2203
+ if isinstance(failure, dict):
2204
+ return _attach_builder_apply_envelope("app_schema_apply", failure)
2205
+ normalized_payload = form_result.get("payload") if isinstance(form_result.get("payload"), dict) else {}
2206
+ add_fields = list(normalized_payload.get("add_fields") or [])
2207
+ inline_form_layout_sections = list(form_result.get("layout_sections") or [])
1714
2208
  result = self._app_schema_apply_once(
1715
2209
  profile=profile,
1716
2210
  app_key=app_key,
@@ -1720,11 +2214,11 @@ class AiBuilderTools(ToolBase):
1720
2214
  icon=icon,
1721
2215
  color=color,
1722
2216
  visibility=visibility,
1723
- create_if_missing=create_if_missing,
1724
2217
  publish=publish,
1725
2218
  add_fields=add_fields,
1726
2219
  update_fields=update_fields,
1727
2220
  remove_fields=remove_fields,
2221
+ inline_form_layout_sections=inline_form_layout_sections,
1728
2222
  )
1729
2223
  result = self._retry_after_self_lock_release(
1730
2224
  profile=profile,
@@ -1738,11 +2232,11 @@ class AiBuilderTools(ToolBase):
1738
2232
  icon=icon,
1739
2233
  color=color,
1740
2234
  visibility=visibility,
1741
- create_if_missing=create_if_missing,
1742
2235
  publish=publish,
1743
2236
  add_fields=add_fields,
1744
2237
  update_fields=update_fields,
1745
2238
  remove_fields=remove_fields,
2239
+ inline_form_layout_sections=inline_form_layout_sections,
1746
2240
  ),
1747
2241
  )
1748
2242
  return _attach_builder_apply_envelope("app_schema_apply", result)
@@ -1753,52 +2247,116 @@ class AiBuilderTools(ToolBase):
1753
2247
  profile: str,
1754
2248
  package_id: int | None,
1755
2249
  visibility: JSONObject | None,
1756
- create_if_missing: bool,
1757
2250
  publish: bool,
1758
2251
  apps: list[JSONObject],
1759
2252
  ) -> JSONObject:
2253
+ started_at = time.perf_counter()
2254
+ validation_ms = 0
2255
+ app_shell_apply_ms = 0
2256
+ relation_compile_ms = 0
2257
+ app_field_apply_ms = 0
2258
+ relation_repair_ms = 0
2259
+ inline_layout_fallback_ms = 0
2260
+ child_schema_write_ms = 0
2261
+ child_inline_layout_ms = 0
2262
+ child_publish_ms = 0
2263
+ child_readback_ms = 0
1760
2264
  normalized_args: JSONObject = {
1761
2265
  "package_id": package_id,
1762
- "create_if_missing": create_if_missing,
1763
2266
  "publish": publish,
1764
2267
  "apps": deepcopy(apps),
1765
2268
  }
1766
2269
  if visibility is not None:
1767
2270
  normalized_args["visibility"] = deepcopy(visibility)
2271
+ parallelism: JSONObject = {"shell_workers": 0, "schema_workers": 0}
2272
+
2273
+ def merge_child_duration(child: JSONObject) -> None:
2274
+ nonlocal child_schema_write_ms
2275
+ nonlocal child_inline_layout_ms
2276
+ nonlocal child_publish_ms
2277
+ nonlocal child_readback_ms
2278
+ breakdown = child.get("duration_breakdown_ms") if isinstance(child, dict) else None
2279
+ if not isinstance(breakdown, dict):
2280
+ return
2281
+ for key, target in (
2282
+ ("schema_write_ms", "child_schema_write_ms"),
2283
+ ("inline_layout_ms", "child_inline_layout_ms"),
2284
+ ("publish_ms", "child_publish_ms"),
2285
+ ("readback_ms", "child_readback_ms"),
2286
+ ):
2287
+ value = breakdown.get(key)
2288
+ if isinstance(value, (int, float)) and value >= 0:
2289
+ if target == "child_schema_write_ms":
2290
+ child_schema_write_ms += int(value)
2291
+ elif target == "child_inline_layout_ms":
2292
+ child_inline_layout_ms += int(value)
2293
+ elif target == "child_publish_ms":
2294
+ child_publish_ms += int(value)
2295
+ elif target == "child_readback_ms":
2296
+ child_readback_ms += int(value)
2297
+
2298
+ def finish(response: JSONObject) -> JSONObject:
2299
+ _merge_duration_breakdown(
2300
+ response,
2301
+ multi_app_validation_ms=validation_ms,
2302
+ app_shell_apply_ms=app_shell_apply_ms,
2303
+ app_shell_parallel_wall_ms=app_shell_apply_ms,
2304
+ relation_compile_ms=relation_compile_ms,
2305
+ app_field_apply_ms=app_field_apply_ms,
2306
+ app_schema_parallel_wall_ms=app_field_apply_ms,
2307
+ relation_repair_parallel_wall_ms=relation_repair_ms,
2308
+ native_inline_layout_ms=child_inline_layout_ms,
2309
+ inline_layout_fallback_ms=inline_layout_fallback_ms,
2310
+ child_schema_write_ms=child_schema_write_ms,
2311
+ child_inline_layout_ms=child_inline_layout_ms,
2312
+ child_publish_ms=child_publish_ms,
2313
+ child_readback_ms=child_readback_ms,
2314
+ total_ms=_elapsed_ms(started_at),
2315
+ )
2316
+ return response
2317
+
2318
+ validation_started_at = time.perf_counter()
1768
2319
  system_field_failure = _reserved_system_field_name_failure_for_apps(
1769
2320
  tool_name="app_schema_apply",
1770
2321
  profile=profile,
1771
2322
  package_id=package_id,
1772
2323
  visibility=visibility,
1773
- create_if_missing=create_if_missing,
1774
2324
  publish=publish,
1775
2325
  apps=apps,
1776
2326
  )
1777
2327
  if system_field_failure is not None:
1778
- return system_field_failure
2328
+ validation_ms += _elapsed_ms(validation_started_at)
2329
+ return finish(system_field_failure)
1779
2330
  if package_id is None:
1780
- return _config_failure(
1781
- tool_name="app_schema_apply",
1782
- message="app_schema_apply multi-app mode requires package_id.",
1783
- fix_hint="Pass package_id and apps[].app_name for new apps, or apps[].app_key for existing apps.",
2331
+ validation_ms += _elapsed_ms(validation_started_at)
2332
+ return finish(
2333
+ _config_failure(
2334
+ tool_name="app_schema_apply",
2335
+ message="app_schema_apply multi-app mode requires package_id.",
2336
+ fix_hint="Pass package_id and apps[].app_name for new apps, or apps[].app_key for existing apps.",
2337
+ )
1784
2338
  )
1785
2339
  if not apps:
1786
- return _config_failure(
1787
- tool_name="app_schema_apply",
1788
- error_code="APPS_FILE_EMPTY",
1789
- message="app_schema_apply multi-app mode requires non-empty apps.",
1790
- fix_hint="Pass apps as a non-empty list of app schema items.",
2340
+ validation_ms += _elapsed_ms(validation_started_at)
2341
+ return finish(
2342
+ _config_failure(
2343
+ tool_name="app_schema_apply",
2344
+ error_code="APPS_FILE_EMPTY",
2345
+ message="app_schema_apply multi-app mode requires non-empty apps.",
2346
+ fix_hint="Pass apps as a non-empty list of app schema items.",
2347
+ )
1791
2348
  )
1792
2349
  shape_failure = _schema_apps_shape_failure(tool_name="app_schema_apply", apps=apps)
1793
2350
  if shape_failure is not None:
1794
- return shape_failure
2351
+ validation_ms += _elapsed_ms(validation_started_at)
2352
+ return finish(shape_failure)
1795
2353
  static_validation_failure = _multi_app_static_validation_failure(
1796
2354
  tool_name="app_schema_apply",
1797
2355
  apps=apps,
1798
- create_if_missing=create_if_missing,
1799
2356
  )
1800
2357
  if static_validation_failure is not None:
1801
- return static_validation_failure
2358
+ validation_ms += _elapsed_ms(validation_started_at)
2359
+ return finish(static_validation_failure)
1802
2360
  icon_errors: list[JSONObject] = []
1803
2361
  seen_new_app_icons: dict[str, int] = {}
1804
2362
  for index, raw_item in enumerate(apps):
@@ -1849,32 +2407,111 @@ class AiBuilderTools(ToolBase):
1849
2407
  else:
1850
2408
  seen_new_app_icons[normalized_icon] = index
1851
2409
  if icon_errors:
1852
- return _config_failure(
1853
- tool_name="app_schema_apply",
1854
- error_code="WORKSPACE_ICON_BATCH_INVALID",
1855
- message="one or more apps have invalid workspace icon configuration",
1856
- fix_hint="Call `qingflow --json builder icon catalog`, choose a distinct non-template icon and color for each new app, then retry.",
1857
- details={"icon_errors": icon_errors},
1858
- allowed_values={
1859
- "workspace_icon.icon_names": list(WORKSPACE_ICON_NAMES),
1860
- "workspace_icon.icon_colors": list(WORKSPACE_ICON_COLORS),
1861
- "workspace_icon.generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
1862
- },
2410
+ validation_ms += _elapsed_ms(validation_started_at)
2411
+ return finish(
2412
+ _config_failure(
2413
+ tool_name="app_schema_apply",
2414
+ error_code="WORKSPACE_ICON_BATCH_INVALID",
2415
+ message="one or more apps have invalid workspace icon configuration",
2416
+ fix_hint="Call `qingflow --json builder icon catalog`, choose a distinct non-template icon and color for each new app, then retry.",
2417
+ details={"icon_errors": icon_errors},
2418
+ allowed_values={
2419
+ "workspace_icon.icon_names": list(WORKSPACE_ICON_NAMES),
2420
+ "workspace_icon.icon_colors": list(WORKSPACE_ICON_COLORS),
2421
+ "workspace_icon.generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
2422
+ },
2423
+ )
1863
2424
  )
2425
+ validation_ms += _elapsed_ms(validation_started_at)
1864
2426
 
1865
2427
  client_key_to_app_key: dict[str, str] = {}
1866
2428
  app_name_to_app_key: dict[str, str] = {}
1867
2429
  created_app_keys: list[str] = []
1868
- results: list[JSONObject] = []
2430
+ results_by_index: list[JSONObject | None] = [None] * len(apps)
2431
+ shell_jobs: list[JSONObject] = []
1869
2432
  any_write_executed = False
1870
2433
  any_write_may_have_succeeded = False
1871
2434
  pending_readback_app_keys: list[str] = []
1872
2435
  pending_readback_app_names: list[str] = []
1873
2436
  client_keys: set[str] = set()
2437
+ client_key_indexes: dict[str, int] = {}
2438
+ app_name_indexes: dict[str, int] = {}
2439
+
2440
+ def worker_count(count: int) -> int:
2441
+ return min(MULTI_APP_SCHEMA_APPLY_PARALLEL_LIMIT, count) if count > 0 else 0
2442
+
2443
+ def mark_inline_layout_not_applied_in_schema(
2444
+ result: JSONObject,
2445
+ *,
2446
+ app_key: str,
2447
+ sections: list[JSONObject],
2448
+ ) -> JSONObject:
2449
+ marked = deepcopy(result)
2450
+ inline_payload: JSONObject = {
2451
+ "requested": True,
2452
+ "section_count": len(sections),
2453
+ "app_key": app_key or None,
2454
+ "applied": False,
2455
+ "applied_in_schema": False,
2456
+ "error_code": "INLINE_FORM_LAYOUT_NOT_APPLIED_IN_SCHEMA",
2457
+ "message": "schema save did not apply inline form layout; multi-app mode does not run app_layout_apply fallback",
2458
+ }
2459
+ marked["inline_form_layout"] = inline_payload
2460
+ verification = marked.get("verification") if isinstance(marked.get("verification"), dict) else {}
2461
+ verification = deepcopy(verification)
2462
+ verification["inline_form_layout_verified"] = False
2463
+ marked["verification"] = verification
2464
+ if marked.get("status") in {"success", "partial_success"}:
2465
+ marked["status"] = "partial_success"
2466
+ marked["error_code"] = marked.get("error_code") or "INLINE_FORM_LAYOUT_NOT_APPLIED_IN_SCHEMA"
2467
+ marked["message"] = "schema applied but inline form layout was not applied in schema save"
2468
+ marked["safe_to_retry"] = False
2469
+ return marked
2470
+
2471
+ def dependency_failure_for_item(index: int, item: JSONObject) -> JSONObject | None:
2472
+ blocked: list[JSONObject] = []
2473
+ for ref in _collect_multi_app_target_refs(item, path=f"apps[{index}]"):
2474
+ ref_value = str(ref.get("value") or "").strip()
2475
+ if not ref_value:
2476
+ continue
2477
+ target_index: int | None = None
2478
+ resolved = False
2479
+ if ref.get("kind") == "target_app_ref":
2480
+ target_index = client_key_indexes.get(ref_value)
2481
+ resolved = ref_value in client_key_to_app_key
2482
+ elif ref.get("kind") == "target_app":
2483
+ target_index = app_name_indexes.get(ref_value)
2484
+ resolved = ref_value in app_name_to_app_key
2485
+ if target_index is None or resolved:
2486
+ continue
2487
+ target_result = results_by_index[target_index] if 0 <= target_index < len(results_by_index) else None
2488
+ blocked.append(
2489
+ {
2490
+ "kind": ref.get("kind"),
2491
+ "value": ref_value,
2492
+ "path": ref.get("path"),
2493
+ "target_index": target_index,
2494
+ "target_status": target_result.get("status") if isinstance(target_result, dict) else None,
2495
+ "target_error_code": target_result.get("error_code") if isinstance(target_result, dict) else None,
2496
+ }
2497
+ )
2498
+ if not blocked:
2499
+ return None
2500
+ raw_item = apps[index] if 0 <= index < len(apps) else item
2501
+ failure = _multi_app_item_failure(
2502
+ index,
2503
+ raw_item,
2504
+ "DEPENDENCY_APP_NOT_READY",
2505
+ "one or more target apps failed or require readback, so this app schema was not written",
2506
+ )
2507
+ failure["stage"] = "dependency_check"
2508
+ failure["safe_to_retry"] = False
2509
+ failure["details"] = {"blocked_dependencies": blocked}
2510
+ return failure
1874
2511
 
1875
2512
  for index, raw_item in enumerate(apps):
1876
2513
  if not isinstance(raw_item, dict):
1877
- results.append(_multi_app_item_failure(index, raw_item, "INVALID_APP_ITEM", "apps[] items must be objects"))
2514
+ results_by_index[index] = _multi_app_item_failure(index, raw_item, "INVALID_APP_ITEM", "apps[] items must be objects")
1878
2515
  continue
1879
2516
  item = deepcopy(raw_item)
1880
2517
  client_key = str(item.get("client_key") or item.get("clientKey") or "").strip()
@@ -1882,51 +2519,124 @@ class AiBuilderTools(ToolBase):
1882
2519
  app_key = str(item.get("app_key") or item.get("appKey") or "").strip()
1883
2520
  if client_key:
1884
2521
  if client_key in client_keys:
1885
- results.append(_multi_app_item_failure(index, item, "DUPLICATE_CLIENT_KEY", f"duplicate client_key '{client_key}'"))
2522
+ results_by_index[index] = _multi_app_item_failure(index, item, "DUPLICATE_CLIENT_KEY", f"duplicate client_key '{client_key}'")
1886
2523
  continue
1887
2524
  client_keys.add(client_key)
2525
+ client_key_indexes[client_key] = index
2526
+ if app_name:
2527
+ app_name_indexes[app_name] = index
1888
2528
  if not app_key and not app_name:
1889
- results.append(_multi_app_item_failure(index, item, "APP_SELECTOR_REQUIRED", "apps[] requires app_key or app_name"))
2529
+ results_by_index[index] = _multi_app_item_failure(index, item, "APP_SELECTOR_REQUIRED", "apps[] requires app_key or app_name")
1890
2530
  continue
1891
-
1892
- initial_add_fields, deferred_add_fields = _split_multi_app_initial_add_fields(item, is_new_app=not bool(app_key))
1893
- item["_deferred_add_fields"] = deferred_add_fields
1894
- shell = self._app_schema_apply_once(
1895
- profile=profile,
1896
- app_key=app_key,
1897
- package_id=package_id if not app_key else None,
1898
- app_name=app_name,
1899
- app_title="",
1900
- icon=str(item.get("icon") or ""),
1901
- color=str(item.get("color") or ""),
1902
- visibility=item.get("visibility", visibility),
1903
- create_if_missing=create_if_missing and not app_key,
1904
- publish=publish and not deferred_add_fields,
1905
- add_fields=initial_add_fields,
1906
- update_fields=[],
1907
- remove_fields=[],
2531
+ shell_jobs.append(
2532
+ {
2533
+ "index": index,
2534
+ "item": item,
2535
+ "client_key": client_key,
2536
+ "app_name": app_name,
2537
+ "app_key": app_key,
2538
+ }
1908
2539
  )
1909
- public_shell = _publicize_package_fields(shell)
1910
- resolved_key = str(public_shell.get("app_key") or "").strip()
1911
- shell_write_executed = _schema_apply_result_has_write(public_shell)
1912
- shell_write_may_have_succeeded = _schema_apply_result_may_have_write(public_shell)
1913
- if shell_write_executed:
1914
- any_write_executed = True
1915
- if shell_write_may_have_succeeded:
1916
- any_write_may_have_succeeded = True
1917
- if public_shell.get("status") not in {"success", "partial_success"} or not resolved_key:
1918
- pending_readback = bool(shell_write_may_have_succeeded or shell_write_executed) and (
1919
- str(public_shell.get("next_action") or "") == "readback_before_retry"
1920
- or bool((public_shell.get("verification") if isinstance(public_shell.get("verification"), dict) else {}).get("readback_before_retry"))
1921
- or not resolved_key
2540
+
2541
+ shell_workers = worker_count(len(shell_jobs))
2542
+ parallelism["shell_workers"] = shell_workers
2543
+
2544
+ def run_shell_job(job: JSONObject) -> JSONObject:
2545
+ index = int(job["index"])
2546
+ item = deepcopy(job["item"])
2547
+ app_key = str(job.get("app_key") or "")
2548
+ app_name = str(job.get("app_name") or "")
2549
+ shell_started_at = time.perf_counter()
2550
+ try:
2551
+ shell = self._app_schema_apply_once(
2552
+ profile=profile,
2553
+ app_key=app_key,
2554
+ package_id=package_id if not app_key else None,
2555
+ app_name=app_name,
2556
+ app_title="",
2557
+ icon=str(item.get("icon") or ""),
2558
+ color=str(item.get("color") or ""),
2559
+ visibility=item.get("visibility", visibility),
2560
+ publish=False,
2561
+ add_fields=[],
2562
+ update_fields=[],
2563
+ remove_fields=[],
2564
+ inline_form_layout_sections=None,
2565
+ allow_inline_layout_fallback=False,
1922
2566
  )
1923
- item_status = "pending_readback" if pending_readback else "failed"
1924
- if pending_readback:
2567
+ return {
2568
+ "index": index,
2569
+ "job": job,
2570
+ "elapsed_ms": _elapsed_ms(shell_started_at),
2571
+ "public_shell": _publicize_package_fields(shell),
2572
+ }
2573
+ except Exception as error: # pragma: no cover - defensive guard for unexpected worker failures
2574
+ return {
2575
+ "index": index,
2576
+ "job": job,
2577
+ "elapsed_ms": _elapsed_ms(shell_started_at),
2578
+ "exception": repr(error),
2579
+ }
2580
+
2581
+ shell_outputs: list[JSONObject] = []
2582
+ shell_phase_started_at = time.perf_counter()
2583
+ if shell_jobs:
2584
+ with ThreadPoolExecutor(max_workers=shell_workers) as executor:
2585
+ futures = [executor.submit(run_shell_job, job) for job in shell_jobs]
2586
+ for future in as_completed(futures):
2587
+ shell_outputs.append(future.result())
2588
+ app_shell_apply_ms += _elapsed_ms(shell_phase_started_at)
2589
+
2590
+ for output in sorted(shell_outputs, key=lambda item: int(item.get("index", 0))):
2591
+ index = int(output["index"])
2592
+ job = output.get("job") if isinstance(output.get("job"), dict) else {}
2593
+ client_key = str(job.get("client_key") or "").strip()
2594
+ app_name = str(job.get("app_name") or "").strip()
2595
+ app_key = str(job.get("app_key") or "").strip()
2596
+ item = deepcopy(job.get("item") if isinstance(job.get("item"), dict) else {})
2597
+ exception = output.get("exception")
2598
+ if exception:
2599
+ results_by_index[index] = {
2600
+ "index": index,
2601
+ "row_number": index + 1,
2602
+ "client_key": client_key or None,
2603
+ "app_name": app_name or None,
2604
+ "app_key": app_key or None,
2605
+ "status": "pending_readback",
2606
+ "stage": "resolve_or_create_shell",
2607
+ "error_code": "APP_SHELL_APPLY_EXCEPTION",
2608
+ "message": str(exception),
2609
+ "write_executed": False,
2610
+ "write_may_have_succeeded": True,
2611
+ "safe_to_retry": False,
2612
+ "next_action": "readback_before_retry",
2613
+ }
2614
+ if app_name:
2615
+ pending_readback_app_names.append(app_name)
2616
+ any_write_may_have_succeeded = True
2617
+ continue
2618
+ public_shell = output.get("public_shell") if isinstance(output.get("public_shell"), dict) else {}
2619
+ merge_child_duration(public_shell)
2620
+ resolved_key = str(public_shell.get("app_key") or "").strip()
2621
+ shell_write_executed = _schema_apply_result_has_write(public_shell)
2622
+ shell_write_may_have_succeeded = _schema_apply_result_may_have_write(public_shell)
2623
+ if shell_write_executed:
2624
+ any_write_executed = True
2625
+ if shell_write_may_have_succeeded:
2626
+ any_write_may_have_succeeded = True
2627
+ if public_shell.get("status") not in {"success", "partial_success"} or not resolved_key:
2628
+ pending_readback = bool(shell_write_may_have_succeeded or shell_write_executed) and (
2629
+ str(public_shell.get("next_action") or "") == "readback_before_retry"
2630
+ or bool((public_shell.get("verification") if isinstance(public_shell.get("verification"), dict) else {}).get("readback_before_retry"))
2631
+ or not resolved_key
2632
+ )
2633
+ item_status = "pending_readback" if pending_readback else "failed"
2634
+ if pending_readback:
1925
2635
  if resolved_key:
1926
2636
  pending_readback_app_keys.append(resolved_key)
1927
2637
  if app_name:
1928
2638
  pending_readback_app_names.append(app_name)
1929
- results.append({
2639
+ results_by_index[index] = {
1930
2640
  "index": index,
1931
2641
  "row_number": index + 1,
1932
2642
  "client_key": client_key or None,
@@ -1941,8 +2651,8 @@ class AiBuilderTools(ToolBase):
1941
2651
  "safe_to_retry": False if pending_readback else (not shell_write_executed and not any_write_executed),
1942
2652
  **({"next_action": "readback_before_retry"} if pending_readback else {}),
1943
2653
  **({"verification": public_shell.get("verification")} if isinstance(public_shell.get("verification"), dict) else {}),
1944
- **({"created": True} if pending_readback and create_if_missing and not app_key else {}),
1945
- })
2654
+ **({"created": True} if pending_readback and not app_key else {}),
2655
+ }
1946
2656
  continue
1947
2657
  if bool(public_shell.get("created")):
1948
2658
  created_app_keys.append(resolved_key)
@@ -1955,7 +2665,7 @@ class AiBuilderTools(ToolBase):
1955
2665
  resolved_name = str(public_shell.get("app_name_after") or public_shell.get("app_name") or app_name or "").strip()
1956
2666
  if resolved_name:
1957
2667
  app_name_to_app_key[resolved_name] = resolved_key
1958
- results.append({
2668
+ results_by_index[index] = {
1959
2669
  "index": index,
1960
2670
  "row_number": index + 1,
1961
2671
  "client_key": client_key or None,
@@ -1966,80 +2676,246 @@ class AiBuilderTools(ToolBase):
1966
2676
  "shell_result": public_shell,
1967
2677
  "shell_field_diff": public_shell.get("field_diff") or {},
1968
2678
  "shell_field_diff_details": public_shell.get("field_diff_details") or {},
1969
- "deferred_add_fields": deferred_add_fields,
1970
- })
2679
+ }
1971
2680
 
1972
- final_items: list[JSONObject] = []
2681
+ final_items_by_index: list[JSONObject | None] = [None] * len(apps)
2682
+ schema_jobs: list[JSONObject] = []
1973
2683
  for index, raw_item in enumerate(apps):
1974
- existing = next((item for item in results if item.get("index") == index), None)
2684
+ existing = results_by_index[index] if 0 <= index < len(results_by_index) else None
1975
2685
  if not existing or existing.get("status") != "shell_ready":
1976
2686
  if existing:
1977
- final_items.append(existing)
2687
+ final_items_by_index[index] = existing
1978
2688
  continue
1979
2689
  item = deepcopy(raw_item)
1980
2690
  app_key = str(existing.get("app_key") or "").strip()
2691
+ dependency_failure = dependency_failure_for_item(index, item)
2692
+ if dependency_failure is not None:
2693
+ final_items_by_index[index] = dependency_failure
2694
+ any_write_executed = True
2695
+ continue
2696
+ compile_started_at = time.perf_counter()
1981
2697
  try:
1982
2698
  compiled_item = _compile_multi_app_schema_item_refs(item, client_key_to_app_key, app_name_to_app_key)
1983
2699
  except ValueError as error:
1984
- final_items.append({
2700
+ relation_compile_ms += _elapsed_ms(compile_started_at)
2701
+ final_items_by_index[index] = {
1985
2702
  **{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
1986
2703
  "status": "failed",
1987
2704
  "stage": "compile_relation_refs",
1988
2705
  "error_code": "TARGET_APP_REF_NOT_FOUND",
1989
2706
  "message": str(error),
1990
2707
  "safe_to_retry": False,
1991
- })
2708
+ }
1992
2709
  any_write_executed = True
1993
2710
  continue
1994
-
1995
- deferred_add_fields = (
1996
- _compiled_multi_app_deferred_add_fields(compiled_item, existing)
1997
- if bool(existing.get("created"))
1998
- else list(compiled_item.get("add_fields") or [])
1999
- )
2711
+ relation_compile_ms += _elapsed_ms(compile_started_at)
2712
+ schema_add_fields = list(compiled_item.get("add_fields") or [])
2000
2713
  update_fields = list(compiled_item.get("update_fields") or [])
2001
2714
  remove_fields = list(compiled_item.get("remove_fields") or [])
2002
- if bool(existing.get("created")) and not deferred_add_fields and not update_fields and not remove_fields:
2003
- shell_result = existing.get("shell_result") if isinstance(existing.get("shell_result"), dict) else {}
2004
- item_status = shell_result.get("status") if shell_result.get("status") in {"success", "partial_success"} else "failed"
2005
- shell_field_diff = existing.get("shell_field_diff") if isinstance(existing.get("shell_field_diff"), dict) else {}
2006
- shell_field_diff_details = existing.get("shell_field_diff_details") if isinstance(existing.get("shell_field_diff_details"), dict) else {}
2007
- final_items.append({
2715
+ layout_sections = list(compiled_item.get("_inline_form_layout_sections") or [])
2716
+ field_phase_app_name = ""
2717
+ field_phase_icon = ""
2718
+ field_phase_color = ""
2719
+ field_phase_visibility = None
2720
+ relation_fields_for_retry = [
2721
+ deepcopy(field)
2722
+ for field in schema_add_fields
2723
+ if isinstance(field, dict) and _multi_app_field_type(field) == PublicFieldType.relation.value
2724
+ ]
2725
+ schema_jobs.append(
2726
+ {
2727
+ "index": index,
2728
+ "existing": existing,
2729
+ "item": item,
2730
+ "app_key": app_key,
2731
+ "field_phase_app_name": field_phase_app_name,
2732
+ "field_phase_icon": field_phase_icon,
2733
+ "field_phase_color": field_phase_color,
2734
+ "field_phase_visibility": field_phase_visibility,
2735
+ "schema_add_fields": schema_add_fields,
2736
+ "update_fields": update_fields,
2737
+ "remove_fields": remove_fields,
2738
+ "layout_sections": layout_sections,
2739
+ "relation_fields_for_retry": relation_fields_for_retry,
2740
+ }
2741
+ )
2742
+
2743
+ schema_workers = worker_count(len(schema_jobs))
2744
+ parallelism["schema_workers"] = schema_workers
2745
+ parallelism["schema_wave_count"] = 0
2746
+ parallelism["schema_wave_sizes"] = []
2747
+
2748
+ def run_schema_job(job: JSONObject) -> JSONObject:
2749
+ index = int(job["index"])
2750
+ app_key = str(job.get("app_key") or "")
2751
+ field_phase_app_name = str(job.get("field_phase_app_name") or "")
2752
+ field_phase_icon = str(job.get("field_phase_icon") or "")
2753
+ field_phase_color = str(job.get("field_phase_color") or "")
2754
+ field_phase_visibility = job.get("field_phase_visibility")
2755
+ schema_add_fields = list(job.get("schema_add_fields") or [])
2756
+ update_fields = list(job.get("update_fields") or [])
2757
+ remove_fields = list(job.get("remove_fields") or [])
2758
+ layout_sections = list(job.get("layout_sections") or [])
2759
+ relation_fields_for_retry = list(job.get("relation_fields_for_retry") or [])
2760
+ field_started_at = time.perf_counter()
2761
+ try:
2762
+ field_result = self._app_schema_apply_once(
2763
+ profile=profile,
2764
+ app_key=app_key,
2765
+ package_id=None,
2766
+ app_name=field_phase_app_name,
2767
+ app_title="",
2768
+ icon=field_phase_icon,
2769
+ color=field_phase_color,
2770
+ visibility=field_phase_visibility,
2771
+ publish=publish,
2772
+ add_fields=schema_add_fields,
2773
+ update_fields=update_fields,
2774
+ remove_fields=remove_fields,
2775
+ inline_form_layout_sections=layout_sections,
2776
+ allow_inline_layout_fallback=False,
2777
+ )
2778
+ public_result = _publicize_package_fields(field_result)
2779
+ except Exception as error: # pragma: no cover - defensive guard for unexpected worker failures
2780
+ return {
2781
+ "index": index,
2782
+ "job": job,
2783
+ "elapsed_ms": _elapsed_ms(field_started_at),
2784
+ "exception": repr(error),
2785
+ }
2786
+ if _multi_app_deferred_relation_retryable(public_result, relation_fields_for_retry):
2787
+ first_error = {
2788
+ "error_code": public_result.get("error_code"),
2789
+ "message": public_result.get("message"),
2790
+ "backend_code": public_result.get("backend_code"),
2791
+ "http_status": public_result.get("http_status"),
2792
+ "details": deepcopy(public_result.get("details")) if isinstance(public_result.get("details"), dict) else None,
2793
+ }
2794
+ retry_attempts: list[JSONObject] = []
2795
+ for retry_index, delay_seconds in enumerate((1.0, 5.0), start=1):
2796
+ retry_readback = self._stabilize_deferred_relation_metadata(
2797
+ profile=profile,
2798
+ source_app_key=app_key,
2799
+ deferred_add_fields=relation_fields_for_retry,
2800
+ delay_seconds=delay_seconds,
2801
+ )
2802
+ retry_result = self._app_schema_apply_once(
2803
+ profile=profile,
2804
+ app_key=app_key,
2805
+ package_id=None,
2806
+ app_name=field_phase_app_name,
2807
+ app_title="",
2808
+ icon=field_phase_icon,
2809
+ color=field_phase_color,
2810
+ visibility=field_phase_visibility,
2811
+ publish=publish,
2812
+ add_fields=schema_add_fields,
2813
+ update_fields=update_fields,
2814
+ remove_fields=remove_fields,
2815
+ inline_form_layout_sections=layout_sections,
2816
+ allow_inline_layout_fallback=False,
2817
+ )
2818
+ public_retry_result = _publicize_package_fields(retry_result)
2819
+ retry_attempts.append(
2820
+ {
2821
+ "attempt": retry_index,
2822
+ "delay_seconds": delay_seconds,
2823
+ "status": public_retry_result.get("status"),
2824
+ "error_code": public_retry_result.get("error_code"),
2825
+ "backend_code": public_retry_result.get("backend_code"),
2826
+ "http_status": public_retry_result.get("http_status"),
2827
+ "readback": retry_readback,
2828
+ }
2829
+ )
2830
+ public_result = public_retry_result
2831
+ if not _multi_app_deferred_relation_retryable(public_result, relation_fields_for_retry):
2832
+ break
2833
+ retry_details = public_result.get("details")
2834
+ if not isinstance(retry_details, dict):
2835
+ retry_details = {}
2836
+ public_result["details"] = retry_details
2837
+ retry_summary: JSONObject = {
2838
+ "attempted": True,
2839
+ "reason": "same_batch_relation_metadata_may_lag",
2840
+ "first_error": first_error,
2841
+ "attempts": retry_attempts,
2842
+ }
2843
+ if retry_attempts:
2844
+ retry_summary["readback"] = retry_attempts[-1].get("readback")
2845
+ retry_details["deferred_relation_retry"] = retry_summary
2846
+ if layout_sections and not _inline_form_layout_was_handled(public_result):
2847
+ public_result = mark_inline_layout_not_applied_in_schema(
2848
+ public_result,
2849
+ app_key=app_key,
2850
+ sections=layout_sections,
2851
+ )
2852
+ return {
2853
+ "index": index,
2854
+ "job": job,
2855
+ "elapsed_ms": _elapsed_ms(field_started_at),
2856
+ "public_result": public_result,
2857
+ }
2858
+
2859
+ def schema_job_dependency_indexes(job: JSONObject) -> set[int]:
2860
+ index = int(job.get("index", -1))
2861
+ item = job.get("item") if isinstance(job.get("item"), dict) else {}
2862
+ dependencies: set[int] = set()
2863
+ for ref in _collect_multi_app_target_refs(item, path=f"apps[{index}]"):
2864
+ ref_value = str(ref.get("value") or "").strip()
2865
+ if not ref_value:
2866
+ continue
2867
+ target_index: int | None = None
2868
+ if ref.get("kind") == "target_app_ref":
2869
+ target_index = client_key_indexes.get(ref_value)
2870
+ elif ref.get("kind") == "target_app":
2871
+ target_index = app_name_indexes.get(ref_value)
2872
+ if target_index is None or target_index == index:
2873
+ continue
2874
+ dependencies.add(target_index)
2875
+ return dependencies
2876
+
2877
+ def schema_dependency_failure(index: int, job: JSONObject, blocked_dependencies: list[JSONObject]) -> JSONObject:
2878
+ existing = job.get("existing") if isinstance(job.get("existing"), dict) else {}
2879
+ return {
2880
+ **{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
2881
+ "status": "failed",
2882
+ "stage": "schema_dependency_check",
2883
+ "error_code": "DEPENDENCY_SCHEMA_NOT_READY",
2884
+ "message": "one or more same-batch target app schemas failed or are not ready, so this app schema was not written",
2885
+ "write_executed": False,
2886
+ "write_may_have_succeeded": False,
2887
+ "safe_to_retry": False,
2888
+ "details": {"blocked_dependencies": blocked_dependencies},
2889
+ }
2890
+
2891
+ def process_schema_output(output: JSONObject) -> None:
2892
+ nonlocal any_write_executed
2893
+ nonlocal any_write_may_have_succeeded
2894
+ index = int(output["index"])
2895
+ job = output.get("job") if isinstance(output.get("job"), dict) else {}
2896
+ existing = job.get("existing") if isinstance(job.get("existing"), dict) else {}
2897
+ app_key = str(job.get("app_key") or "")
2898
+ exception = output.get("exception")
2899
+ if exception:
2900
+ final_items_by_index[index] = {
2008
2901
  **{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
2009
- "status": item_status,
2902
+ "status": "pending_readback",
2010
2903
  "stage": "schema_apply",
2011
- "field_diff": shell_field_diff,
2012
- "field_diff_details": shell_field_diff_details,
2013
- "shell_field_diff": shell_field_diff,
2014
- "shell_field_diff_details": shell_field_diff_details,
2015
- "published": bool(shell_result.get("published")),
2016
- "verified": bool(shell_result.get("verified")),
2017
- "error_code": shell_result.get("error_code"),
2018
- "message": shell_result.get("message"),
2019
- "write_executed": _schema_apply_result_has_write(shell_result),
2020
- "write_may_have_succeeded": _schema_apply_result_may_have_write(shell_result),
2904
+ "error_code": "SCHEMA_APPLY_EXCEPTION",
2905
+ "message": str(exception),
2906
+ "write_executed": False,
2907
+ "write_may_have_succeeded": True,
2021
2908
  "safe_to_retry": False,
2022
- **({"next_action": shell_result.get("next_action")} if shell_result.get("next_action") else {}),
2023
- **({"verification": shell_result.get("verification")} if isinstance(shell_result.get("verification"), dict) else {}),
2024
- })
2025
- continue
2026
-
2027
- field_result = self._app_schema_apply_once(
2028
- profile=profile,
2029
- app_key=app_key,
2030
- package_id=None,
2031
- app_name=str(compiled_item.get("app_name") or compiled_item.get("appTitle") or compiled_item.get("app_title") or ""),
2032
- app_title="",
2033
- icon=str(compiled_item.get("icon") or ""),
2034
- color=str(compiled_item.get("color") or ""),
2035
- visibility=compiled_item.get("visibility"),
2036
- create_if_missing=False,
2037
- publish=publish,
2038
- add_fields=deferred_add_fields,
2039
- update_fields=update_fields,
2040
- remove_fields=remove_fields,
2041
- )
2042
- public_result = _publicize_package_fields(field_result)
2909
+ "next_action": "readback_before_retry",
2910
+ }
2911
+ if app_key:
2912
+ pending_readback_app_keys.append(app_key)
2913
+ if existing.get("app_name"):
2914
+ pending_readback_app_names.append(str(existing.get("app_name")))
2915
+ any_write_may_have_succeeded = True
2916
+ return
2917
+ public_result = output.get("public_result") if isinstance(output.get("public_result"), dict) else {}
2918
+ merge_child_duration(public_result)
2043
2919
  if _schema_apply_result_has_write(public_result):
2044
2920
  any_write_executed = True
2045
2921
  if _schema_apply_result_may_have_write(public_result):
@@ -2054,7 +2930,7 @@ class AiBuilderTools(ToolBase):
2054
2930
  pending_readback_app_keys.append(app_key)
2055
2931
  if existing.get("app_name"):
2056
2932
  pending_readback_app_names.append(str(existing.get("app_name")))
2057
- final_items.append({
2933
+ final_items_by_index[index] = {
2058
2934
  **{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
2059
2935
  "status": item_status,
2060
2936
  "stage": "schema_apply",
@@ -2069,9 +2945,225 @@ class AiBuilderTools(ToolBase):
2069
2945
  "write_executed": _schema_apply_result_has_write(public_result),
2070
2946
  "write_may_have_succeeded": _schema_apply_result_may_have_write(public_result),
2071
2947
  "safe_to_retry": False,
2948
+ **_multi_app_child_diagnostics(public_result),
2072
2949
  **({"next_action": public_result.get("next_action")} if public_result.get("next_action") else {}),
2073
2950
  **({"verification": public_result.get("verification")} if isinstance(public_result.get("verification"), dict) else {}),
2074
- })
2951
+ **({"inline_form_layout": public_result.get("inline_form_layout")} if isinstance(public_result.get("inline_form_layout"), dict) else {}),
2952
+ **({"inline_form_layout_result": public_result.get("inline_form_layout_result")} if isinstance(public_result.get("inline_form_layout_result"), dict) else {}),
2953
+ }
2954
+
2955
+ schema_jobs_by_index = {int(job["index"]): job for job in schema_jobs}
2956
+ schema_dependencies_by_index = {
2957
+ index: schema_job_dependency_indexes(job)
2958
+ for index, job in schema_jobs_by_index.items()
2959
+ }
2960
+ pending_schema_indexes = set(schema_jobs_by_index)
2961
+ schema_phase_started_at = time.perf_counter()
2962
+ while pending_schema_indexes:
2963
+ blocked_this_round = False
2964
+ for index in sorted(list(pending_schema_indexes)):
2965
+ blocked_dependencies: list[JSONObject] = []
2966
+ for dependency_index in sorted(schema_dependencies_by_index.get(index, set())):
2967
+ if dependency_index not in schema_jobs_by_index:
2968
+ continue
2969
+ dependency_result = final_items_by_index[dependency_index]
2970
+ if dependency_result is None:
2971
+ continue
2972
+ dependency_status = dependency_result.get("status")
2973
+ if dependency_status not in {"success", "partial_success"}:
2974
+ blocked_dependencies.append(
2975
+ {
2976
+ "target_index": dependency_index,
2977
+ "target_status": dependency_status,
2978
+ "target_error_code": dependency_result.get("error_code"),
2979
+ "target_app_key": dependency_result.get("app_key"),
2980
+ "target_app_name": dependency_result.get("app_name"),
2981
+ }
2982
+ )
2983
+ if not blocked_dependencies:
2984
+ continue
2985
+ final_items_by_index[index] = schema_dependency_failure(
2986
+ index,
2987
+ schema_jobs_by_index[index],
2988
+ blocked_dependencies,
2989
+ )
2990
+ pending_schema_indexes.remove(index)
2991
+ blocked_this_round = True
2992
+ any_write_executed = True
2993
+ if blocked_this_round:
2994
+ continue
2995
+
2996
+ ready_indexes = [
2997
+ index
2998
+ for index in sorted(pending_schema_indexes)
2999
+ if all(
3000
+ dependency_index not in pending_schema_indexes
3001
+ for dependency_index in schema_dependencies_by_index.get(index, set())
3002
+ if dependency_index in schema_jobs_by_index
3003
+ )
3004
+ ]
3005
+ if not ready_indexes:
3006
+ for index in sorted(pending_schema_indexes):
3007
+ existing = schema_jobs_by_index[index].get("existing")
3008
+ existing = existing if isinstance(existing, dict) else {}
3009
+ final_items_by_index[index] = {
3010
+ **{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
3011
+ "status": "failed",
3012
+ "stage": "schema_dependency_check",
3013
+ "error_code": "DEPENDENCY_SCHEMA_CYCLE",
3014
+ "message": "same-batch target_app_ref dependencies contain a cycle; schema was not written",
3015
+ "write_executed": False,
3016
+ "write_may_have_succeeded": False,
3017
+ "safe_to_retry": False,
3018
+ "details": {
3019
+ "dependency_indexes": sorted(schema_dependencies_by_index.get(index, set())),
3020
+ },
3021
+ }
3022
+ any_write_executed = True
3023
+ pending_schema_indexes.clear()
3024
+ break
3025
+
3026
+ parallelism["schema_wave_count"] = int(parallelism.get("schema_wave_count") or 0) + 1
3027
+ schema_wave_sizes = parallelism.get("schema_wave_sizes")
3028
+ if isinstance(schema_wave_sizes, list):
3029
+ schema_wave_sizes.append(len(ready_indexes))
3030
+ wave_outputs: list[JSONObject] = []
3031
+ wave_workers = worker_count(len(ready_indexes))
3032
+ with ThreadPoolExecutor(max_workers=wave_workers) as executor:
3033
+ futures = [executor.submit(run_schema_job, schema_jobs_by_index[index]) for index in ready_indexes]
3034
+ for future in as_completed(futures):
3035
+ wave_outputs.append(future.result())
3036
+ for output in sorted(wave_outputs, key=lambda item: int(item.get("index", 0))):
3037
+ process_schema_output(output)
3038
+ for index in ready_indexes:
3039
+ pending_schema_indexes.remove(index)
3040
+ app_field_apply_ms += _elapsed_ms(schema_phase_started_at)
3041
+
3042
+ final_items: list[JSONObject] = [
3043
+ item if isinstance(item, dict) else _multi_app_item_failure(index, apps[index], "APP_ITEM_NOT_PROCESSED", "app item was not processed")
3044
+ for index, item in enumerate(final_items_by_index)
3045
+ ]
3046
+
3047
+ def relation_repair_patches(item: JSONObject) -> list[JSONObject]:
3048
+ details = item.get("details") if isinstance(item.get("details"), dict) else {}
3049
+ repair_plan = details.get("relation_repair_plan") if isinstance(details.get("relation_repair_plan"), list) else []
3050
+ patches: list[JSONObject] = []
3051
+ for plan_item in repair_plan:
3052
+ if not isinstance(plan_item, dict):
3053
+ continue
3054
+ patch = plan_item.get("suggested_patch")
3055
+ if isinstance(patch, dict):
3056
+ patches.append(deepcopy(patch))
3057
+ return patches
3058
+
3059
+ relation_repair_jobs: list[JSONObject] = []
3060
+ for item in final_items:
3061
+ if item.get("status") != "partial_success":
3062
+ continue
3063
+ app_key = str(item.get("app_key") or "").strip()
3064
+ patches = relation_repair_patches(item)
3065
+ if app_key and patches:
3066
+ relation_repair_jobs.append({"item": item, "app_key": app_key, "patches": patches})
3067
+
3068
+ def run_relation_repair_job(job: JSONObject) -> JSONObject:
3069
+ app_key = str(job.get("app_key") or "")
3070
+ patches = list(job.get("patches") or [])
3071
+ repair_started_at = time.perf_counter()
3072
+ try:
3073
+ repair_result = self._app_schema_apply_once(
3074
+ profile=profile,
3075
+ app_key=app_key,
3076
+ package_id=None,
3077
+ app_name="",
3078
+ app_title="",
3079
+ icon="",
3080
+ color="",
3081
+ visibility=None,
3082
+ publish=publish,
3083
+ add_fields=[],
3084
+ update_fields=patches,
3085
+ remove_fields=[],
3086
+ inline_form_layout_sections=None,
3087
+ allow_inline_layout_fallback=False,
3088
+ )
3089
+ public_repair = _publicize_package_fields(repair_result)
3090
+ except Exception as error: # pragma: no cover - defensive guard for unexpected worker failures
3091
+ return {"job": job, "elapsed_ms": _elapsed_ms(repair_started_at), "exception": repr(error)}
3092
+ return {
3093
+ "job": job,
3094
+ "elapsed_ms": _elapsed_ms(repair_started_at),
3095
+ "public_repair": public_repair,
3096
+ }
3097
+
3098
+ if relation_repair_jobs:
3099
+ repair_phase_started_at = time.perf_counter()
3100
+ repair_workers = worker_count(len(relation_repair_jobs))
3101
+ repair_outputs: list[JSONObject] = []
3102
+ with ThreadPoolExecutor(max_workers=repair_workers) as executor:
3103
+ futures = [executor.submit(run_relation_repair_job, job) for job in relation_repair_jobs]
3104
+ for future in as_completed(futures):
3105
+ repair_outputs.append(future.result())
3106
+ relation_repair_ms += _elapsed_ms(repair_phase_started_at)
3107
+ for output in repair_outputs:
3108
+ job = output.get("job") if isinstance(output.get("job"), dict) else {}
3109
+ original = job.get("item") if isinstance(job.get("item"), dict) else {}
3110
+ if not original:
3111
+ continue
3112
+ try:
3113
+ index = int(original.get("index"))
3114
+ except (TypeError, ValueError):
3115
+ continue
3116
+ if index < 0 or index >= len(final_items):
3117
+ continue
3118
+ repaired_item = deepcopy(original)
3119
+ exception = output.get("exception")
3120
+ if exception:
3121
+ repaired_item["relation_repair"] = {
3122
+ "attempted": True,
3123
+ "status": "failed",
3124
+ "error_code": "RELATION_REPAIR_EXCEPTION",
3125
+ "message": str(exception),
3126
+ }
3127
+ final_items[index] = repaired_item
3128
+ continue
3129
+ public_repair = output.get("public_repair") if isinstance(output.get("public_repair"), dict) else {}
3130
+ merge_child_duration(public_repair)
3131
+ if _schema_apply_result_has_write(public_repair):
3132
+ any_write_executed = True
3133
+ if _schema_apply_result_may_have_write(public_repair):
3134
+ any_write_may_have_succeeded = True
3135
+ repair_payload: JSONObject = {
3136
+ "attempted": True,
3137
+ "status": public_repair.get("status"),
3138
+ "error_code": public_repair.get("error_code"),
3139
+ "message": public_repair.get("message"),
3140
+ "verified": bool(public_repair.get("verified")),
3141
+ "field_diff": public_repair.get("field_diff") or {},
3142
+ **({"verification": public_repair.get("verification")} if isinstance(public_repair.get("verification"), dict) else {}),
3143
+ **({"details": public_repair.get("details")} if isinstance(public_repair.get("details"), dict) else {}),
3144
+ **({"duration_breakdown_ms": public_repair.get("duration_breakdown_ms")} if isinstance(public_repair.get("duration_breakdown_ms"), dict) else {}),
3145
+ }
3146
+ repaired_item["relation_repair"] = repair_payload
3147
+ repaired_item["field_diff"] = _merge_schema_field_diffs(repaired_item.get("field_diff") or {}, public_repair.get("field_diff") or {})
3148
+ repaired_item["field_diff_details"] = _merge_schema_field_diffs(
3149
+ repaired_item.get("field_diff_details") or {},
3150
+ public_repair.get("field_diff_details") or {},
3151
+ )
3152
+ if public_repair.get("status") == "success" and bool(public_repair.get("verified")):
3153
+ repaired_item["status"] = "success"
3154
+ repaired_item["error_code"] = None
3155
+ repaired_item["message"] = "applied schema patch; relation readback repaired"
3156
+ repaired_item["published"] = bool(public_repair.get("published"))
3157
+ repaired_item["verified"] = True
3158
+ original_verification = repaired_item.get("verification") if isinstance(repaired_item.get("verification"), dict) else {}
3159
+ repair_verification = public_repair.get("verification") if isinstance(public_repair.get("verification"), dict) else {}
3160
+ merged_verification = {**deepcopy(original_verification), **deepcopy(repair_verification)}
3161
+ repaired_item["verification"] = merged_verification
3162
+ details = repaired_item.get("details") if isinstance(repaired_item.get("details"), dict) else {}
3163
+ details = deepcopy(details)
3164
+ details.pop("relation_repair_plan", None)
3165
+ repaired_item["details"] = details
3166
+ final_items[index] = repaired_item
2075
3167
 
2076
3168
  pending_readback = sum(1 for item in final_items if item.get("status") == "pending_readback")
2077
3169
  succeeded = sum(1 for item in final_items if item.get("status") in {"success", "partial_success"})
@@ -2096,6 +3188,7 @@ class AiBuilderTools(ToolBase):
2096
3188
  "package_id": package_id,
2097
3189
  "publish_requested": publish,
2098
3190
  "apps": final_items,
3191
+ "parallelism": parallelism,
2099
3192
  "normalized_args": normalized_args,
2100
3193
  "verification": {
2101
3194
  "all_apps_succeeded": failed == 0,
@@ -2116,7 +3209,69 @@ class AiBuilderTools(ToolBase):
2116
3209
  response["next_action"] = "readback_before_retry"
2117
3210
  response["pending_readback_app_keys"] = list(dict.fromkeys(pending_readback_app_keys))
2118
3211
  response["pending_readback_app_names"] = list(dict.fromkeys(pending_readback_app_names))
2119
- return response
3212
+ return finish(response)
3213
+
3214
+ def _stabilize_deferred_relation_metadata(
3215
+ self,
3216
+ *,
3217
+ profile: str,
3218
+ source_app_key: str,
3219
+ deferred_add_fields: list[JSONObject],
3220
+ delay_seconds: float,
3221
+ ) -> list[JSONObject]:
3222
+ edit_context_result = self._finish_deferred_relation_edit_context(profile=profile, app_key=source_app_key)
3223
+ time.sleep(delay_seconds)
3224
+ app_keys = [source_app_key]
3225
+ for key in _multi_app_relation_target_app_keys(deferred_add_fields):
3226
+ if key and key not in app_keys:
3227
+ app_keys.append(key)
3228
+ readback: list[JSONObject] = []
3229
+ for app_key in app_keys:
3230
+ try:
3231
+ result = self._facade.app_read_fields(profile=profile, app_key=app_key)
3232
+ readback.append(
3233
+ {
3234
+ "app_key": app_key,
3235
+ "status": result.get("status"),
3236
+ "field_count": len(result.get("fields") or []),
3237
+ **({"edit_context": edit_context_result} if app_key == source_app_key else {}),
3238
+ }
3239
+ )
3240
+ except (QingflowApiError, RuntimeError) as error:
3241
+ api_error = _coerce_api_error(error)
3242
+ readback.append(
3243
+ {
3244
+ "app_key": app_key,
3245
+ "status": "failed",
3246
+ "transport_error": {
3247
+ "http_status": api_error.http_status,
3248
+ "backend_code": api_error.backend_code,
3249
+ "category": api_error.category,
3250
+ "request_id": api_error.request_id,
3251
+ },
3252
+ }
3253
+ )
3254
+ return readback
3255
+
3256
+ def _finish_deferred_relation_edit_context(self, *, profile: str, app_key: str) -> JSONObject:
3257
+ try:
3258
+ version_result = self._facade.apps.app_get_edit_version_no(profile=profile, app_key=app_key).get("result") or {}
3259
+ raw_version = version_result.get("editVersionNo") or version_result.get("versionNo") or 1
3260
+ edit_version_no = int(raw_version)
3261
+ self._facade.apps.app_edit_finished(profile=profile, app_key=app_key, payload={"editVersionNo": edit_version_no})
3262
+ return {"status": "success", "edit_version_no": edit_version_no}
3263
+ except (QingflowApiError, RuntimeError, TypeError, ValueError) as error:
3264
+ api_error = _coerce_api_error(error if isinstance(error, (QingflowApiError, RuntimeError)) else RuntimeError(str(error)))
3265
+ return {
3266
+ "status": "failed",
3267
+ "message": api_error.message,
3268
+ "transport_error": {
3269
+ "http_status": api_error.http_status,
3270
+ "backend_code": api_error.backend_code,
3271
+ "category": api_error.category,
3272
+ "request_id": api_error.request_id,
3273
+ },
3274
+ }
2120
3275
 
2121
3276
  def _app_schema_apply_once(
2122
3277
  self,
@@ -2129,14 +3284,17 @@ class AiBuilderTools(ToolBase):
2129
3284
  icon: str = "",
2130
3285
  color: str = "",
2131
3286
  visibility: JSONObject | None = None,
2132
- create_if_missing: bool = False,
2133
3287
  publish: bool = True,
2134
3288
  add_fields: list[JSONObject],
2135
3289
  update_fields: list[JSONObject],
2136
3290
  remove_fields: list[JSONObject],
3291
+ inline_form_layout_sections: list[JSONObject] | None = None,
3292
+ allow_inline_layout_fallback: bool = True,
2137
3293
  ) -> JSONObject:
2138
3294
  """执行内部辅助逻辑。"""
3295
+ started_at = time.perf_counter()
2139
3296
  effective_app_name = app_name or app_title
3297
+ creating = not bool(str(app_key or "").strip()) and package_id is not None and bool(str(effective_app_name or "").strip())
2140
3298
  system_field_failure = _reserved_system_field_name_failure(
2141
3299
  tool_name="app_schema_apply",
2142
3300
  profile=profile,
@@ -2146,7 +3304,6 @@ class AiBuilderTools(ToolBase):
2146
3304
  icon=icon,
2147
3305
  color=color,
2148
3306
  visibility=visibility,
2149
- create_if_missing=create_if_missing,
2150
3307
  publish=publish,
2151
3308
  add_fields=add_fields,
2152
3309
  update_fields=update_fields,
@@ -2158,38 +3315,19 @@ class AiBuilderTools(ToolBase):
2158
3315
  tool_name="app_schema_apply",
2159
3316
  icon=icon,
2160
3317
  color=color,
2161
- creating=not bool(str(app_key or "").strip()) and bool(create_if_missing),
3318
+ creating=creating,
2162
3319
  )
2163
3320
  if icon_failure is not None:
2164
3321
  return icon_failure
2165
- plan_result = self._rewrite_plan_result_for_apply(
2166
- result=self.app_schema_plan(
2167
- profile=profile,
2168
- app_key=app_key,
2169
- package_id=package_id,
2170
- app_name=effective_app_name,
2171
- icon=icon,
2172
- color=color,
2173
- visibility=visibility,
2174
- create_if_missing=create_if_missing,
2175
- add_fields=add_fields,
2176
- update_fields=update_fields,
2177
- remove_fields=remove_fields,
2178
- ),
2179
- profile=profile,
2180
- publish=publish,
2181
- plan_tool_name="app_schema_plan",
2182
- apply_tool_name="app_schema_apply",
2183
- )
2184
- if not isinstance(plan_result, dict) or plan_result.get("status") != "success":
2185
- return plan_result
2186
- plan_args = plan_result.get("normalized_args")
2187
- if not isinstance(plan_args, dict):
2188
- plan_args = {}
2189
3322
  try:
2190
- parsed_add = [FieldPatch.model_validate(item) for item in plan_args.get("add_fields") or []]
2191
- parsed_update = [FieldUpdatePatch.model_validate(item) for item in plan_args.get("update_fields") or []]
2192
- parsed_remove = [FieldRemovePatch.model_validate(item) for item in plan_args.get("remove_fields") or []]
3323
+ parsed_add = [FieldPatch.model_validate(item) for item in add_fields or []]
3324
+ parsed_update = [FieldUpdatePatch.model_validate(item) for item in update_fields or []]
3325
+ parsed_remove = [FieldRemovePatch.model_validate(item) for item in remove_fields or []]
3326
+ parsed_inline_layout = [
3327
+ LayoutSectionPatch.model_validate(item)
3328
+ for item in (inline_form_layout_sections or [])
3329
+ ]
3330
+ parsed_visibility = VisibilityPatch.model_validate(visibility) if visibility is not None else None
2193
3331
  except ValidationError as exc:
2194
3332
  return _validation_failure(
2195
3333
  str(exc),
@@ -2199,57 +3337,170 @@ class AiBuilderTools(ToolBase):
2199
3337
  "tool_name": "app_schema_apply",
2200
3338
  "arguments": {
2201
3339
  "profile": profile,
2202
- "app_key": str(plan_args.get("app_key") or app_key),
2203
- "package_id": plan_args.get("package_id", package_id),
2204
- "app_name": str(plan_args.get("app_name") or effective_app_name),
2205
- "icon": str(plan_args.get("icon") or icon),
2206
- "color": str(plan_args.get("color") or color),
2207
- "visibility": plan_args.get("visibility") or visibility,
2208
- "create_if_missing": bool(plan_args.get("create_if_missing", create_if_missing)),
3340
+ "app_key": str(app_key or ""),
3341
+ "package_id": package_id,
3342
+ "app_name": effective_app_name,
3343
+ "icon": str(icon or ""),
3344
+ "color": str(color or ""),
3345
+ "visibility": visibility,
2209
3346
  "publish": publish,
2210
- "add_fields": plan_args.get("add_fields") or [{"name": "字段名称", "type": "text", "required": False}],
2211
- "update_fields": plan_args.get("update_fields") or [],
2212
- "remove_fields": plan_args.get("remove_fields") or [],
3347
+ "add_fields": add_fields or [{"name": "字段名称", "type": "text", "required": False}],
3348
+ "update_fields": update_fields or [],
3349
+ "remove_fields": remove_fields or [],
2213
3350
  },
2214
3351
  },
2215
3352
  )
2216
3353
  normalized_args = {
2217
- "app_key": str(plan_args.get("app_key") or app_key),
2218
- "package_id": plan_args.get("package_id", package_id),
2219
- "app_name": str(plan_args.get("app_name") or effective_app_name),
2220
- "icon": str(plan_args.get("icon") or icon or ""),
2221
- "color": str(plan_args.get("color") or color or ""),
2222
- "visibility": plan_args.get("visibility") or visibility,
2223
- "create_if_missing": bool(plan_args.get("create_if_missing", create_if_missing)),
3354
+ "app_key": str(app_key or ""),
3355
+ "package_id": package_id,
3356
+ "app_name": effective_app_name,
3357
+ "icon": str(icon or ""),
3358
+ "color": str(color or ""),
3359
+ "visibility": parsed_visibility.model_dump(mode="json") if parsed_visibility is not None else None,
2224
3360
  "publish": publish,
2225
3361
  "add_fields": [patch.model_dump(mode="json") for patch in parsed_add],
2226
3362
  "update_fields": [patch.model_dump(mode="json") for patch in parsed_update],
2227
3363
  "remove_fields": [patch.model_dump(mode="json") for patch in parsed_remove],
3364
+ **(
3365
+ {
3366
+ "inline_form_layout_sections": [
3367
+ section.model_dump(mode="json", exclude_none=True)
3368
+ for section in parsed_inline_layout
3369
+ ]
3370
+ }
3371
+ if parsed_inline_layout
3372
+ else {}
3373
+ ),
2228
3374
  }
2229
3375
  result = _safe_tool_call(
2230
3376
  lambda: self._facade.app_schema_apply(
2231
3377
  profile=profile,
2232
- app_key=str(plan_args.get("app_key") or app_key),
2233
- package_tag_id=plan_args.get("package_id", package_id),
2234
- app_name=str(plan_args.get("app_name") or effective_app_name),
2235
- icon=str(plan_args.get("icon") or icon or ""),
2236
- color=str(plan_args.get("color") or color or ""),
2237
- visibility=VisibilityPatch.model_validate(plan_args.get("visibility")) if plan_args.get("visibility") is not None else None,
2238
- create_if_missing=bool(plan_args.get("create_if_missing", create_if_missing)),
3378
+ app_key=str(app_key or ""),
3379
+ package_tag_id=package_id,
3380
+ app_name=effective_app_name,
3381
+ icon=str(icon or ""),
3382
+ color=str(color or ""),
3383
+ visibility=parsed_visibility,
2239
3384
  publish=publish,
2240
3385
  add_fields=parsed_add,
2241
3386
  update_fields=parsed_update,
2242
3387
  remove_fields=parsed_remove,
3388
+ inline_form_layout_sections=parsed_inline_layout,
2243
3389
  ),
2244
3390
  error_code="SCHEMA_APPLY_FAILED",
2245
3391
  normalized_args=normalized_args,
2246
3392
  suggested_next_call={"tool_name": "app_schema_apply", "arguments": {"profile": profile, **normalized_args}},
2247
3393
  )
2248
- return _publicize_package_fields(result)
3394
+ public_result = _publicize_package_fields(result)
3395
+ if inline_form_layout_sections and allow_inline_layout_fallback and not _inline_form_layout_was_handled(public_result):
3396
+ layout_started_at = time.perf_counter()
3397
+ public_result = self._apply_inline_form_layout_after_schema(
3398
+ profile=profile,
3399
+ schema_result=public_result,
3400
+ fallback_app_key=str(app_key or ""),
3401
+ publish=publish,
3402
+ sections=inline_form_layout_sections,
3403
+ )
3404
+ _merge_duration_breakdown(
3405
+ public_result,
3406
+ inline_layout_ms=_elapsed_ms(layout_started_at),
3407
+ )
3408
+ _merge_duration_breakdown(public_result, total_ms=_elapsed_ms(started_at))
3409
+ return public_result
3410
+
3411
+ def _apply_inline_form_layout_after_schema(
3412
+ self,
3413
+ *,
3414
+ profile: str,
3415
+ schema_result: JSONObject,
3416
+ fallback_app_key: str = "",
3417
+ publish: bool,
3418
+ sections: list[JSONObject],
3419
+ ) -> JSONObject:
3420
+ result = deepcopy(schema_result)
3421
+ if not sections:
3422
+ return result
3423
+ app_key = str(result.get("app_key") or result.get("appKey") or fallback_app_key or "").strip()
3424
+ result["inline_form_layout"] = {
3425
+ "requested": True,
3426
+ "section_count": len(sections),
3427
+ "applied": False,
3428
+ }
3429
+ if result.get("status") not in {"success", "partial_success"}:
3430
+ result["inline_form_layout"]["skipped_reason"] = "schema_apply_failed"
3431
+ verification = result.get("verification") if isinstance(result.get("verification"), dict) else {}
3432
+ verification = deepcopy(verification)
3433
+ verification["inline_form_layout_verified"] = False
3434
+ result["verification"] = verification
3435
+ return result
3436
+ if not app_key:
3437
+ result["status"] = "partial_success"
3438
+ result["error_code"] = result.get("error_code") or "INLINE_FORM_LAYOUT_APP_KEY_MISSING"
3439
+ result["message"] = "schema applied but inline form layout could not run because app_key was not resolved"
3440
+ result["inline_form_layout"]["error_code"] = "INLINE_FORM_LAYOUT_APP_KEY_MISSING"
3441
+ verification = result.get("verification") if isinstance(result.get("verification"), dict) else {}
3442
+ verification = deepcopy(verification)
3443
+ verification["inline_form_layout_verified"] = False
3444
+ result["verification"] = verification
3445
+ result["safe_to_retry"] = False
3446
+ return result
3447
+ layout_result = self._app_layout_apply_once(
3448
+ profile=profile,
3449
+ app_key=app_key,
3450
+ mode="merge",
3451
+ publish=publish,
3452
+ sections=sections,
3453
+ )
3454
+ layout_ok = isinstance(layout_result, dict) and layout_result.get("status") in {"success", "partial_success"}
3455
+ result["inline_form_layout"] = {
3456
+ "requested": True,
3457
+ "section_count": len(sections),
3458
+ "app_key": app_key,
3459
+ "applied": layout_ok,
3460
+ "layout_status": layout_result.get("status") if isinstance(layout_result, dict) else "failed",
3461
+ **({"error_code": layout_result.get("error_code")} if isinstance(layout_result, dict) and layout_result.get("error_code") else {}),
3462
+ **({"message": layout_result.get("message")} if isinstance(layout_result, dict) and layout_result.get("message") else {}),
3463
+ }
3464
+ result["inline_form_layout_result"] = layout_result
3465
+ verification = result.get("verification") if isinstance(result.get("verification"), dict) else {}
3466
+ verification = deepcopy(verification)
3467
+ verification["inline_form_layout_verified"] = layout_ok
3468
+ result["verification"] = verification
3469
+ if layout_ok:
3470
+ result["write_executed"] = True
3471
+ return result
3472
+ result["status"] = "partial_success" if result.get("status") == "success" else result.get("status", "partial_success")
3473
+ result["error_code"] = result.get("error_code") or "INLINE_FORM_LAYOUT_APPLY_FAILED"
3474
+ result["message"] = "schema applied but inline form layout failed"
3475
+ result["safe_to_retry"] = False
3476
+ return result
2249
3477
 
2250
3478
  @tool_cn_name("应用布局应用")
2251
- def app_layout_apply(self, *, profile: str, app_key: str, mode: str = "merge", publish: bool = True, sections: list[JSONObject]) -> JSONObject:
3479
+ def app_layout_apply(
3480
+ self,
3481
+ *,
3482
+ profile: str,
3483
+ app_key: str,
3484
+ mode: str = "merge",
3485
+ publish: bool = True,
3486
+ sections: list[JSONObject],
3487
+ apps: list[JSONObject] | None = None,
3488
+ ) -> JSONObject:
2252
3489
  """执行应用相关逻辑。"""
3490
+ if apps is not None:
3491
+ return self._apply_app_batch(
3492
+ tool_name="app_layout_apply",
3493
+ profile=profile,
3494
+ apps=apps,
3495
+ apply_one=lambda _index, item, item_app_key: self.app_layout_apply(
3496
+ profile=profile,
3497
+ app_key=item_app_key,
3498
+ mode=str(_payload_get(item, "mode", default=mode) or mode),
3499
+ publish=_payload_bool(item, "publish", default=publish),
3500
+ sections=_payload_list(item, "sections", default=sections or []),
3501
+ apps=None,
3502
+ ),
3503
+ )
2253
3504
  result = self._app_layout_apply_once(
2254
3505
  profile=profile,
2255
3506
  app_key=app_key,
@@ -2272,27 +3523,9 @@ class AiBuilderTools(ToolBase):
2272
3523
 
2273
3524
  def _app_layout_apply_once(self, *, profile: str, app_key: str, mode: str = "merge", publish: bool = True, sections: list[JSONObject]) -> JSONObject:
2274
3525
  """执行内部辅助逻辑。"""
2275
- plan_result = self._rewrite_plan_result_for_apply(
2276
- result=self.app_layout_plan(
2277
- profile=profile,
2278
- app_key=app_key,
2279
- mode=mode,
2280
- sections=sections,
2281
- preset=None,
2282
- ),
2283
- profile=profile,
2284
- publish=publish,
2285
- plan_tool_name="app_layout_plan",
2286
- apply_tool_name="app_layout_apply",
2287
- )
2288
- if not isinstance(plan_result, dict) or plan_result.get("status") != "success":
2289
- return plan_result
2290
- plan_args = plan_result.get("normalized_args")
2291
- if not isinstance(plan_args, dict):
2292
- plan_args = {}
2293
3526
  try:
2294
- parsed_mode = LayoutApplyMode(str(plan_args.get("mode") or mode))
2295
- parsed_sections = [LayoutSectionPatch.model_validate(item) for item in plan_args.get("sections") or []]
3527
+ parsed_mode = LayoutApplyMode(str(mode or "merge"))
3528
+ parsed_sections = [LayoutSectionPatch.model_validate(item) for item in sections or []]
2296
3529
  except (ValueError, ValidationError) as exc:
2297
3530
  return _validation_failure(
2298
3531
  str(exc),
@@ -2302,15 +3535,15 @@ class AiBuilderTools(ToolBase):
2302
3535
  "tool_name": "app_layout_apply",
2303
3536
  "arguments": {
2304
3537
  "profile": profile,
2305
- "app_key": str(plan_args.get("app_key") or app_key),
2306
- "mode": str(plan_args.get("mode") or "merge"),
3538
+ "app_key": app_key,
3539
+ "mode": str(mode or "merge"),
2307
3540
  "publish": publish,
2308
- "sections": plan_args.get("sections") or [{"title": "基础信息", "rows": [["字段A", "字段B"]]}],
3541
+ "sections": sections or [{"title": "基础信息", "rows": [["字段A", "字段B"]]}],
2309
3542
  },
2310
3543
  },
2311
3544
  )
2312
3545
  normalized_args = {
2313
- "app_key": str(plan_args.get("app_key") or app_key),
3546
+ "app_key": app_key,
2314
3547
  "mode": parsed_mode.value,
2315
3548
  "publish": publish,
2316
3549
  "sections": [section.model_dump(mode="json", exclude_none=True) for section in parsed_sections],
@@ -2318,7 +3551,7 @@ class AiBuilderTools(ToolBase):
2318
3551
  return _safe_tool_call(
2319
3552
  lambda: self._facade.app_layout_apply(
2320
3553
  profile=profile,
2321
- app_key=str(plan_args.get("app_key") or app_key),
3554
+ app_key=app_key,
2322
3555
  mode=parsed_mode,
2323
3556
  publish=publish,
2324
3557
  sections=parsed_sections,
@@ -2334,51 +3567,148 @@ class AiBuilderTools(ToolBase):
2334
3567
  *,
2335
3568
  profile: str,
2336
3569
  app_key: str,
2337
- spec: JSONObject,
3570
+ mode: str | None = None,
2338
3571
  publish: bool = True,
3572
+ nodes: list[JSONObject] | None = None,
3573
+ transitions: list[JSONObject] | None = None,
3574
+ spec: JSONObject | None = None,
2339
3575
  idempotency_key: str | None = None,
2340
3576
  schema_version: str | None = None,
3577
+ patch_nodes: list[JSONObject] | None = None,
2341
3578
  ) -> JSONObject:
2342
3579
  """执行应用相关逻辑。"""
2343
- if not isinstance(spec, dict) or not spec:
2344
- return _config_failure(
3580
+ if patch_nodes is not None:
3581
+ normalized_args = {
3582
+ "app_key": app_key,
3583
+ "publish": publish,
3584
+ "patch_nodes": patch_nodes,
3585
+ "idempotency_key": idempotency_key,
3586
+ "schema_version": schema_version,
3587
+ }
3588
+ return _attach_builder_apply_envelope("app_flow_apply", _safe_tool_call(
3589
+ lambda: self._facade.flow_patch_nodes(
3590
+ profile=profile,
3591
+ app_key=app_key,
3592
+ patch_nodes=patch_nodes,
3593
+ publish=publish,
3594
+ idempotency_key=idempotency_key,
3595
+ schema_version=schema_version,
3596
+ ),
3597
+ error_code="FLOW_PATCH_FAILED",
3598
+ normalized_args=normalized_args,
3599
+ suggested_next_call={"tool_name": "app_flow_get", "arguments": {"profile": profile, "app_key": app_key}},
3600
+ ))
3601
+ if spec is not None:
3602
+ normalized_args = {
3603
+ "app_key": app_key,
3604
+ "publish": publish,
3605
+ "spec": spec,
3606
+ "idempotency_key": idempotency_key,
3607
+ "schema_version": schema_version,
3608
+ }
3609
+ return _attach_builder_apply_envelope("app_flow_apply", _safe_tool_call(
3610
+ lambda: self._facade.flow_apply(
3611
+ profile=profile,
3612
+ app_key=app_key,
3613
+ spec=spec,
3614
+ publish=publish,
3615
+ idempotency_key=idempotency_key,
3616
+ schema_version=schema_version,
3617
+ ),
3618
+ error_code="FLOW_SPEC_APPLY_FAILED",
3619
+ normalized_args=normalized_args,
3620
+ suggested_next_call={"tool_name": "app_flow_get", "arguments": {"profile": profile, "app_key": app_key}},
3621
+ ))
3622
+ if nodes or transitions:
3623
+ effective_nodes = nodes or []
3624
+ effective_transitions = transitions or []
3625
+ result = self._app_flow_apply_once(
3626
+ profile=profile,
3627
+ app_key=app_key,
3628
+ mode=mode or "replace",
3629
+ publish=publish,
3630
+ nodes=effective_nodes,
3631
+ transitions=effective_transitions,
3632
+ )
3633
+ result = self._retry_after_self_lock_release(
3634
+ profile=profile,
3635
+ result=result,
3636
+ retry_call=lambda: self._app_flow_apply_once(
3637
+ profile=profile,
3638
+ app_key=app_key,
3639
+ mode=mode or "replace",
3640
+ publish=publish,
3641
+ nodes=effective_nodes,
3642
+ transitions=effective_transitions,
3643
+ ),
3644
+ )
3645
+ return _attach_builder_apply_envelope("app_flow_apply", result)
3646
+ normalized_args = {"app_key": app_key, "publish": publish}
3647
+ return _attach_builder_apply_envelope("app_flow_apply", _failed(
3648
+ "FLOW_SPEC_OR_PATCH_REQUIRED",
3649
+ "app_flow_apply requires spec for a complete WorkflowSpec write or patch_nodes for targeted edits.",
3650
+ normalized_args=normalized_args,
3651
+ suggested_next_call={"tool_name": "app_flow_get_schema", "arguments": {"profile": profile}},
3652
+ ))
3653
+
3654
+ def _app_flow_apply_once(
3655
+ self,
3656
+ *,
3657
+ profile: str,
3658
+ app_key: str,
3659
+ mode: str = "replace",
3660
+ publish: bool = True,
3661
+ nodes: list[JSONObject],
3662
+ transitions: list[JSONObject],
3663
+ ) -> JSONObject:
3664
+ """执行内部辅助逻辑。"""
3665
+ try:
3666
+ request = FlowPlanRequest.model_validate(
3667
+ {
3668
+ "app_key": app_key,
3669
+ "mode": mode,
3670
+ "nodes": nodes or [],
3671
+ "transitions": transitions or [],
3672
+ "preset": None,
3673
+ }
3674
+ )
3675
+ except ValidationError as exc:
3676
+ return _validation_failure(
3677
+ str(exc),
2345
3678
  tool_name="app_flow_apply",
2346
- message="app_flow_apply requires a non-empty WorkflowSpecDTO `spec` object.",
2347
- fix_hint="Call app_flow_get_schema, then app_flow_get for GET-first baseline, patch spec, and apply.",
3679
+ exc=exc,
3680
+ suggested_next_call={
3681
+ "tool_name": "app_flow_apply",
3682
+ "arguments": {
3683
+ "profile": profile,
3684
+ "app_key": app_key,
3685
+ "mode": str(mode or "replace"),
3686
+ "publish": publish,
3687
+ "nodes": nodes or [{"id": "start", "type": "start", "name": "发起"}],
3688
+ "transitions": transitions or [],
3689
+ },
3690
+ },
2348
3691
  )
2349
3692
  normalized_args = {
2350
- "app_key": app_key,
3693
+ "app_key": request.app_key,
3694
+ "mode": request.mode,
2351
3695
  "publish": publish,
2352
- "spec": spec,
2353
- "idempotency_key": idempotency_key,
2354
- "schema_version": schema_version,
3696
+ "nodes": [node.model_dump(mode="json") for node in request.nodes],
3697
+ "transitions": [transition.model_dump(mode="json", by_alias=True) for transition in request.transitions],
2355
3698
  }
2356
- result = _safe_tool_call(
2357
- lambda: self._facade.flow_apply(
3699
+ return _safe_tool_call(
3700
+ lambda: self._facade.app_flow_apply(
2358
3701
  profile=profile,
2359
- app_key=app_key,
2360
- spec=spec,
3702
+ app_key=request.app_key,
3703
+ mode=request.mode,
2361
3704
  publish=publish,
2362
- idempotency_key=idempotency_key,
2363
- schema_version=schema_version,
3705
+ nodes=[node.model_dump(mode="json") for node in request.nodes],
3706
+ transitions=[transition.model_dump(mode="json", by_alias=True) for transition in request.transitions],
2364
3707
  ),
2365
3708
  error_code="FLOW_APPLY_FAILED",
2366
3709
  normalized_args=normalized_args,
2367
3710
  suggested_next_call={"tool_name": "app_flow_apply", "arguments": {"profile": profile, **normalized_args}},
2368
3711
  )
2369
- result = self._retry_after_self_lock_release(
2370
- profile=profile,
2371
- result=result,
2372
- retry_call=lambda: self._facade.flow_apply(
2373
- profile=profile,
2374
- app_key=app_key,
2375
- spec=spec,
2376
- publish=publish,
2377
- idempotency_key=idempotency_key,
2378
- schema_version=schema_version,
2379
- ),
2380
- )
2381
- return _attach_builder_apply_envelope("app_flow_apply", result)
2382
3712
 
2383
3713
  @tool_cn_name("应用视图应用")
2384
3714
  def app_views_apply(
@@ -2390,8 +3720,31 @@ class AiBuilderTools(ToolBase):
2390
3720
  upsert_views: list[JSONObject],
2391
3721
  patch_views: list[JSONObject] | None = None,
2392
3722
  remove_views: list[str],
3723
+ apps: list[JSONObject] | None = None,
3724
+ views: list[JSONObject] | None = None,
2393
3725
  ) -> JSONObject:
2394
3726
  """执行应用相关逻辑。"""
3727
+ if views is not None:
3728
+ return self._app_views_apply_view_batch(
3729
+ profile=profile,
3730
+ publish=publish,
3731
+ views=views,
3732
+ )
3733
+ if apps is not None:
3734
+ return self._apply_app_batch(
3735
+ tool_name="app_views_apply",
3736
+ profile=profile,
3737
+ apps=apps,
3738
+ apply_one=lambda _index, item, item_app_key: self.app_views_apply(
3739
+ profile=profile,
3740
+ app_key=item_app_key,
3741
+ publish=_payload_bool(item, "publish", default=publish),
3742
+ upsert_views=_payload_list(item, "upsert_views", "upsertViews", "views", default=upsert_views or []),
3743
+ patch_views=_payload_list(item, "patch_views", "patchViews", default=patch_views or []),
3744
+ remove_views=_payload_list(item, "remove_views", "removeViews", default=remove_views or []),
3745
+ apps=None,
3746
+ ),
3747
+ )
2395
3748
  result = self._app_views_apply_once(
2396
3749
  profile=profile,
2397
3750
  app_key=app_key,
@@ -2414,6 +3767,248 @@ class AiBuilderTools(ToolBase):
2414
3767
  )
2415
3768
  return _attach_builder_apply_envelope("app_views_apply", result)
2416
3769
 
3770
+ def _app_views_apply_view_batch(
3771
+ self,
3772
+ *,
3773
+ profile: str,
3774
+ publish: bool,
3775
+ views: list[JSONObject],
3776
+ ) -> JSONObject:
3777
+ started_at = time.perf_counter()
3778
+ if not isinstance(views, list) or not views:
3779
+ return _attach_builder_apply_envelope(
3780
+ "app_views_apply",
3781
+ _config_failure(
3782
+ tool_name="app_views_apply",
3783
+ message="app_views_apply views[] batch mode requires a non-empty views array.",
3784
+ fix_hint="Pass views as a JSON array; each item must include app_key and operation.",
3785
+ details={"expected_shape": {"views": [{"operation": "upsert", "app_key": "APP_KEY", "name": "业务视图", "type": "table", "columns": ["字段A"]}]}},
3786
+ ),
3787
+ )
3788
+
3789
+ def _infer_operation(item: JSONObject) -> str:
3790
+ raw = str(item.get("operation") or item.get("op") or "").strip().lower()
3791
+ aliases = {
3792
+ "create": "upsert",
3793
+ "update": "upsert",
3794
+ "replace": "upsert",
3795
+ "delete": "remove",
3796
+ "remove": "remove",
3797
+ "patch": "patch",
3798
+ "upsert": "upsert",
3799
+ }
3800
+ if raw:
3801
+ return aliases.get(raw, raw)
3802
+ if "set" in item or "unset" in item or "update" in item or "values" in item:
3803
+ return "patch"
3804
+ if "view_key" in item or "viewKey" in item:
3805
+ return "patch" if ("name" in item or "type" in item or "columns" in item or "fields" in item) else "remove"
3806
+ return "upsert"
3807
+
3808
+ def _view_job_payload(item: JSONObject) -> JSONObject:
3809
+ return {
3810
+ key: deepcopy(value)
3811
+ for key, value in item.items()
3812
+ if key
3813
+ not in {
3814
+ "operation",
3815
+ "op",
3816
+ "app_key",
3817
+ "appKey",
3818
+ "publish",
3819
+ }
3820
+ }
3821
+
3822
+ def _view_selector(payload: JSONObject) -> str:
3823
+ return str(payload.get("view_key") or payload.get("viewKey") or payload.get("viewgraphKey") or payload.get("viewGraphKey") or payload.get("name") or "").strip()
3824
+
3825
+ normalized_jobs: list[JSONObject] = []
3826
+ job_results: list[JSONObject | None] = [None for _ in views]
3827
+ errors: list[JSONObject] = []
3828
+ seen_patch_remove: dict[tuple[str, str], int] = {}
3829
+ seen_upsert_names: dict[tuple[str, str], int] = {}
3830
+
3831
+ def _add_validation_error(index: int, error_code: str, message: str, *, app_key: str = "", operation: str = "", reason_path: str = "") -> None:
3832
+ error = {
3833
+ "index": index,
3834
+ "app_key": app_key,
3835
+ "operation": operation,
3836
+ "status": "failed",
3837
+ "error_code": error_code,
3838
+ "message": message,
3839
+ **({"reason_path": reason_path} if reason_path else {}),
3840
+ }
3841
+ errors.append(error)
3842
+ job_results[index] = error
3843
+
3844
+ for index, item in enumerate(views):
3845
+ if not isinstance(item, dict):
3846
+ _add_validation_error(index, "VIEWS_FILE_ITEM_INVALID", "views[] item must be an object", reason_path=f"views[{index}]")
3847
+ continue
3848
+ app_key = str(item.get("app_key") or item.get("appKey") or "").strip()
3849
+ operation = _infer_operation(item)
3850
+ payload = _view_job_payload(item)
3851
+ if not app_key:
3852
+ _add_validation_error(index, "VIEWS_FILE_APP_KEY_REQUIRED", "views[] item requires app_key", operation=operation, reason_path=f"views[{index}].app_key")
3853
+ continue
3854
+ if operation not in {"upsert", "patch", "remove"}:
3855
+ _add_validation_error(index, "VIEWS_FILE_OPERATION_INVALID", "views[].operation must be upsert, patch, or remove", app_key=app_key, operation=operation, reason_path=f"views[{index}].operation")
3856
+ continue
3857
+ selector = _view_selector(payload)
3858
+ if operation in {"patch", "remove"} and not selector:
3859
+ _add_validation_error(index, "VIEWS_FILE_VIEW_SELECTOR_REQUIRED", f"{operation} view job requires view_key or name", app_key=app_key, operation=operation, reason_path=f"views[{index}].view_key")
3860
+ continue
3861
+ if operation in {"patch", "remove"} and selector:
3862
+ conflict_key = (app_key, selector)
3863
+ first_index = seen_patch_remove.get(conflict_key)
3864
+ if first_index is not None:
3865
+ _add_validation_error(index, "DUPLICATE_VIEW_JOB_CONFLICT", f"views[{index}] conflicts with views[{first_index}] for the same app/view selector", app_key=app_key, operation=operation, reason_path=f"views[{index}]")
3866
+ continue
3867
+ seen_patch_remove[conflict_key] = index
3868
+ if operation == "upsert":
3869
+ name = str(payload.get("name") or "").strip()
3870
+ if name:
3871
+ conflict_key = (app_key, name)
3872
+ first_index = seen_upsert_names.get(conflict_key)
3873
+ if first_index is not None:
3874
+ _add_validation_error(index, "DUPLICATE_VIEW_NAME_IN_BATCH", f"views[{index}] duplicates upsert view name from views[{first_index}] in the same app", app_key=app_key, operation=operation, reason_path=f"views[{index}].name")
3875
+ continue
3876
+ seen_upsert_names[conflict_key] = index
3877
+ normalized_jobs.append(
3878
+ {
3879
+ "index": index,
3880
+ "app_key": app_key,
3881
+ "operation": operation,
3882
+ "publish": _payload_bool(item, "publish", default=publish),
3883
+ "payload": payload,
3884
+ }
3885
+ )
3886
+
3887
+ def _extract_view_identity(result: JSONObject, fallback_payload: JSONObject, fallback_operation: str) -> tuple[str | None, str | None, str]:
3888
+ diff = result.get("views_diff") if isinstance(result.get("views_diff"), dict) else {}
3889
+ for key, operation_name in (("created", "created"), ("updated", "updated"), ("removed", "removed"), ("failed", "failed")):
3890
+ entries = diff.get(key)
3891
+ if isinstance(entries, list) and entries:
3892
+ entry = entries[0]
3893
+ if isinstance(entry, dict):
3894
+ return (
3895
+ str(entry.get("name") or entry.get("view_name") or entry.get("viewName") or fallback_payload.get("name") or "").strip() or None,
3896
+ str(entry.get("view_key") or entry.get("viewKey") or fallback_payload.get("view_key") or fallback_payload.get("viewKey") or "").strip() or None,
3897
+ operation_name,
3898
+ )
3899
+ if entry not in (None, ""):
3900
+ return (str(entry), None, operation_name)
3901
+ fallback_name = str(fallback_payload.get("name") or "").strip() or None
3902
+ fallback_view_key = str(fallback_payload.get("view_key") or fallback_payload.get("viewKey") or "").strip() or None
3903
+ return fallback_name, fallback_view_key, "removed" if fallback_operation == "remove" else "updated"
3904
+
3905
+ def _run_view_job(job: JSONObject) -> JSONObject:
3906
+ payload = deepcopy(job["payload"])
3907
+ operation = str(job["operation"])
3908
+ app_key = str(job["app_key"])
3909
+ job_publish = bool(job.get("publish"))
3910
+ if operation == "upsert":
3911
+ call_kwargs = {"upsert_views": [payload], "patch_views": [], "remove_views": []}
3912
+ elif operation == "patch":
3913
+ call_kwargs = {"upsert_views": [], "patch_views": [payload], "remove_views": []}
3914
+ else:
3915
+ call_kwargs = {"upsert_views": [], "patch_views": [], "remove_views": [_view_selector(payload)]}
3916
+ result = self._app_views_apply_once(
3917
+ profile=profile,
3918
+ app_key=app_key,
3919
+ publish=job_publish,
3920
+ **call_kwargs,
3921
+ )
3922
+ result = self._retry_after_self_lock_release(
3923
+ profile=profile,
3924
+ result=result,
3925
+ retry_call=lambda: self._app_views_apply_once(
3926
+ profile=profile,
3927
+ app_key=app_key,
3928
+ publish=job_publish,
3929
+ **call_kwargs,
3930
+ ),
3931
+ )
3932
+ status = str(result.get("status") or "success") if isinstance(result, dict) else "failed"
3933
+ name, view_key, actual_operation = _extract_view_identity(result if isinstance(result, dict) else {}, payload, operation)
3934
+ item: JSONObject = {
3935
+ "index": job["index"],
3936
+ "app_key": app_key,
3937
+ "operation": actual_operation if status != "failed" else operation,
3938
+ "requested_operation": operation,
3939
+ "status": status,
3940
+ **({"name": name} if name else {}),
3941
+ **({"view_key": view_key} if view_key else {}),
3942
+ "result": result if isinstance(result, dict) else {"status": "failed", "message": str(result)},
3943
+ }
3944
+ if status == "failed":
3945
+ item["error_code"] = item["result"].get("error_code")
3946
+ item["message"] = item["result"].get("message")
3947
+ return item
3948
+
3949
+ view_write_started_at = time.perf_counter()
3950
+ view_workers = min(VIEW_APPLY_PARALLEL_LIMIT, len(normalized_jobs)) if normalized_jobs else 0
3951
+ if normalized_jobs:
3952
+ with ThreadPoolExecutor(max_workers=view_workers) as executor:
3953
+ future_map = {executor.submit(_run_view_job, job): job for job in normalized_jobs}
3954
+ for future in as_completed(future_map):
3955
+ job = future_map[future]
3956
+ try:
3957
+ item_result = future.result()
3958
+ except (QingflowApiError, RuntimeError) as error:
3959
+ api_error = _coerce_api_error(error)
3960
+ item_result = {
3961
+ "index": job["index"],
3962
+ "app_key": job["app_key"],
3963
+ "operation": job["operation"],
3964
+ "requested_operation": job["operation"],
3965
+ "status": "failed",
3966
+ "error_code": "VIEW_BATCH_JOB_FAILED",
3967
+ "message": _public_error_message("VIEW_BATCH_JOB_FAILED", api_error),
3968
+ "details": {"transport_error": _transport_error_payload(api_error)},
3969
+ "request_id": api_error.request_id,
3970
+ "backend_code": api_error.backend_code,
3971
+ "http_status": None if api_error.http_status == 404 else api_error.http_status,
3972
+ }
3973
+ job_results[int(job["index"])] = item_result
3974
+ view_write_parallel_wall_ms = _elapsed_ms(view_write_started_at)
3975
+
3976
+ final_views = [item if isinstance(item, dict) else {"index": index, "status": "failed", "error_code": "VIEW_BATCH_JOB_MISSING", "message": "view batch job did not produce a result"} for index, item in enumerate(job_results)]
3977
+ errors.extend(item for item in final_views if str(item.get("status") or "") == "failed" and item not in errors)
3978
+ partial = sum(1 for item in final_views if str(item.get("status") or "") == "partial_success")
3979
+ succeeded = sum(1 for item in final_views if str(item.get("status") or "") in {"success", "partial_success"})
3980
+ failed = len(final_views) - succeeded
3981
+ status = "failed" if succeeded == 0 else "partial_success" if failed > 0 or partial > 0 else "success"
3982
+ write_executed = any(
3983
+ isinstance(item.get("result"), dict) and bool(item["result"].get("write_executed"))
3984
+ for item in final_views
3985
+ )
3986
+ payload: JSONObject = {
3987
+ "status": status,
3988
+ "error_code": None if status == "success" else "APP_VIEWS_APPLY_BATCH_PARTIAL" if succeeded else "APP_VIEWS_APPLY_BATCH_FAILED",
3989
+ "recoverable": failed > 0 or partial > 0,
3990
+ "message": f"applied app_views_apply to {succeeded}/{len(final_views)} views" if status != "success" else f"applied app_views_apply to {succeeded} views",
3991
+ "normalized_args": {"views": views},
3992
+ "views": final_views,
3993
+ "errors": errors,
3994
+ "parallelism": {"view_workers": view_workers, "same_app_parallel": True},
3995
+ "verification": {
3996
+ "batch_verified": failed == 0 and partial == 0,
3997
+ "succeeded": succeeded,
3998
+ "partial": partial,
3999
+ "failed": failed,
4000
+ },
4001
+ "write_executed": write_executed,
4002
+ "write_succeeded": write_executed and failed == 0 and partial == 0,
4003
+ "safe_to_retry": not write_executed,
4004
+ }
4005
+ _merge_duration_breakdown(
4006
+ payload,
4007
+ view_write_parallel_wall_ms=view_write_parallel_wall_ms,
4008
+ total_ms=_elapsed_ms(started_at),
4009
+ )
4010
+ return _attach_builder_apply_envelope("app_views_apply", payload)
4011
+
2417
4012
  def _app_views_apply_once(
2418
4013
  self,
2419
4014
  *,
@@ -2428,77 +4023,17 @@ class AiBuilderTools(ToolBase):
2428
4023
  reserved_failure = _reserved_system_view_name_failure(
2429
4024
  tool_name="app_views_apply",
2430
4025
  profile=profile,
2431
- app_key=app_key,
2432
- publish=publish,
2433
- upsert_views=upsert_views,
2434
- patch_views=patch_views,
2435
- remove_views=remove_views,
2436
- )
2437
- if reserved_failure is not None:
2438
- return reserved_failure
2439
- if patch_views:
2440
- try:
2441
- parsed_views = [ViewUpsertPatch.model_validate(item) for item in (upsert_views or [])]
2442
- parsed_patch_views = [ViewPartialPatch.model_validate(item) for item in patch_views]
2443
- except ValidationError as exc:
2444
- return _visibility_validation_failure(
2445
- str(exc),
2446
- tool_name="app_views_apply",
2447
- exc=exc,
2448
- suggested_next_call={
2449
- "tool_name": "app_views_apply",
2450
- "arguments": {
2451
- "profile": profile,
2452
- "app_key": app_key,
2453
- "publish": publish,
2454
- "upsert_views": upsert_views or [],
2455
- "patch_views": [
2456
- {"view_key": "VIEW_KEY", "set": {"query_conditions": {"enabled": True, "rows": [["字段A"]]}}},
2457
- ],
2458
- "remove_views": remove_views or [],
2459
- },
2460
- },
2461
- )
2462
- normalized_args = {
2463
- "app_key": app_key,
2464
- "publish": publish,
2465
- "upsert_views": [view.model_dump(mode="json") for view in parsed_views],
2466
- "patch_views": [patch.model_dump(mode="json") for patch in parsed_patch_views],
2467
- "remove_views": list(remove_views or []),
2468
- }
2469
- return _safe_tool_call(
2470
- lambda: self._facade.app_views_apply(
2471
- profile=profile,
2472
- app_key=app_key,
2473
- publish=publish,
2474
- upsert_views=parsed_views,
2475
- patch_views=parsed_patch_views,
2476
- remove_views=list(remove_views or []),
2477
- ),
2478
- error_code="VIEWS_APPLY_FAILED",
2479
- normalized_args=normalized_args,
2480
- suggested_next_call={"tool_name": "app_views_apply", "arguments": {"profile": profile, **normalized_args}},
2481
- )
2482
- plan_result = self._rewrite_plan_result_for_apply(
2483
- result=self.app_views_plan(
2484
- profile=profile,
2485
- app_key=app_key,
2486
- upsert_views=upsert_views,
2487
- remove_views=remove_views,
2488
- preset=None,
2489
- ),
2490
- profile=profile,
4026
+ app_key=app_key,
2491
4027
  publish=publish,
2492
- plan_tool_name="app_views_plan",
2493
- apply_tool_name="app_views_apply",
4028
+ upsert_views=upsert_views,
4029
+ patch_views=patch_views,
4030
+ remove_views=remove_views,
2494
4031
  )
2495
- if not isinstance(plan_result, dict) or plan_result.get("status") != "success":
2496
- return plan_result
2497
- plan_args = plan_result.get("normalized_args")
2498
- if not isinstance(plan_args, dict):
2499
- plan_args = {}
4032
+ if reserved_failure is not None:
4033
+ return reserved_failure
2500
4034
  try:
2501
- parsed_views = [ViewUpsertPatch.model_validate(item) for item in plan_args.get("upsert_views") or []]
4035
+ parsed_views = [ViewUpsertPatch.model_validate(item) for item in (upsert_views or [])]
4036
+ parsed_patch_views = [ViewPartialPatch.model_validate(item) for item in (patch_views or [])]
2502
4037
  except ValidationError as exc:
2503
4038
  return _visibility_validation_failure(
2504
4039
  str(exc),
@@ -2508,27 +4043,29 @@ class AiBuilderTools(ToolBase):
2508
4043
  "tool_name": "app_views_apply",
2509
4044
  "arguments": {
2510
4045
  "profile": profile,
2511
- "app_key": str(plan_args.get("app_key") or app_key),
4046
+ "app_key": app_key,
2512
4047
  "publish": publish,
2513
- "upsert_views": plan_args.get("upsert_views") or [{"name": "业务台账视图", "type": "table", "columns": ["字段A"]}],
2514
- "remove_views": plan_args.get("remove_views") or [],
4048
+ "upsert_views": upsert_views or [{"name": "业务台账视图", "type": "table", "columns": ["字段A"]}],
4049
+ "patch_views": patch_views or [],
4050
+ "remove_views": remove_views or [],
2515
4051
  },
2516
4052
  },
2517
4053
  )
2518
4054
  normalized_args = {
2519
- "app_key": str(plan_args.get("app_key") or app_key),
4055
+ "app_key": app_key,
2520
4056
  "publish": publish,
2521
- "upsert_views": [view.model_dump(mode="json") for view in parsed_views],
2522
- "remove_views": list(plan_args.get("remove_views") or remove_views),
4057
+ "upsert_views": [public_view_upsert_payload(view) for view in parsed_views],
4058
+ "patch_views": [public_view_partial_payload(patch) for patch in parsed_patch_views],
4059
+ "remove_views": list(remove_views or []),
2523
4060
  }
2524
4061
  return _safe_tool_call(
2525
4062
  lambda: self._facade.app_views_apply(
2526
4063
  profile=profile,
2527
- app_key=str(plan_args.get("app_key") or app_key),
4064
+ app_key=app_key,
2528
4065
  publish=publish,
2529
4066
  upsert_views=parsed_views,
2530
- patch_views=[],
2531
- remove_views=list(plan_args.get("remove_views") or remove_views),
4067
+ patch_views=parsed_patch_views,
4068
+ remove_views=list(remove_views or []),
2532
4069
  ),
2533
4070
  error_code="VIEWS_APPLY_FAILED",
2534
4071
  normalized_args=normalized_args,
@@ -2566,8 +4103,24 @@ class AiBuilderTools(ToolBase):
2566
4103
  remove_chart_ids: list[str],
2567
4104
  reorder_chart_ids: list[str],
2568
4105
  patch_charts: list[JSONObject] | None = None,
4106
+ apps: list[JSONObject] | None = None,
2569
4107
  ) -> JSONObject:
2570
4108
  """执行应用相关逻辑。"""
4109
+ if apps is not None:
4110
+ return self._apply_app_batch(
4111
+ tool_name="app_charts_apply",
4112
+ profile=profile,
4113
+ apps=apps,
4114
+ apply_one=lambda _index, item, item_app_key: self.app_charts_apply(
4115
+ profile=profile,
4116
+ app_key=item_app_key,
4117
+ upsert_charts=_payload_list(item, "upsert_charts", "upsertCharts", "charts", default=upsert_charts or []),
4118
+ patch_charts=_payload_list(item, "patch_charts", "patchCharts", default=patch_charts or []),
4119
+ remove_chart_ids=_payload_list(item, "remove_chart_ids", "removeChartIds", default=remove_chart_ids or []),
4120
+ reorder_chart_ids=_payload_list(item, "reorder_chart_ids", "reorderChartIds", default=reorder_chart_ids or []),
4121
+ apps=None,
4122
+ ),
4123
+ )
2571
4124
  try:
2572
4125
  request = ChartApplyRequest.model_validate(
2573
4126
  {
@@ -2623,8 +4176,26 @@ class AiBuilderTools(ToolBase):
2623
4176
  dash_global_config: JSONObject | None = None,
2624
4177
  config: JSONObject | None = None,
2625
4178
  payload: JSONObject | None = None,
4179
+ patch_sections: list[JSONObject] | None = None,
2626
4180
  ) -> JSONObject:
2627
4181
  """执行门户相关逻辑。"""
4182
+ if patch_sections is not None:
4183
+ normalized_args = {
4184
+ "dash_key": dash_key,
4185
+ "publish": publish,
4186
+ "patch_sections": patch_sections,
4187
+ }
4188
+ return _attach_builder_apply_envelope("portal_apply", _publicize_package_fields(_safe_tool_call(
4189
+ lambda: self._facade.portal_patch_sections(
4190
+ profile=profile,
4191
+ dash_key=dash_key,
4192
+ patch_sections=patch_sections,
4193
+ publish=publish,
4194
+ ),
4195
+ error_code="PORTAL_PATCH_FAILED",
4196
+ normalized_args=normalized_args,
4197
+ suggested_next_call={"tool_name": "portal_get", "arguments": {"profile": profile, "dash_key": dash_key}},
4198
+ )))
2628
4199
  request_payload: dict[str, Any] = dict(payload) if isinstance(payload, dict) else {}
2629
4200
  if dash_key:
2630
4201
  request_payload["dash_key"] = dash_key
@@ -2719,9 +4290,22 @@ class AiBuilderTools(ToolBase):
2719
4290
  *,
2720
4291
  profile: str,
2721
4292
  app_key: str,
4293
+ app_keys: list[str] | None = None,
2722
4294
  expected_package_id: int | None = None,
2723
4295
  ) -> JSONObject:
2724
4296
  """执行应用相关逻辑。"""
4297
+ if app_keys is not None:
4298
+ return self._apply_app_batch(
4299
+ tool_name="app_publish_verify",
4300
+ profile=profile,
4301
+ apps=[{"app_key": item, "expected_package_id": expected_package_id} for item in app_keys],
4302
+ apply_one=lambda _index, item, item_app_key: self.app_publish_verify(
4303
+ profile=profile,
4304
+ app_key=item_app_key,
4305
+ app_keys=None,
4306
+ expected_package_id=item.get("expected_package_id") or item.get("expectedPackageId") or expected_package_id,
4307
+ ),
4308
+ )
2725
4309
  normalized_args = {"app_key": app_key, "expected_package_id": expected_package_id}
2726
4310
  result = _publicize_package_fields(_safe_tool_call(
2727
4311
  lambda: self._facade.app_publish_verify(profile=profile, app_key=app_key, expected_package_tag_id=expected_package_id),
@@ -2933,7 +4517,12 @@ def _schema_apps_expected_shape() -> JSONObject:
2933
4517
  "app_name": "员工花名册",
2934
4518
  "icon": "business-personalcard",
2935
4519
  "color": "emerald",
2936
- "add_fields": [{"name": "员工名称", "type": "text", "as_data_title": True}],
4520
+ "form": [
4521
+ {
4522
+ "section": "基础信息",
4523
+ "rows": [[{"name": "员工名称", "type": "text", "data_title": True}]],
4524
+ }
4525
+ ],
2937
4526
  }
2938
4527
  ],
2939
4528
  }
@@ -2942,7 +4531,7 @@ def _schema_apps_expected_shape() -> JSONObject:
2942
4531
  def _schema_apps_expected_shape_json() -> str:
2943
4532
  return (
2944
4533
  '{"package_id":1001,"apps":[{"client_key":"employee","app_name":"员工花名册",'
2945
- '"icon":"business-personalcard","color":"emerald","add_fields":[{"name":"员工名称","type":"text","as_data_title":true}]}]}'
4534
+ '"icon":"business-personalcard","color":"emerald","form":[{"section":"基础信息","rows":[[{"name":"员工名称","type":"text","data_title":true}]]}]}]}'
2946
4535
  )
2947
4536
 
2948
4537
 
@@ -3027,6 +4616,163 @@ def _normalize_schema_apps_argument(*, tool_name: str, package_id: int | None, a
3027
4616
  return {"package_id": normalized_package_id, "apps": normalized_items, "warnings": warnings}
3028
4617
 
3029
4618
 
4619
+ def _normalize_schema_form_field(field: JSONObject) -> JSONObject:
4620
+ payload = deepcopy(field)
4621
+ if "data_title" in payload and "as_data_title" not in payload and "asDataTitle" not in payload:
4622
+ payload["as_data_title"] = payload.pop("data_title")
4623
+ if "dataTitle" in payload and "as_data_title" not in payload and "asDataTitle" not in payload:
4624
+ payload["as_data_title"] = payload.pop("dataTitle")
4625
+ if "data_cover" in payload and "as_data_cover" not in payload and "asDataCover" not in payload:
4626
+ payload["as_data_cover"] = payload.pop("data_cover")
4627
+ if "dataCover" in payload and "as_data_cover" not in payload and "asDataCover" not in payload:
4628
+ payload["as_data_cover"] = payload.pop("dataCover")
4629
+ return payload
4630
+
4631
+
4632
+ def _schema_form_entry_name(entry: object) -> str:
4633
+ if isinstance(entry, dict):
4634
+ for key in ("name", "title", "label", "field_name", "fieldName"):
4635
+ if entry.get(key) is not None:
4636
+ return str(entry.get(key) or "").strip()
4637
+ return ""
4638
+ if isinstance(entry, (str, int)):
4639
+ return str(entry).strip()
4640
+ return ""
4641
+
4642
+
4643
+ def _schema_form_failure(*, tool_name: str, path: str, message: str, fix_hint: str) -> JSONObject:
4644
+ return _config_failure(
4645
+ tool_name=tool_name,
4646
+ error_code="FORM_SHAPE_INVALID",
4647
+ message=message,
4648
+ fix_hint=fix_hint,
4649
+ details={
4650
+ "path": path,
4651
+ "expected_shape": {
4652
+ "form": [
4653
+ {
4654
+ "section": "基础信息",
4655
+ "rows": [
4656
+ [
4657
+ {"name": "标题", "type": "text", "data_title": True},
4658
+ {"name": "状态", "type": "single_select", "options": ["待处理", "处理中", "已完成"]},
4659
+ ]
4660
+ ],
4661
+ }
4662
+ ]
4663
+ },
4664
+ },
4665
+ )
4666
+
4667
+
4668
+ def _compile_schema_form_payload(*, tool_name: str, form: object, path: str = "form") -> JSONObject:
4669
+ if form is None:
4670
+ return {"add_fields": [], "layout_sections": []}
4671
+ if not isinstance(form, list) or not form:
4672
+ return {
4673
+ "failure": _schema_form_failure(
4674
+ tool_name=tool_name,
4675
+ path=path,
4676
+ message=f"{path} must be a non-empty list of form sections.",
4677
+ fix_hint='Use form=[{"section":"基础信息","rows":[[{"name":"标题","type":"text","data_title":true}]]}].',
4678
+ )
4679
+ }
4680
+
4681
+ add_fields: list[JSONObject] = []
4682
+ layout_sections: list[JSONObject] = []
4683
+ for section_index, section in enumerate(form):
4684
+ section_path = f"{path}[{section_index}]"
4685
+ if not isinstance(section, dict):
4686
+ return {
4687
+ "failure": _schema_form_failure(
4688
+ tool_name=tool_name,
4689
+ path=section_path,
4690
+ message=f"{section_path} must be an object.",
4691
+ fix_hint="Each form section must include section and rows.",
4692
+ )
4693
+ }
4694
+ section_title = str(section.get("section") or section.get("title") or section.get("name") or "").strip()
4695
+ rows = section.get("rows")
4696
+ if not section_title:
4697
+ return {
4698
+ "failure": _schema_form_failure(
4699
+ tool_name=tool_name,
4700
+ path=f"{section_path}.section",
4701
+ message=f"{section_path}.section is required.",
4702
+ fix_hint='Set a business section title such as "基础信息" or "生产信息".',
4703
+ )
4704
+ }
4705
+ if not isinstance(rows, list) or not rows:
4706
+ return {
4707
+ "failure": _schema_form_failure(
4708
+ tool_name=tool_name,
4709
+ path=f"{section_path}.rows",
4710
+ message=f"{section_path}.rows must be a non-empty list.",
4711
+ fix_hint="Use rows as a list of field rows; each row is a list of field objects.",
4712
+ )
4713
+ }
4714
+ layout_rows: list[list[str]] = []
4715
+ for row_index, row in enumerate(rows):
4716
+ row_path = f"{section_path}.rows[{row_index}]"
4717
+ if isinstance(row, dict):
4718
+ row_fields = row.get("fields")
4719
+ else:
4720
+ row_fields = row
4721
+ if not isinstance(row_fields, list) or not row_fields:
4722
+ return {
4723
+ "failure": _schema_form_failure(
4724
+ tool_name=tool_name,
4725
+ path=row_path,
4726
+ message=f"{row_path} must be a non-empty field list.",
4727
+ fix_hint='Use rows like [[{"name":"字段A","type":"text"},{"name":"字段B","type":"number"}]].',
4728
+ )
4729
+ }
4730
+ layout_row: list[str] = []
4731
+ for field_index, field in enumerate(row_fields):
4732
+ field_path = f"{row_path}[{field_index}]"
4733
+ field_name = _schema_form_entry_name(field)
4734
+ if not field_name:
4735
+ return {
4736
+ "failure": _schema_form_failure(
4737
+ tool_name=tool_name,
4738
+ path=field_path,
4739
+ message=f"{field_path} must include a field name.",
4740
+ fix_hint='Each new field object must include name, for example {"name":"工单编号","type":"text"}.',
4741
+ )
4742
+ }
4743
+ layout_row.append(field_name)
4744
+ if isinstance(field, dict):
4745
+ add_fields.append(_normalize_schema_form_field(field))
4746
+ layout_rows.append(layout_row)
4747
+ layout_sections.append({"title": section_title, "rows": layout_rows})
4748
+ return {"add_fields": add_fields, "layout_sections": layout_sections}
4749
+
4750
+
4751
+ def _apply_schema_form_to_payload(*, tool_name: str, payload: JSONObject, path: str) -> JSONObject:
4752
+ form = payload.get("form")
4753
+ if form is None:
4754
+ return {"payload": payload, "layout_sections": []}
4755
+ existing_add_fields = payload.get("add_fields") or payload.get("addFields")
4756
+ if isinstance(existing_add_fields, list) and existing_add_fields:
4757
+ return {
4758
+ "failure": _config_failure(
4759
+ tool_name=tool_name,
4760
+ error_code="FORM_WITH_ADD_FIELDS_CONFLICT",
4761
+ message=f"{path} cannot use form and add_fields together.",
4762
+ fix_hint="Use form as the single schema creation shape: put fields under form[].rows[][].",
4763
+ details={"path": path, "conflicting_keys": ["form", "add_fields"]},
4764
+ )
4765
+ }
4766
+ compiled = _compile_schema_form_payload(tool_name=tool_name, form=form, path=f"{path}.form")
4767
+ if isinstance(compiled.get("failure"), dict):
4768
+ return {"failure": compiled["failure"]}
4769
+ normalized = deepcopy(payload)
4770
+ normalized.pop("form", None)
4771
+ normalized["add_fields"] = compiled.get("add_fields") or []
4772
+ normalized["_inline_form_layout_sections"] = compiled.get("layout_sections") or []
4773
+ return {"payload": normalized, "layout_sections": compiled.get("layout_sections") or []}
4774
+
4775
+
3030
4776
  def _multi_app_item_app_name(item: JSONObject) -> str:
3031
4777
  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()
3032
4778
 
@@ -3048,7 +4794,7 @@ def _field_type_for_static_validation(field: JSONObject) -> str:
3048
4794
 
3049
4795
 
3050
4796
  def _is_data_title_field(field: JSONObject) -> bool:
3051
- return bool(field.get("as_data_title") or field.get("asDataTitle"))
4797
+ return bool(field.get("as_data_title") or field.get("asDataTitle") or field.get("data_title") or field.get("dataTitle"))
3052
4798
 
3053
4799
 
3054
4800
  def _collect_multi_app_target_refs(value: object, *, path: str) -> list[JSONObject]:
@@ -3075,7 +4821,6 @@ def _multi_app_static_validation_failure(
3075
4821
  *,
3076
4822
  tool_name: str,
3077
4823
  apps: list[JSONObject],
3078
- create_if_missing: bool,
3079
4824
  ) -> JSONObject | None:
3080
4825
  issues: list[JSONObject] = []
3081
4826
  client_key_indexes: dict[str, int] = {}
@@ -3138,17 +4883,6 @@ def _multi_app_static_validation_failure(
3138
4883
  }
3139
4884
  )
3140
4885
  continue
3141
- if new_app_item and not create_if_missing:
3142
- issues.append(
3143
- {
3144
- "index": index,
3145
- "row_number": index + 1,
3146
- "path": f"apps[{index}]",
3147
- "error_code": "CREATE_IF_MISSING_REQUIRED",
3148
- "message": "new multi-app items require create_if_missing=true",
3149
- "fix_hint": "Set create_if_missing=true, or pass app_key for an existing app.",
3150
- }
3151
- )
3152
4886
 
3153
4887
  add_fields = _multi_app_list_value(item, "add_fields", "addFields")
3154
4888
  if new_app_item:
@@ -3350,6 +5084,86 @@ def _compiled_multi_app_deferred_add_fields(compiled_item: JSONObject, existing_
3350
5084
  ]
3351
5085
 
3352
5086
 
5087
+ def _multi_app_child_diagnostics(result: JSONObject) -> JSONObject:
5088
+ diagnostics: JSONObject = {}
5089
+ for key in (
5090
+ "backend_code",
5091
+ "http_status",
5092
+ "request_id",
5093
+ "details",
5094
+ "suggested_next_call",
5095
+ "warnings",
5096
+ "publish_result",
5097
+ "duration_breakdown_ms",
5098
+ ):
5099
+ value = result.get(key)
5100
+ if value is None:
5101
+ continue
5102
+ if isinstance(value, (dict, list)) and not value:
5103
+ continue
5104
+ diagnostics[key] = deepcopy(value)
5105
+ return diagnostics
5106
+
5107
+
5108
+ def _multi_app_field_type(field: JSONObject) -> str:
5109
+ raw_type = str(field.get("type") or field.get("field_type") or field.get("fieldType") or "").strip()
5110
+ if raw_type:
5111
+ normalized = FIELD_TYPE_ALIASES.get(raw_type.lower())
5112
+ return normalized.value if normalized is not None else raw_type
5113
+ raw_type_id = field.get("type_id") or field.get("typeId") or field.get("field_type_id") or field.get("fieldTypeId")
5114
+ try:
5115
+ normalized_by_id = FIELD_TYPE_ID_ALIASES.get(int(raw_type_id))
5116
+ except (TypeError, ValueError):
5117
+ normalized_by_id = None
5118
+ return normalized_by_id.value if normalized_by_id is not None else ""
5119
+
5120
+
5121
+ def _multi_app_deferred_relation_retryable(result: JSONObject, deferred_add_fields: list[JSONObject]) -> bool:
5122
+ if not deferred_add_fields:
5123
+ return False
5124
+ if not any(_multi_app_field_type(field) == PublicFieldType.relation.value for field in deferred_add_fields if isinstance(field, dict)):
5125
+ return False
5126
+ if result.get("status") in {"success", "partial_success"}:
5127
+ return False
5128
+ if result.get("error_code") != "SCHEMA_APPLY_FAILED":
5129
+ return False
5130
+ if _schema_apply_result_has_write(result):
5131
+ return False
5132
+ return backend_code_value_int(_result_backend_code(result)) == 400
5133
+
5134
+
5135
+ def _result_backend_code(result: JSONObject) -> Any:
5136
+ if result.get("backend_code") is not None:
5137
+ return result.get("backend_code")
5138
+ details = result.get("details")
5139
+ if isinstance(details, dict):
5140
+ transport = details.get("transport_error")
5141
+ if isinstance(transport, dict):
5142
+ return transport.get("backend_code")
5143
+ return None
5144
+
5145
+
5146
+ def _multi_app_relation_target_app_keys(fields: list[JSONObject]) -> list[str]:
5147
+ app_keys: list[str] = []
5148
+
5149
+ def visit(value: Any) -> None:
5150
+ if isinstance(value, list):
5151
+ for item in value:
5152
+ visit(item)
5153
+ return
5154
+ if not isinstance(value, dict):
5155
+ return
5156
+ target_app_key = str(value.get("target_app_key") or value.get("targetAppKey") or "").strip()
5157
+ if target_app_key and target_app_key not in app_keys:
5158
+ app_keys.append(target_app_key)
5159
+ for child in value.values():
5160
+ if isinstance(child, (dict, list)):
5161
+ visit(child)
5162
+
5163
+ visit(fields)
5164
+ return app_keys
5165
+
5166
+
3353
5167
  def _multi_app_list_value(item: JSONObject, *keys: str) -> list[JSONObject]:
3354
5168
  for key in keys:
3355
5169
  value = item.get(key)
@@ -3527,7 +5341,6 @@ def _reserved_system_field_name_failure(
3527
5341
  icon: str | None = None,
3528
5342
  color: str | None = None,
3529
5343
  visibility: JSONObject | None = None,
3530
- create_if_missing: bool = False,
3531
5344
  publish: bool | None = None,
3532
5345
  add_fields: list[JSONObject] | None = None,
3533
5346
  update_fields: list[JSONObject] | None = None,
@@ -3547,7 +5360,6 @@ def _reserved_system_field_name_failure(
3547
5360
  "icon": icon or "",
3548
5361
  "color": color or "",
3549
5362
  "visibility": visibility,
3550
- "create_if_missing": create_if_missing,
3551
5363
  "add_fields": suggested_add_fields,
3552
5364
  "update_fields": update_fields or [],
3553
5365
  "remove_fields": remove_fields or [],
@@ -3576,7 +5388,6 @@ def _reserved_system_field_name_failure_for_apps(
3576
5388
  profile: str,
3577
5389
  package_id: int | None,
3578
5390
  visibility: JSONObject | None,
3579
- create_if_missing: bool,
3580
5391
  publish: bool,
3581
5392
  apps: list[JSONObject],
3582
5393
  ) -> JSONObject | None:
@@ -3592,7 +5403,6 @@ def _reserved_system_field_name_failure_for_apps(
3592
5403
  icon=str(item.get("icon") or ""),
3593
5404
  color=str(item.get("color") or ""),
3594
5405
  visibility=item.get("visibility") if isinstance(item.get("visibility"), dict) else visibility,
3595
- create_if_missing=create_if_missing,
3596
5406
  publish=publish,
3597
5407
  add_fields=list(item.get("add_fields") or item.get("addFields") or []),
3598
5408
  update_fields=list(item.get("update_fields") or item.get("updateFields") or []),
@@ -3610,7 +5420,6 @@ def _reserved_system_field_name_failure_for_apps(
3610
5420
  "profile": profile,
3611
5421
  "package_id": package_id,
3612
5422
  "visibility": visibility,
3613
- "create_if_missing": create_if_missing,
3614
5423
  "publish": publish,
3615
5424
  "apps": fixed_apps,
3616
5425
  }
@@ -3677,6 +5486,23 @@ def _reserved_system_view_name_failure(
3677
5486
  return None
3678
5487
 
3679
5488
 
5489
+ def _view_payload_has_action_buttons(
5490
+ *,
5491
+ upsert_views: list[JSONObject] | None = None,
5492
+ patch_views: list[JSONObject] | None = None,
5493
+ ) -> bool:
5494
+ for item in upsert_views or []:
5495
+ if isinstance(item, dict) and any(key in item for key in ("action_buttons", "actionButtons")):
5496
+ return True
5497
+ for item in patch_views or []:
5498
+ if not isinstance(item, dict):
5499
+ continue
5500
+ raw_set = item.get("set")
5501
+ if isinstance(raw_set, dict) and any(key in raw_set for key in ("action_buttons", "actionButtons")):
5502
+ return True
5503
+ return False
5504
+
5505
+
3680
5506
  def _config_failure(
3681
5507
  *,
3682
5508
  tool_name: str,
@@ -3768,12 +5594,13 @@ def _safe_tool_call(
3768
5594
  normalized_args: JSONObject,
3769
5595
  suggested_next_call: JSONObject | None,
3770
5596
  ) -> JSONObject:
5597
+ started_at = time.perf_counter()
3771
5598
  try:
3772
- return call()
5599
+ result = call()
3773
5600
  except (QingflowApiError, RuntimeError) as error:
3774
5601
  api_error = _coerce_api_error(error)
3775
5602
  public_http_status = None if api_error.http_status == 404 else api_error.http_status
3776
- return {
5603
+ result = {
3777
5604
  "status": "failed",
3778
5605
  "error_code": error_code,
3779
5606
  "recoverable": True,
@@ -3795,6 +5622,46 @@ def _safe_tool_call(
3795
5622
  "noop": False,
3796
5623
  "verification": {},
3797
5624
  }
5625
+ except ValueError as error:
5626
+ public_error_code, public_message = _public_value_error(error, fallback_error_code=error_code)
5627
+ result = {
5628
+ "status": "failed",
5629
+ "error_code": public_error_code,
5630
+ "recoverable": True,
5631
+ "message": public_message,
5632
+ "normalized_args": normalized_args,
5633
+ "missing_fields": [],
5634
+ "allowed_values": {},
5635
+ "details": {
5636
+ "validation_error": {
5637
+ "message": str(error),
5638
+ "fallback_error_code": error_code,
5639
+ }
5640
+ },
5641
+ "suggested_next_call": suggested_next_call,
5642
+ "write_executed": False,
5643
+ "safe_to_retry": True,
5644
+ "noop": False,
5645
+ "verification": {},
5646
+ }
5647
+ if isinstance(result, dict):
5648
+ _merge_duration_breakdown(result, tool_call_ms=_elapsed_ms(started_at))
5649
+ return result
5650
+
5651
+
5652
+ def _public_value_error(error: ValueError, *, fallback_error_code: str) -> tuple[str, str]:
5653
+ message = str(error).strip() or fallback_error_code
5654
+ prefix, separator, rest = message.partition(":")
5655
+ candidate = prefix.strip()
5656
+ if separator and _looks_like_public_error_code(candidate):
5657
+ return candidate, rest.strip() or message
5658
+ return fallback_error_code, message
5659
+
5660
+
5661
+ def _looks_like_public_error_code(value: str) -> bool:
5662
+ if not value or len(value) > 80 or not any(ch.isalpha() for ch in value):
5663
+ return False
5664
+ return all(ch.isupper() or ch.isdigit() or ch == "_" for ch in value)
3798
5665
 
3799
5666
 
3800
5667
  def _publicize_package_fields(value):
@@ -3874,6 +5741,8 @@ def _builder_apply_json_paths() -> JSONObject:
3874
5741
  "warnings": "$.warnings",
3875
5742
  "verification": "$.verification",
3876
5743
  "details": "$.details",
5744
+ "duration_breakdown_ms": "$.duration_breakdown_ms",
5745
+ "total_ms": "$.duration_breakdown_ms.total_ms",
3877
5746
  "write_executed": "$.write_executed",
3878
5747
  "write_may_have_succeeded": "$.write_may_have_succeeded",
3879
5748
  "safe_to_retry": "$.safe_to_retry",
@@ -3910,6 +5779,8 @@ def _builder_apply_summary(payload: JSONObject, resources: list[JSONObject]) ->
3910
5779
  and str(item.get("operation") or "") in {"updated", "layout_updated", "workflow_updated", "verified", "published"}
3911
5780
  )
3912
5781
  published_value = payload.get("published")
5782
+ if published_value is None:
5783
+ published_value = _builder_batch_published_value(payload)
3913
5784
  if published_value is None:
3914
5785
  publish_requested = payload.get("publish_requested")
3915
5786
  published_value = bool(publish_requested) and status in {"success", "partial_success"}
@@ -3939,6 +5810,24 @@ def _builder_apply_summary(payload: JSONObject, resources: list[JSONObject]) ->
3939
5810
  return summary
3940
5811
 
3941
5812
 
5813
+ def _builder_batch_published_value(payload: JSONObject) -> bool | None:
5814
+ apps = payload.get("apps")
5815
+ if not isinstance(apps, list) or not apps:
5816
+ return None
5817
+ values: list[bool] = []
5818
+ for item in apps:
5819
+ if not isinstance(item, dict):
5820
+ continue
5821
+ result = item.get("result") if isinstance(item.get("result"), dict) else {}
5822
+ if "published" in result:
5823
+ values.append(bool(result.get("published")))
5824
+ elif "published" in item:
5825
+ values.append(bool(item.get("published")))
5826
+ if not values:
5827
+ return None
5828
+ return all(values)
5829
+
5830
+
3942
5831
  def _builder_verification_truthy(value: object) -> bool:
3943
5832
  if value is None:
3944
5833
  return True
@@ -3956,6 +5845,8 @@ def _builder_apply_resources(tool_name: str, payload: JSONObject) -> list[JSONOb
3956
5845
  resources = _builder_package_resources(payload)
3957
5846
  elif tool_name == "app_schema_apply":
3958
5847
  resources = _builder_schema_resources(payload)
5848
+ elif isinstance(payload.get("apps"), list) and _builder_apply_tool_is_app_scoped(tool_name):
5849
+ resources = _builder_batch_app_resources(tool_name, payload)
3959
5850
  elif tool_name == "app_layout_apply":
3960
5851
  resources = [_builder_app_resource(payload, operation="layout_updated")]
3961
5852
  elif tool_name == "app_flow_apply":
@@ -3983,6 +5874,38 @@ def _builder_apply_resources(tool_name: str, payload: JSONObject) -> list[JSONOb
3983
5874
  return resources
3984
5875
 
3985
5876
 
5877
+ def _builder_batch_app_resources(tool_name: str, payload: JSONObject) -> list[JSONObject]:
5878
+ resources: list[JSONObject] = []
5879
+ operation_by_tool = {
5880
+ "app_schema_apply": "updated",
5881
+ "app_layout_apply": "layout_updated",
5882
+ "app_flow_apply": "workflow_updated",
5883
+ "app_views_apply": "updated",
5884
+ "app_custom_buttons_apply": "updated",
5885
+ "app_associated_resources_apply": "updated",
5886
+ "app_charts_apply": "updated",
5887
+ "app_publish_verify": "verified",
5888
+ }
5889
+ operation = operation_by_tool.get(tool_name, "updated")
5890
+ for item in payload.get("apps") or []:
5891
+ if not isinstance(item, dict):
5892
+ continue
5893
+ result = item.get("result") if isinstance(item.get("result"), dict) else {}
5894
+ nested_resources = result.get("resources") if isinstance(result, dict) else None
5895
+ if isinstance(nested_resources, list) and nested_resources:
5896
+ resources.extend(resource for resource in nested_resources if isinstance(resource, dict))
5897
+ continue
5898
+ app_payload: JSONObject = {
5899
+ **(result if isinstance(result, dict) else {}),
5900
+ "app_key": item.get("app_key") or (result.get("app_key") if isinstance(result, dict) else None),
5901
+ "status": item.get("status") or (result.get("status") if isinstance(result, dict) else None),
5902
+ "error_code": item.get("error_code") or (result.get("error_code") if isinstance(result, dict) else None),
5903
+ "message": item.get("message") or (result.get("message") if isinstance(result, dict) else None),
5904
+ }
5905
+ resources.append(_builder_app_resource(app_payload, operation=operation))
5906
+ return resources
5907
+
5908
+
3986
5909
  def _builder_apply_tool_is_app_scoped(tool_name: str) -> bool:
3987
5910
  return tool_name in {
3988
5911
  "app_schema_apply",
@@ -4290,6 +6213,33 @@ def _builder_field_resources(field_diff: object, *, parent: JSONObject | None) -
4290
6213
 
4291
6214
 
4292
6215
  def _builder_view_resources(payload: JSONObject) -> list[JSONObject]:
6216
+ if isinstance(payload.get("views"), list):
6217
+ resources: list[JSONObject] = []
6218
+ for item in payload.get("views") or []:
6219
+ if not isinstance(item, dict):
6220
+ continue
6221
+ result = item.get("result") if isinstance(item.get("result"), dict) else {}
6222
+ parent = _builder_app_parent({"app_key": item.get("app_key") or result.get("app_key")})
6223
+ operation = str(item.get("operation") or item.get("requested_operation") or "updated")
6224
+ if operation == "upsert":
6225
+ operation = "updated"
6226
+ resources.append(
6227
+ _builder_resource(
6228
+ resource_type="view",
6229
+ operation=operation,
6230
+ status=str(item.get("status") or result.get("status") or "success"),
6231
+ key=item.get("view_key") or item.get("viewKey"),
6232
+ name=item.get("name") or item.get("view_name") or item.get("viewName"),
6233
+ ids={
6234
+ **({"view_key": item.get("view_key") or item.get("viewKey")} if item.get("view_key") or item.get("viewKey") else {}),
6235
+ **({"app_key": item.get("app_key")} if item.get("app_key") else {}),
6236
+ },
6237
+ parent=parent,
6238
+ error_code=item.get("error_code") or result.get("error_code"),
6239
+ message=item.get("message") or result.get("message"),
6240
+ )
6241
+ )
6242
+ return resources
4293
6243
  diff = payload.get("views_diff") if isinstance(payload.get("views_diff"), dict) else {}
4294
6244
  verification_by_name = _builder_view_verification_by_name(payload)
4295
6245
  parent = _builder_app_parent(payload)
@@ -4331,6 +6281,15 @@ def _builder_view_resources(payload: JSONObject) -> list[JSONObject]:
4331
6281
  message=message,
4332
6282
  )
4333
6283
  )
6284
+ details = payload.get("details") if isinstance(payload.get("details"), dict) else {}
6285
+ action_buttons_result = details.get("action_buttons_result") if isinstance(details, dict) else None
6286
+ if isinstance(action_buttons_result, dict):
6287
+ nested_payload = {
6288
+ **action_buttons_result,
6289
+ "app_key": payload.get("app_key"),
6290
+ "app_name": payload.get("app_name"),
6291
+ }
6292
+ resources.extend(_builder_button_resources(nested_payload))
4334
6293
  return resources
4335
6294
 
4336
6295
 
@@ -4821,7 +6780,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4821
6780
  "allowed_keys": [
4822
6781
  "package_id",
4823
6782
  "package_name",
4824
- "create_if_missing",
4825
6783
  "icon",
4826
6784
  "color",
4827
6785
  "icon_name",
@@ -4834,7 +6792,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4834
6792
  "aliases": {
4835
6793
  "packageId": "package_id",
4836
6794
  "packageName": "package_name",
4837
- "createIfMissing": "create_if_missing",
4838
6795
  "iconName": "icon",
4839
6796
  "iconColor": "color",
4840
6797
  "icon_config.name": "icon",
@@ -4851,6 +6808,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4851
6808
  },
4852
6809
  "execution_notes": [
4853
6810
  "create or update package metadata, visibility, grouping, and ordering in one call",
6811
+ "create mode: package_name without package_id creates a package",
4854
6812
  "creating a package requires explicit icon + color; icon=template is blocked because it is too generic",
4855
6813
  "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",
4856
6814
  "updating a package preserves existing icon/color when omitted; explicit icon/color values are still validated",
@@ -4859,6 +6817,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4859
6817
  "package_id maps internally to backend tagId; do not use tag_id in public calls",
4860
6818
  "items is a full package layout tree; omitting existing app/portal items is blocked unless allow_detach=true",
4861
6819
  "item shapes: {type:'app', app_key}, {type:'portal', dash_key}, or {type:'group', group_id?, name, items:[...]}",
6820
+ "allow_detach=true with flat app/portal items and no groups is treated as an explicit full replacement and skips current layout read plus final package readback",
4862
6821
  "layout apply calls backend package ordering (MoveGroupAuth), so it requires package edit_app permission even when items=[] only clears/deletes existing groups",
4863
6822
  *_VISIBILITY_EXECUTION_NOTES,
4864
6823
  ],
@@ -4878,7 +6837,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4878
6837
  "create_example": {
4879
6838
  "profile": "default",
4880
6839
  "package_name": "项目管理",
4881
- "create_if_missing": True,
4882
6840
  "icon": "briefcase",
4883
6841
  "color": "azure",
4884
6842
  "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
@@ -5097,6 +7055,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5097
7055
  "remove_buttons supports button_id or exact unique button_text",
5098
7056
  "after a remove_buttons DELETE is sent, the tool verifies deletion by single button_id readback; removed[] returns delete_executed, readback_status, and safe_to_retry_delete=false",
5099
7057
  "if a removed button returns readback_status=unavailable or still_exists, treat the result as readback pending and do not blindly repeat the delete",
7058
+ "pure create/update with returned button ids and no view_configs/remove skips the final custom-button inventory read; response details.final_readback_skipped=true",
5100
7059
  "all operations share one edit context and publish after at least one write succeeds; there is no draft-only mode for this tool",
5101
7060
  "background_color and text_color cannot both be white",
5102
7061
  ],
@@ -5190,6 +7149,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5190
7149
  "upsert_resources[].match_mappings[].target_field",
5191
7150
  "upsert_resources[].match_mappings[].source_field",
5192
7151
  "upsert_resources[].match_mappings[].value",
7152
+ "upsert_resources[].match_mappings[].values",
5193
7153
  "upsert_resources[].match_mappings[].operator",
5194
7154
  "upsert_resources[].match_rules",
5195
7155
  "patch_resources[].associated_item_id",
@@ -5224,6 +7184,16 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5224
7184
  "allowed_values": {
5225
7185
  "upsert_resources[].graph_type": ["chart", "view"],
5226
7186
  "upsert_resources[].report_source": ["app", "dataset"],
7187
+ "upsert_resources[].match_mappings[].operator": [
7188
+ "eq",
7189
+ "neq",
7190
+ "in",
7191
+ "contains",
7192
+ "gte",
7193
+ "lte",
7194
+ "is_empty",
7195
+ "not_empty",
7196
+ ],
5227
7197
  "view_configs[].limit_type": ["all", "select"],
5228
7198
  },
5229
7199
  "execution_notes": [
@@ -5236,12 +7206,15 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5236
7206
  "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",
5237
7207
  "graph_type=view uses view_key and internally compiles to the Qingflow view source; graph_type=chart uses chart_key and defaults to report_source=app",
5238
7208
  "report_source=app maps to BI_QINGFLOW; report_source=dataset maps to BI_DATASET for associating an existing dataset report; do not pass raw backend sourceType",
5239
- "use match_mappings for associated view/report filtering; dynamic conditions use source_field and static conditions use value",
7209
+ "use match_mappings for associated view/report filtering; dynamic conditions use source_field and static conditions use value/values",
5240
7210
  "match_mappings.source_field accepts source schema fields plus system fields 数据ID(-17) and 编号(0); match_mappings compiles to backend matchRules",
7211
+ "match_mappings uses the same public operator semantics as view/chart filters: eq, neq, in, contains, gte, lte, is_empty, not_empty; field_name/field are aliases of target_field",
7212
+ "static multi-value conditions use values; is_empty/not_empty conditions do not need source_field/value/values",
5241
7213
  "do not write raw match_rules unless preserving a legacy backend config; match_mappings and match_rules are mutually exclusive",
5242
7214
  "client_key only lets a view_config reference a resource created earlier in the same apply call through associated_item_refs; it is not persisted and cannot deduplicate later apply calls",
5243
7215
  "remove_associated_item_ids sends DELETE and verifies deletion with one associated-resource pool readback because the backend has no confirmed single-item GET; removed[] returns delete_executed, readback_status, and safe_to_retry_delete=false",
5244
7216
  "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",
7217
+ "pure create/update with returned associated_item_id values and no view_configs/remove/reorder skips the final associated-resource pool read; response details.final_readback_skipped=true",
5245
7218
  "this tool publishes after at least one write succeeds; there is no draft-only mode",
5246
7219
  "visible=false hides the associated-resource area without clearing previous selected ids; visible=true with limit_type=all shows the whole app-level pool",
5247
7220
  ],
@@ -5276,7 +7249,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5276
7249
  },
5277
7250
  },
5278
7251
  "app_schema_plan": {
5279
- "allowed_keys": ["app_key", "package_id", "app_name", "icon", "color", "visibility", "create_if_missing", "add_fields", "update_fields", "remove_fields"],
7252
+ "allowed_keys": ["app_key", "package_id", "app_name", "icon", "color", "visibility", "add_fields", "update_fields", "remove_fields"],
5280
7253
  "aliases": {
5281
7254
  "app_title": "app_name",
5282
7255
  "title": "app_name",
@@ -5301,6 +7274,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5301
7274
  "field.customBtnTextStatus": "field.custom_button_text_enabled",
5302
7275
  "field.customBtnText": "field.custom_button_text",
5303
7276
  "field.subfieldUpdates": "field.subfield_updates",
7277
+ "field.data_title": "field.as_data_title",
7278
+ "field.dataTitle": "field.as_data_title",
7279
+ "field.data_cover": "field.as_data_cover",
7280
+ "field.dataCover": "field.as_data_cover",
5304
7281
  "field.asDataTitle": "field.as_data_title",
5305
7282
  "field.asDataCover": "field.as_data_cover",
5306
7283
  },
@@ -5327,8 +7304,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5327
7304
  "icon": "briefcase",
5328
7305
  "color": "emerald",
5329
7306
  "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
5330
- "create_if_missing": True,
5331
- "add_fields": [{"name": "项目名称", "type": "text", "as_data_title": True}],
7307
+ "add_fields": [{"name": "项目名称", "type": "text", "data_title": True}],
5332
7308
  "update_fields": [],
5333
7309
  "remove_fields": [],
5334
7310
  },
@@ -5361,8 +7337,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5361
7337
  "icon_color",
5362
7338
  "icon_config",
5363
7339
  "visibility",
5364
- "create_if_missing",
5365
7340
  "publish",
7341
+ "form",
5366
7342
  "add_fields",
5367
7343
  "update_fields",
5368
7344
  "remove_fields",
@@ -5376,6 +7352,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5376
7352
  "apps[].icon_color",
5377
7353
  "apps[].icon_config",
5378
7354
  "apps[].visibility",
7355
+ "apps[].form",
7356
+ "apps[].form[].section",
7357
+ "apps[].form[].rows",
5379
7358
  "apps[].add_fields",
5380
7359
  "apps[].update_fields",
5381
7360
  "apps[].remove_fields",
@@ -5420,6 +7399,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5420
7399
  "field.customBtnTextStatus": "field.custom_button_text_enabled",
5421
7400
  "field.customBtnText": "field.custom_button_text",
5422
7401
  "field.subfieldUpdates": "field.subfield_updates",
7402
+ "field.data_title": "field.as_data_title",
7403
+ "field.dataTitle": "field.as_data_title",
7404
+ "field.data_cover": "field.as_data_cover",
7405
+ "field.dataCover": "field.as_data_cover",
5423
7406
  "field.asDataTitle": "field.as_data_title",
5424
7407
  "field.asDataCover": "field.as_data_cover",
5425
7408
  },
@@ -5436,13 +7419,17 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5436
7419
  "execution_notes": [
5437
7420
  "use exactly one resource mode",
5438
7421
  "edit mode: app_key, optional app_name to rename the existing app",
5439
- "create mode: package_id + app_name + create_if_missing=true",
7422
+ "create mode: package_id + app_name",
7423
+ "create mode submits a create request directly after basic package add_app permission check; it does not scan app_name duplicates or reuse an existing app by name",
5440
7424
  "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",
5441
- "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",
7425
+ "multi-app mode: pass package_id + apps[]; items with app_name and no app_key are created, items with app_key update existing apps",
5442
7426
  "CLI --apps-file primary shape is {package_id, apps:[...]}; raw app arrays and singleton wrapper arrays are compatibility paths, not recommended examples",
5443
- "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",
7427
+ "create app schemas with form: form[].section names form sections; form[].rows[][] contains field objects; builder splits form into field creation plus form layout apply",
7428
+ "single-app CLI primary shape is --form-file; multi-app primary shape is apps[].form inside --apps-file",
7429
+ "multi-app mode validates static payload errors before writing: duplicate client_key/app_name, missing data title on new apps, and unresolved target_app_ref/target_app",
5444
7430
  "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",
5445
7431
  "multi-app relation fields may also use target_app with another apps[].app_name; prefer target_app_ref/client_key when names may collide",
7432
+ "multi-app schema writes run in dependency waves: apps without same-batch relation dependencies write first, then dependent apps write in parallel by layer; response.parallelism includes schema_wave_count/schema_wave_sizes",
5446
7433
  "multi-app mode is not transactional; read created_app_keys and apps[].status before retrying, and retry only failed app items",
5447
7434
  "create mode defaults new app visibility to workspace/not when visibility is omitted; edit mode preserves current visibility when omitted",
5448
7435
  "create mode requires explicit icon + color; icon=template is blocked because it is too generic",
@@ -5455,8 +7442,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5455
7442
  "do not add form fields named 数据ID, 编号, 申请人, 申请时间, 创建人, 创建时间, 提交人, 提交时间, 更新时间, 更新人, 当前流程状态, 当前处理人, 当前处理节点, or 流程标题; these are Qingflow built-in system fields, not fields to create",
5456
7443
  *_VISIBILITY_EXECUTION_NOTES,
5457
7444
  "update_fields is the field-level partial update path; it reads current form schema and preserves untouched field config",
5458
- "multiple relation fields are backend-risky; read verification.relation_field_limit_verified and warnings before declaring the schema stable",
5459
- "backend 49614 is normalized to MULTIPLE_RELATION_FIELDS_UNSUPPORTED with a workaround message",
7445
+ "an app may contain multiple relation fields; do not downgrade secondary relations to text just because more than one relation exists",
7446
+ "backend relation errors are returned as normal schema apply failures with backend_code/details; they are not normalized into a one-relation modeling fallback",
5460
7447
  "relation_mode=multiple maps to referenceConfig.optionalDataNum=0",
5461
7448
  "relation fields now require both display_field and visible_fields in MCP/CLI payloads",
5462
7449
  "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",
@@ -5481,11 +7468,17 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5481
7468
  "icon": "briefcase",
5482
7469
  "color": "emerald",
5483
7470
  "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
5484
- "create_if_missing": True,
5485
7471
  "publish": True,
5486
- "add_fields": [
5487
- {"name": "项目名称", "type": "text", "as_data_title": True},
5488
- {"name": "项目封面", "type": "attachment", "as_data_cover": True},
7472
+ "form": [
7473
+ {
7474
+ "section": "基础信息",
7475
+ "rows": [
7476
+ [
7477
+ {"name": "项目名称", "type": "text", "data_title": True},
7478
+ {"name": "项目封面", "type": "attachment", "data_cover": True},
7479
+ ]
7480
+ ],
7481
+ }
5489
7482
  ],
5490
7483
  "update_fields": [],
5491
7484
  "remove_fields": [],
@@ -5493,7 +7486,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5493
7486
  "multi_app_example": {
5494
7487
  "profile": "default",
5495
7488
  "package_id": 1001,
5496
- "create_if_missing": True,
5497
7489
  "publish": True,
5498
7490
  "apps": [
5499
7491
  {
@@ -5501,9 +7493,16 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5501
7493
  "app_name": "员工花名册",
5502
7494
  "icon": "business-personalcard",
5503
7495
  "color": "emerald",
5504
- "add_fields": [
5505
- {"name": "员工名称", "type": "text", "as_data_title": True},
5506
- {"name": "员工照片", "type": "attachment", "as_data_cover": True},
7496
+ "form": [
7497
+ {
7498
+ "section": "基础信息",
7499
+ "rows": [
7500
+ [
7501
+ {"name": "员工名称", "type": "text", "data_title": True},
7502
+ {"name": "员工照片", "type": "attachment", "data_cover": True},
7503
+ ]
7504
+ ],
7505
+ }
5507
7506
  ],
5508
7507
  },
5509
7508
  {
@@ -5511,15 +7510,22 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5511
7510
  "app_name": "工时表",
5512
7511
  "icon": "clock",
5513
7512
  "color": "blue",
5514
- "add_fields": [
5515
- {"name": "工时标题", "type": "text", "as_data_title": True},
7513
+ "form": [
5516
7514
  {
5517
- "name": "关联员工",
5518
- "type": "relation",
5519
- "target_app_ref": "employee",
5520
- "display_field": {"name": "员工名称"},
5521
- "visible_fields": [{"name": "员工名称"}],
5522
- },
7515
+ "section": "基础信息",
7516
+ "rows": [
7517
+ [{"name": "工时标题", "type": "text", "data_title": True}],
7518
+ [
7519
+ {
7520
+ "name": "关联员工",
7521
+ "type": "relation",
7522
+ "target_app_ref": "employee",
7523
+ "display_field": {"name": "员工名称"},
7524
+ "visible_fields": [{"name": "员工名称"}],
7525
+ }
7526
+ ],
7527
+ ],
7528
+ }
5523
7529
  ],
5524
7530
  },
5525
7531
  ],
@@ -5703,56 +7709,88 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5703
7709
  "sections": [{"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["项目名称", "项目负责人", "项目阶段", "优先级"]]}],
5704
7710
  },
5705
7711
  },
5706
- "app_flow_get_schema": {
5707
- "allowed_keys": ["schema_version"],
5708
- "aliases": {"schemaVersion": "schema_version"},
5709
- "allowed_values": {},
5710
- "execution_notes": [
5711
- "returns WorkflowSpec JSON Schema from /workflow/spec/schema",
5712
- "call this before authoring a new spec or validating spec shape",
5713
- "worksheet-level approval deduplication toggles (legacy global settings) are not yet part of WorkflowSpec",
5714
- ],
5715
- "minimal_example": {
5716
- "profile": "default",
5717
- "schema_version": "vnext-2026-06",
7712
+ "app_flow_plan": {
7713
+ "allowed_keys": ["app_key", "mode", "nodes", "transitions", "preset"],
7714
+ "aliases": {
7715
+ "overwrite": "replace",
7716
+ "base_preset": "preset",
7717
+ "default_approval": "basic_approval",
7718
+ "node.role_names": "node.assignees.role_names",
7719
+ "node.role_ids": "node.assignees.role_ids",
7720
+ "node.member_names": "node.assignees.member_names",
7721
+ "node.member_emails": "node.assignees.member_emails",
7722
+ "node.member_uids": "node.assignees.member_uids",
7723
+ "node.editable_fields": "node.permissions.editable_fields",
7724
+ "default_approval": "basic_approval",
5718
7725
  },
5719
- },
5720
- "app_flow_get": {
5721
- "allowed_keys": ["app_key", "version_id"],
5722
- "aliases": {"versionId": "version_id"},
5723
- "allowed_values": {},
7726
+ "allowed_values": {
7727
+ "mode": ["replace"],
7728
+ "preset": [member.value for member in FlowPreset],
7729
+ "node.type": PUBLIC_STABLE_FLOW_NODE_TYPES,
7730
+ },
7731
+ "dependency_hints": [
7732
+ "approval-style workflows require an explicit business status select field before app_flow_apply, such as 状态 / 处理状态 / 审批状态 / 工单状态",
7733
+ "approve/fill/copy nodes require at least one assignee",
7734
+ ],
5724
7735
  "execution_notes": [
5725
- "returns current WorkflowSpecDTO for one app via GET /workflow/spec",
5726
- "use GET-first before patching and app_flow_apply",
7736
+ "app_flow_plan is private compatibility plumbing, not an agent-facing workflow path; use app_flow_get/app_flow_get_schema plus app_flow_apply spec or patch_nodes",
7737
+ "do not create platform system workflow fields such as 当前流程状态 / 当前处理人 / 当前处理节点 / 流程标题; use an explicit business status field instead",
5727
7738
  ],
5728
7739
  "minimal_example": {
5729
7740
  "profile": "default",
5730
7741
  "app_key": "APP_KEY",
7742
+ "mode": "replace",
7743
+ "preset": "basic_approval",
7744
+ "nodes": [
7745
+ {
7746
+ "id": "approve_1",
7747
+ "type": "approve",
7748
+ "name": "部门审批",
7749
+ "assignees": {"role_names": ["项目经理"]},
7750
+ "permissions": {"editable_fields": ["状态", "审批意见"]},
7751
+ }
7752
+ ],
7753
+ "transitions": [],
5731
7754
  },
5732
7755
  },
5733
7756
  "app_flow_apply": {
5734
- "allowed_keys": ["app_key", "publish", "spec", "idempotency_key", "schema_version"],
7757
+ "allowed_keys": [
7758
+ "app_key",
7759
+ "publish",
7760
+ "spec",
7761
+ "patch_nodes",
7762
+ "idempotency_key",
7763
+ "schema_version",
7764
+ ],
5735
7765
  "aliases": {
5736
- "schemaVersion": "schema_version",
7766
+ "patchNodes": "patch_nodes",
5737
7767
  "idempotencyKey": "idempotency_key",
7768
+ "schemaVersion": "schema_version",
5738
7769
  },
5739
7770
  "allowed_values": {},
5740
7771
  "dependency_hints": [
5741
- "spec must be a complete WorkflowSpecDTO object (replace-only apply)",
7772
+ "approval-style workflows require an explicit business status select field before app_flow_apply, such as 状态 / 处理状态 / 审批状态 / 工单状态",
7773
+ "WorkflowSpec nodes should declare sync=true for routing nodes and explicit assignees for approval/filling/cc nodes",
5742
7774
  ],
5743
7775
  "execution_notes": [
5744
- "posts to /workflow/spec:apply with appKey, idempotencyKey, schemaVersion, and spec",
5745
- "verification uses appliedSpec, diffSummary, and semanticLint from the apply response",
5746
- "publish=false keeps changes in draft when supported by the backend",
7776
+ "preferred full workflow path is app_flow_get_schema/app_flow_get -> app_flow_apply with spec",
7777
+ "use patch_nodes for targeted existing-node edits; each item uses id plus set/unset",
7778
+ "public workflow writes use only spec or patch_nodes",
7779
+ "do not mix spec and patch_nodes in one call",
7780
+ "do not create platform system workflow fields such as 当前流程状态 / 当前处理人 / 当前处理节点 / 流程标题; use an explicit business status field instead",
7781
+ "workflow readback is app_flow_get; use app_flow_get_schema when raw spec shape is unclear",
5747
7782
  ],
5748
7783
  "minimal_example": {
5749
7784
  "profile": "default",
5750
7785
  "app_key": "APP_KEY",
5751
7786
  "publish": True,
5752
- "spec": {
5753
- "nodes": [{"id": "n1", "type": "APPLICANT", "name": "发起"}],
5754
- "transitions": [],
5755
- },
7787
+ "spec": {"schemaVersion": "1", "nodes": [], "edges": []},
7788
+ },
7789
+ "patch_nodes_example": {
7790
+ "profile": "default",
7791
+ "app_key": "APP_KEY",
7792
+ "publish": True,
7793
+ "patch_nodes": [{"id": "approve_1", "set": {"name": "主管审批"}}],
5756
7794
  },
5757
7795
  },
5758
7796
  "app_views_plan": {
@@ -5768,11 +7806,15 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5768
7806
  "upsert_views[].columns",
5769
7807
  "upsert_views[].filters",
5770
7808
  "upsert_views[].buttons",
7809
+ "upsert_views[].action_buttons",
7810
+ "upsert_views[].action_buttons_mode",
5771
7811
  "upsert_views[].visibility",
5772
7812
  "upsert_views[].query_conditions",
5773
7813
  "patch_views[].view_key",
5774
7814
  "patch_views[].name",
5775
7815
  "patch_views[].set",
7816
+ "patch_views[].set.action_buttons",
7817
+ "patch_views[].set.action_buttons_mode",
5776
7818
  "patch_views[].unset",
5777
7819
  ],
5778
7820
  "aliases": {
@@ -5800,6 +7842,14 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5800
7842
  "buttons[].buttonFormula": "buttons[].button_formula",
5801
7843
  "buttons[].buttonFormulaType": "buttons[].button_formula_type",
5802
7844
  "buttons[].printTpls": "buttons[].print_tpls",
7845
+ "actionButtons": "action_buttons",
7846
+ "actionButtonsMode": "action_buttons_mode",
7847
+ "action_buttons[].button_text": "action_buttons[].text",
7848
+ "action_buttons[].trigger_action": "action_buttons[].action",
7849
+ "action_buttons[].trigger_link_url": "action_buttons[].url",
7850
+ "action_buttons[].visibleWhen": "action_buttons[].visible_when",
7851
+ "action_buttons[].fieldMappings": "action_buttons[].field_mappings",
7852
+ "action_buttons[].defaultValues": "action_buttons[].default_values",
5803
7853
  },
5804
7854
  "allowed_values": {
5805
7855
  "preset": [member.value for member in ViewsPreset],
@@ -5807,6 +7857,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5807
7857
  "view.filter.operator": [member.value for member in ViewFilterOperator],
5808
7858
  "view.buttons.button_type": ["SYSTEM", "CUSTOM"],
5809
7859
  "view.buttons.config_type": ["TOP", "DETAIL", "INSIDE"],
7860
+ "view.action_buttons.action": ["add_data", "link", "qRobot", "wings"],
7861
+ "view.action_buttons.placement": [member.value for member in PublicButtonPlacement],
7862
+ "view.action_buttons_mode": ["merge", "replace"],
5810
7863
  **deepcopy(_VISIBILITY_ALLOWED_VALUES),
5811
7864
  },
5812
7865
  "execution_notes": [
@@ -5816,6 +7869,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5816
7869
  "upsert_views[].query_conditions.rows is a layout matrix of query-panel supported field names; it is compiled to backend queryCondition queIds",
5817
7870
  "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",
5818
7871
  "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",
7872
+ "business buttons should be declared with action_buttons on the target view; app_views_apply writes the view first, then compiles action_buttons through the custom button writer and binds them to the resulting view_key",
7873
+ "action_buttons_mode defaults to merge; replace rewrites this view's custom button bindings, and replace plus action_buttons=[] clears this view's custom button bindings without deleting button bodies",
7874
+ "legacy upsert_views[].buttons is a compatibility path for raw full replacement of existing view button config; do not use it as the normal agent path",
5819
7875
  "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",
5820
7876
  "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",
5821
7877
  "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",
@@ -5846,6 +7902,27 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5846
7902
  ],
5847
7903
  "remove_views": [],
5848
7904
  },
7905
+ "action_buttons_example": {
7906
+ "profile": "default",
7907
+ "app_key": "APP_KEY",
7908
+ "upsert_views": [
7909
+ {
7910
+ "name": "生产工单执行视图",
7911
+ "type": "table",
7912
+ "columns": ["工单编号", "产品", "状态", "负责人"],
7913
+ "action_buttons": [
7914
+ {
7915
+ "text": "开始生产",
7916
+ "action": "link",
7917
+ "url": "https://example.com/start",
7918
+ "placement": "list",
7919
+ "visible_when": [{"field_name": "状态", "operator": "eq", "value": "待排产"}],
7920
+ }
7921
+ ],
7922
+ }
7923
+ ],
7924
+ "remove_views": [],
7925
+ },
5849
7926
  "full_upsert_query_conditions_example": {
5850
7927
  "profile": "default",
5851
7928
  "app_key": "APP_KEY",
@@ -5892,6 +7969,17 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5892
7969
  },
5893
7970
  "app_views_apply": {
5894
7971
  "allowed_keys": [
7972
+ "views",
7973
+ "views[].operation",
7974
+ "views[].app_key",
7975
+ "views[].view_key",
7976
+ "views[].name",
7977
+ "views[].type",
7978
+ "views[].columns",
7979
+ "views[].filters",
7980
+ "views[].action_buttons",
7981
+ "views[].set",
7982
+ "views[].unset",
5895
7983
  "app_key",
5896
7984
  "publish",
5897
7985
  "upsert_views",
@@ -5903,11 +7991,15 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5903
7991
  "upsert_views[].columns",
5904
7992
  "upsert_views[].filters",
5905
7993
  "upsert_views[].buttons",
7994
+ "upsert_views[].action_buttons",
7995
+ "upsert_views[].action_buttons_mode",
5906
7996
  "upsert_views[].visibility",
5907
7997
  "upsert_views[].query_conditions",
5908
7998
  "patch_views[].view_key",
5909
7999
  "patch_views[].name",
5910
8000
  "patch_views[].set",
8001
+ "patch_views[].set.action_buttons",
8002
+ "patch_views[].set.action_buttons_mode",
5911
8003
  "patch_views[].unset",
5912
8004
  ],
5913
8005
  "aliases": {
@@ -5923,6 +8015,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5923
8015
  "queryConditions": "query_conditions",
5924
8016
  "query_condition": "query_conditions",
5925
8017
  "queryCondition": "query_conditions",
8018
+ "views[].op": "views[].operation",
8019
+ "views[].fields": "views[].columns",
8020
+ "views[].viewKey": "views[].view_key",
8021
+ "views[].actionButtons": "views[].action_buttons",
5926
8022
  "patchViews": "patch_views",
5927
8023
  "startField": "start_field",
5928
8024
  "endField": "end_field",
@@ -5935,21 +8031,39 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5935
8031
  "buttons[].buttonFormula": "buttons[].button_formula",
5936
8032
  "buttons[].buttonFormulaType": "buttons[].button_formula_type",
5937
8033
  "buttons[].printTpls": "buttons[].print_tpls",
8034
+ "actionButtons": "action_buttons",
8035
+ "actionButtonsMode": "action_buttons_mode",
8036
+ "action_buttons[].button_text": "action_buttons[].text",
8037
+ "action_buttons[].trigger_action": "action_buttons[].action",
8038
+ "action_buttons[].trigger_link_url": "action_buttons[].url",
8039
+ "action_buttons[].visibleWhen": "action_buttons[].visible_when",
8040
+ "action_buttons[].fieldMappings": "action_buttons[].field_mappings",
8041
+ "action_buttons[].defaultValues": "action_buttons[].default_values",
5938
8042
  },
5939
8043
  "allowed_values": {
8044
+ "views[].operation": ["upsert", "patch", "remove"],
5940
8045
  "view.type": [member.value for member in PublicViewType],
5941
8046
  "view.filter.operator": [member.value for member in ViewFilterOperator],
5942
8047
  "view.buttons.button_type": ["SYSTEM", "CUSTOM"],
5943
8048
  "view.buttons.config_type": ["TOP", "DETAIL", "INSIDE"],
8049
+ "view.action_buttons.action": ["add_data", "link", "qRobot", "wings"],
8050
+ "view.action_buttons.placement": [member.value for member in PublicButtonPlacement],
8051
+ "view.action_buttons_mode": ["merge", "replace"],
5944
8052
  **deepcopy(_VISIBILITY_ALLOWED_VALUES),
5945
8053
  },
5946
8054
  "execution_notes": [
8055
+ "preferred batch input is views[]; each item has app_key and operation=upsert/patch/remove, and the same batch may mix same-app and cross-app views",
8056
+ f"views[] jobs run concurrently with an internal limit of {VIEW_APPLY_PARALLEL_LIMIT}; results are returned in input order",
5947
8057
  "apply may return partial_success when some views land and others fail",
5948
8058
  "creating a new view follows backend createViewgraphConfig and requires both ViewManagementAuth and DataManageAuth; updating/deleting existing views only requires ViewManagementAuth",
5949
8059
  "when duplicate view names exist, supply view_key to target the exact view",
5950
8060
  "read back app_get after any failed or partial view apply",
5951
8061
  "view existence verification and saved-filter verification are separate; treat filters as unverified until verification.view_filters_verified is true",
5952
8062
  "buttons omitted preserves existing button config; buttons=[] clears all buttons; buttons=[...] replaces the full button config",
8063
+ "business buttons should be declared with action_buttons on the target view; app_views_apply writes the view first, then compiles action_buttons through the custom button writer and binds them to the resulting view_key",
8064
+ "patch_views[].set.action_buttons can add buttons to an existing view without rewriting columns/filters; action_buttons_mode defaults to merge, replace rewrites this view's custom button bindings, and replace plus action_buttons=[] clears bindings without deleting button bodies",
8065
+ "publish=false with action_buttons is blocked as VIEW_ACTION_BUTTONS_REQUIRE_PUBLISH because the underlying custom button writer may publish after successful button writes",
8066
+ "legacy upsert_views[].buttons is a compatibility path for raw full replacement of existing view button config; do not use it as the normal agent path",
5953
8067
  "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",
5954
8068
  "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",
5955
8069
  "upsert_views[].query_conditions.rows is a layout matrix of query-panel supported field names; it is compiled to backend queryCondition queIds",
@@ -5965,10 +8079,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5965
8079
  ],
5966
8080
  "minimal_example": {
5967
8081
  "profile": "default",
5968
- "app_key": "APP_KEY",
5969
8082
  "publish": True,
5970
- "upsert_views": [{"name": "项目台账视图", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
5971
- "remove_views": [],
8083
+ "views": [{"operation": "upsert", "app_key": "APP_KEY", "name": "项目台账视图", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
5972
8084
  },
5973
8085
  "query_conditions_example": {
5974
8086
  "profile": "default",
@@ -5987,6 +8099,51 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5987
8099
  ],
5988
8100
  "remove_views": [],
5989
8101
  },
8102
+ "action_buttons_example": {
8103
+ "profile": "default",
8104
+ "app_key": "APP_KEY",
8105
+ "publish": True,
8106
+ "upsert_views": [
8107
+ {
8108
+ "name": "生产工单执行视图",
8109
+ "type": "table",
8110
+ "columns": ["工单编号", "产品", "状态", "负责人"],
8111
+ "action_buttons": [
8112
+ {
8113
+ "text": "开始生产",
8114
+ "action": "link",
8115
+ "url": "https://example.com/start",
8116
+ "placement": "list",
8117
+ "visible_when": [{"field_name": "状态", "operator": "eq", "value": "待排产"}],
8118
+ }
8119
+ ],
8120
+ }
8121
+ ],
8122
+ "remove_views": [],
8123
+ },
8124
+ "action_buttons_patch_example": {
8125
+ "profile": "default",
8126
+ "app_key": "APP_KEY",
8127
+ "publish": True,
8128
+ "patch_views": [
8129
+ {
8130
+ "view_key": "RAW_VIEW_KEY",
8131
+ "set": {
8132
+ "action_buttons": [
8133
+ {
8134
+ "text": "创建验收单",
8135
+ "action": "add_data",
8136
+ "target_app_key": "ARRIVAL_APP",
8137
+ "field_mappings": [{"source_field": "数据ID", "target_field": "关联工单"}],
8138
+ "default_values": {"状态": "待验收"},
8139
+ "placement": "detail",
8140
+ }
8141
+ ]
8142
+ },
8143
+ }
8144
+ ],
8145
+ "remove_views": [],
8146
+ },
5990
8147
  "full_upsert_query_conditions_example": {
5991
8148
  "profile": "default",
5992
8149
  "app_key": "APP_KEY",
@@ -6102,8 +8259,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6102
8259
  "aliases": {},
6103
8260
  "allowed_values": {},
6104
8261
  "execution_notes": [
6105
- "returns WorkflowSpecDTO for one app (alias of app_flow_get)",
6106
- "use app_flow_get_schema then app_flow_get before app_flow_apply",
8262
+ "returns workflow configuration summary for one app",
8263
+ "use this before app_flow_apply when you need the current node structure",
6107
8264
  ],
6108
8265
  "minimal_example": {
6109
8266
  "profile": "default",
@@ -6256,21 +8413,48 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6256
8413
  },
6257
8414
  },
6258
8415
  "portal_apply": {
6259
- "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"],
8416
+ "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"],
6260
8417
  "aliases": {
6261
8418
  "packageId": "package_id",
6262
8419
  "name": "dash_name",
6263
8420
  "sourceType": "source_type",
6264
8421
  "chartRef": "chart_ref",
8422
+ "inlineChart": "chart",
8423
+ "createChart": "chart",
6265
8424
  "viewRef": "view_ref",
6266
8425
  "dashStyleConfigBO": "dash_style_config",
6267
8426
  },
6268
- "section_allowed_keys": ["title", "source_type", "role", "position", "dash_style_config", "config", "chart_ref", "view_ref", "text", "url"],
8427
+ "section_allowed_keys": [
8428
+ "title",
8429
+ "source_type",
8430
+ "role",
8431
+ "position",
8432
+ "dash_style_config",
8433
+ "config",
8434
+ "chart",
8435
+ "chart.app_key",
8436
+ "chart.name",
8437
+ "chart.chart_name",
8438
+ "chart.chart_type",
8439
+ "chart.metric",
8440
+ "chart.metrics",
8441
+ "chart.group_by",
8442
+ "chart.where",
8443
+ "chart.filters",
8444
+ "chart.chart_id",
8445
+ "chart.chart_key",
8446
+ "chart_ref",
8447
+ "view_ref",
8448
+ "text",
8449
+ "url",
8450
+ ],
6269
8451
  "section_aliases": {
6270
8452
  "sourceType": "source_type",
6271
8453
  "zone": "role",
6272
8454
  "sectionRole": "role",
6273
8455
  "chartRef": "chart_ref",
8456
+ "inlineChart": "chart",
8457
+ "createChart": "chart",
6274
8458
  "viewRef": "view_ref",
6275
8459
  "dashStyleConfigBO": "dash_style_config",
6276
8460
  },
@@ -6295,7 +8479,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6295
8479
  "remove a section by omitting it from the new sections list",
6296
8480
  "package_id is required when creating a new portal",
6297
8481
  "publish=false only guarantees draft and base-info updates; it does not claim live has changed",
6298
- "chart_ref resolves by chart_id first, then exact unique chart_name",
8482
+ "sections[].chart is the canonical QingBI chart field: chart_id/chart_key/chart_name references existing charts, while app_key+name/chart_type creates or updates app-source charts inline",
8483
+ f"portal inline chart creation runs as chart jobs with an internal parallel limit of {PORTAL_INLINE_CHART_MAX_WORKERS}; identical same-app chart definitions are reused, conflicting duplicates are rejected",
8484
+ "chart_ref and inline_chart remain compatibility aliases; do not use them as the normal agent path",
6299
8485
  "view_ref resolves by view_key first, then exact unique view_name",
6300
8486
  "pc layout uses a 24-column grid; mobile layout uses a 6-column grid",
6301
8487
  "if unsure about layout, omit position or use layout_preset=auto/dashboard_2col/dashboard_3col",
@@ -6323,7 +8509,12 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6323
8509
  {
6324
8510
  "title": "经营概览",
6325
8511
  "source_type": "chart",
6326
- "chart_ref": {"app_key": "APP_KEY", "chart_name": "数据总量"},
8512
+ "chart": {
8513
+ "app_key": "APP_KEY",
8514
+ "name": "数据总量",
8515
+ "chart_type": "summary",
8516
+ "metric": {"field": "数据ID", "agg": "count"},
8517
+ },
6327
8518
  "position": {
6328
8519
  "pc": {"x": 0, "y": 0, "cols": 12, "rows": 6},
6329
8520
  "mobile": {"x": 0, "y": 0, "cols": 6, "rows": 6},
@@ -6342,7 +8533,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6342
8533
  {
6343
8534
  "title": "销售趋势",
6344
8535
  "source_type": "chart",
6345
- "chart_ref": {"app_key": "APP_KEY", "chart_id": "CHART_ID"},
8536
+ "chart": {"chart_id": "CHART_ID"},
6346
8537
  }
6347
8538
  ],
6348
8539
  }
@@ -6409,6 +8600,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
6409
8600
  _PRIVATE_BUILDER_TOOL_CONTRACTS = {
6410
8601
  "app_schema_plan",
6411
8602
  "app_layout_plan",
8603
+ "app_flow_plan",
6412
8604
  "app_views_plan",
6413
8605
  }
6414
8606