@aicloud360/360-aidrive 0.8.28

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 ADDED
@@ -0,0 +1,101 @@
1
+ # 360 AI Drive CLI
2
+
3
+ `@aicloud360/360-aidrive` 是 360 AI 云盘的官方命令行工具(命令名:`360aidrive`)。它提供了 30 个强大的文件操作和云盘管理命令,支持结构化 JSON 输出,非常适合终端用户和 AI Agent 通过 Shell 直接调用。
4
+
5
+ 结合 [360-ai-cloud-disk-cli-skill](https://github.com/yifangyun/ecs-yunpan-skills/tree/main/360-ai-cloud-disk-cli-skill)(CLI Skill),它可以让任意支持执行系统命令的智能体框架(如 Claude Code, OpenClaw, OpenCode, Cursor 等)快速拥有云盘文件感知和读写能力。
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ npm install -g @aicloud360/360-aidrive
11
+ ```
12
+
13
+ 或者使用 `npx` 零安装运行:
14
+ ```bash
15
+ npx -y @aicloud360/360-aidrive@latest dir ls /
16
+ ```
17
+
18
+ ## 快速开始
19
+
20
+ ### 1. 登录鉴权
21
+
22
+ ```bash
23
+ 360aidrive auth login --api-key <你的_API_KEY>
24
+ ```
25
+ *登录成功后,配置将保存在 `~/.360aidrive/config.json` 中。*
26
+ *(API_KEY 可在 360 AI 云盘 Web 端开发者中心获取。)*
27
+
28
+ ### 2. 常用操作示例
29
+
30
+ ```bash
31
+ # 查看用户信息
32
+ 360aidrive user info
33
+
34
+ # 列出根目录文件
35
+ 360aidrive dir ls /
36
+
37
+ # 搜索文件
38
+ 360aidrive file search "工作汇报"
39
+
40
+ # 获取文件真实下载链接(带时效和防盗链)
41
+ 360aidrive file url <file_id>
42
+
43
+ # 上传本地文件到云盘目录
44
+ 360aidrive file upload ./local-file.txt /remote-dir/
45
+
46
+ # 下载云盘文件到本地目录
47
+ 360aidrive file download <file_id> ./local-dir/
48
+
49
+ # 分享文件(生成分享链接和提取码)
50
+ 360aidrive file share /文档/报告.pdf
51
+
52
+ # 备份本地目录到云盘
53
+ 360aidrive claw-backup --source ~/.cc-switch --dest /cc-switch-backup/
54
+
55
+ # 备份单个本地文件到云盘目录
56
+ 360aidrive claw-backup --source ./notes.txt --dest /cc-switch-backup/
57
+
58
+ # 手动执行 OpenClaw 云控白名单备份
59
+ 360aidrive claw-backup --source-dir ~/.openclaw --claw-name my-claw
60
+
61
+ # 递归恢复云盘目录到本地
62
+ 360aidrive claw-restore --remote /cc-switch-backup/ --target ~/.cc-switch-restored
63
+
64
+ # 启动 OpenClaw 自动备份监听
65
+ 360aidrive claw-auto-backup enable --source-dir ~/.openclaw --claw-name my-claw
66
+ ```
67
+
68
+ ## 命令组概览
69
+
70
+ 本 CLI 工具提供 8 个顶级命令/命令组共 30 个子命令,覆盖绝大部分云盘核心能力:
71
+
72
+ - `auth`: `login` / `whoami` / `logout` —— 鉴权身份管理
73
+ - `user`: `info` —— 获取当前用户信息与容量状态
74
+ - `dir`: `ls` / `mkdir` —— 目录查看与创建
75
+ - `file`: `mv` / `trans-copy` / `rename` / `rm` / `search` / `share` / `url` / `node-info` / `origin-size` / `clear-dir` / `config` / `save` / `append` / `exists` / `upload` / `download` —— 丰富的文件生命周期管理操作
76
+ - `claw-backup`: `--source` / `--dest` 或 `--source-dir` / `--claw-name` —— 上传本地文件、递归上传本地目录,或按云控配置手动执行 OpenClaw 白名单备份
77
+ - `claw-restore`: `--remote` / `--target` —— 递归恢复云盘目录到本地目录
78
+ - `claw-auto-backup`: `enable --source-dir --claw-name` / `disable` / `status` —— 监听 OpenClaw 根目录变化,并按云控 `AFS.backup_dir` / `AFS.backup_match_dir` / `AFS.backup_file` 配置自动备份
79
+ - `completion`: `install` / `uninstall` / `script` —— Shell 自动补全
80
+
81
+ ## 高级特性
82
+
83
+ - **结构化输出**:所有命令默认返回 JSON(包含 `success`, `result`, `meta`),同时支持 `--format text` 和 `--quiet` 模式。
84
+ - **统一错误码**:提供语义化的错误码并与进程退出码对应,极利于自动化脚本集成。
85
+ - **管道集成**:支持 stdin 输入、批量操作,可完美与 `jq` 等终端利器组合。
86
+ - **超时/重试**:内建 `--timeout` 和 `--retries` 全局稳定选项。
87
+
88
+ ## 配置目录与环境变量
89
+
90
+ - 默认配置目录:`~/.360aidrive/`(可在迁移时通过拷贝 `config.json`,或设置环境变量 `AI_CLOUD_DISK_CONFIG_DIR` 指向原目录)。
91
+ - 与 **`@aicloud360/360-ai-cloud-disk-cli`(命令 `360disk`)** 为不同 npm 坐标;两者配置目录互不共享,除非你显式共用 `AI_CLOUD_DISK_CONFIG_DIR`。
92
+
93
+ ## 更多资源
94
+
95
+ - **Skills 仓库**: 如果你正在开发 AI Agent,推荐搭配 [360-ai-cloud-disk-cli-skill](https://github.com/yifangyun/ecs-yunpan-skills/tree/main/360-ai-cloud-disk-cli-skill) 使用。
96
+ - **在线文档**: [360 AI 云盘开放平台文档](https://open.yunpan.360.cn)
97
+ - **问题反馈**: [提交 Issue 到 yifangyun/ecs-yunpan-skills](https://github.com/yifangyun/ecs-yunpan-skills/issues)
98
+
99
+ ## 许可证
100
+
101
+ Apache-2.0
package/README.md.bak ADDED
@@ -0,0 +1,172 @@
1
+ # 360 AI 云盘 MCP Server & CLI
2
+
3
+ 360 AI 云盘的 MCP Server + CLI 工具,为 AI Agent 和开发者提供完整的云盘文件管理能力。为了满足不同的使用场景,我们将产物分为 **三个 npm 包**进行发布:
4
+
5
+ - **360 AI 云盘 CLI (`@aicloud360/360-ai-cloud-disk-cli`)** — 面向终端用户和 AI Agent(通过 Shell)的命令行工具,命令名 **`360disk`**,提供 30 个命令覆盖云盘全部操作,结构化 JSON 输出。
6
+ - **360 AI Drive CLI (`@aicloud360/360-aidrive`)** — 与上者能力相同、不同 npm 坐标的品牌化 CLI,命令名 **`360aidrive`**(配置目录等与 `cli.name` 对齐;详见 [`docs/readme/README-aidrive.md`](docs/readme/README-aidrive.md))。
7
+ - **360 AI 云盘 MCP Server (`@aicloud360/360-ai-cloud-disk-mcp`)** — 面向支持 Model Context Protocol (MCP) 的 AI Agent(如 Claude Desktop)的服务端,支持 Stdio / Streamable HTTP / SSE。
8
+
9
+ ## 1. 360 AI 云盘 CLI
10
+
11
+ ### 安装
12
+
13
+ ```bash
14
+ npm install -g @aicloud360/360-ai-cloud-disk-cli
15
+ ```
16
+
17
+ ### CLI 使用
18
+
19
+ ```bash
20
+ # 登录
21
+ 360disk auth login --api-key yunpan_xxxxxxxxxx
22
+
23
+ # 常用操作
24
+ 360disk dir ls / # 列出目录
25
+ 360disk file search "报告" # 搜索文件
26
+ 360disk file upload ./report.pdf --dest /文档/ # 上传文件
27
+ 360disk file download <nid> --dir ./ # 下载文件
28
+ 360disk file share /文档/报告.pdf # 分享文件
29
+ 360disk claw-backup --source ~/.cc-switch --dest /cc-switch-backup/ # 递归备份本地目录
30
+ 360disk claw-backup --source-dir ~/.openclaw --claw-name my-claw # 手动执行 OpenClaw 云控白名单备份
31
+ 360disk claw-auto-backup enable --source-dir ~/.openclaw --claw-name my-claw # 启动 OpenClaw 自动备份
32
+ ```
33
+
34
+ 详细使用说明请参考:[docs/readme/README-cli.md](docs/readme/README-cli.md)
35
+
36
+ ### AI Drive CLI(品牌化安装)
37
+
38
+ 安装与示例见:[docs/readme/README-aidrive.md](docs/readme/README-aidrive.md)
39
+
40
+ ```bash
41
+ npm install -g @aicloud360/360-aidrive
42
+ 360aidrive auth login --api-key yunpan_xxxxxxxxxx
43
+ ```
44
+
45
+ ## 2. 360 AI 云盘 MCP Server
46
+
47
+ ### 安装
48
+
49
+ ```bash
50
+ npm install -g @aicloud360/360-ai-cloud-disk-mcp
51
+ ```
52
+
53
+ ### MCP Server 接入
54
+
55
+ #### Stdio 方式
56
+
57
+ 在 MCP Client 配置文件中添加(适用于 Cursor、Claude Desktop 等):
58
+
59
+ ```json
60
+ {
61
+ "mcpServers": {
62
+ "360-ai-cloud-disk-mcp": {
63
+ "command": "npx",
64
+ "args": ["-y", "@aicloud360/360-ai-cloud-disk-mcp@latest", "--stdio"],
65
+ "env": {
66
+ "API_KEY": "yunpan_xxxxxxxxxx"
67
+ }
68
+ }
69
+ }
70
+ }
71
+ ```
72
+
73
+ #### Streamable HTTP 方式
74
+
75
+ 无需安装本地环境,通过 URL 直接接入:
76
+
77
+ ```json
78
+ {
79
+ "mcpServers": {
80
+ "360-ai-cloud-disk-mcp": {
81
+ "url": "https://mcp.yunpan.com/mcp?api_key=yunpan_xxxxxxxxxx"
82
+ }
83
+ }
84
+ }
85
+ ```
86
+
87
+ #### SSE 方式
88
+
89
+ 基于 HTTP 长连接的服务器推送:
90
+
91
+ ```json
92
+ {
93
+ "mcpServers": {
94
+ "360-ai-cloud-disk-mcp": {
95
+ "url": "https://mcp.yunpan.com/sse?api_key=yunpan_xxxxxxxxxx"
96
+ }
97
+ }
98
+ }
99
+ ```
100
+
101
+ 详细使用说明请参考:[docs/readme/README-mcp.md](docs/readme/README-mcp.md)
102
+
103
+ ## 功能概览
104
+
105
+ ### MCP 工具(11 个)
106
+
107
+ | 工具 | 说明 |
108
+ |------|------|
109
+ | `file-list` | 获取目录文件列表 |
110
+ | `file-search` | 搜索文件 |
111
+ | `file-save` | 保存文件到云盘(URL / 文本内容) |
112
+ | `file-share` | 生成分享链接 |
113
+ | `file-move` | 移动文件或文件夹 |
114
+ | `file-rename` | 重命名文件或文件夹 |
115
+ | `make-dir` | 创建目录 |
116
+ | `get-download-url` | 获取下载链接 |
117
+ | `user-info` | 获取用户信息 |
118
+ | `file-upload-stdio` | 上传本地文件(仅 Stdio 模式) |
119
+ | `file-download-stdio` | 下载云盘文件(仅 Stdio 模式) |
120
+
121
+ ### CLI 命令(30 个)
122
+
123
+ | 命令组 | 子命令 | 说明 |
124
+ |--------|--------|------|
125
+ | `auth` | `login` / `whoami` / `logout` | 鉴权管理 |
126
+ | `user` | `info` | 用户信息 |
127
+ | `dir` | `ls` / `mkdir` | 目录操作 |
128
+ | `file` | `mv` / `trans-copy` / `rename` / `rm` / `search` / `share` / `url` / `node-info` / `origin-size` / `clear-dir` / `config` / `save` / `append` / `exists` / `upload` / `download` | 文件操作 |
129
+ | `claw-backup` | `--source` / `--dest` 或 `--source-dir` / `--claw-name` | 递归上传本地目录,或按云控配置手动执行 OpenClaw 白名单备份 |
130
+ | `claw-restore` | `--remote` / `--target` | 递归恢复云盘目录到本地目录 |
131
+ | `claw-auto-backup` | `enable --source-dir --claw-name` / `disable` / `status` | 监听 OpenClaw 根目录变化,并按云控配置将白名单目录和配置文件自动备份到云盘 |
132
+ | `completion` | `install` / `uninstall` / `script` | Shell 补全 |
133
+
134
+ ### CLI 特性
135
+
136
+ - **结构化输出**:默认 JSON,支持 `--format text` 和 `--quiet` 模式
137
+ - **统一错误码**:10 个语义化错误码,进程退出码对应
138
+ - **管道集成**:stdin 输入、批量操作,可与 `jq` 等工具组合
139
+ - **超时/重试**:`--timeout` 和 `--retries` 全局选项
140
+ - **Shell 补全**:bash / zsh 自动补全
141
+
142
+ ## Skills 技能库
143
+
144
+ 项目提供两种互补的 Skill 接入方式:
145
+
146
+ | 维度 | CLI Skill(推荐) | MCP Skill |
147
+ |------|-------------------|-----------|
148
+ | 执行方式 | Shell 命令 `360disk ...` | Python executor → MCP Server |
149
+ | 运行时 | Node.js | Python + Node.js |
150
+ | 命令数量 | 25 个 | 11 个 |
151
+ | 适用场景 | Claude Code / Cursor / Windsurf / CI/CD | OpenClaw / Claude Desktop / 各类 Agent 平台 |
152
+ | 特色能力 | 管道组合、批量操作、文件上传下载 | HTTP 远程模式、免安装、75%+ Token 节省 |
153
+
154
+ Skills 仓库:[github.com/yifangyun/ecs-yunpan-skills](https://github.com/yifangyun/ecs-yunpan-skills)
155
+
156
+ ## 认证
157
+
158
+ 使用前需要 360 AI 云盘 API 密钥(`yunpan_` 开头),获取方式参见 [快速接入指南](https://open.yunpan.360.cn/docs/mcp-server/preparation)。
159
+
160
+ 鉴权优先级:`--api-key` 参数 > `API_KEY` 环境变量 > `~/.360disk/config.json`
161
+
162
+ ## 文档
163
+
164
+ - [在线文档](https://open.yunpan.360.cn) — 完整的接入指南和 API 参考
165
+ - [MCP Server 快速开始](https://open.yunpan.360.cn/docs/mcp-server/quick-start)
166
+ - [CLI 命令参考](https://open.yunpan.360.cn/docs/cli/commands)
167
+ - [Skills 使用指南](https://open.yunpan.360.cn/docs/skills/intro)
168
+ - [Agent 接入指南](https://open.yunpan.360.cn/docs/agent/guide)
169
+
170
+ ## 许可证
171
+
172
+ Apache-2.0
package/build/cli.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import{Command as e}from"commander";import t from"fs";import n from"path";import{fileURLToPath as r}from"url";import{homedir as o}from"os";import s from"qrcode-terminal";import i from"jsqr";import{Jimp as a}from"jimp";import c from"crypto";import u from"pino";import l from"iconv-lite";import{promises as f}from"dns";import{spawn as d,execFileSync as h}from"child_process";import p from"fs/promises";import{createRequire as m}from"module";function w(){try{const e=n.dirname(r(import.meta.url)),o=[n.resolve(e,"../package.json"),n.resolve(e,"../../package.json"),n.resolve(e,"../../../package.json"),n.resolve(process.cwd(),"package.json")];for(const e of o)if(t.existsSync(e)){const n=JSON.parse(t.readFileSync(e,"utf8"));if(n.cli?.name)return n.cli.name}}catch{}return"360disk"}function _(){const e=process.env.AI_CLOUD_DISK_CONFIG_DIR?.trim();if(e)return function(e){const t=e.trim();if("~"===t||t.startsWith("~/")){const e="~"===t?"":t.slice(2);return e?n.join(o(),e):o()}return t}(e);const t=w();return n.join(o(),`.${t}`)}function y(){return n.join(_(),"config.json")}function g(){return n.join(_(),"claw-auto-backup.json")}function $(){const e=_();t.existsSync(e)||t.mkdirSync(e,{recursive:!0,mode:448})}function v(){try{const e=y();if(t.existsSync(e))return JSON.parse(t.readFileSync(e,"utf8"))}catch{}return{}}function b(e){$();const n=y();t.writeFileSync(n,JSON.stringify(e,null,2),{mode:384})}function E(e){$();const n=g();t.writeFileSync(n,JSON.stringify(e,null,2),{mode:384})}function k(e){const t=v();return{apiKey:e.apiKey||process.env.API_KEY||t.api_key||"",ecsEnv:e.env||process.env.ECS_ENV||t.ecs_env||"prod",subChannel:e.subChannel||process.env.SUB_CHANNEL||t.sub_channel||"open",timeout:e.timeout?parseInt(e.timeout):void 0,retries:e.retries?parseInt(e.retries):void 0}}var P;!function(e){e[e.SUCCESS=0]="SUCCESS",e[e.GENERAL=1]="GENERAL",e[e.INVALID_ARGS=2]="INVALID_ARGS",e[e.AUTH_ERROR=3]="AUTH_ERROR",e[e.NOT_FOUND=4]="NOT_FOUND",e[e.PERMISSION_DENIED=5]="PERMISSION_DENIED",e[e.NETWORK_ERROR=6]="NETWORK_ERROR",e[e.CONFLICT=7]="CONFLICT",e[e.SERVER_ERROR=8]="SERVER_ERROR",e[e.QUOTA_EXCEEDED=10]="QUOTA_EXCEEDED",e[e.QR_INIT_FAILED=20]="QR_INIT_FAILED",e[e.QR_RENDER_FAILED=21]="QR_RENDER_FAILED",e[e.QR_POLL_TIMEOUT=22]="QR_POLL_TIMEOUT",e[e.QR_POLL_FAILED=23]="QR_POLL_FAILED",e[e.QR_PARSE_FAILED=24]="QR_PARSE_FAILED",e[e.QR_SETCOOKIE_FAILED=25]="QR_SETCOOKIE_FAILED",e[e.QR_INFO_FAILED=26]="QR_INFO_FAILED",e[e.QR_EXCHANGE_APIKEY_FAILED=27]="QR_EXCHANGE_APIKEY_FAILED"}(P||(P={}));class S extends Error{code;traceId;constructor(e,t=P.GENERAL,n){super(e),this.name="CLIError",this.code=t,this.traceId=n}}function I(e){switch(e){case 1001:case 1002:case 1003:case 3001:case 3002:case 3003:return P.AUTH_ERROR;case 3008:return P.NOT_FOUND;case 3007:case 3013:case 3129:return P.CONFLICT;case 3005:case 3006:return P.PERMISSION_DENIED;case 3010:case 3011:return P.QUOTA_EXCEEDED;default:return e>=5e3?P.SERVER_ERROR:P.GENERAL}}function D(e){const t=(e?.message||e?.toString()||"").toLowerCase();return t.includes("超时")||t.includes("timeout")||t.includes("aborted")||"AbortError"===e?.name||t.includes("econnreset")||t.includes("etimedout")||t.includes("econnrefused")||t.includes("fetch failed")||t.includes("网络")?P.NETWORK_ERROR:t.includes("api key")||t.includes("api_key")||t.includes("未配置")||t.includes("token")||t.includes("auth")||t.includes("登录")||t.includes("401")?P.AUTH_ERROR:t.includes("参数")||t.includes("argument")||t.includes("required")||t.includes("互斥")||t.includes("invalid")?P.INVALID_ARGS:t.includes("不存在")||t.includes("not found")||t.includes("404")||t.includes("no such")?P.NOT_FOUND:t.includes("权限")||t.includes("permission")||t.includes("forbidden")||t.includes("403")?P.PERMISSION_DENIED:t.includes("已存在")||t.includes("conflict")||t.includes("duplicate")||t.includes("409")?P.CONFLICT:t.includes("状态码: 5")||t.includes("500")||t.includes("502")||t.includes("503")||t.includes("服务端")?P.SERVER_ERROR:t.includes("空间不足")||t.includes("quota")||t.includes("limit")||t.includes("429")||t.includes("频率")?P.QUOTA_EXCEEDED:P.GENERAL}class O extends Error{traceId;apiErrno;constructor(e,t){super(e),this.name="ApiError",this.traceId=t?.traceId,this.apiErrno=t?.apiErrno}}function R(e){if(!e||"object"!=typeof e)return;const t=e.trace_id??e.traceId;return"string"==typeof t&&t.length>0?t:void 0}function N(e,t,n,r){if(r?.quiet)return void process.stdout.write(JSON.stringify(e)+"\n");const o={success:!0,result:e,meta:{duration_ms:Date.now()-n,command:t}};process.stdout.write(JSON.stringify(o,null,2)+"\n")}function T(e,t,n,r,o,s){if(s?.quiet)return process.stdout.write(JSON.stringify(e)+"\n"),void(process.exitCode=n||1);const i={success:!1,result:e,error:t,code:n,meta:{duration_ms:Date.now()-o,command:r}};process.stdout.write(JSON.stringify(i,null,2)+"\n"),process.exitCode=n||1}function A(e){process.stdout.write(e+"\n")}function C(e,t,n){const r=e instanceof Error?e.message:e;let o;o=e instanceof S?e.code:e instanceof O&&void 0!==e.apiErrno?I(e.apiErrno):D(e);const s={duration_ms:n?Date.now()-n:0,command:t};(e instanceof S&&e.traceId||e instanceof O&&e.traceId)&&(s.trace_id=e.traceId);process.stderr.write(JSON.stringify({success:!1,error:r,code:o,meta:s},null,2)+"\n"),process.exit(o||1)}function L(e,t,n){if(e&&"number"==typeof e.errno&&0!==e.errno){const r=I(e.errno);C(new S(e.errmsg?`${e.errmsg} (errno: ${e.errno})`:`API 错误 (errno: ${e.errno})`,r,"string"==typeof e.trace_id?e.trace_id:"string"==typeof e.traceId?e.traceId:void 0),t,n)}}function F(e,t=2){if(!e||e<=0)return"-";const n=Math.floor(Math.log(e)/Math.log(1024));return parseFloat((e/Math.pow(1024,n)).toFixed(t))+" "+["B","KB","MB","GB","TB"][n]}function x(e){if(!e)return"-";const t="string"==typeof e?parseInt(e):e;if(isNaN(t)||t<=0)return"-";const n=new Date(t<1e12?1e3*t:t),r=e=>String(e).padStart(2,"0");return`${n.getFullYear()}-${r(n.getMonth()+1)}-${r(n.getDate())} ${r(n.getHours())}:${r(n.getMinutes())}`}function M(e){let t=0;for(const n of e)t+=n.charCodeAt(0)>127?2:1;return t}function z(e,t){const n=t-M(e);return n>0?e+" ".repeat(n):e}function U(e,t){const n=e.map((e,n)=>{const r=t.reduce((e,t)=>Math.max(e,M(t[n]||"")),0);return Math.max(M(e),r)}),r=[];r.push(e.map((e,t)=>z(e,n[t])).join(" ")),r.push(n.map(e=>"-".repeat(e)).join(" "));for(const e of t)r.push(e.map((e,t)=>z(e||"",n[t])).join(" "));return r.join("\n")}function q(e){const t=e?.data;if(!t)return{nodes:[],total:0};const n=t.list||t.data||t.node_list||[],r=Array.isArray(n)?n:[];return{nodes:r,total:Number(t.total_count??t.total??r.length)||r.length,page:void 0!==t.page_num?Number(t.page_num):void 0}}function K(e){return!!e&&(1===e.is_dir||"1"===e.is_dir||"dir"===e.type||(1===e.type||"1"===e.type))}function j(e){const{nodes:t,total:n,page:r}=q(e);if(0===t.length)return"(空目录)";const o=U(["类型","名称","大小","修改时间","NID"],t.map(e=>{const t=K(e);return[t?"d":"-",e.name||e.fname||"-",t?"-":F(parseInt(e.count_size||e.size||"0")),x(e.modify_time||e.mtime),e.nid||"-"]}));let s=`共 ${n} 项`;return void 0===r||Number.isNaN(r)||(s+=`,第 ${r} 页`),o+"\n"+s}function B(e){const{nodes:t,total:n}=q(e);if(0===t.length)return"无搜索结果";return U(["类型","名称","大小","路径","NID"],t.map(e=>{const t=K(e);return[t?"d":"-",e.name||e.fname||"-",t?"-":F(parseInt(e.count_size||e.size||"0")),e.path||"-",e.nid||"-"]}))+"\n"+`共找到 ${n} 项`}function W(e,t,n){if(!e||"number"!=typeof e.errno||0!==e.errno)return Y(e,n);const r=e.data;if(!r||"object"!=typeof r)return Y(e,n);const o=e=>null==e?String(e):"object"==typeof e?JSON.stringify(e,null,2):String(e);if("config:read"===t&&"config"in r)return`${n}\n\n${o(r.config)}`;if("config:get"===t&&"value"in r)return`${n}\n\n${o(r.value)}`;if("config:list"===t&&"keys"in r){const e=r.keys;return`${n}\n\n${Array.isArray(e)?e.join("\n"):o(e)}`}return Y(e,n)}function Y(e,t){if(!e)return t;if(e.data?.share_url)return`${t}\n分享链接: ${e.data.share_url}`;if(e.data?.downloadUrl||e.downloadUrl){const n=e.data?.downloadUrl||e.downloadUrl,r=e.filename||"",o=e.fileSize||"",s=e.downloadPath||"";let i=t;return r&&(i+=`\n文件名: ${r}`),o&&(i+=`\n大小: ${o}`),s&&(i+=`\n保存到: ${s}`),n&&(i+=`\n链接: ${n}`),i}if(void 0!==e.fileCount){let n=`${t}\n上传: ${e.fileCount}/${e.totalFileCount} 文件,耗时 ${e.totalTime}秒`;if(e.uploadResults?.length)for(const t of e.uploadResults)n+=`\n - ${t.name||t.uploadRes?.name||"未知"}`;return e.duplicateFiles?.length&&(n+=`\n重名文件: ${e.duplicateFiles.map(e=>e.name).join(", ")}`),n}if(e.data?.task_id||e.data?.file_path){let n=t;return e.data?.file_path&&(n+=`\n路径: ${e.data.file_path}`),e.data?.file_size&&(n+=`\n大小: ${F(e.data.file_size)}`),n}return t}function J(e){if(null==e||""===e)return;if("number"==typeof e)return Number.isFinite(e)&&e>=0?e:void 0;const t=parseInt(String(e),10);return Number.isFinite(t)&&t>=0?t:void 0}function Q(e){const t=e?.data;if(!t)return"无法获取用户信息";const n=function(e){const t=String(e.nickname??e.nick??e.name??"").trim(),n=null!=e.qid?String(e.qid):"";let r,o;if(null!=e.space_used){const t=Number(e.space_used);r=Number.isFinite(t)&&t>=0?t:void 0}else r=J(e.used_size);if(null!=e.space_total){const t=Number(e.space_total);o=Number.isFinite(t)&&t>=0?t:void 0}else o=J(e.total_size);let s="";const i=e.vip_type;s=null!=i&&""!==String(i).trim()?String(i).trim():1===e.is_vip||"1"===e.is_vip||!0===e.is_vip?(null!=e.vip_desc?String(e.vip_desc).trim():"")||"VIP用户":"普通用户";return{nickname:t,qid:n,usedBytes:r,totalBytes:o,vipLabel:s}}(t),r=void 0!==n.usedBytes?F(n.usedBytes):"-",o=void 0!==n.totalBytes?F(n.totalBytes):"-";return[`昵称: ${n.nickname||"-"}`,`QID: ${n.qid||"-"}`,`空间: ${r} / ${o}`,`VIP: ${n.vipLabel||"-"}`].join("\n")}const G=Object.freeze({__proto__:null,checkApiResult:L,executeBatch:async function(e,t){const n=[];let r=0,o=0;for(let s=0;s<e.length;s++)try{const o=await t(e[s],s);n.push({index:s,input:e[s],success:!0,result:o}),r++}catch(t){n.push({index:s,input:e[s],success:!1,error:t.message}),o++}return{total:e.length,succeeded:r,failed:o,items:n}},formatBatchResult:function(e){const t=[];t.push(`批量操作完成: 成功 ${e.succeeded}/${e.total},失败 ${e.failed}/${e.total}`);for(const n of e.items){const e=n.success?"":` (${n.error})`;t.push(` ${n.success?"✓":"✗"} [${n.index+1}] ${JSON.stringify(n.input)}${e}`)}return t.join("\n")},formatBytes:F,formatFileList:j,formatMcpConfigTextResult:W,formatSearchResult:B,formatSimpleResult:Y,formatTable:U,formatUserInfo:Q,outputError:C,outputFailureJson:T,outputJson:N,outputText:A}),V="function"==typeof i?i:i.default;async function X(e){let t=e.text;if(e.imageBuffer)try{const n=await a.read(e.imageBuffer),r={data:new Uint8ClampedArray(n.bitmap.data),width:n.bitmap.width,height:n.bitmap.height},o=V(r.data,r.width,r.height);if(!o||!o.data)throw new Error("无法从图片中识别出二维码内容");t=o.data}catch(e){throw new Error(`解析原始二维码图片失败: ${e.message}`)}if(!t)throw new Error("没有提供二维码文本或图片解析失败");return new Promise(e=>{s.generate(t,{small:!0},t=>{process.stdout.write("\n"+t+"\n"),e()})})}const H={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(e){const t=e||process.env.ECS_ENV||"prod";return{request_url:H[t]||H.prod,client_env:t}}const ee="https://login.360.cn/",te="pcw_aidrive",ne=`${ee}?o=sso&m=getLoginQrcode&s=3&src=${te}&qrcodeType=miniprogram&userCenter=1`,re="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36";function oe(e){const t=e.trim();try{return JSON.parse(t)}catch{}const n=t.match(/^[^(]+\((.+)\)\s*;?\s*$/s);if(!n)throw new S(`无法解析 JSONP 响应: ${t.substring(0,100)}`,P.QR_PARSE_FAILED);try{return JSON.parse(n[1])}catch(e){throw new S(`JSONP 内容 JSON 解析失败: ${e.message}`,P.QR_PARSE_FAILED)}}function se(e){const t=e.headers.get("set-cookie");return t?t.split(/,(?=\s*\w+=)/):[]}function ie(e){return Object.entries(e).map(([e,t])=>`${e}=${t}`).join("; ")}function ae(e,t){for(const n of t){const t=n.match(/^([^=]+)=([^;]*)/);t&&(e[t[1].trim()]=t[2])}}function ce(e){return new Promise(t=>setTimeout(t,e))}function ue(e){const t=Date.now();return`${ee}?func=${e}&src=${te}&from=${te}&charset=UTF-8&requestScema=https&quc_sdk_version=7.3.7&quc_sdk_name=jssdk&mid=&asc=&mname=&o=sso&m=qrLogin&_=${t}`}async function le(){const e={},t=Math.random().toString().substring(2),n=`${ne}&t=0.${t}`;let r;try{r=await fetch(n,{headers:{"User-Agent":re,Referer:"https://i.360.cn/"}})}catch(e){throw new S(`获取二维码请求失败: ${e.message}`,P.QR_INIT_FAILED)}const o=function(e,t){for(const n of e){const e=n.match(new RegExp(`${t}=([^;]+)`));if(e)return e[1]}}(se(r),"i360QRKEY");if(!o)throw new S("未能从二维码响应中获取 i360QRKEY",P.QR_INIT_FAILED);e.i360QRKEY=o;const s=Buffer.from(await r.arrayBuffer());if(s.length<100)throw new S("二维码图片数据异常",P.QR_INIT_FAILED);const i=`data:image/png;base64,${s.toString("base64")}`;return{qrKey:o,qrImageBuffer:s,qrImageDataUrl:i,cookies:e}}async function fe(e,t){const{cookies:n}=e,r=Date.now()+1e3*t.timeoutSec,o=`jQuery${Date.now()}_${Date.now()}`;let s;for(;Date.now()<r;){await ce(t.intervalMs);const e=ue(o);let r;try{const t=await fetch(e,{headers:{"User-Agent":re,Referer:"https://i.360.cn/",Cookie:ie(n)}});ae(n,se(t)),r=await t.text()}catch(e){throw new S(`轮询请求失败: ${e.message}`,P.QR_POLL_FAILED)}const i=oe(r),a=String(i.errno);if("2"!==a){if("1020204"!==a){if("0"===a&&i.s){s=i.s;break}throw new S(`轮询返回异常 errno=${a}: ${i.errmsg||""}`,P.QR_POLL_FAILED)}process.stderr.write("已扫码,请在手机上确认登录...\n")}}if(!s)throw new S("扫码登录超时,请重试",P.QR_POLL_TIMEOUT);process.stderr.write("正在完成登录...\n");let i,a="";const c=async()=>{try{const e=await async function(e){const{q:t,t:n}={q:(r=e.cookies).Q||r.o,t:r.T||r.i};var r;if(!t||!n)return{apiKey:""};const{request_url:o,client_env:s}=Z(e.env),i=new URL(o);let a,c;i.searchParams.append("method","Oauth.getApiKeyByQT"),i.searchParams.append("client_env",s),i.searchParams.append("client_src","pcw_aidrive"),i.searchParams.append("sub_channel",e.subChannel);try{a=await fetch(i.toString(),{method:"GET",headers:{Accept:"application/json",q:t,t:n}})}catch(e){throw new Error(`换取 API Key 请求失败: ${e.message}`)}if(!a.ok)throw new Error(`换取 API Key 失败,HTTP ${a.status}`);try{c=await a.json()}catch{throw new Error("换取 API Key 失败:响应不是合法 JSON")}if(0!==c.errno)throw new Error(c.errmsg||`换取 API Key 失败 errno=${c.errno}`);return{apiKey:c.data?.api_key||c.data?.apiKey||"",extra:c.data}}({cookies:n,env:t.env,subChannel:t.subChannel});e.apiKey&&(a=e.apiKey)}catch(e){i=e?.message||String(e)}};await c();const u=function(e,t){const n=Date.now(),r=encodeURIComponent(t);return`${ee}?func=${e}&src=${te}&from=${te}&charset=UTF-8&requestScema=https&quc_sdk_version=7.3.7&quc_sdk_name=jssdk&mid=&asc=&mname=&o=sso&m=setcookie&s=${r}&_=${n}`}(o,s);try{const e=await fetch(u,{headers:{"User-Agent":re,Referer:"https://i.360.cn/",Cookie:ie(n)}});ae(n,se(e));const t=oe(await e.text());if("0"!==String(t.errno))throw new S(`setcookie 失败: ${t.errmsg||"未知错误"}`,P.QR_SETCOOKIE_FAILED)}catch(e){if(e instanceof S)throw e;throw new S(`setcookie 请求失败: ${e.message}`,P.QR_SETCOOKIE_FAILED)}if(a||await c(),!a)throw new S(i||"换取 API Key 失败:未从登录 Cookie 中取到有效的 Q/T,或 Oauth.getApiKeyByQT 未返回 api_key",P.QR_EXCHANGE_APIKEY_FAILED);const l=function(e){const t=Date.now();return`${ee}?callback=${e}&src=${te}&from=${te}&charset=UTF-8&requestScema=https&quc_sdk_version=7.3.7&quc_sdk_name=jssdk&mid=&asc=&mname=&o=sso&m=info&show_name_flag=1&head_type=b&relmobile_format=hide&_=${t}`}(o);let f;try{const e=await fetch(l,{headers:{"User-Agent":re,Referer:"https://i.360.cn/",Cookie:ie(n)}});f=oe(await e.text())}catch(e){throw new S(`获取用户信息失败: ${e.message}`,P.QR_INFO_FAILED)}if(!f.qid)throw new S("登录校验失败:未获取到用户 QID",P.QR_INFO_FAILED);return{qid:f.qid,username:f.username||f.userName,nickname:f.nickname,mobile:f.mobile,ssoCookies:n,apiKey:a}}function de(){const n=new e("auth").description("鉴权管理").enablePositionalOptions().passThroughOptions(),r=e=>e.option("--format <type>","输出格式 (json/text)","json").option("--quiet","是否静默输出",!1);return r(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 n=v();n.api_key=e.apiKey,n.login_mode="api_key",delete n.sso_login_at,delete n.sso_user,delete n.sso_cookies,e.env&&(n.ecs_env=e.env),e.subChannel&&(n.sub_channel=e.subChannel),b(n);const r=e;"text"===r.format?A(`登录成功!配置已保存到 ${y()}`):N({message:"登录成功",configPath:y()},"auth login",t,{quiet:r.quiet})}catch(e){C(e,"auth login",t)}}),r(n.command("login-wechat")).description("使用微信扫码登录").option("--timeout <sec>","轮询总超时秒数","120").option("--interval <ms>","轮询间隔毫秒","1500").option("--env <env>","环境 (prod/test)","prod").option("--sub-channel <channel>","子渠道","open").action(async e=>{const t=Date.now(),n=e,r=process.argv.includes("--format")&&"json"===n.format;try{if(!r&&!n.quiet){const r=await async function(e){const t=await le();{process.stderr.write("正在获取登录二维码...\n");const e=`https://login.360.cn/?o=sso&m=getLoginQrcode&s=3&src=${te}&qrcodeType=miniprogram&userCenter=1&qrkey=${t.qrKey}`;try{await X({text:e,imageBuffer:t.qrImageBuffer})}catch(t){try{await X({text:e})}catch(e){throw new S(`二维码渲染失败: ${e.message}`,P.QR_RENDER_FAILED)}}process.stderr.write("请使用微信扫描上方二维码登录\n")}return await fe(t,e)}({timeoutSec:parseInt(e.timeout,10),intervalMs:parseInt(e.interval,10),env:e.env,subChannel:e.subChannel}),o=v();o.api_key=r.apiKey,o.ecs_env=e.env,o.sub_channel=e.subChannel,o.login_mode="wechat_qr",o.sso_login_at=Date.now(),o.sso_user={qid:r.qid,username:r.username,nickname:r.nickname,mobile:r.mobile},o.sso_cookies=r.ssoCookies,b(o);const s={message:"微信扫码登录成功",login_mode:"wechat_qr",qid:r.qid,username:r.username,nickname:r.nickname,configPath:y()};return void("text"===n.format?A(`微信扫码登录成功\nQID: ${r.qid}\n昵称: ${r.nickname||"-"}\n配置已保存到 ${y()}`):N(s,"auth login-wechat",t,{quiet:n.quiet}))}const o=await le(),s={type:"qr",qr_key:o.qrKey,qr_image_data_url:o.qrImageDataUrl,expires_in:parseInt(e.timeout,10)};process.stdout.write(`${JSON.stringify(s)}\n`);const i=await fe(o,{timeoutSec:parseInt(e.timeout,10),intervalMs:parseInt(e.interval,10),env:e.env,subChannel:e.subChannel}),a=v();a.api_key=i.apiKey,a.ecs_env=e.env,a.sub_channel=e.subChannel,a.login_mode="wechat_qr",a.sso_login_at=Date.now(),a.sso_user={qid:i.qid,username:i.username,nickname:i.nickname,mobile:i.mobile},a.sso_cookies=i.ssoCookies,b(a);const c={success:!0,result:{message:"微信扫码登录成功",login_mode:"wechat_qr",qid:i.qid,username:i.username,nickname:i.nickname,configPath:y()},meta:{duration_ms:Date.now()-t,command:"auth login-wechat"}};process.stdout.write(`${JSON.stringify(c)}\n`)}catch(e){C(e,"auth login-wechat",t)}}),r(n.command("whoami")).description("查看当前鉴权状态").action(e=>{const t=Date.now();try{const n=v(),r=e,o={logged_in:!!n.api_key,login_mode:n.login_mode||(n.api_key?"api_key":void 0),api_key:n.api_key?n.api_key.substring(0,10)+"***":void 0,ecs_env:n.ecs_env||process.env.ECS_ENV||"prod",sub_channel:n.sub_channel||process.env.SUB_CHANNEL||"open",sso_user:n.sso_user,sso_login_at:n.sso_login_at,config_path:y()};if("text"===r.format)if(o.logged_in){const e=["已登录",`登录方式: ${o.login_mode||"-"}`,`API Key: ${o.api_key}`,`环境: ${o.ecs_env}`,`渠道: ${o.sub_channel}`];o.sso_user?.qid&&e.push(`QID: ${o.sso_user.qid}`),o.sso_user?.nickname&&e.push(`昵称: ${o.sso_user.nickname}`),A(e.join("\n"))}else A("未登录。请使用 auth login --api-key <API_KEY> 或 auth login-wechat 登录");else N(o,"auth whoami",t,{quiet:r.quiet})}catch(e){C(e,"auth whoami",t)}}),r(n.command("logout")).description("退出登录:清除本地配置").action(e=>{const n=Date.now();try{!function(){try{const e=y();t.existsSync(e)&&t.unlinkSync(e)}catch{}}();const r=e;"text"===r.format?A("已退出登录,本地配置已清除"):N({message:"已退出登录"},"auth logout",n,{quiet:r.quiet})}catch(e){C(e,"auth logout",n)}}),n}function he(e,t="e7b24b112a44fdd9ee93bdf998c6ca0e"){const n=Object.keys(e).sort().map(t=>{const n=function(e){return encodeURIComponent(e).replace(/%20/g,"+").replace(/[!'()*~]/g,e=>`%${e.charCodeAt(0).toString(16).toUpperCase()}`)}(e[t]);return`${t}=${n}`});let r=n.join("&");return r+=t,o=r,c.createHash("md5").update(o,"utf8").digest("hex");var o}const pe="production"===process.env.NODE_ENV,me="true"===process.env.LOG_TO_FILE,we=process.env.LOG_FILE_PATH||"/data/logs/ecs-mcp/app.log",_e=(process.env.LOG_TIME_FORMAT||"epoch").toLowerCase();function ye(){const e=new Date,t=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(e).reduce((e,t)=>("literal"!==t.type&&(e[t.type]=t.value),e),{}),n=String(e.getMilliseconds()).padStart(3,"0");return`,"time":"${`${t.year}-${t.month}-${t.day}T${t.hour}:${t.minute}:${t.second}.${n}+08:00`}"`}let ge;if(me)ge=u.destination({dest:we,minLength:512,sync:!1});else{const e=process.env.LOG_STDERR,t="string"==typeof e&&e.length>0,n=Array.isArray(process.argv)?process.argv.slice(2):[],r=n.includes("--stdio")||n.includes("--all"),o=t?"true"===e.toLowerCase():r;ge=u.destination({fd:o?2:1,minLength:256,sync:!0})}const $e=u({level:process.env.LOG_LEVEL||(pe?"info":"debug"),base:{service:process.env.APP_NAME||"ecs-mcp",version:process.env.APP_VERSION||"0.0.0",log_type:"app"},formatters:{level:(e,t)=>({level:e,level_number:t})},redact:{paths:["req.headers.authorization","authInfo.token","authInfo.access_token","req.body.params.arguments.access_token","req.body.params.arguments.token"],censor:"***"},timestamp:"iso"===_e?u.stdTimeFunctions.isoTime:"epoch"===_e?u.stdTimeFunctions.epochTime:"beijing"===_e||"cst"===_e||"asia/shanghai"===_e?ye:u.stdTimeFunctions.isoTime},ge);function ve(){try{ge?.flushSync?.()}catch{}try{ge?.end?.()}catch{}}async function be(e,t){const n=function(e){let t="",n="",r="",o="",s="";e&&(e.apiKey&&(t=e.apiKey),e.subChannel&&(n=e.subChannel),e.q&&(r=e.q),e.t&&(o=e.t),e.clientSrc&&(s=e.clientSrc)),t||(t=process.env.API_KEY||""),n||(n=process.env.SUB_CHANNEL||"open"),s||(s=process.env.CLIENT_SRC||"default");const{client_env:i}=Z(e?.ecsEnv);return{apiKey:t,clientEnv:i,clientSrc:s,subChannel:n,q:r,t:o}}(t);if(!n.apiKey&&!n.q&&!n.t)throw new Error("未配置API_KEY环境变量");try{const{request_url:r}=Z(t?.ecsEnv),o=n.subChannel,s=new URL(r);let i={};const a=e.extraParams;a&&"file-upload-stdio"===a.toolName?(i.method=a.method,i.client_env=a.clientEnv,i.client_src=a.clientSrc,i.qid=a.qid,i.grant_type=a.grantType,i.sub_channel=o):(i.method="Oauth.getAccessTokenByApiKeyOrQT",i.client_env=n.clientEnv,i.client_src=n.clientSrc,i.grant_type="authorization_code",i.sub_channel=o,n.apiKey&&(i.api_key=n.apiKey)),Object.entries(i).forEach(([e,t])=>{s.searchParams.append(e,t)});const c={Accept:"application/json"};n.apiKey&&(c.api_key=n.apiKey),n.q&&(c.q=n.q),n.t&&(c.t=n.t);const u=await fetch(s.toString(),{method:"GET",headers:c});if(!u.ok)throw new Error(`鉴权请求失败,状态码: ${u.status}`);const l=await u.json(),f=R(l);if(0!==l.errno)throw $e.debug({trace_id:f,errno:l.errno,msg:"oauth response"},"oauth response error"),new O(`鉴权请求返回错误: ${l.errmsg}`,{traceId:f,apiErrno:l.errno});$e.debug({trace_id:f,errno:0},"oauth response");const{access_token:d,qid:h,token:p}=l.data;return{access_token:d,qid:h,token:p,sub_channel:o}}catch(e){throw $e.error({err:e},"获取鉴权信息失败"),e}}async function Ee(e={},t){$e.debug({transportAuthInfo:t?{hasApiKey:!!t.apiKey,apiKey:t.apiKey||"",subChannel:t.subChannel,ecsEnv:t.ecsEnv,q:t.q||"",t:t.t||""}:void 0},"getAuthInfo");const n=await be(e,t);if(e.extraParams&&"file-upload-stdio"===e.extraParams.toolName)return n.qid=e.extraParams.qid,n;const r=he(function(e,t,n={}){const r={};if(e.access_token&&(r.access_token=String(e.access_token)),t&&(r.method=String(t)),e.qid&&(r.qid=String(e.qid)),n)for(const[e,t]of Object.entries(n))null!=t&&(r[String(e)]=String(t));return{...r}}({access_token:n.access_token,qid:n.qid},e.method||"",e.extraParams));return n.sign=r,n}process.on("uncaughtException",e=>{$e.error({err:e},"uncaught exception"),ve(),process.exit(1)}),process.on("unhandledRejection",e=>{$e.error({err:e},"unhandled rejection"),ve(),process.exit(1)}),process.on("SIGINT",()=>{ve(),process.exit(0)}),process.on("SIGTERM",()=>{ve(),process.exit(0)}),process.on("beforeExit",()=>{ve()});class ke{apiKey;ecsEnv;subChannel;timeout;retries;retryDelay;constructor(e){this.apiKey=e.apiKey,this.ecsEnv=e.ecsEnv||"prod",this.subChannel=e.subChannel||"open",this.timeout=e.timeout||3e4,this.retries=e.retries||0,this.retryDelay=e.retryDelay||1e3}getTransportAuthInfo(){return{apiKey:this.apiKey,ecsEnv:this.ecsEnv,subChannel:this.subChannel}}logApiResponse(e,t){if(!e)return;const n=R(t),r=t&&"object"==typeof t&&"errno"in t?t.errno:void 0;$e.debug({apiMethod:e,trace_id:n,errno:"number"==typeof r?r:void 0},"api response")}throwIfApiError(e,t="API请求失败"){if(!e||"object"!=typeof e)return;if("number"!=typeof e.errno||0===e.errno)return;const n=R(e);throw new O(e.errmsg||t,{traceId:n,apiErrno:e.errno})}async getAuth(e={}){const t=this.getTransportAuthInfo(),n=await Ee(e,t);return n.request_url=Z(this.ecsEnv).request_url,n}isRetryable(e){return"AbortError"===e.name||("ECONNRESET"===e.code||"ETIMEDOUT"===e.code||"ECONNREFUSED"===e.code||!!e.message?.includes("状态码: 5"))}async fetchWithRetry(e,t=0){const{url:n,init:r}=e(),o=new AbortController,s=setTimeout(()=>o.abort(),this.timeout),i={...r,signal:o.signal};try{const e=await fetch(n,i);if(clearTimeout(s),!e.ok)throw new Error(`API 请求失败,状态码: ${e.status}`);const t=await e.text();try{return JSON.parse(t)}catch{throw new Error(`无法解析API响应: ${t.substring(0,100)}...`)}}catch(n){if(clearTimeout(s),t<this.retries&&this.isRetryable(n)){const r=this.retryDelay*Math.pow(2,t);return $e.debug({attempt:t+1,delay:r,error:n.message},"请求失败,准备重试"),await new Promise(e=>setTimeout(e,r)),this.fetchWithRetry(e,t+1)}if("AbortError"===n.name)throw new Error(`请求超时 (${this.timeout}ms)`);throw n}}async apiGet(e,t){const n=t.method,r=await this.fetchWithRetry(()=>{const n=new URL(e.request_url||"");return Object.entries(t).forEach(([e,t])=>{n.searchParams.append(e,String(t))}),{url:n.toString(),init:{method:"GET",headers:{"Access-Token":e.access_token||""}}}});return this.logApiResponse(n,r),r}async apiPost(e,t,n={}){const r=t.method,o=await this.fetchWithRetry(()=>{const r=new URL(e.request_url||"");Object.entries(t).forEach(([e,t])=>{r.searchParams.append(e,String(t))});const o=new URLSearchParams;return Object.entries(n).forEach(([e,t])=>{o.append(e,String(t))}),{url:r.toString(),init:{method:"POST",headers:{"Access-Token":e.access_token||"","Content-Type":"application/x-www-form-urlencoded"},body:o}}});return this.logApiResponse(r,o),o}buildBaseParams(e,t){return{method:t,access_token:e.access_token||"",qid:e.qid||"",sign:e.sign||"",sub_channel:e.sub_channel}}get logger(){return $e}}class Pe extends ke{async info(){const e=await this.getAuth({}),t=this.buildBaseParams(e,"User.getUserDetail");return t.sign="",await this.apiGet(e,t)}}class Se extends ke{async list(e={}){const{path:t="/",page:n=0,page_size:r=50}=e;let o=t||"/";"/"!==o&&(o.startsWith("/")||(o="/"+o),o.endsWith("/")||(o+="/"));const s={path:o,page:n,page_size:r},i=await this.getAuth({method:"File.getList",extraParams:s}),a={...this.buildBaseParams(i,"File.getList")};for(const[e,t]of Object.entries(s))"access_token"!==e&&(a[e]=String(t));return await this.apiGet(i,a)}async mkdir(e){const t={fname:e},n=await this.getAuth({method:"File.mkdir",extraParams:t}),r=this.buildBaseParams(n,"File.mkdir");return await this.apiPost(n,{},{...r,fname:e})}}const Ie="未配置 API Key,请先执行 auth login --api-key <API_KEY> 或设置 API_KEY 环境变量";function De(e){let t=String(e||"").trim();return t=t.replace(/^\uFEFF/,""),t.startsWith("-e ")&&(t=t.slice(3).trim(),(t.startsWith('"')&&t.endsWith('"')||t.startsWith("'")&&t.endsWith("'"))&&(t=t.slice(1,-1))),t.replace(/\\r\\n/g,"\r\n").replace(/\\n/g,"\n")}async function Oe(){if(process.stdin.isTTY){if("win32"===process.platform)throw new Error("--stdin 模式需要管道输入。Windows cmd 勿用 echo -e(会把 -e 写入内容且编码易乱码),请用 --content、或 chcp 65001 后 echo、或 type/PowerShell Get-Content 读 UTF-8 文件管道;追加/保存示例:type x.md | 360disk file save --stdin ...");throw new Error('--stdin 模式需要管道输入,例如: echo "content" | 360disk file save --stdin')}const e=[];for await(const t of process.stdin)e.push(Buffer.from(t));return function(e){if(0===e.length)return"";const t=e=>e.replace(/^\uFEFF/,"");if("win32"!==process.platform)return t(e.toString("utf-8"));const n=t(e.toString("utf-8"));if(!/\uFFFD/.test(n))return n;try{const n=t(l.decode(e,"gbk"));if(!/\uFFFD/.test(n))return n}catch{}return n}(Buffer.concat(e))}async function Re(){const e=await Oe();return De(e).split(/\r?\n/).map(e=>e.trim()).filter(Boolean)}class Ne extends ke{async move(e){const{src_name:t,new_name:n}=e,r={src_name:t,new_name:n},o=await this.getAuth({method:"File.move",extraParams:r}),s=this.buildBaseParams(o,"File.move");return await this.apiPost(o,{},{...s,src_name:t,new_name:n})}async rename(e){const{src_name:t,new_name:n}=e,r={src_name:t,new_name:n},o=await this.getAuth({method:"File.rename",extraParams:r}),s=this.buildBaseParams(o,"File.rename");return await this.apiPost(o,{},{...s,src_name:t,new_name:n})}async delete(e){const t=await this.getAuth({method:"File.delete"}),n=this.buildBaseParams(t,"File.delete");return await this.apiPost(t,{},{...n,fname:e})}async transOrCopy(e){const{src_name:t,new_path:n,is_delete:r,is_replace:o=0,src_ks_id:s,new_ks_id:i}=e;if(0!==r&&1!==r)throw new Error("is_delete 仅支持 0 或 1");if(0!==o&&1!==o)throw new Error("is_replace 仅支持 0 或 1");const a={src_name:t,new_path:n,is_delete:r},c=await this.getAuth({method:"File.transOrCopy",extraParams:a}),u={...this.buildBaseParams(c,"File.transOrCopy"),src_name:t,new_path:n,is_delete:String(r),is_replace:String(o)};return s&&(u.src_ks_id=s),i&&(u.new_ks_id=i),await this.apiPost(c,{},u)}async countOriginSize(e){const{path:t}=e;if(!t||!t.startsWith("/"))throw new Error("path 必须以 / 开头");const n={path:t},r=await this.getAuth({method:"File.countFileOriginSize",extraParams:n}),o=this.buildBaseParams(r,"File.countFileOriginSize");return await this.apiPost(r,{},{...o,path:t})}async clearDir(e){const{fname:t}=e;if(!t)throw new Error("fname 不能为空");if(t.includes("|"))throw new Error("File.clearDir 仅支持单个目录路径,勿用英文竖线(|)拼接;竖线会被服务端视为路径非法字符(errno:3022)。请对每个目录分别调用");const n=await this.getAuth({method:"File.clearDir"}),r=this.buildBaseParams(n,"File.clearDir");return await this.apiPost(n,{},{...r,fname:t})}async config(e){const{path:t,command:n,type:r,key:o,value:s,content:i}=e;if(!t||!t.startsWith("/"))throw new Error("path 必须以 / 开头");const a="yml"===r?"yaml":r,c={path:t,command:n,type:a},u=await this.getAuth({method:"MCP.config",extraParams:c}),l={...this.buildBaseParams(u,"MCP.config"),path:t,command:n,type:a};return void 0!==o&&(l.key=o),void 0!==s&&(l.value=s),void 0!==i&&(l.content=i),await this.apiPost(u,{},l)}}const Te=["http:","https:"],Ae=/[\\@]/,Ce=[/^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],Le=[22,23,25,53,135,139,445,1433,1521,3306,3389,5432,6379,9200,11211,27017];async function Fe(e,t=5e3){const n=function(e){if(!e||"string"!=typeof e)return{isValid:!1,error:"URL不能为空"};try{const n=e.trim();if(Ae.test(n))return{isValid:!1,error:"URL包含不安全的字符"};const r=new URL(n);if(!Te.includes(r.protocol))return{isValid:!1,error:"不支持的协议,只支持 http/https"};const o=r.hostname.toLowerCase();if(!o)return{isValid:!1,error:"主机名不能为空"};for(const e of Ce)if(e.test(o))return{isValid:!1,error:"禁止访问受限地址"};if(xe(o)&&!Me(o))return{isValid:!1,error:"禁止访问受限IP地址"};if(r.port){const e=parseInt(r.port,10);if(Le.includes(e))return{isValid:!1,error:"禁止访问受限端口"}}return t=r.pathname,[/\.\./,/%2e%2e/i,/%252e%252e/i,/\/etc\//i,/\/proc\//i,/\/sys\//i,/\/dev\//i].some(e=>e.test(t))?{isValid:!1,error:"路径包含不安全的内容"}:{isValid:!0,normalizedUrl:r.toString(),originalHostname:o}}catch(e){return{isValid:!1,error:"URL格式无效"}}var t}(e);if(!n.isValid)return n;try{const r=new URL(e).hostname.toLowerCase();if(xe(r))return{...n,resolvedIP:r,originalHostname:r,needsHostHeader:!1};let o=[];try{const e=f.lookup(r,{all:!0}),n=new Promise((e,n)=>{setTimeout(()=>n(new Error("DNS解析超时")),t)});o=(await Promise.race([e,n])).map(e=>e.address)}catch(e){return{isValid:!1,error:"域名解析失败"}}for(const e of o){for(const t of Ce)if(t.test(e))return{isValid:!1,error:"域名解析到受限IP地址"};if(!Me(e))return{isValid:!1,error:"域名解析到受限IP地址"}}return{isValid:!0,normalizedUrl:n.normalizedUrl,resolvedIP:o[0],resolvedIPs:o,originalHostname:r,needsHostHeader:!1}}catch(e){return{isValid:!1,error:"URL验证过程中发生错误"}}}function xe(e){if(/^(\d{1,3}\.){3}\d{1,3}$/.test(e)){return e.split(".").every(e=>{const t=parseInt(e,10);return t>=0&&t<=255})}return/^([0-9a-f]{0,4}:){2,7}[0-9a-f]{0,4}$/i.test(e)}function Me(e){for(const t of Ce)if(t.test(e))return!1;return!0}class ze extends ke{normalizeUploadResult(e){return{id:e?.id,name:e?.name||e?.uploadRes?.name,path:e?.uploadRes?.name,fid:e?.fid,size:e?.size||e?.uploadRes?.count_size,uploadRes:e?.uploadRes?{nid:e.uploadRes.nid,qid:e.uploadRes.qid,name:e.uploadRes.name,count_size:e.uploadRes.count_size,file_hash:e.uploadRes.file_hash,create_time:e.uploadRes.create_time,modify_time:e.uploadRes.modify_time}:void 0}}normalizeRemoteDir(e){let t=String(e||"").trim();if(!t)throw new Error("远端目录不能为空");return t.startsWith("/")||(t="/"+t),t.endsWith("/")||(t+="/"),t}toPosixRelativePath(e){return e.split(n.sep).join(n.posix.sep)}getMissingUploadPath(e){const t=e?.message||"",r="文件不存在: ";if(t.startsWith(r))return n.resolve(t.slice(7).trim())}async getSysConfig(){const e=await this.getAuth({method:"Oauth.getSysConfig"}),t=this.buildBaseParams(e,"Oauth.getSysConfig"),n=await this.apiGet(e,t);return this.throwIfApiError(n,"获取系统云控配置失败"),n}async search(e){const{key:t="",file_category:n=-1,page:r=1,page_size:o=20}=e;if(!t&&-1===n)throw new Error("必须提供搜索关键词(key)或指定文件类型(file_category)");const s={file_category:n,key:t,page:r,page_size:o},i=await this.getAuth({method:"File.searchList",extraParams:s}),a=this.buildBaseParams(i,"File.searchList"),c={};for(const[e,t]of Object.entries(s))"access_token"!==e&&(c[e]=String(t));return await this.apiPost(i,a,c)}async share(e){const t=await this.getAuth({method:"Share.preShare"}),n=this.buildBaseParams(t,"Share.preShare");return await this.apiPost(t,{},{...n,paths:e})}async getDownloadUrl(e){const{nid:t,fpath:n}=e;if(void 0===t&&!n)throw new Error("必须提供nid或fpath中的一个参数");const r={};void 0!==t?r.nid=t:n&&(r.fpath=n);const o=await this.getAuth({method:"MCP.getDownLoadUrl",extraParams:r}),s={...this.buildBaseParams(o,"MCP.getDownLoadUrl")};return void 0!==t?s.nid=String(t):n&&(s.fpath=n),await this.apiPost(o,{},s)}async getNodeInfoByNid(e){const{nid:t,ks_ext:n=0}=e;if(!t)throw new Error("nid 不能为空");if(0!==n&&1!==n)throw new Error("ks_ext 仅支持 0 或 1");const r={nid:t},o=await this.getAuth({method:"File.getNodeInfoByNid",extraParams:r}),s=this.buildBaseParams(o,"File.getNodeInfoByNid");return await this.apiGet(o,{...s,nid:t,ks_ext:String(n)})}async save(e){const{url:t,content:n,upload_path:r,file_name:o,is_rename:s=1}=e;if(!t&&!n||t&&n)throw new Error("url 与 content 互斥,必须且只能传一个");if(0!==s&&1!==s)throw new Error("is_rename 仅支持 0 或 1");if(t){if(t.includes("|"))throw new Error("MCP.saveFile 仅支持单个下载 URL,请勿使用英文竖线(|)连接多个地址;请对每个 URL 分别执行保存");const e=await Fe(t.trim(),5e3);if(!e.isValid)throw new Error(`URL安全验证失败: ${e.error}`)}const i=await this.getAuth({method:"MCP.saveFile"}),a=this.buildBaseParams(i,"MCP.saveFile"),c={};r&&(c.upload_path=r),t?c.url=t:n&&(c.content=n),o&&(c.file_name=o),c.is_rename=String(s);const u=await this.apiPost(i,a,c);if(u&&0===u.errno){const e=Array.isArray(u.data)?u.data:u.data?[u.data]:[];if(0===e.length)throw new Error("保存文件失败: 未获取到任何任务信息");const t=[],n=[];for(const r of e){const e=r.task_id;if(e)try{t.push(await this.pollTaskStatus(i,e))}catch(t){n.push(`任务 ${e} 失败: ${t.message}`)}else n.push(`URL ${r.url||"未知"} 未获取到任务ID`)}if(0===t.length&&n.length>0)throw new Error(`文件保存失败: ${n.join("; ")}`);return 1===t.length?t[0]:{errno:0,errmsg:n.length>0?`部分任务失败: ${n.join("; ")}`:"",data:{tasks:t,is_multi:!0}}}throw new O(u?.errmsg||"API请求失败",{traceId:R(u),apiErrno:"number"==typeof u?.errno?u.errno:void 0})}async appendContent(e){const{path:t,content:n}=e;if(!t||!t.startsWith("/"))throw new Error("path 必须以 / 开头");if("string"!=typeof n||0===n.length)throw new Error("content 不能为空");const r={path:t,content:n},o=await this.getAuth({method:"File.appendContent",extraParams:r}),s=this.buildBaseParams(o,"File.appendContent");return await this.apiPost(o,{},{...s,path:t,content:n})}async detectFileExists(e){const{path:t,files:n}=e;if(!t||!t.startsWith("/"))throw new Error("path 必须以 / 开头");if(!Array.isArray(n)||0===n.length)throw new Error("files 不能为空");for(const e of n){if(!e?.fname||"string"!=typeof e.fname)throw new Error("files[].fname 必须为非空字符串");if(!Number.isFinite(e.fsize)||e.fsize<0)throw new Error("files[].fsize 必须为大于等于 0 的数字")}const r=JSON.stringify(n),o={path:t,data:r},s=await this.getAuth({method:"Sync.detectFileExists",extraParams:o}),i=this.buildBaseParams(s,"Sync.detectFileExists");return await this.apiGet(s,{...i,path:t,data:r})}async queryTaskStatus(e,t){const n={method:"MCP.query",qid:e.qid||"",access_token:e.access_token||"",sign:e.sign||""};return await this.apiPost(e,n,{task_id:t})}async pollTaskStatus(e,t,n=1e3,r=120){let o,s=0;for(;s<r;){if(s++,o=await this.queryTaskStatus(e,t),0!==o.errno)throw new O(o.errmsg||"查询任务状态失败",{traceId:R(o),apiErrno:o.errno});const r=o.data?.status;if(2===r)return o;if(3===r)throw new O(o.data?.error||"文件保存失败",{traceId:R(o),apiErrno:"number"==typeof o.errno?o.errno:void 0});await new Promise(e=>setTimeout(e,n))}return o}async upload(e){const{filePaths:n,uploadPath:r="/",is_rename:o}=e,s=void 0!==o?String(o):void 0;if(!n||0===n.length)throw new Error("filePaths 为必填参数且不能为空");if(void 0!==o&&0!==o&&1!==o)throw new Error("is_rename 仅支持 0 或 1");for(const e of n)if(!t.existsSync(e))throw new Error(`文件不存在: ${e}`);const i=await this.getAuth({});let a;try{const e=await import("@aicloud360/sec-sdk-node");a=e.UploadNode}catch{throw new Error("请先安装 @aicloud360/sec-sdk-node: npm install @aicloud360/sec-sdk-node")}const c={qid:i.qid||"0",token:i.token||"",access_token:i.access_token||"",env:this.ecsEnv,path:r,is_rename:s,preventAutoTokenRefresh:!1,retryForTokenError:!0,tokenRefreshCallback:async()=>{this.logger.info("SDK 触发 token 刷新");const e=await this.getAuth({});return{token:e.token,access_token:e.access_token}}};return new Promise((e,t)=>{const o=Date.now(),i=[],u=[],l=[],f=new a(c,{success:e=>{i.push(this.normalizeUploadResult(e))},progress:()=>{},error:(e,t)=>{const n=t?.errmsg||t?.message||JSON.stringify(t);u.push({fileName:e?.name||"未知文件",error:n})},duplicateList:e=>{l.push(...e.map(e=>({name:e.name,nid:e.nid,size:e.count_size,path:e.path||r})))},complete:()=>{if(u.length>0&&0===i.length){const e=new Error(`所有文件上传失败: ${u.map(e=>`${e.fileName}: ${e.error}`).join("; ")}`);return e.uploadErrors=u,e.primaryUploadError=u[0]?.error,void t(e)}e({uploadResults:i,uploadErrors:u,duplicateFiles:l,totalTime:((Date.now()-o)/1e3).toFixed(2),fileCount:i.length,totalFileCount:n.length})}});void 0!==s&&f.on("filequeue",(e,t)=>{t&&"object"==typeof t&&(t.is_rename=s,t.file&&"object"==typeof t.file&&(t.file.is_rename=s))}),f.addWaitFile(n)})}async uploadTree(e){const{rootDir:r,filePaths:o,uploadRoot:s,is_rename:i}=e,a=Date.now();if(!r)throw new Error("rootDir 为必填参数");if(!o||0===o.length)throw new Error("filePaths 为必填参数且不能为空");const c=n.resolve(r),u=this.normalizeRemoteDir(s),l=new Map;for(const e of o){const t=n.resolve(e),r=this.toPosixRelativePath(n.relative(c,t));if(!r||".."===r||r.startsWith("../")||n.posix.isAbsolute(r))throw new Error(`文件不在 rootDir 下: ${t}`);const o=n.posix.dirname(r),s="."===o?u:this.normalizeRemoteDir(n.posix.join(u,o)),i=l.get(s)||[];i.push({absolutePath:t,relativePath:r}),l.set(s,i)}const f=[],d=[],h=[];for(const[e,r]of l){let o=[...r];for(;o.length>0;){const r=[];for(const n of o)t.existsSync(n.absolutePath)?r.push(n):d.push({absolutePath:n.absolutePath,relativePath:n.relativePath,remoteDir:e,error:`文件不存在: ${n.absolutePath}`});if(o=r,0===o.length)break;let s;try{s=await this.upload({filePaths:o.map(e=>e.absolutePath),uploadPath:e,is_rename:i})}catch(t){const r=this.getMissingUploadPath(t);if(r){const n=o.findIndex(e=>e.absolutePath===r);if(n>=0){const[r]=o.splice(n,1);d.push({absolutePath:r.absolutePath,relativePath:r.relativePath,remoteDir:e,error:t.message});continue}}const s=t;if(Array.isArray(s.uploadErrors)&&s.uploadErrors.length>0)for(const t of s.uploadErrors){const r=o.find(e=>n.basename(e.absolutePath)===t.fileName);d.push({absolutePath:r?.absolutePath||t.fileName,relativePath:r?.relativePath||t.fileName,remoteDir:e,error:t.error})}else for(const n of o)d.push({absolutePath:n.absolutePath,relativePath:n.relativePath,remoteDir:e,error:t?.message||"上传失败"});break}h.push(...s.duplicateFiles);const a=[...o];for(const t of s.uploadResults){const r=n.basename(String(t?.name||t?.uploadRes?.name||"")),o=a.findIndex(e=>n.basename(e.absolutePath)===r),s=o>=0?a.splice(o,1)[0]:a.shift();s&&f.push({absolutePath:s.absolutePath,relativePath:s.relativePath,remoteDir:e,uploadResult:t})}for(const t of s.uploadErrors){const r=o.find(e=>n.basename(e.absolutePath)===t.fileName);d.push({absolutePath:r?.absolutePath||t.fileName,relativePath:r?.relativePath||t.fileName,remoteDir:e,error:t.error})}break}}return{uploadResults:f,uploadErrors:d,duplicateFiles:h,totalTime:((Date.now()-a)/1e3).toFixed(2),fileCount:f.length,totalFileCount:o.length}}async download(e){const{nid:t,auto:r=!0,downloadDir:s,forceForeground:i=!1}=e,a=s||n.join(o(),".mcp-downloads"),c=await this.getDownloadUrl({nid:String(t)});if(!c)throw new O("API请求失败");this.throwIfApiError(c);const u=c.data?.downloadUrl||"";if(!u)throw new O("未能获取到文件下载链接",{traceId:R(c)});const l=n.basename(String(c.data?.fname||"downloaded_file")),f=Number(c.data?.size||0),d=f>0?f/1048576:0,h=d>0?`${d.toFixed(2)} MB`:"未知大小";if(!r)return{downloadUrl:u,filename:l,fileSize:h,downloadPath:""};await p.mkdir(a,{recursive:!0});const m=n.join(a,l);return d>10&&!i?(this.spawnBackgroundDownload(u,m),{downloadUrl:u,filename:l,fileSize:h,downloadPath:m,background:!0}):(await this.downloadWithCurl(u,m),{downloadUrl:u,filename:l,fileSize:h,downloadPath:m})}extractFileInfoFromUrl(e){try{const t=new URL(e);let r="downloaded_file";const o=t.searchParams.get("fname");if(o?r=n.basename(decodeURIComponent(o)):t.pathname&&t.pathname.length>1&&(r=n.basename(t.pathname)),r=r.replace(/[\x00/]/g,"_"),r&&"."!==r||(r="downloaded_file"),r.length>200){const e=n.extname(r);r=n.basename(r,e).substring(0,200-e.length)+e}const s=t.searchParams.get("fsize");return{filename:r,sizeMB:s?parseInt(s,10)/1048576:0}}catch{return{filename:"downloaded_file",sizeMB:0}}}downloadWithCurl(e,t,n=3e5){return new Promise((r,o)=>{const s=d("curl",["-L","-s","-A","yunpan_mcp_server",e,"-o",t]),i=setTimeout(()=>{s.kill(),o(new Error("下载超时"))},n);s.on("close",e=>{clearTimeout(i),0===e?r():o(new Error(`下载失败,curl 退出码: ${e}`))}),s.on("error",e=>{clearTimeout(i),o(e)})})}spawnBackgroundDownload(e,t){d("curl",["-L","-s","-A","yunpan_mcp_server",e,"-o",t],{detached:!0,stdio:"ignore"}).unref()}}const Ue="未配置 API Key,请先执行 auth login --api-key <API_KEY> 或设置 API_KEY 环境变量";function qe(e,t,n,r,o){"text"===r.format&&o?A(o(e)):N(e,t,n,{quiet:r.quiet})}async function Ke(e,t){const n=await e.delete(t);if(3008===n?.errno&&!t.endsWith("/")){const n=await e.delete(t+"/");if(0===n?.errno)return n}return n}function je(e){return"string"!=typeof e?e:e.replace(/\\r\\n/g,"\r\n").replace(/\\n/g,"\n")}function Be(e){let t;for(const n of function(e){const t=[],n=new Set,r=e=>{const r=String(e).trim();r&&!n.has(r)&&(n.add(r),t.push(r))},o=String(e??"").trim().replace(/^\uFEFF/,"");if(r(o),o.length>=2&&o.startsWith("'")&&o.endsWith("'")){const e=o.slice(1,-1).trim();r(e),r(e.replace(/\\"/g,'"'))}return r(o.replace(/\\"/g,'"')),t}(e))try{t=JSON.parse(n);break}catch{}if(void 0===t)throw new S('files 必须是合法 JSON(示例: [{"fname":"a.txt","fsize":123}])。Windows cmd 不支持单引号包裹参数,请用双引号并对内部 " 转义,或改用 --stdin。',P.INVALID_ARGS);if("string"==typeof t)try{t=JSON.parse(t)}catch{throw new S('files 必须是合法 JSON(示例: [{"fname":"a.txt","fsize":123}])',P.INVALID_ARGS)}if(!Array.isArray(t)||0===t.length)throw new S("files 必须是非空数组",P.INVALID_ARGS);for(const e of t){if(!e||"string"!=typeof e.fname||""===e.fname.trim())throw new S("files[].fname 必须为非空字符串",P.INVALID_ARGS);if(!Number.isFinite(e.fsize)||e.fsize<0)throw new S("files[].fsize 必须为大于等于 0 的数字",P.INVALID_ARGS)}return t}function We(){const t=new e("file").description("文件操作");return t.command("mv").description("移动文件或文件夹").argument("<src>","源路径,多个用 | 分隔").argument("<dest>","目标文件夹路径").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=k(o);if(!t.apiKey)return void C(new S(Ue,P.AUTH_ERROR),"file mv",r);const s=new Ne(t),i=await async function(e,t,n){const r=await e.move({src_name:t,new_name:n}),o=!t.includes("|");if(3008===r?.errno&&o&&!t.endsWith("/")){const r=await e.move({src_name:t+"/",new_name:n});if(0===r?.errno)return r}return r}(s,e,n);L(i,"file mv",r),qe(i,"file mv",r,o,t=>Y(t,`已移动: ${e} -> ${n}`))}catch(e){C(e,"file mv",r)}}),t.command("trans-copy").description("转移或复制文件到目标目录").argument("<src>","源路径,多个用 | 分隔").argument("<dest>","目标目录路径").option("--delete <0|1>","是否删除源文件:1=转移,0=复制","1").option("--replace <0|1>","同名文件处理:0=重命名,1=覆盖","0").option("--src-ks-id <id>","源文件所在群组 ks_id(可选)").option("--new-ks-id <id>","目标目录所在群组 ks_id(可选)").action(async(e,n,r)=>{const o=Date.now(),s=t.parent?.opts()||{};try{const t=k(s);if(!t.apiKey)return void C(new S(Ue,P.AUTH_ERROR),"file trans-copy",o);const i=parseInt(r.delete,10),a=parseInt(r.replace,10);if(![0,1].includes(i))return void C(new S("--delete 仅支持 0 或 1",P.INVALID_ARGS),"file trans-copy",o);if(![0,1].includes(a))return void C(new S("--replace 仅支持 0 或 1",P.INVALID_ARGS),"file trans-copy",o);if(!n.startsWith("/"))return void C(new S("dest 必须以 / 开头",P.INVALID_ARGS),"file trans-copy",o);const c=new Ne(t),u=await c.transOrCopy({src_name:e,new_path:n,is_delete:i,is_replace:a,src_ks_id:r.srcKsId,new_ks_id:r.newKsId});L(u,"file trans-copy",o),qe(u,"file trans-copy",o,s,t=>Y(t,`${1===i?"转移":"复制"}完成: ${e} -> ${n}`))}catch(e){C(e,"file trans-copy",o)}}),t.command("rename").description("重命名文件或文件夹").argument("<path>","原文件完整路径").argument("<new_name>","新名称").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=k(o);if(!t.apiKey)return void C(new S(Ue,P.AUTH_ERROR),"file rename",r);const s=function(e,t){const n=String(e||"").trim();let r=String(t||"").trim();const o=n.endsWith("/");if(!n.startsWith("/"))throw new S("path 必须以 / 开头",P.INVALID_ARGS);if(!r)throw new S("new_name 不能为空",P.INVALID_ARGS);if(o){r=r.replace(/^\/+/,"");const e=r.replace(/\/+$/,"").split("/").filter(Boolean);if(0===e.length)throw new S("new_name 不能为空(目录场景需传新目录名,如 bbb_renamed/)",P.INVALID_ARGS);return r=e[e.length-1]+"/",{src_name:n,new_name:r,isDir:!0}}if(r.includes("/"))throw new S("new_name 仅支持文件名(不含路径)。若要改路径请使用 file mv",P.INVALID_ARGS);return{src_name:n,new_name:r,isDir:!1}}(e,n),i=new Ne(t),a=await i.rename({src_name:s.src_name,new_name:s.new_name});L(a,"file rename",r),qe(a,"file rename",r,o,e=>Y(e,`已重命名: ${s.src_name} -> ${s.new_name}`))}catch(e){C(e,"file rename",r)}}),t.command("rm").description("删除文件或文件夹").argument("<path>","文件路径,多个用 | 分隔").option("--batch","批量模式:从 stdin 读取路径列表(每行一个)").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=k(o);if(!t.apiKey)return void C(new S(Ue,P.AUTH_ERROR),"file rm",r);const s=new Ne(t);if(n.batch){const t=await Re(),n=[e,...t].filter(Boolean),{executeBatch:i,formatBatchResult:a}=await Promise.resolve().then(function(){return G});qe(await i(n,async e=>{const t=await Ke(s,e);if(t&&"number"==typeof t.errno&&0!==t.errno){throw new O(t.errmsg?`${t.errmsg} (errno: ${t.errno})`:`API 错误 (errno: ${t.errno})`,{traceId:R(t),apiErrno:t.errno})}return t}),"file rm --batch",r,o,e=>a(e))}else{const t=await Ke(s,e);L(t,"file rm",r),qe(t,"file rm",r,o,t=>Y(t,`已删除: ${e}`))}}catch(e){C(e,"file rm",r)}}),t.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,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=k(o);if(!t.apiKey)return void C(new S(Ue,P.AUTH_ERROR),"file search",r);const s=new ze(t),i=await s.search({key:e,file_category:parseInt(n.type),page:parseInt(n.page),page_size:parseInt(n.size)});L(i,"file search",r),qe(i,"file search",r,o,B)}catch(e){C(e,"file search",r)}}),t.command("share").description("生成文件分享链接").argument("<paths>","文件路径,多个用 | 分隔").action(async e=>{const n=Date.now(),r=t.parent?.opts()||{};try{const t=k(r);if(!t.apiKey)return void C(new S(Ue,P.AUTH_ERROR),"file share",n);const o=new ze(t),s=await o.share(e);L(s,"file share",n),qe(s,"file share",n,r,e=>Y(e,"分享链接已生成"))}catch(e){C(e,"file share",n)}}),t.command("url").description("获取文件下载链接").argument("<path>","文件路径").option("--nid <nid>","文件 NID(与路径二选一)").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=k(o);if(!t.apiKey)return void C(new S(Ue,P.AUTH_ERROR),"file url",r);const s=new ze(t),i=await s.getDownloadUrl({nid:n.nid,fpath:n.nid?void 0:e});L(i,"file url",r),qe(i,"file url",r,o,e=>Y(e,"下载链接已获取"))}catch(e){C(e,"file url",r)}}),t.command("node-info").description("根据 nid 获取节点信息(可选返回 ks_info)").argument("<nid>","节点 nid(仅支持文件夹/知识库)").option("--ks-ext <0|1>","是否返回 ks_info:0=不返回,1=返回","0").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=k(o);if(!t.apiKey)return void C(new S(Ue,P.AUTH_ERROR),"file node-info",r);const s=parseInt(n.ksExt,10);if(![0,1].includes(s))return void C(new S("--ks-ext 仅支持 0 或 1",P.INVALID_ARGS),"file node-info",r);const i=new ze(t),a=await i.getNodeInfoByNid({nid:e,ks_ext:s});L(a,"file node-info",r),qe(a,"file node-info",r,o,t=>Y(t,`节点信息获取成功: ${e}`))}catch(e){C(e,"file node-info",r)}}),t.command("origin-size").description("统计目录下文件和文件夹(递归)原始大小").argument("<path>","目录完整路径").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=k(o);if(!t.apiKey)return void C(new S(Ue,P.AUTH_ERROR),"file origin-size",r);if(!e.startsWith("/"))return void C(new S("path 必须以 / 开头",P.INVALID_ARGS),"file origin-size",r);const n=new Ne(t),s=await n.countOriginSize({path:e});L(s,"file origin-size",r),qe(s,"file origin-size",r,o,t=>Y(t,`目录大小统计完成: ${e}`))}catch(e){C(e,"file origin-size",r)}}),t.command("clear-dir").description("清空目录下文件,保留目录本身(每次仅一个目录)").argument("<path>","单个目录路径,必须以 / 开头").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=k(o);if(!t.apiKey)return void C(new S(Ue,P.AUTH_ERROR),"file clear-dir",r);if(!e.startsWith("/"))return void C(new S("目录 path 必须以 / 开头",P.INVALID_ARGS),"file clear-dir",r);if(e.includes("|"))return void C(new S("每次仅支持清空一个目录,不支持用英文竖线(|)连接多个路径(会触发服务端非法字符错误);请分多次执行 file clear-dir",P.INVALID_ARGS),"file clear-dir",r);const n=new Ne(t),s=await n.clearDir({fname:e});L(s,"file clear-dir",r),qe(s,"file clear-dir",r,o,t=>Y(t,`目录清空完成: ${e}`))}catch(e){C(e,"file clear-dir",r)}}),t.command("config").description("读取/写入/列出配置文件(INI/JSON/YAML)").requiredOption("--path <path>","配置文件路径,必须以 / 开头").requiredOption("--command <cmd>","操作命令:config:get|config:set|config:delete|config:list|config:read|config:write").requiredOption("--type <type>","配置类型:ini|json|yaml|yml").option("--key <key>","键路径,如 a.b.c").option("--value <value>","写入值(set 场景)").option("--content <text>","写入完整内容(write 场景,与 --stdin 互斥)").option("--stdin","从标准输入读取 content(write 场景,与 --content 互斥)").action(async e=>{const n=Date.now(),r=t.parent?.opts()||{};try{const t=k(r);if(!t.apiKey)return void C(new S(Ue,P.AUTH_ERROR),"file config",n);if(!e.path.startsWith("/"))return void C(new S("--path 必须以 / 开头",P.INVALID_ARGS),"file config",n);if(!new Set(["config:get","config:set","config:delete","config:list","config:read","config:write"]).has(e.command))return void C(new S("--command 非法",P.INVALID_ARGS),"file config",n);if(!new Set(["ini","json","yaml","yml"]).has(e.type))return void C(new S("--type 非法",P.INVALID_ARGS),"file config",n);if("config:write"===e.command){if(1!==[e.content,e.stdin].filter(Boolean).length)return void C(new S("config:write 场景下 --content 与 --stdin 互斥,必须且只能传一个",P.INVALID_ARGS),"file config",n)}else if(e.content||e.stdin)return void C(new S("--content/--stdin 仅在 config:write 场景使用",P.INVALID_ARGS),"file config",n);let o=je(e.content);if(e.stdin&&(o=await Oe(),!o.trim()))return void C(new S("stdin 输入内容为空",P.INVALID_ARGS),"file config",n);const s=new Ne(t),i=await s.config({path:e.path,command:e.command,type:e.type,key:e.key,value:e.value,content:o});L(i,"file config",n),qe(i,"file config",n,r,t=>W(t,e.command,`配置操作完成: ${e.command}`))}catch(e){C(e,"file config",n)}}),t.command("save").description("通过 URL 或文本内容保存文件到云盘").option("--url <url>","单个文件下载地址(与 --content/--stdin 互斥;勿用 | 连接多个 URL)").option("--content <text>","文件内容(与 --url/--stdin 互斥)").option("--stdin","从标准输入读取内容(与 --url/--content 互斥)").option("--dest <path>","云盘存储路径,必须以 / 开头和结尾").option("--filename <name>","保存的文件名").option("--rename <0|1>","同名文件处理策略:0=直接替换,1=自动重命名","1").action(async e=>{const n=Date.now(),r=t.parent?.opts()||{};try{const t=k(r);if(!t.apiKey)return void C(new S(Ue,P.AUTH_ERROR),"file save",n);if(1!==[e.url,e.content,e.stdin].filter(Boolean).length)return void C(new S("--url、--content、--stdin 三者互斥,必须且只能传一个",P.INVALID_ARGS),"file save",n);const o=parseInt(e.rename,10);if(![0,1].includes(o))return void C(new S("--rename 仅支持 0 或 1",P.INVALID_ARGS),"file save",n);if(e.url&&String(e.url).includes("|"))return void C(new S("`--url` 仅支持单个下载地址,不支持用英文竖线(|)连接多个 URL;请对每个地址分别执行 file save",P.INVALID_ARGS),"file save",n);let s=je(e.content);if(e.stdin&&(s=De(await Oe()),!s.trim()))return void C(new S("stdin 输入内容为空",P.INVALID_ARGS),"file save",n);const i=new ze(t),a=await i.save({url:e.url,content:s,upload_path:e.dest,file_name:e.filename,is_rename:o});L(a,"file save",n),qe(a,"file save",n,r,e=>Y(e,"文件保存成功"))}catch(e){C(e,"file save",n)}}),t.command("append").description("向云盘文本文件末尾追加内容").argument("<path>","文件完整路径,必须以 / 开头").option("--content <text>","追加的文本内容(与 --stdin 互斥)").option("--stdin","从标准输入读取追加内容(与 --content 互斥;Windows cmd 用 type 文件 | ... 勿用 cat)").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=k(o);if(!t.apiKey)return void C(new S(Ue,P.AUTH_ERROR),"file append",r);if(!e.startsWith("/"))return void C(new S("path 必须以 / 开头",P.INVALID_ARGS),"file append",r);if(1!==[n.content,n.stdin].filter(Boolean).length)return void C(new S("--content 与 --stdin 互斥,必须且只能传一个",P.INVALID_ARGS),"file append",r);let s=je(n.content);if(n.stdin&&(s=De(await Oe()),!s||!s.trim()))return void C(new S("stdin 输入内容为空",P.INVALID_ARGS),"file append",r);const i=new ze(t),a=await i.appendContent({path:e,content:s});L(a,"file append",r),qe(a,"file append",r,o,t=>Y(t,`已追加内容到: ${e}`))}catch(e){C(e,"file append",r)}}),t.command("exists").description("检测目录下是否存在同名文件").option("--path <path>","目标目录,必须以 / 开头").option("--files <json>",'待检测文件数组 JSON,例如 [{"fname":"a.txt","fsize":123}](与 --stdin 互斥;Windows cmd 勿用单引号,见文档)').option("--stdin","从标准输入读取 files JSON(与 --files 互斥)").action(async e=>{const n=Date.now(),r=t.parent?.opts()||{};try{const t=k(r);if(!t.apiKey)return void C(new S(Ue,P.AUTH_ERROR),"file exists",n);if(!e.path||!e.path.startsWith("/"))return void C(new S("--path 必须以 / 开头",P.INVALID_ARGS),"file exists",n);if(1!==[e.files,e.stdin].filter(Boolean).length)return void C(new S("--files 与 --stdin 互斥,必须且只能传一个",P.INVALID_ARGS),"file exists",n);const o=Be(e.stdin?await Oe():e.files),s=new ze(t),i=await s.detectFileExists({path:e.path,files:o});L(i,"file exists",n),qe(i,"file exists",n,r,t=>Y(t,`检测完成: ${e.path}`))}catch(e){C(e,"file exists",n)}}),t.command("upload").description("上传本地文件到云盘").argument("<files>","本地文件路径,多个用逗号分隔").option("--dest <path>","云盘目标路径","/").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=k(o);if(!t.apiKey)return void C(new S(Ue,P.AUTH_ERROR),"file upload",r);const s=e.split(",").map(e=>e.trim()),i=new ze(t);qe(await i.upload({filePaths:s,uploadPath:n.dest}),"file upload",r,o,e=>Y(e,"上传完成"))}catch(e){C(e,"file upload",r)}}),t.command("download").description("下载云盘文件到本地").argument("<nid>","文件 NID(可通过 file search / dir ls 获取)").option("--dir <path>","本地下载目录").option("--no-auto","仅获取下载链接,不自动下载").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=k(o);if(!t.apiKey)return void C(new S(Ue,P.AUTH_ERROR),"file download",r);const s=new ze(t);qe(await s.download({nid:e,auto:n.auto,downloadDir:n.dir}),"file download",r,o,e=>Y(e,"下载完成"))}catch(e){C(e,"file download",r)}}),t}function Ye(e){return`${e} CLI auto-completion`}function Je(e){return"_"+e.replace(/[^a-zA-Z0-9]/g,"_")}function Qe(){return _()}function Ge(){const e=process.env.SHELL||"";return e.includes("zsh")?"zsh":e.includes("bash")?"bash":"unknown"}function Ve(e){const t=Je(e),n=`~/.${e}`;return`# ${e} bash completion\n# 安装: source ${n}/completion.bash\n# 或添加到 ~/.bashrc: source ${n}/completion.bash\n\n${t}_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 claw-backup claw-restore claw-auto-backup 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 claw-backup)\n COMPREPLY=( $(compgen -W "--source --dest --source-dir --claw-name --help" -- "$cur") )\n ;;\n claw-restore)\n COMPREPLY=( $(compgen -W "--remote --target --help" -- "$cur") )\n ;;\n claw-auto-backup)\n case "\${COMP_WORDS[2]}" in\n enable) COMPREPLY=( $(compgen -W "--source-dir --claw-name --event-log --help" -- "$cur") ) ;;\n disable|status) COMPREPLY=( $(compgen -W "--help" -- "$cur") ) ;;\n *) COMPREPLY=( $(compgen -W "enable disable status --help" -- "$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 ${t}_completions ${e}\n`}function Xe(e){const t=Je(e),n=`~/.${e}`;return`#compdef ${e}\n# ${e} zsh completion\n# 安装: source ${n}/completion.zsh\n# 或添加到 ~/.zshrc: source ${n}/completion.zsh\n\n${t}() {\n local -a top_commands\n top_commands=(\n 'auth:鉴权管理'\n 'user:用户信息'\n 'dir:目录操作'\n 'file:文件操作'\n 'claw-backup:上传本地文件或目录到云盘'\n 'claw-restore:递归恢复云盘目录到本地'\n 'claw-auto-backup:监听目录变化并自动备份到云盘'\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 claw-backup) _arguments '--source[本地源文件或目录]:path:_files' '--dest[云盘目标目录]:path:' '--source-dir[OpenClaw本地根目录]:path:_files -/' '--claw-name[云盘备份目录名]:name:' ;;\n claw-restore) _arguments '--remote[云盘源目录]:path:' '--target[本地目标目录]:path:_files -/' ;;\n claw-auto-backup) _arguments '1:subcommand:(enable disable status)' ;;\n esac\n ;;\n args)\n case "$words[2]" in\n claw-auto-backup)\n case "$words[3]" in\n enable) _arguments '--source-dir[监听的本地根目录]:path:_files -/' '--claw-name[云盘备份目录名]:name:' '--event-log[事件日志文件]:path:' ;;\n esac\n ;;\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${t} "$@"\n`}function He(e){const t=o();return n.join(t,"zsh"===e?".zshrc":".bashrc")}function Ze(){const r=new e("completion").description("Shell 自动补全");return r.command("install").description("安装补全脚本到当前 shell").option("--bash","安装 bash 补全").option("--zsh","安装 zsh 补全").action(e=>{try{const r=e.bash?"bash":e.zsh?"zsh":Ge();if("unknown"===r)return void C("无法检测 shell 类型,请使用 --bash 或 --zsh 指定","completion install");const o=w(),s="zsh"===r?Xe(o):Ve(o),i="zsh"===r?"zsh":"bash",a=Qe(),c=n.join(a,`completion.${i}`);t.existsSync(a)||t.mkdirSync(a,{recursive:!0,mode:448}),t.writeFileSync(c,s,{mode:420});const u=He(r),l=function(e,t){return`source ~/.${e}/completion.${"zsh"===t?"zsh":"bash"} # ${Ye(e)}`}(o,r);let f="";t.existsSync(u)&&(f=t.readFileSync(u,"utf8")),!function(e,t){return!!e.includes(Ye(t))||!("360disk"!==t||!e.includes("360disk auto-completion"))}(f,o)?(t.appendFileSync(u,"\n"+l+"\n"),A(`补全脚本已安装:\n 脚本: ${c}\n 配置: ${u}\n\n请执行 source ${u} 或重新打开终端生效`)):A(`补全脚本已更新: ${c}\n配置已存在于 ${u},无需重复添加`)}catch(e){C(e,"completion install")}}),r.command("uninstall").description("卸载补全脚本").option("--bash","卸载 bash 补全").option("--zsh","卸载 zsh 补全").action(e=>{try{const r=e.bash?"bash":e.zsh?"zsh":Ge();if("unknown"===r)return void C("无法检测 shell 类型,请使用 --bash 或 --zsh 指定","completion uninstall");const o="zsh"===r?"zsh":"bash",s=n.join(Qe(),`completion.${o}`);t.existsSync(s)&&t.unlinkSync(s);const i=He(r);if(t.existsSync(i)){const e=t.readFileSync(i,"utf8"),n=w(),r=Ye(n),o=e.split("\n").filter(e=>!e.includes(r)&&("360disk"!==n||!e.includes("360disk auto-completion")));t.writeFileSync(i,o.join("\n"))}A(`补全脚本已卸载\n请执行 source ${He(r)} 或重新打开终端生效`)}catch(e){C(e,"completion uninstall")}}),r.command("script").description("输出补全脚本到 stdout(手动安装用)").option("--bash","输出 bash 补全脚本").option("--zsh","输出 zsh 补全脚本").action(e=>{try{const t=e.bash?"bash":e.zsh?"zsh":Ge();if("unknown"===t)return void C("无法检测 shell 类型,请使用 --bash 或 --zsh 指定","completion script");const n=w(),r="zsh"===t?Xe(n):Ve(n);process.stdout.write(r)}catch(e){C(e,"completion script")}}),r}const et=524288;function tt(e){const n=t.openSync(e,"r"),r=Buffer.allocUnsafe(et),o=[];try{let e=0;for(;;){const s=t.readSync(n,r,0,et,e);if(0===s)break;const i=c.createHash("sha1").update(r.subarray(0,s)).digest("hex");o.push(i),e+=s}}finally{t.closeSync(n)}return c.createHash("sha1").update(o.join("")).digest("hex")}function nt(e){const t=JSON.stringify({apiKey:e.apiKey,ecsEnv:e.ecsEnv||"prod",subChannel:e.subChannel||"open"});return c.createHash("sha256").update(t).digest("hex")}function rt(e){const t=Array.isArray(e?.uploadResults)?e.uploadResults[0]:void 0,n=t?.uploadRes;return{nid:void 0!==n?.nid?String(n.nid):void 0,remote_file_hash:void 0!==n?.file_hash?String(n.file_hash):void 0,remote_name:n?.name||t?.name}}class ot{db;constructor(e=function(){return n.join(_(),"claw-backup.db")}()){$();const t=m(import.meta.url)("better-sqlite3");this.db=new t(e),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("busy_timeout = 5000"),this.migrate()}getFileState(e){return this.db.prepare("\n SELECT *\n FROM claw_backup_files\n WHERE cache_scope = ?\n AND source_root = ?\n AND dest_root = ?\n AND relative_path = ?\n ").get(e.cacheScope,e.sourceRoot,e.destRoot,e.relativePath)}upsertFileState(e){this.db.prepare("\n INSERT INTO claw_backup_files (\n cache_scope,\n source_root,\n dest_root,\n relative_path,\n absolute_path,\n size,\n mtime_ms,\n fhash,\n nid,\n remote_file_hash,\n remote_name,\n remote_dir,\n uploaded_at,\n created_at,\n updated_at\n ) VALUES (\n @cache_scope,\n @source_root,\n @dest_root,\n @relative_path,\n @absolute_path,\n @size,\n @mtime_ms,\n @fhash,\n @nid,\n @remote_file_hash,\n @remote_name,\n @remote_dir,\n @uploaded_at,\n datetime('now'),\n datetime('now')\n )\n ON CONFLICT(cache_scope, source_root, dest_root, relative_path)\n DO UPDATE SET\n absolute_path = excluded.absolute_path,\n size = excluded.size,\n mtime_ms = excluded.mtime_ms,\n fhash = excluded.fhash,\n nid = excluded.nid,\n remote_file_hash = excluded.remote_file_hash,\n remote_name = excluded.remote_name,\n remote_dir = excluded.remote_dir,\n uploaded_at = excluded.uploaded_at,\n updated_at = datetime('now')\n ").run({...e,nid:e.nid??null,remote_file_hash:e.remote_file_hash??null,remote_name:e.remote_name??null})}touchUnchangedFileState(e,t){this.db.prepare("\n UPDATE claw_backup_files\n SET absolute_path = @absolute_path,\n size = @size,\n mtime_ms = @mtime_ms,\n fhash = @fhash,\n updated_at = datetime('now')\n WHERE id = @id\n ").run({id:e.id,...t})}close(){this.db.close()}migrate(){this.db.exec("\n CREATE TABLE IF NOT EXISTS claw_backup_files (\n -- 自增主键,仅用于本地数据库内部定位记录\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n -- 缓存作用域:由 apiKey、ECS_ENV、SUB_CHANNEL 计算出的 SHA-256 摘要,避免不同账号/环境共用缓存\n cache_scope TEXT NOT NULL,\n -- 本地备份源根路径;目录备份时为目录路径,单文件备份时为文件路径\n source_root TEXT NOT NULL,\n -- 云盘备份目标根目录,和 source_root 共同区分不同备份任务\n dest_root TEXT NOT NULL,\n -- 文件相对 source_root 的 POSIX 路径,用于保持目录结构和定位同一文件\n relative_path TEXT NOT NULL,\n -- 文件当前本地绝对路径,用于诊断源目录移动或单文件备份记录\n absolute_path TEXT NOT NULL,\n -- 本地文件大小,作为快速跳过未变化文件的第一层判断\n size INTEGER NOT NULL,\n -- 本地文件 mtime 毫秒时间戳,用于记录上传时的本地文件状态\n mtime_ms INTEGER NOT NULL,\n -- 业务文件 hash:按 512KB 分块算 bhash 后再 SHA1 得到的 fhash\n fhash TEXT NOT NULL,\n -- 云盘上传成功后返回的节点 ID,用于后续下载、定位或排查\n nid TEXT,\n -- 云盘服务端返回的 file_hash;正常应与本地 fhash 保持一致\n remote_file_hash TEXT,\n -- 云盘服务端返回的文件名或远端完整文件名,用于排查实际上传结果\n remote_name TEXT,\n -- 本次文件实际上传到的云盘目录\n remote_dir TEXT NOT NULL,\n -- 最近一次成功上传该文件的时间\n uploaded_at TEXT NOT NULL,\n -- 本地数据库记录首次创建时间\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n -- 本地数据库记录最近更新时间\n updated_at TEXT NOT NULL DEFAULT (datetime('now')),\n UNIQUE(cache_scope, source_root, dest_root, relative_path)\n );\n\n CREATE UNIQUE INDEX IF NOT EXISTS idx_claw_backup_files_scope_path\n ON claw_backup_files(cache_scope, source_root, dest_root, relative_path);\n\n -- 用于后续按内容 hash 排查或扩展去重能力\n CREATE INDEX IF NOT EXISTS idx_claw_backup_files_fhash\n ON claw_backup_files(fhash);\n ")}}function st(e){return e.split(n.sep).join(n.posix.sep)}function it(e){const t=String(e||"").trim();if(!t||"."===t||".."===t)return;const r=t.split(/[\\/]+/).filter(Boolean).join(n.posix.sep);return r&&"."!==r&&".."!==r&&!r.startsWith("../")?r:void 0}function at(e){if(!Array.isArray(e))return[];const t=new Set;for(const n of e){const e=it(n);e&&t.add(e)}return[...t].sort()}function ct(e){const t=e?.data?.AFS;if(!t||"object"!=typeof t)throw new Error("系统云控配置缺少 data.AFS");const n=at(t.backup_dir),r=at(t.backup_match_dir),o=at(t.backup_file);if(0===n.length&&0===r.length&&0===o.length)throw new Error("系统云控配置 data.AFS 为空");return{backupDirs:n,backupMatchDirs:r,backupFiles:o}}function ut(e,t){const r=st(n.relative(e,t));return Boolean(r)&&".."!==r&&!r.startsWith("../")&&!n.posix.isAbsolute(r)}function lt(e,r){const o=t.realpathSync(e),s=t.realpathSync(r),i=st(n.relative(o,s));return Boolean(i)&&".."!==i&&!i.startsWith("../")&&!n.posix.isAbsolute(i)}function ft(e){const r=[],o=[];if(!t.existsSync(e))return{files:r,errors:o};const s=[e];for(;s.length>0;){const e=s.pop();let i;try{i=t.readdirSync(e,{withFileTypes:!0})}catch(t){o.push({path:e,error:t?.message||"读取目录失败"});continue}for(const t of i){if(".DS_Store"===t.name)continue;const o=n.join(e,t.name);t.isDirectory()?s.push(o):t.isFile()&&r.push(o)}}return{files:r,errors:o}}function dt(e,r,o,s){const i=n.resolve(e,r);if(!ut(e,i))return;if(!t.existsSync(i))return;let a;try{a=t.lstatSync(i)}catch(e){return void s.push({path:i,error:e?.message||"读取目录状态失败"})}if(a.isSymbolicLink())return;if(!a.isDirectory())return;try{if(!lt(e,i))return void s.push({path:i,error:"配置目录指向 source-dir 外部"})}catch(e){return void s.push({path:i,error:e?.message||"校验目录真实路径失败"})}const c=ft(i);for(const e of c.files)o.add(e);s.push(...c.errors)}function ht(e,r,o,s){const i=n.resolve(e,r);if(ut(e,i)&&t.existsSync(i))try{const n=t.lstatSync(i);if(n.isSymbolicLink())return;if(n.isFile()){if(!lt(e,i))return void s.push({path:i,error:"配置文件指向 source-dir 外部"});o.add(i)}}catch(e){s.push({path:i,error:e?.message||"读取文件状态失败"})}}function pt(e,t){return t.backupMatchDirs.some(t=>e.includes(t))}function mt(e,r){const o=new Set,s=[];let i;try{i=t.readdirSync(e,{withFileTypes:!0})}catch(t){return{files:[],errors:[{path:e,error:t?.message||"读取目录失败"}]}}for(const t of r.backupDirs)dt(e,t,o,s);for(const t of r.backupFiles)ht(e,t,o,s);for(const t of i){const i=n.join(e,t.name);if(t.isDirectory()&&pt(t.name,r)){const e=ft(i);for(const t of e.files)o.add(t);s.push(...e.errors)}}return{files:[...o].sort(),errors:s}}function wt(e,t,r){const o=st(n.relative(e,n.resolve(t)));if(!o||".."===o||o.startsWith("../")||n.posix.isAbsolute(o))return!1;if(r.backupFiles.includes(o))return!0;if(r.backupDirs.some(e=>o===e||o.startsWith(`${e}/`)))return!0;const[s]=o.split("/");return!!s&&pt(s,r)}const _t="未配置 API Key,请先执行 auth login --api-key <API_KEY> 或设置 API_KEY 环境变量";function yt(e){return n.resolve(e)}function gt(e){let t=String(e||"").trim();if(!t)throw new S("远端目录不能为空",P.INVALID_ARGS);return t.startsWith("/")||(t="/"+t),t.endsWith("/")||(t+="/"),t}function $t(e){return String(e?.name||e?.fname||"").trim()}function vt(e,t,r){const o=$t(t);if(!o)return"";let s=o;return s.startsWith("/")||(s=n.posix.join(e,s)),r?gt(s):s}function bt(e){return!!e&&(1===e.is_dir||"1"===e.is_dir||"dir"===e.type||(1===e.type||"1"===e.type))}function Et(e){const t=e?.data;if(!t)return[];const n=t.list||t.data||t.node_list||[];return Array.isArray(n)?n:[]}function kt(e,t){const n=e?.data||{},r=Number(n.total_count??n.total??t);return Number.isFinite(r)?r:t}function Pt(e){t.mkdirSync(e,{recursive:!0})}function St(e){return".DS_Store"===e}function It(e){const r=t.statSync(e);if(r.isDirectory())return function(e){const r=[""],o=[];return function e(s,i){const a=t.readdirSync(s,{withFileTypes:!0});for(const t of a){const a=n.join(s,t.name),c=i?n.posix.join(i,t.name):t.name;if(t.isDirectory())r.push(c),e(a,c);else if(t.isFile()){if(St(t.name))continue;o.push(c)}}}(e,""),{dirs:r,files:o}}(e).files;if(r.isFile())return St(n.basename(e))?[]:[n.basename(e)];throw new S(`--source 必须是文件或目录: ${e}`,P.INVALID_ARGS)}async function Dt(e,r){const o=Boolean(e.source||e.dest),s=Boolean(e.sourceDir||e.clawName);if(o&&s)throw new S("通用备份参数 --source/--dest 不能和 OpenClaw 参数 --source-dir/--claw-name 混用",P.INVALID_ARGS);if(!o&&!s)throw new S("必须传入 --source/--dest,或传入 --source-dir/--claw-name",P.INVALID_ARGS);if(s){if(!e.sourceDir)throw new S("--source-dir 为必填参数",P.INVALID_ARGS);const o=yt(e.sourceDir),s=function(e){const t=String(e||"").trim();if(!t)throw new S("--claw-name 不能为空",P.INVALID_ARGS);if("."===t||".."===t||t.includes("/")||t.includes("\\"))throw new S("--claw-name 必须是目录名,不能包含路径分隔符",P.INVALID_ARGS);return t}(e.clawName);if(!t.existsSync(o))throw new S(`source-dir 目录不存在: ${o}`,P.INVALID_ARGS);if(!t.statSync(o).isDirectory())throw new S(`source-dir 必须是本地目录: ${o}`,P.INVALID_ARGS);const i=ct(await r.getSysConfig()),a=mt(o,i);return{sourceRoot:o,uploadRoot:gt(s),uploadTreeRoot:o,files:a.files.map(e=>({absoluteFile:e,relativeFile:Ot(n.relative(o,e))})),initialFailures:a.errors.map(e=>({path:Ot(n.relative(o,e.path))||e.path,error:e.error,code:D(new Error(e.error))}))}}if(!e.source||!e.dest)throw new S("--source 与 --dest 必须同时传入",P.INVALID_ARGS);const i=yt(e.source);if(!t.existsSync(i))throw new S(`本地源路径不存在: ${i}`,P.INVALID_ARGS);const a=t.statSync(i).isDirectory(),c=It(i);return{sourceRoot:i,uploadRoot:gt(e.dest),uploadTreeRoot:a?i:n.dirname(i),files:c.map(e=>({absoluteFile:a?n.join(i,e):i,relativeFile:Ot(e)})),initialFailures:[]}}function Ot(e){return e.split(n.sep).join(n.posix.sep)}function Rt(e){return[P.AUTH_ERROR,P.PERMISSION_DENIED,P.QUOTA_EXCEEDED,P.NETWORK_ERROR,P.SERVER_ERROR].includes(e.code)}function Nt(e){if(e instanceof S)return{message:e.message,code:e.code};const t=e;return{message:e instanceof Error?e.message:String(e||"未知错误"),code:D(t?.primaryUploadError||t?.uploadErrors?.[0]?.error||e)}}function Tt(){const n=new e("claw-backup").description("将本地文件或目录上传到云盘目录").option("--source <path>","本地源文件或目录").option("--dest <path>","云盘目标目录").option("--source-dir <path>","OpenClaw 本地根目录,只备份白名单目录和文件").option("--claw-name <name>","云盘中的 OpenClaw 备份目录名").action(async e=>{const r=Date.now(),o=n.parent?.opts()||{};try{const n=k(o);if(!n.apiKey)return void C(new S(_t,P.AUTH_ERROR),"claw-backup",r);const s=new ze(n),i=await Dt(e,s),a=i.files,c=[],u=[],l=[...i.initialFailures],f=[];let d,h;const p=nt({apiKey:n.apiKey,ecsEnv:n.ecsEnv,subChannel:n.subChannel}),m=new ot;try{for(const e of a){const{absoluteFile:n,relativeFile:r}=e;try{const e=t.statSync(n),o=Math.trunc(e.mtimeMs),s=m.getFileState({cacheScope:p,sourceRoot:i.sourceRoot,destRoot:i.uploadRoot,relativePath:r}),a=s&&s.size!==e.size?void 0:tt(n);if(s&&a&&s.fhash===a){m.touchUnchangedFileState(s,{absolute_path:n,size:e.size,mtime_ms:o,fhash:a}),u.push(r);continue}f.push({absoluteFile:n,relativeFile:r,size:e.size,mtimeMs:o,fhash:a||tt(n)})}catch(e){const t=Nt(e),n={path:r,error:t.message,code:t.code};if(l.push(n),Rt(n)){d=n,h=n.error;break}}}if(f.length>0&&!d){const e=await s.uploadTree({rootDir:i.uploadTreeRoot,filePaths:f.map(e=>e.absoluteFile),uploadRoot:i.uploadRoot,is_rename:0}),t=new Map(f.map(e=>[e.absoluteFile,e]));for(const n of e.uploadResults){const e=t.get(n.absolutePath);if(!e)continue;const r=rt({uploadResults:[n.uploadResult]});m.upsertFileState({cache_scope:p,source_root:i.sourceRoot,dest_root:i.uploadRoot,relative_path:e.relativeFile,absolute_path:e.absoluteFile,size:e.size,mtime_ms:e.mtimeMs,fhash:e.fhash,nid:r.nid,remote_file_hash:r.remote_file_hash||e.fhash,remote_name:r.remote_name,remote_dir:n.remoteDir,uploaded_at:(new Date).toISOString()}),c.push(e.relativeFile)}for(const t of e.uploadErrors){const e=Nt(new Error(t.error)),n={path:t.relativePath,error:t.error,code:e.code};if(l.push(n),Rt(n)){d=n,h=n.error;break}}}}finally{m.close()}const w=a.length+i.initialFailures.length,_=Math.max(w-c.length-l.length-u.length,0),y=u.length+_,g=_>0,$=l.length>0?function(e,t){if(t)return t.code;const n=e.find(e=>e.code!==P.GENERAL)?.code;return n??e[0]?.code??P.GENERAL}(l,d):void 0,v={source:i.sourceRoot,destination:i.uploadRoot,directories_created:0,total_files:w,files_uploaded:c.length,files_failed:l.length,files_skipped:y,files_unchanged:u.length,stopped_early:g,aborted_reason:h,files:c,unchanged_files:u,failed_items:l};if("text"===o.format)A(function(e){const t=[`source: ${e.source}`,`destination: ${e.destination}`,`directories_created: ${e.directories_created}`,`total_files: ${e.total_files}`,`files_uploaded: ${e.files_uploaded}`,`files_failed: ${e.files_failed}`,`files_skipped: ${e.files_skipped}`,`files_unchanged: ${e.files_unchanged}`];if(e.stopped_early&&e.aborted_reason&&t.push(`aborted_reason: ${e.aborted_reason}`),e.failed_items.length>0){t.push("failed_items:");for(const n of e.failed_items)t.push(`- ${n.path}: ${n.error}`)}return t.join("\n")}(v)),void 0!==$&&(process.exitCode=$);else if(l.length>0){T(v,g&&h?`备份提前终止: ${h}`:`部分文件备份失败: ${l.length}/${w}`,$??P.GENERAL,"claw-backup",r,{quiet:o.quiet})}else N(v,"claw-backup",r,{quiet:o.quiet})}catch(e){C(e,"claw-backup",r)}});return n}function At(){const r=new e("claw-restore").description("将云盘目录递归恢复到本地目录").requiredOption("--remote <path>","云盘源目录").requiredOption("--target <path>","本地目标目录").action(async e=>{const o=Date.now(),s=r.parent?.opts()||{};try{const r=k(s);if(!r.apiKey)return void C(new S(_t,P.AUTH_ERROR),"claw-restore",o);const i=gt(e.remote),a=yt(e.target);Pt(a);const c=new Se(r),u=new ze(r),l=await async function(e,t){const n=[gt(t)],r=[];for(;n.length>0;){const t=n.shift();let o=0;const s=1e3;let i=0,a=0;do{const c=await e.list({path:t,page:o,page_size:s});if(0!==c?.errno)throw new O(c?.errmsg||`列目录失败: ${t}`,{traceId:R(c),apiErrno:"number"==typeof c?.errno?c.errno:void 0});const u=Et(c);a=kt(c,u.length),i+=u.length;for(const e of u){const o=$t(e);if(!o)continue;const s=bt(e),i={name:o,path:vt(t,e,s),nid:e?.nid?String(e.nid):void 0,isDir:s};r.push(i),s&&n.push(i.path)}o+=1}while(i<a)}return r}(c,i),f=[];let d=0;for(const e of l){const r=e.path.slice(i.length).replace(/^\/+/,"");if(!r)continue;const o=n.join(a,r.split("/").join(n.sep));if(e.isDir){Pt(o),d+=1;continue}if(!e.nid)throw new Error(`缺少文件 nid,无法下载: ${e.path}`);Pt(n.dirname(o));const s=await u.download({nid:e.nid,downloadDir:n.dirname(o),auto:!0,forceForeground:!0});s.downloadPath&&s.downloadPath!==o&&t.renameSync(s.downloadPath,o),f.push(r)}const h={remote:i,target:a,directories_restored:d,files_restored:f.length,files:f};"text"===s.format?A(Object.entries({remote:i,target:a,directories_restored:d,files_restored:f.length}).map(([e,t])=>`${e}: ${t}`).join("\n")):N(h,"claw-restore",o,{quiet:s.quiet})}catch(e){C(e,"claw-restore",o)}});return r}function Ct(e,r){const o={...r,ts:r.ts||(new Date).toISOString()};return function(e){const r=n.dirname(e);t.existsSync(r)||t.mkdirSync(r,{recursive:!0,mode:493})}(e),t.appendFileSync(e,`${JSON.stringify(o)}\n`,"utf8"),o}const Lt=["**/.DS_Store"];let Ft=null;const xt="未配置 API Key,请先执行 auth login --api-key <API_KEY> 或设置 API_KEY 环境变量";function Mt(e){if(!Number.isInteger(e)||Number(e)<=0)return!1;try{return process.kill(Number(e),0),!0}catch(e){return"EPERM"===e?.code}}function zt(e){let t=String(e||"").trim();if(!t)throw new S("dest 不能为空",P.INVALID_ARGS);return t.startsWith("/")||(t=`/${t}`),t.endsWith("/")||(t+="/"),t}function Ut(e){if(!e.sourceDir)throw new S("--source-dir 为必填参数",P.INVALID_ARGS);const r=n.resolve(String(e.sourceDir||"").trim());const o=function(e){const t=String(e||"").trim();if(!t)throw new S("--claw-name 不能为空",P.INVALID_ARGS);if("."===t||".."===t||t.includes("/")||t.includes("\\"))throw new S("--claw-name 必须是目录名,不能包含路径分隔符",P.INVALID_ARGS);return t}(e.clawName);if(!t.existsSync(r))throw new S(`source-dir 目录不存在: ${r}`,P.INVALID_ARGS);if(!t.statSync(r).isDirectory())throw new S(`source-dir 必须是本地目录: ${r}`,P.INVALID_ARGS);return[{source_dir:r,claw_name:o,upload_root:zt(o)}]}function qt(){return function(){try{const e=g();if(t.existsSync(e))return JSON.parse(t.readFileSync(e,"utf8"))}catch{}return null}()||{enabled:!1,watch_pairs:[]}}function Kt(e,t){if(e)try{Ct(e,t)}catch{}}function jt(e){const t=String(e||"").trim();return t?function(e,t){const r=String(e??t??"").trim();return r?n.resolve(r):"/tmp/360disk-claw-auto-backup-events.ndjson"}(t):void 0}function Bt(e){return{env:e.env,subChannel:e.subChannel,timeout:e.timeout,retries:e.retries,hasApiKey:Boolean(e.apiKey)}}function Wt(e){const t=[];return e.env&&t.push("--env",e.env),e.subChannel&&t.push("--sub-channel",e.subChannel),e.timeout&&t.push("--timeout",e.timeout),e.retries&&t.push("--retries",e.retries),t}function Yt(e){const t=k(e);return{apiKey:t.apiKey,env:t.ecsEnv,subChannel:t.subChannel,timeout:e.timeout,retries:e.retries}}async function Jt(e,t=1e4){if(!e||!Mt(e))return;const n=Date.now();for(;Mt(e);){if(Date.now()-n>=t)throw new S(`等待旧 watcher 退出超时: ${e}`,P.CONFLICT);await new Promise(e=>setTimeout(e,200))}}function Qt(e){if(!e.pid||!e.instance_id||!Mt(e.pid))return!1;const t=function(e){if(!Number.isInteger(e)||Number(e)<=0)return"";try{return"win32"===process.platform?h("powershell.exe",["-NoProfile","-Command",`(Get-CimInstance Win32_Process -Filter "ProcessId = ${String(e)}" | Select-Object -ExpandProperty CommandLine)`],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim():h("ps",["-p",String(e),"-o","command="],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim()}catch{return""}}(e.pid);return!!t&&(t.includes("claw-auto-backup __watch")&&t.includes(`--instance-id ${e.instance_id}`))}async function Gt(e){return!!e.pid&&(!!Qt(e)&&(process.kill(e.pid,"SIGTERM"),await Jt(e.pid),!0))}async function Vt(e,t){return new Promise((n,r)=>{let o=!1;const s=()=>{e.off("error",a),e.off("exit",c)},i=async(e,n=P.GENERAL)=>{o||(o=!0,s(),await async function(e,t){const n=qt();if(n.instance_id===e){if(n.pid&&Mt(n.pid))try{process.kill(n.pid,"SIGTERM"),await Jt(n.pid,2e3)}catch{}Xt(e,{enabled:!1,pid:void 0,instance_id:void 0,last_backup_result:{success:!1,message:t}})}}(t,e),r(new S(e,n)))},a=e=>{i(`自动备份监听启动失败: ${e.message}`)},c=(e,t)=>{i(`自动备份监听启动失败,后台进程已退出${t?`,信号: ${t}`:`,退出码: ${e??"unknown"}`}`)};e.once("error",a),e.once("exit",c);const u=Date.now(),l=()=>{if(o)return;const r=qt();if(r.instance_id===t&&r.enabled&&r.pid===e.pid&&Qt(r))return o=!0,s(),void n(r);Date.now()-u>=6e4?i("等待自动备份 watcher 启动超时",P.CONFLICT):setTimeout(l,100)};l()})}function Xt(e,t){const n=qt();if(n.instance_id!==e)return null;const r={...n,...t};return E(r),r}async function Ht(e,r,o,s){const i=await async function(){return Ft||(Ft=import("chokidar").then(e=>e.default||e)),Ft}(),a=new Map,c=[],u=new Set,l=new Set,f=new Map,d=new Map;let h=!1;const p=e=>{Xt(o,e)},m=async e=>{const t=e.source_dir,n=Date.now(),o=d.get(t);if(o?.config&&o.expiresAt>n)return o.config;if(o?.loading)return o.loading;const s=(async()=>{try{const e=k(r),n=new ze(e),o=ct(await n.getSysConfig());return d.set(t,{config:o,expiresAt:Date.now()+6e4}),o}catch(e){if(o?.config)return o.config;throw e}finally{const e=d.get(t);e?.loading&&d.set(t,{config:e.config,expiresAt:e.expiresAt})}})();return d.set(t,{config:o?.config,expiresAt:o?.expiresAt||0,loading:s}),s},w=async(e,t=!0)=>{if(!h){h=!0;for(const e of a.values())clearTimeout(e);await Promise.all(c.map(e=>e.close())),Kt(s,{type:"watcher_stopped",instance_id:o,success:t,message:e||"watcher stopped"}),Xt(o,{enabled:!1,pid:void 0,instance_id:void 0,event_log_path:s,last_backup_result:e?{success:t,message:e}:qt().last_backup_result}),process.exit(t?0:1)}},_=(e,i={})=>{const{markEvent:c=!0,fsEvent:d,filePath:m}=i,w=e.source_dir;if(c){const t=f.get(w)||[];t.push({fsEvent:d,filePath:m}),f.set(w,t),p({last_event_at:(new Date).toISOString()}),Kt(s,{type:"watch_event",instance_id:o,source:e.source_dir,dest:e.upload_root,fs_event:d,path:m})}a.has(w)&&clearTimeout(a.get(w)),Kt(s,{type:"backup_scheduled",instance_id:o,source:e.source_dir,dest:e.upload_root,fs_event:d,path:m,message:"debounce timer started"});const y=setTimeout(async()=>{if(a.delete(w),f.delete(w),u.has(w))return l.add(w),void Kt(s,{type:"backup_deferred",instance_id:o,source:e.source_dir,dest:e.upload_root,message:"backup already running for source, queued next run"});u.add(w);const i=`${Date.now()}-${Math.random().toString(36).slice(2,8)}`,c=Date.now(),d=async function(e,r){const o=k(r);if(!o.apiKey)throw new S(xt,P.AUTH_ERROR);const s=new ze(o),i=[];let a=0,c=0,u=0;const l=ct(await s.getSysConfig()),f=mt(e.source_dir,l),d=f.files;c+=f.errors.length;const h=nt({apiKey:o.apiKey,ecsEnv:o.ecsEnv,subChannel:o.subChannel}),p=new ot;try{for(const r of d){const o=st(n.relative(e.source_dir,r));try{const n=t.statSync(r);if(!n.isFile())continue;const s=Math.trunc(n.mtimeMs),c=p.getFileState({cacheScope:h,sourceRoot:e.source_dir,destRoot:e.upload_root,relativePath:o}),u=c&&c.size!==n.size?void 0:tt(r);if(c&&u&&c.fhash===u){p.touchUnchangedFileState(c,{absolute_path:r,size:n.size,mtime_ms:s,fhash:u}),a+=1;continue}i.push({absoluteFile:r,relativeFile:o,size:n.size,mtimeMs:s,fhash:u||tt(r)})}catch{c+=1}}if(i.length>0){const t=await s.uploadTree({rootDir:e.source_dir,filePaths:i.map(e=>e.absoluteFile),uploadRoot:e.upload_root,is_rename:0}),n=new Map(i.map(e=>[e.absoluteFile,e]));for(const r of t.uploadResults){const t=n.get(r.absolutePath);if(!t)continue;const o=rt({uploadResults:[r.uploadResult]});p.upsertFileState({cache_scope:h,source_root:e.source_dir,dest_root:e.upload_root,relative_path:t.relativeFile,absolute_path:t.absoluteFile,size:t.size,mtime_ms:t.mtimeMs,fhash:t.fhash,nid:o.nid,remote_file_hash:o.remote_file_hash||t.fhash,remote_name:o.remote_name,remote_dir:r.remoteDir,uploaded_at:(new Date).toISOString()}),u+=1}c+=t.uploadErrors.length}return{totalFiles:d.length,uploadedFiles:u,unchangedFiles:a,failedFiles:c}}finally{p.close()}}(e,r);Kt(s,{type:"backup_started",instance_id:o,run_id:i,source:e.source_dir,dest:e.upload_root,pid:process.pid}),d.then(t=>{const n=Date.now()-c;Kt(s,{type:"backup_finished",instance_id:o,run_id:i,source:e.source_dir,dest:e.upload_root,success:0===t.failedFiles,duration_ms:n,pid:process.pid,exit_code:0,message:`总计 ${t.totalFiles} 个,上传 ${t.uploadedFiles} 个,跳过 ${t.unchangedFiles} 个,失败 ${t.failedFiles} 个`}),h||p({last_backup_at:(new Date).toISOString(),last_backup_result:{success:0===t.failedFiles,message:`已备份: ${e.source_dir} -> ${e.upload_root},总计 ${t.totalFiles} 个,上传 ${t.uploadedFiles} 个,跳过 ${t.unchangedFiles} 个,失败 ${t.failedFiles} 个`}})}).catch(t=>{const n=Date.now()-c;Kt(s,{type:"backup_failed",instance_id:o,run_id:i,source:e.source_dir,dest:e.upload_root,success:!1,duration_ms:n,pid:process.pid,exit_code:t?.code??null,error:t?.message||"备份失败"}),h||p({last_backup_at:(new Date).toISOString(),last_backup_result:{success:!1,message:t?.message||"备份失败"}})}).finally(()=>{u.delete(w),!h&&l.has(w)&&(l.delete(w),f.set(w,[{forceBackup:!0}]),_(e,{markEvent:!1}))})},5e3);a.set(w,y)};for(const t of e){const e=i.watch(t.source_dir,{persistent:!0,ignoreInitial:!0,usePolling:!0,interval:1e4,binaryInterval:1e4,ignored:Lt});e.on("all",(e,n)=>{["add","addDir","change","unlink","unlinkDir"].includes(e)&&(async()=>{try{const r=await m(t);if(!wt(t.source_dir,n,r))return;_(t,{fsEvent:e,filePath:n})}catch(r){p({last_event_at:(new Date).toISOString(),last_backup_result:{success:!1,message:r?.message||"读取自动备份云控配置失败"}}),Kt(s,{type:"backup_failed",instance_id:o,source:t.source_dir,dest:t.upload_root,fs_event:e,path:n,success:!1,error:r?.message||"读取自动备份云控配置失败"})}})()}),e.on("error",e=>{w(`监听失败: ${e.message}`,!1)}),await new Promise((t,n)=>{const r=()=>{e.off("error",o),t()},o=t=>{e.off("ready",r),n(t)};e.once("ready",r),e.once("error",o)}),c.push(e)}p({enabled:!0,pid:process.pid,instance_id:o,started_at:(new Date).toISOString(),event_log_path:s,runtime_options:Bt(r),watch_pairs:e}),Kt(s,{type:"watcher_started",instance_id:o,source:e.map(e=>e.source_dir).join(","),dest:e.map(e=>e.upload_root).join(","),pid:process.pid,message:"watcher ready"}),process.on("SIGINT",()=>{w("收到 SIGINT,监听已停止")}),process.on("SIGTERM",()=>{w("收到 SIGTERM,监听已停止")})}function Zt(){const r=new e("claw-auto-backup").description("监听目录变化并自动备份到云盘");r.command("enable").description("启用自动备份监听").requiredOption("--source-dir <path>","监听的本地根目录,只能是一个目录").requiredOption("--claw-name <name>","云盘中的 OpenClaw 备份目录名").option("--event-log <path>","自动备份事件日志文件路径;仅传入时才写日志").action(async e=>{const o=Date.now(),s=r.parent?.opts()||{};try{const r=Ut(e),a=qt(),c=Yt(s);if(!c.apiKey)return void C(new S(xt,P.AUTH_ERROR),"claw-auto-backup enable",o);const u=jt(e.eventLog);u&&function(e){try{if(t.existsSync(e)&&!t.statSync(e).isFile())throw new S(`event log 必须是文件路径,不能是目录: ${e}`,P.INVALID_ARGS);t.mkdirSync(n.dirname(e),{recursive:!0}),t.appendFileSync(e,"","utf8")}catch(t){if(t instanceof S)throw t;throw new S(`event log 不可写: ${e} (${t?.message||"未知错误"})`,P.INVALID_ARGS)}}(u),await Gt(a);const l=`${Date.now()}-${Math.random().toString(36).slice(2,10)}`,f=[function(){const e=process.argv[1];if(!e)throw new S("无法确定 CLI 入口文件路径",P.GENERAL);return e}(),...Wt(c),"claw-auto-backup","__watch","--instance-id",l];u&&f.push("--event-log",u);const[h]=r;f.push("--source-dir",h.source_dir,"--claw-name",h.claw_name);const p=d(process.execPath,f,{detached:!0,stdio:"ignore",env:(i=c,{...process.env,...i.apiKey?{API_KEY:i.apiKey}:{},...i.env?{ECS_ENV:i.env}:{},...i.subChannel?{SUB_CHANNEL:i.subChannel}:{}}),windowsHide:"win32"===process.platform});if(!p.pid)throw new S("启动自动备份 watcher 失败,未获取到进程 ID",P.GENERAL);E({enabled:!1,pid:p.pid,instance_id:l,started_at:(new Date).toISOString(),event_log_path:u,last_event_at:void 0,last_backup_at:void 0,last_backup_result:void 0,runtime_options:Bt(c),watch_pairs:r});const m=await Vt(p,l);p.unref(),"text"===s.format?A(`自动备份已启用\npid: ${p.pid}\nsource_dir: ${h.source_dir}\nclaw_name: ${h.claw_name}`):N(m,"claw-auto-backup enable",o,{quiet:s.quiet})}catch(e){C(e,"claw-auto-backup enable",o)}var i}),r.command("disable").description("停用自动备份监听").action(async()=>{const e=Date.now(),t=r.parent?.opts()||{};try{const n=qt();await Gt(n);const r={...qt(),enabled:!1,pid:void 0,instance_id:void 0};E(r),"text"===t.format?A("自动备份已停用"):N(r,"claw-auto-backup disable",e,{quiet:t.quiet})}catch(t){C(t,"claw-auto-backup disable",e)}}),r.command("status").description("查看自动备份状态").action(()=>{const e=Date.now(),t=r.parent?.opts()||{};try{const n=function(){const e=qt(),t=e.enabled&&Qt(e);return{...e,enabled:e.enabled&&t,running:t}}();"text"===t.format?A(function(e,t){const n=["enabled: "+(e.enabled&&t?"true":"false"),"running: "+(t?"true":"false"),`pid: ${e.pid||"-"}`,`started_at: ${e.started_at||"-"}`,`event_log_path: ${e.event_log_path||"-"}`,`last_event_at: ${e.last_event_at||"-"}`,`last_backup_at: ${e.last_backup_at||"-"}`,"last_backup_result: "+(e.last_backup_result?`${e.last_backup_result.success?"success":"failed"} - ${e.last_backup_result.message}`:"-"),"watch_pairs:"];if(e.watch_pairs&&0!==e.watch_pairs.length)for(const t of e.watch_pairs)n.push(`- ${t.source_dir||t.source||"-"} -> ${t.upload_root||t.dest||"-"} (claw_name: ${t.claw_name||"-"})`);else n.push("- (empty)");return n.join("\n")}(n,n.running)):N(n,"claw-auto-backup status",e,{quiet:t.quiet})}catch(t){C(t,"claw-auto-backup status",e)}});const o=new e("__watch").description("内部后台 watcher 进程").option("--instance-id <id>","后台 watcher 实例标识").option("--source-dir <path>","监听的本地根目录").option("--claw-name <name>","云盘中的 OpenClaw 备份目录名").option("--event-log <path>","自动备份事件日志文件路径").action(async e=>{try{const t=Ut(e),n=e,r=n.instanceId;if(!r)throw new S("缺少 watcher instance id",P.INVALID_ARGS);const s=Yt(function(e){return e.parent?.parent?.opts?.()||e.parent?.opts?.()||{}}(o)),i=jt(n.eventLog);await Ht(t,s,r,i)}catch(t){const n=e.instanceId;n&&Xt(n,{enabled:!1,pid:void 0,instance_id:void 0,event_log_path:jt(e.eventLog),last_backup_result:{success:!1,message:t?.message||"自动备份监听启动失败"}}),process.exit(1)}});return o.u=!0,r.addCommand(o),r}(async function(){const o=function(){try{const e=n.dirname(r(import.meta.url)),o=[n.resolve(e,"../package.json"),n.resolve(e,"../../package.json"),n.resolve(e,"../../../package.json"),n.resolve(process.cwd(),"package.json")];for(const e of o)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(o.name).description(o.description).version(o.version).enablePositionalOptions().passThroughOptions().option("--api-key <key>","API 密钥").option("--env <env>","环境 (prod/test)").option("--sub-channel <channel>","子渠道").option("--format <type>","输出格式 (json|text)","json").option("--quiet","仅输出结果数据").option("--timeout <ms>","请求超时时间(毫秒)","30000").option("--retries <n>","失败重试次数","0"),s.addCommand(de()),s.addCommand(function(){const t=new e("user").description("用户信息");return t.command("info").description("获取用户详细信息").action(async()=>{const e=Date.now(),n=t.parent?.opts()||{};try{const t=k(n);if(!t.apiKey)return void C(new S("未配置 API Key,请先执行 auth login --api-key <API_KEY> 或设置 API_KEY 环境变量",P.AUTH_ERROR),"user info",e);const r=new Pe(t),o=await r.info();L(o,"user info",e),"text"===n.format?A(Q(o)):N(o,"user info",e,{quiet:n.quiet})}catch(t){C(t,"user info",e)}}),t}()),s.addCommand(function(){const t=new e("dir").description("目录操作");return t.command("ls").description("列出云盘指定目录下的文件和文件夹").argument("[path]","目录路径","/").option("--page <n>","页码","0").option("--size <n>","每页数量","50").action(async(e,n)=>{const r=Date.now(),o=t.parent?.opts()||{};try{const t=k(o);if(!t.apiKey)return void C(new S(Ie,P.AUTH_ERROR),"dir ls",r);const s=new Se(t),i=await s.list({path:e,page:parseInt(n.page),page_size:parseInt(n.size)});L(i,"dir ls",r),"text"===o.format?A(j(i)):N(i,"dir ls",r,{quiet:o.quiet})}catch(e){C(e,"dir ls",r)}}),t.command("mkdir").description("创建新文件夹").argument("<path>","文件夹路径").action(async e=>{const n=Date.now(),r=t.parent?.opts()||{};try{const t=k(r);if(!t.apiKey)return void C(new S(Ie,P.AUTH_ERROR),"dir mkdir",n);const o=new Se(t),s=await o.mkdir(e);L(s,"dir mkdir",n),"text"===r.format?A(Y(s,`已创建目录: ${e}`)):N(s,"dir mkdir",n,{quiet:r.quiet})}catch(e){C(e,"dir mkdir",n)}}),t}()),s.addCommand(We()),s.addCommand(Tt()),s.addCommand(At()),s.addCommand(Zt()),s.addCommand(Ze()),await s.parseAsync()})().then(()=>{(function(){const e=process.argv.findIndex((e,t)=>t>=2&&"claw-auto-backup"===e);return!(e>=0&&"__watch"===process.argv[e+1])})()&&process.exit(process.exitCode??0)}).catch(e=>{process.stderr.write(JSON.stringify({success:!1,error:e.message||"CLI 启动失败"},null,2)+"\n"),process.exit(1)});