@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.
Files changed (92) hide show
  1. package/README.md +31 -0
  2. package/docs/local-agent-install.md +309 -0
  3. package/entry_point.py +13 -0
  4. package/npm/bin/qingflow.mjs +5 -0
  5. package/npm/lib/runtime.mjs +346 -0
  6. package/npm/scripts/postinstall.mjs +16 -0
  7. package/package.json +34 -0
  8. package/pyproject.toml +67 -0
  9. package/qingflow +15 -0
  10. package/src/qingflow_mcp/__init__.py +37 -0
  11. package/src/qingflow_mcp/__main__.py +5 -0
  12. package/src/qingflow_mcp/backend_client.py +649 -0
  13. package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
  14. package/src/qingflow_mcp/builder_facade/models.py +1846 -0
  15. package/src/qingflow_mcp/builder_facade/service.py +16502 -0
  16. package/src/qingflow_mcp/cli/__init__.py +1 -0
  17. package/src/qingflow_mcp/cli/commands/__init__.py +18 -0
  18. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  19. package/src/qingflow_mcp/cli/commands/auth.py +112 -0
  20. package/src/qingflow_mcp/cli/commands/builder.py +539 -0
  21. package/src/qingflow_mcp/cli/commands/chart.py +18 -0
  22. package/src/qingflow_mcp/cli/commands/common.py +62 -0
  23. package/src/qingflow_mcp/cli/commands/imports.py +96 -0
  24. package/src/qingflow_mcp/cli/commands/portal.py +25 -0
  25. package/src/qingflow_mcp/cli/commands/record.py +331 -0
  26. package/src/qingflow_mcp/cli/commands/repo.py +80 -0
  27. package/src/qingflow_mcp/cli/commands/task.py +141 -0
  28. package/src/qingflow_mcp/cli/commands/view.py +18 -0
  29. package/src/qingflow_mcp/cli/commands/workspace.py +110 -0
  30. package/src/qingflow_mcp/cli/context.py +60 -0
  31. package/src/qingflow_mcp/cli/formatters.py +573 -0
  32. package/src/qingflow_mcp/cli/json_io.py +50 -0
  33. package/src/qingflow_mcp/cli/main.py +186 -0
  34. package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
  35. package/src/qingflow_mcp/cli/terminal_ui.py +173 -0
  36. package/src/qingflow_mcp/config.py +407 -0
  37. package/src/qingflow_mcp/errors.py +66 -0
  38. package/src/qingflow_mcp/id_utils.py +49 -0
  39. package/src/qingflow_mcp/import_store.py +121 -0
  40. package/src/qingflow_mcp/json_types.py +18 -0
  41. package/src/qingflow_mcp/list_type_labels.py +76 -0
  42. package/src/qingflow_mcp/public_surface.py +243 -0
  43. package/src/qingflow_mcp/repository_store.py +71 -0
  44. package/src/qingflow_mcp/response_trim.py +841 -0
  45. package/src/qingflow_mcp/server.py +216 -0
  46. package/src/qingflow_mcp/server_app_builder.py +543 -0
  47. package/src/qingflow_mcp/server_app_user.py +386 -0
  48. package/src/qingflow_mcp/session_store.py +369 -0
  49. package/src/qingflow_mcp/solution/__init__.py +6 -0
  50. package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
  51. package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
  52. package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
  53. package/src/qingflow_mcp/solution/compiler/form_compiler.py +495 -0
  54. package/src/qingflow_mcp/solution/compiler/icon_utils.py +187 -0
  55. package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
  56. package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
  57. package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
  58. package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
  59. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  60. package/src/qingflow_mcp/solution/design_session.py +222 -0
  61. package/src/qingflow_mcp/solution/design_store.py +100 -0
  62. package/src/qingflow_mcp/solution/executor.py +2398 -0
  63. package/src/qingflow_mcp/solution/normalizer.py +23 -0
  64. package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
  65. package/src/qingflow_mcp/solution/run_store.py +244 -0
  66. package/src/qingflow_mcp/solution/spec_models.py +855 -0
  67. package/src/qingflow_mcp/tools/__init__.py +1 -0
  68. package/src/qingflow_mcp/tools/ai_builder_tools.py +3449 -0
  69. package/src/qingflow_mcp/tools/app_tools.py +926 -0
  70. package/src/qingflow_mcp/tools/approval_tools.py +1062 -0
  71. package/src/qingflow_mcp/tools/auth_tools.py +1133 -0
  72. package/src/qingflow_mcp/tools/base.py +281 -0
  73. package/src/qingflow_mcp/tools/code_block_tools.py +777 -0
  74. package/src/qingflow_mcp/tools/custom_button_tools.py +202 -0
  75. package/src/qingflow_mcp/tools/directory_tools.py +675 -0
  76. package/src/qingflow_mcp/tools/feedback_tools.py +238 -0
  77. package/src/qingflow_mcp/tools/file_tools.py +409 -0
  78. package/src/qingflow_mcp/tools/import_tools.py +2223 -0
  79. package/src/qingflow_mcp/tools/navigation_tools.py +210 -0
  80. package/src/qingflow_mcp/tools/package_tools.py +326 -0
  81. package/src/qingflow_mcp/tools/portal_tools.py +158 -0
  82. package/src/qingflow_mcp/tools/qingbi_report_tools.py +374 -0
  83. package/src/qingflow_mcp/tools/record_tools.py +14291 -0
  84. package/src/qingflow_mcp/tools/repository_dev_tools.py +552 -0
  85. package/src/qingflow_mcp/tools/resource_read_tools.py +503 -0
  86. package/src/qingflow_mcp/tools/role_tools.py +112 -0
  87. package/src/qingflow_mcp/tools/solution_tools.py +4054 -0
  88. package/src/qingflow_mcp/tools/task_context_tools.py +2986 -0
  89. package/src/qingflow_mcp/tools/task_tools.py +889 -0
  90. package/src/qingflow_mcp/tools/view_tools.py +335 -0
  91. package/src/qingflow_mcp/tools/workflow_tools.py +376 -0
  92. 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