@josephyan/qingflow-cli 1.0.11 → 1.1.2

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 (67) hide show
  1. package/README.md +3 -3
  2. package/npm/bin/qingflow.mjs +40 -2
  3. package/npm/lib/runtime.mjs +386 -15
  4. package/npm/scripts/postinstall.mjs +7 -2
  5. package/package.json +1 -1
  6. package/pyproject.toml +1 -1
  7. package/skills/qingflow-cli/SKILL.md +440 -0
  8. package/skills/qingflow-cli/manifest.yaml +10 -0
  9. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +94 -0
  10. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md +485 -0
  11. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md +237 -0
  12. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_MATCH_RULES.md +137 -0
  13. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md +263 -0
  14. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md +304 -0
  15. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_WORKSPACE_ICONS.md +41 -0
  16. package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +139 -0
  17. package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +84 -0
  18. package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +129 -0
  19. package/skills/qingflow-cli/reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +195 -0
  20. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +159 -0
  21. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +20 -0
  22. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md +176 -0
  23. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +163 -0
  24. package/skills/qingflow-cli/reference/QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md +107 -0
  25. package/skills/qingflow-cli/reference/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md +151 -0
  26. package/skills/qingflow-cli/reference/_batch_schema_complex.json +18 -0
  27. package/skills/qingflow-cli/reference/_batch_schema_scalar.json +17 -0
  28. package/skills/qingflow-cli/reference/charts_remove.example.json +1 -0
  29. package/skills/qingflow-cli/reference/charts_reorder.example.json +1 -0
  30. package/skills/qingflow-cli/reference/charts_upsert_bar.example.json +8 -0
  31. package/skills/qingflow-cli/reference/charts_upsert_dashboard_starter.example.json +37 -0
  32. package/skills/qingflow-cli/reference/charts_upsert_minimal.example.json +13 -0
  33. package/skills/qingflow-cli/reference/portal_sections_all_types.example.json +131 -0
  34. package/skills/qingflow-cli/reference/portal_sections_five_types.example.json +126 -0
  35. package/skills/qingflow-cli/reference/portal_sections_standard_workbench.example.json +128 -0
  36. package/skills/qingflow-cli/reference/schema_add_fields_minimal.example.json +7 -0
  37. package/skills/qingflow-cli/reference/schema_apply_add_fields_all_types.json +78 -0
  38. package/skills/qingflow-cli/reference/views_upsert_table_minimal.example.json +7 -0
  39. package/skills/qingflow-cli/scripts/builder-package-from-app-list.py +140 -0
  40. package/skills/qingflow-cli/scripts/find-app-by-keyword.py +132 -0
  41. package/skills/qingflow-cli/scripts/validate_qingflow_output_files.py +87 -0
  42. package/src/qingflow_mcp/__init__.py +1 -1
  43. package/src/qingflow_mcp/builder_facade/models.py +532 -48
  44. package/src/qingflow_mcp/builder_facade/service.py +9194 -2384
  45. package/src/qingflow_mcp/builder_facade/workflow_spec.py +111 -0
  46. package/src/qingflow_mcp/cli/commands/app.py +3 -16
  47. package/src/qingflow_mcp/cli/commands/builder.py +354 -56
  48. package/src/qingflow_mcp/cli/commands/record.py +89 -2
  49. package/src/qingflow_mcp/cli/formatters.py +32 -1
  50. package/src/qingflow_mcp/cli/main.py +245 -3
  51. package/src/qingflow_mcp/public_surface.py +11 -8
  52. package/src/qingflow_mcp/response_trim.py +143 -14
  53. package/src/qingflow_mcp/server.py +15 -12
  54. package/src/qingflow_mcp/server_app_builder.py +108 -30
  55. package/src/qingflow_mcp/server_app_user.py +17 -18
  56. package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
  57. package/src/qingflow_mcp/solution/compiler/icon_utils.py +294 -0
  58. package/src/qingflow_mcp/solution/executor.py +3 -133
  59. package/src/qingflow_mcp/tools/ai_builder_tools.py +2617 -440
  60. package/src/qingflow_mcp/tools/app_tools.py +53 -8
  61. package/src/qingflow_mcp/tools/package_tools.py +16 -2
  62. package/src/qingflow_mcp/tools/record_tools.py +2095 -176
  63. package/src/qingflow_mcp/tools/resource_read_tools.py +3 -0
  64. package/src/qingflow_mcp/tools/solution_tools.py +30 -2
  65. package/src/qingflow_mcp/tools/workflow_tools.py +3 -31
  66. package/src/qingflow_mcp/version.py +110 -0
  67. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +0 -173
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from copy import deepcopy
4
4
  import json
5
5
  import time
6
+ from typing import Any
6
7
 
7
8
  from pydantic import ValidationError
8
9
 
@@ -17,21 +18,20 @@ from ..config import DEFAULT_PROFILE
17
18
  from ..errors import QingflowApiError
18
19
  from ..json_types import JSONObject
19
20
  from ..builder_facade.models import (
21
+ AssociatedResourcesApplyRequest,
20
22
  ChartApplyRequest,
23
+ CustomButtonsApplyRequest,
21
24
  CustomButtonPatch,
22
25
  FIELD_TYPE_ID_ALIASES,
23
26
  FieldPatch,
24
27
  FieldRemovePatch,
25
28
  FieldUpdatePatch,
26
- FlowPreset,
27
- FlowNodePatch,
28
- FlowPlanRequest,
29
- FlowTransitionPatch,
30
29
  LayoutApplyMode,
31
30
  LayoutPlanRequest,
32
31
  LayoutPreset,
33
32
  LayoutSectionPatch,
34
33
  PortalApplyRequest,
34
+ PublicButtonPlacement,
35
35
  PublicButtonTriggerAction,
36
36
  PublicFieldType,
37
37
  PublicRelationMode,
@@ -40,11 +40,21 @@ from ..builder_facade.models import (
40
40
  SchemaPlanRequest,
41
41
  VisibilityPatch,
42
42
  ViewFilterOperator,
43
+ ViewPartialPatch,
43
44
  ViewUpsertPatch,
44
45
  ViewsPreset,
45
46
  ViewsPlanRequest,
46
47
  )
47
48
  from ..builder_facade.service import AiBuilderFacade, INTEGRATION_OUTPUT_TARGET_FIELD_TYPES
49
+ from ..solution.compiler.icon_utils import (
50
+ GENERIC_WORKSPACE_ICON_NAMES,
51
+ WORKSPACE_ICON_COLORS,
52
+ WORKSPACE_ICON_NAMES,
53
+ normalize_workspace_icon_name,
54
+ validate_workspace_icon_choice,
55
+ workspace_icon_catalog_payload,
56
+ workspace_icon_config,
57
+ )
48
58
  from .app_tools import AppTools
49
59
  from .base import ToolBase, tool_cn_name
50
60
  from .custom_button_tools import CustomButtonTools
@@ -55,9 +65,28 @@ from .qingbi_report_tools import QingbiReportTools
55
65
  from .role_tools import RoleTools
56
66
  from .solution_tools import SolutionTools
57
67
  from .view_tools import ViewTools
58
- from .workflow_tools import WorkflowTools
59
68
 
60
- PUBLIC_STABLE_FLOW_NODE_TYPES = ["start", "approve", "fill", "copy", "webhook", "end"]
69
+
70
+ def _normalize_builder_view_key(value: str) -> str:
71
+ raw = str(value or "").strip()
72
+ if raw.startswith("custom:"):
73
+ return raw.split(":", 1)[1].strip()
74
+ return raw
75
+
76
+
77
+ BUILDER_APPLY_SCHEMA_VERSION = "builder.apply.v1"
78
+ BUILDER_APPLY_TOOL_NAMES = {
79
+ "package_apply",
80
+ "app_schema_apply",
81
+ "app_layout_apply",
82
+ "app_flow_apply",
83
+ "app_views_apply",
84
+ "app_custom_buttons_apply",
85
+ "app_associated_resources_apply",
86
+ "app_charts_apply",
87
+ "portal_apply",
88
+ "app_publish_verify",
89
+ }
61
90
 
62
91
 
63
92
  class AiBuilderTools(ToolBase):
@@ -78,7 +107,6 @@ class AiBuilderTools(ToolBase):
78
107
  buttons=CustomButtonTools(sessions, backend),
79
108
  packages=PackageTools(sessions, backend),
80
109
  views=ViewTools(sessions, backend),
81
- workflows=WorkflowTools(sessions, backend),
82
110
  portals=PortalTools(sessions, backend),
83
111
  charts=QingbiReportTools(sessions, backend),
84
112
  roles=RoleTools(sessions, backend),
@@ -92,6 +120,14 @@ class AiBuilderTools(ToolBase):
92
120
  def builder_tool_contract(tool_name: str = "") -> JSONObject:
93
121
  return self.builder_tool_contract(tool_name=tool_name)
94
122
 
123
+ @mcp.tool()
124
+ def workspace_icon_catalog_get(profile: str = DEFAULT_PROFILE) -> JSONObject:
125
+ return self.workspace_icon_catalog_get(profile=profile)
126
+
127
+ @mcp.tool()
128
+ def package_list(profile: str = DEFAULT_PROFILE, trial_status: str = "all", query: str = "") -> JSONObject:
129
+ return self.package_list(profile=profile, trial_status=trial_status, query=query)
130
+
95
131
  @mcp.tool()
96
132
  def package_get(profile: str = DEFAULT_PROFILE, package_id: int = 0) -> JSONObject:
97
133
  return self.package_get(profile=profile, package_id=package_id)
@@ -220,40 +256,53 @@ class AiBuilderTools(ToolBase):
220
256
  return self.button_style_catalog_get(profile=profile)
221
257
 
222
258
  @mcp.tool()
223
- def app_custom_button_list(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
224
- return self.app_custom_button_list(profile=profile, app_key=app_key)
225
-
226
- @mcp.tool()
227
- def app_custom_button_get(profile: str = DEFAULT_PROFILE, app_key: str = "", button_id: int = 0) -> JSONObject:
228
- return self.app_custom_button_get(profile=profile, app_key=app_key, button_id=button_id)
229
-
230
- @mcp.tool()
231
- def app_custom_button_create(
259
+ def app_custom_buttons_apply(
232
260
  profile: str = DEFAULT_PROFILE,
233
261
  app_key: str = "",
234
- payload: JSONObject | None = None,
262
+ upsert_buttons: list[JSONObject] | None = None,
263
+ patch_buttons: list[JSONObject] | None = None,
264
+ remove_buttons: list[JSONObject] | None = None,
265
+ view_configs: list[JSONObject] | None = None,
266
+ apps: list[JSONObject] | None = None,
235
267
  ) -> JSONObject:
236
- return self.app_custom_button_create(profile=profile, app_key=app_key, payload=payload or {})
268
+ return self.app_custom_buttons_apply(
269
+ profile=profile,
270
+ app_key=app_key,
271
+ upsert_buttons=upsert_buttons or [],
272
+ patch_buttons=patch_buttons or [],
273
+ remove_buttons=remove_buttons or [],
274
+ view_configs=view_configs or [],
275
+ apps=apps,
276
+ )
237
277
 
238
278
  @mcp.tool()
239
- def app_custom_button_update(
279
+ def app_associated_resources_apply(
240
280
  profile: str = DEFAULT_PROFILE,
241
281
  app_key: str = "",
242
- button_id: int = 0,
243
- payload: JSONObject | None = None,
282
+ upsert_resources: list[JSONObject] | None = None,
283
+ patch_resources: list[JSONObject] | None = None,
284
+ remove_associated_item_ids: list[int] | None = None,
285
+ reorder_associated_item_ids: list[int] | None = None,
286
+ view_configs: list[JSONObject] | None = None,
244
287
  ) -> JSONObject:
245
- return self.app_custom_button_update(profile=profile, app_key=app_key, button_id=button_id, payload=payload or {})
246
-
247
- @mcp.tool()
248
- def app_custom_button_delete(profile: str = DEFAULT_PROFILE, app_key: str = "", button_id: int = 0) -> JSONObject:
249
- return self.app_custom_button_delete(profile=profile, app_key=app_key, button_id=button_id)
288
+ return self.app_associated_resources_apply(
289
+ profile=profile,
290
+ app_key=app_key,
291
+ upsert_resources=upsert_resources or [],
292
+ patch_resources=patch_resources or [],
293
+ remove_associated_item_ids=remove_associated_item_ids or [],
294
+ reorder_associated_item_ids=reorder_associated_item_ids or [],
295
+ view_configs=view_configs or [],
296
+ )
250
297
 
251
298
  @mcp.tool()
252
299
  def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
253
300
  return self.app_get(profile=profile, app_key=app_key)
254
301
 
255
302
  @mcp.tool()
256
- def app_get_fields(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
303
+ def app_get_fields(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
304
+ if app_keys:
305
+ return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_fields, data_key="fields", tool_name="app_get_fields")
257
306
  return self.app_get_fields(profile=profile, app_key=app_key)
258
307
 
259
308
  @mcp.tool()
@@ -266,21 +315,41 @@ class AiBuilderTools(ToolBase):
266
315
  return self.app_repair_code_blocks(profile=profile, app_key=app_key, field=field, apply=apply)
267
316
 
268
317
  @mcp.tool()
269
- def app_get_layout(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
318
+ def app_get_layout(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
319
+ if app_keys:
320
+ return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_layout, data_key="sections", tool_name="app_get_layout")
270
321
  return self.app_get_layout(profile=profile, app_key=app_key)
271
322
 
272
323
  @mcp.tool()
273
- def app_get_views(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
324
+ def app_get_views(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
325
+ if app_keys:
326
+ return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_views, data_key="views", tool_name="app_get_views")
274
327
  return self.app_get_views(profile=profile, app_key=app_key)
275
328
 
276
329
  @mcp.tool()
277
- def app_get_flow(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
330
+ def app_get_flow(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
331
+ if app_keys:
332
+ return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_flow, data_key="spec", tool_name="app_get_flow")
278
333
  return self.app_get_flow(profile=profile, app_key=app_key)
279
334
 
280
335
  @mcp.tool()
281
- def app_get_charts(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
336
+ def app_get_charts(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
337
+ if app_keys:
338
+ return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_charts, data_key="charts", tool_name="app_get_charts")
282
339
  return self.app_get_charts(profile=profile, app_key=app_key)
283
340
 
341
+ @mcp.tool()
342
+ def app_get_buttons(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
343
+ if app_keys:
344
+ return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_buttons, data_key="buttons", tool_name="app_get_buttons")
345
+ return self._facade.app_get_buttons(profile=profile, app_key=app_key)
346
+
347
+ @mcp.tool()
348
+ def app_get_associated_resources(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
349
+ if app_keys:
350
+ return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_associated_resources, data_key="associated_resources", tool_name="app_get_associated_resources")
351
+ return self._facade.app_get_associated_resources(profile=profile, app_key=app_key)
352
+
284
353
  @mcp.tool()
285
354
  def portal_list(profile: str = DEFAULT_PROFILE) -> JSONObject:
286
355
  return self.portal_list(profile=profile)
@@ -319,7 +388,32 @@ class AiBuilderTools(ToolBase):
319
388
  add_fields: list[JSONObject] | None = None,
320
389
  update_fields: list[JSONObject] | None = None,
321
390
  remove_fields: list[JSONObject] | None = None,
391
+ apps: list[JSONObject] | None = None,
322
392
  ) -> JSONObject:
393
+ if apps:
394
+ if app_key or app_name or app_title or add_fields or update_fields or remove_fields:
395
+ return _config_failure(
396
+ tool_name="app_schema_apply",
397
+ message="app_schema_apply multi-app mode accepts package_id/create_if_missing plus apps only.",
398
+ fix_hint="Use `apps` for batch mode, or use the single-app arguments without `apps`.",
399
+ )
400
+ if package_id is None:
401
+ return _config_failure(
402
+ tool_name="app_schema_apply",
403
+ message="app_schema_apply multi-app mode requires package_id.",
404
+ fix_hint="Pass package_id and apps[].app_name for new apps, or apps[].app_key for existing apps.",
405
+ )
406
+ return self.app_schema_apply(
407
+ profile=profile,
408
+ package_id=package_id,
409
+ visibility=visibility,
410
+ create_if_missing=create_if_missing,
411
+ publish=publish,
412
+ apps=apps,
413
+ add_fields=[],
414
+ update_fields=[],
415
+ remove_fields=[],
416
+ )
323
417
  has_app_key = bool((app_key or "").strip())
324
418
  has_app_name = bool((app_name or "").strip())
325
419
  has_app_title = bool((app_title or "").strip())
@@ -351,6 +445,7 @@ class AiBuilderTools(ToolBase):
351
445
  add_fields=add_fields or [],
352
446
  update_fields=update_fields or [],
353
447
  remove_fields=remove_fields or [],
448
+ apps=[],
354
449
  )
355
450
 
356
451
  @mcp.tool()
@@ -360,25 +455,40 @@ class AiBuilderTools(ToolBase):
360
455
  mode: str = "merge",
361
456
  publish: bool = True,
362
457
  sections: list[JSONObject] | None = None,
458
+ apps: list[JSONObject] | None = None,
459
+ ) -> JSONObject:
460
+ return self.app_layout_apply(profile=profile, app_key=app_key, mode=mode, publish=publish, sections=sections or [], apps=apps)
461
+
462
+ @mcp.tool()
463
+ def app_flow_get(
464
+ profile: str = DEFAULT_PROFILE,
465
+ app_key: str = "",
466
+ version_id: str = "",
363
467
  ) -> JSONObject:
364
- return self.app_layout_apply(profile=profile, app_key=app_key, mode=mode, publish=publish, sections=sections or [])
468
+ return self.app_get_flow(profile=profile, app_key=app_key, version_id=version_id or None)
469
+
470
+ @mcp.tool()
471
+ def app_flow_get_schema(profile: str = DEFAULT_PROFILE, schema_version: str = "") -> JSONObject:
472
+ return self.app_flow_get_schema(profile=profile, schema_version=schema_version or None)
365
473
 
366
474
  @mcp.tool()
367
475
  def app_flow_apply(
368
476
  profile: str = DEFAULT_PROFILE,
369
477
  app_key: str = "",
370
- mode: str = "replace",
371
478
  publish: bool = True,
372
- nodes: list[JSONObject] | None = None,
373
- transitions: list[JSONObject] | None = None,
479
+ spec: JSONObject | None = None,
480
+ idempotency_key: str = "",
481
+ schema_version: str = "",
482
+ patch_nodes: list[JSONObject] | None = None,
374
483
  ) -> JSONObject:
375
484
  return self.app_flow_apply(
376
485
  profile=profile,
377
486
  app_key=app_key,
378
- mode=mode,
379
487
  publish=publish,
380
- nodes=nodes or [],
381
- transitions=transitions or [],
488
+ spec=spec or {},
489
+ idempotency_key=idempotency_key or None,
490
+ schema_version=schema_version or None,
491
+ patch_nodes=patch_nodes,
382
492
  )
383
493
 
384
494
  @mcp.tool()
@@ -387,14 +497,18 @@ class AiBuilderTools(ToolBase):
387
497
  app_key: str = "",
388
498
  publish: bool = True,
389
499
  upsert_views: list[JSONObject] | None = None,
500
+ patch_views: list[JSONObject] | None = None,
390
501
  remove_views: list[str] | None = None,
502
+ apps: list[JSONObject] | None = None,
391
503
  ) -> JSONObject:
392
504
  return self.app_views_apply(
393
505
  profile=profile,
394
506
  app_key=app_key,
395
507
  publish=publish,
396
508
  upsert_views=upsert_views or [],
509
+ patch_views=patch_views or [],
397
510
  remove_views=remove_views or [],
511
+ apps=apps,
398
512
  )
399
513
 
400
514
  @mcp.tool()
@@ -402,15 +516,19 @@ class AiBuilderTools(ToolBase):
402
516
  profile: str = DEFAULT_PROFILE,
403
517
  app_key: str = "",
404
518
  upsert_charts: list[JSONObject] | None = None,
519
+ patch_charts: list[JSONObject] | None = None,
405
520
  remove_chart_ids: list[str] | None = None,
406
521
  reorder_chart_ids: list[str] | None = None,
522
+ apps: list[JSONObject] | None = None,
407
523
  ) -> JSONObject:
408
524
  return self.app_charts_apply(
409
525
  profile=profile,
410
526
  app_key=app_key,
411
527
  upsert_charts=upsert_charts or [],
528
+ patch_charts=patch_charts or [],
412
529
  remove_chart_ids=remove_chart_ids or [],
413
530
  reorder_chart_ids=reorder_chart_ids or [],
531
+ apps=apps,
414
532
  )
415
533
 
416
534
  @mcp.tool()
@@ -418,9 +536,12 @@ class AiBuilderTools(ToolBase):
418
536
  profile: str = DEFAULT_PROFILE,
419
537
  dash_key: str = "",
420
538
  dash_name: str = "",
539
+ name: str = "",
421
540
  package_id: int | None = None,
422
541
  publish: bool = True,
423
542
  sections: list[JSONObject] | None = None,
543
+ pages: list[JSONObject] | None = None,
544
+ layout_preset: str = "",
424
545
  visibility: JSONObject | None = None,
425
546
  auth: JSONObject | None = None,
426
547
  icon: str | None = None,
@@ -428,10 +549,15 @@ class AiBuilderTools(ToolBase):
428
549
  hide_copyright: bool | None = None,
429
550
  dash_global_config: JSONObject | None = None,
430
551
  config: JSONObject | None = None,
552
+ payload: JSONObject | None = None,
553
+ patch_sections: list[JSONObject] | None = None,
431
554
  ) -> JSONObject:
555
+ payload = payload if isinstance(payload, dict) else {}
432
556
  has_dash_key = bool((dash_key or "").strip())
433
- has_dash_name = bool((dash_name or "").strip())
434
- has_package_id = package_id is not None
557
+ effective_dash_name = (dash_name or name or str(payload.get("dash_name") or payload.get("dashName") or payload.get("name") or "")).strip()
558
+ has_dash_name = bool(effective_dash_name)
559
+ effective_package_id = package_id if package_id is not None else payload.get("package_id") or payload.get("packageId") or payload.get("package_tag_id")
560
+ has_package_id = effective_package_id is not None
435
561
  if has_dash_key and has_package_id:
436
562
  return _config_failure(
437
563
  tool_name="portal_apply",
@@ -448,9 +574,12 @@ class AiBuilderTools(ToolBase):
448
574
  profile=profile,
449
575
  dash_key=dash_key,
450
576
  dash_name=dash_name,
577
+ name=name,
451
578
  package_id=package_id,
452
579
  publish=publish,
453
580
  sections=sections or [],
581
+ pages=pages or [],
582
+ layout_preset=layout_preset,
454
583
  visibility=visibility,
455
584
  auth=auth,
456
585
  icon=icon,
@@ -458,14 +587,25 @@ class AiBuilderTools(ToolBase):
458
587
  hide_copyright=hide_copyright,
459
588
  dash_global_config=dash_global_config,
460
589
  config=config or {},
590
+ payload=payload,
591
+ patch_sections=patch_sections,
461
592
  )
462
593
 
463
594
  @mcp.tool()
464
595
  def app_publish_verify(
465
596
  profile: str = DEFAULT_PROFILE,
466
597
  app_key: str = "",
598
+ app_keys: list[str] | None = None,
467
599
  expected_package_id: int | None = None,
468
600
  ) -> JSONObject:
601
+ if app_keys:
602
+ return self._facade._batch_read_app_keys(
603
+ profile=profile,
604
+ app_keys=app_keys,
605
+ single_reader=lambda profile, app_key: self.app_publish_verify(profile=profile, app_key=app_key, expected_package_id=expected_package_id),
606
+ data_key="verification",
607
+ tool_name="app_publish_verify",
608
+ )
469
609
  return self.app_publish_verify(
470
610
  profile=profile,
471
611
  app_key=app_key,
@@ -473,14 +613,14 @@ class AiBuilderTools(ToolBase):
473
613
  )
474
614
 
475
615
  @tool_cn_name("分组列表查询")
476
- def package_list(self, *, profile: str, trial_status: str = "all") -> JSONObject:
616
+ def package_list(self, *, profile: str, trial_status: str = "all", query: str = "") -> JSONObject:
477
617
  """执行分组与包相关逻辑。"""
478
- normalized_args = {"trial_status": trial_status}
618
+ normalized_args = {"trial_status": trial_status, "query": query}
479
619
  return _safe_tool_call(
480
- lambda: self._facade.package_list(profile=profile, trial_status=trial_status),
620
+ lambda: self._facade.package_list(profile=profile, trial_status=trial_status, query=query),
481
621
  error_code="PACKAGE_LIST_FAILED",
482
622
  normalized_args=normalized_args,
483
- suggested_next_call={"tool_name": "package_list", "arguments": {"profile": profile, "trial_status": trial_status}},
623
+ suggested_next_call={"tool_name": "package_list", "arguments": {"profile": profile, "trial_status": trial_status, "query": query}},
484
624
  )
485
625
 
486
626
  @tool_cn_name("分组解析")
@@ -525,6 +665,7 @@ class AiBuilderTools(ToolBase):
525
665
  "verification": {},
526
666
  "verified": False,
527
667
  }
668
+ contract = _builder_contract_with_apply_output(lookup_name, contract)
528
669
  return {
529
670
  "status": "success",
530
671
  "error_code": None,
@@ -546,6 +687,27 @@ class AiBuilderTools(ToolBase):
546
687
  "contract": contract,
547
688
  }
548
689
 
690
+ @tool_cn_name("工作区图标目录")
691
+ def workspace_icon_catalog_get(self, *, profile: str = DEFAULT_PROFILE) -> JSONObject:
692
+ """读取应用、应用包、门户可用的工作区图标候选。"""
693
+ catalog = workspace_icon_catalog_payload()
694
+ return {
695
+ "status": "success",
696
+ "error_code": None,
697
+ "recoverable": False,
698
+ "message": "loaded workspace icon catalog",
699
+ "profile": profile,
700
+ "icon_names": catalog["icon_names"],
701
+ "icon_colors": catalog["icon_colors"],
702
+ "generic_icon_names": catalog["generic_icon_names"],
703
+ "common_examples": catalog["common_examples"],
704
+ "notes": catalog["notes"],
705
+ "count": len(catalog["icon_names"]),
706
+ "color_count": len(catalog["icon_colors"]),
707
+ "warnings": [],
708
+ "verification": {"source": "backend AiBuildConstant ICON_NAMES/ICON_COLORS"},
709
+ }
710
+
549
711
  @tool_cn_name("分组创建")
550
712
  def package_create(
551
713
  self,
@@ -622,7 +784,18 @@ class AiBuilderTools(ToolBase):
622
784
  try:
623
785
  visibility_patch = VisibilityPatch.model_validate(visibility)
624
786
  except ValidationError as exc:
625
- return _visibility_validation_failure(str(exc), tool_name="package_apply", exc=exc)
787
+ return _attach_builder_apply_envelope(
788
+ "package_apply",
789
+ _visibility_validation_failure(str(exc), tool_name="package_apply", exc=exc),
790
+ )
791
+ icon_failure = _validate_workspace_icon_for_builder(
792
+ tool_name="package_apply",
793
+ icon=icon,
794
+ color=color,
795
+ creating=package_id is None and bool(create_if_missing),
796
+ )
797
+ if icon_failure is not None:
798
+ return _attach_builder_apply_envelope("package_apply", icon_failure)
626
799
  normalized_args = {
627
800
  "package_id": package_id,
628
801
  **({"package_name": package_name} if str(package_name or "").strip() else {}),
@@ -633,7 +806,7 @@ class AiBuilderTools(ToolBase):
633
806
  **({"items": deepcopy(items)} if items is not None else {}),
634
807
  "allow_detach": bool(allow_detach),
635
808
  }
636
- return _publicize_package_fields(_safe_tool_call(
809
+ result = _publicize_package_fields(_safe_tool_call(
637
810
  lambda: self._facade.package_apply(
638
811
  profile=profile,
639
812
  package_id=package_id,
@@ -649,6 +822,7 @@ class AiBuilderTools(ToolBase):
649
822
  normalized_args=normalized_args,
650
823
  suggested_next_call={"tool_name": "package_apply", "arguments": {"profile": profile, **normalized_args}},
651
824
  ))
825
+ return _attach_builder_apply_envelope("package_apply", result)
652
826
 
653
827
  @tool_cn_name("分组更新")
654
828
  def package_update(
@@ -867,6 +1041,134 @@ class AiBuilderTools(ToolBase):
867
1041
  suggested_next_call={"tool_name": "button_style_catalog_get", "arguments": {"profile": profile}},
868
1042
  )
869
1043
 
1044
+ @tool_cn_name("应用按钮声明式应用")
1045
+ def app_custom_buttons_apply(
1046
+ self,
1047
+ *,
1048
+ profile: str,
1049
+ app_key: str,
1050
+ upsert_buttons: list[JSONObject],
1051
+ patch_buttons: list[JSONObject] | None = None,
1052
+ remove_buttons: list[JSONObject],
1053
+ view_configs: list[JSONObject] | None = None,
1054
+ apps: list[JSONObject] | None = None,
1055
+ ) -> JSONObject:
1056
+ """执行应用按钮 apply 逻辑。"""
1057
+ if apps:
1058
+ return self._facade._batch_write_apps(
1059
+ profile=profile,
1060
+ apps=apps,
1061
+ single_writer=lambda profile, app_key, **kw: self.app_custom_buttons_apply(
1062
+ profile=profile,
1063
+ app_key=app_key,
1064
+ upsert_buttons=kw.get("upsert_buttons", []),
1065
+ patch_buttons=kw.get("patch_buttons", []),
1066
+ remove_buttons=kw.get("remove_buttons", []),
1067
+ view_configs=kw.get("view_configs", []),
1068
+ ),
1069
+ tool_name="app_custom_buttons_apply",
1070
+ )
1071
+ raw_request = {
1072
+ "app_key": app_key,
1073
+ "upsert_buttons": upsert_buttons,
1074
+ "patch_buttons": patch_buttons or [],
1075
+ "remove_buttons": remove_buttons,
1076
+ "view_configs": view_configs or [],
1077
+ }
1078
+ try:
1079
+ request = CustomButtonsApplyRequest.model_validate(raw_request)
1080
+ except ValidationError as exc:
1081
+ return _attach_builder_apply_envelope("app_custom_buttons_apply", _validation_failure(
1082
+ str(exc),
1083
+ tool_name="app_custom_buttons_apply",
1084
+ exc=exc,
1085
+ suggested_next_call={
1086
+ "tool_name": "app_custom_buttons_apply",
1087
+ "arguments": {
1088
+ "profile": profile,
1089
+ "app_key": app_key or "APP_KEY",
1090
+ "upsert_buttons": [
1091
+ {
1092
+ "button_text": "同步客户",
1093
+ "style_preset": "primary_blue",
1094
+ "button_icon": "ex-switch",
1095
+ "trigger_action": "link",
1096
+ "trigger_link_url": "https://example.com",
1097
+ }
1098
+ ],
1099
+ "remove_buttons": [],
1100
+ "view_configs": [],
1101
+ },
1102
+ },
1103
+ ))
1104
+ normalized_args = request.model_dump(mode="json")
1105
+ return _attach_builder_apply_envelope("app_custom_buttons_apply", _safe_tool_call(
1106
+ lambda: self._facade.app_custom_buttons_apply(profile=profile, request=request),
1107
+ error_code="CUSTOM_BUTTONS_APPLY_FAILED",
1108
+ normalized_args=normalized_args,
1109
+ suggested_next_call={"tool_name": "app_custom_buttons_apply", "arguments": {"profile": profile, **normalized_args}},
1110
+ ))
1111
+
1112
+ @tool_cn_name("应用关联资源声明式应用")
1113
+ def app_associated_resources_apply(
1114
+ self,
1115
+ *,
1116
+ profile: str,
1117
+ app_key: str,
1118
+ upsert_resources: list[JSONObject],
1119
+ remove_associated_item_ids: list[int],
1120
+ reorder_associated_item_ids: list[int],
1121
+ view_configs: list[JSONObject],
1122
+ patch_resources: list[JSONObject] | None = None,
1123
+ ) -> JSONObject:
1124
+ """执行应用关联资源 apply 逻辑。"""
1125
+ raw_request = {
1126
+ "app_key": app_key,
1127
+ "upsert_resources": upsert_resources,
1128
+ "patch_resources": patch_resources or [],
1129
+ "remove_associated_item_ids": remove_associated_item_ids,
1130
+ "reorder_associated_item_ids": reorder_associated_item_ids,
1131
+ "view_configs": view_configs,
1132
+ }
1133
+ try:
1134
+ request = AssociatedResourcesApplyRequest.model_validate(raw_request)
1135
+ except ValidationError as exc:
1136
+ return _attach_builder_apply_envelope("app_associated_resources_apply", _validation_failure(
1137
+ str(exc),
1138
+ tool_name="app_associated_resources_apply",
1139
+ exc=exc,
1140
+ suggested_next_call={
1141
+ "tool_name": "app_associated_resources_apply",
1142
+ "arguments": {
1143
+ "profile": profile,
1144
+ "app_key": app_key or "APP_KEY",
1145
+ "upsert_resources": [
1146
+ {
1147
+ "client_key": "customer_view",
1148
+ "graph_type": "view",
1149
+ "target_app_key": "TARGET_APP",
1150
+ "view_key": "VIEW_KEY",
1151
+ }
1152
+ ],
1153
+ "view_configs": [
1154
+ {
1155
+ "view_key": "MAIN_VIEW",
1156
+ "visible": True,
1157
+ "limit_type": "select",
1158
+ "associated_item_refs": ["customer_view"],
1159
+ }
1160
+ ],
1161
+ },
1162
+ },
1163
+ ))
1164
+ normalized_args = request.model_dump(mode="json", exclude_none=True)
1165
+ return _attach_builder_apply_envelope("app_associated_resources_apply", _safe_tool_call(
1166
+ lambda: self._facade.app_associated_resources_apply(profile=profile, request=request),
1167
+ error_code="ASSOCIATED_RESOURCES_APPLY_FAILED",
1168
+ normalized_args=normalized_args,
1169
+ suggested_next_call={"tool_name": "app_associated_resources_apply", "arguments": {"profile": profile, **normalized_args}},
1170
+ ))
1171
+
870
1172
  @tool_cn_name("应用按钮列表")
871
1173
  def app_custom_button_list(self, *, profile: str, app_key: str) -> JSONObject:
872
1174
  """执行应用相关逻辑。"""
@@ -909,7 +1211,10 @@ class AiBuilderTools(ToolBase):
909
1211
  "style_preset": "primary_blue",
910
1212
  "button_icon": "ex-plus-circle",
911
1213
  "trigger_action": "addData",
912
- "trigger_add_data_config": {"related_app_key": "TARGET_APP_KEY", "que_relation": []},
1214
+ "trigger_add_data_config": {
1215
+ "target_app_key": "TARGET_APP_KEY",
1216
+ "field_mappings": [{"source_field": "客户名称", "target_field": "客户"}],
1217
+ },
913
1218
  },
914
1219
  },
915
1220
  },
@@ -1001,8 +1306,10 @@ class AiBuilderTools(ToolBase):
1001
1306
  )
1002
1307
 
1003
1308
  @tool_cn_name("应用字段详情查询")
1004
- def app_get_fields(self, *, profile: str, app_key: str) -> JSONObject:
1309
+ def app_get_fields(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
1005
1310
  """执行应用相关逻辑。"""
1311
+ if app_keys:
1312
+ return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_fields, data_key="fields", tool_name="app_get_fields")
1006
1313
  normalized_args = {"app_key": app_key}
1007
1314
  return _safe_tool_call(
1008
1315
  lambda: self._facade.app_get_fields(profile=profile, app_key=app_key),
@@ -1041,8 +1348,10 @@ class AiBuilderTools(ToolBase):
1041
1348
  )
1042
1349
 
1043
1350
  @tool_cn_name("应用布局详情查询")
1044
- def app_get_layout(self, *, profile: str, app_key: str) -> JSONObject:
1351
+ def app_get_layout(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
1045
1352
  """执行应用相关逻辑。"""
1353
+ if app_keys:
1354
+ return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_layout, data_key="sections", tool_name="app_get_layout")
1046
1355
  normalized_args = {"app_key": app_key}
1047
1356
  return _safe_tool_call(
1048
1357
  lambda: self._facade.app_get_layout(profile=profile, app_key=app_key),
@@ -1063,8 +1372,10 @@ class AiBuilderTools(ToolBase):
1063
1372
  )
1064
1373
 
1065
1374
  @tool_cn_name("应用视图详情查询")
1066
- def app_get_views(self, *, profile: str, app_key: str) -> JSONObject:
1375
+ def app_get_views(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
1067
1376
  """执行应用相关逻辑。"""
1377
+ if app_keys:
1378
+ return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_views, data_key="views", tool_name="app_get_views")
1068
1379
  normalized_args = {"app_key": app_key}
1069
1380
  return _safe_tool_call(
1070
1381
  lambda: self._facade.app_get_views(profile=profile, app_key=app_key),
@@ -1073,26 +1384,27 @@ class AiBuilderTools(ToolBase):
1073
1384
  suggested_next_call={"tool_name": "app_get_views", "arguments": {"profile": profile, "app_key": app_key}},
1074
1385
  )
1075
1386
 
1076
- @tool_cn_name("应用流程摘要读取")
1077
- def app_read_flow_summary(self, *, profile: str, app_key: str) -> JSONObject:
1078
- """执行应用相关逻辑。"""
1079
- normalized_args = {"app_key": app_key}
1387
+ @tool_cn_name("Workflow Spec Schema")
1388
+ def app_flow_get_schema(self, *, profile: str, schema_version: str | None = None) -> JSONObject:
1389
+ normalized_args = {"schema_version": schema_version}
1080
1390
  return _safe_tool_call(
1081
- lambda: self._facade.app_read_flow_summary(profile=profile, app_key=app_key),
1082
- error_code="FLOW_READ_FAILED",
1391
+ lambda: self._facade.flow_get_schema(profile=profile, schema_version=schema_version),
1392
+ error_code="FLOW_SPEC_SCHEMA_FAILED",
1083
1393
  normalized_args=normalized_args,
1084
- suggested_next_call={"tool_name": "app_get_flow", "arguments": {"profile": profile, "app_key": app_key}},
1394
+ suggested_next_call={"tool_name": "app_flow_get_schema", "arguments": {"profile": profile, **normalized_args}},
1085
1395
  )
1086
1396
 
1087
- @tool_cn_name("应用流程详情查询")
1088
- def app_get_flow(self, *, profile: str, app_key: str) -> JSONObject:
1397
+ @tool_cn_name("Workflow Spec 读取")
1398
+ def app_get_flow(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None, version_id: str | None = None) -> JSONObject:
1089
1399
  """执行应用相关逻辑。"""
1090
- normalized_args = {"app_key": app_key}
1400
+ if app_keys:
1401
+ return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_flow, data_key="spec", tool_name="app_get_flow")
1402
+ normalized_args = {"app_key": app_key, "version_id": version_id}
1091
1403
  return _safe_tool_call(
1092
- lambda: self._facade.app_get_flow(profile=profile, app_key=app_key),
1404
+ lambda: self._facade.flow_get(profile=profile, app_key=app_key, version_id=version_id),
1093
1405
  error_code="APP_GET_FLOW_FAILED",
1094
1406
  normalized_args=normalized_args,
1095
- suggested_next_call={"tool_name": "app_get_flow", "arguments": {"profile": profile, "app_key": app_key}},
1407
+ suggested_next_call={"tool_name": "app_flow_get", "arguments": {"profile": profile, "app_key": app_key}},
1096
1408
  )
1097
1409
 
1098
1410
  @tool_cn_name("应用图表摘要读取")
@@ -1107,8 +1419,10 @@ class AiBuilderTools(ToolBase):
1107
1419
  )
