@josephyan/qingflow-cli 0.2.0-beta.985 → 0.2.0-beta.987
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/docs/local-agent-install.md +70 -11
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/builder_facade/service.py +376 -19
- package/src/qingflow_mcp/cli/commands/auth.py +14 -43
- package/src/qingflow_mcp/cli/commands/workspace.py +8 -5
- package/src/qingflow_mcp/cli/formatters.py +19 -22
- package/src/qingflow_mcp/config.py +39 -0
- package/src/qingflow_mcp/errors.py +2 -2
- package/src/qingflow_mcp/public_surface.py +4 -6
- package/src/qingflow_mcp/response_trim.py +1 -8
- package/src/qingflow_mcp/server.py +1 -1
- package/src/qingflow_mcp/server_app_builder.py +4 -28
- package/src/qingflow_mcp/server_app_user.py +4 -28
- package/src/qingflow_mcp/session_store.py +31 -5
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +2 -2
- package/src/qingflow_mcp/solution/executor.py +2 -2
- package/src/qingflow_mcp/tools/ai_builder_tools.py +117 -1
- package/src/qingflow_mcp/tools/app_tools.py +51 -1
- package/src/qingflow_mcp/tools/approval_tools.py +82 -1
- package/src/qingflow_mcp/tools/auth_tools.py +306 -288
- package/src/qingflow_mcp/tools/base.py +204 -4
- package/src/qingflow_mcp/tools/code_block_tools.py +21 -0
- package/src/qingflow_mcp/tools/custom_button_tools.py +24 -1
- package/src/qingflow_mcp/tools/directory_tools.py +28 -1
- package/src/qingflow_mcp/tools/feedback_tools.py +8 -0
- package/src/qingflow_mcp/tools/file_tools.py +25 -1
- package/src/qingflow_mcp/tools/import_tools.py +40 -1
- package/src/qingflow_mcp/tools/navigation_tools.py +34 -1
- package/src/qingflow_mcp/tools/package_tools.py +37 -1
- package/src/qingflow_mcp/tools/portal_tools.py +28 -1
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +38 -1
- package/src/qingflow_mcp/tools/record_tools.py +255 -2
- package/src/qingflow_mcp/tools/repository_dev_tools.py +21 -2
- package/src/qingflow_mcp/tools/resource_read_tools.py +23 -1
- package/src/qingflow_mcp/tools/role_tools.py +19 -1
- package/src/qingflow_mcp/tools/solution_tools.py +56 -1
- package/src/qingflow_mcp/tools/task_context_tools.py +72 -1
- package/src/qingflow_mcp/tools/task_tools.py +49 -3
- package/src/qingflow_mcp/tools/view_tools.py +56 -1
- package/src/qingflow_mcp/tools/workflow_tools.py +65 -1
- package/src/qingflow_mcp/tools/workspace_tools.py +100 -217
|
@@ -1,154 +1,145 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import json
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
from mcp.server.fastmcp import FastMCP
|
|
7
|
-
from Crypto.PublicKey import RSA
|
|
8
|
-
from Crypto.Cipher import PKCS1_v1_5
|
|
9
7
|
|
|
10
8
|
from ..backend_client import BackendRequestContext, BackendResponse
|
|
11
9
|
from ..config import (
|
|
12
10
|
DEFAULT_PROFILE,
|
|
13
11
|
get_default_base_url,
|
|
14
12
|
get_default_qf_version,
|
|
13
|
+
get_mcporter_config_path,
|
|
15
14
|
normalize_base_url,
|
|
16
15
|
)
|
|
17
16
|
from ..errors import QingflowApiError, raise_tool_error
|
|
18
17
|
from ..session_store import SessionStore
|
|
19
|
-
from .base import ToolBase
|
|
18
|
+
from .base import ToolBase, tool_cn_name
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
class AuthTools(ToolBase):
|
|
22
|
+
"""认证类工具(中文名:身份与会话工具)。
|
|
23
|
+
|
|
24
|
+
主要职责:
|
|
25
|
+
1. 使用 credential 调用 /mcp/auth/context 建立会话;
|
|
26
|
+
2. 查询当前登录身份与工作区成员信息;
|
|
27
|
+
3. 退出并清理当前 profile 会话。
|
|
28
|
+
"""
|
|
29
|
+
|
|
23
30
|
def __init__(self, sessions: SessionStore, backend) -> None:
|
|
31
|
+
"""执行内部辅助逻辑。"""
|
|
24
32
|
super().__init__(sessions, backend)
|
|
25
33
|
|
|
26
34
|
def register(self, mcp: FastMCP) -> None:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
) -> dict[str, Any]:
|
|
36
|
-
return self.auth_login(profile=profile, base_url=base_url, qf_version=qf_version, email=email, password=password, persist=persist)
|
|
37
|
-
|
|
38
|
-
@mcp.tool()
|
|
39
|
-
def auth_use_token(
|
|
35
|
+
"""注册当前工具到 MCP 服务。"""
|
|
36
|
+
@mcp.tool(
|
|
37
|
+
description=(
|
|
38
|
+
"类型:认证工具;中文名:凭证登录。"
|
|
39
|
+
"用途:使用 createClaw 提供的 credential 交换上下文并建立本地会话。"
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
def auth_use_credential(
|
|
40
43
|
profile: str = DEFAULT_PROFILE,
|
|
41
44
|
base_url: str | None = None,
|
|
42
45
|
qf_version: str | None = None,
|
|
43
|
-
|
|
44
|
-
ws_id: int | None = None,
|
|
46
|
+
credential: str = "",
|
|
45
47
|
persist: bool = False,
|
|
46
48
|
) -> dict[str, Any]:
|
|
47
|
-
return self.
|
|
49
|
+
return self.auth_use_credential(
|
|
50
|
+
profile=profile,
|
|
51
|
+
base_url=base_url,
|
|
52
|
+
qf_version=qf_version,
|
|
53
|
+
credential=credential,
|
|
54
|
+
persist=persist,
|
|
55
|
+
)
|
|
48
56
|
|
|
49
|
-
@mcp.tool(
|
|
57
|
+
@mcp.tool(
|
|
58
|
+
description=(
|
|
59
|
+
"类型:认证工具;中文名:我的身份。"
|
|
60
|
+
"用途:查看当前 profile 的登录身份、工作区与权限信息。"
|
|
61
|
+
)
|
|
62
|
+
)
|
|
50
63
|
def auth_whoami(profile: str = DEFAULT_PROFILE) -> dict[str, Any]:
|
|
51
64
|
return self.auth_whoami(profile=profile)
|
|
52
65
|
|
|
53
|
-
@mcp.tool(
|
|
66
|
+
@mcp.tool(
|
|
67
|
+
description=(
|
|
68
|
+
"类型:认证工具;中文名:退出登录。"
|
|
69
|
+
"用途:退出当前 profile,并可选清理持久化会话。"
|
|
70
|
+
)
|
|
71
|
+
)
|
|
54
72
|
def auth_logout(profile: str = DEFAULT_PROFILE, forget_persisted: bool = False) -> dict[str, Any]:
|
|
55
73
|
return self.auth_logout(profile=profile, forget_persisted=forget_persisted)
|
|
56
74
|
|
|
57
|
-
|
|
75
|
+
@tool_cn_name("凭证登录")
|
|
76
|
+
def auth_use_credential(
|
|
58
77
|
self,
|
|
59
78
|
*,
|
|
60
79
|
profile: str = DEFAULT_PROFILE,
|
|
61
80
|
base_url: str | None = None,
|
|
62
81
|
qf_version: str | None = None,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
persist: bool,
|
|
82
|
+
credential: str | None = None,
|
|
83
|
+
persist: bool = False,
|
|
66
84
|
) -> dict[str, Any]:
|
|
67
|
-
|
|
85
|
+
"""执行认证与会话相关逻辑。"""
|
|
86
|
+
resolved_base_url, resolved_credential = self._resolve_mcporter_auth_inputs(
|
|
87
|
+
base_url=base_url,
|
|
88
|
+
credential=credential,
|
|
89
|
+
)
|
|
90
|
+
normalized_base_url = self._normalize_base_url(resolved_base_url)
|
|
68
91
|
normalized_qf_version, qf_version_source = self._resolve_qf_version_input(qf_version)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
# Try to fetch public key and encrypt password
|
|
73
|
-
public_key_str = self._fetch_public_key(normalized_base_url, qf_version=normalized_qf_version)
|
|
74
|
-
encrypted_password = self._encrypt_password(password, public_key_str) if public_key_str else password
|
|
75
|
-
|
|
76
|
-
try:
|
|
77
|
-
# Try 'email' first (aPaas/Public Cloud style)
|
|
78
|
-
login_response = self.backend.public_request_with_meta(
|
|
79
|
-
"POST",
|
|
80
|
-
normalized_base_url,
|
|
81
|
-
"/user/login",
|
|
82
|
-
json_body={"email": email, "password": encrypted_password},
|
|
83
|
-
qf_version=normalized_qf_version,
|
|
84
|
-
)
|
|
85
|
-
except QingflowApiError as error:
|
|
86
|
-
# If failed, try 'account' (QMC/Private Cloud style)
|
|
87
|
-
try:
|
|
88
|
-
login_response = self.backend.public_request_with_meta(
|
|
89
|
-
"POST",
|
|
90
|
-
normalized_base_url,
|
|
91
|
-
"/user/login",
|
|
92
|
-
json_body={"account": email, "password": encrypted_password},
|
|
93
|
-
qf_version=normalized_qf_version,
|
|
94
|
-
)
|
|
95
|
-
except QingflowApiError:
|
|
96
|
-
# If both failed, raise the original error
|
|
97
|
-
self._handle_error(profile, error)
|
|
98
|
-
raise AssertionError("unreachable")
|
|
99
|
-
login_result = login_response.data
|
|
100
|
-
if not isinstance(login_result, dict):
|
|
101
|
-
raise_tool_error(QingflowApiError(category="auth", message="Login did not return a valid result"))
|
|
102
|
-
|
|
103
|
-
token = login_result.get("token")
|
|
104
|
-
login_token = login_result.get("loginToken")
|
|
105
|
-
|
|
106
|
-
if not token and login_token:
|
|
92
|
+
normalized_credential = str(resolved_credential).strip()
|
|
93
|
+
if not normalized_credential:
|
|
107
94
|
raise_tool_error(
|
|
108
|
-
QingflowApiError.
|
|
109
|
-
"
|
|
95
|
+
QingflowApiError.config_error(
|
|
96
|
+
"credential is required or configure ~/.openclaw/workspace/config/mcporter.json "
|
|
97
|
+
"with mcpServers.qingflow.headers.x-qingflow-client-id"
|
|
110
98
|
)
|
|
111
99
|
)
|
|
112
|
-
|
|
100
|
+
|
|
101
|
+
context_payload, detected_qf_version = self._fetch_auth_context(
|
|
102
|
+
normalized_base_url,
|
|
103
|
+
normalized_credential,
|
|
104
|
+
qf_version=normalized_qf_version,
|
|
105
|
+
)
|
|
106
|
+
token = self._normalize_text(context_payload.get("token"))
|
|
113
107
|
if not token:
|
|
114
|
-
raise_tool_error(QingflowApiError(category="auth", message="
|
|
115
|
-
|
|
108
|
+
raise_tool_error(QingflowApiError(category="auth", message="Credential context did not return a valid Qingflow token"))
|
|
109
|
+
|
|
110
|
+
response_qf_version = self._normalize_text(context_payload.get("qfVersion"))
|
|
116
111
|
resolved_qf_version, resolved_qf_version_source = self._resolve_backend_qf_version(
|
|
117
|
-
detected_qf_version,
|
|
112
|
+
response_qf_version or detected_qf_version,
|
|
118
113
|
fallback_qf_version=normalized_qf_version,
|
|
119
114
|
fallback_source=qf_version_source,
|
|
120
115
|
)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
qf_version_source=resolved_qf_version_source,
|
|
130
|
-
)
|
|
131
|
-
if verified_qf_version:
|
|
132
|
-
resolved_qf_version, resolved_qf_version_source = self._resolve_backend_qf_version(
|
|
133
|
-
verified_qf_version,
|
|
134
|
-
fallback_qf_version=resolved_qf_version,
|
|
135
|
-
fallback_source=resolved_qf_version_source,
|
|
136
|
-
)
|
|
137
|
-
if isinstance(verified_user_info, dict):
|
|
138
|
-
user_info = verified_user_info
|
|
139
|
-
last_ws_info = user_info.get("lastWsInfo") or {}
|
|
116
|
+
resolved_base_url = self._normalize_text(context_payload.get("baseUrl")) or normalized_base_url
|
|
117
|
+
selected_ws_id = self._coerce_int(context_payload.get("wsId"))
|
|
118
|
+
if selected_ws_id is None or selected_ws_id <= 0:
|
|
119
|
+
raise_tool_error(QingflowApiError(category="auth", message="Credential context did not return a valid wsId"))
|
|
120
|
+
selected_ws_name = self._normalize_text(context_payload.get("wsName"))
|
|
121
|
+
uid = self._coerce_int(context_payload.get("uid"))
|
|
122
|
+
if uid is None:
|
|
123
|
+
raise_tool_error(QingflowApiError(category="auth", message="Credential context did not return valid user info"))
|
|
140
124
|
session_profile = self.sessions.save_session(
|
|
141
125
|
profile=profile,
|
|
142
|
-
base_url=
|
|
126
|
+
base_url=resolved_base_url,
|
|
143
127
|
qf_version=resolved_qf_version,
|
|
144
128
|
qf_version_source=resolved_qf_version_source,
|
|
145
129
|
token=token,
|
|
146
|
-
login_token=
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
130
|
+
login_token=None,
|
|
131
|
+
credential=normalized_credential,
|
|
132
|
+
uid=uid,
|
|
133
|
+
email=self._normalize_text(context_payload.get("email")),
|
|
134
|
+
nick_name=self._normalize_text(
|
|
135
|
+
context_payload.get("nickName")
|
|
136
|
+
or context_payload.get("displayName")
|
|
137
|
+
or context_payload.get("name")
|
|
138
|
+
),
|
|
150
139
|
persist=persist,
|
|
151
140
|
)
|
|
141
|
+
session_profile = self.sessions.select_workspace(profile, ws_id=selected_ws_id, ws_name=selected_ws_name)
|
|
142
|
+
|
|
152
143
|
return {
|
|
153
144
|
"profile": session_profile.profile,
|
|
154
145
|
"base_url": session_profile.base_url,
|
|
@@ -159,8 +150,8 @@ class AuthTools(ToolBase):
|
|
|
159
150
|
"nick_name": session_profile.nick_name,
|
|
160
151
|
"selected_ws_id": session_profile.selected_ws_id,
|
|
161
152
|
"selected_ws_name": session_profile.selected_ws_name,
|
|
162
|
-
"suggested_ws_id":
|
|
163
|
-
"suggested_ws_name":
|
|
153
|
+
"suggested_ws_id": session_profile.selected_ws_id,
|
|
154
|
+
"suggested_ws_name": session_profile.selected_ws_name,
|
|
164
155
|
"persisted": session_profile.persisted,
|
|
165
156
|
"request_route": self._request_route_payload(
|
|
166
157
|
BackendRequestContext(
|
|
@@ -173,176 +164,170 @@ class AuthTools(ToolBase):
|
|
|
173
164
|
),
|
|
174
165
|
}
|
|
175
166
|
|
|
176
|
-
def
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
167
|
+
def _resolve_mcporter_auth_inputs(self, *, base_url: str | None, credential: str | None) -> tuple[str | None, str]:
|
|
168
|
+
"""从参数或 mcporter 配置解析登录所需 base_url 与 credential。"""
|
|
169
|
+
normalized_base_url = self._normalize_text(base_url)
|
|
170
|
+
normalized_credential = self._normalize_text(credential)
|
|
171
|
+
if normalized_base_url and normalized_credential:
|
|
172
|
+
return normalized_base_url, normalized_credential
|
|
173
|
+
|
|
174
|
+
mcporter_context = self._read_mcporter_qingflow_context()
|
|
175
|
+
if not normalized_base_url:
|
|
176
|
+
normalized_base_url = self._normalize_text(mcporter_context.get("base_url"))
|
|
177
|
+
if not normalized_credential:
|
|
178
|
+
normalized_credential = self._normalize_text(mcporter_context.get("credential"))
|
|
179
|
+
return normalized_base_url, normalized_credential or ""
|
|
180
|
+
|
|
181
|
+
def _read_mcporter_qingflow_context(self) -> dict[str, str]:
|
|
182
|
+
"""读取 OpenClaw mcporter 中的 Qingflow MCP 上下文。"""
|
|
183
|
+
path = get_mcporter_config_path()
|
|
184
|
+
if not path.exists():
|
|
185
|
+
return {}
|
|
193
186
|
try:
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
187
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
188
|
+
payload = json.load(handle)
|
|
189
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
190
|
+
raise_tool_error(QingflowApiError.config_error(f"failed to read mcporter config '{path}': {exc}"))
|
|
191
|
+
|
|
192
|
+
if not isinstance(payload, dict):
|
|
193
|
+
raise_tool_error(QingflowApiError.config_error(f"mcporter config '{path}' must be a JSON object"))
|
|
194
|
+
mcp_servers = payload.get("mcpServers")
|
|
195
|
+
qingflow = mcp_servers.get("qingflow") if isinstance(mcp_servers, dict) else None
|
|
196
|
+
if not isinstance(qingflow, dict):
|
|
197
|
+
return {}
|
|
198
|
+
headers = qingflow.get("headers")
|
|
199
|
+
credential = None
|
|
200
|
+
if isinstance(headers, dict):
|
|
201
|
+
credential = headers.get("x-qingflow-client-id")
|
|
202
|
+
return {
|
|
203
|
+
"base_url": str(qingflow.get("url") or "").strip(),
|
|
204
|
+
"credential": str(credential or "").strip(),
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
@tool_cn_name("我的身份")
|
|
208
|
+
def auth_whoami(self, *, profile: str = DEFAULT_PROFILE) -> dict[str, Any]:
|
|
209
|
+
"""执行认证与会话相关逻辑。"""
|
|
210
|
+
def build_response(
|
|
211
|
+
session_profile, # type: ignore[no-untyped-def]
|
|
212
|
+
backend_session, # type: ignore[no-untyped-def]
|
|
213
|
+
context: BackendRequestContext,
|
|
214
|
+
) -> dict[str, Any]:
|
|
215
|
+
workspace, workspace_qf_version = self._selected_workspace_snapshot(
|
|
216
|
+
session_profile=session_profile,
|
|
217
|
+
backend_session=backend_session,
|
|
205
218
|
)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
profile=profile,
|
|
212
|
-
base_url=normalized_base_url,
|
|
213
|
-
qf_version=resolved_qf_version,
|
|
214
|
-
qf_version_source=resolved_qf_version_source,
|
|
215
|
-
token=token,
|
|
216
|
-
login_token=None,
|
|
217
|
-
uid=int(uid),
|
|
218
|
-
email=user_info.get("email"),
|
|
219
|
-
nick_name=user_info.get("nickName") or user_info.get("displayName") or user_info.get("name"),
|
|
220
|
-
persist=persist,
|
|
219
|
+
resolved_qf_version = workspace_qf_version or session_profile.qf_version
|
|
220
|
+
resolved_qf_version_source = (
|
|
221
|
+
"workspace_system_version"
|
|
222
|
+
if workspace_qf_version is not None
|
|
223
|
+
else session_profile.qf_version_source
|
|
221
224
|
)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
ws_id,
|
|
228
|
-
qf_version=resolved_qf_version,
|
|
229
|
-
qf_version_source=resolved_qf_version_source,
|
|
225
|
+
if (
|
|
226
|
+
workspace_qf_version is not None
|
|
227
|
+
and (
|
|
228
|
+
workspace_qf_version != session_profile.qf_version
|
|
229
|
+
or session_profile.qf_version_source != "workspace_system_version"
|
|
230
230
|
)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
231
|
+
):
|
|
232
|
+
session_profile = self.sessions.update_route(
|
|
233
|
+
profile,
|
|
234
|
+
qf_version=workspace_qf_version,
|
|
235
|
+
qf_version_source="workspace_system_version",
|
|
236
|
+
)
|
|
237
|
+
backend_session = self.sessions.get_backend_session(profile) or backend_session
|
|
238
|
+
context = BackendRequestContext(
|
|
239
|
+
base_url=backend_session.base_url,
|
|
240
|
+
token=backend_session.token,
|
|
241
|
+
ws_id=session_profile.selected_ws_id,
|
|
242
|
+
qf_version=backend_session.qf_version,
|
|
243
|
+
qf_version_source=backend_session.qf_version_source,
|
|
244
|
+
)
|
|
245
|
+
if self._should_refresh_identity_metadata(session_profile):
|
|
246
|
+
refreshed_profile = self._refresh_identity_metadata(
|
|
247
|
+
profile=profile,
|
|
248
|
+
session_profile=session_profile,
|
|
249
|
+
backend_session=backend_session,
|
|
250
|
+
context=context,
|
|
251
|
+
)
|
|
252
|
+
if refreshed_profile is not None:
|
|
253
|
+
session_profile = refreshed_profile
|
|
254
|
+
response = {
|
|
243
255
|
"profile": session_profile.profile,
|
|
244
256
|
"base_url": session_profile.base_url,
|
|
245
|
-
"qf_version":
|
|
246
|
-
"qf_version_source":
|
|
257
|
+
"qf_version": resolved_qf_version,
|
|
258
|
+
"qf_version_source": resolved_qf_version_source,
|
|
247
259
|
"uid": session_profile.uid,
|
|
248
260
|
"email": session_profile.email,
|
|
249
261
|
"nick_name": session_profile.nick_name,
|
|
250
262
|
"selected_ws_id": session_profile.selected_ws_id,
|
|
251
263
|
"selected_ws_name": session_profile.selected_ws_name,
|
|
252
|
-
"suggested_ws_id": last_ws_info.get("wsId"),
|
|
253
|
-
"suggested_ws_name": last_ws_info.get("wsName") or last_ws_info.get("workspaceName"),
|
|
254
264
|
"persisted": session_profile.persisted,
|
|
255
|
-
"request_route": self._request_route_payload(
|
|
256
|
-
BackendRequestContext(
|
|
257
|
-
base_url=session_profile.base_url,
|
|
258
|
-
token=token,
|
|
259
|
-
ws_id=session_profile.selected_ws_id,
|
|
260
|
-
qf_version=session_profile.qf_version,
|
|
261
|
-
qf_version_source=session_profile.qf_version_source,
|
|
262
|
-
)
|
|
263
|
-
),
|
|
265
|
+
"request_route": self._request_route_payload(context),
|
|
264
266
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
login_token=None,
|
|
274
|
-
uid=0,
|
|
275
|
-
email=None,
|
|
276
|
-
nick_name=None,
|
|
277
|
-
persist=persist,
|
|
278
|
-
)
|
|
279
|
-
if ws_id is not None:
|
|
280
|
-
session_profile = self.sessions.select_workspace(profile, ws_id=ws_id, ws_name=None)
|
|
281
|
-
return {
|
|
282
|
-
"profile": session_profile.profile,
|
|
283
|
-
"base_url": session_profile.base_url,
|
|
284
|
-
"qf_version": session_profile.qf_version,
|
|
285
|
-
"qf_version_source": session_profile.qf_version_source,
|
|
286
|
-
"uid": session_profile.uid,
|
|
287
|
-
"email": session_profile.email,
|
|
288
|
-
"nick_name": session_profile.nick_name,
|
|
289
|
-
"selected_ws_id": session_profile.selected_ws_id,
|
|
290
|
-
"selected_ws_name": session_profile.selected_ws_name,
|
|
291
|
-
"suggested_ws_id": ws_id,
|
|
292
|
-
"suggested_ws_name": None,
|
|
293
|
-
"persisted": session_profile.persisted,
|
|
294
|
-
"provisional": True,
|
|
295
|
-
"request_route": self._request_route_payload(
|
|
296
|
-
BackendRequestContext(
|
|
297
|
-
base_url=session_profile.base_url,
|
|
298
|
-
token=token,
|
|
299
|
-
ws_id=session_profile.selected_ws_id,
|
|
300
|
-
qf_version=session_profile.qf_version,
|
|
301
|
-
qf_version_source=session_profile.qf_version_source,
|
|
302
|
-
)
|
|
303
|
-
),
|
|
304
|
-
}
|
|
305
|
-
self._handle_error(profile, error)
|
|
306
|
-
raise AssertionError("unreachable")
|
|
267
|
+
member_info, member_warnings = self._workspace_member_info(
|
|
268
|
+
session_profile=session_profile,
|
|
269
|
+
backend_session=backend_session,
|
|
270
|
+
)
|
|
271
|
+
response.update(member_info)
|
|
272
|
+
if member_warnings:
|
|
273
|
+
response["warnings"] = member_warnings
|
|
274
|
+
return response
|
|
307
275
|
|
|
308
|
-
|
|
276
|
+
session_profile = None
|
|
277
|
+
backend_session = None
|
|
309
278
|
try:
|
|
310
279
|
session_profile, backend_session, context = self._require_context(profile, require_workspace=False)
|
|
280
|
+
if backend_session.credential:
|
|
281
|
+
self._probe_token_validity(
|
|
282
|
+
session_profile=session_profile,
|
|
283
|
+
backend_session=backend_session,
|
|
284
|
+
)
|
|
285
|
+
return build_response(session_profile, backend_session, context)
|
|
311
286
|
except QingflowApiError as error:
|
|
287
|
+
if (
|
|
288
|
+
error.looks_like_invalid_token()
|
|
289
|
+
and session_profile is not None
|
|
290
|
+
and backend_session is not None
|
|
291
|
+
and self._refresh_session_from_credential(
|
|
292
|
+
profile,
|
|
293
|
+
session_profile=session_profile,
|
|
294
|
+
backend_session=backend_session,
|
|
295
|
+
)
|
|
296
|
+
):
|
|
297
|
+
try:
|
|
298
|
+
refreshed_profile, refreshed_backend_session, refreshed_context = self._require_context(
|
|
299
|
+
profile,
|
|
300
|
+
require_workspace=False,
|
|
301
|
+
)
|
|
302
|
+
return build_response(refreshed_profile, refreshed_backend_session, refreshed_context)
|
|
303
|
+
except QingflowApiError as refreshed_error:
|
|
304
|
+
self._handle_error(profile, refreshed_error)
|
|
312
305
|
self._handle_error(profile, error)
|
|
313
306
|
raise AssertionError("unreachable")
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
"uid": session_profile.uid,
|
|
329
|
-
"email": session_profile.email,
|
|
330
|
-
"nick_name": session_profile.nick_name,
|
|
331
|
-
"selected_ws_id": session_profile.selected_ws_id,
|
|
332
|
-
"selected_ws_name": session_profile.selected_ws_name,
|
|
333
|
-
"persisted": session_profile.persisted,
|
|
334
|
-
"request_route": self._request_route_payload(context),
|
|
335
|
-
}
|
|
336
|
-
member_info, member_warnings = self._workspace_member_info(
|
|
337
|
-
session_profile=session_profile,
|
|
338
|
-
backend_session=backend_session,
|
|
307
|
+
|
|
308
|
+
def _probe_token_validity(
|
|
309
|
+
self,
|
|
310
|
+
*,
|
|
311
|
+
session_profile, # type: ignore[no-untyped-def]
|
|
312
|
+
backend_session, # type: ignore[no-untyped-def]
|
|
313
|
+
) -> None:
|
|
314
|
+
"""执行内部辅助逻辑。"""
|
|
315
|
+
probe_context = BackendRequestContext(
|
|
316
|
+
base_url=backend_session.base_url,
|
|
317
|
+
token=backend_session.token,
|
|
318
|
+
ws_id=session_profile.selected_ws_id,
|
|
319
|
+
qf_version=backend_session.qf_version,
|
|
320
|
+
qf_version_source=backend_session.qf_version_source,
|
|
339
321
|
)
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
322
|
+
try:
|
|
323
|
+
self.backend.request("GET", probe_context, "/user")
|
|
324
|
+
except QingflowApiError as error:
|
|
325
|
+
if error.looks_like_invalid_token():
|
|
326
|
+
raise
|
|
344
327
|
|
|
328
|
+
@tool_cn_name("退出登录")
|
|
345
329
|
def auth_logout(self, *, profile: str = DEFAULT_PROFILE, forget_persisted: bool = False) -> dict[str, Any]:
|
|
330
|
+
"""执行认证与会话相关逻辑。"""
|
|
346
331
|
if not self.sessions.has_profile(profile):
|
|
347
332
|
raise_tool_error(QingflowApiError.auth_required(profile))
|
|
348
333
|
self.sessions.logout(profile, forget_persisted=forget_persisted)
|
|
@@ -353,6 +338,7 @@ class AuthTools(ToolBase):
|
|
|
353
338
|
}
|
|
354
339
|
|
|
355
340
|
def _normalize_base_url(self, base_url: str | None) -> str:
|
|
341
|
+
"""执行内部辅助逻辑。"""
|
|
356
342
|
normalized_base_url = normalize_base_url(base_url) or get_default_base_url()
|
|
357
343
|
if not normalized_base_url:
|
|
358
344
|
raise_tool_error(
|
|
@@ -363,12 +349,14 @@ class AuthTools(ToolBase):
|
|
|
363
349
|
return normalized_base_url
|
|
364
350
|
|
|
365
351
|
def _normalize_qf_version(self, qf_version: str | None) -> str | None:
|
|
352
|
+
"""执行内部辅助逻辑。"""
|
|
366
353
|
if qf_version is not None:
|
|
367
354
|
normalized = str(qf_version).strip()
|
|
368
355
|
return normalized or None
|
|
369
356
|
return get_default_qf_version()
|
|
370
357
|
|
|
371
358
|
def _resolve_qf_version_input(self, qf_version: str | None) -> tuple[str | None, str]:
|
|
359
|
+
"""执行内部辅助逻辑。"""
|
|
372
360
|
if qf_version is not None:
|
|
373
361
|
normalized = self._normalize_qf_version(qf_version)
|
|
374
362
|
return normalized, "explicit" if normalized else "unset"
|
|
@@ -377,11 +365,6 @@ class AuthTools(ToolBase):
|
|
|
377
365
|
return normalized, "default_config"
|
|
378
366
|
return None, "unset"
|
|
379
367
|
|
|
380
|
-
def _should_allow_provisional_token_session(self, error: QingflowApiError) -> bool:
|
|
381
|
-
if error.looks_like_invalid_token():
|
|
382
|
-
return False
|
|
383
|
-
return error.http_status == 404 and error.category in {"http", "auth", "workspace"}
|
|
384
|
-
|
|
385
368
|
def _resolve_backend_qf_version(
|
|
386
369
|
self,
|
|
387
370
|
backend_qf_version: str | None,
|
|
@@ -389,11 +372,41 @@ class AuthTools(ToolBase):
|
|
|
389
372
|
fallback_qf_version: str | None,
|
|
390
373
|
fallback_source: str,
|
|
391
374
|
) -> tuple[str | None, str]:
|
|
375
|
+
"""执行内部辅助逻辑。"""
|
|
392
376
|
if backend_qf_version:
|
|
393
377
|
return backend_qf_version, "backend_response"
|
|
394
378
|
return fallback_qf_version, fallback_source
|
|
395
379
|
|
|
380
|
+
def _fetch_auth_context(
|
|
381
|
+
self,
|
|
382
|
+
base_url: str,
|
|
383
|
+
credential: str,
|
|
384
|
+
*,
|
|
385
|
+
qf_version: str | None,
|
|
386
|
+
) -> tuple[dict[str, Any], str | None]:
|
|
387
|
+
"""执行内部辅助逻辑。"""
|
|
388
|
+
response = self.backend.public_request_with_meta(
|
|
389
|
+
"POST",
|
|
390
|
+
base_url,
|
|
391
|
+
"/mcp/auth/context",
|
|
392
|
+
json_body={"credential": credential},
|
|
393
|
+
qf_version=qf_version,
|
|
394
|
+
)
|
|
395
|
+
payload = self._unwrap_auth_context_payload(response.data)
|
|
396
|
+
return payload, response.qf_response_version
|
|
397
|
+
|
|
398
|
+
def _unwrap_auth_context_payload(self, payload: Any) -> dict[str, Any]:
|
|
399
|
+
"""执行内部辅助逻辑。"""
|
|
400
|
+
if not isinstance(payload, dict):
|
|
401
|
+
raise_tool_error(QingflowApiError(category="auth", message="Credential context did not return a valid result"))
|
|
402
|
+
for key in ("data", "result"):
|
|
403
|
+
nested = payload.get(key)
|
|
404
|
+
if isinstance(nested, dict):
|
|
405
|
+
return nested
|
|
406
|
+
return payload
|
|
407
|
+
|
|
396
408
|
def _workspace_system_version(self, workspace: Any) -> str | None:
|
|
409
|
+
"""执行内部辅助逻辑。"""
|
|
397
410
|
if not isinstance(workspace, dict):
|
|
398
411
|
return None
|
|
399
412
|
value = workspace.get("systemVersion")
|
|
@@ -411,6 +424,7 @@ class AuthTools(ToolBase):
|
|
|
411
424
|
qf_version: str | None,
|
|
412
425
|
qf_version_source: str | None,
|
|
413
426
|
) -> tuple[dict[str, Any], str | None]:
|
|
427
|
+
"""执行内部辅助逻辑。"""
|
|
414
428
|
request_context = BackendRequestContext(
|
|
415
429
|
base_url=base_url,
|
|
416
430
|
token=token,
|
|
@@ -461,6 +475,7 @@ class AuthTools(ToolBase):
|
|
|
461
475
|
qf_version: str | None,
|
|
462
476
|
qf_version_source: str | None,
|
|
463
477
|
) -> tuple[dict[str, Any] | None, str | None]:
|
|
478
|
+
"""执行内部辅助逻辑。"""
|
|
464
479
|
try:
|
|
465
480
|
return self._fetch_user_info(
|
|
466
481
|
base_url,
|
|
@@ -480,6 +495,7 @@ class AuthTools(ToolBase):
|
|
|
480
495
|
qf_version: str | None,
|
|
481
496
|
qf_version_source: str | None,
|
|
482
497
|
) -> tuple[dict[str, Any] | None, str | None]:
|
|
498
|
+
"""执行内部辅助逻辑。"""
|
|
483
499
|
page_response = self.backend.request_with_meta(
|
|
484
500
|
"POST",
|
|
485
501
|
BackendRequestContext(
|
|
@@ -510,6 +526,7 @@ class AuthTools(ToolBase):
|
|
|
510
526
|
qf_version: str | None,
|
|
511
527
|
qf_version_source: str | None,
|
|
512
528
|
) -> dict[str, Any]:
|
|
529
|
+
"""执行内部辅助逻辑。"""
|
|
513
530
|
workspace = self.backend.request(
|
|
514
531
|
"GET",
|
|
515
532
|
BackendRequestContext(
|
|
@@ -525,7 +542,26 @@ class AuthTools(ToolBase):
|
|
|
525
542
|
raise_tool_error(QingflowApiError(category="workspace", message=f"Workspace {ws_id} is not accessible"))
|
|
526
543
|
return workspace
|
|
527
544
|
|
|
545
|
+
def _selected_workspace_snapshot(
|
|
546
|
+
self,
|
|
547
|
+
*,
|
|
548
|
+
session_profile, # type: ignore[no-untyped-def]
|
|
549
|
+
backend_session, # type: ignore[no-untyped-def]
|
|
550
|
+
) -> tuple[dict[str, Any] | None, str | None]:
|
|
551
|
+
ws_id = session_profile.selected_ws_id
|
|
552
|
+
if ws_id is None:
|
|
553
|
+
return None, None
|
|
554
|
+
workspace = self._fetch_workspace_with_name_fallback(
|
|
555
|
+
session_profile.base_url,
|
|
556
|
+
backend_session.token,
|
|
557
|
+
ws_id,
|
|
558
|
+
qf_version=session_profile.qf_version,
|
|
559
|
+
qf_version_source=session_profile.qf_version_source,
|
|
560
|
+
)
|
|
561
|
+
return workspace, self._workspace_system_version(workspace)
|
|
562
|
+
|
|
528
563
|
def _request_route_payload(self, context: BackendRequestContext) -> dict[str, Any]:
|
|
564
|
+
"""执行内部辅助逻辑。"""
|
|
529
565
|
describe_route = getattr(self.backend, "describe_route", None)
|
|
530
566
|
if callable(describe_route):
|
|
531
567
|
payload = describe_route(context)
|
|
@@ -538,6 +574,7 @@ class AuthTools(ToolBase):
|
|
|
538
574
|
}
|
|
539
575
|
|
|
540
576
|
def _should_refresh_identity_metadata(self, session_profile) -> bool: # type: ignore[no-untyped-def]
|
|
577
|
+
"""执行内部辅助逻辑。"""
|
|
541
578
|
return (
|
|
542
579
|
session_profile.uid == 0
|
|
543
580
|
or session_profile.email is None
|
|
@@ -553,6 +590,7 @@ class AuthTools(ToolBase):
|
|
|
553
590
|
backend_session, # type: ignore[no-untyped-def]
|
|
554
591
|
context: BackendRequestContext,
|
|
555
592
|
):
|
|
593
|
+
"""执行内部辅助逻辑。"""
|
|
556
594
|
try:
|
|
557
595
|
user_info, _ = self._fetch_user_info(
|
|
558
596
|
session_profile.base_url,
|
|
@@ -603,6 +641,7 @@ class AuthTools(ToolBase):
|
|
|
603
641
|
session_profile, # type: ignore[no-untyped-def]
|
|
604
642
|
backend_session, # type: ignore[no-untyped-def]
|
|
605
643
|
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
644
|
+
"""执行内部辅助逻辑。"""
|
|
606
645
|
default_payload = {
|
|
607
646
|
"departments": [],
|
|
608
647
|
"roles": [],
|
|
@@ -647,12 +686,14 @@ class AuthTools(ToolBase):
|
|
|
647
686
|
return payload, []
|
|
648
687
|
|
|
649
688
|
def _workspace_auth(self, context: BackendRequestContext, *, ws_id: int) -> int | None:
|
|
689
|
+
"""执行内部辅助逻辑。"""
|
|
650
690
|
workspace = self._fetch_workspace_auth_from_detail(context, ws_id=ws_id)
|
|
651
691
|
if workspace is not None:
|
|
652
692
|
return workspace
|
|
653
693
|
return self._fetch_workspace_auth_from_list(context, ws_id=ws_id)
|
|
654
694
|
|
|
655
695
|
def _fetch_workspace_auth_from_detail(self, context: BackendRequestContext, *, ws_id: int) -> int | None:
|
|
696
|
+
"""执行内部辅助逻辑。"""
|
|
656
697
|
try:
|
|
657
698
|
workspace = self.backend.request("GET", context, f"/user/workspace/{ws_id}")
|
|
658
699
|
except QingflowApiError:
|
|
@@ -662,6 +703,7 @@ class AuthTools(ToolBase):
|
|
|
662
703
|
return self._coerce_auth_value(workspace.get("auth"))
|
|
663
704
|
|
|
664
705
|
def _fetch_workspace_auth_from_list(self, context: BackendRequestContext, *, ws_id: int) -> int | None:
|
|
706
|
+
"""执行内部辅助逻辑。"""
|
|
665
707
|
try:
|
|
666
708
|
payload = self.backend.request(
|
|
667
709
|
"POST",
|
|
@@ -688,6 +730,7 @@ class AuthTools(ToolBase):
|
|
|
688
730
|
email: str | None,
|
|
689
731
|
nick_name: str | None,
|
|
690
732
|
) -> dict[str, Any] | None:
|
|
733
|
+
"""执行内部辅助逻辑。"""
|
|
691
734
|
candidates: list[dict[str, Any]] = []
|
|
692
735
|
for keyword in (email, nick_name):
|
|
693
736
|
member = self._search_member_once(context, uid=uid, keyword=keyword)
|
|
@@ -709,12 +752,14 @@ class AuthTools(ToolBase):
|
|
|
709
752
|
uid: int | None,
|
|
710
753
|
keyword: str | None,
|
|
711
754
|
) -> dict[str, Any] | None:
|
|
755
|
+
"""执行内部辅助逻辑。"""
|
|
712
756
|
for item in self._search_member_items(context, keyword=keyword):
|
|
713
757
|
if self._same_member(item, uid=uid):
|
|
714
758
|
return item
|
|
715
759
|
return None
|
|
716
760
|
|
|
717
761
|
def _search_member_items(self, context: BackendRequestContext, *, keyword: str | None) -> list[dict[str, Any]]:
|
|
762
|
+
"""执行内部辅助逻辑。"""
|
|
718
763
|
params: dict[str, Any] = {"pageNum": 1, "pageSize": 100, "containDisable": True}
|
|
719
764
|
normalized_keyword = str(keyword or "").strip()
|
|
720
765
|
if normalized_keyword:
|
|
@@ -727,6 +772,7 @@ class AuthTools(ToolBase):
|
|
|
727
772
|
return [item for item in items if isinstance(item, dict)]
|
|
728
773
|
|
|
729
774
|
def _same_member(self, item: dict[str, Any], *, uid: int | None) -> bool:
|
|
775
|
+
"""执行内部辅助逻辑。"""
|
|
730
776
|
if uid is None or uid <= 0:
|
|
731
777
|
return False
|
|
732
778
|
for key in ("uid", "id", "userId"):
|
|
@@ -741,6 +787,7 @@ class AuthTools(ToolBase):
|
|
|
741
787
|
return False
|
|
742
788
|
|
|
743
789
|
def _compact_departments(self, member: dict[str, Any]) -> list[dict[str, Any]]:
|
|
790
|
+
"""执行内部辅助逻辑。"""
|
|
744
791
|
items: list[dict[str, Any]] = []
|
|
745
792
|
seen: set[tuple[int | None, str | None]] = set()
|
|
746
793
|
for depart in self._walk_nested_items(member.get("departs")):
|
|
@@ -761,6 +808,7 @@ class AuthTools(ToolBase):
|
|
|
761
808
|
return items
|
|
762
809
|
|
|
763
810
|
def _compact_roles(self, member: dict[str, Any]) -> list[dict[str, Any]]:
|
|
811
|
+
"""执行内部辅助逻辑。"""
|
|
764
812
|
items: list[dict[str, Any]] = []
|
|
765
813
|
seen: set[tuple[int | None, str | None]] = set()
|
|
766
814
|
for role in self._walk_nested_items(member.get("roles")):
|
|
@@ -777,6 +825,7 @@ class AuthTools(ToolBase):
|
|
|
777
825
|
return items
|
|
778
826
|
|
|
779
827
|
def _resolve_permission_level(self, auth_code: int | None) -> str | None:
|
|
828
|
+
"""执行内部辅助逻辑。"""
|
|
780
829
|
mapping = {
|
|
781
830
|
2: "超级管理",
|
|
782
831
|
1: "系统管理员",
|
|
@@ -786,6 +835,7 @@ class AuthTools(ToolBase):
|
|
|
786
835
|
return mapping.get(auth_code)
|
|
787
836
|
|
|
788
837
|
def _coerce_auth_value(self, value: Any) -> int | None:
|
|
838
|
+
"""执行内部辅助逻辑。"""
|
|
789
839
|
coerced = self._coerce_int(value)
|
|
790
840
|
if coerced is not None:
|
|
791
841
|
return coerced
|
|
@@ -804,6 +854,7 @@ class AuthTools(ToolBase):
|
|
|
804
854
|
return None
|
|
805
855
|
|
|
806
856
|
def _extract_items(self, payload: Any) -> list[Any]:
|
|
857
|
+
"""执行内部辅助逻辑。"""
|
|
807
858
|
if isinstance(payload, list):
|
|
808
859
|
return payload
|
|
809
860
|
if not isinstance(payload, dict):
|
|
@@ -824,6 +875,7 @@ class AuthTools(ToolBase):
|
|
|
824
875
|
return []
|
|
825
876
|
|
|
826
877
|
def _walk_nested_items(self, value: Any) -> list[Any]:
|
|
878
|
+
"""执行内部辅助逻辑。"""
|
|
827
879
|
if isinstance(value, list):
|
|
828
880
|
items: list[Any] = []
|
|
829
881
|
for item in value:
|
|
@@ -832,6 +884,7 @@ class AuthTools(ToolBase):
|
|
|
832
884
|
return [value]
|
|
833
885
|
|
|
834
886
|
def _coerce_int(self, value: Any) -> int | None:
|
|
887
|
+
"""执行内部辅助逻辑。"""
|
|
835
888
|
if isinstance(value, bool) or value is None:
|
|
836
889
|
return None
|
|
837
890
|
if isinstance(value, int):
|
|
@@ -842,6 +895,7 @@ class AuthTools(ToolBase):
|
|
|
842
895
|
return None
|
|
843
896
|
|
|
844
897
|
def _normalize_text(self, value: Any) -> str | None:
|
|
898
|
+
"""执行内部辅助逻辑。"""
|
|
845
899
|
if value is None:
|
|
846
900
|
return None
|
|
847
901
|
text = str(value).strip()
|
|
@@ -856,6 +910,7 @@ class AuthTools(ToolBase):
|
|
|
856
910
|
qf_version: str | None,
|
|
857
911
|
qf_version_source: str | None,
|
|
858
912
|
) -> dict[str, Any] | None:
|
|
913
|
+
"""执行内部辅助逻辑。"""
|
|
859
914
|
try:
|
|
860
915
|
workspace = self._fetch_workspace(
|
|
861
916
|
base_url,
|
|
@@ -891,6 +946,7 @@ class AuthTools(ToolBase):
|
|
|
891
946
|
qf_version: str | None,
|
|
892
947
|
qf_version_source: str | None,
|
|
893
948
|
) -> dict[str, Any] | None:
|
|
949
|
+
"""执行内部辅助逻辑。"""
|
|
894
950
|
payload = self.backend.request(
|
|
895
951
|
"POST",
|
|
896
952
|
BackendRequestContext(
|
|
@@ -915,41 +971,3 @@ class AuthTools(ToolBase):
|
|
|
915
971
|
None,
|
|
916
972
|
)
|
|
917
973
|
return found if isinstance(found, dict) else None
|
|
918
|
-
|
|
919
|
-
def _fetch_public_key(self, base_url: str, *, qf_version: str | None) -> str | None:
|
|
920
|
-
# Endpoints to try (order matters, lowercase 'pubkey' is for Public Cloud)
|
|
921
|
-
endpoints = ["/user/pubkey", "/api/user/pubkey", "/user/publicKey", "/api/user/publicKey"]
|
|
922
|
-
for endpoint in endpoints:
|
|
923
|
-
try:
|
|
924
|
-
# We use unwrap=False to handle various response formats
|
|
925
|
-
result = self.backend.public_request("GET", base_url, endpoint, unwrap=False, qf_version=qf_version)
|
|
926
|
-
if isinstance(result, dict):
|
|
927
|
-
# Try various common response structures
|
|
928
|
-
data = result.get("data") or result.get("result") or result
|
|
929
|
-
if isinstance(data, dict):
|
|
930
|
-
# Try case-insensitive keys
|
|
931
|
-
for key in ["pubkey", "publicKey", "pubKey"]:
|
|
932
|
-
if key in data:
|
|
933
|
-
return str(data[key])
|
|
934
|
-
if isinstance(data, str) and not data.startswith("{"):
|
|
935
|
-
return data
|
|
936
|
-
except Exception:
|
|
937
|
-
continue
|
|
938
|
-
|
|
939
|
-
return None
|
|
940
|
-
|
|
941
|
-
def _encrypt_password(self, password: str, public_key_str: str) -> str:
|
|
942
|
-
try:
|
|
943
|
-
if not public_key_str.startswith("-----BEGIN"):
|
|
944
|
-
key_content = public_key_str.strip()
|
|
945
|
-
formatted_key = f"-----BEGIN PUBLIC KEY-----\n{key_content}\n-----END PUBLIC KEY-----"
|
|
946
|
-
else:
|
|
947
|
-
formatted_key = public_key_str
|
|
948
|
-
|
|
949
|
-
key = RSA.import_key(formatted_key)
|
|
950
|
-
cipher = PKCS1_v1_5.new(key)
|
|
951
|
-
encrypted = cipher.encrypt(password.encode("utf-8"))
|
|
952
|
-
return base64.b64encode(encrypted).decode("utf-8")
|
|
953
|
-
except Exception as e:
|
|
954
|
-
# If encryption fails, fallback to plain text (might be a legacy or custom environment)
|
|
955
|
-
return password
|