OpenClaw 收不到图片和语音?一篇讲清排查、修复与更稳妥的语音方案

OpenClawTelegramASR排障

如果你在用 OpenClaw 接 Telegram,很可能遇到过这样的情况:文字消息正常回复,但图片没反应,语音消息一直卡在"处理中"。

先看看效果?这是我打通之后的实测:

语音消息已经能正常转写并回复,识别效果在我的测试里甚至比 Telegram 自带的转写更准一些。下面把整个排查和修复过程掰开来讲。

这类问题看起来像一个故障,实际上经常是两段链路的问题叠在一起:前半段是 Telegram 媒体下载链路不稳定,后半段才是语音转写链路本身有没有跑通。

这篇文章不讲概念,直接给可复用的排障顺序和修复方法,重点解决下面这些问题:

  1. 怎么判断问题到底卡在媒体下载,还是卡在 ASR
  2. 图片和语音都处理不了时,OpenClaw 应该优先检查哪些配置
  3. 语音消息为什么会一直转圈,以及豆包 ASR 怎么接进来
  4. 这套 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: running
  • RPC probe: ok

如果这里已经是 not loadedfailed 或者 probe 不通,那就先别查图片和语音,优先把服务本身拉起来。

2. 用最小复现流程打日志

建议按这个顺序发消息:

  1. 发一条文字
  2. 发一张图片
  3. 发一条 3 到 5 秒的语音

然后马上去看日志。常用位置一般是:

  • /tmp/openclaw/openclaw-YYYY-MM-DD.log
  • ~/.openclaw/logs/gateway.log
  • ~/.openclaw/logs/gateway.err.log

直接搜这些关键词:

  • MediaFetchError
  • fetch failed
  • sendChatAction failed
  • getUpdates timed out
  • audio
  • transcription
  • cli-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
      }
    }
  }
}

这里有两个经验点很关键:

  1. channels.telegram.proxy 尽量直接写 http://https:// 代理,不要直接塞 socks5h://...
  2. 不要只依赖系统环境变量代理,最好在 OpenClaw 这一层也显式配置

实际排障时,不建议一上来把所有参数都改一遍。更高效的方式是先改最关键的两个:

  • proxy
  • timeoutSeconds

确认媒体下载恢复后,再去微调 retryautoSelectFamily

如果你是在群里测,还要额外查群策略

群聊还有一个很容易忽略的坑。假如日志里出现:

channels.telegram.groupPolicy is "allowlist" but groupAllowFrom is empty

这不是网络问题,而是群消息被静默丢弃了。你需要二选一:

  • 配置 groupAllowFrom
  • 或者把 groupPolicy 改成 open

否则你会误以为“图片和语音没处理”,实际上是整条群消息根本没进处理流程。

语音一直处理中时,再去查 ASR

如果图片已经恢复,但语音还是卡住,就说明媒体下载大概率没问题,接下来该查音频转写链路。

我这边实际跑通的一套方案是:OpenClaw + 本地 CLI 包装器 + 火山引擎豆包录音文件识别 v3。

文档里提到的“录音文件识别 v3”和控制台里的“2.0-标准版”,在这个场景下可以理解为同一条可用能力链路。

不过这里要先说一个结论:这套方案能用,但并不是今天最优雅的方案。

为什么说“发语音再转写”并不优雅

很多人一开始会觉得,让用户继续发 Telegram 语音,然后后台自动转文字,是个自然的思路。但实际用起来,它有几个明显问题:

第一,发出去之后往往还得再回听一遍,确认一下录到的内容是不是完整、清楚、可用。毕竟麦克风收音、环境噪声、说话状态都会影响最终效果,不自己再检查一遍,很多时候心里并不踏实。

第二,链路偏长,整体会慢。它至少要经过这些步骤:

  1. 语音先上传到 Telegram
  2. OpenClaw 再去下载音频文件
  3. 音频再上传到转写服务
  4. 转写结果返回后再生成回复

这条链路天然比“边说边转文字,再直接发送文本”更慢。

所以如果你的目标不是“探索 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 服务,都可以自由替换。

豆包录音文件识别的接入思路

如果你还是想把这条链路接上,核心流程并不复杂:

  1. 把本地 .ogg 文件上传到一个可公网访问的地址
  2. 调用提交接口:/api/v3/auc/bigmodel/submit
  3. 轮询查询接口:/api/v3/auc/bigmodel/query
  4. 拿到识别结果后写入 $OUTPUT

其中最关键的参数之一是:

  • Resource ID 必须使用 volc.seedasr.auc

如果这个值不对,后面即使请求格式看起来没问题,也很容易直接失败。

先说清楚:不要默认用 litterbox 做生产方案

原始方案里,我用的是 litterbox.catbox.moe 来临时托管音频文件,主要是因为它简单、快速、方便验证整条流程能不能通。

但这件事更适合放在实验和临时验证阶段,不适合默认当成长期方案。原因很直接:它本质上是第三方临时文件托管服务,你把用户的语音文件先上传到那里,再交给后续 ASR 服务处理,这里面会带来明显的安全和合规风险。

至少要注意这几个问题:

  • 你无法真正控制文件存储位置、保留时间和访问策略
  • 语音内容本身可能包含个人信息、业务信息,甚至敏感数据
  • 第三方临时存储服务的可用性和稳定性并不受你控制
  • 一旦你把这个方案扩展到多人使用场景,风险会进一步放大

如果只是自己做本地实验,短时间验证流程,问题还可控;但只要准备长期使用,或者准备给别人用,我都不建议继续依赖 litterbox。

更稳妥的替代思路

更推荐的做法是把“临时文件托管”换成你自己可控的对象存储。

常见选择有两个:

  1. 用你自己的 OSS / S3 兼容存储
  2. 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

优先检查 AppIDAccess 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 相关错误。必要时可以通过调整证书校验方式规避本地代理干扰,但更推荐优先修正本地代理和证书环境,而不是把“关闭校验”当成默认方案。

推荐的排障顺序

以后只要再遇到“图片不处理”“语音一直转圈”,我建议都按这个顺序查:

  1. 先跑 openclaw gateway status,确认服务在线
  2. 查日志里有没有 MediaFetchError,先判断是不是媒体下载层故障
  3. channels.telegram.proxytimeoutSecondsretryautoSelectFamily
  4. 如果在群聊测试,额外检查 groupPolicygroupAllowFrom
  5. 媒体下载恢复后,再单独验证 ASR 命令能否手工执行
  6. 最后再校验豆包鉴权、Resource ID 和输出文件写回是否成功

这个顺序的意义在于:先修公共链路,再修语音转写。否则很容易在后半段白费时间。

怎么判断自己真的修好了

修完后,建议至少做这三组测试:

  1. 发文字,确认能立即回复
  2. 发图片,确认能触发处理并返回结果
  3. 发 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()

附录 B:火山引擎开通流程

附录 B:火山引擎开通流程
  1. 打开火山引擎控制台并登录:火山引擎官网

2026/03/09/4bb79bcd.blob

  1. 先完成实名认证(点击“前往实名认证”,再用微信或抖音扫脸)。

2026/03/09/f39014aa.blob

2026/03/09/e221482c.blob

  1. 进入豆包语音控制台:豆包语音,点击中间的“创建应用”。

2026/03/09/a6dd0a68.blob

  1. 创建应用时填写:
    • 应用名称:只支持英文(示例:Ollie
    • 应用简介:自己用即可
    • 接入能力:豆包录音文件识别模型 2.0-标准版

应用创建完成后查看 API 密钥页面,并注意妥善保管: