OpenClaw 收不到图片和语音?一篇讲清排查、修复与更稳妥的语音方案
如果你在用 OpenClaw 接 Telegram,很可能遇到过这样的情况:文字消息正常回复,但图片没反应,语音消息一直卡在"处理中"。
先看看效果?这是我打通之后的实测:

语音消息已经能正常转写并回复,识别效果在我的测试里甚至比 Telegram 自带的转写更准一些。下面把整个排查和修复过程掰开来讲。
这类问题看起来像一个故障,实际上经常是两段链路的问题叠在一起:前半段是 Telegram 媒体下载链路不稳定,后半段才是语音转写链路本身有没有跑通。
这篇文章不讲概念,直接给可复用的排障顺序和修复方法,重点解决下面这些问题:
- 怎么判断问题到底卡在媒体下载,还是卡在 ASR
- 图片和语音都处理不了时,OpenClaw 应该优先检查哪些配置
- 语音消息为什么会一直转圈,以及豆包 ASR 怎么接进来
- 这套 ASR 方案的局限在哪,今天更推荐什么替代方案
先判断:到底是哪一层坏了
先别急着改配置。最容易踩的坑,就是把“媒体下载失败”和“语音转写失败”混成一件事。
情况 A:Telegram 媒体下载链路有问题
如果图片和语音都不工作,最常见的根因是 Telegram 媒体下载链路不稳定。日志里通常会出现这些信息:
MediaFetchError ... TypeError: fetch failed
sendChatAction failed
getUpdates timed out
这类报错基本都指向网络层:代理、超时、DNS、重试策略,或者 IPv4/IPv6 选择异常。
之所以文字能发、图片和语音却不行,是因为媒体链路比纯文本多了几步。文字消息通常只要正常收发 API 请求就够了,而图片和语音还要经历 getFile、文件下载、再交给后续处理,链路更长,对网络稳定性的要求也更高。
情况 B:媒体已经下载成功,但 ASR 没跑通
如果你的现象是:
- 文字能回复
- 图片能处理
- 只有语音一直卡在处理中
那问题就不在 Telegram 下载链路了,而在 ASR 这一层。常见原因包括:
- OpenClaw 配置的转写命令不存在
- 本地 CLI 包装器执行失败
- 鉴权参数不对
- 豆包接口的 Resource ID 配错
- 转写结果没有成功写回
$OUTPUT
先把这两类问题区分开,后面的排查效率会高很多。
先看哪些日志,最快能定位
1. 先确认网关本身是不是活着
先执行:
openclaw gateway status
重点看这几项:
Service: LaunchAgent (loaded)Runtime: runningRPC probe: ok
如果这里已经是 not loaded、failed 或者 probe 不通,那就先别查图片和语音,优先把服务本身拉起来。
2. 用最小复现流程打日志
建议按这个顺序发消息:
- 发一条文字
- 发一张图片
- 发一条 3 到 5 秒的语音
然后马上去看日志。常用位置一般是:
/tmp/openclaw/openclaw-YYYY-MM-DD.log~/.openclaw/logs/gateway.log~/.openclaw/logs/gateway.err.log
直接搜这些关键词:
MediaFetchErrorfetch failedsendChatAction failedgetUpdates timed outaudiotranscriptioncli-audio-to-text
3. 先分层,再动手修
排查时建议只回答一个问题:问题到底停在了哪里?
- 如果在
MediaFetchError附近就失败了,先修媒体下载 - 如果媒体下载正常,后面卡在音频命令执行、轮询识别或写回结果,才去修 ASR
这个顺序非常重要。因为如果 Telegram 文件都没拉下来,后面 ASR 配得再好也没用。
图片和语音都不处理时,优先修 Telegram 媒体链路
如果图片和语音都挂了,通常可以先从 ~/.openclaw/openclaw.json 下手,显式把 Telegram 的网络参数配出来。
{
"channels": {
"telegram": {
"proxy": "http://127.0.0.1:7890",
"timeoutSeconds": 120,
"retry": {
"attempts": 8,
"minDelayMs": 1000,
"maxDelayMs": 15000,
"jitter": 0.2
},
"network": {
"autoSelectFamily": true
}
}
}
}
这里有两个经验点很关键:
channels.telegram.proxy尽量直接写http://或https://代理,不要直接塞socks5h://...- 不要只依赖系统环境变量代理,最好在 OpenClaw 这一层也显式配置
实际排障时,不建议一上来把所有参数都改一遍。更高效的方式是先改最关键的两个:
proxytimeoutSeconds
确认媒体下载恢复后,再去微调 retry 和 autoSelectFamily。
如果你是在群里测,还要额外查群策略
群聊还有一个很容易忽略的坑。假如日志里出现:
channels.telegram.groupPolicy is "allowlist" but groupAllowFrom is empty
这不是网络问题,而是群消息被静默丢弃了。你需要二选一:
- 配置
groupAllowFrom - 或者把
groupPolicy改成open
否则你会误以为“图片和语音没处理”,实际上是整条群消息根本没进处理流程。
语音一直处理中时,再去查 ASR
如果图片已经恢复,但语音还是卡住,就说明媒体下载大概率没问题,接下来该查音频转写链路。
我这边实际跑通的一套方案是:OpenClaw + 本地 CLI 包装器 + 火山引擎豆包录音文件识别 v3。
文档里提到的“录音文件识别 v3”和控制台里的“2.0-标准版”,在这个场景下可以理解为同一条可用能力链路。
不过这里要先说一个结论:这套方案能用,但并不是今天最优雅的方案。
为什么说“发语音再转写”并不优雅
很多人一开始会觉得,让用户继续发 Telegram 语音,然后后台自动转文字,是个自然的思路。但实际用起来,它有几个明显问题:
第一,发出去之后往往还得再回听一遍,确认一下录到的内容是不是完整、清楚、可用。毕竟麦克风收音、环境噪声、说话状态都会影响最终效果,不自己再检查一遍,很多时候心里并不踏实。
第二,链路偏长,整体会慢。它至少要经过这些步骤:
- 语音先上传到 Telegram
- OpenClaw 再去下载音频文件
- 音频再上传到转写服务
- 转写结果返回后再生成回复
这条链路天然比“边说边转文字,再直接发送文本”更慢。
所以如果你的目标不是“探索 ASR 接入”,而是“提升日常输入体验”,那现在更推荐的方案其实是直接用语音输入产品,在输入阶段就完成语音转文字,而不是先发语音,再调用豆包转写。
为什么我还是做了这套 ASR 方案
这里也想把背景交代清楚。
我这次最初要解决的问题,其实不是“我要做一个最好的语音输入方案”,而是 OpenClaw 在 Telegram 上看不到媒体内容,图片和语音都处理不了。修这个问题时,既然媒体链路已经顺手打通了,我就把 ASR 这一段也一并接上了。
另外,前一段时间我也确实在探索:如果没有特别顺手、跨端覆盖又好的语音输入产品,能不能先通过 ASR 这条技术路线补一条可用链路。这个方案就是在那个探索阶段做出来的。
但如果放到今天再看,我会更明确地说:
- 如果你只是想提升输入效率,优先选成熟的语音输入产品
- 如果你是在折腾 OpenClaw 的媒体能力,或者想自己掌控整条转写链路,那这套 ASR 方案依然有参考价值
现在更推荐的替代方案
现在更应该优先考虑成熟方案,按预算大致可以这样选:
- Typeless:土豪首选,多端覆盖最完整,PC、Mac、移动端都能用,整体体验最好,月费大约 20 美元
- 闪电说:经济适用,整体效果大概能做到
Typeless的 80%,但成本通常只有后者的十分之一。如果你手里已经有现成的 API,它的可扩展性会更强,比如可以自己接豆包的流式语音模型,再串大模型做文字润色、格式整理,整体性价比很高。 豆包输入法:完全免费,但目前只支持移动端
一句话概括:如果你的目标是“高频输入”,优先选语音输入;如果你的目标是“让机器人理解用户发来的语音媒体”,再考虑 ASR 转写链路。
OpenClaw 的音频转写接口是什么样的
OpenClaw 对外部音频转写器的期望其实很简单,它只认一个 CLI 接口:
Config ($INPUT) -> CLI -> transcription.txt ($OUTPUT)
也就是说,你最终要提供的是一个本地可执行命令,并且能接收这两个参数:
--input-file $INPUT--output-file $OUTPUT
OpenClaw 把音频文件路径传给你,你负责完成转写,再把结果写回输出文件。只要遵守这个接口,底层接豆包、Whisper,还是别的 ASR 服务,都可以自由替换。
豆包录音文件识别的接入思路
如果你还是想把这条链路接上,核心流程并不复杂:
- 把本地
.ogg文件上传到一个可公网访问的地址 - 调用提交接口:
/api/v3/auc/bigmodel/submit - 轮询查询接口:
/api/v3/auc/bigmodel/query - 拿到识别结果后写入
$OUTPUT
其中最关键的参数之一是:
Resource ID必须使用volc.seedasr.auc
如果这个值不对,后面即使请求格式看起来没问题,也很容易直接失败。
先说清楚:不要默认用 litterbox 做生产方案
原始方案里,我用的是 litterbox.catbox.moe 来临时托管音频文件,主要是因为它简单、快速、方便验证整条流程能不能通。
但这件事更适合放在实验和临时验证阶段,不适合默认当成长期方案。原因很直接:它本质上是第三方临时文件托管服务,你把用户的语音文件先上传到那里,再交给后续 ASR 服务处理,这里面会带来明显的安全和合规风险。
至少要注意这几个问题:
- 你无法真正控制文件存储位置、保留时间和访问策略
- 语音内容本身可能包含个人信息、业务信息,甚至敏感数据
- 第三方临时存储服务的可用性和稳定性并不受你控制
- 一旦你把这个方案扩展到多人使用场景,风险会进一步放大
如果只是自己做本地实验,短时间验证流程,问题还可控;但只要准备长期使用,或者准备给别人用,我都不建议继续依赖 litterbox。
更稳妥的替代思路
更推荐的做法是把“临时文件托管”换成你自己可控的对象存储。
常见选择有两个:
- 用你自己的 OSS / S3 兼容存储
- 用
Cloudflare R2搭一个轻量、低成本的临时文件存储层
这两种方案的好处都很明确:
- 你能自己控制桶权限、生命周期和删除策略
- 可以给对象设置短时签名 URL,而不是把文件长期裸露在公网
- 更容易加审计、限时清理和访问控制
- 后续如果要扩展成稳定服务,也更容易演进
如果只是为了让豆包拉到一个短时可访问的音频地址,我建议至少做到下面这几个点:
- 文件上传后自动设置过期删除
- 默认私有桶,不公开列目录
- 通过预签名 URL 提供短时间下载权限
- 不在日志里打印完整敏感 URL
这样虽然比 litterbox 多一点配置成本,但安全性和可控性会好很多。
OpenClaw 配置示例
下面是 OpenClaw 里可工作的音频配置示例:
{
"tools": {
"media": {
"audio": {
"models": {
"doubao": {
"type": "cli",
"command": "/your_user_root/.nvm/versions/node/v24.12.0/bin/node /your_user_root/.nvm/versions/node/v24.12.0/lib/node_modules/openclaw/dist/cli-audio-to-text.js --exec \"python3 /your_user_root/.openclaw/workspace/asr_cli.py --input-file \$INPUT --output-file \$OUTPUT\"",
"patterns": ["*.ogg"]
}
},
"defaultModel": "doubao"
}
}
}
}
这里最重要的不是照抄路径,而是理解它的职责分层:
- OpenClaw 负责把音频文件交给 CLI
cli-audio-to-text.js负责把输入输出协议适配好- 你自己的
asr_cli.py负责真正调用豆包接口并回写结果
只要这一层关系清楚,后面替换成别的 ASR 服务也很容易。
这几个故障点最常见
1. 401 Unauthorized
优先检查 AppID 和 Access Token。这类错误很多时候不是接口逻辑有问题,而是凭证填错、复制错,或者大小写看错了。
2. 1001 Requested resource not granted
这通常意味着权限没开通,或者 Resource ID 不匹配。先确认你走的是 v3 链路,再确认 volc.seedasr.auc 配置无误。
3. 语音长时间处于处理中
先不要急着怀疑豆包接口,先验证你本地配置的命令是否真的存在、能不能独立跑通。
可以直接手工测试:
python3 asr_cli.py --input-file /path/to/test.ogg --output-file /tmp/asr.txt
如果单独执行都跑不通,OpenClaw 里当然也不会成功。
4. 代理环境下 SSL 报错
如果你本地代理会劫持证书,Python 请求阶段可能遇到 SSL 相关错误。必要时可以通过调整证书校验方式规避本地代理干扰,但更推荐优先修正本地代理和证书环境,而不是把“关闭校验”当成默认方案。
推荐的排障顺序
以后只要再遇到“图片不处理”“语音一直转圈”,我建议都按这个顺序查:
- 先跑
openclaw gateway status,确认服务在线 - 查日志里有没有
MediaFetchError,先判断是不是媒体下载层故障 - 修
channels.telegram.proxy、timeoutSeconds、retry、autoSelectFamily - 如果在群聊测试,额外检查
groupPolicy和groupAllowFrom - 媒体下载恢复后,再单独验证 ASR 命令能否手工执行
- 最后再校验豆包鉴权、
Resource ID和输出文件写回是否成功
这个顺序的意义在于:先修公共链路,再修语音转写。否则很容易在后半段白费时间。
怎么判断自己真的修好了
修完后,建议至少做这三组测试:
- 发文字,确认能立即回复
- 发图片,确认能触发处理并返回结果
- 发 3 到 5 秒语音,确认能在可接受时间内完成转写并回复
如果文字和图片都好了,只有语音还在卡,那就继续盯 ASR;如果三种消息都不正常,就不要绕回 ASR,先回到网关和 Telegram 网络链路。
实测效果和完整配置往下看。
附录
附录 A:脚本与命令
附录 A:脚本与命令
下面是我实际跑通的两段脚本,Token、AppID 和本机路径都做了脱敏处理。你可以按自己的环境替换占位符。
asr_doubao.py
#!/usr/bin/env python3
"""
Volcengine Doubao ASR (录音文件识别 v3)
submit + query workflow
"""
import requests
import time
import os
# ========== 配置区(替换成你的) ==========
APPID = "<YOUR_APPID>"
ACCESS_TOKEN = "<YOUR_ACCESS_TOKEN>"
RESOURCE_ID = "volc.seedasr.auc"
SUBMIT_URL = "https://openspeech.bytedance.com/api/v3/auc/bigmodel/submit"
QUERY_URL = "https://openspeech.bytedance.com/api/v3/auc/bigmodel/query"
# ======================================
def upload_to_litterbox(audio_path):
"""Upload audio to temporary storage (litterbox.catbox.moe)"""
with open(audio_path, "rb") as f:
files = {"fileToUpload": (os.path.basename(audio_path), f, "audio/ogg")}
data = {"reqtype": "fileupload", "time": "1h"}
try:
resp = requests.post(
"https://litterbox.catbox.moe/resources/internals/api.php",
data=data,
files=files,
timeout=30,
)
if resp.status_code == 200:
return resp.text.strip()
except Exception as exc:
print(f"Upload error: {exc}")
return None
def submit_task(audio_url, enable_itn=True, enable_punc=True):
headers = {
"Content-Type": "application/json",
"X-Api-App-Key": APPID,
"X-Api-Access-Key": ACCESS_TOKEN,
"X-Api-Resource-Id": RESOURCE_ID,
"X-Api-Request-Id": f"openclaw_{int(time.time() * 1000)}",
"X-Api-Sequence": "-1",
}
payload = {
"user": {"uid": "openclaw_bot"},
"audio": {"format": "ogg", "url": audio_url},
"request": {
"model_name": "bigmodel",
"enable_itn": enable_itn,
"enable_punc": enable_punc,
},
}
resp = requests.post(SUBMIT_URL, headers=headers, json=payload, timeout=10)
status = resp.headers.get("X-Api-Status-Code")
if status == "20000000":
return headers["X-Api-Request-Id"]
print(f"Submit failed: {status}")
return None
def query_task(task_id):
headers = {
"Content-Type": "application/json",
"X-Api-App-Key": APPID,
"X-Api-Access-Key": ACCESS_TOKEN,
"X-Api-Resource-Id": RESOURCE_ID,
"X-Api-Request-Id": task_id,
}
resp = requests.post(QUERY_URL, headers=headers, json={}, timeout=10)
status = resp.headers.get("X-Api-Status-Code")
if status == "20000000":
data = resp.json()
return data.get("result", {}).get("text", "")
return None
def transcribe(audio_path):
print(f"Uploading {audio_path}...")
audio_url = upload_to_litterbox(audio_path)
if not audio_url:
print("Failed to upload audio")
return None
print("Submitting transcription task...")
task_id = submit_task(audio_url)
if not task_id:
print("Failed to submit task")
return None
for _ in range(30):
time.sleep(2)
text = query_task(task_id)
if text:
return text
return None
if __name__ == "__main__":
audio_file = "/path/to/test.ogg"
result = transcribe(audio_file)
if result:
print(result)asr_cli.py
#!/usr/bin/env python3
"""CLI wrapper for OpenClaw audio transcription."""
import sys
import argparse
sys.path.insert(0, "/your_user_root/.openclaw/workspace")
from asr_doubao import transcribe
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--input-file", required=True)
parser.add_argument("--output-file", required=True)
args = parser.parse_args()
result = transcribe(args.input_file)
if result:
with open(args.output_file, "w", encoding="utf-8") as f:
f.write(result)
print(f"Transcription saved to {args.output_file}")
else:
with open(args.output_file, "w", encoding="utf-8") as f:
f.write("")
print("Transcription failed")
if __name__ == "__main__":
main()
