@josephyan/qingflow-cli 0.2.0-beta.985 → 0.2.0-beta.987

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +2 -2
  2. package/docs/local-agent-install.md +70 -11
  3. package/package.json +1 -1
  4. package/pyproject.toml +1 -1
  5. package/src/qingflow_mcp/__init__.py +1 -1
  6. package/src/qingflow_mcp/builder_facade/service.py +376 -19
  7. package/src/qingflow_mcp/cli/commands/auth.py +14 -43
  8. package/src/qingflow_mcp/cli/commands/workspace.py +8 -5
  9. package/src/qingflow_mcp/cli/formatters.py +19 -22
  10. package/src/qingflow_mcp/config.py +39 -0
  11. package/src/qingflow_mcp/errors.py +2 -2
  12. package/src/qingflow_mcp/public_surface.py +4 -6
  13. package/src/qingflow_mcp/response_trim.py +1 -8
  14. package/src/qingflow_mcp/server.py +1 -1
  15. package/src/qingflow_mcp/server_app_builder.py +4 -28
  16. package/src/qingflow_mcp/server_app_user.py +4 -28
  17. package/src/qingflow_mcp/session_store.py +31 -5
  18. package/src/qingflow_mcp/solution/compiler/form_compiler.py +2 -2
  19. package/src/qingflow_mcp/solution/executor.py +2 -2
  20. package/src/qingflow_mcp/tools/ai_builder_tools.py +117 -1
  21. package/src/qingflow_mcp/tools/app_tools.py +51 -1
  22. package/src/qingflow_mcp/tools/approval_tools.py +82 -1
  23. package/src/qingflow_mcp/tools/auth_tools.py +306 -288
  24. package/src/qingflow_mcp/tools/base.py +204 -4
  25. package/src/qingflow_mcp/tools/code_block_tools.py +21 -0
  26. package/src/qingflow_mcp/tools/custom_button_tools.py +24 -1
  27. package/src/qingflow_mcp/tools/directory_tools.py +28 -1
  28. package/src/qingflow_mcp/tools/feedback_tools.py +8 -0
  29. package/src/qingflow_mcp/tools/file_tools.py +25 -1
  30. package/src/qingflow_mcp/tools/import_tools.py +40 -1
  31. package/src/qingflow_mcp/tools/navigation_tools.py +34 -1
  32. package/src/qingflow_mcp/tools/package_tools.py +37 -1
  33. package/src/qingflow_mcp/tools/portal_tools.py +28 -1
  34. package/src/qingflow_mcp/tools/qingbi_report_tools.py +38 -1
  35. package/src/qingflow_mcp/tools/record_tools.py +255 -2
  36. package/src/qingflow_mcp/tools/repository_dev_tools.py +21 -2
  37. package/src/qingflow_mcp/tools/resource_read_tools.py +23 -1
  38. package/src/qingflow_mcp/tools/role_tools.py +19 -1
  39. package/src/qingflow_mcp/tools/solution_tools.py +56 -1
  40. package/src/qingflow_mcp/tools/task_context_tools.py +72 -1
  41. package/src/qingflow_mcp/tools/task_tools.py +49 -3
  42. package/src/qingflow_mcp/tools/view_tools.py +56 -1
  43. package/src/qingflow_mcp/tools/workflow_tools.py +65 -1
  44. package/src/qingflow_mcp/tools/workspace_tools.py +100 -217
@@ -6,11 +6,21 @@ from ..backend_client import BackendRequestContext
6
6
  from ..config import DEFAULT_PROFILE
7
7
  from ..errors import QingflowApiError, raise_tool_error
8
8
  from ..json_types import JSONObject, JSONValue
9
- from .base import ToolBase
9
+ from .base import ToolBase, tool_cn_name
10
10
 
11
11
 
12
12
  class WorkflowTools(ToolBase):
13
+ """流程工具(中文名:流程节点与规则管理)。
14
+
15
+ 类型:流程配置工具。
16
+ 主要职责:
17
+ 1. 查询流程节点与流程图配置;
18
+ 2. 读取与更新流程规则;
19
+ 3. 支持流程发布与流程调试辅助能力。
20
+ """
21
+
13
22
  def register(self, mcp: FastMCP) -> None:
