@qingflow-tech/qingflow-cli 1.1.39 → 1.1.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/pyproject.toml +1 -1
  4. package/src/qingflow_mcp/builder_facade/service.py +8 -8
  5. package/src/qingflow_mcp/server_cloud.py +89 -3
  6. package/src/qingflow_mcp/session_store.py +7 -1
  7. package/src/qingflow_mcp/tools/ai_builder_tools.py +1 -59
  8. package/src/qingflow_mcp/tools/app_tools.py +15 -29
  9. package/src/qingflow_mcp/tools/approval_tools.py +16 -40
  10. package/src/qingflow_mcp/tools/auth_tools.py +18 -13
  11. package/src/qingflow_mcp/tools/base.py +114 -74
  12. package/src/qingflow_mcp/tools/code_block_tools.py +2 -5
  13. package/src/qingflow_mcp/tools/custom_button_tools.py +6 -11
  14. package/src/qingflow_mcp/tools/directory_tools.py +9 -16
  15. package/src/qingflow_mcp/tools/export_tools.py +5 -9
  16. package/src/qingflow_mcp/tools/feedback_tools.py +0 -2
  17. package/src/qingflow_mcp/tools/file_tools.py +3 -5
  18. package/src/qingflow_mcp/tools/import_tools.py +7 -13
  19. package/src/qingflow_mcp/tools/navigation_tools.py +12 -23
  20. package/src/qingflow_mcp/tools/package_tools.py +11 -21
  21. package/src/qingflow_mcp/tools/portal_tools.py +9 -17
  22. package/src/qingflow_mcp/tools/qingbi_report_tools.py +18 -23
  23. package/src/qingflow_mcp/tools/record_tools.py +97 -53
  24. package/src/qingflow_mcp/tools/repository_dev_tools.py +4 -7
  25. package/src/qingflow_mcp/tools/resource_read_tools.py +6 -9
  26. package/src/qingflow_mcp/tools/role_tools.py +5 -9
  27. package/src/qingflow_mcp/tools/solution_tools.py +4 -16
  28. package/src/qingflow_mcp/tools/task_context_tools.py +6 -12
  29. package/src/qingflow_mcp/tools/task_tools.py +12 -29
  30. package/src/qingflow_mcp/tools/view_tools.py +23 -32
  31. package/src/qingflow_mcp/tools/workflow_tools.py +30 -47
  32. package/src/qingflow_mcp/tools/workspace_tools.py +5 -9
@@ -9,7 +9,7 @@ from ..config import DEFAULT_PROFILE
9
9
  from ..errors import QingflowApiError, backend_code_int, backend_code_value_int, is_auth_like_error, message_looks_like_invalid_token, raise_tool_error
10
10
  from ..json_types import JSONObject
11
11
  from ..list_type_labels import get_record_list_type_label
12
- from .base import ToolBase, tool_cn_name
12
+ from .base import ToolBase
13
13
 
14
14
 
15
15
  class ApprovalTools(ToolBase):
@@ -149,7 +149,6 @@ class ApprovalTools(ToolBase):
149
149
  keyword=keyword,
150
150
  )
151
151
 
152
- @tool_cn_name("写评论")
153
152
  def record_comment_write(self, *, profile: str, app_key: str, record_id: int, payload: dict[str, Any]) -> dict[str, Any]:
154
153
  """执行记录相关逻辑。"""
155
154
  raw = self.record_comment_add(profile=profile, app_key=app_key, apply_id=record_id, payload=payload)
@@ -160,7 +159,6 @@ class ApprovalTools(ToolBase):
160
159
  selection={},
161
160
  )
162
161
 
163
- @tool_cn_name("评论提及")
164
162
  def record_comment_mentions(
165
163
  self,
166
164
  *,
@@ -207,7 +205,6 @@ class ApprovalTools(ToolBase):
207
205
  selection=selection,
208
206
  )
209
207
 
210
- @tool_cn_name("任务通过")
211
208
  def task_approve(self, *, profile: str, app_key: str, record_id: int, payload: dict[str, Any], fields: dict[str, Any] | None = None) -> dict[str, Any]:
