docs: decouple favorites, manual ingest, write-for-sort-only

This commit is contained in:
kairee committed 2026-11-01 14:09:09 +08:00
1 parent 6c4287b43a
commit 0d6e62f9ee
5 files changed
+197 -206

No files matched your search

+77 -78
View File
@@ -1,6 +1,6 @@
# 咻咻满 Reaction 视频数据看板开发文档
> **状态:** P0(数据底座与收藏夹同步)、P1(每日快照与粉丝追踪)、P2(只读本地看板)、P3(Telegram 与一键加入)已实施
> **状态:** P0(数据底座与手动入库)、P1(每日快照与粉丝追踪)、P2(只读本地看板)、P3(Telegram 发现与入库)已实施;项目已与 B 站收藏夹解耦
>
> **技术基线:** Python 3.14、Python 标准库、SQLite、原生 HTML/CSS/JavaScript、`uv`
>
@@ -89,10 +89,11 @@ uv run python tests/test_moves.py
# 建库、迁移与黑名单种子(离线,P0)
uv run python app.py init
# 收藏夹只读同步/首次导入(P0)
uv run python app.py sync
# 手动入库:视频链接、BV 号、av 号或 b23.tv 短链(只写本地库)
uv run python app.py ingest BV1x8x166EzY
uv run python app.py ingest https://www.bilibili.com/video/BV1x8x166EzY
# 按配置周期常驻同步(P0);--once 只跑一次
# 按配置周期常驻运行发现与快照(P0/P3);--once 只跑一次
uv run python app.py run
uv run python app.py run --once
@@ -104,9 +105,6 @@ uv run python app.py snapshot --force
uv run python app.py backfill-followers
uv run python app.py backfill-followers --dry-run
# 全量同步默认会在结尾采集缺失粉丝数;可用 --no-capture-followers 跳过
uv run python app.py sync --full --no-capture-followers
# 检查 Cookie 登录态(P1)
uv run python app.py auth-check
@@ -171,7 +169,7 @@ uv sync --all-groups
│ 单一 Python 进程 │
│ │
│ Web 线程:ThreadingHTTPServer :8080 │
│ 调度线程:发现 / 收藏夹同步 / 快照 / 清理 │
│ 调度线程:发现 / 快照 / 清理 │
│ Telegram:getUpdates long polling │
│ 写入线程:SQLite 单写队列 │
│ │
@@ -186,7 +184,7 @@ uv sync --all-groups
- 所有 SQLite 写操作经单写队列串行执行,避免 `database is locked`;
- 读操作使用短生命周期连接;
- 网络请求期间不得持有数据库写事务;
- 收藏操作通过 `fav_action` 状态锁保证幂等;
- 入库操作通过 `video` 存在性判断保证幂等;
- SQLite 启用 WAL、外键和合理的 busy timeout。
```sql
@@ -219,11 +217,11 @@ PRAGMA synchronous = NORMAL;
│ ├── scheduler.py # 内置任务调度
│ ├── logging_json.py # 脱敏 JSON 日志
│ ├── classify.py # 标题归一化、时长解析与打分
│ ├── fav_position.py # 新视频插入位置计算
│ ├── video_ref.py # BV/av/URL/b23.tv 引用解析(纯函数)
│ ├── timeutil.py # 上海时区与每日时间
│ ├── bilibili/
│ │ ├── client.py # 只读 HTTP、重试与限速
│ │ ├── write_client.py # deal / sort 写客户端
│ │ ├── client.py # 只读 HTTP、重试与限速(含 view 详情、短链解析)
│ │ ├── write_client.py # sort 写客户端(仅收藏夹重排序)
│ │ ├── search.py # WBI 搜索与旧接口降级
│ │ ├── provider.py # 凭据变化重建客户端
│ │ ├── auth.py # Cookie、设备指纹与请求头工具
@@ -231,9 +229,8 @@ PRAGMA synchronous = NORMAL;
│ │ └── models.py # 接口响应模型
│ ├── services/
│ │ ├── discovery.py # 搜索发现与候选池
│ │ ├── favorites.py # 收藏 / 忽略 / 拉黑
│ │ ├── telegram_poller.py # getUpdates 与回调分发
│ │ ├── folder_sync.py
│ │ ├── library.py # 入库(候选/手动)/ 忽略 / 拉黑
│ │ ├── telegram_poller.py # getUpdates、回调分发与私聊手动入库
│ │ ├── snapshot.py
│ │ ├── up_profile.py # UP 粉丝快照采集与历史回填
│ │ └── maintenance.py
@@ -292,8 +289,6 @@ PRAGMA synchronous = NORMAL;
"up_blacklist_seed": [261052419, 1908537643, 1701121088, 542262],
"schedule": {
"discover_minutes": 60,
"folder_sync_minutes": 60,
"full_folder_refresh_hours": 6,
"snapshot_at": "00:01",
"candidate_ttl_days": 7
},
@@ -301,17 +296,17 @@ PRAGMA synchronous = NORMAL;
}
```
- `folders` 中的年度夹由人工创建,系统只读;
- `folders` 仅作为历史来源与 `favsort.py` 重排序目标,系统不再自动同步或建夹;
- 配置必须为 `bilibili.read` / `bilibili.write` 双账号结构;不兼容顶层扁平 `mid` / `csrf` / `cookie`;
- P0 已实现的命令行:`python app.py init`、`python app.py sync [--full]`、`python app.py status`。
- P0/P3 已实现的命令行:`python app.py init`、`python app.py ingest`、`python app.py status`、`python app.py run`。
### 5.2 安全规则- Windows 本地开发时应限制 `config.json` 的文件访问权限;部署到 Linux 时权限必须为 `600`;
- 配置文件每 60 秒检查一次 mtime,凭据、收藏夹、分类规则和权重变更后无需重启;
- 配置文件每 60 秒检查一次 mtime,凭据、分类规则和权重变更后无需重启;
- Cookie / CSRF 变化时由 `ClientProvider` 重建 B 站请求客户端,并保留该角色的限速与重试策略;
- `schedule.folder_sync_minutes` 与 `schedule.snapshot_at` 在启动时固化,修改后需重启 `run`;
- `schedule.discover_minutes` 与 `schedule.snapshot_at` 在启动时固化,修改后需重启 `run`;
- Telegram 操作必须同时验证 `from.id` 和 `chat.id` 是否为配置的私聊用户;
- 仅允许环境变量临时覆盖敏感配置,不能将真实凭据硬编码进源代码;
- `GET` 路由绝不进行收藏、忽略、拉黑等写操作。
- `GET` 路由绝不进行入库、忽略、拉黑等写操作。
---
@@ -322,12 +317,11 @@ PRAGMA synchronous = NORMAL;
| 表 | 状态 | 含义 | 是否参与每日快照 |
|---|---|---|---|
| `video` | `active` | 在库追踪的视频 | 是 |
| `video` | `unfaved` | 已从收藏夹移除,但保留历史追踪 | 是 |
| `video` | `unfaved` | 历史遗留状态(迁移 v5 已归一为 `active`) | 是 |
| `video` | `removed` | 视频失效、删除或不可访问 | 否 |
| `video` | `dropped` | 在看板中手动移出库 | 否 |
| `video` | `dropped` | 手动移出库;再次入库会恢复 `active` | 否 |
| `candidate` | `NULL` | 待人工决策 | 否 |
| `candidate` | `rejected` | 已忽略 | 否 |
| `candidate` | `blacklisted` | 所属 UP 已拉黑 | 否 |
| `candidate` | `rejected` | 已忽略 / 所属 UP 已拉黑 | 否 |
### 6.2 建表原则
@@ -335,12 +329,11 @@ PRAGMA synchronous = NORMAL;
- `snapshot` 主键为 `(stat_date, aid)`,保证同日幂等;
- `up_stat` 主键为 `(stat_date, mid)`;
- `folder_resource(media_id, aid)` 记录每个收藏夹实际拥有的资源,不能仅依赖 `video.folder_id`;
- `folder_resource.fav_time` 记录该视频在该夹的加入时间,用于跨夹主归属选择;
- `video.folder_id` 表示视频的主归属夹,按有效收藏关系重算,不依赖收藏夹遍历顺序;
- `up_blacklist` 只在发现任务中生效,收藏夹同步不得用它过滤既有视频;
- `folder` / `folder_resource` 保留为历史数据与重排序参考,**不再由服务写入**;
- `up_blacklist` 只在发现任务中生效,不影响已入库视频;
- `seen_aid` 永久保留搜索去重标记,候选过期删除后也不重复通知;
- `fav_action` 保存 Telegram 或签名链接的动作状态,防止重复点击造成重复写入。
- `fav_action` 保留为通用动作状态表(当前入库幂等由 `video` 存在性判断承担);
- 迁移 v5 把历史 `unfaved` 行归一为 `active`;表结构不做破坏式删除。
### 6.3 迁移规范
@@ -360,13 +353,13 @@ PRAGMA synchronous = NORMAL;
|---|---|---|
| 发现 | `x/web-interface/wbi/search/type` | 每小时搜索候选 |
| 搜索降级 | `x/web-interface/search/type` | WBI 搜索失败时使用 |
| 收藏夹 ID | `x/v3/fav/resource/ids` | 计算新增和移除差集 |
| 收藏夹详情 | `x/v3/fav/resource/infos` | 元数据同步、失效识别 |
| 视频统计 | `x/web-interface/view?aid=` | 日更七项指标的唯一事实来源 |
| 粉丝数 | `x/relation/stat?vmid=` | 每日粉丝快照 |
| 收藏夹列表 | `x/v3/fav/folder/created/list-all` | 容量监控 |
| 加入收藏 | `x/v3/fav/resource/deal` | 确认候选后收藏 |
| 收藏夹排序 | `x/v3/fav/resource/sort` | 定位至同 UP 分组 |
| 视频详情 | `x/web-interface/view?aid=` / `?bvid=` | 快照七项指标、手动入库元数据 |
| 短链解析 | `b23.tv/<token>` | 跟随重定向还原为视频 URL |
| 粉丝数 | `x/relation/stat?vmid=` | 每日粉丝快照 / 入库时粉丝数 |
| 收藏夹 ID | `x/v3/fav/resource/ids` | `favsort.py` 重排序计划 |
| 收藏夹详情 | `x/v3/fav/resource/infos` | `favsort.py` 重排序计划 |
| 收藏夹列表 | `x/v3/fav/folder/created/list-all` | 列出重排序目标夹 |
| 收藏夹排序 | `x/v3/fav/resource/sort` | 收藏夹重排序(唯一写操作) |
B 站请求统一使用 `httpx` 连接池,并按 `data/raw-request.txt` 对齐浏览器特征:
@@ -421,21 +414,17 @@ P3 实现说明:
- 关键词、页数和时间窗由 `discover` 配置控制;
- 同一 aid 在多个关键词/页重复出现时,候选与通知都按 aid 去重;
- 已通知但未决策的候选不会重复通知;未通知的候选会在后续发现轮次补发;
- 人工把候选加入收藏夹后,收藏夹同步会入库并删除同 aid 候选。
- 候选确认入库后会删除同 aid 候选;私聊手动入库也可直接创建视频行。
### 7.3 收藏夹同步
### 7.3 收藏夹解耦
每小时对所有已配置年度夹执行:
项目**不再轮询收藏夹**,也不再根据收藏夹内容标记视频入库/出库:
1. 请求 `resource/ids` 获取远端 aid 集合;
2. 与 `folder_resource` 的有效本地集合做差集;
3. 对新增 aid 每 200 条请求 `resource/infos` 并入库;
4. 对移除 aid 标记对应 `folder_resource.active=0`;若视频不在任何配置夹中,视频状态改为 `unfaved`;
5. 每 6 小时全量刷新 infos,刷新元数据、`attr` 和收藏夹容量;
6. `attr=9` 或请求集合中静默缺失的资源改为 `removed`;
7. 容量达到 950 条时发送预警;系统绝不自动换夹。
`infos` 只能作为元数据和失效同步来源。它缺少点赞、投币、转发,且评论字段不可靠,不能替代 `view` 日更。
- 不再调用 `x/v3/fav/resource/ids` / `infos` 做差集同步;
- 历史 `folder` / `folder_resource` / `video.folder_id` 数据保留,仅供历史展示与 `favsort.py` 重排序参考;
- 迁移 v5 把历史 `unfaved` 状态归一为 `active`;
- 收藏夹由人工维护;需要时用 `favsort.py`(唯一使用 `bilibili.write`)做低频重排序;
- 入库只依赖只读接口(`view` / `relation/stat`)。
### 7.4 每日快照与粉丝采集
@@ -466,45 +455,42 @@ P1 实现说明:
---
## 8. Telegram 与收藏操作
## 8. Telegram 与入库操作
### 8.1 通知动作
候选通知提供:
- **收藏**:加入目标收藏夹并入库;
- **入库**:写入本地 `video(status='active')` 并补当天快照,**不写 B 站收藏夹**;
- **忽略**:候选标记 `rejected`,不再通知;
- **拉黑**:写入 `up_blacklist`,同时拒绝该 UP 的其他待决策候选;
- **打开视频**:外链到 B 站视频页。
拉黑只影响未来搜索发现;不清理、不停止追踪已有视频,也不影响从收藏夹同步进来的视频。
私聊文本消息还会被解析为视频引用(链接 / BV 号 / av 号 / `b23.tv` 短链)并逐条入库。拉黑只影响未来搜索发现;不清理、不停止追踪已有视频。
### 8.2 幂等与失败处理
Telegram `callback_data` 只包含短操作 ID,例如 `fav:<id>`;真实 aid、目标夹、过期时间和状态保存在 `fav_action` 表。
Telegram `callback_data` 只包含短操作 ID,例如 `add:<aid>` / `ign:<aid>` / `blk:<aid>`;`fav:<aid>` 为旧按钮兼容。
收藏流程:
入库流程:
1. 将动作从 `pending` 原子切换为 `processing`;
2. 先读取目标夹 ids,若视频已存在则按成功处理;
3. 按“同 UP 最早视频所在夹优先,否则当年夹”的规则选择目标夹;
4. 调用 `deal` 加入收藏;
5. 调用 `sort` 将视频放进同 UP 连续分组;
6. 回读 ids 校验最终位置;
7. 成功:动作标记 `done`,视频标记 `active`,删除候选;
8. 失败:动作标记 `failed`,保留候选与原始错误,允许用户重试。
1. 候选存在校验;若视频已在库则返回「已在库」;
2. 用只读客户端取一次 `view` 详情(失败时回退候选元数据);
3. 写 `video(status='active')`,删除同 aid 候选;
4. 用取到的 `view` 数据写一条当天快照(同日已有成功记录则跳过);
5. 采集该 UP 当前粉丝数并提升缓存版本。
私聊手动入库按消息中的每个引用重复上述步骤,错误逐条隔离,不中断其余引用。
GET 页面或链接预览只能展示信息,绝不能触发任何写操作。
P3 实现说明:
- `callback_data` 使用 `fav:<aid>` / `ign:<aid>` / `blk:<aid>`,真实状态存 `fav_action`;
- `begin_fav_action` 把 `pending/failed` 原子切换为 `processing`;并发点击返回 `busy`;
- 已在目标夹时直接成功,不重复 `deal`;
- 目标夹优先同 UP 在库最早视频所在夹,其次当前自然年夹,最后最新年度夹;
- 插入位置由 `fav_position.plan_insertion` 计算;组头需要两次 `sort`;
- `deal` 或 `sort` 失败写 `fav_action.state='failed'`,保留候选,用户可再次点击重试;
- Telegram 只接受 `chat_id` 与 `from.id` 都等于配置私聊用户的回调。
- `callback_data` 使用 `add:<aid>` / `ign:<aid>` / `blk:<aid>`(`fav:` 兼容旧消息);
- 入库只写本地库;`video` 的 `removed` / `dropped` / `unfaved` 状态会被重新激活;
- 引用解析是纯函数 `video_ref.extract_video_refs`,`b23.tv` 短链由 `bilibili.client.resolve_short_link` 跟随跳转;
- 手动入库不做「标题含咻咻满」等规则校验;单条消息的引用数量有上限;
- Telegram 只接受 `chat_id` 与 `from.id` 都等于配置私聊用户的回调与消息。
---
@@ -513,16 +499,16 @@ P3 实现说明:
### 9.1 API 原则
- P2 看板**只读**:业务接口只注册 GET,写方法统一返回 `405`;
- 数据管理(收藏、忽略、拉黑、移出库、移除黑名单)继续通过 Telegram、CLI 或数据库完成;
- 数据管理(入库、忽略、拉黑、移出库、移除黑名单)继续通过 Telegram、CLI 或数据库完成;
- `/api/health` 和 `/api/session` 可匿名;
- 首页、榜单、视频/UP、粉丝页面和接口访客可见;
- 配置 `web.auth_token` 后,`/api/candidates`、`/api/system` 和 `/candidates`、`/system` 页面要求 Cookie 或 `Authorization: Bearer <token>`,未登录访问页面会跳转 `/login?next=<原路径>`;
- 未配置 token 时视为无鉴权,全部页面可见;
- 只读接口不需要 CSRF;登录 POST 只建立会话,不修改业务数据;
- API 返回 JSON;前端不复制热度、状态机或收藏夹归属判断;
- API 返回 JSON;前端不复制热度或状态机判断;
- 不提供导出接口,结果只通过页面浏览;
- 首页、榜单和粉丝的聚合结果使用内存 TTL 缓存:首页/榜单 1 小时,粉丝 24 小时;
- 缓存版本存放在 `kv`(`cache:v:videos` / `cache:v:fans`);快照、收藏夹同步、发现、收藏/忽略/拉黑、候选清理和粉丝 CSV 导入都会提升对应版本,使缓存立刻失效。
- 缓存版本存放在 `kv`(`cache:v:videos` / `cache:v:fans`);快照、发现、入库/忽略/拉黑、候选清理和粉丝 CSV 导入都会提升对应版本,使缓存立刻失效。
核心接口:
@@ -633,13 +619,14 @@ uv run python tests/test_moves.py
新增模块至少覆盖:
- 标题归一化、时长解析、分类打分;
- 视频引用解析(BV/av/URL/`b23.tv` 短链)与 `view?bvid=` 契约;
- 黑名单只影响发现;
- `infos` 静默缺失资源的识别;
- `active`、`unfaved`、`removed`、`dropped` 状态迁移;
- 数据库迁移与幂等写入;
- 每日快照缺失处理;
- 热度、增长和互动率计算;
- 收藏夹目标选择与排序锚点;
- 入库动作(候选确认 / 手动)与幂等;
- `fav_action` 的重复点击、失败重试和过期处理;
- Telegram 用户与 chat ID 授权校验;
- 看板查询的筛选、分页、增长冷启动与状态过滤;
@@ -655,7 +642,7 @@ uv run python tests/test_moves.py
## 12. 实施阶段
### P0:数据底座与收藏夹同步 ✅
### P0:数据底座与收藏夹同步 ✅(收藏夹同步已于第 10 轮移除)
- SQLite 迁移、配置加载、B 站只读客户端;
- 收藏夹首次导入与每小时差集同步;
@@ -682,7 +669,7 @@ uv run python tests/test_moves.py
- 已实现 `services/snapshot.py`:`view` 七项指标、`relation/stat` 粉丝数、`ok=0` 失败占位、`ok=1` 断点跳过;
- 实测不可访问视频返回 `62012`,已和旧口径 `-404`、`62002` 一起按 `removed` 处理;
- `run` 调度器同时注册收藏夹同步与每日 `snapshot_at` 任务,启动时立即检查当日是否完成以支持补跑;
- `run` 调度器注册每日 `snapshot_at` 任务,启动时立即检查当日是否完成以支持补跑(早期同时注册收藏夹同步,已于第 10 轮移除);
- 每次快照后生成日备份并清理超过 14 天的日备份;
- `auth-check` 与快照启动时检查 Cookie 登录态,失效写 `kv` 并告警;
- 真实全量联调:795 条视频全部 `ok=1`,0 失败、0 移除,耗时 1124 秒;粉丝 `follower=2767270`;日备份 `data/backup/app-20261031.db` 含 795 条快照;二次执行正确命中完成标记并跳过;
@@ -737,7 +724,7 @@ uv run python tests/test_moves.py
### 第 9 轮:时长效率榜、粉丝快照与导航拆分 ✅(2026-11-01)
- 迁移 v4:`video.pages`、`video.up_follower` / `up_follower_at` / `up_follower_backfilled`、`up_profile`;
- 收藏夹同步结束时采集缺失 UP 的粉丝数(`relation/stat`,匿名可用);`first_seen >= 运行开始 - 1h` 记为实时,更早的记为回填;`sync --no-capture-followers` 可跳过;
- 收藏夹同步(已于第 10 轮移除)结束时采集缺失 UP 的粉丝数(`relation/stat`,匿名可用);`first_seen >= 运行开始 - 1h` 记为实时,更早的记为回填;第 10 轮起改为「入库时」采集;
- 粉丝采集对每条 `relation/stat` 加 45s 硬超时看门狗(防 DNS/传输层挂起),每 25 条输出 `up_profile.progress` 进度;单条失败不阻塞整批;
- `app.py backfill-followers`:一次性回填历史视频粉丝数,支持 `--limit` / `--dry-run`;
- 新增内容效率/互动效率榜与 UP 内容效率(Gamma–Poisson EB 收缩),过滤 < 600 秒与未满 2 天的视频;
@@ -748,6 +735,18 @@ uv run python tests/test_moves.py
- 本地只保留可再生的预览库 `data/preview/app.db`;
- 自动化测试新增 `tests/test_up_profile.py`,并扩展 `test_analytics.py`、`test_repository.py`、`test_config.py`、`test_folder_sync.py`、`test_web_queries.py`、`test_web_server.py`、`test_migrations.py`、`test_database.py`、`test_cli.py`。
### 第 10 轮:收藏夹解耦、手动入库与写鉴权收口 ✅(2026-11-01)
- **入库只写本地库**:候选确认不再调用 `deal` / `sort` 写收藏夹,改为写 `video(status='active')` 并补当天快照;
- **移除收藏夹追踪**:删除 `services/folder_sync.py`、`FavoriteActionService`、`app.py sync` 与调度中的 `folder_sync` 任务;`folder` / `folder_resource` / `video.folder_id` 仅保留为历史数据;
- **迁移 v5**:把历史 `unfaved` 状态归一为 `active`(不再有「取消收藏」语义);
- **手动入库**:新增 `video_ref.py` 纯函数解析 BV/av/URL/`b23.tv` 短链;`bilibili/client.py` 新增 `view?bvid=` 详情与短链跳转;`services/library.py` 统一候选/手动入库;
- **Telegram**:`get_updates` 订阅 `callback_query` + `message`;私聊文本消息自动解析并入库;按钮改为「入库 / 忽略 / 拉黑」(`add:`,兼容旧 `fav:`);
- **写鉴权收口**:删除 `write_client.deal`,`bilibili.write` 仅用于 `favsort.py` 收藏夹重排序;
- **配置**:移除 `schedule.folder_sync_minutes` / `full_folder_refresh_hours`;`folders` 语义变为历史来源与重排序目标;
- 看板:系统页与首页把「收藏夹同步/容量」改为「历史收藏夹」;候选页文案改为「入库」。
- 自动化测试:新增 `tests/test_video_ref.py`、`tests/test_library.py`,重写 `test_telegram.py`,删除 `test_folder_sync.py` / `test_fav_position.py` / `test_favorites.py`,并更新 `test_bilibili_client.py`、`test_cli.py`、`test_migrations.py`、`test_database.py`、`test_web_queries.py`、`test_web_server.py`、`web_fixtures.py`。
### P4:可选部署
- 反向代理、TLS、远程访问鉴权;
@@ -761,7 +760,7 @@ uv run python tests/test_moves.py
第一期不实现:
- 多用户、注册、角色权限;
- 自动创建收藏夹或容量满后自动换夹;
- 自动创建收藏夹、自动加入收藏夹或容量满后自动换夹;
- AI 自动判定 reaction;
- UP 白名单;
- 视频历史统计回填;