23
+ """注册当前工具到 MCP 服务。"""
14
24
  @mcp.tool()
15
25
  def workflow_list_nodes(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
16
26
  return self.workflow_list_nodes(profile=profile, app_key=app_key)
@@ -59,16 +69,22 @@ class WorkflowTools(ToolBase):
59
69
  def workflow_get_print_nodes(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
60
70
  return self.workflow_get_print_nodes(profile=profile, app_key=app_key)
61
71
 
72
+ @tool_cn_name("流程节点列表")
62
73
  def workflow_list_nodes(self, *, profile: str, app_key: str) -> JSONObject:
74
+ """执行流程相关逻辑。"""
63
75
  self._require_app_key(app_key)
64
76
  return self._request(profile, "GET", f"/app/{app_key}/auditNodes", app_key=app_key)
65
77
 
78
+ @tool_cn_name("流程节点详情")
66
79
  def workflow_get_node_detail(self, *, profile: str, app_key: str, audit_node_id: int) -> JSONObject:
80
+ """执行流程相关逻辑。"""
67
81
  self._require_app_key(app_key)
68
82
  self._require_positive("audit_node_id", audit_node_id)
69
83
  return self._request(profile, "GET", f"/app/{app_key}/auditNodes/{audit_node_id}", app_key=app_key, audit_node_id=audit_node_id)
70
84
 
85
+ @tool_cn_name("新增流程节点")
71
86
  def workflow_add_node(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
87
+ """执行流程相关逻辑。"""
72
88
  self._require_app_key(app_key)
73
89
  body = self._require_dict(payload)
74
90
  return self._request_with_post_fallbacks(
@@ -79,7 +95,9 @@ class WorkflowTools(ToolBase):
79
95
  alternate_paths=[f"/app/{app_key}/auditNode"],
80
96
  )
81
97
 
98
+ @tool_cn_name("更新流程节点")
82
99
  def workflow_update_node(self, *, profile: str, app_key: str, audit_node_id: int, payload: JSONObject) -> JSONObject:
100
+ """执行流程相关逻辑。"""
83
101
  self._require_app_key(app_key)
84
102
  self._require_positive("audit_node_id", audit_node_id)
85
103
  body = self._require_dict(payload)
@@ -94,36 +112,50 @@ class WorkflowTools(ToolBase):
94
112
  audit_node_id=audit_node_id,
95
113
  )
96
114
 
115
+ @tool_cn_name("删除流程节点")
97
116
  def workflow_delete_node(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
117
+ """执行流程相关逻辑。"""
98
118
  self._require_app_key(app_key)
99
119
  body = self._require_dict(payload)
100
120
  return self._request(profile, "DELETE", f"/app/{app_key}/auditNode", app_key=app_key, json_body=body, risk_operation="delete", risk_target="workflow node configuration")
101
121
 
122
+ @tool_cn_name("复制粘贴流程节点")
102
123
  def workflow_copy_paste_node(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
124
+ """执行流程相关逻辑。"""
103
125
  self._require_app_key(app_key)
104
126
  body = self._require_dict(payload)
105
127
  return self._request(profile, "POST", f"/app/{app_key}/auditNode/copyAndPaste", app_key=app_key, json_body=body)
106
128
 
129
+ @tool_cn_name("剪切粘贴流程节点")
107
130
  def workflow_cut_paste_node(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
131
+ """执行流程相关逻辑。"""
108
132
  self._require_app_key(app_key)
109
133
  body = self._require_dict(payload)
110
134
  return self._request(profile, "POST", f"/app/{app_key}/auditNode/cutAndPaste", app_key=app_key, json_body=body)
111
135
 
136
+ @tool_cn_name("创建流程分支")
112
137
  def workflow_create_sub_branch(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
138
+ """执行流程相关逻辑。"""
113
139
  self._require_app_key(app_key)
114
140
  body = self._require_dict(payload)
115
141
  return self._request(profile, "POST", f"/app/{app_key}/auditNode/subBranch", app_key=app_key, json_body=body)
116
142
 
143
+ @tool_cn_name("删除流程分支")
117
144
  def workflow_delete_sub_branch(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
145
+ """执行流程相关逻辑。"""
118
146
  self._require_app_key(app_key)
119
147
  body = self._require_dict(payload)
120
148
  return self._request(profile, "DELETE", f"/app/{app_key}/auditNode/subBranch", app_key=app_key, json_body=body, risk_operation="delete", risk_target="workflow branch configuration")
121
149
 
150
+ @tool_cn_name("流程全局设置")
122
151
  def workflow_get_global_settings(self, *, profile: str, app_key: str) -> JSONObject:
152
+ """执行流程相关逻辑。"""
123
153
  self._require_app_key(app_key)
124
154
  return self._request(profile, "GET", f"/app/{app_key}/workflow/global/setting", app_key=app_key)
125
155
 
156
+ @tool_cn_name("更新流程全局设置")
126
157
  def workflow_update_global_settings(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
158
+ """执行流程相关逻辑。"""
127
159
  self._require_app_key(app_key)
128
160
  body = self._require_dict(payload)
129
161
  return self._request_with_post_fallbacks(
@@ -136,7 +168,9 @@ class WorkflowTools(ToolBase):
136
168
  risk_target="workflow global settings",
137
169
  )
138
170
 
171
+ @tool_cn_name("发布流程")
139
172
  def workflow_publish(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
173
+ """执行流程相关逻辑。"""
140
174
  self._require_app_key(app_key)
141
175
  body = self._require_dict(payload)
142
176
  return self._request_with_post_fallbacks(
@@ -147,11 +181,14 @@ class WorkflowTools(ToolBase):
147
181
  alternate_paths=[],
148
182
  )
149
183
 
184
+ @tool_cn_name("流程后续节点")
150
185
  def workflow_get_future_nodes(self, *, profile: str, app_key: str, apply_id: int) -> JSONObject:
186
+ """执行流程相关逻辑。"""
151
187
  self._require_app_key(app_key)
152
188
  self._require_positive("apply_id", apply_id)
153
189
  return self._request(profile, "GET", f"/app/{app_key}/auditNode/futureList/{apply_id}", app_key=app_key, apply_id=apply_id)
154
190
 
191
+ @tool_cn_name("流程后续节点(应用)")
155
192
  def workflow_get_future_nodes_app(
156
193
  self,
157
194
  *,
@@ -161,6 +198,7 @@ class WorkflowTools(ToolBase):
161
198
  role: int,
162
199
  audit_node_id: int | None,
163
200
  ) -> JSONObject:
201
+ """执行流程相关逻辑。"""
164
202
  self._require_app_key(app_key)
165
203
  self._require_positive("apply_id", apply_id)
166
204
  params: JSONObject = {"role": role}
@@ -176,52 +214,72 @@ class WorkflowTools(ToolBase):
176
214
  params=params,
177
215
  )
178
216
 
217
+ @tool_cn_name("流程 Webhook 测试")
179
218
  def workflow_webhook_test(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
219
+ """执行流程相关逻辑。"""
180
220
  self._require_app_key(app_key)
181
221
  body = self._require_dict(payload)
182
222
  return self._request(profile, "POST", f"/app/{app_key}/auditNode/webhookTest", app_key=app_key, json_body=body)
183
223
 
224
+ @tool_cn_name("流程 QSource 查询")
184
225
  def workflow_qsource_query(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
226
+ """执行流程相关逻辑。"""
185
227
  self._require_app_key(app_key)
186
228
  body = self._require_dict(payload)
187
229
  return self._request(profile, "POST", f"/app/{app_key}/auditNode/qSourceQuery", app_key=app_key, json_body=body)
188
230
 
231
+ @tool_cn_name("流程 QSource 测试")
189
232
  def workflow_qsource_test(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
233
+ """执行流程相关逻辑。"""
190
234
  self._require_app_key(app_key)
191
235
  body = self._require_dict(payload)
192
236
  return self._request(profile, "POST", f"/app/{app_key}/auditNode/qSourceQueryTest", app_key=app_key, json_body=body)
193
237
 
238
+ @tool_cn_name("流程主动 QSource 配置")
194
239
  def workflow_get_qsource_active(self, *, profile: str, app_key: str, qsource_id: int) -> JSONObject:
240
+ """执行流程相关逻辑。"""
195
241
  self._require_app_key(app_key)
196
242
  self._require_positive("qsource_id", qsource_id)
197
243
  return self._request(profile, "GET", f"/app/{app_key}/auditNode/active/qsource", app_key=app_key, params={"qSourceId": qsource_id})
198
244
 
245
+ @tool_cn_name("更新主动 QSource 配置")
199
246
  def workflow_upsert_qsource_active(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
247
+ """执行流程相关逻辑。"""
200
248
  self._require_app_key(app_key)
201
249
  body = self._require_dict(payload)
202
250
  return self._request(profile, "POST", f"/app/{app_key}/auditNode/active/qsource", app_key=app_key, json_body=body)
203
251
 
252
+ @tool_cn_name("流程被动 QSource 配置")
204
253
  def workflow_get_qsource_passive(self, *, profile: str, app_key: str, qsource_id: int) -> JSONObject:
254
+ """执行流程相关逻辑。"""
205
255
  self._require_app_key(app_key)
206
256
  self._require_positive("qsource_id", qsource_id)
207
257
  return self._request(profile, "GET", f"/app/{app_key}/auditNode/passive/qsource", app_key=app_key, params={"qSourceId": qsource_id})
208
258
 
259
+ @tool_cn_name("更新被动 QSource 配置")
209
260
  def workflow_upsert_qsource_passive(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
261
+ """执行流程相关逻辑。"""
210
262
  self._require_app_key(app_key)
211
263
  body = self._require_dict(payload)
212
264
  return self._request(profile, "POST", f"/app/{app_key}/auditNode/passive/qsource", app_key=app_key, json_body=body)
213
265
 
266
+ @tool_cn_name("切换 QSource 状态")
214
267
  def workflow_switch_qsource_status(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
268
+ """执行流程相关逻辑。"""
215
269
  self._require_app_key(app_key)
216
270
  body = self._require_dict(payload)
217
271
  return self._request(profile, "POST", f"/app/{app_key}/auditNode/source/status", app_key=app_key, json_body=body)
218
272
 
273
+ @tool_cn_name("删除 QSource 配置")
219
274
  def workflow_delete_qsource(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
275
+ """执行流程相关逻辑。"""
220
276
  self._require_app_key(app_key)
221
277
  body = self._require_dict(payload)
222
278
  return self._request(profile, "DELETE", f"/app/{app_key}/auditNode/qsource", app_key=app_key, json_body=body, risk_operation="delete", risk_target="workflow qsource configuration")
223
279
 
280
+ @tool_cn_name("节点可编辑字段")
224
281
  def workflow_get_editable_question_ids(self, *, profile: str, app_key: str, audit_node_id: int) -> JSONObject:
282
+ """执行流程相关逻辑。"""
225
283
  self._require_app_key(app_key)
226
284
  self._require_positive("audit_node_id", audit_node_id)
227
285
  return self._request(
@@ -232,7 +290,9 @@ class WorkflowTools(ToolBase):
232
290
  audit_node_id=audit_node_id,
233
291
  )
234
292
 
293
+ @tool_cn_name("流程打印节点")
235
294
  def workflow_get_print_nodes(self, *, profile: str, app_key: str) -> JSONObject:
295
+ """执行流程相关逻辑。"""
236
296
  self._require_app_key(app_key)
237
297
  return self._request(profile, "GET", f"/app/{app_key}/auditNode/printNodes", app_key=app_key)
238
298
 
@@ -249,6 +309,7 @@ class WorkflowTools(ToolBase):
249
309
  risk_target: str | None = None,
250
310
  **extra: JSONValue,
251
311
  ) -> JSONObject:
312
+ """执行内部辅助逻辑。"""
252
313
  def runner(session_profile, context):
253
314
  result = self.backend.request(method, context, path, json_body=json_body, params=params)
254
315
  response: JSONObject = {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "result": result}
@@ -271,6 +332,7 @@ class WorkflowTools(ToolBase):
271
332
  risk_target: str | None = None,
272
333
  **extra: JSONValue,
273
334
  ) -> JSONObject:
335
+ """执行内部辅助逻辑。"""
274
336
  def runner(session_profile, context):
275
337
  attempted_contexts = [context]
276
338
  if context.qf_version is not None:
@@ -304,9 +366,11 @@ class WorkflowTools(ToolBase):
304
366
  return self._run(profile, runner)
305
367
 
306
368
  def _require_app_key(self, app_key: str) -> None:
369
+ """执行内部辅助逻辑。"""
307
370
  if not app_key:
308
371
  raise_tool_error(QingflowApiError.config_error("app_key is required"))
309
372
 
310
373
  def _require_positive(self, field_name: str, value: int) -> None:
374
+ """执行内部辅助逻辑。"""
311
375
  if value <= 0:
312
376
  raise_tool_error(QingflowApiError.config_error(f"{field_name} must be positive"))
@@ -7,11 +7,20 @@ from mcp.server.fastmcp import FastMCP
7
7
  from ..backend_client import BackendRequestContext
8
8
  from ..config import DEFAULT_PROFILE
9
9
  from ..errors import QingflowApiError, raise_tool_error
10
- from .base import ToolBase
10
+ from .base import ToolBase, tool_cn_name
11
11
 
12
12
 
13
13
  class WorkspaceTools(ToolBase):
14
+ """工作区工具(中文名:工作区与插件管理)。
15
+
16
+ 类型:基础上下文工具。
17
+ 主要职责:
18
+ 1. 查询当前用户可见工作区列表;
19
+ 2. 管理工作区插件安装状态。
20
+ """
21
+
14
22
  def register(self, mcp: FastMCP) -> None:
23
+ """注册当前工具到 MCP 服务。"""
15
24
  @mcp.tool()
16
25
  def workspace_list(
17
26
  profile: str = DEFAULT_PROFILE,
@@ -27,8 +36,14 @@ class WorkspaceTools(ToolBase):
27
36
  )
28
37
 
29
38
  @mcp.tool()
30
- def workspace_select(profile: str = DEFAULT_PROFILE, ws_id: int = 0) -> dict[str, Any]:
31
- return self.workspace_select(profile=profile, ws_id=ws_id)
39
+ def workspace_get(
40
+ profile: str = DEFAULT_PROFILE,
41
+ ws_id: int = 0,
42
+ ) -> dict[str, Any]:
43
+ return self.workspace_get(
44
+ profile=profile,
45
+ ws_id=ws_id if ws_id > 0 else None,
46
+ )
32
47
 
33
48
  @mcp.tool()
34
49
  def workspace_set_plugin_status(
@@ -42,6 +57,7 @@ class WorkspaceTools(ToolBase):
42
57
  being_installed=being_installed,
43
58
  )
44
59
 
60
+ @tool_cn_name("工作区列表")
45
61
  def workspace_list(
46
62
  self,
47
63
  *,
@@ -50,6 +66,7 @@ class WorkspaceTools(ToolBase):
50
66
  page_size: int = 20,
51
67
  include_external: bool = False,
52
68
  ) -> dict[str, Any]:
69
+ """执行工作区相关逻辑。"""
53
70
  if page_num <= 0 or page_size <= 0:
54
71
  raise_tool_error(QingflowApiError.config_error("page_num and page_size must be positive"))
55
72
 
@@ -86,226 +103,32 @@ class WorkspaceTools(ToolBase):
86
103
 
87
104
  return self._run(profile, runner, require_workspace=False)
88
105
 
89
- def workspace_select(self, *, profile: str = DEFAULT_PROFILE, ws_id: int) -> dict[str, Any]:
90
- if ws_id <= 0:
91
- raise_tool_error(QingflowApiError.config_error("ws_id must be positive"))
92
-
93
- def runner(_, context):
94
- # Create a context with the target ws_id for the API call
95
- # This is necessary because the API requires wsId header even when getting workspace details
96
- call_context = BackendRequestContext(
97
- base_url=context.base_url,
98
- token=context.token,
99
- ws_id=ws_id,
100
- qf_version=context.qf_version,
101
- qf_version_source=context.qf_version_source,
102
- )
103
-
104
- try:
105
- result = self.backend.request("GET", call_context, f"/user/workspace/{ws_id}")
106
- except QingflowApiError as e:
107
- if e.http_status == 404:
108
- result = self._workspace_from_list(call_context, ws_id) or {"wsId": ws_id, "workspaceName": f"Workspace {ws_id}"}
109
- else:
110
- raise
111
-
112
- if isinstance(result, dict):
113
- ws_name = (
114
- str(result.get("workspaceName") or result.get("wsName") or "").strip()
115
- or None
116
- )
117
- if ws_name is None:
118
- fallback_workspace = self._workspace_from_list(call_context, ws_id)
119
- if isinstance(fallback_workspace, dict):
120
- result = fallback_workspace
121
- ws_name = (
122
- str(result.get("workspaceName") or result.get("wsName") or "").strip()
123
- or None
124
- )
125
- else:
126
- ws_name = None
127
- if ws_name is None:
128
- ws_name = f"Workspace {ws_id}"
129
- session_profile = self.sessions.select_workspace(profile, ws_id=ws_id, ws_name=ws_name)
130
- workspace_qf_version = self._workspace_system_version(result)
131
- should_refresh_route = (
132
- workspace_qf_version is not None
133
- and (context.qf_version_source or "unset") != "explicit"
134
- and workspace_qf_version != session_profile.qf_version
135
- )
136
- if should_refresh_route:
137
- session_profile = self.sessions.update_route(
138
- profile,
139
- qf_version=workspace_qf_version,
140
- qf_version_source="workspace_system_version",
141
- )
142
- active_context = BackendRequestContext(
143
- base_url=session_profile.base_url,
144
- token=context.token,
145
- ws_id=session_profile.selected_ws_id,
146
- qf_version=session_profile.qf_version,
147
- qf_version_source=session_profile.qf_version_source,
148
- )
149
- workspace_version = self._workspace_version_summary(
150
- self._fetch_workspace_account_info(active_context),
151
- workspace_base_info=self._fetch_workspace_base_info(active_context),
152
- workspace_detail=result,
153
- )
106
+ @tool_cn_name("工作区详情")
107
+ def workspace_get(
108
+ self,
109
+ *,
110
+ profile: str = DEFAULT_PROFILE,
111
+ ws_id: int | None = None,
112
+ ) -> dict[str, Any]:
113
+ """读取单个工作区详情,并尽量补齐真实 systemVersion。"""
114
+
115
+ def runner(session_profile, context):
116
+ target_ws_id = ws_id or (session_profile.selected_ws_id if session_profile is not None else None)
117
+ if target_ws_id is None or target_ws_id <= 0:
118
+ raise_tool_error(QingflowApiError.workspace_not_selected(profile))
119
+ workspace = self._fetch_workspace_with_fallback(context, ws_id=target_ws_id)
120
+ system_version = self._workspace_system_version(workspace)
154
121
  return {
155
122
  "profile": profile,
156
- "selected_ws_id": session_profile.selected_ws_id,
157
- "selected_ws_name": session_profile.selected_ws_name,
158
- "workspace": result,
159
- "workspace_version": workspace_version,
160
- "qf_version": session_profile.qf_version,
161
- "qf_version_source": session_profile.qf_version_source,
162
- "request_route": self.backend.describe_route(
163
- active_context
164
- ),
123
+ "ws_id": target_ws_id,
124
+ "qf_version": system_version,
125
+ "qf_version_source": "workspace_system_version" if system_version else "unverified",
126
+ "workspace": workspace,
165
127
  }
166
128
 
167
129
  return self._run(profile, runner, require_workspace=False)
168
130
 
169
- def _workspace_from_list(self, context: BackendRequestContext, ws_id: int) -> dict[str, Any] | None:
170
- try:
171
- payload = self.backend.request(
172
- "POST",
173
- context,
174
- "/user/workspaceList/pageQuery",
175
- json_body={"pageNum": 1, "pageSize": 100, "authList": [0, 1, 2]},
176
- )
177
- except Exception:
178
- return None
179
- workspaces = payload.get("list", []) if isinstance(payload, dict) else []
180
- found = next(
181
- (
182
- item
183
- for item in workspaces
184
- if isinstance(item, dict) and item.get("wsId") == ws_id
185
- ),
186
- None,
187
- )
188
- return found if isinstance(found, dict) else None
189
-
190
- def _workspace_system_version(self, workspace: Any) -> str | None:
191
- if not isinstance(workspace, dict):
192
- return None
193
- value = workspace.get("systemVersion")
194
- if value is None:
195
- return None
196
- normalized = str(value).strip()
197
- return normalized or None
198
-
199
- def _fetch_workspace_base_info(self, context: BackendRequestContext) -> dict[str, Any] | None:
200
- try:
201
- payload = self.backend.request("GET", context, "/ws/baseInfo")
202
- except QingflowApiError:
203
- return None
204
- return payload if isinstance(payload, dict) else None
205
-
206
- def _fetch_workspace_account_info(self, context: BackendRequestContext) -> dict[str, Any] | None:
207
- try:
208
- payload = self.backend.request("GET", context, "/ws/account")
209
- except QingflowApiError:
210
- return None
211
- return payload if isinstance(payload, dict) else None
212
-
213
- def _workspace_version_summary(
214
- self,
215
- payload: Any,
216
- *,
217
- workspace_base_info: Any,
218
- workspace_detail: Any,
219
- ) -> dict[str, Any]:
220
- account_info = payload if isinstance(payload, dict) else {}
221
- base_info = workspace_base_info if isinstance(workspace_base_info, dict) else {}
222
- detail_info = workspace_detail if isinstance(workspace_detail, dict) else {}
223
- level_code = self._first_present_int(
224
- account_info.get("accountLevel"),
225
- base_info.get("accountLevel"),
226
- detail_info.get("accountLevel"),
227
- )
228
- level_name = self._account_level_name(level_code)
229
- return {
230
- "level_code": level_code,
231
- "level_name": level_name,
232
- "display_name": self._account_level_display_name(level_name),
233
- "being_trial": self._first_present_bool(
234
- account_info.get("trial"),
235
- base_info.get("trial"),
236
- detail_info.get("trial"),
237
- ),
238
- "expire_date": self._first_present_value(
239
- account_info.get("expireDate"),
240
- base_info.get("expireDate"),
241
- detail_info.get("expireDate"),
242
- ),
243
- }
244
-
245
- def _account_level_name(self, level_code: int | None) -> str | None:
246
- mapping = {
247
- 0: "FREE",
248
- 10: "AIR",
249
- 20: "BASIC",
250
- 30: "TEAM",
251
- 35: "PROFESSIONAL",
252
- 40: "ENTERPRISE",
253
- }
254
- return mapping.get(level_code)
255
-
256
- def _account_level_display_name(self, level_name: str | None) -> str | None:
257
- mapping = {
258
- "FREE": "免费版",
259
- "AIR": "Air版",
260
- "BASIC": "Pro版",
261
- "TEAM": "团队版",
262
- "PROFESSIONAL": "专业版",
263
- "ENTERPRISE": "企业版",
264
- }
265
- return mapping.get(level_name)
266
-
267
- def _coerce_int(self, value: Any) -> int | None:
268
- if isinstance(value, bool) or value is None:
269
- return None
270
- if isinstance(value, int):
271
- return value
272
- try:
273
- return int(str(value).strip())
274
- except (TypeError, ValueError):
275
- return None
276
-
277
- def _coerce_bool(self, value: Any) -> bool | None:
278
- if isinstance(value, bool):
279
- return value
280
- if value is None:
281
- return None
282
- normalized = str(value).strip().lower()
283
- if normalized in {"true", "1"}:
284
- return True
285
- if normalized in {"false", "0"}:
286
- return False
287
- return None
288
-
289
- def _first_present_int(self, *values: Any) -> int | None:
290
- for value in values:
291
- coerced = self._coerce_int(value)
292
- if coerced is not None:
293
- return coerced
294
- return None
295
-
296
- def _first_present_bool(self, *values: Any) -> bool | None:
297
- for value in values:
298
- coerced = self._coerce_bool(value)
299
- if coerced is not None:
300
- return coerced
301
- return None
302
-
303
- def _first_present_value(self, *values: Any) -> Any:
304
- for value in values:
305
- if value is not None:
306
- return value
307
- return None
308
-
131
+ @tool_cn_name("设置工作区插件状态")
309
132
  def workspace_set_plugin_status(
310
133
  self,
311
134
  *,
@@ -313,6 +136,7 @@ class WorkspaceTools(ToolBase):
313
136
  plugin_id: int,
314
137
  being_installed: bool = True,
315
138
  ) -> dict[str, Any]:
139
+ """执行工作区相关逻辑。"""
316
140
  if plugin_id <= 0:
317
141
  raise_tool_error(QingflowApiError.config_error("plugin_id must be positive"))
318
142
 
@@ -334,3 +158,62 @@ class WorkspaceTools(ToolBase):
334
158
  }
335
159
 
336
160
  return self._run(profile, runner)
161
+
162
+ def _fetch_workspace_with_fallback(
163
+ self,
164
+ context: BackendRequestContext,
165
+ *,
166
+ ws_id: int,
167
+ ) -> dict[str, Any]:
168
+ workspace = self.backend.request("GET", context, f"/user/workspace/{ws_id}")
169
+ if not isinstance(workspace, dict):
170
+ raise_tool_error(QingflowApiError(category="workspace", message=f"Workspace {ws_id} is not accessible"))
171
+ if self._workspace_needs_list_fallback(workspace):
172
+ fallback = self._fetch_workspace_from_list(context, ws_id=ws_id)
173
+ if isinstance(fallback, dict):
174
+ merged = dict(workspace)
175
+ for key, value in fallback.items():
176
+ if merged.get(key) in (None, "") and value not in (None, ""):
177
+ merged[key] = value
178
+ workspace = merged
179
+ return workspace
180
+
181
+ def _fetch_workspace_from_list(self, context: BackendRequestContext, *, ws_id: int) -> dict[str, Any] | None:
182
+ payload = self.backend.request(
183
+ "POST",
184
+ BackendRequestContext(
185
+ base_url=context.base_url,
186
+ token=context.token,
187
+ ws_id=None,
188
+ qf_version=context.qf_version,
189
+ qf_version_source=context.qf_version_source,
190
+ ),
191
+ "/user/workspaceList/pageQuery",
192
+ json_body={"pageNum": 1, "pageSize": 100, "authList": [0, 1, 2, 3]},
193
+ )
194
+ workspaces = payload.get("list") if isinstance(payload, dict) else []
195
+ if not isinstance(workspaces, list):
196
+ return None
197
+ found = next(
198
+ (
199
+ item
200
+ for item in workspaces
201
+ if isinstance(item, dict) and item.get("wsId") == ws_id
202
+ ),
203
+ None,
204
+ )
205
+ return found if isinstance(found, dict) else None
206
+
207
+ def _workspace_needs_list_fallback(self, workspace: dict[str, Any]) -> bool:
208
+ workspace_name = str(workspace.get("workspaceName") or workspace.get("wsName") or "").strip()
209
+ system_version = self._workspace_system_version(workspace)
210
+ return not workspace_name or system_version is None
211
+
212
+ def _workspace_system_version(self, workspace: Any) -> str | None:
213
+ if not isinstance(workspace, dict):
214
+ return None
215
+ value = workspace.get("systemVersion")
216
+ if value is None:
217
+ return None
218
+ normalized = str(value).strip()
219
+ return normalized or None