212
209
  """执行任务相关逻辑。"""
213
210
  if fields:
@@ -229,7 +226,6 @@ class ApprovalTools(ToolBase):
229
226
  human_review=True,
230
227
  )
231
228
 
232
- @tool_cn_name("任务拒绝")
233
229
  def task_reject(self, *, profile: str, app_key: str, record_id: int, payload: dict[str, Any], fields: dict[str, Any] | None = None) -> dict[str, Any]:
234
230
  """执行任务相关逻辑。"""
235
231
  if fields:
@@ -251,7 +247,6 @@ class ApprovalTools(ToolBase):
251
247
  human_review=True,
252
248
  )
253
249
 
254
- @tool_cn_name("任务退回候选")
255
250
  def task_rollback_candidates(self, *, profile: str, app_key: str, record_id: int, workflow_node_id: int) -> dict[str, Any]:
256
251
  """执行任务相关逻辑。"""
257
252
  selection = {"app_key": app_key, "record_id": record_id, "workflow_node_id": workflow_node_id}
@@ -274,7 +269,6 @@ class ApprovalTools(ToolBase):
274
269
  selection=selection,
275
270
  )
276
271
 
277
- @tool_cn_name("任务退回")
278
272
  def task_rollback(self, *, profile: str, app_key: str, record_id: int, payload: dict[str, Any], fields: dict[str, Any] | None = None) -> dict[str, Any]:
279
273
  """执行任务相关逻辑。"""
280
274
  if fields:
@@ -296,7 +290,6 @@ class ApprovalTools(ToolBase):
296
290
  human_review=True,
297
291
  )
298
292
 
299
- @tool_cn_name("任务转交")
300
293
  def task_transfer(self, *, profile: str, app_key: str, record_id: int, payload: dict[str, Any], fields: dict[str, Any] | None = None) -> dict[str, Any]:
301
294
  """执行任务相关逻辑。"""
302
295
  if fields:
@@ -319,7 +312,6 @@ class ApprovalTools(ToolBase):
319
312
  human_review=True,
320
313
  )
321
314
 
322
- @tool_cn_name("任务仅保存")
323
315
  def task_save_only(
324
316
  self,
325
317
  *,
@@ -341,7 +333,6 @@ class ApprovalTools(ToolBase):
341
333
  workflow_node_id=workflow_node_id,
342
334
  )
343
335
 
344
- @tool_cn_name("任务转交候选")
345
336
  def task_transfer_candidates(
346
337
  self,
347
338
  *,
@@ -390,7 +381,6 @@ class ApprovalTools(ToolBase):
390
381
  selection=selection,
391
382
  )
392
383
 
393
- @tool_cn_name("新增评论")
394
384
  def record_comment_add(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
395
385
  """执行记录相关逻辑。"""
396
386
  self._require_app_and_apply(app_key, apply_id)
@@ -407,9 +397,8 @@ class ApprovalTools(ToolBase):
407
397
  "request_route": self._request_route_payload(context),
408
398
  }
409
399
 
410
- return self._run(profile, runner)
400
+ return self._run(profile, runner, tool_name='新增评论')
411
401
 
412
- @tool_cn_name("评论列表")
413
402
  def record_comment_list(self, *, profile: str, app_key: str, apply_id: int, page_size: int = 20, list_type: int | None = None, page_num: int | None = 1) -> dict[str, Any]:
414
403
  """执行记录相关逻辑。"""
415
404
  self._require_app_and_apply(app_key, apply_id)
@@ -432,7 +421,7 @@ class ApprovalTools(ToolBase):
432
421
  "request_route": self._request_route_payload(context),
433
422
  }
434
423
 
435
- raw = self._run(profile, runner)
424
+ raw = self._run(profile, runner, tool_name='评论列表')
436
425
  items = _approval_page_items(raw.get("page"))
437
426
  return self._public_page_response(
438
427
  raw,
@@ -447,7 +436,6 @@ class ApprovalTools(ToolBase):
447
436
  selection={"app_key": app_key, "record_id": apply_id, "list_type": list_type},
448
437
  )
