@vchar/nos-cli
lnos-cli
NetEase NOS (网易对象存储) 命令行工具。Node.js 18+,支持单文件 PUT / 大文件分片上传(断点续传)、同桶重命名、图片/音视频信息查询,以及 CDN 缓存刷新。提供 AK/SK 直签 与 后端 token 两种鉴权。
安装
# 官方 npm(npmjs.com)
npm i -g @vchar/nos-cli
# 或从源码
git clone <repo> && cd nos-cli && npm install && npm run build
npm i -g . # 或 npm link
安装后即可使用 lnos-cli 命令(bin 名为 lnos-cli)。
前置要求
- Node.js >= 18
- 鉴权二选一:
- AK/SK 模式:NOS AccessKey / SecretKey(网易云控制台获取)
- token 模式:后端 token 签发端点(默认指向
https://ai-service-lofter.hz.netease.com/api/mcp/nos/token,由 virtual-service-mcp 提供)
配置管理
配置持久化保存在 ~/.nos-cli/config.json,文件权限 0600(仅当前用户可读写)。
# —— AK/SK 直签模式 ——
lnos-cli config set accessKey <你的 AccessKey>
lnos-cli config set secretKey <你的 SecretKey>
lnos-cli config set bucket <bucketName> # AK/SK 模式必填
# —— 后端 token 模式(凭证由后端签发,无需本地 AK/SK)——
# 配了 tokenEndpoint 即启用 token 模式;默认指向公网 MCP 服务,可不配
lnos-cli config set tokenEndpoint <url> # 可选,默认 https://ai-service-lofter.hz.netease.com/api/mcp/nos/token(可覆盖)
lnos-cli config set tokenHeaders '<json>' # 可选,鉴权头(公网默认无需;内网/正式环境需 Bearer skt- 时配)
# —— 通用 ——
lnos-cli config set net <auto|inner|outer> # 网络模式,默认 auto
lnos-cli config set partSize <16> # 分片大小 MB,默认 16
lnos-cli config set encrypt <true|false> # secretKey AES-256-GCM 加密存储
lnos-cli config get <key> # 查看单项
lnos-cli config list # 列出全部(secretKey 显示为 ****)
说明:
- 鉴权优先级:配了
tokenEndpoint→ token 模式(凭证后端签发、x-nos-token头、object key 由后端生成 UUID);否则 AK/SK 直签(本地 HMAC-SHA256 签名、object key 本地命名)。token 模式下bucket可不配(后端返回;配置则为-b提示后端签进 token)。 net取值:auto(默认)— 自动探测:TCP 连接nos-jd.service.163.org:80(800ms 超时),可达则用内网,否则用外网。inner— 强制内网端点nos-jd.service.163.org(http)。outer— 强制外网端点nos-jd.163yun.com(https)。内网 443 暴露的是外网证书,故内网走 http 避免 TLS 域名校验失败。
encrypt true启用后,secretKey 由机器指纹派生密钥加密,写入磁盘时为密文,读取时自动解密。注意这是防窥屏级别的保护,不是强加密。config list显示时 secretKey 固定输出****,不会泄漏明文。
上传文件
基本用法
lnos-cli upload <file...>
支持同时上传一个或多个文件。上传成功后会打印三个 URL(均不含 URL 编码,可直接访问):
✅ 上传成功:photo.jpg
内网: https://my-bucket.nos-jd.service.163.org/20260711234200000_a1b2c3/photo.jpg
外网: https://my-bucket.nos-jd.163yun.com/20260711234200000_a1b2c3/photo.jpg
CDN : https://my-bucket.nosdn.127.net/20260711234200000_a1b2c3/photo.jpg
选项
| 选项 | 简写 | 说明 |
|---|---|---|
--bucket <bucket> |
-b |
覆盖默认 Bucket |
--concurrency <n> |
-c |
上传并发数(默认 = min(CPU 核数, 6);不设上限) |
--net <mode> |
网络模式:auto / inner / outer |
|
--key <objectName> |
自定义对象名(仅单个文件时可用) | |
--part-size <mb> |
分片阈值和分片大小,单位 MB | |
--abort |
中止指定文件的分片上传并清理状态(需同时使用 --key) |
分片上传与断点续传
- 文件大小 ≤ 分片大小时,走单次 PUT 上传。
- 文件大小 > 分片大小时,自动走 Multipart Upload(分片上传)。
- 上传过程中的状态(uploadId、已上传分片的 ETag)实时写入
~/.nos-cli/state/目录。 - 中断后重跑同一文件:自动检测已有的分片状态,跳过已上传分片,只上传缺失部分(断点续传)。
- 文件 mtime 发生变化(被修改后重试),旧状态自动作废,从头重新上传。
- 上传成功后,状态文件自动清理。
--key 和 --abort 约束
--key仅能用于单个文件,否则会报错。--abort需要同时指定--key(即之前上传时使用的对象名),因为随机生成的对象名无法回推。示例:
lnos-cli upload large-file.iso --key custom/object-name --abort
真实示例(端到端)
# 1. 配置
lnos-cli config set accessKey "your-ak"
lnos-cli config set secretKey "your-sk"
lnos-cli config set bucket "my-bucket"
lnos-cli config set net "auto"
# 2. 上传
lnos-cli upload ./screenshot.png ./report.pdf
# 3. 输出示例
✅ 上传成功:screenshot.png
内网: https://my-bucket.nos-jd.service.163.org/20260711234200000_x1y2z3/screenshot.png
外网: https://my-bucket.nos-jd.163yun.com/20260711234200000_x1y2z3/screenshot.png
CDN : https://my-bucket.nosdn.127.net/20260711234200000_x1y2z3/screenshot.png
✅ 上传成功:report.pdf
内网: https://my-bucket.nos-jd.service.163.org/20260711234200000_x1y2z3/report.pdf
外网: https://my-bucket.nos-jd.163yun.com/20260711234200000_x1y2z3/report.pdf
CDN : https://my-bucket.nosdn.127.net/20260711234200000_x1y2z3/report.pdf
对象管理
除上传外,还支持重命名和查询对象信息。这些命令的目标对象都可用两种形式指定:
- objectKey(如
dir/photo.png)— 使用配置或-b指定的 Bucket; - 完整 NOS URL(内网 / 外网 / CDN 任一)— 自动解析出 Bucket 和 objectKey。
所有命令均支持 -b/--bucket 和 --net 选项。
重命名 / 移动对象
lnos-cli rename <源> <新完整key>
- 基于 NOS 的
PUT Object Move(改元数据,不拷贝物理文件)。 - 仅支持同一 Bucket 内(NOS 不支持跨桶 move)。
新完整key是完整的目标对象名(不保留源目录前缀)。- 成功后打印新对象的三个 URL。
lnos-cli rename clitest/pic.png clitest/renamed.png
# ✅ 重命名成功:clitest/pic.png → clitest/renamed.png
# 内网/外网/CDN: ...
查询对象信息
lnos-cli imageinfo <target> # 图片基本信息:Width / Height / Size / Type / Orientation
lnos-cli exif <target> # 图片 EXIF 信息(无 EXIF 时返回空对象)
lnos-cli vinfo <target> # 音视频信息:时长 / 码率 / 编解码 / 分辨率等
以 JSON 打印结果。示例:
lnos-cli imageinfo clitest/pic.png
# {
# "Width": "1210",
# "Height": "1726",
# "Size": "1758637",
# "Type": "PNG",
# "Orientation": ""
# }
lnos-cli vinfo clitest/test.mp4
# { "GetVideoInfo": { "VideoInfo": { "Duration": 14149, "Width": 846, "Height": 1920, ... } } }
注:
imageinfo/exif的 NOS 响应为 XML(工具会解析为扁平对象),vinfo响应为 JSON,工具自动识别两种格式。
CDN 缓存刷新
提交 CDN 缓存刷新任务(文件 / 目录 / 混合)。
lnos-cli cdn refresh <url...> [--type <file|dir|mix>] [--domain-id <id>] [--isp <isp>]
- 走公网 MCP 服务
https://ai-service-lofter.hz.netease.com/api/mcp/cdn/refresh(virtual-service-mcp 在内网代调上游刷新接口,无需 VPN/内网,token 由后端内部复用,lnos-cli 不单独取 token)。 --type:file(文件,默认) /dir(目录) /mix(混合,mix 时目录 URL 需以/结尾)。- URL 最多 100 条,需以
http(s)://开头。 --domain-id/--isp可省略;省略时刷新所有匹配域名。
# 刷新单个文件
lnos-cli cdn refresh "https://my-bucket.nosdn.127.net/abc.jpg"
# 刷新目录
lnos-cli cdn refresh --type dir "https://example.com/dir/"
# 混合(文件 + 目录)
lnos-cli cdn refresh --type mix "https://example.com/a.jpg" "https://example.com/dir/"
# 指定域名 ID / 厂家
lnos-cli cdn refresh --domain-id 123123 --isp cmcc "https://example.com/a.jpg"
成功输出:
✅ CDN 刷新任务已提交:fusion域名my-bucket.nosdn.127.net新增文件刷新
任务 ID: 7870403197
失败时逐条列出上游返回的 {url, detail, suggest}。
对象命名规则
不指定 --key 时,对象名自动生成:
{timestamp}_{rand6}/{baseName}.{ext}
timestamp— 上传时刻的北京时间,格式yyyyMMddHHmmssSSS(17 位数字)。rand6— 6 位随机字母数字(a-z, 0-9)。baseName— 原文件名去除扩展名,中文转无调拼音(通过pinyin-pro库),非法字符(非字母数字和下划线)被移除。.ext— 原文件扩展名保留。
示例:我的照片 (v2).jpg → 20260711234200000_a1b2c3/wodezhaopianv2.jpg
注:token 模式下 object key 由后端生成(UUID + 原扩展名),不走上述本地命名规则。
Content-Type 适配
上传时按 object 扩展名自动设置 Content-Type(参考后端 FileTypeEnum + RFC/IANA 注册类型补齐)。
- NOS 服务端有 Content-Type 白名单:白名单外的类型(即使 RFC 注册)会被 NOS 强制回落
application/octet-stream。故text/markdown、application/yaml、application/toml、application/sql等被拒的文本类型这里回落text/plain(RFC 2046,NOS 接受),使其在浏览器能在线渲染而非强制下载;font/*(二进制)保留 RFC 真值。 - 无扩展名 → 不设 Content-Type(交给 NOS 嗅探);未知扩展名 →
application/octet-stream。
NOS 签名说明
本工具不依赖 NOS 官方 SDK,签名逻辑完全自实现:
- 使用
HMAC-SHA256对规范字符串进行签名,输出 Base64。 - 规范字符串格式为
METHOD\nContent-MD5\nContent-Type\nDate\n[x-nos-* 头]\nResource,与 NOS 文档一致。其中x-nos-*头(如重命名用的x-nos-move-source)按头名排序后参与签名。 - Date 头使用上海时区(
Asia/Shanghai)的 RFC 822 格式,与官方 Java SDK 对齐。 - Resource(签名字符串中的对象路径)中的对象 key 做了 URL 编码,保证特殊字符不破坏签名。
- 部分子资源参与签名(如分片上传的
?uploads、?uploadId),而信息查询子资源(?imageInfo、?exif、?vinfo)不参与签名——这些差异均已通过真机验证确认。
签名实现已通过官方 NOS Java SDK 生成的签名向量做字节级交叉验证:向量固化在 test/fixtures/nos-sign-vectors.json,TypeScript 实现在同一输入上输出与官方 SDK 完全一致。所有对象操作(上传 / 删除 / 重命名 / 信息查询)均已针对真实 NOS 服务端到端验证通过。
开发 / 迭代
npm test # 运行单元测试(vitest)
npm run typecheck # TypeScript 类型检查
npm run build # 构建,产出 dist/cli.js
项目技术栈:TypeScript + commander(CLI 框架)+ undici(HTTP 客户端)+ vitest(测试框架)。
当前代码覆盖 55 个单元测试,涵盖签名、命名、配置、断点续传、并发池、端点探测、分片上传、URL 解析、对象操作、token 模式、mimetype、CDN 刷新等核心逻辑。
阶段二 (Go)
计划将相同的上传协议逻辑移植为 Go 单二进制工具,消除 Node.js 运行时依赖,实现更小的分发体积和更简单的部署。签名、续传、分片等已验证的协议逻辑将直接复用。