1. 桌面文件发不出去,问题到底卡在哪
如果你正在用 OpenClaw 搭配 QQBot 做自动化,大概率遇到过这个场景:文件明明就在桌面上,路径复制得一字不差,结果一发送就报错Media path must be inside QQ Bot media storage。这个报错信息看起来像是权限问题,实际上跟权限一点关系都没有。
QQBot 的媒体发送机制有一套自己的规则:它只接受位于特定媒体存储目录内的文件,或者是一个它能直接下载的 HTTP(S) URL。你电脑上任意路径的文件,比如C:\Users\你的用户名\Desktop\report.pdf,对 QQBot 来说属于"非法输入"——不是文件不存在,而是这个路径不在它的可访问范围内。
这个设计本身有它的道理:QQBot 作为消息通道,需要确保发送的媒体文件来源可控、格式可预期。但对使用者来说,这就造成了一个体验断层——我明明有文件,为什么发不出去?
解决思路其实不复杂:在"本地文件"和"QQBot 发送"之间加一层中转。把文件先复制到 QQBot 认可的媒体目录,再用富媒体标签发送。手动做这件事当然可以,但每次都要记目录、复制、改路径,太繁琐。所以我把这个流程封装成了一个 Skill,叫qqbot-send,让整个链路自动化。
这篇文章会从报错现场开始,一步步带你配置 Skill、写 settings.json、跑通验证请求,最后把常见的坑列出来。目标很明确:让你桌面上的文件能直接发到 QQ,不用手动搬来搬去。
2. 前置准备:TaoToken 与 OpenClaw 环境确认
在动手改配置之前,先确认你的 OpenClaw 环境能正常调用模型。QQBot 的文件发送能力依赖 OpenClaw 的 Skill 机制,而 Skill 的执行又需要模型接口可用。如果你还没配置模型接入,可以先去 TaoToken 拿一个 API Key。
TaoToken 的定位是模型 API 聚合接入,支持 Claude、GPT 等主流模型的统一调用。对于 OpenClaw 这类需要频繁调用模型的工具来说,用聚合接口的好处是切换模型时不用改代码,只换 Key 和 base_url 就行。
具体操作:访问 https://taotoken.net/api 了解接口规范,然后到 https://taotoken.net/api-keys 创建一个 API Key。创建时注意选择对应的模型权限,OpenClaw 里如果用 Claude 系列做 Skill 调度,就确保 Key 有 Claude 的调用权限。
拿到 Key 之后,在 OpenClaw 的配置文件里填入。通常是在~/.openclaw/config.json或项目根目录的.env文件中设置:
{ "model": { "provider": "taotoken", "api_key": "sk-你的Key", "base_url": "https://taotoken.net/api", "model_name": "claude-sonnet-4-20250514" } }如果你用的是环境变量方式:
export TAOTOKEN_API_KEY="sk-你的Key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"配置完成后,可以先跑一个简单的对话测试,确认模型能正常响应。如果这一步就报错,先排查 Key 是否有效、base_url 是否写对。模型对话的在线测试入口在 https://taotoken.net/models ,可以直接在页面上验证 Key 的可用性。
环境确认没问题后,再进入 Skill 的配置环节。这一步看起来跟文件发送无关,但实际上是基础——Skill 的调度逻辑需要模型来解析用户意图,模型不通,后面都白搭。
3. qqbot-send Skill 配置骨架与 settings.json 片段
qqbot-send的核心逻辑分三步:识别文件来源、判断是否需要中转、执行发送。Skill 的配置文件需要定义触发条件、执行脚本路径、以及媒体目录的位置。
先看 Skill 的目录结构。在 OpenClaw 的 skills 目录下创建qqbot-send文件夹:
~/.openclaw/skills/ └── qqbot-send/ ├── skill.json ├── scripts/ │ └── stage_media.py └── README.mdskill.json是 Skill 的入口定义,内容如下:
{ "name": "qqbot-send", "version": "1.0.0", "description": "将本地文件中转至 QQBot 媒体目录并发送", "trigger": { "keywords": ["发送文件", "发到QQ", "send file", "qqbot send"], "patterns": ["发送.*文件", "把.*发到QQ"] }, "actions": { "stage_and_send": { "script": "scripts/stage_media.py", "args": ["{{source_path}}"], "description": "中转本地文件并发送" } }, "media": { "relay_dir": "~/.openclaw/media/qqbot/", "max_size_mb": 10, "allowed_extensions": [".png", ".jpg", ".jpeg", ".gif", ".pdf", ".zip", ".txt", ".docx", ".xlsx"] } }关键字段说明:relay_dir是 QQBot 认可的媒体中转目录,所有本地文件必须先复制到这里;max_size_mb限制单文件大小,超过 10MB 的文件会被拒绝;allowed_extensions定义可发送的文件类型,不在列表里的扩展名会触发错误提示。
接下来是stage_media.py脚本,负责实际的文件复制和中转:
#!/usr/bin/env python3 import os import sys import shutil from pathlib import Path RELAY_DIR = Path.home() / ".openclaw" / "media" / "qqbot" MAX_SIZE_MB = 10 def stage_media(source_path: str) -> str: src = Path(source_path).expanduser().resolve() if not src.exists(): raise FileNotFoundError(f"源文件不存在: {src}") if not src.is_file(): raise ValueError(f"路径不是文件: {src}") size_mb = src.stat().st_size / (1024 * 1024) if size_mb > MAX_SIZE_MB: raise ValueError(f"文件大小 {size_mb:.2f}MB 超过限制 {MAX_SIZE_MB}MB") RELAY_DIR.mkdir(parents=True, exist_ok=True) dest = RELAY_DIR / src.name if dest.exists(): stem = dest.stem suffix = dest.suffix counter = 1 while dest.exists(): dest = RELAY_DIR / f"{stem}_{counter}{suffix}" counter += 1 shutil.copy2(src, dest) return str(dest) if __name__ == "__main__": if len(sys.argv) < 2: print("用法: python stage_media.py <source_path>") sys.exit(1) try: result = stage_media(sys.argv[1]) print(f"STAGED:{result}") except Exception as e: print(f"ERROR:{e}") sys.exit(1)这个脚本做了几件事:检查源文件是否存在、验证大小、创建中转目录、处理文件名冲突、复制文件并返回目标路径。复制用shutil.copy2保留元数据,不修改原文件。
settings.json里需要注册这个 Skill,并配置 QQBot 的媒体发送参数:
{ "skills": { "qqbot-send": { "enabled": true, "path": "~/.openclaw/skills/qqbot-send", "auto_load": true } }, "qqbot": { "media_storage": "~/.openclaw/media/qqbot/", "rich_media_tag": "qqmedia", "send_timeout_seconds": 30 } }media_storage必须和 Skill 里的relay_dir保持一致,否则中转后的文件仍然不在 QQBot 的识别范围内。rich_media_tag定义发送时使用的标签名,默认是qqmedia。
配置写完后,重启 OpenClaw 让 Skill 加载生效。如果启动日志里看到Skill loaded: qqbot-send,说明注册成功。
4. 验证请求:从桌面选取文件到成功发送
配置完成后,跑一次完整的验证流程。我试过用一个桌面上的 PDF 文件来测试,步骤如下。
第一步,确认文件在桌面:
ls ~/Desktop/test-report.pdf输出应该显示文件存在。如果用的是 Windows 路径,在 OpenClaw 的终端里可能需要转换格式,比如C:\Users\你的用户名\Desktop\test-report.pdf对应 WSL 下的/mnt/c/Users/你的用户名/Desktop/test-report.pdf。
第二步,手动执行 stage 脚本,确认中转逻辑正常:
python ~/.openclaw/skills/qqbot-send/scripts/stage_media.py ~/Desktop/test-report.pdf预期输出:
STAGED:/home/你的用户名/.openclaw/media/qqbot/test-report.pdf如果输出ERROR:源文件不存在,检查路径是否正确;如果输出ERROR:文件大小超过限制,换一个小文件测试。
第三步,确认中转目录里文件已就位:
ls -la ~/.openclaw/media/qqbot/应该能看到test-report.pdf出现在列表里。
第四步,通过 OpenClaw 发送。在对话中输入:
把桌面上的 test-report.pdf 发到 QQOpenClaw 会解析意图,触发qqbot-sendSkill,执行 stage 脚本,然后用富媒体标签发送。发送成功后,QQ 端会收到文件消息。
如果你想手动构造发送请求来验证,可以直接调用 OpenClaw 的发送接口:
curl -X POST http://localhost:3000/api/send \ -H "Content-Type: application/json" \ -d '{ "type": "media", "target": "qqbot", "media_path": "~/.openclaw/media/qqbot/test-report.pdf", "tag": "qqmedia" }'返回{"status": "ok", "message_id": "xxx"}表示发送成功。如果返回Media path must be inside QQ Bot media storage,说明media_path没有指向中转目录,检查路径是否写错。
整个验证流程的核心是确认三件事:文件能复制到中转目录、中转目录在 QQBot 的媒体存储范围内、富媒体标签能正确引用中转后的路径。这三步都通过,文件直发链路就打通了。
5. 本篇常见错误排查
即使配置看起来没问题,实际跑的时候还是可能遇到各种报错。下面列出几个高频问题和对策。
报错一:Media path must be inside QQ Bot media storage
这是最典型的错误,说明发送时引用的路径不在 QQBot 的媒体目录内。排查步骤:确认settings.json里的media_storage和 Skill 里的relay_dir是否一致;确认 stage 脚本执行后文件确实复制到了中转目录;确认发送时用的是中转后的路径,而不是原始桌面路径。
报错二:FileNotFoundError: 源文件不存在
stage 脚本找不到源文件。常见原因是路径格式问题:Windows 路径在 WSL 环境下需要转换,中文路径可能需要加引号。另外,如果文件在 OneDrive 同步目录里,实际路径可能跟显示的不一样,用realpath命令确认真实路径。
报错三:文件大小超过限制
默认限制是 10MB,超过这个大小的文件会被拒绝。如果你需要发送更大的文件,可以调整skill.json里的max_size_mb字段,但要注意 QQBot 本身对媒体文件也有大小限制,调太大可能发送失败。建议先压缩文件,或者分卷发送。
报错四:Skill 加载失败,日志显示skill.json parse error
JSON 格式错误,通常是多了逗号、少了引号、或者注释没删干净。用python -m json.tool skill.json验证格式,会指出具体哪一行有问题。
报错五:发送成功但 QQ 端收不到文件
检查 QQBot 的媒体发送权限是否开启,有些机器人配置里默认关闭了富媒体发送。另外确认rich_media_tag的值和 QQBot 实际支持的标签一致,不同版本的 QQBot 可能用不同的标签名。
报错六:文件名冲突导致覆盖
如果中转目录里已经有同名文件,脚本会自动加数字后缀避免覆盖。但如果你手动复制文件到中转目录,可能会覆盖已有文件。建议统一走 stage 脚本,不要手动操作中转目录。
排查时的一个实用技巧:在 stage 脚本里加一行日志输出,把源路径和目标路径都打印出来,方便对比。如果问题出在发送环节,检查 OpenClaw 的日志里有没有qqmedia标签的解析记录。
6. 接入文档与后续扩展
qqbot-send解决的是"本地文件到 QQBot 媒体目录"这一层中转问题。如果你还需要处理更复杂的场景,比如从 URL 下载文件再发送、批量发送多个文件、或者根据文件类型自动选择发送方式,可以在现有 Skill 基础上扩展。
扩展的方向有几个:在 stage 脚本里增加 URL 下载逻辑,用requests库把远程文件拉到中转目录;增加批量处理模式,接受文件列表参数;增加文件类型判断,图片走图片通道、文档走文件通道。这些都可以在skill.json的actions里定义新的 action,然后在脚本里实现对应逻辑。
如果你在配置过程中遇到模型调用相关的问题,比如 Skill 调度时模型返回超时或权限错误,可以到 https://taotoken.net/doc 查看接口文档,确认 base_url 和模型名称是否匹配。长期做编码类任务的话,Coding Plan 的接入方式在 https://taotoken.net/coding-plan 有说明,适合需要频繁调用模型进行代码生成和调试的场景。
整个链路的核心思路其实就一句话:QQBot 不是不能发文件,它只是不能直接发任意路径的文件。补上"中转"这一层,问题就解决了。qqbot-send把这个中转过程自动化,让你不用每次手动复制文件、改路径。配置一次,后面直接说"把桌面上的文件发到 QQ"就行。