Files
douyin-pc/docs/2026-09-06-01-22-notification-subscription.md
T

9.7 KiB
Raw Blame History

互动通知订阅:调研日志、实现与安装说明

结论和入口

在原 src/get_notifications.py 增加 --sub,不删除或替换原分页拉取逻辑。订阅仅覆盖互动/站点通知通道,不处理私信。

# 原行为不变:拉取全部,保存 JSON 快照到 src/notifications.json
python3 src/get_notifications.py

# 原增量拉取行为不变:到指定边界为止,不含边界本身
python3 src/get_notifications.py --last-id <已保存的通知ID>

# 订阅:stdout 每条一行 JSON,可直接交给其他进程处理
python3 src/get_notifications.py --sub

# 订阅并追加文件,不覆盖历史文件;stdout 仍同步输出
python3 src/get_notifications.py --sub --output events.jsonl

# 启动时先补拉一次历史缺口,然后继续订阅
python3 src/get_notifications.py --sub --last-id <已保存的通知ID> --output events.jsonl

Ctrl+C 卸载本脚本的监听器并退出,退出码 130。状态/错误写 stderr,通知 JSONL 写 stdout。默认订阅不抓取已有通知,仅从安装监听器后接收推送;--last-id 才进行一次启动补拉。补拉期间的推送暂存在队列中。

每行包含 received_at(处理输出时间,不是服务端推送时间)、notification(复用原摘要字段)、raw(原始通知详情)。通知 ID 保留字符串。消费方可按 notification.id 做持久化处理记录;同一个聚合通知 ID 可能更新内容,不能永远只处理其第一次出现。

调研证据

  1. 检查已有拉取脚本:使用 /aweme/v1/web/notice/,保留 notice_group=960is_mark_read=0、分页游标和账号一致性校验。
  2. 通过 browser-harness 连接已有 CDP,调用现有当前用户接口确认已登录。未尝试登录、未操作关注/点赞/评论按钮、未打开消息面板,也未调用标记已读接口。
  3. webpackChunkdouyin_web 读取已加载模块,定位 NoticeFrontier。调研时通知模块编号为 511386codec 为 114718实现不硬编码这些模块编号,而是按导出相关源码特征定位。
  4. 通知存在独立 Frontier WebSocket;它不是私信 IM 通道。NoticeFrontier.frontierInstance 已连接,并提供 addEventListener/removeEventListener
  5. 原网页按二进制帧的 service 分流:
    • 20313:中台通知推送,读取 notices[].notice_id_str,按 effect_groups 包含 960/961 过滤。
    • 20003:服务通知推送,读取顶层 notice_id_str;沿用页面类型集合 45、31、9009、9002、514、9067。
    • 其他 service 不交给通知业务处理。
  6. 推送不是完整业务详情。原页面按 ID 调用 /aweme/v1/web/notice/detail/id_list[{"notice_id_str":"...","type":0}];实测返回 status_code=0notice_list_v2 与请求 ID 相符。
  7. 读取 Frontier 的事件管理实现,确认添加独立监听器不需要替换站点 onmessage;卸载监听器不会关闭原连接。

实现