449
438
 
450
- @tool_cn_name("评论提及候选")
451
439
  def record_comment_mention_candidates(
452
440
  self,
453
441
  *,
@@ -480,9 +468,8 @@ class ApprovalTools(ToolBase):
480
468
  "request_route": self._request_route_payload(context),
481
469
  }
482
470
 
483
- return self._run(profile, runner)
471
+ return self._run(profile, runner, tool_name='评论提及候选')
484
472
 
485
- @tool_cn_name("评论标记已读")
486
473
  def record_comment_mark_read(self, *, profile: str, app_key: str, apply_id: int) -> dict[str, Any]:
487
474
  """执行记录相关逻辑。"""
488
475
  self._require_app_and_apply(app_key, apply_id)
@@ -498,7 +485,7 @@ class ApprovalTools(ToolBase):
498
485
  "request_route": self._request_route_payload(context),
499
486
  }
500
487
 
501
- raw = self._run(profile, runner)
488
+ raw = self._run(profile, runner, tool_name='评论标记已读')
502
489
  return self._public_action_response(
503
490
  raw,
504
491
  action="record_comment_mark_read",
@@ -506,7 +493,6 @@ class ApprovalTools(ToolBase):
506
493
  selection={},
507
494
  )
508
495
 
509
- @tool_cn_name("评论统计")
510
496
  def record_comment_stats(self, *, profile: str, app_key: str, apply_id: int) -> dict[str, Any]:
511
497
  """执行记录相关逻辑。"""
512
498
  self._require_app_and_apply(app_key, apply_id)
@@ -522,9 +508,8 @@ class ApprovalTools(ToolBase):
522
508
  "request_route": self._request_route_payload(context),
523
509
  }
524
510
 
525
- return self._run(profile, runner)
511
+ return self._run(profile, runner, tool_name='评论统计')
526
512
 
527
- @tool_cn_name("记录通过")
528
513
  def record_approve(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
529
514
  """执行记录相关逻辑。"""
530
515
  self._require_app_and_apply(app_key, apply_id)
@@ -544,9 +529,8 @@ class ApprovalTools(ToolBase):
544
529
  "request_route": self._request_route_payload(context),
545
530
  }
546
531
 
547
- return self._run(profile, runner)
532
+ return self._run(profile, runner, tool_name='记录通过')
548
533
 
549
- @tool_cn_name("记录拒绝")
550
534
  def record_reject(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
551
535
  """执行记录相关逻辑。"""
552
536
  self._require_app_and_apply(app_key, apply_id)
@@ -566,9 +550,8 @@ class ApprovalTools(ToolBase):
566
550
  "request_route": self._request_route_payload(context),
567
551
  }
568
552
 
569
- return self._run(profile, runner)
553
+ return self._run(profile, runner, tool_name='记录拒绝')
570
554
 
571
- @tool_cn_name("记录退回候选")
572
555
  def record_rollback_candidates(self, *, profile: str, app_key: str, apply_id: int, audit_node_id: int) -> dict[str, Any]:
573
556
  """执行记录相关逻辑。"""
574
557
  self._require_app_and_apply(app_key, apply_id)
@@ -591,9 +574,8 @@ class ApprovalTools(ToolBase):
591
574
  "request_route": self._request_route_payload(context),
592
575
  }
593
576
 
594
- return self._run(profile, runner)
577
+ return self._run(profile, runner, tool_name='记录退回候选')
595
578
 
596
- @tool_cn_name("记录退回")
597
579
  def record_rollback(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
598
580
  """执行记录相关逻辑。"""
599
581
  self._require_app_and_apply(app_key, apply_id)
@@ -611,9 +593,8 @@ class ApprovalTools(ToolBase):
611
593
  "request_route": self._request_route_payload(context),
612
594
  }
613
595
 
614
- return self._run(profile, runner)
596
+ return self._run(profile, runner, tool_name='记录退回')
615
597
 
