@aicloud360/mcp-server-disk 0.5.8 → 0.8.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/README.md CHANGED
@@ -1,355 +1,145 @@
1
- # 360 AI 云盘 MCP
1
+ # 360 AI 云盘 MCP Server & CLI
2
2
 
3
- 360 AI 云盘的 Model Context Protocol 接入服务,让 AI 模型能够通过 MCP 协议直接操作云盘,提供完整的云盘文件管理能力。
3
+ 360 AI 云盘的 MCP Server + CLI 工具,为 AI Agent 和开发者提供完整的云盘文件管理能力。
4
4
 
5
- ## 📚 简介
5
+ - **MCP Server** — 标准 MCP 协议接入,支持 Stdio / Streamable HTTP / SSE
6
+ - **360disk CLI** — 18 个命令覆盖云盘全部操作,结构化 JSON 输出
7
+ - **Skills 技能库** — 开箱即用的 AI Agent 技能,节省 75%+ Token
6
8
 
7
- 本项目为 360 AI 云盘的 MCP(Model Context Protocol)服务实现,允许各类 AI 模型(如大语言模型)通过标准的 MCP 协议与 360 AI 云盘进行交互。通过这种方式,AI 模型可以帮助用户管理云盘文件,极大地提升了文件管理的智能化和便捷性。
9
+ ## 快速开始
8
10
 
9
- ## 🔧 配置方式(在 Cursor 中配置)
11
+ ### 安装
10
12
 
11
- ### Stdio 接入方式
13
+ ```bash
14
+ npm install -g @aicloud360/mcp-server-disk
15
+ ```
16
+
17
+ ### MCP Server 接入
12
18
 
13
- `~/.cursor/mcp.json` 文件中添加以下配置,连接 360 AI 云盘 MCP 服务:
19
+ #### Stdio 方式
20
+
21
+ 在 MCP Client 配置文件中添加(适用于 Cursor、Claude Desktop 等):
14
22
 
15
23
  ```json
16
24
  {
17
25
  "mcpServers": {
18
- "360-mcp-server-disk": {
26
+ "360-ai-cloud-disk": {
19
27
  "command": "npx",
20
- "args": [
21
- "-y",
22
- "@aicloud360/mcp-server-disk"
23
- ],
28
+ "args": ["-y", "@aicloud360/mcp-server-disk", "--stdio"],
24
29
  "env": {
25
- "API_KEY": "_xxxxxxxxx"
30
+ "API_KEY": "yunpan_xxxxxxxxxx"
26
31
  }
27
32
  }
28
33
  }
29
34
  }
30
35
  ```
31
36
 
32
- ### Streamable HTTP 接入方式
37
+ #### Streamable HTTP 方式
33
38
 
34
- 如果您希望通过HTTP方式接入,可以使用以下配置:
39
+ 无需安装本地环境,通过 URL 直接接入:
35
40
 
36
41
  ```json
37
42
  {
38
43
  "mcpServers": {
39
- "mcp-server-disk-http": {
40
- "url": "https://mcp.yunpan.com/mcp?api_key=_xxxxxxxxx"
44
+ "360-ai-cloud-disk": {
45
+ "url": "https://mcp.yunpan.com/mcp?api_key=yunpan_xxxxxxxxxx"
41
46
  }
42
47
  }
43
48
  }
44
49
  ```
45
50
 
46
- Streamable HTTP接入方式的特点:
47
- - 无需安装 nodejs 环境
48
- - 无需下载到本地运行
49
- - 通过URL参数传递API_KEY进行认证
50
- - 适合需要HTTP接口的集成场景
51
-
52
- ### SSE 接入方式
51
+ #### SSE 方式
53
52
 
54
- 如果您希望通过SSE(Server-Sent Events)方式接入,可以使用以下配置:
53
+ 基于 HTTP 长连接的服务器推送:
55
54
 
56
55
  ```json
57
56
  {
58
57
  "mcpServers": {
59
- "mcp-server-disk-sse": {
60
- "url": "https://mcp.yunpan.com/sse?api_key=_xxxxxxxxx"
58
+ "360-ai-cloud-disk": {
59
+ "url": "https://mcp.yunpan.com/sse?api_key=yunpan_xxxxxxxxxx"
61
60
  }
62
61
  }
63
62
  }
64
63
  ```
65
64
 
