baiduwangpan-cli 1.1.5 → 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/lib/http.js CHANGED
@@ -1,122 +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
- async function getBdstoken() {
102
- const fields = encodeURIComponent('["bdstoken","token","uk","isdocuser","servertime"]');
103
- const data = await webJson(
104
- `https://pan.baidu.com/api/gettemplatevariable?clienttype=0&app_id=250528&web=1&fields=${fields}`
105
- );
106
- const token = data?.result?.bdstoken || null;
107
- if (!token) {
108
- const cfg = require("./config").get();
109
- throw new Error(
110
- `Cannot get bdstoken. The web session is not authenticated.\n` +
111
- ` BDUSS: ${cfg.bduss ? "set" : "MISSING"} STOKEN: ${cfg.stoken ? "set" : "MISSING"}\n` +
112
- ` Web API transport: ${cfg.webTransport}\n` +
113
- ` Run: bdp login, or bdp login --bduss <value> --stoken <value>`
114
- );
115
- }
116
- return token;
117
- }
118
-
119
- const APP_ID = "250528";
120
- const PAN_BASE = "https://pan.baidu.com";
121
-
122
- 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,5 +1,7 @@
1
- const pan = require("./pan");
2
- const group = require("./group");
3
- const config = require("./config");
4
-
5
- module.exports = { pan, group, config };
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 };
@@ -0,0 +1,67 @@
1
+ // ── 文件名匹配(纯函数,无 IO)──────────────────────────
2
+ //
3
+ // 供群文件搜索(lib/group.js 的 gsearch)与网盘搜索(bdp search 集成建议)共用。
4
+ // 所有函数都是同步纯函数:同样的输入永远得到同样的输出,不做任何网络/文件操作。
5
+ //
6
+ // 匹配模式(options.mode):
7
+ // - "all" (默认)空格分词,每个词都必须出现(AND,顺序无关)
8
+ // - "any" 空格分词,任一词出现即可(OR)
9
+ // - "exact" 整名精确相等(大小写不敏感,按 basename 比较,忽略目录前缀)
10
+ // - "regex" pattern 直接作为正则表达式
11
+ // - "glob" pattern 按 shell glob(* 任意串、? 单字符)匹配
12
+ // 单关键词时 all/any 都退化为子串包含(与 group.js 旧行为兼容)。
13
+
14
+ // 正则元字符转义:普通字符串 → 可安全嵌入正则的等价字面量
15
+ function escapeRegExp(s) {
16
+ return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
17
+ }
18
+
19
+ // shell glob 转正则:* → .*(任意串,含空)、? → .(任意单字符)、
20
+ // 其余字符全部按字面量转义(如 "1.5版" 的 "." 在 glob 中就是普通句点)。
21
+ // 结果整体锚定(^...$),即 glob 必须匹配整个文件名(shell 语义)。
22
+ function globToRegExp(pattern) {
23
+ const escaped = String(pattern).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
24
+ const src = escaped.replace(/\\\*/g, ".*").replace(/\\\?/g, ".");
25
+ return new RegExp(`^(?:${src})$`);
26
+ }
27
+
28
+ // 核心匹配函数。
29
+ // pattern: 用户输入的关键词/模式
30
+ // name: 待匹配的文件名(可为完整路径,exact 模式会自动取 basename)
31
+ // options: { mode, ignoreCase },ignoreCase 默认 true(与 group.js 旧行为一致)
32
+ // 返回布尔值。
33
+ function matchesName(pattern, name, options = {}) {
34
+ const mode = options.mode || "all";
35
+ const ignoreCase = options.ignoreCase !== false;
36
+ const raw = String(pattern);
37
+ const k = ignoreCase ? raw.toLowerCase() : raw;
38
+ const n = String(name || "");
39
+ const nn = ignoreCase ? n.toLowerCase() : n;
40
+
41
+ // regex:pattern 直接编译为正则(非法正则向上抛 SyntaxError,由调用方决定处理)
42
+ if (mode === "regex") {
43
+ return new RegExp(raw, ignoreCase ? "i" : "").test(n);
44
+ }
45
+
46
+ // glob:经 globToRegExp 转换后整体匹配
47
+ if (mode === "glob") {
48
+ return globToRegExp(raw).test(n);
49
+ }
50
+
51
+ // exact:整名精确相等。按 basename 比较(忽略 "/dir/" 前缀),
52
+ // 顶层分享 name 形如 "/xxx",子目录文件 name 无前缀,两者统一处理。
53
+ if (mode === "exact") {
54
+ const base = nn.includes("/") ? nn.slice(nn.lastIndexOf("/") + 1) : nn;
55
+ return base === k;
56
+ }
57
+
58
+ // all / any:空格分词。空关键词(含纯空白)不过滤(匹配一切,兼容旧行为);
59
+ // 单关键词退化为子串包含(兼容旧行为)。
60
+ const words = k.trim().split(/\s+/).filter(Boolean);
61
+ if (words.length === 0) return true;
62
+ if (words.length === 1) return nn.includes(words[0]);
63
+ if (mode === "any") return words.some((w) => nn.includes(w));
64
+ return words.every((w) => nn.includes(w)); // 默认 all:每词都需出现(AND)
65
+ }
66
+
67
+ module.exports = { globToRegExp, matchesName, escapeRegExp };