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