feat: add push subscription mode for interaction notifications
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
# 互动通知订阅:调研日志、实现与安装说明
|
||||
|
||||
## 结论和入口
|
||||
|
||||
在原 `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"
|
||||
```
|
||||
|
||||
手动打开抖音并完成登录,再运行脚本;不会自动登录。已有登录浏览器直接复用,不必另建配置。安装参考:<https://github.com/browser-use/browser-harness/blob/main/install.md>。本次在现有环境验证,未声称在空白机器重新安装验证。
|
||||
|
||||
```bash
|
||||
# 离线检查;JS 桥接测试要求 node 在 PATH 中
|
||||
python3 src/test_notifications.py
|
||||
python3 src/test_subscribe_notifications.py
|
||||
```
|
||||
|
||||
`.gitignore` 已新增 `*.jsonl`,防止通知原文、用户资料等运行输出进入公共仓库。本文不记录真实账号或通知 ID。
|
||||
Reference in New Issue
Block a user