@josephyan/qingflow-app-builder-mcp 0.2.0-beta.3 → 0.2.0-beta.5
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.5
|
|
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.5 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",
|
|
@@ -762,16 +768,23 @@ class AiBuilderFacade:
|
|
|
762
768
|
app_name=str(resolved["app_name"]),
|
|
763
769
|
tag_ids=_coerce_int_list(resolved.get("tag_ids")),
|
|
764
770
|
)
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
app_key=target.app_key
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
771
|
+
schema_readback_delayed = False
|
|
772
|
+
try:
|
|
773
|
+
schema_result, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=target.app_key)
|
|
774
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
775
|
+
api_error = _coerce_api_error(error)
|
|
776
|
+
if not bool(resolved.get("created")) or api_error.http_status != 404:
|
|
777
|
+
return _failed_from_api_error(
|
|
778
|
+
"SCHEMA_READBACK_FAILED",
|
|
779
|
+
api_error,
|
|
780
|
+
normalized_args=normalized_args,
|
|
781
|
+
allowed_values={"field_types": [item.value for item in PublicFieldType]},
|
|
782
|
+
details={"app_key": target.app_key},
|
|
783
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
784
|
+
)
|
|
785
|
+
schema_result = _empty_schema_result(target.app_name)
|
|
786
|
+
_schema_source = "synthetic_new_app"
|
|
787
|
+
schema_readback_delayed = True
|
|
775
788
|
parsed = _parse_schema(schema_result)
|
|
776
789
|
current_fields = parsed["fields"]
|
|
777
790
|
layout = parsed["layout"]
|
|
@@ -879,13 +892,8 @@ class AiBuilderFacade:
|
|
|
879
892
|
},
|
|
880
893
|
suggested_next_call={"tool_name": "app_read_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
881
894
|
)
|
|
882
|
-
verified = self.app_read(profile=profile, app_key=target.app_key, include_raw=False)
|
|
883
|
-
verified_field_names = {field["name"] for field in verified["schema"]["fields"]}
|
|
884
|
-
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"))
|
|
885
|
-
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)
|
|
886
|
-
package_attached = None if package_tag_id is None else package_tag_id in tag_ids_after
|
|
887
895
|
response = {
|
|
888
|
-
"status": "success"
|
|
896
|
+
"status": "success",
|
|
889
897
|
"error_code": None,
|
|
890
898
|
"recoverable": False,
|
|
891
899
|
"message": "applied schema patch",
|
|
@@ -894,21 +902,11 @@ class AiBuilderFacade:
|
|
|
894
902
|
"allowed_values": {"field_types": [item.value for item in PublicFieldType]},
|
|
895
903
|
"details": {},
|
|
896
904
|
"request_id": None,
|
|
897
|
-
"suggested_next_call": None
|
|
898
|
-
if package_attached is not False
|
|
899
|
-
else {
|
|
900
|
-
"tool_name": "package_attach_app",
|
|
901
|
-
"arguments": {
|
|
902
|
-
"profile": profile,
|
|
903
|
-
"tag_id": package_tag_id,
|
|
904
|
-
"app_key": target.app_key,
|
|
905
|
-
"app_title": app_name or target.app_name,
|
|
906
|
-
},
|
|
907
|
-
},
|
|
905
|
+
"suggested_next_call": None,
|
|
908
906
|
"noop": False,
|
|
909
907
|
"verification": {
|
|
910
|
-
"fields_verified":
|
|
911
|
-
"package_attached":
|
|
908
|
+
"fields_verified": False,
|
|
909
|
+
"package_attached": None,
|
|
912
910
|
},
|
|
913
911
|
"app_key": target.app_key,
|
|
914
912
|
"created": bool(resolved.get("created")),
|
|
@@ -917,11 +915,71 @@ class AiBuilderFacade:
|
|
|
917
915
|
"updated": updated,
|
|
918
916
|
"removed": removed,
|
|
919
917
|
},
|
|
920
|
-
"verified":
|
|
921
|
-
"tag_ids_after":
|
|
922
|
-
"package_attached":
|
|
918
|
+
"verified": False,
|
|
919
|
+
"tag_ids_after": [],
|
|
920
|
+
"package_attached": None,
|
|
923
921
|
}
|
|
924
|
-
|
|
922
|
+
if schema_readback_delayed:
|
|
923
|
+
response["verification"]["schema_readback_delayed"] = True
|
|
924
|
+
response = self._append_publish_result(profile=profile, app_key=target.app_key, publish=publish, response=response)
|
|
925
|
+
verification_ok = False
|
|
926
|
+
tag_ids_after: list[int] = []
|
|
927
|
+
package_attached: bool | None = None
|
|
928
|
+
verification_error: QingflowApiError | None = None
|
|
929
|
+
try:
|
|
930
|
+
verified = self.app_read(profile=profile, app_key=target.app_key, include_raw=False)
|
|
931
|
+
verified_field_names = {field["name"] for field in verified["schema"]["fields"]}
|
|
932
|
+
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)
|
|
933
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
934
|
+
verification_error = _coerce_api_error(error)
|
|
935
|
+
verification_ok = False
|
|
936
|
+
try:
|
|
937
|
+
base_info = self.apps.app_get_base(profile=profile, app_key=target.app_key, include_raw=True).get("result") or {}
|
|
938
|
+
tag_ids_after = _coerce_int_list(base_info.get("tagIds"))
|
|
939
|
+
package_attached = None if package_tag_id is None else package_tag_id in tag_ids_after
|
|
940
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
941
|
+
base_error = _coerce_api_error(error)
|
|
942
|
+
if verification_error is None:
|
|
943
|
+
verification_error = base_error
|
|
944
|
+
tag_ids_after = []
|
|
945
|
+
package_attached = None if package_tag_id is None else False
|
|
946
|
+
response["verification"]["fields_verified"] = verification_ok
|
|
947
|
+
response["verification"]["package_attached"] = package_attached
|
|
948
|
+
response["verified"] = verification_ok
|
|
949
|
+
response["tag_ids_after"] = tag_ids_after
|
|
950
|
+
response["package_attached"] = package_attached
|
|
951
|
+
if package_attached is False:
|
|
952
|
+
response["suggested_next_call"] = {
|
|
953
|
+
"tool_name": "package_attach_app",
|
|
954
|
+
"arguments": {
|
|
955
|
+
"profile": profile,
|
|
956
|
+
"tag_id": package_tag_id,
|
|
957
|
+
"app_key": target.app_key,
|
|
958
|
+
"app_title": app_name or target.app_name,
|
|
959
|
+
},
|
|
960
|
+
}
|
|
961
|
+
publish_failed = bool(response.get("publish_requested")) and not bool(response.get("published"))
|
|
962
|
+
if verification_ok and package_attached is not False and not publish_failed:
|
|
963
|
+
response["status"] = "success"
|
|
964
|
+
else:
|
|
965
|
+
response["status"] = "partial_success"
|
|
966
|
+
if verification_error is not None:
|
|
967
|
+
response["recoverable"] = True
|
|
968
|
+
response["error_code"] = response.get("error_code") or (
|
|
969
|
+
"READBACK_PENDING" if verification_error.http_status == 404 else "READBACK_FAILED"
|
|
970
|
+
)
|
|
971
|
+
response["message"] = f"{response.get('message') or 'apply succeeded'}; readback pending"
|
|
972
|
+
response["request_id"] = response.get("request_id") or verification_error.request_id
|
|
973
|
+
details = response.get("details")
|
|
974
|
+
if not isinstance(details, dict):
|
|
975
|
+
details = {}
|
|
976
|
+
response["details"] = details
|
|
977
|
+
details["verification_error"] = {
|
|
978
|
+
"message": verification_error.message,
|
|
979
|
+
"http_status": verification_error.http_status,
|
|
980
|
+
"backend_code": verification_error.backend_code,
|
|
981
|
+
}
|
|
982
|
+
return response
|
|
925
983
|
|
|
926
984
|
def app_layout_apply(
|
|
927
985
|
self,
|
|
@@ -938,16 +996,7 @@ class AiBuilderFacade:
|
|
|
938
996
|
"sections": [section.model_dump(mode="json") for section in sections],
|
|
939
997
|
"publish": publish,
|
|
940
998
|
}
|
|
941
|
-
|
|
942
|
-
profile=profile,
|
|
943
|
-
app_key=app_key,
|
|
944
|
-
form_type=1,
|
|
945
|
-
being_draft=True,
|
|
946
|
-
being_apply=None,
|
|
947
|
-
audit_node_id=None,
|
|
948
|
-
include_raw=True,
|
|
949
|
-
)
|
|
950
|
-
schema_result = schema.get("result") if isinstance(schema.get("result"), dict) else {}
|
|
999
|
+
schema_result, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
951
1000
|
parsed = _parse_schema(schema_result)
|
|
952
1001
|
current_fields = parsed["fields"]
|
|
953
1002
|
fields_by_name = {field["name"]: field for field in current_fields}
|
|
@@ -1130,15 +1179,7 @@ class AiBuilderFacade:
|
|
|
1130
1179
|
suggested_next_call={"tool_name": "app_flow_plan", "arguments": {"profile": profile, **normalized_args}},
|
|
1131
1180
|
)
|
|
1132
1181
|
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
1133
|
-
schema = self.
|
|
1134
|
-
profile=profile,
|
|
1135
|
-
app_key=app_key,
|
|
1136
|
-
form_type=1,
|
|
1137
|
-
being_draft=True,
|
|
1138
|
-
being_apply=None,
|
|
1139
|
-
audit_node_id=None,
|
|
1140
|
-
include_raw=True,
|
|
1141
|
-
).get("result") or {}
|
|
1182
|
+
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
1142
1183
|
entity = _entity_spec_from_app(base_info=base, schema=schema, views=None)
|
|
1143
1184
|
workflow_spec = _build_public_workflow_spec(nodes=nodes, transitions=transitions)
|
|
1144
1185
|
if workflow_spec.get("status") == "failed":
|
|
@@ -1246,15 +1287,7 @@ class AiBuilderFacade:
|
|
|
1246
1287
|
}
|
|
1247
1288
|
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
1248
1289
|
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
1249
|
-
schema = self.
|
|
1250
|
-
profile=profile,
|
|
1251
|
-
app_key=app_key,
|
|
1252
|
-
form_type=1,
|
|
1253
|
-
being_draft=True,
|
|
1254
|
-
being_apply=None,
|
|
1255
|
-
audit_node_id=None,
|
|
1256
|
-
include_raw=True,
|
|
1257
|
-
).get("result") or {}
|
|
1290
|
+
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
1258
1291
|
existing_views = self.views.view_list_flat(profile=profile, app_key=app_key).get("result") or []
|
|
1259
1292
|
existing_by_name = {}
|
|
1260
1293
|
for view in existing_views if isinstance(existing_views, list) else []:
|
|
@@ -1527,27 +1560,49 @@ class AiBuilderFacade:
|
|
|
1527
1560
|
|
|
1528
1561
|
def _load_app_state(self, *, profile: str, app_key: str) -> dict[str, Any]:
|
|
1529
1562
|
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True)
|
|
1530
|
-
|
|
1531
|
-
profile=profile,
|
|
1532
|
-
app_key=app_key,
|
|
1533
|
-
form_type=1,
|
|
1534
|
-
being_draft=True,
|
|
1535
|
-
being_apply=None,
|
|
1536
|
-
audit_node_id=None,
|
|
1537
|
-
include_raw=True,
|
|
1538
|
-
)
|
|
1563
|
+
schema_result, schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
1539
1564
|
views = self.views.view_list_flat(profile=profile, app_key=app_key)
|
|
1540
1565
|
workflow = self.workflows.workflow_list_nodes(profile=profile, app_key=app_key)
|
|
1541
1566
|
base_result = base.get("result") if isinstance(base.get("result"), dict) else {}
|
|
1542
|
-
schema_result = schema.get("result") if isinstance(schema.get("result"), dict) else {}
|
|
1543
1567
|
return {
|
|
1544
1568
|
"base": base_result,
|
|
1545
1569
|
"schema": schema_result,
|
|
1546
1570
|
"parsed": _parse_schema(schema_result),
|
|
1547
1571
|
"views": views.get("result"),
|
|
1548
1572
|
"workflow": workflow.get("result"),
|
|
1573
|
+
"schema_source": schema_source,
|
|
1549
1574
|
}
|
|
1550
1575
|
|
|
1576
|
+
def _read_schema_with_fallback(self, *, profile: str, app_key: str) -> tuple[dict[str, Any], str]:
|
|
1577
|
+
attempts = (
|
|
1578
|
+
("draft", True),
|
|
1579
|
+
("current", None),
|
|
1580
|
+
("published", False),
|
|
1581
|
+
)
|
|
1582
|
+
last_error: Exception | None = None
|
|
1583
|
+
for label, being_draft in attempts:
|
|
1584
|
+
try:
|
|
1585
|
+
schema = self.apps.app_get_form_schema(
|
|
1586
|
+
profile=profile,
|
|
1587
|
+
app_key=app_key,
|
|
1588
|
+
form_type=1,
|
|
1589
|
+
being_draft=being_draft,
|
|
1590
|
+
being_apply=None,
|
|
1591
|
+
audit_node_id=None,
|
|
1592
|
+
include_raw=True,
|
|
1593
|
+
)
|
|
1594
|
+
result = schema.get("result")
|
|
1595
|
+
return (result if isinstance(result, dict) else {}), label
|
|
1596
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1597
|
+
api_error = _coerce_api_error(error)
|
|
1598
|
+
last_error = error
|
|
1599
|
+
if api_error.http_status == 404:
|
|
1600
|
+
continue
|
|
1601
|
+
raise
|
|
1602
|
+
if last_error is not None:
|
|
1603
|
+
raise last_error
|
|
1604
|
+
return {}, "unknown"
|
|
1605
|
+
|
|
1551
1606
|
def _preview_target_app(
|
|
1552
1607
|
self,
|
|
1553
1608
|
*,
|
|
@@ -1635,7 +1690,28 @@ class AiBuilderFacade:
|
|
|
1635
1690
|
new_app_key = str(result.get("appKey") or (result.get("appKeys")[0] if isinstance(result.get("appKeys"), list) and result.get("appKeys") else ""))
|
|
1636
1691
|
if not new_app_key:
|
|
1637
1692
|
return _failed("APP_CREATE_FAILED", "failed to create app shell", details={"result": result}, suggested_next_call=None)
|
|
1638
|
-
|
|
1693
|
+
try:
|
|
1694
|
+
base = self.apps.app_get_base(profile=profile, app_key=new_app_key, include_raw=True).get("result") or {}
|
|
1695
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1696
|
+
api_error = _coerce_api_error(error)
|
|
1697
|
+
if api_error.http_status != 404:
|
|
1698
|
+
return _failed_from_api_error(
|
|
1699
|
+
"APP_CREATE_READBACK_FAILED",
|
|
1700
|
+
api_error,
|
|
1701
|
+
details={"app_key": new_app_key, "app_name": app_name, "package_tag_id": package_tag_id},
|
|
1702
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": new_app_key}},
|
|
1703
|
+
)
|
|
1704
|
+
return {
|
|
1705
|
+
"status": "success",
|
|
1706
|
+
"error_code": None,
|
|
1707
|
+
"recoverable": False,
|
|
1708
|
+
"message": "created app; base readback pending",
|
|
1709
|
+
"suggested_next_call": None,
|
|
1710
|
+
"app_key": new_app_key,
|
|
1711
|
+
"app_name": app_name or "未命名应用",
|
|
1712
|
+
"tag_ids": [],
|
|
1713
|
+
"created": True,
|
|
1714
|
+
}
|
|
1639
1715
|
return {
|
|
1640
1716
|
"status": "success",
|
|
1641
1717
|
"error_code": None,
|
|
@@ -1807,6 +1883,15 @@ def _coerce_int_list(values: Any) -> list[int]:
|
|
|
1807
1883
|
return result
|
|
1808
1884
|
|
|
1809
1885
|
|
|
1886
|
+
def _empty_schema_result(title: str) -> dict[str, Any]:
|
|
1887
|
+
return {
|
|
1888
|
+
"formTitle": title,
|
|
1889
|
+
"editVersionNo": 1,
|
|
1890
|
+
"formQues": [],
|
|
1891
|
+
"questionRelations": [],
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
|
|
1810
1895
|
def _slugify(text: str, *, default: str) -> str:
|
|
1811
1896
|
normalized = "".join(ch.lower() if ch.isalnum() else "_" for ch in str(text or ""))
|
|
1812
1897
|
collapsed = "_".join(part for part in normalized.split("_") if part)
|
|
@@ -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
|