616
- @tool_cn_name("记录转交")
617
598
  def record_transfer(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
618
599
  """执行记录相关逻辑。"""
619
600
  self._require_app_and_apply(app_key, apply_id)
@@ -631,9 +612,8 @@ class ApprovalTools(ToolBase):
631
612
  "request_route": self._request_route_payload(context),
632
613
  }
633
614
 
634
- return self._run(profile, runner)
615
+ return self._run(profile, runner, tool_name='记录转交')
635
616
 
636
- @tool_cn_name("记录转交候选")
637
617
  def record_transfer_candidates(
638
618
  self,
639
619
  *,
@@ -664,9 +644,8 @@ class ApprovalTools(ToolBase):
664
644
  "request_route": self._request_route_payload(context),
665
645
  }
666
646
 
667
- return self._run(profile, runner)
647
+ return self._run(profile, runner, tool_name='记录转交候选')
668
648
 
669
- @tool_cn_name("记录改派信息")
670
649
  def record_reassign_get(self, *, profile: str, app_key: str, apply_id: int) -> dict[str, Any]:
671
650
  """执行记录相关逻辑。"""
672
651
  self._require_app_and_apply(app_key, apply_id)
@@ -682,9 +661,8 @@ class ApprovalTools(ToolBase):
682
661
  "request_route": self._request_route_payload(context),
683
662
  }
684
663
 
685
- return self._run(profile, runner)
664
+ return self._run(profile, runner, tool_name='记录改派信息')
686
665
 
687
- @tool_cn_name("记录改派")
688
666
  def record_reassign(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
689
667
  """执行记录相关逻辑。"""
690
668
  self._require_app_and_apply(app_key, apply_id)
@@ -701,9 +679,8 @@ class ApprovalTools(ToolBase):
701
679
  "request_route": self._request_route_payload(context),
702
680
  }
703
681
 
704
- return self._run(profile, runner)
682
+ return self._run(profile, runner, tool_name='记录改派')
705
683
 
706
- @tool_cn_name("会签候选")
707
684
  def record_countersign_candidates(
708
685
  self,
709
686
  *,
@@ -734,9 +711,8 @@ class ApprovalTools(ToolBase):
734
711
  "request_route": self._request_route_payload(context),
735
712
  }
736
713
 
737
- return self._run(profile, runner)
714
+ return self._run(profile, runner, tool_name='会签候选')
738
715
 
739
- @tool_cn_name("会签")
740
716
  def record_countersign(self, *, profile: str, app_key: str, apply_id: int, payload: dict[str, Any]) -> dict[str, Any]:
741
717
  """执行记录相关逻辑。"""
742
718
  self._require_app_and_apply(app_key, apply_id)
@@ -754,7 +730,7 @@ class ApprovalTools(ToolBase):
754
730
  "request_route": self._request_route_payload(context),
755
731
  }
756
732
 
757
- return self._run(profile, runner)
733
+ return self._run(profile, runner, tool_name='会签')
758
734
 
759
735
  def _request_route_payload(self, context) -> JSONObject: # type: ignore[no-untyped-def]
760
736
  """执行内部辅助逻辑。"""
@@ -1,8 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
- from typing import Any
5
4
  import os
5
+ from typing import Any
6
6
 
7
7
  from mcp.server.fastmcp import FastMCP
8
8
 
@@ -17,7 +17,7 @@ from ..config import (
17
17
  )
18
18
  from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
19
19
  from ..session_store import SessionStore
20
- from .base import ToolBase, tool_cn_name
20
+ from .base import ToolBase
21
21
 
22
22
 
23
23
  class AuthTools(ToolBase):
@@ -75,7 +75,6 @@ class AuthTools(ToolBase):
75
75
  def auth_logout(profile: str = DEFAULT_PROFILE, forget_persisted: bool = False) -> dict[str, Any]:
76
76
  return self.auth_logout(profile=profile, forget_persisted=forget_persisted)
77
77
 
