@josephyan/qingflow-app-builder-mcp 0.2.0-beta.4 → 0.2.0-beta.41

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 (39) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/pyproject.toml +3 -1
  4. package/skills/qingflow-app-builder/SKILL.md +154 -22
  5. package/skills/qingflow-app-builder/references/create-app.md +51 -21
  6. package/skills/qingflow-app-builder/references/environments.md +1 -1
  7. package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
  8. package/skills/qingflow-app-builder/references/gotchas.md +28 -1
  9. package/skills/qingflow-app-builder/references/solution-playbooks.md +14 -12
  10. package/skills/qingflow-app-builder/references/tool-selection.md +45 -17
  11. package/skills/qingflow-app-builder/references/update-flow.md +112 -25
  12. package/skills/qingflow-app-builder/references/update-layout.md +11 -24
  13. package/skills/qingflow-app-builder/references/update-schema.md +1 -23
  14. package/skills/qingflow-app-builder/references/update-views.md +87 -21
  15. package/src/qingflow_mcp/__init__.py +1 -1
  16. package/src/qingflow_mcp/backend_client.py +189 -0
  17. package/src/qingflow_mcp/builder_facade/models.py +584 -1
  18. package/src/qingflow_mcp/builder_facade/service.py +4698 -262
  19. package/src/qingflow_mcp/config.py +39 -0
  20. package/src/qingflow_mcp/import_store.py +121 -0
  21. package/src/qingflow_mcp/list_type_labels.py +24 -0
  22. package/src/qingflow_mcp/server.py +131 -16
  23. package/src/qingflow_mcp/server_app_builder.py +132 -72
  24. package/src/qingflow_mcp/server_app_user.py +143 -187
  25. package/src/qingflow_mcp/solution/compiler/form_compiler.py +14 -4
  26. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +41 -2
  27. package/src/qingflow_mcp/solution/executor.py +44 -7
  28. package/src/qingflow_mcp/tools/ai_builder_tools.py +1567 -144
  29. package/src/qingflow_mcp/tools/app_tools.py +243 -14
  30. package/src/qingflow_mcp/tools/approval_tools.py +411 -76
  31. package/src/qingflow_mcp/tools/directory_tools.py +203 -31
  32. package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
  33. package/src/qingflow_mcp/tools/file_tools.py +1 -0
  34. package/src/qingflow_mcp/tools/import_tools.py +1164 -0
  35. package/src/qingflow_mcp/tools/portal_tools.py +31 -0
  36. package/src/qingflow_mcp/tools/record_tools.py +4943 -1025
  37. package/src/qingflow_mcp/tools/task_context_tools.py +1335 -0
  38. package/src/qingflow_mcp/tools/task_tools.py +376 -225
  39. package/src/qingflow_mcp/tools/workflow_tools.py +78 -4
@@ -1,31 +1,52 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from copy import deepcopy
4
+ import json
5
+ import time
6
+
3
7
  from pydantic import ValidationError
4
8
 
5
9
  from ..config import DEFAULT_PROFILE
10
+ from ..errors import QingflowApiError
6
11
  from ..json_types import JSONObject
7
12
  from ..builder_facade.models import (
13
+ ChartApplyRequest,
14
+ FIELD_TYPE_ID_ALIASES,
8
15
  FieldPatch,
9
16
  FieldRemovePatch,
10
17
  FieldUpdatePatch,
18
+ FlowPreset,
11
19
  FlowNodePatch,
12
20
  FlowPlanRequest,
13
21
  FlowTransitionPatch,
14
22
  LayoutApplyMode,
15
23
  LayoutPlanRequest,
24
+ LayoutPreset,
16
25
  LayoutSectionPatch,
26
+ PortalApplyRequest,
27
+ PublicFieldType,
28
+ PublicChartType,
29
+ PublicViewType,
17
30
  SchemaPlanRequest,
31
+ ViewFilterOperator,
18
32
  ViewUpsertPatch,
33
+ ViewsPreset,
19
34
  ViewsPlanRequest,
20
35
  )
21
36
  from ..builder_facade.service import AiBuilderFacade
22
37
  from .app_tools import AppTools
23
38
  from .base import ToolBase
39
+ from .directory_tools import DirectoryTools
24
40
  from .package_tools import PackageTools
41
+ from .portal_tools import PortalTools
42
+ from .qingbi_report_tools import QingbiReportTools
43
+ from .role_tools import RoleTools
25
44
  from .solution_tools import SolutionTools
26
45
  from .view_tools import ViewTools
27
46
  from .workflow_tools import WorkflowTools
28
47
 
48
+ PUBLIC_STABLE_FLOW_NODE_TYPES = ["start", "approve", "fill", "copy", "webhook", "end"]
49
+
29
50
 
30
51
  class AiBuilderTools(ToolBase):
31
52
  def __init__(self, sessions, backend) -> None:
@@ -35,6 +56,10 @@ class AiBuilderTools(ToolBase):
35
56
  packages=PackageTools(sessions, backend),
36
57
  views=ViewTools(sessions, backend),
37
58
  workflows=WorkflowTools(sessions, backend),
59
+ portals=PortalTools(sessions, backend),
60
+ charts=QingbiReportTools(sessions, backend),
61
+ roles=RoleTools(sessions, backend),
62
+ directory=DirectoryTools(sessions, backend),
38
63
  solutions=SolutionTools(sessions, backend),
39
64
  )
40
65
 
@@ -47,6 +72,57 @@ class AiBuilderTools(ToolBase):
47
72
  def package_resolve(profile: str = DEFAULT_PROFILE, package_name: str = "") -> JSONObject:
48
73
  return self.package_resolve(profile=profile, package_name=package_name)
49
74
 
75
+ @mcp.tool()
76
+ def builder_tool_contract(tool_name: str = "") -> JSONObject:
77
+ return self.builder_tool_contract(tool_name=tool_name)
78
+
79
+ @mcp.tool()
80
+ def package_create(profile: str = DEFAULT_PROFILE, package_name: str = "") -> JSONObject:
81
+ return self.package_create(profile=profile, package_name=package_name)
82
+
83
+ @mcp.tool()
84
+ def member_search(
85
+ profile: str = DEFAULT_PROFILE,
86
+ query: str = "",
87
+ page_num: int = 1,
88
+ page_size: int = 20,
89
+ contain_disable: bool = False,
90
+ ) -> JSONObject:
91
+ return self.member_search(
92
+ profile=profile,
93
+ query=query,
94
+ page_num=page_num,
95
+ page_size=page_size,
96
+ contain_disable=contain_disable,
97
+ )
98
+
99
+ @mcp.tool()
100
+ def role_search(
101
+ profile: str = DEFAULT_PROFILE,
102
+ keyword: str = "",
103
+ page_num: int = 1,
104
+ page_size: int = 20,
105
+ ) -> JSONObject:
106
+ return self.role_search(profile=profile, keyword=keyword, page_num=page_num, page_size=page_size)
107
+
108
+ @mcp.tool()
109
+ def role_create(
110
+ profile: str = DEFAULT_PROFILE,
111
+ role_name: str = "",
112
+ member_uids: list[int] | None = None,
113
+ member_emails: list[str] | None = None,
114
+ member_names: list[str] | None = None,
115
+ role_icon: str = "ex-user-outlined",
116
+ ) -> JSONObject:
117
+ return self.role_create(
118
+ profile=profile,
119
+ role_name=role_name,
120
+ member_uids=member_uids or [],
121
+ member_emails=member_emails or [],
122
+ member_names=member_names or [],
123
+ role_icon=role_icon,
124
+ )
125
+
50
126
  @mcp.tool()
51
127
  def package_attach_app(
52
128
  profile: str = DEFAULT_PROFILE,
@@ -56,6 +132,20 @@ class AiBuilderTools(ToolBase):
56
132
  ) -> JSONObject:
57
133
  return self.package_attach_app(profile=profile, tag_id=tag_id, app_key=app_key, app_title=app_title)
58
134
 
135
+ @mcp.tool()
136
+ def app_release_edit_lock_if_mine(
137
+ profile: str = DEFAULT_PROFILE,
138
+ app_key: str = "",
139
+ lock_owner_email: str = "",
140
+ lock_owner_name: str = "",
141
+ ) -> JSONObject:
142
+ return self.app_release_edit_lock_if_mine(
143
+ profile=profile,
144
+ app_key=app_key,
145
+ lock_owner_email=lock_owner_email,
146
+ lock_owner_name=lock_owner_name,
147
+ )
148
+
59
149
  @mcp.tool()
