先把问题拆成控制面和数据面
按 SRE 的写法,这不是“语音没发出来”那么简单,而是控制面和数据面同时失真:控制面是登录态复用与 runtime
对齐,数据面是 voice-*.webm 被错误投递成 file。只有两边都闭环,验收才算完成。
这次要修的不是单点 bug,而是一条完整链路:自动化要复用你已经登录的飞书,真实群消息要进入
voice-lite,机器人最终还要在群里显示为飞书原生语音,而不是
[文件] voice-*.webm。到 2026-03-17 09:56:13,这三个目标已经同时成立。
smoke-20260316-voice-full-06
清理重启:2026-03-17 10:53:09
storage.json 可复用,probe 多次回报 needsLogin=false。msg_type=audio。media.ts、测试、网关重启。
按 SRE 的写法,这不是“语音没发出来”那么简单,而是控制面和数据面同时失真:控制面是登录态复用与 runtime
对齐,数据面是 voice-*.webm 被错误投递成 file。只有两边都闭环,验收才算完成。
真正的问题出在边界处:飞书 UI、网关 dispatch、媒体标准化、Feishu API 类型选择。把每个 handoff 都留下可核对证据, 才能知道“发了没回”究竟卡在哪一跳。
这轮最关键的认知升级,是把“WebM 文件”视为可被标准化的语音中间产物,而不是最终投递形态。飞书是否显示成语音,
取决于 file_type、duration 和 msg_type 的组合,而不只是文件名。
结论可以压缩成一句话:这次不是“再试一次就好”的问题,而是把一条原本只到
voice-*.webm 文件的半成品链路,收口成“登录可复用、dispatch 可达、语音原生显示、重启后仍干净”的完整链路。
如果自动化上下文一开始就失去飞书登录态,后面所有“没回”判断都会混在一起。先把 session 复用变成稳定前提,后续日志才有解释力。
ReferenceError 属于 dispatch 控制面;[文件] voice-*.webm 属于媒体语义失配。两者同时出现时,不能用一个结论吃掉全部现象。
核心 TTS 输出 WebM 没有错,但它只是中间产物。真正要给飞书的,是 opus + duration + audio 这个最终契约。
真正的成功标准不是“日志里回复了”,而是群成员能同时看到双语可见文字和飞书原生可播放语音。验收口径要始终落在用户界面。
下面这张泳道图把这次从登录复用、问题定位、补丁落地到最终验收的顺序串起来。灰色卡片表示故障或人工环节。
| 时间 | 观测 | 意义 |
|---|---|---|
| 2026-03-16 13:42 / 16:34 / 16:56 / 18:38 | 群侧多次出现 [文件] voice-*.webm。 |
证明语音已经生成,但 Feishu 插件把它当作通用文件发送,数据面语义错了。 |
| 2026-03-16 17:47:51 → 17:59:28 | ReferenceError: shouldPassthroughTextCommandForAgent is not defined |
这是控制面的临时回归,消息在 dispatch 前就失败,和语音格式问题不是同一层。 |
| 2026-03-17 09:03:52 → 09:05:00 | 网关 client ready,随后注册本地 Feishu plugin 工具。 |
说明本地 live plugin 和运行时重新对齐,链路恢复到可验收状态。 |
| 2026-03-17 09:53:49 | 用户发出 smoke-20260316-voice-full-06 验收消息。 |
这是最终端到端 smoke 的起点。 |
| 2026-03-17 09:54:49 | 网关日志记录群消息进入 agent:voice-lite:feishu:group:... |
证明真实群消息已进入目标 agent 会话。 |
| 2026-03-17 09:55:30 | 按当时 API 验收记录,配对的双语可见文字以 post 形式落群。 |
文本层先成功,说明 reply 结构和群规则是对的。 |
| 2026-03-17 09:56:13 | 原生音频落群,网关记录 dispatch complete (queuedFinal=true, replies=1) |
最终验收完成;当时 API 记录中的音频消息时长为 9026 ms。 |
| 2026-03-17 10:53:09 → 10:53:12 | 做一次“无功能变更”的清理重启,工具重新注册,client ready 与 ws client ready 正常。 |
清掉临时调试态,留下更干净的运行面。 |
为了把这次排障从“文字叙述”提升成“可复核结构”,这里再补五张微型图,分别对应会话、边界、重复试验、分叉决策和接口关系。
configs/feishu/storage.json,探针多次回报 needsLogin=false 且 verifiedAfterReload=true。语音,绑定的 chat_id 为 oc_c79495…90a2f。.openclaw/openclaw.json 中,voice-lite 已绑定到该群,群会话作用域为 group。plugins.allow 包含 feishu-official-local,plugins.load.paths 指向本地 live plugin 目录 DEVELOPE/Tool/openclaw-feishu。webm-24khz-16bit-mono-opus,也就是“生成 WebM,再由插件决定怎么投递”。DEVELOPE/Tool/openclaw-feishu/src/media.ts 增加 voice-* 识别逻辑,覆盖 .webm/.mp3/.m4a/.wav/.ogg/.oga。ffmpeg 转成 .opus,再调用 ffprobe 取时长。duration,并把最终 msg_type 从 file 改成 audio。voice-123.webm -> voice-123.opus -> audio 的单测,锁住最关键的标准化行为。ffmpeg 产出和 ffprobe 返回 1.25s,验证最终 duration=1250。src/bot.ts 中为排障临时加过的 debug stack logging 已经移除,没有留在最终行为里。storage.json。shouldPassthroughTextCommandForAgent 未定义,导致消息在 dispatch 层直接失败。file_type=opus、毫秒时长、以及 msg_type=audio 的组合。tmp/_codex/feishu-e2e/feishu-send-probe-20260316-183748.json 与同类 probe 持续回报 needsLogin=false。语音,并在 reload 后继续保持有效。.openclaw/logs/gateway.log 在 09:54:49 记录收到 smoke,并把会话送入 voice-lite。dispatch complete (queuedFinal=true, replies=1)。post + audio,音频消息 ID 为 om_x100b54b…60b6a,时长 9026 ms。
备注:当前 live plugin 目录带有运行依赖,但本地并没有可直接执行的 vitest runner,因此这次报告只能把新增单测作为“代码证据”记录,不能把“本机已跑绿”作为最终结论的一部分。
现在的标准化过程依赖 ffmpeg 和 ffprobe。代码里已经做了失败回退,但一旦机器上缺这两个工具,行为会退回 file 上传。
configs/feishu/storage.json 解决的是“复用你当前已登录状态”,不是永久免登录。如果租户会话过期,仍需要重新采集一次。
推荐把这次 media.ts 的标准化逻辑抽成可配置或可 upstream 的能力,其次补齐 live plugin 的本地测试 runner,避免未来只能靠群里现象回归。