@qingflow-tech/qingflow-cli 1.1.40 → 1.1.41
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
CHANGED
|
@@ -7,7 +7,7 @@ Qingflow CLI 是轻流面向开发者和 AI Agent 的命令行工具。它把轻
|
|
|
7
7
|
## 安装
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm install @qingflow-tech/qingflow-cli@1.1.
|
|
10
|
+
npm install @qingflow-tech/qingflow-cli@1.1.41
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
推荐从官网复制这一条一键安装 Qingflow CLI:
|
|
@@ -21,7 +21,7 @@ npx qingflow-cli@latest install
|
|
|
21
21
|
## 运行
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
npx -y -p @qingflow-tech/qingflow-cli@1.1.
|
|
24
|
+
npx -y -p @qingflow-tech/qingflow-cli@1.1.41 qingflow
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
全局安装后可以直接运行:
|
|
@@ -55,7 +55,7 @@ qingflow-skills install --agent codex --scope user
|
|
|
55
55
|
如果你通过一次性 `npx -p` 执行 skills 安装,建议加 `--copy`,避免 symlink 指向 npm 临时执行缓存:
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
npx -y -p @qingflow-tech/qingflow-cli@1.1.
|
|
58
|
+
npx -y -p @qingflow-tech/qingflow-cli@1.1.41 qingflow-skills install --agent codex --scope user --copy
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
`qingflow-skills install` 默认使用 symlink,且遇到同名 skill 不会覆盖;只有显式传入 `--force` 才会替换。
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -105,7 +105,7 @@ class SessionStore:
|
|
|
105
105
|
persist: bool,
|
|
106
106
|
) -> SessionProfile:
|
|
107
107
|
now = _utcnow()
|
|
108
|
-
previous = self.
|
|
108
|
+
previous = self.peek_profile(profile)
|
|
109
109
|
persisted = False
|
|
110
110
|
if persist:
|
|
111
111
|
persisted = self._set_secret(self._token_key(profile), token)
|
|
@@ -163,6 +163,12 @@ class SessionStore:
|
|
|
163
163
|
session_profile = SessionProfile.from_dict(raw_profile) if isinstance(raw_profile, dict) else None
|
|
164
164
|
return session_profile
|
|
165
165
|
|
|
166
|
+
def peek_profile(self, profile: str) -> SessionProfile | None:
|
|
167
|
+
"""读取 profile,但不触发缺失、过期会话的自动刷新。"""
|
|
168
|
+
payload = self._load_profiles()
|
|
169
|
+
raw_profile = payload.get("profiles", {}).get(profile)
|
|
170
|
+
return SessionProfile.from_dict(raw_profile) if isinstance(raw_profile, dict) else None
|
|
171
|
+
|
|
166
172
|
def get_backend_session(self, profile: str) -> BackendSession | None:
|
|
167
173
|
if profile in self._logged_out_profiles:
|
|
168
174
|
return None
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
from typing import Any
|
|
5
4
|
import os
|
|
5
|
+
from typing import Any
|
|
6
6
|
|
|
7
7
|
from mcp.server.fastmcp import FastMCP
|
|
8
8
|
|
|
@@ -85,7 +85,8 @@ class AuthTools(ToolBase):
|
|
|
85
85
|
persist: bool = False,
|
|
86
86
|
) -> dict[str, Any]:
|
|
87
87
|
"""执行认证与会话相关逻辑。"""
|
|
88
|
-
resolved_base_url, resolved_credential = self.
|
|
88
|
+
resolved_base_url, resolved_credential = self._resolve_auth_inputs(
|
|
89
|
+
profile=profile,
|
|
89
90
|
base_url=base_url,
|
|
90
91
|
credential=credential,
|
|
91
92
|
)
|
|
@@ -306,10 +307,18 @@ class AuthTools(ToolBase):
|
|
|
306
307
|
),
|
|
307
308
|
}
|
|
308
309
|
|
|
309
|
-
def
|
|
310
|
-
|
|
310
|
+
def _resolve_auth_inputs(
|
|
311
|
+
self,
|
|
312
|
+
*,
|
|
313
|
+
profile: str,
|
|
314
|
+
base_url: str | None,
|
|
315
|
+
credential: str | None,
|
|
316
|
+
) -> tuple[str | None, str]:
|
|
317
|
+
"""按显式参数、环境变量、mcporter、profile 的顺序解析认证输入。"""
|
|
311
318
|
normalized_base_url = self._normalize_text(base_url)
|
|
312
319
|
normalized_credential = self._normalize_text(credential)
|
|
320
|
+
if not normalized_credential:
|
|
321
|
+
normalized_credential = self._normalize_text(os.getenv("QINGFLOW_CREDENTIAL"))
|
|
313
322
|
if normalized_base_url and normalized_credential:
|
|
314
323
|
return normalized_base_url, normalized_credential
|
|
315
324
|
|
|
@@ -317,11 +326,13 @@ class AuthTools(ToolBase):
|
|
|
317
326
|
if not normalized_base_url:
|
|
318
327
|
normalized_base_url = self._normalize_text(mcporter_context.get("base_url"))
|
|
319
328
|
if not normalized_base_url:
|
|
320
|
-
normalized_base_url = os.getenv("QINGFLOW_BASE_URL")
|
|
329
|
+
normalized_base_url = self._normalize_text(os.getenv("QINGFLOW_BASE_URL"))
|
|
321
330
|
if not normalized_credential:
|
|
322
331
|
normalized_credential = self._normalize_text(mcporter_context.get("credential"))
|
|
323
332
|
if not normalized_credential:
|
|
324
|
-
|
|
333
|
+
session_profile = self.sessions.peek_profile(profile)
|
|
334
|
+
if session_profile is not None:
|
|
335
|
+
normalized_credential = self._normalize_text(session_profile.credential)
|
|
325
336
|
return normalized_base_url, normalized_credential or ""
|
|
326
337
|
|
|
327
338
|
def _read_mcporter_qingflow_context(self) -> dict[str, str]:
|
|
@@ -355,15 +366,12 @@ class AuthTools(ToolBase):
|
|
|
355
366
|
profile: str,
|
|
356
367
|
session_profile,
|
|
357
368
|
) -> bool:
|
|
358
|
-
backend_session = self.sessions.get_backend_session(profile) if session_profile is not None else None
|
|
359
369
|
base_url = session_profile.base_url if session_profile is not None else None
|
|
360
|
-
credential = backend_session.credential if backend_session is not None else None
|
|
361
370
|
persist = bool(session_profile.persisted) if session_profile is not None else False
|
|
362
371
|
try:
|
|
363
372
|
self.auth_use_credential(
|
|
364
373
|
profile=profile,
|
|
365
374
|
base_url=base_url,
|
|
366
|
-
credential=credential,
|
|
367
375
|
persist=persist,
|
|
368
376
|
)
|
|
369
377
|
except (QingflowApiError, RuntimeError):
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
from contextvars import ContextVar
|
|
4
5
|
from typing import Any, Callable, TypeVar
|
|
5
6
|
|
|
@@ -29,6 +30,7 @@ class ToolBase:
|
|
|
29
30
|
if cloud_context is not None:
|
|
30
31
|
return cloud_context.as_tool_context(profile)
|
|
31
32
|
|
|
33
|
+
self._ensure_environment_credential_matches(profile)
|
|
32
34
|
session_profile = self.sessions.get_profile(profile)
|
|
33
35
|
if session_profile is None:
|
|
34
36
|
raise QingflowApiError.auth_required(profile)
|
|
@@ -151,50 +153,106 @@ class ToolBase:
|
|
|
151
153
|
if not credential:
|
|
152
154
|
return False
|
|
153
155
|
try:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
session_profile
|
|
157
|
-
"/mcp/auth/context",
|
|
158
|
-
json_body={"credential": credential},
|
|
159
|
-
qf_version=backend_session.qf_version,
|
|
160
|
-
)
|
|
161
|
-
payload = self._unwrap_context_payload(response.data)
|
|
162
|
-
token = self._normalize_text(payload.get("token"))
|
|
163
|
-
ws_id = self._coerce_positive_int(payload.get("wsId"))
|
|
164
|
-
uid = self._coerce_positive_int(payload.get("uid")) or session_profile.uid
|
|
165
|
-
if not token or ws_id is None:
|
|
166
|
-
return False
|
|
167
|
-
backend_qf_version = self._normalize_text(payload.get("qfVersion")) or response.qf_response_version
|
|
168
|
-
qf_version = backend_qf_version if backend_qf_version is not None else backend_session.qf_version
|
|
169
|
-
qf_version_source = (
|
|
170
|
-
"backend_response"
|
|
171
|
-
if backend_qf_version is not None
|
|
172
|
-
else backend_session.qf_version_source
|
|
173
|
-
)
|
|
174
|
-
base_url = self._normalize_text(payload.get("baseUrl")) or session_profile.base_url
|
|
175
|
-
ws_name = self._normalize_text(payload.get("wsName")) or session_profile.selected_ws_name
|
|
176
|
-
self.sessions.save_session(
|
|
177
|
-
profile=profile,
|
|
178
|
-
base_url=base_url,
|
|
179
|
-
qf_version=qf_version,
|
|
180
|
-
qf_version_source=qf_version_source,
|
|
181
|
-
token=token,
|
|
182
|
-
login_token=None,
|
|
156
|
+
self._exchange_session_from_credential(
|
|
157
|
+
profile,
|
|
158
|
+
session_profile=session_profile,
|
|
183
159
|
credential=credential,
|
|
184
|
-
uid=uid,
|
|
185
|
-
email=self._normalize_text(payload.get("email")) or session_profile.email,
|
|
186
|
-
nick_name=(
|
|
187
|
-
self._normalize_text(payload.get("nickName"))
|
|
188
|
-
or self._normalize_text(payload.get("displayName"))
|
|
189
|
-
or self._normalize_text(payload.get("name"))
|
|
190
|
-
or session_profile.nick_name
|
|
191
|
-
),
|
|
192
|
-
persist=session_profile.persisted,
|
|
193
160
|
)
|
|
194
|
-
self.sessions.select_workspace(profile, ws_id=ws_id, ws_name=ws_name)
|
|
195
|
-
return True
|
|
196
161
|
except QingflowApiError:
|
|
197
162
|
return False
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
def _ensure_environment_credential_matches(self, profile: str) -> None:
|
|
166
|
+
environment_credential = self._normalize_text(os.getenv("QINGFLOW_CREDENTIAL"))
|
|
167
|
+
if environment_credential is None:
|
|
168
|
+
return
|
|
169
|
+
session_profile = self.sessions.peek_profile(profile)
|
|
170
|
+
if session_profile is None:
|
|
171
|
+
return
|
|
172
|
+
if self._normalize_text(session_profile.credential) == environment_credential:
|
|
173
|
+
return
|
|
174
|
+
try:
|
|
175
|
+
self._exchange_session_from_credential(
|
|
176
|
+
profile,
|
|
177
|
+
session_profile=session_profile,
|
|
178
|
+
credential=environment_credential,
|
|
179
|
+
preserve_existing_metadata=False,
|
|
180
|
+
)
|
|
181
|
+
except QingflowApiError as error:
|
|
182
|
+
raise QingflowApiError(
|
|
183
|
+
category="auth",
|
|
184
|
+
message=(
|
|
185
|
+
f"QINGFLOW_CREDENTIAL changed for profile '{profile}', "
|
|
186
|
+
"but the session could not be refreshed."
|
|
187
|
+
),
|
|
188
|
+
backend_code=error.backend_code,
|
|
189
|
+
request_id=error.request_id,
|
|
190
|
+
http_status=error.http_status,
|
|
191
|
+
details={
|
|
192
|
+
"reason": "environment_credential_changed",
|
|
193
|
+
},
|
|
194
|
+
) from error
|
|
195
|
+
|
|
196
|
+
def _exchange_session_from_credential(
|
|
197
|
+
self,
|
|
198
|
+
profile: str,
|
|
199
|
+
*,
|
|
200
|
+
session_profile: SessionProfile,
|
|
201
|
+
credential: str,
|
|
202
|
+
preserve_existing_metadata: bool = True,
|
|
203
|
+
) -> None:
|
|
204
|
+
response = self.backend.public_request_with_meta(
|
|
205
|
+
"POST",
|
|
206
|
+
session_profile.base_url,
|
|
207
|
+
"/mcp/auth/context",
|
|
208
|
+
json_body={"credential": credential},
|
|
209
|
+
qf_version=session_profile.qf_version,
|
|
210
|
+
)
|
|
211
|
+
payload = self._unwrap_context_payload(response.data)
|
|
212
|
+
token = self._normalize_text(payload.get("token"))
|
|
213
|
+
ws_id = self._coerce_positive_int(payload.get("wsId"))
|
|
214
|
+
response_uid = self._coerce_positive_int(payload.get("uid"))
|
|
215
|
+
if not token or ws_id is None or (response_uid is None and not preserve_existing_metadata):
|
|
216
|
+
raise QingflowApiError(
|
|
217
|
+
category="auth",
|
|
218
|
+
message="Credential context did not return a valid user session and workspace.",
|
|
219
|
+
request_id=response.request_id,
|
|
220
|
+
http_status=response.http_status,
|
|
221
|
+
)
|
|
222
|
+
uid = response_uid or session_profile.uid
|
|
223
|
+
backend_qf_version = self._normalize_text(payload.get("qfVersion")) or response.qf_response_version
|
|
224
|
+
qf_version = backend_qf_version if backend_qf_version is not None else session_profile.qf_version
|
|
225
|
+
qf_version_source = (
|
|
226
|
+
"backend_response"
|
|
227
|
+
if backend_qf_version is not None
|
|
228
|
+
else session_profile.qf_version_source
|
|
229
|
+
)
|
|
230
|
+
base_url = self._normalize_text(payload.get("baseUrl")) or session_profile.base_url
|
|
231
|
+
ws_name = self._normalize_text(payload.get("wsName"))
|
|
232
|
+
email = self._normalize_text(payload.get("email"))
|
|
233
|
+
nick_name = (
|
|
234
|
+
self._normalize_text(payload.get("nickName"))
|
|
235
|
+
or self._normalize_text(payload.get("displayName"))
|
|
236
|
+
or self._normalize_text(payload.get("name"))
|
|
237
|
+
)
|
|
238
|
+
if preserve_existing_metadata:
|
|
239
|
+
ws_name = ws_name or session_profile.selected_ws_name
|
|
240
|
+
email = email or session_profile.email
|
|
241
|
+
nick_name = nick_name or session_profile.nick_name
|
|
242
|
+
self.sessions.save_session(
|
|
243
|
+
profile=profile,
|
|
244
|
+
base_url=base_url,
|
|
245
|
+
qf_version=qf_version,
|
|
246
|
+
qf_version_source=qf_version_source,
|
|
247
|
+
token=token,
|
|
248
|
+
login_token=None,
|
|
249
|
+
credential=credential,
|
|
250
|
+
uid=uid,
|
|
251
|
+
email=email,
|
|
252
|
+
nick_name=nick_name,
|
|
253
|
+
persist=session_profile.persisted,
|
|
254
|
+
)
|
|
255
|
+
self.sessions.select_workspace(profile, ws_id=ws_id, ws_name=ws_name)
|
|
198
256
|
|
|
199
257
|
def _unwrap_context_payload(self, payload: Any) -> dict[str, Any]:
|
|
200
258
|
if not isinstance(payload, dict):
|