From 76f2124765368faa3f082ac7799d73a6cd74274a Mon Sep 17 00:00:00 2001 From: liuwei Date: Thu, 23 Apr 2026 14:12:30 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=93=8D=E5=BA=94=E6=8C=87?= =?UTF-8?q?=E4=BB=A4=E8=AF=AD=E9=9F=B3=E5=8F=91=E9=80=81=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E8=AF=AF=E5=88=A4=E5=AF=BC=E8=87=B4ffmpeg=E8=A7=A3=E7=A0=81?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 语音发送逻辑改为优先按文件后缀推断格式,参考message_push.py的稳定实现。 2. 新增语音发送兜底重试机制:首选格式失败后自动尝试mp3/wav/amr。 3. 增加详细日志,便于排查配置格式与文件真实格式不一致问题。 --- plugins/fun_command_play/main.py | 75 ++++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/plugins/fun_command_play/main.py b/plugins/fun_command_play/main.py index 01ff4f7..4e3a104 100644 --- a/plugins/fun_command_play/main.py +++ b/plugins/fun_command_play/main.py @@ -208,6 +208,70 @@ class FunCommandPlayPlugin(MessagePluginInterface): self._put_media_bytes_to_cache(cache_key, payload) return payload + @staticmethod + def _infer_voice_format_by_path(voice_path: str, configured_format: str) -> str: + """根据语音文件路径推断发送格式。 + + 设计说明: + 1. 参考 message_push.py 的既有稳定逻辑:优先按文件后缀判断格式。 + 2. 若后缀可识别(wav/mp3/amr),直接使用后缀,避免“配置格式与真实文件不一致”导致解码失败。 + 3. 若后缀不可识别,再回退到配置值;配置也无效时最终默认 mp3。 + """ + suffix = Path(str(voice_path or "")).suffix.lower().strip() + if suffix == ".wav": + return "wav" + if suffix == ".amr": + return "amr" + if suffix == ".mp3": + return "mp3" + + normalized_cfg = str(configured_format or "").strip().lower() + if normalized_cfg in {"wav", "mp3", "amr"}: + return normalized_cfg + return "mp3" + + async def _send_voice_with_fallback( + self, + bot: WechatAPIClient, + target_id: str, + voice_bytes: bytes, + voice_path: str, + configured_format: str, + ) -> None: + """发送语音并进行格式兜底重试。 + + 背景: + - 在线上规则维护中,常见问题是“文件实际是 wav,但配置里误写 mp3”, + 这会触发 ffmpeg 解码失败(Header missing)。 + + 策略: + 1. 首次发送:使用“后缀优先”推断格式(与 message_push.py 保持一致)。 + 2. 失败后自动尝试其它格式(mp3/wav/amr),提高容错性,降低人工维护成本。 + """ + first_try = self._infer_voice_format_by_path(voice_path=voice_path, configured_format=configured_format) + candidates: List[str] = [first_try] + for fallback in ("mp3", "wav", "amr"): + if fallback not in candidates: + candidates.append(fallback) + + last_error: Optional[Exception] = None + for fmt in candidates: + try: + await bot.send_voice_message(target_id, voice_bytes, fmt) + if fmt != first_try: + self.LOG.warning( + f"[{self.name}] 语音发送触发格式兜底重试成功: path={voice_path}, first={first_try}, final={fmt}" + ) + return + except Exception as e: + last_error = e + self.LOG.warning( + f"[{self.name}] 语音发送失败,尝试下一个格式: path={voice_path}, format={fmt}, error={e}" + ) + + if last_error: + raise last_error + def start(self) -> bool: self.status = PluginStatus.RUNNING self.LOG.info(f"[{self.name}] 插件已启动") @@ -425,10 +489,13 @@ class FunCommandPlayPlugin(MessagePluginInterface): voice_format = str(action.get("format", "") or "").strip().lower() voice_bytes = self._load_media_bytes(media_kind="voice", media_path=voice_path) if voice_bytes: - if not voice_format: - suffix = Path(voice_path).suffix.lower() - voice_format = "wav" if suffix == ".wav" else "mp3" - await bot.send_voice_message(target_id, voice_bytes, voice_format) + await self._send_voice_with_fallback( + bot=bot, + target_id=target_id, + voice_bytes=voice_bytes, + voice_path=voice_path, + configured_format=voice_format, + ) return if action_type == "video":