@josephyan/qingflow-app-builder-mcp 0.2.0-beta.4 → 0.2.0-beta.6
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
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.
|
|
6
|
+
npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.6
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.
|
|
12
|
+
npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.6 qingflow-app-builder-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -221,8 +221,14 @@ class AiBuilderFacade:
|
|
|
221
221
|
if app_key:
|
|
222
222
|
try:
|
|
223
223
|
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True)
|
|
224
|
-
except RuntimeError as exc:
|
|
225
|
-
|
|
224
|
+
except (QingflowApiError, RuntimeError) as exc:
|
|
225
|
+
api_error = _coerce_api_error(exc)
|
|
226
|
+
return _failed_from_api_error(
|
|
227
|
+
"APP_NOT_FOUND" if api_error.http_status == 404 else "APP_RESOLVE_FAILED",
|
|
228
|
+
api_error,
|
|
229
|
+
details={"app_key": app_key},
|
|
230
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
231
|
+
)
|
|
226
232
|
result = base.get("result") if isinstance(base.get("result"), dict) else {}
|
|
227
233
|
return {
|
|
228
234
|
"status": "success",
|
|
@@ -299,8 +305,30 @@ class AiBuilderFacade:
|
|
|
299
305
|
}
|
|
300
306
|
|
|
301
307
|
def app_read_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
302
|
-
|
|
308
|
+
try:
|
|
309
|
+
state = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
310
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
311
|
+
api_error = _coerce_api_error(error)
|
|
312
|
+
return _failed_from_api_error(
|
|
313
|
+
"APP_READ_FAILED",
|
|
314
|
+
api_error,
|
|
315
|
+
normalized_args={"app_key": app_key},
|
|
316
|
+
details={"app_key": app_key},
|
|
317
|
+
suggested_next_call={"tool_name": "app_resolve", "arguments": {"profile": profile, "app_key": app_key}},
|
|
318
|
+
)
|
|
319
|
+
views, views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
320
|
+
workflow, workflow_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
303
321
|
parsed = state["parsed"]
|
|
322
|
+
verification_hints = _build_verification_hints(
|
|
323
|
+
tag_ids=_coerce_int_list(state["base"].get("tagIds")),
|
|
324
|
+
fields=parsed["fields"],
|
|
325
|
+
layout=parsed["layout"],
|
|
326
|
+
views=_summarize_views(views),
|
|
327
|
+
)
|
|
328
|
+
if views_unavailable:
|
|
329
|
+
verification_hints.append("views_read_unavailable")
|
|
330
|
+
if workflow_unavailable:
|
|
331
|
+
verification_hints.append("workflow_read_unavailable")
|
|
304
332
|
response = AppReadSummaryResponse(
|
|
305
333
|
app_key=app_key,
|
|
306
334
|
title=state["base"].get("formTitle"),
|
|
@@ -308,14 +336,9 @@ class AiBuilderFacade:
|
|
|
308
336
|
publish_status=state["base"].get("appPublishStatus"),
|
|
309
337
|
field_count=len(parsed["fields"]),
|
|
310
338
|
layout_section_count=len(parsed["layout"].get("sections", [])),
|
|
311
|
-
view_count=len(_summarize_views(
|
|
312
|
-
workflow_enabled=bool(
|
|
313
|
-
verification_hints=
|
|
314
|
-
tag_ids=_coerce_int_list(state["base"].get("tagIds")),
|
|
315
|
-
fields=parsed["fields"],
|
|
316
|
-
layout=parsed["layout"],
|
|
317
|
-
views=_summarize_views(state["views"]),
|
|
318
|
-
),
|
|
339
|
+
view_count=len(_summarize_views(views)),
|
|
340
|
+
workflow_enabled=bool(workflow),
|
|
341
|
+
verification_hints=verification_hints,
|
|
319
342
|
)
|
|
320
343
|
return {
|
|
321
344
|
"status": "success",
|
|
@@ -334,7 +357,17 @@ class AiBuilderFacade:
|
|
|
334
357
|
}
|
|
335
358
|
|
|
336
359
|
def app_read_fields(self, *, profile: str, app_key: str) -> JSONObject:
|
|
337
|
-
|
|
360
|
+
try:
|
|
361
|
+
state = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
362
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
363
|
+
api_error = _coerce_api_error(error)
|
|
364
|
+
return _failed_from_api_error(
|
|
365
|
+
"FIELDS_READ_FAILED",
|
|
366
|
+
api_error,
|
|
367
|
+
normalized_args={"app_key": app_key},
|
|
368
|
+
details={"app_key": app_key},
|
|
369
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
370
|
+
)
|
|
338
371
|
parsed = state["parsed"]
|
|
339
372
|
response = AppFieldsReadResponse(
|
|
340
373
|
app_key=app_key,
|
|
@@ -368,7 +401,17 @@ class AiBuilderFacade:
|
|
|
368
401
|
}
|
|
369
402
|
|
|
370
403
|
def app_read_layout_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
371
|
-
|
|
404
|
+
try:
|
|
405
|
+
state = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
406
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
407
|
+
api_error = _coerce_api_error(error)
|
|
408
|
+
return _failed_from_api_error(
|
|
409
|
+
"LAYOUT_READ_FAILED",
|
|
410
|
+
api_error,
|
|
411
|
+
normalized_args={"app_key": app_key},
|
|
412
|
+
details={"app_key": app_key},
|
|
413
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
414
|
+
)
|
|
372
415
|
parsed = state["parsed"]
|
|
373
416
|
layout = parsed["layout"]
|
|
374
417
|
response = AppLayoutReadResponse(
|
|
@@ -394,10 +437,20 @@ class AiBuilderFacade:
|
|
|
394
437
|
}
|
|
395
438
|
|
|
396
439
|
def app_read_views_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
397
|
-
|
|
440
|
+
try:
|
|
441
|
+
views, _ = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=False)
|
|
442
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
443
|
+
api_error = _coerce_api_error(error)
|
|
444
|
+
return _failed_from_api_error(
|
|
445
|
+
"VIEWS_READ_FAILED",
|
|
446
|
+
api_error,
|
|
447
|
+
normalized_args={"app_key": app_key},
|
|
448
|
+
details={"app_key": app_key},
|
|
449
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
450
|
+
)
|
|
398
451
|
response = AppViewsReadResponse(
|
|
399
452
|
app_key=app_key,
|
|
400
|
-
views=_summarize_views(
|
|
453
|
+
views=_summarize_views(views),
|
|
401
454
|
)
|
|
402
455
|
return {
|
|
403
456
|
"status": "success",
|
|
@@ -416,11 +469,21 @@ class AiBuilderFacade:
|
|
|
416
469
|
}
|
|
417
470
|
|
|
418
471
|
def app_read_flow_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
419
|
-
|
|
472
|
+
try:
|
|
473
|
+
workflow, workflow_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
474
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
475
|
+
api_error = _coerce_api_error(error)
|
|
476
|
+
return _failed_from_api_error(
|
|
477
|
+
"FLOW_READ_FAILED",
|
|
478
|
+
api_error,
|
|
479
|
+
normalized_args={"app_key": app_key},
|
|
480
|
+
details={"app_key": app_key},
|
|
481
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
482
|
+
)
|
|
420
483
|
response = AppFlowReadResponse(
|
|
421
484
|
app_key=app_key,
|
|
422
|
-
enabled=bool(
|
|
423
|
-
nodes=_summarize_workflow_nodes(
|
|
485
|
+
enabled=bool(workflow),
|
|
486
|
+
nodes=_summarize_workflow_nodes(workflow),
|
|
424
487
|
transitions=[],
|
|
425
488
|
)
|
|
426
489
|
return {
|
|
@@ -435,7 +498,7 @@ class AiBuilderFacade:
|
|
|
435
498
|
"request_id": None,
|
|
436
499
|
"suggested_next_call": None,
|
|
437
500
|
"noop": False,
|
|
438
|
-
"verification": {"app_exists": True},
|
|
501
|
+
"verification": {"app_exists": True, "workflow_read_unavailable": workflow_unavailable},
|
|
439
502
|
**response.model_dump(mode="json"),
|
|
440
503
|
}
|
|
441
504
|
|
|
@@ -453,7 +516,11 @@ class AiBuilderFacade:
|
|
|
453
516
|
return target
|
|
454
517
|
current_fields: list[dict[str, Any]] = []
|
|
455
518
|
if not bool(target.get("would_create")):
|
|
456
|
-
|
|
519
|
+
fields_result = self.app_read_fields(profile=profile, app_key=str(target["app_key"]))
|
|
520
|
+
if fields_result.get("status") == "failed":
|
|
521
|
+
fields_result.setdefault("normalized_args", normalized_args)
|
|
522
|
+
return fields_result
|
|
523
|
+
current_fields = fields_result.get("fields", [])
|
|
457
524
|
current_by_name = {str(field.get("name") or ""): field for field in current_fields}
|
|
458
525
|
blocking_issues: list[dict[str, Any]] = []
|
|
459
526
|
preview_added: list[str] = []
|
|
@@ -511,8 +578,12 @@ class AiBuilderFacade:
|
|
|
511
578
|
|
|
512
579
|
def app_layout_plan(self, *, profile: str, request: LayoutPlanRequest) -> JSONObject:
|
|
513
580
|
read_fields = self.app_read_fields(profile=profile, app_key=request.app_key)
|
|
581
|
+
if read_fields.get("status") == "failed":
|
|
582
|
+
return read_fields
|
|
514
583
|
current_names = [str(field.get("name") or "") for field in read_fields.get("fields", []) if field.get("name")]
|
|
515
584
|
current_layout = self.app_read_layout_summary(profile=profile, app_key=request.app_key)
|
|
585
|
+
if current_layout.get("status") == "failed":
|
|
586
|
+
return current_layout
|
|
516
587
|
requested_sections = [section.model_dump(mode="json") for section in request.sections]
|
|
517
588
|
if request.preset is not None:
|
|
518
589
|
requested_sections = _build_layout_preset_sections(preset=request.preset, field_names=current_names)
|
|
@@ -575,7 +646,10 @@ class AiBuilderFacade:
|
|
|
575
646
|
transitions = [transition.model_dump(mode="json", by_alias=True) for transition in request.transitions]
|
|
576
647
|
if request.preset is not None:
|
|
577
648
|
nodes, transitions = _build_flow_preset(request.preset)
|
|
578
|
-
|
|
649
|
+
fields_result = self.app_read_fields(profile=profile, app_key=request.app_key)
|
|
650
|
+
if fields_result.get("status") == "failed":
|
|
651
|
+
return fields_result
|
|
652
|
+
current_fields = fields_result.get("fields", [])
|
|
579
653
|
status_field_present = _infer_status_field_id(current_fields) is not None
|
|
580
654
|
node_types = {str(node.get("type") or "") for node in nodes}
|
|
581
655
|
if ("approve" in node_types or request.preset in {FlowPreset.basic_approval, FlowPreset.basic_fill_then_approve}) and not status_field_present:
|
|
@@ -645,7 +719,10 @@ class AiBuilderFacade:
|
|
|
645
719
|
}
|
|
646
720
|
|
|
647
721
|
def app_views_plan(self, *, profile: str, request: ViewsPlanRequest) -> JSONObject:
|
|
648
|
-
|
|
722
|
+
fields_result = self.app_read_fields(profile=profile, app_key=request.app_key)
|
|
723
|
+
if fields_result.get("status") == "failed":
|
|
724
|
+
return fields_result
|
|
725
|
+
current_fields = fields_result.get("fields", [])
|
|
649
726
|
field_names = {str(field.get("name") or "") for field in current_fields}
|
|
650
727
|
upsert_views = [view.model_dump(mode="json") for view in request.upsert_views]
|
|
651
728
|
if request.preset is not None:
|
|
@@ -762,7 +839,23 @@ class AiBuilderFacade:
|
|
|
762
839
|
app_name=str(resolved["app_name"]),
|
|
763
840
|
tag_ids=_coerce_int_list(resolved.get("tag_ids")),
|
|
764
841
|
)
|
|
765
|
-
|
|
842
|
+
schema_readback_delayed = False
|
|
843
|
+
try:
|
|
844
|
+
schema_result, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=target.app_key)
|
|
845
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
846
|
+
api_error = _coerce_api_error(error)
|
|
847
|
+
if not bool(resolved.get("created")) or api_error.http_status != 404:
|
|
848
|
+
return _failed_from_api_error(
|
|
849
|
+
"SCHEMA_READBACK_FAILED",
|
|
850
|
+
api_error,
|
|
851
|
+
normalized_args=normalized_args,
|
|
852
|
+
allowed_values={"field_types": [item.value for item in PublicFieldType]},
|
|
853
|
+
details={"app_key": target.app_key},
|
|
854
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
855
|
+
)
|
|
856
|
+
schema_result = _empty_schema_result(target.app_name)
|
|
857
|
+
_schema_source = "synthetic_new_app"
|
|
858
|
+
schema_readback_delayed = True
|
|
766
859
|
parsed = _parse_schema(schema_result)
|
|
767
860
|
current_fields = parsed["fields"]
|
|
768
861
|
layout = parsed["layout"]
|
|
@@ -870,13 +963,8 @@ class AiBuilderFacade:
|
|
|
870
963
|
},
|
|
871
964
|
suggested_next_call={"tool_name": "app_read_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
872
965
|
)
|
|
873
|
-
verified = self.app_read(profile=profile, app_key=target.app_key, include_raw=False)
|
|
874
|
-
verified_field_names = {field["name"] for field in verified["schema"]["fields"]}
|
|
875
|
-
tag_ids_after = _coerce_int_list((self.apps.app_get_base(profile=profile, app_key=target.app_key, include_raw=True).get("result") or {}).get("tagIds"))
|
|
876
|
-
verification_ok = all(name in verified_field_names for name in added + updated) and all(name not in verified_field_names for name in removed)
|
|
877
|
-
package_attached = None if package_tag_id is None else package_tag_id in tag_ids_after
|
|
878
966
|
response = {
|
|
879
|
-
"status": "success"
|
|
967
|
+
"status": "success",
|
|
880
968
|
"error_code": None,
|
|
881
969
|
"recoverable": False,
|
|
882
970
|
"message": "applied schema patch",
|
|
@@ -885,21 +973,11 @@ class AiBuilderFacade:
|
|
|
885
973
|
"allowed_values": {"field_types": [item.value for item in PublicFieldType]},
|
|
886
974
|
"details": {},
|
|
887
975
|
"request_id": None,
|
|
888
|
-
"suggested_next_call": None
|
|
889
|
-
if package_attached is not False
|
|
890
|
-
else {
|
|
891
|
-
"tool_name": "package_attach_app",
|
|
892
|
-
"arguments": {
|
|
893
|
-
"profile": profile,
|
|
894
|
-
"tag_id": package_tag_id,
|
|
895
|
-
"app_key": target.app_key,
|
|
896
|
-
"app_title": app_name or target.app_name,
|
|
897
|
-
},
|
|
898
|
-
},
|
|
976
|
+
"suggested_next_call": None,
|
|
899
977
|
"noop": False,
|
|
900
978
|
"verification": {
|
|
901
|
-
"fields_verified":
|
|
902
|
-
"package_attached":
|
|
979
|
+
"fields_verified": False,
|
|
980
|
+
"package_attached": None,
|
|
903
981
|
},
|
|
904
982
|
"app_key": target.app_key,
|
|
905
983
|
"created": bool(resolved.get("created")),
|
|
@@ -908,11 +986,71 @@ class AiBuilderFacade:
|
|
|
908
986
|
"updated": updated,
|
|
909
987
|
"removed": removed,
|
|
910
988
|
},
|
|
911
|
-
"verified":
|
|
912
|
-
"tag_ids_after":
|
|
913
|
-
"package_attached":
|
|
989
|
+
"verified": False,
|
|
990
|
+
"tag_ids_after": [],
|
|
991
|
+
"package_attached": None,
|
|
914
992
|
}
|
|
915
|
-
|
|
993
|
+
if schema_readback_delayed:
|
|
994
|
+
response["verification"]["schema_readback_delayed"] = True
|
|
995
|
+
response = self._append_publish_result(profile=profile, app_key=target.app_key, publish=publish, response=response)
|
|
996
|
+
verification_ok = False
|
|
997
|
+
tag_ids_after: list[int] = []
|
|
998
|
+
package_attached: bool | None = None
|
|
999
|
+
verification_error: QingflowApiError | None = None
|
|
1000
|
+
try:
|
|
1001
|
+
verified = self.app_read(profile=profile, app_key=target.app_key, include_raw=False)
|
|
1002
|
+
verified_field_names = {field["name"] for field in verified["schema"]["fields"]}
|
|
1003
|
+
verification_ok = all(name in verified_field_names for name in added + updated) and all(name not in verified_field_names for name in removed)
|
|
1004
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1005
|
+
verification_error = _coerce_api_error(error)
|
|
1006
|
+
verification_ok = False
|
|
1007
|
+
try:
|
|
1008
|
+
base_info = self.apps.app_get_base(profile=profile, app_key=target.app_key, include_raw=True).get("result") or {}
|
|
1009
|
+
tag_ids_after = _coerce_int_list(base_info.get("tagIds"))
|
|
1010
|
+
package_attached = None if package_tag_id is None else package_tag_id in tag_ids_after
|
|
1011
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1012
|
+
base_error = _coerce_api_error(error)
|
|
1013
|
+
if verification_error is None:
|
|
1014
|
+
verification_error = base_error
|
|
1015
|
+
tag_ids_after = []
|
|
1016
|
+
package_attached = None if package_tag_id is None else False
|
|
1017
|
+
response["verification"]["fields_verified"] = verification_ok
|
|
1018
|
+
response["verification"]["package_attached"] = package_attached
|
|
1019
|
+
response["verified"] = verification_ok
|
|
1020
|
+
response["tag_ids_after"] = tag_ids_after
|
|
1021
|
+
response["package_attached"] = package_attached
|
|
1022
|
+
if package_attached is False:
|
|
1023
|
+
response["suggested_next_call"] = {
|
|
1024
|
+
"tool_name": "package_attach_app",
|
|
1025
|
+
"arguments": {
|
|
1026
|
+
"profile": profile,
|
|
1027
|
+
"tag_id": package_tag_id,
|
|
1028
|
+
"app_key": target.app_key,
|
|
1029
|
+
"app_title": app_name or target.app_name,
|
|
1030
|
+
},
|
|
1031
|
+
}
|
|
1032
|
+
publish_failed = bool(response.get("publish_requested")) and not bool(response.get("published"))
|
|
1033
|
+
if verification_ok and package_attached is not False and not publish_failed:
|
|
1034
|
+
response["status"] = "success"
|
|
1035
|
+
else:
|
|
1036
|
+
response["status"] = "partial_success"
|
|
1037
|
+
if verification_error is not None:
|
|
1038
|
+
response["recoverable"] = True
|
|
1039
|
+
response["error_code"] = response.get("error_code") or (
|
|
1040
|
+
"READBACK_PENDING" if verification_error.http_status == 404 else "READBACK_FAILED"
|
|
1041
|
+
)
|
|
1042
|
+
response["message"] = f"{response.get('message') or 'apply succeeded'}; readback pending"
|
|
1043
|
+
response["request_id"] = response.get("request_id") or verification_error.request_id
|
|
1044
|
+
details = response.get("details")
|
|
1045
|
+
if not isinstance(details, dict):
|
|
1046
|
+
details = {}
|
|
1047
|
+
response["details"] = details
|
|
1048
|
+
details["verification_error"] = {
|
|
1049
|
+
"message": verification_error.message,
|
|
1050
|
+
"http_status": verification_error.http_status,
|
|
1051
|
+
"backend_code": verification_error.backend_code,
|
|
1052
|
+
}
|
|
1053
|
+
return response
|
|
916
1054
|
|
|
917
1055
|
def app_layout_apply(
|
|
918
1056
|
self,
|
|
@@ -929,7 +1067,17 @@ class AiBuilderFacade:
|
|
|
929
1067
|
"sections": [section.model_dump(mode="json") for section in sections],
|
|
930
1068
|
"publish": publish,
|
|
931
1069
|
}
|
|
932
|
-
|
|
1070
|
+
try:
|
|
1071
|
+
schema_result, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
1072
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1073
|
+
api_error = _coerce_api_error(error)
|
|
1074
|
+
return _failed_from_api_error(
|
|
1075
|
+
"LAYOUT_READ_FAILED",
|
|
1076
|
+
api_error,
|
|
1077
|
+
normalized_args=normalized_args,
|
|
1078
|
+
details={"app_key": app_key},
|
|
1079
|
+
suggested_next_call={"tool_name": "app_read_layout_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1080
|
+
)
|
|
933
1081
|
parsed = _parse_schema(schema_result)
|
|
934
1082
|
current_fields = parsed["fields"]
|
|
935
1083
|
fields_by_name = {field["name"]: field for field in current_fields}
|
|
@@ -1060,7 +1208,32 @@ class AiBuilderFacade:
|
|
|
1060
1208
|
},
|
|
1061
1209
|
suggested_next_call={"tool_name": "app_layout_plan", "arguments": {"profile": profile, **normalized_args}},
|
|
1062
1210
|
)
|
|
1063
|
-
verified = self.
|
|
1211
|
+
verified = self.app_read_layout_summary(profile=profile, app_key=app_key)
|
|
1212
|
+
if verified.get("status") == "failed":
|
|
1213
|
+
response = {
|
|
1214
|
+
"status": "partial_success",
|
|
1215
|
+
"error_code": "LAYOUT_READBACK_PENDING",
|
|
1216
|
+
"recoverable": True,
|
|
1217
|
+
"message": "applied app layout; layout readback pending",
|
|
1218
|
+
"normalized_args": normalized_args,
|
|
1219
|
+
"missing_fields": [],
|
|
1220
|
+
"allowed_values": {"modes": ["merge", "replace"]},
|
|
1221
|
+
"details": {},
|
|
1222
|
+
"request_id": verified.get("request_id"),
|
|
1223
|
+
"suggested_next_call": {"tool_name": "app_read_layout_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1224
|
+
"noop": False,
|
|
1225
|
+
"verification": {"layout_verified": False, "layout_read_unavailable": True},
|
|
1226
|
+
"app_key": app_key,
|
|
1227
|
+
"layout_diff": {
|
|
1228
|
+
"mode": mode.value,
|
|
1229
|
+
"replaced": mode == LayoutApplyMode.replace,
|
|
1230
|
+
"merged": mode == LayoutApplyMode.merge,
|
|
1231
|
+
"auto_added_fields": merged["auto_added_fields"] if mode == LayoutApplyMode.merge else [],
|
|
1232
|
+
"fallback_applied": fallback_applied,
|
|
1233
|
+
},
|
|
1234
|
+
"verified": False,
|
|
1235
|
+
}
|
|
1236
|
+
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
1064
1237
|
response = {
|
|
1065
1238
|
"status": "partial_success" if fallback_applied else "success",
|
|
1066
1239
|
"error_code": None,
|
|
@@ -1073,7 +1246,7 @@ class AiBuilderFacade:
|
|
|
1073
1246
|
"request_id": None,
|
|
1074
1247
|
"suggested_next_call": None,
|
|
1075
1248
|
"noop": False,
|
|
1076
|
-
"verification": {"layout_verified": verified["
|
|
1249
|
+
"verification": {"layout_verified": verified["sections"] == applied_layout.get("sections", [])},
|
|
1077
1250
|
"app_key": app_key,
|
|
1078
1251
|
"layout_diff": {
|
|
1079
1252
|
"mode": mode.value,
|
|
@@ -1082,7 +1255,7 @@ class AiBuilderFacade:
|
|
|
1082
1255
|
"auto_added_fields": merged["auto_added_fields"] if mode == LayoutApplyMode.merge else [],
|
|
1083
1256
|
"fallback_applied": fallback_applied,
|
|
1084
1257
|
},
|
|
1085
|
-
"verified": verified["
|
|
1258
|
+
"verified": verified["sections"] == applied_layout.get("sections", []),
|
|
1086
1259
|
}
|
|
1087
1260
|
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
1088
1261
|
|
|
@@ -1111,8 +1284,18 @@ class AiBuilderFacade:
|
|
|
1111
1284
|
allowed_values={"modes": ["replace"]},
|
|
1112
1285
|
suggested_next_call={"tool_name": "app_flow_plan", "arguments": {"profile": profile, **normalized_args}},
|
|
1113
1286
|
)
|
|
1114
|
-
|
|
1115
|
-
|
|
1287
|
+
try:
|
|
1288
|
+
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
1289
|
+
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
1290
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1291
|
+
api_error = _coerce_api_error(error)
|
|
1292
|
+
return _failed_from_api_error(
|
|
1293
|
+
"FLOW_READ_FAILED",
|
|
1294
|
+
api_error,
|
|
1295
|
+
normalized_args=normalized_args,
|
|
1296
|
+
details={"app_key": app_key},
|
|
1297
|
+
suggested_next_call={"tool_name": "app_read_flow_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1298
|
+
)
|
|
1116
1299
|
entity = _entity_spec_from_app(base_info=base, schema=schema, views=None)
|
|
1117
1300
|
workflow_spec = _build_public_workflow_spec(nodes=nodes, transitions=transitions)
|
|
1118
1301
|
if workflow_spec.get("status") == "failed":
|
|
@@ -1121,7 +1304,8 @@ class AiBuilderFacade:
|
|
|
1121
1304
|
workflow_spec["suggested_next_call"] = {"tool_name": "app_flow_plan", "arguments": {"profile": profile, **normalized_args}}
|
|
1122
1305
|
return workflow_spec
|
|
1123
1306
|
desired_node_count = len([node for node in nodes if node.get("type") != "end"])
|
|
1124
|
-
|
|
1307
|
+
current_workflow, _workflow_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
1308
|
+
current_node_count = len(_summarize_workflow_nodes(current_workflow))
|
|
1125
1309
|
if current_node_count == desired_node_count and desired_node_count > 0:
|
|
1126
1310
|
# Lightweight idempotency check for repeat submissions of same simple graph.
|
|
1127
1311
|
pass
|
|
@@ -1165,12 +1349,12 @@ class AiBuilderFacade:
|
|
|
1165
1349
|
failed["normalized_args"] = normalized_args
|
|
1166
1350
|
failed["suggested_next_call"] = failed.get("suggested_next_call") or {"tool_name": "app_flow_plan", "arguments": {"profile": profile, **normalized_args}}
|
|
1167
1351
|
return failed
|
|
1168
|
-
verified_nodes = self.
|
|
1352
|
+
verified_nodes, verified_nodes_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
1169
1353
|
response = {
|
|
1170
|
-
"status": "success",
|
|
1354
|
+
"status": "success" if bool(verified_nodes) or not verified_nodes_unavailable else "partial_success",
|
|
1171
1355
|
"error_code": None,
|
|
1172
|
-
"recoverable":
|
|
1173
|
-
"message": "applied workflow patch",
|
|
1356
|
+
"recoverable": bool(verified_nodes_unavailable),
|
|
1357
|
+
"message": "applied workflow patch" if not verified_nodes_unavailable else "applied workflow patch; flow readback pending",
|
|
1174
1358
|
"normalized_args": normalized_args,
|
|
1175
1359
|
"missing_fields": [],
|
|
1176
1360
|
"allowed_values": {"modes": ["replace"]},
|
|
@@ -1178,11 +1362,14 @@ class AiBuilderFacade:
|
|
|
1178
1362
|
"request_id": None,
|
|
1179
1363
|
"suggested_next_call": None,
|
|
1180
1364
|
"noop": False,
|
|
1181
|
-
"verification": {"workflow_verified": bool(verified_nodes)},
|
|
1365
|
+
"verification": {"workflow_verified": bool(verified_nodes), "workflow_read_unavailable": verified_nodes_unavailable},
|
|
1182
1366
|
"app_key": app_key,
|
|
1183
1367
|
"flow_diff": {"mode": "replace", "node_count": desired_node_count},
|
|
1184
1368
|
"verified": bool(verified_nodes),
|
|
1185
1369
|
}
|
|
1370
|
+
if verified_nodes_unavailable:
|
|
1371
|
+
response["error_code"] = "FLOW_READBACK_PENDING"
|
|
1372
|
+
response["suggested_next_call"] = {"tool_name": "app_read_flow_summary", "arguments": {"profile": profile, "app_key": app_key}}
|
|
1186
1373
|
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
1187
1374
|
|
|
1188
1375
|
def app_views_apply(
|
|
@@ -1219,9 +1406,20 @@ class AiBuilderFacade:
|
|
|
1219
1406
|
"verified": True,
|
|
1220
1407
|
}
|
|
1221
1408
|
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1409
|
+
try:
|
|
1410
|
+
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
1411
|
+
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
1412
|
+
existing_views, _views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=False)
|
|
1413
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1414
|
+
api_error = _coerce_api_error(error)
|
|
1415
|
+
return _failed_from_api_error(
|
|
1416
|
+
"VIEWS_READ_FAILED",
|
|
1417
|
+
api_error,
|
|
1418
|
+
normalized_args=normalized_args,
|
|
1419
|
+
details={"app_key": app_key},
|
|
1420
|
+
suggested_next_call={"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1421
|
+
)
|
|
1422
|
+
existing_views = existing_views or []
|
|
1225
1423
|
existing_by_name = {}
|
|
1226
1424
|
for view in existing_views if isinstance(existing_views, list) else []:
|
|
1227
1425
|
if not isinstance(view, dict):
|
|
@@ -1334,22 +1532,37 @@ class AiBuilderFacade:
|
|
|
1334
1532
|
"arguments": {"profile": profile, "app_key": app_key},
|
|
1335
1533
|
},
|
|
1336
1534
|
)
|
|
1337
|
-
|
|
1338
|
-
|
|
1535
|
+
try:
|
|
1536
|
+
verified_view_result, verified_views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
1537
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1538
|
+
api_error = _coerce_api_error(error)
|
|
1539
|
+
return _failed_from_api_error(
|
|
1540
|
+
"VIEWS_READ_FAILED",
|
|
1541
|
+
api_error,
|
|
1542
|
+
normalized_args=normalized_args,
|
|
1543
|
+
details={"app_key": app_key},
|
|
1544
|
+
suggested_next_call={"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1545
|
+
)
|
|
1546
|
+
verified_names = {
|
|
1547
|
+
item.get("viewgraphName") or item.get("viewName") or item.get("title")
|
|
1548
|
+
for item in (verified_view_result or [])
|
|
1549
|
+
if isinstance(item, dict)
|
|
1550
|
+
}
|
|
1551
|
+
verified = (not verified_views_unavailable) and all(name in verified_names for name in created + updated) and all(name not in verified_names for name in removed)
|
|
1339
1552
|
noop = not created and not updated and not removed
|
|
1340
1553
|
response = {
|
|
1341
1554
|
"status": "success" if verified else "partial_success",
|
|
1342
|
-
"error_code": None,
|
|
1343
|
-
"recoverable":
|
|
1344
|
-
"message": "applied view patch",
|
|
1555
|
+
"error_code": None if not verified_views_unavailable else "VIEWS_READBACK_PENDING",
|
|
1556
|
+
"recoverable": bool(verified_views_unavailable),
|
|
1557
|
+
"message": "applied view patch" if not verified_views_unavailable else "applied view patch; views readback pending",
|
|
1345
1558
|
"normalized_args": normalized_args,
|
|
1346
1559
|
"missing_fields": [],
|
|
1347
1560
|
"allowed_values": {"view_types": ["table", "card", "board"]},
|
|
1348
1561
|
"details": {},
|
|
1349
1562
|
"request_id": None,
|
|
1350
|
-
"suggested_next_call": None,
|
|
1563
|
+
"suggested_next_call": None if not verified_views_unavailable else {"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1351
1564
|
"noop": noop,
|
|
1352
|
-
"verification": {"views_verified": verified},
|
|
1565
|
+
"verification": {"views_verified": verified, "views_read_unavailable": verified_views_unavailable},
|
|
1353
1566
|
"app_key": app_key,
|
|
1354
1567
|
"views_diff": {"created": created, "updated": updated, "removed": removed},
|
|
1355
1568
|
"verified": verified,
|
|
@@ -1364,12 +1577,33 @@ class AiBuilderFacade:
|
|
|
1364
1577
|
expected_package_tag_id: int | None = None,
|
|
1365
1578
|
) -> JSONObject:
|
|
1366
1579
|
normalized_args = {"app_key": app_key, "expected_package_tag_id": expected_package_tag_id}
|
|
1367
|
-
|
|
1580
|
+
try:
|
|
1581
|
+
base_before = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
1582
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1583
|
+
api_error = _coerce_api_error(error)
|
|
1584
|
+
return _failed_from_api_error(
|
|
1585
|
+
"APP_READ_FAILED",
|
|
1586
|
+
api_error,
|
|
1587
|
+
normalized_args=normalized_args,
|
|
1588
|
+
details={"app_key": app_key},
|
|
1589
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1590
|
+
)
|
|
1368
1591
|
tag_ids_before = _coerce_int_list(base_before.get("tagIds"))
|
|
1369
1592
|
already_published = bool(base_before.get("appPublishStatus") in {1, 2})
|
|
1370
1593
|
package_already_attached = None if not expected_package_tag_id else expected_package_tag_id in tag_ids_before
|
|
1371
|
-
|
|
1372
|
-
|
|
1594
|
+
try:
|
|
1595
|
+
views_before, views_before_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
1596
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1597
|
+
api_error = _coerce_api_error(error)
|
|
1598
|
+
return _failed_from_api_error(
|
|
1599
|
+
"VIEWS_READ_FAILED",
|
|
1600
|
+
api_error,
|
|
1601
|
+
normalized_args=normalized_args,
|
|
1602
|
+
details={"app_key": app_key},
|
|
1603
|
+
suggested_next_call={"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1604
|
+
)
|
|
1605
|
+
views_before = views_before or []
|
|
1606
|
+
if already_published and package_already_attached is not False and isinstance(views_before, list) and not views_before_unavailable:
|
|
1373
1607
|
return {
|
|
1374
1608
|
"status": "success",
|
|
1375
1609
|
"error_code": None,
|
|
@@ -1404,17 +1638,38 @@ class AiBuilderFacade:
|
|
|
1404
1638
|
details={"app_key": app_key, "edit_version_no": edit_version_no},
|
|
1405
1639
|
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1406
1640
|
)
|
|
1407
|
-
|
|
1641
|
+
try:
|
|
1642
|
+
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
1643
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1644
|
+
api_error = _coerce_api_error(error)
|
|
1645
|
+
return _failed_from_api_error(
|
|
1646
|
+
"APP_READ_FAILED",
|
|
1647
|
+
api_error,
|
|
1648
|
+
normalized_args=normalized_args,
|
|
1649
|
+
details={"app_key": app_key},
|
|
1650
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1651
|
+
)
|
|
1408
1652
|
tag_ids_after = _coerce_int_list(base.get("tagIds"))
|
|
1409
1653
|
package_attached = None if not expected_package_tag_id else expected_package_tag_id in tag_ids_after
|
|
1410
|
-
|
|
1411
|
-
|
|
1654
|
+
try:
|
|
1655
|
+
views, views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
1656
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1657
|
+
api_error = _coerce_api_error(error)
|
|
1658
|
+
return _failed_from_api_error(
|
|
1659
|
+
"VIEWS_READ_FAILED",
|
|
1660
|
+
api_error,
|
|
1661
|
+
normalized_args=normalized_args,
|
|
1662
|
+
details={"app_key": app_key},
|
|
1663
|
+
suggested_next_call={"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1664
|
+
)
|
|
1665
|
+
views = views or []
|
|
1666
|
+
views_ok = isinstance(views, list) and not views_unavailable
|
|
1412
1667
|
verified = bool(base.get("appPublishStatus") in {1, 2}) and (package_attached is not False) and views_ok
|
|
1413
1668
|
return {
|
|
1414
1669
|
"status": "success" if verified else "partial_success",
|
|
1415
|
-
"error_code": None,
|
|
1416
|
-
"recoverable":
|
|
1417
|
-
"message": "published and verified app",
|
|
1670
|
+
"error_code": None if not views_unavailable else "VIEWS_READBACK_PENDING",
|
|
1671
|
+
"recoverable": bool(views_unavailable),
|
|
1672
|
+
"message": "published and verified app" if not views_unavailable else "published app; views readback pending",
|
|
1418
1673
|
"normalized_args": normalized_args,
|
|
1419
1674
|
"missing_fields": [],
|
|
1420
1675
|
"allowed_values": {},
|
|
@@ -1427,7 +1682,7 @@ class AiBuilderFacade:
|
|
|
1427
1682
|
"arguments": {"profile": profile, "tag_id": expected_package_tag_id, "app_key": app_key},
|
|
1428
1683
|
},
|
|
1429
1684
|
"noop": False,
|
|
1430
|
-
"verification": {"published": bool(base.get("appPublishStatus") in {1, 2}), "package_attached": package_attached, "views_ok": views_ok},
|
|
1685
|
+
"verification": {"published": bool(base.get("appPublishStatus") in {1, 2}), "package_attached": package_attached, "views_ok": views_ok, "views_read_unavailable": views_unavailable},
|
|
1431
1686
|
"app_key": app_key,
|
|
1432
1687
|
"published": bool(base.get("appPublishStatus") in {1, 2}),
|
|
1433
1688
|
"package_attached": package_attached,
|
|
@@ -1491,21 +1746,59 @@ class AiBuilderFacade:
|
|
|
1491
1746
|
response["suggested_next_call"] = publish_result.get("suggested_next_call")
|
|
1492
1747
|
return response
|
|
1493
1748
|
|
|
1494
|
-
def
|
|
1749
|
+
def _load_base_schema_state(self, *, profile: str, app_key: str) -> dict[str, Any]:
|
|
1495
1750
|
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True)
|
|
1496
1751
|
schema_result, schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
1497
|
-
views = self.views.view_list_flat(profile=profile, app_key=app_key)
|
|
1498
|
-
workflow = self.workflows.workflow_list_nodes(profile=profile, app_key=app_key)
|
|
1499
1752
|
base_result = base.get("result") if isinstance(base.get("result"), dict) else {}
|
|
1500
1753
|
return {
|
|
1501
1754
|
"base": base_result,
|
|
1502
1755
|
"schema": schema_result,
|
|
1503
1756
|
"parsed": _parse_schema(schema_result),
|
|
1504
|
-
"views": views.get("result"),
|
|
1505
|
-
"workflow": workflow.get("result"),
|
|
1506
1757
|
"schema_source": schema_source,
|
|
1507
1758
|
}
|
|
1508
1759
|
|
|
1760
|
+
def _load_views_result(self, *, profile: str, app_key: str, tolerate_404: bool) -> tuple[Any, bool]:
|
|
1761
|
+
try:
|
|
1762
|
+
views = self.views.view_list_flat(profile=profile, app_key=app_key)
|
|
1763
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1764
|
+
api_error = _coerce_api_error(error)
|
|
1765
|
+
if api_error.http_status == 404:
|
|
1766
|
+
try:
|
|
1767
|
+
legacy_views = self.views.view_list(profile=profile, app_key=app_key)
|
|
1768
|
+
except (QingflowApiError, RuntimeError) as legacy_error:
|
|
1769
|
+
legacy_api_error = _coerce_api_error(legacy_error)
|
|
1770
|
+
if tolerate_404 and legacy_api_error.http_status == 404:
|
|
1771
|
+
return [], True
|
|
1772
|
+
raise
|
|
1773
|
+
legacy_result = legacy_views.get("result")
|
|
1774
|
+
if _is_view_collection_shape(legacy_result):
|
|
1775
|
+
return _normalize_view_collection(legacy_result), False
|
|
1776
|
+
if tolerate_404:
|
|
1777
|
+
return [], True
|
|
1778
|
+
raise error
|
|
1779
|
+
raise
|
|
1780
|
+
return _normalize_view_collection(views.get("result")), False
|
|
1781
|
+
|
|
1782
|
+
def _load_workflow_result(self, *, profile: str, app_key: str, tolerate_404: bool) -> tuple[Any, bool]:
|
|
1783
|
+
try:
|
|
1784
|
+
workflow = self.workflows.workflow_list_nodes(profile=profile, app_key=app_key)
|
|
1785
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1786
|
+
api_error = _coerce_api_error(error)
|
|
1787
|
+
if tolerate_404 and api_error.http_status == 404:
|
|
1788
|
+
return [], True
|
|
1789
|
+
raise
|
|
1790
|
+
return workflow.get("result"), False
|
|
1791
|
+
|
|
1792
|
+
def _load_app_state(self, *, profile: str, app_key: str) -> dict[str, Any]:
|
|
1793
|
+
state = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
1794
|
+
views, views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
1795
|
+
workflow, workflow_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
1796
|
+
state["views"] = views
|
|
1797
|
+
state["workflow"] = workflow
|
|
1798
|
+
state["views_unavailable"] = views_unavailable
|
|
1799
|
+
state["workflow_unavailable"] = workflow_unavailable
|
|
1800
|
+
return state
|
|
1801
|
+
|
|
1509
1802
|
def _read_schema_with_fallback(self, *, profile: str, app_key: str) -> tuple[dict[str, Any], str]:
|
|
1510
1803
|
attempts = (
|
|
1511
1804
|
("draft", True),
|
|
@@ -1623,7 +1916,28 @@ class AiBuilderFacade:
|
|
|
1623
1916
|
new_app_key = str(result.get("appKey") or (result.get("appKeys")[0] if isinstance(result.get("appKeys"), list) and result.get("appKeys") else ""))
|
|
1624
1917
|
if not new_app_key:
|
|
1625
1918
|
return _failed("APP_CREATE_FAILED", "failed to create app shell", details={"result": result}, suggested_next_call=None)
|
|
1626
|
-
|
|
1919
|
+
try:
|
|
1920
|
+
base = self.apps.app_get_base(profile=profile, app_key=new_app_key, include_raw=True).get("result") or {}
|
|
1921
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1922
|
+
api_error = _coerce_api_error(error)
|
|
1923
|
+
if api_error.http_status != 404:
|
|
1924
|
+
return _failed_from_api_error(
|
|
1925
|
+
"APP_CREATE_READBACK_FAILED",
|
|
1926
|
+
api_error,
|
|
1927
|
+
details={"app_key": new_app_key, "app_name": app_name, "package_tag_id": package_tag_id},
|
|
1928
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": new_app_key}},
|
|
1929
|
+
)
|
|
1930
|
+
return {
|
|
1931
|
+
"status": "success",
|
|
1932
|
+
"error_code": None,
|
|
1933
|
+
"recoverable": False,
|
|
1934
|
+
"message": "created app; base readback pending",
|
|
1935
|
+
"suggested_next_call": None,
|
|
1936
|
+
"app_key": new_app_key,
|
|
1937
|
+
"app_name": app_name or "未命名应用",
|
|
1938
|
+
"tag_ids": [],
|
|
1939
|
+
"created": True,
|
|
1940
|
+
}
|
|
1627
1941
|
return {
|
|
1628
1942
|
"status": "success",
|
|
1629
1943
|
"error_code": None,
|
|
@@ -1718,18 +2032,30 @@ def _failed_from_api_error(
|
|
|
1718
2032
|
suggested_next_call: JSONObject | None = None,
|
|
1719
2033
|
recoverable: bool = True,
|
|
1720
2034
|
) -> JSONObject:
|
|
2035
|
+
public_message = _public_error_message(error_code, error)
|
|
2036
|
+
public_http_status = None if error.http_status == 404 else error.http_status
|
|
2037
|
+
merged_details = dict(details or {})
|
|
2038
|
+
if error.http_status is not None or error.backend_code is not None:
|
|
2039
|
+
merged_details.setdefault(
|
|
2040
|
+
"transport_error",
|
|
2041
|
+
{
|
|
2042
|
+
"http_status": error.http_status,
|
|
2043
|
+
"backend_code": error.backend_code,
|
|
2044
|
+
"category": error.category,
|
|
2045
|
+
},
|
|
2046
|
+
)
|
|
1721
2047
|
return _failed(
|
|
1722
2048
|
error_code,
|
|
1723
|
-
|
|
2049
|
+
public_message,
|
|
1724
2050
|
recoverable=recoverable,
|
|
1725
2051
|
normalized_args=normalized_args,
|
|
1726
2052
|
missing_fields=missing_fields,
|
|
1727
2053
|
allowed_values=allowed_values,
|
|
1728
|
-
details=
|
|
2054
|
+
details=merged_details,
|
|
1729
2055
|
suggested_next_call=suggested_next_call,
|
|
1730
2056
|
request_id=error.request_id,
|
|
1731
2057
|
backend_code=error.backend_code,
|
|
1732
|
-
http_status=
|
|
2058
|
+
http_status=public_http_status,
|
|
1733
2059
|
)
|
|
1734
2060
|
|
|
1735
2061
|
|
|
@@ -1768,6 +2094,27 @@ def _coerce_api_error(error: Exception) -> QingflowApiError:
|
|
|
1768
2094
|
return QingflowApiError(category="runtime", message=str(error))
|
|
1769
2095
|
|
|
1770
2096
|
|
|
2097
|
+
def _public_error_message(error_code: str, error: QingflowApiError) -> str:
|
|
2098
|
+
if error.http_status != 404:
|
|
2099
|
+
return error.message
|
|
2100
|
+
mapping = {
|
|
2101
|
+
"APP_READ_FAILED": "app base or schema is unavailable in the current route",
|
|
2102
|
+
"FIELDS_READ_FAILED": "app fields are unavailable in the current route",
|
|
2103
|
+
"LAYOUT_READ_FAILED": "layout resource is unavailable for this app in the current route",
|
|
2104
|
+
"VIEWS_READ_FAILED": "views resource is unavailable for this app in the current route",
|
|
2105
|
+
"FLOW_READ_FAILED": "workflow resource is unavailable for this app in the current route",
|
|
2106
|
+
"SCHEMA_READBACK_FAILED": "schema was written but schema readback is unavailable in the current route",
|
|
2107
|
+
"CREATE_APP_ROUTE_NOT_FOUND": "create app route is unavailable in the current workspace route",
|
|
2108
|
+
"APP_CREATE_READBACK_FAILED": "app was created but base readback is unavailable in the current route",
|
|
2109
|
+
"PACKAGE_ATTACH_FAILED": "package attachment could not be verified in the current route",
|
|
2110
|
+
"PUBLISH_FAILED": "publish route is unavailable in the current route",
|
|
2111
|
+
"VIEW_APPLY_FAILED": "view resource rejected the operation or is unavailable in the current route",
|
|
2112
|
+
"LAYOUT_APPLY_FAILED": "layout resource rejected the operation or is unavailable in the current route",
|
|
2113
|
+
"SCHEMA_APPLY_FAILED": "schema resource rejected the operation or is unavailable in the current route",
|
|
2114
|
+
}
|
|
2115
|
+
return mapping.get(error_code, "requested builder resource is unavailable in the current route")
|
|
2116
|
+
|
|
2117
|
+
|
|
1771
2118
|
def _coerce_positive_int(value: Any) -> int | None:
|
|
1772
2119
|
if isinstance(value, bool) or value is None:
|
|
1773
2120
|
return None
|
|
@@ -1795,6 +2142,34 @@ def _coerce_int_list(values: Any) -> list[int]:
|
|
|
1795
2142
|
return result
|
|
1796
2143
|
|
|
1797
2144
|
|
|
2145
|
+
def _normalize_view_collection(values: Any) -> list[dict[str, Any]]:
|
|
2146
|
+
if isinstance(values, list):
|
|
2147
|
+
return [item for item in values if isinstance(item, dict)]
|
|
2148
|
+
if isinstance(values, dict):
|
|
2149
|
+
for key in ("list", "viewList", "views", "result"):
|
|
2150
|
+
candidate = values.get(key)
|
|
2151
|
+
if isinstance(candidate, list):
|
|
2152
|
+
return [item for item in candidate if isinstance(item, dict)]
|
|
2153
|
+
return []
|
|
2154
|
+
|
|
2155
|
+
|
|
2156
|
+
def _is_view_collection_shape(values: Any) -> bool:
|
|
2157
|
+
if isinstance(values, list):
|
|
2158
|
+
return True
|
|
2159
|
+
if isinstance(values, dict):
|
|
2160
|
+
return any(isinstance(values.get(key), list) for key in ("list", "viewList", "views", "result"))
|
|
2161
|
+
return False
|
|
2162
|
+
|
|
2163
|
+
|
|
2164
|
+
def _empty_schema_result(title: str) -> dict[str, Any]:
|
|
2165
|
+
return {
|
|
2166
|
+
"formTitle": title,
|
|
2167
|
+
"editVersionNo": 1,
|
|
2168
|
+
"formQues": [],
|
|
2169
|
+
"questionRelations": [],
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
|
|
1798
2173
|
def _slugify(text: str, *, default: str) -> str:
|
|
1799
2174
|
normalized = "".join(ch.lower() if ch.isalnum() else "_" for ch in str(text or ""))
|
|
1800
2175
|
collapsed = "_".join(part for part in normalized.split("_") if part)
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
|
|
3
5
|
from pydantic import ValidationError
|
|
4
6
|
|
|
5
7
|
from ..config import DEFAULT_PROFILE
|
|
8
|
+
from ..errors import QingflowApiError
|
|
6
9
|
from ..json_types import JSONObject
|
|
7
10
|
from ..builder_facade.models import (
|
|
8
11
|
FieldPatch,
|
|
@@ -240,31 +243,85 @@ class AiBuilderTools(ToolBase):
|
|
|
240
243
|
)
|
|
241
244
|
|
|
242
245
|
def package_list(self, *, profile: str, trial_status: str = "all") -> JSONObject:
|
|
243
|
-
|
|
246
|
+
normalized_args = {"trial_status": trial_status}
|
|
247
|
+
return _safe_tool_call(
|
|
248
|
+
lambda: self._facade.package_list(profile=profile, trial_status=trial_status),
|
|
249
|
+
error_code="PACKAGE_LIST_FAILED",
|
|
250
|
+
normalized_args=normalized_args,
|
|
251
|
+
suggested_next_call={"tool_name": "package_list", "arguments": {"profile": profile, "trial_status": trial_status}},
|
|
252
|
+
)
|
|
244
253
|
|
|
245
254
|
def package_resolve(self, *, profile: str, package_name: str) -> JSONObject:
|
|
246
|
-
|
|
255
|
+
normalized_args = {"package_name": package_name}
|
|
256
|
+
return _safe_tool_call(
|
|
257
|
+
lambda: self._facade.package_resolve(profile=profile, package_name=package_name),
|
|
258
|
+
error_code="PACKAGE_RESOLVE_FAILED",
|
|
259
|
+
normalized_args=normalized_args,
|
|
260
|
+
suggested_next_call={"tool_name": "package_resolve", "arguments": {"profile": profile, "package_name": package_name}},
|
|
261
|
+
)
|
|
247
262
|
|
|
248
263
|
def package_attach_app(self, *, profile: str, tag_id: int, app_key: str, app_title: str = "") -> JSONObject:
|
|
249
|
-
|
|
264
|
+
normalized_args = {"tag_id": tag_id, "app_key": app_key, "app_title": app_title}
|
|
265
|
+
return _safe_tool_call(
|
|
266
|
+
lambda: self._facade.package_attach_app(profile=profile, tag_id=tag_id, app_key=app_key, app_title=app_title),
|
|
267
|
+
error_code="PACKAGE_ATTACH_FAILED",
|
|
268
|
+
normalized_args=normalized_args,
|
|
269
|
+
suggested_next_call={"tool_name": "package_attach_app", "arguments": {"profile": profile, **normalized_args}},
|
|
270
|
+
)
|
|
250
271
|
|
|
251
272
|
def app_resolve(self, *, profile: str, app_key: str = "", app_name: str = "", package_tag_id: int | None = None) -> JSONObject:
|
|
252
|
-
|
|
273
|
+
normalized_args = {"app_key": app_key, "app_name": app_name, "package_tag_id": package_tag_id}
|
|
274
|
+
return _safe_tool_call(
|
|
275
|
+
lambda: self._facade.app_resolve(profile=profile, app_key=app_key, app_name=app_name, package_tag_id=package_tag_id),
|
|
276
|
+
error_code="APP_RESOLVE_FAILED",
|
|
277
|
+
normalized_args=normalized_args,
|
|
278
|
+
suggested_next_call={"tool_name": "app_resolve", "arguments": {"profile": profile, **normalized_args}},
|
|
279
|
+
)
|
|
253
280
|
|
|
254
281
|
def app_read_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
255
|
-
|
|
282
|
+
normalized_args = {"app_key": app_key}
|
|
283
|
+
return _safe_tool_call(
|
|
284
|
+
lambda: self._facade.app_read_summary(profile=profile, app_key=app_key),
|
|
285
|
+
error_code="APP_READ_FAILED",
|
|
286
|
+
normalized_args=normalized_args,
|
|
287
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
288
|
+
)
|
|
256
289
|
|
|
257
290
|
def app_read_fields(self, *, profile: str, app_key: str) -> JSONObject:
|
|
258
|
-
|
|
291
|
+
normalized_args = {"app_key": app_key}
|
|
292
|
+
return _safe_tool_call(
|
|
293
|
+
lambda: self._facade.app_read_fields(profile=profile, app_key=app_key),
|
|
294
|
+
error_code="FIELDS_READ_FAILED",
|
|
295
|
+
normalized_args=normalized_args,
|
|
296
|
+
suggested_next_call={"tool_name": "app_read_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
297
|
+
)
|
|
259
298
|
|
|
260
299
|
def app_read_layout_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
261
|
-
|
|
300
|
+
normalized_args = {"app_key": app_key}
|
|
301
|
+
return _safe_tool_call(
|
|
302
|
+
lambda: self._facade.app_read_layout_summary(profile=profile, app_key=app_key),
|
|
303
|
+
error_code="LAYOUT_READ_FAILED",
|
|
304
|
+
normalized_args=normalized_args,
|
|
305
|
+
suggested_next_call={"tool_name": "app_read_layout_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
306
|
+
)
|
|
262
307
|
|
|
263
308
|
def app_read_views_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
264
|
-
|
|
309
|
+
normalized_args = {"app_key": app_key}
|
|
310
|
+
return _safe_tool_call(
|
|
311
|
+
lambda: self._facade.app_read_views_summary(profile=profile, app_key=app_key),
|
|
312
|
+
error_code="VIEWS_READ_FAILED",
|
|
313
|
+
normalized_args=normalized_args,
|
|
314
|
+
suggested_next_call={"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
315
|
+
)
|
|
265
316
|
|
|
266
317
|
def app_read_flow_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
267
|
-
|
|
318
|
+
normalized_args = {"app_key": app_key}
|
|
319
|
+
return _safe_tool_call(
|
|
320
|
+
lambda: self._facade.app_read_flow_summary(profile=profile, app_key=app_key),
|
|
321
|
+
error_code="FLOW_READ_FAILED",
|
|
322
|
+
normalized_args=normalized_args,
|
|
323
|
+
suggested_next_call={"tool_name": "app_read_flow_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
324
|
+
)
|
|
268
325
|
|
|
269
326
|
def app_schema_plan(
|
|
270
327
|
self,
|
|
@@ -307,7 +364,12 @@ class AiBuilderTools(ToolBase):
|
|
|
307
364
|
},
|
|
308
365
|
},
|
|
309
366
|
)
|
|
310
|
-
return
|
|
367
|
+
return _safe_tool_call(
|
|
368
|
+
lambda: self._facade.app_schema_plan(profile=profile, request=request),
|
|
369
|
+
error_code="SCHEMA_PLAN_FAILED",
|
|
370
|
+
normalized_args=request.model_dump(mode="json"),
|
|
371
|
+
suggested_next_call={"tool_name": "app_schema_plan", "arguments": {"profile": profile, **request.model_dump(mode="json")}},
|
|
372
|
+
)
|
|
311
373
|
|
|
312
374
|
def app_layout_plan(
|
|
313
375
|
self,
|
|
@@ -341,7 +403,12 @@ class AiBuilderTools(ToolBase):
|
|
|
341
403
|
},
|
|
342
404
|
},
|
|
343
405
|
)
|
|
344
|
-
return
|
|
406
|
+
return _safe_tool_call(
|
|
407
|
+
lambda: self._facade.app_layout_plan(profile=profile, request=request),
|
|
408
|
+
error_code="LAYOUT_PLAN_FAILED",
|
|
409
|
+
normalized_args=request.model_dump(mode="json"),
|
|
410
|
+
suggested_next_call={"tool_name": "app_layout_plan", "arguments": {"profile": profile, **request.model_dump(mode="json")}},
|
|
411
|
+
)
|
|
345
412
|
|
|
346
413
|
def app_flow_plan(
|
|
347
414
|
self,
|
|
@@ -378,7 +445,12 @@ class AiBuilderTools(ToolBase):
|
|
|
378
445
|
},
|
|
379
446
|
},
|
|
380
447
|
)
|
|
381
|
-
return
|
|
448
|
+
return _safe_tool_call(
|
|
449
|
+
lambda: self._facade.app_flow_plan(profile=profile, request=request),
|
|
450
|
+
error_code="FLOW_PLAN_FAILED",
|
|
451
|
+
normalized_args=request.model_dump(mode="json"),
|
|
452
|
+
suggested_next_call={"tool_name": "app_flow_plan", "arguments": {"profile": profile, **request.model_dump(mode="json")}},
|
|
453
|
+
)
|
|
382
454
|
|
|
383
455
|
def app_views_plan(
|
|
384
456
|
self,
|
|
@@ -412,7 +484,12 @@ class AiBuilderTools(ToolBase):
|
|
|
412
484
|
},
|
|
413
485
|
},
|
|
414
486
|
)
|
|
415
|
-
return
|
|
487
|
+
return _safe_tool_call(
|
|
488
|
+
lambda: self._facade.app_views_plan(profile=profile, request=request),
|
|
489
|
+
error_code="VIEWS_PLAN_FAILED",
|
|
490
|
+
normalized_args=request.model_dump(mode="json"),
|
|
491
|
+
suggested_next_call={"tool_name": "app_views_plan", "arguments": {"profile": profile, **request.model_dump(mode="json")}},
|
|
492
|
+
)
|
|
416
493
|
|
|
417
494
|
def app_schema_apply(
|
|
418
495
|
self,
|
|
@@ -450,16 +527,31 @@ class AiBuilderTools(ToolBase):
|
|
|
450
527
|
},
|
|
451
528
|
},
|
|
452
529
|
)
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
530
|
+
normalized_args = {
|
|
531
|
+
"app_key": app_key,
|
|
532
|
+
"package_tag_id": package_tag_id,
|
|
533
|
+
"app_name": effective_app_name,
|
|
534
|
+
"create_if_missing": create_if_missing,
|
|
535
|
+
"publish": publish,
|
|
536
|
+
"add_fields": [patch.model_dump(mode="json") for patch in parsed_add],
|
|
537
|
+
"update_fields": [patch.model_dump(mode="json") for patch in parsed_update],
|
|
538
|
+
"remove_fields": [patch.model_dump(mode="json") for patch in parsed_remove],
|
|
539
|
+
}
|
|
540
|
+
return _safe_tool_call(
|
|
541
|
+
lambda: self._facade.app_schema_apply(
|
|
542
|
+
profile=profile,
|
|
543
|
+
app_key=app_key,
|
|
544
|
+
package_tag_id=package_tag_id,
|
|
545
|
+
app_name=effective_app_name,
|
|
546
|
+
create_if_missing=create_if_missing,
|
|
547
|
+
publish=publish,
|
|
548
|
+
add_fields=parsed_add,
|
|
549
|
+
update_fields=parsed_update,
|
|
550
|
+
remove_fields=parsed_remove,
|
|
551
|
+
),
|
|
552
|
+
error_code="SCHEMA_APPLY_FAILED",
|
|
553
|
+
normalized_args=normalized_args,
|
|
554
|
+
suggested_next_call={"tool_name": "app_schema_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
463
555
|
)
|
|
464
556
|
|
|
465
557
|
def app_layout_apply(self, *, profile: str, app_key: str, mode: str = "merge", publish: bool = True, sections: list[JSONObject]) -> JSONObject:
|
|
@@ -495,7 +587,18 @@ class AiBuilderTools(ToolBase):
|
|
|
495
587
|
},
|
|
496
588
|
},
|
|
497
589
|
)
|
|
498
|
-
|
|
590
|
+
normalized_args = {
|
|
591
|
+
"app_key": app_key,
|
|
592
|
+
"mode": parsed_mode.value,
|
|
593
|
+
"publish": publish,
|
|
594
|
+
"sections": [section.model_dump(mode="json") for section in parsed_sections],
|
|
595
|
+
}
|
|
596
|
+
return _safe_tool_call(
|
|
597
|
+
lambda: self._facade.app_layout_apply(profile=profile, app_key=app_key, mode=parsed_mode, publish=publish, sections=parsed_sections),
|
|
598
|
+
error_code="LAYOUT_APPLY_FAILED",
|
|
599
|
+
normalized_args=normalized_args,
|
|
600
|
+
suggested_next_call={"tool_name": "app_layout_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
601
|
+
)
|
|
499
602
|
|
|
500
603
|
def app_flow_apply(
|
|
501
604
|
self,
|
|
@@ -531,13 +634,25 @@ class AiBuilderTools(ToolBase):
|
|
|
531
634
|
},
|
|
532
635
|
},
|
|
533
636
|
)
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
637
|
+
normalized_args = {
|
|
638
|
+
"app_key": request.app_key,
|
|
639
|
+
"mode": request.mode,
|
|
640
|
+
"publish": publish,
|
|
641
|
+
"nodes": [node.model_dump(mode="json") for node in request.nodes],
|
|
642
|
+
"transitions": [transition.model_dump(mode="json", by_alias=True) for transition in request.transitions],
|
|
643
|
+
}
|
|
644
|
+
return _safe_tool_call(
|
|
645
|
+
lambda: self._facade.app_flow_apply(
|
|
646
|
+
profile=profile,
|
|
647
|
+
app_key=request.app_key,
|
|
648
|
+
mode=request.mode,
|
|
649
|
+
publish=publish,
|
|
650
|
+
nodes=[node.model_dump(mode="json") for node in request.nodes],
|
|
651
|
+
transitions=[transition.model_dump(mode="json", by_alias=True) for transition in request.transitions],
|
|
652
|
+
),
|
|
653
|
+
error_code="FLOW_APPLY_FAILED",
|
|
654
|
+
normalized_args=normalized_args,
|
|
655
|
+
suggested_next_call={"tool_name": "app_flow_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
541
656
|
)
|
|
542
657
|
|
|
543
658
|
def app_views_apply(
|
|
@@ -564,10 +679,27 @@ class AiBuilderTools(ToolBase):
|
|
|
564
679
|
},
|
|
565
680
|
},
|
|
566
681
|
)
|
|
567
|
-
|
|
682
|
+
normalized_args = {
|
|
683
|
+
"app_key": app_key,
|
|
684
|
+
"publish": publish,
|
|
685
|
+
"upsert_views": [view.model_dump(mode="json") for view in parsed_views],
|
|
686
|
+
"remove_views": list(remove_views),
|
|
687
|
+
}
|
|
688
|
+
return _safe_tool_call(
|
|
689
|
+
lambda: self._facade.app_views_apply(profile=profile, app_key=app_key, publish=publish, upsert_views=parsed_views, remove_views=remove_views),
|
|
690
|
+
error_code="VIEWS_APPLY_FAILED",
|
|
691
|
+
normalized_args=normalized_args,
|
|
692
|
+
suggested_next_call={"tool_name": "app_views_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
693
|
+
)
|
|
568
694
|
|
|
569
695
|
def app_publish_verify(self, *, profile: str, app_key: str, expected_package_tag_id: int | None = None) -> JSONObject:
|
|
570
|
-
|
|
696
|
+
normalized_args = {"app_key": app_key, "expected_package_tag_id": expected_package_tag_id}
|
|
697
|
+
return _safe_tool_call(
|
|
698
|
+
lambda: self._facade.app_publish_verify(profile=profile, app_key=app_key, expected_package_tag_id=expected_package_tag_id),
|
|
699
|
+
error_code="PUBLISH_VERIFY_FAILED",
|
|
700
|
+
normalized_args=normalized_args,
|
|
701
|
+
suggested_next_call={"tool_name": "app_publish_verify", "arguments": {"profile": profile, **normalized_args}},
|
|
702
|
+
)
|
|
571
703
|
|
|
572
704
|
|
|
573
705
|
def _validation_failure(detail: str, *, suggested_next_call: JSONObject | None = None) -> JSONObject:
|
|
@@ -587,3 +719,86 @@ def _validation_failure(detail: str, *, suggested_next_call: JSONObject | None =
|
|
|
587
719
|
"noop": False,
|
|
588
720
|
"verification": {},
|
|
589
721
|
}
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def _safe_tool_call(
|
|
725
|
+
call,
|
|
726
|
+
*,
|
|
727
|
+
error_code: str,
|
|
728
|
+
normalized_args: JSONObject,
|
|
729
|
+
suggested_next_call: JSONObject | None,
|
|
730
|
+
) -> JSONObject:
|
|
731
|
+
try:
|
|
732
|
+
return call()
|
|
733
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
734
|
+
api_error = _coerce_api_error(error)
|
|
735
|
+
public_http_status = None if api_error.http_status == 404 else api_error.http_status
|
|
736
|
+
return {
|
|
737
|
+
"status": "failed",
|
|
738
|
+
"error_code": error_code,
|
|
739
|
+
"recoverable": True,
|
|
740
|
+
"message": _public_error_message(error_code, api_error),
|
|
741
|
+
"normalized_args": normalized_args,
|
|
742
|
+
"missing_fields": [],
|
|
743
|
+
"allowed_values": {},
|
|
744
|
+
"details": {
|
|
745
|
+
"transport_error": {
|
|
746
|
+
"http_status": api_error.http_status,
|
|
747
|
+
"backend_code": api_error.backend_code,
|
|
748
|
+
"category": api_error.category,
|
|
749
|
+
}
|
|
750
|
+
},
|
|
751
|
+
"suggested_next_call": suggested_next_call,
|
|
752
|
+
"request_id": api_error.request_id,
|
|
753
|
+
"backend_code": api_error.backend_code,
|
|
754
|
+
"http_status": public_http_status,
|
|
755
|
+
"noop": False,
|
|
756
|
+
"verification": {},
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def _coerce_api_error(error: Exception) -> QingflowApiError:
|
|
761
|
+
if isinstance(error, QingflowApiError):
|
|
762
|
+
return error
|
|
763
|
+
if isinstance(error, RuntimeError):
|
|
764
|
+
try:
|
|
765
|
+
payload = json.loads(str(error))
|
|
766
|
+
except json.JSONDecodeError:
|
|
767
|
+
payload = None
|
|
768
|
+
if isinstance(payload, dict) and payload.get("category") and payload.get("message"):
|
|
769
|
+
details = payload.get("details")
|
|
770
|
+
return QingflowApiError(
|
|
771
|
+
category=str(payload.get("category")),
|
|
772
|
+
message=str(payload.get("message")),
|
|
773
|
+
backend_code=payload.get("backend_code"),
|
|
774
|
+
request_id=payload.get("request_id"),
|
|
775
|
+
http_status=payload.get("http_status"),
|
|
776
|
+
details=details if isinstance(details, dict) else None,
|
|
777
|
+
)
|
|
778
|
+
return QingflowApiError(category="runtime", message=str(error))
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def _public_error_message(error_code: str, error: QingflowApiError) -> str:
|
|
782
|
+
if error.http_status != 404:
|
|
783
|
+
return error.message
|
|
784
|
+
mapping = {
|
|
785
|
+
"PACKAGE_LIST_FAILED": "package list is unavailable in the current route",
|
|
786
|
+
"PACKAGE_RESOLVE_FAILED": "package resolution is unavailable in the current route",
|
|
787
|
+
"PACKAGE_ATTACH_FAILED": "package attachment could not be verified in the current route",
|
|
788
|
+
"APP_RESOLVE_FAILED": "app resolution is unavailable in the current route",
|
|
789
|
+
"APP_READ_FAILED": "app base or schema is unavailable in the current route",
|
|
790
|
+
"FIELDS_READ_FAILED": "app fields are unavailable in the current route",
|
|
791
|
+
"LAYOUT_READ_FAILED": "layout resource is unavailable for this app in the current route",
|
|
792
|
+
"VIEWS_READ_FAILED": "views resource is unavailable for this app in the current route",
|
|
793
|
+
"FLOW_READ_FAILED": "workflow resource is unavailable for this app in the current route",
|
|
794
|
+
"SCHEMA_PLAN_FAILED": "schema planning could not load the required app state in the current route",
|
|
795
|
+
"LAYOUT_PLAN_FAILED": "layout planning could not load the required app state in the current route",
|
|
796
|
+
"FLOW_PLAN_FAILED": "flow planning could not load the required app state in the current route",
|
|
797
|
+
"VIEWS_PLAN_FAILED": "views planning could not load the required app state in the current route",
|
|
798
|
+
"SCHEMA_APPLY_FAILED": "schema apply could not complete because the app route or readback is unavailable",
|
|
799
|
+
"LAYOUT_APPLY_FAILED": "layout apply could not complete because the layout route or readback is unavailable",
|
|
800
|
+
"FLOW_APPLY_FAILED": "flow apply could not complete because the workflow route or readback is unavailable",
|
|
801
|
+
"VIEWS_APPLY_FAILED": "views apply could not complete because the views route or readback is unavailable",
|
|
802
|
+
"PUBLISH_VERIFY_FAILED": "publish verification is unavailable in the current route",
|
|
803
|
+
}
|
|
804
|
+
return mapping.get(error_code, "requested builder resource is unavailable in the current route")
|
|
@@ -237,7 +237,7 @@ class AppTools(ToolBase):
|
|
|
237
237
|
|
|
238
238
|
def runner(session_profile, context):
|
|
239
239
|
attempted_contexts = [context]
|
|
240
|
-
if context.qf_version is not None
|
|
240
|
+
if context.qf_version is not None:
|
|
241
241
|
attempted_contexts.append(
|
|
242
242
|
BackendRequestContext(
|
|
243
243
|
base_url=context.base_url,
|
|
@@ -270,7 +270,6 @@ class AppTools(ToolBase):
|
|
|
270
270
|
is_retryable_404 = (
|
|
271
271
|
error.http_status == 404
|
|
272
272
|
and call_context.qf_version is not None
|
|
273
|
-
and (context.qf_version_source or "unset") != "explicit"
|
|
274
273
|
)
|
|
275
274
|
if not is_retryable_404:
|
|
276
275
|
raise
|