@josephyan/qingflow-app-builder-mcp 1.1.20 → 1.1.22
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/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +1 -1
- package/skills/qingflow-app-builder/references/complete-system-development-guide.md +2 -2
- package/skills/qingflow-app-builder/references/create-app.md +3 -6
- package/skills/qingflow-app-builder/references/tool-selection.md +4 -4
- package/src/qingflow_mcp/builder_facade/models.py +0 -1
- package/src/qingflow_mcp/builder_facade/service.py +1730 -488
- package/src/qingflow_mcp/cli/commands/builder.py +7 -19
- package/src/qingflow_mcp/server_app_builder.py +8 -13
- package/src/qingflow_mcp/tools/ai_builder_tools.py +761 -368
|
@@ -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,
|
|
@@ -107,6 +108,26 @@ def _payload_bool(payload: JSONObject, *keys: str, default: bool = True) -> bool
|
|
|
107
108
|
return bool(value)
|
|
108
109
|
|
|
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
|
+
|
|
110
131
|
PUBLIC_STABLE_FLOW_NODE_TYPES = ["start", "approve", "fill", "copy", "webhook", "end"]
|
|
111
132
|
BUILDER_APPLY_SCHEMA_VERSION = "builder.apply.v1"
|
|
112
133
|
BUILDER_APPLY_TOOL_NAMES = {
|
|
@@ -122,6 +143,7 @@ BUILDER_APPLY_TOOL_NAMES = {
|
|
|
122
143
|
"portal_delete",
|
|
123
144
|
"app_publish_verify",
|
|
124
145
|
}
|
|
146
|
+
MULTI_APP_SCHEMA_APPLY_PARALLEL_LIMIT = 10
|
|
125
147
|
|
|
126
148
|
|
|
127
149
|
class AiBuilderTools(ToolBase):
|
|
@@ -151,13 +173,28 @@ class AiBuilderTools(ToolBase):
|
|
|
151
173
|
)
|
|
152
174
|
|
|
153
175
|
def _apply_app_batch(self, *, tool_name: str, profile: str, apps: list[JSONObject], apply_one) -> JSONObject:
|
|
176
|
+
started_at = time.perf_counter()
|
|
177
|
+
batch_item_apply_ms = 0
|
|
178
|
+
child_total_ms = 0
|
|
179
|
+
|
|
180
|
+
def finish(payload: JSONObject) -> JSONObject:
|
|
181
|
+
_merge_duration_breakdown(
|
|
182
|
+
payload,
|
|
183
|
+
batch_item_apply_ms=batch_item_apply_ms,
|
|
184
|
+
child_total_ms=child_total_ms,
|
|
185
|
+
total_ms=_elapsed_ms(started_at),
|
|
186
|
+
)
|
|
187
|
+
return _attach_builder_apply_envelope(tool_name, payload)
|
|
188
|
+
|
|
154
189
|
if not isinstance(apps, list) or not apps:
|
|
155
|
-
return
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
190
|
+
return finish(
|
|
191
|
+
_config_failure(
|
|
192
|
+
tool_name=tool_name,
|
|
193
|
+
message=f"{tool_name} batch mode requires non-empty apps[].",
|
|
194
|
+
fix_hint="Pass apps as a JSON array; each item must include app_key plus that resource's normal payload.",
|
|
195
|
+
details={"expected_shape": {"apps": [{"app_key": "APP_KEY"}]}},
|
|
196
|
+
)
|
|
197
|
+
)
|
|
161
198
|
|
|
162
199
|
app_results: list[JSONObject] = []
|
|
163
200
|
errors: list[JSONObject] = []
|
|
@@ -186,8 +223,10 @@ class AiBuilderTools(ToolBase):
|
|
|
186
223
|
app_results.append(error)
|
|
187
224
|
continue
|
|
188
225
|
try:
|
|
226
|
+
item_started_at = time.perf_counter()
|
|
189
227
|
result = apply_one(index, item, app_key)
|
|
190
228
|
except (QingflowApiError, RuntimeError) as error:
|
|
229
|
+
batch_item_apply_ms += _elapsed_ms(item_started_at)
|
|
191
230
|
api_error = _coerce_api_error(error)
|
|
192
231
|
result = {
|
|
193
232
|
"status": "failed",
|
|
@@ -201,6 +240,13 @@ class AiBuilderTools(ToolBase):
|
|
|
201
240
|
"backend_code": api_error.backend_code,
|
|
202
241
|
"http_status": None if api_error.http_status == 404 else api_error.http_status,
|
|
203
242
|
}
|
|
243
|
+
else:
|
|
244
|
+
batch_item_apply_ms += _elapsed_ms(item_started_at)
|
|
245
|
+
if isinstance(result, dict):
|
|
246
|
+
child_breakdown = result.get("duration_breakdown_ms")
|
|
247
|
+
child_total = child_breakdown.get("total_ms") if isinstance(child_breakdown, dict) else None
|
|
248
|
+
if isinstance(child_total, (int, float)) and child_total >= 0:
|
|
249
|
+
child_total_ms += int(child_total)
|
|
204
250
|
status = str(result.get("status") or "success") if isinstance(result, dict) else "failed"
|
|
205
251
|
wrapped: JSONObject = {
|
|
206
252
|
"index": index,
|
|
@@ -250,7 +296,7 @@ class AiBuilderTools(ToolBase):
|
|
|
250
296
|
"write_succeeded": write_executed and failed == 0 and partial == 0,
|
|
251
297
|
"safe_to_retry": not write_executed,
|
|
252
298
|
}
|
|
253
|
-
return
|
|
299
|
+
return finish(payload)
|
|
254
300
|
|
|
255
301
|
def _read_app_batch(self, *, tool_name: str, profile: str, app_keys: list[str], read_one) -> JSONObject:
|
|
256
302
|
normalized_keys = [str(item).strip() for item in app_keys if str(item).strip()]
|
|
@@ -336,7 +382,6 @@ class AiBuilderTools(ToolBase):
|
|
|
336
382
|
profile: str = DEFAULT_PROFILE,
|
|
337
383
|
package_id: int | None = None,
|
|
338
384
|
package_name: str | None = None,
|
|
339
|
-
create_if_missing: bool | None = None,
|
|
340
385
|
icon: str | JSONObject | None = None,
|
|
341
386
|
color: str | None = None,
|
|
342
387
|
icon_name: str | None = None,
|
|
@@ -350,7 +395,6 @@ class AiBuilderTools(ToolBase):
|
|
|
350
395
|
profile=profile,
|
|
351
396
|
package_id=package_id,
|
|
352
397
|
package_name=package_name,
|
|
353
|
-
create_if_missing=create_if_missing,
|
|
354
398
|
icon=icon,
|
|
355
399
|
color=color,
|
|
356
400
|
icon_name=icon_name,
|
|
@@ -579,7 +623,6 @@ class AiBuilderTools(ToolBase):
|
|
|
579
623
|
icon_color: str | None = None,
|
|
580
624
|
icon_config: JSONObject | None = None,
|
|
581
625
|
visibility: JSONObject | None = None,
|
|
582
|
-
create_if_missing: bool | None = None,
|
|
583
626
|
publish: bool = True,
|
|
584
627
|
form: list[JSONObject] | None = None,
|
|
585
628
|
add_fields: list[JSONObject] | None = None,
|
|
@@ -591,7 +634,7 @@ class AiBuilderTools(ToolBase):
|
|
|
591
634
|
if app_key or app_name or app_title or form or add_fields or update_fields or remove_fields:
|
|
592
635
|
return _config_failure(
|
|
593
636
|
tool_name="app_schema_apply",
|
|
594
|
-
message="app_schema_apply multi-app mode accepts package_id plus apps only
|
|
637
|
+
message="app_schema_apply multi-app mode accepts package_id plus apps only.",
|
|
595
638
|
fix_hint="Put per-app form in apps[].form when using batch mode.",
|
|
596
639
|
)
|
|
597
640
|
if package_id is None:
|
|
@@ -604,7 +647,6 @@ class AiBuilderTools(ToolBase):
|
|
|
604
647
|
profile=profile,
|
|
605
648
|
package_id=package_id,
|
|
606
649
|
visibility=visibility,
|
|
607
|
-
create_if_missing=True if create_if_missing is None else bool(create_if_missing),
|
|
608
650
|
publish=publish,
|
|
609
651
|
apps=apps,
|
|
610
652
|
form=None,
|
|
@@ -616,20 +658,19 @@ class AiBuilderTools(ToolBase):
|
|
|
616
658
|
has_app_name = bool((app_name or "").strip())
|
|
617
659
|
has_app_title = bool((app_title or "").strip())
|
|
618
660
|
has_package_id = package_id is not None
|
|
619
|
-
effective_create_if_missing = bool(create_if_missing)
|
|
620
661
|
if has_app_key:
|
|
621
|
-
if
|
|
662
|
+
if has_package_id:
|
|
622
663
|
return _config_failure(
|
|
623
664
|
tool_name="app_schema_apply",
|
|
624
665
|
message="app_schema_apply edit mode accepts app_key and optional app_name rename only.",
|
|
625
|
-
fix_hint="For existing apps, use `app_key` and optionally `app_name`. For create mode, use `package_id + app_name
|
|
666
|
+
fix_hint="For existing apps, use `app_key` and optionally `app_name`. For create mode, use `package_id + app_name`.",
|
|
626
667
|
)
|
|
627
|
-
elif not (
|
|
668
|
+
elif not (has_package_id and (has_app_name or has_app_title)):
|
|
628
669
|
return _config_failure(
|
|
629
670
|
tool_name="app_schema_apply",
|
|
630
|
-
message="app_schema_apply create mode requires package_id
|
|
631
|
-
|
|
632
|
-
|
|
671
|
+
message="app_schema_apply create mode requires package_id and app_name.",
|
|
672
|
+
fix_hint="Use `app_key` for existing apps, or pass `package_id + app_name` to create a new app.",
|
|
673
|
+
)
|
|
633
674
|
return self.app_schema_apply(
|
|
634
675
|
profile=profile,
|
|
635
676
|
app_key=app_key,
|
|
@@ -642,7 +683,6 @@ class AiBuilderTools(ToolBase):
|
|
|
642
683
|
icon_color=icon_color,
|
|
643
684
|
icon_config=icon_config,
|
|
644
685
|
visibility=visibility,
|
|
645
|
-
create_if_missing=effective_create_if_missing,
|
|
646
686
|
publish=publish,
|
|
647
687
|
form=form or [],
|
|
648
688
|
add_fields=add_fields or [],
|
|
@@ -989,7 +1029,6 @@ class AiBuilderTools(ToolBase):
|
|
|
989
1029
|
profile: str,
|
|
990
1030
|
package_id: int | None = None,
|
|
991
1031
|
package_name: str | None = None,
|
|
992
|
-
create_if_missing: bool | None = None,
|
|
993
1032
|
icon: str | JSONObject | None = None,
|
|
994
1033
|
color: str | None = None,
|
|
995
1034
|
icon_name: str | None = None,
|
|
@@ -1020,14 +1059,13 @@ class AiBuilderTools(ToolBase):
|
|
|
1020
1059
|
tool_name="package_apply",
|
|
1021
1060
|
icon=icon,
|
|
1022
1061
|
color=color,
|
|
1023
|
-
creating=package_id is None and bool(
|
|
1062
|
+
creating=package_id is None and bool(str(package_name or "").strip()),
|
|
1024
1063
|
)
|
|
1025
1064
|
if icon_failure is not None:
|
|
1026
1065
|
return _attach_builder_apply_envelope("package_apply", icon_failure)
|
|
1027
1066
|
normalized_args = {
|
|
1028
1067
|
"package_id": package_id,
|
|
1029
1068
|
**({"package_name": package_name} if str(package_name or "").strip() else {}),
|
|
1030
|
-
"create_if_missing": bool(create_if_missing),
|
|
1031
1069
|
**({"icon": icon} if icon else {}),
|
|
1032
1070
|
**({"color": color} if color else {}),
|
|
1033
1071
|
**({"visibility": visibility_patch.model_dump(mode="json")} if visibility_patch is not None else {}),
|
|
@@ -1039,7 +1077,6 @@ class AiBuilderTools(ToolBase):
|
|
|
1039
1077
|
profile=profile,
|
|
1040
1078
|
package_id=package_id,
|
|
1041
1079
|
package_name=package_name,
|
|
1042
|
-
create_if_missing=create_if_missing,
|
|
1043
1080
|
icon=icon,
|
|
1044
1081
|
color=color,
|
|
1045
1082
|
visibility=visibility_patch,
|
|
@@ -1859,7 +1896,6 @@ class AiBuilderTools(ToolBase):
|
|
|
1859
1896
|
icon: str = "",
|
|
1860
1897
|
color: str = "",
|
|
1861
1898
|
visibility: JSONObject | None = None,
|
|
1862
|
-
create_if_missing: bool | None = None,
|
|
1863
1899
|
add_fields: list[JSONObject],
|
|
1864
1900
|
update_fields: list[JSONObject],
|
|
1865
1901
|
remove_fields: list[JSONObject],
|
|
@@ -1874,7 +1910,6 @@ class AiBuilderTools(ToolBase):
|
|
|
1874
1910
|
icon=icon,
|
|
1875
1911
|
color=color,
|
|
1876
1912
|
visibility=visibility,
|
|
1877
|
-
create_if_missing=create_if_missing,
|
|
1878
1913
|
add_fields=add_fields,
|
|
1879
1914
|
update_fields=update_fields,
|
|
1880
1915
|
remove_fields=remove_fields,
|
|
@@ -1890,7 +1925,6 @@ class AiBuilderTools(ToolBase):
|
|
|
1890
1925
|
"icon": icon,
|
|
1891
1926
|
"color": color,
|
|
1892
1927
|
"visibility": visibility,
|
|
1893
|
-
"create_if_missing": create_if_missing,
|
|
1894
1928
|
"add_fields": add_fields,
|
|
1895
1929
|
"update_fields": update_fields,
|
|
1896
1930
|
"remove_fields": remove_fields,
|
|
@@ -1911,7 +1945,6 @@ class AiBuilderTools(ToolBase):
|
|
|
1911
1945
|
"icon": icon,
|
|
1912
1946
|
"color": color,
|
|
1913
1947
|
"visibility": visibility,
|
|
1914
|
-
"create_if_missing": create_if_missing,
|
|
1915
1948
|
"add_fields": [{"name": "字段名称", "type": "text"}],
|
|
1916
1949
|
"update_fields": [],
|
|
1917
1950
|
"remove_fields": [],
|
|
@@ -2106,7 +2139,6 @@ class AiBuilderTools(ToolBase):
|
|
|
2106
2139
|
icon_color: str | None = None,
|
|
2107
2140
|
icon_config: JSONObject | None = None,
|
|
2108
2141
|
visibility: JSONObject | None = None,
|
|
2109
|
-
create_if_missing: bool | None = None,
|
|
2110
2142
|
publish: bool = True,
|
|
2111
2143
|
form: list[JSONObject] | None = None,
|
|
2112
2144
|
add_fields: list[JSONObject],
|
|
@@ -2153,7 +2185,6 @@ class AiBuilderTools(ToolBase):
|
|
|
2153
2185
|
profile=profile,
|
|
2154
2186
|
package_id=package_id,
|
|
2155
2187
|
visibility=visibility,
|
|
2156
|
-
create_if_missing=True if create_if_missing is None else bool(create_if_missing),
|
|
2157
2188
|
publish=publish,
|
|
2158
2189
|
apps=apps,
|
|
2159
2190
|
)
|
|
@@ -2187,7 +2218,6 @@ class AiBuilderTools(ToolBase):
|
|
|
2187
2218
|
icon=icon,
|
|
2188
2219
|
color=color,
|
|
2189
2220
|
visibility=visibility,
|
|
2190
|
-
create_if_missing=bool(create_if_missing),
|
|
2191
2221
|
publish=publish,
|
|
2192
2222
|
add_fields=add_fields,
|
|
2193
2223
|
update_fields=update_fields,
|
|
@@ -2206,7 +2236,6 @@ class AiBuilderTools(ToolBase):
|
|
|
2206
2236
|
icon=icon,
|
|
2207
2237
|
color=color,
|
|
2208
2238
|
visibility=visibility,
|
|
2209
|
-
create_if_missing=create_if_missing,
|
|
2210
2239
|
publish=publish,
|
|
2211
2240
|
add_fields=add_fields,
|
|
2212
2241
|
update_fields=update_fields,
|
|
@@ -2222,52 +2251,114 @@ class AiBuilderTools(ToolBase):
|
|
|
2222
2251
|
profile: str,
|
|
2223
2252
|
package_id: int | None,
|
|
2224
2253
|
visibility: JSONObject | None,
|
|
2225
|
-
create_if_missing: bool,
|
|
2226
2254
|
publish: bool,
|
|
2227
2255
|
apps: list[JSONObject],
|
|
2228
2256
|
) -> JSONObject:
|
|
2257
|
+
started_at = time.perf_counter()
|
|
2258
|
+
validation_ms = 0
|
|
2259
|
+
app_shell_apply_ms = 0
|
|
2260
|
+
relation_compile_ms = 0
|
|
2261
|
+
app_field_apply_ms = 0
|
|
2262
|
+
inline_layout_fallback_ms = 0
|
|
2263
|
+
child_schema_write_ms = 0
|
|
2264
|
+
child_inline_layout_ms = 0
|
|
2265
|
+
child_publish_ms = 0
|
|
2266
|
+
child_readback_ms = 0
|
|
2229
2267
|
normalized_args: JSONObject = {
|
|
2230
2268
|
"package_id": package_id,
|
|
2231
|
-
"create_if_missing": create_if_missing,
|
|
2232
2269
|
"publish": publish,
|
|
2233
2270
|
"apps": deepcopy(apps),
|
|
2234
2271
|
}
|
|
2235
2272
|
if visibility is not None:
|
|
2236
2273
|
normalized_args["visibility"] = deepcopy(visibility)
|
|
2274
|
+
parallelism: JSONObject = {"shell_workers": 0, "schema_workers": 0}
|
|
2275
|
+
|
|
2276
|
+
def merge_child_duration(child: JSONObject) -> None:
|
|
2277
|
+
nonlocal child_schema_write_ms
|
|
2278
|
+
nonlocal child_inline_layout_ms
|
|
2279
|
+
nonlocal child_publish_ms
|
|
2280
|
+
nonlocal child_readback_ms
|
|
2281
|
+
breakdown = child.get("duration_breakdown_ms") if isinstance(child, dict) else None
|
|
2282
|
+
if not isinstance(breakdown, dict):
|
|
2283
|
+
return
|
|
2284
|
+
for key, target in (
|
|
2285
|
+
("schema_write_ms", "child_schema_write_ms"),
|
|
2286
|
+
("inline_layout_ms", "child_inline_layout_ms"),
|
|
2287
|
+
("publish_ms", "child_publish_ms"),
|
|
2288
|
+
("readback_ms", "child_readback_ms"),
|
|
2289
|
+
):
|
|
2290
|
+
value = breakdown.get(key)
|
|
2291
|
+
if isinstance(value, (int, float)) and value >= 0:
|
|
2292
|
+
if target == "child_schema_write_ms":
|
|
2293
|
+
child_schema_write_ms += int(value)
|
|
2294
|
+
elif target == "child_inline_layout_ms":
|
|
2295
|
+
child_inline_layout_ms += int(value)
|
|
2296
|
+
elif target == "child_publish_ms":
|
|
2297
|
+
child_publish_ms += int(value)
|
|
2298
|
+
elif target == "child_readback_ms":
|
|
2299
|
+
child_readback_ms += int(value)
|
|
2300
|
+
|
|
2301
|
+
def finish(response: JSONObject) -> JSONObject:
|
|
2302
|
+
_merge_duration_breakdown(
|
|
2303
|
+
response,
|
|
2304
|
+
multi_app_validation_ms=validation_ms,
|
|
2305
|
+
app_shell_apply_ms=app_shell_apply_ms,
|
|
2306
|
+
app_shell_parallel_wall_ms=app_shell_apply_ms,
|
|
2307
|
+
relation_compile_ms=relation_compile_ms,
|
|
2308
|
+
app_field_apply_ms=app_field_apply_ms,
|
|
2309
|
+
app_schema_parallel_wall_ms=app_field_apply_ms,
|
|
2310
|
+
native_inline_layout_ms=child_inline_layout_ms,
|
|
2311
|
+
inline_layout_fallback_ms=inline_layout_fallback_ms,
|
|
2312
|
+
child_schema_write_ms=child_schema_write_ms,
|
|
2313
|
+
child_inline_layout_ms=child_inline_layout_ms,
|
|
2314
|
+
child_publish_ms=child_publish_ms,
|
|
2315
|
+
child_readback_ms=child_readback_ms,
|
|
2316
|
+
total_ms=_elapsed_ms(started_at),
|
|
2317
|
+
)
|
|
2318
|
+
return response
|
|
2319
|
+
|
|
2320
|
+
validation_started_at = time.perf_counter()
|
|
2237
2321
|
system_field_failure = _reserved_system_field_name_failure_for_apps(
|
|
2238
2322
|
tool_name="app_schema_apply",
|
|
2239
2323
|
profile=profile,
|
|
2240
2324
|
package_id=package_id,
|
|
2241
2325
|
visibility=visibility,
|
|
2242
|
-
create_if_missing=create_if_missing,
|
|
2243
2326
|
publish=publish,
|
|
2244
2327
|
apps=apps,
|
|
2245
2328
|
)
|
|
2246
2329
|
if system_field_failure is not None:
|
|
2247
|
-
|
|
2330
|
+
validation_ms += _elapsed_ms(validation_started_at)
|
|
2331
|
+
return finish(system_field_failure)
|
|
2248
2332
|
if package_id is None:
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2333
|
+
validation_ms += _elapsed_ms(validation_started_at)
|
|
2334
|
+
return finish(
|
|
2335
|
+
_config_failure(
|
|
2336
|
+
tool_name="app_schema_apply",
|
|
2337
|
+
message="app_schema_apply multi-app mode requires package_id.",
|
|
2338
|
+
fix_hint="Pass package_id and apps[].app_name for new apps, or apps[].app_key for existing apps.",
|
|
2339
|
+
)
|
|
2253
2340
|
)
|
|
2254
2341
|
if not apps:
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2342
|
+
validation_ms += _elapsed_ms(validation_started_at)
|
|
2343
|
+
return finish(
|
|
2344
|
+
_config_failure(
|
|
2345
|
+
tool_name="app_schema_apply",
|
|
2346
|
+
error_code="APPS_FILE_EMPTY",
|
|
2347
|
+
message="app_schema_apply multi-app mode requires non-empty apps.",
|
|
2348
|
+
fix_hint="Pass apps as a non-empty list of app schema items.",
|
|
2349
|
+
)
|
|
2260
2350
|
)
|
|
2261
2351
|
shape_failure = _schema_apps_shape_failure(tool_name="app_schema_apply", apps=apps)
|
|
2262
2352
|
if shape_failure is not None:
|
|
2263
|
-
|
|
2353
|
+
validation_ms += _elapsed_ms(validation_started_at)
|
|
2354
|
+
return finish(shape_failure)
|
|
2264
2355
|
static_validation_failure = _multi_app_static_validation_failure(
|
|
2265
2356
|
tool_name="app_schema_apply",
|
|
2266
2357
|
apps=apps,
|
|
2267
|
-
create_if_missing=create_if_missing,
|
|
2268
2358
|
)
|
|
2269
2359
|
if static_validation_failure is not None:
|
|
2270
|
-
|
|
2360
|
+
validation_ms += _elapsed_ms(validation_started_at)
|
|
2361
|
+
return finish(static_validation_failure)
|
|
2271
2362
|
icon_errors: list[JSONObject] = []
|
|
2272
2363
|
seen_new_app_icons: dict[str, int] = {}
|
|
2273
2364
|
for index, raw_item in enumerate(apps):
|
|
@@ -2318,32 +2409,111 @@ class AiBuilderTools(ToolBase):
|
|
|
2318
2409
|
else:
|
|
2319
2410
|
seen_new_app_icons[normalized_icon] = index
|
|
2320
2411
|
if icon_errors:
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
"
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2412
|
+
validation_ms += _elapsed_ms(validation_started_at)
|
|
2413
|
+
return finish(
|
|
2414
|
+
_config_failure(
|
|
2415
|
+
tool_name="app_schema_apply",
|
|
2416
|
+
error_code="WORKSPACE_ICON_BATCH_INVALID",
|
|
2417
|
+
message="one or more apps have invalid workspace icon configuration",
|
|
2418
|
+
fix_hint="Call `qingflow --json builder icon catalog`, choose a distinct non-template icon and color for each new app, then retry.",
|
|
2419
|
+
details={"icon_errors": icon_errors},
|
|
2420
|
+
allowed_values={
|
|
2421
|
+
"workspace_icon.icon_names": list(WORKSPACE_ICON_NAMES),
|
|
2422
|
+
"workspace_icon.icon_colors": list(WORKSPACE_ICON_COLORS),
|
|
2423
|
+
"workspace_icon.generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
|
|
2424
|
+
},
|
|
2425
|
+
)
|
|
2332
2426
|
)
|
|
2427
|
+
validation_ms += _elapsed_ms(validation_started_at)
|
|
2333
2428
|
|
|
2334
2429
|
client_key_to_app_key: dict[str, str] = {}
|
|
2335
2430
|
app_name_to_app_key: dict[str, str] = {}
|
|
2336
2431
|
created_app_keys: list[str] = []
|
|
2337
|
-
|
|
2432
|
+
results_by_index: list[JSONObject | None] = [None] * len(apps)
|
|
2433
|
+
shell_jobs: list[JSONObject] = []
|
|
2338
2434
|
any_write_executed = False
|
|
2339
2435
|
any_write_may_have_succeeded = False
|
|
2340
2436
|
pending_readback_app_keys: list[str] = []
|
|
2341
2437
|
pending_readback_app_names: list[str] = []
|
|
2342
2438
|
client_keys: set[str] = set()
|
|
2439
|
+
client_key_indexes: dict[str, int] = {}
|
|
2440
|
+
app_name_indexes: dict[str, int] = {}
|
|
2441
|
+
|
|
2442
|
+
def worker_count(count: int) -> int:
|
|
2443
|
+
return min(MULTI_APP_SCHEMA_APPLY_PARALLEL_LIMIT, count) if count > 0 else 0
|
|
2444
|
+
|
|
2445
|
+
def mark_inline_layout_not_applied_in_schema(
|
|
2446
|
+
result: JSONObject,
|
|
2447
|
+
*,
|
|
2448
|
+
app_key: str,
|
|
2449
|
+
sections: list[JSONObject],
|
|
2450
|
+
) -> JSONObject:
|
|
2451
|
+
marked = deepcopy(result)
|
|
2452
|
+
inline_payload: JSONObject = {
|
|
2453
|
+
"requested": True,
|
|
2454
|
+
"section_count": len(sections),
|
|
2455
|
+
"app_key": app_key or None,
|
|
2456
|
+
"applied": False,
|
|
2457
|
+
"applied_in_schema": False,
|
|
2458
|
+
"error_code": "INLINE_FORM_LAYOUT_NOT_APPLIED_IN_SCHEMA",
|
|
2459
|
+
"message": "schema save did not apply inline form layout; multi-app mode does not run app_layout_apply fallback",
|
|
2460
|
+
}
|
|
2461
|
+
marked["inline_form_layout"] = inline_payload
|
|
2462
|
+
verification = marked.get("verification") if isinstance(marked.get("verification"), dict) else {}
|
|
2463
|
+
verification = deepcopy(verification)
|
|
2464
|
+
verification["inline_form_layout_verified"] = False
|
|
2465
|
+
marked["verification"] = verification
|
|
2466
|
+
if marked.get("status") in {"success", "partial_success"}:
|
|
2467
|
+
marked["status"] = "partial_success"
|
|
2468
|
+
marked["error_code"] = marked.get("error_code") or "INLINE_FORM_LAYOUT_NOT_APPLIED_IN_SCHEMA"
|
|
2469
|
+
marked["message"] = "schema applied but inline form layout was not applied in schema save"
|
|
2470
|
+
marked["safe_to_retry"] = False
|
|
2471
|
+
return marked
|
|
2472
|
+
|
|
2473
|
+
def dependency_failure_for_item(index: int, item: JSONObject) -> JSONObject | None:
|
|
2474
|
+
blocked: list[JSONObject] = []
|
|
2475
|
+
for ref in _collect_multi_app_target_refs(item, path=f"apps[{index}]"):
|
|
2476
|
+
ref_value = str(ref.get("value") or "").strip()
|
|
2477
|
+
if not ref_value:
|
|
2478
|
+
continue
|
|
2479
|
+
target_index: int | None = None
|
|
2480
|
+
resolved = False
|
|
2481
|
+
if ref.get("kind") == "target_app_ref":
|
|
2482
|
+
target_index = client_key_indexes.get(ref_value)
|
|
2483
|
+
resolved = ref_value in client_key_to_app_key
|
|
2484
|
+
elif ref.get("kind") == "target_app":
|
|
2485
|
+
target_index = app_name_indexes.get(ref_value)
|
|
2486
|
+
resolved = ref_value in app_name_to_app_key
|
|
2487
|
+
if target_index is None or resolved:
|
|
2488
|
+
continue
|
|
2489
|
+
target_result = results_by_index[target_index] if 0 <= target_index < len(results_by_index) else None
|
|
2490
|
+
blocked.append(
|
|
2491
|
+
{
|
|
2492
|
+
"kind": ref.get("kind"),
|
|
2493
|
+
"value": ref_value,
|
|
2494
|
+
"path": ref.get("path"),
|
|
2495
|
+
"target_index": target_index,
|
|
2496
|
+
"target_status": target_result.get("status") if isinstance(target_result, dict) else None,
|
|
2497
|
+
"target_error_code": target_result.get("error_code") if isinstance(target_result, dict) else None,
|
|
2498
|
+
}
|
|
2499
|
+
)
|
|
2500
|
+
if not blocked:
|
|
2501
|
+
return None
|
|
2502
|
+
raw_item = apps[index] if 0 <= index < len(apps) else item
|
|
2503
|
+
failure = _multi_app_item_failure(
|
|
2504
|
+
index,
|
|
2505
|
+
raw_item,
|
|
2506
|
+
"DEPENDENCY_APP_NOT_READY",
|
|
2507
|
+
"one or more target apps failed or require readback, so this app schema was not written",
|
|
2508
|
+
)
|
|
2509
|
+
failure["stage"] = "dependency_check"
|
|
2510
|
+
failure["safe_to_retry"] = False
|
|
2511
|
+
failure["details"] = {"blocked_dependencies": blocked}
|
|
2512
|
+
return failure
|
|
2343
2513
|
|
|
2344
2514
|
for index, raw_item in enumerate(apps):
|
|
2345
2515
|
if not isinstance(raw_item, dict):
|
|
2346
|
-
|
|
2516
|
+
results_by_index[index] = _multi_app_item_failure(index, raw_item, "INVALID_APP_ITEM", "apps[] items must be objects")
|
|
2347
2517
|
continue
|
|
2348
2518
|
item = deepcopy(raw_item)
|
|
2349
2519
|
client_key = str(item.get("client_key") or item.get("clientKey") or "").strip()
|
|
@@ -2351,31 +2521,104 @@ class AiBuilderTools(ToolBase):
|
|
|
2351
2521
|
app_key = str(item.get("app_key") or item.get("appKey") or "").strip()
|
|
2352
2522
|
if client_key:
|
|
2353
2523
|
if client_key in client_keys:
|
|
2354
|
-
|
|
2524
|
+
results_by_index[index] = _multi_app_item_failure(index, item, "DUPLICATE_CLIENT_KEY", f"duplicate client_key '{client_key}'")
|
|
2355
2525
|
continue
|
|
2356
2526
|
client_keys.add(client_key)
|
|
2527
|
+
client_key_indexes[client_key] = index
|
|
2528
|
+
if app_name:
|
|
2529
|
+
app_name_indexes[app_name] = index
|
|
2357
2530
|
if not app_key and not app_name:
|
|
2358
|
-
|
|
2531
|
+
results_by_index[index] = _multi_app_item_failure(index, item, "APP_SELECTOR_REQUIRED", "apps[] requires app_key or app_name")
|
|
2359
2532
|
continue
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
app_title="",
|
|
2369
|
-
icon=str(item.get("icon") or ""),
|
|
2370
|
-
color=str(item.get("color") or ""),
|
|
2371
|
-
visibility=item.get("visibility", visibility),
|
|
2372
|
-
create_if_missing=create_if_missing and not app_key,
|
|
2373
|
-
publish=publish and not deferred_add_fields,
|
|
2374
|
-
add_fields=initial_add_fields,
|
|
2375
|
-
update_fields=[],
|
|
2376
|
-
remove_fields=[],
|
|
2533
|
+
shell_jobs.append(
|
|
2534
|
+
{
|
|
2535
|
+
"index": index,
|
|
2536
|
+
"item": item,
|
|
2537
|
+
"client_key": client_key,
|
|
2538
|
+
"app_name": app_name,
|
|
2539
|
+
"app_key": app_key,
|
|
2540
|
+
}
|
|
2377
2541
|
)
|
|
2378
|
-
|
|
2542
|
+
|
|
2543
|
+
shell_workers = worker_count(len(shell_jobs))
|
|
2544
|
+
parallelism["shell_workers"] = shell_workers
|
|
2545
|
+
|
|
2546
|
+
def run_shell_job(job: JSONObject) -> JSONObject:
|
|
2547
|
+
index = int(job["index"])
|
|
2548
|
+
item = deepcopy(job["item"])
|
|
2549
|
+
app_key = str(job.get("app_key") or "")
|
|
2550
|
+
app_name = str(job.get("app_name") or "")
|
|
2551
|
+
shell_started_at = time.perf_counter()
|
|
2552
|
+
try:
|
|
2553
|
+
shell = self._app_schema_apply_once(
|
|
2554
|
+
profile=profile,
|
|
2555
|
+
app_key=app_key,
|
|
2556
|
+
package_id=package_id if not app_key else None,
|
|
2557
|
+
app_name=app_name,
|
|
2558
|
+
app_title="",
|
|
2559
|
+
icon=str(item.get("icon") or ""),
|
|
2560
|
+
color=str(item.get("color") or ""),
|
|
2561
|
+
visibility=item.get("visibility", visibility),
|
|
2562
|
+
publish=False,
|
|
2563
|
+
add_fields=[],
|
|
2564
|
+
update_fields=[],
|
|
2565
|
+
remove_fields=[],
|
|
2566
|
+
inline_form_layout_sections=None,
|
|
2567
|
+
allow_inline_layout_fallback=False,
|
|
2568
|
+
)
|
|
2569
|
+
return {
|
|
2570
|
+
"index": index,
|
|
2571
|
+
"job": job,
|
|
2572
|
+
"elapsed_ms": _elapsed_ms(shell_started_at),
|
|
2573
|
+
"public_shell": _publicize_package_fields(shell),
|
|
2574
|
+
}
|
|
2575
|
+
except Exception as error: # pragma: no cover - defensive guard for unexpected worker failures
|
|
2576
|
+
return {
|
|
2577
|
+
"index": index,
|
|
2578
|
+
"job": job,
|
|
2579
|
+
"elapsed_ms": _elapsed_ms(shell_started_at),
|
|
2580
|
+
"exception": repr(error),
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
shell_outputs: list[JSONObject] = []
|
|
2584
|
+
shell_phase_started_at = time.perf_counter()
|
|
2585
|
+
if shell_jobs:
|
|
2586
|
+
with ThreadPoolExecutor(max_workers=shell_workers) as executor:
|
|
2587
|
+
futures = [executor.submit(run_shell_job, job) for job in shell_jobs]
|
|
2588
|
+
for future in as_completed(futures):
|
|
2589
|
+
shell_outputs.append(future.result())
|
|
2590
|
+
app_shell_apply_ms += _elapsed_ms(shell_phase_started_at)
|
|
2591
|
+
|
|
2592
|
+
for output in sorted(shell_outputs, key=lambda item: int(item.get("index", 0))):
|
|
2593
|
+
index = int(output["index"])
|
|
2594
|
+
job = output.get("job") if isinstance(output.get("job"), dict) else {}
|
|
2595
|
+
client_key = str(job.get("client_key") or "").strip()
|
|
2596
|
+
app_name = str(job.get("app_name") or "").strip()
|
|
2597
|
+
app_key = str(job.get("app_key") or "").strip()
|
|
2598
|
+
item = deepcopy(job.get("item") if isinstance(job.get("item"), dict) else {})
|
|
2599
|
+
exception = output.get("exception")
|
|
2600
|
+
if exception:
|
|
2601
|
+
results_by_index[index] = {
|
|
2602
|
+
"index": index,
|
|
2603
|
+
"row_number": index + 1,
|
|
2604
|
+
"client_key": client_key or None,
|
|
2605
|
+
"app_name": app_name or None,
|
|
2606
|
+
"app_key": app_key or None,
|
|
2607
|
+
"status": "pending_readback",
|
|
2608
|
+
"stage": "resolve_or_create_shell",
|
|
2609
|
+
"error_code": "APP_SHELL_APPLY_EXCEPTION",
|
|
2610
|
+
"message": str(exception),
|
|
2611
|
+
"write_executed": False,
|
|
2612
|
+
"write_may_have_succeeded": True,
|
|
2613
|
+
"safe_to_retry": False,
|
|
2614
|
+
"next_action": "readback_before_retry",
|
|
2615
|
+
}
|
|
2616
|
+
if app_name:
|
|
2617
|
+
pending_readback_app_names.append(app_name)
|
|
2618
|
+
any_write_may_have_succeeded = True
|
|
2619
|
+
continue
|
|
2620
|
+
public_shell = output.get("public_shell") if isinstance(output.get("public_shell"), dict) else {}
|
|
2621
|
+
merge_child_duration(public_shell)
|
|
2379
2622
|
resolved_key = str(public_shell.get("app_key") or "").strip()
|
|
2380
2623
|
shell_write_executed = _schema_apply_result_has_write(public_shell)
|
|
2381
2624
|
shell_write_may_have_succeeded = _schema_apply_result_may_have_write(public_shell)
|
|
@@ -2395,7 +2638,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2395
2638
|
pending_readback_app_keys.append(resolved_key)
|
|
2396
2639
|
if app_name:
|
|
2397
2640
|
pending_readback_app_names.append(app_name)
|
|
2398
|
-
|
|
2641
|
+
results_by_index[index] = {
|
|
2399
2642
|
"index": index,
|
|
2400
2643
|
"row_number": index + 1,
|
|
2401
2644
|
"client_key": client_key or None,
|
|
@@ -2410,8 +2653,8 @@ class AiBuilderTools(ToolBase):
|
|
|
2410
2653
|
"safe_to_retry": False if pending_readback else (not shell_write_executed and not any_write_executed),
|
|
2411
2654
|
**({"next_action": "readback_before_retry"} if pending_readback else {}),
|
|
2412
2655
|
**({"verification": public_shell.get("verification")} if isinstance(public_shell.get("verification"), dict) else {}),
|
|
2413
|
-
**({"created": True} if pending_readback and
|
|
2414
|
-
}
|
|
2656
|
+
**({"created": True} if pending_readback and not app_key else {}),
|
|
2657
|
+
}
|
|
2415
2658
|
continue
|
|
2416
2659
|
if bool(public_shell.get("created")):
|
|
2417
2660
|
created_app_keys.append(resolved_key)
|
|
@@ -2424,7 +2667,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2424
2667
|
resolved_name = str(public_shell.get("app_name_after") or public_shell.get("app_name") or app_name or "").strip()
|
|
2425
2668
|
if resolved_name:
|
|
2426
2669
|
app_name_to_app_key[resolved_name] = resolved_key
|
|
2427
|
-
|
|
2670
|
+
results_by_index[index] = {
|
|
2428
2671
|
"index": index,
|
|
2429
2672
|
"row_number": index + 1,
|
|
2430
2673
|
"client_key": client_key or None,
|
|
@@ -2435,104 +2678,218 @@ class AiBuilderTools(ToolBase):
|
|
|
2435
2678
|
"shell_result": public_shell,
|
|
2436
2679
|
"shell_field_diff": public_shell.get("field_diff") or {},
|
|
2437
2680
|
"shell_field_diff_details": public_shell.get("field_diff_details") or {},
|
|
2438
|
-
|
|
2439
|
-
})
|
|
2681
|
+
}
|
|
2440
2682
|
|
|
2441
|
-
|
|
2683
|
+
final_items_by_index: list[JSONObject | None] = [None] * len(apps)
|
|
2684
|
+
schema_jobs: list[JSONObject] = []
|
|
2442
2685
|
for index, raw_item in enumerate(apps):
|
|
2443
|
-
existing =
|
|
2686
|
+
existing = results_by_index[index] if 0 <= index < len(results_by_index) else None
|
|
2444
2687
|
if not existing or existing.get("status") != "shell_ready":
|
|
2445
2688
|
if existing:
|
|
2446
|
-
|
|
2689
|
+
final_items_by_index[index] = existing
|
|
2447
2690
|
continue
|
|
2448
2691
|
item = deepcopy(raw_item)
|
|
2449
2692
|
app_key = str(existing.get("app_key") or "").strip()
|
|
2693
|
+
dependency_failure = dependency_failure_for_item(index, item)
|
|
2694
|
+
if dependency_failure is not None:
|
|
2695
|
+
final_items_by_index[index] = dependency_failure
|
|
2696
|
+
any_write_executed = True
|
|
2697
|
+
continue
|
|
2698
|
+
compile_started_at = time.perf_counter()
|
|
2450
2699
|
try:
|
|
2451
2700
|
compiled_item = _compile_multi_app_schema_item_refs(item, client_key_to_app_key, app_name_to_app_key)
|
|
2452
2701
|
except ValueError as error:
|
|
2453
|
-
|
|
2702
|
+
relation_compile_ms += _elapsed_ms(compile_started_at)
|
|
2703
|
+
final_items_by_index[index] = {
|
|
2454
2704
|
**{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
|
|
2455
2705
|
"status": "failed",
|
|
2456
2706
|
"stage": "compile_relation_refs",
|
|
2457
2707
|
"error_code": "TARGET_APP_REF_NOT_FOUND",
|
|
2458
2708
|
"message": str(error),
|
|
2459
2709
|
"safe_to_retry": False,
|
|
2460
|
-
}
|
|
2710
|
+
}
|
|
2461
2711
|
any_write_executed = True
|
|
2462
2712
|
continue
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
_compiled_multi_app_deferred_add_fields(compiled_item, existing)
|
|
2466
|
-
if bool(existing.get("created"))
|
|
2467
|
-
else list(compiled_item.get("add_fields") or [])
|
|
2468
|
-
)
|
|
2713
|
+
relation_compile_ms += _elapsed_ms(compile_started_at)
|
|
2714
|
+
schema_add_fields = list(compiled_item.get("add_fields") or [])
|
|
2469
2715
|
update_fields = list(compiled_item.get("update_fields") or [])
|
|
2470
2716
|
remove_fields = list(compiled_item.get("remove_fields") or [])
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2717
|
+
layout_sections = list(compiled_item.get("_inline_form_layout_sections") or [])
|
|
2718
|
+
field_phase_app_name = ""
|
|
2719
|
+
field_phase_icon = ""
|
|
2720
|
+
field_phase_color = ""
|
|
2721
|
+
field_phase_visibility = None
|
|
2722
|
+
relation_fields_for_retry = [
|
|
2723
|
+
deepcopy(field)
|
|
2724
|
+
for field in schema_add_fields
|
|
2725
|
+
if isinstance(field, dict) and _multi_app_field_type(field) == PublicFieldType.relation.value
|
|
2726
|
+
]
|
|
2727
|
+
schema_jobs.append(
|
|
2728
|
+
{
|
|
2729
|
+
"index": index,
|
|
2730
|
+
"existing": existing,
|
|
2731
|
+
"app_key": app_key,
|
|
2732
|
+
"field_phase_app_name": field_phase_app_name,
|
|
2733
|
+
"field_phase_icon": field_phase_icon,
|
|
2734
|
+
"field_phase_color": field_phase_color,
|
|
2735
|
+
"field_phase_visibility": field_phase_visibility,
|
|
2736
|
+
"schema_add_fields": schema_add_fields,
|
|
2737
|
+
"update_fields": update_fields,
|
|
2738
|
+
"remove_fields": remove_fields,
|
|
2739
|
+
"layout_sections": layout_sections,
|
|
2740
|
+
"relation_fields_for_retry": relation_fields_for_retry,
|
|
2741
|
+
}
|
|
2742
|
+
)
|
|
2743
|
+
|
|
2744
|
+
schema_workers = worker_count(len(schema_jobs))
|
|
2745
|
+
parallelism["schema_workers"] = schema_workers
|
|
2746
|
+
|
|
2747
|
+
def run_schema_job(job: JSONObject) -> JSONObject:
|
|
2748
|
+
index = int(job["index"])
|
|
2749
|
+
app_key = str(job.get("app_key") or "")
|
|
2750
|
+
field_phase_app_name = str(job.get("field_phase_app_name") or "")
|
|
2751
|
+
field_phase_icon = str(job.get("field_phase_icon") or "")
|
|
2752
|
+
field_phase_color = str(job.get("field_phase_color") or "")
|
|
2753
|
+
field_phase_visibility = job.get("field_phase_visibility")
|
|
2754
|
+
schema_add_fields = list(job.get("schema_add_fields") or [])
|
|
2755
|
+
update_fields = list(job.get("update_fields") or [])
|
|
2756
|
+
remove_fields = list(job.get("remove_fields") or [])
|
|
2757
|
+
layout_sections = list(job.get("layout_sections") or [])
|
|
2758
|
+
relation_fields_for_retry = list(job.get("relation_fields_for_retry") or [])
|
|
2759
|
+
field_started_at = time.perf_counter()
|
|
2760
|
+
try:
|
|
2761
|
+
field_result = self._app_schema_apply_once(
|
|
2762
|
+
profile=profile,
|
|
2763
|
+
app_key=app_key,
|
|
2764
|
+
package_id=None,
|
|
2765
|
+
app_name=field_phase_app_name,
|
|
2766
|
+
app_title="",
|
|
2767
|
+
icon=field_phase_icon,
|
|
2768
|
+
color=field_phase_color,
|
|
2769
|
+
visibility=field_phase_visibility,
|
|
2770
|
+
publish=publish,
|
|
2771
|
+
add_fields=schema_add_fields,
|
|
2772
|
+
update_fields=update_fields,
|
|
2773
|
+
remove_fields=remove_fields,
|
|
2774
|
+
inline_form_layout_sections=layout_sections,
|
|
2775
|
+
allow_inline_layout_fallback=False,
|
|
2776
|
+
)
|
|
2777
|
+
public_result = _publicize_package_fields(field_result)
|
|
2778
|
+
except Exception as error: # pragma: no cover - defensive guard for unexpected worker failures
|
|
2779
|
+
return {
|
|
2780
|
+
"index": index,
|
|
2781
|
+
"job": job,
|
|
2782
|
+
"elapsed_ms": _elapsed_ms(field_started_at),
|
|
2783
|
+
"exception": repr(error),
|
|
2784
|
+
}
|
|
2785
|
+
if _multi_app_deferred_relation_retryable(public_result, relation_fields_for_retry):
|
|
2786
|
+
first_error = {
|
|
2787
|
+
"error_code": public_result.get("error_code"),
|
|
2788
|
+
"message": public_result.get("message"),
|
|
2789
|
+
"backend_code": public_result.get("backend_code"),
|
|
2790
|
+
"http_status": public_result.get("http_status"),
|
|
2791
|
+
"details": deepcopy(public_result.get("details")) if isinstance(public_result.get("details"), dict) else None,
|
|
2792
|
+
}
|
|
2793
|
+
retry_attempts: list[JSONObject] = []
|
|
2794
|
+
for retry_index, delay_seconds in enumerate((1.0, 5.0), start=1):
|
|
2795
|
+
retry_readback = self._stabilize_deferred_relation_metadata(
|
|
2796
|
+
profile=profile,
|
|
2797
|
+
source_app_key=app_key,
|
|
2798
|
+
deferred_add_fields=relation_fields_for_retry,
|
|
2799
|
+
delay_seconds=delay_seconds,
|
|
2800
|
+
)
|
|
2801
|
+
retry_result = self._app_schema_apply_once(
|
|
2476
2802
|
profile=profile,
|
|
2477
|
-
|
|
2478
|
-
|
|
2803
|
+
app_key=app_key,
|
|
2804
|
+
package_id=None,
|
|
2805
|
+
app_name=field_phase_app_name,
|
|
2806
|
+
app_title="",
|
|
2807
|
+
icon=field_phase_icon,
|
|
2808
|
+
color=field_phase_color,
|
|
2809
|
+
visibility=field_phase_visibility,
|
|
2479
2810
|
publish=publish,
|
|
2480
|
-
|
|
2811
|
+
add_fields=schema_add_fields,
|
|
2812
|
+
update_fields=update_fields,
|
|
2813
|
+
remove_fields=remove_fields,
|
|
2814
|
+
inline_form_layout_sections=layout_sections,
|
|
2815
|
+
allow_inline_layout_fallback=False,
|
|
2816
|
+
)
|
|
2817
|
+
public_retry_result = _publicize_package_fields(retry_result)
|
|
2818
|
+
retry_attempts.append(
|
|
2819
|
+
{
|
|
2820
|
+
"attempt": retry_index,
|
|
2821
|
+
"delay_seconds": delay_seconds,
|
|
2822
|
+
"status": public_retry_result.get("status"),
|
|
2823
|
+
"error_code": public_retry_result.get("error_code"),
|
|
2824
|
+
"backend_code": public_retry_result.get("backend_code"),
|
|
2825
|
+
"http_status": public_retry_result.get("http_status"),
|
|
2826
|
+
"readback": retry_readback,
|
|
2827
|
+
}
|
|
2481
2828
|
)
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2829
|
+
public_result = public_retry_result
|
|
2830
|
+
if not _multi_app_deferred_relation_retryable(public_result, relation_fields_for_retry):
|
|
2831
|
+
break
|
|
2832
|
+
retry_details = public_result.get("details")
|
|
2833
|
+
if not isinstance(retry_details, dict):
|
|
2834
|
+
retry_details = {}
|
|
2835
|
+
public_result["details"] = retry_details
|
|
2836
|
+
retry_summary: JSONObject = {
|
|
2837
|
+
"attempted": True,
|
|
2838
|
+
"reason": "same_batch_relation_metadata_may_lag",
|
|
2839
|
+
"first_error": first_error,
|
|
2840
|
+
"attempts": retry_attempts,
|
|
2841
|
+
}
|
|
2842
|
+
if retry_attempts:
|
|
2843
|
+
retry_summary["readback"] = retry_attempts[-1].get("readback")
|
|
2844
|
+
retry_details["deferred_relation_retry"] = retry_summary
|
|
2845
|
+
if layout_sections and not _inline_form_layout_was_handled(public_result):
|
|
2846
|
+
public_result = mark_inline_layout_not_applied_in_schema(
|
|
2847
|
+
public_result,
|
|
2848
|
+
app_key=app_key,
|
|
2849
|
+
sections=layout_sections,
|
|
2850
|
+
)
|
|
2851
|
+
return {
|
|
2852
|
+
"index": index,
|
|
2853
|
+
"job": job,
|
|
2854
|
+
"elapsed_ms": _elapsed_ms(field_started_at),
|
|
2855
|
+
"public_result": public_result,
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
schema_outputs: list[JSONObject] = []
|
|
2859
|
+
schema_phase_started_at = time.perf_counter()
|
|
2860
|
+
if schema_jobs:
|
|
2861
|
+
with ThreadPoolExecutor(max_workers=schema_workers) as executor:
|
|
2862
|
+
futures = [executor.submit(run_schema_job, job) for job in schema_jobs]
|
|
2863
|
+
for future in as_completed(futures):
|
|
2864
|
+
schema_outputs.append(future.result())
|
|
2865
|
+
app_field_apply_ms += _elapsed_ms(schema_phase_started_at)
|
|
2866
|
+
|
|
2867
|
+
for output in sorted(schema_outputs, key=lambda item: int(item.get("index", 0))):
|
|
2868
|
+
index = int(output["index"])
|
|
2869
|
+
job = output.get("job") if isinstance(output.get("job"), dict) else {}
|
|
2870
|
+
existing = job.get("existing") if isinstance(job.get("existing"), dict) else {}
|
|
2871
|
+
app_key = str(job.get("app_key") or "")
|
|
2872
|
+
exception = output.get("exception")
|
|
2873
|
+
if exception:
|
|
2874
|
+
final_items_by_index[index] = {
|
|
2490
2875
|
**{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
|
|
2491
|
-
"status":
|
|
2876
|
+
"status": "pending_readback",
|
|
2492
2877
|
"stage": "schema_apply",
|
|
2493
|
-
"
|
|
2494
|
-
"
|
|
2495
|
-
"
|
|
2496
|
-
"
|
|
2497
|
-
"published": bool(shell_result.get("published")),
|
|
2498
|
-
"verified": bool(shell_result.get("verified")),
|
|
2499
|
-
"error_code": shell_result.get("error_code"),
|
|
2500
|
-
"message": shell_result.get("message"),
|
|
2501
|
-
"write_executed": _schema_apply_result_has_write(shell_result),
|
|
2502
|
-
"write_may_have_succeeded": _schema_apply_result_may_have_write(shell_result),
|
|
2878
|
+
"error_code": "SCHEMA_APPLY_EXCEPTION",
|
|
2879
|
+
"message": str(exception),
|
|
2880
|
+
"write_executed": False,
|
|
2881
|
+
"write_may_have_succeeded": True,
|
|
2503
2882
|
"safe_to_retry": False,
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2883
|
+
"next_action": "readback_before_retry",
|
|
2884
|
+
}
|
|
2885
|
+
if app_key:
|
|
2886
|
+
pending_readback_app_keys.append(app_key)
|
|
2887
|
+
if existing.get("app_name"):
|
|
2888
|
+
pending_readback_app_names.append(str(existing.get("app_name")))
|
|
2889
|
+
any_write_may_have_succeeded = True
|
|
2509
2890
|
continue
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
profile=profile,
|
|
2513
|
-
app_key=app_key,
|
|
2514
|
-
package_id=None,
|
|
2515
|
-
app_name=str(compiled_item.get("app_name") or compiled_item.get("appTitle") or compiled_item.get("app_title") or ""),
|
|
2516
|
-
app_title="",
|
|
2517
|
-
icon=str(compiled_item.get("icon") or ""),
|
|
2518
|
-
color=str(compiled_item.get("color") or ""),
|
|
2519
|
-
visibility=compiled_item.get("visibility"),
|
|
2520
|
-
create_if_missing=False,
|
|
2521
|
-
publish=publish,
|
|
2522
|
-
add_fields=deferred_add_fields,
|
|
2523
|
-
update_fields=update_fields,
|
|
2524
|
-
remove_fields=remove_fields,
|
|
2525
|
-
)
|
|
2526
|
-
public_result = _publicize_package_fields(field_result)
|
|
2527
|
-
layout_sections = list(compiled_item.get("_inline_form_layout_sections") or [])
|
|
2528
|
-
if layout_sections:
|
|
2529
|
-
public_result = self._apply_inline_form_layout_after_schema(
|
|
2530
|
-
profile=profile,
|
|
2531
|
-
schema_result=public_result,
|
|
2532
|
-
fallback_app_key=app_key,
|
|
2533
|
-
publish=publish,
|
|
2534
|
-
sections=layout_sections,
|
|
2535
|
-
)
|
|
2891
|
+
public_result = output.get("public_result") if isinstance(output.get("public_result"), dict) else {}
|
|
2892
|
+
merge_child_duration(public_result)
|
|
2536
2893
|
if _schema_apply_result_has_write(public_result):
|
|
2537
2894
|
any_write_executed = True
|
|
2538
2895
|
if _schema_apply_result_may_have_write(public_result):
|
|
@@ -2547,7 +2904,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2547
2904
|
pending_readback_app_keys.append(app_key)
|
|
2548
2905
|
if existing.get("app_name"):
|
|
2549
2906
|
pending_readback_app_names.append(str(existing.get("app_name")))
|
|
2550
|
-
|
|
2907
|
+
final_items_by_index[index] = {
|
|
2551
2908
|
**{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
|
|
2552
2909
|
"status": item_status,
|
|
2553
2910
|
"stage": "schema_apply",
|
|
@@ -2562,12 +2919,17 @@ class AiBuilderTools(ToolBase):
|
|
|
2562
2919
|
"write_executed": _schema_apply_result_has_write(public_result),
|
|
2563
2920
|
"write_may_have_succeeded": _schema_apply_result_may_have_write(public_result),
|
|
2564
2921
|
"safe_to_retry": False,
|
|
2922
|
+
**_multi_app_child_diagnostics(public_result),
|
|
2565
2923
|
**({"next_action": public_result.get("next_action")} if public_result.get("next_action") else {}),
|
|
2566
2924
|
**({"verification": public_result.get("verification")} if isinstance(public_result.get("verification"), dict) else {}),
|
|
2567
2925
|
**({"inline_form_layout": public_result.get("inline_form_layout")} if isinstance(public_result.get("inline_form_layout"), dict) else {}),
|
|
2568
2926
|
**({"inline_form_layout_result": public_result.get("inline_form_layout_result")} if isinstance(public_result.get("inline_form_layout_result"), dict) else {}),
|
|
2569
|
-
}
|
|
2927
|
+
}
|
|
2570
2928
|
|
|
2929
|
+
final_items: list[JSONObject] = [
|
|
2930
|
+
item if isinstance(item, dict) else _multi_app_item_failure(index, apps[index], "APP_ITEM_NOT_PROCESSED", "app item was not processed")
|
|
2931
|
+
for index, item in enumerate(final_items_by_index)
|
|
2932
|
+
]
|
|
2571
2933
|
pending_readback = sum(1 for item in final_items if item.get("status") == "pending_readback")
|
|
2572
2934
|
succeeded = sum(1 for item in final_items if item.get("status") in {"success", "partial_success"})
|
|
2573
2935
|
failed = sum(1 for item in final_items if item.get("status") == "failed")
|
|
@@ -2591,6 +2953,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2591
2953
|
"package_id": package_id,
|
|
2592
2954
|
"publish_requested": publish,
|
|
2593
2955
|
"apps": final_items,
|
|
2956
|
+
"parallelism": parallelism,
|
|
2594
2957
|
"normalized_args": normalized_args,
|
|
2595
2958
|
"verification": {
|
|
2596
2959
|
"all_apps_succeeded": failed == 0,
|
|
@@ -2611,7 +2974,69 @@ class AiBuilderTools(ToolBase):
|
|
|
2611
2974
|
response["next_action"] = "readback_before_retry"
|
|
2612
2975
|
response["pending_readback_app_keys"] = list(dict.fromkeys(pending_readback_app_keys))
|
|
2613
2976
|
response["pending_readback_app_names"] = list(dict.fromkeys(pending_readback_app_names))
|
|
2614
|
-
return response
|
|
2977
|
+
return finish(response)
|
|
2978
|
+
|
|
2979
|
+
def _stabilize_deferred_relation_metadata(
|
|
2980
|
+
self,
|
|
2981
|
+
*,
|
|
2982
|
+
profile: str,
|
|
2983
|
+
source_app_key: str,
|
|
2984
|
+
deferred_add_fields: list[JSONObject],
|
|
2985
|
+
delay_seconds: float,
|
|
2986
|
+
) -> list[JSONObject]:
|
|
2987
|
+
edit_context_result = self._finish_deferred_relation_edit_context(profile=profile, app_key=source_app_key)
|
|
2988
|
+
time.sleep(delay_seconds)
|
|
2989
|
+
app_keys = [source_app_key]
|
|
2990
|
+
for key in _multi_app_relation_target_app_keys(deferred_add_fields):
|
|
2991
|
+
if key and key not in app_keys:
|
|
2992
|
+
app_keys.append(key)
|
|
2993
|
+
readback: list[JSONObject] = []
|
|
2994
|
+
for app_key in app_keys:
|
|
2995
|
+
try:
|
|
2996
|
+
result = self._facade.app_read_fields(profile=profile, app_key=app_key)
|
|
2997
|
+
readback.append(
|
|
2998
|
+
{
|
|
2999
|
+
"app_key": app_key,
|
|
3000
|
+
"status": result.get("status"),
|
|
3001
|
+
"field_count": len(result.get("fields") or []),
|
|
3002
|
+
**({"edit_context": edit_context_result} if app_key == source_app_key else {}),
|
|
3003
|
+
}
|
|
3004
|
+
)
|
|
3005
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
3006
|
+
api_error = _coerce_api_error(error)
|
|
3007
|
+
readback.append(
|
|
3008
|
+
{
|
|
3009
|
+
"app_key": app_key,
|
|
3010
|
+
"status": "failed",
|
|
3011
|
+
"transport_error": {
|
|
3012
|
+
"http_status": api_error.http_status,
|
|
3013
|
+
"backend_code": api_error.backend_code,
|
|
3014
|
+
"category": api_error.category,
|
|
3015
|
+
"request_id": api_error.request_id,
|
|
3016
|
+
},
|
|
3017
|
+
}
|
|
3018
|
+
)
|
|
3019
|
+
return readback
|
|
3020
|
+
|
|
3021
|
+
def _finish_deferred_relation_edit_context(self, *, profile: str, app_key: str) -> JSONObject:
|
|
3022
|
+
try:
|
|
3023
|
+
version_result = self._facade.apps.app_get_edit_version_no(profile=profile, app_key=app_key).get("result") or {}
|
|
3024
|
+
raw_version = version_result.get("editVersionNo") or version_result.get("versionNo") or 1
|
|
3025
|
+
edit_version_no = int(raw_version)
|
|
3026
|
+
self._facade.apps.app_edit_finished(profile=profile, app_key=app_key, payload={"editVersionNo": edit_version_no})
|
|
3027
|
+
return {"status": "success", "edit_version_no": edit_version_no}
|
|
3028
|
+
except (QingflowApiError, RuntimeError, TypeError, ValueError) as error:
|
|
3029
|
+
api_error = _coerce_api_error(error if isinstance(error, (QingflowApiError, RuntimeError)) else RuntimeError(str(error)))
|
|
3030
|
+
return {
|
|
3031
|
+
"status": "failed",
|
|
3032
|
+
"message": api_error.message,
|
|
3033
|
+
"transport_error": {
|
|
3034
|
+
"http_status": api_error.http_status,
|
|
3035
|
+
"backend_code": api_error.backend_code,
|
|
3036
|
+
"category": api_error.category,
|
|
3037
|
+
"request_id": api_error.request_id,
|
|
3038
|
+
},
|
|
3039
|
+
}
|
|
2615
3040
|
|
|
2616
3041
|
def _app_schema_apply_once(
|
|
2617
3042
|
self,
|
|
@@ -2624,15 +3049,17 @@ class AiBuilderTools(ToolBase):
|
|
|
2624
3049
|
icon: str = "",
|
|
2625
3050
|
color: str = "",
|
|
2626
3051
|
visibility: JSONObject | None = None,
|
|
2627
|
-
create_if_missing: bool = False,
|
|
2628
3052
|
publish: bool = True,
|
|
2629
3053
|
add_fields: list[JSONObject],
|
|
2630
3054
|
update_fields: list[JSONObject],
|
|
2631
3055
|
remove_fields: list[JSONObject],
|
|
2632
3056
|
inline_form_layout_sections: list[JSONObject] | None = None,
|
|
3057
|
+
allow_inline_layout_fallback: bool = True,
|
|
2633
3058
|
) -> JSONObject:
|
|
2634
3059
|
"""执行内部辅助逻辑。"""
|
|
3060
|
+
started_at = time.perf_counter()
|
|
2635
3061
|
effective_app_name = app_name or app_title
|
|
3062
|
+
creating = not bool(str(app_key or "").strip()) and package_id is not None and bool(str(effective_app_name or "").strip())
|
|
2636
3063
|
system_field_failure = _reserved_system_field_name_failure(
|
|
2637
3064
|
tool_name="app_schema_apply",
|
|
2638
3065
|
profile=profile,
|
|
@@ -2642,7 +3069,6 @@ class AiBuilderTools(ToolBase):
|
|
|
2642
3069
|
icon=icon,
|
|
2643
3070
|
color=color,
|
|
2644
3071
|
visibility=visibility,
|
|
2645
|
-
create_if_missing=create_if_missing,
|
|
2646
3072
|
publish=publish,
|
|
2647
3073
|
add_fields=add_fields,
|
|
2648
3074
|
update_fields=update_fields,
|
|
@@ -2654,38 +3080,19 @@ class AiBuilderTools(ToolBase):
|
|
|
2654
3080
|
tool_name="app_schema_apply",
|
|
2655
3081
|
icon=icon,
|
|
2656
3082
|
color=color,
|
|
2657
|
-
creating=
|
|
3083
|
+
creating=creating,
|
|
2658
3084
|
)
|
|
2659
3085
|
if icon_failure is not None:
|
|
2660
3086
|
return icon_failure
|
|
2661
|
-
plan_result = self._rewrite_plan_result_for_apply(
|
|
2662
|
-
result=self.app_schema_plan(
|
|
2663
|
-
profile=profile,
|
|
2664
|
-
app_key=app_key,
|
|
2665
|
-
package_id=package_id,
|
|
2666
|
-
app_name=effective_app_name,
|
|
2667
|
-
icon=icon,
|
|
2668
|
-
color=color,
|
|
2669
|
-
visibility=visibility,
|
|
2670
|
-
create_if_missing=create_if_missing,
|
|
2671
|
-
add_fields=add_fields,
|
|
2672
|
-
update_fields=update_fields,
|
|
2673
|
-
remove_fields=remove_fields,
|
|
2674
|
-
),
|
|
2675
|
-
profile=profile,
|
|
2676
|
-
publish=publish,
|
|
2677
|
-
plan_tool_name="app_schema_plan",
|
|
2678
|
-
apply_tool_name="app_schema_apply",
|
|
2679
|
-
)
|
|
2680
|
-
if not isinstance(plan_result, dict) or plan_result.get("status") != "success":
|
|
2681
|
-
return plan_result
|
|
2682
|
-
plan_args = plan_result.get("normalized_args")
|
|
2683
|
-
if not isinstance(plan_args, dict):
|
|
2684
|
-
plan_args = {}
|
|
2685
3087
|
try:
|
|
2686
|
-
parsed_add = [FieldPatch.model_validate(item) for item in
|
|
2687
|
-
parsed_update = [FieldUpdatePatch.model_validate(item) for item in
|
|
2688
|
-
parsed_remove = [FieldRemovePatch.model_validate(item) for item in
|
|
3088
|
+
parsed_add = [FieldPatch.model_validate(item) for item in add_fields or []]
|
|
3089
|
+
parsed_update = [FieldUpdatePatch.model_validate(item) for item in update_fields or []]
|
|
3090
|
+
parsed_remove = [FieldRemovePatch.model_validate(item) for item in remove_fields or []]
|
|
3091
|
+
parsed_inline_layout = [
|
|
3092
|
+
LayoutSectionPatch.model_validate(item)
|
|
3093
|
+
for item in (inline_form_layout_sections or [])
|
|
3094
|
+
]
|
|
3095
|
+
parsed_visibility = VisibilityPatch.model_validate(visibility) if visibility is not None else None
|
|
2689
3096
|
except ValidationError as exc:
|
|
2690
3097
|
return _validation_failure(
|
|
2691
3098
|
str(exc),
|
|
@@ -2695,61 +3102,75 @@ class AiBuilderTools(ToolBase):
|
|
|
2695
3102
|
"tool_name": "app_schema_apply",
|
|
2696
3103
|
"arguments": {
|
|
2697
3104
|
"profile": profile,
|
|
2698
|
-
"app_key": str(
|
|
2699
|
-
"package_id":
|
|
2700
|
-
"app_name":
|
|
2701
|
-
"icon": str(
|
|
2702
|
-
"color": str(
|
|
2703
|
-
"visibility":
|
|
2704
|
-
"create_if_missing": bool(plan_args.get("create_if_missing", create_if_missing)),
|
|
3105
|
+
"app_key": str(app_key or ""),
|
|
3106
|
+
"package_id": package_id,
|
|
3107
|
+
"app_name": effective_app_name,
|
|
3108
|
+
"icon": str(icon or ""),
|
|
3109
|
+
"color": str(color or ""),
|
|
3110
|
+
"visibility": visibility,
|
|
2705
3111
|
"publish": publish,
|
|
2706
|
-
"add_fields":
|
|
2707
|
-
"update_fields":
|
|
2708
|
-
"remove_fields":
|
|
3112
|
+
"add_fields": add_fields or [{"name": "字段名称", "type": "text", "required": False}],
|
|
3113
|
+
"update_fields": update_fields or [],
|
|
3114
|
+
"remove_fields": remove_fields or [],
|
|
2709
3115
|
},
|
|
2710
3116
|
},
|
|
2711
3117
|
)
|
|
2712
3118
|
normalized_args = {
|
|
2713
|
-
"app_key": str(
|
|
2714
|
-
"package_id":
|
|
2715
|
-
"app_name":
|
|
2716
|
-
"icon": str(
|
|
2717
|
-
"color": str(
|
|
2718
|
-
"visibility":
|
|
2719
|
-
"create_if_missing": bool(plan_args.get("create_if_missing", create_if_missing)),
|
|
3119
|
+
"app_key": str(app_key or ""),
|
|
3120
|
+
"package_id": package_id,
|
|
3121
|
+
"app_name": effective_app_name,
|
|
3122
|
+
"icon": str(icon or ""),
|
|
3123
|
+
"color": str(color or ""),
|
|
3124
|
+
"visibility": parsed_visibility.model_dump(mode="json") if parsed_visibility is not None else None,
|
|
2720
3125
|
"publish": publish,
|
|
2721
3126
|
"add_fields": [patch.model_dump(mode="json") for patch in parsed_add],
|
|
2722
3127
|
"update_fields": [patch.model_dump(mode="json") for patch in parsed_update],
|
|
2723
3128
|
"remove_fields": [patch.model_dump(mode="json") for patch in parsed_remove],
|
|
3129
|
+
**(
|
|
3130
|
+
{
|
|
3131
|
+
"inline_form_layout_sections": [
|
|
3132
|
+
section.model_dump(mode="json", exclude_none=True)
|
|
3133
|
+
for section in parsed_inline_layout
|
|
3134
|
+
]
|
|
3135
|
+
}
|
|
3136
|
+
if parsed_inline_layout
|
|
3137
|
+
else {}
|
|
3138
|
+
),
|
|
2724
3139
|
}
|
|
2725
3140
|
result = _safe_tool_call(
|
|
2726
3141
|
lambda: self._facade.app_schema_apply(
|
|
2727
3142
|
profile=profile,
|
|
2728
|
-
app_key=str(
|
|
2729
|
-
package_tag_id=
|
|
2730
|
-
app_name=
|
|
2731
|
-
icon=str(
|
|
2732
|
-
color=str(
|
|
2733
|
-
visibility=
|
|
2734
|
-
create_if_missing=bool(plan_args.get("create_if_missing", create_if_missing)),
|
|
3143
|
+
app_key=str(app_key or ""),
|
|
3144
|
+
package_tag_id=package_id,
|
|
3145
|
+
app_name=effective_app_name,
|
|
3146
|
+
icon=str(icon or ""),
|
|
3147
|
+
color=str(color or ""),
|
|
3148
|
+
visibility=parsed_visibility,
|
|
2735
3149
|
publish=publish,
|
|
2736
3150
|
add_fields=parsed_add,
|
|
2737
3151
|
update_fields=parsed_update,
|
|
2738
3152
|
remove_fields=parsed_remove,
|
|
3153
|
+
inline_form_layout_sections=parsed_inline_layout,
|
|
2739
3154
|
),
|
|
2740
3155
|
error_code="SCHEMA_APPLY_FAILED",
|
|
2741
3156
|
normalized_args=normalized_args,
|
|
2742
3157
|
suggested_next_call={"tool_name": "app_schema_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
2743
3158
|
)
|
|
2744
3159
|
public_result = _publicize_package_fields(result)
|
|
2745
|
-
if inline_form_layout_sections:
|
|
3160
|
+
if inline_form_layout_sections and allow_inline_layout_fallback and not _inline_form_layout_was_handled(public_result):
|
|
3161
|
+
layout_started_at = time.perf_counter()
|
|
2746
3162
|
public_result = self._apply_inline_form_layout_after_schema(
|
|
2747
3163
|
profile=profile,
|
|
2748
3164
|
schema_result=public_result,
|
|
2749
|
-
fallback_app_key=str(
|
|
3165
|
+
fallback_app_key=str(app_key or ""),
|
|
2750
3166
|
publish=publish,
|
|
2751
3167
|
sections=inline_form_layout_sections,
|
|
2752
3168
|
)
|
|
3169
|
+
_merge_duration_breakdown(
|
|
3170
|
+
public_result,
|
|
3171
|
+
inline_layout_ms=_elapsed_ms(layout_started_at),
|
|
3172
|
+
)
|
|
3173
|
+
_merge_duration_breakdown(public_result, total_ms=_elapsed_ms(started_at))
|
|
2753
3174
|
return public_result
|
|
2754
3175
|
|
|
2755
3176
|
def _apply_inline_form_layout_after_schema(
|
|
@@ -2867,27 +3288,9 @@ class AiBuilderTools(ToolBase):
|
|
|
2867
3288
|
|
|
2868
3289
|
def _app_layout_apply_once(self, *, profile: str, app_key: str, mode: str = "merge", publish: bool = True, sections: list[JSONObject]) -> JSONObject:
|
|
2869
3290
|
"""执行内部辅助逻辑。"""
|
|
2870
|
-
plan_result = self._rewrite_plan_result_for_apply(
|
|
2871
|
-
result=self.app_layout_plan(
|
|
2872
|
-
profile=profile,
|
|
2873
|
-
app_key=app_key,
|
|
2874
|
-
mode=mode,
|
|
2875
|
-
sections=sections,
|
|
2876
|
-
preset=None,
|
|
2877
|
-
),
|
|
2878
|
-
profile=profile,
|
|
2879
|
-
publish=publish,
|
|
2880
|
-
plan_tool_name="app_layout_plan",
|
|
2881
|
-
apply_tool_name="app_layout_apply",
|
|
2882
|
-
)
|
|
2883
|
-
if not isinstance(plan_result, dict) or plan_result.get("status") != "success":
|
|
2884
|
-
return plan_result
|
|
2885
|
-
plan_args = plan_result.get("normalized_args")
|
|
2886
|
-
if not isinstance(plan_args, dict):
|
|
2887
|
-
plan_args = {}
|
|
2888
3291
|
try:
|
|
2889
|
-
parsed_mode = LayoutApplyMode(str(
|
|
2890
|
-
parsed_sections = [LayoutSectionPatch.model_validate(item) for item in
|
|
3292
|
+
parsed_mode = LayoutApplyMode(str(mode or "merge"))
|
|
3293
|
+
parsed_sections = [LayoutSectionPatch.model_validate(item) for item in sections or []]
|
|
2891
3294
|
except (ValueError, ValidationError) as exc:
|
|
2892
3295
|
return _validation_failure(
|
|
2893
3296
|
str(exc),
|
|
@@ -2897,15 +3300,15 @@ class AiBuilderTools(ToolBase):
|
|
|
2897
3300
|
"tool_name": "app_layout_apply",
|
|
2898
3301
|
"arguments": {
|
|
2899
3302
|
"profile": profile,
|
|
2900
|
-
"app_key":
|
|
2901
|
-
"mode": str(
|
|
3303
|
+
"app_key": app_key,
|
|
3304
|
+
"mode": str(mode or "merge"),
|
|
2902
3305
|
"publish": publish,
|
|
2903
|
-
"sections":
|
|
3306
|
+
"sections": sections or [{"title": "基础信息", "rows": [["字段A", "字段B"]]}],
|
|
2904
3307
|
},
|
|
2905
3308
|
},
|
|
2906
3309
|
)
|
|
2907
3310
|
normalized_args = {
|
|
2908
|
-
"app_key":
|
|
3311
|
+
"app_key": app_key,
|
|
2909
3312
|
"mode": parsed_mode.value,
|
|
2910
3313
|
"publish": publish,
|
|
2911
3314
|
"sections": [section.model_dump(mode="json", exclude_none=True) for section in parsed_sections],
|
|
@@ -2913,7 +3316,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2913
3316
|
return _safe_tool_call(
|
|
2914
3317
|
lambda: self._facade.app_layout_apply(
|
|
2915
3318
|
profile=profile,
|
|
2916
|
-
app_key=
|
|
3319
|
+
app_key=app_key,
|
|
2917
3320
|
mode=parsed_mode,
|
|
2918
3321
|
publish=publish,
|
|
2919
3322
|
sections=parsed_sections,
|
|
@@ -3016,32 +3419,13 @@ class AiBuilderTools(ToolBase):
|
|
|
3016
3419
|
transitions: list[JSONObject],
|
|
3017
3420
|
) -> JSONObject:
|
|
3018
3421
|
"""执行内部辅助逻辑。"""
|
|
3019
|
-
plan_result = self._rewrite_plan_result_for_apply(
|
|
3020
|
-
result=self.app_flow_plan(
|
|
3021
|
-
profile=profile,
|
|
3022
|
-
app_key=app_key,
|
|
3023
|
-
mode=mode,
|
|
3024
|
-
nodes=nodes,
|
|
3025
|
-
transitions=transitions,
|
|
3026
|
-
preset=None,
|
|
3027
|
-
),
|
|
3028
|
-
profile=profile,
|
|
3029
|
-
publish=publish,
|
|
3030
|
-
plan_tool_name="app_flow_plan",
|
|
3031
|
-
apply_tool_name="app_flow_apply",
|
|
3032
|
-
)
|
|
3033
|
-
if not isinstance(plan_result, dict) or plan_result.get("status") != "success":
|
|
3034
|
-
return plan_result
|
|
3035
|
-
plan_args = plan_result.get("normalized_args")
|
|
3036
|
-
if not isinstance(plan_args, dict):
|
|
3037
|
-
plan_args = {}
|
|
3038
3422
|
try:
|
|
3039
3423
|
request = FlowPlanRequest.model_validate(
|
|
3040
3424
|
{
|
|
3041
|
-
"app_key":
|
|
3042
|
-
"mode":
|
|
3043
|
-
"nodes":
|
|
3044
|
-
"transitions":
|
|
3425
|
+
"app_key": app_key,
|
|
3426
|
+
"mode": mode,
|
|
3427
|
+
"nodes": nodes or [],
|
|
3428
|
+
"transitions": transitions or [],
|
|
3045
3429
|
"preset": None,
|
|
3046
3430
|
}
|
|
3047
3431
|
)
|
|
@@ -3054,11 +3438,11 @@ class AiBuilderTools(ToolBase):
|
|
|
3054
3438
|
"tool_name": "app_flow_apply",
|
|
3055
3439
|
"arguments": {
|
|
3056
3440
|
"profile": profile,
|
|
3057
|
-
"app_key":
|
|
3058
|
-
"mode": str(
|
|
3441
|
+
"app_key": app_key,
|
|
3442
|
+
"mode": str(mode or "replace"),
|
|
3059
3443
|
"publish": publish,
|
|
3060
|
-
"nodes":
|
|
3061
|
-
"transitions":
|
|
3444
|
+
"nodes": nodes or [{"id": "start", "type": "start", "name": "发起"}],
|
|
3445
|
+
"transitions": transitions or [],
|
|
3062
3446
|
},
|
|
3063
3447
|
},
|
|
3064
3448
|
)
|
|
@@ -3155,69 +3539,9 @@ class AiBuilderTools(ToolBase):
|
|
|
3155
3539
|
)
|
|
3156
3540
|
if reserved_failure is not None:
|
|
3157
3541
|
return reserved_failure
|
|
3158
|
-
if patch_views or _view_payload_has_action_buttons(upsert_views=upsert_views, patch_views=patch_views):
|
|
3159
|
-
try:
|
|
3160
|
-
parsed_views = [ViewUpsertPatch.model_validate(item) for item in (upsert_views or [])]
|
|
3161
|
-
parsed_patch_views = [ViewPartialPatch.model_validate(item) for item in patch_views]
|
|
3162
|
-
except ValidationError as exc:
|
|
3163
|
-
return _visibility_validation_failure(
|
|
3164
|
-
str(exc),
|
|
3165
|
-
tool_name="app_views_apply",
|
|
3166
|
-
exc=exc,
|
|
3167
|
-
suggested_next_call={
|
|
3168
|
-
"tool_name": "app_views_apply",
|
|
3169
|
-
"arguments": {
|
|
3170
|
-
"profile": profile,
|
|
3171
|
-
"app_key": app_key,
|
|
3172
|
-
"publish": publish,
|
|
3173
|
-
"upsert_views": upsert_views or [],
|
|
3174
|
-
"patch_views": [
|
|
3175
|
-
{"view_key": "VIEW_KEY", "set": {"query_conditions": {"enabled": True, "rows": [["字段A"]]}}},
|
|
3176
|
-
],
|
|
3177
|
-
"remove_views": remove_views or [],
|
|
3178
|
-
},
|
|
3179
|
-
},
|
|
3180
|
-
)
|
|
3181
|
-
normalized_args = {
|
|
3182
|
-
"app_key": app_key,
|
|
3183
|
-
"publish": publish,
|
|
3184
|
-
"upsert_views": [public_view_upsert_payload(view) for view in parsed_views],
|
|
3185
|
-
"patch_views": [public_view_partial_payload(patch) for patch in parsed_patch_views],
|
|
3186
|
-
"remove_views": list(remove_views or []),
|
|
3187
|
-
}
|
|
3188
|
-
return _safe_tool_call(
|
|
3189
|
-
lambda: self._facade.app_views_apply(
|
|
3190
|
-
profile=profile,
|
|
3191
|
-
app_key=app_key,
|
|
3192
|
-
publish=publish,
|
|
3193
|
-
upsert_views=parsed_views,
|
|
3194
|
-
patch_views=parsed_patch_views,
|
|
3195
|
-
remove_views=list(remove_views or []),
|
|
3196
|
-
),
|
|
3197
|
-
error_code="VIEWS_APPLY_FAILED",
|
|
3198
|
-
normalized_args=normalized_args,
|
|
3199
|
-
suggested_next_call={"tool_name": "app_views_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
3200
|
-
)
|
|
3201
|
-
plan_result = self._rewrite_plan_result_for_apply(
|
|
3202
|
-
result=self.app_views_plan(
|
|
3203
|
-
profile=profile,
|
|
3204
|
-
app_key=app_key,
|
|
3205
|
-
upsert_views=upsert_views,
|
|
3206
|
-
remove_views=remove_views,
|
|
3207
|
-
preset=None,
|
|
3208
|
-
),
|
|
3209
|
-
profile=profile,
|
|
3210
|
-
publish=publish,
|
|
3211
|
-
plan_tool_name="app_views_plan",
|
|
3212
|
-
apply_tool_name="app_views_apply",
|
|
3213
|
-
)
|
|
3214
|
-
if not isinstance(plan_result, dict) or plan_result.get("status") != "success":
|
|
3215
|
-
return plan_result
|
|
3216
|
-
plan_args = plan_result.get("normalized_args")
|
|
3217
|
-
if not isinstance(plan_args, dict):
|
|
3218
|
-
plan_args = {}
|
|
3219
3542
|
try:
|
|
3220
|
-
parsed_views = [ViewUpsertPatch.model_validate(item) for item in
|
|
3543
|
+
parsed_views = [ViewUpsertPatch.model_validate(item) for item in (upsert_views or [])]
|
|
3544
|
+
parsed_patch_views = [ViewPartialPatch.model_validate(item) for item in (patch_views or [])]
|
|
3221
3545
|
except ValidationError as exc:
|
|
3222
3546
|
return _visibility_validation_failure(
|
|
3223
3547
|
str(exc),
|
|
@@ -3227,28 +3551,29 @@ class AiBuilderTools(ToolBase):
|
|
|
3227
3551
|
"tool_name": "app_views_apply",
|
|
3228
3552
|
"arguments": {
|
|
3229
3553
|
"profile": profile,
|
|
3230
|
-
"app_key":
|
|
3554
|
+
"app_key": app_key,
|
|
3231
3555
|
"publish": publish,
|
|
3232
|
-
"upsert_views":
|
|
3233
|
-
"
|
|
3556
|
+
"upsert_views": upsert_views or [{"name": "业务台账视图", "type": "table", "columns": ["字段A"]}],
|
|
3557
|
+
"patch_views": patch_views or [],
|
|
3558
|
+
"remove_views": remove_views or [],
|
|
3234
3559
|
},
|
|
3235
3560
|
},
|
|
3236
3561
|
)
|
|
3237
3562
|
normalized_args = {
|
|
3238
|
-
"app_key":
|
|
3563
|
+
"app_key": app_key,
|
|
3239
3564
|
"publish": publish,
|
|
3240
3565
|
"upsert_views": [public_view_upsert_payload(view) for view in parsed_views],
|
|
3241
|
-
"patch_views": [],
|
|
3242
|
-
"remove_views": list(
|
|
3566
|
+
"patch_views": [public_view_partial_payload(patch) for patch in parsed_patch_views],
|
|
3567
|
+
"remove_views": list(remove_views or []),
|
|
3243
3568
|
}
|
|
3244
3569
|
return _safe_tool_call(
|
|
3245
3570
|
lambda: self._facade.app_views_apply(
|
|
3246
3571
|
profile=profile,
|
|
3247
|
-
app_key=
|
|
3572
|
+
app_key=app_key,
|
|
3248
3573
|
publish=publish,
|
|
3249
3574
|
upsert_views=parsed_views,
|
|
3250
|
-
patch_views=
|
|
3251
|
-
remove_views=list(
|
|
3575
|
+
patch_views=parsed_patch_views,
|
|
3576
|
+
remove_views=list(remove_views or []),
|
|
3252
3577
|
),
|
|
3253
3578
|
error_code="VIEWS_APPLY_FAILED",
|
|
3254
3579
|
normalized_args=normalized_args,
|
|
@@ -4004,7 +4329,6 @@ def _multi_app_static_validation_failure(
|
|
|
4004
4329
|
*,
|
|
4005
4330
|
tool_name: str,
|
|
4006
4331
|
apps: list[JSONObject],
|
|
4007
|
-
create_if_missing: bool,
|
|
4008
4332
|
) -> JSONObject | None:
|
|
4009
4333
|
issues: list[JSONObject] = []
|
|
4010
4334
|
client_key_indexes: dict[str, int] = {}
|
|
@@ -4067,17 +4391,6 @@ def _multi_app_static_validation_failure(
|
|
|
4067
4391
|
}
|
|
4068
4392
|
)
|
|
4069
4393
|
continue
|
|
4070
|
-
if new_app_item and not create_if_missing:
|
|
4071
|
-
issues.append(
|
|
4072
|
-
{
|
|
4073
|
-
"index": index,
|
|
4074
|
-
"row_number": index + 1,
|
|
4075
|
-
"path": f"apps[{index}]",
|
|
4076
|
-
"error_code": "CREATE_IF_MISSING_REQUIRED",
|
|
4077
|
-
"message": "apps[] item with app_name but no app_key cannot run when create_if_missing=false",
|
|
4078
|
-
"fix_hint": "Omit create_if_missing in multi-app mode, set it to true, or pass app_key for every existing app item.",
|
|
4079
|
-
}
|
|
4080
|
-
)
|
|
4081
4394
|
|
|
4082
4395
|
add_fields = _multi_app_list_value(item, "add_fields", "addFields")
|
|
4083
4396
|
if new_app_item:
|
|
@@ -4279,6 +4592,86 @@ def _compiled_multi_app_deferred_add_fields(compiled_item: JSONObject, existing_
|
|
|
4279
4592
|
]
|
|
4280
4593
|
|
|
4281
4594
|
|
|
4595
|
+
def _multi_app_child_diagnostics(result: JSONObject) -> JSONObject:
|
|
4596
|
+
diagnostics: JSONObject = {}
|
|
4597
|
+
for key in (
|
|
4598
|
+
"backend_code",
|
|
4599
|
+
"http_status",
|
|
4600
|
+
"request_id",
|
|
4601
|
+
"details",
|
|
4602
|
+
"suggested_next_call",
|
|
4603
|
+
"warnings",
|
|
4604
|
+
"publish_result",
|
|
4605
|
+
"duration_breakdown_ms",
|
|
4606
|
+
):
|
|
4607
|
+
value = result.get(key)
|
|
4608
|
+
if value is None:
|
|
4609
|
+
continue
|
|
4610
|
+
if isinstance(value, (dict, list)) and not value:
|
|
4611
|
+
continue
|
|
4612
|
+
diagnostics[key] = deepcopy(value)
|
|
4613
|
+
return diagnostics
|
|
4614
|
+
|
|
4615
|
+
|
|
4616
|
+
def _multi_app_field_type(field: JSONObject) -> str:
|
|
4617
|
+
raw_type = str(field.get("type") or field.get("field_type") or field.get("fieldType") or "").strip()
|
|
4618
|
+
if raw_type:
|
|
4619
|
+
normalized = FIELD_TYPE_ALIASES.get(raw_type.lower())
|
|
4620
|
+
return normalized.value if normalized is not None else raw_type
|
|
4621
|
+
raw_type_id = field.get("type_id") or field.get("typeId") or field.get("field_type_id") or field.get("fieldTypeId")
|
|
4622
|
+
try:
|
|
4623
|
+
normalized_by_id = FIELD_TYPE_ID_ALIASES.get(int(raw_type_id))
|
|
4624
|
+
except (TypeError, ValueError):
|
|
4625
|
+
normalized_by_id = None
|
|
4626
|
+
return normalized_by_id.value if normalized_by_id is not None else ""
|
|
4627
|
+
|
|
4628
|
+
|
|
4629
|
+
def _multi_app_deferred_relation_retryable(result: JSONObject, deferred_add_fields: list[JSONObject]) -> bool:
|
|
4630
|
+
if not deferred_add_fields:
|
|
4631
|
+
return False
|
|
4632
|
+
if not any(_multi_app_field_type(field) == PublicFieldType.relation.value for field in deferred_add_fields if isinstance(field, dict)):
|
|
4633
|
+
return False
|
|
4634
|
+
if result.get("status") in {"success", "partial_success"}:
|
|
4635
|
+
return False
|
|
4636
|
+
if result.get("error_code") != "SCHEMA_APPLY_FAILED":
|
|
4637
|
+
return False
|
|
4638
|
+
if _schema_apply_result_has_write(result):
|
|
4639
|
+
return False
|
|
4640
|
+
return backend_code_value_int(_result_backend_code(result)) == 400
|
|
4641
|
+
|
|
4642
|
+
|
|
4643
|
+
def _result_backend_code(result: JSONObject) -> Any:
|
|
4644
|
+
if result.get("backend_code") is not None:
|
|
4645
|
+
return result.get("backend_code")
|
|
4646
|
+
details = result.get("details")
|
|
4647
|
+
if isinstance(details, dict):
|
|
4648
|
+
transport = details.get("transport_error")
|
|
4649
|
+
if isinstance(transport, dict):
|
|
4650
|
+
return transport.get("backend_code")
|
|
4651
|
+
return None
|
|
4652
|
+
|
|
4653
|
+
|
|
4654
|
+
def _multi_app_relation_target_app_keys(fields: list[JSONObject]) -> list[str]:
|
|
4655
|
+
app_keys: list[str] = []
|
|
4656
|
+
|
|
4657
|
+
def visit(value: Any) -> None:
|
|
4658
|
+
if isinstance(value, list):
|
|
4659
|
+
for item in value:
|
|
4660
|
+
visit(item)
|
|
4661
|
+
return
|
|
4662
|
+
if not isinstance(value, dict):
|
|
4663
|
+
return
|
|
4664
|
+
target_app_key = str(value.get("target_app_key") or value.get("targetAppKey") or "").strip()
|
|
4665
|
+
if target_app_key and target_app_key not in app_keys:
|
|
4666
|
+
app_keys.append(target_app_key)
|
|
4667
|
+
for child in value.values():
|
|
4668
|
+
if isinstance(child, (dict, list)):
|
|
4669
|
+
visit(child)
|
|
4670
|
+
|
|
4671
|
+
visit(fields)
|
|
4672
|
+
return app_keys
|
|
4673
|
+
|
|
4674
|
+
|
|
4282
4675
|
def _multi_app_list_value(item: JSONObject, *keys: str) -> list[JSONObject]:
|
|
4283
4676
|
for key in keys:
|
|
4284
4677
|
value = item.get(key)
|
|
@@ -4456,7 +4849,6 @@ def _reserved_system_field_name_failure(
|
|
|
4456
4849
|
icon: str | None = None,
|
|
4457
4850
|
color: str | None = None,
|
|
4458
4851
|
visibility: JSONObject | None = None,
|
|
4459
|
-
create_if_missing: bool = False,
|
|
4460
4852
|
publish: bool | None = None,
|
|
4461
4853
|
add_fields: list[JSONObject] | None = None,
|
|
4462
4854
|
update_fields: list[JSONObject] | None = None,
|
|
@@ -4476,7 +4868,6 @@ def _reserved_system_field_name_failure(
|
|
|
4476
4868
|
"icon": icon or "",
|
|
4477
4869
|
"color": color or "",
|
|
4478
4870
|
"visibility": visibility,
|
|
4479
|
-
"create_if_missing": create_if_missing,
|
|
4480
4871
|
"add_fields": suggested_add_fields,
|
|
4481
4872
|
"update_fields": update_fields or [],
|
|
4482
4873
|
"remove_fields": remove_fields or [],
|
|
@@ -4505,7 +4896,6 @@ def _reserved_system_field_name_failure_for_apps(
|
|
|
4505
4896
|
profile: str,
|
|
4506
4897
|
package_id: int | None,
|
|
4507
4898
|
visibility: JSONObject | None,
|
|
4508
|
-
create_if_missing: bool,
|
|
4509
4899
|
publish: bool,
|
|
4510
4900
|
apps: list[JSONObject],
|
|
4511
4901
|
) -> JSONObject | None:
|
|
@@ -4521,7 +4911,6 @@ def _reserved_system_field_name_failure_for_apps(
|
|
|
4521
4911
|
icon=str(item.get("icon") or ""),
|
|
4522
4912
|
color=str(item.get("color") or ""),
|
|
4523
4913
|
visibility=item.get("visibility") if isinstance(item.get("visibility"), dict) else visibility,
|
|
4524
|
-
create_if_missing=create_if_missing,
|
|
4525
4914
|
publish=publish,
|
|
4526
4915
|
add_fields=list(item.get("add_fields") or item.get("addFields") or []),
|
|
4527
4916
|
update_fields=list(item.get("update_fields") or item.get("updateFields") or []),
|
|
@@ -4539,7 +4928,6 @@ def _reserved_system_field_name_failure_for_apps(
|
|
|
4539
4928
|
"profile": profile,
|
|
4540
4929
|
"package_id": package_id,
|
|
4541
4930
|
"visibility": visibility,
|
|
4542
|
-
"create_if_missing": create_if_missing,
|
|
4543
4931
|
"publish": publish,
|
|
4544
4932
|
"apps": fixed_apps,
|
|
4545
4933
|
}
|
|
@@ -4714,12 +5102,13 @@ def _safe_tool_call(
|
|
|
4714
5102
|
normalized_args: JSONObject,
|
|
4715
5103
|
suggested_next_call: JSONObject | None,
|
|
4716
5104
|
) -> JSONObject:
|
|
5105
|
+
started_at = time.perf_counter()
|
|
4717
5106
|
try:
|
|
4718
|
-
|
|
5107
|
+
result = call()
|
|
4719
5108
|
except (QingflowApiError, RuntimeError) as error:
|
|
4720
5109
|
api_error = _coerce_api_error(error)
|
|
4721
5110
|
public_http_status = None if api_error.http_status == 404 else api_error.http_status
|
|
4722
|
-
|
|
5111
|
+
result = {
|
|
4723
5112
|
"status": "failed",
|
|
4724
5113
|
"error_code": error_code,
|
|
4725
5114
|
"recoverable": True,
|
|
@@ -4741,6 +5130,9 @@ def _safe_tool_call(
|
|
|
4741
5130
|
"noop": False,
|
|
4742
5131
|
"verification": {},
|
|
4743
5132
|
}
|
|
5133
|
+
if isinstance(result, dict):
|
|
5134
|
+
_merge_duration_breakdown(result, tool_call_ms=_elapsed_ms(started_at))
|
|
5135
|
+
return result
|
|
4744
5136
|
|
|
4745
5137
|
|
|
4746
5138
|
def _publicize_package_fields(value):
|
|
@@ -4820,6 +5212,8 @@ def _builder_apply_json_paths() -> JSONObject:
|
|
|
4820
5212
|
"warnings": "$.warnings",
|
|
4821
5213
|
"verification": "$.verification",
|
|
4822
5214
|
"details": "$.details",
|
|
5215
|
+
"duration_breakdown_ms": "$.duration_breakdown_ms",
|
|
5216
|
+
"total_ms": "$.duration_breakdown_ms.total_ms",
|
|
4823
5217
|
"write_executed": "$.write_executed",
|
|
4824
5218
|
"write_may_have_succeeded": "$.write_may_have_succeeded",
|
|
4825
5219
|
"safe_to_retry": "$.safe_to_retry",
|
|
@@ -5830,7 +6224,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5830
6224
|
"allowed_keys": [
|
|
5831
6225
|
"package_id",
|
|
5832
6226
|
"package_name",
|
|
5833
|
-
"create_if_missing",
|
|
5834
6227
|
"icon",
|
|
5835
6228
|
"color",
|
|
5836
6229
|
"icon_name",
|
|
@@ -5843,7 +6236,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5843
6236
|
"aliases": {
|
|
5844
6237
|
"packageId": "package_id",
|
|
5845
6238
|
"packageName": "package_name",
|
|
5846
|
-
"createIfMissing": "create_if_missing",
|
|
5847
6239
|
"iconName": "icon",
|
|
5848
6240
|
"iconColor": "color",
|
|
5849
6241
|
"icon_config.name": "icon",
|
|
@@ -5860,6 +6252,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5860
6252
|
},
|
|
5861
6253
|
"execution_notes": [
|
|
5862
6254
|
"create or update package metadata, visibility, grouping, and ordering in one call",
|
|
6255
|
+
"create mode: package_name without package_id creates a package",
|
|
5863
6256
|
"creating a package requires explicit icon + color; icon=template is blocked because it is too generic",
|
|
5864
6257
|
"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",
|
|
5865
6258
|
"updating a package preserves existing icon/color when omitted; explicit icon/color values are still validated",
|
|
@@ -5868,6 +6261,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5868
6261
|
"package_id maps internally to backend tagId; do not use tag_id in public calls",
|
|
5869
6262
|
"items is a full package layout tree; omitting existing app/portal items is blocked unless allow_detach=true",
|
|
5870
6263
|
"item shapes: {type:'app', app_key}, {type:'portal', dash_key}, or {type:'group', group_id?, name, items:[...]}",
|
|
6264
|
+
"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",
|
|
5871
6265
|
"layout apply calls backend package ordering (MoveGroupAuth), so it requires package edit_app permission even when items=[] only clears/deletes existing groups",
|
|
5872
6266
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
5873
6267
|
],
|
|
@@ -5887,7 +6281,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5887
6281
|
"create_example": {
|
|
5888
6282
|
"profile": "default",
|
|
5889
6283
|
"package_name": "项目管理",
|
|
5890
|
-
"create_if_missing": True,
|
|
5891
6284
|
"icon": "briefcase",
|
|
5892
6285
|
"color": "azure",
|
|
5893
6286
|
"visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
|
|
@@ -6106,6 +6499,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6106
6499
|
"remove_buttons supports button_id or exact unique button_text",
|
|
6107
6500
|
"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",
|
|
6108
6501
|
"if a removed button returns readback_status=unavailable or still_exists, treat the result as readback pending and do not blindly repeat the delete",
|
|
6502
|
+
"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",
|
|
6109
6503
|
"all operations share one edit context and publish after at least one write succeeds; there is no draft-only mode for this tool",
|
|
6110
6504
|
"background_color and text_color cannot both be white",
|
|
6111
6505
|
],
|
|
@@ -6251,6 +6645,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6251
6645
|
"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",
|
|
6252
6646
|
"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",
|
|
6253
6647
|
"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",
|
|
6648
|
+
"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",
|
|
6254
6649
|
"this tool publishes after at least one write succeeds; there is no draft-only mode",
|
|
6255
6650
|
"visible=false hides the associated-resource area without clearing previous selected ids; visible=true with limit_type=all shows the whole app-level pool",
|
|
6256
6651
|
],
|
|
@@ -6285,7 +6680,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6285
6680
|
},
|
|
6286
6681
|
},
|
|
6287
6682
|
"app_schema_plan": {
|
|
6288
|
-
"allowed_keys": ["app_key", "package_id", "app_name", "icon", "color", "visibility", "
|
|
6683
|
+
"allowed_keys": ["app_key", "package_id", "app_name", "icon", "color", "visibility", "add_fields", "update_fields", "remove_fields"],
|
|
6289
6684
|
"aliases": {
|
|
6290
6685
|
"app_title": "app_name",
|
|
6291
6686
|
"title": "app_name",
|
|
@@ -6340,7 +6735,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6340
6735
|
"icon": "briefcase",
|
|
6341
6736
|
"color": "emerald",
|
|
6342
6737
|
"visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
|
|
6343
|
-
"create_if_missing": True,
|
|
6344
6738
|
"add_fields": [{"name": "项目名称", "type": "text", "data_title": True}],
|
|
6345
6739
|
"update_fields": [],
|
|
6346
6740
|
"remove_fields": [],
|
|
@@ -6374,7 +6768,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6374
6768
|
"icon_color",
|
|
6375
6769
|
"icon_config",
|
|
6376
6770
|
"visibility",
|
|
6377
|
-
"create_if_missing",
|
|
6378
6771
|
"publish",
|
|
6379
6772
|
"form",
|
|
6380
6773
|
"add_fields",
|
|
@@ -6457,13 +6850,14 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6457
6850
|
"execution_notes": [
|
|
6458
6851
|
"use exactly one resource mode",
|
|
6459
6852
|
"edit mode: app_key, optional app_name to rename the existing app",
|
|
6460
|
-
"create mode: package_id + app_name
|
|
6853
|
+
"create mode: package_id + app_name",
|
|
6854
|
+
"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",
|
|
6461
6855
|
"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",
|
|
6462
|
-
"multi-app mode: pass package_id + apps[];
|
|
6856
|
+
"multi-app mode: pass package_id + apps[]; items with app_name and no app_key are created, items with app_key update existing apps",
|
|
6463
6857
|
"CLI --apps-file primary shape is {package_id, apps:[...]}; raw app arrays and singleton wrapper arrays are compatibility paths, not recommended examples",
|
|
6464
6858
|
"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",
|
|
6465
6859
|
"single-app CLI primary shape is --form-file; multi-app primary shape is apps[].form inside --apps-file",
|
|
6466
|
-
"multi-app mode
|
|
6860
|
+
"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",
|
|
6467
6861
|
"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",
|
|
6468
6862
|
"multi-app relation fields may also use target_app with another apps[].app_name; prefer target_app_ref/client_key when names may collide",
|
|
6469
6863
|
"multi-app mode is not transactional; read created_app_keys and apps[].status before retrying, and retry only failed app items",
|
|
@@ -6504,7 +6898,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6504
6898
|
"icon": "briefcase",
|
|
6505
6899
|
"color": "emerald",
|
|
6506
6900
|
"visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
|
|
6507
|
-
"create_if_missing": True,
|
|
6508
6901
|
"publish": True,
|
|
6509
6902
|
"form": [
|
|
6510
6903
|
{
|