环境
| 项 |
值 |
| AstrBot |
v4.28.0 |
| astrbot_plugin_parser |
v1.5.7 |
| 协议端 |
NapCat(motricseven7/snowluma 镜像,Docker) |
| 系统 |
Debian 12 (Linux, x86_64) |
| ffmpeg / ffprobe |
5.1.9 |
现象
在群里发送抖音分享链接(形如 https://v.douyin.com/xxxxxxxx/,由抖音 App「分享 → 复制链接」生成)后:
- 机器人先给原消息贴上一个表情回应(即
EmojiLikeArbiter 仲裁占用的 emoji 289),
此后就再没有任何下一步;
- 群里看不到解析卡片,也看不到视频;
- 同一时间段的 B 站视频解析能正常显示。
用户在群里实际观察到的表现(:
只给那条消息贴了个表情占位,然后没有下一步。
需要特别说明:这个"只有占位表情、没有下文"的表象具有误导性 ——
它会让人以为机器人卡在了仲裁或解析阶段。
而实际上按日志,消息已经成功发出(协议端有发送记录并返回了 message_id),
只是由于下述音频编码原因,QQ 客户端没有把它渲染出来,
于是呈现为"贴完表情就没动静了"。
排查:问题不在解析,也不在发送
链路各环节均有证据表明是成功的:
| 环节 |
证据 |
| 链接匹配 |
v.douyin.com/[a-zA-Z0-9_-]+ 对实际消息文本可命中 |
| 仲裁 EmojiLikeArbiter |
协议端日志可见 [Event] 表情回应 ... +[289] |
| 解析 + 下载 |
缓存产物 cache/.mp4(371,182 字节),ffprobe 可正常读取 |
| AstrBot 提交 |
send_group_msg -> group=<群号> segs=['video'],无异常 |
| 协议端接收 |
send_group_msg <- ret={'message_id': 1090905876} |
| 协议端转发 |
INFO [OneBot] 群聊 <群号> | 发送:[视频] |
两者唯一处于"不兼容档位"的差异是音频 profile:抖音侧为 HE-AACv2,B 站侧为 AAC-LC。
结合两组事实——(a) AstrBot 与协议端都报告发送成功、且协议端返回了 message_id,
(b) 把音频换成 AAC-LC 后该视频即可正常显示——
判定本问题为 QQ 侧对该视频的 HE-AACv2 音频渲染失败,
表现为"消息已发出但客户端不可见"。
(说明:QQ 客户端内部行为无法直接观测,此处是由对比实验反推的结论。)
复现方式:
ffprobe -v error -select_streams a:0 \
-show_entries stream=profile,codec_name \
-of default=nw=1:nk=1 <下载到的抖音视频>.mp4
# 输出:
# aac
# HE-AACv2
建议的修复方向
在发送视频前检测音频 profile,仅当不是 LC 时才转码(视频流直接 copy,几乎无耗时):
# 1) 探测
ffprobe -v error -select_streams a:0 -show_entries stream=profile \
-of default=nw=1:nk=1 input.mp4
# 2) 若非 LC,则只重编码音频
ffmpeg -y -v error -i input.mp4 \
-c:v copy -c:a aac -profile:a aac_low -movflags +faststart output.mp4
要点:
- 只对非 LC 的视频生效,B 站等本来就是 LC 的视频直接跳过,不引入额外开销;
- 视频流
-c:v copy,只重编码音频,成本极低;
- 转码产物建议放在插件自有 cache 目录,交由已有的
CacheCleaner 清理。
我采用的实现(供参考)
改动集中在 core/sender.py,共 3 处。只对非 LC 音频的视频生效,B 站等本来就是 LC 的视频会直接跳过。
1) 文件头补 import
import asyncio
from itertools import chain
from pathlib import Path
2) 新增转码方法(与 _video_from_path 同级)
@staticmethod
async def _transcode_video_for_qq(path: Path) -> Path:
"""把 QQ 可能不渲染的音频(如 HE-AACv2)转成 AAC-LC,视频流直接复制。
抖音视频常见 HE-AACv2 音频,QQ 收下后会静默不显示;B站的 LC 音频不受影响,
因此这里先用 ffprobe 判断,已是 LC 就直接跳过,不做无谓转码。
"""
try:
probe = await asyncio.create_subprocess_exec(
"ffprobe",
"-v", "error",
"-select_streams", "a:0",
"-show_entries", "stream=profile",
"-of", "default=nw=1:nk=1",
str(path),
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.DEVNULL,
)
out, _ = await probe.communicate()
profile = (out or b"").decode(errors="ignore").strip()
except Exception as e:
logger.warning(f"[视频] ffprobe 探测失败,跳过转码: {e}")
return path
if not profile or profile.lower() in ("lc", "aac_lc", "aac lc"):
return path
out_path = path.with_name(path.stem + "_lc.mp4")
try:
proc = await asyncio.create_subprocess_exec(
"ffmpeg",
"-y", "-v", "error",
"-i", str(path),
"-c:v", "copy",
"-c:a", "aac",
"-profile:a", "aac_low",
"-movflags", "+faststart",
str(out_path),
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.PIPE,
)
_, err = await proc.communicate()
except Exception as e:
logger.warning(f"[视频] ffmpeg 调用异常,跳过转码: {e}")
return path
if proc.returncode != 0 or not out_path.exists():
logger.warning(
f"[视频] 音频转码失败(profile={profile}),改用原文件: "
f"{(err or b'')[:200]!r}"
)
return path
logger.info(
f"[视频] 音频 {profile} -> AAC-LC 完成: {out_path.name} "
f"({out_path.stat().st_size} bytes)"
)
return out_path
3) 在 _build_segments 的重媒体分支调用
case VideoContent() | DynamicContent():
path = await self._transcode_video_for_qq(path)
segs.append(self._video_from_path(path))
设计要点
- ffprobe 探测失败 / 无音频流 → 直接用原文件,不因工具缺失或异常中断发送;
- 已是 LC → 立即返回原路径,零额外开销(B 站视频走这条);
- 转码失败 → 回退原文件并打 WARNING,不影响主流程;
- 转码产物命名为
<原名>_lc.mp4,与源文件同目录,交由插件已有的 CacheCleaner 清理。
排查过程中已排除的可能(供参考,避免重复走弯路)
以下都曾被怀疑,但均已被实测或日志证伪:
- 链接匹配失败 —— 实测正则对真实消息文本可命中;
- 仲裁失败 —— 协议端日志可见
表情回应 +[289];
- ttwid / cookie 缺失 —— 缓存里有完整的 371KB mp4;
desc 字段为空导致降级 —— 代码中不存在此分支,desc 仅用于日志与 title;
_ROUTER_DATA 缺少 videoInfoRes —— 与"已下载到完整 mp4"的事实矛盾;
- 合并转发(
Nodes)导致 —— 将 forward_threshold 调大改为 Video 直发后,现象不变;
- NapCat 读不到
file:// 本地路径 —— B 站用同样的 file:// 形式可正常显示;
- 特定群存在限制 —— 换到此前成功过的群,现象不变;
- AstrBot 与协议端 WS 连接半死 —— 重启协议端容器后现象不变。
环境
现象
在群里发送抖音分享链接(形如
https://v.douyin.com/xxxxxxxx/,由抖音 App「分享 → 复制链接」生成)后:EmojiLikeArbiter仲裁占用的 emoji 289), 此后就再没有任何下一步;用户在群里实际观察到的表现(:
需要特别说明:这个"只有占位表情、没有下文"的表象具有误导性 —— 它会让人以为机器人卡在了仲裁或解析阶段。 而实际上按日志,消息已经成功发出(协议端有发送记录并返回了
message_id), 只是由于下述音频编码原因,QQ 客户端没有把它渲染出来, 于是呈现为"贴完表情就没动静了"。排查:问题不在解析,也不在发送
链路各环节均有证据表明是成功的:
两者唯一处于"不兼容档位"的差异是音频 profile:抖音侧为 HE-AACv2,B 站侧为 AAC-LC。
结合两组事实——(a) AstrBot 与协议端都报告发送成功、且协议端返回了
复现方式:message_id, (b) 把音频换成 AAC-LC 后该视频即可正常显示—— 判定本问题为 QQ 侧对该视频的 HE-AACv2 音频渲染失败, 表现为"消息已发出但客户端不可见"。 (说明:QQ 客户端内部行为无法直接观测,此处是由对比实验反推的结论。)建议的修复方向
在发送视频前检测音频 profile,仅当不是 LC 时才转码(视频流直接 copy,几乎无耗时):
要点:
-c:v copy,只重编码音频,成本极低;CacheCleaner清理。我采用的实现(供参考)
改动集中在
core/sender.py,共 3 处。只对非 LC 音频的视频生效,B 站等本来就是 LC 的视频会直接跳过。1) 文件头补 import
2) 新增转码方法(与
_video_from_path同级)3) 在
_build_segments的重媒体分支调用设计要点
<原名>_lc.mp4,与源文件同目录,交由插件已有的CacheCleaner清理。排查过程中已排除的可能(供参考,避免重复走弯路)
以下都曾被怀疑,但均已被实测或日志证伪:
表情回应 +[289];desc字段为空导致降级 —— 代码中不存在此分支,desc仅用于日志与title;_ROUTER_DATA缺少videoInfoRes—— 与"已下载到完整 mp4"的事实矛盾;Nodes)导致 —— 将forward_threshold调大改为 Video 直发后,现象不变;file://本地路径 —— B 站用同样的file://形式可正常显示;