@josephyan/qingflow-cli 0.2.0-beta.55 → 0.2.0-beta.56

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.
@@ -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