1108
1420
 
1109
1421
  @tool_cn_name("应用图表详情查询")
1110
- def app_get_charts(self, *, profile: str, app_key: str) -> JSONObject:
1422
+ def app_get_charts(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
1111
1423
  """执行应用相关逻辑。"""
1424
+ if app_keys:
1425
+ return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_charts, data_key="charts", tool_name="app_get_charts")
1112
1426
  normalized_args = {"app_key": app_key}
1113
1427
  return _safe_tool_call(
1114
1428
  lambda: self._facade.app_get_charts(profile=profile, app_key=app_key),
@@ -1117,6 +1431,32 @@ class AiBuilderTools(ToolBase):
1117
1431
  suggested_next_call={"tool_name": "app_get_charts", "arguments": {"profile": profile, "app_key": app_key}},
1118
1432
  )
1119
1433
 
1434
+ @tool_cn_name("自定义按钮读取")
1435
+ def app_get_buttons(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
1436
+ """执行按钮相关逻辑。"""
1437
+ if app_keys:
1438
+ return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_buttons, data_key="buttons", tool_name="app_get_buttons")
1439
+ normalized_args = {"app_key": app_key}
1440
+ return _safe_tool_call(
1441
+ lambda: self._facade.app_get_buttons(profile=profile, app_key=app_key),
1442
+ error_code="APP_GET_BUTTONS_FAILED",
1443
+ normalized_args=normalized_args,
1444
+ suggested_next_call={"tool_name": "app_get_buttons", "arguments": {"profile": profile, "app_key": app_key}},
1445
+ )
1446
+
1447
+ @tool_cn_name("关联资源读取")
1448
+ def app_get_associated_resources(self, *, profile: str, app_key: str = "", app_keys: list[str] | None = None) -> JSONObject:
1449
+ """执行关联资源相关逻辑。"""
1450
+ if app_keys:
1451
+ return self._facade._batch_read_app_keys(profile=profile, app_keys=app_keys, single_reader=self._facade.app_get_associated_resources, data_key="associated_resources", tool_name="app_get_associated_resources")
1452
+ normalized_args = {"app_key": app_key}
1453
+ return _safe_tool_call(
1454
+ lambda: self._facade.app_get_associated_resources(profile=profile, app_key=app_key),
1455
+ error_code="APP_GET_ASSOCIATED_RESOURCES_FAILED",
1456
+ normalized_args=normalized_args,
1457
+ suggested_next_call={"tool_name": "app_get_associated_resources", "arguments": {"profile": profile, "app_key": app_key}},
1458
+ )
1459
+
1120
1460
  @tool_cn_name("门户列表查询")
1121
1461
  def portal_list(self, *, profile: str) -> JSONObject:
1122
1462
  """执行门户相关逻辑。"""
@@ -1152,7 +1492,7 @@ class AiBuilderTools(ToolBase):
1152
1492
  @tool_cn_name("视图详情查询")
1153
1493
  def view_get(self, *, profile: str, view_key: str = "", viewgraph_key: str = "") -> JSONObject:
1154
1494
  """执行视图相关逻辑。"""
1155
- resolved_view_key = str(view_key or viewgraph_key or "").strip()
1495
+ resolved_view_key = _normalize_builder_view_key(str(view_key or viewgraph_key or "").strip())
1156
1496
  normalized_args = {"view_key": resolved_view_key}
1157
1497
  return _safe_tool_call(
1158
1498
  lambda: self._facade.view_get(profile=profile, view_key=resolved_view_key),
@@ -1287,52 +1627,6 @@ class AiBuilderTools(ToolBase):
1287
1627
  suggested_next_call={"tool_name": "app_layout_plan", "arguments": {"profile": profile, **normalized_request}},
1288
1628
  )
1289
1629
 
1290
- @tool_cn_name("应用流程规划")
1291
- def app_flow_plan(
1292
- self,
1293
- *,
1294
- profile: str,
1295
- app_key: str,
1296
- mode: str = "replace",
1297
- nodes: list[JSONObject] | None = None,
1298
- transitions: list[JSONObject] | None = None,
1299
- preset: str | None = None,
1300
- ) -> JSONObject:
1301
- """执行应用相关逻辑。"""
1302
- try:
1303
- request = FlowPlanRequest.model_validate(
1304
- {
1305
- "app_key": app_key,
1306
- "mode": mode,
1307
- "nodes": nodes or [],
1308
- "transitions": transitions or [],
1309
- "preset": preset,
1310
- }
1311
- )
1312
- except ValidationError as exc:
1313
- return _validation_failure(
1314
- str(exc),
1315
- tool_name="app_flow_plan",
1316
- exc=exc,
1317
- suggested_next_call={
1318
- "tool_name": "app_flow_plan",
1319
- "arguments": {
1320
- "profile": profile,
1321
- "app_key": app_key,
1322
- "mode": "replace",
1323
- "preset": "basic_approval",
1324
- "nodes": [],
1325
- "transitions": [],
1326
- },
1327
- },
1328
- )
1329
- return _safe_tool_call(
1330
- lambda: self._facade.app_flow_plan(profile=profile, request=request),
1331
- error_code="FLOW_PLAN_FAILED",
1332
- normalized_args=request.model_dump(mode="json"),
1333
- suggested_next_call={"tool_name": "app_flow_plan", "arguments": {"profile": profile, **request.model_dump(mode="json")}},
1334
- )
1335
-
1336
1630
  @tool_cn_name("应用视图规划")
1337
1631
  def app_views_plan(
1338
1632
  self,
@@ -1393,8 +1687,19 @@ class AiBuilderTools(ToolBase):
1393
1687
  add_fields: list[JSONObject],
1394
1688
  update_fields: list[JSONObject],
1395
1689
  remove_fields: list[JSONObject],
1690
+ apps: list[JSONObject] | None = None,
1396
1691
  ) -> JSONObject:
1397
1692
  """执行应用相关逻辑。"""
1693
+ if apps:
1694
+ result = self._app_schema_apply_multi(
1695
+ profile=profile,
1696
+ package_id=package_id,
1697
+ visibility=visibility,
1698
+ create_if_missing=create_if_missing,
1699
+ publish=publish,
1700
+ apps=apps,
1701
+ )
1702
+ return _attach_builder_apply_envelope("app_schema_apply", result)
1398
1703
  result = self._app_schema_apply_once(
1399
1704
  profile=profile,
1400
1705
  app_key=app_key,
@@ -1410,7 +1715,7 @@ class AiBuilderTools(ToolBase):
1410
1715
  update_fields=update_fields,
1411
1716
  remove_fields=remove_fields,
1412
1717
  )
