@josephyan/qingflow-cli 0.2.0-beta.1000
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 +31 -0
- package/docs/local-agent-install.md +309 -0
- package/entry_point.py +13 -0
- package/npm/bin/qingflow.mjs +5 -0
- package/npm/lib/runtime.mjs +346 -0
- package/npm/scripts/postinstall.mjs +16 -0
- package/package.json +34 -0
- package/pyproject.toml +67 -0
- package/qingflow +15 -0
- package/src/qingflow_mcp/__init__.py +37 -0
- package/src/qingflow_mcp/__main__.py +5 -0
- package/src/qingflow_mcp/backend_client.py +649 -0
- package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
- package/src/qingflow_mcp/builder_facade/models.py +1846 -0
- package/src/qingflow_mcp/builder_facade/service.py +16502 -0
- package/src/qingflow_mcp/cli/__init__.py +1 -0
- package/src/qingflow_mcp/cli/commands/__init__.py +18 -0
- package/src/qingflow_mcp/cli/commands/app.py +40 -0
- package/src/qingflow_mcp/cli/commands/auth.py +112 -0
- package/src/qingflow_mcp/cli/commands/builder.py +539 -0
- package/src/qingflow_mcp/cli/commands/chart.py +18 -0
- package/src/qingflow_mcp/cli/commands/common.py +62 -0
- package/src/qingflow_mcp/cli/commands/imports.py +96 -0
- package/src/qingflow_mcp/cli/commands/portal.py +25 -0
- package/src/qingflow_mcp/cli/commands/record.py +331 -0
- package/src/qingflow_mcp/cli/commands/repo.py +80 -0
- package/src/qingflow_mcp/cli/commands/task.py +141 -0
- package/src/qingflow_mcp/cli/commands/view.py +18 -0
- package/src/qingflow_mcp/cli/commands/workspace.py +110 -0
- package/src/qingflow_mcp/cli/context.py +60 -0
- package/src/qingflow_mcp/cli/formatters.py +573 -0
- package/src/qingflow_mcp/cli/json_io.py +50 -0
- package/src/qingflow_mcp/cli/main.py +186 -0
- package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
- package/src/qingflow_mcp/cli/terminal_ui.py +173 -0
- package/src/qingflow_mcp/config.py +407 -0
- package/src/qingflow_mcp/errors.py +66 -0
- package/src/qingflow_mcp/id_utils.py +49 -0
- package/src/qingflow_mcp/import_store.py +121 -0
- package/src/qingflow_mcp/json_types.py +18 -0
- package/src/qingflow_mcp/list_type_labels.py +76 -0
- package/src/qingflow_mcp/public_surface.py +243 -0
- package/src/qingflow_mcp/repository_store.py +71 -0
- package/src/qingflow_mcp/response_trim.py +841 -0
- package/src/qingflow_mcp/server.py +216 -0
- package/src/qingflow_mcp/server_app_builder.py +543 -0
- package/src/qingflow_mcp/server_app_user.py +386 -0
- package/src/qingflow_mcp/session_store.py +369 -0
- package/src/qingflow_mcp/solution/__init__.py +6 -0
- package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
- package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
- package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +495 -0
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +187 -0
- package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
- package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
- package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
- package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
- package/src/qingflow_mcp/solution/design_session.py +222 -0
- package/src/qingflow_mcp/solution/design_store.py +100 -0
- package/src/qingflow_mcp/solution/executor.py +2398 -0
- package/src/qingflow_mcp/solution/normalizer.py +23 -0
- package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
- package/src/qingflow_mcp/solution/run_store.py +244 -0
- package/src/qingflow_mcp/solution/spec_models.py +855 -0
- package/src/qingflow_mcp/tools/__init__.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +3449 -0
- package/src/qingflow_mcp/tools/app_tools.py +926 -0
- package/src/qingflow_mcp/tools/approval_tools.py +1062 -0
- package/src/qingflow_mcp/tools/auth_tools.py +1133 -0
- package/src/qingflow_mcp/tools/base.py +281 -0
- package/src/qingflow_mcp/tools/code_block_tools.py +777 -0
- package/src/qingflow_mcp/tools/custom_button_tools.py +202 -0
- package/src/qingflow_mcp/tools/directory_tools.py +675 -0
- package/src/qingflow_mcp/tools/feedback_tools.py +238 -0
- package/src/qingflow_mcp/tools/file_tools.py +409 -0
- package/src/qingflow_mcp/tools/import_tools.py +2223 -0
- package/src/qingflow_mcp/tools/navigation_tools.py +210 -0
- package/src/qingflow_mcp/tools/package_tools.py +326 -0
- package/src/qingflow_mcp/tools/portal_tools.py +158 -0
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +374 -0
- package/src/qingflow_mcp/tools/record_tools.py +14291 -0
- package/src/qingflow_mcp/tools/repository_dev_tools.py +552 -0
- package/src/qingflow_mcp/tools/resource_read_tools.py +503 -0
- package/src/qingflow_mcp/tools/role_tools.py +112 -0
- package/src/qingflow_mcp/tools/solution_tools.py +4054 -0
- package/src/qingflow_mcp/tools/task_context_tools.py +2986 -0
- package/src/qingflow_mcp/tools/task_tools.py +889 -0
- package/src/qingflow_mcp/tools/view_tools.py +335 -0
- package/src/qingflow_mcp/tools/workflow_tools.py +376 -0
- package/src/qingflow_mcp/tools/workspace_tools.py +266 -0
|
@@ -0,0 +1,1133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from mcp.server.fastmcp import FastMCP
|
|
7
|
+
|
|
8
|
+
from ..backend_client import BackendRequestContext, BackendResponse
|
|
9
|
+
from ..config import (
|
|
10
|
+
DEFAULT_PROFILE,
|
|
11
|
+
get_default_base_url,
|
|
12
|
+
get_default_qf_version,
|
|
13
|
+
get_mcporter_config_path,
|
|
14
|
+
normalize_base_url,
|
|
15
|
+
)
|
|
16
|
+
from ..errors import QingflowApiError, raise_tool_error
|
|
17
|
+
from ..session_store import SessionStore
|
|
18
|
+
from .base import ToolBase, tool_cn_name
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AuthTools(ToolBase):
|
|
22
|
+
"""认证类工具(中文名:身份与会话工具)。
|
|
23
|
+
|
|
24
|
+
主要职责:
|
|
25
|
+
1. 使用 credential 调用 /mcp/auth/context 建立会话;
|
|
26
|
+
2. 查询当前登录身份与工作区成员信息;
|
|
27
|
+
3. 退出并清理当前 profile 会话。
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, sessions: SessionStore, backend) -> None:
|
|
31
|
+
"""执行内部辅助逻辑。"""
|
|
32
|
+
super().__init__(sessions, backend)
|
|
33
|
+
|
|
34
|
+
def register(self, mcp: FastMCP) -> None:
|
|
35
|
+
"""注册当前工具到 MCP 服务。"""
|
|
36
|
+
@mcp.tool(
|
|
37
|
+
description=(
|
|
38
|
+
"类型:认证工具;中文名:凭证登录。"
|
|
39
|
+
"用途:使用 createClaw 提供的 credential 交换上下文并建立本地会话。"
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
def auth_use_credential(
|
|
43
|
+
profile: str = DEFAULT_PROFILE,
|
|
44
|
+
base_url: str | None = None,
|
|
45
|
+
qf_version: str | None = None,
|
|
46
|
+
credential: str = "",
|
|
47
|
+
persist: bool = False,
|
|
48
|
+
) -> dict[str, Any]:
|
|
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
|
+
)
|
|
56
|
+
|
|
57
|
+
@mcp.tool(
|
|
58
|
+
description=(
|
|
59
|
+
"类型:认证工具;中文名:我的身份。"
|
|
60
|
+
"用途:查看当前 profile 的登录身份、工作区与权限信息。"
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
def auth_whoami(profile: str = DEFAULT_PROFILE) -> dict[str, Any]:
|
|
64
|
+
return self.auth_whoami(profile=profile)
|
|
65
|
+
|
|
66
|
+
@mcp.tool(
|
|
67
|
+
description=(
|
|
68
|
+
"类型:认证工具;中文名:退出登录。"
|
|
69
|
+
"用途:退出当前 profile,并可选清理持久化会话。"
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
def auth_logout(profile: str = DEFAULT_PROFILE, forget_persisted: bool = False) -> dict[str, Any]:
|
|
73
|
+
return self.auth_logout(profile=profile, forget_persisted=forget_persisted)
|
|
74
|
+
|
|
75
|
+
@tool_cn_name("凭证登录")
|
|
76
|
+
def auth_use_credential(
|
|
77
|
+
self,
|
|
78
|
+
*,
|
|
79
|
+
profile: str = DEFAULT_PROFILE,
|
|
80
|
+
base_url: str | None = None,
|
|
81
|
+
qf_version: str | None = None,
|
|
82
|
+
credential: str | None = None,
|
|
83
|
+
persist: bool = False,
|
|
84
|
+
) -> dict[str, Any]:
|
|
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)
|
|
91
|
+
normalized_qf_version, qf_version_source = self._resolve_qf_version_input(qf_version)
|
|
92
|
+
normalized_credential = str(resolved_credential).strip()
|
|
93
|
+
if not normalized_credential:
|
|
94
|
+
raise_tool_error(
|
|
95
|
+
QingflowApiError.config_error(
|
|
96
|
+
"credential is required or configure ~/.openclaw/workspace/config/mcporter.json "
|
|
97
|
+
"with mcpServers.qingflow.headers.x-qingflow-client-id"
|
|
98
|
+
)
|
|
99
|
+
)
|
|
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"))
|
|
107
|
+
if not token:
|
|
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"))
|
|
111
|
+
resolved_qf_version, resolved_qf_version_source = self._resolve_backend_qf_version(
|
|
112
|
+
response_qf_version or detected_qf_version,
|
|
113
|
+
fallback_qf_version=normalized_qf_version,
|
|
114
|
+
fallback_source=qf_version_source,
|
|
115
|
+
)
|
|
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"))
|
|
124
|
+
session_profile = self.sessions.save_session(
|
|
125
|
+
profile=profile,
|
|
126
|
+
base_url=resolved_base_url,
|
|
127
|
+
qf_version=resolved_qf_version,
|
|
128
|
+
qf_version_source=resolved_qf_version_source,
|
|
129
|
+
token=token,
|
|
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
|
+
),
|
|
139
|
+
persist=persist,
|
|
140
|
+
)
|
|
141
|
+
session_profile = self.sessions.select_workspace(profile, ws_id=selected_ws_id, ws_name=selected_ws_name)
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
"profile": session_profile.profile,
|
|
145
|
+
"base_url": session_profile.base_url,
|
|
146
|
+
"qf_version": session_profile.qf_version,
|
|
147
|
+
"qf_version_source": session_profile.qf_version_source,
|
|
148
|
+
"uid": session_profile.uid,
|
|
149
|
+
"email": session_profile.email,
|
|
150
|
+
"nick_name": session_profile.nick_name,
|
|
151
|
+
"selected_ws_id": session_profile.selected_ws_id,
|
|
152
|
+
"selected_ws_name": session_profile.selected_ws_name,
|
|
153
|
+
"suggested_ws_id": session_profile.selected_ws_id,
|
|
154
|
+
"suggested_ws_name": session_profile.selected_ws_name,
|
|
155
|
+
"persisted": session_profile.persisted,
|
|
156
|
+
"request_route": self._request_route_payload(
|
|
157
|
+
BackendRequestContext(
|
|
158
|
+
base_url=session_profile.base_url,
|
|
159
|
+
token=token,
|
|
160
|
+
ws_id=session_profile.selected_ws_id,
|
|
161
|
+
qf_version=session_profile.qf_version,
|
|
162
|
+
qf_version_source=session_profile.qf_version_source,
|
|
163
|
+
)
|
|
164
|
+
),
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
def auth_use_token(
|
|
168
|
+
self,
|
|
169
|
+
*,
|
|
170
|
+
profile: str = DEFAULT_PROFILE,
|
|
171
|
+
base_url: str | None = None,
|
|
172
|
+
qf_version: str | None = None,
|
|
173
|
+
token: str | None = None,
|
|
174
|
+
login_token: str | None = None,
|
|
175
|
+
persist: bool = False,
|
|
176
|
+
user_info: dict[str, Any] | None = None,
|
|
177
|
+
) -> dict[str, Any]:
|
|
178
|
+
"""使用已获得的 Qingflow token 建立本地会话。"""
|
|
179
|
+
normalized_base_url = self._normalize_base_url(base_url)
|
|
180
|
+
normalized_qf_version, qf_version_source = self._resolve_qf_version_input(qf_version)
|
|
181
|
+
normalized_token = self._normalize_text(token)
|
|
182
|
+
normalized_login_token = self._normalize_text(login_token)
|
|
183
|
+
if not normalized_token:
|
|
184
|
+
raise_tool_error(QingflowApiError.config_error("token is required"))
|
|
185
|
+
|
|
186
|
+
resolved_user_info = user_info if isinstance(user_info, dict) else None
|
|
187
|
+
response_qf_version: str | None = None
|
|
188
|
+
if resolved_user_info is None:
|
|
189
|
+
resolved_user_info, response_qf_version = self._fetch_user_info(
|
|
190
|
+
normalized_base_url,
|
|
191
|
+
normalized_token,
|
|
192
|
+
None,
|
|
193
|
+
qf_version=normalized_qf_version,
|
|
194
|
+
qf_version_source=qf_version_source,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
last_workspace = resolved_user_info.get("lastWsInfo")
|
|
198
|
+
selected_ws_id = self._coerce_positive_int(
|
|
199
|
+
last_workspace.get("wsId") if isinstance(last_workspace, dict) else None
|
|
200
|
+
)
|
|
201
|
+
selected_ws_name = self._normalize_text(
|
|
202
|
+
(last_workspace.get("wsName") if isinstance(last_workspace, dict) else None)
|
|
203
|
+
or (last_workspace.get("workspaceName") if isinstance(last_workspace, dict) else None)
|
|
204
|
+
or (last_workspace.get("remark") if isinstance(last_workspace, dict) else None)
|
|
205
|
+
)
|
|
206
|
+
workspace_qf_version = (
|
|
207
|
+
self._workspace_system_version(last_workspace) if isinstance(last_workspace, dict) else None
|
|
208
|
+
)
|
|
209
|
+
if selected_ws_id is None:
|
|
210
|
+
fallback_workspace, fallback_qf_version = self._fetch_first_workspace(
|
|
211
|
+
normalized_base_url,
|
|
212
|
+
normalized_token,
|
|
213
|
+
qf_version=normalized_qf_version,
|
|
214
|
+
qf_version_source=qf_version_source,
|
|
215
|
+
)
|
|
216
|
+
if isinstance(fallback_workspace, dict):
|
|
217
|
+
selected_ws_id = self._coerce_positive_int(fallback_workspace.get("wsId"))
|
|
218
|
+
selected_ws_name = self._normalize_text(
|
|
219
|
+
fallback_workspace.get("workspaceName")
|
|
220
|
+
or fallback_workspace.get("wsName")
|
|
221
|
+
or fallback_workspace.get("remark")
|
|
222
|
+
) or selected_ws_name
|
|
223
|
+
workspace_qf_version = self._workspace_system_version(fallback_workspace) or fallback_qf_version
|
|
224
|
+
elif selected_ws_name is None or workspace_qf_version is None:
|
|
225
|
+
workspace = self._fetch_workspace_with_name_fallback(
|
|
226
|
+
normalized_base_url,
|
|
227
|
+
normalized_token,
|
|
228
|
+
selected_ws_id,
|
|
229
|
+
qf_version=normalized_qf_version,
|
|
230
|
+
qf_version_source=qf_version_source,
|
|
231
|
+
)
|
|
232
|
+
if isinstance(workspace, dict):
|
|
233
|
+
selected_ws_name = self._normalize_text(
|
|
234
|
+
workspace.get("workspaceName")
|
|
235
|
+
or workspace.get("wsName")
|
|
236
|
+
or workspace.get("remark")
|
|
237
|
+
) or selected_ws_name
|
|
238
|
+
workspace_qf_version = self._workspace_system_version(workspace) or workspace_qf_version
|
|
239
|
+
|
|
240
|
+
if workspace_qf_version is not None:
|
|
241
|
+
resolved_qf_version, resolved_qf_version_source = workspace_qf_version, "workspace_system_version"
|
|
242
|
+
else:
|
|
243
|
+
resolved_qf_version, resolved_qf_version_source = self._resolve_backend_qf_version(
|
|
244
|
+
response_qf_version,
|
|
245
|
+
fallback_qf_version=normalized_qf_version,
|
|
246
|
+
fallback_source=qf_version_source,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
uid = self._coerce_positive_int(resolved_user_info.get("uid"))
|
|
250
|
+
if uid is None:
|
|
251
|
+
raise_tool_error(QingflowApiError(category="auth", message="Token validation did not return valid user info"))
|
|
252
|
+
|
|
253
|
+
session_profile = self.sessions.save_session(
|
|
254
|
+
profile=profile,
|
|
255
|
+
base_url=normalized_base_url,
|
|
256
|
+
qf_version=resolved_qf_version,
|
|
257
|
+
qf_version_source=resolved_qf_version_source,
|
|
258
|
+
token=normalized_token,
|
|
259
|
+
login_token=normalized_login_token,
|
|
260
|
+
credential=None,
|
|
261
|
+
uid=uid,
|
|
262
|
+
email=self._normalize_text(resolved_user_info.get("email")),
|
|
263
|
+
nick_name=self._normalize_text(
|
|
264
|
+
resolved_user_info.get("nickName")
|
|
265
|
+
or resolved_user_info.get("displayName")
|
|
266
|
+
or resolved_user_info.get("name")
|
|
267
|
+
),
|
|
268
|
+
persist=persist,
|
|
269
|
+
)
|
|
270
|
+
if selected_ws_id is not None:
|
|
271
|
+
session_profile = self.sessions.select_workspace(profile, ws_id=selected_ws_id, ws_name=selected_ws_name)
|
|
272
|
+
backend_session = self.sessions.get_backend_session(profile)
|
|
273
|
+
permission_level = (
|
|
274
|
+
self._workspace_permission_level(
|
|
275
|
+
session_profile=session_profile,
|
|
276
|
+
backend_session=backend_session,
|
|
277
|
+
)
|
|
278
|
+
if backend_session is not None
|
|
279
|
+
else None
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
"profile": session_profile.profile,
|
|
284
|
+
"base_url": session_profile.base_url,
|
|
285
|
+
"qf_version": session_profile.qf_version,
|
|
286
|
+
"qf_version_source": session_profile.qf_version_source,
|
|
287
|
+
"uid": session_profile.uid,
|
|
288
|
+
"email": session_profile.email,
|
|
289
|
+
"nick_name": session_profile.nick_name,
|
|
290
|
+
"selected_ws_id": session_profile.selected_ws_id,
|
|
291
|
+
"selected_ws_name": session_profile.selected_ws_name,
|
|
292
|
+
"suggested_ws_id": session_profile.selected_ws_id,
|
|
293
|
+
"suggested_ws_name": session_profile.selected_ws_name,
|
|
294
|
+
"permission_level": permission_level,
|
|
295
|
+
"persisted": session_profile.persisted,
|
|
296
|
+
"request_route": self._request_route_payload(
|
|
297
|
+
BackendRequestContext(
|
|
298
|
+
base_url=session_profile.base_url,
|
|
299
|
+
token=normalized_token,
|
|
300
|
+
ws_id=session_profile.selected_ws_id,
|
|
301
|
+
qf_version=session_profile.qf_version,
|
|
302
|
+
qf_version_source=session_profile.qf_version_source,
|
|
303
|
+
)
|
|
304
|
+
),
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
def _resolve_mcporter_auth_inputs(self, *, base_url: str | None, credential: str | None) -> tuple[str | None, str]:
|
|
308
|
+
"""从参数或 mcporter 配置解析登录所需 base_url 与 credential。"""
|
|
309
|
+
normalized_base_url = self._normalize_text(base_url)
|
|
310
|
+
normalized_credential = self._normalize_text(credential)
|
|
311
|
+
if normalized_base_url and normalized_credential:
|
|
312
|
+
return normalized_base_url, normalized_credential
|
|
313
|
+
|
|
314
|
+
mcporter_context = self._read_mcporter_qingflow_context()
|
|
315
|
+
if not normalized_base_url:
|
|
316
|
+
normalized_base_url = self._normalize_text(mcporter_context.get("base_url"))
|
|
317
|
+
if not normalized_credential:
|
|
318
|
+
normalized_credential = self._normalize_text(mcporter_context.get("credential"))
|
|
319
|
+
return normalized_base_url, normalized_credential or ""
|
|
320
|
+
|
|
321
|
+
def _read_mcporter_qingflow_context(self) -> dict[str, str]:
|
|
322
|
+
"""读取 OpenClaw mcporter 中的 Qingflow MCP 上下文。"""
|
|
323
|
+
path = get_mcporter_config_path()
|
|
324
|
+
if not path.exists():
|
|
325
|
+
return {}
|
|
326
|
+
try:
|
|
327
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
328
|
+
payload = json.load(handle)
|
|
329
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
330
|
+
raise_tool_error(QingflowApiError.config_error(f"failed to read mcporter config '{path}': {exc}"))
|
|
331
|
+
|
|
332
|
+
if not isinstance(payload, dict):
|
|
333
|
+
raise_tool_error(QingflowApiError.config_error(f"mcporter config '{path}' must be a JSON object"))
|
|
334
|
+
mcp_servers = payload.get("mcpServers")
|
|
335
|
+
qingflow = mcp_servers.get("qingflow") if isinstance(mcp_servers, dict) else None
|
|
336
|
+
if not isinstance(qingflow, dict):
|
|
337
|
+
return {}
|
|
338
|
+
headers = qingflow.get("headers")
|
|
339
|
+
credential = None
|
|
340
|
+
if isinstance(headers, dict):
|
|
341
|
+
credential = headers.get("x-qingflow-client-id")
|
|
342
|
+
return {
|
|
343
|
+
"base_url": str(qingflow.get("url") or "").strip(),
|
|
344
|
+
"credential": str(credential or "").strip(),
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
@tool_cn_name("我的身份")
|
|
348
|
+
def auth_whoami(self, *, profile: str = DEFAULT_PROFILE) -> dict[str, Any]:
|
|
349
|
+
"""执行认证与会话相关逻辑。"""
|
|
350
|
+
def build_response(
|
|
351
|
+
session_profile, # type: ignore[no-untyped-def]
|
|
352
|
+
backend_session, # type: ignore[no-untyped-def]
|
|
353
|
+
context: BackendRequestContext,
|
|
354
|
+
) -> dict[str, Any]:
|
|
355
|
+
workspace, workspace_qf_version = self._selected_workspace_snapshot(
|
|
356
|
+
session_profile=session_profile,
|
|
357
|
+
backend_session=backend_session,
|
|
358
|
+
)
|
|
359
|
+
resolved_qf_version = workspace_qf_version or session_profile.qf_version
|
|
360
|
+
resolved_qf_version_source = (
|
|
361
|
+
"workspace_system_version"
|
|
362
|
+
if workspace_qf_version is not None
|
|
363
|
+
else session_profile.qf_version_source
|
|
364
|
+
)
|
|
365
|
+
if (
|
|
366
|
+
workspace_qf_version is not None
|
|
367
|
+
and (
|
|
368
|
+
workspace_qf_version != session_profile.qf_version
|
|
369
|
+
or session_profile.qf_version_source != "workspace_system_version"
|
|
370
|
+
)
|
|
371
|
+
):
|
|
372
|
+
session_profile = self.sessions.update_route(
|
|
373
|
+
profile,
|
|
374
|
+
qf_version=workspace_qf_version,
|
|
375
|
+
qf_version_source="workspace_system_version",
|
|
376
|
+
)
|
|
377
|
+
backend_session = self.sessions.get_backend_session(profile) or backend_session
|
|
378
|
+
context = BackendRequestContext(
|
|
379
|
+
base_url=backend_session.base_url,
|
|
380
|
+
token=backend_session.token,
|
|
381
|
+
ws_id=session_profile.selected_ws_id,
|
|
382
|
+
qf_version=backend_session.qf_version,
|
|
383
|
+
qf_version_source=backend_session.qf_version_source,
|
|
384
|
+
)
|
|
385
|
+
if self._should_refresh_identity_metadata(session_profile):
|
|
386
|
+
refreshed_profile = self._refresh_identity_metadata(
|
|
387
|
+
profile=profile,
|
|
388
|
+
session_profile=session_profile,
|
|
389
|
+
backend_session=backend_session,
|
|
390
|
+
context=context,
|
|
391
|
+
)
|
|
392
|
+
if refreshed_profile is not None:
|
|
393
|
+
session_profile = refreshed_profile
|
|
394
|
+
response = {
|
|
395
|
+
"profile": session_profile.profile,
|
|
396
|
+
"base_url": session_profile.base_url,
|
|
397
|
+
"qf_version": resolved_qf_version,
|
|
398
|
+
"qf_version_source": resolved_qf_version_source,
|
|
399
|
+
"uid": session_profile.uid,
|
|
400
|
+
"email": session_profile.email,
|
|
401
|
+
"nick_name": session_profile.nick_name,
|
|
402
|
+
"selected_ws_id": session_profile.selected_ws_id,
|
|
403
|
+
"selected_ws_name": session_profile.selected_ws_name,
|
|
404
|
+
"persisted": session_profile.persisted,
|
|
405
|
+
"request_route": self._request_route_payload(context),
|
|
406
|
+
}
|
|
407
|
+
member_info, member_warnings = self._workspace_member_info(
|
|
408
|
+
session_profile=session_profile,
|
|
409
|
+
backend_session=backend_session,
|
|
410
|
+
)
|
|
411
|
+
response.update(member_info)
|
|
412
|
+
if member_warnings:
|
|
413
|
+
response["warnings"] = member_warnings
|
|
414
|
+
return response
|
|
415
|
+
|
|
416
|
+
session_profile = None
|
|
417
|
+
backend_session = None
|
|
418
|
+
try:
|
|
419
|
+
session_profile, backend_session, context = self._require_context(profile, require_workspace=False)
|
|
420
|
+
if backend_session.credential:
|
|
421
|
+
self._probe_token_validity(
|
|
422
|
+
session_profile=session_profile,
|
|
423
|
+
backend_session=backend_session,
|
|
424
|
+
)
|
|
425
|
+
return build_response(session_profile, backend_session, context)
|
|
426
|
+
except QingflowApiError as error:
|
|
427
|
+
if (
|
|
428
|
+
error.looks_like_invalid_token()
|
|
429
|
+
and session_profile is not None
|
|
430
|
+
and backend_session is not None
|
|
431
|
+
and self._refresh_session_from_credential(
|
|
432
|
+
profile,
|
|
433
|
+
session_profile=session_profile,
|
|
434
|
+
backend_session=backend_session,
|
|
435
|
+
)
|
|
436
|
+
):
|
|
437
|
+
try:
|
|
438
|
+
refreshed_profile, refreshed_backend_session, refreshed_context = self._require_context(
|
|
439
|
+
profile,
|
|
440
|
+
require_workspace=False,
|
|
441
|
+
)
|
|
442
|
+
return build_response(refreshed_profile, refreshed_backend_session, refreshed_context)
|
|
443
|
+
except QingflowApiError as refreshed_error:
|
|
444
|
+
self._handle_error(profile, refreshed_error)
|
|
445
|
+
self._handle_error(profile, error)
|
|
446
|
+
raise AssertionError("unreachable")
|
|
447
|
+
|
|
448
|
+
def _probe_token_validity(
|
|
449
|
+
self,
|
|
450
|
+
*,
|
|
451
|
+
session_profile, # type: ignore[no-untyped-def]
|
|
452
|
+
backend_session, # type: ignore[no-untyped-def]
|
|
453
|
+
) -> None:
|
|
454
|
+
"""执行内部辅助逻辑。"""
|
|
455
|
+
probe_context = BackendRequestContext(
|
|
456
|
+
base_url=backend_session.base_url,
|
|
457
|
+
token=backend_session.token,
|
|
458
|
+
ws_id=session_profile.selected_ws_id,
|
|
459
|
+
qf_version=backend_session.qf_version,
|
|
460
|
+
qf_version_source=backend_session.qf_version_source,
|
|
461
|
+
)
|
|
462
|
+
try:
|
|
463
|
+
self.backend.request("GET", probe_context, "/user")
|
|
464
|
+
except QingflowApiError as error:
|
|
465
|
+
if error.looks_like_invalid_token():
|
|
466
|
+
raise
|
|
467
|
+
|
|
468
|
+
@tool_cn_name("退出登录")
|
|
469
|
+
def auth_logout(self, *, profile: str = DEFAULT_PROFILE, forget_persisted: bool = False) -> dict[str, Any]:
|
|
470
|
+
"""执行认证与会话相关逻辑。"""
|
|
471
|
+
if not self.sessions.has_profile(profile):
|
|
472
|
+
raise_tool_error(QingflowApiError.auth_required(profile))
|
|
473
|
+
self.sessions.logout(profile, forget_persisted=forget_persisted)
|
|
474
|
+
return {
|
|
475
|
+
"profile": profile,
|
|
476
|
+
"logged_out": True,
|
|
477
|
+
"forgot_persisted": forget_persisted,
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
def _normalize_base_url(self, base_url: str | None) -> str:
|
|
481
|
+
"""执行内部辅助逻辑。"""
|
|
482
|
+
normalized_base_url = normalize_base_url(base_url) or get_default_base_url()
|
|
483
|
+
if not normalized_base_url:
|
|
484
|
+
raise_tool_error(
|
|
485
|
+
QingflowApiError.config_error(
|
|
486
|
+
"base_url is required or configure default_base_url / QINGFLOW_MCP_DEFAULT_BASE_URL"
|
|
487
|
+
)
|
|
488
|
+
)
|
|
489
|
+
return normalized_base_url
|
|
490
|
+
|
|
491
|
+
def _normalize_qf_version(self, qf_version: str | None) -> str | None:
|
|
492
|
+
"""执行内部辅助逻辑。"""
|
|
493
|
+
if qf_version is not None:
|
|
494
|
+
normalized = str(qf_version).strip()
|
|
495
|
+
return normalized or None
|
|
496
|
+
return get_default_qf_version()
|
|
497
|
+
|
|
498
|
+
def _resolve_qf_version_input(self, qf_version: str | None) -> tuple[str | None, str]:
|
|
499
|
+
"""执行内部辅助逻辑。"""
|
|
500
|
+
if qf_version is not None:
|
|
501
|
+
normalized = self._normalize_qf_version(qf_version)
|
|
502
|
+
return normalized, "explicit" if normalized else "unset"
|
|
503
|
+
normalized = self._normalize_qf_version(None)
|
|
504
|
+
if normalized:
|
|
505
|
+
return normalized, "default_config"
|
|
506
|
+
return None, "unset"
|
|
507
|
+
|
|
508
|
+
def _resolve_backend_qf_version(
|
|
509
|
+
self,
|
|
510
|
+
backend_qf_version: str | None,
|
|
511
|
+
*,
|
|
512
|
+
fallback_qf_version: str | None,
|
|
513
|
+
fallback_source: str,
|
|
514
|
+
) -> tuple[str | None, str]:
|
|
515
|
+
"""执行内部辅助逻辑。"""
|
|
516
|
+
if backend_qf_version:
|
|
517
|
+
return backend_qf_version, "backend_response"
|
|
518
|
+
return fallback_qf_version, fallback_source
|
|
519
|
+
|
|
520
|
+
def _fetch_auth_context(
|
|
521
|
+
self,
|
|
522
|
+
base_url: str,
|
|
523
|
+
credential: str,
|
|
524
|
+
*,
|
|
525
|
+
qf_version: str | None,
|
|
526
|
+
) -> tuple[dict[str, Any], str | None]:
|
|
527
|
+
"""执行内部辅助逻辑。"""
|
|
528
|
+
response = self.backend.public_request_with_meta(
|
|
529
|
+
"POST",
|
|
530
|
+
base_url,
|
|
531
|
+
"/mcp/auth/context",
|
|
532
|
+
json_body={"credential": credential},
|
|
533
|
+
qf_version=qf_version,
|
|
534
|
+
)
|
|
535
|
+
payload = self._unwrap_auth_context_payload(response.data)
|
|
536
|
+
return payload, response.qf_response_version
|
|
537
|
+
|
|
538
|
+
def _unwrap_auth_context_payload(self, payload: Any) -> dict[str, Any]:
|
|
539
|
+
"""执行内部辅助逻辑。"""
|
|
540
|
+
if not isinstance(payload, dict):
|
|
541
|
+
raise_tool_error(QingflowApiError(category="auth", message="Credential context did not return a valid result"))
|
|
542
|
+
for key in ("data", "result"):
|
|
543
|
+
nested = payload.get(key)
|
|
544
|
+
if isinstance(nested, dict):
|
|
545
|
+
return nested
|
|
546
|
+
return payload
|
|
547
|
+
|
|
548
|
+
def _workspace_system_version(self, workspace: Any) -> str | None:
|
|
549
|
+
"""执行内部辅助逻辑。"""
|
|
550
|
+
if not isinstance(workspace, dict):
|
|
551
|
+
return None
|
|
552
|
+
value = workspace.get("systemVersion")
|
|
553
|
+
if value is None:
|
|
554
|
+
return None
|
|
555
|
+
normalized = str(value).strip()
|
|
556
|
+
return normalized or None
|
|
557
|
+
|
|
558
|
+
def _fetch_user_info(
|
|
559
|
+
self,
|
|
560
|
+
base_url: str,
|
|
561
|
+
token: str,
|
|
562
|
+
ws_id: int | None,
|
|
563
|
+
*,
|
|
564
|
+
qf_version: str | None,
|
|
565
|
+
qf_version_source: str | None,
|
|
566
|
+
) -> tuple[dict[str, Any], str | None]:
|
|
567
|
+
"""执行内部辅助逻辑。"""
|
|
568
|
+
request_context = BackendRequestContext(
|
|
569
|
+
base_url=base_url,
|
|
570
|
+
token=token,
|
|
571
|
+
ws_id=ws_id,
|
|
572
|
+
qf_version=qf_version,
|
|
573
|
+
qf_version_source=qf_version_source,
|
|
574
|
+
)
|
|
575
|
+
try:
|
|
576
|
+
user_response = self.backend.request_with_meta("GET", request_context, "/user")
|
|
577
|
+
user_info = user_response.data
|
|
578
|
+
if isinstance(user_info, dict):
|
|
579
|
+
return user_info, user_response.qf_response_version
|
|
580
|
+
except QingflowApiError as original_error:
|
|
581
|
+
if ws_id is not None:
|
|
582
|
+
raise original_error
|
|
583
|
+
first_workspace, workspace_qf_version = self._fetch_first_workspace(
|
|
584
|
+
base_url,
|
|
585
|
+
token,
|
|
586
|
+
qf_version=qf_version,
|
|
587
|
+
qf_version_source=qf_version_source,
|
|
588
|
+
)
|
|
589
|
+
if not first_workspace:
|
|
590
|
+
raise original_error
|
|
591
|
+
first_ws_id = first_workspace.get("wsId")
|
|
592
|
+
if not first_ws_id:
|
|
593
|
+
raise original_error
|
|
594
|
+
effective_qf_version = workspace_qf_version or qf_version
|
|
595
|
+
effective_qf_version_source = "backend_response" if workspace_qf_version else qf_version_source
|
|
596
|
+
fallback_context = BackendRequestContext(
|
|
597
|
+
base_url=base_url,
|
|
598
|
+
token=token,
|
|
599
|
+
ws_id=int(first_ws_id),
|
|
600
|
+
qf_version=effective_qf_version,
|
|
601
|
+
qf_version_source=effective_qf_version_source,
|
|
602
|
+
)
|
|
603
|
+
user_response = self.backend.request_with_meta("GET", fallback_context, "/user")
|
|
604
|
+
user_info = user_response.data
|
|
605
|
+
if isinstance(user_info, dict):
|
|
606
|
+
return user_info, user_response.qf_response_version or effective_qf_version
|
|
607
|
+
raise original_error
|
|
608
|
+
raise_tool_error(QingflowApiError(category="auth", message="Token validation did not return valid user info"))
|
|
609
|
+
|
|
610
|
+
def _try_fetch_user_info(
|
|
611
|
+
self,
|
|
612
|
+
base_url: str,
|
|
613
|
+
token: str,
|
|
614
|
+
*,
|
|
615
|
+
qf_version: str | None,
|
|
616
|
+
qf_version_source: str | None,
|
|
617
|
+
) -> tuple[dict[str, Any] | None, str | None]:
|
|
618
|
+
"""执行内部辅助逻辑。"""
|
|
619
|
+
try:
|
|
620
|
+
return self._fetch_user_info(
|
|
621
|
+
base_url,
|
|
622
|
+
token,
|
|
623
|
+
None,
|
|
624
|
+
qf_version=qf_version,
|
|
625
|
+
qf_version_source=qf_version_source,
|
|
626
|
+
)
|
|
627
|
+
except QingflowApiError:
|
|
628
|
+
return None, None
|
|
629
|
+
|
|
630
|
+
def _fetch_first_workspace(
|
|
631
|
+
self,
|
|
632
|
+
base_url: str,
|
|
633
|
+
token: str,
|
|
634
|
+
*,
|
|
635
|
+
qf_version: str | None,
|
|
636
|
+
qf_version_source: str | None,
|
|
637
|
+
) -> tuple[dict[str, Any] | None, str | None]:
|
|
638
|
+
"""执行内部辅助逻辑。"""
|
|
639
|
+
page_response = self.backend.request_with_meta(
|
|
640
|
+
"POST",
|
|
641
|
+
BackendRequestContext(
|
|
642
|
+
base_url=base_url,
|
|
643
|
+
token=token,
|
|
644
|
+
ws_id=None,
|
|
645
|
+
qf_version=qf_version,
|
|
646
|
+
qf_version_source=qf_version_source,
|
|
647
|
+
),
|
|
648
|
+
"/user/workspaceList/pageQuery",
|
|
649
|
+
json_body={"pageNum": 1, "pageSize": 1},
|
|
650
|
+
)
|
|
651
|
+
page = page_response.data
|
|
652
|
+
if not isinstance(page, dict):
|
|
653
|
+
return None, page_response.qf_response_version
|
|
654
|
+
workspaces = page.get("list") or []
|
|
655
|
+
if not workspaces:
|
|
656
|
+
return None, page_response.qf_response_version
|
|
657
|
+
first_workspace = workspaces[0]
|
|
658
|
+
return (first_workspace if isinstance(first_workspace, dict) else None), page_response.qf_response_version
|
|
659
|
+
|
|
660
|
+
def _fetch_workspace(
|
|
661
|
+
self,
|
|
662
|
+
base_url: str,
|
|
663
|
+
token: str,
|
|
664
|
+
ws_id: int,
|
|
665
|
+
*,
|
|
666
|
+
qf_version: str | None,
|
|
667
|
+
qf_version_source: str | None,
|
|
668
|
+
) -> dict[str, Any]:
|
|
669
|
+
"""执行内部辅助逻辑。"""
|
|
670
|
+
workspace = self.backend.request(
|
|
671
|
+
"GET",
|
|
672
|
+
BackendRequestContext(
|
|
673
|
+
base_url=base_url,
|
|
674
|
+
token=token,
|
|
675
|
+
ws_id=None,
|
|
676
|
+
qf_version=qf_version,
|
|
677
|
+
qf_version_source=qf_version_source,
|
|
678
|
+
),
|
|
679
|
+
f"/user/workspace/{ws_id}",
|
|
680
|
+
)
|
|
681
|
+
if not isinstance(workspace, dict):
|
|
682
|
+
raise_tool_error(QingflowApiError(category="workspace", message=f"Workspace {ws_id} is not accessible"))
|
|
683
|
+
return workspace
|
|
684
|
+
|
|
685
|
+
def _selected_workspace_snapshot(
|
|
686
|
+
self,
|
|
687
|
+
*,
|
|
688
|
+
session_profile, # type: ignore[no-untyped-def]
|
|
689
|
+
backend_session, # type: ignore[no-untyped-def]
|
|
690
|
+
) -> tuple[dict[str, Any] | None, str | None]:
|
|
691
|
+
ws_id = session_profile.selected_ws_id
|
|
692
|
+
if ws_id is None:
|
|
693
|
+
return None, None
|
|
694
|
+
workspace = self._fetch_workspace_with_name_fallback(
|
|
695
|
+
session_profile.base_url,
|
|
696
|
+
backend_session.token,
|
|
697
|
+
ws_id,
|
|
698
|
+
qf_version=session_profile.qf_version,
|
|
699
|
+
qf_version_source=session_profile.qf_version_source,
|
|
700
|
+
)
|
|
701
|
+
return workspace, self._workspace_system_version(workspace)
|
|
702
|
+
|
|
703
|
+
def _request_route_payload(self, context: BackendRequestContext) -> dict[str, Any]:
|
|
704
|
+
"""执行内部辅助逻辑。"""
|
|
705
|
+
describe_route = getattr(self.backend, "describe_route", None)
|
|
706
|
+
if callable(describe_route):
|
|
707
|
+
payload = describe_route(context)
|
|
708
|
+
if isinstance(payload, dict):
|
|
709
|
+
return payload
|
|
710
|
+
return {
|
|
711
|
+
"base_url": context.base_url,
|
|
712
|
+
"qf_version": context.qf_version,
|
|
713
|
+
"qf_version_source": context.qf_version_source or ("context" if context.qf_version else "unknown"),
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
def _should_refresh_identity_metadata(self, session_profile) -> bool: # type: ignore[no-untyped-def]
|
|
717
|
+
"""执行内部辅助逻辑。"""
|
|
718
|
+
return (
|
|
719
|
+
session_profile.uid == 0
|
|
720
|
+
or session_profile.email is None
|
|
721
|
+
or session_profile.nick_name is None
|
|
722
|
+
or session_profile.selected_ws_name is None
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
def _refresh_identity_metadata(
|
|
726
|
+
self,
|
|
727
|
+
*,
|
|
728
|
+
profile: str,
|
|
729
|
+
session_profile, # type: ignore[no-untyped-def]
|
|
730
|
+
backend_session, # type: ignore[no-untyped-def]
|
|
731
|
+
context: BackendRequestContext,
|
|
732
|
+
):
|
|
733
|
+
"""执行内部辅助逻辑。"""
|
|
734
|
+
try:
|
|
735
|
+
user_info, _ = self._fetch_user_info(
|
|
736
|
+
session_profile.base_url,
|
|
737
|
+
backend_session.token,
|
|
738
|
+
session_profile.selected_ws_id,
|
|
739
|
+
qf_version=session_profile.qf_version,
|
|
740
|
+
qf_version_source=session_profile.qf_version_source,
|
|
741
|
+
)
|
|
742
|
+
except QingflowApiError:
|
|
743
|
+
return None
|
|
744
|
+
|
|
745
|
+
ws_name = session_profile.selected_ws_name
|
|
746
|
+
if session_profile.selected_ws_id is not None:
|
|
747
|
+
workspace = self._fetch_workspace_with_name_fallback(
|
|
748
|
+
session_profile.base_url,
|
|
749
|
+
backend_session.token,
|
|
750
|
+
session_profile.selected_ws_id,
|
|
751
|
+
qf_version=session_profile.qf_version,
|
|
752
|
+
qf_version_source=session_profile.qf_version_source,
|
|
753
|
+
)
|
|
754
|
+
if isinstance(workspace, dict):
|
|
755
|
+
ws_name = (
|
|
756
|
+
str(workspace.get("workspaceName") or workspace.get("wsName") or workspace.get("remark") or "").strip()
|
|
757
|
+
or ws_name
|
|
758
|
+
)
|
|
759
|
+
email = user_info["email"] if "email" in user_info else session_profile.email
|
|
760
|
+
nick_name = (
|
|
761
|
+
user_info.get("nickName")
|
|
762
|
+
or user_info.get("displayName")
|
|
763
|
+
or user_info.get("name")
|
|
764
|
+
or session_profile.nick_name
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
uid = user_info.get("uid")
|
|
768
|
+
refreshed = self.sessions.update_profile_metadata(
|
|
769
|
+
profile,
|
|
770
|
+
uid=int(uid) if uid is not None else session_profile.uid,
|
|
771
|
+
email=email,
|
|
772
|
+
nick_name=nick_name,
|
|
773
|
+
selected_ws_id=session_profile.selected_ws_id,
|
|
774
|
+
selected_ws_name=ws_name,
|
|
775
|
+
)
|
|
776
|
+
return refreshed
|
|
777
|
+
|
|
778
|
+
def _workspace_member_info(
|
|
779
|
+
self,
|
|
780
|
+
*,
|
|
781
|
+
session_profile, # type: ignore[no-untyped-def]
|
|
782
|
+
backend_session, # type: ignore[no-untyped-def]
|
|
783
|
+
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
784
|
+
"""执行内部辅助逻辑。"""
|
|
785
|
+
default_payload = {
|
|
786
|
+
"departments": [],
|
|
787
|
+
"roles": [],
|
|
788
|
+
"permission_level": None,
|
|
789
|
+
}
|
|
790
|
+
ws_id = session_profile.selected_ws_id
|
|
791
|
+
if ws_id is None:
|
|
792
|
+
return default_payload, []
|
|
793
|
+
|
|
794
|
+
permission_level = self._workspace_permission_level(
|
|
795
|
+
session_profile=session_profile,
|
|
796
|
+
backend_session=backend_session,
|
|
797
|
+
)
|
|
798
|
+
payload = dict(default_payload)
|
|
799
|
+
payload["permission_level"] = permission_level
|
|
800
|
+
|
|
801
|
+
context = BackendRequestContext(
|
|
802
|
+
base_url=backend_session.base_url,
|
|
803
|
+
token=backend_session.token,
|
|
804
|
+
ws_id=ws_id,
|
|
805
|
+
qf_version=backend_session.qf_version,
|
|
806
|
+
qf_version_source=backend_session.qf_version_source,
|
|
807
|
+
)
|
|
808
|
+
member = self._lookup_current_member(
|
|
809
|
+
context=context,
|
|
810
|
+
uid=session_profile.uid,
|
|
811
|
+
email=session_profile.email,
|
|
812
|
+
nick_name=session_profile.nick_name,
|
|
813
|
+
)
|
|
814
|
+
if member is None:
|
|
815
|
+
return payload, [
|
|
816
|
+
{
|
|
817
|
+
"code": "CURRENT_MEMBER_PROFILE_UNAVAILABLE",
|
|
818
|
+
"message": (
|
|
819
|
+
"auth_whoami could not resolve current member departments and roles "
|
|
820
|
+
f"in workspace {ws_id}."
|
|
821
|
+
),
|
|
822
|
+
}
|
|
823
|
+
]
|
|
824
|
+
|
|
825
|
+
payload["departments"] = self._compact_departments(member)
|
|
826
|
+
payload["roles"] = self._compact_roles(member)
|
|
827
|
+
return payload, []
|
|
828
|
+
|
|
829
|
+
def _workspace_permission_level(
|
|
830
|
+
self,
|
|
831
|
+
*,
|
|
832
|
+
session_profile, # type: ignore[no-untyped-def]
|
|
833
|
+
backend_session, # type: ignore[no-untyped-def]
|
|
834
|
+
) -> str | None:
|
|
835
|
+
"""Resolve the selected workspace permission label without requiring member lookup."""
|
|
836
|
+
ws_id = session_profile.selected_ws_id
|
|
837
|
+
if ws_id is None:
|
|
838
|
+
return None
|
|
839
|
+
context = BackendRequestContext(
|
|
840
|
+
base_url=backend_session.base_url,
|
|
841
|
+
token=backend_session.token,
|
|
842
|
+
ws_id=ws_id,
|
|
843
|
+
qf_version=backend_session.qf_version,
|
|
844
|
+
qf_version_source=backend_session.qf_version_source,
|
|
845
|
+
)
|
|
846
|
+
return self._resolve_permission_level(self._workspace_auth(context, ws_id=ws_id))
|
|
847
|
+
|
|
848
|
+
def _workspace_auth(self, context: BackendRequestContext, *, ws_id: int) -> int | None:
|
|
849
|
+
"""执行内部辅助逻辑。"""
|
|
850
|
+
workspace = self._fetch_workspace_auth_from_detail(context, ws_id=ws_id)
|
|
851
|
+
if workspace is not None:
|
|
852
|
+
return workspace
|
|
853
|
+
return self._fetch_workspace_auth_from_list(context, ws_id=ws_id)
|
|
854
|
+
|
|
855
|
+
def _fetch_workspace_auth_from_detail(self, context: BackendRequestContext, *, ws_id: int) -> int | None:
|
|
856
|
+
"""执行内部辅助逻辑。"""
|
|
857
|
+
try:
|
|
858
|
+
workspace = self.backend.request("GET", context, f"/user/workspace/{ws_id}")
|
|
859
|
+
except QingflowApiError:
|
|
860
|
+
return None
|
|
861
|
+
if not isinstance(workspace, dict):
|
|
862
|
+
return None
|
|
863
|
+
return self._coerce_auth_value(workspace.get("auth"))
|
|
864
|
+
|
|
865
|
+
def _fetch_workspace_auth_from_list(self, context: BackendRequestContext, *, ws_id: int) -> int | None:
|
|
866
|
+
"""执行内部辅助逻辑。"""
|
|
867
|
+
try:
|
|
868
|
+
payload = self.backend.request(
|
|
869
|
+
"POST",
|
|
870
|
+
context,
|
|
871
|
+
"/user/workspaceList/pageQuery",
|
|
872
|
+
json_body={"pageNum": 1, "pageSize": 100, "authList": [0, 1, 2, 3]},
|
|
873
|
+
)
|
|
874
|
+
except QingflowApiError:
|
|
875
|
+
return None
|
|
876
|
+
workspaces = payload.get("list") if isinstance(payload, dict) else []
|
|
877
|
+
if not isinstance(workspaces, list):
|
|
878
|
+
return None
|
|
879
|
+
for item in workspaces:
|
|
880
|
+
if not isinstance(item, dict) or item.get("wsId") != ws_id:
|
|
881
|
+
continue
|
|
882
|
+
return self._coerce_auth_value(item.get("auth"))
|
|
883
|
+
return None
|
|
884
|
+
|
|
885
|
+
def _lookup_current_member(
|
|
886
|
+
self,
|
|
887
|
+
*,
|
|
888
|
+
context: BackendRequestContext,
|
|
889
|
+
uid: int | None,
|
|
890
|
+
email: str | None,
|
|
891
|
+
nick_name: str | None,
|
|
892
|
+
) -> dict[str, Any] | None:
|
|
893
|
+
"""执行内部辅助逻辑。"""
|
|
894
|
+
candidates: list[dict[str, Any]] = []
|
|
895
|
+
for keyword in (email, nick_name):
|
|
896
|
+
member = self._search_member_once(context, uid=uid, keyword=keyword)
|
|
897
|
+
if member is not None:
|
|
898
|
+
return member
|
|
899
|
+
if keyword:
|
|
900
|
+
candidates.extend(self._search_member_items(context, keyword=keyword))
|
|
901
|
+
if uid is not None and uid > 0:
|
|
902
|
+
for item in candidates:
|
|
903
|
+
if self._same_member(item, uid=uid):
|
|
904
|
+
return item
|
|
905
|
+
return self._search_member_once(context, uid=uid, keyword=None)
|
|
906
|
+
return None
|
|
907
|
+
|
|
908
|
+
def _search_member_once(
|
|
909
|
+
self,
|
|
910
|
+
context: BackendRequestContext,
|
|
911
|
+
*,
|
|
912
|
+
uid: int | None,
|
|
913
|
+
keyword: str | None,
|
|
914
|
+
) -> dict[str, Any] | None:
|
|
915
|
+
"""执行内部辅助逻辑。"""
|
|
916
|
+
for item in self._search_member_items(context, keyword=keyword):
|
|
917
|
+
if self._same_member(item, uid=uid):
|
|
918
|
+
return item
|
|
919
|
+
return None
|
|
920
|
+
|
|
921
|
+
def _search_member_items(self, context: BackendRequestContext, *, keyword: str | None) -> list[dict[str, Any]]:
|
|
922
|
+
"""执行内部辅助逻辑。"""
|
|
923
|
+
params: dict[str, Any] = {"pageNum": 1, "pageSize": 100, "containDisable": True}
|
|
924
|
+
normalized_keyword = str(keyword or "").strip()
|
|
925
|
+
if normalized_keyword:
|
|
926
|
+
params["keyword"] = normalized_keyword
|
|
927
|
+
try:
|
|
928
|
+
payload = self.backend.request("GET", context, "/contact", params=params)
|
|
929
|
+
except QingflowApiError:
|
|
930
|
+
return []
|
|
931
|
+
items = self._extract_items(payload)
|
|
932
|
+
return [item for item in items if isinstance(item, dict)]
|
|
933
|
+
|
|
934
|
+
def _same_member(self, item: dict[str, Any], *, uid: int | None) -> bool:
|
|
935
|
+
"""执行内部辅助逻辑。"""
|
|
936
|
+
if uid is None or uid <= 0:
|
|
937
|
+
return False
|
|
938
|
+
for key in ("uid", "id", "userId"):
|
|
939
|
+
value = item.get(key)
|
|
940
|
+
if value is None:
|
|
941
|
+
continue
|
|
942
|
+
coerced = self._coerce_int(value)
|
|
943
|
+
if coerced is not None and coerced == uid:
|
|
944
|
+
return True
|
|
945
|
+
if str(value).strip() == str(uid):
|
|
946
|
+
return True
|
|
947
|
+
return False
|
|
948
|
+
|
|
949
|
+
def _compact_departments(self, member: dict[str, Any]) -> list[dict[str, Any]]:
|
|
950
|
+
"""执行内部辅助逻辑。"""
|
|
951
|
+
items: list[dict[str, Any]] = []
|
|
952
|
+
seen: set[tuple[int | None, str | None]] = set()
|
|
953
|
+
for depart in self._walk_nested_items(member.get("departs")):
|
|
954
|
+
if not isinstance(depart, dict):
|
|
955
|
+
continue
|
|
956
|
+
dept_id = self._coerce_int(
|
|
957
|
+
depart.get("deptId", depart.get("departId", depart.get("id")))
|
|
958
|
+
)
|
|
959
|
+
dept_name = self._normalize_text(
|
|
960
|
+
depart.get("deptName", depart.get("departName", depart.get("name")))
|
|
961
|
+
)
|
|
962
|
+
key = (dept_id, dept_name)
|
|
963
|
+
if key in seen or (dept_id is None and dept_name is None):
|
|
964
|
+
continue
|
|
965
|
+
seen.add(key)
|
|
966
|
+
item = {"dept_id": dept_id, "dept_name": dept_name}
|
|
967
|
+
items.append({k: v for k, v in item.items() if v is not None})
|
|
968
|
+
return items
|
|
969
|
+
|
|
970
|
+
def _compact_roles(self, member: dict[str, Any]) -> list[dict[str, Any]]:
|
|
971
|
+
"""执行内部辅助逻辑。"""
|
|
972
|
+
items: list[dict[str, Any]] = []
|
|
973
|
+
seen: set[tuple[int | None, str | None]] = set()
|
|
974
|
+
for role in self._walk_nested_items(member.get("roles")):
|
|
975
|
+
if not isinstance(role, dict):
|
|
976
|
+
continue
|
|
977
|
+
role_id = self._coerce_int(role.get("roleId", role.get("id")))
|
|
978
|
+
role_name = self._normalize_text(role.get("roleName", role.get("name")))
|
|
979
|
+
key = (role_id, role_name)
|
|
980
|
+
if key in seen or (role_id is None and role_name is None):
|
|
981
|
+
continue
|
|
982
|
+
seen.add(key)
|
|
983
|
+
item = {"role_id": role_id, "role_name": role_name}
|
|
984
|
+
items.append({k: v for k, v in item.items() if v is not None})
|
|
985
|
+
return items
|
|
986
|
+
|
|
987
|
+
def _resolve_permission_level(self, auth_code: int | None) -> str | None:
|
|
988
|
+
"""执行内部辅助逻辑。"""
|
|
989
|
+
mapping = {
|
|
990
|
+
2: "超级管理",
|
|
991
|
+
1: "系统管理员",
|
|
992
|
+
3: "子管理员",
|
|
993
|
+
0: "基本成员",
|
|
994
|
+
}
|
|
995
|
+
return mapping.get(auth_code)
|
|
996
|
+
|
|
997
|
+
def _coerce_auth_value(self, value: Any) -> int | None:
|
|
998
|
+
"""执行内部辅助逻辑。"""
|
|
999
|
+
coerced = self._coerce_int(value)
|
|
1000
|
+
if coerced is not None:
|
|
1001
|
+
return coerced
|
|
1002
|
+
normalized = self._normalize_text(value)
|
|
1003
|
+
if normalized is None:
|
|
1004
|
+
return None
|
|
1005
|
+
lowered = normalized.lower()
|
|
1006
|
+
if lowered in {"creator", "workspaccreator", "workspacecreator"}:
|
|
1007
|
+
return 2
|
|
1008
|
+
if lowered in {"admin", "administrator"}:
|
|
1009
|
+
return 1
|
|
1010
|
+
if lowered in {"subadmin", "dataadmin"}:
|
|
1011
|
+
return 3
|
|
1012
|
+
if lowered in {"member", "visitor", "normal"}:
|
|
1013
|
+
return 0
|
|
1014
|
+
return None
|
|
1015
|
+
|
|
1016
|
+
def _extract_items(self, payload: Any) -> list[Any]:
|
|
1017
|
+
"""执行内部辅助逻辑。"""
|
|
1018
|
+
if isinstance(payload, list):
|
|
1019
|
+
return payload
|
|
1020
|
+
if not isinstance(payload, dict):
|
|
1021
|
+
return []
|
|
1022
|
+
for key in ("list", "items", "rows", "result"):
|
|
1023
|
+
value = payload.get(key)
|
|
1024
|
+
if isinstance(value, list):
|
|
1025
|
+
return value
|
|
1026
|
+
for key in ("data", "page"):
|
|
1027
|
+
nested = payload.get(key)
|
|
1028
|
+
if isinstance(nested, list):
|
|
1029
|
+
return nested
|
|
1030
|
+
if isinstance(nested, dict):
|
|
1031
|
+
for nested_key in ("list", "items", "rows", "result"):
|
|
1032
|
+
value = nested.get(nested_key)
|
|
1033
|
+
if isinstance(value, list):
|
|
1034
|
+
return value
|
|
1035
|
+
return []
|
|
1036
|
+
|
|
1037
|
+
def _walk_nested_items(self, value: Any) -> list[Any]:
|
|
1038
|
+
"""执行内部辅助逻辑。"""
|
|
1039
|
+
if isinstance(value, list):
|
|
1040
|
+
items: list[Any] = []
|
|
1041
|
+
for item in value:
|
|
1042
|
+
items.extend(self._walk_nested_items(item))
|
|
1043
|
+
return items
|
|
1044
|
+
return [value]
|
|
1045
|
+
|
|
1046
|
+
def _coerce_int(self, value: Any) -> int | None:
|
|
1047
|
+
"""执行内部辅助逻辑。"""
|
|
1048
|
+
if isinstance(value, bool) or value is None:
|
|
1049
|
+
return None
|
|
1050
|
+
if isinstance(value, int):
|
|
1051
|
+
return value
|
|
1052
|
+
try:
|
|
1053
|
+
return int(str(value).strip())
|
|
1054
|
+
except (TypeError, ValueError):
|
|
1055
|
+
return None
|
|
1056
|
+
|
|
1057
|
+
def _normalize_text(self, value: Any) -> str | None:
|
|
1058
|
+
"""执行内部辅助逻辑。"""
|
|
1059
|
+
if value is None:
|
|
1060
|
+
return None
|
|
1061
|
+
text = str(value).strip()
|
|
1062
|
+
return text or None
|
|
1063
|
+
|
|
1064
|
+
def _fetch_workspace_with_name_fallback(
|
|
1065
|
+
self,
|
|
1066
|
+
base_url: str,
|
|
1067
|
+
token: str,
|
|
1068
|
+
ws_id: int,
|
|
1069
|
+
*,
|
|
1070
|
+
qf_version: str | None,
|
|
1071
|
+
qf_version_source: str | None,
|
|
1072
|
+
) -> dict[str, Any] | None:
|
|
1073
|
+
"""执行内部辅助逻辑。"""
|
|
1074
|
+
try:
|
|
1075
|
+
workspace = self._fetch_workspace(
|
|
1076
|
+
base_url,
|
|
1077
|
+
token,
|
|
1078
|
+
ws_id,
|
|
1079
|
+
qf_version=qf_version,
|
|
1080
|
+
qf_version_source=qf_version_source,
|
|
1081
|
+
)
|
|
1082
|
+
except QingflowApiError:
|
|
1083
|
+
workspace = None
|
|
1084
|
+
if isinstance(workspace, dict):
|
|
1085
|
+
workspace_name = str(workspace.get("workspaceName") or workspace.get("wsName") or "").strip()
|
|
1086
|
+
if workspace_name:
|
|
1087
|
+
return workspace
|
|
1088
|
+
try:
|
|
1089
|
+
fallback = self._fetch_workspace_from_list(
|
|
1090
|
+
base_url,
|
|
1091
|
+
token,
|
|
1092
|
+
ws_id,
|
|
1093
|
+
qf_version=qf_version,
|
|
1094
|
+
qf_version_source=qf_version_source,
|
|
1095
|
+
)
|
|
1096
|
+
except QingflowApiError:
|
|
1097
|
+
fallback = None
|
|
1098
|
+
return fallback or workspace
|
|
1099
|
+
|
|
1100
|
+
def _fetch_workspace_from_list(
|
|
1101
|
+
self,
|
|
1102
|
+
base_url: str,
|
|
1103
|
+
token: str,
|
|
1104
|
+
ws_id: int,
|
|
1105
|
+
*,
|
|
1106
|
+
qf_version: str | None,
|
|
1107
|
+
qf_version_source: str | None,
|
|
1108
|
+
) -> dict[str, Any] | None:
|
|
1109
|
+
"""执行内部辅助逻辑。"""
|
|
1110
|
+
payload = self.backend.request(
|
|
1111
|
+
"POST",
|
|
1112
|
+
BackendRequestContext(
|
|
1113
|
+
base_url=base_url,
|
|
1114
|
+
token=token,
|
|
1115
|
+
ws_id=ws_id,
|
|
1116
|
+
qf_version=qf_version,
|
|
1117
|
+
qf_version_source=qf_version_source,
|
|
1118
|
+
),
|
|
1119
|
+
"/user/workspaceList/pageQuery",
|
|
1120
|
+
json_body={"pageNum": 1, "pageSize": 100, "authList": [0, 1, 2]},
|
|
1121
|
+
)
|
|
1122
|
+
workspaces = payload.get("list") if isinstance(payload, dict) else []
|
|
1123
|
+
if not isinstance(workspaces, list):
|
|
1124
|
+
return None
|
|
1125
|
+
found = next(
|
|
1126
|
+
(
|
|
1127
|
+
item
|
|
1128
|
+
for item in workspaces
|
|
1129
|
+
if isinstance(item, dict) and item.get("wsId") == ws_id
|
|
1130
|
+
),
|
|
1131
|
+
None,
|
|
1132
|
+
)
|
|
1133
|
+
return found if isinstance(found, dict) else None
|