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