
Weiyun
- 8 installs
- 33 repo stars
- Updated April 26, 2026
- bighardperson/computer-science-skills-collection
Weiyun is a Claude skill and MCP integration that manages Tencent Weiyun cloud-drive files with list, upload, download, delete, and share tools.
About
Weiyun is an MCP skill for managing Tencent Weiyun cloud-drive files. It exposes six MCP tools to list, download, delete, upload, and generate share links, plus a version-check tool. Uploads use a two-stage protocol where the client computes streaming SHA1 block hashes locally before sending metadata, enabling instant dedup uploads. A developer uses it to script Weiyun file management or implement its upload protocol.
- Manages Tencent Weiyun cloud-drive files via 6 MCP tools
- list, download, delete, upload, and gen_share_link plus a version-check tool
- Two-stage upload with client-side streaming SHA1 block hashing
Weiyun by the numbers
- 8 all-time installs (skills.sh)
- Ranked #1,522 of 2,719 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Jul 30, 2026 (Skillselion catalog sync)
weiyun capabilities & compatibility
Uses a hosted Weiyun MCP server; a Weiyun account/token is needed to access the user's own drive.
- Capabilities
- file management · cloud storage · file sharing
- Use cases
- devops
- Runs
- Remote server
- Pricing
- Free
What weiyun says it does
Manage Tencent Weiyun cloud drive files (list, upload, download, delete, share)
提供微云(Weiyun)网盘通过 MCP 协议进行文件管理的完整能力,包含 **6 个 MCP Tool**
微云文件上传采用**两阶段协议**
文件哈希计算和 `block_sha_list` 生成**必须在客户端/本地完成**
npx skills add https://github.com/bighardperson/computer-science-skills-collection --skill weiyunAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 8 |
|---|---|
| repo stars | ★ 33 |
| Last updated | April 26, 2026 |
| Repository | bighardperson/computer-science-skills-collection ↗ |
What it does
Manage Tencent Weiyun cloud-drive files (list, upload, download, delete, share) through MCP tools.
Who is it for?
Scripting Tencent Weiyun cloud-drive file operations via MCP
Skip if: Downloading files from Weiyun share links; it only handles files in the user's own drive
When should I use this skill?
You need to list, upload, download, delete, or share Tencent Weiyun cloud-drive files
What you get
Programmatic Weiyun file management including uploads with instant dedup
- file list
- download links
- share links
By the numbers
- 6 MCP tools
- two-stage upload protocol
- 512KB (524288-byte) block size
Files
微云网盘 MCP 技能
版本更新检查(每天首次使用前必须执行)
每天使用本 SKILL 前进行一次更新检查,按照如下步骤执行:
1. 查看当前版本 version
读取本文件顶部 frontmatter 中的 version 字段;格式为 MAJOR.MINOR.PATCH。
2. 查看最新版本 latest
通过命令获取最新版本信息 latest,输入参数 version 为上一步获取的当前版本:
mcporter call "https://www.weiyun.com/api/v3/mcpserver" "check_skill_update" --args '{"version": "当前版本"}'JSON 格式数据返回,返回参数示例:
| 字段 | 类型 | 说明 |
|---|---|---|
latest | string | 最新版本号,格式为 MAJOR.MINOR.PATCH |
release_note | string | 最新版本发布说明 |
instruction | string | 更新指令 |
3. 更新版本
如果当前版本 version 低于最新版本 latest,则遵循 instruction 指令进行更新,或提示用户更新。
---
功能概述
提供微云(Weiyun)网盘通过 MCP 协议进行文件管理的完整能力,包含 6 个 MCP Tool:
| Tool 名称 | 功能 | 说明 |
|---|---|---|
weiyun.list | 目录列表查询 | 按目录查看文件和子目录,支持分页和排序 |
weiyun.download | 批量下载 | 批量获取文件的 HTTPS 下载链接 |
weiyun.delete | 批量删除 | 批量删除文件或目录,支持回收站或彻底删除 |
weiyun.upload | 文件上传 | 两阶段协议:预上传 + 分片上传,支持秒传 |
weiyun.gen_share_link | 生成分享外链 | 为文件或目录生成分享短链接 |
check_skill_update | 技能版本检查更新 | 检查当前 Skill 版本是否为最新,获取更新指令 |
核心架构原则:文件哈希计算和 block_sha_list 生成必须在客户端/本地完成。服务端只接收预计算好的哈希值,不会接收原始文件数据来计算哈希。这种设计是为了防止海量请求打爆服务器的存储和 CPU。
详细参考文档:
- MCP API 完整参考:
references/mcp_api.md(所有 Tool 的请求/响应字段详解) - 上传协议详解:
references/upload_protocol.md(两阶段上传、分块 SHA1 算法、check_sha/check_data 计算)
触发场景
- 使用微云 MCP 工具进行文件管理(查询、下载、删除、上传、分享)
- 上传文件到微云:优先使用
scripts/upload_to_weiyun.py一键完成,无需手动计算参数或调用 MCP - 实现或调试微云 MCP 文件上传(
weiyun.uploadTool) - 计算
block_sha_list、check_sha、check_data等上传参数 - 理解微云两阶段上传协议(预上传 → 分片上传)
- 检查技能版本更新(
check_skill_update) - 调试 FTN 上传错误或 SHA1 校验不匹配问题
接口一览
1. weiyun.list — 目录列表查询
查询微云网盘的目录内容,返回子目录和文件列表。
请求参数:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| get_type | uint32 | 否 | 获取类型:0-所有,1-仅目录,2-仅文件 |
| offset | uint32 | 否 | 分页起始偏移量,从 0 开始 |
| limit | uint32 | 是 | 每页返回数量,最大 50 |
| order_by | uint32 | 否 | 排序字段:0-不排序,1-按名字,2-按修改时间 |
| asc | bool | 否 | true-升序,false-降序(默认) |
| dir_key | string | 否 | 要查询的目录 key(hex 编码),为空则使用 token 绑定的 dirkey |
| pdir_key | string | 否 | 要查询的父目录 key(hex 编码),为空则使用 token 绑定的 pdirkey |
响应:返回 pdir_key(父目录 key)、dir_list(目录列表)、file_list(文件列表)、finish_flag(是否拉取完毕)。
注意:腾讯文档文件会被自动过滤,不出现在返回结果中。
2. weiyun.download — 批量下载
批量获取微云文件的 HTTPS 下载链接。
注意事项:
本功能无法下载微云分享的链接里面的文件,只能下载用户微云网盘中的文件。
请求参数:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| items | McpDownloadFileItem[] | 是 | 需要下载的文件列表 |
每个 McpDownloadFileItem 包含 file_id(文件 ID)和 pdir_key(所在目录 key),均为必填。
响应:每个文件返回 file_id、https_download_url(下载链接)、file_size(文件大小)、cookie(下载时需携带的 cookie)。
权限校验:只能下载当前用户拥有的文件(通过 pdir_key 判断目录所有权)。
3. weiyun.delete — 批量删除
批量删除微云网盘中的文件或目录。
请求参数:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| file_list | McpDeleteFileItem[] | 否 | 待删除的文件列表(file_id + pdir_key) |
| dir_list | McpDeleteDirItem[] | 否 | 待删除的目录列表(dir_key + pdir_key) |
| delete_completely | bool | 否 | false-移到回收站(默认),true-彻底删除 |
注意:file_list 和 dir_list 至少要填一个。
响应:返回 freed_space(释放的空间字节数)和 freed_index_cnt(删除的文件/目录总数)。
4. weiyun.gen_share_link — 生成分享外链
为微云文件或目录生成分享短链接。
请求参数:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| file_list | McpShareFileItem[] | 否 | 待分享的文件列表(file_id + pdir_key) |
| dir_list | McpShareDirItem[] | 否 | 待分享的目录列表(dir_key + pdir_key) |
| share_name | string | 否 | 分享名称,不填则使用第一个文件或目录名 |
注意:
file_list 和 dir_list 至少要填一个。
分享外链无法使用本 skill 进行下载,需要提示用户打开网页进行下载
⚠️ 关键:pdir_key 不能为空!
pdir_key必须使用weiyun.list响应中顶层的 `pdir_key`,而不是文件自身的pdir_key字段(该字段可能为空字符串)- 如果传空的
pdir_key,可能导致分享链接异常,强烈建议调用方显式传入正确的pdir_key - 错误示例:直接用
file_list[i].pdir_key(可能为空) - 正确示例:使用
weiyun.list响应顶层的pdir_key字段值
响应:返回 short_url(分享短链接)和 share_name(分享名称)。
5. weiyun.upload — 文件上传
微云文件上传采用两阶段协议:
阶段一:预上传
发送文件元数据和分块 SHA1 列表,检查是否可以秒传,或获取上传通道。
必填字段:filename、file_size、file_sha、block_sha_list、check_sha 可选字段:file_md5、check_data、pdir_key
关键行为:file_sha 必须等于 block_sha_list 的最后一个值,否则校验会失败。
响应判断:
file_exist=true→ 秒传成功,上传完毕file_exist=false→ 使用返回的upload_key、channel_list、ex进行分片上传
阶段二:分片上传
根据预上传返回的通道列表,逐片上传文件数据。
必填字段:upload_key、channel_list、channel_id、ex、file_data、filename
上传状态:
1= 继续上传下一分片2= 上传完成3= 等待其他通道完成
6. check_skill_update — 技能版本检查更新
检查当前 Skill 版本是否为最新,如有新版本则返回更新指令。
请求参数:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| version | string | 是 | 当前 Skill 版本号,格式 MAJOR.MINOR.PATCH |
响应:
| 字段 | 类型 | 说明 |
|---|---|---|
| latest | string | 最新版本号,格式 MAJOR.MINOR.PATCH |
| release_note | string | 最新版本发布说明 |
| instruction | string | 更新指令(需要更新时遵循此指令执行) |
注意:每天首次使用本 Skill 前必须执行一次版本检查(详见文档顶部「版本更新检查」章节)。
分块 SHA1 计算算法
这是上传功能最核心的部分。微云不使用标准的独立分块 SHA1,而是使用流式 SHA1 内部状态。
算法步骤(分块大小 = 512KB = 524288 字节)
1. 创建一个共享的 SHA1 哈希对象 2. 对于除最后一块之外的每个块:
- 读取 524288 字节并
update()到 SHA1 对象 - 提取 SHA1 内部寄存器(h0, h1, h2, h3, h4)以小端序输出
- 输出为 40 字符 hex 字符串 → 该块的
sha值
3. 对于最后一块(可能不足 524288 字节):
- 继续用相同 SHA1 对象 update 剩余数据
sha值为整个文件的标准 SHA1 hexdigest(大端序,含 finalization)
check_sha 和 check_data 计算
用于服务端防篡改验证:
lastBlockSize = file_size % 524288(若为 0 则取 524288)
checkBlockSize = lastBlockSize % 128(若为 0 则取 128)
check_sha:处理完所有非最后块后,继续 update 最后块中前 (lastBlockSize - checkBlockSize) 字节,
然后取 SHA1 内部寄存器 h0-h4 小端序输出为 hex
check_data:文件末尾 checkBlockSize 字节的 Base64 编码使用脚本
一键上传脚本(推荐)
直接上传本地文件到微云,整合了参数计算 + 预上传 + 分片上传的完整流程:
# 基本用法
python3 scripts/upload_to_weiyun.py /path/to/file --token <mcp_token> --env_id <env_id>
# 指定上传目录
python3 scripts/upload_to_weiyun.py /path/to/file --token <mcp_token> --pdir_key <dir_key>
# 使用环境变量
export WEIYUN_MCP_TOKEN=<mcp_token>
export WEIYUN_ENV_ID=<env_id>
python3 scripts/upload_to_weiyun.py /path/to/file脚本参数:
| 参数 | 必填 | 说明 |
|---|---|---|
file_path | 是 | 本地文件路径(位置参数) |
--token | 是 | MCP token(或设 WEIYUN_MCP_TOKEN 环境变量) |
--env_id | 否 | 环境标识(如 sit-0cd15bb3,或设 WEIYUN_ENV_ID) |
--pdir_key | 否 | 上传目标目录 key(不填使用 token 绑定目录) |
--mcp_url | 否 | MCP 服务地址(默认 https://www.weiyun.com/api/v3/mcpserver) |
--max_rounds | 否 | 最大上传轮数(默认 50) |
上传策略:循环「预上传获取通道 → 上传一片 → 重新预上传」直到完成。每次预上传会自动跳过已成功的分片(offset 随进度递增),支持秒传。
AI Agent 使用时:只需要 execute_command 运行此脚本即可,无需手动计算 block_sha_list 或调用 MCP。
参数计算脚本
仅计算上传参数(不执行上传),用于调试或手动调用 MCP:
python3 scripts/gen_block_info_list.py /path/to/file输出包括:block_sha_list、file_sha、file_md5、check_sha、check_data、block_size、block_count。
两个脚本均包含纯 Python 的 SHA1 实现,支持提取未经 finalization 的内部状态 — 这是 Python 标准库 hashlib.sha1 无法做到的。
错误码说明
MCP 接口在出现异常时会返回以下错误码,调用方可根据错误码进行相应处理:
| 错误码 | 名称 | 说明 |
|---|---|---|
| 117401 | ERR_RATE_LIMIT | 每日调用配额已耗尽,请明天再试 |
| 117402 | ERR_MCP_TOKEN_INVALID | MCP token 无效或已过期,请重新生成 token |
| 117403 | ERR_MCP_PARAM_EMPTY | 请求必填参数为空(如删除接口 file_list 和 dir_list 都为空) |
| 117404 | ERR_MCP_PARAM_INVALID | 请求参数不合法(如 file_id 或 pdir_key 格式错误) |
| 117405 | ERR_MCP_PERMISSION_DENIED | 无权操作非本人目录的文件 |
| 117406 | ERR_MCP_BACKEND_FAIL | 后端服务调用失败,请稍后重试 |
处理建议:
- 117401:等待次日零点配额自动重置,或开通微云会员提升配额
- 117402:重新生成 token
- 117403/117404:检查请求参数是否完整且格式正确
- 117405:确认操作的文件/目录属于当前用户
- 117406:属于服务端临时异常,可重试
常见操作工作流
工作流 1:查找并下载文件
当需要在微云中找到某个文件并下载到本地时,按以下步骤操作:
第一步:查询根目录
调用 weiyun.list,参数:limit=50, get_type=0- 响应中的
file_list包含文件,dir_list包含子目录 - 记住响应顶层的 `pdir_key`(后续下载需要用到)
- 如果文件在根目录 → 进入第三步
- 如果文件不在根目录 → 需要遍历子目录(第二步)
第二步:遍历子目录查找文件
调用 weiyun.list,参数:
dir_key = <子目录的 dir_key>(从 dir_list 中获取)
pdir_key = <子目录所在父目录的 pdir_key>(即上一次 list 响应顶层的 pdir_key,或子目录所在目录的 dir_key)
limit = 50⚠️ 关键:查询子目录时 dir_key 和 pdir_key 的含义:
dir_key:要查询的目标子目录的 key(从dir_list中的dir_key字段获取)pdir_key:该子目录所在的父目录 key(从上一级weiyun.list响应顶层的pdir_key获取)
如果还有嵌套子目录,递归重复此步骤。
第三步:获取下载链接
调用 weiyun.download,参数:
items = [{"file_id": "<文件的 file_id>", "pdir_key": "<文件所在目录的 pdir_key>"}]file_id:从file_list中获取pdir_key:使用weiyun.list响应中顶层的 `pdir_key`(不是文件自身的pdir_key字段)
第四步:下载文件到本地
curl -s -L -o <本地文件名> -b "<cookie>" "<https_download_url>"-L:跟随重定向(必须)-b:携带 cookie(从 download 响应中获取,格式如FTN5K=08bfd4be)- 下载完成后验证文件大小与
file_size一致
工作流 2:上传文件到微云
推荐方式(一键脚本):
# 1. 先查根目录获取 pdir_key
# 调用 weiyun.list limit=50 → 记住响应中的 pdir_key
# 2. 上传
python3 scripts/upload_to_weiyun.py /path/to/file --pdir_key <pdir_key>手动方式:参见上方「5. weiyun.upload — 文件上传」章节。
工作流 3:生成分享链接
# 1. 先查目录获取文件信息和 pdir_key
调用 weiyun.list → 找到目标文件的 file_id,记住响应**顶层** pdir_key
# 2. 生成分享链接(pdir_key 必须非空!)
调用 weiyun.gen_share_link,参数:
file_list = [{"file_id": "<file_id>", "pdir_key": "<响应顶层的 pdir_key>"}]
share_name = "<文件名>"⚠️ 关键:pdir_key 必须使用 weiyun.list 响应中顶层的 `pdir_key`,绝对不能传空字符串!文件项中的 pdir_key 字段可能为空,不可使用。
工作流 4:删除文件
# 1. 先查目录获取文件信息
调用 weiyun.list → 找到目标文件的 file_id,记住响应顶层 pdir_key
# 2. 删除
调用 weiyun.delete,参数:
file_list = [{"file_id": "<file_id>", "pdir_key": "<pdir_key>"}]
delete_completely = false (移到回收站,更安全)认证机制
所有 MCP 工具需要通过 WyHeader HTTP 头传递 mcp_token:
WyHeader: mcp_token=<token>常见问题
1. 上传文件应该怎么做:直接用 python3 scripts/upload_to_weiyun.py <文件路径> --pdir_key <目录key>,无需手动计算参数或调用 MCP 2. 下载时 pdir_key 应该填什么:使用 weiyun.list 响应中顶层的 `pdir_key`,而不是文件自身的 pdir_key 字段(该字段可能为空字符串) 3. 生成分享链接时 pdir_key 不能为空:必须先调用 weiyun.list,使用响应顶层的 `pdir_key`(不是 file_list[i].pdir_key,该字段通常为空)。pdir_key 为空会导致分享链接打开异常 4. 查询子目录时 pdir_key 怎么填:填入子目录所在父目录的 key。对于根目录下的子目录,就是根目录 weiyun.list 响应顶层的 pdir_key 5. 下载时需要携带 cookie:weiyun.download 返回的下载链接需要用 curl -b "<cookie>" 携带 cookie 值,同时 -L 跟随重定向 6. 上传报 "Cannot upload to a directory that you do not own":必须指定 --pdir_key 参数。先调用 weiyun.list 获取响应中顶层的 pdir_key 7. 分片上传通道 len=0:每轮上传完一片后,返回的通道列表可能全部 len=0,需要重新预上传获取下一批通道。upload_to_weiyun.py 已自动处理此问题 8. SHA1 不匹配:确保分块 SHA 值使用流式 SHA1 内部状态(小端序),而非独立分块 SHA1 9. file_sha 被覆盖:服务端用最后一个 block 的 SHA 覆盖 file_sha — 两者必须相等 10. Base64 双重编码:MCP 框架自动将 base64 字符串转为 bytes 传给 file_data 字段,服务端会再次进行 Base64 解码 11. 通道 ID 不匹配:上传分片时 channel_id 必须与 channel_list 中某个条目匹配 12. 环境标识:SIT 环境需在 Cookie 中携带 env_id=sit-xxxxx 13. 权限校验:下载、删除、分享操作会校验目录所有权,非本人目录的文件会被跳过 14. 腾讯文档过滤:列表查询会自动过滤腾讯文档类型的文件 15. pip install requests:上传脚本依赖 requests 库,如提示缺少请先安装:pip install requests 16. 所有需要 pdir_key 的操作(下载、删除、分享、上传),都应使用 weiyun.list 响应顶层的 pdir_key,而不是文件/目录条目自身的 pdir_key 字段
微云 MCP API 完整参考
服务信息
- MCP 服务地址:
https://www.weiyun.com/api/v3/mcpserver(Stateless HTTP 模式)
---
Tool: weiyun.list — 目录列表查询
查询微云网盘的目录内容,支持按目录查看文件和子目录,支持分页和排序。
请求(McpListReq)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| get_type | uint32 | 否 | 获取类型:0-所有(默认),1-仅目录,2-仅文件,其他值返回所有 |
| offset | uint32 | 否 | 分页起始偏移量,从 0 开始 |
| limit | uint32 | 是 | 每页返回数量,最大 50 |
| order_by | uint32 | 否 | 排序字段:0-不排序(默认),1-按名字排序,2-按修改时间排序 |
| asc | bool | 否 | 排序方向:true-升序,false-降序(默认) |
响应(McpListRsp)
| 字段 | 类型 | 说明 |
|---|---|---|
| pdir_key | string | 当前列表的父目录 key(hex 编码) |
| total_dir_count | uint32 | 子目录总数量 |
| total_file_count | uint32 | 文件总数量 |
| dir_list | McpDirItem[] | 目录列表 |
| file_list | McpFileItem[] | 文件列表 |
| finish_flag | bool | 是否已拉取完毕,true 表示已全部返回 |
| error | string | 错误信息,操作失败时返回具体的错误描述 |
McpDirItem(目录项)
| 字段 | 类型 | 说明 |
|---|---|---|
| dir_key | string | 目录 key(hex 编码) |
| dir_name | string | 目录名称 |
| dir_ctime | int64 | 目录创建时间,单位毫秒 |
| dir_mtime | int64 | 目录修改时间,单位毫秒 |
McpFileItem(文件项)
| 字段 | 类型 | 说明 |
|---|---|---|
| file_id | string | 文件唯一标识符 |
| filename | string | 文件名称 |
| file_size | int64 | 文件大小,单位字节 |
| file_ctime | int64 | 文件创建时间,单位毫秒 |
| file_mtime | int64 | 文件修改时间,单位毫秒 |
| pdir_key | string | 所在目录 key(hex 编码) |
注意事项
- 查询范围由
mcp_token绑定的dirkey和pdirkey决定 - 腾讯文档类型的文件会被自动过滤,不出现在返回结果中
---
Tool: weiyun.download — 批量下载
批量获取微云网盘文件的 HTTPS 下载链接。
请求(McpDownloadReq)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| items | McpDownloadFileItem[] | 是 | 需要获取下载链接的文件列表 |
McpDownloadFileItem(下载文件项)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| file_id | string | 是 | 文件唯一标识符 |
| pdir_key | string | 是 | 文件所在目录 key(hex 编码) |
响应(McpDownloadRsp)
| 字段 | 类型 | 说明 |
|---|---|---|
| items | McpDownloadResultItem[] | 下载结果列表 |
| error | string | 错误信息,操作失败时返回具体的错误描述 |
McpDownloadResultItem(下载结果项)
| 字段 | 类型 | 说明 |
|---|---|---|
| file_id | string | 文件唯一标识符 |
| https_download_url | string | HTTPS 下载链接 |
| file_size | int64 | 文件大小,单位字节 |
| cookie | string | 下载时需要携带的 cookie(格式:cookieName=cookieValue) |
注意事项
- 如果
pdir_key为空,使用 token 绑定的dirkey - 会校验目录所有权,非本人目录的文件会被跳过
- 下载链接无限速
---
Tool: weiyun.delete — 批量删除
批量删除微云网盘中的文件或目录,支持移动到回收站或彻底删除。
请求(McpDeleteReq)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| file_list | McpDeleteFileItem[] | 否 | 待删除的文件列表 |
| dir_list | McpDeleteDirItem[] | 否 | 待删除的目录列表 |
| delete_completely | bool | 否 | 是否彻底删除:false-移动到回收站(默认),true-彻底删除 |
注意:file_list 和 dir_list 至少要有一个非空。
McpDeleteFileItem(删除文件项)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| file_id | string | 是 | 文件唯一标识符 |
| pdir_key | string | 是 | 文件所在目录 key(hex 编码) |
McpDeleteDirItem(删除目录项)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| dir_key | string | 是 | 目录 key(hex 编码) |
| pdir_key | string | 是 | 父目录 key(hex 编码) |
响应(McpDeleteRsp)
| 字段 | 类型 | 说明 |
|---|---|---|
| freed_space | int64 | 释放的空间大小,单位字节 |
| freed_index_cnt | uint32 | 删除的文件数和目录数之和 |
| error | string | 错误信息,操作失败时返回具体的错误描述 |
注意事项
- 文件删除会校验目录所有权,非本人目录的文件会被跳过
- 删除之后会进回收站,一般7天内可以恢复,会员和超级会员则分别是 30 天和 90 天
---
Tool: weiyun.upload — 文件上传
上传文件到微云网盘,采用两阶段协议:预上传 + 分片上传,支持秒传。 注意: 除非用户提了明确的要求,修改文件名称,否则,尽量使用原本的文件名称进行上传。
请求(McpUploadReq)
预上传阶段(upload_key 为空)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| filename | string | 是 | 文件名称 |
| file_size | uint64 | 是 | 文件大小,单位字节 |
| file_sha | string | 是 | 整个文件的 SHA1 值(hex 编码,40 字符)。必须等于 block_sha_list 最后一个值 |
| block_sha_list | string[] | 是 | 每个分块的 SHA1 状态列表(hex 编码,每个 40 字符),按 512KB 分块顺序排列。计算方法详见 SKILL.md 中的分块 SHA1 算法 |
| check_sha | string | 是 | SHA1 校验中间状态(hex 编码,40 字符) |
| check_data | string | 否 | 文件末尾 checkBlockSize 字节的 Base64 编码 |
| file_md5 | string | 否 | 整个文件的 MD5 值(hex 编码,32 字符),用于秒传校验 |
| pdir_key | string | 否 | 上传到的目录 key(hex 编码),不填则使用 token 绑定的目录 |
分片上传阶段(upload_key 非空)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| upload_key | string | 是 | 预上传返回的 upload_key(hex 编码) |
| channel_list | McpChannelInfo[] | 是 | 预上传返回的通道列表 |
| channel_id | uint32 | 是 | 分片上传使用的通道编号 |
| ex | string | 是 | 预上传返回的扩展字段(hex 编码) |
| file_data | bytes | 是 | 当前分片的文件数据(二进制,JSON 传输时为 base64) |
| filename | string | 是 | 文件名称(与预上传相同) |
McpChannelInfo(上传通道信息)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | uint32 | 通道编号 |
| offset | uint64 | 分片在文件中的偏移量 |
| len | uint32 | 分片的数据长度 |
响应(McpUploadRsp)
| 字段 | 类型 | 说明 |
|---|---|---|
| file_id | string | 文件唯一标识符,预上传成功后返回 |
| filename | string | 上传后的文件名 |
| file_exist | bool | 是否秒传成功,true 表示文件已存在无需上传分片 |
| upload_state | int32 | 上传状态:1-需要上传下一分片,2-上传完成,3-等待其他通道完成 |
| upload_key | string | upload_key(hex 编码),后续分片上传时需携带 |
| channel_list | McpChannelInfo[] | 通道列表,用于后续分片上传 |
| ex | string | 扩展字段(hex 编码),后续分片上传时需携带 |
| error | string | 错误信息,操作失败时返回具体的错误描述 |
注意事项
- 预上传会校验目录所有权,非本人目录禁止上传
- 如果
pdir_key为空,使用 token 绑定的dirkey - 文件已存在时执行覆盖策略
- 默认使用多通道上传模式(最多 4 通道)
- 分片上传时
file_data经过 Base64 双重处理:MCP 框架自动 base64 → bytes,服务端再次进行 Base64 解码
---
Tool: weiyun.gen_share_link — 生成分享外链
为微云网盘中的文件或目录生成分享短链接。
请求(McpGenShareLinkReq)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| file_list | McpShareFileItem[] | 否 | 待分享的文件列表 |
| dir_list | McpShareDirItem[] | 否 | 待分享的目录列表 |
| share_name | string | 否 | 分享名称,不填则使用第一个文件或目录名 |
注意:file_list 和 dir_list 至少要有一个非空。
McpShareFileItem(分享文件项)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| file_id | string | 是 | 文件唯一标识符 |
| pdir_key | string | 是 | 文件所在目录 key(hex 编码)。必须使用 `weiyun.list` 响应顶层的 `pdir_key`,不可为空 |
McpShareDirItem(分享目录项)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| dir_key | string | 是 | 目录 key(hex 编码) |
| pdir_key | string | 是 | 父目录 key(hex 编码)。必须使用 `weiyun.list` 响应顶层的 `pdir_key`,不可为空 |
响应(McpGenShareLinkRsp)
| 字段 | 类型 | 说明 |
|---|---|---|
| short_url | string | 分享短链接 |
| share_name | string | 分享名称 |
| error | string | 错误信息,操作失败时返回具体的错误描述 |
注意事项
- ⚠️ pdir_key 不可为空:如果传入空的
pdir_key,可能导致分享链接异常,强烈建议调用方显式传入正确值 pdir_key应使用weiyun.list响应中顶层的 `pdir_key` 字段值,而非文件项中的pdir_key(该字段可能为空字符串)
---
Tool: check_skill_update — 技能版本检查更新
检查当前 Skill 版本是否有新版本可用,返回最新版本号、发布说明和更新指令。
请求(CheckSkillUpdateReq)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| version | string | 是 | 当前技能版本号,格式为 MAJOR.MINOR.PATCH |
响应(CheckSkillUpdateRsp)
| 字段 | 类型 | 说明 |
|---|---|---|
| latest | string | 最新版本号,格式为 MAJOR.MINOR.PATCH |
| release_note | string | 最新版本发布说明 |
| instruction | string | 更新指令,需要更新时遵循此指令执行 |
注意事项
- 如果当前已是最新版本,返回请求中传入的版本号
- 不需要认证:此接口不依赖
mcp_token,无需登录即可调用
---
认证机制
MCP Token
所有 MCP 工具(除 check_skill_update 外)需要通过 WyHeader HTTP 头传递 mcp_token:
WyHeader: mcp_token=<token>Token 可通过微云授权页面获取:https://www.weiyun.com/disk/authorization
微云上传协议参考
1. 上传流程概览
MCP 客户端 (AI Agent)
│
│ 1. 在本地计算 block_sha_list、check_sha、check_data
│ (使用 scripts/gen_block_info_list.py 或等价逻辑)
│
├─── 2. 预上传 ──→ weiyun.upload(upload_key="")
│ │
│ └── 返回: file_exist(是否秒传), upload_key, channel_list, ex
│
├─── 3. 分片上传 (循环) ──→ weiyun.upload(upload_key="xxx")
│ │
│ └── 返回: upload_state(1=继续/2=完成/3=等待), channel_list, ex
│
└─── 4. 完成 (upload_state == 2)关键原则:哈希计算在客户端完成。服务端在预上传阶段不接收原始文件数据用于哈希计算。
2. 预上传参数说明
预上传时需要提供以下参数(通过 weiyun.upload MCP Tool 传入):
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| filename | string | 是 | 文件名称 |
| file_size | uint64 | 是 | 文件大小(字节) |
| file_sha | string | 是 | 整个文件的 SHA1(40 字符 hex),必须等于 block_sha_list 最后一个值 |
| file_md5 | string | 否 | 整个文件的 MD5(32 字符 hex),用于秒传校验 |
| block_sha_list | string[] | 是 | 分块 SHA1 列表(每个 40 字符 hex) |
| check_sha | string | 是 | SHA1 校验中间状态(40 字符 hex) |
| check_data | string | 否 | 文件末尾校验字节的 Base64 编码 |
| pdir_key | string | 否 | 上传目标目录 key(不填使用 token 绑定目录) |
预上传响应判断
file_exist=true→ 秒传成功,上传完毕file_exist=false→ 使用返回的upload_key、channel_list、ex进行分片上传
3. 分片上传参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| upload_key | string | 是 | 预上传返回的 upload_key |
| channel_list | McpChannelInfo[] | 是 | 预上传返回的通道列表 |
| channel_id | uint32 | 是 | 使用的通道编号 |
| ex | string | 是 | 预上传返回的扩展字段 |
| file_data | bytes | 是 | 分片数据(JSON 传输时为 base64) |
| filename | string | 是 | 文件名称 |
上传状态
| 状态码 | 含义 |
|---|---|
| 1 | 需要上传下一分片 |
| 2 | 上传完成 |
| 3 | 等待其他通道完成 |
4. 分块 SHA1 算法 — 详细步骤
常量
BLOCK_SIZE = 524288(512KB)
文件大小派生变量
lastBlockSize = file_size % BLOCK_SIZE
如果 lastBlockSize == 0:lastBlockSize = BLOCK_SIZE
checkBlockSize = lastBlockSize % 128
如果 checkBlockSize == 0:checkBlockSize = 128
beforeBlockSize = file_size - lastBlockSize逐步算法
sha1 = new SHA1() // 单个共享的 SHA1 对象
// 第一步:处理最后一块之前的所有块
for offset in range(0, beforeBlockSize, BLOCK_SIZE):
data = read_file(offset, BLOCK_SIZE)
sha1.update(data)
block_sha_list.append(sha1.get_internal_state_little_endian())
// get_internal_state_little_endian() 返回 h0-h4 以小端序输出的 20 字节 hex 编码
// 第二步:处理最后一块的前半部分,计算 check_sha
between_data = read_file(beforeBlockSize, lastBlockSize - checkBlockSize)
sha1.update(between_data)
check_sha = sha1.get_internal_state_little_endian()
// 第三步:处理文件末尾的 checkBlockSize 字节
check_data_bytes = read_file(file_size - checkBlockSize, checkBlockSize)
sha1.update(check_data_bytes)
file_sha = sha1.hexdigest() // 标准 SHA1 含 finalization(大端序)
check_data = base64_encode(check_data_bytes)
// 第四步:最后一块的 sha = file_sha(不是内部状态!)
block_sha_list.append(file_sha)内部状态提取(get_state)
get_state() 函数读取 SHA1 内部寄存器 h0-h4(每个 32 位),以小端序字节顺序输出:
result = b""
for h in (h0, h1, h2, h3, h4):
result += struct.pack("<I", h) # 小端序 uint32
return result.hex() # 40 字符 hex 字符串重要:调用 get_state() 时,已处理的数据长度必须精确对齐到 64 字节边界(SHA1 block 大小)。BLOCK_SIZE 为 524288,能被 64 整除,因此所有非最后块都能保证对齐。
5. 常见错误码
上传过程中可能遇到的错误码:
| 错误码 | 说明 |
|---|---|
| -6014 | 空间不足 |
| -20001 | SHA1 校验错误 |
| -44001 | 参数无效 |
| -44003 | 权限不足 |
| -44010 | 文件名过长 |
| -89001 | 分片 SHA 校验失败 |
6. 使用建议
upload_key 和 ex
这两个是预上传返回的不透明字符串,分片上传时原样传递,不要额外做编码或解码。
推荐使用一键上传脚本
直接使用 scripts/upload_to_weiyun.py 可以跳过手动参数计算,一键完成上传:
python3 scripts/upload_to_weiyun.py /path/to/file --token <mcp_token>#!/usr/bin/env python3
"""
微云上传参数生成脚本 — 生成 block_sha_list、file_sha、check_sha、check_data 等上传参数。
与微云 FTN 预上传请求中的分块信息一致。
算法说明:
1. 用一个共享的 SHA1 对象流式(streaming)处理整个文件
2. 对每个非最后 block,通过 get_state() 获取 SHA1 内部中间状态(h0-h4,小端序输出)
3. 最后一个 block 的 sha 为标准 SHA1 hexdigest(带 finalization 的完整文件 SHA1,大端序)
4. check_sha 为最后一块数据去掉 checkBlockSize 之后的 SHA1 内部状态(小端序)
5. check_data 为文件末尾 checkBlockSize 字节的 Base64 编码
使用方法:
python3 gen_block_info_list.py <文件路径>
输出内容:
- file_sha:整个文件的标准 SHA1(40 字符 hex)
- file_md5:整个文件的 MD5(32 字符 hex)
- check_sha:SHA1 校验中间状态(40 字符 hex)
- check_data:文件末尾校验字节的 Base64 编码
- block_sha_list:可直接用于 McpUploadReq.block_sha_list 的列表
- 可直接用于 MCP 调用的预上传 JSON 片段
重要说明:
本脚本包含纯 Python SHA1 实现,支持提取未经 finalization 的内部寄存器状态。
这是 Python 标准库 hashlib.sha1 无法做到的,也是微云上传协议的核心需求。
"""
import json
import sys
import os
import base64
import struct
# ========== 纯 Python SHA1 实现(支持获取内部状态)==========
def _left_rotate(n, b):
"""32-bit 左旋转"""
return ((n << b) | (n >> (32 - b))) & 0xFFFFFFFF
class SHA1:
"""纯 Python SHA1 实现,支持获取内部中间状态(不做 finalization)"""
def __init__(self):
# SHA1 初始 h0-h4
self.h0 = 0x67452301
self.h1 = 0xEFCDAB89
self.h2 = 0x98BADCFE
self.h3 = 0x10325476
self.h4 = 0xC3D2E1F0
self._message_byte_length = 0
self._unprocessed = b""
def update(self, data):
"""往 SHA1 对象中追加数据"""
self._unprocessed += data
self._message_byte_length += len(data)
# 每 64 字节处理一次
while len(self._unprocessed) >= 64:
self._process_chunk(self._unprocessed[:64])
self._unprocessed = self._unprocessed[64:]
def _process_chunk(self, chunk):
"""处理一个 64 字节的 SHA1 block"""
assert len(chunk) == 64
w = [0] * 80
for i in range(16):
w[i] = struct.unpack(">I", chunk[i * 4:(i + 1) * 4])[0]
for i in range(16, 80):
w[i] = _left_rotate(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1)
a, b, c, d, e = self.h0, self.h1, self.h2, self.h3, self.h4
for i in range(80):
if 0 <= i <= 19:
f = (b & c) | ((~b) & d)
k = 0x5A827999
elif 20 <= i <= 39:
f = b ^ c ^ d
k = 0x6ED9EBA1
elif 40 <= i <= 59:
f = (b & c) | (b & d) | (c & d)
k = 0x8F1BBCDC
elif 60 <= i <= 79:
f = b ^ c ^ d
k = 0xCA62C1D6
temp = (_left_rotate(a, 5) + f + e + k + w[i]) & 0xFFFFFFFF
e = d
d = c
c = _left_rotate(b, 30)
b = a
a = temp
self.h0 = (self.h0 + a) & 0xFFFFFFFF
self.h1 = (self.h1 + b) & 0xFFFFFFFF
self.h2 = (self.h2 + c) & 0xFFFFFFFF
self.h3 = (self.h3 + d) & 0xFFFFFFFF
self.h4 = (self.h4 + e) & 0xFFFFFFFF
def get_state(self):
"""
获取 SHA1 内部状态(h0-h4),以小端序输出 20 字节的 hex 字符串。
注意:不做 padding/finalization,直接读取内部寄存器。
要求调用时 unprocessed 缓冲区为空(即已处理数据长度是 64 字节的整数倍)。
"""
assert len(self._unprocessed) == 0, \
f"get_state 要求 unprocessed 为空,当前有 {len(self._unprocessed)} 字节未处理"
result = b""
for h in (self.h0, self.h1, self.h2, self.h3, self.h4):
result += struct.pack("<I", h) # 小端序
return result.hex()
def hexdigest(self):
"""返回带 finalization 的标准 SHA1 digest(大端序,与 hashlib.sha1 一致)"""
# 先复制状态,不影响原对象
message_byte_length = self._message_byte_length
unprocessed = self._unprocessed
h0, h1, h2, h3, h4 = self.h0, self.h1, self.h2, self.h3, self.h4
# padding
unprocessed += b"\x80"
unprocessed += b"\x00" * ((56 - len(unprocessed) % 64) % 64)
unprocessed += struct.pack(">Q", message_byte_length * 8)
# 临时处理剩余 chunk
tmp = SHA1.__new__(SHA1)
tmp.h0, tmp.h1, tmp.h2, tmp.h3, tmp.h4 = h0, h1, h2, h3, h4
tmp._unprocessed = b""
tmp._message_byte_length = message_byte_length
while len(unprocessed) >= 64:
tmp._process_chunk(unprocessed[:64])
unprocessed = unprocessed[64:]
return "{:08x}{:08x}{:08x}{:08x}{:08x}".format(
tmp.h0, tmp.h1, tmp.h2, tmp.h3, tmp.h4)
# ========== 业务逻辑 ==========
BLOCK_SIZE = 524288 # 512KB,与正确请求体中的 block_size 一致
def gen_upload_params(file_path, block_size):
"""
生成上传所需的参数:
block_info_list、file_sha、check_sha、check_data
"""
file_size = os.path.getsize(file_path)
# 计算 lastBlockSize 和 checkBlockSize
last_block_size = file_size % block_size
if last_block_size == 0:
last_block_size = block_size
check_block_size = last_block_size % 128
if check_block_size == 0:
check_block_size = 128
before_block_size = file_size - last_block_size
block_info_list = []
sha1 = SHA1()
with open(file_path, "rb") as f:
# 处理除最后一块之外的所有 block
for offset in range(0, before_block_size, block_size):
data = f.read(block_size)
sha1.update(data)
block_info_list.append({
"sha": sha1.get_state(),
"offset": offset,
"size": block_size,
})
# 读取最后一块中去掉 checkBlockSize 之后的部分
between_data = f.read(last_block_size - check_block_size)
sha1.update(between_data)
check_sha = sha1.get_state()
# 读取 checkData 部分
check_data_bytes = f.read(check_block_size)
sha1.update(check_data_bytes)
file_sha = sha1.hexdigest()
check_data = base64.b64encode(check_data_bytes).decode("utf-8")
# 最后一个 block 的 sha 为 file_sha
block_info_list.append({
"sha": file_sha,
"offset": before_block_size,
"size": last_block_size,
})
return block_info_list, file_sha, check_sha, check_data
def gen_file_md5(file_path):
"""计算整个文件的 MD5"""
import hashlib
md5 = hashlib.md5()
with open(file_path, "rb") as f:
while True:
data = f.read(65536)
if not data:
break
md5.update(data)
return md5.hexdigest()
def main():
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <file_path>")
sys.exit(1)
file_path = sys.argv[1]
if not os.path.isfile(file_path):
print(f"Error: file not found: {file_path}")
sys.exit(1)
file_size = os.path.getsize(file_path)
filename = os.path.basename(file_path)
block_info_list, file_sha, check_sha, check_data = gen_upload_params(file_path, BLOCK_SIZE)
file_md5 = gen_file_md5(file_path)
print(f"file_path: {file_path}")
print(f"filename: {filename}")
print(f"file_size: {file_size}")
print(f"file_sha: {file_sha}")
print(f"file_md5: {file_md5}")
print(f"check_sha: {check_sha}")
print(f"check_data: {check_data}")
print(f"block_size: {BLOCK_SIZE}")
print(f"block_count: {len(block_info_list)}")
print()
# 输出 block_sha_list(MCP McpUploadReq 需要的格式)
block_sha_list = [b["sha"] for b in block_info_list]
print("block_sha_list (for McpUploadReq.block_sha_list):")
print(json.dumps(block_sha_list, indent=2))
print()
print("block_info_list (full detail):")
print(json.dumps(block_info_list, indent=4))
# 输出可直接用于 MCP 调用的 JSON 片段
print()
print("=== McpUploadReq pre-upload fields ===")
mcp_req = {
"filename": filename,
"file_size": file_size,
"file_sha": file_sha,
"file_md5": file_md5,
"block_sha_list": block_sha_list,
"check_sha": check_sha,
"check_data": check_data,
}
print(json.dumps(mcp_req, indent=2))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
微云通用上传脚本 — 一键上传本地文件到微云网盘。
整合了以下三个步骤:
1. 计算上传参数(block_sha_list、file_sha、check_sha、check_data、file_md5)
2. MCP 预上传(检查秒传或获取上传通道)
3. MCP 分片上传(循环「预上传获取通道 → 上传一片」直到完成)
使用方法:
python3 upload_to_weiyun.py <文件路径> [--pdir_key <目录key>]
环境变量(可选,也可通过命令行参数传入):
WEIYUN_MCP_URL — MCP 服务地址,默认 https://www.weiyun.com/api/v3/mcpserver
WEIYUN_MCP_TOKEN — MCP token,必须提供(命令行 --token 或环境变量)
WEIYUN_ENV_ID — 环境标识(如 sit-0cd15bb3),可选
示例:
# 使用命令行参数
python3 upload_to_weiyun.py /tmp/Test666.json --token 1fc54abae52bb44d4b8a421cc2734c04
# 使用环境变量
export WEIYUN_MCP_TOKEN=1fc54abae52bb44d4b8a421cc2734c04
python3 upload_to_weiyun.py /tmp/Test666.json
# 指定上传目录
python3 upload_to_weiyun.py /tmp/Test666.json --token xxx --pdir_key abc123
依赖:
pip install requests(如未安装)
重要说明:
本脚本包含纯 Python SHA1 实现,支持提取未经 finalization 的内部寄存器状态。
这是 Python 标准库 hashlib.sha1 无法做到的,也是微云上传协议的核心需求。
"""
import argparse
import base64
import hashlib
import json
import os
import struct
import sys
import time
try:
import requests
except ImportError:
print("错误:需要 requests 库,请执行 pip install requests")
sys.exit(1)
# ========== 纯 Python SHA1 实现(支持获取内部状态)==========
def _left_rotate(n, b):
"""32-bit 左旋转"""
return ((n << b) | (n >> (32 - b))) & 0xFFFFFFFF
class SHA1:
"""纯 Python SHA1 实现,支持获取内部中间状态(不做 finalization)"""
def __init__(self):
# SHA1 初始 h0-h4
self.h0 = 0x67452301
self.h1 = 0xEFCDAB89
self.h2 = 0x98BADCFE
self.h3 = 0x10325476
self.h4 = 0xC3D2E1F0
self._message_byte_length = 0
self._unprocessed = b""
def update(self, data):
"""往 SHA1 对象中追加数据"""
self._unprocessed += data
self._message_byte_length += len(data)
# 每 64 字节处理一次
while len(self._unprocessed) >= 64:
self._process_chunk(self._unprocessed[:64])
self._unprocessed = self._unprocessed[64:]
def _process_chunk(self, chunk):
"""处理一个 64 字节的 SHA1 block"""
assert len(chunk) == 64
w = [0] * 80
for i in range(16):
w[i] = struct.unpack(">I", chunk[i * 4:(i + 1) * 4])[0]
for i in range(16, 80):
w[i] = _left_rotate(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1)
a, b, c, d, e = self.h0, self.h1, self.h2, self.h3, self.h4
for i in range(80):
if 0 <= i <= 19:
f = (b & c) | ((~b) & d)
k = 0x5A827999
elif 20 <= i <= 39:
f = b ^ c ^ d
k = 0x6ED9EBA1
elif 40 <= i <= 59:
f = (b & c) | (b & d) | (c & d)
k = 0x8F1BBCDC
elif 60 <= i <= 79:
f = b ^ c ^ d
k = 0xCA62C1D6
temp = (_left_rotate(a, 5) + f + e + k + w[i]) & 0xFFFFFFFF
e = d
d = c
c = _left_rotate(b, 30)
b = a
a = temp
self.h0 = (self.h0 + a) & 0xFFFFFFFF
self.h1 = (self.h1 + b) & 0xFFFFFFFF
self.h2 = (self.h2 + c) & 0xFFFFFFFF
self.h3 = (self.h3 + d) & 0xFFFFFFFF
self.h4 = (self.h4 + e) & 0xFFFFFFFF
def get_state(self):
"""
获取 SHA1 内部状态(h0-h4),以小端序输出 20 字节的 hex 字符串。
注意:不做 padding/finalization,直接读取内部寄存器。
要求调用时 unprocessed 缓冲区为空(即已处理数据长度是 64 字节的整数倍)。
"""
assert len(self._unprocessed) == 0, \
f"get_state 要求 unprocessed 为空,当前有 {len(self._unprocessed)} 字节未处理"
result = b""
for h in (self.h0, self.h1, self.h2, self.h3, self.h4):
result += struct.pack("<I", h) # 小端序
return result.hex()
def hexdigest(self):
"""返回带 finalization 的标准 SHA1 digest(大端序,与 hashlib.sha1 一致)"""
# 先复制状态,不影响原对象
message_byte_length = self._message_byte_length
unprocessed = self._unprocessed
h0, h1, h2, h3, h4 = self.h0, self.h1, self.h2, self.h3, self.h4
# padding
unprocessed += b"\x80"
unprocessed += b"\x00" * ((56 - len(unprocessed) % 64) % 64)
unprocessed += struct.pack(">Q", message_byte_length * 8)
# 临时处理剩余 chunk
tmp = SHA1.__new__(SHA1)
tmp.h0, tmp.h1, tmp.h2, tmp.h3, tmp.h4 = h0, h1, h2, h3, h4
tmp._unprocessed = b""
tmp._message_byte_length = message_byte_length
while len(unprocessed) >= 64:
tmp._process_chunk(unprocessed[:64])
unprocessed = unprocessed[64:]
return "{:08x}{:08x}{:08x}{:08x}{:08x}".format(
tmp.h0, tmp.h1, tmp.h2, tmp.h3, tmp.h4)
# ========== 文件参数计算 ==========
BLOCK_SIZE = 524288 # 512KB
def calc_upload_params(file_path):
"""
计算微云上传所需的全部参数:
- block_sha_list:分块 SHA1 列表
- file_sha:整个文件的标准 SHA1
- file_md5:整个文件的 MD5
- check_sha:SHA1 校验中间状态
- check_data:文件末尾校验字节的 Base64 编码
返回 dict,可直接用于 MCP 预上传请求。
"""
file_size = os.path.getsize(file_path)
filename = os.path.basename(file_path)
# 计算 lastBlockSize 和 checkBlockSize
last_block_size = file_size % BLOCK_SIZE
if last_block_size == 0:
last_block_size = BLOCK_SIZE
check_block_size = last_block_size % 128
if check_block_size == 0:
check_block_size = 128
before_block_size = file_size - last_block_size
block_sha_list = []
sha1 = SHA1()
md5 = hashlib.md5()
with open(file_path, "rb") as f:
# 处理除最后一块之外的所有 block
for offset in range(0, before_block_size, BLOCK_SIZE):
data = f.read(BLOCK_SIZE)
sha1.update(data)
md5.update(data)
block_sha_list.append(sha1.get_state())
# 读取最后一块中去掉 checkBlockSize 之后的部分
between_data = f.read(last_block_size - check_block_size)
sha1.update(between_data)
md5.update(between_data)
check_sha = sha1.get_state()
# 读取 checkData 部分
check_data_bytes = f.read(check_block_size)
sha1.update(check_data_bytes)
md5.update(check_data_bytes)
file_sha = sha1.hexdigest()
check_data = base64.b64encode(check_data_bytes).decode("utf-8")
# 最后一个 block 的 sha 为 file_sha
block_sha_list.append(file_sha)
file_md5 = md5.hexdigest()
return {
"filename": filename,
"file_size": file_size,
"file_sha": file_sha,
"file_md5": file_md5,
"block_sha_list": block_sha_list,
"check_sha": check_sha,
"check_data": check_data,
}
# ========== MCP 调用 ==========
_request_id = 0
def mcp_call(mcp_url, headers, tool_name, arguments):
"""调用微云 MCP Tool,返回解析后的响应 dict"""
global _request_id
_request_id += 1
payload = {
"jsonrpc": "2.0",
"id": _request_id,
"method": "tools/call",
"params": {"name": tool_name, "arguments": arguments},
}
resp = requests.post(mcp_url, headers=headers, json=payload, timeout=120)
resp.raise_for_status()
result = resp.json()
content = result.get("result", {}).get("content", [])
for item in content:
if item.get("type") == "text":
return json.loads(item["text"])
# 如果没有 text 类型的 content,返回原始 result
return result
# ========== 上传核心逻辑 ==========
def upload_file(file_path, mcp_url, headers, pdir_key=None, max_rounds=50):
"""
上传文件到微云的完整流程:
1. 计算分块参数
2. 预上传(检查秒传)
3. 循环分片上传
参数:
file_path — 本地文件路径
mcp_url — MCP 服务地址
headers — HTTP 请求头(含 Cookie 和 WyHeader)
pdir_key — 上传目标目录 key(可选,不填使用 token 绑定的目录)
max_rounds — 最大上传轮数,防止死循环
返回:
成功时返回 dict: {"file_id": "...", "filename": "..."}
失败时抛出异常
"""
file_size = os.path.getsize(file_path)
filename = os.path.basename(file_path)
# 第一步:计算上传参数
print(f"[1/3] 计算上传参数: {filename} ({file_size} 字节)...")
params = calc_upload_params(file_path)
print(f" block_count={len(params['block_sha_list'])}, "
f"file_sha={params['file_sha'][:16]}..., "
f"file_md5={params['file_md5'][:16]}...")
# 构建预上传请求参数
pre_upload_args = {
"filename": params["filename"],
"file_size": params["file_size"],
"file_sha": params["file_sha"],
"file_md5": params["file_md5"],
"block_sha_list": params["block_sha_list"],
"check_sha": params["check_sha"],
"check_data": params["check_data"],
}
if pdir_key:
pre_upload_args["pdir_key"] = pdir_key
# 读取文件数据到内存(用于分片上传)
with open(file_path, "rb") as f:
file_data = f.read()
# 第二步 & 第三步:循环「预上传 → 上传一片」
print(f"[2/3] 开始上传...")
round_num = 0
while round_num < max_rounds:
round_num += 1
# 预上传,获取当前需要上传的通道
pre_rsp = mcp_call(mcp_url, headers, "weiyun.upload", pre_upload_args)
# 检查错误
if pre_rsp.get("error"):
raise RuntimeError(f"预上传失败: {pre_rsp['error']}")
# 检查秒传
if pre_rsp.get("file_exist", False):
file_id = pre_rsp.get("file_id", "")
fname = pre_rsp.get("filename", filename)
print(f" ✅ 秒传成功!file_id={file_id}")
return {"file_id": file_id, "filename": fname}
# 获取通道列表
ch_list = pre_rsp.get("channel_list", [])
uk = pre_rsp.get("upload_key", "")
ex = pre_rsp.get("ex", "")
# 找第一个 len > 0 的通道
ch = None
for c in ch_list:
if int(c.get("len", 0)) > 0:
ch = c
break
if ch is None:
state = int(pre_rsp.get("upload_state", 0))
if state == 2:
file_id = pre_rsp.get("file_id", "")
fname = pre_rsp.get("filename", filename)
print(f" ✅ 上传完成!file_id={file_id}")
return {"file_id": file_id, "filename": fname}
raise RuntimeError(f"无可上传通道,upload_state={state}")
offset = int(ch["offset"])
length = int(ch["len"])
channel_id = int(ch["id"])
actual_len = min(length, len(file_data) - offset)
progress_pct = min(100, int(offset / file_size * 100))
print(f" [{round_num}] 上传分片: offset={offset}, len={actual_len}, "
f"ch={channel_id}, progress≈{progress_pct}%")
# 准备分片数据
chunk = file_data[offset:offset + actual_len]
chunk_b64 = base64.b64encode(chunk).decode("utf-8")
cl = [{"id": int(c["id"]), "offset": int(c["offset"]), "len": int(c["len"])}
for c in ch_list]
# 上传分片
up_rsp = mcp_call(mcp_url, headers, "weiyun.upload", {
"filename": filename,
"file_size": file_size,
"file_sha": params["file_sha"],
"block_sha_list": [],
"check_sha": params["check_sha"],
"upload_key": uk,
"channel_list": cl,
"channel_id": channel_id,
"ex": ex,
"file_data": chunk_b64,
})
# 检查上传结果
if up_rsp.get("error"):
raise RuntimeError(f"分片上传失败: {up_rsp['error']}")
state = int(up_rsp.get("upload_state", 0))
if state == 2:
file_id = up_rsp.get("file_id", "")
fname = up_rsp.get("filename", filename)
print(f"[3/3] ✅ 上传完成!file_id={file_id}, filename={fname}")
return {"file_id": file_id, "filename": fname}
raise RuntimeError(f"超过最大上传轮数 ({max_rounds}),上传未完成")
# ========== 命令行入口 ==========
def main():
parser = argparse.ArgumentParser(
description="微云通用上传脚本 — 一键上传本地文件到微云网盘",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
%(prog)s /tmp/Test666.json --token 1fc54abae52bb44d4b8a421cc2734c04
%(prog)s /tmp/Test666.json --token xxx --pdir_key abc123
%(prog)s /tmp/Test666.json --token xxx --env_id sit-0cd15bb3
环境变量:
WEIYUN_MCP_URL MCP 服务地址(默认 https://www.weiyun.com/api/v3/mcpserver)
WEIYUN_MCP_TOKEN MCP token
WEIYUN_ENV_ID 环境标识
""",
)
parser.add_argument("file_path", help="要上传的本地文件路径")
parser.add_argument("--token", default=None,
help="MCP token(也可通过 WEIYUN_MCP_TOKEN 环境变量设置)")
parser.add_argument("--pdir_key", default=None,
help="上传目标目录 key(不填则使用 token 绑定的目录)")
parser.add_argument("--mcp_url", default=None,
help="MCP 服务地址(默认 https://www.weiyun.com/api/v3/mcpserver)")
parser.add_argument("--env_id", default=None,
help="环境标识(如 sit-0cd15bb3,也可通过 WEIYUN_ENV_ID 环境变量设置)")
parser.add_argument("--max_rounds", type=int, default=50,
help="最大上传轮数(默认 50)")
args = parser.parse_args()
# 校验文件存在
if not os.path.isfile(args.file_path):
print(f"错误:文件不存在: {args.file_path}")
sys.exit(1)
# 解析参数(命令行 > 环境变量 > 默认值)
mcp_url = args.mcp_url or os.environ.get("WEIYUN_MCP_URL", "https://www.weiyun.com/api/v3/mcpserver")
mcp_token = args.token or os.environ.get("WEIYUN_MCP_TOKEN")
env_id = args.env_id or os.environ.get("WEIYUN_ENV_ID")
if not mcp_token:
print("错误:必须提供 MCP token(--token 参数或 WEIYUN_MCP_TOKEN 环境变量)")
sys.exit(1)
# 构建 HTTP 请求头
headers = {
"Content-Type": "application/json",
"WyHeader": f"mcp_token={mcp_token}",
}
if env_id:
headers["Cookie"] = f"env_id={env_id}"
# 执行上传
print(f"=" * 60)
print(f"微云上传: {args.file_path}")
print(f"MCP URL: {mcp_url}")
print(f"Token: {mcp_token[:8]}...{mcp_token[-4:]}")
if args.pdir_key:
print(f"目标目录: {args.pdir_key}")
if env_id:
print(f"环境标识: {env_id}")
print(f"=" * 60)
start_time = time.time()
try:
result = upload_file(
file_path=args.file_path,
mcp_url=mcp_url,
headers=headers,
pdir_key=args.pdir_key,
max_rounds=args.max_rounds,
)
elapsed = time.time() - start_time
file_size = os.path.getsize(args.file_path)
speed = file_size / elapsed / 1024 if elapsed > 0 else 0
print(f"\n{'=' * 60}")
print(f"上传成功!")
print(f" 文件名: {result['filename']}")
print(f" 文件ID: {result['file_id']}")
print(f" 文件大小: {file_size} 字节")
print(f" 耗时: {elapsed:.1f} 秒")
print(f" 平均速度: {speed:.1f} KB/s")
print(f"{'=' * 60}")
except Exception as e:
elapsed = time.time() - start_time
print(f"\n上传失败(耗时 {elapsed:.1f} 秒): {e}")
sys.exit(1)
if __name__ == "__main__":
main()
Related skills
FAQ
How many MCP tools does Weiyun provide?
It provides six MCP tools: weiyun.list, weiyun.download, weiyun.delete, weiyun.upload, weiyun.gen_share_link, and check_skill_update for version checks.
How does Weiyun's upload work?
It uses a two-stage protocol: a preupload that sends file metadata and block SHA1 list to check for instant upload, then chunked upload of file data. File hashing must be computed on the client, not the server.