@josephyan/qingflow-app-builder-mcp 1.1.21 → 1.1.23
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 +1581 -487
- 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 +719 -371
|
@@ -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,
|
|
@@ -142,6 +143,7 @@ BUILDER_APPLY_TOOL_NAMES = {
|
|
|
142
143
|
"portal_delete",
|
|
143
144
|
"app_publish_verify",
|
|
144
145
|
}
|
|
146
|
+
MULTI_APP_SCHEMA_APPLY_PARALLEL_LIMIT = 10
|
|
145
147
|
|
|
146
148
|
|
|
147
149
|
class AiBuilderTools(ToolBase):
|
|
@@ -171,13 +173,28 @@ class AiBuilderTools(ToolBase):
|
|
|
171
173
|
)
|
|
172
174
|
|
|
173
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
|
+
|
|
174
189
|
if not isinstance(apps, list) or not apps:
|
|
175
|
-
return
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
+
)
|
|
181
198
|
|
|
182
199
|
app_results: list[JSONObject] = []
|
|
183
200
|
errors: list[JSONObject] = []
|
|
@@ -206,8 +223,10 @@ class AiBuilderTools(ToolBase):
|
|
|
206
223
|
app_results.append(error)
|
|
207
224
|
continue
|
|
208
225
|
try:
|
|
226
|
+
item_started_at = time.perf_counter()
|
|
209
227
|
result = apply_one(index, item, app_key)
|
|
210
228
|
except (QingflowApiError, RuntimeError) as error:
|
|
229
|
+
batch_item_apply_ms += _elapsed_ms(item_started_at)
|
|
211
230
|
api_error = _coerce_api_error(error)
|
|
212
231
|
result = {
|
|
213
232
|
"status": "failed",
|
|
@@ -221,6 +240,13 @@ class AiBuilderTools(ToolBase):
|
|
|
221
240
|
"backend_code": api_error.backend_code,
|
|
222
241
|
"http_status": None if api_error.http_status == 404 else api_error.http_status,
|
|
223
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)
|
|
224
250
|
status = str(result.get("status") or "success") if isinstance(result, dict) else "failed"
|
|
225
251
|
wrapped: JSONObject = {
|
|
226
252
|
"index": index,
|
|
@@ -270,7 +296,7 @@ class AiBuilderTools(ToolBase):
|
|
|
270
296
|
"write_succeeded": write_executed and failed == 0 and partial == 0,
|
|
271
297
|
"safe_to_retry": not write_executed,
|
|
272
298
|
}
|
|
273
|
-
return
|
|
299
|
+
return finish(payload)
|
|
274
300
|
|
|
275
301
|
def _read_app_batch(self, *, tool_name: str, profile: str, app_keys: list[str], read_one) -> JSONObject:
|
|
276
302
|
normalized_keys = [str(item).strip() for item in app_keys if str(item).strip()]
|
|
@@ -356,7 +382,6 @@ class AiBuilderTools(ToolBase):
|
|
|
356
382
|
profile: str = DEFAULT_PROFILE,
|
|
357
383
|
package_id: int | None = None,
|
|
358
384
|
package_name: str | None = None,
|
|
359
|
-
create_if_missing: bool | None = None,
|
|
360
385
|
icon: str | JSONObject | None = None,
|
|
361
386
|
color: str | None = None,
|
|
362
387
|
icon_name: str | None = None,
|
|
@@ -370,7 +395,6 @@ class AiBuilderTools(ToolBase):
|
|
|
370
395
|
profile=profile,
|
|
371
396
|
package_id=package_id,
|
|
372
397
|
package_name=package_name,
|
|
373
|
-
create_if_missing=create_if_missing,
|
|
374
398
|
icon=icon,
|
|
375
399
|
color=color,
|
|
376
400
|
icon_name=icon_name,
|
|
@@ -599,7 +623,6 @@ class AiBuilderTools(ToolBase):
|
|
|
599
623
|
icon_color: str | None = None,
|
|
600
624
|
icon_config: JSONObject | None = None,
|
|
601
625
|
visibility: JSONObject | None = None,
|
|
602
|
-
create_if_missing: bool | None = None,
|
|
603
626
|
publish: bool = True,
|
|
604
627
|
form: list[JSONObject] | None = None,
|
|
605
628
|
add_fields: list[JSONObject] | None = None,
|
|
@@ -611,7 +634,7 @@ class AiBuilderTools(ToolBase):
|
|
|
611
634
|
if app_key or app_name or app_title or form or add_fields or update_fields or remove_fields:
|
|
612
635
|
return _config_failure(
|
|
613
636
|
tool_name="app_schema_apply",
|
|
614
|
-
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.",
|
|
615
638
|
fix_hint="Put per-app form in apps[].form when using batch mode.",
|
|
616
639
|
)
|
|
617
640
|
if package_id is None:
|
|
@@ -624,7 +647,6 @@ class AiBuilderTools(ToolBase):
|
|
|
624
647
|
profile=profile,
|
|
625
648
|
package_id=package_id,
|
|
626
649
|
visibility=visibility,
|
|
627
|
-
create_if_missing=True if create_if_missing is None else bool(create_if_missing),
|
|
628
650
|
publish=publish,
|
|
629
651
|
apps=apps,
|
|
630
652
|
form=None,
|
|
@@ -636,20 +658,19 @@ class AiBuilderTools(ToolBase):
|
|
|
636
658
|
has_app_name = bool((app_name or "").strip())
|
|
637
659
|
has_app_title = bool((app_title or "").strip())
|
|
638
660
|
has_package_id = package_id is not None
|
|
639
|
-
effective_create_if_missing = bool(create_if_missing)
|
|
640
661
|
if has_app_key:
|
|
641
|
-
if
|
|
662
|
+
if has_package_id:
|
|
642
663
|
return _config_failure(
|
|
643
664
|
tool_name="app_schema_apply",
|
|
644
665
|
message="app_schema_apply edit mode accepts app_key and optional app_name rename only.",
|
|
645
|
-
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`.",
|
|
646
667
|
)
|
|
647
|
-
elif not (
|
|
668
|
+
elif not (has_package_id and (has_app_name or has_app_title)):
|
|
648
669
|
return _config_failure(
|
|
649
670
|
tool_name="app_schema_apply",
|
|
650
|
-
message="app_schema_apply create mode requires package_id
|
|
651
|
-
|
|
652
|
-
|
|
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
|
+
)
|
|
653
674
|
return self.app_schema_apply(
|
|
654
675
|
profile=profile,
|
|
655
676
|
app_key=app_key,
|
|
@@ -662,7 +683,6 @@ class AiBuilderTools(ToolBase):
|
|
|
662
683
|
icon_color=icon_color,
|
|
663
684
|
icon_config=icon_config,
|
|
664
685
|
visibility=visibility,
|
|
665
|
-
create_if_missing=effective_create_if_missing,
|
|
666
686
|
publish=publish,
|
|
667
687
|
form=form or [],
|
|
668
688
|
add_fields=add_fields or [],
|
|
@@ -1009,7 +1029,6 @@ class AiBuilderTools(ToolBase):
|
|
|
1009
1029
|
profile: str,
|
|
1010
1030
|
package_id: int | None = None,
|
|
1011
1031
|
package_name: str | None = None,
|
|
1012
|
-
create_if_missing: bool | None = None,
|
|
1013
1032
|
icon: str | JSONObject | None = None,
|
|
1014
1033
|
color: str | None = None,
|
|
1015
1034
|
icon_name: str | None = None,
|
|
@@ -1040,14 +1059,13 @@ class AiBuilderTools(ToolBase):
|
|
|
1040
1059
|
tool_name="package_apply",
|
|
1041
1060
|
icon=icon,
|
|
1042
1061
|
color=color,
|
|
1043
|
-
creating=package_id is None and bool(
|
|
1062
|
+
creating=package_id is None and bool(str(package_name or "").strip()),
|
|
1044
1063
|
)
|
|
1045
1064
|
if icon_failure is not None:
|
|
1046
1065
|
return _attach_builder_apply_envelope("package_apply", icon_failure)
|
|
1047
1066
|
normalized_args = {
|
|
1048
1067
|
"package_id": package_id,
|
|
1049
1068
|
**({"package_name": package_name} if str(package_name or "").strip() else {}),
|
|
1050
|
-
"create_if_missing": bool(create_if_missing),
|
|
1051
1069
|
**({"icon": icon} if icon else {}),
|
|
1052
1070
|
**({"color": color} if color else {}),
|
|
1053
1071
|
**({"visibility": visibility_patch.model_dump(mode="json")} if visibility_patch is not None else {}),
|
|
@@ -1059,7 +1077,6 @@ class AiBuilderTools(ToolBase):
|
|
|
1059
1077
|
profile=profile,
|
|
1060
1078
|
package_id=package_id,
|
|
1061
1079
|
package_name=package_name,
|
|
1062
|
-
create_if_missing=create_if_missing,
|
|
1063
1080
|
icon=icon,
|
|
1064
1081
|
color=color,
|
|
1065
1082
|
visibility=visibility_patch,
|
|
@@ -1879,7 +1896,6 @@ class AiBuilderTools(ToolBase):
|
|
|
1879
1896
|
icon: str = "",
|
|
1880
1897
|
color: str = "",
|
|
1881
1898
|
visibility: JSONObject | None = None,
|
|
1882
|
-
create_if_missing: bool | None = None,
|
|
1883
1899
|
add_fields: list[JSONObject],
|
|
1884
1900
|
update_fields: list[JSONObject],
|
|
1885
1901
|
remove_fields: list[JSONObject],
|
|
@@ -1894,7 +1910,6 @@ class AiBuilderTools(ToolBase):
|
|
|
1894
1910
|
icon=icon,
|
|
1895
1911
|
color=color,
|
|
1896
1912
|
visibility=visibility,
|
|
1897
|
-
create_if_missing=create_if_missing,
|
|
1898
1913
|
add_fields=add_fields,
|
|
1899
1914
|
update_fields=update_fields,
|
|
1900
1915
|
remove_fields=remove_fields,
|
|
@@ -1910,7 +1925,6 @@ class AiBuilderTools(ToolBase):
|
|
|
1910
1925
|
"icon": icon,
|
|
1911
1926
|
"color": color,
|
|
1912
1927
|
"visibility": visibility,
|
|
1913
|
-
"create_if_missing": create_if_missing,
|
|
1914
1928
|
"add_fields": add_fields,
|
|
1915
1929
|
"update_fields": update_fields,
|
|
1916
1930
|
"remove_fields": remove_fields,
|
|
@@ -1931,7 +1945,6 @@ class AiBuilderTools(ToolBase):
|
|
|
1931
1945
|
"icon": icon,
|
|
1932
1946
|
"color": color,
|
|
1933
1947
|
"visibility": visibility,
|
|
1934
|
-
"create_if_missing": create_if_missing,
|
|
1935
1948
|
"add_fields": [{"name": "字段名称", "type": "text"}],
|
|
1936
1949
|
"update_fields": [],
|
|
1937
1950
|
"remove_fields": [],
|
|
@@ -2126,7 +2139,6 @@ class AiBuilderTools(ToolBase):
|
|
|
2126
2139
|
icon_color: str | None = None,
|
|
2127
2140
|
icon_config: JSONObject | None = None,
|
|
2128
2141
|
visibility: JSONObject | None = None,
|
|
2129
|
-
create_if_missing: bool | None = None,
|
|
2130
2142
|
publish: bool = True,
|
|
2131
2143
|
form: list[JSONObject] | None = None,
|
|
2132
2144
|
add_fields: list[JSONObject],
|
|
@@ -2173,7 +2185,6 @@ class AiBuilderTools(ToolBase):
|
|
|
2173
2185
|
profile=profile,
|
|
2174
2186
|
package_id=package_id,
|
|
2175
2187
|
visibility=visibility,
|
|
2176
|
-
create_if_missing=True if create_if_missing is None else bool(create_if_missing),
|
|
2177
2188
|
publish=publish,
|
|
2178
2189
|
apps=apps,
|
|
2179
2190
|
)
|
|
@@ -2207,7 +2218,6 @@ class AiBuilderTools(ToolBase):
|
|
|
2207
2218
|
icon=icon,
|
|
2208
2219
|
color=color,
|
|
2209
2220
|
visibility=visibility,
|
|
2210
|
-
create_if_missing=bool(create_if_missing),
|
|
2211
2221
|
publish=publish,
|
|
2212
2222
|
add_fields=add_fields,
|
|
2213
2223
|
update_fields=update_fields,
|
|
@@ -2226,7 +2236,6 @@ class AiBuilderTools(ToolBase):
|
|
|
2226
2236
|
icon=icon,
|
|
2227
2237
|
color=color,
|
|
2228
2238
|
visibility=visibility,
|
|
2229
|
-
create_if_missing=create_if_missing,
|
|
2230
2239
|
publish=publish,
|
|
2231
2240
|
add_fields=add_fields,
|
|
2232
2241
|
update_fields=update_fields,
|
|
@@ -2242,52 +2251,114 @@ class AiBuilderTools(ToolBase):
|
|
|
2242
2251
|
profile: str,
|
|
2243
2252
|
package_id: int | None,
|
|
2244
2253
|
visibility: JSONObject | None,
|
|
2245
|
-
create_if_missing: bool,
|
|
2246
2254
|
publish: bool,
|
|
2247
2255
|
apps: list[JSONObject],
|
|
2248
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
|
|
2249
2267
|
normalized_args: JSONObject = {
|
|
2250
2268
|
"package_id": package_id,
|
|
2251
|
-
"create_if_missing": create_if_missing,
|
|
2252
2269
|
"publish": publish,
|
|
2253
2270
|
"apps": deepcopy(apps),
|
|
2254
2271
|
}
|
|
2255
2272
|
if visibility is not None:
|
|
2256
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()
|
|
2257
2321
|
system_field_failure = _reserved_system_field_name_failure_for_apps(
|
|
2258
2322
|
tool_name="app_schema_apply",
|
|
2259
2323
|
profile=profile,
|
|
2260
2324
|
package_id=package_id,
|
|
2261
2325
|
visibility=visibility,
|
|
2262
|
-
create_if_missing=create_if_missing,
|
|
2263
2326
|
publish=publish,
|
|
2264
2327
|
apps=apps,
|
|
2265
2328
|
)
|
|
2266
2329
|
if system_field_failure is not None:
|
|
2267
|
-
|
|
2330
|
+
validation_ms += _elapsed_ms(validation_started_at)
|
|
2331
|
+
return finish(system_field_failure)
|
|
2268
2332
|
if package_id is None:
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
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
|
+
)
|
|
2273
2340
|
)
|
|
2274
2341
|
if not apps:
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
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
|
+
)
|
|
2280
2350
|
)
|
|
2281
2351
|
shape_failure = _schema_apps_shape_failure(tool_name="app_schema_apply", apps=apps)
|
|
2282
2352
|
if shape_failure is not None:
|
|
2283
|
-
|
|
2353
|
+
validation_ms += _elapsed_ms(validation_started_at)
|
|
2354
|
+
return finish(shape_failure)
|
|
2284
2355
|
static_validation_failure = _multi_app_static_validation_failure(
|
|
2285
2356
|
tool_name="app_schema_apply",
|
|
2286
2357
|
apps=apps,
|
|
2287
|
-
create_if_missing=create_if_missing,
|
|
2288
2358
|
)
|
|
2289
2359
|
if static_validation_failure is not None:
|
|
2290
|
-
|
|
2360
|
+
validation_ms += _elapsed_ms(validation_started_at)
|
|
2361
|
+
return finish(static_validation_failure)
|
|
2291
2362
|
icon_errors: list[JSONObject] = []
|
|
2292
2363
|
seen_new_app_icons: dict[str, int] = {}
|
|
2293
2364
|
for index, raw_item in enumerate(apps):
|
|
@@ -2338,32 +2409,111 @@ class AiBuilderTools(ToolBase):
|
|
|
2338
2409
|
else:
|
|
2339
2410
|
seen_new_app_icons[normalized_icon] = index
|
|
2340
2411
|
if icon_errors:
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
"
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
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
|
+
)
|
|
2352
2426
|
)
|
|
2427
|
+
validation_ms += _elapsed_ms(validation_started_at)
|
|
2353
2428
|
|
|
2354
2429
|
client_key_to_app_key: dict[str, str] = {}
|
|
2355
2430
|
app_name_to_app_key: dict[str, str] = {}
|
|
2356
2431
|
created_app_keys: list[str] = []
|
|
2357
|
-
|
|
2432
|
+
results_by_index: list[JSONObject | None] = [None] * len(apps)
|
|
2433
|
+
shell_jobs: list[JSONObject] = []
|
|
2358
2434
|
any_write_executed = False
|
|
2359
2435
|
any_write_may_have_succeeded = False
|
|
2360
2436
|
pending_readback_app_keys: list[str] = []
|
|
2361
2437
|
pending_readback_app_names: list[str] = []
|
|
2362
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
|
|
2363
2513
|
|
|
2364
2514
|
for index, raw_item in enumerate(apps):
|
|
2365
2515
|
if not isinstance(raw_item, dict):
|
|
2366
|
-
|
|
2516
|
+
results_by_index[index] = _multi_app_item_failure(index, raw_item, "INVALID_APP_ITEM", "apps[] items must be objects")
|
|
2367
2517
|
continue
|
|
2368
2518
|
item = deepcopy(raw_item)
|
|
2369
2519
|
client_key = str(item.get("client_key") or item.get("clientKey") or "").strip()
|
|
@@ -2371,33 +2521,104 @@ class AiBuilderTools(ToolBase):
|
|
|
2371
2521
|
app_key = str(item.get("app_key") or item.get("appKey") or "").strip()
|
|
2372
2522
|
if client_key:
|
|
2373
2523
|
if client_key in client_keys:
|
|
2374
|
-
|
|
2524
|
+
results_by_index[index] = _multi_app_item_failure(index, item, "DUPLICATE_CLIENT_KEY", f"duplicate client_key '{client_key}'")
|
|
2375
2525
|
continue
|
|
2376
2526
|
client_keys.add(client_key)
|
|
2527
|
+
client_key_indexes[client_key] = index
|
|
2528
|
+
if app_name:
|
|
2529
|
+
app_name_indexes[app_name] = index
|
|
2377
2530
|
if not app_key and not app_name:
|
|
2378
|
-
|
|
2531
|
+
results_by_index[index] = _multi_app_item_failure(index, item, "APP_SELECTOR_REQUIRED", "apps[] requires app_key or app_name")
|
|
2379
2532
|
continue
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
app_name=app_name,
|
|
2389
|
-
app_title="",
|
|
2390
|
-
icon=str(item.get("icon") or ""),
|
|
2391
|
-
color=str(item.get("color") or ""),
|
|
2392
|
-
visibility=item.get("visibility", visibility),
|
|
2393
|
-
create_if_missing=create_if_missing and not app_key,
|
|
2394
|
-
publish=publish and not deferred_add_fields,
|
|
2395
|
-
add_fields=initial_add_fields,
|
|
2396
|
-
update_fields=[],
|
|
2397
|
-
remove_fields=[],
|
|
2398
|
-
inline_form_layout_sections=inline_layout_sections if not deferred_add_fields else None,
|
|
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
|
+
}
|
|
2399
2541
|
)
|
|
2400
|
-
|
|
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)
|
|
2401
2622
|
resolved_key = str(public_shell.get("app_key") or "").strip()
|
|
2402
2623
|
shell_write_executed = _schema_apply_result_has_write(public_shell)
|
|
2403
2624
|
shell_write_may_have_succeeded = _schema_apply_result_may_have_write(public_shell)
|
|
@@ -2417,7 +2638,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2417
2638
|
pending_readback_app_keys.append(resolved_key)
|
|
2418
2639
|
if app_name:
|
|
2419
2640
|
pending_readback_app_names.append(app_name)
|
|
2420
|
-
|
|
2641
|
+
results_by_index[index] = {
|
|
2421
2642
|
"index": index,
|
|
2422
2643
|
"row_number": index + 1,
|
|
2423
2644
|
"client_key": client_key or None,
|
|
@@ -2432,8 +2653,8 @@ class AiBuilderTools(ToolBase):
|
|
|
2432
2653
|
"safe_to_retry": False if pending_readback else (not shell_write_executed and not any_write_executed),
|
|
2433
2654
|
**({"next_action": "readback_before_retry"} if pending_readback else {}),
|
|
2434
2655
|
**({"verification": public_shell.get("verification")} if isinstance(public_shell.get("verification"), dict) else {}),
|
|
2435
|
-
**({"created": True} if pending_readback and
|
|
2436
|
-
}
|
|
2656
|
+
**({"created": True} if pending_readback and not app_key else {}),
|
|
2657
|
+
}
|
|
2437
2658
|
continue
|
|
2438
2659
|
if bool(public_shell.get("created")):
|
|
2439
2660
|
created_app_keys.append(resolved_key)
|
|
@@ -2446,7 +2667,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2446
2667
|
resolved_name = str(public_shell.get("app_name_after") or public_shell.get("app_name") or app_name or "").strip()
|
|
2447
2668
|
if resolved_name:
|
|
2448
2669
|
app_name_to_app_key[resolved_name] = resolved_key
|
|
2449
|
-
|
|
2670
|
+
results_by_index[index] = {
|
|
2450
2671
|
"index": index,
|
|
2451
2672
|
"row_number": index + 1,
|
|
2452
2673
|
"client_key": client_key or None,
|
|
@@ -2457,105 +2678,218 @@ class AiBuilderTools(ToolBase):
|
|
|
2457
2678
|
"shell_result": public_shell,
|
|
2458
2679
|
"shell_field_diff": public_shell.get("field_diff") or {},
|
|
2459
2680
|
"shell_field_diff_details": public_shell.get("field_diff_details") or {},
|
|
2460
|
-
|
|
2461
|
-
})
|
|
2681
|
+
}
|
|
2462
2682
|
|
|
2463
|
-
|
|
2683
|
+
final_items_by_index: list[JSONObject | None] = [None] * len(apps)
|
|
2684
|
+
schema_jobs: list[JSONObject] = []
|
|
2464
2685
|
for index, raw_item in enumerate(apps):
|
|
2465
|
-
existing =
|
|
2686
|
+
existing = results_by_index[index] if 0 <= index < len(results_by_index) else None
|
|
2466
2687
|
if not existing or existing.get("status") != "shell_ready":
|
|
2467
2688
|
if existing:
|
|
2468
|
-
|
|
2689
|
+
final_items_by_index[index] = existing
|
|
2469
2690
|
continue
|
|
2470
2691
|
item = deepcopy(raw_item)
|
|
2471
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()
|
|
2472
2699
|
try:
|
|
2473
2700
|
compiled_item = _compile_multi_app_schema_item_refs(item, client_key_to_app_key, app_name_to_app_key)
|
|
2474
2701
|
except ValueError as error:
|
|
2475
|
-
|
|
2702
|
+
relation_compile_ms += _elapsed_ms(compile_started_at)
|
|
2703
|
+
final_items_by_index[index] = {
|
|
2476
2704
|
**{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
|
|
2477
2705
|
"status": "failed",
|
|
2478
2706
|
"stage": "compile_relation_refs",
|
|
2479
2707
|
"error_code": "TARGET_APP_REF_NOT_FOUND",
|
|
2480
2708
|
"message": str(error),
|
|
2481
2709
|
"safe_to_retry": False,
|
|
2482
|
-
}
|
|
2710
|
+
}
|
|
2483
2711
|
any_write_executed = True
|
|
2484
2712
|
continue
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
_compiled_multi_app_deferred_add_fields(compiled_item, existing)
|
|
2488
|
-
if bool(existing.get("created"))
|
|
2489
|
-
else list(compiled_item.get("add_fields") or [])
|
|
2490
|
-
)
|
|
2713
|
+
relation_compile_ms += _elapsed_ms(compile_started_at)
|
|
2714
|
+
schema_add_fields = list(compiled_item.get("add_fields") or [])
|
|
2491
2715
|
update_fields = list(compiled_item.get("update_fields") or [])
|
|
2492
2716
|
remove_fields = list(compiled_item.get("remove_fields") or [])
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
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(
|
|
2498
2796
|
profile=profile,
|
|
2499
|
-
|
|
2500
|
-
|
|
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(
|
|
2802
|
+
profile=profile,
|
|
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,
|
|
2501
2810
|
publish=publish,
|
|
2502
|
-
|
|
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,
|
|
2503
2816
|
)
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
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
|
+
}
|
|
2828
|
+
)
|
|
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] = {
|
|
2512
2875
|
**{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
|
|
2513
|
-
"status":
|
|
2876
|
+
"status": "pending_readback",
|
|
2514
2877
|
"stage": "schema_apply",
|
|
2515
|
-
"
|
|
2516
|
-
"
|
|
2517
|
-
"
|
|
2518
|
-
"
|
|
2519
|
-
"published": bool(shell_result.get("published")),
|
|
2520
|
-
"verified": bool(shell_result.get("verified")),
|
|
2521
|
-
"error_code": shell_result.get("error_code"),
|
|
2522
|
-
"message": shell_result.get("message"),
|
|
2523
|
-
"write_executed": _schema_apply_result_has_write(shell_result),
|
|
2524
|
-
"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,
|
|
2525
2882
|
"safe_to_retry": False,
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
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
|
|
2531
2890
|
continue
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
profile=profile,
|
|
2535
|
-
app_key=app_key,
|
|
2536
|
-
package_id=None,
|
|
2537
|
-
app_name=str(compiled_item.get("app_name") or compiled_item.get("appTitle") or compiled_item.get("app_title") or ""),
|
|
2538
|
-
app_title="",
|
|
2539
|
-
icon=str(compiled_item.get("icon") or ""),
|
|
2540
|
-
color=str(compiled_item.get("color") or ""),
|
|
2541
|
-
visibility=compiled_item.get("visibility"),
|
|
2542
|
-
create_if_missing=False,
|
|
2543
|
-
publish=publish,
|
|
2544
|
-
add_fields=deferred_add_fields,
|
|
2545
|
-
update_fields=update_fields,
|
|
2546
|
-
remove_fields=remove_fields,
|
|
2547
|
-
inline_form_layout_sections=list(compiled_item.get("_inline_form_layout_sections") or []),
|
|
2548
|
-
)
|
|
2549
|
-
public_result = _publicize_package_fields(field_result)
|
|
2550
|
-
layout_sections = list(compiled_item.get("_inline_form_layout_sections") or [])
|
|
2551
|
-
if layout_sections and not _inline_form_layout_was_handled(public_result):
|
|
2552
|
-
public_result = self._apply_inline_form_layout_after_schema(
|
|
2553
|
-
profile=profile,
|
|
2554
|
-
schema_result=public_result,
|
|
2555
|
-
fallback_app_key=app_key,
|
|
2556
|
-
publish=publish,
|
|
2557
|
-
sections=layout_sections,
|
|
2558
|
-
)
|
|
2891
|
+
public_result = output.get("public_result") if isinstance(output.get("public_result"), dict) else {}
|
|
2892
|
+
merge_child_duration(public_result)
|
|
2559
2893
|
if _schema_apply_result_has_write(public_result):
|
|
2560
2894
|
any_write_executed = True
|
|
2561
2895
|
if _schema_apply_result_may_have_write(public_result):
|
|
@@ -2570,7 +2904,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2570
2904
|
pending_readback_app_keys.append(app_key)
|
|
2571
2905
|
if existing.get("app_name"):
|
|
2572
2906
|
pending_readback_app_names.append(str(existing.get("app_name")))
|
|
2573
|
-
|
|
2907
|
+
final_items_by_index[index] = {
|
|
2574
2908
|
**{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
|
|
2575
2909
|
"status": item_status,
|
|
2576
2910
|
"stage": "schema_apply",
|
|
@@ -2585,12 +2919,17 @@ class AiBuilderTools(ToolBase):
|
|
|
2585
2919
|
"write_executed": _schema_apply_result_has_write(public_result),
|
|
2586
2920
|
"write_may_have_succeeded": _schema_apply_result_may_have_write(public_result),
|
|
2587
2921
|
"safe_to_retry": False,
|
|
2922
|
+
**_multi_app_child_diagnostics(public_result),
|
|
2588
2923
|
**({"next_action": public_result.get("next_action")} if public_result.get("next_action") else {}),
|
|
2589
2924
|
**({"verification": public_result.get("verification")} if isinstance(public_result.get("verification"), dict) else {}),
|
|
2590
2925
|
**({"inline_form_layout": public_result.get("inline_form_layout")} if isinstance(public_result.get("inline_form_layout"), dict) else {}),
|
|
2591
2926
|
**({"inline_form_layout_result": public_result.get("inline_form_layout_result")} if isinstance(public_result.get("inline_form_layout_result"), dict) else {}),
|
|
2592
|
-
}
|
|
2927
|
+
}
|
|
2593
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
|
+
]
|
|
2594
2933
|
pending_readback = sum(1 for item in final_items if item.get("status") == "pending_readback")
|
|
2595
2934
|
succeeded = sum(1 for item in final_items if item.get("status") in {"success", "partial_success"})
|
|
2596
2935
|
failed = sum(1 for item in final_items if item.get("status") == "failed")
|
|
@@ -2614,6 +2953,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2614
2953
|
"package_id": package_id,
|
|
2615
2954
|
"publish_requested": publish,
|
|
2616
2955
|
"apps": final_items,
|
|
2956
|
+
"parallelism": parallelism,
|
|
2617
2957
|
"normalized_args": normalized_args,
|
|
2618
2958
|
"verification": {
|
|
2619
2959
|
"all_apps_succeeded": failed == 0,
|
|
@@ -2634,7 +2974,69 @@ class AiBuilderTools(ToolBase):
|
|
|
2634
2974
|
response["next_action"] = "readback_before_retry"
|
|
2635
2975
|
response["pending_readback_app_keys"] = list(dict.fromkeys(pending_readback_app_keys))
|
|
2636
2976
|
response["pending_readback_app_names"] = list(dict.fromkeys(pending_readback_app_names))
|
|
2637
|
-
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
|
+
}
|
|
2638
3040
|
|
|
2639
3041
|
def _app_schema_apply_once(
|
|
2640
3042
|
self,
|
|
@@ -2647,16 +3049,17 @@ class AiBuilderTools(ToolBase):
|
|
|
2647
3049
|
icon: str = "",
|
|
2648
3050
|
color: str = "",
|
|
2649
3051
|
visibility: JSONObject | None = None,
|
|
2650
|
-
create_if_missing: bool = False,
|
|
2651
3052
|
publish: bool = True,
|
|
2652
3053
|
add_fields: list[JSONObject],
|
|
2653
3054
|
update_fields: list[JSONObject],
|
|
2654
3055
|
remove_fields: list[JSONObject],
|
|
2655
3056
|
inline_form_layout_sections: list[JSONObject] | None = None,
|
|
3057
|
+
allow_inline_layout_fallback: bool = True,
|
|
2656
3058
|
) -> JSONObject:
|
|
2657
3059
|
"""执行内部辅助逻辑。"""
|
|
2658
3060
|
started_at = time.perf_counter()
|
|
2659
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())
|
|
2660
3063
|
system_field_failure = _reserved_system_field_name_failure(
|
|
2661
3064
|
tool_name="app_schema_apply",
|
|
2662
3065
|
profile=profile,
|
|
@@ -2666,7 +3069,6 @@ class AiBuilderTools(ToolBase):
|
|
|
2666
3069
|
icon=icon,
|
|
2667
3070
|
color=color,
|
|
2668
3071
|
visibility=visibility,
|
|
2669
|
-
create_if_missing=create_if_missing,
|
|
2670
3072
|
publish=publish,
|
|
2671
3073
|
add_fields=add_fields,
|
|
2672
3074
|
update_fields=update_fields,
|
|
@@ -2678,42 +3080,19 @@ class AiBuilderTools(ToolBase):
|
|
|
2678
3080
|
tool_name="app_schema_apply",
|
|
2679
3081
|
icon=icon,
|
|
2680
3082
|
color=color,
|
|
2681
|
-
creating=
|
|
3083
|
+
creating=creating,
|
|
2682
3084
|
)
|
|
2683
3085
|
if icon_failure is not None:
|
|
2684
3086
|
return icon_failure
|
|
2685
|
-
plan_result = self._rewrite_plan_result_for_apply(
|
|
2686
|
-
result=self.app_schema_plan(
|
|
2687
|
-
profile=profile,
|
|
2688
|
-
app_key=app_key,
|
|
2689
|
-
package_id=package_id,
|
|
2690
|
-
app_name=effective_app_name,
|
|
2691
|
-
icon=icon,
|
|
2692
|
-
color=color,
|
|
2693
|
-
visibility=visibility,
|
|
2694
|
-
create_if_missing=create_if_missing,
|
|
2695
|
-
add_fields=add_fields,
|
|
2696
|
-
update_fields=update_fields,
|
|
2697
|
-
remove_fields=remove_fields,
|
|
2698
|
-
),
|
|
2699
|
-
profile=profile,
|
|
2700
|
-
publish=publish,
|
|
2701
|
-
plan_tool_name="app_schema_plan",
|
|
2702
|
-
apply_tool_name="app_schema_apply",
|
|
2703
|
-
)
|
|
2704
|
-
if not isinstance(plan_result, dict) or plan_result.get("status") != "success":
|
|
2705
|
-
return plan_result
|
|
2706
|
-
plan_args = plan_result.get("normalized_args")
|
|
2707
|
-
if not isinstance(plan_args, dict):
|
|
2708
|
-
plan_args = {}
|
|
2709
3087
|
try:
|
|
2710
|
-
parsed_add = [FieldPatch.model_validate(item) for item in
|
|
2711
|
-
parsed_update = [FieldUpdatePatch.model_validate(item) for item in
|
|
2712
|
-
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 []]
|
|
2713
3091
|
parsed_inline_layout = [
|
|
2714
3092
|
LayoutSectionPatch.model_validate(item)
|
|
2715
3093
|
for item in (inline_form_layout_sections or [])
|
|
2716
3094
|
]
|
|
3095
|
+
parsed_visibility = VisibilityPatch.model_validate(visibility) if visibility is not None else None
|
|
2717
3096
|
except ValidationError as exc:
|
|
2718
3097
|
return _validation_failure(
|
|
2719
3098
|
str(exc),
|
|
@@ -2723,28 +3102,26 @@ class AiBuilderTools(ToolBase):
|
|
|
2723
3102
|
"tool_name": "app_schema_apply",
|
|
2724
3103
|
"arguments": {
|
|
2725
3104
|
"profile": profile,
|
|
2726
|
-
"app_key": str(
|
|
2727
|
-
"package_id":
|
|
2728
|
-
"app_name":
|
|
2729
|
-
"icon": str(
|
|
2730
|
-
"color": str(
|
|
2731
|
-
"visibility":
|
|
2732
|
-
"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,
|
|
2733
3111
|
"publish": publish,
|
|
2734
|
-
"add_fields":
|
|
2735
|
-
"update_fields":
|
|
2736
|
-
"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 [],
|
|
2737
3115
|
},
|
|
2738
3116
|
},
|
|
2739
3117
|
)
|
|
2740
3118
|
normalized_args = {
|
|
2741
|
-
"app_key": str(
|
|
2742
|
-
"package_id":
|
|
2743
|
-
"app_name":
|
|
2744
|
-
"icon": str(
|
|
2745
|
-
"color": str(
|
|
2746
|
-
"visibility":
|
|
2747
|
-
"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,
|
|
2748
3125
|
"publish": publish,
|
|
2749
3126
|
"add_fields": [patch.model_dump(mode="json") for patch in parsed_add],
|
|
2750
3127
|
"update_fields": [patch.model_dump(mode="json") for patch in parsed_update],
|
|
@@ -2763,13 +3140,12 @@ class AiBuilderTools(ToolBase):
|
|
|
2763
3140
|
result = _safe_tool_call(
|
|
2764
3141
|
lambda: self._facade.app_schema_apply(
|
|
2765
3142
|
profile=profile,
|
|
2766
|
-
app_key=str(
|
|
2767
|
-
package_tag_id=
|
|
2768
|
-
app_name=
|
|
2769
|
-
icon=str(
|
|
2770
|
-
color=str(
|
|
2771
|
-
visibility=
|
|
2772
|
-
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,
|
|
2773
3149
|
publish=publish,
|
|
2774
3150
|
add_fields=parsed_add,
|
|
2775
3151
|
update_fields=parsed_update,
|
|
@@ -2781,12 +3157,12 @@ class AiBuilderTools(ToolBase):
|
|
|
2781
3157
|
suggested_next_call={"tool_name": "app_schema_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
2782
3158
|
)
|
|
2783
3159
|
public_result = _publicize_package_fields(result)
|
|
2784
|
-
if inline_form_layout_sections and not _inline_form_layout_was_handled(public_result):
|
|
3160
|
+
if inline_form_layout_sections and allow_inline_layout_fallback and not _inline_form_layout_was_handled(public_result):
|
|
2785
3161
|
layout_started_at = time.perf_counter()
|
|
2786
3162
|
public_result = self._apply_inline_form_layout_after_schema(
|
|
2787
3163
|
profile=profile,
|
|
2788
3164
|
schema_result=public_result,
|
|
2789
|
-
fallback_app_key=str(
|
|
3165
|
+
fallback_app_key=str(app_key or ""),
|
|
2790
3166
|
publish=publish,
|
|
2791
3167
|
sections=inline_form_layout_sections,
|
|
2792
3168
|
)
|
|
@@ -2912,27 +3288,9 @@ class AiBuilderTools(ToolBase):
|
|
|
2912
3288
|
|
|
2913
3289
|
def _app_layout_apply_once(self, *, profile: str, app_key: str, mode: str = "merge", publish: bool = True, sections: list[JSONObject]) -> JSONObject:
|
|
2914
3290
|
"""执行内部辅助逻辑。"""
|
|
2915
|
-
plan_result = self._rewrite_plan_result_for_apply(
|
|
2916
|
-
result=self.app_layout_plan(
|
|
2917
|
-
profile=profile,
|
|
2918
|
-
app_key=app_key,
|
|
2919
|
-
mode=mode,
|
|
2920
|
-
sections=sections,
|
|
2921
|
-
preset=None,
|
|
2922
|
-
),
|
|
2923
|
-
profile=profile,
|
|
2924
|
-
publish=publish,
|
|
2925
|
-
plan_tool_name="app_layout_plan",
|
|
2926
|
-
apply_tool_name="app_layout_apply",
|
|
2927
|
-
)
|
|
2928
|
-
if not isinstance(plan_result, dict) or plan_result.get("status") != "success":
|
|
2929
|
-
return plan_result
|
|
2930
|
-
plan_args = plan_result.get("normalized_args")
|
|
2931
|
-
if not isinstance(plan_args, dict):
|
|
2932
|
-
plan_args = {}
|
|
2933
3291
|
try:
|
|
2934
|
-
parsed_mode = LayoutApplyMode(str(
|
|
2935
|
-
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 []]
|
|
2936
3294
|
except (ValueError, ValidationError) as exc:
|
|
2937
3295
|
return _validation_failure(
|
|
2938
3296
|
str(exc),
|
|
@@ -2942,15 +3300,15 @@ class AiBuilderTools(ToolBase):
|
|
|
2942
3300
|
"tool_name": "app_layout_apply",
|
|
2943
3301
|
"arguments": {
|
|
2944
3302
|
"profile": profile,
|
|
2945
|
-
"app_key":
|
|
2946
|
-
"mode": str(
|
|
3303
|
+
"app_key": app_key,
|
|
3304
|
+
"mode": str(mode or "merge"),
|
|
2947
3305
|
"publish": publish,
|
|
2948
|
-
"sections":
|
|
3306
|
+
"sections": sections or [{"title": "基础信息", "rows": [["字段A", "字段B"]]}],
|
|
2949
3307
|
},
|
|
2950
3308
|
},
|
|
2951
3309
|
)
|
|
2952
3310
|
normalized_args = {
|
|
2953
|
-
"app_key":
|
|
3311
|
+
"app_key": app_key,
|
|
2954
3312
|
"mode": parsed_mode.value,
|
|
2955
3313
|
"publish": publish,
|
|
2956
3314
|
"sections": [section.model_dump(mode="json", exclude_none=True) for section in parsed_sections],
|
|
@@ -2958,7 +3316,7 @@ class AiBuilderTools(ToolBase):
|
|
|
2958
3316
|
return _safe_tool_call(
|
|
2959
3317
|
lambda: self._facade.app_layout_apply(
|
|
2960
3318
|
profile=profile,
|
|
2961
|
-
app_key=
|
|
3319
|
+
app_key=app_key,
|
|
2962
3320
|
mode=parsed_mode,
|
|
2963
3321
|
publish=publish,
|
|
2964
3322
|
sections=parsed_sections,
|
|
@@ -3061,32 +3419,13 @@ class AiBuilderTools(ToolBase):
|
|
|
3061
3419
|
transitions: list[JSONObject],
|
|
3062
3420
|
) -> JSONObject:
|
|
3063
3421
|
"""执行内部辅助逻辑。"""
|
|
3064
|
-
plan_result = self._rewrite_plan_result_for_apply(
|
|
3065
|
-
result=self.app_flow_plan(
|
|
3066
|
-
profile=profile,
|
|
3067
|
-
app_key=app_key,
|
|
3068
|
-
mode=mode,
|
|
3069
|
-
nodes=nodes,
|
|
3070
|
-
transitions=transitions,
|
|
3071
|
-
preset=None,
|
|
3072
|
-
),
|
|
3073
|
-
profile=profile,
|
|
3074
|
-
publish=publish,
|
|
3075
|
-
plan_tool_name="app_flow_plan",
|
|
3076
|
-
apply_tool_name="app_flow_apply",
|
|
3077
|
-
)
|
|
3078
|
-
if not isinstance(plan_result, dict) or plan_result.get("status") != "success":
|
|
3079
|
-
return plan_result
|
|
3080
|
-
plan_args = plan_result.get("normalized_args")
|
|
3081
|
-
if not isinstance(plan_args, dict):
|
|
3082
|
-
plan_args = {}
|
|
3083
3422
|
try:
|
|
3084
3423
|
request = FlowPlanRequest.model_validate(
|
|
3085
3424
|
{
|
|
3086
|
-
"app_key":
|
|
3087
|
-
"mode":
|
|
3088
|
-
"nodes":
|
|
3089
|
-
"transitions":
|
|
3425
|
+
"app_key": app_key,
|
|
3426
|
+
"mode": mode,
|
|
3427
|
+
"nodes": nodes or [],
|
|
3428
|
+
"transitions": transitions or [],
|
|
3090
3429
|
"preset": None,
|
|
3091
3430
|
}
|
|
3092
3431
|
)
|
|
@@ -3099,11 +3438,11 @@ class AiBuilderTools(ToolBase):
|
|
|
3099
3438
|
"tool_name": "app_flow_apply",
|
|
3100
3439
|
"arguments": {
|
|
3101
3440
|
"profile": profile,
|
|
3102
|
-
"app_key":
|
|
3103
|
-
"mode": str(
|
|
3441
|
+
"app_key": app_key,
|
|
3442
|
+
"mode": str(mode or "replace"),
|
|
3104
3443
|
"publish": publish,
|
|
3105
|
-
"nodes":
|
|
3106
|
-
"transitions":
|
|
3444
|
+
"nodes": nodes or [{"id": "start", "type": "start", "name": "发起"}],
|
|
3445
|
+
"transitions": transitions or [],
|
|
3107
3446
|
},
|
|
3108
3447
|
},
|
|
3109
3448
|
)
|
|
@@ -3200,69 +3539,9 @@ class AiBuilderTools(ToolBase):
|
|
|
3200
3539
|
)
|
|
3201
3540
|
if reserved_failure is not None:
|
|
3202
3541
|
return reserved_failure
|
|
3203
|
-
if patch_views or _view_payload_has_action_buttons(upsert_views=upsert_views, patch_views=patch_views):
|
|
3204
|
-
try:
|
|
3205
|
-
parsed_views = [ViewUpsertPatch.model_validate(item) for item in (upsert_views or [])]
|
|
3206
|
-
parsed_patch_views = [ViewPartialPatch.model_validate(item) for item in patch_views]
|
|
3207
|
-
except ValidationError as exc:
|
|
3208
|
-
return _visibility_validation_failure(
|
|
3209
|
-
str(exc),
|
|
3210
|
-
tool_name="app_views_apply",
|
|
3211
|
-
exc=exc,
|
|
3212
|
-
suggested_next_call={
|
|
3213
|
-
"tool_name": "app_views_apply",
|
|
3214
|
-
"arguments": {
|
|
3215
|
-
"profile": profile,
|
|
3216
|
-
"app_key": app_key,
|
|
3217
|
-
"publish": publish,
|
|
3218
|
-
"upsert_views": upsert_views or [],
|
|
3219
|
-
"patch_views": [
|
|
3220
|
-
{"view_key": "VIEW_KEY", "set": {"query_conditions": {"enabled": True, "rows": [["字段A"]]}}},
|
|
3221
|
-
],
|
|
3222
|
-
"remove_views": remove_views or [],
|
|
3223
|
-
},
|
|
3224
|
-
},
|
|
3225
|
-
)
|
|
3226
|
-
normalized_args = {
|
|
3227
|
-
"app_key": app_key,
|
|
3228
|
-
"publish": publish,
|
|
3229
|
-
"upsert_views": [public_view_upsert_payload(view) for view in parsed_views],
|
|
3230
|
-
"patch_views": [public_view_partial_payload(patch) for patch in parsed_patch_views],
|
|
3231
|
-
"remove_views": list(remove_views or []),
|
|
3232
|
-
}
|
|
3233
|
-
return _safe_tool_call(
|
|
3234
|
-
lambda: self._facade.app_views_apply(
|
|
3235
|
-
profile=profile,
|
|
3236
|
-
app_key=app_key,
|
|
3237
|
-
publish=publish,
|
|
3238
|
-
upsert_views=parsed_views,
|
|
3239
|
-
patch_views=parsed_patch_views,
|
|
3240
|
-
remove_views=list(remove_views or []),
|
|
3241
|
-
),
|
|
3242
|
-
error_code="VIEWS_APPLY_FAILED",
|
|
3243
|
-
normalized_args=normalized_args,
|
|
3244
|
-
suggested_next_call={"tool_name": "app_views_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
3245
|
-
)
|
|
3246
|
-
plan_result = self._rewrite_plan_result_for_apply(
|
|
3247
|
-
result=self.app_views_plan(
|
|
3248
|
-
profile=profile,
|
|
3249
|
-
app_key=app_key,
|
|
3250
|
-
upsert_views=upsert_views,
|
|
3251
|
-
remove_views=remove_views,
|
|
3252
|
-
preset=None,
|
|
3253
|
-
),
|
|
3254
|
-
profile=profile,
|
|
3255
|
-
publish=publish,
|
|
3256
|
-
plan_tool_name="app_views_plan",
|
|
3257
|
-
apply_tool_name="app_views_apply",
|
|
3258
|
-
)
|
|
3259
|
-
if not isinstance(plan_result, dict) or plan_result.get("status") != "success":
|
|
3260
|
-
return plan_result
|
|
3261
|
-
plan_args = plan_result.get("normalized_args")
|
|
3262
|
-
if not isinstance(plan_args, dict):
|
|
3263
|
-
plan_args = {}
|
|
3264
3542
|
try:
|
|
3265
|
-
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 [])]
|
|
3266
3545
|
except ValidationError as exc:
|
|
3267
3546
|
return _visibility_validation_failure(
|
|
3268
3547
|
str(exc),
|
|
@@ -3272,28 +3551,29 @@ class AiBuilderTools(ToolBase):
|
|
|
3272
3551
|
"tool_name": "app_views_apply",
|
|
3273
3552
|
"arguments": {
|
|
3274
3553
|
"profile": profile,
|
|
3275
|
-
"app_key":
|
|
3554
|
+
"app_key": app_key,
|
|
3276
3555
|
"publish": publish,
|
|
3277
|
-
"upsert_views":
|
|
3278
|
-
"
|
|
3556
|
+
"upsert_views": upsert_views or [{"name": "业务台账视图", "type": "table", "columns": ["字段A"]}],
|
|
3557
|
+
"patch_views": patch_views or [],
|
|
3558
|
+
"remove_views": remove_views or [],
|
|
3279
3559
|
},
|
|
3280
3560
|
},
|
|
3281
3561
|
)
|
|
3282
3562
|
normalized_args = {
|
|
3283
|
-
"app_key":
|
|
3563
|
+
"app_key": app_key,
|
|
3284
3564
|
"publish": publish,
|
|
3285
3565
|
"upsert_views": [public_view_upsert_payload(view) for view in parsed_views],
|
|
3286
|
-
"patch_views": [],
|
|
3287
|
-
"remove_views": list(
|
|
3566
|
+
"patch_views": [public_view_partial_payload(patch) for patch in parsed_patch_views],
|
|
3567
|
+
"remove_views": list(remove_views or []),
|
|
3288
3568
|
}
|
|
3289
3569
|
return _safe_tool_call(
|
|
3290
3570
|
lambda: self._facade.app_views_apply(
|
|
3291
3571
|
profile=profile,
|
|
3292
|
-
app_key=
|
|
3572
|
+
app_key=app_key,
|
|
3293
3573
|
publish=publish,
|
|
3294
3574
|
upsert_views=parsed_views,
|
|
3295
|
-
patch_views=
|
|
3296
|
-
remove_views=list(
|
|
3575
|
+
patch_views=parsed_patch_views,
|
|
3576
|
+
remove_views=list(remove_views or []),
|
|
3297
3577
|
),
|
|
3298
3578
|
error_code="VIEWS_APPLY_FAILED",
|
|
3299
3579
|
normalized_args=normalized_args,
|
|
@@ -4049,7 +4329,6 @@ def _multi_app_static_validation_failure(
|
|
|
4049
4329
|
*,
|
|
4050
4330
|
tool_name: str,
|
|
4051
4331
|
apps: list[JSONObject],
|
|
4052
|
-
create_if_missing: bool,
|
|
4053
4332
|
) -> JSONObject | None:
|
|
4054
4333
|
issues: list[JSONObject] = []
|
|
4055
4334
|
client_key_indexes: dict[str, int] = {}
|
|
@@ -4112,17 +4391,6 @@ def _multi_app_static_validation_failure(
|
|
|
4112
4391
|
}
|
|
4113
4392
|
)
|
|
4114
4393
|
continue
|
|
4115
|
-
if new_app_item and not create_if_missing:
|
|
4116
|
-
issues.append(
|
|
4117
|
-
{
|
|
4118
|
-
"index": index,
|
|
4119
|
-
"row_number": index + 1,
|
|
4120
|
-
"path": f"apps[{index}]",
|
|
4121
|
-
"error_code": "CREATE_IF_MISSING_REQUIRED",
|
|
4122
|
-
"message": "apps[] item with app_name but no app_key cannot run when create_if_missing=false",
|
|
4123
|
-
"fix_hint": "Omit create_if_missing in multi-app mode, set it to true, or pass app_key for every existing app item.",
|
|
4124
|
-
}
|
|
4125
|
-
)
|
|
4126
4394
|
|
|
4127
4395
|
add_fields = _multi_app_list_value(item, "add_fields", "addFields")
|
|
4128
4396
|
if new_app_item:
|
|
@@ -4324,6 +4592,86 @@ def _compiled_multi_app_deferred_add_fields(compiled_item: JSONObject, existing_
|
|
|
4324
4592
|
]
|
|
4325
4593
|
|
|
4326
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
|
+
|
|
4327
4675
|
def _multi_app_list_value(item: JSONObject, *keys: str) -> list[JSONObject]:
|
|
4328
4676
|
for key in keys:
|
|
4329
4677
|
value = item.get(key)
|
|
@@ -4501,7 +4849,6 @@ def _reserved_system_field_name_failure(
|
|
|
4501
4849
|
icon: str | None = None,
|
|
4502
4850
|
color: str | None = None,
|
|
4503
4851
|
visibility: JSONObject | None = None,
|
|
4504
|
-
create_if_missing: bool = False,
|
|
4505
4852
|
publish: bool | None = None,
|
|
4506
4853
|
add_fields: list[JSONObject] | None = None,
|
|
4507
4854
|
update_fields: list[JSONObject] | None = None,
|
|
@@ -4521,7 +4868,6 @@ def _reserved_system_field_name_failure(
|
|
|
4521
4868
|
"icon": icon or "",
|
|
4522
4869
|
"color": color or "",
|
|
4523
4870
|
"visibility": visibility,
|
|
4524
|
-
"create_if_missing": create_if_missing,
|
|
4525
4871
|
"add_fields": suggested_add_fields,
|
|
4526
4872
|
"update_fields": update_fields or [],
|
|
4527
4873
|
"remove_fields": remove_fields or [],
|
|
@@ -4550,7 +4896,6 @@ def _reserved_system_field_name_failure_for_apps(
|
|
|
4550
4896
|
profile: str,
|
|
4551
4897
|
package_id: int | None,
|
|
4552
4898
|
visibility: JSONObject | None,
|
|
4553
|
-
create_if_missing: bool,
|
|
4554
4899
|
publish: bool,
|
|
4555
4900
|
apps: list[JSONObject],
|
|
4556
4901
|
) -> JSONObject | None:
|
|
@@ -4566,7 +4911,6 @@ def _reserved_system_field_name_failure_for_apps(
|
|
|
4566
4911
|
icon=str(item.get("icon") or ""),
|
|
4567
4912
|
color=str(item.get("color") or ""),
|
|
4568
4913
|
visibility=item.get("visibility") if isinstance(item.get("visibility"), dict) else visibility,
|
|
4569
|
-
create_if_missing=create_if_missing,
|
|
4570
4914
|
publish=publish,
|
|
4571
4915
|
add_fields=list(item.get("add_fields") or item.get("addFields") or []),
|
|
4572
4916
|
update_fields=list(item.get("update_fields") or item.get("updateFields") or []),
|
|
@@ -4584,7 +4928,6 @@ def _reserved_system_field_name_failure_for_apps(
|
|
|
4584
4928
|
"profile": profile,
|
|
4585
4929
|
"package_id": package_id,
|
|
4586
4930
|
"visibility": visibility,
|
|
4587
|
-
"create_if_missing": create_if_missing,
|
|
4588
4931
|
"publish": publish,
|
|
4589
4932
|
"apps": fixed_apps,
|
|
4590
4933
|
}
|
|
@@ -4759,12 +5102,13 @@ def _safe_tool_call(
|
|
|
4759
5102
|
normalized_args: JSONObject,
|
|
4760
5103
|
suggested_next_call: JSONObject | None,
|
|
4761
5104
|
) -> JSONObject:
|
|
5105
|
+
started_at = time.perf_counter()
|
|
4762
5106
|
try:
|
|
4763
|
-
|
|
5107
|
+
result = call()
|
|
4764
5108
|
except (QingflowApiError, RuntimeError) as error:
|
|
4765
5109
|
api_error = _coerce_api_error(error)
|
|
4766
5110
|
public_http_status = None if api_error.http_status == 404 else api_error.http_status
|
|
4767
|
-
|
|
5111
|
+
result = {
|
|
4768
5112
|
"status": "failed",
|
|
4769
5113
|
"error_code": error_code,
|
|
4770
5114
|
"recoverable": True,
|
|
@@ -4786,6 +5130,9 @@ def _safe_tool_call(
|
|
|
4786
5130
|
"noop": False,
|
|
4787
5131
|
"verification": {},
|
|
4788
5132
|
}
|
|
5133
|
+
if isinstance(result, dict):
|
|
5134
|
+
_merge_duration_breakdown(result, tool_call_ms=_elapsed_ms(started_at))
|
|
5135
|
+
return result
|
|
4789
5136
|
|
|
4790
5137
|
|
|
4791
5138
|
def _publicize_package_fields(value):
|
|
@@ -4865,6 +5212,8 @@ def _builder_apply_json_paths() -> JSONObject:
|
|
|
4865
5212
|
"warnings": "$.warnings",
|
|
4866
5213
|
"verification": "$.verification",
|
|
4867
5214
|
"details": "$.details",
|
|
5215
|
+
"duration_breakdown_ms": "$.duration_breakdown_ms",
|
|
5216
|
+
"total_ms": "$.duration_breakdown_ms.total_ms",
|
|
4868
5217
|
"write_executed": "$.write_executed",
|
|
4869
5218
|
"write_may_have_succeeded": "$.write_may_have_succeeded",
|
|
4870
5219
|
"safe_to_retry": "$.safe_to_retry",
|
|
@@ -5875,7 +6224,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5875
6224
|
"allowed_keys": [
|
|
5876
6225
|
"package_id",
|
|
5877
6226
|
"package_name",
|
|
5878
|
-
"create_if_missing",
|
|
5879
6227
|
"icon",
|
|
5880
6228
|
"color",
|
|
5881
6229
|
"icon_name",
|
|
@@ -5888,7 +6236,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5888
6236
|
"aliases": {
|
|
5889
6237
|
"packageId": "package_id",
|
|
5890
6238
|
"packageName": "package_name",
|
|
5891
|
-
"createIfMissing": "create_if_missing",
|
|
5892
6239
|
"iconName": "icon",
|
|
5893
6240
|
"iconColor": "color",
|
|
5894
6241
|
"icon_config.name": "icon",
|
|
@@ -5905,6 +6252,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5905
6252
|
},
|
|
5906
6253
|
"execution_notes": [
|
|
5907
6254
|
"create or update package metadata, visibility, grouping, and ordering in one call",
|
|
6255
|
+
"create mode: package_name without package_id creates a package",
|
|
5908
6256
|
"creating a package requires explicit icon + color; icon=template is blocked because it is too generic",
|
|
5909
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",
|
|
5910
6258
|
"updating a package preserves existing icon/color when omitted; explicit icon/color values are still validated",
|
|
@@ -5913,6 +6261,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5913
6261
|
"package_id maps internally to backend tagId; do not use tag_id in public calls",
|
|
5914
6262
|
"items is a full package layout tree; omitting existing app/portal items is blocked unless allow_detach=true",
|
|
5915
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",
|
|
5916
6265
|
"layout apply calls backend package ordering (MoveGroupAuth), so it requires package edit_app permission even when items=[] only clears/deletes existing groups",
|
|
5917
6266
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
5918
6267
|
],
|
|
@@ -5932,7 +6281,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5932
6281
|
"create_example": {
|
|
5933
6282
|
"profile": "default",
|
|
5934
6283
|
"package_name": "项目管理",
|
|
5935
|
-
"create_if_missing": True,
|
|
5936
6284
|
"icon": "briefcase",
|
|
5937
6285
|
"color": "azure",
|
|
5938
6286
|
"visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
|
|
@@ -6151,6 +6499,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6151
6499
|
"remove_buttons supports button_id or exact unique button_text",
|
|
6152
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",
|
|
6153
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",
|
|
6154
6503
|
"all operations share one edit context and publish after at least one write succeeds; there is no draft-only mode for this tool",
|
|
6155
6504
|
"background_color and text_color cannot both be white",
|
|
6156
6505
|
],
|
|
@@ -6296,6 +6645,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6296
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",
|
|
6297
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",
|
|
6298
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",
|
|
6299
6649
|
"this tool publishes after at least one write succeeds; there is no draft-only mode",
|
|
6300
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",
|
|
6301
6651
|
],
|
|
@@ -6330,7 +6680,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6330
6680
|
},
|
|
6331
6681
|
},
|
|
6332
6682
|
"app_schema_plan": {
|
|
6333
|
-
"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"],
|
|
6334
6684
|
"aliases": {
|
|
6335
6685
|
"app_title": "app_name",
|
|
6336
6686
|
"title": "app_name",
|
|
@@ -6385,7 +6735,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6385
6735
|
"icon": "briefcase",
|
|
6386
6736
|
"color": "emerald",
|
|
6387
6737
|
"visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
|
|
6388
|
-
"create_if_missing": True,
|
|
6389
6738
|
"add_fields": [{"name": "项目名称", "type": "text", "data_title": True}],
|
|
6390
6739
|
"update_fields": [],
|
|
6391
6740
|
"remove_fields": [],
|
|
@@ -6419,7 +6768,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6419
6768
|
"icon_color",
|
|
6420
6769
|
"icon_config",
|
|
6421
6770
|
"visibility",
|
|
6422
|
-
"create_if_missing",
|
|
6423
6771
|
"publish",
|
|
6424
6772
|
"form",
|
|
6425
6773
|
"add_fields",
|
|
@@ -6502,13 +6850,14 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6502
6850
|
"execution_notes": [
|
|
6503
6851
|
"use exactly one resource mode",
|
|
6504
6852
|
"edit mode: app_key, optional app_name to rename the existing app",
|
|
6505
|
-
"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",
|
|
6506
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",
|
|
6507
|
-
"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",
|
|
6508
6857
|
"CLI --apps-file primary shape is {package_id, apps:[...]}; raw app arrays and singleton wrapper arrays are compatibility paths, not recommended examples",
|
|
6509
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",
|
|
6510
6859
|
"single-app CLI primary shape is --form-file; multi-app primary shape is apps[].form inside --apps-file",
|
|
6511
|
-
"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",
|
|
6512
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",
|
|
6513
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",
|
|
6514
6863
|
"multi-app mode is not transactional; read created_app_keys and apps[].status before retrying, and retry only failed app items",
|
|
@@ -6549,7 +6898,6 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
6549
6898
|
"icon": "briefcase",
|
|
6550
6899
|
"color": "emerald",
|
|
6551
6900
|
"visibility": deepcopy(_VISIBILITY_WORKSPACE_EXAMPLE),
|
|
6552
|
-
"create_if_missing": True,
|
|
6553
6901
|
"publish": True,
|
|
6554
6902
|
"form": [
|
|
6555
6903
|
{
|