biliup → AstrBot(QQ) 录播完成通知 修复

1648 字
8 分钟
biliup → AstrBot(QQ) 录播完成通知 修复

1. 原本设想的链路#

biliup 录制 → 上传 B 站 → 后处理(notify) → push_lite(:9966) → AstrBot → QQ 群
  • biliup:ghcr.io/biliup/caution(录+传一体),配置在 WebUI,落库到 sqlite data.sqlite3
  • push_lite:AstrBot 插件 astrbot_plugin_push_lite v0.2.2,监听 0.0.0.0:9966
    • 路由 POST /send,Body 需 content + umo,Header Authorization: Bearer <token>
  • AstrBot:soulter/AstrBot v4.26.7,QQ 后端走 NapCat(aiocqhttp 适配器,platform_id = ai

2. 为什么一直「没收到 webhook」(两个独立根因)#

根因 A:biliup 的 run 后处理根本不执行#

biliup-cli 1.2.1 的「后置任务(run)」并不会在上传完成后去调用外部脚本。

证据:

  • 每天的实际上传日志里只有 mv 步骤,从没出现 run
  • push_lite 在真实上传时间点(如 02<24>)完全没有任何请求记录;
  • 也就是说 notify.py 从头到尾没被调用过 → 自然一条通知都发不出。

结论: 依赖 biliup 后处理去触发通知这条路,从根上就走不通,必须绕开。

根因 B:umo 会话格式写错,即使触发也会被 AstrBot 拒绝#

最开始写的 umogroup_485476610,AstrBot 侧直接报错:

写的格式AstrBot 报错
group_485476610not enough values to unpack (expected 3, got 1)
ai:group:485476610'group' is not a valid MessageType

查 AstrBot 源码 message_type.py 确认:会话标识是三段式

platform_id : message_type_value : session_id
  • 中间段必须是 MessageType 枚举的「值」,首字母大写:GroupMessage / FriendMessage
  • QQ 适配器的 platform_id 是 ai

正确写法:ai:GroupMessage:485476610(这一步即便根因 A 修好,格式错也照样发不出去)。


3. 最终修复方案(绕过后处理,改用目录监听)#

放弃 biliup 后处理,改为监听备份目录的脚本:

项目内容
脚本路径/opt/notify/watch_backup.py(zero 用户运行)
保活zero 的 crontab 每分钟拉起;flock 单实例锁防多开
监听目录/opt/biliup/backup(biliup 的 mv 后处理把录播移到这里)
轮询周期每 30 秒扫一次
触发条件发现新媒体文件,且文件修改时间稳定 ≥ 5 秒
媒体扩展名.flv .mp4 .mkv .ts .mov .avi .m4v .mp3 .m4a .m4s(其余如 .log 会被过滤,避免误推)
推送目标POST http://127.0.0.1:9966/send
认证Authorization: Bearer cUoGKubE8IJvNu8nMwpK_Z1K-TbcvfurPzug10UWak0
Body{"content":"录播已上传完成\n文件:<文件名>","umo":"ai:GroupMessage:485476610"}
去重已通知的文件名写入 /opt/notify/watched_state.json仅当 HTTP 200 成功才记录 → 失败自动重试,不漏不重

配套修正:

  • biliup 的 postprocessor 已用 sudo 还原为 [{"mv":"/opt/backup"}](去掉 run,避免将来双通知);
  • 脚本加了 MEDIA 扩展名过滤,修掉了早期版本把 download.log 当媒体推送的 bug。

脚本核心逻辑(节选):

st = notify(key)
if st == 200: # 只有成功才记 state
state[key] = int(now)
save_state(state)
print("notified:", key)
# 失败则不记录 → 下一轮轮询会重试

4. 已验证可用的证据(2026-07-20 诊断)#

  • ✅ watcher 进程存活(PID 3409022,父进程 1,STAT S = 正常 30s 轮询中睡觉,CPU 时间 0 属正常,不是卡死);
  • curl 用脚本同款 payload 直发 push_lite → HTTP 200;端口 9966 监听 0.0.0.0
  • watch.log 已显示两个真实录制被 notified:
    • 【2026-07-19】咪茉Mimo直播回放.flv
    • 【2026-07-20】咪茉Mimo直播回放.flv → 生产链路已经实际工作过。

关于「TEST_NOTIFY.flv 没触发」的假阴性说明#

早前在排查时放过一个 TEST_NOTIFY.flv 测试文件,但它没被通知。这不是逻辑 bug,而是假阴性

  • 测试文件没有在「watcher 正常运行期间」持续留在 /opt/biliup/backup 里(要么放的时候 watcher 被重启/杀掉,要么文件太新未过 5s 稳定期就被清理/挪走);
  • 当前 state 文件里也没有 TEST_NOTIFY.flv 这条记录,说明它从没被成功扫到过。

换句话说:真实录播文件能被正常通知(见上方 ✅ 证据),测试文件只是没赶上好时机。


5. 手动端到端验证(确认 QQ 群真能收到)#

Terminal window
# 1) 直接在 backup 目录生成测试文件(不经过 /tmp)
echo test | sudo tee /opt/biliup/backup/TEST_NOTIFY.flv > /dev/null
# 2) 盯日志,等 ~40 秒(30s 轮询 + 5s 稳定判定)
tail -f /opt/notify/watch.log
# 应出现:notified: TEST_NOTIFY.flv
# 同时 QQ 群 485476610 收到「录播已上传完成 / 文件:TEST_NOTIFY.flv」
# 3) 确认后清理(state 里的记录无害,不会重复推送)
sudo rm -f /opt/biliup/backup/TEST_NOTIFY.flv

注意第 2 步要真等够 ~40 秒,前 30 多秒没反应是正常的。

若等满 40 秒仍无 notified:,把 tail -f 的输出贴回来,再看是否 watcher 被 cron 重启打断(sudo pkill -f watch_backup.py 后让它重新拉起即可)。


6. 监听脚本完整代码#

密钥全部走环境变量,不要把 token 写死进仓库。服务器实际部署版只是把下列变量硬编码成了真实值,逻辑完全一致。

#!/usr/bin/env python3
"""
watch_backup.py — 录播备份目录监听通知
当 biliup 等录制工具把完成的媒体文件 mv 到监控目录后,
本脚本会检测新文件(稳定 N 秒后)并推送到 AstrBot 的 push_lite 插件,
进而发到指定 QQ 群 / 私聊。
配置全部通过环境变量注入(不要把 token 写死进仓库):
PUSH_LITE_URL 推送地址,默认 http://127.0.0.1:9966/send
PUSH_LITE_TOKEN push_lite 的 Bearer token(必填)
PUSH_LITE_UMO 目标会话,如 ai:GroupMessage:123456
WATCH_DIR 监控目录,如 /opt/biliup/backup
STATE_FILE 去重状态文件,默认 <脚本目录>/watched_state.json
LOCK_FILE 单实例锁文件,默认 <脚本目录>/watch.lock
POLL 轮询间隔秒,默认 30
STABLE_SECS 文件需稳定多少秒才认为写完,默认 5
依赖:仅 Python 3 标准库,无需 pip install。
"""
import os
import sys
import json
import time
import glob
import fcntl
import urllib.request
URL = os.environ.get("PUSH_LITE_URL", "http://127.0.0.1:9966/send")
TOKEN = os.environ.get("PUSH_LITE_TOKEN", "")
UMO = os.environ.get("PUSH_LITE_UMO", "ai:GroupMessage:000000")
WATCH_DIR = os.environ.get("WATCH_DIR", "/opt/biliup/backup")
_HERE = os.path.dirname(os.path.abspath(__file__))
STATE = os.environ.get("STATE_FILE", os.path.join(_HERE, "watched_state.json"))
LOCK = os.environ.get("LOCK_FILE", os.path.join(_HERE, "watch.lock"))
POLL = int(os.environ.get("POLL", "30"))
STABLE_SECS = int(os.environ.get("STABLE_SECS", "5"))
# 只推送这些媒体扩展名,避免把 download.log 之类当媒体误推
MEDIA = {".flv", ".mp4", ".mkv", ".ts", ".mov", ".avi", ".m4v",
".mp3", ".m4a", ".m4s"}
_lf = open(LOCK, "w")
try:
fcntl.flock(_lf, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError:
print("another instance running, exit", flush=True)
sys.exit(0)
def load_state():
try:
return json.load(open(STATE))
except Exception:
return {}
def save_state(s):
tmp = STATE + ".tmp"
json.dump(s, open(tmp, "w"), ensure_ascii=False)
os.replace(tmp, STATE)
def notify(fname):
content = "录播已上传完成\n文件:" + fname
payload = json.dumps({"content": content, "umo": UMO}).encode("utf-8")
req = urllib.request.Request(URL, data=payload, method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", "Bearer " + TOKEN)
try:
with urllib.request.urlopen(req, timeout=15) as r:
return r.status
except Exception as e:
print("notify failed:", e, flush=True)
return None
def main():
state = load_state()
print("watcher started, watch_dir=%s" % WATCH_DIR, flush=True)
while True:
try:
files = [f for f in glob.glob(os.path.join(WATCH_DIR, "*"))
if os.path.isfile(f)]
now = time.time()
for f in files:
if os.path.splitext(f)[1].lower() not in MEDIA:
continue
if now - os.path.getmtime(f) < STABLE_SECS:
continue
key = os.path.basename(f)
if key in state:
continue
st = notify(key)
if st == 200:
state[key] = int(now)
save_state(state)
print("notified:", key, flush=True)
except Exception as e:
print("loop err:", e, flush=True)
time.sleep(POLL)
if __name__ == "__main__":
main()

跑法示例:

Terminal window
export PUSH_LITE_URL="http://127.0.0.1:9966/send"
export PUSH_LITE_TOKEN="你的push_lite_token"
export PUSH_LITE_UMO="ai:GroupMessage:485476610"
export WATCH_DIR="/opt/biliup/backup"
python3 watch_backup.py

cron 保活(零依赖用户级):

Terminal window
* * * * * pgrep -f /opt/notify/watch_backup.py > /dev/null || (PUSH_LITE_TOKEN=xxx PUSH_LITE_UMO=ai:GroupMessage:485476610 python3 /opt/notify/watch_backup.py >> /opt/notify/watch.log 2>&1 &)

附:关键常量速查#

服务器 IP : 192.168.7.102
push_lite 端口 : 9966 (0.0.0.0)
token : cUoGKubE8IJvNu8nMwpK_Z1K-TbcvfurPzug10UWak0
目标群 umo : ai:GroupMessage:485476610
监听脚本 : /opt/notify/watch_backup.py
监听目录 : /opt/biliup/backup
状态文件 : /opt/notify/watched_state.json
日志 : /opt/notify/watch.log

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

biliup → AstrBot(QQ) 录播完成通知 修复
https://vtdd.vip/posts/biliup--astrbotqq-录播完成通知-修复/
作者
Zero
发布于
2026-07-20
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Zero
有三件事人类都要经历:出生、生活和死亡。他们出生时无知无觉,死到临头,痛不欲生,活着的时候却又怠慢了人生。——拉布吕耶尔
分类
标签
最新动态
站点统计
文章
25
动态
1
分类
7
标签
27
总字数
11,266
运行时长
0
最后活动
0 天前
站点信息
构建平台
Local
博客版本
Firefly v6.14.3
文章许可
CC BY-NC-SA 4.0