@josephyan/qingflow-cli 0.2.0-beta.55

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