baiduwangpan-cli 1.6.0 → 1.7.0
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/LICENSE +21 -21
- package/README.md +504 -471
- package/bin/bdp.js +1647 -1452
- package/lib/browser-login.js +427 -427
- package/lib/config.js +213 -213
- package/lib/console.js +99 -99
- package/lib/file-type.js +109 -0
- package/lib/group.js +929 -925
- package/lib/http.js +130 -130
- package/lib/index.js +7 -7
- package/lib/mcp.js +287 -0
- package/lib/name-match.js +67 -67
- package/lib/pan.js +1034 -767
- package/lib/partial.js +53 -53
- package/lib/pcs-extra.js +327 -318
- package/lib/table.js +173 -0
- package/package.json +48 -48
- package/scripts/postinstall.js +340 -340
package/lib/http.js
CHANGED
|
@@ -1,130 +1,130 @@
|
|
|
1
|
-
const { spawnSync } = require("child_process");
|
|
2
|
-
const config = require("./config");
|
|
3
|
-
|
|
4
|
-
function buildCookieHeader(cfg) {
|
|
5
|
-
if (cfg.cookie) return cfg.cookie;
|
|
6
|
-
return `BDUSS=${cfg.bduss}; STOKEN=${cfg.stoken}`;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function curlGet(url, extraHeaders = {}) {
|
|
10
|
-
const cfg = config.get();
|
|
11
|
-
const headers = {
|
|
12
|
-
Cookie: buildCookieHeader(cfg),
|
|
13
|
-
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
14
|
-
Referer: "https://pan.baidu.com/disk/main",
|
|
15
|
-
...extraHeaders,
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const args = ["-s", "-L", "--max-time", "30", "--compressed"];
|
|
19
|
-
for (const [k, v] of Object.entries(headers)) {
|
|
20
|
-
args.push("-H", `${k}: ${v}`);
|
|
21
|
-
}
|
|
22
|
-
args.push(url);
|
|
23
|
-
|
|
24
|
-
const result = spawnSync("curl", args, { encoding: "utf-8", maxBuffer: 50 * 1024 * 1024 });
|
|
25
|
-
return result.stdout || "";
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function curlPost(url, body = "", extraHeaders = {}) {
|
|
29
|
-
const cfg = config.get();
|
|
30
|
-
const headers = {
|
|
31
|
-
Cookie: buildCookieHeader(cfg),
|
|
32
|
-
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
33
|
-
Referer: "https://pan.baidu.com/disk/main",
|
|
34
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
35
|
-
...extraHeaders,
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const args = ["-s", "-L", "-X", "POST", "--max-time", "30", "--compressed"];
|
|
39
|
-
for (const [k, v] of Object.entries(headers)) {
|
|
40
|
-
args.push("-H", `${k}: ${v}`);
|
|
41
|
-
}
|
|
42
|
-
if (body) args.push("-d", body);
|
|
43
|
-
args.push(url);
|
|
44
|
-
|
|
45
|
-
const result = spawnSync("curl", args, { encoding: "utf-8", maxBuffer: 50 * 1024 * 1024 });
|
|
46
|
-
return result.stdout || "";
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function curlRange(url, start, end, ua) {
|
|
50
|
-
const rangeArg = end != null ? `${start}-${end}` : `${start}-`;
|
|
51
|
-
const args = [
|
|
52
|
-
"-s", "-L",
|
|
53
|
-
"-r", rangeArg,
|
|
54
|
-
"-H", `User-Agent: ${ua}`,
|
|
55
|
-
"-H", `Range: bytes=${rangeArg}`,
|
|
56
|
-
"--max-time", "30",
|
|
57
|
-
url,
|
|
58
|
-
];
|
|
59
|
-
const result = spawnSync("curl", args, { maxBuffer: 60 * 1024 * 1024 });
|
|
60
|
-
return result.stdout || Buffer.alloc(0);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function curlJson(url, options = {}) {
|
|
64
|
-
const text = options.method === "POST"
|
|
65
|
-
? curlPost(url, options.body || "", options.headers || {})
|
|
66
|
-
: curlGet(url, options.headers || {});
|
|
67
|
-
const cleaned = text.replace(/^\s+/, "");
|
|
68
|
-
try {
|
|
69
|
-
return JSON.parse(cleaned);
|
|
70
|
-
} catch {
|
|
71
|
-
return { _raw: cleaned };
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function webJson(url, options = {}) {
|
|
76
|
-
const cfg = config.get();
|
|
77
|
-
if (cfg.webTransport !== "browser") return curlJson(url, options);
|
|
78
|
-
|
|
79
|
-
const { browserJson } = require("./browser-login");
|
|
80
|
-
const headers = { ...(options.headers || {}) };
|
|
81
|
-
if (options.method === "POST" && !headers["Content-Type"]) {
|
|
82
|
-
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
83
|
-
}
|
|
84
|
-
return browserJson(
|
|
85
|
-
url,
|
|
86
|
-
{
|
|
87
|
-
method: options.method || "GET",
|
|
88
|
-
...(Object.prototype.hasOwnProperty.call(options, "body") ? { body: options.body } : {}),
|
|
89
|
-
headers,
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
profileDir: cfg.browserProfile,
|
|
93
|
-
port: cfg.browserPort,
|
|
94
|
-
onPort: (browserPort) => {
|
|
95
|
-
if (browserPort !== cfg.browserPort) config.update({ browserPort });
|
|
96
|
-
},
|
|
97
|
-
}
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// bdstoken 短缓存:getFileMetaWeb 每次下载/peek 都要取一次 bdstoken(1 个 HTTP 往返),
|
|
102
|
-
// 而 token 实际有效期很长,缓存 10 分钟显著减少重复请求
|
|
103
|
-
let bdstokenCache = { token: null, expiresAt: 0 };
|
|
104
|
-
|
|
105
|
-
async function getBdstoken({ forceRefresh = false } = {}) {
|
|
106
|
-
if (!forceRefresh && bdstokenCache.token && Date.now() < bdstokenCache.expiresAt) {
|
|
107
|
-
return bdstokenCache.token;
|
|
108
|
-
}
|
|
109
|
-
const fields = encodeURIComponent('["bdstoken","token","uk","isdocuser","servertime"]');
|
|
110
|
-
const data = await webJson(
|
|
111
|
-
`https://pan.baidu.com/api/gettemplatevariable?clienttype=0&app_id=250528&web=1&fields=${fields}`
|
|
112
|
-
);
|
|
113
|
-
const token = data?.result?.bdstoken || null;
|
|
114
|
-
if (!token) {
|
|
115
|
-
const cfg = require("./config").get();
|
|
116
|
-
throw new Error(
|
|
117
|
-
`无法获取 bdstoken: Web 会话未认证。\n` +
|
|
118
|
-
` BDUSS: ${cfg.bduss ? "set" : "MISSING"} STOKEN: ${cfg.stoken ? "set" : "MISSING"}\n` +
|
|
119
|
-
` Web API transport: ${cfg.webTransport}\n` +
|
|
120
|
-
` Run: bdp login, or bdp login --bduss <value> --stoken <value>`
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
bdstokenCache = { token, expiresAt: Date.now() + 10 * 60 * 1000 };
|
|
124
|
-
return token;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const APP_ID = "250528";
|
|
128
|
-
const PAN_BASE = "https://pan.baidu.com";
|
|
129
|
-
|
|
130
|
-
module.exports = { curlGet, curlPost, curlRange, curlJson, webJson, getBdstoken, APP_ID, PAN_BASE };
|
|
1
|
+
const { spawnSync } = require("child_process");
|
|
2
|
+
const config = require("./config");
|
|
3
|
+
|
|
4
|
+
function buildCookieHeader(cfg) {
|
|
5
|
+
if (cfg.cookie) return cfg.cookie;
|
|
6
|
+
return `BDUSS=${cfg.bduss}; STOKEN=${cfg.stoken}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function curlGet(url, extraHeaders = {}) {
|
|
10
|
+
const cfg = config.get();
|
|
11
|
+
const headers = {
|
|
12
|
+
Cookie: buildCookieHeader(cfg),
|
|
13
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
14
|
+
Referer: "https://pan.baidu.com/disk/main",
|
|
15
|
+
...extraHeaders,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const args = ["-s", "-L", "--max-time", "30", "--compressed"];
|
|
19
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
20
|
+
args.push("-H", `${k}: ${v}`);
|
|
21
|
+
}
|
|
22
|
+
args.push(url);
|
|
23
|
+
|
|
24
|
+
const result = spawnSync("curl", args, { encoding: "utf-8", maxBuffer: 50 * 1024 * 1024 });
|
|
25
|
+
return result.stdout || "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function curlPost(url, body = "", extraHeaders = {}) {
|
|
29
|
+
const cfg = config.get();
|
|
30
|
+
const headers = {
|
|
31
|
+
Cookie: buildCookieHeader(cfg),
|
|
32
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
33
|
+
Referer: "https://pan.baidu.com/disk/main",
|
|
34
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
35
|
+
...extraHeaders,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const args = ["-s", "-L", "-X", "POST", "--max-time", "30", "--compressed"];
|
|
39
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
40
|
+
args.push("-H", `${k}: ${v}`);
|
|
41
|
+
}
|
|
42
|
+
if (body) args.push("-d", body);
|
|
43
|
+
args.push(url);
|
|
44
|
+
|
|
45
|
+
const result = spawnSync("curl", args, { encoding: "utf-8", maxBuffer: 50 * 1024 * 1024 });
|
|
46
|
+
return result.stdout || "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function curlRange(url, start, end, ua) {
|
|
50
|
+
const rangeArg = end != null ? `${start}-${end}` : `${start}-`;
|
|
51
|
+
const args = [
|
|
52
|
+
"-s", "-L",
|
|
53
|
+
"-r", rangeArg,
|
|
54
|
+
"-H", `User-Agent: ${ua}`,
|
|
55
|
+
"-H", `Range: bytes=${rangeArg}`,
|
|
56
|
+
"--max-time", "30",
|
|
57
|
+
url,
|
|
58
|
+
];
|
|
59
|
+
const result = spawnSync("curl", args, { maxBuffer: 60 * 1024 * 1024 });
|
|
60
|
+
return result.stdout || Buffer.alloc(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function curlJson(url, options = {}) {
|
|
64
|
+
const text = options.method === "POST"
|
|
65
|
+
? curlPost(url, options.body || "", options.headers || {})
|
|
66
|
+
: curlGet(url, options.headers || {});
|
|
67
|
+
const cleaned = text.replace(/^\s+/, "");
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(cleaned);
|
|
70
|
+
} catch {
|
|
71
|
+
return { _raw: cleaned };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function webJson(url, options = {}) {
|
|
76
|
+
const cfg = config.get();
|
|
77
|
+
if (cfg.webTransport !== "browser") return curlJson(url, options);
|
|
78
|
+
|
|
79
|
+
const { browserJson } = require("./browser-login");
|
|
80
|
+
const headers = { ...(options.headers || {}) };
|
|
81
|
+
if (options.method === "POST" && !headers["Content-Type"]) {
|
|
82
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
83
|
+
}
|
|
84
|
+
return browserJson(
|
|
85
|
+
url,
|
|
86
|
+
{
|
|
87
|
+
method: options.method || "GET",
|
|
88
|
+
...(Object.prototype.hasOwnProperty.call(options, "body") ? { body: options.body } : {}),
|
|
89
|
+
headers,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
profileDir: cfg.browserProfile,
|
|
93
|
+
port: cfg.browserPort,
|
|
94
|
+
onPort: (browserPort) => {
|
|
95
|
+
if (browserPort !== cfg.browserPort) config.update({ browserPort });
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// bdstoken 短缓存:getFileMetaWeb 每次下载/peek 都要取一次 bdstoken(1 个 HTTP 往返),
|
|
102
|
+
// 而 token 实际有效期很长,缓存 10 分钟显著减少重复请求
|
|
103
|
+
let bdstokenCache = { token: null, expiresAt: 0 };
|
|
104
|
+
|
|
105
|
+
async function getBdstoken({ forceRefresh = false } = {}) {
|
|
106
|
+
if (!forceRefresh && bdstokenCache.token && Date.now() < bdstokenCache.expiresAt) {
|
|
107
|
+
return bdstokenCache.token;
|
|
108
|
+
}
|
|
109
|
+
const fields = encodeURIComponent('["bdstoken","token","uk","isdocuser","servertime"]');
|
|
110
|
+
const data = await webJson(
|
|
111
|
+
`https://pan.baidu.com/api/gettemplatevariable?clienttype=0&app_id=250528&web=1&fields=${fields}`
|
|
112
|
+
);
|
|
113
|
+
const token = data?.result?.bdstoken || null;
|
|
114
|
+
if (!token) {
|
|
115
|
+
const cfg = require("./config").get();
|
|
116
|
+
throw new Error(
|
|
117
|
+
`无法获取 bdstoken: Web 会话未认证。\n` +
|
|
118
|
+
` BDUSS: ${cfg.bduss ? "set" : "MISSING"} STOKEN: ${cfg.stoken ? "set" : "MISSING"}\n` +
|
|
119
|
+
` Web API transport: ${cfg.webTransport}\n` +
|
|
120
|
+
` Run: bdp login, or bdp login --bduss <value> --stoken <value>`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
bdstokenCache = { token, expiresAt: Date.now() + 10 * 60 * 1000 };
|
|
124
|
+
return token;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const APP_ID = "250528";
|
|
128
|
+
const PAN_BASE = "https://pan.baidu.com";
|
|
129
|
+
|
|
130
|
+
module.exports = { curlGet, curlPost, curlRange, curlJson, webJson, getBdstoken, APP_ID, PAN_BASE };
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
const pan = require("./pan");
|
|
2
|
-
const group = require("./group");
|
|
3
|
-
const config = require("./config");
|
|
4
|
-
const pcsExtra = require("./pcs-extra");
|
|
5
|
-
const nameMatch = require("./name-match");
|
|
6
|
-
|
|
7
|
-
module.exports = { pan, group, config, pcsExtra, nameMatch };
|
|
1
|
+
const pan = require("./pan");
|
|
2
|
+
const group = require("./group");
|
|
3
|
+
const config = require("./config");
|
|
4
|
+
const pcsExtra = require("./pcs-extra");
|
|
5
|
+
const nameMatch = require("./name-match");
|
|
6
|
+
|
|
7
|
+
module.exports = { pan, group, config, pcsExtra, nameMatch };
|
package/lib/mcp.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// lib/mcp.js — MCP (Model Context Protocol) 服务器(stdio 传输,JSON-RPC 2.0,换行分隔)
|
|
2
|
+
//
|
|
3
|
+
// 为什么要有它:CLI 的参数格式要靠 Agent 记住/猜测,而 MCP 客户端能自动发现
|
|
4
|
+
// 工具描述与参数 schema,参数类型由 schema 强约束。把 bdp 暴露成 MCP 之后,
|
|
5
|
+
// Agent 不再需要记忆"ls 的参数名是 path 还是 dir"。
|
|
6
|
+
//
|
|
7
|
+
// 协议要点(stdio 传输):
|
|
8
|
+
// - 每行一个 JSON-RPC 2.0 消息
|
|
9
|
+
// - initialize → 返回 protocolVersion / capabilities / serverInfo
|
|
10
|
+
// - notifications/* → 客户端通知,无需响应
|
|
11
|
+
// - ping → 返回 {}
|
|
12
|
+
// - tools/list → 返回工具数组
|
|
13
|
+
// - tools/call → 执行工具,返回 { content:[{type:"text",text}], isError }
|
|
14
|
+
|
|
15
|
+
const PROTOCOL_VERSION = "2024-11-05";
|
|
16
|
+
const SERVER_NAME = "bdp";
|
|
17
|
+
const SERVER_VERSION = require("../package.json").version;
|
|
18
|
+
|
|
19
|
+
// 工具定义(tools/list 返回值)——描述写给 Agent 看,务必说清参数语义
|
|
20
|
+
function toolDefs() {
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
name: "bdp_ls",
|
|
24
|
+
description: "列出百度网盘目录内容。返回结构化条目(name/path/isDir/size/mtime)。path 为网盘绝对路径,默认根目录 /。",
|
|
25
|
+
inputSchema: {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {
|
|
28
|
+
path: { type: "string", description: "网盘目录绝对路径,如 /中医、/玄学" },
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "bdp_search",
|
|
34
|
+
description: "按文件名递归搜索网盘。默认子串匹配;mode 可选 exact/regex/glob/any。返回分页结果(limit 默认 50)。",
|
|
35
|
+
inputSchema: {
|
|
36
|
+
type: "object",
|
|
37
|
+
properties: {
|
|
38
|
+
keyword: { type: "string", description: "搜索关键词(必填)" },
|
|
39
|
+
path: { type: "string", description: "限定搜索目录,默认 /" },
|
|
40
|
+
mode: { type: "string", enum: ["substring", "exact", "regex", "glob", "any"], description: "匹配模式,默认 substring" },
|
|
41
|
+
limit: { type: "number", description: "单页返回条数,默认 50" },
|
|
42
|
+
page: { type: "number", description: "页码,默认 1" },
|
|
43
|
+
},
|
|
44
|
+
required: ["keyword"],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "bdp_read",
|
|
49
|
+
description: "读取网盘文本文件的前 N 行(免下载)。自动处理 UTF-8/GBK 编码;二进制文件会报错而不是返回乱码。",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {
|
|
53
|
+
path: { type: "string", description: "网盘文件绝对路径(必填)" },
|
|
54
|
+
lines: { type: "number", description: "行数,默认 20" },
|
|
55
|
+
},
|
|
56
|
+
required: ["path"],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "bdp_peek",
|
|
61
|
+
description: "查看网盘文件元信息:大小、md5、修改时间、类型(text/pdf/zip/image…)、编码,以及前 10 行预览。",
|
|
62
|
+
inputSchema: {
|
|
63
|
+
type: "object",
|
|
64
|
+
properties: { path: { type: "string", description: "网盘文件绝对路径(必填)" } },
|
|
65
|
+
required: ["path"],
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "bdp_quota",
|
|
70
|
+
description: "查询网盘配额:总空间/已用/剩余/使用率。",
|
|
71
|
+
inputSchema: { type: "object", properties: {} },
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "bdp_get",
|
|
75
|
+
description: "下载网盘文件到本地(分块并发 + size 校验 + 断点续传)。",
|
|
76
|
+
inputSchema: {
|
|
77
|
+
type: "object",
|
|
78
|
+
properties: {
|
|
79
|
+
path: { type: "string", description: "网盘文件路径(必填)" },
|
|
80
|
+
outDir: { type: "string", description: "本地保存目录,默认当前目录" },
|
|
81
|
+
},
|
|
82
|
+
required: ["path"],
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "bdp_put",
|
|
87
|
+
description: "上传本地文件到网盘。",
|
|
88
|
+
inputSchema: {
|
|
89
|
+
type: "object",
|
|
90
|
+
properties: {
|
|
91
|
+
local: { type: "string", description: "本地文件路径(必填)" },
|
|
92
|
+
remote: { type: "string", description: "网盘目标路径(必填)" },
|
|
93
|
+
},
|
|
94
|
+
required: ["local", "remote"],
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "bdp_mkdir",
|
|
99
|
+
description: "创建网盘目录(幂等:已存在不算失败)。",
|
|
100
|
+
inputSchema: {
|
|
101
|
+
type: "object",
|
|
102
|
+
properties: { path: { type: "string", description: "要创建的网盘目录路径(必填)" } },
|
|
103
|
+
required: ["path"],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "bdp_groups",
|
|
108
|
+
description: "列出所有百度网盘群组(gid + 群名)。",
|
|
109
|
+
inputSchema: { type: "object", properties: {} },
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "bdp_gsearch",
|
|
113
|
+
description: "在网盘群组的分享库里搜索文件名(全量遍历,带缓存)。",
|
|
114
|
+
inputSchema: {
|
|
115
|
+
type: "object",
|
|
116
|
+
properties: {
|
|
117
|
+
gid: { type: "string", description: "群组 gid(必填)" },
|
|
118
|
+
keyword: { type: "string", description: "搜索关键词(必填)" },
|
|
119
|
+
limit: { type: "number", description: "返回条数,默认 50" },
|
|
120
|
+
},
|
|
121
|
+
required: ["gid", "keyword"],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 执行一个工具 → 返回可 JSON 序列化的结果对象
|
|
128
|
+
async function callTool(name, args = {}) {
|
|
129
|
+
const pan = require("./pan");
|
|
130
|
+
const group = require("./group");
|
|
131
|
+
const pcsExtra = require("./pcs-extra");
|
|
132
|
+
|
|
133
|
+
switch (name) {
|
|
134
|
+
case "bdp_ls": {
|
|
135
|
+
const info = pan.lsEntries(args.path || "/");
|
|
136
|
+
return { path: info.path, total: info.entries.length, entries: info.entries };
|
|
137
|
+
}
|
|
138
|
+
case "bdp_search": {
|
|
139
|
+
const mode = args.mode || "substring";
|
|
140
|
+
let engineKeyword = args.keyword;
|
|
141
|
+
if (mode === "regex" || mode === "glob") {
|
|
142
|
+
const runs = String(args.keyword).split(/[.*+?^¶{}()|[\]\\*?]/).filter(Boolean);
|
|
143
|
+
engineKeyword = runs.find((r) => r.length >= 2) || runs[0] || args.keyword;
|
|
144
|
+
} else if (mode === "any") {
|
|
145
|
+
engineKeyword = String(args.keyword).split(/\s+/)[0];
|
|
146
|
+
}
|
|
147
|
+
const info = pan.searchEntries(args.keyword, args.path || "/", { mode, engineKeyword });
|
|
148
|
+
const limit = args.limit > 0 ? args.limit : 50;
|
|
149
|
+
const page = args.page > 0 ? args.page : 1;
|
|
150
|
+
const startIdx = (page - 1) * limit;
|
|
151
|
+
const sliced = info.entries.slice(startIdx, startIdx + limit);
|
|
152
|
+
return {
|
|
153
|
+
keyword: info.keyword,
|
|
154
|
+
mode: info.mode,
|
|
155
|
+
total: info.matched,
|
|
156
|
+
returned: sliced.length,
|
|
157
|
+
page,
|
|
158
|
+
pageSize: limit,
|
|
159
|
+
hasMore: startIdx + sliced.length < info.matched,
|
|
160
|
+
nextPage: startIdx + sliced.length < info.matched ? page + 1 : null,
|
|
161
|
+
entries: sliced,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
case "bdp_read": {
|
|
165
|
+
const lines = pan.head(args.path, args.lines > 0 ? args.lines : 20);
|
|
166
|
+
return { path: args.path, lines: lines.length, content: lines.join("\n") };
|
|
167
|
+
}
|
|
168
|
+
case "bdp_peek": {
|
|
169
|
+
return await pan.peek(args.path);
|
|
170
|
+
}
|
|
171
|
+
case "bdp_quota": {
|
|
172
|
+
return pcsExtra.quota();
|
|
173
|
+
}
|
|
174
|
+
case "bdp_get": {
|
|
175
|
+
return await pan.download(args.path, args.outDir, {});
|
|
176
|
+
}
|
|
177
|
+
case "bdp_put": {
|
|
178
|
+
const raw = pan.upload(args.local, args.remote);
|
|
179
|
+
return { local: args.local, remote: args.remote, ok: true, raw };
|
|
180
|
+
}
|
|
181
|
+
case "bdp_mkdir": {
|
|
182
|
+
return pan.mkdir(args.path);
|
|
183
|
+
}
|
|
184
|
+
case "bdp_groups": {
|
|
185
|
+
return await group.listGroups();
|
|
186
|
+
}
|
|
187
|
+
case "bdp_gsearch": {
|
|
188
|
+
return await group.searchFiles(args.gid, args.keyword, {
|
|
189
|
+
limit: args.limit > 0 ? args.limit : 50,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
default:
|
|
193
|
+
throw new Error("未知工具: " + name);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// JSON-RPC 响应
|
|
198
|
+
function reply(output, id, result) {
|
|
199
|
+
output.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n");
|
|
200
|
+
}
|
|
201
|
+
function replyError(output, id, code, message) {
|
|
202
|
+
output.write(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }) + "\n");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function handleMessage(msg, output) {
|
|
206
|
+
const { id, method, params } = msg;
|
|
207
|
+
const isNotification = id === undefined || id === null;
|
|
208
|
+
|
|
209
|
+
switch (method) {
|
|
210
|
+
case "initialize":
|
|
211
|
+
reply(output, id, {
|
|
212
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
213
|
+
capabilities: { tools: { listChanged: false } },
|
|
214
|
+
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
|
|
215
|
+
});
|
|
216
|
+
return;
|
|
217
|
+
case "notifications/initialized":
|
|
218
|
+
case "initialized":
|
|
219
|
+
return; // 通知,无需响应
|
|
220
|
+
case "ping":
|
|
221
|
+
if (!isNotification) reply(output, id, {});
|
|
222
|
+
return;
|
|
223
|
+
case "tools/list":
|
|
224
|
+
reply(output, id, { tools: toolDefs() });
|
|
225
|
+
return;
|
|
226
|
+
case "tools/call": {
|
|
227
|
+
const name = params && params.name;
|
|
228
|
+
const args = (params && params.arguments) || {};
|
|
229
|
+
try {
|
|
230
|
+
const result = await callTool(name, args);
|
|
231
|
+
reply(output, id, {
|
|
232
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
233
|
+
isError: false,
|
|
234
|
+
});
|
|
235
|
+
} catch (e) {
|
|
236
|
+
// 工具执行失败 → isError:true(而不是 JSON-RPC error),客户端仍能读到内容
|
|
237
|
+
reply(output, id, {
|
|
238
|
+
content: [{ type: "text", text: "错误: " + e.message }],
|
|
239
|
+
isError: true,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
default:
|
|
245
|
+
if (!isNotification) {
|
|
246
|
+
replyError(output, id, -32601, "Method not found: " + method);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 启动 MCP 服务器(stdio)。
|
|
253
|
+
* @param {{input?: NodeJS.ReadableStream, output?: NodeJS.WritableStream}} opts
|
|
254
|
+
*/
|
|
255
|
+
function startServer(opts = {}) {
|
|
256
|
+
const input = opts.input || process.stdin;
|
|
257
|
+
const output = opts.output || process.stdout;
|
|
258
|
+
let buffer = "";
|
|
259
|
+
|
|
260
|
+
input.setEncoding("utf-8");
|
|
261
|
+
input.on("data", (chunk) => {
|
|
262
|
+
buffer += chunk;
|
|
263
|
+
let idx;
|
|
264
|
+
while ((idx = buffer.indexOf("\n")) !== -1) {
|
|
265
|
+
const line = buffer.slice(0, idx).trim();
|
|
266
|
+
buffer = buffer.slice(idx + 1);
|
|
267
|
+
if (!line) continue;
|
|
268
|
+
let msg;
|
|
269
|
+
try {
|
|
270
|
+
msg = JSON.parse(line);
|
|
271
|
+
} catch {
|
|
272
|
+
continue; // 忽略非法行
|
|
273
|
+
}
|
|
274
|
+
Promise.resolve(handleMessage(msg, output)).catch((e) => {
|
|
275
|
+
if (msg && msg.id !== undefined && msg.id !== null) {
|
|
276
|
+
replyError(output, msg.id, -32603, e.message);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
input.on("end", () => {
|
|
282
|
+
try { input.pause(); } catch {}
|
|
283
|
+
});
|
|
284
|
+
return { input, output };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
module.exports = { startServer, callTool, toolDefs, PROTOCOL_VERSION, SERVER_NAME, SERVER_VERSION };
|