78
- @tool_cn_name("凭证登录")
79
78
  def auth_use_credential(
80
79
  self,
81
80
  *,
@@ -86,7 +85,8 @@ class AuthTools(ToolBase):
86
85
  persist: bool = False,
87
86
  ) -> dict[str, Any]:
88
87
  """执行认证与会话相关逻辑。"""
89
- resolved_base_url, resolved_credential = self._resolve_mcporter_auth_inputs(
88
+ resolved_base_url, resolved_credential = self._resolve_auth_inputs(
89
+ profile=profile,
90
90
  base_url=base_url,
91
91
  credential=credential,
92
92
  )
@@ -307,10 +307,18 @@ class AuthTools(ToolBase):
307
307
  ),
308
308
  }
309
309
 
310
- def _resolve_mcporter_auth_inputs(self, *, base_url: str | None, credential: str | None) -> tuple[str | None, str]:
311
- """从参数或 mcporter 配置解析登录所需 base_url 与 credential。"""
310
+ def _resolve_auth_inputs(
311
+ self,
312
+ *,
313
+ profile: str,
314
+ base_url: str | None,
315
+ credential: str | None,
316
+ ) -> tuple[str | None, str]:
317
+ """按显式参数、环境变量、mcporter、profile 的顺序解析认证输入。"""
312
318
  normalized_base_url = self._normalize_text(base_url)
313
319
  normalized_credential = self._normalize_text(credential)
320
+ if not normalized_credential:
321
+ normalized_credential = self._normalize_text(os.getenv("QINGFLOW_CREDENTIAL"))
314
322
  if normalized_base_url and normalized_credential:
315
323
  return normalized_base_url, normalized_credential
316
324
 
@@ -318,11 +326,13 @@ class AuthTools(ToolBase):
318
326
  if not normalized_base_url:
319
327
  normalized_base_url = self._normalize_text(mcporter_context.get("base_url"))
320
328
  if not normalized_base_url:
321
- normalized_base_url = os.getenv("QINGFLOW_BASE_URL")
329
+ normalized_base_url = self._normalize_text(os.getenv("QINGFLOW_BASE_URL"))
322
330
  if not normalized_credential:
323
331
  normalized_credential = self._normalize_text(mcporter_context.get("credential"))
324
332
  if not normalized_credential:
325
- normalized_credential = os.getenv("QINGFLOW_CREDENTIAL")
333
+ session_profile = self.sessions.peek_profile(profile)
334
+ if session_profile is not None:
335
+ normalized_credential = self._normalize_text(session_profile.credential)
326
336
  return normalized_base_url, normalized_credential or ""
327
337
 
328
338
  def _read_mcporter_qingflow_context(self) -> dict[str, str]:
@@ -356,22 +366,18 @@ class AuthTools(ToolBase):
356
366
  profile: str,
357
367
  session_profile,
358
368
  ) -> bool:
359
- backend_session = self.sessions.get_backend_session(profile) if session_profile is not None else None
360
369
  base_url = session_profile.base_url if session_profile is not None else None
361
- credential = backend_session.credential if backend_session is not None else None
362
370
  persist = bool(session_profile.persisted) if session_profile is not None else False
363
371
  try:
364
372
  self.auth_use_credential(
365
373
  profile=profile,
366
374
  base_url=base_url,
367
- credential=credential,
368
375
  persist=persist,
369
376
  )
370
377
  except (QingflowApiError, RuntimeError):
371
378
  return False
372
379
  return True
373
380
 
374
- @tool_cn_name("我的身份")
375
381
  def auth_whoami(self, *, profile: str = DEFAULT_PROFILE) -> dict[str, Any]:
376
382
  """执行认证与会话相关逻辑。"""
