@josephyan/qingflow-cli 0.2.0-beta.57 → 0.2.0-beta.59
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 +3 -2
- package/docs/local-agent-install.md +9 -0
- package/npm/bin/qingflow.mjs +1 -1
- package/npm/lib/runtime.mjs +156 -21
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/builder_facade/service.py +137 -5
- package/src/qingflow_mcp/cli/commands/app.py +16 -16
- package/src/qingflow_mcp/cli/commands/auth.py +19 -16
- package/src/qingflow_mcp/cli/commands/builder.py +124 -162
- package/src/qingflow_mcp/cli/commands/common.py +21 -95
- package/src/qingflow_mcp/cli/commands/imports.py +42 -34
- package/src/qingflow_mcp/cli/commands/record.py +131 -133
- package/src/qingflow_mcp/cli/commands/task.py +43 -44
- package/src/qingflow_mcp/cli/commands/workspace.py +10 -10
- package/src/qingflow_mcp/cli/context.py +35 -32
- package/src/qingflow_mcp/cli/formatters.py +124 -121
- package/src/qingflow_mcp/cli/main.py +52 -17
- package/src/qingflow_mcp/server_app_builder.py +122 -190
- package/src/qingflow_mcp/server_app_user.py +63 -662
- package/src/qingflow_mcp/tools/solution_tools.py +95 -3
- package/src/qingflow_mcp/ops/__init__.py +0 -3
- package/src/qingflow_mcp/ops/apps.py +0 -64
- package/src/qingflow_mcp/ops/auth.py +0 -121
- package/src/qingflow_mcp/ops/base.py +0 -290
- package/src/qingflow_mcp/ops/builder.py +0 -323
- package/src/qingflow_mcp/ops/context.py +0 -120
- package/src/qingflow_mcp/ops/directory.py +0 -171
- package/src/qingflow_mcp/ops/feedback.py +0 -49
- package/src/qingflow_mcp/ops/files.py +0 -78
- package/src/qingflow_mcp/ops/imports.py +0 -140
- package/src/qingflow_mcp/ops/records.py +0 -415
- package/src/qingflow_mcp/ops/tasks.py +0 -171
- package/src/qingflow_mcp/ops/workspace.py +0 -76
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from copy import deepcopy
|
|
4
|
+
import json
|
|
4
5
|
from typing import Any
|
|
5
6
|
from uuid import uuid4
|
|
6
7
|
|
|
@@ -8,6 +9,7 @@ from mcp.server.fastmcp import FastMCP
|
|
|
8
9
|
from pydantic import BaseModel, ValidationError
|
|
9
10
|
|
|
10
11
|
from ..config import DEFAULT_PROFILE
|
|
12
|
+
from ..errors import QingflowApiError
|
|
11
13
|
from ..list_type_labels import get_record_list_type_label
|
|
12
14
|
from ..solution.build_assembly_store import BuildAssemblyStore, default_manifest
|
|
13
15
|
from ..solution.compiler import CompiledSolution, ExecutionPlan, ExecutionStep, build_execution_plan, compile_solution
|
|
@@ -1850,7 +1852,15 @@ class SolutionTools(ToolBase):
|
|
|
1850
1852
|
packages = PackageTools(self.sessions, self.backend)
|
|
1851
1853
|
normalized_name = package_name.strip()
|
|
1852
1854
|
if package_tag_id > 0:
|
|
1853
|
-
|
|
1855
|
+
try:
|
|
1856
|
+
result = packages.package_get(profile=profile, tag_id=package_tag_id, include_raw=False)
|
|
1857
|
+
except (QingflowApiError, RuntimeError) as exc:
|
|
1858
|
+
return _builder_package_resolution_failed(
|
|
1859
|
+
package_name=normalized_name,
|
|
1860
|
+
package_tag_id=package_tag_id,
|
|
1861
|
+
error=_coerce_solution_api_error(exc),
|
|
1862
|
+
retried=False,
|
|
1863
|
+
)
|
|
1854
1864
|
summary = result.get("result") if isinstance(result.get("result"), dict) else {}
|
|
1855
1865
|
return {
|
|
1856
1866
|
"status": "resolved",
|
|
@@ -1869,12 +1879,28 @@ class SolutionTools(ToolBase):
|
|
|
1869
1879
|
"source_shape": None,
|
|
1870
1880
|
"retried": False,
|
|
1871
1881
|
}
|
|
1872
|
-
|
|
1882
|
+
try:
|
|
1883
|
+
result = packages.package_list(profile=profile, trial_status="all", include_raw=False)
|
|
1884
|
+
except (QingflowApiError, RuntimeError) as exc:
|
|
1885
|
+
return _builder_package_resolution_failed(
|
|
1886
|
+
package_name=normalized_name,
|
|
1887
|
+
package_tag_id=package_tag_id,
|
|
1888
|
+
error=_coerce_solution_api_error(exc),
|
|
1889
|
+
retried=False,
|
|
1890
|
+
)
|
|
1873
1891
|
items = result.get("items") if isinstance(result.get("items"), list) else []
|
|
1874
1892
|
source_shape = result.get("source_shape")
|
|
1875
1893
|
retried = False
|
|
1876
1894
|
if not items:
|
|
1877
|
-
|
|
1895
|
+
try:
|
|
1896
|
+
retry_result = packages.package_list(profile=profile, trial_status="all", include_raw=False)
|
|
1897
|
+
except (QingflowApiError, RuntimeError) as exc:
|
|
1898
|
+
return _builder_package_resolution_failed(
|
|
1899
|
+
package_name=normalized_name,
|
|
1900
|
+
package_tag_id=package_tag_id,
|
|
1901
|
+
error=_coerce_solution_api_error(exc),
|
|
1902
|
+
retried=True,
|
|
1903
|
+
)
|
|
1878
1904
|
retry_items = retry_result.get("items") if isinstance(retry_result.get("items"), list) else []
|
|
1879
1905
|
if retry_items:
|
|
1880
1906
|
items = retry_items
|
|
@@ -2982,6 +3008,72 @@ def _solution_error_fields(
|
|
|
2982
3008
|
}
|
|
2983
3009
|
|
|
2984
3010
|
|
|
3011
|
+
def _builder_package_resolution_failed(
|
|
3012
|
+
*,
|
|
3013
|
+
package_name: str,
|
|
3014
|
+
package_tag_id: int,
|
|
3015
|
+
error: QingflowApiError,
|
|
3016
|
+
retried: bool,
|
|
3017
|
+
) -> dict[str, Any]:
|
|
3018
|
+
if package_tag_id > 0:
|
|
3019
|
+
detail = f"failed to resolve package_tag_id '{package_tag_id}': {error.message}"
|
|
3020
|
+
elif error.backend_code in {40002, 40027}:
|
|
3021
|
+
detail = (
|
|
3022
|
+
f"failed to resolve package '{package_name}' because package listing is permission-restricted; "
|
|
3023
|
+
"provide package_tag_id explicitly or use an account that can list packages"
|
|
3024
|
+
)
|
|
3025
|
+
else:
|
|
3026
|
+
detail = f"failed to resolve package '{package_name}': {error.message}"
|
|
3027
|
+
error_fields = _solution_error_fields(category="config", detail=detail, suggested_next_call=None, stage="app")
|
|
3028
|
+
error_fields["error_code"] = "PACKAGE_RESOLVE_FAILED"
|
|
3029
|
+
return {
|
|
3030
|
+
"status": "failed",
|
|
3031
|
+
"response": {
|
|
3032
|
+
"status": "failed",
|
|
3033
|
+
"mode": "plan",
|
|
3034
|
+
"stage": "app",
|
|
3035
|
+
"errors": [{"category": "config", "detail": detail}],
|
|
3036
|
+
"package_resolution": {
|
|
3037
|
+
"status": "failed",
|
|
3038
|
+
"requested_name": package_name or None,
|
|
3039
|
+
"requested_tag_id": package_tag_id or None,
|
|
3040
|
+
"resolution_policy": "exact_name_only" if package_name else "tag_id_only",
|
|
3041
|
+
"candidates": [],
|
|
3042
|
+
"source_shape": None,
|
|
3043
|
+
"retried": retried,
|
|
3044
|
+
"transport_error": {
|
|
3045
|
+
"http_status": error.http_status,
|
|
3046
|
+
"backend_code": error.backend_code,
|
|
3047
|
+
"category": error.category,
|
|
3048
|
+
"request_id": error.request_id,
|
|
3049
|
+
},
|
|
3050
|
+
},
|
|
3051
|
+
**error_fields,
|
|
3052
|
+
},
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
|
|
3056
|
+
def _coerce_solution_api_error(error: Exception) -> QingflowApiError:
|
|
3057
|
+
if isinstance(error, QingflowApiError):
|
|
3058
|
+
return error
|
|
3059
|
+
if isinstance(error, RuntimeError):
|
|
3060
|
+
try:
|
|
3061
|
+
payload = json.loads(str(error))
|
|
3062
|
+
except json.JSONDecodeError:
|
|
3063
|
+
payload = None
|
|
3064
|
+
if isinstance(payload, dict) and payload.get("category") and payload.get("message"):
|
|
3065
|
+
details = payload.get("details")
|
|
3066
|
+
return QingflowApiError(
|
|
3067
|
+
category=str(payload.get("category")),
|
|
3068
|
+
message=str(payload.get("message")),
|
|
3069
|
+
backend_code=payload.get("backend_code"),
|
|
3070
|
+
request_id=payload.get("request_id"),
|
|
3071
|
+
http_status=payload.get("http_status"),
|
|
3072
|
+
details=details if isinstance(details, dict) else None,
|
|
3073
|
+
)
|
|
3074
|
+
return QingflowApiError(category="runtime", message=str(error))
|
|
3075
|
+
|
|
3076
|
+
|
|
2985
3077
|
def _coerce_count(value: Any) -> int | None:
|
|
2986
3078
|
if isinstance(value, bool) or value is None:
|
|
2987
3079
|
return None
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
from .base import normalize_exception, success_result
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class AppOperations:
|
|
9
|
-
def __init__(self, tools: Any) -> None:
|
|
10
|
-
self._tools = tools
|
|
11
|
-
|
|
12
|
-
def list(self, *, profile: str) -> dict:
|
|
13
|
-
try:
|
|
14
|
-
raw = self._tools.app.app_list(profile=profile)
|
|
15
|
-
except Exception as error: # noqa: BLE001
|
|
16
|
-
return normalize_exception(error)
|
|
17
|
-
items = raw.get("items") if isinstance(raw.get("items"), list) else []
|
|
18
|
-
return success_result(
|
|
19
|
-
"APPS_LISTED",
|
|
20
|
-
"已读取应用列表",
|
|
21
|
-
data={"items": items, "count": raw.get("count", len(items))},
|
|
22
|
-
meta={"profile": profile, "workspace_id": raw.get("ws_id")},
|
|
23
|
-
legacy=raw,
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
def find(self, *, profile: str, keyword: str, page_num: int, page_size: int) -> dict:
|
|
27
|
-
try:
|
|
28
|
-
raw = self._tools.app.app_search(profile=profile, keyword=keyword, page_num=page_num, page_size=page_size)
|
|
29
|
-
except Exception as error: # noqa: BLE001
|
|
30
|
-
return normalize_exception(error)
|
|
31
|
-
return success_result(
|
|
32
|
-
"APPS_FOUND",
|
|
33
|
-
"已完成应用搜索",
|
|
34
|
-
data={
|
|
35
|
-
"items": raw.get("items") if isinstance(raw.get("items"), list) else [],
|
|
36
|
-
"total": raw.get("total"),
|
|
37
|
-
"keyword": keyword,
|
|
38
|
-
"page_num": page_num,
|
|
39
|
-
"page_size": page_size,
|
|
40
|
-
},
|
|
41
|
-
meta={"profile": profile, "workspace_id": raw.get("ws_id")},
|
|
42
|
-
legacy=raw,
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
def show(self, *, profile: str, app_key: str) -> dict:
|
|
46
|
-
try:
|
|
47
|
-
raw = self._tools.app.app_get(profile=profile, app_key=app_key)
|
|
48
|
-
except Exception as error: # noqa: BLE001
|
|
49
|
-
return normalize_exception(error)
|
|
50
|
-
data = raw.get("data") if isinstance(raw.get("data"), dict) else {}
|
|
51
|
-
return success_result(
|
|
52
|
-
"APP_SHOWN",
|
|
53
|
-
"已读取应用信息",
|
|
54
|
-
data={
|
|
55
|
-
"app_key": data.get("app_key", app_key),
|
|
56
|
-
"app_name": data.get("app_name"),
|
|
57
|
-
"can_create": data.get("can_create"),
|
|
58
|
-
"import_capability": data.get("import_capability"),
|
|
59
|
-
"accessible_views": data.get("accessible_views") if isinstance(data.get("accessible_views"), list) else [],
|
|
60
|
-
},
|
|
61
|
-
warnings=raw.get("warnings") if isinstance(raw.get("warnings"), list) else [],
|
|
62
|
-
meta={"profile": profile, "workspace_id": raw.get("ws_id"), "request_route": raw.get("request_route")},
|
|
63
|
-
legacy=raw,
|
|
64
|
-
)
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
from .base import normalize_exception, success_result
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class AuthOperations:
|
|
9
|
-
def __init__(self, tools: Any) -> None:
|
|
10
|
-
self._tools = tools
|
|
11
|
-
|
|
12
|
-
def login(
|
|
13
|
-
self,
|
|
14
|
-
*,
|
|
15
|
-
profile: str,
|
|
16
|
-
base_url: str | None,
|
|
17
|
-
qf_version: str | None,
|
|
18
|
-
email: str,
|
|
19
|
-
password: str,
|
|
20
|
-
persist: bool,
|
|
21
|
-
) -> dict:
|
|
22
|
-
try:
|
|
23
|
-
raw = self._tools.auth.auth_login(
|
|
24
|
-
profile=profile,
|
|
25
|
-
base_url=base_url,
|
|
26
|
-
qf_version=qf_version,
|
|
27
|
-
email=email,
|
|
28
|
-
password=password,
|
|
29
|
-
persist=persist,
|
|
30
|
-
)
|
|
31
|
-
except Exception as error: # noqa: BLE001
|
|
32
|
-
return normalize_exception(error)
|
|
33
|
-
return success_result(
|
|
34
|
-
"AUTHENTICATED",
|
|
35
|
-
"登录成功",
|
|
36
|
-
data=_auth_payload(raw),
|
|
37
|
-
meta={"profile": profile},
|
|
38
|
-
legacy=raw,
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
def use_token(
|
|
42
|
-
self,
|
|
43
|
-
*,
|
|
44
|
-
profile: str,
|
|
45
|
-
base_url: str | None,
|
|
46
|
-
qf_version: str | None,
|
|
47
|
-
token: str,
|
|
48
|
-
ws_id: int | None,
|
|
49
|
-
persist: bool,
|
|
50
|
-
) -> dict:
|
|
51
|
-
try:
|
|
52
|
-
raw = self._tools.auth.auth_use_token(
|
|
53
|
-
profile=profile,
|
|
54
|
-
base_url=base_url,
|
|
55
|
-
qf_version=qf_version,
|
|
56
|
-
token=token,
|
|
57
|
-
ws_id=ws_id,
|
|
58
|
-
persist=persist,
|
|
59
|
-
)
|
|
60
|
-
except Exception as error: # noqa: BLE001
|
|
61
|
-
return normalize_exception(error)
|
|
62
|
-
return success_result(
|
|
63
|
-
"TOKEN_ACCEPTED",
|
|
64
|
-
"已接入会话令牌",
|
|
65
|
-
data=_auth_payload(raw),
|
|
66
|
-
meta={"profile": profile},
|
|
67
|
-
legacy=raw,
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
def me(self, *, profile: str) -> dict:
|
|
71
|
-
try:
|
|
72
|
-
raw = self._tools.auth.auth_whoami(profile=profile)
|
|
73
|
-
except Exception as error: # noqa: BLE001
|
|
74
|
-
return normalize_exception(error)
|
|
75
|
-
return success_result(
|
|
76
|
-
"SESSION_READY",
|
|
77
|
-
"当前会话已就绪",
|
|
78
|
-
data=_auth_payload(raw),
|
|
79
|
-
meta={"profile": profile},
|
|
80
|
-
legacy=raw,
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
def logout(self, *, profile: str, forget_persisted: bool) -> dict:
|
|
84
|
-
try:
|
|
85
|
-
raw = self._tools.auth.auth_logout(profile=profile, forget_persisted=forget_persisted)
|
|
86
|
-
except Exception as error: # noqa: BLE001
|
|
87
|
-
return normalize_exception(error)
|
|
88
|
-
return success_result(
|
|
89
|
-
"SESSION_CLEARED",
|
|
90
|
-
"已退出当前会话",
|
|
91
|
-
data={
|
|
92
|
-
"profile": raw.get("profile", profile),
|
|
93
|
-
"logged_out": bool(raw.get("logged_out", True)),
|
|
94
|
-
"forgot_persisted": bool(raw.get("forgot_persisted", forget_persisted)),
|
|
95
|
-
},
|
|
96
|
-
meta={"profile": profile},
|
|
97
|
-
legacy=raw,
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def _auth_payload(raw: dict) -> dict:
|
|
102
|
-
return {
|
|
103
|
-
"profile": raw.get("profile"),
|
|
104
|
-
"base_url": raw.get("base_url"),
|
|
105
|
-
"qf_version": raw.get("qf_version"),
|
|
106
|
-
"qf_version_source": raw.get("qf_version_source"),
|
|
107
|
-
"user": {
|
|
108
|
-
"uid": raw.get("uid"),
|
|
109
|
-
"email": raw.get("email"),
|
|
110
|
-
"nick_name": raw.get("nick_name"),
|
|
111
|
-
},
|
|
112
|
-
"workspace": {
|
|
113
|
-
"ws_id": raw.get("selected_ws_id"),
|
|
114
|
-
"name": raw.get("selected_ws_name"),
|
|
115
|
-
},
|
|
116
|
-
"suggested_workspace": {
|
|
117
|
-
"ws_id": raw.get("suggested_ws_id"),
|
|
118
|
-
"name": raw.get("suggested_ws_name"),
|
|
119
|
-
},
|
|
120
|
-
"persisted": bool(raw.get("persisted")),
|
|
121
|
-
}
|
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
from ..errors import QingflowApiError
|
|
7
|
-
from ..json_types import JSONObject
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
_COMMON_KEYS = {
|
|
11
|
-
"ok",
|
|
12
|
-
"status",
|
|
13
|
-
"message",
|
|
14
|
-
"error_code",
|
|
15
|
-
"details",
|
|
16
|
-
"warnings",
|
|
17
|
-
"verification",
|
|
18
|
-
"profile",
|
|
19
|
-
"ws_id",
|
|
20
|
-
"request_id",
|
|
21
|
-
"request_route",
|
|
22
|
-
"output_profile",
|
|
23
|
-
"recoverable",
|
|
24
|
-
"missing_fields",
|
|
25
|
-
"allowed_values",
|
|
26
|
-
"suggested_next_call",
|
|
27
|
-
"noop",
|
|
28
|
-
"compact",
|
|
29
|
-
"summary",
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
_INTERNAL_LEGACY_KEY = "__legacy__"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def success_result(
|
|
36
|
-
code: str,
|
|
37
|
-
message: str,
|
|
38
|
-
*,
|
|
39
|
-
data: Any = None,
|
|
40
|
-
warnings: list[Any] | None = None,
|
|
41
|
-
meta: JSONObject | None = None,
|
|
42
|
-
legacy: Any = None,
|
|
43
|
-
) -> JSONObject:
|
|
44
|
-
payload: JSONObject = {
|
|
45
|
-
"ok": True,
|
|
46
|
-
"code": code,
|
|
47
|
-
"message": message,
|
|
48
|
-
"data": data if data is not None else {},
|
|
49
|
-
"warnings": list(warnings or []),
|
|
50
|
-
"meta": dict(meta or {}),
|
|
51
|
-
}
|
|
52
|
-
if legacy is not None:
|
|
53
|
-
payload[_INTERNAL_LEGACY_KEY] = legacy
|
|
54
|
-
return payload
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def error_result(
|
|
58
|
-
code: str,
|
|
59
|
-
message: str,
|
|
60
|
-
*,
|
|
61
|
-
details: Any = None,
|
|
62
|
-
suggested_next: Any = None,
|
|
63
|
-
meta: JSONObject | None = None,
|
|
64
|
-
legacy: Any = None,
|
|
65
|
-
) -> JSONObject:
|
|
66
|
-
payload: JSONObject = {
|
|
67
|
-
"ok": False,
|
|
68
|
-
"code": code,
|
|
69
|
-
"message": message,
|
|
70
|
-
"details": details if details is not None else {},
|
|
71
|
-
"meta": dict(meta or {}),
|
|
72
|
-
}
|
|
73
|
-
if suggested_next is not None:
|
|
74
|
-
payload["suggested_next"] = suggested_next
|
|
75
|
-
if legacy is not None:
|
|
76
|
-
payload[_INTERNAL_LEGACY_KEY] = legacy
|
|
77
|
-
return payload
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def build_meta(raw: Any, *, extra: JSONObject | None = None) -> JSONObject:
|
|
81
|
-
meta: JSONObject = {}
|
|
82
|
-
if isinstance(raw, dict):
|
|
83
|
-
if raw.get("profile") is not None:
|
|
84
|
-
meta["profile"] = raw.get("profile")
|
|
85
|
-
if raw.get("ws_id") is not None:
|
|
86
|
-
meta["workspace_id"] = raw.get("ws_id")
|
|
87
|
-
if raw.get("request_id") is not None:
|
|
88
|
-
meta["request_id"] = raw.get("request_id")
|
|
89
|
-
if isinstance(raw.get("request_route"), dict):
|
|
90
|
-
meta["request_route"] = raw.get("request_route")
|
|
91
|
-
if isinstance(raw.get("verification"), dict) and raw.get("verification"):
|
|
92
|
-
meta["verification"] = raw.get("verification")
|
|
93
|
-
if extra:
|
|
94
|
-
meta.update(extra)
|
|
95
|
-
return meta
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def collect_warnings(raw: Any) -> list[Any]:
|
|
99
|
-
warnings: list[Any] = []
|
|
100
|
-
if isinstance(raw, dict):
|
|
101
|
-
if isinstance(raw.get("warnings"), list):
|
|
102
|
-
warnings.extend(raw["warnings"])
|
|
103
|
-
status = str(raw.get("status") or "").lower()
|
|
104
|
-
if status == "partial_success":
|
|
105
|
-
warnings.append(
|
|
106
|
-
{
|
|
107
|
-
"code": "PARTIAL_SUCCESS",
|
|
108
|
-
"message": str(raw.get("message") or "operation completed with warnings"),
|
|
109
|
-
}
|
|
110
|
-
)
|
|
111
|
-
return warnings
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def tool_payload(raw: Any) -> Any:
|
|
115
|
-
if not isinstance(raw, dict):
|
|
116
|
-
return raw
|
|
117
|
-
if "data" in raw:
|
|
118
|
-
return raw.get("data")
|
|
119
|
-
if "items" in raw:
|
|
120
|
-
return {
|
|
121
|
-
"items": raw.get("items") if isinstance(raw.get("items"), list) else [],
|
|
122
|
-
"count": raw.get("count"),
|
|
123
|
-
}
|
|
124
|
-
if "page" in raw:
|
|
125
|
-
page = raw.get("page")
|
|
126
|
-
if isinstance(page, dict):
|
|
127
|
-
return {
|
|
128
|
-
"items": page.get("list") if isinstance(page.get("list"), list) else [],
|
|
129
|
-
"page": page,
|
|
130
|
-
}
|
|
131
|
-
if "result" in raw:
|
|
132
|
-
return raw.get("result")
|
|
133
|
-
return {key: value for key, value in raw.items() if key not in _COMMON_KEYS}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def is_failed_tool_result(raw: Any) -> bool:
|
|
137
|
-
if not isinstance(raw, dict):
|
|
138
|
-
return False
|
|
139
|
-
if raw.get("ok") is False:
|
|
140
|
-
return True
|
|
141
|
-
status = str(raw.get("status") or "").lower()
|
|
142
|
-
return status in {"failed", "blocked"}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def tool_result_to_cli(
|
|
146
|
-
raw: Any,
|
|
147
|
-
*,
|
|
148
|
-
code: str,
|
|
149
|
-
message: str,
|
|
150
|
-
data: Any = None,
|
|
151
|
-
meta: JSONObject | None = None,
|
|
152
|
-
) -> JSONObject:
|
|
153
|
-
if is_failed_tool_result(raw):
|
|
154
|
-
payload = normalize_tool_error(raw)
|
|
155
|
-
payload["meta"] = build_meta(raw, extra=meta)
|
|
156
|
-
return payload
|
|
157
|
-
return success_result(
|
|
158
|
-
code,
|
|
159
|
-
message,
|
|
160
|
-
data=tool_payload(raw) if data is None else data,
|
|
161
|
-
warnings=collect_warnings(raw),
|
|
162
|
-
meta=build_meta(raw, extra=meta),
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def normalize_tool_error(raw: Any) -> JSONObject:
|
|
167
|
-
if isinstance(raw, dict):
|
|
168
|
-
details = raw.get("details")
|
|
169
|
-
if not isinstance(details, dict):
|
|
170
|
-
details = {}
|
|
171
|
-
if raw.get("backend_code") is not None:
|
|
172
|
-
details.setdefault("backend_code", raw.get("backend_code"))
|
|
173
|
-
if raw.get("request_id") is not None:
|
|
174
|
-
details.setdefault("request_id", raw.get("request_id"))
|
|
175
|
-
if raw.get("verification") not in (None, {}):
|
|
176
|
-
details.setdefault("verification", raw.get("verification"))
|
|
177
|
-
return error_result(
|
|
178
|
-
str(raw.get("error_code") or raw.get("code") or raw.get("category") or "OPERATION_FAILED"),
|
|
179
|
-
str(raw.get("message") or "operation failed"),
|
|
180
|
-
details=details,
|
|
181
|
-
suggested_next=raw.get("suggested_next_call"),
|
|
182
|
-
legacy=raw,
|
|
183
|
-
)
|
|
184
|
-
return error_result("OPERATION_FAILED", str(raw))
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
def coerce_runtime_error(exc: RuntimeError) -> QingflowApiError:
|
|
188
|
-
raw = str(exc)
|
|
189
|
-
try:
|
|
190
|
-
payload = json.loads(raw)
|
|
191
|
-
except json.JSONDecodeError:
|
|
192
|
-
return QingflowApiError(category="runtime", message=raw)
|
|
193
|
-
if not isinstance(payload, dict):
|
|
194
|
-
return QingflowApiError(category="runtime", message=raw)
|
|
195
|
-
return QingflowApiError(
|
|
196
|
-
category=str(payload.get("category") or "runtime"),
|
|
197
|
-
message=str(payload.get("message") or raw),
|
|
198
|
-
backend_code=payload.get("backend_code"),
|
|
199
|
-
request_id=payload.get("request_id"),
|
|
200
|
-
details=payload.get("details") if isinstance(payload.get("details"), dict) else None,
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def normalize_exception(error: Exception) -> JSONObject:
|
|
205
|
-
if isinstance(error, RuntimeError):
|
|
206
|
-
api_error = coerce_runtime_error(error)
|
|
207
|
-
elif isinstance(error, QingflowApiError):
|
|
208
|
-
api_error = error
|
|
209
|
-
else:
|
|
210
|
-
return error_result("RUNTIME_ERROR", str(error))
|
|
211
|
-
|
|
212
|
-
details = dict(api_error.details or {})
|
|
213
|
-
if api_error.backend_code is not None:
|
|
214
|
-
details.setdefault("backend_code", api_error.backend_code)
|
|
215
|
-
if api_error.request_id is not None:
|
|
216
|
-
details.setdefault("request_id", api_error.request_id)
|
|
217
|
-
code = str(details.pop("error_code", None) or api_error.category or "OPERATION_FAILED").upper()
|
|
218
|
-
if code == "CONFIG":
|
|
219
|
-
code = "INVALID_ARGUMENT"
|
|
220
|
-
elif code == "AUTH":
|
|
221
|
-
code = "AUTH_REQUIRED"
|
|
222
|
-
elif code == "WORKSPACE":
|
|
223
|
-
code = "WORKSPACE_NOT_SELECTED"
|
|
224
|
-
legacy_payload = None
|
|
225
|
-
if isinstance(error, RuntimeError):
|
|
226
|
-
raw = str(error)
|
|
227
|
-
try:
|
|
228
|
-
parsed = json.loads(raw)
|
|
229
|
-
except json.JSONDecodeError:
|
|
230
|
-
parsed = None
|
|
231
|
-
if isinstance(parsed, dict):
|
|
232
|
-
legacy_payload = parsed
|
|
233
|
-
return error_result(code, api_error.message, details=details, legacy=legacy_payload)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
def public_result(payload: Any) -> Any:
|
|
237
|
-
if isinstance(payload, dict):
|
|
238
|
-
return {
|
|
239
|
-
key: public_result(value)
|
|
240
|
-
for key, value in payload.items()
|
|
241
|
-
if key != _INTERNAL_LEGACY_KEY
|
|
242
|
-
}
|
|
243
|
-
if isinstance(payload, list):
|
|
244
|
-
return [public_result(item) for item in payload]
|
|
245
|
-
return payload
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
def mcp_result_from_operation(result: Any) -> Any:
|
|
249
|
-
if not isinstance(result, dict):
|
|
250
|
-
return result
|
|
251
|
-
legacy = result.get(_INTERNAL_LEGACY_KEY)
|
|
252
|
-
if isinstance(legacy, dict):
|
|
253
|
-
return legacy
|
|
254
|
-
meta = result.get("meta") if isinstance(result.get("meta"), dict) else {}
|
|
255
|
-
if result.get("ok") is False:
|
|
256
|
-
payload: JSONObject = {
|
|
257
|
-
"ok": False,
|
|
258
|
-
"status": "failed",
|
|
259
|
-
"error_code": result.get("code") or "OPERATION_FAILED",
|
|
260
|
-
"message": result.get("message") or "operation failed",
|
|
261
|
-
"details": result.get("details") if isinstance(result.get("details"), dict) else {},
|
|
262
|
-
"warnings": result.get("warnings") if isinstance(result.get("warnings"), list) else [],
|
|
263
|
-
}
|
|
264
|
-
if isinstance(meta.get("request_route"), dict):
|
|
265
|
-
payload["request_route"] = meta["request_route"]
|
|
266
|
-
if isinstance(meta.get("verification"), dict):
|
|
267
|
-
payload["verification"] = meta["verification"]
|
|
268
|
-
if meta.get("profile") is not None:
|
|
269
|
-
payload["profile"] = meta.get("profile")
|
|
270
|
-
if meta.get("workspace_id") is not None:
|
|
271
|
-
payload["ws_id"] = meta.get("workspace_id")
|
|
272
|
-
if result.get("suggested_next") is not None:
|
|
273
|
-
payload["suggested_next_call"] = result.get("suggested_next")
|
|
274
|
-
return payload
|
|
275
|
-
payload = {
|
|
276
|
-
"ok": True,
|
|
277
|
-
"status": "success",
|
|
278
|
-
"message": result.get("message") or "ok",
|
|
279
|
-
"data": result.get("data"),
|
|
280
|
-
"warnings": result.get("warnings") if isinstance(result.get("warnings"), list) else [],
|
|
281
|
-
}
|
|
282
|
-
if isinstance(meta.get("request_route"), dict):
|
|
283
|
-
payload["request_route"] = meta["request_route"]
|
|
284
|
-
if isinstance(meta.get("verification"), dict):
|
|
285
|
-
payload["verification"] = meta["verification"]
|
|
286
|
-
if meta.get("profile") is not None:
|
|
287
|
-
payload["profile"] = meta.get("profile")
|
|
288
|
-
if meta.get("workspace_id") is not None:
|
|
289
|
-
payload["ws_id"] = meta.get("workspace_id")
|
|
290
|
-
return payload
|