# 互动通知订阅:调研日志、实现与安装说明 ## 结论和入口 在原 `src/get_notifications.py` 增加 `--sub`,不删除或替换原分页拉取逻辑。订阅仅覆盖互动/站点通知通道,不处理私信。 ```bash # 原行为不变:拉取全部,保存 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=960`、`is_mark_read=0`、分页游标和账号一致性校验。 2. 通过 browser-harness 连接已有 CDP,调用现有当前用户接口确认已登录。未尝试登录、未操作关注/点赞/评论按钮、未打开消息面板,也未调用标记已读接口。 3. 从 `webpackChunkdouyin_web` 读取已加载模块,定位 `NoticeFrontier`。调研时通知模块编号为 `511386`,codec 为 `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=0`、`notice_list_v2` 与请求 ID 相符。 7. 读取 Frontier 的事件管理实现,确认添加独立监听器不需要替换站点 `onmessage`;卸载监听器不会关闭原连接。 ## 实现 ```text 页面现有通知 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/`,不要只复制入口文件。 ```bash 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 ``` 若没有启用远程调试的浏览器,使用本机浏览器可执行文件和独立用户目录启动,例如: ```bash chromium --remote-debugging-address=127.0.0.1 --remote-debugging-port=9222 \ --user-data-dir="$HOME/.douyin-browser" ``` 手动打开抖音并完成登录,再运行脚本;不会自动登录。已有登录浏览器直接复用,不必另建配置。安装参考:。本次在现有环境验证,未声称在空白机器重新安装验证。 ```bash # 离线检查;JS 桥接测试要求 node 在 PATH 中 python3 src/test_notifications.py python3 src/test_subscribe_notifications.py ``` `.gitignore` 已新增 `*.jsonl`,防止通知原文、用户资料等运行输出进入公共仓库。本文不记录真实账号或通知 ID。