博客网盘 Skill 重构记录:从单功能上传到全能文件管理器
背景
我的博客是 Nuxt 3 + FastAPI 的全栈项目,带了一个完整的网盘功能(Pan),支持密码保护、下载限制、过期时间、文件夹分类。之前为了给 QQ 机器人提供文件发送能力,做了一个基础的 upload 脚本,让机器人能上传文件到网盘然后返回下载链接。
最初的版本很简单:一个 Python 脚本,硬编码传 upload,固定 10 次下载、1 天过期、8 位密码。SKILL.md 也是随便写的,描述上传流程,给机器人照做。
用了一段时间后,问题开始暴露。
需求升级
QQ 机器人上线后,用户(其实就是我)的请求越来越多样:
- "帮我上传这个文件" — upload,没问题
- "看看网盘里有什么" — 没命令,只能让机器人翻文件
- "123.txt 的链接给我" — 没命令,机器人答不上来,不知道是分享链接还是直链
- "之前那个文件的直链" — 没命令,机器人只能绕路
- "帮我改了那个文件的密码和下载次数" — 没命令
- "把这个文件删了" — 没命令
痛点很清晰:脚本只有 upload,但实际场景需要上传、查看、给链接、生成直链、修改、删除六种操作。每次要让机器人做 upload 之外的事,都需要手动修脚本或者人工处理。
设计思路:多模式 CLI
决定大改。参考已有的 article Skill(博客文章发布)的设计思路,把脚本改成多模式 CLI,用 argparse 子命令区分操作:
upload — 上传文件
list — 查看文件列表
share-link — 获取分享链接+密码
direct-link — 生成直链
delete — 删除文件
update — 修改属性
每个模式对应一个后端 API 端点,用 _request 统一封装 HTTP 请求。
默认值统一
之前的版本三方说法各不同:
- SKILL.md 写「下载次数 30 次,过期 30 天,密码 4 位」
- 脚本硬编码
download_limit=10, expire_days=1 - 后端 API 默认 0(不限制)
重构后统一为:30 次下载、30 天过期、4 位密码,写入脚本和 SKILL.md。
核心实现
认证复用
认证逻辑没变:从 env 文件读博客地址和账号密码,自动 /api/auth/login 获取 JWT token,内存缓存 7 天。所有请求共享同一个登录态。
关键改进是用 urllib.parse.urlencode 替换了原来的字符串拼接 query params,避免密码含 &、# 等特殊字符时 URL 截断。
文件查找:_resolve_file
六个命令里有四个(share-link、direct-link、delete、update)都需要先找到文件。提炼了 _resolve_file 函数:
def _resolve_file(query, folder=None) -> dict:
# 1. 纯数字 → 按 ID 精确匹配
# 2. 字符串 → 按文件名模糊搜索(跨所有文件夹)
# 3. 无匹配 → exit 并提示
# 4. 多匹配 → 列出所有让调用方选择
# 5. 唯一匹配 → 返回文件 dict
这个函数被四个命令共用,避免了重复代码。匹配逻辑先按 ID(数字)精确查,不行再按文件名关键字搜索,不区分大小写。如果搜到多个同名文件,列出 ID 和大小让用户选。
直链生成
后端直链接口 POST /api/pan/{id}/direct 每次调用都会生成一个新的 token(secrets.token_hex(32)),之前的不失效。所以每次用户要直链就调一次,后端生成新的,返回完整 URL。
对这个行为,SKILL.md 里注明:直链无密码保护,任何人可下载,仅在你要求时生成。
文件流式上传
原来的 multipart 构造是字符串+字节混拼:
body = ""
body += f"--{boundary}\r\n"
body_bytes = body.encode() + file_data + ...
重构后用纯 bytes 分段构建,逻辑更清晰:
parts = [
f"--{boundary}\r\n".encode(),
b"Content-Type: application/octet-stream\r\n\r\n",
]
with open(file_path, "rb") as f:
parts.append(f.read())
return b"".join(parts)
SKILL.md 迭代:从参数表格到决策树
一开始的 SKILL.md 是按命令组织的,每个命令配参数表格、示例输出。结果实际测试时,机器人拿着文档走流程发现几个问题。
问题一:4 个问题串行问太啰嗦
原 workflow 要求机器人依次问四项:
① 下载次数多少? ② 过期几天? ③ 自己设密码还是自动生成? ④ 要不要直链?
实际场景中,用户大部分时候想用默认值(30次/30天/自动密码),被问四次很烦。机器人自己优化成了"默认还是自定义?"一次问完,我采纳了这个改进写进 SKILL.md。
问题二:上传+直链分不清
用户说"帮我生成一个直链"但发了文件,原 workflow 走到了"直链"分支,调 direct-link 去查已有文件,绕了一圈。实际上用户是想上传新文件并直接拿直链。
修复后的决策树:
├─ 明确说"直链" →
│ ├─ 附带文件 → upload --direct(上传+直链一步到位)
│ └─ 没文件,只提文件名 → direct-link <文件名或ID>
问题三:关键词死板
只写了"链"/"分享"/"下载链接",用户说"发我""给个地址""提取这个文件"就不认识了。扩展了同义表达范围("存一下""发我""提取""地址""续期"等)。
最终结构
重构后的 SKILL.md 从 6KB 精简到 3.4KB,去掉了参数表格、示例输出这些机器人不需要的细节,保留核心:
- When to Use — 触发条件 + 同义表达示例
- Workflow — 决策树,机器人照着走
- Commands — 一行签名,带默认值
- Notes — 安全、行为说明
踩坑记录
urlencode 而不是字符串拼接
初版脚本手动拼 query string:
params = f"password={password}&download_limit={limit}"
如果密码包含 &、+、# 等字符,URL 会被截断。换成 urllib.parse.urlencode 后自动处理转义。
_resolve_file 的死代码
_resolve_file 内部处理了未找到和多个匹配的情况——都调 sys.exit()。但是调用方还写了一堆防御性检查:
matches = _resolve_file(...)
if not matches:
return
if isinstance(matches, list):
return
这些永远不会走到,纯属误导。重构后统一删掉,调用方直接拿结果用。
multipart 字符串+字节混拼
原始脚本用 body.encode() + file_data + ...encode() 构造 multipart,字符串到字节的转换点容易出错。改用纯 bytes 分段拼接后逻辑更清晰。
update --expire-days 是重置不是续期
最初没意识到后端 PUT /api/pan/{id} 传 expire_at 是直接重设过期时间,不是"在原来基础上延长"。如果用户说"再延 7 天",传 --expire-days 7 会从今天开始算 7 天,而不是从原过期日往后加。SKILL.md 里专门写了这点,机器人在用户说"续期"时需告知。
最终功能