1413
- return self._retry_after_self_lock_release(
1718
+ result = self._retry_after_self_lock_release(
1414
1719
  profile=profile,
1415
1720
  result=result,
1416
1721
  retry_call=lambda: self._app_schema_apply_once(
@@ -1429,6 +1734,325 @@ class AiBuilderTools(ToolBase):
1429
1734
  remove_fields=remove_fields,
1430
1735
  ),
1431
1736
  )
1737
+ return _attach_builder_apply_envelope("app_schema_apply", result)
1738
+
1739
+ def _app_schema_apply_multi(
1740
+ self,
1741
+ *,
1742
+ profile: str,
1743
+ package_id: int | None,
1744
+ visibility: JSONObject | None,
1745
+ create_if_missing: bool,
1746
+ publish: bool,
1747
+ apps: list[JSONObject],
1748
+ ) -> JSONObject:
1749
+ normalized_args: JSONObject = {
1750
+ "package_id": package_id,
1751
+ "create_if_missing": create_if_missing,
1752
+ "publish": publish,
1753
+ "apps": deepcopy(apps),
1754
+ }
1755
+ if visibility is not None:
1756
+ normalized_args["visibility"] = deepcopy(visibility)
1757
+ if package_id is None:
1758
+ return _config_failure(
1759
+ tool_name="app_schema_apply",
1760
+ message="app_schema_apply multi-app mode requires package_id.",
1761
+ fix_hint="Pass package_id and apps[].app_name for new apps, or apps[].app_key for existing apps.",
1762
+ )
1763
+ if not apps:
1764
+ return _config_failure(
1765
+ tool_name="app_schema_apply",
1766
+ message="app_schema_apply multi-app mode requires non-empty apps.",
1767
+ fix_hint="Pass apps as a non-empty list of app schema items.",
1768
+ )
1769
+ icon_errors: list[JSONObject] = []
1770
+ seen_new_app_icons: dict[str, int] = {}
1771
+ for index, raw_item in enumerate(apps):
1772
+ if not isinstance(raw_item, dict):
1773
+ continue
1774
+ app_key = str(raw_item.get("app_key") or raw_item.get("appKey") or "").strip()
1775
+ creating_item = not app_key
1776
+ icon_failure = _validate_workspace_icon_for_builder(
1777
+ tool_name="app_schema_apply",
1778
+ icon=str(raw_item.get("icon") or ""),
1779
+ color=str(raw_item.get("color") or ""),
1780
+ creating=creating_item,
1781
+ )
1782
+ if icon_failure is not None:
1783
+ icon_errors.append(
1784
+ {
1785
+ "index": index,
1786
+ "row_number": index + 1,
1787
+ "error_code": icon_failure.get("error_code"),
1788
+ "message": icon_failure.get("message"),
1789
+ "details": icon_failure.get("details"),
1790
+ }
1791
+ )
1792
+ continue
1793
+ _ok, _error_code, _message, icon_details = validate_workspace_icon_choice(
1794
+ icon=str(raw_item.get("icon") or ""),
1795
+ color=str(raw_item.get("color") or ""),
1796
+ require_explicit=creating_item,
1797
+ disallow_generic=creating_item,
1798
+ )
1799
+ normalized_icon = str(icon_details.get("normalized_icon") or "").strip()
1800
+ if creating_item and normalized_icon:
1801
+ if normalized_icon in seen_new_app_icons:
1802
+ icon_errors.append(
1803
+ {
1804
+ "index": index,
1805
+ "row_number": index + 1,
1806
+ "error_code": "DUPLICATE_WORKSPACE_ICON_IN_BATCH",
1807
+ "message": f"apps[{index}] reuses icon '{normalized_icon}' from apps[{seen_new_app_icons[normalized_icon]}]",
1808
+ "details": {
1809
+ "icon": normalized_icon,
1810
+ "first_index": seen_new_app_icons[normalized_icon],
1811
+ "duplicate_index": index,
1812
+ "icon_catalog_command": "qingflow --json builder icon catalog",
1813
+ },
1814
+ }
1815
+ )
1816
+ else:
1817
+ seen_new_app_icons[normalized_icon] = index
1818
+ if icon_errors:
1819
+ return _config_failure(
1820
+ tool_name="app_schema_apply",
1821
+ error_code="WORKSPACE_ICON_BATCH_INVALID",
1822
+ message="one or more apps have invalid workspace icon configuration",
1823
+ fix_hint="Call `qingflow --json builder icon catalog`, choose a distinct non-template icon and color for each new app, then retry.",
1824
+ details={"icon_errors": icon_errors},
1825
+ allowed_values={
1826
+ "workspace_icon.icon_names": list(WORKSPACE_ICON_NAMES),
1827
+ "workspace_icon.icon_colors": list(WORKSPACE_ICON_COLORS),
1828
+ "workspace_icon.generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
1829
+ },
1830
+ )
1831
+
1832
+ client_key_to_app_key: dict[str, str] = {}
1833
+ created_app_keys: list[str] = []
1834
+ results: list[JSONObject] = []
1835
+ any_write_executed = False
1836
+ client_keys: set[str] = set()
1837
+
1838
+ for index, raw_item in enumerate(apps):
1839
+ if not isinstance(raw_item, dict):
1840
+ results.append(_multi_app_item_failure(index, raw_item, "INVALID_APP_ITEM", "apps[] items must be objects"))
1841
+ continue
1842
+ item = deepcopy(raw_item)
1843
+ client_key = str(item.get("client_key") or item.get("clientKey") or "").strip()
1844
+ app_name = str(item.get("app_name") or item.get("appTitle") or item.get("app_title") or item.get("title") or "").strip()
1845
+ app_key = str(item.get("app_key") or item.get("appKey") or "").strip()
1846
+ if client_key:
1847
+ if client_key in client_keys:
1848
+ results.append(_multi_app_item_failure(index, item, "DUPLICATE_CLIENT_KEY", f"duplicate client_key '{client_key}'"))
1849
+ continue
1850
+ client_keys.add(client_key)
1851
+ if not app_key and not app_name:
1852
+ results.append(_multi_app_item_failure(index, item, "APP_SELECTOR_REQUIRED", "apps[] requires app_key or app_name"))
1853
+ continue
1854
+
1855
+ initial_add_fields, deferred_add_fields = _split_multi_app_initial_add_fields(item, is_new_app=not bool(app_key))
1856
+ item["_deferred_add_fields"] = deferred_add_fields
1857
+ shell = self._app_schema_apply_once(
1858
+ profile=profile,
1859
+ app_key=app_key,
1860
+ package_id=package_id if not app_key else None,
1861
+ app_name=app_name,
1862
+ app_title="",
1863
+ icon=str(item.get("icon") or ""),
1864
+ color=str(item.get("color") or ""),
1865
+ visibility=item.get("visibility", visibility),
1866
+ create_if_missing=create_if_missing and not app_key,
1867
+ publish=publish and not deferred_add_fields,
1868
+ add_fields=initial_add_fields,
1869
+ update_fields=[],
1870
+ remove_fields=[],
1871
+ )
1872
+ public_shell = _publicize_package_fields(shell)
1873
+ resolved_key = str(public_shell.get("app_key") or "").strip()
1874
+ if public_shell.get("status") not in {"success", "partial_success"} or not resolved_key:
1875
+ results.append({
1876
+ "index": index,
1877
+ "row_number": index + 1,
1878
+ "client_key": client_key or None,
1879
+ "app_name": app_name or None,
1880
+ "app_key": resolved_key or app_key or None,
1881
+ "status": "failed",
1882
+ "stage": "resolve_or_create_shell",
1883
+ "error_code": public_shell.get("error_code") or "APP_SHELL_APPLY_FAILED",
1884
+ "message": public_shell.get("message") or "app shell resolve/create failed",
1885
+ "safe_to_retry": not any_write_executed,
1886
+ })
1887
+ continue
1888
+ if bool(public_shell.get("created")):
1889
+ created_app_keys.append(resolved_key)
1890
+ if _schema_apply_result_has_write(public_shell):
1891
+ any_write_executed = True
1892
+ if client_key:
1893
+ client_key_to_app_key[client_key] = resolved_key
1894
+ results.append({
1895
+ "index": index,
1896
+ "row_number": index + 1,
1897
+ "client_key": client_key or None,
1898
+ "app_name": str(public_shell.get("app_name_after") or public_shell.get("app_name") or app_name or "").strip() or None,
1899
+ "app_key": resolved_key,
1900
+ "status": "shell_ready",
1901
+ "created": bool(public_shell.get("created")),
1902
+ "shell_result": public_shell,
1903
+ "shell_field_diff": public_shell.get("field_diff") or {},
1904
+ "shell_field_diff_details": public_shell.get("field_diff_details") or {},
1905
+ "deferred_add_fields": deferred_add_fields,
1906
+ })
1907
+
1908
+ final_items: list[JSONObject] = []
1909
+ for index, raw_item in enumerate(apps):
1910
+ existing = next((item for item in results if item.get("index") == index), None)
1911
+ if not existing or existing.get("status") != "shell_ready":
1912
+ if existing:
1913
+ final_items.append(existing)
1914
+ continue
1915
+ item = deepcopy(raw_item)
1916
+ app_key = str(existing.get("app_key") or "").strip()
1917
+ try:
1918
+ compiled_item = _compile_multi_app_schema_item_refs(item, client_key_to_app_key)
1919
+ except ValueError as error:
1920
+ final_items.append({
1921
+ **{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
1922
+ "status": "failed",
1923
+ "stage": "compile_relation_refs",
1924
+ "error_code": "TARGET_APP_REF_NOT_FOUND",
1925
+ "message": str(error),
1926
+ "safe_to_retry": False,
1927
+ })
1928
+ any_write_executed = True
1929
+ continue
1930
+
1931
+ deferred_add_fields = (
1932
+ _compiled_multi_app_deferred_add_fields(compiled_item, existing)
1933
+ if bool(existing.get("created"))
1934
+ else list(compiled_item.get("add_fields") or [])
1935
+ )
1936
+ update_fields = list(compiled_item.get("update_fields") or [])
1937
+ remove_fields = list(compiled_item.get("remove_fields") or [])
1938
+ if bool(existing.get("created")) and not deferred_add_fields and not update_fields and not remove_fields:
1939
+ shell_result = existing.get("shell_result") if isinstance(existing.get("shell_result"), dict) else {}
1940
+ item_status = shell_result.get("status") if shell_result.get("status") in {"success", "partial_success"} else "failed"
1941
+ shell_field_diff = existing.get("shell_field_diff") if isinstance(existing.get("shell_field_diff"), dict) else {}
1942
+ shell_field_diff_details = existing.get("shell_field_diff_details") if isinstance(existing.get("shell_field_diff_details"), dict) else {}
1943
+ final_items.append({
1944
+ **{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
1945
+ "status": item_status,
1946
+ "stage": "schema_apply",
1947
+ "field_diff": shell_field_diff,
1948
+ "field_diff_details": shell_field_diff_details,
1949
+ "shell_field_diff": shell_field_diff,
1950
+ "shell_field_diff_details": shell_field_diff_details,
1951
+ "published": bool(shell_result.get("published")),
1952
+ "verified": bool(shell_result.get("verified")),
1953
+ "error_code": shell_result.get("error_code"),
1954
+ "message": shell_result.get("message"),
1955
+ "safe_to_retry": False,
1956
+ })
1957
+ continue
1958
+
1959
+ field_result = self._app_schema_apply_once(
1960
+ profile=profile,
1961
+ app_key=app_key,
1962
+ package_id=None,
1963
+ app_name=str(compiled_item.get("app_name") or compiled_item.get("appTitle") or compiled_item.get("app_title") or ""),
1964
+ app_title="",
1965
+ icon=str(compiled_item.get("icon") or ""),
1966
+ color=str(compiled_item.get("color") or ""),
1967
+ visibility=compiled_item.get("visibility"),
1968
+ create_if_missing=False,
1969
+ publish=publish,
1970
+ add_fields=deferred_add_fields,
1971
+ update_fields=update_fields,
1972
+ remove_fields=remove_fields,
1973
+ )
1974
+ public_result = _publicize_package_fields(field_result)
1975
+ if _schema_apply_result_has_write(public_result):
1976
+ any_write_executed = True
1977
+ item_status = public_result.get("status") if public_result.get("status") in {"success", "partial_success"} else "failed"
1978
+ shell_field_diff = existing.get("shell_field_diff") if isinstance(existing.get("shell_field_diff"), dict) else {}
1979
+ shell_field_diff_details = existing.get("shell_field_diff_details") if isinstance(existing.get("shell_field_diff_details"), dict) else {}
1980
+ field_diff = _merge_schema_field_diffs(shell_field_diff, public_result.get("field_diff") or {})
1981
+ field_diff_details = _merge_schema_field_diffs(shell_field_diff_details, public_result.get("field_diff_details") or {})
1982
+ final_items.append({
1983
+ **{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
1984
+ "status": item_status,
1985
+ "stage": "schema_apply",
1986
+ "field_diff": field_diff,
1987
+ "field_diff_details": field_diff_details,
1988
+ "shell_field_diff": shell_field_diff,
1989
+ "shell_field_diff_details": shell_field_diff_details,
1990
+ "published": bool(public_result.get("published")),
1991
+ "verified": bool(public_result.get("verified")),
1992
+ "error_code": public_result.get("error_code"),
1993
+ "message": public_result.get("message"),
1994
+ "safe_to_retry": False,
1995
+ })
1996
+
1997
+ # Inline layout: apply layout for each successful app that included a layout field.
1998
+ for index, raw_item in enumerate(apps):
1999
+ if not isinstance(raw_item, dict):
2000
+ continue
2001
+ layout_spec = raw_item.get("layout")
2002
+ if not isinstance(layout_spec, dict) or not layout_spec:
2003
+ continue
2004
+ final_item = next((it for it in final_items if it.get("index") == index), None)
2005
+ if not final_item or final_item.get("status") not in {"success", "partial_success"}:
2006
+ continue
2007
+ resolved_app_key = str(final_item.get("app_key") or "").strip()
2008
+ if not resolved_app_key:
2009
+ continue
2010
+ layout_mode = str(layout_spec.get("mode") or "merge").strip() or "merge"
2011
+ layout_sections = layout_spec.get("sections") or []
2012
+ try:
2013
+ layout_result = self.app_layout_apply(
2014
+ profile=profile,
2015
+ app_key=resolved_app_key,
2016
+ mode=layout_mode,
2017
+ publish=publish,
2018
+ sections=list(layout_sections),
2019
+ )
2020
+ layout_ok = layout_result.get("status") in {"success", "partial_success"}
2021
+ except Exception as layout_error:
2022
+ layout_ok = False
2023
+ layout_result = {"status": "failed", "message": str(layout_error)}
2024
+ if layout_ok:
2025
+ final_item["layout_applied"] = True
2026
+ final_item["layout_status"] = layout_result.get("status")
2027
+ else:
2028
+ final_item["layout_warning"] = layout_result.get("error_code") or "LAYOUT_APPLY_FAILED"
2029
+ final_item["layout_message"] = layout_result.get("message")
2030
+
2031
+ succeeded = sum(1 for item in final_items if item.get("status") in {"success", "partial_success"})
2032
+ failed = len(final_items) - succeeded
2033
+ overall_status = "success" if failed == 0 else ("partial_success" if succeeded > 0 or any_write_executed else "failed")
2034
+ return {
2035
+ "status": overall_status,
2036
+ "mode": "multi_app",
2037
+ "total": len(apps),
2038
+ "succeeded": succeeded,
2039
+ "failed": failed,
2040
+ "created_app_keys": created_app_keys,
2041
+ "write_executed": any_write_executed,
2042
+ "safe_to_retry": not any_write_executed,
2043
+ "package_id": package_id,
2044
+ "publish_requested": publish,
2045
+ "apps": final_items,
2046
+ "normalized_args": normalized_args,
2047
+ "verification": {
2048
+ "all_apps_succeeded": failed == 0,
2049
+ "created_app_count": len(created_app_keys),
2050
+ },
2051
+ "request_id": None,
2052
+ "error_code": None if overall_status != "failed" else "MULTI_APP_SCHEMA_APPLY_FAILED",
2053
+ "recoverable": overall_status != "success",
2054
+ "message": "multi-app schema apply completed" if overall_status != "failed" else "multi-app schema apply failed",
2055
+ }
1432
2056
 
1433
2057
  def _app_schema_apply_once(
1434
2058
  self,
@@ -1449,6 +2073,14 @@ class AiBuilderTools(ToolBase):
1449
2073
  ) -> JSONObject:
1450
2074
  """执行内部辅助逻辑。"""
1451
2075
  effective_app_name = app_name or app_title
2076
+ icon_failure = _validate_workspace_icon_for_builder(
2077
+ tool_name="app_schema_apply",
2078
+ icon=icon,
2079
+ color=color,
2080
+ creating=not bool(str(app_key or "").strip()) and bool(create_if_missing),
2081
+ )
2082
+ if icon_failure is not None:
2083
+ return icon_failure
1452
2084
  plan_result = self._rewrite_plan_result_for_apply(
1453
2085
  result=self.app_schema_plan(
1454
2086
  profile=profile,
@@ -1535,8 +2167,21 @@ class AiBuilderTools(ToolBase):
1535
2167
  return _publicize_package_fields(result)
1536
2168
 
1537
2169
  @tool_cn_name("应用布局应用")
1538
- def app_layout_apply(self, *, profile: str, app_key: str, mode: str = "merge", publish: bool = True, sections: list[JSONObject]) -> JSONObject:
2170
+ def app_layout_apply(self, *, profile: str, app_key: str, mode: str = "merge", publish: bool = True, sections: list[JSONObject], apps: list[JSONObject] | None = None) -> JSONObject:
1539
2171
  """执行应用相关逻辑。"""
2172
+ if apps:
2173
+ return self._facade._batch_write_apps(
2174
+ profile=profile,
2175
+ apps=apps,
2176
+ single_writer=lambda profile, app_key, **kw: self.app_layout_apply(
2177
+ profile=profile,
2178
+ app_key=app_key,
2179
+ mode=kw.get("mode", mode),
2180
+ publish=kw.get("publish", publish),
2181
+ sections=kw.get("sections", []),
2182
+ ),
2183
+ tool_name="app_layout_apply",
2184
+ )
1540
2185
  result = self._app_layout_apply_once(
1541
2186
  profile=profile,
1542
2187
  app_key=app_key,
@@ -1544,7 +2189,7 @@ class AiBuilderTools(ToolBase):
1544
2189
  publish=publish,
1545
2190
  sections=sections,
1546
2191
  )
1547
- return self._retry_after_self_lock_release(
2192
+ result = self._retry_after_self_lock_release(
1548
2193
  profile=profile,
1549
2194
  result=result,
1550
2195
  retry_call=lambda: self._app_layout_apply_once(
@@ -1555,6 +2200,7 @@ class AiBuilderTools(ToolBase):
1555
2200
  sections=sections,
1556
2201
  ),
1557
2202
  )
2203
+ return _attach_builder_apply_envelope("app_layout_apply", result)
1558
2204
 
1559
2205
  def _app_layout_apply_once(self, *, profile: str, app_key: str, mode: str = "merge", publish: bool = True, sections: list[JSONObject]) -> JSONObject:
1560
2206
  """执行内部辅助逻辑。"""
@@ -1620,130 +2266,104 @@ class AiBuilderTools(ToolBase):
1620
2266
  *,
1621
2267
  profile: str,
1622
2268
  app_key: str,
1623
- mode: str = "replace",
2269
+ spec: JSONObject,
1624
2270
  publish: bool = True,
1625
- nodes: list[JSONObject],
1626
- transitions: list[JSONObject],
2271
+ idempotency_key: str | None = None,
2272
+ schema_version: str | None = None,
2273
+ patch_nodes: list[JSONObject] | None = None,
1627
2274
  ) -> JSONObject:
1628
2275
  """执行应用相关逻辑。"""
1629
- result = self._app_flow_apply_once(
1630
- profile=profile,
1631
- app_key=app_key,
1632
- mode=mode,
1633
- publish=publish,
1634
- nodes=nodes,
1635
- transitions=transitions,
2276
+ if patch_nodes:
2277
+ result = _safe_tool_call(
2278
+ lambda: self._facade.flow_patch_nodes(
2279
+ profile=profile,
2280
+ app_key=app_key,
2281
+ patch_nodes=patch_nodes,
2282
+ publish=publish,
2283
+ idempotency_key=idempotency_key,
2284
+ schema_version=schema_version,
2285
+ ),
2286
+ error_code="FLOW_APPLY_FAILED",
2287
+ normalized_args={"app_key": app_key, "patch_nodes": patch_nodes, "publish": publish},
2288
+ suggested_next_call={"tool_name": "app_flow_apply", "arguments": {"profile": profile, "app_key": app_key, "patch_nodes": patch_nodes}},
2289
+ )
2290
+ return _attach_builder_apply_envelope("app_flow_apply", result)
2291
+ if not isinstance(spec, dict) or not spec:
2292
+ return _config_failure(
2293
+ tool_name="app_flow_apply",
2294
+ message="app_flow_apply requires a non-empty WorkflowSpecDTO `spec` object.",
2295
+ fix_hint="Call app_flow_get_schema, then app_flow_get for GET-first baseline, patch spec, and apply.",
2296
+ )
2297
+ normalized_args = {
2298
+ "app_key": app_key,
2299
+ "publish": publish,
2300
+ "spec": spec,
2301
+ "idempotency_key": idempotency_key,
2302
+ "schema_version": schema_version,
2303
+ }
2304
+ result = _safe_tool_call(
2305
+ lambda: self._facade.flow_apply(
2306
+ profile=profile,
2307
+ app_key=app_key,
2308
+ spec=spec,
2309
+ publish=publish,
2310
+ idempotency_key=idempotency_key,
2311
+ schema_version=schema_version,
2312
+ ),
2313
+ error_code="FLOW_APPLY_FAILED",
2314
+ normalized_args=normalized_args,
2315
+ suggested_next_call={"tool_name": "app_flow_apply", "arguments": {"profile": profile, **normalized_args}},
1636
2316
  )
1637
- return self._retry_after_self_lock_release(
2317
+ result = self._retry_after_self_lock_release(
1638
2318
  profile=profile,
1639
2319
  result=result,
1640
- retry_call=lambda: self._app_flow_apply_once(
2320
+ retry_call=lambda: self._facade.flow_apply(
1641
2321
  profile=profile,
1642
2322
  app_key=app_key,
1643
- mode=mode,
2323
+ spec=spec,
1644
2324
  publish=publish,
1645
- nodes=nodes,
1646
- transitions=transitions,
2325
+ idempotency_key=idempotency_key,
2326
+ schema_version=schema_version,
1647
2327
  ),
1648
2328
  )
2329
+ return _attach_builder_apply_envelope("app_flow_apply", result)
1649
2330
 
1650
- def _app_flow_apply_once(
2331
+ @tool_cn_name("应用视图应用")
2332
+ def app_views_apply(
1651
2333
  self,
1652
2334
  *,
1653
2335
  profile: str,
1654
2336
  app_key: str,
1655
- mode: str = "replace",
1656
2337
  publish: bool = True,
1657
- nodes: list[JSONObject],
1658
- transitions: list[JSONObject],
2338
+ upsert_views: list[JSONObject],
2339
+ patch_views: list[JSONObject] | None = None,
2340
+ remove_views: list[str],
2341
+ apps: list[JSONObject] | None = None,
1659
2342
  ) -> JSONObject:
1660
- """执行内部辅助逻辑。"""
1661
- plan_result = self._rewrite_plan_result_for_apply(
1662
- result=self.app_flow_plan(
2343
+ """执行应用相关逻辑。"""
2344
+ if apps:
2345
+ return self._facade._batch_write_apps(
1663
2346
  profile=profile,
1664
- app_key=app_key,
1665
- mode=mode,
1666
- nodes=nodes,
1667
- transitions=transitions,
1668
- preset=None,
1669
- ),
2347
+ apps=apps,
2348
+ single_writer=lambda profile, app_key, **kw: self.app_views_apply(
2349
+ profile=profile,
2350
+ app_key=app_key,
2351
+ publish=kw.get("publish", publish),
2352
+ upsert_views=kw.get("upsert_views", []),
2353
+ patch_views=kw.get("patch_views", []),
2354
+ remove_views=kw.get("remove_views", []),
2355
+ ),
2356
+ tool_name="app_views_apply",
2357
+ )
2358
+ result = self._app_views_apply_once(
1670
2359
  profile=profile,
2360
+ app_key=app_key,
1671
2361
  publish=publish,
1672
- plan_tool_name="app_flow_plan",
1673
- apply_tool_name="app_flow_apply",
2362
+ upsert_views=upsert_views,
2363
+ patch_views=patch_views or [],
2364
+ remove_views=remove_views,
1674
2365
  )
1675
- if not isinstance(plan_result, dict) or plan_result.get("status") != "success":
1676
- return plan_result
1677
- plan_args = plan_result.get("normalized_args")
1678
- if not isinstance(plan_args, dict):
1679
- plan_args = {}
1680
- try:
1681
- request = FlowPlanRequest.model_validate(
1682
- {
1683
- "app_key": plan_args.get("app_key") or app_key,
1684
- "mode": plan_args.get("mode") or mode,
1685
- "nodes": plan_args.get("nodes") or [],
1686
- "transitions": plan_args.get("transitions") or [],
1687
- "preset": None,
1688
- }
1689
- )
1690
- except ValidationError as exc:
1691
- return _validation_failure(
1692
- str(exc),
1693
- tool_name="app_flow_apply",
1694
- exc=exc,
1695
- suggested_next_call={
1696
- "tool_name": "app_flow_apply",
1697
- "arguments": {
1698
- "profile": profile,
1699
- "app_key": str(plan_args.get("app_key") or app_key),
1700
- "mode": str(plan_args.get("mode") or "replace"),
1701
- "publish": publish,
1702
- "nodes": plan_args.get("nodes") or [{"id": "start", "type": "start", "name": "发起"}],
1703
- "transitions": plan_args.get("transitions") or [],
1704
- },
1705
- },
1706
- )
1707
- normalized_args = {
1708
- "app_key": request.app_key,
1709
- "mode": request.mode,
1710
- "publish": publish,
1711
- "nodes": [node.model_dump(mode="json") for node in request.nodes],
1712
- "transitions": [transition.model_dump(mode="json", by_alias=True) for transition in request.transitions],
1713
- }
1714
- return _safe_tool_call(
1715
- lambda: self._facade.app_flow_apply(
1716
- profile=profile,
1717
- app_key=request.app_key,
1718
- mode=request.mode,
1719
- publish=publish,
1720
- nodes=[node.model_dump(mode="json") for node in request.nodes],
1721
- transitions=[transition.model_dump(mode="json", by_alias=True) for transition in request.transitions],
1722
- ),
1723
- error_code="FLOW_APPLY_FAILED",
1724
- normalized_args=normalized_args,
1725
- suggested_next_call={"tool_name": "app_flow_apply", "arguments": {"profile": profile, **normalized_args}},
1726
- )
1727
-
1728
- @tool_cn_name("应用视图应用")
1729
- def app_views_apply(
1730
- self,
1731
- *,
1732
- profile: str,
1733
- app_key: str,
1734
- publish: bool = True,
1735
- upsert_views: list[JSONObject],
1736
- remove_views: list[str],
1737
- ) -> JSONObject:
1738
- """执行应用相关逻辑。"""
1739
- result = self._app_views_apply_once(
1740
- profile=profile,
1741
- app_key=app_key,
1742
- publish=publish,
1743
- upsert_views=upsert_views,
1744
- remove_views=remove_views,
1745
- )
1746
- return self._retry_after_self_lock_release(
2366
+ result = self._retry_after_self_lock_release(
1747
2367
  profile=profile,
1748
2368
  result=result,
1749
2369
  retry_call=lambda: self._app_views_apply_once(
@@ -1752,8 +2372,10 @@ class AiBuilderTools(ToolBase):
1752
2372
  publish=publish,
1753
2373
  remove_views=remove_views,
1754
2374
  upsert_views=upsert_views,
2375
+ patch_views=patch_views or [],
1755
2376
  ),
1756
2377
  )
2378
+ return _attach_builder_apply_envelope("app_views_apply", result)
1757
2379
 
1758
2380
  def _app_views_apply_once(
1759
2381
  self,
@@ -1762,9 +2384,53 @@ class AiBuilderTools(ToolBase):
1762
2384
  app_key: str,
1763
2385
  publish: bool = True,
1764
2386
  upsert_views: list[JSONObject],
2387
+ patch_views: list[JSONObject],
1765
2388
  remove_views: list[str],
1766
2389
  ) -> JSONObject:
1767
2390
  """执行内部辅助逻辑。"""
2391
+ if patch_views:
2392
+ try:
2393
+ parsed_views = [ViewUpsertPatch.model_validate(item) for item in (upsert_views or [])]
2394
+ parsed_patch_views = [ViewPartialPatch.model_validate(item) for item in patch_views]
2395
+ except ValidationError as exc:
2396
+ return _visibility_validation_failure(
2397
+ str(exc),
2398
+ tool_name="app_views_apply",
2399
+ exc=exc,
2400
+ suggested_next_call={
2401
+ "tool_name": "app_views_apply",
2402
+ "arguments": {
2403
+ "profile": profile,
2404
+ "app_key": app_key,
2405
+ "publish": publish,
2406
+ "upsert_views": upsert_views or [],
2407
+ "patch_views": [
2408
+ {"view_key": "VIEW_KEY", "set": {"query_conditions": {"enabled": True, "rows": [["字段A"]]}}},
2409
+ ],
2410
+ "remove_views": remove_views or [],
2411
+ },
2412
+ },
2413
+ )
2414
+ normalized_args = {
2415
+ "app_key": app_key,
2416
+ "publish": publish,
2417
+ "upsert_views": [view.model_dump(mode="json") for view in parsed_views],
2418
+ "patch_views": [patch.model_dump(mode="json") for patch in parsed_patch_views],
2419
+ "remove_views": list(remove_views or []),
2420
+ }
2421
+ return _safe_tool_call(
2422
+ lambda: self._facade.app_views_apply(
2423
+ profile=profile,
2424
+ app_key=app_key,
2425
+ publish=publish,
2426
+ upsert_views=parsed_views,
2427
+ patch_views=parsed_patch_views,
2428
+ remove_views=list(remove_views or []),
2429
+ ),
2430
+ error_code="VIEWS_APPLY_FAILED",
2431
+ normalized_args=normalized_args,
2432
+ suggested_next_call={"tool_name": "app_views_apply", "arguments": {"profile": profile, **normalized_args}},
2433
+ )
1768
2434
  plan_result = self._rewrite_plan_result_for_apply(
1769
2435
  result=self.app_views_plan(
1770
2436
  profile=profile,
@@ -1813,6 +2479,7 @@ class AiBuilderTools(ToolBase):
1813
2479
  app_key=str(plan_args.get("app_key") or app_key),
1814
2480
  publish=publish,
1815
2481
  upsert_views=parsed_views,
2482
+ patch_views=[],
1816
2483
  remove_views=list(plan_args.get("remove_views") or remove_views),
1817
2484
  ),
1818
2485
  error_code="VIEWS_APPLY_FAILED",
@@ -1827,6 +2494,7 @@ class AiBuilderTools(ToolBase):
1827
2494
  profile: str,
1828
2495
  app_key: str,
1829
2496
  upsert_charts: list[JSONObject],
2497
+ patch_charts: list[JSONObject] | None = None,
1830
2498
  remove_chart_ids: list[str],
1831
2499
  reorder_chart_ids: list[str],
1832
2500
  ) -> JSONObject:
@@ -1835,6 +2503,7 @@ class AiBuilderTools(ToolBase):
1835
2503
  profile=profile,
1836
2504
  app_key=app_key,
1837
2505
  upsert_charts=upsert_charts,
2506
+ patch_charts=patch_charts or [],
1838
2507
  remove_chart_ids=remove_chart_ids,
1839
2508
  reorder_chart_ids=reorder_chart_ids,
1840
2509
  )
@@ -1848,19 +2517,36 @@ class AiBuilderTools(ToolBase):
1848
2517
  upsert_charts: list[JSONObject],
1849
2518
  remove_chart_ids: list[str],
1850
2519
  reorder_chart_ids: list[str],
2520
+ patch_charts: list[JSONObject] | None = None,
2521
+ apps: list[JSONObject] | None = None,
1851
2522
  ) -> JSONObject:
1852
2523
  """执行应用相关逻辑。"""
2524
+ if apps:
2525
+ return self._facade._batch_write_apps(
2526
+ profile=profile,
2527
+ apps=apps,
2528
+ single_writer=lambda profile, app_key, **kw: self.app_charts_apply(
2529
+ profile=profile,
2530
+ app_key=app_key,
2531
+ upsert_charts=kw.get("upsert_charts", []),
2532
+ patch_charts=kw.get("patch_charts", []),
2533
+ remove_chart_ids=kw.get("remove_chart_ids", []),
2534
+ reorder_chart_ids=kw.get("reorder_chart_ids", []),
2535
+ ),
2536
+ tool_name="app_charts_apply",
2537
+ )
1853
2538
  try:
1854
2539
  request = ChartApplyRequest.model_validate(
1855
2540
  {
1856
2541
  "app_key": app_key,
1857
2542
  "upsert_charts": upsert_charts or [],
2543
+ "patch_charts": patch_charts or [],
1858
2544
  "remove_chart_ids": remove_chart_ids or [],
1859
2545
  "reorder_chart_ids": reorder_chart_ids or [],
1860
2546
  }
1861
2547
  )
1862
2548
  except ValidationError as exc:
1863
- return _visibility_validation_failure(
2549
+ return _attach_builder_apply_envelope("app_charts_apply", _visibility_validation_failure(
1864
2550
  str(exc),
1865
2551
  tool_name="app_charts_apply",
1866
2552
  exc=exc,
@@ -1874,14 +2560,14 @@ class AiBuilderTools(ToolBase):
1874
2560
  "reorder_chart_ids": [],
1875
2561
  },
1876
2562
  },
1877
- )
2563
+ ))
1878
2564
  normalized_args = request.model_dump(mode="json")
1879
- return _safe_tool_call(
2565
+ return _attach_builder_apply_envelope("app_charts_apply", _safe_tool_call(
1880
2566
  lambda: self._facade.chart_apply(profile=profile, request=request),
1881
2567
  error_code="CHART_APPLY_FAILED",
1882
2568
  normalized_args=normalized_args,
1883
2569
  suggested_next_call={"tool_name": "app_charts_apply", "arguments": {"profile": profile, **normalized_args}},
1884
- )
2570
+ ))
1885
2571
 
1886
2572
  @tool_cn_name("门户配置应用")
1887
2573
  def portal_apply(
@@ -1890,9 +2576,12 @@ class AiBuilderTools(ToolBase):
1890
2576
  profile: str,
1891
2577
  dash_key: str = "",
1892
2578
  dash_name: str = "",
2579
+ name: str = "",
1893
2580
  package_id: int | None = None,
1894
2581
  publish: bool = True,
1895
2582
  sections: list[JSONObject] | None = None,
2583
+ pages: list[JSONObject] | None = None,
2584
+ layout_preset: str = "",
1896
2585
  visibility: JSONObject | None = None,
1897
2586
  auth: JSONObject | None = None,
1898
2587
  icon: str | None = None,
@@ -1900,27 +2589,66 @@ class AiBuilderTools(ToolBase):
1900
2589
  hide_copyright: bool | None = None,
1901
2590
  dash_global_config: JSONObject | None = None,
1902
2591
  config: JSONObject | None = None,
2592
+ payload: JSONObject | None = None,
2593
+ patch_sections: list[JSONObject] | None = None,
1903
2594
  ) -> JSONObject:
1904
2595
  """执行门户相关逻辑。"""
1905
- try:
1906
- request = PortalApplyRequest.model_validate(
1907
- {
1908
- "dash_key": dash_key or None,
1909
- "dash_name": dash_name or None,
1910
- "package_tag_id": package_id,
1911
- "publish": publish,
1912
- "sections": sections or [],
1913
- "visibility": visibility,
1914
- "auth": auth,
1915
- "icon": icon,
1916
- "color": color,
1917
- "hide_copyright": hide_copyright,
1918
- "dash_global_config": dash_global_config,
1919
- "config": config or {},
1920
- }
2596
+ if patch_sections:
2597
+ if not dash_key:
2598
+ return _attach_builder_apply_envelope("portal_apply", _config_failure(
2599
+ tool_name="portal_apply",
2600
+ message="patch_sections requires dash_key to identify the portal",
2601
+ fix_hint="Provide dash_key from portal_list or portal_get",
2602
+ ))
2603
+ result = _safe_tool_call(
2604
+ lambda: self._facade.portal_patch_sections(
2605
+ profile=profile,
2606
+ dash_key=dash_key,
2607
+ patch_sections=patch_sections,
2608
+ publish=publish,
2609
+ ),
2610
+ error_code="PORTAL_APPLY_FAILED",
2611
+ normalized_args={"dash_key": dash_key, "patch_sections": patch_sections, "publish": publish},
2612
+ suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, "dash_key": dash_key, "patch_sections": patch_sections}},
1921
2613
  )
2614
+ return _attach_builder_apply_envelope("portal_apply", result)
2615
+ request_payload: dict[str, Any] = dict(payload) if isinstance(payload, dict) else {}
2616
+ if dash_key:
2617
+ request_payload["dash_key"] = dash_key
2618
+ if dash_name:
2619
+ request_payload["dash_name"] = dash_name
2620
+ elif name:
2621
+ request_payload["name"] = name
2622
+ if package_id is not None:
2623
+ request_payload["package_id"] = package_id
2624
+ if "publish" not in request_payload or publish is False:
2625
+ request_payload["publish"] = publish
2626
+ if sections:
2627
+ request_payload["sections"] = sections
2628
+ if pages:
2629
+ request_payload["pages"] = pages
2630
+ if layout_preset:
2631
+ request_payload["layout_preset"] = layout_preset
2632
+ if visibility is not None:
2633
+ request_payload["visibility"] = visibility
2634
+ if auth is not None:
2635
+ request_payload["auth"] = auth
2636
+ if icon is not None:
2637
+ request_payload["icon"] = icon
2638
+ if color is not None:
2639
+ request_payload["color"] = color
2640
+ if hide_copyright is not None:
2641
+ request_payload["hide_copyright"] = hide_copyright
2642
+ if dash_global_config is not None:
2643
+ request_payload["dash_global_config"] = dash_global_config
2644
+ if config:
2645
+ merged_config = dict(request_payload.get("config") or {}) if isinstance(request_payload.get("config"), dict) else {}
2646
+ merged_config.update(config)
2647
+ request_payload["config"] = merged_config
2648
+ try:
2649
+ request = PortalApplyRequest.model_validate(request_payload)
1922
2650
  except ValidationError as exc:
1923
- return _visibility_validation_failure(
2651
+ return _attach_builder_apply_envelope("portal_apply", _visibility_validation_failure(
1924
2652
  str(exc),
1925
2653
  tool_name="portal_apply",
1926
2654
  exc=exc,
@@ -1931,6 +2659,7 @@ class AiBuilderTools(ToolBase):
1931
2659
  "dash_name": dash_name or "业务门户",
1932
2660
  "package_id": package_id or 1001,
1933
2661
  "publish": True,
2662
+ "layout_preset": "dashboard_2col",
1934
2663
  "sections": [
1935
2664
  {
1936
2665
  "title": "经营概览",
@@ -1940,25 +2669,43 @@ class AiBuilderTools(ToolBase):
1940
2669
  ],
1941
2670
  },
1942
2671
  },
1943
- )
2672
+ ))
1944
2673
  normalized_args = request.model_dump(mode="json")
1945
2674
  normalized_args["package_id"] = normalized_args.pop("package_tag_id", package_id)
1946
- return _publicize_package_fields(_safe_tool_call(
2675
+ icon_failure = _validate_workspace_icon_for_builder(
2676
+ tool_name="portal_apply",
2677
+ icon=str(request.icon or ""),
2678
+ color=str(request.color or ""),
2679
+ creating=not bool(str(request.dash_key or "").strip()),
2680
+ )
2681
+ if icon_failure is not None:
2682
+ return _attach_builder_apply_envelope("portal_apply", icon_failure)
2683
+ result = _publicize_package_fields(_safe_tool_call(
1947
2684
  lambda: self._facade.portal_apply(profile=profile, request=request),
1948
2685
  error_code="PORTAL_APPLY_FAILED",
1949
2686
  normalized_args=normalized_args,
1950
2687
  suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
1951
2688
  ))
2689
+ return _attach_builder_apply_envelope("portal_apply", result)
1952
2690
 
1953
2691
  @tool_cn_name("应用发布校验")
1954
2692
  def app_publish_verify(
1955
2693
  self,
1956
2694
  *,
1957
2695
  profile: str,
1958
- app_key: str,
2696
+ app_key: str = "",
2697
+ app_keys: list[str] | None = None,
1959
2698
  expected_package_id: int | None = None,
1960
2699
  ) -> JSONObject:
1961
2700
  """执行应用相关逻辑。"""
2701
+ if app_keys:
2702
+ return self._facade._batch_read_app_keys(
2703
+ profile=profile,
2704
+ app_keys=app_keys,
2705
+ single_reader=lambda profile, app_key: self._facade.app_publish_verify(profile=profile, app_key=app_key, expected_package_tag_id=expected_package_id),
2706
+ data_key="verification",
2707
+ tool_name="app_publish_verify",
2708
+ )
1962
2709
  normalized_args = {"app_key": app_key, "expected_package_id": expected_package_id}
1963
2710
  result = _publicize_package_fields(_safe_tool_call(
1964
2711
  lambda: self._facade.app_publish_verify(profile=profile, app_key=app_key, expected_package_tag_id=expected_package_id),
@@ -1966,7 +2713,7 @@ class AiBuilderTools(ToolBase):
1966
2713
  normalized_args=normalized_args,
1967
2714
  suggested_next_call={"tool_name": "app_publish_verify", "arguments": {"profile": profile, **normalized_args}},
1968
2715
  ))
1969
- return _publicize_package_fields(self._retry_after_self_lock_release(
2716
+ result = _publicize_package_fields(self._retry_after_self_lock_release(
1970
2717
  profile=profile,
1971
2718
  result=result,
1972
2719
  retry_call=lambda: self._facade.app_publish_verify(
@@ -1975,6 +2722,7 @@ class AiBuilderTools(ToolBase):
1975
2722
  expected_package_tag_id=expected_package_id,
1976
2723
  ),
1977
2724
  ))
2725
+ return _attach_builder_apply_envelope("app_publish_verify", result)
1978
2726
 
1979
2727
  def _retry_after_self_lock_release(self, *, profile: str, result: JSONObject, retry_call) -> JSONObject:
1980
2728
  """执行内部辅助逻辑。"""
@@ -2093,6 +2841,128 @@ class AiBuilderTools(ToolBase):
2093
2841
  return rewritten
2094
2842
 
2095
2843
 
2844
+ def _multi_app_item_failure(index: int, item: object, error_code: str, message: str) -> JSONObject:
2845
+ app_name = None
2846
+ client_key = None
2847
+ app_key = None
2848
+ if isinstance(item, dict):
2849
+ app_name = str(item.get("app_name") or item.get("appTitle") or item.get("app_title") or item.get("title") or "").strip() or None
2850
+ client_key = str(item.get("client_key") or item.get("clientKey") or "").strip() or None
2851
+ app_key = str(item.get("app_key") or item.get("appKey") or "").strip() or None
2852
+ return {
2853
+ "index": index,
2854
+ "row_number": index + 1,
2855
+ "client_key": client_key,
2856
+ "app_name": app_name,
2857
+ "app_key": app_key,
2858
+ "status": "failed",
2859
+ "stage": "validate_item",
2860
+ "error_code": error_code,
2861
+ "message": message,
2862
+ "safe_to_retry": True,
2863
+ }
2864
+
2865
+
2866
+ def _compile_multi_app_schema_item_refs(item: JSONObject, client_key_to_app_key: dict[str, str]) -> JSONObject:
2867
+ compiled = deepcopy(item)
2868
+
2869
+ def visit(value):
2870
+ if isinstance(value, list):
2871
+ return [visit(entry) for entry in value]
2872
+ if not isinstance(value, dict):
2873
+ return value
2874
+ payload = {key: visit(entry) for key, entry in value.items()}
2875
+ ref = (
2876
+ payload.pop("target_app_ref", None)
2877
+ or payload.pop("targetAppRef", None)
2878
+ or payload.pop("target_app_client_key", None)
2879
+ or payload.pop("targetAppClientKey", None)
2880
+ )
2881
+ if ref is not None:
2882
+ ref_key = str(ref or "").strip()
2883
+ target_app_key = client_key_to_app_key.get(ref_key)
2884
+ if not target_app_key:
2885
+ raise ValueError(f"target_app_ref '{ref_key}' did not match any apps[].client_key")
2886
+ payload["target_app_key"] = target_app_key
2887
+ return payload
2888
+
2889
+ return visit(compiled)
2890
+
2891
+
2892
+ def _split_multi_app_initial_add_fields(item: JSONObject, *, is_new_app: bool) -> tuple[list[JSONObject], list[JSONObject]]:
2893
+ add_fields = _multi_app_list_value(item, "add_fields", "addFields")
2894
+ if not is_new_app:
2895
+ return [], add_fields
2896
+ initial: list[JSONObject] = []
2897
+ deferred: list[JSONObject] = []
2898
+ for field in add_fields:
2899
+ if _contains_multi_app_target_ref(field):
2900
+ deferred.append(field)
2901
+ else:
2902
+ initial.append(field)
2903
+ return initial, deferred
2904
+
2905
+
2906
+ def _compiled_multi_app_deferred_add_fields(compiled_item: JSONObject, existing_result: JSONObject) -> list[JSONObject]:
2907
+ deferred = existing_result.get("deferred_add_fields")
2908
+ if not isinstance(deferred, list):
2909
+ return list(compiled_item.get("add_fields") or [])
2910
+ deferred_names = {str(item.get("name") or item.get("title") or item.get("label") or "").strip() for item in deferred if isinstance(item, dict)}
2911
+ if not deferred_names:
2912
+ return []
2913
+ return [
2914
+ deepcopy(field)
2915
+ for field in list(compiled_item.get("add_fields") or [])
2916
+ if isinstance(field, dict)
2917
+ and str(field.get("name") or field.get("title") or field.get("label") or "").strip() in deferred_names
2918
+ ]
2919
+
2920
+
2921
+ def _multi_app_list_value(item: JSONObject, *keys: str) -> list[JSONObject]:
2922
+ for key in keys:
2923
+ value = item.get(key)
2924
+ if isinstance(value, list):
2925
+ return [deepcopy(entry) for entry in value if isinstance(entry, dict)]
2926
+ return []
2927
+
2928
+
2929
+ def _contains_multi_app_target_ref(value: object) -> bool:
2930
+ if isinstance(value, list):
2931
+ return any(_contains_multi_app_target_ref(item) for item in value)
2932
+ if not isinstance(value, dict):
2933
+ return False
2934
+ for key, entry in value.items():
2935
+ if key in {"target_app_ref", "targetAppRef", "target_app_client_key", "targetAppClientKey"}:
2936
+ return True
2937
+ if _contains_multi_app_target_ref(entry):
2938
+ return True
2939
+ return False
2940
+
2941
+
2942
+ def _merge_schema_field_diffs(*diffs: object) -> JSONObject:
2943
+ merged: JSONObject = {"added": [], "updated": [], "removed": []}
2944
+ for diff in diffs:
2945
+ if not isinstance(diff, dict):
2946
+ continue
2947
+ for key in ("added", "updated", "removed"):
2948
+ values = diff.get(key)
2949
+ if not isinstance(values, list):
2950
+ continue
2951
+ for value in values:
2952
+ if value not in merged[key]:
2953
+ merged[key].append(value)
2954
+ return merged
2955
+
2956
+
2957
+ def _schema_apply_result_has_write(result: JSONObject) -> bool:
2958
+ if bool(result.get("created")) or bool(result.get("published")) or bool(result.get("app_base_updated")):
2959
+ return True
2960
+ field_diff = result.get("field_diff")
2961
+ if isinstance(field_diff, dict):
2962
+ return any(bool(field_diff.get(key)) for key in ("added", "updated", "removed"))
2963
+ return False
2964
+
2965
+
2096
2966
  def _validation_failure(
2097
2967
  detail: str,
2098
2968
  *,
@@ -2173,20 +3043,34 @@ def _visibility_validation_failure(
2173
3043
  return result
2174
3044
 
2175
3045
 
2176
- def _config_failure(*, tool_name: str, message: str, fix_hint: str) -> JSONObject:
3046
+ def _config_failure(
3047
+ *,
3048
+ tool_name: str,
3049
+ message: str,
3050
+ fix_hint: str,
3051
+ error_code: str = "CONFIG_ERROR",
3052
+ details: JSONObject | None = None,
3053
+ allowed_values: JSONObject | None = None,
3054
+ ) -> JSONObject:
2177
3055
  contract = _BUILDER_TOOL_CONTRACTS.get(tool_name or "")
3056
+ public_allowed_values = deepcopy(contract.get("allowed_values", {})) if isinstance(contract, dict) else {}
3057
+ if allowed_values:
3058
+ public_allowed_values.update(deepcopy(allowed_values))
3059
+ public_details: JSONObject = {
3060
+ "fix_hint": fix_hint,
3061
+ "allowed_keys": deepcopy(contract.get("allowed_keys", [])) if isinstance(contract, dict) else [],
3062
+ }
3063
+ if details:
3064
+ public_details.update(deepcopy(details))
2178
3065
  return {
2179
3066
  "status": "failed",
2180
- "error_code": "CONFIG_ERROR",
3067
+ "error_code": error_code,
2181
3068
  "recoverable": True,
2182
3069
  "message": message,
2183
3070
  "normalized_args": {},
2184
3071
  "missing_fields": [],
2185
- "allowed_values": deepcopy(contract.get("allowed_values", {})) if isinstance(contract, dict) else {},
2186
- "details": {
2187
- "fix_hint": fix_hint,
2188
- "allowed_keys": deepcopy(contract.get("allowed_keys", [])) if isinstance(contract, dict) else [],
2189
- },
3072
+ "allowed_values": public_allowed_values,
3073
+ "details": public_details,
2190
3074
  "suggested_next_call": None,
2191
3075
  "request_id": None,
2192
3076
  "backend_code": None,
@@ -2196,6 +3080,52 @@ def _config_failure(*, tool_name: str, message: str, fix_hint: str) -> JSONObjec
2196
3080
  }
2197
3081
 
2198
3082
 
3083
+ def _workspace_icon_config_failure(
3084
+ *,
3085
+ tool_name: str,
3086
+ error_code: str,
3087
+ message: str,
3088
+ details: JSONObject,
3089
+ ) -> JSONObject:
3090
+ return _config_failure(
3091
+ tool_name=tool_name,
3092
+ error_code=error_code,
3093
+ message=message,
3094
+ fix_hint="Call `qingflow --json builder icon catalog`, choose an explicit non-template icon and color, then retry.",
3095
+ details=details,
3096
+ allowed_values={
3097
+ "workspace_icon.icon_names": list(WORKSPACE_ICON_NAMES),
3098
+ "workspace_icon.icon_colors": list(WORKSPACE_ICON_COLORS),
3099
+ "workspace_icon.generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
3100
+ },
3101
+ )
3102
+
3103
+
3104
+ def _validate_workspace_icon_for_builder(
3105
+ *,
3106
+ tool_name: str,
3107
+ icon: str | None,
3108
+ color: str | None,
3109
+ creating: bool,
3110
+ ) -> JSONObject | None:
3111
+ if not creating and not (str(icon or "").strip() or str(color or "").strip()):
3112
+ return None
3113
+ ok, error_code, message, details = validate_workspace_icon_choice(
3114
+ icon=icon,
3115
+ color=color,
3116
+ require_explicit=creating,
3117
+ disallow_generic=creating,
3118
+ )
3119
+ if ok:
3120
+ return None
3121
+ return _workspace_icon_config_failure(
3122
+ tool_name=tool_name,
3123
+ error_code=error_code or "WORKSPACE_ICON_INVALID",
3124
+ message=message or "invalid workspace icon configuration",
3125
+ details=details,
3126
+ )
3127
+
3128
+
2199
3129
  def _safe_tool_call(
2200
3130
  call,
2201
3131
  *,
@@ -2244,6 +3174,7 @@ def _publicize_package_fields(value):
2244
3174
  "tag_ids_after": "package_ids_after",
2245
3175
  "tag_name": "package_name",
2246
3176
  "tag_icon": "icon",
3177
+ "iconConfig": "icon_config",
2247
3178
  "package_tag_id": "package_id",
2248
3179
  "package_tag_ids": "package_ids",
2249
3180
  "expected_package_tag_id": "expected_package_id",
@@ -2255,6 +3186,742 @@ def _publicize_package_fields(value):
2255
3186
  return public
2256
3187
 
2257
3188
 
3189
+ def _builder_contract_with_apply_output(tool_name: str, contract: JSONObject) -> JSONObject:
3190
+ public = deepcopy(contract)
3191
+ if tool_name not in BUILDER_APPLY_TOOL_NAMES:
3192
+ return public
3193
+ notes = public.setdefault("execution_notes", [])
3194
+ if isinstance(notes, list):
3195
+ note = "apply/write output includes schema_version, operation, summary, and resources[]; UI and agents should read resources[].id/key/name first and use legacy fields only for compatibility/debugging"
3196
+ if note not in notes:
3197
+ notes.append(note)
3198
+ public["output_contract"] = {
3199
+ "schema_version": BUILDER_APPLY_SCHEMA_VERSION,
3200
+ "preferred_ui_fields": ["operation", "summary", "resources"],
3201
+ "resource_fields": ["resource_type", "operation", "status", "id", "key", "name", "ids", "parent", "icon_config", "error_code", "message"],
3202
+ "legacy_fields_preserved": True,
3203
+ }
3204
+ return public
3205
+
3206
+
3207
+ def _attach_builder_apply_envelope(tool_name: str, payload: JSONObject) -> JSONObject:
3208
+ if not isinstance(payload, dict):
3209
+ return payload
3210
+ resources = _builder_apply_resources(tool_name, payload)
3211
+ payload["schema_version"] = BUILDER_APPLY_SCHEMA_VERSION
3212
+ payload["operation"] = tool_name
3213
+ payload["resources"] = resources
3214
+ payload["summary"] = _builder_apply_summary(payload, resources)
3215
+ return payload
3216
+
3217
+
3218
+ def _builder_apply_summary(payload: JSONObject, resources: list[JSONObject]) -> JSONObject:
3219
+ status = str(payload.get("status") or "")
3220
+ failed = sum(1 for item in resources if str(item.get("status") or "") == "failed")
3221
+ created = sum(1 for item in resources if str(item.get("operation") or "") == "created" and str(item.get("status") or "") != "failed")
3222
+ removed = sum(1 for item in resources if str(item.get("operation") or "") == "removed" and str(item.get("status") or "") != "failed")
3223
+ updated = sum(
3224
+ 1
3225
+ for item in resources
3226
+ if str(item.get("status") or "") != "failed"
3227
+ and str(item.get("operation") or "") in {"updated", "layout_updated", "workflow_updated", "verified", "published"}
3228
+ )
3229
+ published_value = payload.get("published")
3230
+ if published_value is None:
3231
+ publish_requested = payload.get("publish_requested")
3232
+ published_value = bool(publish_requested) and status in {"success", "partial_success"}
3233
+ verified_value = payload.get("verified")
3234
+ if verified_value is None:
3235
+ verification = payload.get("verification")
3236
+ verified_value = status == "success" and failed == 0 and _builder_verification_truthy(verification)
3237
+ summary: JSONObject = {
3238
+ "total": len(resources),
3239
+ "created": created,
3240
+ "updated": updated,
3241
+ "removed": removed,
3242
+ "failed": failed,
3243
+ "published": bool(published_value),
3244
+ "verified": bool(verified_value),
3245
+ }
3246
+ if "write_executed" in payload:
3247
+ summary["write_executed"] = bool(payload.get("write_executed"))
3248
+ if "safe_to_retry" in payload:
3249
+ summary["safe_to_retry"] = bool(payload.get("safe_to_retry"))
3250
+ return summary
3251
+
3252
+
3253
+ def _builder_verification_truthy(value: object) -> bool:
3254
+ if value is None:
3255
+ return True
3256
+ if isinstance(value, bool):
3257
+ return value
3258
+ if isinstance(value, dict):
3259
+ booleans = [item for item in value.values() if isinstance(item, bool)]
3260
+ return all(booleans) if booleans else True
3261
+ return True
3262
+
3263
+
3264
+ def _builder_apply_resources(tool_name: str, payload: JSONObject) -> list[JSONObject]:
3265
+ resources: list[JSONObject]
3266
+ if tool_name == "package_apply":
3267
+ resources = _builder_package_resources(payload)
3268
+ elif tool_name == "app_schema_apply":
3269
+ resources = _builder_schema_resources(payload)
3270
+ elif tool_name == "app_layout_apply":
3271
+ resources = [_builder_app_resource(payload, operation="layout_updated")]
3272
+ elif tool_name == "app_flow_apply":
3273
+ resources = [_builder_app_resource(payload, operation="workflow_updated")]
3274
+ elif tool_name == "app_views_apply":
3275
+ resources = _builder_view_resources(payload)
3276
+ elif tool_name == "app_charts_apply":
3277
+ resources = _builder_chart_resources(payload)
3278
+ elif tool_name == "portal_apply":
3279
+ resources = _builder_portal_resources(payload)
3280
+ elif tool_name == "app_custom_buttons_apply":
3281
+ resources = _builder_button_resources(payload)
3282
+ elif tool_name == "app_associated_resources_apply":
3283
+ resources = _builder_associated_resource_resources(payload)
3284
+ elif tool_name == "app_publish_verify":
3285
+ resources = [_builder_app_resource(payload, operation="verified")]
3286
+ else:
3287
+ resources = []
3288
+ if not resources and _builder_status(payload, "") == "failed" and _builder_apply_tool_is_app_scoped(tool_name):
3289
+ app_key = _builder_payload_app_key(payload)
3290
+ if app_key not in (None, ""):
3291
+ resources = [_builder_app_resource(payload, operation="failed")]
3292
+ return resources
3293
+
3294
+
3295
+ def _builder_apply_tool_is_app_scoped(tool_name: str) -> bool:
3296
+ return tool_name in {
3297
+ "app_schema_apply",
3298
+ "app_layout_apply",
3299
+ "app_flow_apply",
3300
+ "app_views_apply",
3301
+ "app_custom_buttons_apply",
3302
+ "app_associated_resources_apply",
3303
+ "app_charts_apply",
3304
+ "app_publish_verify",
3305
+ }
3306
+
3307
+
3308
+ def _builder_status(payload_or_item: JSONObject, fallback: str = "success") -> str:
3309
+ status = str(payload_or_item.get("status") or fallback or "success")
3310
+ return status
3311
+
3312
+
3313
+ def _builder_operation(value: object, fallback: str = "updated") -> str:
3314
+ raw = str(value or fallback or "updated").strip()
3315
+ mapping = {
3316
+ "create": "created",
3317
+ "created": "created",
3318
+ "add": "created",
3319
+ "update": "updated",
3320
+ "updated": "updated",
3321
+ "patch": "updated",
3322
+ "remove": "removed",
3323
+ "removed": "removed",
3324
+ "delete": "removed",
3325
+ "deleted": "removed",
3326
+ "unchanged": "unchanged",
3327
+ "failed": "failed",
3328
+ }
3329
+ return mapping.get(raw, raw)
3330
+
3331
+
3332
+ def _builder_parent(resource_type: str, *, key: object = None, name: object = None, id_value: object = None) -> JSONObject:
3333
+ return {
3334
+ "resource_type": resource_type,
3335
+ "id": id_value,
3336
+ "key": str(key) if key not in (None, "") else None,
3337
+ "name": str(name) if name not in (None, "") else None,
3338
+ }
3339
+
3340
+
3341
+ def _builder_app_parent(payload: JSONObject) -> JSONObject | None:
3342
+ app_key = payload.get("app_key") or payload.get("appKey")
3343
+ app_name = payload.get("app_name_after") or payload.get("app_name") or payload.get("appTitle") or payload.get("app_title")
3344
+ if app_key in (None, "") and app_name in (None, ""):
3345
+ return None
3346
+ return _builder_parent("app", key=app_key, name=app_name)
3347
+
3348
+
3349
+ def _builder_icon_config(raw_icon: object = None, *, icon: object = None, color: object = None) -> JSONObject | None:
3350
+ raw = str(raw_icon).strip() if raw_icon not in (None, "") else ""
3351
+ explicit_icon = str(icon).strip() if icon not in (None, "") else ""
3352
+ explicit_color = str(color).strip() if color not in (None, "") else ""
3353
+ if raw:
3354
+ if raw.startswith("{") and raw.endswith("}"):
3355
+ config = workspace_icon_config(raw)
3356
+ else:
3357
+ config = {
3358
+ "icon_name": normalize_workspace_icon_name(raw),
3359
+ "icon_color": explicit_color or None,
3360
+ "icon_text": None,
3361
+ "raw": raw,
3362
+ }
3363
+ if any(config.get(key) for key in ("icon_name", "icon_color", "icon_text", "raw")):
3364
+ return config
3365
+ if explicit_icon or explicit_color:
3366
+ return {
3367
+ "icon_name": normalize_workspace_icon_name(explicit_icon) if explicit_icon else None,
3368
+ "icon_color": explicit_color or None,
3369
+ "icon_text": None,
3370
+ "raw": None,
3371
+ }
3372
+ return None
3373
+
3374
+
3375
+ def _builder_container_icon_config(container: object, *, raw_keys: tuple[str, ...], icon_keys: tuple[str, ...] = ("icon",), color_keys: tuple[str, ...] = ("color",)) -> JSONObject | None:
3376
+ if not isinstance(container, dict):
3377
+ return None
3378
+ raw_icon = next((container.get(key) for key in raw_keys if container.get(key) not in (None, "")), None)
3379
+ icon = next((container.get(key) for key in icon_keys if container.get(key) not in (None, "")), None)
3380
+ color = next((container.get(key) for key in color_keys if container.get(key) not in (None, "")), None)
3381
+ return _builder_icon_config(raw_icon, icon=icon, color=color)
3382
+
3383
+
3384
+ def _builder_resource(
3385
+ *,
3386
+ resource_type: str,
3387
+ operation: str,
3388
+ status: str,
3389
+ id_value: object = None,
3390
+ key: object = None,
3391
+ name: object = None,
3392
+ ids: JSONObject | None = None,
3393
+ parent: JSONObject | None = None,
3394
+ icon_config: JSONObject | None = None,
3395
+ error_code: object = None,
3396
+ message: object = None,
3397
+ ) -> JSONObject:
3398
+ resource = {
3399
+ "resource_type": resource_type,
3400
+ "operation": operation,
3401
+ "status": status,
3402
+ "id": id_value,
3403
+ "key": str(key) if key not in (None, "") else None,
3404
+ "name": str(name) if name not in (None, "") else None,
3405
+ "ids": ids or {},
3406
+ "parent": parent,
3407
+ "error_code": str(error_code) if error_code not in (None, "") else None,
3408
+ "message": str(message) if message not in (None, "") else None,
3409
+ }
3410
+ if icon_config:
3411
+ resource["icon_config"] = icon_config
3412
+ return resource
3413
+
3414
+
3415
+ def _builder_app_resource(payload: JSONObject, *, operation: str) -> JSONObject:
3416
+ status = _builder_status(payload, "success")
3417
+ if status == "failed":
3418
+ operation = "failed"
3419
+ app_key = _builder_payload_app_key(payload)
3420
+ app_name = _builder_payload_app_name(payload)
3421
+ normalized_args = payload.get("normalized_args") if isinstance(payload.get("normalized_args"), dict) else {}
3422
+ icon_config = (
3423
+ _builder_container_icon_config(payload, raw_keys=("app_icon", "appIcon"))
3424
+ or _builder_container_icon_config(normalized_args, raw_keys=("app_icon", "appIcon", "icon"))
3425
+ )
3426
+ return _builder_resource(
3427
+ resource_type="app",
3428
+ operation=operation,
3429
+ status=status,
3430
+ key=app_key,
3431
+ name=app_name,
3432
+ ids={"app_key": app_key} if app_key not in (None, "") else {},
3433
+ icon_config=icon_config,
3434
+ error_code=payload.get("error_code"),
3435
+ message=payload.get("message") if status == "failed" else None,
3436
+ )
3437
+
3438
+
3439
+ def _builder_payload_app_key(payload: JSONObject) -> object:
3440
+ return _builder_payload_identity_value(payload, ("app_key", "appKey"))
3441
+
3442
+
3443
+ def _builder_payload_app_name(payload: JSONObject) -> object:
3444
+ return _builder_payload_identity_value(payload, ("app_name_after", "app_name", "appName", "appTitle", "app_title", "name", "title"))
3445
+
3446
+
3447
+ def _builder_payload_identity_value(payload: JSONObject, keys: tuple[str, ...]) -> object:
3448
+ for key in keys:
3449
+ value = payload.get(key)
3450
+ if value not in (None, ""):
3451
+ return value
3452
+ for container_key in ("normalized_args", "canonical_arguments"):
3453
+ container = payload.get(container_key)
3454
+ if isinstance(container, dict):
3455
+ for key in keys:
3456
+ value = container.get(key)
3457
+ if value not in (None, ""):
3458
+ return value
3459
+ details = payload.get("details")
3460
+ if isinstance(details, dict):
3461
+ for container_key in ("normalized_args", "canonical_arguments"):
3462
+ container = details.get(container_key)
3463
+ if isinstance(container, dict):
3464
+ for key in keys:
3465
+ value = container.get(key)
3466
+ if value not in (None, ""):
3467
+ return value
3468
+ return None
3469
+
3470
+
3471
+ def _builder_package_resources(payload: JSONObject) -> list[JSONObject]:
3472
+ package_id = payload.get("package_id") or payload.get("id")
3473
+ package_name = payload.get("package_name") or payload.get("name")
3474
+ status = _builder_status(payload, "success")
3475
+ operation = "failed" if status == "failed" else ("created" if bool(payload.get("created")) else "updated")
3476
+ normalized_args = payload.get("normalized_args") if isinstance(payload.get("normalized_args"), dict) else {}
3477
+ icon_config = (
3478
+ _builder_container_icon_config(payload, raw_keys=("icon", "tagIcon", "tag_icon"))
3479
+ or _builder_container_icon_config(normalized_args, raw_keys=("icon", "tagIcon", "tag_icon"))
3480
+ )
3481
+ return [
3482
+ _builder_resource(
3483
+ resource_type="package",
3484
+ operation=operation,
3485
+ status=status,
3486
+ id_value=package_id,
3487
+ key=str(package_id) if package_id not in (None, "") else None,
3488
+ name=package_name,
3489
+ ids={"package_id": package_id} if package_id not in (None, "") else {},
3490
+ icon_config=icon_config,
3491
+ error_code=payload.get("error_code"),
3492
+ message=payload.get("message") if status == "failed" else None,
3493
+ )
3494
+ ]
3495
+
3496
+
3497
+ def _builder_schema_resources(payload: JSONObject) -> list[JSONObject]:
3498
+ if payload.get("mode") == "multi_app" and isinstance(payload.get("apps"), list):
3499
+ resources: list[JSONObject] = []
3500
+ package_id = payload.get("package_id")
3501
+ package_parent = _builder_parent("package", id_value=package_id, key=package_id) if package_id not in (None, "") else None
3502
+ for item in payload.get("apps") or []:
3503
+ if not isinstance(item, dict):
3504
+ continue
3505
+ status = _builder_status(item, "success")
3506
+ operation = "failed" if status == "failed" else ("created" if bool(item.get("created")) else "updated")
3507
+ parent = _builder_parent("app", key=item.get("app_key"), name=item.get("app_name"))
3508
+ icon_config = (
3509
+ _builder_container_icon_config(item, raw_keys=("app_icon", "appIcon", "icon"))
3510
+ or _builder_container_icon_config(item.get("shell_result"), raw_keys=("app_icon", "appIcon", "icon"))
3511
+ )
3512
+ resources.append(
3513
+ _builder_resource(
3514
+ resource_type="app",
3515
+ operation=operation,
3516
+ status=status,
3517
+ key=item.get("app_key"),
3518
+ name=item.get("app_name"),
3519
+ ids={
3520
+ **({"app_key": item.get("app_key")} if item.get("app_key") else {}),
3521
+ **({"package_id": package_id} if package_id not in (None, "") else {}),
3522
+ },
3523
+ parent=package_parent,
3524
+ icon_config=icon_config,
3525
+ error_code=item.get("error_code"),
3526
+ message=item.get("message") if status == "failed" else None,
3527
+ )
3528
+ )
3529
+ resources.extend(_builder_field_resources(item.get("field_diff_details") or item.get("field_diff"), parent=parent))
3530
+ return resources
3531
+
3532
+ status = _builder_status(payload, "success")
3533
+ operation = "failed" if status == "failed" else ("created" if bool(payload.get("created")) else "updated")
3534
+ app_key = payload.get("app_key")
3535
+ app_name = payload.get("app_name_after") or payload.get("app_name")
3536
+ parent = _builder_parent("app", key=app_key, name=app_name)
3537
+ normalized_args = payload.get("normalized_args") if isinstance(payload.get("normalized_args"), dict) else {}
3538
+ icon_config = (
3539
+ _builder_container_icon_config(payload, raw_keys=("app_icon", "appIcon", "icon"))
3540
+ or _builder_container_icon_config(normalized_args, raw_keys=("icon", "app_icon", "appIcon"))
3541
+ )
3542
+ resources = [
3543
+ _builder_resource(
3544
+ resource_type="app",
3545
+ operation=operation,
3546
+ status=status,
3547
+ key=app_key,
3548
+ name=app_name,
3549
+ ids={"app_key": app_key} if app_key else {},
3550
+ icon_config=icon_config,
3551
+ error_code=payload.get("error_code"),
3552
+ message=payload.get("message") if status == "failed" else None,
3553
+ )
3554
+ ]
3555
+ resources.extend(_builder_field_resources(payload.get("field_diff_details") or payload.get("field_diff"), parent=parent))
3556
+ return resources
3557
+
3558
+
3559
+ def _builder_field_resources(field_diff: object, *, parent: JSONObject | None) -> list[JSONObject]:
3560
+ if not isinstance(field_diff, dict):
3561
+ return []
3562
+ resources: list[JSONObject] = []
3563
+ for key, operation in (("added", "created"), ("updated", "updated"), ("removed", "removed")):
3564
+ for field in field_diff.get(key) or []:
3565
+ if isinstance(field, dict):
3566
+ name = field.get("name") or field.get("title") or field.get("field_name") or field.get("queTitle")
3567
+ field_id = field.get("field_id") or field.get("queId")
3568
+ que_id = field.get("que_id") or field.get("queId")
3569
+ else:
3570
+ name = field
3571
+ field_id = None
3572
+ que_id = None
3573
+ resource_id = que_id if que_id not in (None, "") else field_id
3574
+ resources.append(
3575
+ _builder_resource(
3576
+ resource_type="field",
3577
+ operation=operation,
3578
+ status="success",
3579
+ id_value=resource_id,
3580
+ key=field_id if field_id not in (None, "") else name,
3581
+ name=name,
3582
+ ids={
3583
+ **({"field_id": field_id} if field_id not in (None, "") else {}),
3584
+ **({"que_id": que_id} if que_id not in (None, "") else {}),
3585
+ **({"app_key": parent.get("key")} if isinstance(parent, dict) and parent.get("key") else {}),
3586
+ },
3587
+ parent=parent,
3588
+ )
3589
+ )
3590
+ return resources
3591
+
3592
+
3593
+ def _builder_view_resources(payload: JSONObject) -> list[JSONObject]:
3594
+ diff = payload.get("views_diff") if isinstance(payload.get("views_diff"), dict) else {}
3595
+ verification_by_name = _builder_view_verification_by_name(payload)
3596
+ parent = _builder_app_parent(payload)
3597
+ resources: list[JSONObject] = []
3598
+ for key, operation in (("created", "created"), ("updated", "updated"), ("removed", "removed")):
3599
+ for item in diff.get(key) or []:
3600
+ name, view_key, status, error_code, message = _builder_view_identity(item, verification_by_name)
3601
+ resources.append(
3602
+ _builder_resource(
3603
+ resource_type="view",
3604
+ operation=operation,
3605
+ status=status,
3606
+ key=view_key,
3607
+ name=name,
3608
+ ids={
3609
+ **({"view_key": view_key} if view_key else {}),
3610
+ **({"app_key": payload.get("app_key")} if payload.get("app_key") else {}),
3611
+ },
3612
+ parent=parent,
3613
+ error_code=error_code,
3614
+ message=message,
3615
+ )
3616
+ )
3617
+ for item in diff.get("failed") or []:
3618
+ name, view_key, _status, error_code, message = _builder_view_identity(item, verification_by_name)
3619
+ resources.append(
3620
+ _builder_resource(
3621
+ resource_type="view",
3622
+ operation="failed",
3623
+ status="failed",
3624
+ key=view_key,
3625
+ name=name,
3626
+ ids={
3627
+ **({"view_key": view_key} if view_key else {}),
3628
+ **({"app_key": payload.get("app_key")} if payload.get("app_key") else {}),
3629
+ },
3630
+ parent=parent,
3631
+ error_code=error_code,
3632
+ message=message,
3633
+ )
3634
+ )
3635
+ return resources
3636
+
3637
+
3638
+ def _builder_view_verification_by_name(payload: JSONObject) -> dict[str, JSONObject]:
3639
+ verification = payload.get("verification")
3640
+ by_view = verification.get("by_view") if isinstance(verification, dict) else None
3641
+ result: dict[str, JSONObject] = {}
3642
+ if isinstance(by_view, list):
3643
+ for item in by_view:
3644
+ if isinstance(item, dict):
3645
+ name = str(item.get("name") or "").strip()
3646
+ if name:
3647
+ result[name] = item
3648
+ return result
3649
+
3650
+
3651
+ def _builder_view_identity(item: object, verification_by_name: dict[str, JSONObject]) -> tuple[str | None, str | None, str, object, object]:
3652
+ verification: JSONObject | None = None
3653
+ if isinstance(item, dict):
3654
+ name = item.get("name") or item.get("view_name") or item.get("viewName")
3655
+ view_key = item.get("view_key") or item.get("viewKey")
3656
+ status = str(item.get("status") or "success")
3657
+ error_code = item.get("error_code")
3658
+ message = item.get("message")
3659
+ else:
3660
+ name = str(item) if item not in (None, "") else None
3661
+ view_key = None
3662
+ status = "success"
3663
+ error_code = None
3664
+ message = None
3665
+ if name and not view_key:
3666
+ verification = verification_by_name.get(str(name))
3667
+ if isinstance(verification, dict):
3668
+ view_key = verification.get("view_key") or verification.get("viewKey")
3669
+ if not view_key:
3670
+ matching = verification.get("matching_view_keys")
3671
+ if isinstance(matching, list) and matching:
3672
+ view_key = matching[0]
3673
+ if name and verification is None:
3674
+ verification = verification_by_name.get(str(name))
3675
+ if isinstance(verification, dict):
3676
+ verification_status = str(verification.get("status") or "").strip()
3677
+ if verification_status in {"removed", "readback_pending"}:
3678
+ status = "readback_pending"
3679
+ if verification_status == "removed":
3680
+ status = "removed"
3681
+ error_code = error_code or verification.get("error_code")
3682
+ message = message or verification.get("message")
3683
+ return (
3684
+ str(name) if name not in (None, "") else None,
3685
+ str(view_key) if view_key not in (None, "") else None,
3686
+ status,
3687
+ error_code,
3688
+ message,
3689
+ )
3690
+
3691
+
3692
+ def _builder_chart_resources(payload: JSONObject) -> list[JSONObject]:
3693
+ parent = _builder_app_parent(payload)
3694
+ resources: list[JSONObject] = []
3695
+ for item in payload.get("chart_results") or []:
3696
+ if not isinstance(item, dict):
3697
+ continue
3698
+ status = str(item.get("status") or "success")
3699
+ operation = _builder_operation(item.get("operation") or status, fallback="updated")
3700
+ if status == "failed":
3701
+ operation = "failed"
3702
+ resource_status = "failed" if status == "failed" else ("readback_pending" if status == "readback_pending" else "success")
3703
+ chart_id = item.get("chart_id") or item.get("chartId")
3704
+ chart_key = item.get("chart_key") or item.get("chartKey")
3705
+ resources.append(
3706
+ _builder_resource(
3707
+ resource_type="chart",
3708
+ operation=operation,
3709
+ status=resource_status,
3710
+ id_value=chart_id,
3711
+ key=chart_key or chart_id,
3712
+ name=item.get("name") or item.get("chart_name") or item.get("chartName"),
3713
+ ids={
3714
+ **({"chart_id": chart_id} if chart_id not in (None, "") else {}),
3715
+ **({"chart_key": chart_key} if chart_key else {}),
3716
+ **({"app_key": payload.get("app_key")} if payload.get("app_key") else {}),
3717
+ **({"chart_type": item.get("chart_type")} if item.get("chart_type") else {}),
3718
+ },
3719
+ parent=parent,
3720
+ error_code=item.get("error_code"),
3721
+ message=item.get("message") if status in {"failed", "readback_pending"} else None,
3722
+ )
3723
+ )
3724
+ return resources
3725
+
3726
+
3727
+ def _builder_portal_resources(payload: JSONObject) -> list[JSONObject]:
3728
+ status = _builder_status(payload, "success")
3729
+ draft_result = payload.get("draft_result") if isinstance(payload.get("draft_result"), dict) else {}
3730
+ live_result = payload.get("live_result") if isinstance(payload.get("live_result"), dict) else {}
3731
+ normalized_args = payload.get("normalized_args") if isinstance(payload.get("normalized_args"), dict) else {}
3732
+ dash_key = (
3733
+ payload.get("dash_key")
3734
+ or payload.get("dashKey")
3735
+ or draft_result.get("dashKey")
3736
+ or draft_result.get("dash_key")
3737
+ or live_result.get("dashKey")
3738
+ or live_result.get("dash_key")
3739
+ )
3740
+ dash_name = (
3741
+ payload.get("dash_name")
3742
+ or payload.get("dashName")
3743
+ or payload.get("name")
3744
+ or draft_result.get("dashName")
3745
+ or draft_result.get("dash_name")
3746
+ or draft_result.get("name")
3747
+ or live_result.get("dashName")
3748
+ or live_result.get("dash_name")
3749
+ or live_result.get("name")
3750
+ or normalized_args.get("dash_name")
3751
+ or normalized_args.get("dashName")
3752
+ )
3753
+ package_id = payload.get("package_id") or normalized_args.get("package_id") or normalized_args.get("package_tag_id")
3754
+ if package_id in (None, "") and isinstance(draft_result.get("tags"), list) and draft_result.get("tags"):
3755
+ first_tag = draft_result.get("tags")[0]
3756
+ if isinstance(first_tag, dict):
3757
+ package_id = first_tag.get("tagId") or first_tag.get("tag_id") or first_tag.get("id")
3758
+ operation = "failed" if status == "failed" else ("created" if bool(payload.get("created")) else "updated")
3759
+ parent = None
3760
+ if package_id:
3761
+ parent = _builder_parent("package", id_value=package_id, key=package_id)
3762
+ icon_config = (
3763
+ _builder_container_icon_config(payload, raw_keys=("dash_icon", "dashIcon", "icon"))
3764
+ or _builder_container_icon_config(draft_result, raw_keys=("dashIcon", "dash_icon", "icon"))
3765
+ or _builder_container_icon_config(live_result, raw_keys=("dashIcon", "dash_icon", "icon"))
3766
+ or _builder_container_icon_config(normalized_args, raw_keys=("icon", "dash_icon", "dashIcon"))
3767
+ )
3768
+ return [
3769
+ _builder_resource(
3770
+ resource_type="portal",
3771
+ operation=operation,
3772
+ status=status,
3773
+ key=dash_key,
3774
+ name=dash_name,
3775
+ ids={
3776
+ **({"dash_key": dash_key} if dash_key else {}),
3777
+ **({"package_id": package_id} if package_id else {}),
3778
+ },
3779
+ parent=parent,
3780
+ icon_config=icon_config,
3781
+ error_code=payload.get("error_code"),
3782
+ message=payload.get("message") if status == "failed" else None,
3783
+ )
3784
+ ]
3785
+
3786
+
3787
+ def _builder_button_resources(payload: JSONObject) -> list[JSONObject]:
3788
+ parent = _builder_app_parent(payload)
3789
+ resources: list[JSONObject] = []
3790
+ for key, operation in (("created", "created"), ("updated", "updated"), ("removed", "removed"), ("failed", "failed")):
3791
+ for item in payload.get(key) or []:
3792
+ if not isinstance(item, dict):
3793
+ continue
3794
+ status = "failed" if operation == "failed" or item.get("status") == "failed" else str(item.get("status") or "success")
3795
+ button_id = item.get("button_id") or item.get("buttonId")
3796
+ resources.append(
3797
+ _builder_resource(
3798
+ resource_type="button",
3799
+ operation=operation if operation != "failed" else "failed",
3800
+ status=status,
3801
+ id_value=button_id,
3802
+ key=button_id,
3803
+ name=item.get("button_text") or item.get("buttonText"),
3804
+ ids={
3805
+ **({"button_id": button_id} if button_id not in (None, "") else {}),
3806
+ **({"app_key": payload.get("app_key")} if payload.get("app_key") else {}),
3807
+ },
3808
+ parent=parent,
3809
+ error_code=item.get("error_code"),
3810
+ message=item.get("message") if status in {"failed", "readback_pending"} else None,
3811
+ )
3812
+ )
3813
+ for item in payload.get("view_configs") or []:
3814
+ if isinstance(item, dict):
3815
+ status = str(item.get("status") or "success")
3816
+ view_key = item.get("view_key") or item.get("viewKey")
3817
+ resources.append(
3818
+ _builder_resource(
3819
+ resource_type="button_binding",
3820
+ operation="updated" if status != "failed" else "failed",
3821
+ status=status,
3822
+ key=view_key,
3823
+ name=item.get("view_name") or item.get("viewName") or view_key,
3824
+ ids={
3825
+ **({"view_key": view_key} if view_key else {}),
3826
+ **({"app_key": payload.get("app_key")} if payload.get("app_key") else {}),
3827
+ },
3828
+ parent=parent,
3829
+ error_code=item.get("error_code"),
3830
+ message=item.get("message") if status in {"failed", "readback_pending"} else None,
3831
+ )
3832
+ )
3833
+ return resources
3834
+
3835
+
3836
+ def _builder_associated_resource_resources(payload: JSONObject) -> list[JSONObject]:
3837
+ parent = _builder_app_parent(payload)
3838
+ readback_by_id = _builder_associated_resource_readback_by_id(payload)
3839
+ resources: list[JSONObject] = []
3840
+ for key, operation in (
3841
+ ("created", "created"),
3842
+ ("updated", "updated"),
3843
+ ("unchanged", "unchanged"),
3844
+ ("removed", "removed"),
3845
+ ("failed", "failed"),
3846
+ ):
3847
+ for item in payload.get(key) or []:
3848
+ if not isinstance(item, dict):
3849
+ continue
3850
+ status = "failed" if operation == "failed" or item.get("status") == "failed" else str(item.get("status") or "success")
3851
+ associated_item_id = item.get("associated_item_id") or item.get("associatedItemId")
3852
+ readback = readback_by_id.get(str(associated_item_id)) if associated_item_id not in (None, "") else None
3853
+ view_key = item.get("view_key") or item.get("viewKey") or (readback or {}).get("view_key") or (readback or {}).get("viewKey")
3854
+ chart_key = item.get("chart_key") or item.get("chartKey") or (readback or {}).get("chart_key") or (readback or {}).get("chartKey")
3855
+ target_app_key = item.get("target_app_key") or (readback or {}).get("target_app_key")
3856
+ name = item.get("name") or item.get("resource_name") or (readback or {}).get("name") or view_key or chart_key
3857
+ resources.append(
3858
+ _builder_resource(
3859
+ resource_type="associated_resource",
3860
+ operation=operation if operation != "failed" else "failed",
3861
+ status=status,
3862
+ id_value=associated_item_id,
3863
+ key=view_key or chart_key or associated_item_id,
3864
+ name=name,
3865
+ ids={
3866
+ **({"associated_item_id": associated_item_id} if associated_item_id not in (None, "") else {}),
3867
+ **({"app_key": payload.get("app_key")} if payload.get("app_key") else {}),
3868
+ **({"target_app_key": target_app_key} if target_app_key else {}),
3869
+ **({"view_key": view_key} if view_key else {}),
3870
+ **({"chart_key": chart_key} if chart_key else {}),
3871
+ },
3872
+ parent=parent,
3873
+ error_code=item.get("error_code"),
3874
+ message=item.get("message") if status in {"failed", "readback_pending"} else None,
3875
+ )
3876
+ )
3877
+ for item in payload.get("view_configs") or []:
3878
+ if isinstance(item, dict):
3879
+ status = str(item.get("status") or "success")
3880
+ view_key = item.get("view_key") or item.get("viewKey")
3881
+ resources.append(
3882
+ _builder_resource(
3883
+ resource_type="associated_resource_binding",
3884
+ operation="updated" if status != "failed" else "failed",
3885
+ status=status,
3886
+ key=view_key,
3887
+ name=item.get("view_name") or item.get("viewName") or view_key,
3888
+ ids={
3889
+ **({"view_key": view_key} if view_key else {}),
3890
+ **({"app_key": payload.get("app_key")} if payload.get("app_key") else {}),
3891
+ },
3892
+ parent=parent,
3893
+ error_code=item.get("error_code"),
3894
+ message=item.get("message") if status == "failed" else None,
3895
+ )
3896
+ )
3897
+ return resources
3898
+
3899
+
3900
+ def _builder_associated_resource_readback_by_id(payload: JSONObject) -> dict[str, JSONObject]:
3901
+ result: dict[str, JSONObject] = {}
3902
+
3903
+ def collect(items: object) -> None:
3904
+ if not isinstance(items, list):
3905
+ return
3906
+ for item in items:
3907
+ if not isinstance(item, dict):
3908
+ continue
3909
+ associated_item_id = item.get("associated_item_id") or item.get("associatedItemId")
3910
+ if associated_item_id in (None, ""):
3911
+ continue
3912
+ result[str(associated_item_id)] = item
3913
+
3914
+ collect(payload.get("associated_resources"))
3915
+ for config in payload.get("view_configs") or []:
3916
+ if not isinstance(config, dict):
3917
+ continue
3918
+ for key in ("actual", "expected"):
3919
+ value = config.get(key)
3920
+ if isinstance(value, dict):
3921
+ collect(value.get("items"))
3922
+ return result
3923
+
3924
+
2258
3925
  def _coerce_api_error(error: Exception) -> QingflowApiError:
2259
3926
  if isinstance(error, QingflowApiError):
2260
3927
  return error
@@ -2403,6 +4070,40 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
2403
4070
  "tool_name": "chart_get",
2404
4071
  },
2405
4072
  },
4073
+ "workspace_icon_catalog_get": {
4074
+ "allowed_keys": [],
4075
+ "aliases": {},
4076
+ "allowed_values": {
4077
+ "icon": list(WORKSPACE_ICON_NAMES),
4078
+ "color": list(WORKSPACE_ICON_COLORS),
4079
+ "generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
4080
+ },
4081
+ "execution_notes": [
4082
+ "read this before creating app packages, apps, or portals when choosing supported workspace icons",
4083
+ "the CLI validates icon/color candidates but does not infer business defaults from resource names",
4084
+ "new app/package/portal creation requires explicit non-template icon + color",
4085
+ ],
4086
+ "minimal_example": {
4087
+ "profile": "default",
4088
+ },
4089
+ },
4090
+ "package_list": {
4091
+ "allowed_keys": ["trial_status", "query"],
4092
+ "aliases": {"trialStatus": "trial_status", "keyword": "query"},
4093
+ "allowed_values": {"trial_status": ["all"]},
4094
+ "execution_notes": [
4095
+ "lists app packages visible to the current builder profile by calling backend GET /tag?trialStatus=...",
4096
+ "query is applied locally to package_id/tag_id/package_name/tag_name after /tag returns",
4097
+ "does not fall back to app list because app list cannot represent empty packages, duplicate package names, or package-level permissions",
4098
+ "returns package_id/package_name plus compatible tag_id/tag_name; use package_get for package detail before editing",
4099
+ "permission failures are returned as PACKAGE_LIST_FAILED with backend transport details",
4100
+ ],
4101
+ "minimal_example": {
4102
+ "profile": "default",
4103
+ "trial_status": "all",
4104
+ "query": "产品研发",
4105
+ },
4106
+ },
2406
4107
  "package_get": {
2407
4108
  "allowed_keys": ["package_id"],
2408
4109
  "aliases": {"packageId": "package_id"},
@@ -2427,9 +4128,18 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
2427
4128
  "iconColor": "color",
2428
4129
  "allowDetach": "allow_detach",
2429
4130
  },
2430
- "allowed_values": deepcopy(_VISIBILITY_ALLOWED_VALUES),
4131
+ "allowed_values": {
4132
+ **deepcopy(_VISIBILITY_ALLOWED_VALUES),
4133
+ "workspace_icon.icon_names": list(WORKSPACE_ICON_NAMES),
4134
+ "workspace_icon.icon_colors": list(WORKSPACE_ICON_COLORS),
4135
+ "workspace_icon.generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
4136
+ },
2431
4137
  "execution_notes": [
2432
4138
  "create or update package metadata, visibility, grouping, and ordering in one call",
4139
+ "creating a package requires explicit icon + color; icon=template is blocked because it is too generic",
4140
+ "updating a package preserves existing icon/color when omitted; explicit icon/color values are still validated",
4141
+ "call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
4142
+ "metadata keys omitted on update are preserved",
2433
4143
  "package_id maps internally to backend tagId; do not use tag_id in public calls",
2434
4144
  "items is a full package layout tree; omitting existing app/portal items is blocked unless allow_detach=true",
2435
4145
  "item shapes: {type:'app', app_key}, {type:'portal', dash_key}, or {type:'group', group_id?, name, items:[...]}",
@@ -2452,7 +4162,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
2452
4162
  "profile": "default",
2453
4163
  "package_name": "项目管理",
2454
4164
  "create_if_missing": True,
2455
- "icon": "files-folder",
4165
+ "icon": "briefcase",
2456
4166
  "color": "azure",
2457
4167
  "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
2458
4168
  },
@@ -2565,122 +4275,287 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
2565
4275
  },
2566
4276
  "execution_notes": [
2567
4277
  "use this read-only tool before button writes when an agent needs a supported icon or color choice",
2568
- "current frontend only supports template icons and template colors from this catalog",
4278
+ "current frontend only supports button icons and button colors from this catalog",
2569
4279
  "text/icon color is unified through text_color; there is no separate icon_color",
2570
4280
  ],
2571
4281
  "minimal_example": {
2572
4282
  "profile": "default",
2573
4283
  },
2574
4284
  },
2575
- "app_custom_button_list": {
2576
- "allowed_keys": ["app_key"],
2577
- "aliases": {},
2578
- "allowed_values": {},
2579
- "minimal_example": {
2580
- "profile": "default",
2581
- "app_key": "APP_KEY",
2582
- },
2583
- },
2584
- "app_custom_button_get": {
2585
- "allowed_keys": ["app_key", "button_id"],
2586
- "aliases": {"buttonId": "button_id"},
2587
- "allowed_values": {},
2588
- "minimal_example": {
2589
- "profile": "default",
2590
- "app_key": "APP_KEY",
2591
- "button_id": 1001,
2592
- },
2593
- },
2594
- "app_custom_button_create": {
2595
- "allowed_keys": ["app_key", "payload"],
4285
+ "app_custom_buttons_apply": {
4286
+ "allowed_keys": [
4287
+ "app_key",
4288
+ "apps",
4289
+ "upsert_buttons",
4290
+ "patch_buttons",
4291
+ "remove_buttons",
4292
+ "view_configs",
4293
+ "upsert_buttons[].client_key",
4294
+ "upsert_buttons[].button_id",
4295
+ "upsert_buttons[].button_text",
4296
+ "upsert_buttons[].style_preset",
4297
+ "upsert_buttons[].background_color",
4298
+ "upsert_buttons[].text_color",
4299
+ "upsert_buttons[].button_icon",
4300
+ "upsert_buttons[].trigger_action",
4301
+ "upsert_buttons[].trigger_link_url",
4302
+ "upsert_buttons[].trigger_add_data_config",
4303
+ "upsert_buttons[].trigger_add_data_config.target_app_key",
4304
+ "upsert_buttons[].trigger_add_data_config.field_mappings",
4305
+ "upsert_buttons[].trigger_add_data_config.field_mappings[].source_field",
4306
+ "upsert_buttons[].trigger_add_data_config.field_mappings[].target_field",
4307
+ "upsert_buttons[].trigger_add_data_config.default_values",
4308
+ "patch_buttons[].button_id",
4309
+ "patch_buttons[].button_text",
4310
+ "patch_buttons[].set",
4311
+ "patch_buttons[].unset",
4312
+ "view_configs[].view_key",
4313
+ "view_configs[].mode",
4314
+ "view_configs[].buttons",
4315
+ "view_configs[].buttons[].button_ref",
4316
+ "view_configs[].buttons[].placement",
4317
+ "view_configs[].buttons[].primary",
4318
+ "view_configs[].buttons[].button_limit",
4319
+ "view_configs[].buttons[].button_formula",
4320
+ "view_configs[].buttons[].button_formula_type",
4321
+ "view_configs[].buttons[].print_tpls",
4322
+ "remove_buttons[].button_id",
4323
+ "remove_buttons[].button_text",
4324
+ ],
2596
4325
  "aliases": {
2597
- "payload.buttonText": "payload.button_text",
2598
- "payload.stylePreset": "payload.style_preset",
2599
- "payload.backgroundColor": "payload.background_color",
2600
- "payload.textColor": "payload.text_color",
2601
- "payload.buttonIcon": "payload.button_icon",
2602
- "payload.triggerAction": "payload.trigger_action",
2603
- "payload.triggerLinkUrl": "payload.trigger_link_url",
2604
- "payload.triggerAddDataConfig": "payload.trigger_add_data_config",
2605
- "payload.externalQrobotConfig": "payload.external_qrobot_config",
2606
- "payload.customButtonExternalQRobotRelationVO": "payload.external_qrobot_config",
2607
- "payload.triggerWingsConfig": "payload.trigger_wings_config",
4326
+ "upsertButtons": "upsert_buttons",
4327
+ "patchButtons": "patch_buttons",
4328
+ "removeButtons": "remove_buttons",
4329
+ "viewConfigs": "view_configs",
4330
+ "clientKey": "client_key",
4331
+ "buttonId": "button_id",
4332
+ "buttonText": "button_text",
4333
+ "stylePreset": "style_preset",
4334
+ "backgroundColor": "background_color",
4335
+ "textColor": "text_color",
4336
+ "buttonIcon": "button_icon",
4337
+ "triggerAction": "trigger_action",
4338
+ "triggerLinkUrl": "trigger_link_url",
4339
+ "triggerAddDataConfig": "trigger_add_data_config",
4340
+ "targetAppKey": "trigger_add_data_config.target_app_key",
4341
+ "fieldMappings": "trigger_add_data_config.field_mappings",
4342
+ "defaultValues": "trigger_add_data_config.default_values",
4343
+ "mode": "view_configs[].mode",
4344
+ "applyMode": "view_configs[].mode",
4345
+ "buttonRef": "view_configs[].buttons[].button_ref",
4346
+ "beingMain": "view_configs[].buttons[].primary",
4347
+ "buttonLimit": "view_configs[].buttons[].button_limit",
4348
+ "visibleWhen": "view_configs[].buttons[].button_limit",
4349
+ "buttonFormula": "view_configs[].buttons[].button_formula",
4350
+ "buttonFormulaType": "view_configs[].buttons[].button_formula_type",
4351
+ "printTpls": "view_configs[].buttons[].print_tpls",
4352
+ "externalQrobotConfig": "external_qrobot_config",
4353
+ "customButtonExternalQRobotRelationVO": "external_qrobot_config",
4354
+ "triggerWingsConfig": "trigger_wings_config",
2608
4355
  },
2609
4356
  "allowed_values": {
2610
- "payload.trigger_action": [member.value for member in PublicButtonTriggerAction],
2611
- "payload.style_preset": [item["key"] for item in BUTTON_STYLE_PRESETS],
2612
- "payload.button_icon": list(BUTTON_ICONS),
2613
- "payload.background_color": list(BUTTON_BACKGROUND_COLORS),
2614
- "payload.text_color": list(BUTTON_TEXT_COLORS),
4357
+ "upsert_buttons[].trigger_action": [member.value for member in PublicButtonTriggerAction],
4358
+ "upsert_buttons[].style_preset": [item["key"] for item in BUTTON_STYLE_PRESETS],
4359
+ "upsert_buttons[].button_icon": list(BUTTON_ICONS),
4360
+ "upsert_buttons[].background_color": list(BUTTON_BACKGROUND_COLORS),
4361
+ "upsert_buttons[].text_color": list(BUTTON_TEXT_COLORS),
4362
+ "view_configs[].mode": ["merge", "replace"],
4363
+ "view_configs[].buttons[].placement": [member.value for member in PublicButtonPlacement],
2615
4364
  },
2616
4365
  "execution_notes": [
2617
- "custom button writes now auto-publish the current app draft as a fixed closing step",
4366
+ "this is the default custom-button write path; old list/get/create/update/delete tools are hidden from the public agent surface",
4367
+ "use patch_buttons for partial parameter replacement on existing buttons; the tool reads current button detail, merges patch_buttons[].set/unset, then submits the backend full-save payload internally",
4368
+ "button_id targets an existing button; without button_id, button_text is used as an exact unique upsert key, otherwise a new button is created",
4369
+ "for addData buttons, use trigger_add_data_config.target_app_key plus field_mappings/default_values; field_mappings compiles to backend queRelation",
4370
+ "field_mappings.source_field accepts source schema fields and supported system fields: 数据ID/row_record_id/apply_id/_id maps to current record id (-17), 编号/record_number maps to visible record number (0)",
4371
+ "to fill a target relation field with the current source record, map source_field='数据ID' to the target relation field; default_values is for static constants, not dynamic current-record values",
4372
+ "do not write raw que_relation unless maintaining a legacy config; field_mappings/default_values and que_relation are mutually exclusive",
4373
+ "view_configs binds custom buttons into views in the same apply call; button_ref may be a same-call client_key, a button_id, or an exact unique existing button_text",
4374
+ "view_configs[].view_key is the raw builder view key from app_get.views[].view_key; do not pass record-data view_id values like custom:VIEW_KEY",
4375
+ "view_configs[].buttons is required in merge mode; omitting buttons is blocked to avoid no-op writes and accidental publish",
4376
+ "view_configs[].mode defaults to merge; use mode=replace or an explicit empty buttons list to replace/clear a view's custom button bindings",
4377
+ "advanced view button binding supports button_limit, button_formula, button_formula_type, and print_tpls when visibility or print-template behavior is required",
4378
+ "default placements are header and detail; header maps to frontend top buttons",
4379
+ "placement=list configures backend INSIDE row/list buttons; header maps to TOP and detail maps to DETAIL",
4380
+ "remove_buttons supports button_id or exact unique button_text",
4381
+ "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",
4382
+ "if a removed button returns readback_status=unavailable or still_exists, treat the result as readback pending and do not blindly repeat the delete",
4383
+ "all operations share one edit context and publish after at least one write succeeds; there is no draft-only mode for this tool",
2618
4384
  "background_color and text_color cannot both be white",
2619
- "payload accepts either style_preset + optional button_icon, or explicit button_icon/background_color/text_color from button_style_catalog_get",
2620
- "for addData buttons, put field mappings in payload.trigger_add_data_config.que_relation",
4385
+ "accepts apps[] for multi-app batch; each item is {app_key, upsert_buttons?, patch_buttons?, remove_buttons?, view_configs?}",
2621
4386
  ],
2622
4387
  "minimal_example": {
2623
4388
  "profile": "default",
2624
4389
  "app_key": "APP_KEY",
2625
- "payload": {
2626
- "button_text": "新增记录",
2627
- "style_preset": "primary_blue",
2628
- "button_icon": "ex-plus-circle",
2629
- "trigger_action": "link",
2630
- "trigger_link_url": "https://example.com",
2631
- },
4390
+ "upsert_buttons": [
4391
+ {
4392
+ "button_text": "同步客户",
4393
+ "style_preset": "primary_blue",
4394
+ "button_icon": "ex-switch",
4395
+ "trigger_action": "link",
4396
+ "trigger_link_url": "https://example.com",
4397
+ }
4398
+ ],
4399
+ "patch_buttons": [
4400
+ {
4401
+ "button_text": "同步客户",
4402
+ "set": {"trigger_link_url": "https://example.com/new"},
4403
+ }
4404
+ ],
4405
+ "remove_buttons": [{"button_text": "旧按钮"}],
4406
+ "view_configs": [],
4407
+ },
4408
+ "add_data_example": {
4409
+ "profile": "default",
4410
+ "app_key": "APP_KEY",
4411
+ "upsert_buttons": [
4412
+ {
4413
+ "client_key": "add_order",
4414
+ "button_text": "新增订单",
4415
+ "style_preset": "neutral_outline",
4416
+ "button_icon": "ex-plus-circle",
4417
+ "trigger_action": "addData",
4418
+ "trigger_add_data_config": {
4419
+ "target_app_key": "TARGET_APP",
4420
+ "field_mappings": [{"source_field": "客户名称", "target_field": "客户"}],
4421
+ "default_values": {"状态": "待提交"},
4422
+ },
4423
+ }
4424
+ ],
4425
+ "remove_buttons": [],
4426
+ "view_configs": [
4427
+ {
4428
+ "view_key": "VIEW_KEY",
4429
+ "buttons": [{"button_ref": "add_order", "placement": "detail", "primary": True}],
4430
+ }
4431
+ ],
4432
+ },
4433
+ "relation_current_record_example": {
4434
+ "profile": "default",
4435
+ "app_key": "EMPLOYEE_APP",
4436
+ "upsert_buttons": [
4437
+ {
4438
+ "client_key": "add_worklog",
4439
+ "button_text": "快捷添加工时",
4440
+ "style_preset": "neutral_outline",
4441
+ "button_icon": "ex-plus-circle",
4442
+ "trigger_action": "addData",
4443
+ "trigger_add_data_config": {
4444
+ "target_app_key": "WORKLOG_APP",
4445
+ "field_mappings": [{"source_field": "数据ID", "target_field": "关联员工"}],
4446
+ "default_values": {"状态": "待提交"},
4447
+ },
4448
+ }
4449
+ ],
4450
+ "view_configs": [
4451
+ {
4452
+ "view_key": "RAW_VIEW_KEY",
4453
+ "buttons": [{"button_ref": "add_worklog", "placement": "detail", "primary": True}],
4454
+ }
4455
+ ],
2632
4456
  },
2633
4457
  },
2634
- "app_custom_button_update": {
2635
- "allowed_keys": ["app_key", "button_id", "payload"],
4458
+ "app_associated_resources_apply": {
4459
+ "allowed_keys": [
4460
+ "app_key",
4461
+ "upsert_resources",
4462
+ "patch_resources",
4463
+ "remove_associated_item_ids",
4464
+ "reorder_associated_item_ids",
4465
+ "view_configs",
4466
+ "upsert_resources[].client_key",
4467
+ "upsert_resources[].associated_item_id",
4468
+ "upsert_resources[].graph_type",
4469
+ "upsert_resources[].target_app_key",
4470
+ "upsert_resources[].view_key",
4471
+ "upsert_resources[].chart_key",
4472
+ "upsert_resources[].report_source",
4473
+ "upsert_resources[].match_mappings",
4474
+ "upsert_resources[].match_mappings[].target_field",
4475
+ "upsert_resources[].match_mappings[].source_field",
4476
+ "upsert_resources[].match_mappings[].value",
4477
+ "upsert_resources[].match_mappings[].operator",
4478
+ "upsert_resources[].match_rules",
4479
+ "patch_resources[].associated_item_id",
4480
+ "patch_resources[].set",
4481
+ "patch_resources[].unset",
4482
+ "view_configs[].view_key",
4483
+ "view_configs[].visible",
4484
+ "view_configs[].limit_type",
4485
+ "view_configs[].associated_item_ids",
4486
+ "view_configs[].associated_item_refs",
4487
+ ],
2636
4488
  "aliases": {
2637
- "buttonId": "button_id",
2638
- "payload.buttonText": "payload.button_text",
2639
- "payload.stylePreset": "payload.style_preset",
2640
- "payload.backgroundColor": "payload.background_color",
2641
- "payload.textColor": "payload.text_color",
2642
- "payload.buttonIcon": "payload.button_icon",
2643
- "payload.triggerAction": "payload.trigger_action",
2644
- "payload.triggerLinkUrl": "payload.trigger_link_url",
2645
- "payload.triggerAddDataConfig": "payload.trigger_add_data_config",
2646
- "payload.externalQrobotConfig": "payload.external_qrobot_config",
2647
- "payload.customButtonExternalQRobotRelationVO": "payload.external_qrobot_config",
2648
- "payload.triggerWingsConfig": "payload.trigger_wings_config",
4489
+ "upsertResources": "upsert_resources",
4490
+ "patchResources": "patch_resources",
4491
+ "resources": "upsert_resources",
4492
+ "removeAssociatedItemIds": "remove_associated_item_ids",
4493
+ "reorderAssociatedItemIds": "reorder_associated_item_ids",
4494
+ "viewConfigs": "view_configs",
4495
+ "associatedItemId": "associated_item_id",
4496
+ "graphType": "graph_type",
4497
+ "targetAppKey": "target_app_key",
4498
+ "chartKey": "chart_key",
4499
+ "chartId": "chart_key",
4500
+ "viewKey": "view_key",
4501
+ "viewgraphKey": "view_key",
4502
+ "reportSource": "report_source",
4503
+ "matchMappings": "match_mappings",
4504
+ "matchRules": "match_rules",
4505
+ "associatedItemRefs": "associated_item_refs",
4506
+ "associatedItemIds": "associated_item_ids",
2649
4507
  },
2650
4508
  "allowed_values": {
2651
- "payload.trigger_action": [member.value for member in PublicButtonTriggerAction],
2652
- "payload.style_preset": [item["key"] for item in BUTTON_STYLE_PRESETS],
2653
- "payload.button_icon": list(BUTTON_ICONS),
2654
- "payload.background_color": list(BUTTON_BACKGROUND_COLORS),
2655
- "payload.text_color": list(BUTTON_TEXT_COLORS),
4509
+ "upsert_resources[].graph_type": ["chart", "view"],
4510
+ "upsert_resources[].report_source": ["app", "dataset"],
4511
+ "view_configs[].limit_type": ["all", "select"],
2656
4512
  },
2657
4513
  "execution_notes": [
2658
- "custom button writes now auto-publish the current app draft as a fixed closing step",
2659
- "background_color and text_color cannot both be white",
2660
- "payload accepts either style_preset + optional button_icon, or explicit button_icon/background_color/text_color from button_style_catalog_get",
2661
- "for addData buttons, put field mappings in payload.trigger_add_data_config.que_relation",
4514
+ "this tool manages Qingflow in-app associated report/view display; it does not create or edit QingBI report bodies/configs",
4515
+ "create or edit app-source BI report bodies first with app_charts_apply, then attach the resulting chart_id here with graph_type=chart; dataset BI reports can only be attached when they already exist",
4516
+ "this is the default associated report/view path; it manages both the app-level associated resource pool and per-view display config",
4517
+ "use patch_resources for partial parameter replacement on existing associated resources; the tool reads the current resource including backend-required raw fields, merges patch_resources[].set/unset, then submits the backend full-save payload internally",
4518
+ "associated_item_id is form_asos_chart.id from app_get.associated_resources[].associated_item_id; view_configs/remove/reorder may also pass an existing associated resource's chart_id/chart_key/view_key and the tool resolves it to the internal id",
4519
+ "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",
4520
+ "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",
4521
+ "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",
4522
+ "use match_mappings for associated view/report filtering; dynamic conditions use source_field and static conditions use value",
4523
+ "match_mappings.source_field accepts source schema fields plus system fields 数据ID(-17) and 编号(0); match_mappings compiles to backend matchRules",
4524
+ "do not write raw match_rules unless preserving a legacy backend config; match_mappings and match_rules are mutually exclusive",
4525
+ "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",
4526
+ "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",
4527
+ "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",
4528
+ "this tool publishes after at least one write succeeds; there is no draft-only mode",
4529
+ "visible=false hides the associated-resource area without clearing previous selected ids; visible=true with limit_type=all shows the whole app-level pool",
2662
4530
  ],
2663
4531
  "minimal_example": {
2664
4532
  "profile": "default",
2665
4533
  "app_key": "APP_KEY",
2666
- "button_id": 1001,
2667
- "payload": {
2668
- "button_text": "查看详情",
2669
- "style_preset": "neutral_outline",
2670
- "button_icon": "ex-edit",
2671
- "trigger_action": "link",
2672
- "trigger_link_url": "https://example.com/detail",
2673
- },
2674
- },
2675
- },
2676
- "app_custom_button_delete": {
2677
- "allowed_keys": ["app_key", "button_id"],
2678
- "aliases": {"buttonId": "button_id"},
2679
- "allowed_values": {},
2680
- "minimal_example": {
2681
- "profile": "default",
2682
- "app_key": "APP_KEY",
2683
- "button_id": 1001,
4534
+ "upsert_resources": [
4535
+ {
4536
+ "client_key": "customer_view",
4537
+ "graph_type": "view",
4538
+ "target_app_key": "TARGET_APP",
4539
+ "view_key": "VIEW_KEY",
4540
+ "match_mappings": [{"target_field": "关联员工", "source_field": "数据ID"}],
4541
+ }
4542
+ ],
4543
+ "patch_resources": [
4544
+ {
4545
+ "associated_item_id": 123,
4546
+ "set": {"match_mappings": [{"target_field": "状态", "value": "待提交"}]},
4547
+ }
4548
+ ],
4549
+ "remove_associated_item_ids": [],
4550
+ "reorder_associated_item_ids": [],
4551
+ "view_configs": [
4552
+ {
4553
+ "view_key": "MAIN_VIEW",
4554
+ "visible": True,
4555
+ "limit_type": "select",
4556
+ "associated_item_refs": ["customer_view"],
4557
+ }
4558
+ ],
2684
4559
  },
2685
4560
  },
2686
4561
  "app_schema_plan": {
@@ -2709,26 +4584,33 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
2709
4584
  "field.customBtnTextStatus": "field.custom_button_text_enabled",
2710
4585
  "field.customBtnText": "field.custom_button_text",
2711
4586
  "field.subfieldUpdates": "field.subfield_updates",
4587
+ "field.asDataTitle": "field.as_data_title",
4588
+ "field.asDataCover": "field.as_data_cover",
2712
4589
  },
2713
4590
  "allowed_values": {
2714
4591
  "field.type": [member.value for member in PublicFieldType],
2715
4592
  "field.relation_mode": [member.value for member in PublicRelationMode],
2716
4593
  "field_type_ids": sorted(FIELD_TYPE_ID_ALIASES.keys()),
4594
+ "workspace_icon.icon_names": list(WORKSPACE_ICON_NAMES),
4595
+ "workspace_icon.icon_colors": list(WORKSPACE_ICON_COLORS),
4596
+ "workspace_icon.generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
2717
4597
  **deepcopy(_VISIBILITY_ALLOWED_VALUES),
2718
4598
  },
2719
4599
  "execution_notes": [
2720
4600
  "create mode may set visibility for the new app; edit mode may update visibility on an existing app",
4601
+ "create mode should include explicit non-template icon + color; apply mode enforces this before writing",
4602
+ "call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
2721
4603
  *_VISIBILITY_EXECUTION_NOTES,
2722
4604
  ],
2723
4605
  "minimal_example": {
2724
4606
  "profile": "default",
2725
4607
  "app_name": "研发项目管理",
2726
4608
  "package_id": 1001,
2727
- "icon": "template",
4609
+ "icon": "briefcase",
2728
4610
  "color": "emerald",
2729
4611
  "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
2730
4612
  "create_if_missing": True,
2731
- "add_fields": [{"name": "项目名称", "type": "text"}],
4613
+ "add_fields": [{"name": "项目名称", "type": "text", "as_data_title": True}],
2732
4614
  "update_fields": [],
2733
4615
  "remove_fields": [],
2734
4616
  },
@@ -2750,11 +4632,40 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
2750
4632
  },
2751
4633
  },
2752
4634
  "app_schema_apply": {
2753
- "allowed_keys": ["app_key", "package_id", "app_name", "icon", "color", "visibility", "create_if_missing", "publish", "add_fields", "update_fields", "remove_fields"],
4635
+ "allowed_keys": [
4636
+ "app_key",
4637
+ "package_id",
4638
+ "app_name",
4639
+ "icon",
4640
+ "color",
4641
+ "visibility",
4642
+ "create_if_missing",
4643
+ "publish",
4644
+ "add_fields",
4645
+ "update_fields",
4646
+ "remove_fields",
4647
+ "apps",
4648
+ "apps[].client_key",
4649
+ "apps[].app_key",
4650
+ "apps[].app_name",
4651
+ "apps[].icon",
4652
+ "apps[].color",
4653
+ "apps[].visibility",
4654
+ "apps[].add_fields",
4655
+ "apps[].update_fields",
4656
+ "apps[].remove_fields",
4657
+ "apps[].add_fields[].target_app_ref",
4658
+ ],
2754
4659
  "aliases": {
2755
4660
  "app_title": "app_name",
2756
4661
  "title": "app_name",
2757
4662
  "packageId": "package_id",
4663
+ "apps[].clientKey": "apps[].client_key",
4664
+ "apps[].appKey": "apps[].app_key",
4665
+ "apps[].appName": "apps[].app_name",
4666
+ "apps[].appTitle": "apps[].app_name",
4667
+ "field.targetAppRef": "field.target_app_ref",
4668
+ "field.targetAppClientKey": "field.target_app_ref",
2758
4669
  "field.title": "field.name",
2759
4670
  "field.label": "field.name",
2760
4671
  "field.fields": "field.subfields",
@@ -2775,6 +4686,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
2775
4686
  "field.customBtnTextStatus": "field.custom_button_text_enabled",
2776
4687
  "field.customBtnText": "field.custom_button_text",
2777
4688
  "field.subfieldUpdates": "field.subfield_updates",
4689
+ "field.asDataTitle": "field.as_data_title",
4690
+ "field.asDataCover": "field.as_data_cover",
2778
4691
  },
2779
4692
  "allowed_values": {
2780
4693
  "field.type": [member.value for member in PublicFieldType],
@@ -2789,8 +4702,16 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
2789
4702
  "use exactly one resource mode",
2790
4703
  "edit mode: app_key, optional app_name to rename the existing app",
2791
4704
  "create mode: package_id + app_name + create_if_missing=true",
4705
+ "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",
4706
+ "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",
4707
+ "multi-app mode is not transactional; read created_app_keys and apps[].status before retrying, and retry only failed app items",
2792
4708
  "create mode defaults new app visibility to workspace/not when visibility is omitted; edit mode preserves current visibility when omitted",
4709
+ "create mode requires explicit icon + color; icon=template is blocked because it is too generic",
4710
+ "multi-app create mode requires each new app item to include a distinct non-template icon and a valid color",
4711
+ "edit mode preserves existing icon/color when omitted; explicit icon/color values are still validated",
4712
+ "call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
2793
4713
  *_VISIBILITY_EXECUTION_NOTES,
4714
+ "update_fields is the field-level partial update path; it reads current form schema and preserves untouched field config",
2794
4715
  "multiple relation fields are backend-risky; read verification.relation_field_limit_verified and warnings before declaring the schema stable",
2795
4716
  "backend 49614 is normalized to MULTIPLE_RELATION_FIELDS_UNSUPPORTED with a workaround message",
2796
4717
  "relation_mode=multiple maps to referenceConfig.optionalDataNum=0",
@@ -2806,20 +4727,59 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
2806
4727
  "code block outputs must be emitted through qf_output assignment; use qf_output = {...} or assign qf_output after building the result object, never const/let qf_output =",
2807
4728
  "builder automatically normalizes const/let qf_output assignments on write and rejects output-bound code blocks that still do not contain a valid qf_output assignment",
2808
4729
  "code_block_binding and q_linker_binding target fields are limited to text, long_text, number, amount, date, datetime, single_select, multi_select, and boolean",
4730
+ "data title is required: mark exactly one top-level field with as_data_title=true; use a readable name/number/date-like field",
4731
+ "data cover is optional: mark at most one top-level attachment field with as_data_cover=true",
2809
4732
  ],
2810
4733
  "minimal_example": {
2811
4734
  "profile": "default",
2812
4735
  "app_name": "研发项目管理",
2813
4736
  "package_id": 1001,
2814
- "icon": "template",
4737
+ "icon": "briefcase",
2815
4738
  "color": "emerald",
2816
4739
  "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
2817
4740
  "create_if_missing": True,
2818
4741
  "publish": True,
2819
- "add_fields": [{"name": "项目名称", "type": "text"}],
4742
+ "add_fields": [
4743
+ {"name": "项目名称", "type": "text", "as_data_title": True},
4744
+ {"name": "项目封面", "type": "attachment", "as_data_cover": True},
4745
+ ],
2820
4746
  "update_fields": [],
2821
4747
  "remove_fields": [],
2822
4748
  },
4749
+ "multi_app_example": {
4750
+ "profile": "default",
4751
+ "package_id": 1001,
4752
+ "create_if_missing": True,
4753
+ "publish": True,
4754
+ "apps": [
4755
+ {
4756
+ "client_key": "employee",
4757
+ "app_name": "员工花名册",
4758
+ "icon": "business-personalcard",
4759
+ "color": "emerald",
4760
+ "add_fields": [
4761
+ {"name": "员工名称", "type": "text", "as_data_title": True},
4762
+ {"name": "员工照片", "type": "attachment", "as_data_cover": True},
4763
+ ],
4764
+ },
4765
+ {
4766
+ "client_key": "worklog",
4767
+ "app_name": "工时表",
4768
+ "icon": "clock",
4769
+ "color": "blue",
4770
+ "add_fields": [
4771
+ {"name": "工时标题", "type": "text", "as_data_title": True},
4772
+ {
4773
+ "name": "关联员工",
4774
+ "type": "relation",
4775
+ "target_app_ref": "employee",
4776
+ "display_field": {"name": "员工名称"},
4777
+ "visible_fields": [{"name": "员工名称"}],
4778
+ },
4779
+ ],
4780
+ },
4781
+ ],
4782
+ },
2823
4783
  "rename_example": {
2824
4784
  "profile": "default",
2825
4785
  "app_key": "APP_PROJECT",
@@ -2972,7 +4932,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
2972
4932
  "preset_example": {"profile": "default", "app_key": "APP_KEY", "mode": "merge", "preset": "balanced", "sections": []},
2973
4933
  },
2974
4934
  "app_layout_apply": {
2975
- "allowed_keys": ["app_key", "mode", "publish", "sections"],
4935
+ "allowed_keys": ["app_key", "mode", "publish", "sections", "apps"],
2976
4936
  "aliases": {"overwrite": "replace", "sectionId": "section_id"},
2977
4937
  "section_allowed_keys": ["type", "paragraph_id", "section_id", "title", "rows"],
2978
4938
  "section_aliases": {
@@ -2985,8 +4945,11 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
2985
4945
  },
2986
4946
  "allowed_values": {"mode": [member.value for member in LayoutApplyMode]},
2987
4947
  "execution_notes": [
4948
+ "mode=merge is the layout partial update path: mentioned sections are merged and unmentioned fields are preserved",
4949
+ "mode=replace is full layout replacement and should be used only when intentionally rewriting all sections",
2988
4950
  "layout verification is split into layout_verified and layout_summary_verified",
2989
4951
  "LAYOUT_SUMMARY_UNVERIFIED means raw form readback is stronger than the compact summary",
4952
+ "accepts apps[] for multi-app batch; each item is {app_key, mode?, sections, publish?}; top-level publish is used as default for items that omit publish",
2990
4953
  ],
2991
4954
  "minimal_section_example": {"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["字段A", "字段B", "字段C", "字段D"]]},
2992
4955
  "minimal_example": {
@@ -2996,96 +4959,95 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
2996
4959
  "publish": True,
2997
4960
  "sections": [{"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["项目名称", "项目负责人", "项目阶段", "优先级"]]}],
2998
4961
  },
2999
- },
3000
- "app_flow_plan": {
3001
- "allowed_keys": ["app_key", "mode", "nodes", "transitions", "preset"],
3002
- "aliases": {
3003
- "overwrite": "replace",
3004
- "base_preset": "preset",
3005
- "default_approval": "basic_approval",
3006
- "node.role_names": "node.assignees.role_names",
3007
- "node.role_ids": "node.assignees.role_ids",
3008
- "node.member_names": "node.assignees.member_names",
3009
- "node.member_emails": "node.assignees.member_emails",
3010
- "node.member_uids": "node.assignees.member_uids",
3011
- "node.editable_fields": "node.permissions.editable_fields",
3012
- "default_approval": "basic_approval",
3013
- },
3014
- "allowed_values": {
3015
- "mode": ["replace"],
3016
- "preset": [member.value for member in FlowPreset],
3017
- "node.type": PUBLIC_STABLE_FLOW_NODE_TYPES,
4962
+ "batch_example": {
4963
+ "profile": "default",
4964
+ "publish": True,
4965
+ "apps": [
4966
+ {"app_key": "APP_KEY_1", "mode": "merge", "sections": [{"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["字段A", "字段B"]]}]},
4967
+ {"app_key": "APP_KEY_2", "mode": "merge", "sections": [{"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["字段C", "字段D"]]}]},
4968
+ ],
3018
4969
  },
3019
- "dependency_hints": [
3020
- "approval-style workflows require an explicit status field",
3021
- "approve/fill/copy nodes require at least one assignee",
4970
+ },
4971
+ "app_flow_get_schema": {
4972
+ "allowed_keys": ["schema_version"],
4973
+ "aliases": {"schemaVersion": "schema_version"},
4974
+ "allowed_values": {},
4975
+ "execution_notes": [
4976
+ "returns WorkflowSpec JSON Schema from /workflow/spec/schema",
4977
+ "call this before authoring a new spec or validating spec shape",
4978
+ "worksheet-level approval deduplication toggles (legacy global settings) are not yet part of WorkflowSpec",
3022
4979
  ],
4980
+ "minimal_example": {
4981
+ "profile": "default",
4982
+ "schema_version": "vnext-2026-06",
4983
+ },
4984
+ },
4985
+ "app_flow_get": {
4986
+ "allowed_keys": ["app_key", "version_id"],
4987
+ "aliases": {"versionId": "version_id"},
4988
+ "allowed_values": {},
3023
4989
  "execution_notes": [
3024
- "public flow building is intentionally limited to linear workflows",
3025
- "branch and condition nodes are disabled because the backend workflow route is not front-end stable for these node types",
4990
+ "returns current WorkflowSpecDTO for one app via GET /workflow/spec",
4991
+ "use GET-first before patching and app_flow_apply",
3026
4992
  ],
3027
4993
  "minimal_example": {
3028
4994
  "profile": "default",
3029
4995
  "app_key": "APP_KEY",
3030
- "mode": "replace",
3031
- "preset": "basic_approval",
3032
- "nodes": [
3033
- {
3034
- "id": "approve_1",
3035
- "type": "approve",
3036
- "name": "部门审批",
3037
- "assignees": {"role_names": ["项目经理"]},
3038
- "permissions": {"editable_fields": ["状态", "审批意见"]},
3039
- }
3040
- ],
3041
- "transitions": [],
3042
4996
  },
3043
4997
  },
3044
4998
  "app_flow_apply": {
3045
- "allowed_keys": ["app_key", "mode", "publish", "nodes", "transitions"],
4999
+ "allowed_keys": ["app_key", "publish", "spec", "idempotency_key", "schema_version", "patch_nodes"],
3046
5000
  "aliases": {
3047
- "overwrite": "replace",
3048
- "node.role_names": "node.assignees.role_names",
3049
- "node.role_ids": "node.assignees.role_ids",
3050
- "node.member_names": "node.assignees.member_names",
3051
- "node.member_emails": "node.assignees.member_emails",
3052
- "node.member_uids": "node.assignees.member_uids",
3053
- "node.editable_fields": "node.permissions.editable_fields",
3054
- },
3055
- "allowed_values": {
3056
- "mode": ["replace"],
3057
- "node.type": PUBLIC_STABLE_FLOW_NODE_TYPES,
5001
+ "schemaVersion": "schema_version",
5002
+ "idempotencyKey": "idempotency_key",
3058
5003
  },
5004
+ "allowed_values": {},
3059
5005
  "dependency_hints": [
3060
- "approval-style workflows require an explicit status field",
3061
- "approve/fill/copy nodes require at least one assignee",
5006
+ "when using spec: must be a complete WorkflowSpecDTO object (replace-only apply); when using patch_nodes[]: spec is not required",
5007
+ "use patch_nodes[] instead of spec when updating only specific nodes; patch_nodes reads current flow, merges set/unset, then writes back — spec is not needed when patch_nodes is supplied",
3062
5008
  ],
3063
5009
  "execution_notes": [
3064
- "public flow building is intentionally limited to linear workflows",
3065
- "branch and condition nodes are disabled because the backend workflow route is not front-end stable for these node types",
3066
- "workflow verification only covers linear node structure in the public tool surface",
5010
+ "posts to /workflow/spec:apply with appKey, idempotencyKey, schemaVersion, and spec",
5011
+ "verification uses appliedSpec, diffSummary, and semanticLint from the apply response",
5012
+ "publish=false keeps changes in draft when supported by the backend",
5013
+ "patch_nodes[] items are {id, set, unset}; id must match an existing node id from app_get_flow.spec.nodes; FLOW_NODE_NOT_FOUND is returned if id is missing",
3067
5014
  ],
3068
5015
  "minimal_example": {
3069
5016
  "profile": "default",
3070
5017
  "app_key": "APP_KEY",
3071
- "mode": "replace",
3072
5018
  "publish": True,
3073
- "nodes": [
3074
- {"id": "start", "type": "start", "name": "发起"},
3075
- {
3076
- "id": "approve_1",
3077
- "type": "approve",
3078
- "name": "部门审批",
3079
- "assignees": {"role_names": ["项目经理"]},
3080
- "permissions": {"editable_fields": ["状态", "审批意见"]},
3081
- },
3082
- {"id": "end", "type": "end", "name": "结束"},
3083
- ],
3084
- "transitions": [{"from": "start", "to": "approve_1"}, {"from": "approve_1", "to": "end"}],
5019
+ "spec": {
5020
+ "nodes": [{"id": "n1", "type": "APPLICANT", "name": "发起"}],
5021
+ "transitions": [],
5022
+ },
5023
+ },
5024
+ "patch_nodes_example": {
5025
+ "profile": "default",
5026
+ "app_key": "APP_KEY",
5027
+ "publish": True,
5028
+ "patch_nodes": [{"id": "approve_1", "set": {"name": "总监审批", "assignees": {"role_names": ["总监"]}}}],
3085
5029
  },
3086
5030
  },
3087
5031
  "app_views_plan": {
3088
- "allowed_keys": ["app_key", "upsert_views", "remove_views", "preset", "upsert_views[].view_key", "upsert_views[].buttons", "upsert_views[].visibility"],
5032
+ "allowed_keys": [
5033
+ "app_key",
5034
+ "upsert_views",
5035
+ "patch_views",
5036
+ "remove_views",
5037
+ "preset",
5038
+ "upsert_views[].view_key",
5039
+ "upsert_views[].name",
5040
+ "upsert_views[].type",
5041
+ "upsert_views[].columns",
5042
+ "upsert_views[].filters",
5043
+ "upsert_views[].buttons",
5044
+ "upsert_views[].visibility",
5045
+ "upsert_views[].query_conditions",
5046
+ "patch_views[].view_key",
5047
+ "patch_views[].name",
5048
+ "patch_views[].set",
5049
+ "patch_views[].unset",
5050
+ ],
3089
5051
  "aliases": {
3090
5052
  "fields": "columns",
3091
5053
  "column_names": "columns",
@@ -3096,6 +5058,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
3096
5058
  "kanban": "board",
3097
5059
  "filter_rules": "filters",
3098
5060
  "filterRules": "filters",
5061
+ "queryConditions": "query_conditions",
5062
+ "query_condition": "query_conditions",
5063
+ "queryCondition": "query_conditions",
5064
+ "patchViews": "patch_views",
3099
5065
  "startField": "start_field",
3100
5066
  "endField": "end_field",
3101
5067
  "titleField": "title_field",
@@ -3113,11 +5079,18 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
3113
5079
  "view.type": [member.value for member in PublicViewType],
3114
5080
  "view.filter.operator": [member.value for member in ViewFilterOperator],
3115
5081
  "view.buttons.button_type": ["SYSTEM", "CUSTOM"],
3116
- "view.buttons.config_type": ["TOP", "DETAIL"],
5082
+ "view.buttons.config_type": ["TOP", "DETAIL", "INSIDE"],
3117
5083
  **deepcopy(_VISIBILITY_ALLOWED_VALUES),
3118
5084
  },
3119
5085
  "execution_notes": [
3120
5086
  "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",
5087
+ "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",
5088
+ "upsert_views[].query_conditions.rows is a layout matrix of field names; it is compiled to backend queryCondition queIds",
5089
+ "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",
5090
+ "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",
5091
+ "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",
5092
+ "new views created by app_views_apply default associated report/view display to visible with limit_type=all; existing views preserve their current associated display unless patch_views/upsert_views explicitly changes associated_resources",
5093
+ "associated report/view resource pool and per-view selected resources are configured through app_associated_resources_apply; app_views_apply only keeps legacy associated_resources input compatible",
3121
5094
  "for multi-value operators such as in, pass values as a list; value may also be used as an alias when it already contains a list",
3122
5095
  *_VISIBILITY_EXECUTION_NOTES,
3123
5096
  ],
@@ -3127,6 +5100,41 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
3127
5100
  "upsert_views": [{"name": "全部数据", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
3128
5101
  "remove_views": [],
3129
5102
  },
5103
+ "query_conditions_example": {
5104
+ "profile": "default",
5105
+ "app_key": "APP_KEY",
5106
+ "patch_views": [
5107
+ {
5108
+ "view_key": "VIEW_KEY",
5109
+ "set": {
5110
+ "query_conditions": {
5111
+ "enabled": True,
5112
+ "rows": [["客户名称", "负责人"], ["创建时间"]],
5113
+ }
5114
+ },
5115
+ }
5116
+ ],
5117
+ "remove_views": [],
5118
+ },
5119
+ "full_upsert_query_conditions_example": {
5120
+ "profile": "default",
5121
+ "app_key": "APP_KEY",
5122
+ "upsert_views": [
5123
+ {
5124
+ "name": "客户查询视图",
5125
+ "type": "table",
5126
+ "columns": ["客户名称", "负责人", "客户状态", "创建时间"],
5127
+ "filters": [{"field_name": "客户状态", "operator": "eq", "value": "有效"}],
5128
+ "query_conditions": {
5129
+ "enabled": True,
5130
+ "exact": False,
5131
+ "hide_before_query": False,
5132
+ "rows": [["客户名称", "负责人"], ["创建时间"]],
5133
+ },
5134
+ }
5135
+ ],
5136
+ "remove_views": [],
5137
+ },
3130
5138
  "gantt_example": {
3131
5139
  "profile": "default",
3132
5140
  "app_key": "APP_KEY",
@@ -3145,7 +5153,26 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
3145
5153
  },
3146
5154
  },
3147
5155
  "app_views_apply": {
3148
- "allowed_keys": ["app_key", "publish", "upsert_views", "remove_views", "upsert_views[].view_key", "upsert_views[].buttons", "upsert_views[].visibility"],
5156
+ "allowed_keys": [
5157
+ "app_key",
5158
+ "apps",
5159
+ "publish",
5160
+ "upsert_views",
5161
+ "patch_views",
5162
+ "remove_views",
5163
+ "upsert_views[].view_key",
5164
+ "upsert_views[].name",
5165
+ "upsert_views[].type",
5166
+ "upsert_views[].columns",
5167
+ "upsert_views[].filters",
5168
+ "upsert_views[].buttons",
5169
+ "upsert_views[].visibility",
5170
+ "upsert_views[].query_conditions",
5171
+ "patch_views[].view_key",
5172
+ "patch_views[].name",
5173
+ "patch_views[].set",
5174
+ "patch_views[].unset",
5175
+ ],
3149
5176
  "aliases": {
3150
5177
  "fields": "columns",
3151
5178
  "column_names": "columns",
@@ -3156,6 +5183,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
3156
5183
  "kanban": "board",
3157
5184
  "filter_rules": "filters",
3158
5185
  "filterRules": "filters",
5186
+ "queryConditions": "query_conditions",
5187
+ "query_condition": "query_conditions",
5188
+ "queryCondition": "query_conditions",
5189
+ "patchViews": "patch_views",
3159
5190
  "startField": "start_field",
3160
5191
  "endField": "end_field",
3161
5192
  "titleField": "title_field",
@@ -3172,17 +5203,25 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
3172
5203
  "view.type": [member.value for member in PublicViewType],
3173
5204
  "view.filter.operator": [member.value for member in ViewFilterOperator],
3174
5205
  "view.buttons.button_type": ["SYSTEM", "CUSTOM"],
3175
- "view.buttons.config_type": ["TOP", "DETAIL"],
5206
+ "view.buttons.config_type": ["TOP", "DETAIL", "INSIDE"],
3176
5207
  **deepcopy(_VISIBILITY_ALLOWED_VALUES),
3177
5208
  },
3178
5209
  "execution_notes": [
3179
5210
  "apply may return partial_success when some views land and others fail",
3180
5211
  "when duplicate view names exist, supply view_key to target the exact view",
3181
- "read back app_get_views after any failed or partial view apply",
5212
+ "read back app_get after any failed or partial view apply",
3182
5213
  "view existence verification and saved-filter verification are separate; treat filters as unverified until verification.view_filters_verified is true",
3183
5214
  "buttons omitted preserves existing button config; buttons=[] clears all buttons; buttons=[...] replaces the full button config",
3184
5215
  "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",
5216
+ "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",
5217
+ "upsert_views[].query_conditions.rows is a layout matrix of field names; it is compiled to backend queryCondition queIds",
5218
+ "use patch_views for partial parameter replacement on existing views; the public update mode is patch even though the backend save is still a full view payload",
5219
+ "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",
5220
+ "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",
5221
+ "new views created by app_views_apply default associated report/view display to visible with limit_type=all; existing views preserve their current associated display unless patch_views/upsert_views explicitly changes associated_resources",
5222
+ "associated report/view resource pool and per-view selected resources are configured through app_associated_resources_apply; app_views_apply keeps legacy associated_resources input compatible but it is no longer the recommended public contract",
3185
5223
  "for multi-value operators such as in, pass values as a list; value may also be used as an alias when it already contains a list",
5224
+ "accepts apps[] for multi-app batch; each item is {app_key, upsert_views?, patch_views?, remove_views?, publish?}; top-level publish is used as default for items that omit publish",
3186
5225
  *_VISIBILITY_EXECUTION_NOTES,
3187
5226
  ],
3188
5227
  "minimal_example": {
@@ -3192,6 +5231,43 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
3192
5231
  "upsert_views": [{"name": "全部数据", "type": "table", "columns": ["项目名称"], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
3193
5232
  "remove_views": [],
3194
5233
  },
5234
+ "query_conditions_example": {
5235
+ "profile": "default",
5236
+ "app_key": "APP_KEY",
5237
+ "publish": True,
5238
+ "patch_views": [
5239
+ {
5240
+ "view_key": "VIEW_KEY",
5241
+ "set": {
5242
+ "query_conditions": {
5243
+ "enabled": True,
5244
+ "rows": [["客户名称", "负责人"], ["创建时间"]],
5245
+ }
5246
+ },
5247
+ }
5248
+ ],
5249
+ "remove_views": [],
5250
+ },
5251
+ "full_upsert_query_conditions_example": {
5252
+ "profile": "default",
5253
+ "app_key": "APP_KEY",
5254
+ "publish": True,
5255
+ "upsert_views": [
5256
+ {
5257
+ "name": "客户查询视图",
5258
+ "type": "table",
5259
+ "columns": ["客户名称", "负责人", "客户状态", "创建时间"],
5260
+ "filters": [{"field_name": "客户状态", "operator": "eq", "value": "有效"}],
5261
+ "query_conditions": {
5262
+ "enabled": True,
5263
+ "exact": False,
5264
+ "hide_before_query": False,
5265
+ "rows": [["客户名称", "负责人"], ["创建时间"]],
5266
+ },
5267
+ }
5268
+ ],
5269
+ "remove_views": [],
5270
+ },
3195
5271
  "gantt_example": {
3196
5272
  "profile": "default",
3197
5273
  "app_key": "APP_KEY",
@@ -3215,12 +5291,14 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
3215
5291
  "aliases": {},
3216
5292
  "allowed_values": {},
3217
5293
  "execution_notes": [
3218
- "returns builder-side app configuration summary and editability",
3219
- "use this as the default builder discovery read before fields/layout/views/flow/charts detail reads",
5294
+ "returns builder-side app map: base summary, editability, field/view/chart/button counts, compact views, compact charts, custom_buttons, and app-level associated_resources",
5295
+ "use this as the default builder discovery read before view_get/chart_get/apply detail work",
3220
5296
  "editability is route-aware builder capability summary, not end-user data visibility",
3221
5297
  "can_edit_app_base covers app base-info writes such as app_name, icon, and visibility",
3222
5298
  "can_edit_form covers form/schema routes only and does not imply app base-info writes",
3223
5299
  "returns normalized app visibility when backend auth is readable",
5300
+ "custom_buttons[].button_id is the id required by app_custom_buttons_apply view_configs[].buttons[].button_ref",
5301
+ "associated_resources[].associated_item_id is the internal id; app_associated_resources_apply.view_configs/remove/reorder may also pass an existing resource's chart_id/chart_key/view_key",
3224
5302
  ],
3225
5303
  "minimal_example": {
3226
5304
  "profile": "default",
@@ -3228,13 +5306,16 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
3228
5306
  },
3229
5307
  },
3230
5308
  "app_get_fields": {
3231
- "allowed_keys": ["app_key"],
5309
+ "allowed_keys": ["app_key", "app_keys"],
3232
5310
  "aliases": {},
3233
5311
  "allowed_values": {},
3234
5312
  "execution_notes": [
3235
5313
  "returns compact current field configuration for one app",
3236
5314
  "use this before app_schema_apply when you need exact field definitions",
5315
+ "also returns chart_fields from QingBI datasource fields; app_charts_apply field selectors should use chart_fields because record/schema-visible fields and QingBI fields are not the same schema",
5316
+ "chart_fields[].field_id supports field_<queId> selectors, while chart_fields[].bi_field_id is the raw QingBI fieldId accepted by report configs",
3237
5317
  "subtable fields include nested subfields using the same compact field shape",
5318
+ "accepts app_keys[] for batch read; batch returns {status, apps[], errors[]} where each apps[] item has {app_key, fields}",
3238
5319
  ],
3239
5320
  "minimal_example": {
3240
5321
  "profile": "default",
@@ -3242,12 +5323,13 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
3242
5323
  },
3243
5324
  },
3244
5325
  "app_get_layout": {
3245
- "allowed_keys": ["app_key"],
5326
+ "allowed_keys": ["app_key", "app_keys"],
3246
5327
  "aliases": {},
3247
5328
  "allowed_values": {},
3248
5329
  "execution_notes": [
3249
5330
  "returns compact current layout configuration for one app",
3250
5331
  "use this before app_layout_apply when you need paragraph and row structure",
5332
+ "accepts app_keys[] for batch read; batch returns {status, apps[], errors[]} where each apps[] item has {app_key, sections}",
3251
5333
  ],
3252
5334
  "minimal_example": {
3253
5335
  "profile": "default",
@@ -3255,13 +5337,15 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
3255
5337
  },
3256
5338
  },
3257
5339
  "app_get_views": {
3258
- "allowed_keys": ["app_key"],
5340
+ "allowed_keys": ["app_key", "app_keys"],
3259
5341
  "aliases": {},
3260
5342
  "allowed_values": {},
3261
5343
  "execution_notes": [
3262
5344
  "returns compact current view inventory for one app",
3263
- "use this before app_views_apply when you need exact current view keys",
5345
+ "compatibility/specialized inventory tool; default builder discovery should start with app_get",
5346
+ "use this before app_views_apply only when you need an exact current view inventory beyond app_get",
3264
5347
  "view items include visibility_summary when backend view auth is readable",
5348
+ "accepts app_keys[] for batch read; batch returns {status, apps[], errors[]} where each apps[] item has {app_key, views}",
3265
5349
  ],
3266
5350
  "minimal_example": {
3267
5351
  "profile": "default",
@@ -3269,12 +5353,13 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
3269
5353
  },
3270
5354
  },
3271
5355
  "app_get_flow": {
3272
- "allowed_keys": ["app_key"],
5356
+ "allowed_keys": ["app_key", "app_keys"],
3273
5357
  "aliases": {},
3274
5358
  "allowed_values": {},
3275
5359
  "execution_notes": [
3276
- "returns workflow configuration summary for one app",
3277
- "use this before app_flow_apply when you need the current node structure",
5360
+ "returns WorkflowSpecDTO for one app (alias of app_flow_get)",
5361
+ "use app_flow_get_schema then app_flow_get before app_flow_apply",
5362
+ "accepts app_keys[] for batch read; batch returns {status, apps[], errors[]} where each apps[] item has {app_key, spec}",
3278
5363
  ],
3279
5364
  "minimal_example": {
3280
5365
  "profile": "default",
@@ -3282,14 +5367,46 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
3282
5367
  },
3283
5368
  },
3284
5369
  "app_get_charts": {
3285
- "allowed_keys": ["app_key"],
5370
+ "allowed_keys": ["app_key", "app_keys"],
3286
5371
  "aliases": {},
3287
5372
  "allowed_values": {},
3288
5373
  "execution_notes": [
3289
5374
  "returns a compact current chart inventory for one app",
3290
- "use this before app_charts_apply when you need exact current chart_id values",
5375
+ "compatibility/specialized inventory tool; default builder discovery should start with app_get",
5376
+ "use this before app_charts_apply when you need exact current chart_id values beyond the app_get summary",
3291
5377
  "chart summaries do not include full qingbi config payloads",
3292
5378
  "chart items include visibility_summary when QingBI base info is readable",
5379
+ "accepts app_keys[] for batch read; batch returns {status, apps[], errors[]} where each apps[] item has {app_key, charts}",
5380
+ ],
5381
+ "minimal_example": {
5382
+ "profile": "default",
5383
+ "app_key": "APP_KEY",
5384
+ },
5385
+ },
5386
+ "app_get_buttons": {
5387
+ "allowed_keys": ["app_key", "app_keys"],
5388
+ "aliases": {},
5389
+ "allowed_values": {},
5390
+ "execution_notes": [
5391
+ "returns custom button list (draft state) for one app",
5392
+ "also returns view_configs read from view bindings, so app_custom_buttons_apply view_configs can be patched from the same read result",
5393
+ "accepts app_keys[] for batch read; batch returns {status, apps[], errors[]} where each apps[] item preserves the single-read fields such as {app_key, buttons, view_configs, warnings, verification}",
5394
+ "use before app_custom_buttons_apply when you need current button_id values",
5395
+ ],
5396
+ "minimal_example": {
5397
+ "profile": "default",
5398
+ "app_key": "APP_KEY",
5399
+ },
5400
+ },
5401
+ "app_get_associated_resources": {
5402
+ "allowed_keys": ["app_key", "app_keys"],
5403
+ "aliases": {},
5404
+ "allowed_values": {},
5405
+ "execution_notes": [
5406
+ "returns associated resource pool (draft state) for one app",
5407
+ "also returns view_configs read from view bindings, so app_associated_resources_apply view_configs can be patched from the same read result",
5408
+ "accepts app_keys[] for batch read; batch returns {status, apps[], errors[]} where each apps[] item preserves the single-read fields such as {app_key, associated_resources, view_configs, warnings, verification}",
5409
+ "use before app_associated_resources_apply when you need current associated_item_id values",
3293
5410
  ],
3294
5411
  "minimal_example": {
3295
5412
  "profile": "default",
@@ -3326,8 +5443,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
3326
5443
  },
3327
5444
  },
3328
5445
  "app_charts_apply": {
3329
- "allowed_keys": ["app_key", "upsert_charts", "remove_chart_ids", "reorder_chart_ids", "upsert_charts[].visibility"],
5446
+ "allowed_keys": ["app_key", "apps", "upsert_charts", "patch_charts", "remove_chart_ids", "reorder_chart_ids", "upsert_charts[].visibility", "patch_charts[].chart_id", "patch_charts[].name", "patch_charts[].set", "patch_charts[].unset"],
3330
5447
  "aliases": {
5448
+ "patchCharts": "patch_charts",
3331
5449
  "chart.id": "chart.chart_id",
3332
5450
  "chart.type": "chart.chart_type",
3333
5451
  "chart.dimension_fields": "chart.dimension_field_ids",
@@ -3341,18 +5459,31 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
3341
5459
  **deepcopy(_VISIBILITY_ALLOWED_VALUES),
3342
5460
  },
3343
5461
  "execution_notes": [
5462
+ "this tool manages QingBI report bodies/configs; it does not attach reports to Qingflow app associated-resource display",
5463
+ "app_charts_apply creates/updates app-source QingBI reports only; generated payloads use dataSourceType=qingflow",
5464
+ "dataset BI reports are not created or edited by this tool yet; create them in QingBI first, then attach the existing report with app_associated_resources_apply report_source=dataset",
5465
+ "after creating or updating an app-source report body, use app_associated_resources_apply when the report should appear inside a Qingflow app/view",
3344
5466
  "app_charts_apply is immediate-live and does not publish",
5467
+ "use patch_charts for partial parameter replacement on existing charts; the tool reads current chart base/config, merges patch_charts[].set/unset, then submits full QingBI base/config payloads internally",
3345
5468
  "chart matching precedence is chart_id first, then exact unique chart name",
3346
5469
  "when chart names are not unique, supply chart_id instead of guessing by name",
3347
5470
  "successful create results must return a real backend chart_id",
3348
5471
  "upsert_charts[].visibility compiles to QingBI base visibleAuth only",
3349
5472
  "visibility-only updates keep the existing chart config and do not rewrite rawDataConfigDTO.authInfo",
5473
+ "chart dimension/metric/filter/query fields are resolved from app_get_fields.chart_fields (QingBI datasource fields), not record schema or form-only fields",
5474
+ "system fields such as 申请人/申请时间/编号 are usable only when they appear in chart_fields; otherwise app_charts_apply returns CHART_FIELD_NOT_IN_QINGBI_SCHEMA",
5475
+ "low-frequency chart types have local prevalidation: gauge requires 0 dimensions and 2 non-duplicated metrics; histogram requires at most 1 dimension and exactly 1 plain numeric metric",
5476
+ "chart rule failures return chart_results[].diagnostics with rule_code, expected, actual, offending_fields, and next_action; backend 81002/81005 are translated when possible",
5477
+ "remove_chart_ids deletes by chart_id and verifies each deleted chart with single chart_id readback; pure delete does not read the full chart list",
5478
+ "if delete readback is unavailable or still finds the chart, chart_results[] returns delete_executed=true, readback_status, and safe_to_retry_delete=false; do not blindly repeat delete",
5479
+ "accepts apps[] for multi-app batch; each item is {app_key, upsert_charts?, patch_charts?, remove_chart_ids?, reorder_chart_ids?}",
3350
5480
  *_VISIBILITY_EXECUTION_NOTES,
3351
5481
  ],
3352
5482
  "minimal_example": {
3353
5483
  "profile": "default",
3354
5484
  "app_key": "APP_KEY",
3355
5485
  "upsert_charts": [{"name": "数据总量", "chart_type": "target", "indicator_field_ids": [], "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE)}],
5486
+ "patch_charts": [{"chart_id": "CHART_ID", "set": {"name": "数据总量-新版"}}],
3356
5487
  "remove_chart_ids": [],
3357
5488
  "reorder_chart_ids": [],
3358
5489
  },
@@ -3364,6 +5495,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
3364
5495
  "execution_notes": [
3365
5496
  "returns one builder-side view definition detail",
3366
5497
  "does not return record data; use user-side view_get or record_list for runtime rows",
5498
+ "view_key is a raw builder view key; if a record-data custom:VIEW_KEY value is passed, the tool normalizes it to VIEW_KEY",
3367
5499
  "use this after builder portal_get when a component references a view_ref.view_key",
3368
5500
  "returns normalized view visibility when backend auth is readable",
3369
5501
  ],
@@ -3388,9 +5520,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
3388
5520
  },
3389
5521
  },
3390
5522
  "portal_apply": {
3391
- "allowed_keys": ["dash_key", "dash_name", "package_id", "publish", "sections", "visibility", "auth", "icon", "color", "hide_copyright", "dash_global_config", "config"],
5523
+ "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"],
3392
5524
  "aliases": {
3393
5525
  "packageId": "package_id",
5526
+ "name": "dash_name",
3394
5527
  "sourceType": "source_type",
3395
5528
  "chartRef": "chart_ref",
3396
5529
  "viewRef": "view_ref",
@@ -3403,19 +5536,35 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
3403
5536
  "viewRef": "view_ref",
3404
5537
  "dashStyleConfigBO": "dash_style_config",
3405
5538
  },
3406
- "allowed_values": {"section.source_type": ["chart", "view", "grid", "filter", "text", "link"], **deepcopy(_VISIBILITY_ALLOWED_VALUES)},
3407
- "execution_notes": [
3408
- "use exactly one resource mode",
3409
- "update mode: dash_key",
3410
- "create mode: package_id + dash_name",
3411
- "portal_apply uses replace semantics for sections",
3412
- "when editing an existing portal, sections may be omitted to update only base info such as visibility, icon, or package",
3413
- "remove a section by omitting it from the new sections list",
3414
- "package_id is required when creating a new portal",
3415
- "publish=false only guarantees draft and base-info updates; it does not claim live has changed",
3416
- "chart_ref resolves by chart_id first, then exact unique chart_name",
5539
+ "allowed_values": {
5540
+ "section.source_type": ["chart", "view", "grid", "filter", "text", "link"],
5541
+ "workspace_icon.icon_names": list(WORKSPACE_ICON_NAMES),
5542
+ "workspace_icon.icon_colors": list(WORKSPACE_ICON_COLORS),
5543
+ "workspace_icon.generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
5544
+ **deepcopy(_VISIBILITY_ALLOWED_VALUES),
5545
+ },
5546
+ "execution_notes": [
5547
+ "use exactly one resource mode",
5548
+ "update mode: dash_key",
5549
+ "create mode: package_id + dash_name",
5550
+ "create mode requires explicit icon + color; icon=template is blocked because it is too generic",
5551
+ "edit mode preserves existing icon/color when omitted; explicit icon/color values are still validated",
5552
+ "call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
5553
+ "portal_apply uses replace semantics for sections",
5554
+ "when editing an existing portal, sections may be omitted to update only base info such as visibility, icon, or package",
5555
+ "use patch_sections[] for targeted section updates without replacing all sections; each item needs one selector (chart_ref with chart_id/chart_key/chart_name, view_ref with view_key/view_name, or order as 0-based index) plus set/unset",
5556
+ "when sections[] is supplied without patch_sections[], it uses replace semantics for all sections",
5557
+ "remove a section by omitting it from the sections list (replace mode) or by unset in patch_sections (patch mode)",
5558
+ "package_id is required when creating a new portal",
5559
+ "publish=false only guarantees draft and base-info updates; it does not claim live has changed",
5560
+ "chart_ref resolves by chart_id/chart_key first, then exact unique chart_name",
3417
5561
  "view_ref resolves by view_key first, then exact unique view_name",
5562
+ "pc layout uses a 24-column grid; mobile layout uses a 6-column grid",
5563
+ "if unsure about layout, omit position or use layout_preset=auto/dashboard_2col/dashboard_3col",
5564
+ "two-column pc layout should use x=0/12 with cols=12; three-column pc layout should use x=0/8/16 with cols=8",
5565
+ "x=0/6 with cols=6 only occupies the left half of the pc portal and triggers PORTAL_LAYOUT_HALF_WIDTH",
3418
5566
  "position.pc/mobile is the canonical portal layout shape",
5567
+ "compat payload accepts name -> dash_name and single pages[0].components -> sections",
3419
5568
  "visibility is the canonical public auth shape; auth is kept only as a deprecated compatibility alias",
3420
5569
  "passing visibility and auth together is rejected as VISIBILITY_CONFLICT",
3421
5570
  *_VISIBILITY_EXECUTION_NOTES,
@@ -3424,7 +5573,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
3424
5573
  "profile": "default",
3425
5574
  "dash_name": "经营门户",
3426
5575
  "package_id": 1001,
5576
+ "icon": "view-grid",
5577
+ "color": "blue",
3427
5578
  "publish": True,
5579
+ "layout_preset": "dashboard_2col",
3428
5580
  "visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
3429
5581
  "sections": [
3430
5582
  {
@@ -3438,6 +5590,23 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
3438
5590
  }
3439
5591
  ],
3440
5592
  },
5593
+ "compat_payload_example": {
5594
+ "name": "经营门户",
5595
+ "package_id": 1001,
5596
+ "layout_preset": "dashboard_2col",
5597
+ "pages": [
5598
+ {
5599
+ "title": "经营总览",
5600
+ "components": [
5601
+ {
5602
+ "title": "销售趋势",
5603
+ "source_type": "chart",
5604
+ "chart_ref": {"app_key": "APP_KEY", "chart_id": "CHART_ID"},
5605
+ }
5606
+ ],
5607
+ }
5608
+ ],
5609
+ },
3441
5610
  "minimal_section_example": {
3442
5611
  "title": "订单概览",
3443
5612
  "source_type": "view",
@@ -3447,6 +5616,15 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
3447
5616
  "mobile": {"x": 0, "y": 0, "cols": 6, "rows": 8},
3448
5617
  },
3449
5618
  },
5619
+ "patch_sections_example": {
5620
+ "profile": "default",
5621
+ "dash_key": "DASH_KEY",
5622
+ "publish": True,
5623
+ "patch_sections": [
5624
+ {"chart_ref": {"chart_id": "CHART_ID"}, "set": {"title": "销售总览-新版"}},
5625
+ {"order": 2, "set": {"title": "任务列表-新版"}},
5626
+ ],
5627
+ },
3450
5628
  },
3451
5629
  "app_publish_verify": {
3452
5630
  "allowed_keys": ["app_key", "expected_package_id"],
@@ -3485,7 +5663,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
3485
5663
  _PRIVATE_BUILDER_TOOL_CONTRACTS = {
3486
5664
  "app_schema_plan",
3487
5665
  "app_layout_plan",
3488
- "app_flow_plan",
3489
5666
  "app_views_plan",
3490
5667
  }
3491
5668