@josephyan/qingflow-app-builder-mcp 0.2.0-beta.7 → 0.2.0-beta.71

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 (70) hide show
  1. package/README.md +5 -3
  2. package/docs/local-agent-install.md +21 -5
  3. package/npm/bin/qingflow-app-builder-mcp.mjs +1 -1
  4. package/npm/lib/runtime.mjs +168 -12
  5. package/package.json +1 -1
  6. package/pyproject.toml +4 -1
  7. package/skills/qingflow-app-builder/SKILL.md +155 -22
  8. package/skills/qingflow-app-builder/references/create-app.md +51 -21
  9. package/skills/qingflow-app-builder/references/environments.md +1 -1
  10. package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
  11. package/skills/qingflow-app-builder/references/gotchas.md +28 -1
  12. package/skills/qingflow-app-builder/references/solution-playbooks.md +14 -12
  13. package/skills/qingflow-app-builder/references/tool-selection.md +47 -19
  14. package/skills/qingflow-app-builder/references/update-flow.md +112 -25
  15. package/skills/qingflow-app-builder/references/update-layout.md +11 -24
  16. package/skills/qingflow-app-builder/references/update-schema.md +1 -23
  17. package/skills/qingflow-app-builder/references/update-views.md +87 -21
  18. package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
  19. package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
  20. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
  21. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
  22. package/src/qingflow_mcp/__init__.py +1 -1
  23. package/src/qingflow_mcp/backend_client.py +210 -0
  24. package/src/qingflow_mcp/builder_facade/models.py +1252 -3
  25. package/src/qingflow_mcp/builder_facade/service.py +11367 -2389
  26. package/src/qingflow_mcp/cli/__init__.py +1 -0
  27. package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
  28. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  29. package/src/qingflow_mcp/cli/commands/auth.py +78 -0
  30. package/src/qingflow_mcp/cli/commands/builder.py +515 -0
  31. package/src/qingflow_mcp/cli/commands/common.py +62 -0
  32. package/src/qingflow_mcp/cli/commands/imports.py +96 -0
  33. package/src/qingflow_mcp/cli/commands/record.py +304 -0
  34. package/src/qingflow_mcp/cli/commands/task.py +89 -0
  35. package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
  36. package/src/qingflow_mcp/cli/context.py +48 -0
  37. package/src/qingflow_mcp/cli/formatters.py +355 -0
  38. package/src/qingflow_mcp/cli/json_io.py +50 -0
  39. package/src/qingflow_mcp/cli/main.py +149 -0
  40. package/src/qingflow_mcp/config.py +39 -0
  41. package/src/qingflow_mcp/import_store.py +121 -0
  42. package/src/qingflow_mcp/list_type_labels.py +24 -0
  43. package/src/qingflow_mcp/response_trim.py +668 -0
  44. package/src/qingflow_mcp/server.py +160 -18
  45. package/src/qingflow_mcp/server_app_builder.py +275 -68
  46. package/src/qingflow_mcp/server_app_user.py +219 -191
  47. package/src/qingflow_mcp/session_store.py +41 -1
  48. package/src/qingflow_mcp/solution/compiler/form_compiler.py +43 -4
  49. package/src/qingflow_mcp/solution/compiler/icon_utils.py +119 -45
  50. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +41 -2
  51. package/src/qingflow_mcp/solution/executor.py +107 -11
  52. package/src/qingflow_mcp/solution/spec_models.py +2 -0
  53. package/src/qingflow_mcp/tools/ai_builder_tools.py +2032 -127
  54. package/src/qingflow_mcp/tools/app_tools.py +419 -12
  55. package/src/qingflow_mcp/tools/approval_tools.py +571 -72
  56. package/src/qingflow_mcp/tools/auth_tools.py +398 -2
  57. package/src/qingflow_mcp/tools/code_block_tools.py +756 -0
  58. package/src/qingflow_mcp/tools/custom_button_tools.py +179 -0
  59. package/src/qingflow_mcp/tools/directory_tools.py +203 -31
  60. package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
  61. package/src/qingflow_mcp/tools/file_tools.py +1 -0
  62. package/src/qingflow_mcp/tools/import_tools.py +2150 -0
  63. package/src/qingflow_mcp/tools/package_tools.py +18 -4
  64. package/src/qingflow_mcp/tools/portal_tools.py +31 -0
  65. package/src/qingflow_mcp/tools/qingbi_report_tools.py +109 -7
  66. package/src/qingflow_mcp/tools/record_tools.py +9894 -1104
  67. package/src/qingflow_mcp/tools/solution_tools.py +115 -3
  68. package/src/qingflow_mcp/tools/task_context_tools.py +2040 -0
  69. package/src/qingflow_mcp/tools/task_tools.py +376 -225
  70. package/src/qingflow_mcp/tools/workspace_tools.py +163 -19
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from copy import deepcopy
3
4
  import json
5
+ import time
4
6
 
5
7
  from pydantic import ValidationError
6
8
 
@@ -8,36 +10,61 @@ from ..config import DEFAULT_PROFILE
8
10
  from ..errors import QingflowApiError
9
11
  from ..json_types import JSONObject
10
12
  from ..builder_facade.models import (
13
+ ChartApplyRequest,
14
+ CustomButtonPatch,
15
+ FIELD_TYPE_ID_ALIASES,
11
16
  FieldPatch,
12
17
  FieldRemovePatch,
13
18
  FieldUpdatePatch,
19
+ FlowPreset,
14
20
  FlowNodePatch,
15
21
  FlowPlanRequest,
16
22
  FlowTransitionPatch,
17
23
  LayoutApplyMode,
18
24
  LayoutPlanRequest,
25
+ LayoutPreset,
19
26
  LayoutSectionPatch,
27
+ PortalApplyRequest,
28
+ PublicButtonTriggerAction,
29
+ PublicFieldType,
30
+ PublicRelationMode,
31
+ PublicChartType,
32
+ PublicViewType,
20
33
  SchemaPlanRequest,
34
+ ViewFilterOperator,
21
35
  ViewUpsertPatch,
36
+ ViewsPreset,
22
37
  ViewsPlanRequest,
23
38
  )
24
- from ..builder_facade.service import AiBuilderFacade
39
+ from ..builder_facade.service import AiBuilderFacade, INTEGRATION_OUTPUT_TARGET_FIELD_TYPES
25
40
  from .app_tools import AppTools
26
41
  from .base import ToolBase
42
+ from .custom_button_tools import CustomButtonTools
43
+ from .directory_tools import DirectoryTools
27
44
  from .package_tools import PackageTools
45
+ from .portal_tools import PortalTools
46
+ from .qingbi_report_tools import QingbiReportTools
47
+ from .role_tools import RoleTools
28
48
  from .solution_tools import SolutionTools
29
49
  from .view_tools import ViewTools
30
50
  from .workflow_tools import WorkflowTools
31
51
 
52
+ PUBLIC_STABLE_FLOW_NODE_TYPES = ["start", "approve", "fill", "copy", "webhook", "end"]
53
+
32
54
 
33
55
  class AiBuilderTools(ToolBase):
34
56
  def __init__(self, sessions, backend) -> None:
35
57
  super().__init__(sessions, backend)
36
58
  self._facade = AiBuilderFacade(
37
59
  apps=AppTools(sessions, backend),
60
+ buttons=CustomButtonTools(sessions, backend),
38
61
  packages=PackageTools(sessions, backend),
39
62
  views=ViewTools(sessions, backend),
40
63
  workflows=WorkflowTools(sessions, backend),
64
+ portals=PortalTools(sessions, backend),
65
+ charts=QingbiReportTools(sessions, backend),
66
+ roles=RoleTools(sessions, backend),
67
+ directory=DirectoryTools(sessions, backend),
41
68
  solutions=SolutionTools(sessions, backend),
42
69
  )
43
70
 
@@ -50,14 +77,69 @@ class AiBuilderTools(ToolBase):
50
77
  def package_resolve(profile: str = DEFAULT_PROFILE, package_name: str = "") -> JSONObject:
51
78
  return self.package_resolve(profile=profile, package_name=package_name)
52
79
 
80
+ @mcp.tool()
81
+ def builder_tool_contract(tool_name: str = "") -> JSONObject:
82
+ return self.builder_tool_contract(tool_name=tool_name)
83
+
84
+ @mcp.tool()
85
+ def package_create(
86
+ profile: str = DEFAULT_PROFILE,
87
+ package_name: str = "",
88
+ icon: str | None = None,
89
+ color: str | None = None,
90
+ ) -> JSONObject:
91
+ return self.package_create(profile=profile, package_name=package_name, icon=icon, color=color)
92
+
93
+ @mcp.tool()
94
+ def member_search(
95
+ profile: str = DEFAULT_PROFILE,
96
+ query: str = "",
97
+ page_num: int = 1,
98
+ page_size: int = 20,
99
+ contain_disable: bool = False,
100
+ ) -> JSONObject:
101
+ return self.member_search(
102
+ profile=profile,
103
+ query=query,
104
+ page_num=page_num,
105
+ page_size=page_size,
106
+ contain_disable=contain_disable,
107
+ )
108
+
109
+ @mcp.tool()
110
+ def role_search(
111
+ profile: str = DEFAULT_PROFILE,
112
+ keyword: str = "",
113
+ page_num: int = 1,
114
+ page_size: int = 20,
115
+ ) -> JSONObject:
116
+ return self.role_search(profile=profile, keyword=keyword, page_num=page_num, page_size=page_size)
117
+
118
+ @mcp.tool()
119
+ def role_create(
120
+ profile: str = DEFAULT_PROFILE,
121
+ role_name: str = "",
122
+ member_uids: list[int] | None = None,
123
+ member_emails: list[str] | None = None,
124
+ member_names: list[str] | None = None,
125
+ role_icon: str = "ex-user-outlined",
126
+ ) -> JSONObject:
127
+ return self.role_create(
128
+ profile=profile,
129
+ role_name=role_name,
130
+ member_uids=member_uids or [],
131
+ member_emails=member_emails or [],
132
+ member_names=member_names or [],
133
+ role_icon=role_icon,
134
+ )
135
+
53
136
  @mcp.tool()
54
137
  def package_attach_app(
55
138
  profile: str = DEFAULT_PROFILE,
56
139
  tag_id: int = 0,
57
140
  app_key: str = "",
58
- app_title: str = "",
59
141
  ) -> JSONObject:
60
- return self.package_attach_app(profile=profile, tag_id=tag_id, app_key=app_key, app_title=app_title)
142
+ return self.package_attach_app(profile=profile, tag_id=tag_id, app_key=app_key)
61
143
 
62
144
  @mcp.tool()
63
145
  def app_release_edit_lock_if_mine(
@@ -80,8 +162,52 @@ class AiBuilderTools(ToolBase):
80
162
  app_name: str = "",
81
163
  package_tag_id: int | None = None,
82
164
  ) -> JSONObject:
165
+ has_app_key = bool((app_key or "").strip())
166
+ has_app_name = bool((app_name or "").strip())
167
+ has_package_tag_id = package_tag_id is not None
168
+ if has_app_key and (has_app_name or has_package_tag_id):
169
+ return _config_failure(
170
+ tool_name="app_resolve",
171
+ message="app_resolve accepts exactly one selector mode.",
172
+ fix_hint="Use only `app_key`, or use `app_name` together with `package_tag_id`.",
173
+ )
174
+ if not has_app_key and not (has_app_name and has_package_tag_id):
175
+ return _config_failure(
176
+ tool_name="app_resolve",
177
+ message="app_resolve requires either app_key, or app_name together with package_tag_id.",
178
+ fix_hint="For an existing known app, pass `app_key`. For package-scoped lookup, pass both `app_name` and `package_tag_id`.",
179
+ )
83
180
  return self.app_resolve(profile=profile, app_key=app_key, app_name=app_name, package_tag_id=package_tag_id)
84
181
 
