bdpan-cli 0.1.0__py3-none-any.whl
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.
- bdpan_cli/__init__.py +15 -0
- bdpan_cli/_vendor/__init__.py +9 -0
- bdpan_cli/_vendor/openapi_client/__init__.py +27 -0
- bdpan_cli/_vendor/openapi_client/api/__init__.py +6 -0
- bdpan_cli/_vendor/openapi_client/api/auth_api.py +649 -0
- bdpan_cli/_vendor/openapi_client/api/fileinfo_api.py +737 -0
- bdpan_cli/_vendor/openapi_client/api/filemanager_api.py +669 -0
- bdpan_cli/_vendor/openapi_client/api/fileupload_api.py +599 -0
- bdpan_cli/_vendor/openapi_client/api/multimediafile_api.py +391 -0
- bdpan_cli/_vendor/openapi_client/api/userinfo_api.py +315 -0
- bdpan_cli/_vendor/openapi_client/api_client.py +864 -0
- bdpan_cli/_vendor/openapi_client/configuration.py +454 -0
- bdpan_cli/_vendor/openapi_client/exceptions.py +197 -0
- bdpan_cli/_vendor/openapi_client/model/__init__.py +5 -0
- bdpan_cli/_vendor/openapi_client/model/oauth_token_authorization_code_response.py +276 -0
- bdpan_cli/_vendor/openapi_client/model/oauth_token_device_code_response.py +276 -0
- bdpan_cli/_vendor/openapi_client/model/oauth_token_device_token_response.py +276 -0
- bdpan_cli/_vendor/openapi_client/model/oauth_token_refresh_token_response.py +276 -0
- bdpan_cli/_vendor/openapi_client/model/quotaresponse.py +276 -0
- bdpan_cli/_vendor/openapi_client/model/uinforesponse.py +284 -0
- bdpan_cli/_vendor/openapi_client/model_utils.py +2039 -0
- bdpan_cli/_vendor/openapi_client/rest.py +385 -0
- bdpan_cli/_version.py +3 -0
- bdpan_cli/account.py +78 -0
- bdpan_cli/apply_scrape_rename.py +99 -0
- bdpan_cli/auth.py +218 -0
- bdpan_cli/cli.py +1428 -0
- bdpan_cli/config.py +26 -0
- bdpan_cli/dedup.py +310 -0
- bdpan_cli/downloader.py +178 -0
- bdpan_cli/file_manager.py +389 -0
- bdpan_cli/foldering.py +181 -0
- bdpan_cli/logger.py +101 -0
- bdpan_cli/mcp_server.py +547 -0
- bdpan_cli/media_info.py +251 -0
- bdpan_cli/ops.py +173 -0
- bdpan_cli/organize.py +108 -0
- bdpan_cli/preview_scrape.py +182 -0
- bdpan_cli/scraper.py +505 -0
- bdpan_cli/uploader.py +391 -0
- bdpan_cli-0.1.0.dist-info/METADATA +167 -0
- bdpan_cli-0.1.0.dist-info/RECORD +44 -0
- bdpan_cli-0.1.0.dist-info/WHEEL +4 -0
- bdpan_cli-0.1.0.dist-info/entry_points.txt +5 -0
bdpan_cli/mcp_server.py
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
"""BdpanMcpServer: 百度网盘官方开放平台 MCP (Model Context Protocol) 服务的原生纯净实现。
|
|
2
|
+
|
|
3
|
+
遵循 Model Context Protocol 规范与 JSON-RPC 2.0 协议标准,通过标准 stdio 与 LLM(Claude Code、
|
|
4
|
+
Antigravity、Cursor 等 AI 编程助手)进行双向通信,自省暴露网盘核心文件管理与可观测性能力。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Any, Callable
|
|
14
|
+
|
|
15
|
+
from bdpan_cli.account import AccountClient
|
|
16
|
+
from bdpan_cli.file_manager import FileManager, FileManagerError
|
|
17
|
+
from bdpan_cli.media_info import MediaInfoClient, MediaInfoError
|
|
18
|
+
from bdpan_cli.ops import OperationLogger, UndoEngine, UndoError
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger("bdpan_cli.mcp")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _format_size(size_bytes: int) -> str:
|
|
24
|
+
if size_bytes < 1024:
|
|
25
|
+
return f"{size_bytes} B"
|
|
26
|
+
elif size_bytes < 1024 * 1024:
|
|
27
|
+
return f"{size_bytes / 1024:.2f} KB"
|
|
28
|
+
elif size_bytes < 1024 * 1024 * 1024:
|
|
29
|
+
return f"{size_bytes / (1024 * 1024):.2f} MB"
|
|
30
|
+
else:
|
|
31
|
+
return f"{size_bytes / (1024 * 1024 * 1024):.2f} GB"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BdpanMcpServer:
|
|
35
|
+
"""网盘原生 MCP Server。"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, token_getter: Callable[[], str]) -> None:
|
|
38
|
+
self.token_getter = token_getter
|
|
39
|
+
self.protocol_version = "2024-11-05"
|
|
40
|
+
self.server_info = {
|
|
41
|
+
"name": "bdpan-mcp",
|
|
42
|
+
"version": "0.1.0",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
def _get_file_manager(self) -> FileManager:
|
|
46
|
+
token = self.token_getter()
|
|
47
|
+
return FileManager(token)
|
|
48
|
+
|
|
49
|
+
def _get_account_client(self) -> AccountClient:
|
|
50
|
+
token = self.token_getter()
|
|
51
|
+
return AccountClient(token)
|
|
52
|
+
|
|
53
|
+
def _get_media_info_client(self) -> MediaInfoClient:
|
|
54
|
+
token = self.token_getter()
|
|
55
|
+
return MediaInfoClient(token)
|
|
56
|
+
|
|
57
|
+
def get_tool_definitions(self) -> list[dict[str, Any]]:
|
|
58
|
+
"""获取所有自省暴露的 MCP 工具元数据清单与 JSON Schema。"""
|
|
59
|
+
return [
|
|
60
|
+
{
|
|
61
|
+
"name": "bdpan_ls",
|
|
62
|
+
"description": "列出网盘指定目录的内容,获取子目录与文件条目清单(包含文件名、大小、修改时间与类型)",
|
|
63
|
+
"inputSchema": {
|
|
64
|
+
"type": "object",
|
|
65
|
+
"properties": {
|
|
66
|
+
"path": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"description": "网盘目录绝对路径 (默认: '/')",
|
|
69
|
+
"default": "/",
|
|
70
|
+
},
|
|
71
|
+
"limit": {
|
|
72
|
+
"type": "integer",
|
|
73
|
+
"description": "最多读取条目数 (默认: 1000)",
|
|
74
|
+
"default": 1000,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"name": "bdpan_mkdir",
|
|
81
|
+
"description": "在网盘中创建新目录",
|
|
82
|
+
"inputSchema": {
|
|
83
|
+
"type": "object",
|
|
84
|
+
"properties": {
|
|
85
|
+
"path": {
|
|
86
|
+
"type": "string",
|
|
87
|
+
"description": "待创建的目录绝对路径",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
"required": ["path"],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "bdpan_mv",
|
|
95
|
+
"description": "在网盘中移动或重命名文件/目录(支持单源移动改名,多源批量移动至已存在目录)",
|
|
96
|
+
"inputSchema": {
|
|
97
|
+
"type": "object",
|
|
98
|
+
"properties": {
|
|
99
|
+
"srcs": {
|
|
100
|
+
"type": "array",
|
|
101
|
+
"items": {"type": "string"},
|
|
102
|
+
"description": "一个或多个源文件/目录绝对路径",
|
|
103
|
+
},
|
|
104
|
+
"dest": {
|
|
105
|
+
"type": "string",
|
|
106
|
+
"description": "目标目录路径或 '目标目录/新文件名'",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
"required": ["srcs", "dest"],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"name": "bdpan_cp",
|
|
114
|
+
"description": "在网盘中复制文件或目录",
|
|
115
|
+
"inputSchema": {
|
|
116
|
+
"type": "object",
|
|
117
|
+
"properties": {
|
|
118
|
+
"srcs": {
|
|
119
|
+
"type": "array",
|
|
120
|
+
"items": {"type": "string"},
|
|
121
|
+
"description": "一个或多个待复制的源路径",
|
|
122
|
+
},
|
|
123
|
+
"dest": {
|
|
124
|
+
"type": "string",
|
|
125
|
+
"description": "目标目录路径",
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
"required": ["srcs", "dest"],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"name": "bdpan_rm",
|
|
133
|
+
"description": "删除网盘文件或目录(服务端安全移入回收站)",
|
|
134
|
+
"inputSchema": {
|
|
135
|
+
"type": "object",
|
|
136
|
+
"properties": {
|
|
137
|
+
"paths": {
|
|
138
|
+
"type": "array",
|
|
139
|
+
"items": {"type": "string"},
|
|
140
|
+
"description": "一个或多个待删除的网盘文件/目录路径",
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
"required": ["paths"],
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"name": "bdpan_rename",
|
|
148
|
+
"description": "原地重命名网盘文件或目录",
|
|
149
|
+
"inputSchema": {
|
|
150
|
+
"type": "object",
|
|
151
|
+
"properties": {
|
|
152
|
+
"path": {
|
|
153
|
+
"type": "string",
|
|
154
|
+
"description": "待重命名的文件或目录路径",
|
|
155
|
+
},
|
|
156
|
+
"new_name": {
|
|
157
|
+
"type": "string",
|
|
158
|
+
"description": "新文件名(仅名称,不包含父目录)",
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
"required": ["path", "new_name"],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"name": "bdpan_search",
|
|
166
|
+
"description": "按关键词全盘或在指定目录下搜索文件与目录",
|
|
167
|
+
"inputSchema": {
|
|
168
|
+
"type": "object",
|
|
169
|
+
"properties": {
|
|
170
|
+
"key": {
|
|
171
|
+
"type": "string",
|
|
172
|
+
"description": "搜索关键词",
|
|
173
|
+
},
|
|
174
|
+
"dir": {
|
|
175
|
+
"type": "string",
|
|
176
|
+
"description": "限定搜索目录 (默认全盘)",
|
|
177
|
+
},
|
|
178
|
+
"recursion": {
|
|
179
|
+
"type": "boolean",
|
|
180
|
+
"description": "是否递归子目录 (默认 True)",
|
|
181
|
+
"default": True,
|
|
182
|
+
},
|
|
183
|
+
"page": {"type": "integer", "default": 1},
|
|
184
|
+
"num": {"type": "integer", "default": 100},
|
|
185
|
+
},
|
|
186
|
+
"required": ["key"],
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"name": "bdpan_tree",
|
|
191
|
+
"description": "递归遍历网盘目录树结构,输出完整的层级拓扑与容量统计",
|
|
192
|
+
"inputSchema": {
|
|
193
|
+
"type": "object",
|
|
194
|
+
"properties": {
|
|
195
|
+
"path": {
|
|
196
|
+
"type": "string",
|
|
197
|
+
"description": "待遍历的根目录绝对路径 (默认: '/')",
|
|
198
|
+
"default": "/",
|
|
199
|
+
},
|
|
200
|
+
"depth": {
|
|
201
|
+
"type": "integer",
|
|
202
|
+
"description": "最大递归深度 (默认不限)",
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"name": "bdpan_quota",
|
|
209
|
+
"description": "查询网盘账号的总容量、已用空间、剩余可用配额及会员到期时间",
|
|
210
|
+
"inputSchema": {
|
|
211
|
+
"type": "object",
|
|
212
|
+
"properties": {
|
|
213
|
+
"check_expire": {"type": "boolean", "default": True},
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"name": "bdpan_whoami",
|
|
219
|
+
"description": "查询当前网盘账号身份、百度用户名、网盘名与会员等级 (普通/普通会员/超级会员)",
|
|
220
|
+
"inputSchema": {
|
|
221
|
+
"type": "object",
|
|
222
|
+
"properties": {},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"name": "bdpan_info",
|
|
227
|
+
"description": "探测网盘视频媒体的深度元数据(包括精准分辨率、4K/1080P标称、时长、视频编码格式HEVC/H.264、音轨信息与大小)",
|
|
228
|
+
"inputSchema": {
|
|
229
|
+
"type": "object",
|
|
230
|
+
"properties": {
|
|
231
|
+
"path": {
|
|
232
|
+
"type": "string",
|
|
233
|
+
"description": "视频文件网盘绝对路径",
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
"required": ["path"],
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
"name": "bdpan_stats",
|
|
241
|
+
"description": "递归聚合统计网盘指定目录下的总文件数、子目录数、总容量大小及各类媒体扩展名分布",
|
|
242
|
+
"inputSchema": {
|
|
243
|
+
"type": "object",
|
|
244
|
+
"properties": {
|
|
245
|
+
"path": {
|
|
246
|
+
"type": "string",
|
|
247
|
+
"description": "网盘目录绝对路径 (默认: '/')",
|
|
248
|
+
"default": "/",
|
|
249
|
+
},
|
|
250
|
+
"depth": {
|
|
251
|
+
"type": "integer",
|
|
252
|
+
"description": "最大遍历深度",
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
"name": "bdpan_ops_list",
|
|
259
|
+
"description": "查看近期执行的网盘批量操作审计流水列表(包含操作ID、时间、涉及条目数与撤销状态)",
|
|
260
|
+
"inputSchema": {
|
|
261
|
+
"type": "object",
|
|
262
|
+
"properties": {
|
|
263
|
+
"limit": {"type": "integer", "default": 20},
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"name": "bdpan_undo",
|
|
269
|
+
"description": "基于操作审计流水号,对历史误操作或批量变更进行一键逆向撤销回滚复原",
|
|
270
|
+
"inputSchema": {
|
|
271
|
+
"type": "object",
|
|
272
|
+
"properties": {
|
|
273
|
+
"op_id": {
|
|
274
|
+
"type": "string",
|
|
275
|
+
"description": "操作流水号 (如 op_xxxx)",
|
|
276
|
+
},
|
|
277
|
+
"dry_run": {
|
|
278
|
+
"type": "boolean",
|
|
279
|
+
"description": "是否仅预演逆向方案,不实际执行变更 (默认 False)",
|
|
280
|
+
"default": False,
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
"required": ["op_id"],
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
def execute_tool(self, name: str, args: dict[str, Any]) -> dict[str, Any]:
|
|
289
|
+
"""执行具体的 MCP 工具调用,返回 MCP 标准结果。"""
|
|
290
|
+
try:
|
|
291
|
+
if name == "bdpan_ls":
|
|
292
|
+
fm = self._get_file_manager()
|
|
293
|
+
path = args.get("path", "/")
|
|
294
|
+
limit = args.get("limit", 1000)
|
|
295
|
+
entries = fm.list_dir(path, limit=limit)
|
|
296
|
+
dirs = [e for e in entries if e.get("isdir") == 1]
|
|
297
|
+
files = [e for e in entries if e.get("isdir") != 1]
|
|
298
|
+
total_sz = sum(f.get("size", 0) for f in files)
|
|
299
|
+
txt = f"📂 目录: {path} (共 {len(entries)} 项,包含 {len(dirs)} 个子目录,{len(files)} 个文件,总大小: {_format_size(total_sz)})\n"
|
|
300
|
+
for idx, e in enumerate(entries, 1):
|
|
301
|
+
tag = "[DIR]" if e.get("isdir") == 1 else "[FILE]"
|
|
302
|
+
sz = "" if e.get("isdir") == 1 else f" ({_format_size(e.get('size', 0))})"
|
|
303
|
+
txt += f" {idx}. {tag} {e.get('server_filename')}{sz}\n"
|
|
304
|
+
return {"content": [{"type": "text", "text": txt}], "isError": False}
|
|
305
|
+
|
|
306
|
+
elif name == "bdpan_mkdir":
|
|
307
|
+
fm = self._get_file_manager()
|
|
308
|
+
path = args["path"]
|
|
309
|
+
fm.mkdir(path)
|
|
310
|
+
return {"content": [{"type": "text", "text": f"✅ 目录创建成功: {path}"}], "isError": False}
|
|
311
|
+
|
|
312
|
+
elif name == "bdpan_mv":
|
|
313
|
+
fm = self._get_file_manager()
|
|
314
|
+
srcs = args["srcs"]
|
|
315
|
+
dest = args["dest"]
|
|
316
|
+
if len(srcs) == 1:
|
|
317
|
+
src = srcs[0]
|
|
318
|
+
dest_dir = dest.rstrip("/") if dest.endswith("/") else os.path.dirname(dest)
|
|
319
|
+
dest_name = os.path.basename(dest) if not dest.endswith("/") else os.path.basename(src)
|
|
320
|
+
fm.batch_move([(src, dest_dir, dest_name)])
|
|
321
|
+
dest_path = f"{dest_dir.rstrip('/')}/{dest_name}"
|
|
322
|
+
op_id = OperationLogger().record_operation("mv", [{"src": src, "dest": dest_path}], account="mcp")
|
|
323
|
+
return {"content": [{"type": "text", "text": f"✅ 移动成功: {src} -> {dest_path} (流水号: {op_id})"}], "isError": False}
|
|
324
|
+
else:
|
|
325
|
+
dest_dir = dest.rstrip("/")
|
|
326
|
+
payloads = [(s, dest_dir, os.path.basename(s.rstrip("/"))) for s in srcs]
|
|
327
|
+
fm.batch_move(payloads)
|
|
328
|
+
items = [{"src": s, "dest": f"{dest_dir.rstrip('/')}/{n}"} for s, _, n in payloads]
|
|
329
|
+
op_id = OperationLogger().record_operation("mv", items, account="mcp")
|
|
330
|
+
return {"content": [{"type": "text", "text": f"✅ 批量移动成功: {len(payloads)} 项 -> {dest_dir}/ (流水号: {op_id})"}], "isError": False}
|
|
331
|
+
|
|
332
|
+
elif name == "bdpan_cp":
|
|
333
|
+
fm = self._get_file_manager()
|
|
334
|
+
srcs = args["srcs"]
|
|
335
|
+
dest = args["dest"].rstrip("/")
|
|
336
|
+
payloads = [(s, dest, os.path.basename(s.rstrip("/"))) for s in srcs]
|
|
337
|
+
fm.batch_copy(payloads)
|
|
338
|
+
return {"content": [{"type": "text", "text": f"✅ 批量复制成功: {len(payloads)} 项 -> {dest}/"}], "isError": False}
|
|
339
|
+
|
|
340
|
+
elif name == "bdpan_rm":
|
|
341
|
+
fm = self._get_file_manager()
|
|
342
|
+
paths = args["paths"]
|
|
343
|
+
failed = 0
|
|
344
|
+
for p in paths:
|
|
345
|
+
try:
|
|
346
|
+
fm.delete(p)
|
|
347
|
+
except FileManagerError:
|
|
348
|
+
failed += 1
|
|
349
|
+
return {"content": [{"type": "text", "text": f"✅ 删除执行完毕: 成功 {len(paths) - failed} 项,失败 {failed} 项(移入回收站)"}], "isError": failed > 0}
|
|
350
|
+
|
|
351
|
+
elif name == "bdpan_rename":
|
|
352
|
+
fm = self._get_file_manager()
|
|
353
|
+
path = args["path"]
|
|
354
|
+
new_name = args["new_name"]
|
|
355
|
+
fm.rename(path, new_name)
|
|
356
|
+
parent = os.path.dirname(path) or "/"
|
|
357
|
+
dest_path = f"{parent.rstrip('/')}/{new_name}"
|
|
358
|
+
op_id = OperationLogger().record_operation("rename", [{"src": path, "dest": dest_path}], account="mcp")
|
|
359
|
+
return {"content": [{"type": "text", "text": f"✅ 重命名成功: {path} -> {new_name} (流水号: {op_id})"}], "isError": False}
|
|
360
|
+
|
|
361
|
+
elif name == "bdpan_search":
|
|
362
|
+
fm = self._get_file_manager()
|
|
363
|
+
entries = fm.search(args["key"], dir_path=args.get("dir"), recursion=args.get("recursion", True), page=args.get("page", 1), num=args.get("num", 100))
|
|
364
|
+
txt = f"🔍 搜索关键词 [{args['key']}] (匹配到 {len(entries)} 项):\n"
|
|
365
|
+
for idx, e in enumerate(entries, 1):
|
|
366
|
+
tag = "[DIR]" if e.get("isdir") == 1 else "[FILE]"
|
|
367
|
+
sz = "" if e.get("isdir") == 1 else f" ({_format_size(e.get('size', 0))})"
|
|
368
|
+
txt += f" {idx}. {tag} {e.get('path')}{sz}\n"
|
|
369
|
+
return {"content": [{"type": "text", "text": txt}], "isError": False}
|
|
370
|
+
|
|
371
|
+
elif name == "bdpan_tree":
|
|
372
|
+
fm = self._get_file_manager()
|
|
373
|
+
entries, errors = fm.walk(args.get("path", "/"), max_depth=args.get("depth"))
|
|
374
|
+
dirs = [e for _, e in entries if e.get("isdir") == 1]
|
|
375
|
+
files = [e for _, e in entries if e.get("isdir") != 1]
|
|
376
|
+
total_sz = sum(f.get("size", 0) for f in files)
|
|
377
|
+
txt = f"🌲 目录树: {args.get('path', '/')} (共 {len(dirs)} 目录, {len(files)} 文件, 总计 {_format_size(total_sz)}):\n"
|
|
378
|
+
for depth, e in entries:
|
|
379
|
+
indent = " " * depth
|
|
380
|
+
marker = "📁" if e.get("isdir") == 1 else "📄"
|
|
381
|
+
sz = "" if e.get("isdir") == 1 else f" ({_format_size(e.get('size', 0))})"
|
|
382
|
+
txt += f"{indent}{marker} {e.get('server_filename')}{sz}\n"
|
|
383
|
+
return {"content": [{"type": "text", "text": txt}], "isError": False}
|
|
384
|
+
|
|
385
|
+
elif name == "bdpan_quota":
|
|
386
|
+
ac = self._get_account_client()
|
|
387
|
+
res = ac.quota(check_expire=args.get("check_expire", True))
|
|
388
|
+
tot = int(res.get("total") or 0)
|
|
389
|
+
used = int(res.get("used") or 0)
|
|
390
|
+
txt = f"💾 网盘空间配额:\n 总空间: {_format_size(tot)}\n 已用: {_format_size(used)}\n 可用: {_format_size(max(tot - used, 0))} ({used/tot*100:.1f}%)"
|
|
391
|
+
return {"content": [{"type": "text", "text": txt}], "isError": False}
|
|
392
|
+
|
|
393
|
+
elif name == "bdpan_whoami":
|
|
394
|
+
ac = self._get_account_client()
|
|
395
|
+
res = ac.user_info()
|
|
396
|
+
vip_map = {0: "普通用户", 1: "普通会员", 2: "超级会员"}
|
|
397
|
+
vip_desc = vip_map.get(res.get("vip_type"), f"未知({res.get('vip_type')})")
|
|
398
|
+
txt = f"👤 网盘用户信息:\n 百度账号: {res.get('baidu_name') or '-'}\n 网盘昵称: {res.get('netdisk_name') or '-'}\n 会员等级: {vip_desc}"
|
|
399
|
+
return {"content": [{"type": "text", "text": txt}], "isError": False}
|
|
400
|
+
|
|
401
|
+
elif name == "bdpan_info":
|
|
402
|
+
mc = self._get_media_info_client()
|
|
403
|
+
info = mc.get_media_info(args["path"])
|
|
404
|
+
txt = (
|
|
405
|
+
f"🎬 媒体元数据 [{info['filename']}]:\n"
|
|
406
|
+
f" 路径: {info['path']}\n"
|
|
407
|
+
f" 大小: {_format_size(info['size'])}\n"
|
|
408
|
+
f" 分辨率: {info['resolution']}\n"
|
|
409
|
+
f" 时长: {info['duration_formatted']} ({info['duration_seconds']} 秒)\n"
|
|
410
|
+
f" 视频编码: {info['video_codec']}\n"
|
|
411
|
+
f" 音轨数: {info['audio_tracks_count']}"
|
|
412
|
+
)
|
|
413
|
+
return {"content": [{"type": "text", "text": txt}], "isError": False}
|
|
414
|
+
|
|
415
|
+
elif name == "bdpan_stats":
|
|
416
|
+
mc = self._get_media_info_client()
|
|
417
|
+
stats = mc.get_dir_stats(args.get("path", "/"), max_depth=args.get("depth"))
|
|
418
|
+
txt = (
|
|
419
|
+
f"📊 目录聚合统计 [{stats['root']}]:\n"
|
|
420
|
+
f" 总容量: {_format_size(stats['total_size'])}\n"
|
|
421
|
+
f" 文件总数: {stats['total_files']} 个\n"
|
|
422
|
+
f" 子目录数: {stats['total_dirs']} 个\n"
|
|
423
|
+
f" 类别分布: 视频 {stats['by_category']['video']['count']} 个 ({_format_size(stats['by_category']['video']['size'])}), "
|
|
424
|
+
f"字幕 {stats['by_category']['subtitle']['count']} 个, "
|
|
425
|
+
f"图片 {stats['by_category']['image']['count']} 个"
|
|
426
|
+
)
|
|
427
|
+
return {"content": [{"type": "text", "text": txt}], "isError": False}
|
|
428
|
+
|
|
429
|
+
elif name == "bdpan_ops_list":
|
|
430
|
+
ops = OperationLogger().list_operations(limit=args.get("limit", 20))
|
|
431
|
+
txt = f"📜 最近操作审计流水 (共 {len(ops)} 条):\n"
|
|
432
|
+
for o in ops:
|
|
433
|
+
status = "[已撤销]" if o.get("undone") else "[正常]"
|
|
434
|
+
cnt = len(o.get("items") or [])
|
|
435
|
+
txt += f" [{o['op_id']}] | {o.get('created_at', '')[:19]} | 命令: {o.get('command')} | 影响: {cnt} 项 | {status}\n"
|
|
436
|
+
return {"content": [{"type": "text", "text": txt}], "isError": False}
|
|
437
|
+
|
|
438
|
+
elif name == "bdpan_undo":
|
|
439
|
+
fm = self._get_file_manager()
|
|
440
|
+
engine = UndoEngine(fm)
|
|
441
|
+
op_id = args["op_id"]
|
|
442
|
+
dry_run = args.get("dry_run", False)
|
|
443
|
+
if dry_run:
|
|
444
|
+
plan = engine.plan_undo(op_id)
|
|
445
|
+
reversals = plan.get("reversals") or []
|
|
446
|
+
txt = f"🔍 [dry-run] 逆向回滚预演 [{op_id}] (共 {len(reversals)} 项):\n"
|
|
447
|
+
for r in reversals:
|
|
448
|
+
txt += f" ↩️ {r['src']} -> {r['dest_dir']}/{r['newname']}\n"
|
|
449
|
+
return {"content": [{"type": "text", "text": txt}], "isError": False}
|
|
450
|
+
res = engine.execute_undo(op_id, dry_run=False)
|
|
451
|
+
return {"content": [{"type": "text", "text": f"🎉 成功逆向还原 {res['undone_count']} 项!"}], "isError": False}
|
|
452
|
+
|
|
453
|
+
else:
|
|
454
|
+
return {
|
|
455
|
+
"content": [{"type": "text", "text": f"❌ 未知的工具: {name}"}],
|
|
456
|
+
"isError": True,
|
|
457
|
+
}
|
|
458
|
+
except Exception as e:
|
|
459
|
+
logger.exception("执行 MCP 工具异常: %s", name)
|
|
460
|
+
return {
|
|
461
|
+
"content": [{"type": "text", "text": f"❌ 工具执行失败 [{name}]: {e}"}],
|
|
462
|
+
"isError": True,
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
def handle_request(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
466
|
+
"""处理单次 JSON-RPC 2.0 请求并返回响应。"""
|
|
467
|
+
req_id = request.get("id")
|
|
468
|
+
method = request.get("method")
|
|
469
|
+
params = request.get("params") or {}
|
|
470
|
+
|
|
471
|
+
if method == "initialize":
|
|
472
|
+
return {
|
|
473
|
+
"jsonrpc": "2.0",
|
|
474
|
+
"id": req_id,
|
|
475
|
+
"result": {
|
|
476
|
+
"protocolVersion": self.protocol_version,
|
|
477
|
+
"serverInfo": self.server_info,
|
|
478
|
+
"capabilities": {
|
|
479
|
+
"tools": {},
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
elif method == "notifications/initialized":
|
|
485
|
+
# 客户端通知无需响应
|
|
486
|
+
return {}
|
|
487
|
+
|
|
488
|
+
elif method == "ping":
|
|
489
|
+
return {
|
|
490
|
+
"jsonrpc": "2.0",
|
|
491
|
+
"id": req_id,
|
|
492
|
+
"result": {},
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
elif method == "tools/list":
|
|
496
|
+
return {
|
|
497
|
+
"jsonrpc": "2.0",
|
|
498
|
+
"id": req_id,
|
|
499
|
+
"result": {
|
|
500
|
+
"tools": self.get_tool_definitions(),
|
|
501
|
+
},
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
elif method == "tools/call":
|
|
505
|
+
tool_name = params.get("name", "")
|
|
506
|
+
tool_args = params.get("arguments") or {}
|
|
507
|
+
result = self.execute_tool(tool_name, tool_args)
|
|
508
|
+
return {
|
|
509
|
+
"jsonrpc": "2.0",
|
|
510
|
+
"id": req_id,
|
|
511
|
+
"result": result,
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
else:
|
|
515
|
+
return {
|
|
516
|
+
"jsonrpc": "2.0",
|
|
517
|
+
"id": req_id,
|
|
518
|
+
"error": {
|
|
519
|
+
"code": -32601,
|
|
520
|
+
"message": f"Method not found: {method}",
|
|
521
|
+
},
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
def run_stdio_server(self) -> None:
|
|
525
|
+
"""启动基于标准输入输出 (stdio) 的 MCP 服务监听循环。"""
|
|
526
|
+
logger.info("bdpan MCP Server starting on stdio...")
|
|
527
|
+
for line in sys.stdin:
|
|
528
|
+
line_str = line.strip()
|
|
529
|
+
if not line_str:
|
|
530
|
+
continue
|
|
531
|
+
try:
|
|
532
|
+
req = json.loads(line_str)
|
|
533
|
+
resp = self.handle_request(req)
|
|
534
|
+
if resp: # 通知无需响应
|
|
535
|
+
sys.stdout.write(json.dumps(resp, ensure_ascii=False) + "\n")
|
|
536
|
+
sys.stdout.flush()
|
|
537
|
+
except Exception as e:
|
|
538
|
+
err_resp = {
|
|
539
|
+
"jsonrpc": "2.0",
|
|
540
|
+
"id": None,
|
|
541
|
+
"error": {
|
|
542
|
+
"code": -32700,
|
|
543
|
+
"message": f"Parse error or unhandled exception: {e}",
|
|
544
|
+
},
|
|
545
|
+
}
|
|
546
|
+
sys.stdout.write(json.dumps(err_resp, ensure_ascii=False) + "\n")
|
|
547
|
+
sys.stdout.flush()
|