@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.
- package/README.md +3 -3
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/builder_facade/service.py +8 -8
- package/src/qingflow_mcp/server_cloud.py +89 -3
- package/src/qingflow_mcp/session_store.py +7 -1
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1 -59
- package/src/qingflow_mcp/tools/app_tools.py +15 -29
- package/src/qingflow_mcp/tools/approval_tools.py +16 -40
- package/src/qingflow_mcp/tools/auth_tools.py +18 -13
- package/src/qingflow_mcp/tools/base.py +114 -74
- package/src/qingflow_mcp/tools/code_block_tools.py +2 -5
- package/src/qingflow_mcp/tools/custom_button_tools.py +6 -11
- package/src/qingflow_mcp/tools/directory_tools.py +9 -16
- package/src/qingflow_mcp/tools/export_tools.py +5 -9
- package/src/qingflow_mcp/tools/feedback_tools.py +0 -2
- package/src/qingflow_mcp/tools/file_tools.py +3 -5
- package/src/qingflow_mcp/tools/import_tools.py +7 -13
- package/src/qingflow_mcp/tools/navigation_tools.py +12 -23
- package/src/qingflow_mcp/tools/package_tools.py +11 -21
- package/src/qingflow_mcp/tools/portal_tools.py +9 -17
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +18 -23
- package/src/qingflow_mcp/tools/record_tools.py +97 -53
- package/src/qingflow_mcp/tools/repository_dev_tools.py +4 -7
- package/src/qingflow_mcp/tools/resource_read_tools.py +6 -9
- package/src/qingflow_mcp/tools/role_tools.py +5 -9
- package/src/qingflow_mcp/tools/solution_tools.py +4 -16
- package/src/qingflow_mcp/tools/task_context_tools.py +6 -12
- package/src/qingflow_mcp/tools/task_tools.py +12 -29
- package/src/qingflow_mcp/tools/view_tools.py +23 -32
- package/src/qingflow_mcp/tools/workflow_tools.py +30 -47
- 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
|
|
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
|
|
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.
|
|
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
|
|
311
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
session_profile
|
|
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)
|