182
+ @mcp.tool()
183
+ def app_custom_button_list(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
184
+ return self.app_custom_button_list(profile=profile, app_key=app_key)
185
+
186
+ @mcp.tool()
187
+ def app_custom_button_get(profile: str = DEFAULT_PROFILE, app_key: str = "", button_id: int = 0) -> JSONObject:
188
+ return self.app_custom_button_get(profile=profile, app_key=app_key, button_id=button_id)
189
+
190
+ @mcp.tool()
191
+ def app_custom_button_create(
192
+ profile: str = DEFAULT_PROFILE,
193
+ app_key: str = "",
194
+ payload: JSONObject | None = None,
195
+ ) -> JSONObject:
196
+ return self.app_custom_button_create(profile=profile, app_key=app_key, payload=payload or {})
197
+
198
+ @mcp.tool()
199
+ def app_custom_button_update(
200
+ profile: str = DEFAULT_PROFILE,
201
+ app_key: str = "",
202
+ button_id: int = 0,
203
+ payload: JSONObject | None = None,
204
+ ) -> JSONObject:
205
+ return self.app_custom_button_update(profile=profile, app_key=app_key, button_id=button_id, payload=payload or {})
206
+
207
+ @mcp.tool()
208
+ def app_custom_button_delete(profile: str = DEFAULT_PROFILE, app_key: str = "", button_id: int = 0) -> JSONObject:
209
+ return self.app_custom_button_delete(profile=profile, app_key=app_key, button_id=button_id)
210
+
85
211
  @mcp.tool()
86
212
  def app_read_summary(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
87
213
  return self.app_read_summary(profile=profile, app_key=app_key)
@@ -103,75 +229,51 @@ class AiBuilderTools(ToolBase):
103
229
  return self.app_read_flow_summary(profile=profile, app_key=app_key)
104
230
 
105
231
  @mcp.tool()
106
- def app_schema_plan(
107
- profile: str = DEFAULT_PROFILE,
108
- app_key: str = "",
109
- package_tag_id: int | None = None,
110
- app_name: str = "",
111
- create_if_missing: bool = False,
112
- add_fields: list[JSONObject] | None = None,
113
- update_fields: list[JSONObject] | None = None,
114
- remove_fields: list[JSONObject] | None = None,
115
- ) -> JSONObject:
116
- return self.app_schema_plan(
117
- profile=profile,
118
- app_key=app_key,
119
- package_tag_id=package_tag_id,
120
- app_name=app_name,
121
- create_if_missing=create_if_missing,
122
- add_fields=add_fields or [],
123
- update_fields=update_fields or [],
124
- remove_fields=remove_fields or [],
125
- )
232
+ def app_read_charts_summary(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
233
+ return self.app_read_charts_summary(profile=profile, app_key=app_key)
234
+
235
+ @mcp.tool()
236
+ def portal_list(profile: str = DEFAULT_PROFILE) -> JSONObject:
237
+ return self.portal_list(profile=profile)
126
238
 
127
239
  @mcp.tool()
128
- def app_layout_plan(
240
+ def portal_get(
129
241
  profile: str = DEFAULT_PROFILE,
130
- app_key: str = "",
131
- mode: str = "merge",
132
- sections: list[JSONObject] | None = None,
133
- preset: str | None = None,
242
+ dash_key: str = "",
243
+ being_draft: bool = True,
134
244
  ) -> JSONObject:
135
- return self.app_layout_plan(
136
- profile=profile,
137
- app_key=app_key,
138
- mode=mode,
139
- sections=sections or [],
140
- preset=preset,
141
- )
245
+ return self.portal_get(profile=profile, dash_key=dash_key, being_draft=being_draft)
142
246
 
143
247
  @mcp.tool()
144
- def app_flow_plan(
248
+ def portal_read_summary(
145
249
  profile: str = DEFAULT_PROFILE,
146
- app_key: str = "",
147
- mode: str = "replace",
148
- nodes: list[JSONObject] | None = None,
149
- transitions: list[JSONObject] | None = None,
150
- preset: str | None = None,
250
+ dash_key: str = "",
251
+ being_draft: bool = True,
151
252
  ) -> JSONObject:
152
- return self.app_flow_plan(
153
- profile=profile,
154
- app_key=app_key,
155
- mode=mode,
156
- nodes=nodes or [],
157
- transitions=transitions or [],
158
- preset=preset,
159
- )
253
+ return self.portal_read_summary(profile=profile, dash_key=dash_key, being_draft=being_draft)
160
254
 
161
255
  @mcp.tool()
162
- def app_views_plan(
256
+ def view_get(profile: str = DEFAULT_PROFILE, viewgraph_key: str = "") -> JSONObject:
257
+ return self.view_get(profile=profile, viewgraph_key=viewgraph_key)
258
+
259
+ @mcp.tool()
260
+ def chart_get(
163
261
  profile: str = DEFAULT_PROFILE,
164
- app_key: str = "",
165
- upsert_views: list[JSONObject] | None = None,
166
- remove_views: list[str] | None = None,
167
- preset: str | None = None,
262
+ chart_id: str = "",
263
+ data_payload: JSONObject | None = None,
264
+ page_num: int | None = None,
265
+ page_size: int | None = None,
266
+ page_num_y: int | None = None,
267
+ page_size_y: int | None = None,
168
268
  ) -> JSONObject:
169
- return self.app_views_plan(
269
+ return self.chart_get(
170
270
  profile=profile,
171
- app_key=app_key,
172
- upsert_views=upsert_views or [],
173
- remove_views=remove_views or [],
174
- preset=preset,
271
+ chart_id=chart_id,
272
+ data_payload=data_payload or {},
273
+ page_num=page_num,
274
+ page_size=page_size,
275
+ page_num_y=page_num_y,
276
+ page_size_y=page_size_y,
175
277
  )
176
278
 
177
279
  @mcp.tool()
@@ -181,18 +283,39 @@ class AiBuilderTools(ToolBase):
181
283
  package_tag_id: int | None = None,
182
284
  app_name: str = "",
183
285
  app_title: str = "",
286
+ icon: str = "",
287
+ color: str = "",
184
288
  create_if_missing: bool = False,
185
289
  publish: bool = True,
186
290
  add_fields: list[JSONObject] | None = None,
187
291
  update_fields: list[JSONObject] | None = None,
188
292
  remove_fields: list[JSONObject] | None = None,
189
293
  ) -> JSONObject:
294
+ has_app_key = bool((app_key or "").strip())
295
+ has_app_name = bool((app_name or "").strip())
296
+ has_app_title = bool((app_title or "").strip())
297
+ has_package_tag_id = package_tag_id is not None
298
+ if has_app_key:
299
+ if create_if_missing or has_app_name or has_package_tag_id or has_app_title:
300
+ return _config_failure(
301
+ tool_name="app_schema_apply",
302
+ message="app_schema_apply edit mode only accepts app_key as the resource selector.",
303
+ fix_hint="For existing apps, pass `app_key` only. For create mode, use `package_tag_id + app_name + create_if_missing=true`.",
304
+ )
305
+ elif not (create_if_missing and has_package_tag_id and has_app_name):
306
+ return _config_failure(
307
+ tool_name="app_schema_apply",
308
+ message="app_schema_apply create mode requires package_tag_id, app_name, and create_if_missing=true.",
309
+ fix_hint="Use `app_key` for existing apps, or pass `package_tag_id + app_name + create_if_missing=true` to create a new app.",
310
+ )
190
311
  return self.app_schema_apply(
191
312
  profile=profile,
192
313
  app_key=app_key,
193
314
  package_tag_id=package_tag_id,
194
315
  app_name=app_name,
195
316
  app_title=app_title,
317
+ icon=icon,
318
+ color=color,
196
319
  create_if_missing=create_if_missing,
197
320
  publish=publish,
198
321
  add_fields=add_fields or [],
@@ -244,6 +367,67 @@ class AiBuilderTools(ToolBase):
244
367
  remove_views=remove_views or [],
245
368
  )
246
369
 
370
+ @mcp.tool()
371
+ def app_charts_apply(
372
+ profile: str = DEFAULT_PROFILE,
373
+ app_key: str = "",
374
+ upsert_charts: list[JSONObject] | None = None,
375
+ remove_chart_ids: list[str] | None = None,
376
+ reorder_chart_ids: list[str] | None = None,
377
+ ) -> JSONObject:
378
+ return self.app_charts_apply(
379
+ profile=profile,
380
+ app_key=app_key,
381
+ upsert_charts=upsert_charts or [],
382
+ remove_chart_ids=remove_chart_ids or [],
383
+ reorder_chart_ids=reorder_chart_ids or [],
384
+ )
385
+
386
+ @mcp.tool()
387
+ def portal_apply(
388
+ profile: str = DEFAULT_PROFILE,
389
+ dash_key: str = "",
390
+ dash_name: str = "",
391
+ package_tag_id: int | None = None,
392
+ publish: bool = True,
393
+ sections: list[JSONObject] | None = None,
394
+ auth: JSONObject | None = None,
395
+ icon: str | None = None,
396
+ color: str | None = None,
397
+ hide_copyright: bool | None = None,
398
+ dash_global_config: JSONObject | None = None,
399
+ config: JSONObject | None = None,
400
+ ) -> JSONObject:
401
+ has_dash_key = bool((dash_key or "").strip())
402
+ has_dash_name = bool((dash_name or "").strip())
403
+ has_package_tag_id = package_tag_id is not None
404
+ if has_dash_key and has_package_tag_id:
405
+ return _config_failure(
406
+ tool_name="portal_apply",
407
+ message="portal_apply accepts exactly one selector mode.",
408
+ fix_hint="Use `dash_key` to update an existing portal, or use `package_tag_id + dash_name` to create a new portal.",
409
+ )
410
+ if not has_dash_key and not (has_package_tag_id and has_dash_name):
411
+ return _config_failure(
412
+ tool_name="portal_apply",
413
+ message="portal_apply requires either dash_key, or package_tag_id together with dash_name.",
414
+ fix_hint="Use `dash_key` for an existing portal. For create mode, pass `package_tag_id + dash_name`.",
415
+ )
416
+ return self.portal_apply(
417
+ profile=profile,
418
+ dash_key=dash_key,
419
+ dash_name=dash_name,
420
+ package_tag_id=package_tag_id,
421
+ publish=publish,
422
+ sections=sections or [],
423
+ auth=auth,
424
+ icon=icon,
425
+ color=color,
426
+ hide_copyright=hide_copyright,
427
+ dash_global_config=dash_global_config,
428
+ config=config or {},
429
+ )
430
+
247
431
  @mcp.tool()
248
432
  def app_publish_verify(
249
433
  profile: str = DEFAULT_PROFILE,
@@ -274,14 +458,174 @@ class AiBuilderTools(ToolBase):
274
458
  suggested_next_call={"tool_name": "package_resolve", "arguments": {"profile": profile, "package_name": package_name}},
275
459
  )
276
460
 
461
+ def builder_tool_contract(self, *, tool_name: str) -> JSONObject:
462
+ requested = str(tool_name or "").strip()
463
+ public_tool_names = sorted(
464
+ name
465
+ for name in _BUILDER_TOOL_CONTRACTS.keys()
466
+ if name not in _PRIVATE_BUILDER_TOOL_CONTRACTS and name not in _BUILDER_TOOL_CONTRACT_ALIASES
467
+ )
468
+ if requested in _PRIVATE_BUILDER_TOOL_CONTRACTS:
469
+ lookup_name = ""
470
+ elif requested in _BUILDER_TOOL_CONTRACTS:
471
+ lookup_name = requested
472
+ else:
473
+ lookup_name = _BUILDER_TOOL_CONTRACT_ALIASES.get(requested, requested)
474
+ contract = _BUILDER_TOOL_CONTRACTS.get(lookup_name)
475
+ if contract is None:
476
+ return {
477
+ "status": "failed",
478
+ "error_code": "TOOL_CONTRACT_NOT_FOUND",
479
+ "recoverable": True,
480
+ "message": "tool contract is not defined for the requested public builder tool",
481
+ "normalized_args": {"tool_name": requested},
482
+ "missing_fields": [],
483
+ "allowed_values": {"tool_name": public_tool_names},
484
+ "details": {"reason_path": "tool_name"},
485
+ "suggested_next_call": None,
486
+ "request_id": None,
487
+ "backend_code": None,
488
+ "http_status": None,
489
+ "noop": False,
490
+ "warnings": [],
491
+ "verification": {},
492
+ "verified": False,
493
+ }
494
+ return {
495
+ "status": "success",
496
+ "error_code": None,
497
+ "recoverable": False,
498
+ "message": "loaded builder tool contract",
499
+ "normalized_args": {"tool_name": requested},
500
+ "missing_fields": [],
501
+ "allowed_values": {},
502
+ "details": {},
503
+ "suggested_next_call": None,
504
+ "request_id": None,
505
+ "backend_code": None,
506
+ "http_status": None,
507
+ "noop": False,
508
+ "warnings": [],
509
+ "verification": {},
510
+ "verified": True,
511
+ "tool_name": requested,
512
+ "contract": contract,
513
+ }
514
+
515
+ def package_create(
516
+ self,
517
+ *,
518
+ profile: str,
519
+ package_name: str,
520
+ icon: str | None = None,
521
+ color: str | None = None,
522
+ ) -> JSONObject:
523
+ normalized_args = {
524
+ "package_name": package_name,
525
+ **({"icon": icon} if icon else {}),
526
+ **({"color": color} if color else {}),
527
+ }
528
+ return _safe_tool_call(
529
+ lambda: self._facade.package_create(profile=profile, package_name=package_name, icon=icon, color=color),
530
+ error_code="PACKAGE_CREATE_FAILED",
531
+ normalized_args=normalized_args,
532
+ suggested_next_call={
533
+ "tool_name": "package_create",
534
+ "arguments": {
535
+ "profile": profile,
536
+ "package_name": package_name,
537
+ **({"icon": icon} if icon else {}),
538
+ **({"color": color} if color else {}),
539
+ },
540
+ },
541
+ )
542
+
543
+ def member_search(
544
+ self,
545
+ *,
546
+ profile: str,
547
+ query: str,
548
+ page_num: int = 1,
549
+ page_size: int = 20,
550
+ contain_disable: bool = False,
551
+ ) -> JSONObject:
552
+ normalized_args = {
553
+ "query": query,
554
+ "page_num": page_num,
555
+ "page_size": page_size,
556
+ "contain_disable": contain_disable,
557
+ }
558
+ return _safe_tool_call(
559
+ lambda: self._facade.member_search(
560
+ profile=profile,
561
+ query=query,
562
+ page_num=page_num,
563
+ page_size=page_size,
564
+ contain_disable=contain_disable,
565
+ ),
566
+ error_code="MEMBER_SEARCH_FAILED",
567
+ normalized_args=normalized_args,
568
+ suggested_next_call={"tool_name": "member_search", "arguments": {"profile": profile, **normalized_args}},
569
+ )
570
+
571
+ def role_search(self, *, profile: str, keyword: str, page_num: int = 1, page_size: int = 20) -> JSONObject:
572
+ normalized_args = {"keyword": keyword, "page_num": page_num, "page_size": page_size}
573
+ return _safe_tool_call(
574
+ lambda: self._facade.role_search(profile=profile, keyword=keyword, page_num=page_num, page_size=page_size),
575
+ error_code="ROLE_SEARCH_FAILED",
576
+ normalized_args=normalized_args,
577
+ suggested_next_call={"tool_name": "role_search", "arguments": {"profile": profile, **normalized_args}},
578
+ )
579
+
580
+ def role_create(
581
+ self,
582
+ *,
583
+ profile: str,
584
+ role_name: str,
585
+ member_uids: list[int],
586
+ member_emails: list[str],
587
+ member_names: list[str],
588
+ role_icon: str = "ex-user-outlined",
589
+ ) -> JSONObject:
590
+ normalized_args = {
591
+ "role_name": role_name,
592
+ "member_uids": member_uids,
593
+ "member_emails": member_emails,
594
+ "member_names": member_names,
595
+ "role_icon": role_icon,
596
+ }
597
+ return _safe_tool_call(
598
+ lambda: self._facade.role_create(
599
+ profile=profile,
600
+ role_name=role_name,
601
+ member_uids=member_uids,
602
+ member_emails=member_emails,
603
+ member_names=member_names,
604
+ role_icon=role_icon,
605
+ ),
606
+ error_code="ROLE_CREATE_FAILED",
607
+ normalized_args=normalized_args,
608
+ suggested_next_call={"tool_name": "role_create", "arguments": {"profile": profile, **normalized_args}},
609
+ )
610
+
277
611
  def package_attach_app(self, *, profile: str, tag_id: int, app_key: str, app_title: str = "") -> JSONObject:
278
612
  normalized_args = {"tag_id": tag_id, "app_key": app_key, "app_title": app_title}
279
- return _safe_tool_call(
613
+ result = _safe_tool_call(
280
614
  lambda: self._facade.package_attach_app(profile=profile, tag_id=tag_id, app_key=app_key, app_title=app_title),
281
615
  error_code="PACKAGE_ATTACH_FAILED",
282
616
  normalized_args=normalized_args,
283
617
  suggested_next_call={"tool_name": "package_attach_app", "arguments": {"profile": profile, **normalized_args}},
284
618
  )
619
+ return self._retry_after_self_lock_release(
620
+ profile=profile,
621
+ result=result,
622
+ retry_call=lambda: self._facade.package_attach_app(
623
+ profile=profile,
624
+ tag_id=tag_id,
625
+ app_key=app_key,
626
+ app_title=app_title,
627
+ ),
628
+ )
285
629
 
286
630
  def app_release_edit_lock_if_mine(
287
631
  self,
@@ -317,6 +661,98 @@ class AiBuilderTools(ToolBase):
317
661
  suggested_next_call={"tool_name": "app_resolve", "arguments": {"profile": profile, **normalized_args}},
318
662
  )
319
663
 
664
+ def app_custom_button_list(self, *, profile: str, app_key: str) -> JSONObject:
665
+ normalized_args = {"app_key": app_key}
666
+ return _safe_tool_call(
667
+ lambda: self._facade.app_custom_button_list(profile=profile, app_key=app_key),
668
+ error_code="CUSTOM_BUTTON_LIST_FAILED",
669
+ normalized_args=normalized_args,
670
+ suggested_next_call={"tool_name": "app_custom_button_list", "arguments": {"profile": profile, **normalized_args}},
671
+ )
672
+
673
+ def app_custom_button_get(self, *, profile: str, app_key: str, button_id: int) -> JSONObject:
674
+ normalized_args = {"app_key": app_key, "button_id": button_id}
675
+ return _safe_tool_call(
676
+ lambda: self._facade.app_custom_button_get(profile=profile, app_key=app_key, button_id=button_id),
677
+ error_code="CUSTOM_BUTTON_GET_FAILED",
678
+ normalized_args=normalized_args,
679
+ suggested_next_call={"tool_name": "app_custom_button_get", "arguments": {"profile": profile, **normalized_args}},
680
+ )
681
+
682
+ def app_custom_button_create(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
683
+ try:
684
+ request = CustomButtonPatch.model_validate(payload)
685
+ except ValidationError as exc:
686
+ return _validation_failure(
687
+ str(exc),
688
+ tool_name="app_custom_button_create",
689
+ exc=exc,
690
+ suggested_next_call={
691
+ "tool_name": "app_custom_button_create",
692
+ "arguments": {
693
+ "profile": profile,
694
+ "app_key": app_key,
695
+ "payload": {
696
+ "button_text": "新增记录",
697
+ "background_color": "#FFFFFF",
698
+ "text_color": "#494F57",
699
+ "button_icon": "ex-add-outlined",
700
+ "trigger_action": "addData",
701
+ "trigger_add_data_config": {"related_app_key": "TARGET_APP_KEY", "que_relation": []},
702
+ },
703
+ },
704
+ },
705
+ )
706
+ normalized_args = {"app_key": app_key, "payload": request.model_dump(mode="json")}
707
+ return _safe_tool_call(
708
+ lambda: self._facade.app_custom_button_create(profile=profile, app_key=app_key, payload=request),
709
+ error_code="CUSTOM_BUTTON_CREATE_FAILED",
710
+ normalized_args=normalized_args,
711
+ suggested_next_call={"tool_name": "app_custom_button_create", "arguments": {"profile": profile, **normalized_args}},
712
+ )
713
+
714
+ def app_custom_button_update(self, *, profile: str, app_key: str, button_id: int, payload: JSONObject) -> JSONObject:
715
+ try:
716
+ request = CustomButtonPatch.model_validate(payload)
717
+ except ValidationError as exc:
718
+ return _validation_failure(
719
+ str(exc),
720
+ tool_name="app_custom_button_update",
721
+ exc=exc,
722
+ suggested_next_call={
723
+ "tool_name": "app_custom_button_update",
724
+ "arguments": {
725
+ "profile": profile,
726
+ "app_key": app_key,
727
+ "button_id": button_id,
728
+ "payload": {
729
+ "button_text": "新增记录",
730
+ "background_color": "#FFFFFF",
731
+ "text_color": "#494F57",
732
+ "button_icon": "ex-add-outlined",
733
+ "trigger_action": "link",
734
+ "trigger_link_url": "https://example.com",
735
+ },
736
+ },
737
+ },
738
+ )
739
+ normalized_args = {"app_key": app_key, "button_id": button_id, "payload": request.model_dump(mode="json")}
740
+ return _safe_tool_call(
741
+ lambda: self._facade.app_custom_button_update(profile=profile, app_key=app_key, button_id=button_id, payload=request),
742
+ error_code="CUSTOM_BUTTON_UPDATE_FAILED",
743
+ normalized_args=normalized_args,
744
+ suggested_next_call={"tool_name": "app_custom_button_update", "arguments": {"profile": profile, **normalized_args}},
745
+ )
746
+
747
+ def app_custom_button_delete(self, *, profile: str, app_key: str, button_id: int) -> JSONObject:
748
+ normalized_args = {"app_key": app_key, "button_id": button_id}
749
+ return _safe_tool_call(
750
+ lambda: self._facade.app_custom_button_delete(profile=profile, app_key=app_key, button_id=button_id),
751
+ error_code="CUSTOM_BUTTON_DELETE_FAILED",
752
+ normalized_args=normalized_args,
753
+ suggested_next_call={"tool_name": "app_custom_button_delete", "arguments": {"profile": profile, **normalized_args}},
754
+ )
755
+
320
756
  def app_read_summary(self, *, profile: str, app_key: str) -> JSONObject:
321
757
  normalized_args = {"app_key": app_key}
322
758
  return _safe_tool_call(
@@ -362,6 +798,84 @@ class AiBuilderTools(ToolBase):
362
798
  suggested_next_call={"tool_name": "app_read_flow_summary", "arguments": {"profile": profile, "app_key": app_key}},
363
799
  )
364
800
 
801
+ def app_read_charts_summary(self, *, profile: str, app_key: str) -> JSONObject:
802
+ normalized_args = {"app_key": app_key}
803
+ return _safe_tool_call(
804
+ lambda: self._facade.app_read_charts_summary(profile=profile, app_key=app_key),
805
+ error_code="CHARTS_READ_FAILED",
806
+ normalized_args=normalized_args,
807
+ suggested_next_call={"tool_name": "app_read_charts_summary", "arguments": {"profile": profile, "app_key": app_key}},
808
+ )
809
+
810
+ def portal_list(self, *, profile: str) -> JSONObject:
811
+ return _safe_tool_call(
812
+ lambda: self._facade.portal_list(profile=profile),
813
+ error_code="PORTAL_LIST_FAILED",
814
+ normalized_args={},
815
+ suggested_next_call={"tool_name": "portal_list", "arguments": {"profile": profile}},
816
+ )
817
+
818
+ def portal_get(self, *, profile: str, dash_key: str, being_draft: bool = True) -> JSONObject:
819
+ normalized_args = {"dash_key": dash_key, "being_draft": being_draft}
820
+ return _safe_tool_call(
821
+ lambda: self._facade.portal_get(profile=profile, dash_key=dash_key, being_draft=being_draft),
822
+ error_code="PORTAL_GET_FAILED",
823
+ normalized_args=normalized_args,
824
+ suggested_next_call={"tool_name": "portal_get", "arguments": {"profile": profile, **normalized_args}},
825
+ )
826
+
827
+ def portal_read_summary(self, *, profile: str, dash_key: str, being_draft: bool = True) -> JSONObject:
828
+ normalized_args = {"dash_key": dash_key, "being_draft": being_draft}
829
+ return _safe_tool_call(
830
+ lambda: self._facade.portal_read_summary(profile=profile, dash_key=dash_key, being_draft=being_draft),
831
+ error_code="PORTAL_READ_FAILED",
832
+ normalized_args=normalized_args,
833
+ suggested_next_call={"tool_name": "portal_read_summary", "arguments": {"profile": profile, **normalized_args}},
834
+ )
835
+
836
+ def view_get(self, *, profile: str, viewgraph_key: str) -> JSONObject:
837
+ normalized_args = {"viewgraph_key": viewgraph_key}
838
+ return _safe_tool_call(
839
+ lambda: self._facade.view_get(profile=profile, viewgraph_key=viewgraph_key),
840
+ error_code="VIEW_GET_FAILED",
841
+ normalized_args=normalized_args,
842
+ suggested_next_call={"tool_name": "view_get", "arguments": {"profile": profile, **normalized_args}},
843
+ )
844
+
845
+ def chart_get(
846
+ self,
847
+ *,
848
+ profile: str,
849
+ chart_id: str,
850
+ data_payload: JSONObject | None = None,
851
+ page_num: int | None = None,
852
+ page_size: int | None = None,
853
+ page_num_y: int | None = None,
854
+ page_size_y: int | None = None,
855
+ ) -> JSONObject:
856
+ normalized_args = {
857
+ "chart_id": chart_id,
858
+ "data_payload": deepcopy(data_payload) if isinstance(data_payload, dict) else {},
859
+ "page_num": page_num,
860
+ "page_size": page_size,
861
+ "page_num_y": page_num_y,
862
+ "page_size_y": page_size_y,
863
+ }
864
+ return _safe_tool_call(
865
+ lambda: self._facade.chart_get(
866
+ profile=profile,
867
+ chart_id=chart_id,
868
+ data_payload=normalized_args["data_payload"],
869
+ page_num=page_num,
870
+ page_size=page_size,
871
+ page_num_y=page_num_y,
872
+ page_size_y=page_size_y,
873
+ ),
874
+ error_code="CHART_GET_FAILED",
875
+ normalized_args=normalized_args,
876
+ suggested_next_call={"tool_name": "chart_get", "arguments": {"profile": profile, "chart_id": chart_id}},
877
+ )
878
+
365
879
  def app_schema_plan(
366
880
  self,
367
881
  *,
@@ -369,6 +883,8 @@ class AiBuilderTools(ToolBase):
369
883
  app_key: str = "",
370
884
  package_tag_id: int | None = None,
371
885
  app_name: str = "",
886
+ icon: str = "",
887
+ color: str = "",
372
888
  create_if_missing: bool = False,
373
889
  add_fields: list[JSONObject],
374
890
  update_fields: list[JSONObject],
@@ -380,6 +896,8 @@ class AiBuilderTools(ToolBase):
380
896
  "app_key": app_key,
381
897
  "package_tag_id": package_tag_id,
382
898
  "app_name": app_name,
899
+ "icon": icon,
900
+ "color": color,
383
901
  "create_if_missing": create_if_missing,
384
902
  "add_fields": add_fields,
385
903
  "update_fields": update_fields,
@@ -389,6 +907,8 @@ class AiBuilderTools(ToolBase):
389
907
  except ValidationError as exc:
390
908
  return _validation_failure(
391
909
  str(exc),
910
+ tool_name="app_schema_plan",
911
+ exc=exc,
392
912
  suggested_next_call={
393
913
  "tool_name": "app_schema_plan",
394
914
  "arguments": {
@@ -396,6 +916,8 @@ class AiBuilderTools(ToolBase):
396
916
  "app_key": app_key,
397
917
  "package_tag_id": package_tag_id,
398
918
  "app_name": app_name,
919
+ "icon": icon,
920
+ "color": color,
399
921
  "create_if_missing": create_if_missing,
400
922
  "add_fields": [{"name": "字段名称", "type": "text"}],
401
923
  "update_fields": [],
@@ -431,22 +953,29 @@ class AiBuilderTools(ToolBase):
431
953
  except ValidationError as exc:
432
954
  return _validation_failure(
433
955
  str(exc),
956
+ tool_name="app_layout_plan",
957
+ exc=exc,
434
958
  suggested_next_call={
435
959
  "tool_name": "app_layout_plan",
436
960
  "arguments": {
437
961
  "profile": profile,
438
962
  "app_key": app_key,
439
963
  "mode": "merge",
440
- "preset": "balanced",
441
- "sections": [],
964
+ "sections": [{
965
+ "type": "paragraph",
966
+ "paragraph_id": "basic",
967
+ "title": "基础信息",
968
+ "rows": [["字段A", "字段B", "字段C", "字段D"]],
969
+ }],
442
970
  },
443
971
  },
444
972
  )
973
+ normalized_request = request.model_dump(mode="json", exclude_none=True)
445
974
  return _safe_tool_call(
446
975
  lambda: self._facade.app_layout_plan(profile=profile, request=request),
447
976
  error_code="LAYOUT_PLAN_FAILED",
448
- normalized_args=request.model_dump(mode="json"),
449
- suggested_next_call={"tool_name": "app_layout_plan", "arguments": {"profile": profile, **request.model_dump(mode="json")}},
977
+ normalized_args=normalized_request,
978
+ suggested_next_call={"tool_name": "app_layout_plan", "arguments": {"profile": profile, **normalized_request}},
450
979
  )
451
980
 
452
981
  def app_flow_plan(
@@ -472,6 +1001,8 @@ class AiBuilderTools(ToolBase):
472
1001
  except ValidationError as exc:
473
1002
  return _validation_failure(
474
1003
  str(exc),
1004
+ tool_name="app_flow_plan",
1005
+ exc=exc,
475
1006
  suggested_next_call={
476
1007
  "tool_name": "app_flow_plan",
477
1008
  "arguments": {
@@ -512,6 +1043,8 @@ class AiBuilderTools(ToolBase):
512
1043
  except ValidationError as exc:
513
1044
  return _validation_failure(
514
1045
  str(exc),
1046
+ tool_name="app_views_plan",
1047
+ exc=exc,
515
1048
  suggested_next_call={
516
1049
  "tool_name": "app_views_plan",
517
1050
  "arguments": {
@@ -538,6 +1071,57 @@ class AiBuilderTools(ToolBase):
538
1071
  package_tag_id: int | None = None,
539
1072
  app_name: str = "",
540
1073
  app_title: str = "",
1074
+ icon: str = "",
1075
+ color: str = "",
1076
+ create_if_missing: bool = False,
1077
+ publish: bool = True,
1078
+ add_fields: list[JSONObject],
1079
+ update_fields: list[JSONObject],
1080
+ remove_fields: list[JSONObject],
1081
+ ) -> JSONObject:
1082
+ result = self._app_schema_apply_once(
1083
+ profile=profile,
1084
+ app_key=app_key,
1085
+ package_tag_id=package_tag_id,
1086
+ app_name=app_name,
1087
+ app_title=app_title,
1088
+ icon=icon,
1089
+ color=color,
1090
+ create_if_missing=create_if_missing,
1091
+ publish=publish,
1092
+ add_fields=add_fields,
1093
+ update_fields=update_fields,
1094
+ remove_fields=remove_fields,
1095
+ )
1096
+ return self._retry_after_self_lock_release(
1097
+ profile=profile,
1098
+ result=result,
1099
+ retry_call=lambda: self._app_schema_apply_once(
1100
+ profile=profile,
1101
+ app_key=app_key,
1102
+ package_tag_id=package_tag_id,
1103
+ app_name=app_name,
1104
+ app_title=app_title,
1105
+ icon=icon,
1106
+ color=color,
1107
+ create_if_missing=create_if_missing,
1108
+ publish=publish,
1109
+ add_fields=add_fields,
1110
+ update_fields=update_fields,
1111
+ remove_fields=remove_fields,
1112
+ ),
1113
+ )
1114
+
1115
+ def _app_schema_apply_once(
1116
+ self,
1117
+ *,
1118
+ profile: str,
1119
+ app_key: str = "",
1120
+ package_tag_id: int | None = None,
1121
+ app_name: str = "",
1122
+ app_title: str = "",
1123
+ icon: str = "",
1124
+ color: str = "",
541
1125
  create_if_missing: bool = False,
542
1126
  publish: bool = True,
543
1127
  add_fields: list[JSONObject],
@@ -545,44 +1129,76 @@ class AiBuilderTools(ToolBase):
545
1129
  remove_fields: list[JSONObject],
546
1130
  ) -> JSONObject:
547
1131
  effective_app_name = app_name or app_title
1132
+ plan_result = self._rewrite_plan_result_for_apply(
1133
+ result=self.app_schema_plan(
1134
+ profile=profile,
1135
+ app_key=app_key,
1136
+ package_tag_id=package_tag_id,
1137
+ app_name=effective_app_name,
1138
+ icon=icon,
1139
+ color=color,
1140
+ create_if_missing=create_if_missing,
1141
+ add_fields=add_fields,
1142
+ update_fields=update_fields,
1143
+ remove_fields=remove_fields,
1144
+ ),
1145
+ profile=profile,
1146
+ publish=publish,
1147
+ plan_tool_name="app_schema_plan",
1148
+ apply_tool_name="app_schema_apply",
1149
+ )
1150
+ if not isinstance(plan_result, dict) or plan_result.get("status") != "success":
1151
+ return plan_result
1152
+ plan_args = plan_result.get("normalized_args")
1153
+ if not isinstance(plan_args, dict):
1154
+ plan_args = {}
548
1155
  try:
549
- parsed_add = [FieldPatch.model_validate(item) for item in add_fields]
550
- parsed_update = [FieldUpdatePatch.model_validate(item) for item in update_fields]
551
- parsed_remove = [FieldRemovePatch.model_validate(item) for item in remove_fields]
1156
+ parsed_add = [FieldPatch.model_validate(item) for item in plan_args.get("add_fields") or []]
1157
+ parsed_update = [FieldUpdatePatch.model_validate(item) for item in plan_args.get("update_fields") or []]
1158
+ parsed_remove = [FieldRemovePatch.model_validate(item) for item in plan_args.get("remove_fields") or []]
552
1159
  except ValidationError as exc:
553
1160
  return _validation_failure(
554
1161
  str(exc),
1162
+ tool_name="app_schema_apply",
1163
+ exc=exc,
555
1164
  suggested_next_call={
556
1165
  "tool_name": "app_schema_apply",
557
1166
  "arguments": {
558
1167
  "profile": profile,
559
- "app_key": app_key,
560
- "package_tag_id": package_tag_id,
561
- "app_name": effective_app_name,
562
- "create_if_missing": create_if_missing,
563
- "add_fields": [{"name": "字段名称", "type": "text", "required": False}],
564
- "update_fields": [],
565
- "remove_fields": [],
1168
+ "app_key": str(plan_args.get("app_key") or app_key),
1169
+ "package_tag_id": plan_args.get("package_tag_id", package_tag_id),
1170
+ "app_name": str(plan_args.get("app_name") or effective_app_name),
1171
+ "icon": str(plan_args.get("icon") or icon),
1172
+ "color": str(plan_args.get("color") or color),
1173
+ "create_if_missing": bool(plan_args.get("create_if_missing", create_if_missing)),
1174
+ "publish": publish,
1175
+ "add_fields": plan_args.get("add_fields") or [{"name": "字段名称", "type": "text", "required": False}],
1176
+ "update_fields": plan_args.get("update_fields") or [],
1177
+ "remove_fields": plan_args.get("remove_fields") or [],
566
1178
  },
567
1179
  },
568
1180
  )
569
1181
  normalized_args = {
570
- "app_key": app_key,
571
- "package_tag_id": package_tag_id,
572
- "app_name": effective_app_name,
573
- "create_if_missing": create_if_missing,
1182
+ "app_key": str(plan_args.get("app_key") or app_key),
1183
+ "package_tag_id": plan_args.get("package_tag_id", package_tag_id),
1184
+ "app_name": str(plan_args.get("app_name") or effective_app_name),
1185
+ "icon": str(plan_args.get("icon") or icon or ""),
1186
+ "color": str(plan_args.get("color") or color or ""),
1187
+ "create_if_missing": bool(plan_args.get("create_if_missing", create_if_missing)),
574
1188
  "publish": publish,
575
1189
  "add_fields": [patch.model_dump(mode="json") for patch in parsed_add],
576
1190
  "update_fields": [patch.model_dump(mode="json") for patch in parsed_update],
577
1191
  "remove_fields": [patch.model_dump(mode="json") for patch in parsed_remove],
578
1192
  }
579
- return _safe_tool_call(
1193
+ result = _safe_tool_call(
580
1194
  lambda: self._facade.app_schema_apply(
581
1195
  profile=profile,
582
- app_key=app_key,
583
- package_tag_id=package_tag_id,
584
- app_name=effective_app_name,
585
- create_if_missing=create_if_missing,
1196
+ app_key=str(plan_args.get("app_key") or app_key),
1197
+ package_tag_id=plan_args.get("package_tag_id", package_tag_id),
1198
+ app_name=str(plan_args.get("app_name") or effective_app_name),
1199
+ icon=str(plan_args.get("icon") or icon or ""),
1200
+ color=str(plan_args.get("color") or color or ""),
1201
+ create_if_missing=bool(plan_args.get("create_if_missing", create_if_missing)),
586
1202
  publish=publish,
587
1203
  add_fields=parsed_add,
588
1204
  update_fields=parsed_update,
@@ -592,48 +1208,80 @@ class AiBuilderTools(ToolBase):
592
1208
  normalized_args=normalized_args,
593
1209
  suggested_next_call={"tool_name": "app_schema_apply", "arguments": {"profile": profile, **normalized_args}},
594
1210
  )
1211
+ return result
595
1212
 
596
1213
  def app_layout_apply(self, *, profile: str, app_key: str, mode: str = "merge", publish: bool = True, sections: list[JSONObject]) -> JSONObject:
1214
+ result = self._app_layout_apply_once(
1215
+ profile=profile,
1216
+ app_key=app_key,
1217
+ mode=mode,
1218
+ publish=publish,
1219
+ sections=sections,
1220
+ )
1221
+ return self._retry_after_self_lock_release(
1222
+ profile=profile,
1223
+ result=result,
1224
+ retry_call=lambda: self._app_layout_apply_once(
1225
+ profile=profile,
1226
+ app_key=app_key,
1227
+ mode=mode,
1228
+ publish=publish,
1229
+ sections=sections,
1230
+ ),
1231
+ )
1232
+
1233
+ def _app_layout_apply_once(self, *, profile: str, app_key: str, mode: str = "merge", publish: bool = True, sections: list[JSONObject]) -> JSONObject:
1234
+ plan_result = self._rewrite_plan_result_for_apply(
1235
+ result=self.app_layout_plan(
1236
+ profile=profile,
1237
+ app_key=app_key,
1238
+ mode=mode,
1239
+ sections=sections,
1240
+ preset=None,
1241
+ ),
1242
+ profile=profile,
1243
+ publish=publish,
1244
+ plan_tool_name="app_layout_plan",
1245
+ apply_tool_name="app_layout_apply",
1246
+ )
1247
+ if not isinstance(plan_result, dict) or plan_result.get("status") != "success":
1248
+ return plan_result
1249
+ plan_args = plan_result.get("normalized_args")
1250
+ if not isinstance(plan_args, dict):
1251
+ plan_args = {}
597
1252
  try:
598
- normalized_mode = str(mode or "merge").strip().lower()
599
- if normalized_mode in {"overwrite"}:
600
- normalized_mode = "replace"
601
- parsed_mode = LayoutApplyMode(normalized_mode)
602
- parsed_sections = [LayoutSectionPatch.model_validate(item) for item in sections]
603
- except ValueError:
604
- return _validation_failure(
605
- "mode must be one of: merge, replace",
606
- suggested_next_call={
607
- "tool_name": "app_layout_apply",
608
- "arguments": {
609
- "profile": profile,
610
- "app_key": app_key,
611
- "mode": "merge",
612
- "sections": [{"title": "基础信息", "rows": [["字段A", "字段B"]]}],
613
- },
614
- },
615
- )
616
- except ValidationError as exc:
1253
+ parsed_mode = LayoutApplyMode(str(plan_args.get("mode") or mode))
1254
+ parsed_sections = [LayoutSectionPatch.model_validate(item) for item in plan_args.get("sections") or []]
1255
+ except (ValueError, ValidationError) as exc:
617
1256
  return _validation_failure(
618
1257
  str(exc),
1258
+ tool_name="app_layout_apply",
1259
+ exc=exc if isinstance(exc, ValidationError) else None,
619
1260
  suggested_next_call={
620
1261
  "tool_name": "app_layout_apply",
621
1262
  "arguments": {
622
1263
  "profile": profile,
623
- "app_key": app_key,
624
- "mode": "merge",
625
- "sections": [{"title": "基础信息", "rows": [["字段A", "字段B"]]}],
1264
+ "app_key": str(plan_args.get("app_key") or app_key),
1265
+ "mode": str(plan_args.get("mode") or "merge"),
1266
+ "publish": publish,
1267
+ "sections": plan_args.get("sections") or [{"title": "基础信息", "rows": [["字段A", "字段B"]]}],
626
1268
  },
627
1269
  },
628
1270
  )
629
1271
  normalized_args = {
630
- "app_key": app_key,
1272
+ "app_key": str(plan_args.get("app_key") or app_key),
631
1273
  "mode": parsed_mode.value,
632
1274
  "publish": publish,
633
- "sections": [section.model_dump(mode="json") for section in parsed_sections],
1275
+ "sections": [section.model_dump(mode="json", exclude_none=True) for section in parsed_sections],
634
1276
  }
635
1277
  return _safe_tool_call(
636
- lambda: self._facade.app_layout_apply(profile=profile, app_key=app_key, mode=parsed_mode, publish=publish, sections=parsed_sections),
1278
+ lambda: self._facade.app_layout_apply(
1279
+ profile=profile,
1280
+ app_key=str(plan_args.get("app_key") or app_key),
1281
+ mode=parsed_mode,
1282
+ publish=publish,
1283
+ sections=parsed_sections,
1284
+ ),
637
1285
  error_code="LAYOUT_APPLY_FAILED",
638
1286
  normalized_args=normalized_args,
639
1287
  suggested_next_call={"tool_name": "app_layout_apply", "arguments": {"profile": profile, **normalized_args}},
@@ -649,27 +1297,80 @@ class AiBuilderTools(ToolBase):
649
1297
  nodes: list[JSONObject],
650
1298
  transitions: list[JSONObject],
651
1299
  ) -> JSONObject:
1300
+ result = self._app_flow_apply_once(
1301
+ profile=profile,
1302
+ app_key=app_key,
1303
+ mode=mode,
1304
+ publish=publish,
1305
+ nodes=nodes,
1306
+ transitions=transitions,
1307
+ )
1308
+ return self._retry_after_self_lock_release(
1309
+ profile=profile,
1310
+ result=result,
1311
+ retry_call=lambda: self._app_flow_apply_once(
1312
+ profile=profile,
1313
+ app_key=app_key,
1314
+ mode=mode,
1315
+ publish=publish,
1316
+ nodes=nodes,
1317
+ transitions=transitions,
1318
+ ),
1319
+ )
1320
+
1321
+ def _app_flow_apply_once(
1322
+ self,
1323
+ *,
1324
+ profile: str,
1325
+ app_key: str,
1326
+ mode: str = "replace",
1327
+ publish: bool = True,
1328
+ nodes: list[JSONObject],
1329
+ transitions: list[JSONObject],
1330
+ ) -> JSONObject:
1331
+ plan_result = self._rewrite_plan_result_for_apply(
1332
+ result=self.app_flow_plan(
1333
+ profile=profile,
1334
+ app_key=app_key,
1335
+ mode=mode,
1336
+ nodes=nodes,
1337
+ transitions=transitions,
1338
+ preset=None,
1339
+ ),
1340
+ profile=profile,
1341
+ publish=publish,
1342
+ plan_tool_name="app_flow_plan",
1343
+ apply_tool_name="app_flow_apply",
1344
+ )
1345
+ if not isinstance(plan_result, dict) or plan_result.get("status") != "success":
1346
+ return plan_result
1347
+ plan_args = plan_result.get("normalized_args")
1348
+ if not isinstance(plan_args, dict):
1349
+ plan_args = {}
652
1350
  try:
653
1351
  request = FlowPlanRequest.model_validate(
654
1352
  {
655
- "app_key": app_key,
656
- "mode": mode,
657
- "nodes": nodes,
658
- "transitions": transitions,
1353
+ "app_key": plan_args.get("app_key") or app_key,
1354
+ "mode": plan_args.get("mode") or mode,
1355
+ "nodes": plan_args.get("nodes") or [],
1356
+ "transitions": plan_args.get("transitions") or [],
659
1357
  "preset": None,
660
1358
  }
661
1359
  )
662
1360
  except ValidationError as exc:
663
1361
  return _validation_failure(
664
1362
  str(exc),
1363
+ tool_name="app_flow_apply",
1364
+ exc=exc,
665
1365
  suggested_next_call={
666
1366
  "tool_name": "app_flow_apply",
667
1367
  "arguments": {
668
1368
  "profile": profile,
669
- "app_key": app_key,
670
- "mode": "replace",
671
- "nodes": [{"id": "start", "type": "start", "name": "发起"}],
672
- "transitions": [],
1369
+ "app_key": str(plan_args.get("app_key") or app_key),
1370
+ "mode": str(plan_args.get("mode") or "replace"),
1371
+ "publish": publish,
1372
+ "nodes": plan_args.get("nodes") or [{"id": "start", "type": "start", "name": "发起"}],
1373
+ "transitions": plan_args.get("transitions") or [],
673
1374
  },
674
1375
  },
675
1376
  )
@@ -703,45 +1404,363 @@ class AiBuilderTools(ToolBase):
703
1404
  upsert_views: list[JSONObject],
704
1405
  remove_views: list[str],
705
1406
  ) -> JSONObject:
1407
+ result = self._app_views_apply_once(
1408
+ profile=profile,
1409
+ app_key=app_key,
1410
+ publish=publish,
1411
+ upsert_views=upsert_views,
1412
+ remove_views=remove_views,
1413
+ )
1414
+ return self._retry_after_self_lock_release(
1415
+ profile=profile,
1416
+ result=result,
1417
+ retry_call=lambda: self._app_views_apply_once(
1418
+ profile=profile,
1419
+ app_key=app_key,
1420
+ publish=publish,
1421
+ remove_views=remove_views,
1422
+ upsert_views=upsert_views,
1423
+ ),
1424
+ )
1425
+
1426
+ def _app_views_apply_once(
1427
+ self,
1428
+ *,
1429
+ profile: str,
1430
+ app_key: str,
1431
+ publish: bool = True,
1432
+ upsert_views: list[JSONObject],
1433
+ remove_views: list[str],
1434
+ ) -> JSONObject:
1435
+ plan_result = self._rewrite_plan_result_for_apply(
1436
+ result=self.app_views_plan(
1437
+ profile=profile,
1438
+ app_key=app_key,
1439
+ upsert_views=upsert_views,
1440
+ remove_views=remove_views,
1441
+ preset=None,
1442
+ ),
1443
+ profile=profile,
1444
+ publish=publish,
1445
+ plan_tool_name="app_views_plan",
1446
+ apply_tool_name="app_views_apply",
1447
+ )
1448
+ if not isinstance(plan_result, dict) or plan_result.get("status") != "success":
1449
+ return plan_result
1450
+ plan_args = plan_result.get("normalized_args")
1451
+ if not isinstance(plan_args, dict):
1452
+ plan_args = {}
706
1453
  try:
707
- parsed_views = [ViewUpsertPatch.model_validate(item) for item in upsert_views]
1454
+ parsed_views = [ViewUpsertPatch.model_validate(item) for item in plan_args.get("upsert_views") or []]
708
1455
  except ValidationError as exc:
709
1456
  return _validation_failure(
710
1457
  str(exc),
1458
+ tool_name="app_views_apply",
1459
+ exc=exc,
711
1460
  suggested_next_call={
712
1461
  "tool_name": "app_views_apply",
713
1462
  "arguments": {
714
1463
  "profile": profile,
715
- "app_key": app_key,
716
- "upsert_views": [{"name": "全部数据", "type": "table", "columns": ["字段A"]}],
717
- "remove_views": [],
1464
+ "app_key": str(plan_args.get("app_key") or app_key),
1465
+ "publish": publish,
1466
+ "upsert_views": plan_args.get("upsert_views") or [{"name": "全部数据", "type": "table", "columns": ["字段A"]}],
1467
+ "remove_views": plan_args.get("remove_views") or [],
718
1468
  },
719
1469
  },
720
1470
  )
721
1471
  normalized_args = {
722
- "app_key": app_key,
1472
+ "app_key": str(plan_args.get("app_key") or app_key),
723
1473
  "publish": publish,
724
1474
  "upsert_views": [view.model_dump(mode="json") for view in parsed_views],
725
- "remove_views": list(remove_views),
1475
+ "remove_views": list(plan_args.get("remove_views") or remove_views),
726
1476
  }
727
1477
  return _safe_tool_call(
728
- lambda: self._facade.app_views_apply(profile=profile, app_key=app_key, publish=publish, upsert_views=parsed_views, remove_views=remove_views),
1478
+ lambda: self._facade.app_views_apply(
1479
+ profile=profile,
1480
+ app_key=str(plan_args.get("app_key") or app_key),
1481
+ publish=publish,
1482
+ upsert_views=parsed_views,
1483
+ remove_views=list(plan_args.get("remove_views") or remove_views),
1484
+ ),
729
1485
  error_code="VIEWS_APPLY_FAILED",
730
1486
  normalized_args=normalized_args,
731
1487
  suggested_next_call={"tool_name": "app_views_apply", "arguments": {"profile": profile, **normalized_args}},
732
1488
  )
733
1489
 
1490
+ def chart_apply(
1491
+ self,
1492
+ *,
1493
+ profile: str,
1494
+ app_key: str,
1495
+ upsert_charts: list[JSONObject],
1496
+ remove_chart_ids: list[str],
1497
+ reorder_chart_ids: list[str],
1498
+ ) -> JSONObject:
1499
+ return self.app_charts_apply(
1500
+ profile=profile,
1501
+ app_key=app_key,
1502
+ upsert_charts=upsert_charts,
1503
+ remove_chart_ids=remove_chart_ids,
1504
+ reorder_chart_ids=reorder_chart_ids,
1505
+ )
1506
+
1507
+ def app_charts_apply(
1508
+ self,
1509
+ *,
1510
+ profile: str,
1511
+ app_key: str,
1512
+ upsert_charts: list[JSONObject],
1513
+ remove_chart_ids: list[str],
1514
+ reorder_chart_ids: list[str],
1515
+ ) -> JSONObject:
1516
+ try:
1517
+ request = ChartApplyRequest.model_validate(
1518
+ {
1519
+ "app_key": app_key,
1520
+ "upsert_charts": upsert_charts or [],
1521
+ "remove_chart_ids": remove_chart_ids or [],
1522
+ "reorder_chart_ids": reorder_chart_ids or [],
1523
+ }
1524
+ )
1525
+ except ValidationError as exc:
1526
+ return _validation_failure(
1527
+ str(exc),
1528
+ tool_name="app_charts_apply",
1529
+ exc=exc,
1530
+ suggested_next_call={
1531
+ "tool_name": "app_charts_apply",
1532
+ "arguments": {
1533
+ "profile": profile,
1534
+ "app_key": app_key,
1535
+ "upsert_charts": [{"name": "销售总量", "chart_type": "target", "indicator_field_ids": []}],
1536
+ "remove_chart_ids": [],
1537
+ "reorder_chart_ids": [],
1538
+ },
1539
+ },
1540
+ )
1541
+ normalized_args = request.model_dump(mode="json")
1542
+ return _safe_tool_call(
1543
+ lambda: self._facade.chart_apply(profile=profile, request=request),
1544
+ error_code="CHART_APPLY_FAILED",
1545
+ normalized_args=normalized_args,
1546
+ suggested_next_call={"tool_name": "app_charts_apply", "arguments": {"profile": profile, **normalized_args}},
1547
+ )
1548
+
1549
+ def portal_apply(
1550
+ self,
1551
+ *,
1552
+ profile: str,
1553
+ dash_key: str = "",
1554
+ dash_name: str = "",
1555
+ package_tag_id: int | None = None,
1556
+ publish: bool = True,
1557
+ sections: list[JSONObject],
1558
+ auth: JSONObject | None = None,
1559
+ icon: str | None = None,
1560
+ color: str | None = None,
1561
+ hide_copyright: bool | None = None,
1562
+ dash_global_config: JSONObject | None = None,
1563
+ config: JSONObject | None = None,
1564
+ ) -> JSONObject:
1565
+ try:
1566
+ request = PortalApplyRequest.model_validate(
1567
+ {
1568
+ "dash_key": dash_key or None,
1569
+ "dash_name": dash_name or None,
1570
+ "package_tag_id": package_tag_id,
1571
+ "publish": publish,
1572
+ "sections": sections or [],
1573
+ "auth": auth,
1574
+ "icon": icon,
1575
+ "color": color,
1576
+ "hide_copyright": hide_copyright,
1577
+ "dash_global_config": dash_global_config,
1578
+ "config": config or {},
1579
+ }
1580
+ )
1581
+ except ValidationError as exc:
1582
+ return _validation_failure(
1583
+ str(exc),
1584
+ tool_name="portal_apply",
1585
+ exc=exc,
1586
+ suggested_next_call={
1587
+ "tool_name": "portal_apply",
1588
+ "arguments": {
1589
+ "profile": profile,
1590
+ "dash_name": dash_name or "业务门户",
1591
+ "package_tag_id": package_tag_id or 1001,
1592
+ "publish": True,
1593
+ "sections": [
1594
+ {
1595
+ "title": "经营概览",
1596
+ "source_type": "text",
1597
+ "text": "欢迎使用业务门户",
1598
+ }
1599
+ ],
1600
+ },
1601
+ },
1602
+ )
1603
+ normalized_args = request.model_dump(mode="json")
1604
+ return _safe_tool_call(
1605
+ lambda: self._facade.portal_apply(profile=profile, request=request),
1606
+ error_code="PORTAL_APPLY_FAILED",
1607
+ normalized_args=normalized_args,
1608
+ suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
1609
+ )
1610
+
734
1611
  def app_publish_verify(self, *, profile: str, app_key: str, expected_package_tag_id: int | None = None) -> JSONObject:
735
1612
  normalized_args = {"app_key": app_key, "expected_package_tag_id": expected_package_tag_id}
736
- return _safe_tool_call(
1613
+ result = _safe_tool_call(
737
1614
  lambda: self._facade.app_publish_verify(profile=profile, app_key=app_key, expected_package_tag_id=expected_package_tag_id),
738
1615
  error_code="PUBLISH_VERIFY_FAILED",
739
1616
  normalized_args=normalized_args,
740
1617
  suggested_next_call={"tool_name": "app_publish_verify", "arguments": {"profile": profile, **normalized_args}},
741
1618
  )
1619
+ return self._retry_after_self_lock_release(
1620
+ profile=profile,
1621
+ result=result,
1622
+ retry_call=lambda: self._facade.app_publish_verify(
1623
+ profile=profile,
1624
+ app_key=app_key,
1625
+ expected_package_tag_id=expected_package_tag_id,
1626
+ ),
1627
+ )
1628
+
1629
+ def _retry_after_self_lock_release(self, *, profile: str, result: JSONObject, retry_call) -> JSONObject:
1630
+ if not isinstance(result, dict) or result.get("status") != "failed" or result.get("error_code") != "APP_EDIT_LOCKED":
1631
+ return result
1632
+ suggested = result.get("suggested_next_call")
1633
+ if not isinstance(suggested, dict) or suggested.get("tool_name") != "app_release_edit_lock_if_mine":
1634
+ return result
1635
+ arguments = suggested.get("arguments")
1636
+ if not isinstance(arguments, dict):
1637
+ return result
1638
+ app_key = str(arguments.get("app_key") or "")
1639
+ lock_owner_email = str(arguments.get("lock_owner_email") or "")
1640
+ lock_owner_name = str(arguments.get("lock_owner_name") or "")
1641
+ release_attempts: list[JSONObject] = []
1642
+ retried: JSONObject = result
1643
+ for _ in range(3):
1644
+ release_result = self.app_release_edit_lock_if_mine(
1645
+ profile=profile,
1646
+ app_key=app_key,
1647
+ lock_owner_email=lock_owner_email,
1648
+ lock_owner_name=lock_owner_name,
1649
+ )
1650
+ release_attempts.append(release_result)
1651
+ if not isinstance(release_result, dict) or release_result.get("status") != "success":
1652
+ result.setdefault("details", {})
1653
+ if isinstance(result["details"], dict):
1654
+ result["details"]["edit_lock_release_result"] = release_result
1655
+ result["details"]["edit_lock_release_attempts"] = release_attempts
1656
+ return result
1657
+ retried = retry_call()
1658
+ if not (
1659
+ isinstance(retried, dict)
1660
+ and retried.get("status") == "failed"
1661
+ and retried.get("error_code") == "APP_EDIT_LOCKED"
1662
+ ):
1663
+ break
1664
+ time.sleep(0.2)
1665
+ if (
1666
+ isinstance(retried, dict)
1667
+ and retried.get("status") == "failed"
1668
+ and retried.get("error_code") == "APP_EDIT_LOCKED"
1669
+ ):
1670
+ retried = {
1671
+ **retried,
1672
+ "error_code": "PERSISTENT_SELF_LOCK",
1673
+ "message": "app remains locked by the current user's active editor session after repeated forced release attempts",
1674
+ "recoverable": True,
1675
+ "suggested_next_call": None,
1676
+ }
1677
+ if isinstance(retried, dict):
1678
+ retried.setdefault("details", {})
1679
+ if isinstance(retried["details"], dict):
1680
+ retried["details"]["edit_lock_release_result"] = release_attempts[-1] if release_attempts else None
1681
+ retried["details"]["edit_lock_release_attempts"] = release_attempts
1682
+ retried["edit_lock_released"] = bool(release_attempts)
1683
+ retried["retried_after_edit_lock_release"] = True
1684
+ return retried
1685
+
1686
+ def _rewrite_plan_result_for_apply(
1687
+ self,
1688
+ *,
1689
+ result: JSONObject,
1690
+ profile: str,
1691
+ publish: bool,
1692
+ plan_tool_name: str,
1693
+ apply_tool_name: str,
1694
+ ) -> JSONObject:
1695
+ if not isinstance(result, dict):
1696
+ return result
1697
+ rewritten = dict(result)
1698
+ if rewritten.get("error_code") == "VALIDATION_ERROR":
1699
+ contract = _BUILDER_TOOL_CONTRACTS.get(apply_tool_name)
1700
+ details = rewritten.get("details")
1701
+ if not isinstance(details, dict):
1702
+ details = {}
1703
+ rewritten["details"] = details
1704
+ if isinstance(contract, dict):
1705
+ rewritten["allowed_values"] = deepcopy(contract.get("allowed_values", {}))
1706
+ details["allowed_keys"] = deepcopy(contract.get("allowed_keys", []))
1707
+ details["section_allowed_keys"] = deepcopy(contract.get("section_allowed_keys", []))
1708
+ details["section_aliases"] = deepcopy(contract.get("section_aliases", {}))
1709
+ details["minimal_section_example"] = deepcopy(contract.get("minimal_section_example"))
1710
+ suggested_next_call = rewritten.get("suggested_next_call")
1711
+ if isinstance(suggested_next_call, dict):
1712
+ if suggested_next_call.get("tool_name") == plan_tool_name:
1713
+ arguments = suggested_next_call.get("arguments")
1714
+ normalized_arguments = dict(arguments) if isinstance(arguments, dict) else {}
1715
+ normalized_arguments.setdefault("profile", profile)
1716
+ normalized_arguments["publish"] = publish
1717
+ rewritten["suggested_next_call"] = {
1718
+ **suggested_next_call,
1719
+ "tool_name": apply_tool_name,
1720
+ "arguments": normalized_arguments,
1721
+ }
1722
+ return rewritten
1723
+ if rewritten.get("error_code") == "VALIDATION_ERROR" and suggested_next_call.get("tool_name") == apply_tool_name:
1724
+ arguments = suggested_next_call.get("arguments")
1725
+ normalized_arguments = dict(arguments) if isinstance(arguments, dict) else {}
1726
+ normalized_arguments.setdefault("profile", profile)
1727
+ normalized_arguments["publish"] = publish
1728
+ if isinstance(details, dict):
1729
+ details["canonical_arguments"] = normalized_arguments
1730
+ rewritten["suggested_next_call"] = {
1731
+ **suggested_next_call,
1732
+ "arguments": normalized_arguments,
1733
+ }
1734
+ if rewritten.get("status") == "success":
1735
+ normalized_args = rewritten.get("normalized_args")
1736
+ if isinstance(normalized_args, dict):
1737
+ rewritten["suggested_next_call"] = {
1738
+ "tool_name": apply_tool_name,
1739
+ "arguments": {"profile": profile, **normalized_args, "publish": publish},
1740
+ }
1741
+ return rewritten
742
1742
 
743
1743
 
744
- def _validation_failure(detail: str, *, suggested_next_call: JSONObject | None = None) -> JSONObject:
1744
+ def _validation_failure(
1745
+ detail: str,
1746
+ *,
1747
+ tool_name: str | None = None,
1748
+ exc: ValidationError | None = None,
1749
+ suggested_next_call: JSONObject | None = None,
1750
+ ) -> JSONObject:
1751
+ contract = _BUILDER_TOOL_CONTRACTS.get(tool_name or "")
1752
+ reason_path = None
1753
+ if exc is not None:
1754
+ errors = exc.errors()
1755
+ if errors:
1756
+ loc = errors[0].get("loc")
1757
+ if isinstance(loc, (tuple, list)):
1758
+ reason_path = ".".join(str(part) for part in loc)
1759
+ canonical_arguments = None
1760
+ if isinstance(suggested_next_call, dict):
1761
+ arguments = suggested_next_call.get("arguments")
1762
+ if isinstance(arguments, dict):
1763
+ canonical_arguments = arguments
745
1764
  return {
746
1765
  "status": "failed",
747
1766
  "error_code": "VALIDATION_ERROR",
@@ -749,8 +1768,16 @@ def _validation_failure(detail: str, *, suggested_next_call: JSONObject | None =
749
1768
  "message": detail,
750
1769
  "normalized_args": {},
751
1770
  "missing_fields": [],
752
- "allowed_values": {},
753
- "details": {"validation_detail": detail},
1771
+ "allowed_values": deepcopy(contract.get("allowed_values", {})) if isinstance(contract, dict) else {},
1772
+ "details": {
1773
+ "validation_detail": detail,
1774
+ "reason_path": reason_path,
1775
+ "allowed_keys": deepcopy(contract.get("allowed_keys", [])) if isinstance(contract, dict) else [],
1776
+ "canonical_arguments": canonical_arguments,
1777
+ "section_allowed_keys": deepcopy(contract.get("section_allowed_keys", [])) if isinstance(contract, dict) else [],
1778
+ "section_aliases": deepcopy(contract.get("section_aliases", {})) if isinstance(contract, dict) else {},
1779
+ "minimal_section_example": deepcopy(contract.get("minimal_section_example")) if isinstance(contract, dict) else None,
1780
+ },
754
1781
  "suggested_next_call": suggested_next_call,
755
1782
  "request_id": None,
756
1783
  "backend_code": None,
@@ -760,6 +1787,29 @@ def _validation_failure(detail: str, *, suggested_next_call: JSONObject | None =
760
1787
  }
761
1788
 
762
1789
 
1790
+ def _config_failure(*, tool_name: str, message: str, fix_hint: str) -> JSONObject:
1791
+ contract = _BUILDER_TOOL_CONTRACTS.get(tool_name or "")
1792
+ return {
1793
+ "status": "failed",
1794
+ "error_code": "CONFIG_ERROR",
1795
+ "recoverable": True,
1796
+ "message": message,
1797
+ "normalized_args": {},
1798
+ "missing_fields": [],
1799
+ "allowed_values": deepcopy(contract.get("allowed_values", {})) if isinstance(contract, dict) else {},
1800
+ "details": {
1801
+ "fix_hint": fix_hint,
1802
+ "allowed_keys": deepcopy(contract.get("allowed_keys", [])) if isinstance(contract, dict) else [],
1803
+ },
1804
+ "suggested_next_call": None,
1805
+ "request_id": None,
1806
+ "backend_code": None,
1807
+ "http_status": None,
1808
+ "noop": False,
1809
+ "verification": {},
1810
+ }
1811
+
1812
+
763
1813
  def _safe_tool_call(
764
1814
  call,
765
1815
  *,
@@ -827,6 +1877,11 @@ def _public_error_message(error_code: str, error: QingflowApiError) -> str:
827
1877
  "PACKAGE_RESOLVE_FAILED": "package resolution is unavailable in the current route",
828
1878
  "PACKAGE_ATTACH_FAILED": "package attachment could not be verified in the current route",
829
1879
  "APP_RESOLVE_FAILED": "app resolution is unavailable in the current route",
1880
+ "CUSTOM_BUTTON_LIST_FAILED": "custom button list is unavailable in the current route",
1881
+ "CUSTOM_BUTTON_GET_FAILED": "custom button detail is unavailable in the current route",
1882
+ "CUSTOM_BUTTON_CREATE_FAILED": "custom button create could not complete because the route or readback is unavailable",
1883
+ "CUSTOM_BUTTON_UPDATE_FAILED": "custom button update could not complete because the route or readback is unavailable",
1884
+ "CUSTOM_BUTTON_DELETE_FAILED": "custom button delete could not complete because the route is unavailable",
830
1885
  "APP_READ_FAILED": "app base or schema is unavailable in the current route",
831
1886
  "FIELDS_READ_FAILED": "app fields are unavailable in the current route",
832
1887
  "LAYOUT_READ_FAILED": "layout resource is unavailable for this app in the current route",
@@ -840,6 +1895,856 @@ def _public_error_message(error_code: str, error: QingflowApiError) -> str:
840
1895
  "LAYOUT_APPLY_FAILED": "layout apply could not complete because the layout route or readback is unavailable",
841
1896
  "FLOW_APPLY_FAILED": "flow apply could not complete because the workflow route or readback is unavailable",
842
1897
  "VIEWS_APPLY_FAILED": "views apply could not complete because the views route or readback is unavailable",
1898
+ "CHART_APPLY_FAILED": "chart apply could not complete because the QingBI route or readback is unavailable",
1899
+ "PORTAL_APPLY_FAILED": "portal apply could not complete because the portal route or readback is unavailable",
843
1900
  "PUBLISH_VERIFY_FAILED": "publish verification is unavailable in the current route",
844
1901
  }
845
1902
  return mapping.get(error_code, "requested builder resource is unavailable in the current route")
1903
+
1904
+
1905
+ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
1906
+ "package_list": {
1907
+ "allowed_keys": ["trial_status"],
1908
+ "aliases": {"trialStatus": "trial_status"},
1909
+ "allowed_values": {"trial_status": ["all", "trial", "formal"]},
1910
+ "minimal_example": {
1911
+ "profile": "default",
1912
+ "trial_status": "all",
1913
+ },
1914
+ },
1915
+ "package_resolve": {
1916
+ "allowed_keys": ["package_name"],
1917
+ "aliases": {"packageName": "package_name"},
1918
+ "allowed_values": {},
1919
+ "minimal_example": {
1920
+ "profile": "default",
1921
+ "package_name": "PLM(备用,施工中)",
1922
+ },
1923
+ },
1924
+ "package_create": {
1925
+ "allowed_keys": ["package_name", "icon", "color"],
1926
+ "aliases": {
1927
+ "packageName": "package_name",
1928
+ "iconName": "icon",
1929
+ "iconColor": "color",
1930
+ },
1931
+ "allowed_values": {},
1932
+ "minimal_example": {
1933
+ "profile": "default",
1934
+ "package_name": "项目管理",
1935
+ "icon": "files-folder",
1936
+ "color": "azure",
1937
+ },
1938
+ },
1939
+ "member_search": {
1940
+ "allowed_keys": ["query", "page_num", "page_size", "contain_disable"],
1941
+ "aliases": {},
1942
+ "allowed_values": {},
1943
+ "minimal_example": {
1944
+ "profile": "default",
1945
+ "query": "严琪东",
1946
+ "page_num": 1,
1947
+ "page_size": 20,
1948
+ "contain_disable": False,
1949
+ },
1950
+ },
1951
+ "role_search": {
1952
+ "allowed_keys": ["keyword", "page_num", "page_size"],
1953
+ "aliases": {},
1954
+ "allowed_values": {},
1955
+ "minimal_example": {
1956
+ "profile": "default",
1957
+ "keyword": "项目经理",
1958
+ "page_num": 1,
1959
+ "page_size": 20,
1960
+ },
1961
+ },
1962
+ "role_create": {
1963
+ "allowed_keys": ["role_name", "member_uids", "member_emails", "member_names", "role_icon"],
1964
+ "aliases": {},
1965
+ "allowed_values": {},
1966
+ "minimal_example": {
1967
+ "profile": "default",
1968
+ "role_name": "研发负责人",
1969
+ "member_names": ["严琪东"],
1970
+ "member_uids": [],
1971
+ "member_emails": [],
1972
+ "role_icon": "ex-user-outlined",
1973
+ },
1974
+ },
1975
+ "package_attach_app": {
1976
+ "allowed_keys": ["tag_id", "app_key"],
1977
+ "aliases": {},
1978
+ "allowed_values": {},
1979
+ "execution_notes": [
1980
+ "attach one existing app to one existing package",
1981
+ "app_title is no longer accepted as a public selector; resolve the app first and pass app_key",
1982
+ ],
1983
+ "minimal_example": {
1984
+ "profile": "default",
1985
+ "tag_id": 1001,
1986
+ "app_key": "APP_KEY",
1987
+ },
1988
+ },
1989
+ "app_resolve": {
1990
+ "allowed_keys": ["app_key", "app_name", "package_tag_id"],
1991
+ "aliases": {},
1992
+ "allowed_values": {},
1993
+ "execution_notes": [
1994
+ "use exactly one selector mode",
1995
+ "mode 1: app_key",
1996
+ "mode 2: app_name + package_tag_id",
1997
+ ],
1998
+ "minimal_example": {
1999
+ "profile": "default",
2000
+ "app_key": "APP_KEY",
2001
+ },
2002
+ "package_scoped_example": {
2003
+ "profile": "default",
2004
+ "app_name": "研发项目管理",
2005
+ "package_tag_id": 1001,
2006
+ },
2007
+ },
2008
+ "app_custom_button_list": {
2009
+ "allowed_keys": ["app_key"],
2010
+ "aliases": {},
2011
+ "allowed_values": {},
2012
+ "minimal_example": {
2013
+ "profile": "default",
2014
+ "app_key": "APP_KEY",
2015
+ },
2016
+ },
2017
+ "app_custom_button_get": {
2018
+ "allowed_keys": ["app_key", "button_id"],
2019
+ "aliases": {"buttonId": "button_id"},
2020
+ "allowed_values": {},
2021
+ "minimal_example": {
2022
+ "profile": "default",
2023
+ "app_key": "APP_KEY",
2024
+ "button_id": 1001,
2025
+ },
2026
+ },
2027
+ "app_custom_button_create": {
2028
+ "allowed_keys": ["app_key", "payload"],
2029
+ "aliases": {
2030
+ "payload.buttonText": "payload.button_text",
2031
+ "payload.backgroundColor": "payload.background_color",
2032
+ "payload.textColor": "payload.text_color",
2033
+ "payload.buttonIcon": "payload.button_icon",
2034
+ "payload.iconColor": "payload.icon_color",
2035
+ "payload.triggerAction": "payload.trigger_action",
2036
+ "payload.triggerLinkUrl": "payload.trigger_link_url",
2037
+ "payload.triggerAddDataConfig": "payload.trigger_add_data_config",
2038
+ "payload.externalQrobotConfig": "payload.external_qrobot_config",
2039
+ "payload.customButtonExternalQRobotRelationVO": "payload.external_qrobot_config",
2040
+ "payload.triggerWingsConfig": "payload.trigger_wings_config",
2041
+ },
2042
+ "allowed_values": {
2043
+ "payload.trigger_action": [member.value for member in PublicButtonTriggerAction],
2044
+ },
2045
+ "minimal_example": {
2046
+ "profile": "default",
2047
+ "app_key": "APP_KEY",
2048
+ "payload": {
2049
+ "button_text": "新增记录",
2050
+ "background_color": "#FFFFFF",
2051
+ "text_color": "#494F57",
2052
+ "button_icon": "ex-add-outlined",
2053
+ "icon_color": "#494F57",
2054
+ "trigger_action": "link",
2055
+ "trigger_link_url": "https://example.com",
2056
+ },
2057
+ },
2058
+ },
2059
+ "app_custom_button_update": {
2060
+ "allowed_keys": ["app_key", "button_id", "payload"],
2061
+ "aliases": {
2062
+ "buttonId": "button_id",
2063
+ "payload.buttonText": "payload.button_text",
2064
+ "payload.backgroundColor": "payload.background_color",
2065
+ "payload.textColor": "payload.text_color",
2066
+ "payload.buttonIcon": "payload.button_icon",
2067
+ "payload.iconColor": "payload.icon_color",
2068
+ "payload.triggerAction": "payload.trigger_action",
2069
+ "payload.triggerLinkUrl": "payload.trigger_link_url",
2070
+ "payload.triggerAddDataConfig": "payload.trigger_add_data_config",
2071
+ "payload.externalQrobotConfig": "payload.external_qrobot_config",
2072
+ "payload.customButtonExternalQRobotRelationVO": "payload.external_qrobot_config",
2073
+ "payload.triggerWingsConfig": "payload.trigger_wings_config",
2074
+ },
2075
+ "allowed_values": {
2076
+ "payload.trigger_action": [member.value for member in PublicButtonTriggerAction],
2077
+ },
2078
+ "minimal_example": {
2079
+ "profile": "default",
2080
+ "app_key": "APP_KEY",
2081
+ "button_id": 1001,
2082
+ "payload": {
2083
+ "button_text": "查看详情",
2084
+ "background_color": "#FFFFFF",
2085
+ "text_color": "#494F57",
2086
+ "button_icon": "ex-link-outlined",
2087
+ "icon_color": "#494F57",
2088
+ "trigger_action": "link",
2089
+ "trigger_link_url": "https://example.com/detail",
2090
+ },
2091
+ },
2092
+ },
2093
+ "app_custom_button_delete": {
2094
+ "allowed_keys": ["app_key", "button_id"],
2095
+ "aliases": {"buttonId": "button_id"},
2096
+ "allowed_values": {},
2097
+ "minimal_example": {
2098
+ "profile": "default",
2099
+ "app_key": "APP_KEY",
2100
+ "button_id": 1001,
2101
+ },
2102
+ },
2103
+ "app_schema_plan": {
2104
+ "allowed_keys": ["app_key", "package_tag_id", "app_name", "icon", "color", "create_if_missing", "add_fields", "update_fields", "remove_fields"],
2105
+ "aliases": {
2106
+ "app_title": "app_name",
2107
+ "title": "app_name",
2108
+ "field.title": "field.name",
2109
+ "field.label": "field.name",
2110
+ "field.fields": "field.subfields",
2111
+ "field.type_id": "field.type",
2112
+ "field.relationMode": "field.relation_mode",
2113
+ "field.selection_mode": "field.relation_mode",
2114
+ "field.selectionMode": "field.relation_mode",
2115
+ "field.multiple": "field.relation_mode",
2116
+ "field.allow_multiple": "field.relation_mode",
2117
+ "field.optional_data_num": "field.relation_mode",
2118
+ "field.optionalDataNum": "field.relation_mode",
2119
+ "field.remoteLookupConfig": "field.remote_lookup_config",
2120
+ "field.qLinkerBinding": "field.q_linker_binding",
2121
+ "field.codeBlockConfig": "field.code_block_config",
2122
+ "field.codeBlockBinding": "field.code_block_binding",
2123
+ "field.autoTrigger": "field.auto_trigger",
2124
+ "field.customBtnTextStatus": "field.custom_button_text_enabled",
2125
+ "field.customBtnText": "field.custom_button_text",
2126
+ },
2127
+ "allowed_values": {
2128
+ "field.type": [member.value for member in PublicFieldType],
2129
+ "field.relation_mode": [member.value for member in PublicRelationMode],
2130
+ "field_type_ids": sorted(FIELD_TYPE_ID_ALIASES.keys()),
2131
+ },
2132
+ "minimal_example": {
2133
+ "profile": "default",
2134
+ "app_name": "研发项目管理",
2135
+ "package_tag_id": 1001,
2136
+ "icon": "template",
2137
+ "color": "emerald",
2138
+ "create_if_missing": True,
2139
+ "add_fields": [{"name": "项目名称", "type": "text"}],
2140
+ "update_fields": [],
2141
+ "remove_fields": [],
2142
+ },
2143
+ "relation_example": {
2144
+ "profile": "default",
2145
+ "app_key": "APP_ITERATION",
2146
+ "add_fields": [
2147
+ {
2148
+ "name": "需求反馈引用",
2149
+ "type": "relation",
2150
+ "target_app_key": "APP_FEEDBACK",
2151
+ "relation_mode": "multiple",
2152
+ "display_field": {"name": "反馈标题"},
2153
+ "visible_fields": [{"name": "反馈标题"}, {"name": "优先级判断"}],
2154
+ }
2155
+ ],
2156
+ "update_fields": [],
2157
+ "remove_fields": [],
2158
+ },
2159
+ },
2160
+ "app_schema_apply": {
2161
+ "allowed_keys": ["app_key", "package_tag_id", "app_name", "icon", "color", "create_if_missing", "publish", "add_fields", "update_fields", "remove_fields"],
2162
+ "aliases": {
2163
+ "app_title": "app_name",
2164
+ "title": "app_name",
2165
+ "field.title": "field.name",
2166
+ "field.label": "field.name",
2167
+ "field.fields": "field.subfields",
2168
+ "field.type_id": "field.type",
2169
+ "field.relationMode": "field.relation_mode",
2170
+ "field.selection_mode": "field.relation_mode",
2171
+ "field.selectionMode": "field.relation_mode",
2172
+ "field.multiple": "field.relation_mode",
2173
+ "field.allow_multiple": "field.relation_mode",
2174
+ "field.optional_data_num": "field.relation_mode",
2175
+ "field.optionalDataNum": "field.relation_mode",
2176
+ "field.remoteLookupConfig": "field.remote_lookup_config",
2177
+ "field.qLinkerBinding": "field.q_linker_binding",
2178
+ "field.codeBlockConfig": "field.code_block_config",
2179
+ "field.codeBlockBinding": "field.code_block_binding",
2180
+ "field.autoTrigger": "field.auto_trigger",
2181
+ "field.customBtnTextStatus": "field.custom_button_text_enabled",
2182
+ "field.customBtnText": "field.custom_button_text",
2183
+ },
2184
+ "allowed_values": {
2185
+ "field.type": [member.value for member in PublicFieldType],
2186
+ "field.relation_mode": [member.value for member in PublicRelationMode],
2187
+ "field.code_block_binding.outputs.target_field.type": list(INTEGRATION_OUTPUT_TARGET_FIELD_TYPES),
2188
+ "field.q_linker_binding.outputs.target_field.type": list(INTEGRATION_OUTPUT_TARGET_FIELD_TYPES),
2189
+ "field_type_ids": sorted(FIELD_TYPE_ID_ALIASES.keys()),
2190
+ },
2191
+ "execution_notes": [
2192
+ "use exactly one resource mode",
2193
+ "edit mode: app_key",
2194
+ "create mode: package_tag_id + app_name + create_if_missing=true",
2195
+ "multiple relation fields are backend-risky; read verification.relation_field_limit_verified and warnings before declaring the schema stable",
2196
+ "backend 49614 is normalized to MULTIPLE_RELATION_FIELDS_UNSUPPORTED with a workaround message",
2197
+ "relation_mode=multiple maps to referenceConfig.optionalDataNum=0",
2198
+ "if relation target metadata lookup is blocked by 40161/40002/40027, explicit display_field.name and visible_fields[].name let builder degrade verification and still continue schema write",
2199
+ "q_linker_binding lets you declare request config, dynamic inputs, alias parsing, and target-field bindings in one step; builder writes remoteLookupConfig plus the existing backend relation-default and questionRelations structures",
2200
+ "code_block_binding lets you declare inputs, code, alias parsing, and target-field bindings in one step; builder writes codeBlockConfig plus the existing backend relation-default and questionRelations structures",
2201
+ "builder configures code blocks only; it does not execute or trigger code blocks",
2202
+ "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",
2203
+ ],
2204
+ "minimal_example": {
2205
+ "profile": "default",
2206
+ "app_name": "研发项目管理",
2207
+ "package_tag_id": 1001,
2208
+ "icon": "template",
2209
+ "color": "emerald",
2210
+ "create_if_missing": True,
2211
+ "publish": True,
2212
+ "add_fields": [{"name": "项目名称", "type": "text"}],
2213
+ "update_fields": [],
2214
+ "remove_fields": [],
2215
+ },
2216
+ "relation_example": {
2217
+ "profile": "default",
2218
+ "app_key": "APP_ITERATION",
2219
+ "publish": True,
2220
+ "add_fields": [
2221
+ {
2222
+ "name": "需求反馈引用",
2223
+ "type": "relation",
2224
+ "target_app_key": "APP_FEEDBACK",
2225
+ "relation_mode": "multiple",
2226
+ "display_field": {"name": "反馈标题"},
2227
+ "visible_fields": [{"name": "反馈标题"}, {"name": "优先级判断"}],
2228
+ }
2229
+ ],
2230
+ "update_fields": [],
2231
+ "remove_fields": [],
2232
+ },
2233
+ "code_block_example": {
2234
+ "profile": "default",
2235
+ "app_key": "APP_SCRIPT",
2236
+ "publish": True,
2237
+ "add_fields": [
2238
+ {"name": "客户等级", "type": "text"},
2239
+ {
2240
+ "name": "查询代码块",
2241
+ "type": "code_block",
2242
+ "code_block_binding": {
2243
+ "inputs": [
2244
+ {"field": {"name": "客户名称"}, "var": "customerName"},
2245
+ {"field": {"name": "预算金额"}, "var": "budget"},
2246
+ ],
2247
+ "code": "qf_output = {}; qf_output.customerLevel = budget > 100000 ? 'A' : 'B';",
2248
+ "auto_trigger": True,
2249
+ "custom_button_text_enabled": True,
2250
+ "custom_button_text": "评估客户",
2251
+ "outputs": [
2252
+ {"alias": "customerLevel", "path": "$.customerLevel", "target_field": {"name": "客户等级"}},
2253
+ ],
2254
+ },
2255
+ }
2256
+ ],
2257
+ "update_fields": [],
2258
+ "remove_fields": [],
2259
+ },
2260
+ "q_linker_example": {
2261
+ "profile": "default",
2262
+ "app_key": "APP_CUSTOMER",
2263
+ "publish": True,
2264
+ "add_fields": [
2265
+ {"name": "客户名称", "type": "text"},
2266
+ {"name": "企业名称", "type": "text"},
2267
+ {"name": "统一社会信用代码", "type": "text"},
2268
+ {
2269
+ "name": "企业信息查询",
2270
+ "type": "q_linker",
2271
+ "q_linker_binding": {
2272
+ "inputs": [
2273
+ {"field": {"name": "客户名称"}, "key": "keyword", "source": "query_param"},
2274
+ ],
2275
+ "request": {
2276
+ "url": "https://example.com/company/search",
2277
+ "method": "GET",
2278
+ "headers": [],
2279
+ "query_params": [],
2280
+ "body_type": 1,
2281
+ "url_encoded_value": [],
2282
+ "result_type": 1,
2283
+ "auto_trigger": True,
2284
+ "custom_button_text_enabled": True,
2285
+ "custom_button_text": "查询企业信息",
2286
+ },
2287
+ "outputs": [
2288
+ {"alias": "company_name", "path": "$.data.name", "target_field": {"name": "企业名称"}},
2289
+ {"alias": "credit_code", "path": "$.data.creditCode", "target_field": {"name": "统一社会信用代码"}},
2290
+ ],
2291
+ },
2292
+ },
2293
+ ],
2294
+ "update_fields": [],
2295
+ "remove_fields": [],
2296
+ },
2297
+ },
2298
+ "app_layout_plan": {
2299
+ "allowed_keys": ["app_key", "mode", "sections", "preset"],
2300
+ "aliases": {"overwrite": "replace", "sectionId": "section_id"},
2301
+ "section_allowed_keys": ["type", "paragraph_id", "section_id", "title", "rows"],
2302
+ "section_aliases": {
2303
+ "name": "title",
2304
+ "paragraphId": "paragraph_id",
2305
+ "sectionId": "section_id",
2306
+ "fields": "rows",
2307
+ "field_ids": "rows",
2308
+ "columns": "rows_chunk_size",
2309
+ },
2310
+ "allowed_values": {"mode": [member.value for member in LayoutApplyMode], "preset": [member.value for member in LayoutPreset]},
2311
+ "minimal_section_example": {"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["字段A", "字段B", "字段C", "字段D"]]},
2312
+ "minimal_example": {
2313
+ "profile": "default",
2314
+ "app_key": "APP_KEY",
2315
+ "mode": "merge",
2316
+ "sections": [{"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["字段A", "字段B", "字段C", "字段D"]]}],
2317
+ },
2318
+ "preset_example": {"profile": "default", "app_key": "APP_KEY", "mode": "merge", "preset": "balanced", "sections": []},
2319
+ },
2320
+ "app_layout_apply": {
2321
+ "allowed_keys": ["app_key", "mode", "publish", "sections"],
2322
+ "aliases": {"overwrite": "replace", "sectionId": "section_id"},
2323
+ "section_allowed_keys": ["type", "paragraph_id", "section_id", "title", "rows"],
2324
+ "section_aliases": {
2325
+ "name": "title",
2326
+ "paragraphId": "paragraph_id",
2327
+ "sectionId": "section_id",
2328
+ "fields": "rows",
2329
+ "field_ids": "rows",
2330
+ "columns": "rows_chunk_size",
2331
+ },
2332
+ "allowed_values": {"mode": [member.value for member in LayoutApplyMode]},
2333
+ "execution_notes": [
2334
+ "layout verification is split into layout_verified and layout_summary_verified",
2335
+ "LAYOUT_SUMMARY_UNVERIFIED means raw form readback is stronger than the compact summary",
2336
+ ],
2337
+ "minimal_section_example": {"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["字段A", "字段B", "字段C", "字段D"]]},
2338
+ "minimal_example": {
2339
+ "profile": "default",
2340
+ "app_key": "APP_KEY",
2341
+ "mode": "merge",
2342
+ "publish": True,
2343
+ "sections": [{"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["项目名称", "项目负责人", "项目阶段", "优先级"]]}],
2344
+ },
2345
+ },
2346
+ "app_flow_plan": {
2347
+ "allowed_keys": ["app_key", "mode", "nodes", "transitions", "preset"],
2348
+ "aliases": {
2349
+ "overwrite": "replace",
2350
+ "base_preset": "preset",
2351
+ "default_approval": "basic_approval",
2352
+ "node.role_names": "node.assignees.role_names",
2353
+ "node.role_ids": "node.assignees.role_ids",
2354
+ "node.member_names": "node.assignees.member_names",
2355
+ "node.member_emails": "node.assignees.member_emails",
2356
+ "node.member_uids": "node.assignees.member_uids",
2357
+ "node.editable_fields": "node.permissions.editable_fields",
2358
+ "default_approval": "basic_approval",
2359
+ },
2360
+ "allowed_values": {
2361
+ "mode": ["replace"],
2362
+ "preset": [member.value for member in FlowPreset],
2363
+ "node.type": PUBLIC_STABLE_FLOW_NODE_TYPES,
2364
+ },
2365
+ "dependency_hints": [
2366
+ "approval-style workflows require an explicit status field",
2367
+ "approve/fill/copy nodes require at least one assignee",
2368
+ ],
2369
+ "execution_notes": [
2370
+ "public flow building is intentionally limited to linear workflows",
2371
+ "branch and condition nodes are disabled because the backend workflow route is not front-end stable for these node types",
2372
+ ],
2373
+ "minimal_example": {
2374
+ "profile": "default",
2375
+ "app_key": "APP_KEY",
2376
+ "mode": "replace",
2377
+ "preset": "basic_approval",
2378
+ "nodes": [
2379
+ {
2380
+ "id": "approve_1",
2381
+ "type": "approve",
2382
+ "name": "部门审批",
2383
+ "assignees": {"role_names": ["项目经理"]},
2384
+ "permissions": {"editable_fields": ["状态", "审批意见"]},
2385
+ }
2386
+ ],
2387
+ "transitions": [],
2388
+ },
2389
+ },
2390
+ "app_flow_apply": {
2391
+ "allowed_keys": ["app_key", "mode", "publish", "nodes", "transitions"],
2392
+ "aliases": {
2393
+ "overwrite": "replace",
2394
+ "node.role_names": "node.assignees.role_names",
2395
+ "node.role_ids": "node.assignees.role_ids",
2396
+ "node.member_names": "node.assignees.member_names",
2397
+ "node.member_emails": "node.assignees.member_emails",
2398
+ "node.member_uids": "node.assignees.member_uids",
2399
+ "node.editable_fields": "node.permissions.editable_fields",
2400
+ },
2401
+ "allowed_values": {
2402
+ "mode": ["replace"],
2403
+ "node.type": PUBLIC_STABLE_FLOW_NODE_TYPES,
2404
+ },
2405
+ "dependency_hints": [
2406
+ "approval-style workflows require an explicit status field",
2407
+ "approve/fill/copy nodes require at least one assignee",
2408
+ ],
2409
+ "execution_notes": [
2410
+ "public flow building is intentionally limited to linear workflows",
2411
+ "branch and condition nodes are disabled because the backend workflow route is not front-end stable for these node types",
2412
+ "workflow verification only covers linear node structure in the public tool surface",
2413
+ ],
2414
+ "minimal_example": {
2415
+ "profile": "default",
2416
+ "app_key": "APP_KEY",
2417
+ "mode": "replace",
2418
+ "publish": True,
2419
+ "nodes": [
2420
+ {"id": "start", "type": "start", "name": "发起"},
2421
+ {
2422
+ "id": "approve_1",
2423
+ "type": "approve",
2424
+ "name": "部门审批",
2425
+ "assignees": {"role_names": ["项目经理"]},
2426
+ "permissions": {"editable_fields": ["状态", "审批意见"]},
2427
+ },
2428
+ {"id": "end", "type": "end", "name": "结束"},
2429
+ ],
2430
+ "transitions": [{"from": "start", "to": "approve_1"}, {"from": "approve_1", "to": "end"}],
2431
+ },
2432
+ },
2433
+ "app_views_plan": {
2434
+ "allowed_keys": ["app_key", "upsert_views", "remove_views", "preset", "upsert_views[].view_key", "upsert_views[].buttons"],
2435
+ "aliases": {
2436
+ "fields": "columns",
2437
+ "column_names": "columns",
2438
+ "columnNames": "columns",
2439
+ "viewKey": "view_key",
2440
+ "tableView": "table",
2441
+ "cardView": "card",
2442
+ "kanban": "board",
2443
+ "filter_rules": "filters",
2444
+ "filterRules": "filters",
2445
+ "startField": "start_field",
2446
+ "endField": "end_field",
2447
+ "titleField": "title_field",
2448
+ "buttons[].buttonType": "buttons[].button_type",
2449
+ "buttons[].configType": "buttons[].config_type",
2450
+ "buttons[].buttonId": "buttons[].button_id",
2451
+ "buttons[].beingMain": "buttons[].being_main",
2452
+ "buttons[].buttonLimit": "buttons[].button_limit",
2453
+ "buttons[].buttonFormula": "buttons[].button_formula",
2454
+ "buttons[].buttonFormulaType": "buttons[].button_formula_type",
2455
+ "buttons[].printTpls": "buttons[].print_tpls",
2456
+ },
2457
+ "allowed_values": {
2458
+ "preset": [member.value for member in ViewsPreset],
2459
+ "view.type": [member.value for member in PublicViewType],
2460
+ "view.filter.operator": [member.value for member in ViewFilterOperator],
2461
+ "view.buttons.button_type": ["SYSTEM", "CUSTOM"],
2462
+ "view.buttons.config_type": ["TOP", "DETAIL"],
2463
+ },
2464
+ "minimal_example": {
2465
+ "profile": "default",
2466
+ "app_key": "APP_KEY",
2467
+ "upsert_views": [{"name": "全部数据", "type": "table", "columns": ["项目名称"]}],
2468
+ "remove_views": [],
2469
+ },
2470
+ "gantt_example": {
2471
+ "profile": "default",
2472
+ "app_key": "APP_KEY",
2473
+ "upsert_views": [
2474
+ {
2475
+ "name": "项目甘特图",
2476
+ "type": "gantt",
2477
+ "columns": ["项目名称", "开始日期", "结束日期"],
2478
+ "start_field": "开始日期",
2479
+ "end_field": "结束日期",
2480
+ "title_field": "项目名称",
2481
+ "filters": [{"field_name": "状态", "operator": "eq", "value": "进行中"}],
2482
+ }
2483
+ ],
2484
+ "remove_views": [],
2485
+ },
2486
+ },
2487
+ "app_views_apply": {
2488
+ "allowed_keys": ["app_key", "publish", "upsert_views", "remove_views", "upsert_views[].view_key", "upsert_views[].buttons"],
2489
+ "aliases": {
2490
+ "fields": "columns",
2491
+ "column_names": "columns",
2492
+ "columnNames": "columns",
2493
+ "viewKey": "view_key",
2494
+ "tableView": "table",
2495
+ "cardView": "card",
2496
+ "kanban": "board",
2497
+ "filter_rules": "filters",
2498
+ "filterRules": "filters",
2499
+ "startField": "start_field",
2500
+ "endField": "end_field",
2501
+ "titleField": "title_field",
2502
+ "buttons[].buttonType": "buttons[].button_type",
2503
+ "buttons[].configType": "buttons[].config_type",
2504
+ "buttons[].buttonId": "buttons[].button_id",
2505
+ "buttons[].beingMain": "buttons[].being_main",
2506
+ "buttons[].buttonLimit": "buttons[].button_limit",
2507
+ "buttons[].buttonFormula": "buttons[].button_formula",
2508
+ "buttons[].buttonFormulaType": "buttons[].button_formula_type",
2509
+ "buttons[].printTpls": "buttons[].print_tpls",
2510
+ },
2511
+ "allowed_values": {
2512
+ "view.type": [member.value for member in PublicViewType],
2513
+ "view.filter.operator": [member.value for member in ViewFilterOperator],
2514
+ "view.buttons.button_type": ["SYSTEM", "CUSTOM"],
2515
+ "view.buttons.config_type": ["TOP", "DETAIL"],
2516
+ },
2517
+ "execution_notes": [
2518
+ "apply may return partial_success when some views land and others fail",
2519
+ "when duplicate view names exist, supply view_key to target the exact view",
2520
+ "read back app_read_views_summary after any failed or partial view apply",
2521
+ "view existence verification and saved-filter verification are separate; treat filters as unverified until verification.view_filters_verified is true",
2522
+ "buttons omitted preserves existing button config; buttons=[] clears all buttons; buttons=[...] replaces the full button config",
2523
+ ],
2524
+ "minimal_example": {
2525
+ "profile": "default",
2526
+ "app_key": "APP_KEY",
2527
+ "publish": True,
2528
+ "upsert_views": [{"name": "全部数据", "type": "table", "columns": ["项目名称"]}],
2529
+ "remove_views": [],
2530
+ },
2531
+ "gantt_example": {
2532
+ "profile": "default",
2533
+ "app_key": "APP_KEY",
2534
+ "publish": True,
2535
+ "upsert_views": [
2536
+ {
2537
+ "name": "项目甘特图",
2538
+ "type": "gantt",
2539
+ "columns": ["项目名称", "开始日期", "结束日期"],
2540
+ "start_field": "开始日期",
2541
+ "end_field": "结束日期",
2542
+ "title_field": "项目名称",
2543
+ "filters": [{"field_name": "状态", "operator": "eq", "value": "进行中"}],
2544
+ }
2545
+ ],
2546
+ "remove_views": [],
2547
+ },
2548
+ },
2549
+ "app_read_charts_summary": {
2550
+ "allowed_keys": ["app_key"],
2551
+ "aliases": {},
2552
+ "allowed_values": {},
2553
+ "execution_notes": [
2554
+ "returns a compact current chart inventory for one app",
2555
+ "use this before app_charts_apply when you need exact current chart_id values",
2556
+ "chart summaries do not include full qingbi config payloads",
2557
+ ],
2558
+ "minimal_example": {
2559
+ "profile": "default",
2560
+ "app_key": "APP_KEY",
2561
+ },
2562
+ },
2563
+ "portal_list": {
2564
+ "allowed_keys": [],
2565
+ "aliases": {},
2566
+ "allowed_values": {},
2567
+ "execution_notes": [
2568
+ "returns the current user's accessible portal list",
2569
+ "use this as the portal discovery path before portal_get",
2570
+ "results are compact list items, not raw dash payloads",
2571
+ ],
2572
+ "minimal_example": {
2573
+ "profile": "default",
2574
+ },
2575
+ },
2576
+ "portal_get": {
2577
+ "allowed_keys": ["dash_key", "being_draft"],
2578
+ "aliases": {"beingDraft": "being_draft"},
2579
+ "allowed_values": {},
2580
+ "execution_notes": [
2581
+ "returns portal-level detail plus a component inventory",
2582
+ "chart and view components are returned as refs only; use chart_get or view_get for more detail",
2583
+ "being_draft=true reads the current draft view; being_draft=false reads live",
2584
+ ],
2585
+ "minimal_example": {
2586
+ "profile": "default",
2587
+ "dash_key": "DASH_KEY",
2588
+ "being_draft": True,
2589
+ },
2590
+ },
2591
+ "app_charts_apply": {
2592
+ "allowed_keys": ["app_key", "upsert_charts", "remove_chart_ids", "reorder_chart_ids"],
2593
+ "aliases": {
2594
+ "chart.id": "chart.chart_id",
2595
+ "chart.type": "chart.chart_type",
2596
+ "chart.dimension_fields": "chart.dimension_field_ids",
2597
+ "chart.indicator_fields": "chart.indicator_field_ids",
2598
+ "chart.metric_field_ids": "chart.indicator_field_ids",
2599
+ "chart.filter.op": "chart.filter.operator",
2600
+ },
2601
+ "allowed_values": {
2602
+ "chart.chart_type": [member.value for member in PublicChartType],
2603
+ "chart.filter.operator": [member.value for member in ViewFilterOperator],
2604
+ },
2605
+ "execution_notes": [
2606
+ "app_charts_apply is immediate-live and does not publish",
2607
+ "chart matching precedence is chart_id first, then exact unique chart name",
2608
+ "when chart names are not unique, supply chart_id instead of guessing by name",
2609
+ "successful create results must return a real backend chart_id",
2610
+ ],
2611
+ "minimal_example": {
2612
+ "profile": "default",
2613
+ "app_key": "APP_KEY",
2614
+ "upsert_charts": [{"name": "数据总量", "chart_type": "target", "indicator_field_ids": []}],
2615
+ "remove_chart_ids": [],
2616
+ "reorder_chart_ids": [],
2617
+ },
2618
+ },
2619
+ "chart_apply": {
2620
+ "allowed_keys": ["app_key", "upsert_charts", "remove_chart_ids", "reorder_chart_ids"],
2621
+ "aliases": {
2622
+ "legacy_tool_name": "app_charts_apply",
2623
+ },
2624
+ "allowed_values": {
2625
+ "chart.chart_type": [member.value for member in PublicChartType],
2626
+ "chart.filter.operator": [member.value for member in ViewFilterOperator],
2627
+ },
2628
+ "execution_notes": [
2629
+ "legacy compatibility alias; prefer app_charts_apply in new builder flows",
2630
+ "behavior matches app_charts_apply exactly",
2631
+ ],
2632
+ "minimal_example": {
2633
+ "profile": "default",
2634
+ "app_key": "APP_KEY",
2635
+ "upsert_charts": [{"name": "数据总量", "chart_type": "target", "indicator_field_ids": []}],
2636
+ "remove_chart_ids": [],
2637
+ "reorder_chart_ids": [],
2638
+ },
2639
+ },
2640
+ "portal_read_summary": {
2641
+ "allowed_keys": ["dash_key", "being_draft"],
2642
+ "aliases": {"beingDraft": "being_draft"},
2643
+ "allowed_values": {},
2644
+ "execution_notes": [
2645
+ "returns a compact portal summary instead of the raw dash payload",
2646
+ "being_draft=true reads the current draft view; being_draft=false reads live",
2647
+ "use this before portal_apply when you need the current section inventory or target dash metadata",
2648
+ ],
2649
+ "minimal_example": {
2650
+ "profile": "default",
2651
+ "dash_key": "DASH_KEY",
2652
+ "being_draft": True,
2653
+ },
2654
+ },
2655
+ "view_get": {
2656
+ "allowed_keys": ["viewgraph_key"],
2657
+ "aliases": {"viewKey": "viewgraph_key"},
2658
+ "allowed_values": {},
2659
+ "execution_notes": [
2660
+ "returns one view's definition detail",
2661
+ "does not return record data; use record_list with app_key + view_id for rows",
2662
+ "use this after portal_get when a component references a view_ref.view_key",
2663
+ ],
2664
+ "minimal_example": {
2665
+ "profile": "default",
2666
+ "viewgraph_key": "VIEW_KEY",
2667
+ },
2668
+ },
2669
+ "chart_get": {
2670
+ "allowed_keys": ["chart_id", "data_payload", "page_num", "page_size", "page_num_y", "page_size_y"],
2671
+ "aliases": {"payload": "data_payload"},
2672
+ "allowed_values": {},
2673
+ "execution_notes": [
2674
+ "returns chart base info, chart config, and chart data together",
2675
+ "chart_id is required; chart names are not accepted here",
2676
+ "data_payload defaults to {} so chart_get queries concrete chart data by default",
2677
+ ],
2678
+ "minimal_example": {
2679
+ "profile": "default",
2680
+ "chart_id": "CHART_ID",
2681
+ "data_payload": {},
2682
+ },
2683
+ },
2684
+ "portal_apply": {
2685
+ "allowed_keys": ["dash_key", "dash_name", "package_tag_id", "publish", "sections", "auth", "icon", "color", "hide_copyright", "dash_global_config", "config"],
2686
+ "aliases": {
2687
+ "sourceType": "source_type",
2688
+ "chartRef": "chart_ref",
2689
+ "viewRef": "view_ref",
2690
+ "dashStyleConfigBO": "dash_style_config",
2691
+ },
2692
+ "section_allowed_keys": ["title", "source_type", "position", "dash_style_config", "config", "chart_ref", "view_ref", "text", "url"],
2693
+ "section_aliases": {
2694
+ "sourceType": "source_type",
2695
+ "chartRef": "chart_ref",
2696
+ "viewRef": "view_ref",
2697
+ "dashStyleConfigBO": "dash_style_config",
2698
+ },
2699
+ "allowed_values": {"section.source_type": ["chart", "view", "grid", "filter", "text", "link"]},
2700
+ "execution_notes": [
2701
+ "use exactly one resource mode",
2702
+ "update mode: dash_key",
2703
+ "create mode: package_tag_id + dash_name",
2704
+ "portal_apply uses replace semantics for sections",
2705
+ "remove a section by omitting it from the new sections list",
2706
+ "package_tag_id is required when creating a new portal",
2707
+ "publish=false only guarantees draft and base-info updates; it does not claim live has changed",
2708
+ "chart_ref resolves by chart_id first, then exact unique chart_name",
2709
+ "view_ref resolves by view_key first, then exact unique view_name",
2710
+ "position.pc/mobile is the canonical portal layout shape",
2711
+ ],
2712
+ "minimal_example": {
2713
+ "profile": "default",
2714
+ "dash_name": "经营门户",
2715
+ "package_tag_id": 1001,
2716
+ "publish": True,
2717
+ "sections": [
2718
+ {
2719
+ "title": "经营概览",
2720
+ "source_type": "chart",
2721
+ "chart_ref": {"app_key": "APP_KEY", "chart_name": "数据总量"},
2722
+ "position": {
2723
+ "pc": {"x": 0, "y": 0, "cols": 12, "rows": 6},
2724
+ "mobile": {"x": 0, "y": 0, "cols": 6, "rows": 6},
2725
+ },
2726
+ }
2727
+ ],
2728
+ },
2729
+ "minimal_section_example": {
2730
+ "title": "订单概览",
2731
+ "source_type": "view",
2732
+ "view_ref": {"app_key": "APP_KEY", "view_key": "VIEW_KEY"},
2733
+ "position": {
2734
+ "pc": {"x": 0, "y": 0, "cols": 24, "rows": 8},
2735
+ "mobile": {"x": 0, "y": 0, "cols": 6, "rows": 8},
2736
+ },
2737
+ },
2738
+ },
2739
+ }
2740
+
2741
+ _PRIVATE_BUILDER_TOOL_CONTRACTS = {
2742
+ "app_schema_plan",
2743
+ "app_layout_plan",
2744
+ "app_flow_plan",
2745
+ "app_views_plan",
2746
+ }
2747
+
2748
+ _BUILDER_TOOL_CONTRACT_ALIASES = {
2749
+ "chart_apply": "app_charts_apply",
2750
+ }