页面现有通知 WebSocket
  → SDK decodedFrame 解码
  → 自有 message 监听器 + 内存事件队列
  → Promise 事件唤醒 → browser-harness → Python
  → 按 ID 请求通知详情(is_mark_read=0
  → 校验账号与 ID → 去重 → JSONL,立即 flush

新增 src/subscribe_notifications.py 负责订阅桥接,通过原脚本的 --sub 分支调用。复用 get_current_user.py 登录校验,以及 get_notifications.py 的摘要/补拉函数,不新装 WebSocket/Protobuf/签名库,不提取或保存 Cookie。

不属于定时通知拉取

  • 没有新事件时,本实现不周期性请求通知列表或详情。
  • 本地等待每 2 秒返回一次空队列,目的是低于 browser-harness 的 IPC 超时并检查页面状态;这是浏览器到 Python 的本地桥接心跳,不是每 2 秒请求抖音。
  • 事件到达会立即唤醒等待,不必等到心跳到期。Python 正在处理上一批时,新推送进入队列。
  • 页面自己的既有轮询/心跳不受本脚本控制,因此不能保证浏览器整体没有其他通知请求。
  • 详情暂不可见时仅针对当前推送重试,最多 3 次,间隔 0.5/1 秒;不无限重试或定时全量扫描。

错误与边界

  • 登录检查失败时停止,交由用户手动登录。页面刷新、账号切换或通知实例销毁后停止并提示重新运行;不自动导航或登录。
  • SDK 自有网络重连仍保留,脚本报告 open/close;断线期间不保证补发,不承诺零丢失。需要时用 --last-id 重新启动补拉;这仍受原列表范围(960)及服务端保留历史的限制,不能保证补回每次聚合更新或所有其他站点通知。
  • 当前不持久化 ACK/队列,也不自动恢复页面刷新后的监听。进程异常中止或详情失败时,尚未处理的队列可能丢失;消费方应保存处理记录并在恢复时补拉。
  • 队列最多 1000 个事件,超出时明确报错退出;内存去重保留最近 4096 个 ID,相同 ID/相同完整详情不重复输出,内容变化仍输出。跨进程幂等由消费方负责。
  • --output 是追加写 JSONL,与拉取模式的 JSON 快照格式不同,请使用不同文件名;每行 flush,但不承诺断电持久化。
  • 保持已登录抖音标签页和浏览器运行。browser-harness 共享当前标签状态;不要并发切换账号/标签。后台标签节流、系统休眠或浏览器卡顿可能造成 IPC 超时并退出,需恢复页面后重启;推荐保持标签页活动。当前依赖浏览器 SDK,不是脱离浏览器的纯 Python WebSocket 客户端。
  • 平台可能不推送某些通知,或自行聚合/延迟;源码表明推送通道存在,不代表每一次点赞/关注/评论都必定一对一即时推送。
  • 模块导出特征、帧格式、服务类型和详情 API 都是站点内部实现;变化时需重新调研。

验证记录(区分模拟与真实)

  • python3 src/test_notifications.py:原分页/摘要离线检查通过。
  • python3 src/test_subscribe_notifications.py:通过 ID 精度、分组/类型过滤、异常结构、详情重试、账号变化、追加输出、去重/聚合更新、--sub 路由及原拉取分支检查。
  • Node.js 假 Webpack/FWS 检查:二进制 payload、忽略其他 service、事件到达立即唤醒、退出移除监听器而不关闭连接、实例变化报错,全部通过。
  • 真实浏览器安装监听成功,状态为 connected;空闲等待约 3.2 秒(含进程调用开销)返回空事件;卸载后原通知 WebSocket 仍连接。
  • 真实 python3 src/get_notifications.py --sub 运行约 6 秒后发送 SIGINT:安装/停止提示正常,退出码 130,剩余自有监听器为 0,原 WebSocket 仍连接。期间没有自然新通知,不以此证明真实互动推送到达。
  • 质量检查:运行时导入、离线测试、真实 CLI 均通过。LSP 对新增同目录模块报两处 reportMissingImports,已按运行证据登记为误报,未向源码添加忽略注释或修改导入路径;独立 basedpyright 检查 45 秒超时,不能声称该检查通过。Ruff 在原 get_notifications.py 留有已有的 EXE001/TRY004 风格提示,未为订阅功能改动原逻辑。
  • 使用真实 SDK 的 encodeFrame 生成模拟推送帧,只调用本次安装的自有监听器,未触发站点原监听器、未发送网络通知。实际 decodedFrame 解码成功,Python 解析出 ID,再通过真实详情接口读回对应通知。
  • 上述模拟验证不等于已收到外部新点赞/关注/评论的完整端到端测试。仍需由另一账号产生一条真实互动,观察订阅 stdout;本次没有代替其他账号执行互动,也不虚构实时延迟结论。

调研中修正的问题

  • 最初 15 秒 JS 等待超过 harness IPC 超时,改为 2 秒本地等待;事件本身仍立即唤醒。
  • 重复使用同一 Webpack 临时 chunk ID 会导致 runtime 回调不再执行,现使用每次唯一标识。
  • 离线 codec 最初返回普通数组,修正为真实解码器使用的 Uint8Array。
  • 真实 Frontier 的监听项是 {fn,ctx},并非函数本身;模拟注入改为只调用自有监听项。
  • 编码器要求大写 SeqID/LogID,修正测试帧后真实 codec 链路验证通过;这些字段不需要业务脚本自行构造。

空白环境安装与复用

Python 3.11+、Chrome/Chromium、browser-harness;测试 JS 桥接另需 Node.js 18+。本项目新增代码只使用 Python 标准库。保留整个 src/,不要只复制入口文件。

python3 -m venv .venv
. .venv/bin/activate
python -m pip install 'browser-harness==0.1.9'
# Windows 激活方式为 .venv\Scripts\activate

# 也可通过 uv 独立安装 CLI
# uv tool install --python 3.12 'browser-harness==0.1.9'

# 根据自己的浏览器修改端点,不要将 CDP 端口公开到公网
export BU_CDP_URL=http://localhost:9222
browser-harness <<'PY'
print(list_tabs())
PY

python3 src/get_notifications.py --sub

若没有启用远程调试的浏览器,使用本机浏览器可执行文件和独立用户目录启动,例如:

chromium --remote-debugging-address=127.0.0.1 --remote-debugging-port=9222 \
  --user-data-dir="$HOME/.douyin-browser"

手动打开抖音并完成登录,再运行脚本;不会自动登录。已有登录浏览器直接复用,不必另建配置。安装参考:https://github.com/browser-use/browser-harness/blob/main/install.md。本次在现有环境验证,未声称在空白机器重新安装验证。

# 离线检查;JS 桥接测试要求 node 在 PATH 中
python3 src/test_notifications.py
python3 src/test_subscribe_notifications.py

.gitignore 已新增 *.jsonl,防止通知原文、用户资料等运行输出进入公共仓库。本文不记录真实账号或通知 ID。