自动打包 skill 上传腾讯文档
把 ~/.workbuddy/skills/<技能名>/ 打成一个 zip,上传到腾讯文档的文件夹里存起来,供换机恢复或分享。
〇、使用前提(首次使用必看)
- 腾讯文档 MCP 连接器已授权——连接器授权是账号级的,必须由使用者本人完成,别人替不了。
没授权的话会在第一个接口调用就失败,先让用户去授权再跑。 - 先消歧账号体系:官方工具文档注明——若同时连着个人版(
tencent-docs)与企业版(tencent-docs-oa),
多数接口会要求先确认用哪个账号。别自己猜,先用AskUserQuestion问清"这次操作针对哪个账号"。
自己只连了个人版时跳过这一步。 - 目标目录的
folder_id跟账号走,不跟机器走。所以本技能不写死 ID,而是按下面的三级解析现取。
一、环境事实(照抄,别重新摸索)
1. 腾讯文档能存 zip
.zip 以「原始文件」形式存在腾讯文档里(ext=zip,链接形如 https://docs.qq.com/document/DRxxxx),可导出下载,不会被转成在线文档。上传后文件名带 .zip 后缀。
2. 上传三步(MCP 工具链)
| 步骤 | 调用 | 说明 |
|---|---|---|
| 1 | manage.pre_import |
传 file_name / file_size / file_md5,返回 upload_url(COS 预签名)/ file_key / task_id |
| 2 | HTTP PUT upload_url |
不经 MCP,本地脚本直传;返回 200 即上传成功 |
| 3 | manage.async_import |
传 file_key / file_name / file_md5 / file_size / task_id,触发导入 |
| 4 | manage.import_progress |
轮询 task_id,progress=100 时返回 file_id / file_url |
| 5 | manage.move_file |
file_id + target_folder_id 移入目标目录 |
> 导入落地的文件默认在首页根目录,必须再 move_file 才会进指定文件夹。
3. 三个必踩的坑
- 必须带环境 CA:本机 Python 直连出网会被代理自签证书拦截,报
SSLCertVerificationError: self-signed certificate in certificate chain。所有出网请求都要
ssl.create_default_context(cafile="/storage/Users/currentUser/.workbuddy/ssl/ca-bundle.pem")。 - 预签名 URL 只签了
content-length;host:PUT 时只发这两个头,别加Content-Type,否则可能签名不匹配;用http.client的putheader("Content-Length", ...)即天然满足。 - 本机
/tmp是只读的:打包、下载中间产物一律落到工作目录(如…/WorkBuddy工作区/Others/<日期任务>/),写/tmp会OSError: [Errno 30] Read-only file system。 - 签名 URL 有效期 1800 秒(30 分钟):实测本机出口带宽约 60 KB/s,84 MB 包要传 约 21 分钟,已贴着上限。大于 ~100 MB 的包不要走这条链(先裁剪,或拆成多个小包);一旦超时,必须重新
pre_import换新链接再传,旧链接直接 403。
4. 目标目录:三级解析(换用户/换账号也能用)
绝不要把 folder_id 当常量用——它是账号级的,我这个账号是 GjtEMqvxNCnV(ZY个人文档/AI_Skills),
别人用技能包时那个 ID 完全无效。按下面三级顺序解析,前面成功就不打扰用户:
| 级 | 条件 | 动作 | 用户感知 |
|---|---|---|---|
| ① 缓存 | folder.json 存在 |
query_folder_meta(folder_id=...) 校验 → 通过就用 |
完全无感 |
| ② 探测 | 无缓存 / 校验失败 | 从根 folder_list 按标题找,最多下钻 2 层,resolve_folder.py rank 打分排序 |
需要点选一次 |
| ③ 询问 | 探测无候选 | 提示 + 询问用户(见下面模板) | 需要决策一次 |
# ① 先看本地缓存(每次开跑第一步)
python3 scripts/resolve_folder.py status
# ② 探测:把 manage.folder_list 的返回存成 JSON,打分排序
python3 scripts/resolve_folder.py rank --input ./_folders.json
# → 输出推荐候选(★ 分级)+ CANDIDATES:<json>,推荐项排第一
# → ⚠ 返回 finish=false 说明还有下一页,必须带 start 继续翻再合并,否则可能漏掉目标目录
# ③ 用户点选后写回,之后永久静默
python3 scripts/resolve_folder.py save --id <folder_id> --title <标题> \
--path "ZY个人文档/AI_Skills" --account personal
# 账号换了 / 目录被删了重建 → 清缓存重新询问
python3 scripts/resolve_folder.py clear
探测与询问要点
- 根层 folder_list(不传 folder_id)返回的是文件夹与文件混排,先筛 is_folder == true。
- 目标通常不在根层而在下一层(如我的账号是 根 → ZY个人文档 → AI_Skills),所以要逐层下钻,
但别超过 2 层,否则接口调用量失控;resolve_folder.py rank 会给出该层的排序候选。
- query_folder_meta 会返回 parent_id,所以除了"从上往下找",也能"从下往上确认":
已知一个 folder_id 时可直接问出它挂在谁下面,用来验证解析出来的目录层级对不对。
实测:query_folder_meta("GjtEMqvxNCnV") → {"title":"AI_Skills","parent_id":"aE3367lv8i_100010"}。
- 目录里通常同时有同主题的 xxxSkill 子文件夹(说明性文档)与 xxx.zip(技能包本体),包放平级,别塞进子文件夹。
- search_file 只能作最后兜底:搜到的条目只给 file_id,且 zip 不支持 query_file_info,
别指望从 zip 反查父目录。
询问模板(用 AskUserQuestion,推荐项排第一,绝不让模型自己挑)
探测到候选时:
要把「<包名>.zip」放到哪个目录?
● AI_Skills (在「我的文档」下,检测到已有 3 个技能包)
○ <其它候选标题>
○ 不确定,我来指定 ← 保留手动输入
探测不到时(提示 + 询问,不擅自建目录):
没在你的腾讯文档里找到技能包存放目录,要怎么办?
○ 从根目录里挑一个现有目录
○ 先放在根目录(我会明确告诉你实际落点)
选定的目录必须是用户有写入权限的,否则 move_file 会失败——只读的分享目录不能选。
folder.json 是使用者本地配置
- 位置 <技能目录>/folder.json,内容为 folder_id / title / path / account / saved_at。
- 可用环境变量 SKILL_FOLDER_CONFIG 覆盖路径。
- ⚠ pack_skill.py 打包时会自动排除它(含 *.local.json),避免把我的目录 ID 随包发给别人。
所以别人拿到包后第一次必然走②③级、被问一次,之后才静默——这是设计行为,不是 bug。
换机恢复同理(自己也会被问一次),因为该文件不进包、也不进 git 镜像。
5. 需用户拍板的两件事(默认先问)
- 打包范围:旧包惯例是只含代码+文档(
SKILL.md+scripts/+references/,~50 KB);带data/里的数据 JSON 与核对表约 200 KB;含报价单原件等大文件约 84 MB(慢)。 - 同名旧包:覆盖(
manage.delete_file删旧 → 移入新包,沿用同名)还是保留旧包、新版加_YYYYMMDD后缀。
> 同名替换的正确顺序:先上传并确认 import_progress=100 → 再 delete_file 删旧 → 最后 move_file 新包进目录。反过来做,新包移入时会被自动改名为 xxx(1).zip。
二、用法
解析目标目录(每次开跑第一步)
S=~/.workbuddy/skills/自动打包skill上传腾讯文档/scripts
python3 $S/resolve_folder.py status # 有无缓存
python3 $S/resolve_folder.py rank --input ./_folders.json # 探测后打分排序
python3 $S/resolve_folder.py save --id <id> --title <标题> --account personal
python3 $S/resolve_folder.py clear # 换账号后重置
打包
cd "<工作目录>"
python3 ~/.workbuddy/skills/自动打包skill上传腾讯文档/scripts/pack_skill.py "食堂每日进货单核对" \
-o "./食堂每日进货单核对.zip"
# 输出:文件数 / 大小 / MD5 / 包内清单
# 自动排除 __pycache__、*.pyc、*.bak、*.tmp,以及使用者本地配置 folder.json / *.local.json
--no-data 只打代码与文档(对齐旧包惯例);默认全量。
上传
python3 ~/.workbuddy/skills/自动打包skill上传腾讯文档/scripts/put_upload.py \
--file "./食堂每日进货单核对.zip" \
--upload-url "<manage.pre_import 返回的 upload_url>"
# 带环境 CA、分块发送、打印进度;HTTP 200 即成功
完整流程(agent 执行)
- 先解析目标目录(第 4 节的三级流程):
resolve_folder.py status→ 有缓存就query_folder_meta校验,
没有/失效则从根探测 + 询问用户点选 →resolve_folder.py save。
这一步必须在move_file之前完成,且失败时不会浪费已上传的文件(前 4 步与账号无关)。 pack_skill.py打包 → 拿到file_size/file_md5。- 调 MCP
manage.pre_import(三个参数)→ 拿upload_url/file_key/task_id。 put_upload.pyPUT 上传。几 MB 以内前台直接跑(实测 7.7 KB 仅 0.1 秒);
几十 MB 以上才转后台任务,别占前台。- 调 MCP
manage.async_import→manage.import_progress轮询到 100。 - 先
folder_list(传第 0 步解析出的 folder_id)看有没有同名包:没有 = 首次上传,直接跳到第 6 步的move_file;
有 = 替换旧包,先manage.delete_file(delete_type=origin)删旧,再manage.move_file移新。 - 如需硬校验:
manage.export_file+export_progress拿直链后——
- 小包(≲10 MB)整包下载比对 MD5,最彻底(7.7 KB 秒级完成);
- 大包用Range只取首 4 字节与尾部 256 字节(PK\x03\x04头 + EOCD 条目数),
外加比对Content-Length与本地字节数 —— 秒级完成,不必整包重下。
三、注意事项
- 上传前确认包里没有敏感文件(token、密码、
config.json等,含folder.json——打包脚本已自动排除)。 - 打包用顶层目录名 = skill 名(
zip内路径技能名/…),解包即得到可用的 skill 目录。 - 换机恢复:下载 zip → 解包到
~/.workbuddy/skills/<技能名>/→ 直接可跑(首次会要求选一次上传目录)。 - 把技能分发给别人:他看不到我
AI_Skills里的 zip(那是私有目录),必须把 zip 单独发给他;
他解包 + 授权自己的腾讯文档连接器后,首次运行会走第 4 节的②③级、被问一次目录,之后才静默。 - 大文件仍按既有约定:工作区
skill_<技能名>备份/镜像里.gitignore排除大文件目录,云备份 zip 是另一条兜底路径。
技能改名时(改名后要重传)
云端包名 = zip 文件名 = 技能名,改名必然产生一个新包名,等于「上传新包 + 删掉旧包」,
不构成同名冲突,所以不适用"先删旧再移入"那条顺序约束。改名三件事,一件别落:
1. mv ~/.workbuddy/skills/<旧名> ~/.workbuddy/skills/<新名>(folder.json 缓存跟着目录走,不用重新解析目录);
2. SKILL.md 里替换全部旧名(frontmatter name:、一级标题、所有路径示例);
3. 工作区镜像 git mv skill_<旧名>备份 skill_<新名>备份 并同步内容。
> 脚本里绝不要硬编码技能名——只要 scripts/*.py 里没有旧名(grep 一下确认),改名就完全不用碰代码。