upload <file>
[--password] [--password-length] [--download-limit 30]
[--expire-days 30] [--direct] [--folder robot_uploads]
list [--folder] [--search]
默认 robot_uploads,--folder . 表示根目录
share-link <query> [--folder]
返回分享链接+密码
direct-link <query> [--folder]
自动生成直链
delete <query> [--folder]
删除文件,操作不可逆
update <query>
[--password] [--download-limit] [--expire-days]
只改传了的字段
总结
这次重构的几个关键经验:
不要等到需求明确再设计:一开始只有上传,但多模式 CLI 的扩展成本很低——加一个子命令加一段 handler,不改现有逻辑。预留扩展点比之后大改省事得多。
Workflow 比 API 文档更重要:对 Agent 来说,知道"什么时候调用什么命令"比知道"每个命令有哪些参数"关键得多。决策树式的工作流让机器人在复杂对话中不迷路。
让机器人的使用反馈驱动优化:重构后的第一个版本 workflow 写得"完美",结果机器人实际跑了几轮就发现串行问参数很蠢。让实际对话数据说话,别坐在那里猜什么是最好的交互方式。
标准化错误处理节省大量时间:统一
_request封装 HTTP 请求和错误解析,统一_resolve_file处理文件查找的三种结果(找到/找不到/多匹配),四个命令白捡这些能力。Skil 文档的精简原则:每次加载 SKILL.md 都在消耗上下文的 Token。参数和示例输出是运行时可见的,不需要写在文档里;决策树和规则才是 AI Agent 真正需要的东西。