66
- SSE接入方式的特点:
67
- - 基于HTTP长连接的服务器推送技术
68
- - 实时性更强,适合需要即时响应的场景
69
- - 单向通信,服务器向客户端推送数据
70
- - 无需安装额外环境,浏览器原生支持
71
- - 通过URL参数传递API_KEY进行认证
72
-
73
- ## 🔐 认证配置
74
-
75
- 使用 360 AI 云盘 MCP 服务需要以下认证信息:
76
-
77
- - `API_KEY`:360AI云盘 API 密钥,格式为 "yunpan_" 开头的字符串
78
-
79
- 您可以通过以下方式获取API_KEY:
80
- - 参照 [快速接入](https://open.yunpan.360.cn/docs/mcp-server/preparation) MCP Server
81
-
82
- ### 360 AI 云盘开放平台优势
83
-
84
- 360 AI 云盘开放平台提供了多元化的产品能力和一站式文件服务:
85
-
86
- - **账号一键关联**:无需重新注册账号,现有360 AI 云盘账号一键关联,实现"多平台,一账号"的无缝登录体验
87
- - **支持 MCP 协议接入**:支持 Stdio/SSH/Streamable HTTP/SSE 协议,通过 MCP Client 轻松接入
88
- - **丰富接口能力**:提供文件上传、下载、搜索、新建、重命名、移动、分享等 API,满足不同场景需求
89
-
90
- 访问 [360 AI 云盘开放平台官网](https://open.yunpan.360.cn) 获取更多详细信息和最新的开发文档。
91
-
92
- ## ✨ 功能概览
93
-
94
- 本 MCP 服务提供与 360AI 云盘交互的多种操作,包括:
65
+ ### CLI 使用
95
66
 
96
- - 📁 文件列表浏览 - 查看云盘目录内容
97
- - 🔍 文件搜索 - 根据关键词搜索云盘文件
98
- - ⬆️ 文件上传 - 将文件上传至 360 云盘
99
- - ⬇️ 文件下载 - 获取云盘文件下载链接并支持直接下载
100
- - 🎬 视频下载 - 通过URL下载视频到云盘,支持批量下载和实时进度监控
101
- - 💾 文件保存 - 通过URL或文本内容保存文件到云盘
102
- - 📂 目录创建 - 在云盘中创建新文件夹
103
- - ✏️ 文件重命名 - 修改云盘文件或文件夹名称
104
- - 🚚 文件移动 - 将文件移动到其他位置
105
- - 🔗 文件分享 - 将指定文件生成分享链接
106
- - 🔑 用户个人信息 - 获取用户信息
67
+ ```bash
68
+ # 登录
69
+ 360disk auth login --api-key yunpan_xxxxxxxxxx
107
70
 
108
- ## 🛠️ 工具使用指南
109
-
110
- 当连接到 360 AI 云盘 MCP 服务后,可以使用以下工具与云盘交互:
111
-
112
- ### 文件上传 (file-upload-stdio) - 仅支持Stdio接入方式
113
-
114
- 将本地文件上传到 360 AI云盘指定路径。
115
-
116
- **参数:**
117
- - `filePaths`: 本地文件的完整路径(必填,可以是字符串数组包含多个文件)
118
- - `uploadPath`: 上传到云盘的目标目录,默认为根目录 `/`
119
-
120
- **示例:**
121
- ```json
122
- {
123
- "filePaths": ["/Users/username/Documents/报告.docx", "/Users/username/Documents/数据.xlsx"],
124
- "uploadPath": "/工作文件"
125
- }
71
+ # 常用操作
72
+ 360disk dir ls / # 列出目录
73
+ 360disk file search "报告" # 搜索文件
74
+ 360disk file upload ./report.pdf --dest /文档/ # 上传文件
75
+ 360disk file download <nid> --dir ./ # 下载文件
76
+ 360disk file share /文档/报告.pdf # 分享文件
126
77
  ```
127
78
 
128
- **单文件上传示例:**
129
- ```json
130
- {
131
- "filePaths": "/Users/username/Desktop/测试文档.pdf",
132
- "uploadPath": "/文档"
133
- }
134
- ```
79
+ ## 功能概览
135
80
 
136
- ### 文件下载 (file-download-stdio) - 仅支持Stdio接入方式
81
+ ### MCP 工具(11 个)
137
82
 
138
- 获取云盘中指定文件的下载链接并支持直接下载文件。
83
+ | 工具 | 说明 |
84
+ |------|------|
85
+ | `file-list` | 获取目录文件列表 |
86
+ | `file-search` | 搜索文件 |
87
+ | `file-save` | 保存文件到云盘(URL / 文本内容) |
88
+ | `file-share` | 生成分享链接 |
89
+ | `file-move` | 移动文件或文件夹 |
90
+ | `file-rename` | 重命名文件或文件夹 |
91
+ | `make-dir` | 创建目录 |
92
+ | `get-download-url` | 获取下载链接 |
93
+ | `user-info` | 获取用户信息 |
94
+ | `file-upload-stdio` | 上传本地文件(仅 Stdio 模式) |
95
+ | `file-download-stdio` | 下载云盘文件(仅 Stdio 模式) |
139
96
 
140
- **参数:**
141
- - `nid`: 文件的唯一标识ID,可通过文件列表或搜索获取(必填)
142
- - `auto`: 是否直接下载文件,默认为true
143
- - `downloadDir`: 指定下载目录,必须有读写权限,默认为用户主目录下的.mcp-downloads文件夹
97
+ ### CLI 命令(18 个)
144
98
 
145
- **仅获取下载链接示例:**
146
- ```json
147
- {
148
- "nid": "12345678",
149
- "auto": false
150
- }
151
- ```
152
-
153
- **下载到指定目录示例:**
154
- ```json
155
- {
156
- "nid": "12345678",
157
- "auto": true,
158
- "downloadDir": "/Users/username/Downloads"
159
- }
160
- ```
99
+ | 命令组 | 子命令 | 说明 |
100
+ |--------|--------|------|
101
+ | `auth` | `login` / `whoami` / `logout` | 鉴权管理 |
102
+ | `user` | `info` | 用户信息 |
103
+ | `dir` | `ls` / `mkdir` | 目录操作 |
104
+ | `file` | `mv` / `rename` / `rm` / `search` / `share` / `url` / `save` / `upload` / `download` | 文件操作 |
105
+ | `completion` | `install` / `uninstall` / `script` | Shell 补全 |
161
106
 
162
- ### 文件列表查询 (file-list)
163
-
164
- 获取 360 AI云盘指定路径下的文件和文件夹列表。
165
-
166
- **参数:**
167
- - `path`: 要查询的路径,默认为根目录 `/`
168
- - `page`: 页码,默认为 0
169
- - `page_size`: 每页显示条数,默认为 50
170
-
171
- **示例:**
172
- ```json
173
- {
174
- "path": "/文档",
175
- "page": 1,
176
- "page_size": 20
177
- }
178
- ```
107
+ ### CLI 特性
179
108
 
180
- ### 文件搜索 (file-search)
181
-
182
- 根据关键词搜索 360 AI云盘文件。
183
-
184
- **参数:**
185
- - `key`: 搜索关键词(必填)
186
- - `file_category`: 文件类型(-1:全部,0:其他,1:图片,2:文档,3:音乐,4:视频),默认为 -1
187
- - `page`: 页码,默认为 1
188
- - `page_size`: 每页显示条数,默认为 20
189
-
190
- **示例:**
191
- ```json
192
- {
193
- "key": "报告",
194
- "file_category": 2,
195
- "page": 1
196
- }
197
- ```
198
-
199
- ### 文件保存 (file-save)
200
-
201
- 通过URL或文本内容保存文件到云盘。
202
-
203
- **参数:**
204
- - `url`: 文件下载地址(url或content必传1个)
205
- - `content`: 文件内容,支持markdown格式(url或content必传1个)
206
- - `upload_path`: 云盘存储路径,必须以/开头,默认为"/来自:mcp_server/"
207
-
208
- **通过URL保存示例:**
209
- ```json
210
- {
211
- "url": "https://example.com/sample.pdf",
212
- "upload_path": "/文档/下载/"
213
- }
214
- ```
215
-
216
- **通过文本内容保存示例:**
217
- ```json
218
- {
219
- "content": "# 标题\n这是一段Markdown格式的文本内容",
220
- "upload_path": "/笔记/"
221
- }
222
- ```
223
-
224
- ### 视频下载 (video-download)
225
-
226
- 通过URL下载视频到云盘,支持批量下载和实时进度监控。此操作可能需要较长时间,建议客户端设置更长的超时时间(建议300秒以上)。
227
-
228
- **参数:**
229
- - `urls`: 视频URL,多个URL使用英文竖线'|'分隔(必填)
230
-
231
- **单视频下载示例:**
232
- ```json
233
- {
234
- "urls": "https://example.com/video.mp4"
235
- }
236
- ```
237
-
238
- **批量视频下载示例:**
239
- ```json
240
- {
241
- "urls": "https://example.com/video1.mp4|https://example.com/video2.mp4|https://example.com/video3.mp4"
242
- }
243
- ```
244
-
245
- **功能特点:**
246
- - 🎯 **批量下载**:支持同时下载多个视频URL
247
- - 📊 **实时进度**:提供详细的下载进度监控,包括任务状态分布
248
- - 🔄 **自动轮询**:自动轮询任务状态直到完成,无需手动查询
249
- - 📁 **云盘存储**:下载的视频直接保存到云盘,提供云盘文件链接
250
- - ⚡ **状态跟踪**:实时跟踪任务状态(待开始/下载中/下载成功/上传成功/失败)
251
- - 🔗 **便捷访问**:完成后提供云盘文件链接,方便直接访问
252
-
253
- **返回结果说明:**
254
- - 成功下载的视频会显示云盘文件路径、文件大小、访问链接等信息
255
- - 失败的视频会显示具体的失败原因
256
- - 支持结构化数据返回,便于程序处理
257
-
258
- ### 创建文件夹 (make-dir)
259
-
260
- 在 360 AI云盘中创建新文件夹。
261
-
262
- **参数:**
263
- - `fname`: 文件夹路径,例如:`/新文件夹/`(必填)
264
-
265
- **示例:**
266
- ```json
267
- {
268
- "fname": "/工作文件/项目A/"
269
- }
270
- ```
271
-
272
- ### 文件分享 (file-share)
273
-
274
- 将指定文件生成分享链接。
275
-
276
- **参数:**
277
- - `paths`: 要分享的文件路径,多个文件路径用竖线(|)隔开(必填)
278
-
279
- **示例:**
280
- ```json
281
- {
282
- "paths": "/文档/报告.docx|/文档/数据.xlsx"
283
- }
284
- ```
285
-
286
- ### 移动文件 (file-move)
287
-
288
- 移动 360 AI云盘中的文件或文件夹到新位置。
289
-
290
- **参数:**
291
- - `src_name`: 文件原路径,多个路径用竖线隔开(必填)
292
- - `new_name`: 目标路径(必填)
293
-
294
- **示例:**
295
- ```json
296
- {
297
- "src_name": "/文档/报告.docx|/文档/数据.xlsx",
298
- "new_name": "/归档文件夹/"
299
- }
300
- ```
301
-
302
- ### 重命名文件 (file-rename)
303
-
304
- 重命名 360AI 云盘中的文件或文件夹。
305
-
306
- **参数:**
307
- - `src_name`: 原路径名称,如:`/文件夹/旧文件名.txt`(必填)
308
- - `new_name`: 新名称,如:`新文件名.txt`(必填)
309
-
310
- **示例:**
311
- ```json
312
- {
313
- "src_name": "/文档/草稿.docx",
314
- "new_name": "最终报告.docx"
315
- }
316
- ```
109
+ - **结构化输出**:默认 JSON,支持 `--format text` 和 `--quiet` 模式
110
+ - **统一错误码**:10 个语义化错误码,进程退出码对应
111
+ - **管道集成**:stdin 输入、批量操作,可与 `jq` 等工具组合
112
+ - **超时/重试**:`--timeout` 和 `--retries` 全局选项
113
+ - **Shell 补全**:bash / zsh 自动补全
317
114
 
318
- ### 用户个人信息 (user-info)
115
+ ## Skills 技能库
319
116
 
320
- 获取 360 AI云盘用户个人信息。
117
+ 项目提供两种互补的 Skill 接入方式:
321
118
 
322
- **参数:**
323
- - 无
119
+ | 维度 | CLI Skill(推荐) | MCP Skill |
120
+ |------|-------------------|-----------|
121
+ | 执行方式 | Shell 命令 `360disk ...` | Python executor → MCP Server |
122
+ | 运行时 | Node.js | Python + Node.js |
123
+ | 命令数量 | 18 个 | 10 个 |
124
+ | 适用场景 | Claude Code / Cursor / Windsurf / CI/CD | OpenClaw / Claude Desktop / 各类 Agent 平台 |
125
+ | 特色能力 | 管道组合、批量操作、文件上传下载 | HTTP 远程模式、免安装、75%+ Token 节省 |
324
126
 
127
+ Skills 仓库:[github.com/yifangyun/ecs-yunpan-skills](https://github.com/yifangyun/ecs-yunpan-skills)
325
128
 
326
- ## 🧠 AI 应用场景
129
+ ## 认证
327
130
 
328
- 通过 360 AI 云盘 MCP 接入,AI 可以帮助用户实现以下场景:
131
+ 使用前需要 360 AI 云盘 API 密钥(`yunpan_` 开头),获取方式参见 [快速接入指南](https://open.yunpan.360.cn/docs/mcp-server/preparation)。
329
132
 
330
- - **智能文件整理**:AI 可以分析用户文件内容,并自动归类整理
331
- - **文档智能检索**:使用自然语言描述查找云盘内的文档
332
- - **自动文件备份**:根据用户习惯,提供智能备份建议
333
- - **文件内容分析**:分析文档内容并提供摘要或见解
334
- - **基于对话的文件操作**:用户可以通过对话方式管理云盘文件
335
- - **文件上传及分享**:用户可以通过对话方式保存文件到云盘,并生成文件分享链接,方便把文件分享给他人
336
- - **网络资源保存**:用户可以通过提供URL,让AI帮助将网络资源保存到云盘
337
- - **文件内容创建与保存**:AI可以根据用户需求创建文档内容,并直接保存到云盘
338
- - **云盘文件下载**:用户可以通过对话方式从云盘下载文件到本地
339
- - **视频资源下载**:用户可以通过提供视频URL,让AI帮助将视频下载到云盘,支持批量下载和进度监控
133
+ 鉴权优先级:`--api-key` 参数 > `API_KEY` 环境变量 > `~/.360disk/config.json`
340
134
 
341
- ## 🔑 关键词
135
+ ## 文档
342
136
 
343
- - 360 AI 云盘
344
- - mcp
345
- - modelcontextprotocol
346
- - ai助手
347
- - 文件管理
348
- - 视频下载
349
- - 批量下载
350
- - sse
351
- - streamable http
137
+ - [在线文档](https://open.yunpan.360.cn) 完整的接入指南和 API 参考
138
+ - [MCP Server 快速开始](https://open.yunpan.360.cn/docs/mcp-server/quick-start)
139
+ - [CLI 命令参考](https://open.yunpan.360.cn/docs/cli/commands)
140
+ - [Skills 使用指南](https://open.yunpan.360.cn/docs/skills/intro)
141
+ - [Agent 接入指南](https://open.yunpan.360.cn/docs/agent/guide)
352
142
 
353
- ## 📄 许可证
143
+ ## 许可证
354
144
 
355
145
  Apache-2.0
package/build/cli.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import n from"dotenv";import{Command as e}from"commander";import t from"fs";import o from"path";import{homedir as s}from"os";import r from"crypto";import a from"pino";import{promises as i}from"dns";import{spawn as c}from"child_process";import l from"fs/promises";function u(){const n=function(){try{const n=o.dirname(new URL(import.meta.url).pathname),e=[o.resolve(n,"../../package.json"),o.resolve(n,"../../../package.json")];for(const n of e)if(t.existsSync(n)){const e=JSON.parse(t.readFileSync(n,"utf8"));if(e.cli?.name)return e.cli.name}}catch{}return"360disk"}();return o.join(s(),`.${n}`)}function m(){return o.join(u(),"config.json")}function d(){try{const n=m();if(t.existsSync(n))return JSON.parse(t.readFileSync(n,"utf8"))}catch{}return{}}function h(n){!function(){const n=u();t.existsSync(n)||t.mkdirSync(n,{recursive:!0,mode:448})}();const e=m();t.writeFileSync(e,JSON.stringify(n,null,2),{mode:384})}function p(n){const e=d();return{apiKey:n.apiKey||process.env.API_KEY||e.api_key||"",ecsEnv:n.env||process.env.ECS_ENV||e.ecs_env||"prod",subChannel:n.subChannel||process.env.SUB_CHANNEL||e.sub_channel||"open",timeout:n.timeout?parseInt(n.timeout):void 0,retries:n.retries?parseInt(n.retries):void 0}}var f;!function(n){n[n.SUCCESS=0]="SUCCESS",n[n.GENERAL=1]="GENERAL",n[n.INVALID_ARGS=2]="INVALID_ARGS",n[n.AUTH_ERROR=3]="AUTH_ERROR",n[n.NOT_FOUND=4]="NOT_FOUND",n[n.PERMISSION_DENIED=5]="PERMISSION_DENIED",n[n.NETWORK_ERROR=6]="NETWORK_ERROR",n[n.CONFLICT=7]="CONFLICT",n[n.SERVER_ERROR=8]="SERVER_ERROR",n[n.QUOTA_EXCEEDED=10]="QUOTA_EXCEEDED"}(f||(f={}));class w extends Error{code;constructor(n,e=f.GENERAL){super(n),this.name="CLIError",this.code=e}}function $(n,e,t,o){if(o?.quiet)return void process.stdout.write(JSON.stringify(n)+"\n");const s={success:!0,result:n,meta:{duration_ms:Date.now()-t,command:e}};process.stdout.write(JSON.stringify(s,null,2)+"\n")}function _(n){process.stdout.write(n+"\n")}function g(n,e,t){const o=n instanceof Error?n.message:n;let s;s=n instanceof w?n.code:function(n){const e=(n?.message||n?.toString()||"").toLowerCase();return e.includes("超时")||e.includes("timeout")||e.includes("aborted")||"AbortError"===n?.name||e.includes("econnreset")||e.includes("etimedout")||e.includes("econnrefused")||e.includes("fetch failed")||e.includes("网络")?f.NETWORK_ERROR:e.includes("api key")||e.includes("api_key")||e.includes("未配置")||e.includes("token")||e.includes("auth")||e.includes("登录")||e.includes("401")?f.AUTH_ERROR:e.includes("参数")||e.includes("argument")||e.includes("required")||e.includes("互斥")||e.includes("invalid")?f.INVALID_ARGS:e.includes("不存在")||e.includes("not found")||e.includes("404")||e.includes("no such")?f.NOT_FOUND:e.includes("权限")||e.includes("permission")||e.includes("forbidden")||e.includes("403")?f.PERMISSION_DENIED:e.includes("已存在")||e.includes("conflict")||e.includes("duplicate")||e.includes("409")?f.CONFLICT:e.includes("状态码: 5")||e.includes("500")||e.includes("502")||e.includes("503")||e.includes("服务端")?f.SERVER_ERROR:e.includes("空间不足")||e.includes("quota")||e.includes("limit")||e.includes("429")||e.includes("频率")?f.QUOTA_EXCEEDED:f.GENERAL}(n);const r={success:!1,error:o,code:s,meta:{duration_ms:t?Date.now()-t:0,command:e}};process.stderr.write(JSON.stringify(r,null,2)+"\n"),process.exit(s||1)}function P(n,e=2){if(!n||n<=0)return"-";const t=Math.floor(Math.log(n)/Math.log(1024));return parseFloat((n/Math.pow(1024,t)).toFixed(e))+" "+["B","KB","MB","GB","TB"][t]}function y(n){if(!n)return"-";const e="string"==typeof n?parseInt(n):n;if(isNaN(e)||e<=0)return"-";const t=new Date(e<1e12?1e3*e:e),o=n=>String(n).padStart(2,"0");return`${t.getFullYear()}-${o(t.getMonth()+1)}-${o(t.getDate())} ${o(t.getHours())}:${o(t.getMinutes())}`}function v(n){let e=0;for(const t of n)e+=t.charCodeAt(0)>127?2:1;return e}function E(n,e){const t=e-v(n);return t>0?n+" ".repeat(t):n}function b(n,e){const t=n.map(((n,t)=>{const o=e.reduce(((n,e)=>Math.max(n,v(e[t]||""))),0);return Math.max(v(n),o)})),o=[];o.push(n.map(((n,e)=>E(n,t[e]))).join(" ")),o.push(t.map((n=>"-".repeat(n))).join(" "));for(const n of e)o.push(n.map(((n,e)=>E(n||"",t[e]))).join(" "));return o.join("\n")}function k(n){const e=n?.data;if(!e)return"(空目录)";const t=e.list||e.data||[];if(!Array.isArray(t)||0===t.length)return"(空目录)";const o=b(["类型","名称","大小","修改时间","NID"],t.map((n=>{const e=1===n.is_dir||"1"===n.is_dir||"dir"===n.type;return[e?"d":"-",n.name||n.fname||"-",e?"-":P(parseInt(n.count_size||n.size||"0")),y(n.modify_time||n.mtime),n.nid||"-"]}))),s=void 0!==e.page_num?e.page_num:"";let r=`共 ${e.total_count||e.total||t.length} 项`;return""!==s&&(r+=`,第 ${s} 页`),o+"\n"+r}function R(n){const e=n?.data;if(!e)return"无搜索结果";const t=e.list||e.data||[];if(!Array.isArray(t)||0===t.length)return"无搜索结果";return b(["类型","名称","大小","路径","NID"],t.map((n=>{const e=1===n.is_dir||"1"===n.is_dir;return[e?"d":"-",n.name||n.fname||"-",e?"-":P(parseInt(n.count_size||n.size||"0")),n.path||"-",n.nid||"-"]})))+"\n"+`共找到 ${e.total_count||e.total||t.length} 项`}function O(n,e){if(!n)return e;if(n.data?.share_url)return`${e}\n分享链接: ${n.data.share_url}`;if(n.data?.downloadUrl||n.downloadUrl){const t=n.data?.downloadUrl||n.downloadUrl,o=n.filename||"",s=n.fileSize||"",r=n.downloadPath||"";let a=e;return o&&(a+=`\n文件名: ${o}`),s&&(a+=`\n大小: ${s}`),r&&(a+=`\n保存到: ${r}`),t&&(a+=`\n链接: ${t}`),a}if(void 0!==n.fileCount){let t=`${e}\n上传: ${n.fileCount}/${n.totalFileCount} 文件,耗时 ${n.totalTime}秒`;if(n.uploadResults?.length)for(const e of n.uploadResults)t+=`\n - ${e.name||e.uploadRes?.name||"未知"}`;return n.duplicateFiles?.length&&(t+=`\n重名文件: ${n.duplicateFiles.map((n=>n.name)).join(", ")}`),t}if(n.data?.task_id||n.data?.file_path){let t=e;return n.data?.file_path&&(t+=`\n路径: ${n.data.file_path}`),n.data?.file_size&&(t+=`\n大小: ${P(n.data.file_size)}`),t}return e}function C(n){const e=n?.data;if(!e)return"无法获取用户信息";const t=[];return(e.nickname||e.nick)&&t.push(`昵称: ${e.nickname||e.nick}`),e.qid&&t.push(`QID: ${e.qid}`),void 0!==e.space_used&&void 0!==e.space_total&&t.push(`空间: ${P(e.space_used)} / ${P(e.space_total)}`),e.vip_type&&t.push(`VIP: ${e.vip_type}`),t.length>0?t.join("\n"):JSON.stringify(e,null,2)}const L=Object.freeze({__proto__:null,executeBatch:async function(n,e){const t=[];let o=0,s=0;for(let r=0;r<n.length;r++)try{const s=await e(n[r],r);t.push({index:r,input:n[r],success:!0,result:s}),o++}catch(e){t.push({index:r,input:n[r],success:!1,error:e.message}),s++}return{total:n.length,succeeded:o,failed:s,items:t}},formatBatchResult:function(n){const e=[];e.push(`批量操作完成: 成功 ${n.succeeded}/${n.total},失败 ${n.failed}/${n.total}`);for(const t of n.items){const n=t.success?"":` (${t.error})`;e.push(` ${t.success?"✓":"✗"} [${t.index+1}] ${JSON.stringify(t.input)}${n}`)}return e.join("\n")},formatBytes:P,formatFileList:k,formatSearchResult:R,formatSimpleResult:O,formatTable:b,formatUserInfo:C,outputError:g,outputJson:$,outputText:_});function I(){const n=new e("auth").description("鉴权管理").enablePositionalOptions().passThroughOptions();return n.command("login").description("登录:保存 API Key 到本地配置").requiredOption("--api-key <api_key>","API 密钥").option("--env <env>","环境 (prod/test)","prod").option("--sub-channel <channel>","子渠道","open").action((e=>{const t=Date.now();try{const o=d();o.api_key=e.apiKey,e.env&&(o.ecs_env=e.env),e.subChannel&&(o.sub_channel=e.subChannel),h(o);const s=n.parent?.opts()||{};"text"===s.format?_(`登录成功!配置已保存到 ${m()}`):$({message:"登录成功",configPath:m()},"auth login",t,{quiet:s.quiet})}catch(n){g(n,"auth login",t)}})),n.command("whoami").description("查看当前鉴权状态").action((()=>{const e=Date.now();try{const t=d(),o=n.parent?.opts()||{},s={logged_in:!!t.api_key,api_key:t.api_key?t.api_key.substring(0,10)+"***":void 0,ecs_env:t.ecs_env||process.env.ECS_ENV||"prod",sub_channel:t.sub_channel||process.env.SUB_CHANNEL||"open",config_path:m()};"text"===o.format?_(s.logged_in?`已登录\nAPI Key: ${s.api_key}\n环境: ${s.ecs_env}\n渠道: ${s.sub_channel}`:"未登录。请使用 auth login --api-key <API_KEY> 登录"):$(s,"auth whoami",e,{quiet:o.quiet})}catch(n){g(n,"auth whoami",e)}})),n.command("logout").description("退出登录:清除本地配置").action((()=>{const e=Date.now();try{!function(){try{const n=m();t.existsSync(n)&&t.unlinkSync(n)}catch{}}();const o=n.parent?.opts()||{};"text"===o.format?_("已退出登录,本地配置已清除"):$({message:"已退出登录"},"auth logout",e,{quiet:o.quiet})}catch(n){g(n,"auth logout",e)}})),n}const M={test:"https://qaopen.eyun.360.cn/intf.php",hgtest:"https://hg-openapi.eyun.360.cn/intf.php",prod:"https://openapi.eyun.360.cn/intf.php"};function z(n){const e=n||process.env.ECS_ENV||"prod";return{request_url:M[e]||M.prod,client_id:"e4757e933b6486c08ed206ecb6d5d9e684fcb4e2",client_secret:"test"===e?"b11b8fff1c75a5d227c8cc93aaeb0bb70c8eee47":"885fd3231f1c1e37c9f462261a09b8c38cde0c2b"}}function S(n,e="e7b24b112a44fdd9ee93bdf998c6ca0e"){const t=Object.keys(n).sort().map((e=>{const t=function(n){return encodeURIComponent(n).replace(/%20/g,"+").replace(/[!'()*~]/g,(n=>`%${n.charCodeAt(0).toString(16).toUpperCase()}`))}(n[e]);return`${e}=${t}`}));let o=t.join("&");return o+=e,s=o,r.createHash("md5").update(s,"utf8").digest("hex");var s}n.config();const D="production"===process.env.NODE_ENV,W="true"===process.env.LOG_TO_FILE,A=process.env.LOG_FILE_PATH||"/data/logs/ecs-mcp/app.log",Y=(process.env.LOG_TIME_FORMAT||"epoch").toLowerCase();function U(){const n=new Date,e=new Intl.DateTimeFormat("en-CA",{timeZone:"Asia/Shanghai",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1}).formatToParts(n).reduce(((n,e)=>("literal"!==e.type&&(n[e.type]=e.value),n)),{}),t=String(n.getMilliseconds()).padStart(3,"0");return`,"time":"${`${e.year}-${e.month}-${e.day}T${e.hour}:${e.minute}:${e.second}.${t}+08:00`}"`}let N;if(W)N=a.destination({dest:A,minLength:512,sync:!1});else{const n=process.env.LOG_STDERR,e="string"==typeof n&&n.length>0,t=Array.isArray(process.argv)?process.argv.slice(2):[],o=t.includes("--stdio")||t.includes("--all"),s=e?"true"===n.toLowerCase():o;N=a.destination({fd:s?2:1,minLength:256,sync:!0})}const x=a({level:process.env.LOG_LEVEL||(D?"info":"debug"),base:{service:process.env.APP_NAME||"ecs-mcp",version:process.env.APP_VERSION||"0.0.0",log_type:"app"},formatters:{level:(n,e)=>({level:n,level_number:e})},redact:{paths:["req.headers.authorization","authInfo.token","authInfo.access_token","req.body.params.arguments.access_token","req.body.params.arguments.token"],censor:"***"},timestamp:"iso"===Y?a.stdTimeFunctions.isoTime:"epoch"===Y?a.stdTimeFunctions.epochTime:"beijing"===Y||"cst"===Y||"asia/shanghai"===Y?U:a.stdTimeFunctions.isoTime},N),T=n=>{n&&x.error({err:n},"fatal error");try{N?.flushSync?.()}catch{}try{N?.end?.()}catch{}process.exit(1)};async function q(n,e){const t=function(n){let e="",t="",o="",s="";n&&(n.apiKey&&(e=n.apiKey),n.subChannel&&(t=n.subChannel),n.q&&(o=n.q),n.t&&(s=n.t)),e||(e=process.env.API_KEY||""),t||(t=process.env.SUB_CHANNEL||"open");const{client_id:r,client_secret:a}=z(n?.ecsEnv);return{apiKey:e,clientId:r,clientSecret:a,subChannel:t,q:o,t:s}}(e);if(!t.apiKey&&!t.q&&!t.t)throw new Error("未配置YUNPAN_API_KEY环境变量");try{const{request_url:o}=z(e?.ecsEnv),s=t.subChannel,r=new URL(o);let a={};const i=n.extraParams;i&&"file-upload-stdio"===i.toolName?(a.method=i.method,a.client_id=i.clientId,a.client_secret=i.clientSecret,a.qid=i.qid,a.grant_type=i.grantType,a.sub_channel=s):(a.method="Oauth.getAccessTokenByApiKey",a.client_id=t.clientId,a.client_secret=t.clientSecret,a.grant_type="authorization_code",a.sub_channel=s,t.apiKey&&(a.api_key=t.apiKey)),Object.entries(a).forEach((([n,e])=>{r.searchParams.append(n,e)}));const c={Accept:"application/json"};t.apiKey&&(c.api_key=t.apiKey),t.q&&(c.q=t.q),t.t&&(c.t=t.t);const l=await fetch(r.toString(),{method:"GET",headers:c});if(!l.ok)throw new Error(`鉴权请求失败,状态码: ${l.status}`);const u=await l.json();if(0!==u.errno)throw new Error(`鉴权请求返回错误: ${u.errmsg}`);const{access_token:m,qid:d,token:h}=u.data;return{access_token:m,qid:d,token:h,sub_channel:s}}catch(n){throw x.error({err:n},"获取鉴权信息失败"),n}}async function F(n={},e){x.debug({transportAuthInfo:e?{hasApiKey:!!e.apiKey,apiKey:e.apiKey||"",subChannel:e.subChannel,ecsEnv:e.ecsEnv,q:e.q||"",t:e.t||""}:void 0},"getAuthInfo");const t=await q(n,e);if(n.extraParams&&"file-upload-stdio"===n.extraParams.toolName)return t.qid=n.extraParams.qid,t;const o=S(function(n,e,t={}){const o={};if(n.access_token&&(o.access_token=String(n.access_token)),e&&(o.method=String(e)),n.qid&&(o.qid=String(n.qid)),t)for(const[n,e]of Object.entries(t))null!=e&&(o[String(n)]=String(e));return{...o}}({access_token:t.access_token,qid:t.qid},n.method||"",n.extraParams));return t.sign=o,t}["SIGINT","SIGTERM","beforeExit","uncaughtException"].forEach((n=>{process.on(n,T)})),process.on("unhandledRejection",(n=>T(n))),n.config();class K{apiKey;ecsEnv;subChannel;timeout;retries;retryDelay;constructor(n){this.apiKey=n.apiKey,this.ecsEnv=n.ecsEnv||"prod",this.subChannel=n.subChannel||"open",this.timeout=n.timeout||3e4,this.retries=n.retries||0,this.retryDelay=n.retryDelay||1e3}getTransportAuthInfo(){return{apiKey:this.apiKey,ecsEnv:this.ecsEnv,subChannel:this.subChannel}}async getAuth(n={}){const e=this.getTransportAuthInfo(),t=await F(n,e);return t.request_url=z(this.ecsEnv).request_url,t}isRetryable(n){return"AbortError"===n.name||("ECONNRESET"===n.code||"ETIMEDOUT"===n.code||"ECONNREFUSED"===n.code||!!n.message?.includes("状态码: 5"))}async fetchWithRetry(n,e=0){const{url:t,init:o}=n(),s=new AbortController,r=setTimeout((()=>s.abort()),this.timeout),a={...o,signal:s.signal};try{const n=await fetch(t,a);if(clearTimeout(r),!n.ok)throw new Error(`API 请求失败,状态码: ${n.status}`);const e=await n.text();try{return JSON.parse(e)}catch{throw new Error(`无法解析API响应: ${e.substring(0,100)}...`)}}catch(t){if(clearTimeout(r),e<this.retries&&this.isRetryable(t)){const o=this.retryDelay*Math.pow(2,e);return x.debug({attempt:e+1,delay:o,error:t.message},"请求失败,准备重试"),await new Promise((n=>setTimeout(n,o))),this.fetchWithRetry(n,e+1)}if("AbortError"===t.name)throw new Error(`请求超时 (${this.timeout}ms)`);throw t}}async apiGet(n,e){return this.fetchWithRetry((()=>{const t=new URL(n.request_url||"");return Object.entries(e).forEach((([n,e])=>{t.searchParams.append(n,String(e))})),{url:t.toString(),init:{method:"GET",headers:{"Access-Token":n.access_token||""}}}}))}async apiPost(n,e,t={}){return this.fetchWithRetry((()=>{const o=new URL(n.request_url||"");Object.entries(e).forEach((([n,e])=>{o.searchParams.append(n,String(e))}));const s=new URLSearchParams;return Object.entries(t).forEach((([n,e])=>{s.append(n,String(e))})),{url:o.toString(),init:{method:"POST",headers:{"Access-Token":n.access_token||"","Content-Type":"application/x-www-form-urlencoded"},body:s}}}))}buildBaseParams(n,e){return{method:e,access_token:n.access_token||"",qid:n.qid||"",sign:n.sign||"",sub_channel:n.sub_channel}}get logger(){return x}}class j extends K{async info(){const n=await this.getAuth({}),e=this.buildBaseParams(n,"User.getUserDetail");return e.sign="",await this.apiGet(n,e)}}class B extends K{async list(n={}){const{path:e="/",page:t=0,page_size:o=50}=n;let s=e||"/";"/"!==s&&(s.startsWith("/")||(s="/"+s),s.endsWith("/")||(s+="/"));const r={path:s,page:t,page_size:o},a=await this.getAuth({method:"File.getList",extraParams:r}),i={...this.buildBaseParams(a,"File.getList")};for(const[n,e]of Object.entries(r))"access_token"!==n&&(i[n]=String(e));return await this.apiGet(a,i)}async mkdir(n){const e={fname:n},t=await this.getAuth({method:"File.mkdir",extraParams:e}),o=this.buildBaseParams(t,"File.mkdir");return await this.apiPost(t,{},{...o,fname:n})}}const V="未配置 API Key,请先执行 auth login --api-key <API_KEY> 或设置 API_KEY 环境变量";async function J(){if(process.stdin.isTTY)throw new Error('--stdin 模式需要管道输入,例如: echo "content" | 360disk file save --stdin');const n=[];for await(const e of process.stdin)n.push(Buffer.from(e));return Buffer.concat(n).toString("utf-8")}class G extends K{async move(n){const{src_name:e,new_name:t}=n,o={src_name:e,new_name:t},s=await this.getAuth({method:"File.move",extraParams:o}),r=this.buildBaseParams(s,"File.move");return await this.apiPost(s,{},{...r,src_name:e,new_name:t})}async rename(n){const{src_name:e,new_name:t}=n,o={src_name:e,new_name:t},s=await this.getAuth({method:"File.rename",extraParams:o}),r=this.buildBaseParams(s,"File.rename");return await this.apiPost(s,{},{...r,src_name:e,new_name:t})}async delete(n){const e=await this.getAuth({method:"File.delete"}),t=this.buildBaseParams(e,"File.delete");return await this.apiPost(e,{},{...t,fname:n})}}const H=["http:","https:"],Q=/[\\@]/,X=[/^127\./,/^10\./,/^11\./,/^172\.(1[6-9]|2\d|3[01])\./,/^192\.168\./,/^169\.254\./,/^224\./,/^0\./,/^255\./,/^::1$/,/^fe80::/i,/^fc00::/i,/^fd00::/i,/^localhost$/i,/\.local$/i,/\.lan$/i,/\.internal$/i,/\.intranet$/i,/\.corp$/i,/\.home$/i],Z=[22,23,25,53,135,139,445,1433,1521,3306,3389,5432,6379,9200,11211,27017];async function nn(n,e=5e3){const t=function(n){if(!n||"string"!=typeof n)return{isValid:!1,error:"URL不能为空"};try{const t=n.trim();if(Q.test(t))return{isValid:!1,error:"URL包含不安全的字符"};const o=new URL(t);if(!H.includes(o.protocol))return{isValid:!1,error:"不支持的协议,只支持 http/https"};const s=o.hostname.toLowerCase();if(!s)return{isValid:!1,error:"主机名不能为空"};for(const n of X)if(n.test(s))return{isValid:!1,error:"禁止访问受限地址"};if(en(s)&&!tn(s))return{isValid:!1,error:"禁止访问受限IP地址"};if(o.port){const n=parseInt(o.port,10);if(Z.includes(n))return{isValid:!1,error:"禁止访问受限端口"}}return e=o.pathname,[/\.\./,/%2e%2e/i,/%252e%252e/i,/\/etc\//i,/\/proc\//i,/\/sys\//i,/\/dev\//i].some((n=>n.test(e)))?{isValid:!1,error:"路径包含不安全的内容"}:{isValid:!0,normalizedUrl:o.toString(),originalHostname:s}}catch(n){return{isValid:!1,error:"URL格式无效"}}var e}(n);if(!t.isValid)return t;try{const o=new URL(n).hostname.toLowerCase();if(en(o))return{...t,resolvedIP:o,originalHostname:o,needsHostHeader:!1};let s=[];try{const n=i.lookup(o,{all:!0}),t=new Promise(((n,t)=>{setTimeout((()=>t(new Error("DNS解析超时"))),e)}));s=(await Promise.race([n,t])).map((n=>n.address))}catch(n){return{isValid:!1,error:"域名解析失败"}}for(const n of s){for(const e of X)if(e.test(n))return{isValid:!1,error:"域名解析到受限IP地址"};if(!tn(n))return{isValid:!1,error:"域名解析到受限IP地址"}}return{isValid:!0,normalizedUrl:t.normalizedUrl,resolvedIP:s[0],resolvedIPs:s,originalHostname:o,needsHostHeader:!1}}catch(n){return{isValid:!1,error:"URL验证过程中发生错误"}}}function en(n){if(/^(\d{1,3}\.){3}\d{1,3}$/.test(n)){return n.split(".").every((n=>{const e=parseInt(n,10);return e>=0&&e<=255}))}return/^([0-9a-f]{0,4}:){2,7}[0-9a-f]{0,4}$/i.test(n)}function tn(n){for(const e of X)if(e.test(n))return!1;return!0}class on extends K{normalizeUploadResult(n){return{id:n?.id,name:n?.name||n?.uploadRes?.name,path:n?.uploadRes?.name,fid:n?.fid,size:n?.size||n?.uploadRes?.count_size,uploadRes:n?.uploadRes?{nid:n.uploadRes.nid,qid:n.uploadRes.qid,name:n.uploadRes.name,count_size:n.uploadRes.count_size,file_hash:n.uploadRes.file_hash,create_time:n.uploadRes.create_time,modify_time:n.uploadRes.modify_time}:void 0}}async search(n){const{key:e="",file_category:t=-1,page:o=1,page_size:s=20}=n;if(!e&&-1===t)throw new Error("必须提供搜索关键词(key)或指定文件类型(file_category)");const r={file_category:t,key:e,page:o,page_size:s},a=await this.getAuth({method:"File.searchList",extraParams:r}),i=this.buildBaseParams(a,"File.searchList"),c={};for(const[n,e]of Object.entries(r))"access_token"!==n&&(c[n]=String(e));return await this.apiPost(a,i,c)}async share(n){const e=await this.getAuth({method:"Share.preShare"}),t=this.buildBaseParams(e,"Share.preShare");return await this.apiPost(e,{},{...t,paths:n})}async getDownloadUrl(n){const{nid:e,fpath:t}=n;if(void 0===e&&!t)throw new Error("必须提供nid或fpath中的一个参数");const o={};void 0!==e?o.nid=e:t&&(o.fpath=t);const s=await this.getAuth({method:"MCP.getDownLoadUrl",extraParams:o}),r={...this.buildBaseParams(s,"MCP.getDownLoadUrl")};return void 0!==e?r.nid=String(e):t&&(r.fpath=t),await this.apiPost(s,{},r)}async save(n){const{url:e,content:t,upload_path:o,file_name:s}=n;if(!e&&!t||e&&t)throw new Error("url 与 content 互斥,必须且只能传一个");if(e){const n=await nn(e,5e3);if(!n.isValid)throw new Error(`URL安全验证失败: ${n.error}`)}const r=await this.getAuth({method:"MCP.saveFile"}),a=this.buildBaseParams(r,"MCP.saveFile"),i={};o&&(i.upload_path=o),e?i.url=e:t&&(i.content=t),s&&(i.file_name=s);const c=await this.apiPost(r,a,i);if(c&&0===c.errno){const n=c.data?.task_id;if(!n)throw new Error("保存文件失败: 未获取到任务ID");return await this.pollTaskStatus(r,n)}throw new Error(c?.errmsg||"API请求失败")}async queryTaskStatus(n,e){const t={method:"MCP.query",qid:n.qid||"",access_token:n.access_token||"",sign:n.sign||""};return await this.apiPost(n,t,{task_id:e})}async pollTaskStatus(n,e,t=1e3,o=120){let s,r=0;for(;r<o;){if(r++,s=await this.queryTaskStatus(n,e),0!==s.errno)throw new Error(s.errmsg||"查询任务状态失败");const o=s.data?.status;if(2===o)return s;if(3===o)throw new Error(s.data?.error||"文件保存失败");await new Promise((n=>setTimeout(n,t)))}return s}async upload(n){const{filePaths:e,uploadPath:o="/"}=n;if(!e||0===e.length)throw new Error("filePaths 为必填参数且不能为空");for(const n of e)if(!t.existsSync(n))throw new Error(`文件不存在: ${n}`);const s=await this.getAuth({});let r;try{const n=await import("@aicloud360/sec-sdk-node");r=n.UploadNode}catch{throw new Error("请先安装 @aicloud360/sec-sdk-node: npm install @aicloud360/sec-sdk-node")}const a={qid:s.qid||"0",token:s.token||"",access_token:s.access_token||"",env:this.ecsEnv,path:o};return new Promise(((n,t)=>{const s=Date.now(),i=[],c=[],l=[];new r(a,{success:n=>{i.push(this.normalizeUploadResult(n))},progress:()=>{},error:(n,e)=>{const t=e?.errmsg||e?.message||JSON.stringify(e);c.push({fileName:n?.name||"未知文件",error:t})},duplicateList:n=>{l.push(...n.map((n=>({name:n.name,nid:n.nid,size:n.count_size,path:n.path||o}))))},complete:()=>{c.length>0&&0===i.length?t(new Error(`所有文件上传失败: ${c.map((n=>`${n.fileName}: ${n.error}`)).join("; ")}`)):n({uploadResults:i,uploadErrors:c,duplicateFiles:l,totalTime:((Date.now()-s)/1e3).toFixed(2),fileCount:i.length,totalFileCount:e.length})}}).addWaitFile(e)}))}async download(n){const{nid:e,auto:t=!0,downloadDir:r}=n,a=r||o.join(s(),".mcp-downloads"),i=await this.getAuth({method:"Sync.getVerifiedDownLoadUrl",extraParams:{nid:e}}),c={...this.buildBaseParams(i,"Sync.getVerifiedDownLoadUrl"),nid:String(e)},u=await this.apiGet(i,c);if(!u||0!==u.errno)throw new Error(u?.errmsg||"API请求失败");const m=u.data?.downloadUrl||"";if(!m)throw new Error("未能获取到文件下载链接");const{filename:d,sizeMB:h}=this.extractFileInfoFromUrl(m),p=h>0?`${h.toFixed(2)} MB`:"未知大小";if(!t)return{downloadUrl:m,filename:d,fileSize:p,downloadPath:""};await l.mkdir(a,{recursive:!0});const f=o.join(a,d);return h>10?(this.spawnBackgroundDownload(m,f),{downloadUrl:m,filename:d,fileSize:p,downloadPath:f,background:!0}):(await this.downloadWithCurl(m,f),{downloadUrl:m,filename:d,fileSize:p,downloadPath:f})}extractFileInfoFromUrl(n){try{const e=new URL(n);let t="downloaded_file";const s=e.searchParams.get("fname");if(s?t=o.basename(decodeURIComponent(s)):e.pathname&&e.pathname.length>1&&(t=o.basename(e.pathname)),t=t.replace(/[\x00/]/g,"_"),t&&"."!==t||(t="downloaded_file"),t.length>200){const n=o.extname(t);t=o.basename(t,n).substring(0,200-n.length)+n}const r=e.searchParams.get("fsize");return{filename:t,sizeMB:r?parseInt(r,10)/1048576:0}}catch{return{filename:"downloaded_file",sizeMB:0}}}downloadWithCurl(n,e,t=3e5){return new Promise(((o,s)=>{const r=c("curl",["-L","-s","-A","yunpan_mcp_server",n,"-o",e]),a=setTimeout((()=>{r.kill(),s(new Error("下载超时"))}),t);r.on("close",(n=>{clearTimeout(a),0===n?o():s(new Error(`下载失败,curl 退出码: ${n}`))})),r.on("error",(n=>{clearTimeout(a),s(n)}))}))}spawnBackgroundDownload(n,e){c("curl",["-L","-s","-A","yunpan_mcp_server",n,"-o",e],{detached:!0,stdio:"ignore"}).unref()}}const sn="未配置 API Key,请先执行 auth login --api-key <API_KEY> 或设置 API_KEY 环境变量";function rn(n,e,t,o,s){"text"===o.format&&s?_(s(n)):$(n,e,t,{quiet:o.quiet})}function an(){const n=new e("file").description("文件操作");return n.command("mv").description("移动文件或文件夹").argument("<src>","源路径,多个用 | 分隔").argument("<dest>","目标文件夹路径").action((async(e,t)=>{const o=Date.now(),s=n.parent?.opts()||{};try{const n=p(s);if(!n.apiKey)return void g(new w(sn,f.AUTH_ERROR),"file mv",o);const r=new G(n);rn(await r.move({src_name:e,new_name:t}),"file mv",o,s,(n=>O(n,`已移动: ${e} -> ${t}`)))}catch(n){g(n,"file mv",o)}})),n.command("rename").description("重命名文件或文件夹").argument("<path>","原文件完整路径").argument("<new_name>","新名称").action((async(e,t)=>{const o=Date.now(),s=n.parent?.opts()||{};try{const n=p(s);if(!n.apiKey)return void g(new w(sn,f.AUTH_ERROR),"file rename",o);const r=new G(n);rn(await r.rename({src_name:e,new_name:t}),"file rename",o,s,(n=>O(n,`已重命名: ${e} -> ${t}`)))}catch(n){g(n,"file rename",o)}})),n.command("rm").description("删除文件或文件夹").argument("<path>","文件路径,多个用 | 分隔").option("--batch","批量模式:从 stdin 读取路径列表(每行一个)").action((async(e,t)=>{const o=Date.now(),s=n.parent?.opts()||{};try{const n=p(s);if(!n.apiKey)return void g(new w(sn,f.AUTH_ERROR),"file rm",o);const r=new G(n);if(t.batch){const n=await async function(){return(await J()).split("\n").map((n=>n.trim())).filter(Boolean)}(),t=[e,...n].filter(Boolean),{executeBatch:a,formatBatchResult:i}=await Promise.resolve().then((function(){return L}));rn(await a(t,(async n=>await r.delete(n))),"file rm --batch",o,s,(n=>i(n)))}else{rn(await r.delete(e),"file rm",o,s,(n=>O(n,`已删除: ${e}`)))}}catch(n){g(n,"file rm",o)}})),n.command("search").description("按关键词搜索文件").argument("<keyword>","搜索关键词").option("--type <type>","文件类型: -1(全部) 0(其他) 1(图片) 2(文档) 3(音乐) 4(视频)","-1").option("--page <n>","页码","1").option("--size <n>","每页数量","20").action((async(e,t)=>{const o=Date.now(),s=n.parent?.opts()||{};try{const n=p(s);if(!n.apiKey)return void g(new w(sn,f.AUTH_ERROR),"file search",o);const r=new on(n);rn(await r.search({key:e,file_category:parseInt(t.type),page:parseInt(t.page),page_size:parseInt(t.size)}),"file search",o,s,R)}catch(n){g(n,"file search",o)}})),n.command("share").description("生成文件分享链接").argument("<paths>","文件路径,多个用 | 分隔").action((async e=>{const t=Date.now(),o=n.parent?.opts()||{};try{const n=p(o);if(!n.apiKey)return void g(new w(sn,f.AUTH_ERROR),"file share",t);const s=new on(n);rn(await s.share(e),"file share",t,o,(n=>O(n,"分享链接已生成")))}catch(n){g(n,"file share",t)}})),n.command("url").description("获取文件下载链接").argument("<path>","文件路径").option("--nid <nid>","文件 NID(与路径二选一)").action((async(e,t)=>{const o=Date.now(),s=n.parent?.opts()||{};try{const n=p(s);if(!n.apiKey)return void g(new w(sn,f.AUTH_ERROR),"file url",o);const r=new on(n);rn(await r.getDownloadUrl({nid:t.nid,fpath:t.nid?void 0:e}),"file url",o,s,(n=>O(n,"下载链接已获取")))}catch(n){g(n,"file url",o)}})),n.command("save").description("通过 URL 或文本内容保存文件到云盘").option("--url <url>","文件下载地址(与 --content/--stdin 互斥)").option("--content <text>","文件内容(与 --url/--stdin 互斥)").option("--stdin","从标准输入读取内容(与 --url/--content 互斥)").option("--dest <path>","云盘存储路径,必须以 / 开头和结尾").option("--filename <name>","保存的文件名").action((async e=>{const t=Date.now(),o=n.parent?.opts()||{};try{const n=p(o);if(!n.apiKey)return void g(new w(sn,f.AUTH_ERROR),"file save",t);if(1!==[e.url,e.content,e.stdin].filter(Boolean).length)return void g(new w("--url、--content、--stdin 三者互斥,必须且只能传一个",f.INVALID_ARGS),"file save",t);let s=e.content;if(e.stdin&&(s=await J(),!s.trim()))return void g(new w("stdin 输入内容为空",f.INVALID_ARGS),"file save",t);const r=new on(n);rn(await r.save({url:e.url,content:s,upload_path:e.dest,file_name:e.filename}),"file save",t,o,(n=>O(n,"文件保存成功")))}catch(n){g(n,"file save",t)}})),n.command("upload").description("上传本地文件到云盘").argument("<files>","本地文件路径,多个用逗号分隔").option("--dest <path>","云盘目标路径","/").action((async(e,t)=>{const o=Date.now(),s=n.parent?.opts()||{};try{const n=p(s);if(!n.apiKey)return void g(new w(sn,f.AUTH_ERROR),"file upload",o);const r=e.split(",").map((n=>n.trim())),a=new on(n);rn(await a.upload({filePaths:r,uploadPath:t.dest}),"file upload",o,s,(n=>O(n,"上传完成")))}catch(n){g(n,"file upload",o)}})),n.command("download").description("下载云盘文件到本地").argument("<nid>","文件 NID(可通过 file search / dir ls 获取)").option("--dir <path>","本地下载目录").option("--no-auto","仅获取下载链接,不自动下载").action((async(e,t)=>{const o=Date.now(),s=n.parent?.opts()||{};try{const n=p(s);if(!n.apiKey)return void g(new w(sn,f.AUTH_ERROR),"file download",o);const r=new on(n);rn(await r.download({nid:e,auto:t.auto,downloadDir:t.dir}),"file download",o,s,(n=>O(n,"下载完成")))}catch(n){g(n,"file download",o)}})),n}function cn(){return o.join(s(),".360disk")}function ln(){const n=process.env.SHELL||"";return n.includes("zsh")?"zsh":n.includes("bash")?"bash":"unknown"}function un(n){const e=s();return o.join(e,"zsh"===n?".zshrc":".bashrc")}n.config(),async function(){const n=function(){try{const n=[o.resolve(process.cwd(),"package.json"),o.resolve(new URL(".",import.meta.url).pathname,"../../package.json"),o.resolve(new URL(".",import.meta.url).pathname,"../../../package.json")];for(const e of n)if(t.existsSync(e)){const n=JSON.parse(t.readFileSync(e,"utf8"));return{name:n.cli?.name||"360disk",version:n.version||"0.0.0",description:n.cli?.description||"360 AI 云盘 CLI"}}}catch{}return{name:"360disk",version:"0.0.0",description:"360 AI 云盘 CLI"}}(),s=new e;s.name(n.name).description(n.description).version(n.version).enablePositionalOptions().passThroughOptions().option("--api-key <key>","API 密钥").option("--env <env>","环境 (prod/test)","prod").option("--sub-channel <channel>","子渠道","open").option("--format <type>","输出格式 (json|text)","json").option("--quiet","仅输出结果数据").option("--timeout <ms>","请求超时时间(毫秒)","30000").option("--retries <n>","失败重试次数","0"),s.addCommand(I()),s.addCommand(function(){const n=new e("user").description("用户信息");return n.command("info").description("获取用户详细信息").action((async()=>{const e=Date.now(),t=n.parent?.opts()||{};try{const n=p(t);if(!n.apiKey)return void g(new w("未配置 API Key,请先执行 auth login --api-key <API_KEY> 或设置 API_KEY 环境变量",f.AUTH_ERROR),"user info",e);const o=new j(n),s=await o.info();"text"===t.format?_(C(s)):$(s,"user info",e,{quiet:t.quiet})}catch(n){g(n,"user info",e)}})),n}()),s.addCommand(function(){const n=new e("dir").description("目录操作");return n.command("ls").description("列出云盘指定目录下的文件和文件夹").argument("[path]","目录路径","/").option("--page <n>","页码","0").option("--size <n>","每页数量","50").action((async(e,t)=>{const o=Date.now(),s=n.parent?.opts()||{};try{const n=p(s);if(!n.apiKey)return void g(new w(V,f.AUTH_ERROR),"dir ls",o);const r=new B(n),a=await r.list({path:e,page:parseInt(t.page),page_size:parseInt(t.size)});"text"===s.format?_(k(a)):$(a,"dir ls",o,{quiet:s.quiet})}catch(n){g(n,"dir ls",o)}})),n.command("mkdir").description("创建新文件夹").argument("<path>","文件夹路径").action((async e=>{const t=Date.now(),o=n.parent?.opts()||{};try{const n=p(o);if(!n.apiKey)return void g(new w(V,f.AUTH_ERROR),"dir mkdir",t);const s=new B(n),r=await s.mkdir(e);"text"===o.format?_(O(r,`已创建目录: ${e}`)):$(r,"dir mkdir",t,{quiet:o.quiet})}catch(n){g(n,"dir mkdir",t)}})),n}()),s.addCommand(an()),s.addCommand(function(){const n=new e("completion").description("Shell 自动补全");return n.command("install").description("安装补全脚本到当前 shell").option("--bash","安装 bash 补全").option("--zsh","安装 zsh 补全").action((n=>{try{const e=n.bash?"bash":n.zsh?"zsh":ln();if("unknown"===e)return void g("无法检测 shell 类型,请使用 --bash 或 --zsh 指定","completion install");const s="zsh"===e?"#compdef 360disk\n# 360disk zsh completion\n# 安装: source ~/.360disk/completion.zsh\n# 或添加到 ~/.zshrc: source ~/.360disk/completion.zsh\n\n_360disk() {\n local -a top_commands\n top_commands=(\n 'auth:鉴权管理'\n 'user:用户信息'\n 'dir:目录操作'\n 'file:文件操作'\n 'completion:Shell 自动补全'\n 'help:显示帮助'\n )\n\n local -a auth_commands\n auth_commands=(\n 'login:登录(保存 API Key)'\n 'whoami:查看鉴权状态'\n 'logout:退出登录'\n )\n\n local -a user_commands\n user_commands=(\n 'info:获取用户详细信息'\n )\n\n local -a dir_commands\n dir_commands=(\n 'ls:列出目录内容'\n 'mkdir:创建文件夹'\n )\n\n local -a file_commands\n file_commands=(\n 'mv:移动文件或文件夹'\n 'rename:重命名文件或文件夹'\n 'rm:删除文件或文件夹'\n 'search:搜索文件'\n 'share:生成分享链接'\n 'url:获取下载链接'\n 'save:保存文件到云盘'\n 'upload:上传文件'\n 'download:下载文件'\n )\n\n local -a completion_commands\n completion_commands=(\n 'install:安装补全脚本'\n 'uninstall:卸载补全脚本'\n 'script:输出补全脚本'\n )\n\n _arguments -C \\\n '--api-key[API 密钥]:key:' \\\n '--env[环境]:env:(prod test)' \\\n '--sub-channel[子渠道]:channel:' \\\n '--format[输出格式]:format:(json text)' \\\n '--quiet[仅输出结果]' \\\n '--timeout[超时时间(ms)]:ms:' \\\n '--retries[重试次数]:n:' \\\n '--help[显示帮助]' \\\n '--version[显示版本]' \\\n '1:command:->command' \\\n '2:subcommand:->subcommand' \\\n '*::arg:->args'\n\n case \"$state\" in\n command)\n _describe 'command' top_commands\n ;;\n subcommand)\n case \"$words[2]\" in\n auth) _describe 'auth command' auth_commands ;;\n user) _describe 'user command' user_commands ;;\n dir) _describe 'dir command' dir_commands ;;\n file) _describe 'file command' file_commands ;;\n completion) _describe 'completion command' completion_commands ;;\n esac\n ;;\n args)\n case \"$words[2]\" in\n file)\n case \"$words[3]\" in\n search) _arguments '--type[文件类型]:type:(-1 0 1 2 3 4)' '--page[页码]:n:' '--size[每页数量]:n:' ;;\n save) _arguments '--url[文件URL]:url:' '--content[文件内容]:text:' '--stdin[从stdin读取]' '--dest[目标路径]:path:' '--filename[文件名]:name:' ;;\n upload) _arguments '--dest[云盘路径]:path:' ;;\n download) _arguments '--dir[下载目录]:path:_files -/' '--no-auto[不自动下载]' ;;\n url) _arguments '--nid[文件NID]:nid:' ;;\n rm) _arguments '--batch[批量模式]' ;;\n esac\n ;;\n dir)\n case \"$words[3]\" in\n ls) _arguments '--page[页码]:n:' '--size[每页数量]:n:' ;;\n esac\n ;;\n auth)\n case \"$words[3]\" in\n login) _arguments '--api-key[API密钥]:key:' '--env[环境]:env:(prod test)' '--sub-channel[渠道]:channel:' ;;\n esac\n ;;\n esac\n ;;\n esac\n}\n\n_360disk \"$@\"\n":'# 360disk bash completion\n# 安装: source ~/.360disk/completion.bash\n# 或添加到 ~/.bashrc: source ~/.360disk/completion.bash\n\n_360disk_completions() {\n local cur prev commands\n COMPREPLY=()\n cur="${COMP_WORDS[COMP_CWORD]}"\n prev="${COMP_WORDS[COMP_CWORD-1]}"\n\n # 顶级命令\n local top_commands="auth user dir file completion help"\n\n # 子命令映射\n local auth_commands="login whoami logout"\n local user_commands="info"\n local dir_commands="ls mkdir"\n local file_commands="mv rename rm search share url save upload download"\n local completion_commands="install uninstall script"\n\n # 全局选项\n local global_options="--api-key --env --sub-channel --format --quiet --timeout --retries --help --version"\n\n case "${COMP_WORDS[1]}" in\n auth)\n case "$prev" in\n login) COMPREPLY=( $(compgen -W "--api-key --env --sub-channel --help" -- "$cur") ) ;;\n whoami|logout) COMPREPLY=( $(compgen -W "--help" -- "$cur") ) ;;\n *) COMPREPLY=( $(compgen -W "$auth_commands" -- "$cur") ) ;;\n esac\n ;;\n user)\n case "$prev" in\n info) COMPREPLY=( $(compgen -W "--help" -- "$cur") ) ;;\n *) COMPREPLY=( $(compgen -W "$user_commands" -- "$cur") ) ;;\n esac\n ;;\n dir)\n case "$prev" in\n ls) COMPREPLY=( $(compgen -W "--page --size --help" -- "$cur") ) ;;\n mkdir) COMPREPLY=( $(compgen -W "--help" -- "$cur") ) ;;\n *) COMPREPLY=( $(compgen -W "$dir_commands" -- "$cur") ) ;;\n esac\n ;;\n file)\n case "$prev" in\n search) COMPREPLY=( $(compgen -W "--type --page --size --help" -- "$cur") ) ;;\n url) COMPREPLY=( $(compgen -W "--nid --help" -- "$cur") ) ;;\n save) COMPREPLY=( $(compgen -W "--url --content --stdin --dest --filename --help" -- "$cur") ) ;;\n upload) COMPREPLY=( $(compgen -W "--dest --help" -- "$cur") ) ;;\n download) COMPREPLY=( $(compgen -W "--dir --no-auto --help" -- "$cur") ) ;;\n rm) COMPREPLY=( $(compgen -W "--batch --help" -- "$cur") ) ;;\n mv|rename|share) COMPREPLY=( $(compgen -W "--help" -- "$cur") ) ;;\n *) COMPREPLY=( $(compgen -W "$file_commands" -- "$cur") ) ;;\n esac\n ;;\n completion)\n case "$prev" in\n install|uninstall) COMPREPLY=( $(compgen -W "--bash --zsh --help" -- "$cur") ) ;;\n script) COMPREPLY=( $(compgen -W "--bash --zsh --help" -- "$cur") ) ;;\n *) COMPREPLY=( $(compgen -W "$completion_commands" -- "$cur") ) ;;\n esac\n ;;\n *)\n if [[ "$cur" == -* ]]; then\n COMPREPLY=( $(compgen -W "$global_options" -- "$cur") )\n else\n COMPREPLY=( $(compgen -W "$top_commands" -- "$cur") )\n fi\n ;;\n esac\n}\n\ncomplete -F _360disk_completions 360disk\n',r="zsh"===e?"zsh":"bash",a=cn(),i=o.join(a,`completion.${r}`);t.existsSync(a)||t.mkdirSync(a,{recursive:!0,mode:448}),t.writeFileSync(i,s,{mode:420});const c=un(e),l=function(n){return`source ~/.360disk/completion.${"zsh"===n?"zsh":"bash"} # 360disk auto-completion`}(e);let u="";t.existsSync(c)&&(u=t.readFileSync(c,"utf8")),u.includes("360disk auto-completion")?_(`补全脚本已更新: ${i}\n配置已存在于 ${c},无需重复添加`):(t.appendFileSync(c,"\n"+l+"\n"),_(`补全脚本已安装:\n 脚本: ${i}\n 配置: ${c}\n\n请执行 source ${c} 或重新打开终端生效`))}catch(n){g(n,"completion install")}})),n.command("uninstall").description("卸载补全脚本").option("--bash","卸载 bash 补全").option("--zsh","卸载 zsh 补全").action((n=>{try{const e=n.bash?"bash":n.zsh?"zsh":ln();if("unknown"===e)return void g("无法检测 shell 类型,请使用 --bash 或 --zsh 指定","completion uninstall");const s="zsh"===e?"zsh":"bash",r=o.join(cn(),`completion.${s}`);t.existsSync(r)&&t.unlinkSync(r);const a=un(e);if(t.existsSync(a)){const n=t.readFileSync(a,"utf8").split("\n").filter((n=>!n.includes("360disk auto-completion")));t.writeFileSync(a,n.join("\n"))}_(`补全脚本已卸载\n请执行 source ${un(e)} 或重新打开终端生效`)}catch(n){g(n,"completion uninstall")}})),n.command("script").description("输出补全脚本到 stdout(手动安装用)").option("--bash","输出 bash 补全脚本").option("--zsh","输出 zsh 补全脚本").action((n=>{try{const e=n.bash?"bash":n.zsh?"zsh":ln();if("unknown"===e)return void g("无法检测 shell 类型,请使用 --bash 或 --zsh 指定","completion script");process.stdout.write("zsh"===e?"#compdef 360disk\n# 360disk zsh completion\n# 安装: source ~/.360disk/completion.zsh\n# 或添加到 ~/.zshrc: source ~/.360disk/completion.zsh\n\n_360disk() {\n local -a top_commands\n top_commands=(\n 'auth:鉴权管理'\n 'user:用户信息'\n 'dir:目录操作'\n 'file:文件操作'\n 'completion:Shell 自动补全'\n 'help:显示帮助'\n )\n\n local -a auth_commands\n auth_commands=(\n 'login:登录(保存 API Key)'\n 'whoami:查看鉴权状态'\n 'logout:退出登录'\n )\n\n local -a user_commands\n user_commands=(\n 'info:获取用户详细信息'\n )\n\n local -a dir_commands\n dir_commands=(\n 'ls:列出目录内容'\n 'mkdir:创建文件夹'\n )\n\n local -a file_commands\n file_commands=(\n 'mv:移动文件或文件夹'\n 'rename:重命名文件或文件夹'\n 'rm:删除文件或文件夹'\n 'search:搜索文件'\n 'share:生成分享链接'\n 'url:获取下载链接'\n 'save:保存文件到云盘'\n 'upload:上传文件'\n 'download:下载文件'\n )\n\n local -a completion_commands\n completion_commands=(\n 'install:安装补全脚本'\n 'uninstall:卸载补全脚本'\n 'script:输出补全脚本'\n )\n\n _arguments -C \\\n '--api-key[API 密钥]:key:' \\\n '--env[环境]:env:(prod test)' \\\n '--sub-channel[子渠道]:channel:' \\\n '--format[输出格式]:format:(json text)' \\\n '--quiet[仅输出结果]' \\\n '--timeout[超时时间(ms)]:ms:' \\\n '--retries[重试次数]:n:' \\\n '--help[显示帮助]' \\\n '--version[显示版本]' \\\n '1:command:->command' \\\n '2:subcommand:->subcommand' \\\n '*::arg:->args'\n\n case \"$state\" in\n command)\n _describe 'command' top_commands\n ;;\n subcommand)\n case \"$words[2]\" in\n auth) _describe 'auth command' auth_commands ;;\n user) _describe 'user command' user_commands ;;\n dir) _describe 'dir command' dir_commands ;;\n file) _describe 'file command' file_commands ;;\n completion) _describe 'completion command' completion_commands ;;\n esac\n ;;\n args)\n case \"$words[2]\" in\n file)\n case \"$words[3]\" in\n search) _arguments '--type[文件类型]:type:(-1 0 1 2 3 4)' '--page[页码]:n:' '--size[每页数量]:n:' ;;\n save) _arguments '--url[文件URL]:url:' '--content[文件内容]:text:' '--stdin[从stdin读取]' '--dest[目标路径]:path:' '--filename[文件名]:name:' ;;\n upload) _arguments '--dest[云盘路径]:path:' ;;\n download) _arguments '--dir[下载目录]:path:_files -/' '--no-auto[不自动下载]' ;;\n url) _arguments '--nid[文件NID]:nid:' ;;\n rm) _arguments '--batch[批量模式]' ;;\n esac\n ;;\n dir)\n case \"$words[3]\" in\n ls) _arguments '--page[页码]:n:' '--size[每页数量]:n:' ;;\n esac\n ;;\n auth)\n case \"$words[3]\" in\n login) _arguments '--api-key[API密钥]:key:' '--env[环境]:env:(prod test)' '--sub-channel[渠道]:channel:' ;;\n esac\n ;;\n esac\n ;;\n esac\n}\n\n_360disk \"$@\"\n":'# 360disk bash completion\n# 安装: source ~/.360disk/completion.bash\n# 或添加到 ~/.bashrc: source ~/.360disk/completion.bash\n\n_360disk_completions() {\n local cur prev commands\n COMPREPLY=()\n cur="${COMP_WORDS[COMP_CWORD]}"\n prev="${COMP_WORDS[COMP_CWORD-1]}"\n\n # 顶级命令\n local top_commands="auth user dir file completion help"\n\n # 子命令映射\n local auth_commands="login whoami logout"\n local user_commands="info"\n local dir_commands="ls mkdir"\n local file_commands="mv rename rm search share url save upload download"\n local completion_commands="install uninstall script"\n\n # 全局选项\n local global_options="--api-key --env --sub-channel --format --quiet --timeout --retries --help --version"\n\n case "${COMP_WORDS[1]}" in\n auth)\n case "$prev" in\n login) COMPREPLY=( $(compgen -W "--api-key --env --sub-channel --help" -- "$cur") ) ;;\n whoami|logout) COMPREPLY=( $(compgen -W "--help" -- "$cur") ) ;;\n *) COMPREPLY=( $(compgen -W "$auth_commands" -- "$cur") ) ;;\n esac\n ;;\n user)\n case "$prev" in\n info) COMPREPLY=( $(compgen -W "--help" -- "$cur") ) ;;\n *) COMPREPLY=( $(compgen -W "$user_commands" -- "$cur") ) ;;\n esac\n ;;\n dir)\n case "$prev" in\n ls) COMPREPLY=( $(compgen -W "--page --size --help" -- "$cur") ) ;;\n mkdir) COMPREPLY=( $(compgen -W "--help" -- "$cur") ) ;;\n *) COMPREPLY=( $(compgen -W "$dir_commands" -- "$cur") ) ;;\n esac\n ;;\n file)\n case "$prev" in\n search) COMPREPLY=( $(compgen -W "--type --page --size --help" -- "$cur") ) ;;\n url) COMPREPLY=( $(compgen -W "--nid --help" -- "$cur") ) ;;\n save) COMPREPLY=( $(compgen -W "--url --content --stdin --dest --filename --help" -- "$cur") ) ;;\n upload) COMPREPLY=( $(compgen -W "--dest --help" -- "$cur") ) ;;\n download) COMPREPLY=( $(compgen -W "--dir --no-auto --help" -- "$cur") ) ;;\n rm) COMPREPLY=( $(compgen -W "--batch --help" -- "$cur") ) ;;\n mv|rename|share) COMPREPLY=( $(compgen -W "--help" -- "$cur") ) ;;\n *) COMPREPLY=( $(compgen -W "$file_commands" -- "$cur") ) ;;\n esac\n ;;\n completion)\n case "$prev" in\n install|uninstall) COMPREPLY=( $(compgen -W "--bash --zsh --help" -- "$cur") ) ;;\n script) COMPREPLY=( $(compgen -W "--bash --zsh --help" -- "$cur") ) ;;\n *) COMPREPLY=( $(compgen -W "$completion_commands" -- "$cur") ) ;;\n esac\n ;;\n *)\n if [[ "$cur" == -* ]]; then\n COMPREPLY=( $(compgen -W "$global_options" -- "$cur") )\n else\n COMPREPLY=( $(compgen -W "$top_commands" -- "$cur") )\n fi\n ;;\n esac\n}\n\ncomplete -F _360disk_completions 360disk\n')}catch(n){g(n,"completion script")}})),n}()),await s.parseAsync()}().then((()=>{process.exit(0)})).catch((n=>{process.stderr.write(JSON.stringify({success:!1,error:n.message||"CLI 启动失败"},null,2)+"\n"),process.exit(1)}));