@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.
@@ -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="输出原始 JSON")
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
- payload = _parse_error_payload(exc)
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
- payload = exc.to_dict()
57
- return _emit_error(payload, json_mode=bool(args.json), stdout=out, stderr=err)
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 4
140
- status = str(result.get("status") or "").lower()
141
- if status in {"failed", "blocked"}:
142
- return 4
143
- return 0
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,3 @@
1
+ from .context import OperationsRuntime, build_operations_runtime
2
+
3
+ __all__ = ["OperationsRuntime", "build_operations_runtime"]
@@ -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