@josephyan/qingflow-app-builder-mcp 0.2.0-beta.7 → 0.2.0-beta.9
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/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/builder_facade/models.py +2 -0
- package/src/qingflow_mcp/builder_facade/service.py +58 -8
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +1 -1
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +9 -2
- package/src/qingflow_mcp/solution/executor.py +34 -7
- package/src/qingflow_mcp/tools/ai_builder_tools.py +128 -6
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.9
|
|
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.9 qingflow-app-builder-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -332,6 +332,8 @@ def _normalize_field_payload(value: Any) -> Any:
|
|
|
332
332
|
if not isinstance(value, dict):
|
|
333
333
|
return value
|
|
334
334
|
payload = dict(value)
|
|
335
|
+
if "fields" in payload and "subfields" not in payload:
|
|
336
|
+
payload["subfields"] = payload.pop("fields")
|
|
335
337
|
raw_type = payload.get("type")
|
|
336
338
|
if isinstance(raw_type, str):
|
|
337
339
|
normalized = FIELD_TYPE_ALIASES.get(raw_type.strip().lower())
|
|
@@ -233,8 +233,9 @@ class AiBuilderFacade:
|
|
|
233
233
|
recoverable=False,
|
|
234
234
|
suggested_next_call={"tool_name": "auth_whoami", "arguments": {"profile": profile}},
|
|
235
235
|
)
|
|
236
|
-
|
|
237
|
-
|
|
236
|
+
identity = self._resolve_current_user_identity(profile=profile)
|
|
237
|
+
current_email = str(identity.get("email") or "").strip().lower()
|
|
238
|
+
current_name = str(identity.get("nick_name") or "").strip()
|
|
238
239
|
requested_owner_email = str(lock_owner_email or "").strip().lower()
|
|
239
240
|
requested_owner_name = str(lock_owner_name or "").strip()
|
|
240
241
|
if not requested_owner_email and not requested_owner_name:
|
|
@@ -244,8 +245,8 @@ class AiBuilderFacade:
|
|
|
244
245
|
normalized_args=normalized_args,
|
|
245
246
|
recoverable=False,
|
|
246
247
|
details={
|
|
247
|
-
"current_user_email":
|
|
248
|
-
"current_user_name":
|
|
248
|
+
"current_user_email": identity.get("email"),
|
|
249
|
+
"current_user_name": identity.get("nick_name"),
|
|
249
250
|
},
|
|
250
251
|
suggested_next_call=None,
|
|
251
252
|
)
|
|
@@ -263,8 +264,8 @@ class AiBuilderFacade:
|
|
|
263
264
|
details={
|
|
264
265
|
"lock_owner_email": requested_owner_email or None,
|
|
265
266
|
"lock_owner_name": requested_owner_name or None,
|
|
266
|
-
"current_user_email":
|
|
267
|
-
"current_user_name":
|
|
267
|
+
"current_user_email": identity.get("email"),
|
|
268
|
+
"current_user_name": identity.get("nick_name"),
|
|
268
269
|
},
|
|
269
270
|
suggested_next_call=None,
|
|
270
271
|
)
|
|
@@ -307,8 +308,8 @@ class AiBuilderFacade:
|
|
|
307
308
|
"details": {
|
|
308
309
|
"lock_owner_email": requested_owner_email or None,
|
|
309
310
|
"lock_owner_name": requested_owner_name or None,
|
|
310
|
-
"current_user_email":
|
|
311
|
-
"current_user_name":
|
|
311
|
+
"current_user_email": identity.get("email"),
|
|
312
|
+
"current_user_name": identity.get("nick_name"),
|
|
312
313
|
"edit_version_no": edit_version_no,
|
|
313
314
|
},
|
|
314
315
|
"request_id": None,
|
|
@@ -1057,6 +1058,11 @@ class AiBuilderFacade:
|
|
|
1057
1058
|
fields=current_fields,
|
|
1058
1059
|
layout=layout,
|
|
1059
1060
|
)
|
|
1061
|
+
payload["editVersionNo"] = self._resolve_form_edit_version(
|
|
1062
|
+
profile=profile,
|
|
1063
|
+
app_key=target.app_key,
|
|
1064
|
+
current_schema=schema_result,
|
|
1065
|
+
)
|
|
1060
1066
|
try:
|
|
1061
1067
|
self.apps.app_update_form_schema(profile=profile, app_key=target.app_key, payload=payload)
|
|
1062
1068
|
except (QingflowApiError, RuntimeError) as error:
|
|
@@ -1272,6 +1278,11 @@ class AiBuilderFacade:
|
|
|
1272
1278
|
current_schema=schema_result,
|
|
1273
1279
|
layout=target_layout,
|
|
1274
1280
|
)
|
|
1281
|
+
payload["editVersionNo"] = self._resolve_form_edit_version(
|
|
1282
|
+
profile=profile,
|
|
1283
|
+
app_key=app_key,
|
|
1284
|
+
current_schema=schema_result,
|
|
1285
|
+
)
|
|
1275
1286
|
applied_layout = target_layout
|
|
1276
1287
|
fallback_applied = None
|
|
1277
1288
|
try:
|
|
@@ -1284,6 +1295,11 @@ class AiBuilderFacade:
|
|
|
1284
1295
|
current_schema=schema_result,
|
|
1285
1296
|
layout=flattened_layout,
|
|
1286
1297
|
)
|
|
1298
|
+
fallback_payload["editVersionNo"] = self._resolve_form_edit_version(
|
|
1299
|
+
profile=profile,
|
|
1300
|
+
app_key=app_key,
|
|
1301
|
+
current_schema=schema_result,
|
|
1302
|
+
)
|
|
1287
1303
|
try:
|
|
1288
1304
|
self.apps.app_update_form_schema(profile=profile, app_key=app_key, payload=fallback_payload)
|
|
1289
1305
|
applied_layout = flattened_layout
|
|
@@ -1874,6 +1890,13 @@ class AiBuilderFacade:
|
|
|
1874
1890
|
"published": True,
|
|
1875
1891
|
}
|
|
1876
1892
|
|
|
1893
|
+
def _resolve_form_edit_version(self, *, profile: str, app_key: str, current_schema: dict[str, Any]) -> int:
|
|
1894
|
+
try:
|
|
1895
|
+
version_result = self.apps.app_get_edit_version_no(profile=profile, app_key=app_key).get("result") or {}
|
|
1896
|
+
except (QingflowApiError, RuntimeError):
|
|
1897
|
+
version_result = {}
|
|
1898
|
+
return _coerce_positive_int(version_result.get("editVersionNo") or version_result.get("versionNo")) or int(current_schema.get("editVersionNo") or 1)
|
|
1899
|
+
|
|
1877
1900
|
def _append_publish_result(self, *, profile: str, app_key: str, publish: bool, response: JSONObject) -> JSONObject:
|
|
1878
1901
|
response["publish_requested"] = publish
|
|
1879
1902
|
if not publish:
|
|
@@ -2118,6 +2141,33 @@ class AiBuilderFacade:
|
|
|
2118
2141
|
payload.setdefault("ws_id", session_profile.selected_ws_id)
|
|
2119
2142
|
return payload
|
|
2120
2143
|
|
|
2144
|
+
def _resolve_current_user_identity(self, *, profile: str) -> JSONObject:
|
|
2145
|
+
session_profile = self.apps.sessions.get_profile(profile)
|
|
2146
|
+
backend_session = self.apps.sessions.get_backend_session(profile)
|
|
2147
|
+
current_email = str((session_profile.email if session_profile else None) or "").strip()
|
|
2148
|
+
current_name = str((session_profile.nick_name if session_profile else None) or "").strip()
|
|
2149
|
+
if current_email or current_name or session_profile is None or backend_session is None:
|
|
2150
|
+
return {"email": current_email or None, "nick_name": current_name or None}
|
|
2151
|
+
try:
|
|
2152
|
+
user_info = self.apps.backend.request(
|
|
2153
|
+
"GET",
|
|
2154
|
+
BackendRequestContext(
|
|
2155
|
+
base_url=backend_session.base_url,
|
|
2156
|
+
token=backend_session.token,
|
|
2157
|
+
ws_id=session_profile.selected_ws_id,
|
|
2158
|
+
qf_version=backend_session.qf_version,
|
|
2159
|
+
qf_version_source=backend_session.qf_version_source,
|
|
2160
|
+
),
|
|
2161
|
+
"/user",
|
|
2162
|
+
)
|
|
2163
|
+
except (QingflowApiError, RuntimeError):
|
|
2164
|
+
return {"email": current_email or None, "nick_name": current_name or None}
|
|
2165
|
+
if not isinstance(user_info, dict):
|
|
2166
|
+
return {"email": current_email or None, "nick_name": current_name or None}
|
|
2167
|
+
resolved_email = str(user_info.get("email") or "").strip() or None
|
|
2168
|
+
resolved_name = str(user_info.get("nickName") or user_info.get("displayName") or user_info.get("name") or "").strip() or None
|
|
2169
|
+
return {"email": resolved_email, "nick_name": resolved_name}
|
|
2170
|
+
|
|
2121
2171
|
def _attach_app_to_package(self, *, profile: str, app_key: str, app_title: str, package_tag_id: int) -> None:
|
|
2122
2172
|
detail = self.packages.package_get(profile=profile, tag_id=package_tag_id, include_raw=True)
|
|
2123
2173
|
result = detail.get("result") if isinstance(detail.get("result"), dict) else {}
|
|
@@ -249,7 +249,7 @@ def build_question(field: dict[str, Any], temp_id: int) -> tuple[dict[str, Any],
|
|
|
249
249
|
sub_question, next_temp_id = build_question(subfield, next_temp_id)
|
|
250
250
|
sub_questions.append(sub_question)
|
|
251
251
|
question["subQuestions"] = sub_questions
|
|
252
|
-
question["innerQuestions"] = deepcopy(sub_questions)
|
|
252
|
+
question["innerQuestions"] = [deepcopy(sub_questions)]
|
|
253
253
|
question["queDefaultValues"] = {"queId": temp_id, "queTitle": field["label"], "queType": que_type, "values": [], "tableValues": []}
|
|
254
254
|
return question, next_temp_id
|
|
255
255
|
|
|
@@ -36,6 +36,11 @@ def compile_workflow(entity: EntitySpec) -> dict[str, Any] | None:
|
|
|
36
36
|
actions: list[dict[str, Any]] = []
|
|
37
37
|
seen_node_ids: set[str] = set()
|
|
38
38
|
created_extra_branch_lanes: set[str] = set()
|
|
39
|
+
start_node_ids = {
|
|
40
|
+
node.node_id
|
|
41
|
+
for node in workflow.nodes
|
|
42
|
+
if node.node_type == WorkflowNodeType.start
|
|
43
|
+
}
|
|
39
44
|
for node in workflow.nodes:
|
|
40
45
|
if node.node_type == WorkflowNodeType.start:
|
|
41
46
|
seen_node_ids.add(node.node_id)
|
|
@@ -69,7 +74,7 @@ def compile_workflow(entity: EntitySpec) -> dict[str, Any] | None:
|
|
|
69
74
|
"auditNodeName": node.name,
|
|
70
75
|
"type": WORKFLOW_TYPE_MAP[node.node_type]["type"],
|
|
71
76
|
"dealType": WORKFLOW_TYPE_MAP[node.node_type]["dealType"],
|
|
72
|
-
"prevNodeRef": _prev_node_ref(node, branch_lane_ref),
|
|
77
|
+
"prevNodeRef": _prev_node_ref(node, branch_lane_ref, start_node_ids),
|
|
73
78
|
"auditUserInfos": _build_audit_user_infos(node)
|
|
74
79
|
if node.node_type in {WorkflowNodeType.audit, WorkflowNodeType.fill, WorkflowNodeType.copy}
|
|
75
80
|
else None,
|
|
@@ -106,11 +111,13 @@ def _build_audit_user_infos(node) -> dict[str, Any]:
|
|
|
106
111
|
return audit_user_infos
|
|
107
112
|
|
|
108
113
|
|
|
109
|
-
def _prev_node_ref(node, branch_lane_ref: str | None) -> str:
|
|
114
|
+
def _prev_node_ref(node, branch_lane_ref: str | None, start_node_ids: set[str]) -> str:
|
|
110
115
|
if branch_lane_ref:
|
|
111
116
|
if node.parent_node_id and node.parent_node_id != node.branch_parent_id:
|
|
112
117
|
return node.parent_node_id
|
|
113
118
|
return branch_lane_ref
|
|
119
|
+
if node.parent_node_id in start_node_ids:
|
|
120
|
+
return "__applicant__"
|
|
114
121
|
return node.parent_node_id or "__applicant__"
|
|
115
122
|
|
|
116
123
|
|
|
@@ -415,13 +415,13 @@ class SolutionExecutor:
|
|
|
415
415
|
current_nodes = _coerce_workflow_nodes(existing_nodes)
|
|
416
416
|
existing_nodes_by_name = {
|
|
417
417
|
node.get("auditNodeName"): int(node_id)
|
|
418
|
-
for node_id, node in
|
|
418
|
+
for node_id, node in current_nodes.items()
|
|
419
419
|
if isinstance(node, dict) and node.get("auditNodeName")
|
|
420
420
|
}
|
|
421
421
|
applicant_node_id = next(
|
|
422
422
|
(
|
|
423
423
|
int(node_id)
|
|
424
|
-
for node_id, node in
|
|
424
|
+
for node_id, node in current_nodes.items()
|
|
425
425
|
if isinstance(node, dict) and node.get("type") == 0 and node.get("dealType") == 3
|
|
426
426
|
),
|
|
427
427
|
None,
|
|
@@ -429,11 +429,24 @@ class SolutionExecutor:
|
|
|
429
429
|
if applicant_node_id is not None:
|
|
430
430
|
node_artifacts.setdefault("__applicant__", applicant_node_id)
|
|
431
431
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
432
|
+
desired_global_settings = deepcopy(entity.workflow_plan["global_settings"])
|
|
433
|
+
explicit_global_settings = _has_explicit_workflow_global_settings(desired_global_settings)
|
|
434
|
+
current_global_settings: dict[str, Any] = {}
|
|
435
|
+
if explicit_global_settings:
|
|
436
|
+
current_global_settings = self.workflow_tools.workflow_get_global_settings(profile=profile, app_key=app_key).get("result") or {}
|
|
437
|
+
else:
|
|
438
|
+
try:
|
|
439
|
+
current_global_settings = self.workflow_tools.workflow_get_global_settings(profile=profile, app_key=app_key).get("result") or {}
|
|
440
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
441
|
+
api_error = QingflowApiError(**_coerce_nested_error_payload(error))
|
|
442
|
+
if api_error.http_status != 404:
|
|
443
|
+
raise
|
|
444
|
+
current_global_settings = {}
|
|
445
|
+
if explicit_global_settings:
|
|
446
|
+
global_settings = deepcopy(current_global_settings if isinstance(current_global_settings, dict) else {})
|
|
447
|
+
global_settings.update(desired_global_settings)
|
|
448
|
+
global_settings["editVersionNo"] = workflow_edit_version_no or global_settings.get("editVersionNo") or 1
|
|
449
|
+
self.workflow_tools.workflow_update_global_settings(profile=profile, app_key=app_key, payload=global_settings)
|
|
437
450
|
for action in entity.workflow_plan["actions"]:
|
|
438
451
|
if action["action"] == "create_sub_branch" and node_artifacts.get(action["node_id"]) is not None:
|
|
439
452
|
continue
|
|
@@ -2046,6 +2059,20 @@ def _find_created_sub_branch_lane_id(
|
|
|
2046
2059
|
return candidates[0] if candidates else None
|
|
2047
2060
|
|
|
2048
2061
|
|
|
2062
|
+
def _has_explicit_workflow_global_settings(global_settings: dict[str, Any] | None) -> bool:
|
|
2063
|
+
if not isinstance(global_settings, dict):
|
|
2064
|
+
return False
|
|
2065
|
+
for key, value in global_settings.items():
|
|
2066
|
+
if key == "editVersionNo":
|
|
2067
|
+
continue
|
|
2068
|
+
if value is None:
|
|
2069
|
+
continue
|
|
2070
|
+
if isinstance(value, (list, dict)) and not value:
|
|
2071
|
+
continue
|
|
2072
|
+
return True
|
|
2073
|
+
return False
|
|
2074
|
+
|
|
2075
|
+
|
|
2049
2076
|
def _is_navigation_plugin_unavailable(error: QingflowApiError) -> bool:
|
|
2050
2077
|
try:
|
|
2051
2078
|
backend_code = int(error.backend_code)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import time
|
|
4
5
|
|
|
5
6
|
from pydantic import ValidationError
|
|
6
7
|
|
|
@@ -276,12 +277,22 @@ class AiBuilderTools(ToolBase):
|
|
|
276
277
|
|
|
277
278
|
def package_attach_app(self, *, profile: str, tag_id: int, app_key: str, app_title: str = "") -> JSONObject:
|
|
278
279
|
normalized_args = {"tag_id": tag_id, "app_key": app_key, "app_title": app_title}
|
|
279
|
-
|
|
280
|
+
result = _safe_tool_call(
|
|
280
281
|
lambda: self._facade.package_attach_app(profile=profile, tag_id=tag_id, app_key=app_key, app_title=app_title),
|
|
281
282
|
error_code="PACKAGE_ATTACH_FAILED",
|
|
282
283
|
normalized_args=normalized_args,
|
|
283
284
|
suggested_next_call={"tool_name": "package_attach_app", "arguments": {"profile": profile, **normalized_args}},
|
|
284
285
|
)
|
|
286
|
+
return self._retry_after_self_lock_release(
|
|
287
|
+
profile=profile,
|
|
288
|
+
result=result,
|
|
289
|
+
retry_call=lambda: self._facade.package_attach_app(
|
|
290
|
+
profile=profile,
|
|
291
|
+
tag_id=tag_id,
|
|
292
|
+
app_key=app_key,
|
|
293
|
+
app_title=app_title,
|
|
294
|
+
),
|
|
295
|
+
)
|
|
285
296
|
|
|
286
297
|
def app_release_edit_lock_if_mine(
|
|
287
298
|
self,
|
|
@@ -576,7 +587,7 @@ class AiBuilderTools(ToolBase):
|
|
|
576
587
|
"update_fields": [patch.model_dump(mode="json") for patch in parsed_update],
|
|
577
588
|
"remove_fields": [patch.model_dump(mode="json") for patch in parsed_remove],
|
|
578
589
|
}
|
|
579
|
-
|
|
590
|
+
result = _safe_tool_call(
|
|
580
591
|
lambda: self._facade.app_schema_apply(
|
|
581
592
|
profile=profile,
|
|
582
593
|
app_key=app_key,
|
|
@@ -592,6 +603,17 @@ class AiBuilderTools(ToolBase):
|
|
|
592
603
|
normalized_args=normalized_args,
|
|
593
604
|
suggested_next_call={"tool_name": "app_schema_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
594
605
|
)
|
|
606
|
+
return self._retry_after_self_lock_release(profile=profile, result=result, retry_call=lambda: self._facade.app_schema_apply(
|
|
607
|
+
profile=profile,
|
|
608
|
+
app_key=app_key,
|
|
609
|
+
package_tag_id=package_tag_id,
|
|
610
|
+
app_name=effective_app_name,
|
|
611
|
+
create_if_missing=create_if_missing,
|
|
612
|
+
publish=publish,
|
|
613
|
+
add_fields=parsed_add,
|
|
614
|
+
update_fields=parsed_update,
|
|
615
|
+
remove_fields=parsed_remove,
|
|
616
|
+
))
|
|
595
617
|
|
|
596
618
|
def app_layout_apply(self, *, profile: str, app_key: str, mode: str = "merge", publish: bool = True, sections: list[JSONObject]) -> JSONObject:
|
|
597
619
|
try:
|
|
@@ -632,12 +654,23 @@ class AiBuilderTools(ToolBase):
|
|
|
632
654
|
"publish": publish,
|
|
633
655
|
"sections": [section.model_dump(mode="json") for section in parsed_sections],
|
|
634
656
|
}
|
|
635
|
-
|
|
657
|
+
result = _safe_tool_call(
|
|
636
658
|
lambda: self._facade.app_layout_apply(profile=profile, app_key=app_key, mode=parsed_mode, publish=publish, sections=parsed_sections),
|
|
637
659
|
error_code="LAYOUT_APPLY_FAILED",
|
|
638
660
|
normalized_args=normalized_args,
|
|
639
661
|
suggested_next_call={"tool_name": "app_layout_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
640
662
|
)
|
|
663
|
+
return self._retry_after_self_lock_release(
|
|
664
|
+
profile=profile,
|
|
665
|
+
result=result,
|
|
666
|
+
retry_call=lambda: self._facade.app_layout_apply(
|
|
667
|
+
profile=profile,
|
|
668
|
+
app_key=app_key,
|
|
669
|
+
mode=parsed_mode,
|
|
670
|
+
publish=publish,
|
|
671
|
+
sections=parsed_sections,
|
|
672
|
+
),
|
|
673
|
+
)
|
|
641
674
|
|
|
642
675
|
def app_flow_apply(
|
|
643
676
|
self,
|
|
@@ -680,7 +713,7 @@ class AiBuilderTools(ToolBase):
|
|
|
680
713
|
"nodes": [node.model_dump(mode="json") for node in request.nodes],
|
|
681
714
|
"transitions": [transition.model_dump(mode="json", by_alias=True) for transition in request.transitions],
|
|
682
715
|
}
|
|
683
|
-
|
|
716
|
+
result = _safe_tool_call(
|
|
684
717
|
lambda: self._facade.app_flow_apply(
|
|
685
718
|
profile=profile,
|
|
686
719
|
app_key=request.app_key,
|
|
@@ -693,6 +726,18 @@ class AiBuilderTools(ToolBase):
|
|
|
693
726
|
normalized_args=normalized_args,
|
|
694
727
|
suggested_next_call={"tool_name": "app_flow_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
695
728
|
)
|
|
729
|
+
return self._retry_after_self_lock_release(
|
|
730
|
+
profile=profile,
|
|
731
|
+
result=result,
|
|
732
|
+
retry_call=lambda: self._facade.app_flow_apply(
|
|
733
|
+
profile=profile,
|
|
734
|
+
app_key=request.app_key,
|
|
735
|
+
mode=request.mode,
|
|
736
|
+
publish=publish,
|
|
737
|
+
nodes=[node.model_dump(mode="json") for node in request.nodes],
|
|
738
|
+
transitions=[transition.model_dump(mode="json", by_alias=True) for transition in request.transitions],
|
|
739
|
+
),
|
|
740
|
+
)
|
|
696
741
|
|
|
697
742
|
def app_views_apply(
|
|
698
743
|
self,
|
|
@@ -724,21 +769,98 @@ class AiBuilderTools(ToolBase):
|
|
|
724
769
|
"upsert_views": [view.model_dump(mode="json") for view in parsed_views],
|
|
725
770
|
"remove_views": list(remove_views),
|
|
726
771
|
}
|
|
727
|
-
|
|
772
|
+
result = _safe_tool_call(
|
|
728
773
|
lambda: self._facade.app_views_apply(profile=profile, app_key=app_key, publish=publish, upsert_views=parsed_views, remove_views=remove_views),
|
|
729
774
|
error_code="VIEWS_APPLY_FAILED",
|
|
730
775
|
normalized_args=normalized_args,
|
|
731
776
|
suggested_next_call={"tool_name": "app_views_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
732
777
|
)
|
|
778
|
+
return self._retry_after_self_lock_release(
|
|
779
|
+
profile=profile,
|
|
780
|
+
result=result,
|
|
781
|
+
retry_call=lambda: self._facade.app_views_apply(
|
|
782
|
+
profile=profile,
|
|
783
|
+
app_key=app_key,
|
|
784
|
+
publish=publish,
|
|
785
|
+
upsert_views=parsed_views,
|
|
786
|
+
remove_views=remove_views,
|
|
787
|
+
),
|
|
788
|
+
)
|
|
733
789
|
|
|
734
790
|
def app_publish_verify(self, *, profile: str, app_key: str, expected_package_tag_id: int | None = None) -> JSONObject:
|
|
735
791
|
normalized_args = {"app_key": app_key, "expected_package_tag_id": expected_package_tag_id}
|
|
736
|
-
|
|
792
|
+
result = _safe_tool_call(
|
|
737
793
|
lambda: self._facade.app_publish_verify(profile=profile, app_key=app_key, expected_package_tag_id=expected_package_tag_id),
|
|
738
794
|
error_code="PUBLISH_VERIFY_FAILED",
|
|
739
795
|
normalized_args=normalized_args,
|
|
740
796
|
suggested_next_call={"tool_name": "app_publish_verify", "arguments": {"profile": profile, **normalized_args}},
|
|
741
797
|
)
|
|
798
|
+
return self._retry_after_self_lock_release(
|
|
799
|
+
profile=profile,
|
|
800
|
+
result=result,
|
|
801
|
+
retry_call=lambda: self._facade.app_publish_verify(
|
|
802
|
+
profile=profile,
|
|
803
|
+
app_key=app_key,
|
|
804
|
+
expected_package_tag_id=expected_package_tag_id,
|
|
805
|
+
),
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
def _retry_after_self_lock_release(self, *, profile: str, result: JSONObject, retry_call) -> JSONObject:
|
|
809
|
+
if not isinstance(result, dict) or result.get("status") != "failed" or result.get("error_code") != "APP_EDIT_LOCKED":
|
|
810
|
+
return result
|
|
811
|
+
suggested = result.get("suggested_next_call")
|
|
812
|
+
if not isinstance(suggested, dict) or suggested.get("tool_name") != "app_release_edit_lock_if_mine":
|
|
813
|
+
return result
|
|
814
|
+
arguments = suggested.get("arguments")
|
|
815
|
+
if not isinstance(arguments, dict):
|
|
816
|
+
return result
|
|
817
|
+
app_key = str(arguments.get("app_key") or "")
|
|
818
|
+
lock_owner_email = str(arguments.get("lock_owner_email") or "")
|
|
819
|
+
lock_owner_name = str(arguments.get("lock_owner_name") or "")
|
|
820
|
+
release_attempts: list[JSONObject] = []
|
|
821
|
+
retried: JSONObject = result
|
|
822
|
+
for _ in range(3):
|
|
823
|
+
release_result = self.app_release_edit_lock_if_mine(
|
|
824
|
+
profile=profile,
|
|
825
|
+
app_key=app_key,
|
|
826
|
+
lock_owner_email=lock_owner_email,
|
|
827
|
+
lock_owner_name=lock_owner_name,
|
|
828
|
+
)
|
|
829
|
+
release_attempts.append(release_result)
|
|
830
|
+
if not isinstance(release_result, dict) or release_result.get("status") != "success":
|
|
831
|
+
result.setdefault("details", {})
|
|
832
|
+
if isinstance(result["details"], dict):
|
|
833
|
+
result["details"]["edit_lock_release_result"] = release_result
|
|
834
|
+
result["details"]["edit_lock_release_attempts"] = release_attempts
|
|
835
|
+
return result
|
|
836
|
+
retried = retry_call()
|
|
837
|
+
if not (
|
|
838
|
+
isinstance(retried, dict)
|
|
839
|
+
and retried.get("status") == "failed"
|
|
840
|
+
and retried.get("error_code") == "APP_EDIT_LOCKED"
|
|
841
|
+
):
|
|
842
|
+
break
|
|
843
|
+
time.sleep(0.2)
|
|
844
|
+
if (
|
|
845
|
+
isinstance(retried, dict)
|
|
846
|
+
and retried.get("status") == "failed"
|
|
847
|
+
and retried.get("error_code") == "APP_EDIT_LOCKED"
|
|
848
|
+
):
|
|
849
|
+
retried = {
|
|
850
|
+
**retried,
|
|
851
|
+
"error_code": "PERSISTENT_SELF_LOCK",
|
|
852
|
+
"message": "app remains locked by the current user's active editor session after repeated forced release attempts",
|
|
853
|
+
"recoverable": True,
|
|
854
|
+
"suggested_next_call": None,
|
|
855
|
+
}
|
|
856
|
+
if isinstance(retried, dict):
|
|
857
|
+
retried.setdefault("details", {})
|
|
858
|
+
if isinstance(retried["details"], dict):
|
|
859
|
+
retried["details"]["edit_lock_release_result"] = release_attempts[-1] if release_attempts else None
|
|
860
|
+
retried["details"]["edit_lock_release_attempts"] = release_attempts
|
|
861
|
+
retried["edit_lock_released"] = bool(release_attempts)
|
|
862
|
+
retried["retried_after_edit_lock_release"] = True
|
|
863
|
+
return retried
|
|
742
864
|
|
|
743
865
|
|
|
744
866
|
def _validation_failure(detail: str, *, suggested_next_call: JSONObject | None = None) -> JSONObject:
|