377
383
  def build_response(
@@ -503,7 +509,6 @@ class AuthTools(ToolBase):
503
509
  if error.looks_like_invalid_token():
504
510
  raise
505
511
 
506
- @tool_cn_name("退出登录")
507
512
  def auth_logout(self, *, profile: str = DEFAULT_PROFILE, forget_persisted: bool = False) -> dict[str, Any]:
508
513
  """执行认证与会话相关逻辑。"""
509
514
  if not self.sessions.has_profile(profile):
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- import inspect
3
+ import os
4
4
  from contextvars import ContextVar
5
5
  from typing import Any, Callable, TypeVar
6
6
 
@@ -20,17 +20,6 @@ T = TypeVar("T")
20
20
  _RUN_DEPTH: ContextVar[int] = ContextVar("qingflow_mcp_tool_run_depth", default=0)
21
21
 
22
22
 
23
- def tool_cn_name(name: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
24
- """为 tools 方法声明中文展示名(用于计费 extraInfo 记录)。"""
25
- normalized = str(name).strip()
26
-
27
- def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
28
- setattr(func, "__tool_cn_name__", normalized)
29
- return func
30
-
31
- return decorator
32
-
33
-
34
23
  class ToolBase:
35
24
  def __init__(self, sessions: SessionStore, backend: BackendClient) -> None:
36
25
  self.sessions = sessions
@@ -41,6 +30,7 @@ class ToolBase:
41
30
  if cloud_context is not None:
42
31
  return cloud_context.as_tool_context(profile)
43
32
 
33
+ self._ensure_environment_credential_matches(profile)
44
34
  session_profile = self.sessions.get_profile(profile)
45
35
  if session_profile is None:
46
36
  raise QingflowApiError.auth_required(profile)
@@ -62,19 +52,26 @@ class ToolBase:
62
52
  )
63
53
  return session_profile, backend_session, context
64
54
 
65
- def _run(self, profile: str, func: Callable[[SessionProfile, BackendRequestContext], T], *, require_workspace: bool = True) -> T:
55
+ def _run(
56
+ self,
57
+ profile: str,
58
+ func: Callable[[SessionProfile, BackendRequestContext], T],
59
+ *,
60
+ require_workspace: bool = True,
61
+ tool_name: str,
62
+ ) -> T:
66
63
  depth = _RUN_DEPTH.get()
67
64
  depth_token = _RUN_DEPTH.set(depth + 1)
68
65
  is_top_level_call = depth == 0
69
66
  session_profile: SessionProfile | None = None
70
67
  backend_session: BackendSession | None = None
71
- tool_name = self._resolve_calling_tool_name()
68
+ resolved_tool_name = self._normalize_tool_name(tool_name)
72
69
  try:
73
70
  session_profile, backend_session, context = self._require_context(profile, require_workspace=require_workspace)
74
71
  if is_top_level_call:
75
72
  self._charge_tool_call(
76
73
  profile=profile,
77
- tool_name=tool_name,
74
+ tool_name=resolved_tool_name,
78
75
  session_profile=session_profile,
79
76
  context=context,
80
77
  )
@@ -106,6 +103,12 @@ class ToolBase:
106
103
  _RUN_DEPTH.reset(depth_token)
107
104
  raise AssertionError("unreachable")
108
105
 
106
+ def _normalize_tool_name(self, tool_name: str) -> str:
107
+ normalized = str(tool_name or "").strip()
108
+ if not normalized:
109
+ raise_tool_error(QingflowApiError.config_error("tool_name is required for credit usage tracking"))
110
+ return normalized
111
+
109
112
  def _handle_error(self, profile: str, error: QingflowApiError) -> None:
110
113
  if error.looks_like_invalid_token():
111
114
  cloud_context = current_cloud_context.get()
@@ -150,50 +153,106 @@ class ToolBase:
150
153
  if not credential:
151
154
  return False
152
155
  try:
153
- response = self.backend.public_request_with_meta(
154
- "POST",
155
- session_profile.base_url,
156
- "/mcp/auth/context",
157
- json_body={"credential": credential},
158
- qf_version=backend_session.qf_version,
159
- )
160
- payload = self._unwrap_context_payload(response.data)
161
- token = self._normalize_text(payload.get("token"))
162
- ws_id = self._coerce_positive_int(payload.get("wsId"))
163
- uid = self._coerce_positive_int(payload.get("uid")) or session_profile.uid
164
- if not token or ws_id is None:
165
- return False
166
- backend_qf_version = self._normalize_text(payload.get("qfVersion")) or response.qf_response_version
167
- qf_version = backend_qf_version if backend_qf_version is not None else backend_session.qf_version
168
- qf_version_source = (
169
- "backend_response"
170
- if backend_qf_version is not None
171
- else backend_session.qf_version_source
172
- )
173
- base_url = self._normalize_text(payload.get("baseUrl")) or session_profile.base_url
174
- ws_name = self._normalize_text(payload.get("wsName")) or session_profile.selected_ws_name
175
- self.sessions.save_session(
176
- profile=profile,
177
- base_url=base_url,
178
- qf_version=qf_version,
179
- qf_version_source=qf_version_source,
180
- token=token,
181
- login_token=None,
156
+ self._exchange_session_from_credential(
157
+ profile,
158
+ session_profile=session_profile,
182
159
  credential=credential,
183
- uid=uid,
184
- email=self._normalize_text(payload.get("email")) or session_profile.email,
185
- nick_name=(
186
- self._normalize_text(payload.get("nickName"))
187
- or self._normalize_text(payload.get("displayName"))
188
- or self._normalize_text(payload.get("name"))
189
- or session_profile.nick_name
190
- ),
191
- persist=session_profile.persisted,
192
160
  )
193
- self.sessions.select_workspace(profile, ws_id=ws_id, ws_name=ws_name)
194
- return True
195
161
  except QingflowApiError:
196
162
  return False
163
+ return True
164
+
165
+ def _ensure_environment_credential_matches(self, profile: str) -> None:
166
+ environment_credential = self._normalize_text(os.getenv("QINGFLOW_CREDENTIAL"))
167
+ if environment_credential is None:
168
+ return
169
+ session_profile = self.sessions.peek_profile(profile)
170
+ if session_profile is None:
171
+ return
172
+ if self._normalize_text(session_profile.credential) == environment_credential:
173
+ return
174
+ try:
175
+ self._exchange_session_from_credential(
176
+ profile,
177
+ session_profile=session_profile,
178
+ credential=environment_credential,
179
+ preserve_existing_metadata=False,
180
+ )
181
+ except QingflowApiError as error:
182
+ raise QingflowApiError(
183
+ category="auth",
184
+ message=(
185
+ f"QINGFLOW_CREDENTIAL changed for profile '{profile}', "
186
+ "but the session could not be refreshed."
187
+ ),
188
+ backend_code=error.backend_code,
189
+ request_id=error.request_id,
190
+ http_status=error.http_status,
191
+ details={
192
+ "reason": "environment_credential_changed",
193
+ },
194
+ ) from error
195
+
196
+ def _exchange_session_from_credential(
197
+ self,
198
+ profile: str,
199
+ *,
200
+ session_profile: SessionProfile,
201
+ credential: str,
202
+ preserve_existing_metadata: bool = True,
203
+ ) -> None:
204
+ response = self.backend.public_request_with_meta(
205
+ "POST",
206
+ session_profile.base_url,
207
+ "/mcp/auth/context",
208
+ json_body={"credential": credential},
209
+ qf_version=session_profile.qf_version,
210
+ )
211
+ payload = self._unwrap_context_payload(response.data)
212
+ token = self._normalize_text(payload.get("token"))
213
+ ws_id = self._coerce_positive_int(payload.get("wsId"))
214
+ response_uid = self._coerce_positive_int(payload.get("uid"))
215
+ if not token or ws_id is None or (response_uid is None and not preserve_existing_metadata):
216
+ raise QingflowApiError(
217
+ category="auth",
218
+ message="Credential context did not return a valid user session and workspace.",
219
+ request_id=response.request_id,
220
+ http_status=response.http_status,
221
+ )
222
+ uid = response_uid or session_profile.uid
223
+ backend_qf_version = self._normalize_text(payload.get("qfVersion")) or response.qf_response_version
224
+ qf_version = backend_qf_version if backend_qf_version is not None else session_profile.qf_version
225
+ qf_version_source = (
226
+ "backend_response"
227
+ if backend_qf_version is not None
228
+ else session_profile.qf_version_source
229
+ )
230
+ base_url = self._normalize_text(payload.get("baseUrl")) or session_profile.base_url
231
+ ws_name = self._normalize_text(payload.get("wsName"))
232
+ email = self._normalize_text(payload.get("email"))
233
+ nick_name = (
234
+ self._normalize_text(payload.get("nickName"))
235
+ or self._normalize_text(payload.get("displayName"))
236
+ or self._normalize_text(payload.get("name"))
237
+ )
238
+ if preserve_existing_metadata:
239
+ ws_name = ws_name or session_profile.selected_ws_name
240
+ email = email or session_profile.email
241
+ nick_name = nick_name or session_profile.nick_name
242
+ self.sessions.save_session(
243
+ profile=profile,
244
+ base_url=base_url,
245
+ qf_version=qf_version,
246
+ qf_version_source=qf_version_source,
247
+ token=token,
248
+ login_token=None,
249
+ credential=credential,
250
+ uid=uid,
251
+ email=email,
252
+ nick_name=nick_name,
253
+ persist=session_profile.persisted,
254
+ )
255
+ self.sessions.select_workspace(profile, ws_id=ws_id, ws_name=ws_name)
197
256
 
198
257
  def _unwrap_context_payload(self, payload: Any) -> dict[str, Any]:
199
258
  if not isinstance(payload, dict):
@@ -221,25 +280,6 @@ class ToolBase:
221
280
  return None
222
281
  return parsed if parsed > 0 else None
223
282
 
224
- def _resolve_calling_tool_name(self) -> str:
225
- frame = inspect.currentframe()
226
- if frame is None:
227
- return "unknown_tool"
228
- caller = frame.f_back.f_back if frame.f_back is not None else None
229
- try:
230
- if caller is None:
231
- return "unknown_tool"
232
- name = caller.f_code.co_name
233
- if not name:
234
- return "unknown_tool"
235
- method = getattr(self, name, None)
236
- cn_name = getattr(method, "__tool_cn_name__", None)
237
- if isinstance(cn_name, str) and cn_name.strip():
238
- return cn_name.strip()
239
- return name
240
- finally:
241
- del frame
242
-
243
283
  def _charge_tool_call(
244
284
  self,
245
285
  *,
@@ -7,7 +7,6 @@ from mcp.server.fastmcp import FastMCP
7
7
  from ..config import DEFAULT_PROFILE, DEFAULT_RECORD_LIST_TYPE
8
8
  from ..errors import QingflowApiError, backend_code_int, backend_code_value_int, is_auth_like_error, message_looks_like_invalid_token, raise_tool_error
9
9
  from ..json_types import JSONObject, JSONValue
10
- from .base import tool_cn_name
11
10
  from .record_tools import (
12
11
  ATTACHMENT_QUE_TYPES,
13
12
  DEPARTMENT_QUE_TYPES,
@@ -156,7 +155,6 @@ class CodeBlockTools(RecordTools):
156
155
  output_profile=output_profile,
157
156
  )
158
157
 
159
- @tool_cn_name("代码块 Schema")
160
158
  def record_code_block_schema_get_public(
161
159
  self,
162
160
  *,
@@ -263,9 +261,8 @@ class CodeBlockTools(RecordTools):
263
261
  )
264
262
  return response
265
263
 
266
- return self._run_record_tool(profile, runner)
264
+ return self._run_record_tool(profile, runner, tool_name="代码块 Schema")
267
265
 
268
- @tool_cn_name("运行代码块")
269
266
  def record_code_block_run(
270
267
  self,
271
268
  *,
@@ -549,7 +546,7 @@ class CodeBlockTools(RecordTools):
549
546
  }
550
547
  return response
551
548
 
552
- return self._run_record_tool(profile, runner)
549
+ return self._run_record_tool(profile, runner, tool_name="运行代码块")
553
550
 
554
551
  def _resolve_code_block_field_for_run(self, selector: str | int, index: FieldIndex) -> FormField:
555
552
  field_id = _coerce_count(selector)