60
150
  def app_resolve(
61
151
  profile: str = DEFAULT_PROFILE,
@@ -86,76 +176,16 @@ class AiBuilderTools(ToolBase):
86
176
  return self.app_read_flow_summary(profile=profile, app_key=app_key)
87
177
 
88
178
  @mcp.tool()
89
- def app_schema_plan(
90
- profile: str = DEFAULT_PROFILE,
91
- app_key: str = "",
92
- package_tag_id: int | None = None,
93
- app_name: str = "",
94
- create_if_missing: bool = False,
95
- add_fields: list[JSONObject] | None = None,
96
- update_fields: list[JSONObject] | None = None,
97
- remove_fields: list[JSONObject] | None = None,
98
- ) -> JSONObject:
99
- return self.app_schema_plan(
100
- profile=profile,
101
- app_key=app_key,
102
- package_tag_id=package_tag_id,
103
- app_name=app_name,
104
- create_if_missing=create_if_missing,
105
- add_fields=add_fields or [],
106
- update_fields=update_fields or [],
107
- remove_fields=remove_fields or [],
108
- )
179
+ def app_read_charts_summary(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
180
+ return self.app_read_charts_summary(profile=profile, app_key=app_key)
109
181
 
110
182
  @mcp.tool()
111
- def app_layout_plan(
183
+ def portal_read_summary(
112
184
  profile: str = DEFAULT_PROFILE,
113
- app_key: str = "",
114
- mode: str = "merge",
115
- sections: list[JSONObject] | None = None,
116
- preset: str | None = None,
185
+ dash_key: str = "",
186
+ being_draft: bool = True,
117
187
  ) -> JSONObject:
118
- return self.app_layout_plan(
119
- profile=profile,
120
- app_key=app_key,
121
- mode=mode,
122
- sections=sections or [],
123
- preset=preset,
124
- )
125
-
126
- @mcp.tool()
127
- def app_flow_plan(
128
- profile: str = DEFAULT_PROFILE,
129
- app_key: str = "",
130
- mode: str = "replace",
131
- nodes: list[JSONObject] | None = None,
132
- transitions: list[JSONObject] | None = None,
133
- preset: str | None = None,
134
- ) -> JSONObject:
135
- return self.app_flow_plan(
136
- profile=profile,
137
- app_key=app_key,
138
- mode=mode,
139
- nodes=nodes or [],
140
- transitions=transitions or [],
141
- preset=preset,
142
- )
143
-
144
- @mcp.tool()
145
- def app_views_plan(
146
- profile: str = DEFAULT_PROFILE,
147
- app_key: str = "",
148
- upsert_views: list[JSONObject] | None = None,
149
- remove_views: list[str] | None = None,
150
- preset: str | None = None,
151
- ) -> JSONObject:
152
- return self.app_views_plan(
153
- profile=profile,
154
- app_key=app_key,
155
- upsert_views=upsert_views or [],
156
- remove_views=remove_views or [],
157
- preset=preset,
158
- )
188
+ return self.portal_read_summary(profile=profile, dash_key=dash_key, being_draft=being_draft)
159
189
 
160
190
  @mcp.tool()
161
191
  def app_schema_apply(
@@ -227,6 +257,52 @@ class AiBuilderTools(ToolBase):
227
257
  remove_views=remove_views or [],
228
258
  )
229
259
 
260
+ @mcp.tool()
261
+ def app_charts_apply(
262
+ profile: str = DEFAULT_PROFILE,
263
+ app_key: str = "",
264
+ upsert_charts: list[JSONObject] | None = None,
265
+ remove_chart_ids: list[str] | None = None,
266
+ reorder_chart_ids: list[str] | None = None,
267
+ ) -> JSONObject:
268
+ return self.app_charts_apply(
269
+ profile=profile,
270
+ app_key=app_key,
271
+ upsert_charts=upsert_charts or [],
272
+ remove_chart_ids=remove_chart_ids or [],
273
+ reorder_chart_ids=reorder_chart_ids or [],
274
+ )
275
+
276
+ @mcp.tool()
277
+ def portal_apply(
278
+ profile: str = DEFAULT_PROFILE,
279
+ dash_key: str = "",
280
+ dash_name: str = "",
281
+ package_tag_id: int | None = None,
282
+ publish: bool = True,
283
+ sections: list[JSONObject] | None = None,
284
+ auth: JSONObject | None = None,
285
+ icon: str | None = None,
286
+ color: str | None = None,
287
+ hide_copyright: bool | None = None,
288
+ dash_global_config: JSONObject | None = None,
289
+ config: JSONObject | None = None,
290
+ ) -> JSONObject:
291
+ return self.portal_apply(
292
+ profile=profile,
293
+ dash_key=dash_key,
294
+ dash_name=dash_name,
295
+ package_tag_id=package_tag_id,
296
+ publish=publish,
297
+ sections=sections or [],
298
+ auth=auth,
299
+ icon=icon,
300
+ color=color,
301
+ hide_copyright=hide_copyright,
302
+ dash_global_config=dash_global_config,
303
+ config=config or {},
304
+ )
305
+
230
306
  @mcp.tool()
231
307
  def app_publish_verify(
232
308
  profile: str = DEFAULT_PROFILE,
@@ -240,31 +316,269 @@ class AiBuilderTools(ToolBase):
240
316
  )
241
317
 
242
318
  def package_list(self, *, profile: str, trial_status: str = "all") -> JSONObject:
243
- return self._facade.package_list(profile=profile, trial_status=trial_status)
319
+ normalized_args = {"trial_status": trial_status}
320
+ return _safe_tool_call(
321
+ lambda: self._facade.package_list(profile=profile, trial_status=trial_status),
322
+ error_code="PACKAGE_LIST_FAILED",
323
+ normalized_args=normalized_args,
324
+ suggested_next_call={"tool_name": "package_list", "arguments": {"profile": profile, "trial_status": trial_status}},
325
+ )
244
326
 
245
327
  def package_resolve(self, *, profile: str, package_name: str) -> JSONObject:
246
- return self._facade.package_resolve(profile=profile, package_name=package_name)
328
+ normalized_args = {"package_name": package_name}
329
+ return _safe_tool_call(
330
+ lambda: self._facade.package_resolve(profile=profile, package_name=package_name),
331
+ error_code="PACKAGE_RESOLVE_FAILED",
332
+ normalized_args=normalized_args,
333
+ suggested_next_call={"tool_name": "package_resolve", "arguments": {"profile": profile, "package_name": package_name}},
334
+ )
335
+
336
+ def builder_tool_contract(self, *, tool_name: str) -> JSONObject:
337
+ requested = str(tool_name or "").strip()
338
+ public_tool_names = sorted(
339
+ name
340
+ for name in _BUILDER_TOOL_CONTRACTS.keys()
341
+ if name not in _PRIVATE_BUILDER_TOOL_CONTRACTS and name not in _BUILDER_TOOL_CONTRACT_ALIASES
342
+ )
343
+ if requested in _PRIVATE_BUILDER_TOOL_CONTRACTS:
344
+ lookup_name = ""
345
+ elif requested in _BUILDER_TOOL_CONTRACTS:
346
+ lookup_name = requested
347
+ else:
348
+ lookup_name = _BUILDER_TOOL_CONTRACT_ALIASES.get(requested, requested)
349
+ contract = _BUILDER_TOOL_CONTRACTS.get(lookup_name)
350
+ if contract is None:
351
+ return {
352
+ "status": "failed",
353
+ "error_code": "TOOL_CONTRACT_NOT_FOUND",
354
+ "recoverable": True,
355
+ "message": "tool contract is not defined for the requested public builder tool",
356
+ "normalized_args": {"tool_name": requested},
357
+ "missing_fields": [],
358
+ "allowed_values": {"tool_name": public_tool_names},
359
+ "details": {"reason_path": "tool_name"},
360
+ "suggested_next_call": None,
361
+ "request_id": None,
362
+ "backend_code": None,
363
+ "http_status": None,
364
+ "noop": False,
365
+ "warnings": [],
366
+ "verification": {},
367
+ "verified": False,
368
+ }
369
+ return {
370
+ "status": "success",
371
+ "error_code": None,
372
+ "recoverable": False,
373
+ "message": "loaded builder tool contract",
374
+ "normalized_args": {"tool_name": requested},
375
+ "missing_fields": [],
376
+ "allowed_values": {},
377
+ "details": {},
378
+ "suggested_next_call": None,
379
+ "request_id": None,
380
+ "backend_code": None,
381
+ "http_status": None,
382
+ "noop": False,
383
+ "warnings": [],
384
+ "verification": {},
385
+ "verified": True,
386
+ "tool_name": requested,
387
+ "contract": contract,
388
+ }
389
+
390
+ def package_create(self, *, profile: str, package_name: str) -> JSONObject:
391
+ normalized_args = {"package_name": package_name}
392
+ return _safe_tool_call(
393
+ lambda: self._facade.package_create(profile=profile, package_name=package_name),
394
+ error_code="PACKAGE_CREATE_FAILED",
395
+ normalized_args=normalized_args,
396
+ suggested_next_call={"tool_name": "package_create", "arguments": {"profile": profile, "package_name": package_name}},
397
+ )
398
+
399
+ def member_search(
400
+ self,
401
+ *,
402
+ profile: str,
403
+ query: str,
404
+ page_num: int = 1,
405
+ page_size: int = 20,
406
+ contain_disable: bool = False,
407
+ ) -> JSONObject:
408
+ normalized_args = {
409
+ "query": query,
410
+ "page_num": page_num,
411
+ "page_size": page_size,
412
+ "contain_disable": contain_disable,
413
+ }
414
+ return _safe_tool_call(
415
+ lambda: self._facade.member_search(
416
+ profile=profile,
417
+ query=query,
418
+ page_num=page_num,
419
+ page_size=page_size,
420
+ contain_disable=contain_disable,
421
+ ),
422
+ error_code="MEMBER_SEARCH_FAILED",
423
+ normalized_args=normalized_args,
424
+ suggested_next_call={"tool_name": "member_search", "arguments": {"profile": profile, **normalized_args}},
425
+ )
426
+
427
+ def role_search(self, *, profile: str, keyword: str, page_num: int = 1, page_size: int = 20) -> JSONObject:
428
+ normalized_args = {"keyword": keyword, "page_num": page_num, "page_size": page_size}
429
+ return _safe_tool_call(
430
+ lambda: self._facade.role_search(profile=profile, keyword=keyword, page_num=page_num, page_size=page_size),
431
+ error_code="ROLE_SEARCH_FAILED",
432
+ normalized_args=normalized_args,
433
+ suggested_next_call={"tool_name": "role_search", "arguments": {"profile": profile, **normalized_args}},
434
+ )
435
+
436
+ def role_create(
437
+ self,
438
+ *,
439
+ profile: str,
440
+ role_name: str,
441
+ member_uids: list[int],
442
+ member_emails: list[str],
443
+ member_names: list[str],
444
+ role_icon: str = "ex-user-outlined",
445
+ ) -> JSONObject:
446
+ normalized_args = {
447
+ "role_name": role_name,
448
+ "member_uids": member_uids,
449
+ "member_emails": member_emails,
450
+ "member_names": member_names,
451
+ "role_icon": role_icon,
452
+ }
453
+ return _safe_tool_call(
454
+ lambda: self._facade.role_create(
455
+ profile=profile,
456
+ role_name=role_name,
457
+ member_uids=member_uids,
458
+ member_emails=member_emails,
459
+ member_names=member_names,
460
+ role_icon=role_icon,
461
+ ),
462
+ error_code="ROLE_CREATE_FAILED",
463
+ normalized_args=normalized_args,
464
+ suggested_next_call={"tool_name": "role_create", "arguments": {"profile": profile, **normalized_args}},
465
+ )
247
466
 
248
467
  def package_attach_app(self, *, profile: str, tag_id: int, app_key: str, app_title: str = "") -> JSONObject:
249
- return self._facade.package_attach_app(profile=profile, tag_id=tag_id, app_key=app_key, app_title=app_title)
468
+ normalized_args = {"tag_id": tag_id, "app_key": app_key, "app_title": app_title}
469
+ result = _safe_tool_call(
470
+ lambda: self._facade.package_attach_app(profile=profile, tag_id=tag_id, app_key=app_key, app_title=app_title),
471
+ error_code="PACKAGE_ATTACH_FAILED",
472
+ normalized_args=normalized_args,
473
+ suggested_next_call={"tool_name": "package_attach_app", "arguments": {"profile": profile, **normalized_args}},
474
+ )
475
+ return self._retry_after_self_lock_release(
476
+ profile=profile,
477
+ result=result,
478
+ retry_call=lambda: self._facade.package_attach_app(
479
+ profile=profile,
480
+ tag_id=tag_id,
481
+ app_key=app_key,
482
+ app_title=app_title,
483
+ ),
484
+ )
485
+
486
+ def app_release_edit_lock_if_mine(
487
+ self,
488
+ *,
489
+ profile: str,
490
+ app_key: str,
491
+ lock_owner_email: str = "",
492
+ lock_owner_name: str = "",
493
+ ) -> JSONObject:
494
+ normalized_args = {
495
+ "app_key": app_key,
496
+ "lock_owner_email": lock_owner_email,
497
+ "lock_owner_name": lock_owner_name,
498
+ }
499
+ return _safe_tool_call(
500
+ lambda: self._facade.app_release_edit_lock_if_mine(
501
+ profile=profile,
502
+ app_key=app_key,
503
+ lock_owner_email=lock_owner_email,
504
+ lock_owner_name=lock_owner_name,
505
+ ),
506
+ error_code="EDIT_LOCK_RELEASE_FAILED",
507
+ normalized_args=normalized_args,
508
+ suggested_next_call={"tool_name": "app_release_edit_lock_if_mine", "arguments": {"profile": profile, **normalized_args}},
509
+ )
250
510
 
251
511
  def app_resolve(self, *, profile: str, app_key: str = "", app_name: str = "", package_tag_id: int | None = None) -> JSONObject:
252
- return self._facade.app_resolve(profile=profile, app_key=app_key, app_name=app_name, package_tag_id=package_tag_id)
512
+ normalized_args = {"app_key": app_key, "app_name": app_name, "package_tag_id": package_tag_id}
513
+ return _safe_tool_call(
514
+ lambda: self._facade.app_resolve(profile=profile, app_key=app_key, app_name=app_name, package_tag_id=package_tag_id),
515
+ error_code="APP_RESOLVE_FAILED",
516
+ normalized_args=normalized_args,
517
+ suggested_next_call={"tool_name": "app_resolve", "arguments": {"profile": profile, **normalized_args}},
518
+ )
253
519
 
254
520
  def app_read_summary(self, *, profile: str, app_key: str) -> JSONObject:
255
- return self._facade.app_read_summary(profile=profile, app_key=app_key)
521
+ normalized_args = {"app_key": app_key}
522
+ return _safe_tool_call(
523
+ lambda: self._facade.app_read_summary(profile=profile, app_key=app_key),
524
+ error_code="APP_READ_FAILED",
525
+ normalized_args=normalized_args,
526
+ suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
527
+ )
256
528
 
257
529
  def app_read_fields(self, *, profile: str, app_key: str) -> JSONObject:
258
- return self._facade.app_read_fields(profile=profile, app_key=app_key)
530
+ normalized_args = {"app_key": app_key}
531
+ return _safe_tool_call(
532
+ lambda: self._facade.app_read_fields(profile=profile, app_key=app_key),
533
+ error_code="FIELDS_READ_FAILED",
534
+ normalized_args=normalized_args,
535
+ suggested_next_call={"tool_name": "app_read_fields", "arguments": {"profile": profile, "app_key": app_key}},
536
+ )
259
537
 
260
538
  def app_read_layout_summary(self, *, profile: str, app_key: str) -> JSONObject:
261
- return self._facade.app_read_layout_summary(profile=profile, app_key=app_key)
539
+ normalized_args = {"app_key": app_key}
540
+ return _safe_tool_call(
541
+ lambda: self._facade.app_read_layout_summary(profile=profile, app_key=app_key),
542
+ error_code="LAYOUT_READ_FAILED",
543
+ normalized_args=normalized_args,
544
+ suggested_next_call={"tool_name": "app_read_layout_summary", "arguments": {"profile": profile, "app_key": app_key}},
545
+ )
262
546
 
263
547
  def app_read_views_summary(self, *, profile: str, app_key: str) -> JSONObject:
264
- return self._facade.app_read_views_summary(profile=profile, app_key=app_key)
548
+ normalized_args = {"app_key": app_key}
549
+ return _safe_tool_call(
550
+ lambda: self._facade.app_read_views_summary(profile=profile, app_key=app_key),
551
+ error_code="VIEWS_READ_FAILED",
552
+ normalized_args=normalized_args,
553
+ suggested_next_call={"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
554
+ )
265
555
 
266
556
  def app_read_flow_summary(self, *, profile: str, app_key: str) -> JSONObject:
267
- return self._facade.app_read_flow_summary(profile=profile, app_key=app_key)
557
+ normalized_args = {"app_key": app_key}
558
+ return _safe_tool_call(
559
+ lambda: self._facade.app_read_flow_summary(profile=profile, app_key=app_key),
560
+ error_code="FLOW_READ_FAILED",
561
+ normalized_args=normalized_args,
562
+ suggested_next_call={"tool_name": "app_read_flow_summary", "arguments": {"profile": profile, "app_key": app_key}},
563
+ )
564
+
565
+ def app_read_charts_summary(self, *, profile: str, app_key: str) -> JSONObject:
566
+ normalized_args = {"app_key": app_key}
567
+ return _safe_tool_call(
568
+ lambda: self._facade.app_read_charts_summary(profile=profile, app_key=app_key),
569
+ error_code="CHARTS_READ_FAILED",
570
+ normalized_args=normalized_args,
571
+ suggested_next_call={"tool_name": "app_read_charts_summary", "arguments": {"profile": profile, "app_key": app_key}},
572
+ )
573
+
574
+ def portal_read_summary(self, *, profile: str, dash_key: str, being_draft: bool = True) -> JSONObject:
575
+ normalized_args = {"dash_key": dash_key, "being_draft": being_draft}
576
+ return _safe_tool_call(
577
+ lambda: self._facade.portal_read_summary(profile=profile, dash_key=dash_key, being_draft=being_draft),
578
+ error_code="PORTAL_READ_FAILED",
579
+ normalized_args=normalized_args,
580
+ suggested_next_call={"tool_name": "portal_read_summary", "arguments": {"profile": profile, **normalized_args}},
581
+ )
268
582
 
269
583
  def app_schema_plan(
270
584
  self,
@@ -293,6 +607,8 @@ class AiBuilderTools(ToolBase):
293
607
  except ValidationError as exc:
294
608
  return _validation_failure(
295
609
  str(exc),
610
+ tool_name="app_schema_plan",
611
+ exc=exc,
296
612
  suggested_next_call={
297
613
  "tool_name": "app_schema_plan",
298
614
  "arguments": {
@@ -307,7 +623,12 @@ class AiBuilderTools(ToolBase):
307
623
  },
308
624
  },
309
625
  )
310
- return self._facade.app_schema_plan(profile=profile, request=request)
626
+ return _safe_tool_call(
627
+ lambda: self._facade.app_schema_plan(profile=profile, request=request),
628
+ error_code="SCHEMA_PLAN_FAILED",
629
+ normalized_args=request.model_dump(mode="json"),
630
+ suggested_next_call={"tool_name": "app_schema_plan", "arguments": {"profile": profile, **request.model_dump(mode="json")}},
631
+ )
311
632
 
312
633
  def app_layout_plan(
313
634
  self,
@@ -330,18 +651,24 @@ class AiBuilderTools(ToolBase):
330
651
  except ValidationError as exc:
331
652
  return _validation_failure(
332
653
  str(exc),
654
+ tool_name="app_layout_plan",
655
+ exc=exc,
333
656
  suggested_next_call={
334
657
  "tool_name": "app_layout_plan",
335
658
  "arguments": {
336
659
  "profile": profile,
337
660
  "app_key": app_key,
338
661
  "mode": "merge",
339
- "preset": "balanced",
340
- "sections": [],
662
+ "sections": [{"title": "基础信息", "rows": [["字段A", "字段B"]]}],
341
663
  },
342
664
  },
343
665
  )
344
- return self._facade.app_layout_plan(profile=profile, request=request)
666
+ return _safe_tool_call(
667
+ lambda: self._facade.app_layout_plan(profile=profile, request=request),
668
+ error_code="LAYOUT_PLAN_FAILED",
669
+ normalized_args=request.model_dump(mode="json"),
670
+ suggested_next_call={"tool_name": "app_layout_plan", "arguments": {"profile": profile, **request.model_dump(mode="json")}},
671
+ )
345
672
 
346
673
  def app_flow_plan(
347
674
  self,
@@ -366,6 +693,8 @@ class AiBuilderTools(ToolBase):
366
693
  except ValidationError as exc:
367
694
  return _validation_failure(
368
695
  str(exc),
696
+ tool_name="app_flow_plan",
697
+ exc=exc,
369
698
  suggested_next_call={
370
699
  "tool_name": "app_flow_plan",
371
700
  "arguments": {
@@ -378,7 +707,12 @@ class AiBuilderTools(ToolBase):
378
707
  },
379
708
  },
380
709
  )
381
- return self._facade.app_flow_plan(profile=profile, request=request)
710
+ return _safe_tool_call(
711
+ lambda: self._facade.app_flow_plan(profile=profile, request=request),
712
+ error_code="FLOW_PLAN_FAILED",
713
+ normalized_args=request.model_dump(mode="json"),
714
+ suggested_next_call={"tool_name": "app_flow_plan", "arguments": {"profile": profile, **request.model_dump(mode="json")}},
715
+ )
382
716
 
383
717
  def app_views_plan(
384
718
  self,
@@ -401,6 +735,8 @@ class AiBuilderTools(ToolBase):
401
735
  except ValidationError as exc:
402
736
  return _validation_failure(
403
737
  str(exc),
738
+ tool_name="app_views_plan",
739
+ exc=exc,
404
740
  suggested_next_call={
405
741
  "tool_name": "app_views_plan",
406
742
  "arguments": {
@@ -412,7 +748,12 @@ class AiBuilderTools(ToolBase):
412
748
  },
413
749
  },
414
750
  )
415
- return self._facade.app_views_plan(profile=profile, request=request)
751
+ return _safe_tool_call(
752
+ lambda: self._facade.app_views_plan(profile=profile, request=request),
753
+ error_code="VIEWS_PLAN_FAILED",
754
+ normalized_args=request.model_dump(mode="json"),
755
+ suggested_next_call={"tool_name": "app_views_plan", "arguments": {"profile": profile, **request.model_dump(mode="json")}},
756
+ )
416
757
 
417
758
  def app_schema_apply(
418
759
  self,
@@ -427,75 +768,200 @@ class AiBuilderTools(ToolBase):
427
768
  add_fields: list[JSONObject],
428
769
  update_fields: list[JSONObject],
429
770
  remove_fields: list[JSONObject],
771
+ ) -> JSONObject:
772
+ result = self._app_schema_apply_once(
773
+ profile=profile,
774
+ app_key=app_key,
775
+ package_tag_id=package_tag_id,
776
+ app_name=app_name,
777
+ app_title=app_title,
778
+ create_if_missing=create_if_missing,
779
+ publish=publish,
780
+ add_fields=add_fields,
781
+ update_fields=update_fields,
782
+ remove_fields=remove_fields,
783
+ )
784
+ return self._retry_after_self_lock_release(
785
+ profile=profile,
786
+ result=result,
787
+ retry_call=lambda: self._app_schema_apply_once(
788
+ profile=profile,
789
+ app_key=app_key,
790
+ package_tag_id=package_tag_id,
791
+ app_name=app_name,
792
+ app_title=app_title,
793
+ create_if_missing=create_if_missing,
794
+ publish=publish,
795
+ add_fields=add_fields,
796
+ update_fields=update_fields,
797
+ remove_fields=remove_fields,
798
+ ),
799
+ )
800
+
801
+ def _app_schema_apply_once(
802
+ self,
803
+ *,
804
+ profile: str,
805
+ app_key: str = "",
806
+ package_tag_id: int | None = None,
807
+ app_name: str = "",
808
+ app_title: str = "",
809
+ create_if_missing: bool = False,
810
+ publish: bool = True,
811
+ add_fields: list[JSONObject],
812
+ update_fields: list[JSONObject],
813
+ remove_fields: list[JSONObject],
430
814
  ) -> JSONObject:
431
815
  effective_app_name = app_name or app_title
816
+ plan_result = self._rewrite_plan_result_for_apply(
817
+ result=self.app_schema_plan(
818
+ profile=profile,
819
+ app_key=app_key,
820
+ package_tag_id=package_tag_id,
821
+ app_name=effective_app_name,
822
+ create_if_missing=create_if_missing,
823
+ add_fields=add_fields,
824
+ update_fields=update_fields,
825
+ remove_fields=remove_fields,
826
+ ),
827
+ profile=profile,
828
+ publish=publish,
829
+ plan_tool_name="app_schema_plan",
830
+ apply_tool_name="app_schema_apply",
831
+ )
832
+ if not isinstance(plan_result, dict) or plan_result.get("status") != "success":
833
+ return plan_result
834
+ plan_args = plan_result.get("normalized_args")
835
+ if not isinstance(plan_args, dict):
836
+ plan_args = {}
432
837
  try:
433
- parsed_add = [FieldPatch.model_validate(item) for item in add_fields]
434
- parsed_update = [FieldUpdatePatch.model_validate(item) for item in update_fields]
435
- parsed_remove = [FieldRemovePatch.model_validate(item) for item in remove_fields]
838
+ parsed_add = [FieldPatch.model_validate(item) for item in plan_args.get("add_fields") or []]
839
+ parsed_update = [FieldUpdatePatch.model_validate(item) for item in plan_args.get("update_fields") or []]
840
+ parsed_remove = [FieldRemovePatch.model_validate(item) for item in plan_args.get("remove_fields") or []]
436
841
  except ValidationError as exc:
437
842
  return _validation_failure(
438
843
  str(exc),
844
+ tool_name="app_schema_apply",
845
+ exc=exc,
439
846
  suggested_next_call={
440
847
  "tool_name": "app_schema_apply",
441
848
  "arguments": {
442
849
  "profile": profile,
443
- "app_key": app_key,
444
- "package_tag_id": package_tag_id,
445
- "app_name": effective_app_name,
446
- "create_if_missing": create_if_missing,
447
- "add_fields": [{"name": "字段名称", "type": "text", "required": False}],
448
- "update_fields": [],
449
- "remove_fields": [],
850
+ "app_key": str(plan_args.get("app_key") or app_key),
851
+ "package_tag_id": plan_args.get("package_tag_id", package_tag_id),
852
+ "app_name": str(plan_args.get("app_name") or effective_app_name),
853
+ "create_if_missing": bool(plan_args.get("create_if_missing", create_if_missing)),
854
+ "publish": publish,
855
+ "add_fields": plan_args.get("add_fields") or [{"name": "字段名称", "type": "text", "required": False}],
856
+ "update_fields": plan_args.get("update_fields") or [],
857
+ "remove_fields": plan_args.get("remove_fields") or [],
450
858
  },
451
859
  },
452
860
  )
453
- return self._facade.app_schema_apply(
861
+ normalized_args = {
862
+ "app_key": str(plan_args.get("app_key") or app_key),
863
+ "package_tag_id": plan_args.get("package_tag_id", package_tag_id),
864
+ "app_name": str(plan_args.get("app_name") or effective_app_name),
865
+ "create_if_missing": bool(plan_args.get("create_if_missing", create_if_missing)),
866
+ "publish": publish,
867
+ "add_fields": [patch.model_dump(mode="json") for patch in parsed_add],
868
+ "update_fields": [patch.model_dump(mode="json") for patch in parsed_update],
869
+ "remove_fields": [patch.model_dump(mode="json") for patch in parsed_remove],
870
+ }
871
+ result = _safe_tool_call(
872
+ lambda: self._facade.app_schema_apply(
873
+ profile=profile,
874
+ app_key=str(plan_args.get("app_key") or app_key),
875
+ package_tag_id=plan_args.get("package_tag_id", package_tag_id),
876
+ app_name=str(plan_args.get("app_name") or effective_app_name),
877
+ create_if_missing=bool(plan_args.get("create_if_missing", create_if_missing)),
878
+ publish=publish,
879
+ add_fields=parsed_add,
880
+ update_fields=parsed_update,
881
+ remove_fields=parsed_remove,
882
+ ),
883
+ error_code="SCHEMA_APPLY_FAILED",
884
+ normalized_args=normalized_args,
885
+ suggested_next_call={"tool_name": "app_schema_apply", "arguments": {"profile": profile, **normalized_args}},
886
+ )
887
+ return result
888
+
889
+ def app_layout_apply(self, *, profile: str, app_key: str, mode: str = "merge", publish: bool = True, sections: list[JSONObject]) -> JSONObject:
890
+ result = self._app_layout_apply_once(
454
891
  profile=profile,
455
892
  app_key=app_key,
456
- package_tag_id=package_tag_id,
457
- app_name=effective_app_name,
458
- create_if_missing=create_if_missing,
893
+ mode=mode,
459
894
  publish=publish,
460
- add_fields=parsed_add,
461
- update_fields=parsed_update,
462
- remove_fields=parsed_remove,
895
+ sections=sections,
896
+ )
897
+ return self._retry_after_self_lock_release(
898
+ profile=profile,
899
+ result=result,
900
+ retry_call=lambda: self._app_layout_apply_once(
901
+ profile=profile,
902
+ app_key=app_key,
903
+ mode=mode,
904
+ publish=publish,
905
+ sections=sections,
906
+ ),
463
907
  )
464
908
 
465
- def app_layout_apply(self, *, profile: str, app_key: str, mode: str = "merge", publish: bool = True, sections: list[JSONObject]) -> JSONObject:
909
+ def _app_layout_apply_once(self, *, profile: str, app_key: str, mode: str = "merge", publish: bool = True, sections: list[JSONObject]) -> JSONObject:
910
+ plan_result = self._rewrite_plan_result_for_apply(
911
+ result=self.app_layout_plan(
912
+ profile=profile,
913
+ app_key=app_key,
914
+ mode=mode,
915
+ sections=sections,
916
+ preset=None,
917
+ ),
918
+ profile=profile,
919
+ publish=publish,
920
+ plan_tool_name="app_layout_plan",
921
+ apply_tool_name="app_layout_apply",
922
+ )
923
+ if not isinstance(plan_result, dict) or plan_result.get("status") != "success":
924
+ return plan_result
925
+ plan_args = plan_result.get("normalized_args")
926
+ if not isinstance(plan_args, dict):
927
+ plan_args = {}
466
928
  try:
467
- normalized_mode = str(mode or "merge").strip().lower()
468
- if normalized_mode in {"overwrite"}:
469
- normalized_mode = "replace"
470
- parsed_mode = LayoutApplyMode(normalized_mode)
471
- parsed_sections = [LayoutSectionPatch.model_validate(item) for item in sections]
472
- except ValueError:
473
- return _validation_failure(
474
- "mode must be one of: merge, replace",
475
- suggested_next_call={
476
- "tool_name": "app_layout_apply",
477
- "arguments": {
478
- "profile": profile,
479
- "app_key": app_key,
480
- "mode": "merge",
481
- "sections": [{"title": "基础信息", "rows": [["字段A", "字段B"]]}],
482
- },
483
- },
484
- )
485
- except ValidationError as exc:
929
+ parsed_mode = LayoutApplyMode(str(plan_args.get("mode") or mode))
930
+ parsed_sections = [LayoutSectionPatch.model_validate(item) for item in plan_args.get("sections") or []]
931
+ except (ValueError, ValidationError) as exc:
486
932
  return _validation_failure(
487
933
  str(exc),
934
+ tool_name="app_layout_apply",
935
+ exc=exc if isinstance(exc, ValidationError) else None,
488
936
  suggested_next_call={
489
937
  "tool_name": "app_layout_apply",
490
938
  "arguments": {
491
939
  "profile": profile,
492
- "app_key": app_key,
493
- "mode": "merge",
494
- "sections": [{"title": "基础信息", "rows": [["字段A", "字段B"]]}],
940
+ "app_key": str(plan_args.get("app_key") or app_key),
941
+ "mode": str(plan_args.get("mode") or "merge"),
942
+ "publish": publish,
943
+ "sections": plan_args.get("sections") or [{"title": "基础信息", "rows": [["字段A", "字段B"]]}],
495
944
  },
496
945
  },
497
946
  )
498
- return self._facade.app_layout_apply(profile=profile, app_key=app_key, mode=parsed_mode, publish=publish, sections=parsed_sections)
947
+ normalized_args = {
948
+ "app_key": str(plan_args.get("app_key") or app_key),
949
+ "mode": parsed_mode.value,
950
+ "publish": publish,
951
+ "sections": [section.model_dump(mode="json") for section in parsed_sections],
952
+ }
953
+ return _safe_tool_call(
954
+ lambda: self._facade.app_layout_apply(
955
+ profile=profile,
956
+ app_key=str(plan_args.get("app_key") or app_key),
957
+ mode=parsed_mode,
958
+ publish=publish,
959
+ sections=parsed_sections,
960
+ ),
961
+ error_code="LAYOUT_APPLY_FAILED",
962
+ normalized_args=normalized_args,
963
+ suggested_next_call={"tool_name": "app_layout_apply", "arguments": {"profile": profile, **normalized_args}},
964
+ )
499
965
 
500
966
  def app_flow_apply(
501
967
  self,
@@ -507,40 +973,133 @@ class AiBuilderTools(ToolBase):
507
973
  nodes: list[JSONObject],
508
974
  transitions: list[JSONObject],
509
975
  ) -> JSONObject:
976
+ result = self._app_flow_apply_once(
977
+ profile=profile,
978
+ app_key=app_key,
979
+ mode=mode,
980
+ publish=publish,
981
+ nodes=nodes,
982
+ transitions=transitions,
983
+ )
984
+ return self._retry_after_self_lock_release(
985
+ profile=profile,
986
+ result=result,
987
+ retry_call=lambda: self._app_flow_apply_once(
988
+ profile=profile,
989
+ app_key=app_key,
990
+ mode=mode,
991
+ publish=publish,
992
+ nodes=nodes,
993
+ transitions=transitions,
994
+ ),
995
+ )
996
+
997
+ def _app_flow_apply_once(
998
+ self,
999
+ *,
1000
+ profile: str,
1001
+ app_key: str,
1002
+ mode: str = "replace",
1003
+ publish: bool = True,
1004
+ nodes: list[JSONObject],
1005
+ transitions: list[JSONObject],
1006
+ ) -> JSONObject:
1007
+ plan_result = self._rewrite_plan_result_for_apply(
1008
+ result=self.app_flow_plan(
1009
+ profile=profile,
1010
+ app_key=app_key,
1011
+ mode=mode,
1012
+ nodes=nodes,
1013
+ transitions=transitions,
1014
+ preset=None,
1015
+ ),
1016
+ profile=profile,
1017
+ publish=publish,
1018
+ plan_tool_name="app_flow_plan",
1019
+ apply_tool_name="app_flow_apply",
1020
+ )
1021
+ if not isinstance(plan_result, dict) or plan_result.get("status") != "success":
1022
+ return plan_result
1023
+ plan_args = plan_result.get("normalized_args")
1024
+ if not isinstance(plan_args, dict):
1025
+ plan_args = {}
510
1026
  try:
511
1027
  request = FlowPlanRequest.model_validate(
512
1028
  {
513
- "app_key": app_key,
514
- "mode": mode,
515
- "nodes": nodes,
516
- "transitions": transitions,
1029
+ "app_key": plan_args.get("app_key") or app_key,
1030
+ "mode": plan_args.get("mode") or mode,
1031
+ "nodes": plan_args.get("nodes") or [],
1032
+ "transitions": plan_args.get("transitions") or [],
517
1033
  "preset": None,
518
1034
  }
519
1035
  )
520
1036
  except ValidationError as exc:
521
1037
  return _validation_failure(
522
1038
  str(exc),
1039
+ tool_name="app_flow_apply",
1040
+ exc=exc,
523
1041
  suggested_next_call={
524
1042
  "tool_name": "app_flow_apply",
525
1043
  "arguments": {
526
1044
  "profile": profile,
527
- "app_key": app_key,
528
- "mode": "replace",
529
- "nodes": [{"id": "start", "type": "start", "name": "发起"}],
530
- "transitions": [],
1045
+ "app_key": str(plan_args.get("app_key") or app_key),
1046
+ "mode": str(plan_args.get("mode") or "replace"),
1047
+ "publish": publish,
1048
+ "nodes": plan_args.get("nodes") or [{"id": "start", "type": "start", "name": "发起"}],
1049
+ "transitions": plan_args.get("transitions") or [],
531
1050
  },
532
1051
  },
533
1052
  )
534
- return self._facade.app_flow_apply(
1053
+ normalized_args = {
1054
+ "app_key": request.app_key,
1055
+ "mode": request.mode,
1056
+ "publish": publish,
1057
+ "nodes": [node.model_dump(mode="json") for node in request.nodes],
1058
+ "transitions": [transition.model_dump(mode="json", by_alias=True) for transition in request.transitions],
1059
+ }
1060
+ return _safe_tool_call(
1061
+ lambda: self._facade.app_flow_apply(
1062
+ profile=profile,
1063
+ app_key=request.app_key,
1064
+ mode=request.mode,
1065
+ publish=publish,
1066
+ nodes=[node.model_dump(mode="json") for node in request.nodes],
1067
+ transitions=[transition.model_dump(mode="json", by_alias=True) for transition in request.transitions],
1068
+ ),
1069
+ error_code="FLOW_APPLY_FAILED",
1070
+ normalized_args=normalized_args,
1071
+ suggested_next_call={"tool_name": "app_flow_apply", "arguments": {"profile": profile, **normalized_args}},
1072
+ )
1073
+
1074
+ def app_views_apply(
1075
+ self,
1076
+ *,
1077
+ profile: str,
1078
+ app_key: str,
1079
+ publish: bool = True,
1080
+ upsert_views: list[JSONObject],
1081
+ remove_views: list[str],
1082
+ ) -> JSONObject:
1083
+ result = self._app_views_apply_once(
535
1084
  profile=profile,
536
- app_key=request.app_key,
537
- mode=request.mode,
1085
+ app_key=app_key,
538
1086
  publish=publish,
539
- nodes=[node.model_dump(mode="json") for node in request.nodes],
540
- transitions=[transition.model_dump(mode="json", by_alias=True) for transition in request.transitions],
1087
+ upsert_views=upsert_views,
1088
+ remove_views=remove_views,
1089
+ )
1090
+ return self._retry_after_self_lock_release(
1091
+ profile=profile,
1092
+ result=result,
1093
+ retry_call=lambda: self._app_views_apply_once(
1094
+ profile=profile,
1095
+ app_key=app_key,
1096
+ publish=publish,
1097
+ remove_views=remove_views,
1098
+ upsert_views=upsert_views,
1099
+ ),
541
1100
  )
542
1101
 
543
- def app_views_apply(
1102
+ def _app_views_apply_once(
544
1103
  self,
545
1104
  *,
546
1105
  profile: str,
@@ -549,28 +1108,335 @@ class AiBuilderTools(ToolBase):
549
1108
  upsert_views: list[JSONObject],
550
1109
  remove_views: list[str],
551
1110
  ) -> JSONObject:
1111
+ plan_result = self._rewrite_plan_result_for_apply(
1112
+ result=self.app_views_plan(
1113
+ profile=profile,
1114
+ app_key=app_key,
1115
+ upsert_views=upsert_views,
1116
+ remove_views=remove_views,
1117
+ preset=None,
1118
+ ),
1119
+ profile=profile,
1120
+ publish=publish,
1121
+ plan_tool_name="app_views_plan",
1122
+ apply_tool_name="app_views_apply",
1123
+ )
1124
+ if not isinstance(plan_result, dict) or plan_result.get("status") != "success":
1125
+ return plan_result
1126
+ plan_args = plan_result.get("normalized_args")
1127
+ if not isinstance(plan_args, dict):
1128
+ plan_args = {}
552
1129
  try:
553
- parsed_views = [ViewUpsertPatch.model_validate(item) for item in upsert_views]
1130
+ parsed_views = [ViewUpsertPatch.model_validate(item) for item in plan_args.get("upsert_views") or []]
554
1131
  except ValidationError as exc:
555
1132
  return _validation_failure(
556
1133
  str(exc),
1134
+ tool_name="app_views_apply",
1135
+ exc=exc,
557
1136
  suggested_next_call={
558
1137
  "tool_name": "app_views_apply",
1138
+ "arguments": {
1139
+ "profile": profile,
1140
+ "app_key": str(plan_args.get("app_key") or app_key),
1141
+ "publish": publish,
1142
+ "upsert_views": plan_args.get("upsert_views") or [{"name": "全部数据", "type": "table", "columns": ["字段A"]}],
1143
+ "remove_views": plan_args.get("remove_views") or [],
1144
+ },
1145
+ },
1146
+ )
1147
+ normalized_args = {
1148
+ "app_key": str(plan_args.get("app_key") or app_key),
1149
+ "publish": publish,
1150
+ "upsert_views": [view.model_dump(mode="json") for view in parsed_views],
1151
+ "remove_views": list(plan_args.get("remove_views") or remove_views),
1152
+ }
1153
+ return _safe_tool_call(
1154
+ lambda: self._facade.app_views_apply(
1155
+ profile=profile,
1156
+ app_key=str(plan_args.get("app_key") or app_key),
1157
+ publish=publish,
1158
+ upsert_views=parsed_views,
1159
+ remove_views=list(plan_args.get("remove_views") or remove_views),
1160
+ ),
1161
+ error_code="VIEWS_APPLY_FAILED",
1162
+ normalized_args=normalized_args,
1163
+ suggested_next_call={"tool_name": "app_views_apply", "arguments": {"profile": profile, **normalized_args}},
1164
+ )
1165
+
1166
+ def chart_apply(
1167
+ self,
1168
+ *,
1169
+ profile: str,
1170
+ app_key: str,
1171
+ upsert_charts: list[JSONObject],
1172
+ remove_chart_ids: list[str],
1173
+ reorder_chart_ids: list[str],
1174
+ ) -> JSONObject:
1175
+ return self.app_charts_apply(
1176
+ profile=profile,
1177
+ app_key=app_key,
1178
+ upsert_charts=upsert_charts,
1179
+ remove_chart_ids=remove_chart_ids,
1180
+ reorder_chart_ids=reorder_chart_ids,
1181
+ )
1182
+
1183
+ def app_charts_apply(
1184
+ self,
1185
+ *,
1186
+ profile: str,
1187
+ app_key: str,
1188
+ upsert_charts: list[JSONObject],
1189
+ remove_chart_ids: list[str],
1190
+ reorder_chart_ids: list[str],
1191
+ ) -> JSONObject:
1192
+ try:
1193
+ request = ChartApplyRequest.model_validate(
1194
+ {
1195
+ "app_key": app_key,
1196
+ "upsert_charts": upsert_charts or [],
1197
+ "remove_chart_ids": remove_chart_ids or [],
1198
+ "reorder_chart_ids": reorder_chart_ids or [],
1199
+ }
1200
+ )
1201
+ except ValidationError as exc:
1202
+ return _validation_failure(
1203
+ str(exc),
1204
+ tool_name="app_charts_apply",
1205
+ exc=exc,
1206
+ suggested_next_call={
1207
+ "tool_name": "app_charts_apply",
559
1208
  "arguments": {
560
1209
  "profile": profile,
561
1210
  "app_key": app_key,
562
- "upsert_views": [{"name": "全部数据", "type": "table", "columns": ["字段A"]}],
563
- "remove_views": [],
1211
+ "upsert_charts": [{"name": "销售总量", "chart_type": "target", "indicator_field_ids": []}],
1212
+ "remove_chart_ids": [],
1213
+ "reorder_chart_ids": [],
564
1214
  },
565
1215
  },
566
1216
  )
567
- return self._facade.app_views_apply(profile=profile, app_key=app_key, publish=publish, upsert_views=parsed_views, remove_views=remove_views)
1217
+ normalized_args = request.model_dump(mode="json")
1218
+ return _safe_tool_call(
1219
+ lambda: self._facade.chart_apply(profile=profile, request=request),
1220
+ error_code="CHART_APPLY_FAILED",
1221
+ normalized_args=normalized_args,
1222
+ suggested_next_call={"tool_name": "app_charts_apply", "arguments": {"profile": profile, **normalized_args}},
1223
+ )
1224
+
1225
+ def portal_apply(
1226
+ self,
1227
+ *,
1228
+ profile: str,
1229
+ dash_key: str = "",
1230
+ dash_name: str = "",
1231
+ package_tag_id: int | None = None,
1232
+ publish: bool = True,
1233
+ sections: list[JSONObject],
1234
+ auth: JSONObject | None = None,
1235
+ icon: str | None = None,
1236
+ color: str | None = None,
1237
+ hide_copyright: bool | None = None,
1238
+ dash_global_config: JSONObject | None = None,
1239
+ config: JSONObject | None = None,
1240
+ ) -> JSONObject:
1241
+ try:
1242
+ request = PortalApplyRequest.model_validate(
1243
+ {
1244
+ "dash_key": dash_key or None,
1245
+ "dash_name": dash_name or None,
1246
+ "package_tag_id": package_tag_id,
1247
+ "publish": publish,
1248
+ "sections": sections or [],
1249
+ "auth": auth,
1250
+ "icon": icon,
1251
+ "color": color,
1252
+ "hide_copyright": hide_copyright,
1253
+ "dash_global_config": dash_global_config,
1254
+ "config": config or {},
1255
+ }
1256
+ )
1257
+ except ValidationError as exc:
1258
+ return _validation_failure(
1259
+ str(exc),
1260
+ tool_name="portal_apply",
1261
+ exc=exc,
1262
+ suggested_next_call={
1263
+ "tool_name": "portal_apply",
1264
+ "arguments": {
1265
+ "profile": profile,
1266
+ "dash_name": dash_name or "业务门户",
1267
+ "package_tag_id": package_tag_id or 1001,
1268
+ "publish": True,
1269
+ "sections": [
1270
+ {
1271
+ "title": "经营概览",
1272
+ "source_type": "text",
1273
+ "text": "欢迎使用业务门户",
1274
+ }
1275
+ ],
1276
+ },
1277
+ },
1278
+ )
1279
+ normalized_args = request.model_dump(mode="json")
1280
+ return _safe_tool_call(
1281
+ lambda: self._facade.portal_apply(profile=profile, request=request),
1282
+ error_code="PORTAL_APPLY_FAILED",
1283
+ normalized_args=normalized_args,
1284
+ suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
1285
+ )
568
1286
 
569
1287
  def app_publish_verify(self, *, profile: str, app_key: str, expected_package_tag_id: int | None = None) -> JSONObject:
570
- return self._facade.app_publish_verify(profile=profile, app_key=app_key, expected_package_tag_id=expected_package_tag_id)
1288
+ normalized_args = {"app_key": app_key, "expected_package_tag_id": expected_package_tag_id}
1289
+ result = _safe_tool_call(
1290
+ lambda: self._facade.app_publish_verify(profile=profile, app_key=app_key, expected_package_tag_id=expected_package_tag_id),
1291
+ error_code="PUBLISH_VERIFY_FAILED",
1292
+ normalized_args=normalized_args,
1293
+ suggested_next_call={"tool_name": "app_publish_verify", "arguments": {"profile": profile, **normalized_args}},
1294
+ )
1295
+ return self._retry_after_self_lock_release(
1296
+ profile=profile,
1297
+ result=result,
1298
+ retry_call=lambda: self._facade.app_publish_verify(
1299
+ profile=profile,
1300
+ app_key=app_key,
1301
+ expected_package_tag_id=expected_package_tag_id,
1302
+ ),
1303
+ )
571
1304
 
1305
+ def _retry_after_self_lock_release(self, *, profile: str, result: JSONObject, retry_call) -> JSONObject:
1306
+ if not isinstance(result, dict) or result.get("status") != "failed" or result.get("error_code") != "APP_EDIT_LOCKED":
1307
+ return result
1308
+ suggested = result.get("suggested_next_call")
1309
+ if not isinstance(suggested, dict) or suggested.get("tool_name") != "app_release_edit_lock_if_mine":
1310
+ return result
1311
+ arguments = suggested.get("arguments")
1312
+ if not isinstance(arguments, dict):
1313
+ return result
1314
+ app_key = str(arguments.get("app_key") or "")
1315
+ lock_owner_email = str(arguments.get("lock_owner_email") or "")
1316
+ lock_owner_name = str(arguments.get("lock_owner_name") or "")
1317
+ release_attempts: list[JSONObject] = []
1318
+ retried: JSONObject = result
1319
+ for _ in range(3):
1320
+ release_result = self.app_release_edit_lock_if_mine(
1321
+ profile=profile,
1322
+ app_key=app_key,
1323
+ lock_owner_email=lock_owner_email,
1324
+ lock_owner_name=lock_owner_name,
1325
+ )
1326
+ release_attempts.append(release_result)
1327
+ if not isinstance(release_result, dict) or release_result.get("status") != "success":
1328
+ result.setdefault("details", {})
1329
+ if isinstance(result["details"], dict):
1330
+ result["details"]["edit_lock_release_result"] = release_result
1331
+ result["details"]["edit_lock_release_attempts"] = release_attempts
1332
+ return result
1333
+ retried = retry_call()
1334
+ if not (
1335
+ isinstance(retried, dict)
1336
+ and retried.get("status") == "failed"
1337
+ and retried.get("error_code") == "APP_EDIT_LOCKED"
1338
+ ):
1339
+ break
1340
+ time.sleep(0.2)
1341
+ if (
1342
+ isinstance(retried, dict)
1343
+ and retried.get("status") == "failed"
1344
+ and retried.get("error_code") == "APP_EDIT_LOCKED"
1345
+ ):
1346
+ retried = {
1347
+ **retried,
1348
+ "error_code": "PERSISTENT_SELF_LOCK",
1349
+ "message": "app remains locked by the current user's active editor session after repeated forced release attempts",
1350
+ "recoverable": True,
1351
+ "suggested_next_call": None,
1352
+ }
1353
+ if isinstance(retried, dict):
1354
+ retried.setdefault("details", {})
1355
+ if isinstance(retried["details"], dict):
1356
+ retried["details"]["edit_lock_release_result"] = release_attempts[-1] if release_attempts else None
1357
+ retried["details"]["edit_lock_release_attempts"] = release_attempts
1358
+ retried["edit_lock_released"] = bool(release_attempts)
1359
+ retried["retried_after_edit_lock_release"] = True
1360
+ return retried
572
1361
 
573
- def _validation_failure(detail: str, *, suggested_next_call: JSONObject | None = None) -> JSONObject:
1362
+ def _rewrite_plan_result_for_apply(
1363
+ self,
1364
+ *,
1365
+ result: JSONObject,
1366
+ profile: str,
1367
+ publish: bool,
1368
+ plan_tool_name: str,
1369
+ apply_tool_name: str,
1370
+ ) -> JSONObject:
1371
+ if not isinstance(result, dict):
1372
+ return result
1373
+ rewritten = dict(result)
1374
+ if rewritten.get("error_code") == "VALIDATION_ERROR":
1375
+ contract = _BUILDER_TOOL_CONTRACTS.get(apply_tool_name)
1376
+ details = rewritten.get("details")
1377
+ if not isinstance(details, dict):
1378
+ details = {}
1379
+ rewritten["details"] = details
1380
+ if isinstance(contract, dict):
1381
+ rewritten["allowed_values"] = deepcopy(contract.get("allowed_values", {}))
1382
+ details["allowed_keys"] = deepcopy(contract.get("allowed_keys", []))
1383
+ details["section_allowed_keys"] = deepcopy(contract.get("section_allowed_keys", []))
1384
+ details["section_aliases"] = deepcopy(contract.get("section_aliases", {}))
1385
+ details["minimal_section_example"] = deepcopy(contract.get("minimal_section_example"))
1386
+ suggested_next_call = rewritten.get("suggested_next_call")
1387
+ if isinstance(suggested_next_call, dict):
1388
+ if suggested_next_call.get("tool_name") == plan_tool_name:
1389
+ arguments = suggested_next_call.get("arguments")
1390
+ normalized_arguments = dict(arguments) if isinstance(arguments, dict) else {}
1391
+ normalized_arguments.setdefault("profile", profile)
1392
+ normalized_arguments["publish"] = publish
1393
+ rewritten["suggested_next_call"] = {
1394
+ **suggested_next_call,
1395
+ "tool_name": apply_tool_name,
1396
+ "arguments": normalized_arguments,
1397
+ }
1398
+ return rewritten
1399
+ if rewritten.get("error_code") == "VALIDATION_ERROR" and suggested_next_call.get("tool_name") == apply_tool_name:
1400
+ arguments = suggested_next_call.get("arguments")
1401
+ normalized_arguments = dict(arguments) if isinstance(arguments, dict) else {}
1402
+ normalized_arguments.setdefault("profile", profile)
1403
+ normalized_arguments["publish"] = publish
1404
+ if isinstance(details, dict):
1405
+ details["canonical_arguments"] = normalized_arguments
1406
+ rewritten["suggested_next_call"] = {
1407
+ **suggested_next_call,
1408
+ "arguments": normalized_arguments,
1409
+ }
1410
+ if rewritten.get("status") == "success":
1411
+ normalized_args = rewritten.get("normalized_args")
1412
+ if isinstance(normalized_args, dict):
1413
+ rewritten["suggested_next_call"] = {
1414
+ "tool_name": apply_tool_name,
1415
+ "arguments": {"profile": profile, **normalized_args, "publish": publish},
1416
+ }
1417
+ return rewritten
1418
+
1419
+
1420
+ def _validation_failure(
1421
+ detail: str,
1422
+ *,
1423
+ tool_name: str | None = None,
1424
+ exc: ValidationError | None = None,
1425
+ suggested_next_call: JSONObject | None = None,
1426
+ ) -> JSONObject:
1427
+ contract = _BUILDER_TOOL_CONTRACTS.get(tool_name or "")
1428
+ reason_path = None
1429
+ if exc is not None:
1430
+ errors = exc.errors()
1431
+ if errors:
1432
+ loc = errors[0].get("loc")
1433
+ if isinstance(loc, (tuple, list)):
1434
+ reason_path = ".".join(str(part) for part in loc)
1435
+ canonical_arguments = None
1436
+ if isinstance(suggested_next_call, dict):
1437
+ arguments = suggested_next_call.get("arguments")
1438
+ if isinstance(arguments, dict):
1439
+ canonical_arguments = arguments
574
1440
  return {
575
1441
  "status": "failed",
576
1442
  "error_code": "VALIDATION_ERROR",
@@ -578,8 +1444,16 @@ def _validation_failure(detail: str, *, suggested_next_call: JSONObject | None =
578
1444
  "message": detail,
579
1445
  "normalized_args": {},
580
1446
  "missing_fields": [],
581
- "allowed_values": {},
582
- "details": {"validation_detail": detail},
1447
+ "allowed_values": deepcopy(contract.get("allowed_values", {})) if isinstance(contract, dict) else {},
1448
+ "details": {
1449
+ "validation_detail": detail,
1450
+ "reason_path": reason_path,
1451
+ "allowed_keys": deepcopy(contract.get("allowed_keys", [])) if isinstance(contract, dict) else [],
1452
+ "canonical_arguments": canonical_arguments,
1453
+ "section_allowed_keys": deepcopy(contract.get("section_allowed_keys", [])) if isinstance(contract, dict) else [],
1454
+ "section_aliases": deepcopy(contract.get("section_aliases", {})) if isinstance(contract, dict) else {},
1455
+ "minimal_section_example": deepcopy(contract.get("minimal_section_example")) if isinstance(contract, dict) else None,
1456
+ },
583
1457
  "suggested_next_call": suggested_next_call,
584
1458
  "request_id": None,
585
1459
  "backend_code": None,
@@ -587,3 +1461,552 @@ def _validation_failure(detail: str, *, suggested_next_call: JSONObject | None =
587
1461
  "noop": False,
588
1462
  "verification": {},
589
1463
  }
1464
+
1465
+
1466
+ def _safe_tool_call(
1467
+ call,
1468
+ *,
1469
+ error_code: str,
1470
+ normalized_args: JSONObject,
1471
+ suggested_next_call: JSONObject | None,
1472
+ ) -> JSONObject:
1473
+ try:
1474
+ return call()
1475
+ except (QingflowApiError, RuntimeError) as error:
1476
+ api_error = _coerce_api_error(error)
1477
+ public_http_status = None if api_error.http_status == 404 else api_error.http_status
1478
+ return {
1479
+ "status": "failed",
1480
+ "error_code": error_code,
1481
+ "recoverable": True,
1482
+ "message": _public_error_message(error_code, api_error),
1483
+ "normalized_args": normalized_args,
1484
+ "missing_fields": [],
1485
+ "allowed_values": {},
1486
+ "details": {
1487
+ "transport_error": {
1488
+ "http_status": api_error.http_status,
1489
+ "backend_code": api_error.backend_code,
1490
+ "category": api_error.category,
1491
+ }
1492
+ },
1493
+ "suggested_next_call": suggested_next_call,
1494
+ "request_id": api_error.request_id,
1495
+ "backend_code": api_error.backend_code,
1496
+ "http_status": public_http_status,
1497
+ "noop": False,
1498
+ "verification": {},
1499
+ }
1500
+
1501
+
1502
+ def _coerce_api_error(error: Exception) -> QingflowApiError:
1503
+ if isinstance(error, QingflowApiError):
1504
+ return error
1505
+ if isinstance(error, RuntimeError):
1506
+ try:
1507
+ payload = json.loads(str(error))
1508
+ except json.JSONDecodeError:
1509
+ payload = None
1510
+ if isinstance(payload, dict) and payload.get("category") and payload.get("message"):
1511
+ details = payload.get("details")
1512
+ return QingflowApiError(
1513
+ category=str(payload.get("category")),
1514
+ message=str(payload.get("message")),
1515
+ backend_code=payload.get("backend_code"),
1516
+ request_id=payload.get("request_id"),
1517
+ http_status=payload.get("http_status"),
1518
+ details=details if isinstance(details, dict) else None,
1519
+ )
1520
+ return QingflowApiError(category="runtime", message=str(error))
1521
+
1522
+
1523
+ def _public_error_message(error_code: str, error: QingflowApiError) -> str:
1524
+ if error.backend_code == 40074 or error_code == "APP_EDIT_LOCKED":
1525
+ return "app is currently locked by another active editor session"
1526
+ if error.http_status != 404:
1527
+ return error.message
1528
+ mapping = {
1529
+ "PACKAGE_LIST_FAILED": "package list is unavailable in the current route",
1530
+ "PACKAGE_RESOLVE_FAILED": "package resolution is unavailable in the current route",
1531
+ "PACKAGE_ATTACH_FAILED": "package attachment could not be verified in the current route",
1532
+ "APP_RESOLVE_FAILED": "app resolution is unavailable in the current route",
1533
+ "APP_READ_FAILED": "app base or schema is unavailable in the current route",
1534
+ "FIELDS_READ_FAILED": "app fields are unavailable in the current route",
1535
+ "LAYOUT_READ_FAILED": "layout resource is unavailable for this app in the current route",
1536
+ "VIEWS_READ_FAILED": "views resource is unavailable for this app in the current route",
1537
+ "FLOW_READ_FAILED": "workflow resource is unavailable for this app in the current route",
1538
+ "SCHEMA_PLAN_FAILED": "schema planning could not load the required app state in the current route",
1539
+ "LAYOUT_PLAN_FAILED": "layout planning could not load the required app state in the current route",
1540
+ "FLOW_PLAN_FAILED": "flow planning could not load the required app state in the current route",
1541
+ "VIEWS_PLAN_FAILED": "views planning could not load the required app state in the current route",
1542
+ "SCHEMA_APPLY_FAILED": "schema apply could not complete because the app route or readback is unavailable",
1543
+ "LAYOUT_APPLY_FAILED": "layout apply could not complete because the layout route or readback is unavailable",
1544
+ "FLOW_APPLY_FAILED": "flow apply could not complete because the workflow route or readback is unavailable",
1545
+ "VIEWS_APPLY_FAILED": "views apply could not complete because the views route or readback is unavailable",
1546
+ "CHART_APPLY_FAILED": "chart apply could not complete because the QingBI route or readback is unavailable",
1547
+ "PORTAL_APPLY_FAILED": "portal apply could not complete because the portal route or readback is unavailable",
1548
+ "PUBLISH_VERIFY_FAILED": "publish verification is unavailable in the current route",
1549
+ }
1550
+ return mapping.get(error_code, "requested builder resource is unavailable in the current route")
1551
+
1552
+
1553
+ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
1554
+ "member_search": {
1555
+ "allowed_keys": ["query", "page_num", "page_size", "contain_disable"],
1556
+ "aliases": {},
1557
+ "allowed_values": {},
1558
+ "minimal_example": {
1559
+ "profile": "default",
1560
+ "query": "严琪东",
1561
+ "page_num": 1,
1562
+ "page_size": 20,
1563
+ "contain_disable": False,
1564
+ },
1565
+ },
1566
+ "role_search": {
1567
+ "allowed_keys": ["keyword", "page_num", "page_size"],
1568
+ "aliases": {},
1569
+ "allowed_values": {},
1570
+ "minimal_example": {
1571
+ "profile": "default",
1572
+ "keyword": "项目经理",
1573
+ "page_num": 1,
1574
+ "page_size": 20,
1575
+ },
1576
+ },
1577
+ "role_create": {
1578
+ "allowed_keys": ["role_name", "member_uids", "member_emails", "member_names", "role_icon"],
1579
+ "aliases": {},
1580
+ "allowed_values": {},
1581
+ "minimal_example": {
1582
+ "profile": "default",
1583
+ "role_name": "研发负责人",
1584
+ "member_names": ["严琪东"],
1585
+ "member_uids": [],
1586
+ "member_emails": [],
1587
+ "role_icon": "ex-user-outlined",
1588
+ },
1589
+ },
1590
+ "app_schema_plan": {
1591
+ "allowed_keys": ["app_key", "package_tag_id", "app_name", "create_if_missing", "add_fields", "update_fields", "remove_fields"],
1592
+ "aliases": {
1593
+ "app_title": "app_name",
1594
+ "title": "app_name",
1595
+ "field.title": "field.name",
1596
+ "field.label": "field.name",
1597
+ "field.fields": "field.subfields",
1598
+ "field.type_id": "field.type",
1599
+ },
1600
+ "allowed_values": {
1601
+ "field.type": [member.value for member in PublicFieldType],
1602
+ "field_type_ids": sorted(FIELD_TYPE_ID_ALIASES.keys()),
1603
+ },
1604
+ "minimal_example": {
1605
+ "profile": "default",
1606
+ "app_name": "研发项目管理",
1607
+ "package_tag_id": 1001,
1608
+ "create_if_missing": True,
1609
+ "add_fields": [{"name": "项目名称", "type": "text"}],
1610
+ "update_fields": [],
1611
+ "remove_fields": [],
1612
+ },
1613
+ },
1614
+ "app_schema_apply": {
1615
+ "allowed_keys": ["app_key", "package_tag_id", "app_name", "create_if_missing", "publish", "add_fields", "update_fields", "remove_fields"],
1616
+ "aliases": {
1617
+ "app_title": "app_name",
1618
+ "title": "app_name",
1619
+ "field.title": "field.name",
1620
+ "field.label": "field.name",
1621
+ "field.fields": "field.subfields",
1622
+ "field.type_id": "field.type",
1623
+ },
1624
+ "allowed_values": {
1625
+ "field.type": [member.value for member in PublicFieldType],
1626
+ "field_type_ids": sorted(FIELD_TYPE_ID_ALIASES.keys()),
1627
+ },
1628
+ "execution_notes": [
1629
+ "multiple relation fields are backend-risky; read verification.relation_field_limit_verified and warnings before declaring the schema stable",
1630
+ "backend 49614 is normalized to MULTIPLE_RELATION_FIELDS_UNSUPPORTED with a workaround message",
1631
+ ],
1632
+ "minimal_example": {
1633
+ "profile": "default",
1634
+ "app_name": "研发项目管理",
1635
+ "package_tag_id": 1001,
1636
+ "create_if_missing": True,
1637
+ "publish": True,
1638
+ "add_fields": [{"name": "项目名称", "type": "text"}],
1639
+ "update_fields": [],
1640
+ "remove_fields": [],
1641
+ },
1642
+ },
1643
+ "app_layout_plan": {
1644
+ "allowed_keys": ["app_key", "mode", "sections", "preset"],
1645
+ "aliases": {"overwrite": "replace", "sectionId": "section_id"},
1646
+ "section_allowed_keys": ["section_id", "title", "rows"],
1647
+ "section_aliases": {
1648
+ "name": "title",
1649
+ "sectionId": "section_id",
1650
+ "fields": "rows",
1651
+ "field_ids": "rows",
1652
+ "columns": "rows_chunk_size",
1653
+ },
1654
+ "allowed_values": {"mode": [member.value for member in LayoutApplyMode], "preset": [member.value for member in LayoutPreset]},
1655
+ "minimal_section_example": {"title": "基础信息", "rows": [["字段A", "字段B"]]},
1656
+ "minimal_example": {
1657
+ "profile": "default",
1658
+ "app_key": "APP_KEY",
1659
+ "mode": "merge",
1660
+ "sections": [{"title": "基础信息", "rows": [["字段A", "字段B"]]}],
1661
+ },
1662
+ "preset_example": {"profile": "default", "app_key": "APP_KEY", "mode": "merge", "preset": "balanced", "sections": []},
1663
+ },
1664
+ "app_layout_apply": {
1665
+ "allowed_keys": ["app_key", "mode", "publish", "sections"],
1666
+ "aliases": {"overwrite": "replace", "sectionId": "section_id"},
1667
+ "section_allowed_keys": ["section_id", "title", "rows"],
1668
+ "section_aliases": {
1669
+ "name": "title",
1670
+ "sectionId": "section_id",
1671
+ "fields": "rows",
1672
+ "field_ids": "rows",
1673
+ "columns": "rows_chunk_size",
1674
+ },
1675
+ "allowed_values": {"mode": [member.value for member in LayoutApplyMode]},
1676
+ "execution_notes": [
1677
+ "layout verification is split into layout_verified and layout_summary_verified",
1678
+ "LAYOUT_SUMMARY_UNVERIFIED means raw form readback is stronger than the compact summary",
1679
+ ],
1680
+ "minimal_section_example": {"title": "基础信息", "rows": [["字段A", "字段B"]]},
1681
+ "minimal_example": {
1682
+ "profile": "default",
1683
+ "app_key": "APP_KEY",
1684
+ "mode": "merge",
1685
+ "publish": True,
1686
+ "sections": [{"title": "基础信息", "rows": [["项目名称", "项目负责人"]]}],
1687
+ },
1688
+ },
1689
+ "app_flow_plan": {
1690
+ "allowed_keys": ["app_key", "mode", "nodes", "transitions", "preset"],
1691
+ "aliases": {
1692
+ "overwrite": "replace",
1693
+ "base_preset": "preset",
1694
+ "default_approval": "basic_approval",
1695
+ "node.role_names": "node.assignees.role_names",
1696
+ "node.role_ids": "node.assignees.role_ids",
1697
+ "node.member_names": "node.assignees.member_names",
1698
+ "node.member_emails": "node.assignees.member_emails",
1699
+ "node.member_uids": "node.assignees.member_uids",
1700
+ "node.editable_fields": "node.permissions.editable_fields",
1701
+ "default_approval": "basic_approval",
1702
+ },
1703
+ "allowed_values": {
1704
+ "mode": ["replace"],
1705
+ "preset": [member.value for member in FlowPreset],
1706
+ "node.type": PUBLIC_STABLE_FLOW_NODE_TYPES,
1707
+ },
1708
+ "dependency_hints": [
1709
+ "approval-style workflows require an explicit status field",
1710
+ "approve/fill/copy nodes require at least one assignee",
1711
+ ],
1712
+ "execution_notes": [
1713
+ "public flow building is intentionally limited to linear workflows",
1714
+ "branch and condition nodes are disabled because the backend workflow route is not front-end stable for these node types",
1715
+ ],
1716
+ "minimal_example": {
1717
+ "profile": "default",
1718
+ "app_key": "APP_KEY",
1719
+ "mode": "replace",
1720
+ "preset": "basic_approval",
1721
+ "nodes": [
1722
+ {
1723
+ "id": "approve_1",
1724
+ "type": "approve",
1725
+ "name": "部门审批",
1726
+ "assignees": {"role_names": ["项目经理"]},
1727
+ "permissions": {"editable_fields": ["状态", "审批意见"]},
1728
+ }
1729
+ ],
1730
+ "transitions": [],
1731
+ },
1732
+ },
1733
+ "app_flow_apply": {
1734
+ "allowed_keys": ["app_key", "mode", "publish", "nodes", "transitions"],
1735
+ "aliases": {
1736
+ "overwrite": "replace",
1737
+ "node.role_names": "node.assignees.role_names",
1738
+ "node.role_ids": "node.assignees.role_ids",
1739
+ "node.member_names": "node.assignees.member_names",
1740
+ "node.member_emails": "node.assignees.member_emails",
1741
+ "node.member_uids": "node.assignees.member_uids",
1742
+ "node.editable_fields": "node.permissions.editable_fields",
1743
+ },
1744
+ "allowed_values": {
1745
+ "mode": ["replace"],
1746
+ "node.type": PUBLIC_STABLE_FLOW_NODE_TYPES,
1747
+ },
1748
+ "dependency_hints": [
1749
+ "approval-style workflows require an explicit status field",
1750
+ "approve/fill/copy nodes require at least one assignee",
1751
+ ],
1752
+ "execution_notes": [
1753
+ "public flow building is intentionally limited to linear workflows",
1754
+ "branch and condition nodes are disabled because the backend workflow route is not front-end stable for these node types",
1755
+ "workflow verification only covers linear node structure in the public tool surface",
1756
+ ],
1757
+ "minimal_example": {
1758
+ "profile": "default",
1759
+ "app_key": "APP_KEY",
1760
+ "mode": "replace",
1761
+ "publish": True,
1762
+ "nodes": [
1763
+ {"id": "start", "type": "start", "name": "发起"},
1764
+ {
1765
+ "id": "approve_1",
1766
+ "type": "approve",
1767
+ "name": "部门审批",
1768
+ "assignees": {"role_names": ["项目经理"]},
1769
+ "permissions": {"editable_fields": ["状态", "审批意见"]},
1770
+ },
1771
+ {"id": "end", "type": "end", "name": "结束"},
1772
+ ],
1773
+ "transitions": [{"from": "start", "to": "approve_1"}, {"from": "approve_1", "to": "end"}],
1774
+ },
1775
+ },
1776
+ "app_views_plan": {
1777
+ "allowed_keys": ["app_key", "upsert_views", "remove_views", "preset", "upsert_views[].view_key"],
1778
+ "aliases": {
1779
+ "fields": "columns",
1780
+ "column_names": "columns",
1781
+ "columnNames": "columns",
1782
+ "viewKey": "view_key",
1783
+ "tableView": "table",
1784
+ "cardView": "card",
1785
+ "kanban": "board",
1786
+ "filter_rules": "filters",
1787
+ "filterRules": "filters",
1788
+ "startField": "start_field",
1789
+ "endField": "end_field",
1790
+ "titleField": "title_field",
1791
+ },
1792
+ "allowed_values": {
1793
+ "preset": [member.value for member in ViewsPreset],
1794
+ "view.type": [member.value for member in PublicViewType],
1795
+ "view.filter.operator": [member.value for member in ViewFilterOperator],
1796
+ },
1797
+ "minimal_example": {
1798
+ "profile": "default",
1799
+ "app_key": "APP_KEY",
1800
+ "upsert_views": [{"name": "全部数据", "type": "table", "columns": ["项目名称"]}],
1801
+ "remove_views": [],
1802
+ },
1803
+ "gantt_example": {
1804
+ "profile": "default",
1805
+ "app_key": "APP_KEY",
1806
+ "upsert_views": [
1807
+ {
1808
+ "name": "项目甘特图",
1809
+ "type": "gantt",
1810
+ "columns": ["项目名称", "开始日期", "结束日期"],
1811
+ "start_field": "开始日期",
1812
+ "end_field": "结束日期",
1813
+ "title_field": "项目名称",
1814
+ "filters": [{"field_name": "状态", "operator": "eq", "value": "进行中"}],
1815
+ }
1816
+ ],
1817
+ "remove_views": [],
1818
+ },
1819
+ },
1820
+ "app_views_apply": {
1821
+ "allowed_keys": ["app_key", "publish", "upsert_views", "remove_views", "upsert_views[].view_key"],
1822
+ "aliases": {
1823
+ "fields": "columns",
1824
+ "column_names": "columns",
1825
+ "columnNames": "columns",
1826
+ "viewKey": "view_key",
1827
+ "tableView": "table",
1828
+ "cardView": "card",
1829
+ "kanban": "board",
1830
+ "filter_rules": "filters",
1831
+ "filterRules": "filters",
1832
+ "startField": "start_field",
1833
+ "endField": "end_field",
1834
+ "titleField": "title_field",
1835
+ },
1836
+ "allowed_values": {
1837
+ "view.type": [member.value for member in PublicViewType],
1838
+ "view.filter.operator": [member.value for member in ViewFilterOperator],
1839
+ },
1840
+ "execution_notes": [
1841
+ "apply may return partial_success when some views land and others fail",
1842
+ "when duplicate view names exist, supply view_key to target the exact view",
1843
+ "read back app_read_views_summary after any failed or partial view apply",
1844
+ "view existence verification and saved-filter verification are separate; treat filters as unverified until verification.view_filters_verified is true",
1845
+ ],
1846
+ "minimal_example": {
1847
+ "profile": "default",
1848
+ "app_key": "APP_KEY",
1849
+ "publish": True,
1850
+ "upsert_views": [{"name": "全部数据", "type": "table", "columns": ["项目名称"]}],
1851
+ "remove_views": [],
1852
+ },
1853
+ "gantt_example": {
1854
+ "profile": "default",
1855
+ "app_key": "APP_KEY",
1856
+ "publish": True,
1857
+ "upsert_views": [
1858
+ {
1859
+ "name": "项目甘特图",
1860
+ "type": "gantt",
1861
+ "columns": ["项目名称", "开始日期", "结束日期"],
1862
+ "start_field": "开始日期",
1863
+ "end_field": "结束日期",
1864
+ "title_field": "项目名称",
1865
+ "filters": [{"field_name": "状态", "operator": "eq", "value": "进行中"}],
1866
+ }
1867
+ ],
1868
+ "remove_views": [],
1869
+ },
1870
+ },
1871
+ "app_read_charts_summary": {
1872
+ "allowed_keys": ["app_key"],
1873
+ "aliases": {},
1874
+ "allowed_values": {},
1875
+ "execution_notes": [
1876
+ "returns a compact current chart inventory for one app",
1877
+ "use this before app_charts_apply when you need exact current chart_id values",
1878
+ "chart summaries do not include full qingbi config payloads",
1879
+ ],
1880
+ "minimal_example": {
1881
+ "profile": "default",
1882
+ "app_key": "APP_KEY",
1883
+ },
1884
+ },
1885
+ "app_charts_apply": {
1886
+ "allowed_keys": ["app_key", "upsert_charts", "remove_chart_ids", "reorder_chart_ids"],
1887
+ "aliases": {
1888
+ "chart.id": "chart.chart_id",
1889
+ "chart.type": "chart.chart_type",
1890
+ "chart.dimension_fields": "chart.dimension_field_ids",
1891
+ "chart.indicator_fields": "chart.indicator_field_ids",
1892
+ "chart.metric_field_ids": "chart.indicator_field_ids",
1893
+ "chart.filter.op": "chart.filter.operator",
1894
+ },
1895
+ "allowed_values": {
1896
+ "chart.chart_type": [member.value for member in PublicChartType],
1897
+ "chart.filter.operator": [member.value for member in ViewFilterOperator],
1898
+ },
1899
+ "execution_notes": [
1900
+ "app_charts_apply is immediate-live and does not publish",
1901
+ "chart matching precedence is chart_id first, then exact unique chart name",
1902
+ "when chart names are not unique, supply chart_id instead of guessing by name",
1903
+ "successful create results must return a real backend chart_id",
1904
+ ],
1905
+ "minimal_example": {
1906
+ "profile": "default",
1907
+ "app_key": "APP_KEY",
1908
+ "upsert_charts": [{"name": "数据总量", "chart_type": "target", "indicator_field_ids": []}],
1909
+ "remove_chart_ids": [],
1910
+ "reorder_chart_ids": [],
1911
+ },
1912
+ },
1913
+ "chart_apply": {
1914
+ "allowed_keys": ["app_key", "upsert_charts", "remove_chart_ids", "reorder_chart_ids"],
1915
+ "aliases": {
1916
+ "legacy_tool_name": "app_charts_apply",
1917
+ },
1918
+ "allowed_values": {
1919
+ "chart.chart_type": [member.value for member in PublicChartType],
1920
+ "chart.filter.operator": [member.value for member in ViewFilterOperator],
1921
+ },
1922
+ "execution_notes": [
1923
+ "legacy compatibility alias; prefer app_charts_apply in new builder flows",
1924
+ "behavior matches app_charts_apply exactly",
1925
+ ],
1926
+ "minimal_example": {
1927
+ "profile": "default",
1928
+ "app_key": "APP_KEY",
1929
+ "upsert_charts": [{"name": "数据总量", "chart_type": "target", "indicator_field_ids": []}],
1930
+ "remove_chart_ids": [],
1931
+ "reorder_chart_ids": [],
1932
+ },
1933
+ },
1934
+ "portal_read_summary": {
1935
+ "allowed_keys": ["dash_key", "being_draft"],
1936
+ "aliases": {"beingDraft": "being_draft"},
1937
+ "allowed_values": {},
1938
+ "execution_notes": [
1939
+ "returns a compact portal summary instead of the raw dash payload",
1940
+ "being_draft=true reads the current draft view; being_draft=false reads live",
1941
+ "use this before portal_apply when you need the current section inventory or target dash metadata",
1942
+ ],
1943
+ "minimal_example": {
1944
+ "profile": "default",
1945
+ "dash_key": "DASH_KEY",
1946
+ "being_draft": True,
1947
+ },
1948
+ },
1949
+ "portal_apply": {
1950
+ "allowed_keys": ["dash_key", "dash_name", "package_tag_id", "publish", "sections", "auth", "icon", "color", "hide_copyright", "dash_global_config", "config"],
1951
+ "aliases": {
1952
+ "sourceType": "source_type",
1953
+ "chartRef": "chart_ref",
1954
+ "viewRef": "view_ref",
1955
+ "dashStyleConfigBO": "dash_style_config",
1956
+ },
1957
+ "section_allowed_keys": ["title", "source_type", "position", "dash_style_config", "config", "chart_ref", "view_ref", "text", "url"],
1958
+ "section_aliases": {
1959
+ "sourceType": "source_type",
1960
+ "chartRef": "chart_ref",
1961
+ "viewRef": "view_ref",
1962
+ "dashStyleConfigBO": "dash_style_config",
1963
+ },
1964
+ "allowed_values": {"section.source_type": ["chart", "view", "grid", "filter", "text", "link"]},
1965
+ "execution_notes": [
1966
+ "portal_apply uses replace semantics for sections",
1967
+ "remove a section by omitting it from the new sections list",
1968
+ "package_tag_id is required when creating a new portal",
1969
+ "publish=false only guarantees draft and base-info updates; it does not claim live has changed",
1970
+ "chart_ref resolves by chart_id first, then exact unique chart_name",
1971
+ "view_ref resolves by view_key first, then exact unique view_name",
1972
+ "position.pc/mobile is the canonical portal layout shape",
1973
+ ],
1974
+ "minimal_example": {
1975
+ "profile": "default",
1976
+ "dash_name": "经营门户",
1977
+ "package_tag_id": 1001,
1978
+ "publish": True,
1979
+ "sections": [
1980
+ {
1981
+ "title": "经营概览",
1982
+ "source_type": "chart",
1983
+ "chart_ref": {"app_key": "APP_KEY", "chart_name": "数据总量"},
1984
+ "position": {
1985
+ "pc": {"x": 0, "y": 0, "cols": 12, "rows": 6},
1986
+ "mobile": {"x": 0, "y": 0, "cols": 6, "rows": 6},
1987
+ },
1988
+ }
1989
+ ],
1990
+ },
1991
+ "minimal_section_example": {
1992
+ "title": "订单概览",
1993
+ "source_type": "view",
1994
+ "view_ref": {"app_key": "APP_KEY", "view_key": "VIEW_KEY"},
1995
+ "position": {
1996
+ "pc": {"x": 0, "y": 0, "cols": 24, "rows": 8},
1997
+ "mobile": {"x": 0, "y": 0, "cols": 6, "rows": 8},
1998
+ },
1999
+ },
2000
+ },
2001
+ }
2002
+
2003
+ _PRIVATE_BUILDER_TOOL_CONTRACTS = {
2004
+ "app_schema_plan",
2005
+ "app_layout_plan",
2006
+ "app_flow_plan",
2007
+ "app_views_plan",
2008
+ }
2009
+
2010
+ _BUILDER_TOOL_CONTRACT_ALIASES = {
2011
+ "chart_apply": "app_charts_apply",
2012
+ }