@josephyan/qingflow-cli 0.2.0-beta.55 → 0.2.0-beta.57
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/cli/commands/app.py +16 -16
- package/src/qingflow_mcp/cli/commands/auth.py +16 -19
- package/src/qingflow_mcp/cli/commands/builder.py +162 -124
- package/src/qingflow_mcp/cli/commands/common.py +95 -21
- package/src/qingflow_mcp/cli/commands/imports.py +34 -42
- package/src/qingflow_mcp/cli/commands/record.py +133 -131
- package/src/qingflow_mcp/cli/commands/task.py +44 -43
- package/src/qingflow_mcp/cli/commands/workspace.py +10 -10
- package/src/qingflow_mcp/cli/context.py +32 -35
- package/src/qingflow_mcp/cli/formatters.py +121 -124
- package/src/qingflow_mcp/cli/main.py +17 -52
- package/src/qingflow_mcp/ops/__init__.py +3 -0
- package/src/qingflow_mcp/ops/apps.py +64 -0
- package/src/qingflow_mcp/ops/auth.py +121 -0
- package/src/qingflow_mcp/ops/base.py +290 -0
- package/src/qingflow_mcp/ops/builder.py +323 -0
- package/src/qingflow_mcp/ops/context.py +120 -0
- package/src/qingflow_mcp/ops/directory.py +171 -0
- package/src/qingflow_mcp/ops/feedback.py +49 -0
- package/src/qingflow_mcp/ops/files.py +78 -0
- package/src/qingflow_mcp/ops/imports.py +140 -0
- package/src/qingflow_mcp/ops/records.py +415 -0
- package/src/qingflow_mcp/ops/tasks.py +171 -0
- package/src/qingflow_mcp/ops/workspace.py +76 -0
- package/src/qingflow_mcp/server_app_builder.py +190 -122
- package/src/qingflow_mcp/server_app_user.py +662 -63
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
-
import json
|
|
5
4
|
import sys
|
|
6
5
|
from typing import Any, Callable, TextIO
|
|
7
6
|
|
|
8
7
|
from ..errors import QingflowApiError
|
|
8
|
+
from ..ops.base import normalize_exception
|
|
9
|
+
from .commands import register_all_commands
|
|
9
10
|
from .context import CliContext, build_cli_context
|
|
10
11
|
from .formatters import emit_json_result, emit_text_result
|
|
11
|
-
from .commands import register_all_commands
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
Handler = Callable[[argparse.Namespace, CliContext], dict[str, Any]]
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def build_parser() -> argparse.ArgumentParser:
|
|
18
|
-
parser = argparse.ArgumentParser(prog="qingflow", description="Qingflow CLI")
|
|
18
|
+
parser = argparse.ArgumentParser(prog="qingflow", description="Qingflow CLI 2.0")
|
|
19
19
|
parser.add_argument("--profile", default="default", help="会话 profile,默认 default")
|
|
20
|
-
parser.add_argument("--json", action="store_true", help="
|
|
20
|
+
parser.add_argument("--json", action="store_true", help="输出结构化 JSON")
|
|
21
21
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
22
22
|
register_all_commands(subparsers)
|
|
23
23
|
return parser
|
|
@@ -46,15 +46,16 @@ def run(
|
|
|
46
46
|
if handler is None:
|
|
47
47
|
parser.print_help(out)
|
|
48
48
|
return 2
|
|
49
|
+
|
|
49
50
|
context = context_factory()
|
|
50
51
|
try:
|
|
51
52
|
result = handler(args, context)
|
|
52
53
|
except RuntimeError as exc:
|
|
53
|
-
|
|
54
|
-
return _emit_error(payload, json_mode=bool(args.json), stdout=out, stderr=err)
|
|
54
|
+
result = normalize_exception(exc)
|
|
55
55
|
except QingflowApiError as exc:
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
result = normalize_exception(exc)
|
|
57
|
+
except Exception as exc: # noqa: BLE001
|
|
58
|
+
result = normalize_exception(exc)
|
|
58
59
|
finally:
|
|
59
60
|
context.close()
|
|
60
61
|
|
|
@@ -94,53 +95,17 @@ def _normalize_global_args(argv: list[str]) -> list[str]:
|
|
|
94
95
|
return global_args + remaining
|
|
95
96
|
|
|
96
97
|
|
|
97
|
-
def _parse_error_payload(exc: RuntimeError) -> dict[str, Any]:
|
|
98
|
-
raw = str(exc)
|
|
99
|
-
try:
|
|
100
|
-
payload = json.loads(raw)
|
|
101
|
-
except json.JSONDecodeError:
|
|
102
|
-
return {"category": "runtime", "message": raw}
|
|
103
|
-
return payload if isinstance(payload, dict) else {"category": "runtime", "message": raw}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def _emit_error(payload: dict[str, Any], *, json_mode: bool, stdout: TextIO, stderr: TextIO) -> int:
|
|
107
|
-
exit_code = _error_exit_code(payload)
|
|
108
|
-
if json_mode:
|
|
109
|
-
emit_json_result(payload, stream=stdout)
|
|
110
|
-
return exit_code
|
|
111
|
-
lines = [
|
|
112
|
-
f"Category: {payload.get('category') or 'error'}",
|
|
113
|
-
f"Message: {payload.get('message') or 'Unknown error'}",
|
|
114
|
-
]
|
|
115
|
-
if payload.get("backend_code") is not None:
|
|
116
|
-
lines.append(f"Backend Code: {payload.get('backend_code')}")
|
|
117
|
-
if payload.get("request_id"):
|
|
118
|
-
lines.append(f"Request ID: {payload.get('request_id')}")
|
|
119
|
-
details = payload.get("details")
|
|
120
|
-
if isinstance(details, dict):
|
|
121
|
-
for key, value in details.items():
|
|
122
|
-
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
123
|
-
lines.append(f"{key}: {value}")
|
|
124
|
-
stderr.write("\n".join(lines) + "\n")
|
|
125
|
-
return exit_code
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def _error_exit_code(payload: dict[str, Any]) -> int:
|
|
129
|
-
category = str(payload.get("category") or "").lower()
|
|
130
|
-
if category in {"auth", "workspace"}:
|
|
131
|
-
return 3
|
|
132
|
-
return 4
|
|
133
|
-
|
|
134
|
-
|
|
135
98
|
def _result_exit_code(result: dict[str, Any]) -> int:
|
|
136
99
|
if not isinstance(result, dict):
|
|
137
100
|
return 0
|
|
138
|
-
if result.get("ok") is False:
|
|
139
|
-
return
|
|
140
|
-
|
|
141
|
-
if
|
|
142
|
-
return
|
|
143
|
-
|
|
101
|
+
if result.get("ok") is not False:
|
|
102
|
+
return 0
|
|
103
|
+
code = str(result.get("code") or "").upper()
|
|
104
|
+
if code in {"AUTH_REQUIRED", "WORKSPACE_NOT_SELECTED"}:
|
|
105
|
+
return 3
|
|
106
|
+
if code in {"INVALID_ARGUMENT", "CONFIG"}:
|
|
107
|
+
return 2
|
|
108
|
+
return 4
|
|
144
109
|
|
|
145
110
|
|
|
146
111
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
)
|
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
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
|