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
+196 -205

No files matched your search

+77 -78
View File
@@ -1,6 +1,6 @@
# 咻咻满 Reaction 视频数据看板开发文档 # 咻咻满 Reaction 视频数据看板开发文档
> **状态:** P0(数据底座与收藏夹同步)、P1(每日快照与粉丝追踪)、P2(只读本地看板)、P3(Telegram 与一键加入)已实施 > **状态:** P0(数据底座与手动入库)、P1(每日快照与粉丝追踪)、P2(只读本地看板)、P3(Telegram 发现与入库)已实施;项目已与 B 站收藏夹解耦
> >
> **技术基线:** Python 3.14、Python 标准库、SQLite、原生 HTML/CSS/JavaScript、`uv` > **技术基线:** Python 3.14、Python 标准库、SQLite、原生 HTML/CSS/JavaScript、`uv`
> >
@@ -89,10 +89,11 @@ uv run python tests/test_moves.py
# 建库、迁移与黑名单种子(离线,P0) # 建库、迁移与黑名单种子(离线,P0)
uv run python app.py init uv run python app.py init
# 收藏夹只读同步/首次导入(P0) # 手动入库:视频链接、BV 号、av 号或 b23.tv 短链(只写本地库)
uv run python app.py sync 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
uv run python app.py run --once 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
uv run python app.py backfill-followers --dry-run uv run python app.py backfill-followers --dry-run
# 全量同步默认会在结尾采集缺失粉丝数;可用 --no-capture-followers 跳过
uv run python app.py sync --full --no-capture-followers
# 检查 Cookie 登录态(P1) # 检查 Cookie 登录态(P1)
uv run python app.py auth-check uv run python app.py auth-check
@@ -171,7 +169,7 @@ uv sync --all-groups
│ 单一 Python 进程 │ │ 单一 Python 进程 │
│ │ │ │
│ Web 线程:ThreadingHTTPServer :8080 │ │ Web 线程:ThreadingHTTPServer :8080 │
│ 调度线程:发现 / 收藏夹同步 / 快照 / 清理 │ │ 调度线程:发现 / 快照 / 清理 │
│ Telegram:getUpdates long polling │ │ Telegram:getUpdates long polling │
│ 写入线程:SQLite 单写队列 │ │ 写入线程:SQLite 单写队列 │
│ │ │ │
@@ -186,7 +184,7 @@ uv sync --all-groups
- 所有 SQLite 写操作经单写队列串行执行,避免 `database is locked`; - 所有 SQLite 写操作经单写队列串行执行,避免 `database is locked`;
- 读操作使用短生命周期连接; - 读操作使用短生命周期连接;
- 网络请求期间不得持有数据库写事务; - 网络请求期间不得持有数据库写事务;
- 收藏操作通过 `fav_action` 状态锁保证幂等; - 入库操作通过 `video` 存在性判断保证幂等;
- SQLite 启用 WAL、外键和合理的 busy timeout。 - SQLite 启用 WAL、外键和合理的 busy timeout。
```sql ```sql
@@ -219,11 +217,11 @@ PRAGMA synchronous = NORMAL;
│ ├── scheduler.py # 内置任务调度 │ ├── scheduler.py # 内置任务调度
│ ├── logging_json.py # 脱敏 JSON 日志 │ ├── logging_json.py # 脱敏 JSON 日志
│ ├── classify.py # 标题归一化、时长解析与打分 │ ├── classify.py # 标题归一化、时长解析与打分
│ ├── fav_position.py # 新视频插入位置计算 │ ├── video_ref.py # BV/av/URL/b23.tv 引用解析(纯函数)
│ ├── timeutil.py # 上海时区与每日时间 │ ├── timeutil.py # 上海时区与每日时间
│ ├── bilibili/ │ ├── bilibili/
│ │ ├── client.py # 只读 HTTP、重试与限速 │ │ ├── client.py # 只读 HTTP、重试与限速(含 view 详情、短链解析)
│ │ ├── write_client.py # deal / sort 写客户端 │ │ ├── write_client.py # sort 写客户端(仅收藏夹重排序)
│ │ ├── search.py # WBI 搜索与旧接口降级 │ │ ├── search.py # WBI 搜索与旧接口降级
│ │ ├── provider.py # 凭据变化重建客户端 │ │ ├── provider.py # 凭据变化重建客户端
│ │ ├── auth.py # Cookie、设备指纹与请求头工具 │ │ ├── auth.py # Cookie、设备指纹与请求头工具
@@ -231,9 +229,8 @@ PRAGMA synchronous = NORMAL;
│ │ └── models.py # 接口响应模型 │ │ └── models.py # 接口响应模型
│ ├── services/ │ ├── services/
│ │ ├── discovery.py # 搜索发现与候选池 │ │ ├── discovery.py # 搜索发现与候选池
│ │ ├── favorites.py # 收藏 / 忽略 / 拉黑 │ │ ├── library.py # 入库(候选/手动)/ 忽略 / 拉黑
│ │ ├── telegram_poller.py # getUpdates 与回调分发 │ │ ├── telegram_poller.py # getUpdates、回调分发与私聊手动入库
│ │ ├── folder_sync.py
│ │ ├── snapshot.py │ │ ├── snapshot.py
│ │ ├── up_profile.py # UP 粉丝快照采集与历史回填 │ │ ├── up_profile.py # UP 粉丝快照采集与历史回填
│ │ └── maintenance.py │ │ └── maintenance.py
@@ -292,8 +289,6 @@ PRAGMA synchronous = NORMAL;
"up_blacklist_seed": [261052419, 1908537643, 1701121088, 542262], "up_blacklist_seed": [261052419, 1908537643, 1701121088, 542262],
"schedule": { "schedule": {
"discover_minutes": 60, "discover_minutes": 60,
"folder_sync_minutes": 60,
"full_folder_refresh_hours": 6,
"snapshot_at": "00:01", "snapshot_at": "00:01",
"candidate_ttl_days": 7 "candidate_ttl_days": 7
}, },
@@ -301,17 +296,17 @@ PRAGMA synchronous = NORMAL;
} }
``` ```
- `folders` 中的年度夹由人工创建,系统只读; - `folders` 仅作为历史来源与 `favsort.py` 重排序目标,系统不再自动同步或建夹;
- 配置必须为 `bilibili.read` / `bilibili.write` 双账号结构;不兼容顶层扁平 `mid` / `csrf` / `cookie`; - 配置必须为 `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`; ### 5.2 安全规则- Windows 本地开发时应限制 `config.json` 的文件访问权限;部署到 Linux 时权限必须为 `600`;
- 配置文件每 60 秒检查一次 mtime,凭据、收藏夹、分类规则和权重变更后无需重启; - 配置文件每 60 秒检查一次 mtime,凭据、分类规则和权重变更后无需重启;
- Cookie / CSRF 变化时由 `ClientProvider` 重建 B 站请求客户端,并保留该角色的限速与重试策略; - Cookie / CSRF 变化时由 `ClientProvider` 重建 B 站请求客户端,并保留该角色的限速与重试策略;
- `schedule.folder_sync_minutes` 与 `schedule.snapshot_at` 在启动时固化,修改后需重启 `run`; - `schedule.discover_minutes` 与 `schedule.snapshot_at` 在启动时固化,修改后需重启 `run`;
- Telegram 操作必须同时验证 `from.id` 和 `chat.id` 是否为配置的私聊用户; - Telegram 操作必须同时验证 `from.id` 和 `chat.id` 是否为配置的私聊用户;
- 仅允许环境变量临时覆盖敏感配置,不能将真实凭据硬编码进源代码; - 仅允许环境变量临时覆盖敏感配置,不能将真实凭据硬编码进源代码;
- `GET` 路由绝不进行收藏、忽略、拉黑等写操作。 - `GET` 路由绝不进行入库、忽略、拉黑等写操作。
--- ---
@@ -322,12 +317,11 @@ PRAGMA synchronous = NORMAL;
| 表 | 状态 | 含义 | 是否参与每日快照 | | 表 | 状态 | 含义 | 是否参与每日快照 |
|---|---|---|---| |---|---|---|---|
| `video` | `active` | 在库追踪的视频 | 是 | | `video` | `active` | 在库追踪的视频 | 是 |
| `video` | `unfaved` | 已从收藏夹移除,但保留历史追踪 | 是 | | `video` | `unfaved` | 历史遗留状态(迁移 v5 已归一为 `active`) | 是 |
| `video` | `removed` | 视频失效、删除或不可访问 | 否 | | `video` | `removed` | 视频失效、删除或不可访问 | 否 |
| `video` | `dropped` | 在看板中手动移出库 | 否 | | `video` | `dropped` | 手动移出库;再次入库会恢复 `active` | 否 |
| `candidate` | `NULL` | 待人工决策 | 否 | | `candidate` | `NULL` | 待人工决策 | 否 |
| `candidate` | `rejected` | 已忽略 | 否 | | `candidate` | `rejected` | 已忽略 / 所属 UP 已拉黑 | 否 |
| `candidate` | `blacklisted` | 所属 UP 已拉黑 | 否 |
### 6.2 建表原则 ### 6.2 建表原则
@@ -335,12 +329,11 @@ PRAGMA synchronous = NORMAL;
- `snapshot` 主键为 `(stat_date, aid)`,保证同日幂等; - `snapshot` 主键为 `(stat_date, aid)`,保证同日幂等;
- `up_stat` 主键为 `(stat_date, mid)`; - `up_stat` 主键为 `(stat_date, mid)`;
- `folder_resource(media_id, aid)` 记录每个收藏夹实际拥有的资源,不能仅依赖 `video.folder_id`; - `folder` / `folder_resource` 保留为历史数据与重排序参考,**不再由服务写入**;
- `folder_resource.fav_time` 记录该视频在该夹的加入时间,用于跨夹主归属选择; - `up_blacklist` 只在发现任务中生效,不影响已入库视频;
- `video.folder_id` 表示视频的主归属夹,按有效收藏关系重算,不依赖收藏夹遍历顺序;
- `up_blacklist` 只在发现任务中生效,收藏夹同步不得用它过滤既有视频;
- `seen_aid` 永久保留搜索去重标记,候选过期删除后也不重复通知; - `seen_aid` 永久保留搜索去重标记,候选过期删除后也不重复通知;
- `fav_action` 保存 Telegram 或签名链接的动作状态,防止重复点击造成重复写入。 - `fav_action` 保留为通用动作状态表(当前入库幂等由 `video` 存在性判断承担);
- 迁移 v5 把历史 `unfaved` 行归一为 `active`;表结构不做破坏式删除。
### 6.3 迁移规范 ### 6.3 迁移规范
@@ -360,13 +353,13 @@ PRAGMA synchronous = NORMAL;
|---|---|---| |---|---|---|
| 发现 | `x/web-interface/wbi/search/type` | 每小时搜索候选 | | 发现 | `x/web-interface/wbi/search/type` | 每小时搜索候选 |
| 搜索降级 | `x/web-interface/search/type` | WBI 搜索失败时使用 | | 搜索降级 | `x/web-interface/search/type` | WBI 搜索失败时使用 |
| 收藏夹 ID | `x/v3/fav/resource/ids` | 计算新增和移除差集 | | 视频详情 | `x/web-interface/view?aid=` / `?bvid=` | 快照七项指标、手动入库元数据 |
| 收藏夹详情 | `x/v3/fav/resource/infos` | 元数据同步、失效识别 | | 短链解析 | `b23.tv/<token>` | 跟随重定向还原为视频 URL |
| 视频统计 | `x/web-interface/view?aid=` | 日更七项指标的唯一事实来源 | | 粉丝数 | `x/relation/stat?vmid=` | 每日粉丝快照 / 入库时粉丝数 |
| 粉丝数 | `x/relation/stat?vmid=` | 每日粉丝快照 | | 收藏夹 ID | `x/v3/fav/resource/ids` | `favsort.py` 重排序计划 |
| 收藏夹列表 | `x/v3/fav/folder/created/list-all` | 容量监控 | | 收藏夹详情 | `x/v3/fav/resource/infos` | `favsort.py` 重排序计划 |
| 加入收藏 | `x/v3/fav/resource/deal` | 确认候选后收藏 | | 收藏夹列表 | `x/v3/fav/folder/created/list-all` | 列出重排序目标夹 |
| 收藏夹排序 | `x/v3/fav/resource/sort` | 定位至同 UP 分组 | | 收藏夹排序 | `x/v3/fav/resource/sort` | 收藏夹重排序(唯一写操作) |
B 站请求统一使用 `httpx` 连接池,并按 `data/raw-request.txt` 对齐浏览器特征: B 站请求统一使用 `httpx` 连接池,并按 `data/raw-request.txt` 对齐浏览器特征:
@@ -421,21 +414,17 @@ P3 实现说明:
- 关键词、页数和时间窗由 `discover` 配置控制; - 关键词、页数和时间窗由 `discover` 配置控制;
- 同一 aid 在多个关键词/页重复出现时,候选与通知都按 aid 去重; - 同一 aid 在多个关键词/页重复出现时,候选与通知都按 aid 去重;
- 已通知但未决策的候选不会重复通知;未通知的候选会在后续发现轮次补发; - 已通知但未决策的候选不会重复通知;未通知的候选会在后续发现轮次补发;
- 人工把候选加入收藏夹后,收藏夹同步会入库并删除同 aid 候选。 - 候选确认入库后会删除同 aid 候选;私聊手动入库也可直接创建视频行。
### 7.3 收藏夹同步 ### 7.3 收藏夹解耦
每小时对所有已配置年度夹执行: 项目**不再轮询收藏夹**,也不再根据收藏夹内容标记视频入库/出库:
1. 请求 `resource/ids` 获取远端 aid 集合; - 不再调用 `x/v3/fav/resource/ids` / `infos` 做差集同步;
2. 与 `folder_resource` 的有效本地集合做差集; - 历史 `folder` / `folder_resource` / `video.folder_id` 数据保留,仅供历史展示与 `favsort.py` 重排序参考;
3. 对新增 aid 每 200 条请求 `resource/infos` 并入库; - 迁移 v5 把历史 `unfaved` 状态归一为 `active`;
4. 对移除 aid 标记对应 `folder_resource.active=0`;若视频不在任何配置夹中,视频状态改为 `unfaved`; - 收藏夹由人工维护;需要时用 `favsort.py`(唯一使用 `bilibili.write`)做低频重排序;
5. 每 6 小时全量刷新 infos,刷新元数据、`attr` 和收藏夹容量; - 入库只依赖只读接口(`view` / `relation/stat`)。
6. `attr=9` 或请求集合中静默缺失的资源改为 `removed`;
7. 容量达到 950 条时发送预警;系统绝不自动换夹。
`infos` 只能作为元数据和失效同步来源。它缺少点赞、投币、转发,且评论字段不可靠,不能替代 `view` 日更。
### 7.4 每日快照与粉丝采集 ### 7.4 每日快照与粉丝采集
@@ -466,45 +455,42 @@ P1 实现说明:
--- ---
## 8. Telegram 与收藏操作 ## 8. Telegram 与入库操作
### 8.1 通知动作 ### 8.1 通知动作
候选通知提供: 候选通知提供:
- **收藏**:加入目标收藏夹并入库; - **入库**:写入本地 `video(status='active')` 并补当天快照,**不写 B 站收藏夹**;
- **忽略**:候选标记 `rejected`,不再通知; - **忽略**:候选标记 `rejected`,不再通知;
- **拉黑**:写入 `up_blacklist`,同时拒绝该 UP 的其他待决策候选; - **拉黑**:写入 `up_blacklist`,同时拒绝该 UP 的其他待决策候选;
- **打开视频**:外链到 B 站视频页。 - **打开视频**:外链到 B 站视频页。
拉黑只影响未来搜索发现;不清理、不停止追踪已有视频,也不影响从收藏夹同步进来的视频。 私聊文本消息还会被解析为视频引用(链接 / BV 号 / av 号 / `b23.tv` 短链)并逐条入库。拉黑只影响未来搜索发现;不清理、不停止追踪已有视频。
### 8.2 幂等与失败处理 ### 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`; 1. 候选存在校验;若视频已在库则返回「已在库」;
2. 先读取目标夹 ids,若视频已存在则按成功处理; 2. 用只读客户端取一次 `view` 详情(失败时回退候选元数据);
3. 按“同 UP 最早视频所在夹优先,否则当年夹”的规则选择目标夹; 3. 写 `video(status='active')`,删除同 aid 候选;
4. 调用 `deal` 加入收藏; 4. 用取到的 `view` 数据写一条当天快照(同日已有成功记录则跳过);
5. 调用 `sort` 将视频放进同 UP 连续分组; 5. 采集该 UP 当前粉丝数并提升缓存版本。
6. 回读 ids 校验最终位置;
7. 成功:动作标记 `done`,视频标记 `active`,删除候选; 私聊手动入库按消息中的每个引用重复上述步骤,错误逐条隔离,不中断其余引用。
8. 失败:动作标记 `failed`,保留候选与原始错误,允许用户重试。
GET 页面或链接预览只能展示信息,绝不能触发任何写操作。 GET 页面或链接预览只能展示信息,绝不能触发任何写操作。
P3 实现说明: P3 实现说明:
- `callback_data` 使用 `fav:<aid>` / `ign:<aid>` / `blk:<aid>`,真实状态存 `fav_action`; - `callback_data` 使用 `add:<aid>` / `ign:<aid>` / `blk:<aid>`(`fav:` 兼容旧消息);
- `begin_fav_action` 把 `pending/failed` 原子切换为 `processing`;并发点击返回 `busy`; - 入库只写本地库;`video` 的 `removed` / `dropped` / `unfaved` 状态会被重新激活;
- 已在目标夹时直接成功,不重复 `deal`; - 引用解析是纯函数 `video_ref.extract_video_refs`,`b23.tv` 短链由 `bilibili.client.resolve_short_link` 跟随跳转;
- 目标夹优先同 UP 在库最早视频所在夹,其次当前自然年夹,最后最新年度夹; - 手动入库不做「标题含咻咻满」等规则校验;单条消息的引用数量有上限;
- 插入位置由 `fav_position.plan_insertion` 计算;组头需要两次 `sort`; - Telegram 只接受 `chat_id` 与 `from.id` 都等于配置私聊用户的回调与消息。
- `deal` 或 `sort` 失败写 `fav_action.state='failed'`,保留候选,用户可再次点击重试;
- Telegram 只接受 `chat_id` 与 `from.id` 都等于配置私聊用户的回调。
--- ---
@@ -513,16 +499,16 @@ P3 实现说明:
### 9.1 API 原则 ### 9.1 API 原则
- P2 看板**只读**:业务接口只注册 GET,写方法统一返回 `405`; - P2 看板**只读**:业务接口只注册 GET,写方法统一返回 `405`;
- 数据管理(收藏、忽略、拉黑、移出库、移除黑名单)继续通过 Telegram、CLI 或数据库完成; - 数据管理(入库、忽略、拉黑、移出库、移除黑名单)继续通过 Telegram、CLI 或数据库完成;
- `/api/health` 和 `/api/session` 可匿名; - `/api/health` 和 `/api/session` 可匿名;
- 首页、榜单、视频/UP、粉丝页面和接口访客可见; - 首页、榜单、视频/UP、粉丝页面和接口访客可见;
- 配置 `web.auth_token` 后,`/api/candidates`、`/api/system` 和 `/candidates`、`/system` 页面要求 Cookie 或 `Authorization: Bearer <token>`,未登录访问页面会跳转 `/login?next=<原路径>`; - 配置 `web.auth_token` 后,`/api/candidates`、`/api/system` 和 `/candidates`、`/system` 页面要求 Cookie 或 `Authorization: Bearer <token>`,未登录访问页面会跳转 `/login?next=<原路径>`;
- 未配置 token 时视为无鉴权,全部页面可见; - 未配置 token 时视为无鉴权,全部页面可见;
- 只读接口不需要 CSRF;登录 POST 只建立会话,不修改业务数据; - 只读接口不需要 CSRF;登录 POST 只建立会话,不修改业务数据;
- API 返回 JSON;前端不复制热度、状态机或收藏夹归属判断; - API 返回 JSON;前端不复制热度或状态机判断;
- 不提供导出接口,结果只通过页面浏览; - 不提供导出接口,结果只通过页面浏览;
- 首页、榜单和粉丝的聚合结果使用内存 TTL 缓存:首页/榜单 1 小时,粉丝 24 小时; - 首页、榜单和粉丝的聚合结果使用内存 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` 静默缺失资源的识别; - `infos` 静默缺失资源的识别;
- `active`、`unfaved`、`removed`、`dropped` 状态迁移; - `active`、`unfaved`、`removed`、`dropped` 状态迁移;
- 数据库迁移与幂等写入; - 数据库迁移与幂等写入;
- 每日快照缺失处理; - 每日快照缺失处理;
- 热度、增长和互动率计算; - 热度、增长和互动率计算;
- 收藏夹目标选择与排序锚点; - 入库动作(候选确认 / 手动)与幂等;
- `fav_action` 的重复点击、失败重试和过期处理; - `fav_action` 的重复点击、失败重试和过期处理;
- Telegram 用户与 chat ID 授权校验; - Telegram 用户与 chat ID 授权校验;
- 看板查询的筛选、分页、增长冷启动与状态过滤; - 看板查询的筛选、分页、增长冷启动与状态过滤;
@@ -655,7 +642,7 @@ uv run python tests/test_moves.py
## 12. 实施阶段 ## 12. 实施阶段
### P0:数据底座与收藏夹同步 ✅ ### P0:数据底座与收藏夹同步 ✅(收藏夹同步已于第 10 轮移除)
- SQLite 迁移、配置加载、B 站只读客户端; - SQLite 迁移、配置加载、B 站只读客户端;
- 收藏夹首次导入与每小时差集同步; - 收藏夹首次导入与每小时差集同步;
@@ -682,7 +669,7 @@ uv run python tests/test_moves.py
- 已实现 `services/snapshot.py`:`view` 七项指标、`relation/stat` 粉丝数、`ok=0` 失败占位、`ok=1` 断点跳过; - 已实现 `services/snapshot.py`:`view` 七项指标、`relation/stat` 粉丝数、`ok=0` 失败占位、`ok=1` 断点跳过;
- 实测不可访问视频返回 `62012`,已和旧口径 `-404`、`62002` 一起按 `removed` 处理; - 实测不可访问视频返回 `62012`,已和旧口径 `-404`、`62002` 一起按 `removed` 处理;
- `run` 调度器同时注册收藏夹同步与每日 `snapshot_at` 任务,启动时立即检查当日是否完成以支持补跑; - `run` 调度器注册每日 `snapshot_at` 任务,启动时立即检查当日是否完成以支持补跑(早期同时注册收藏夹同步,已于第 10 轮移除);
- 每次快照后生成日备份并清理超过 14 天的日备份; - 每次快照后生成日备份并清理超过 14 天的日备份;
- `auth-check` 与快照启动时检查 Cookie 登录态,失效写 `kv` 并告警; - `auth-check` 与快照启动时检查 Cookie 登录态,失效写 `kv` 并告警;
- 真实全量联调:795 条视频全部 `ok=1`,0 失败、0 移除,耗时 1124 秒;粉丝 `follower=2767270`;日备份 `data/backup/app-20261031.db` 含 795 条快照;二次执行正确命中完成标记并跳过; - 真实全量联调: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) ### 第 9 轮:时长效率榜、粉丝快照与导航拆分 ✅(2026-11-01)
- 迁移 v4:`video.pages`、`video.up_follower` / `up_follower_at` / `up_follower_backfilled`、`up_profile`; - 迁移 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` 进度;单条失败不阻塞整批; - 粉丝采集对每条 `relation/stat` 加 45s 硬超时看门狗(防 DNS/传输层挂起),每 25 条输出 `up_profile.progress` 进度;单条失败不阻塞整批;
- `app.py backfill-followers`:一次性回填历史视频粉丝数,支持 `--limit` / `--dry-run`; - `app.py backfill-followers`:一次性回填历史视频粉丝数,支持 `--limit` / `--dry-run`;
- 新增内容效率/互动效率榜与 UP 内容效率(Gamma–Poisson EB 收缩),过滤 < 600 秒与未满 2 天的视频; - 新增内容效率/互动效率榜与 UP 内容效率(Gamma–Poisson EB 收缩),过滤 < 600 秒与未满 2 天的视频;
@@ -748,6 +735,18 @@ uv run python tests/test_moves.py
- 本地只保留可再生的预览库 `data/preview/app.db`; - 本地只保留可再生的预览库 `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`。 - 自动化测试新增 `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:可选部署 ### P4:可选部署
- 反向代理、TLS、远程访问鉴权; - 反向代理、TLS、远程访问鉴权;
@@ -761,7 +760,7 @@ uv run python tests/test_moves.py
第一期不实现: 第一期不实现:
- 多用户、注册、角色权限; - 多用户、注册、角色权限;
- 自动创建收藏夹或容量满后自动换夹; - 自动创建收藏夹、自动加入收藏夹或容量满后自动换夹;
- AI 自动判定 reaction; - AI 自动判定 reaction;
- UP 白名单; - UP 白名单;
- 视频历史统计回填; - 视频历史统计回填;
+53 -61
View File
@@ -2,11 +2,11 @@
**咻咻满 Reaction 视频数据看板与收藏夹管理工具。** **咻咻满 Reaction 视频数据看板与收藏夹管理工具。**
项目以 B 站年度收藏夹为可信视频来源,补充关键词发现和 Telegram 人工确认,持续采集 reaction 视频的七项数据与咻咻满粉丝数,并提供本地数据看板。现有的收藏夹排序工具仍保留并作为一键加入后的排序能力基础。 项目以本地 SQLite 视频库为可信来源:搜索发现命中的候选经 Telegram 人工确认后入库,也可以在 Telegram 私聊直接发送视频链接或 BV 号手动入库。入库后持续采集七项数据与咻咻满粉丝数,并提供本地数据看板。B 站收藏夹由人工维护,项目只用写鉴权对收藏夹做低频重排序,不再自动加入收藏夹,也不再轮询收藏夹来标记入库/出库。
> 当前状态:**P0(数据底座与收藏夹同步)、P1(快照与粉丝追踪)、P2(只读本地看板)、P3(Telegram 与一键加入)已实施。** > 当前状态:**P0(数据底座)、P1(快照与粉丝追踪)、P2(只读本地看板)、P3(Telegram 发现与入库)已实施,并已完成「收藏夹解耦 + 手动入库」改造。**
> >
> 当前已可用:扫码授权、收藏夹读取、排序计划生成、执行与校验;SQLite 建库迁移、收藏夹首次导入与增量同步、失效识别、黑名单种子;每日七项视频快照、粉丝数快照、失败占位、断点续跑与日备份;搜索发现、候选池、Telegram 通知与收藏/忽略/拉黑回调;只读本地看板与可重复生成的预览数据。 > 当前已可用:扫码授权、收藏夹重排序(`favsort.py`);SQLite 建库迁移与黑名单种子;每日七项视频快照、粉丝数快照、失败占位、断点续跑与日备份;搜索发现、候选池、Telegram 入库/忽略/拉黑回调与私聊手动入库;只读本地看板与可重复生成的预览数据。
> >
> 详细业务口径、接口验证和风险记录见 [project.md](project.md);开发架构与实施计划见 [DEVELOPMENT.md](DEVELOPMENT.md)。 > 详细业务口径、接口验证和风险记录见 [project.md](project.md);开发架构与实施计划见 [DEVELOPMENT.md](DEVELOPMENT.md)。
@@ -14,31 +14,29 @@
| 功能 | 状态 | 说明 | | 功能 | 状态 | 说明 |
|---|---|---| |---|---|---|
| 收藏夹排序 | 可用 | 同一 UP 连续,组内按发布时间倒序,组间按该 UP 最早视频发布时间倒序。 | | 收藏夹排序 | 可用 | 同一 UP 连续,组内按发布时间倒序,组间按该 UP 最早视频发布时间倒序。唯一使用写鉴权的功能。 |
| 扫码授权与 Cookie 续期 | 可用 | 本地生成二维码;更新身份 Cookie 时保留设备指纹。 | | 扫码授权与 Cookie 续期 | 可用 | 本地生成二维码;更新身份 Cookie 时保留设备指纹。 |
| 收藏夹同步与首次导入 | 可用(P0) | 监控已配置年度夹,自动发现人工新收藏的视频与失效资源。 | | 视频入库 | 可用 | 搜索候选经 Telegram 确认入库;私聊发送视频链接、BV 号、av 号或 `b23.tv` 短链手动入库。只写本地库。 |
| 视频日快照 | 可用(P1) | 逐条采集播放、点赞、投币、收藏、转发、评论、弹幕。 | | 视频日快照 | 可用(P1) | 逐条采集播放、点赞、投币、收藏、转发、评论、弹幕。 |
| 粉丝追踪 | 可用(P1) | 每日记录咻咻满等配置 UP 的粉丝数。 | | 粉丝追踪 | 可用(P1) | 每日记录咻咻满等配置 UP 的粉丝数。 |
| 搜索发现与候选池 | 可用(P3) | 每小时搜索前 2 页,分类打分后写入候选池。 | | 搜索发现与候选池 | 可用(P3) | 每小时搜索前 2 页,分类打分后写入候选池。 |
| Telegram 通知与一键加入 | 可用(P3) | 收藏 / 忽略 / 拉黑按钮,`deal` + `sort` 回读校验。 | | Telegram 通知与入库 | 可用(P3) | 入库 / 忽略 / 拉黑按钮;私聊消息手动入库。 |
| 本地数据看板 | 可用(P2/P9) | 只读总览、视频榜、UP 主榜、内容效率、视频/UP 详情、粉丝曲线、数据说明、候选池和系统状态。 | | 本地数据看板 | 可用(P2/P9) | 只读总览、视频榜、UP 主榜、内容效率、视频/UP 详情、粉丝曲线、数据说明、候选池和系统状态。 |
## 核心工作流 ## 核心工作流
```text ```text
关键词搜索(补漏) ──► 候选池 / Telegram 人工确认 ──► 收藏夹 + 视频库 关键词搜索(补漏) ──► 候选池 ──► Telegram「入库」──┐
│ ├──► 本地视频库(active)──► 每日指标与粉丝快照 ──► 本地看板
人工加入年度收藏夹 ──► 收藏夹同步(真相源)───────────┤ Telegram 私聊发链接/BV 号 ──► 解析 ──► 入库 ─────────┘
▼
每日指标与粉丝快照 B 站收藏夹(人工维护)──► favsort.py 重排序(唯一使用写鉴权,频率极低)
▼
本地看板
``` ```
- **收录范围**:只收录他人观看、试听或点评咻咻满内容的 reaction 视频;最终是否收录由人工确认。 - **收录范围**:只收录他人观看、试听或点评咻咻满内容的 reaction 视频;最终是否收录由人工确认。
- **收藏夹是真相源**:手工加入已配置年度夹的视频自动入库;关键词搜索仅负责发现候选。 - **本地库是真相源**:入库来自「搜索候选确认」与「Telegram 私聊手动入库」;收藏夹不再自动同步。
- **黑名单仅作用于发现**:已收藏或已入库的黑名单 UP 视频不会被清理,也会继续追踪。 - **收藏夹解耦**:项目不再轮询收藏夹,也不会因取消收藏而改变视频状态;收藏夹由人工维护,重排序使用 `favsort.py`。
- **年度夹不自动创建或切换**:同一 UP 的新视频优先进入该 UP 最早视频所在的收藏夹;容量接近上限时预警,失败时告警,不擅自改夹。 - **黑名单仅作用于发现**:已入库的黑名单 UP 视频不会被清理,也会继续追踪。
- **数据不回填**:B 站不提供任意日期的历史指标;新视频从首次入库日起开始积累快照。 - **数据不回填**:B 站不提供任意日期的历史指标;新视频从首次入库日起开始积累快照。
## 本地开发环境 ## 本地开发环境
@@ -58,35 +56,32 @@ uv run bili-fav help
完整的 `uv` 使用方式、依赖规范、架构与开发约定见 [DEVELOPMENT.md](DEVELOPMENT.md)。 完整的 `uv` 使用方式、依赖规范、架构与开发约定见 [DEVELOPMENT.md](DEVELOPMENT.md)。
## P0:数据底座与收藏夹同步 ## P0:数据底座、采集与手动入库
P0 已实现 SQLite 迁移、配置热加载、B 站只读客户端、收藏夹首次导入与增量同步、`infos` 全量元数据刷新、失效识别、容量预警和黑名单种子。所有请求均为只读,不会修改收藏夹。 P0 已实现 SQLite 迁移、配置热加载、B 站只读客户端、黑名单种子与每日快照调度。入库来源为搜索候选确认与 Telegram 私聊手动入库;**项目不再轮询收藏夹**,收藏夹由人工维护。
```powershell ```powershell
# 建库、执行迁移、写入配置文件夹与黑名单种子(离线) # 建库、执行迁移、写入黑名单种子(离线)
uv run python app.py init uv run python app.py init
# 首次导入或增量同步配置中的年度收藏夹(只读联网) # 手动入库:视频链接、BV 号、av 号或 b23.tv 短链(只写本地库)
uv run python app.py sync uv run python app.py ingest BV1x8x166EzY
uv run python app.py ingest https://www.bilibili.com/video/BV1x8x166EzY
uv run python app.py ingest av12345
# 按配置周期常驻运行收藏夹同步 + 每日快照;--once 只跑一次到期任务 # 按配置周期常驻运行发现 + 每日快照;--once 只跑一次到期任务
uv run python app.py run uv run python app.py run
uv run python app.py run --once uv run python app.py run --once
# 强制全量刷新元数据(每 6 小时自动执行一次)
uv run python app.py sync --full
# 查看库内状态(离线) # 查看库内状态(离线)
uv run python app.py status uv run python app.py status
``` ```
- 数据写入 `data/app.db`(SQLite WAL);升级已有数据库前会自动备份到 `data/backup/`; - 数据写入 `data/app.db`(SQLite WAL);升级已有数据库前会自动备份到 `data/backup/`;
- 内置调度器按 `schedule.folder_sync_minutes` 周期运行,`run` 常驻、`sync` 单次; - 入库只写本地 `video(status='active')`,不写 B 站收藏夹;已失效/移出库的视频被再次入库时会重新激活;
- `infos` 静默缺失或 `attr=9` 的资源标记为 `removed`,保留历史; - 视频不可访问(`view` 返回 `-404` / `62002` / `62012`)由每日快照标记为 `removed`;
- 从收藏夹移除的资源标记为 `unfaved`,仍保留数据; - 黑名单只作为发现阶段的种子写入,不影响已入库视频;
- 黑名单只作为发现阶段的种子写入,**不会过滤收藏夹同步**; - `folders` 配置仅作为历史来源与重排序目标,系统不再自动同步或换夹。
- 收藏夹达到 950 条时预警,系统绝不自动换夹;
- 远端返回 0 条但本地仍有记录时默认按异常跳过差集,确认收藏夹确实被清空后才使用 `--allow-empty-folder`。
## P1:每日快照与粉丝追踪 ## P1:每日快照与粉丝追踪
@@ -103,10 +98,7 @@ uv run python app.py snapshot --force
uv run python app.py backfill-followers uv run python app.py backfill-followers
uv run python app.py backfill-followers --dry-run uv run python app.py backfill-followers --dry-run
# 全量同步默认也会在结尾采集缺失的 UP 粉丝数;如只想刷新元数据可跳过 # 分别为只读和写入账号扫码授权(默认 read;write 仅用于 favsort.py)
uv run python app.py sync --full --no-capture-followers
# 分别为只读和写入账号扫码授权(默认 read)
uv run python app.py auth uv run python app.py auth
uv run python app.py auth --account write uv run python app.py auth --account write
@@ -128,9 +120,9 @@ uv run python app.py auth-check --account all
首次真实全量采集结果:795 条视频全部成功、0 失败、0 移除,耗时约 19 分钟;粉丝 `follower=2767270`;日备份为 `data/backup/app-20261031.db`。 首次真实全量采集结果:795 条视频全部成功、0 失败、0 移除,耗时约 19 分钟;粉丝 `follower=2767270`;日备份为 `data/backup/app-20261031.db`。
## P3:搜索发现与 Telegram 一键加入 ## P3:搜索发现、Telegram 入库与手动入库
P3 已实现 WBI 搜索发现、候选池、Telegram 通知与按钮回调,以及使用写账号完成的 `deal` / `sort` 加入流程。 P3 已实现 WBI 搜索发现、候选池、Telegram 通知与按钮回调(仅写本地库),以及 Telegram 私聊手动入库。
```powershell ```powershell
# 执行一次发现;--no-notify 只写候选池,不发 Telegram # 执行一次发现;--no-notify 只写候选池,不发 Telegram
@@ -141,21 +133,21 @@ uv run python app.py discover --no-notify
uv run python app.py candidates uv run python app.py candidates
uv run python app.py candidates --decision rejected uv run python app.py candidates --decision rejected
# 常驻运行:同步 + 快照 + 发现 + Telegram long polling # 常驻运行:快照 + 发现 + Telegram long polling
uv run python app.py run uv run python app.py run
``` ```
- 搜索使用 `x/web-interface/wbi/search/type`,WBI 密钥缓存 6 小时,失败时降级旧版 `search/type`; - 搜索使用 `x/web-interface/wbi/search/type`,WBI 密钥缓存 6 小时,失败时降级旧版 `search/type`;
- 每轮按 `discover.keywords` 搜索前 `discover.pages` 页,过滤已入库/已见/超时间窗/黑名单/标题不含要求词的结果; - 每轮按 `discover.keywords` 搜索前 `discover.pages` 页,过滤已入库/已见/超时间窗/黑名单/标题不含要求词的结果;
- 分数规则与 `classify` 配置一致;`score >= notify.min_score` 才发送 Telegram; - 分数规则与 `classify` 配置一致;`score >= notify.min_score` 才发送 Telegram;
- 通知按钮:**收藏**、**忽略**、**拉黑**;回调只接受 `telegram.chat_id` 对应的私聊用户; - 通知按钮:**入库**、**忽略**、**拉黑**;回调只接受 `telegram.chat_id` 对应的私聊用户;
- 收藏使用 `bilibili.write`:先查目标夹是否已存在,再 `deal` 加入、`sort` 插入同 UP 组头、回读校验位置; - 「入库」把视频写入本地库并置为 `active`,**不写 B 站收藏夹**;同时用取到的 `view` 数据补一条当天快照;
- 失败保留候选并写 `fav_action.state='failed'`,可直接重试;不自动换夹; - 私聊文本消息会自动解析其中的视频链接 / BV 号 / av 号 / `b23.tv` 短链并逐条入库,回执汇总结果;
- 忽略写 `candidate.decision='rejected'`;拉黑写 `up_blacklist` 并拒绝该 UP 的其他待决策候选; - 忽略写 `candidate.decision='rejected'`;拉黑写 `up_blacklist` 并拒绝该 UP 的其他待决策候选;
- 候选超过 `schedule.candidate_ttl_days` 且未决策时自动删除,`seen_aid` 防止重复通知; - 候选超过 `schedule.candidate_ttl_days` 且未决策时自动删除,`seen_aid` 防止重复通知;
- 人工把候选加入收藏夹后,收藏夹同步会入库并删除同 aid 候选。 - 手动入库不做「标题含咻咻满」等规则校验,用户明确指定即入库。
> 真实 `deal` / `sort` 需要用户点击 Telegram 按钮才会触发;离线测试使用契约一致的 Mock 覆盖。 > 入库只依赖只读接口(`view`);写鉴权仅用于 `favsort.py` 收藏夹重排序。离线测试使用契约一致的 Mock 覆盖。
## P2:只读本地看板 ## P2:只读本地看板
@@ -173,7 +165,7 @@ uv run python tools/seed_preview_db.py --days 7
uv run python app.py serve --preview --config data/preview/config.json uv run python app.py serve --preview --config data/preview/config.json
``` ```
- 首页默认“最新动态”:今日增量、最新入库、最近发布、增长动态、热度速览、收藏夹容量与采集状态; - 首页默认“最新动态”:今日增量、最新入库、最近发布、增长动态、热度速览、历史收藏夹与采集状态;
- 「视频」与「UP主」是两个独立入口;工具条顺序为「排序」下拉 → 时间下拉(无需 label)→ 右侧自适应搜索框(最大 320px,放大镜按钮在输入框内),三者在 PC 与移动端都保持一行; - 「视频」与「UP主」是两个独立入口;工具条顺序为「排序」下拉 → 时间下拉(无需 label)→ 右侧自适应搜索框(最大 320px,放大镜按钮在输入框内),三者在 PC 与移动端都保持一行;
- 视频排序:内容效率(默认)、综合热度、发布时间、入库时间、播放/投币/收藏/评论、互动率、播放增量/播放增速;UP 排序:内容效率、互动效率、内容量、总播放、视频数、平均播放、总互动、最近发布; - 视频排序:内容效率(默认)、综合热度、发布时间、入库时间、播放/投币/收藏/评论、互动率、播放增量/播放增速;UP 排序:内容效率、互动效率、内容量、总播放、视频数、平均播放、总互动、最近发布;
- 时间筛选:全部 / 近 7 天 / 近 30 天 / 近 90 天 / 近 1 年 / 已入库的年份;不再提供按收藏夹过滤,UP 页也不再按视频数筛选; - 时间筛选:全部 / 近 7 天 / 近 30 天 / 近 90 天 / 近 1 年 / 已入库的年份;不再提供按收藏夹过滤,UP 页也不再按视频数筛选;
@@ -187,7 +179,7 @@ uv run python app.py serve --preview --config data/preview/config.json
- 配置 `web.auth_token` 后,“候选”和“系统”页面及其接口需要登录,启用登录页和 HttpOnly Cookie;访客的底部导航只显示公开页面; - 配置 `web.auth_token` 后,“候选”和“系统”页面及其接口需要登录,启用登录页和 HttpOnly Cookie;访客的底部导航只显示公开页面;
- 本地回环地址且未配置 token 时视为无鉴权,全部页面可见;非回环地址且未配置 token 时拒绝启动; - 本地回环地址且未配置 token 时视为无鉴权,全部页面可见;非回环地址且未配置 token 时拒绝启动;
- 所有业务接口只接受 GET,写方法返回 `405`; - 所有业务接口只接受 GET,写方法返回 `405`;
- 首页/榜单聚合结果缓存 1 小时,粉丝结果缓存 24 小时;快照、收藏夹同步、发现、收藏操作和粉丝 CSV 导入会立即让对应缓存失效。 - 首页/榜单聚合结果缓存 1 小时,粉丝结果缓存 24 小时;快照、发现、入库/忽略/拉黑、候选清理和粉丝 CSV 导入会立即让对应缓存失效。
### 预览数据 ### 预览数据
@@ -296,18 +288,18 @@ Copy-Item config.example.json config.json
} }
``` ```
- `bilibili.read`:年度收藏夹同步、视频数据快照、粉丝数与其他只读查询使用; - `bilibili.read`:视频数据快照、粉丝数、搜索发现与入库等所有只读查询使用;
- `bilibili.write`:后续加入目标收藏夹、回读验证与现有 `favsort.py` 排序使用; - `bilibili.write`:仅 `favsort.py` 收藏夹重排序使用;
- 两个账号的 Cookie、CSRF 与设备指纹严格隔离;顶层 `mid` / `cookie` / `csrf` **不再支持**; - 两个账号的 Cookie、CSRF 与设备指纹严格隔离;顶层 `mid` / `cookie` / `csrf` **不再支持**;
- `overrides`:可选,为代发或联合投稿视频指定归属 UP; - `overrides`:可选,为代发或联合投稿视频指定归属 UP;
- `folders`:年度收藏夹配置(`year` + `media_id` + `title`),系统不自动建夹; - `folders`:历史来源与重排序目标(`year` + `media_id` + `title`),系统不再自动同步或建夹;
- `up_blacklist_seed`:首次启动写入数据库的 UP 黑名单种子; - `up_blacklist_seed`:首次启动写入数据库的 UP 黑名单种子;
- `track_fans`:需要每日追踪粉丝数的 UP mid; - `track_fans`:需要每日追踪粉丝数的 UP mid;
- `discover`:搜索关键词、页数与时间窗(`keywords` / `pages` / `window_days`); - `discover`:搜索关键词、页数与时间窗(`keywords` / `pages` / `window_days`);
- `storage`:数据库与备份目录; - `storage`:数据库与备份目录;
- `schedule` / `notify` / `classify` / `heat_weights`:任务节奏、通知阈值、分类规则和热度权重。 - `schedule` / `notify` / `classify` / `heat_weights`:任务节奏、通知阈值、分类规则和热度权重。
配置每 60 秒检查一次 mtime,凭据、收藏夹、分类规则等变更无需重启即可在下次任务读取;但 `schedule.folder_sync_minutes` 与 `schedule.snapshot_at` 在启动时固化,修改后需重启 `run` 进程。 配置每 60 秒检查一次 mtime,凭据、分类规则等变更无需重启即可在下次任务读取;但 `schedule.discover_minutes` 与 `schedule.snapshot_at` 在启动时固化,修改后需重启 `run` 进程。
完整配置口径以 [project.md](project.md) 和 [DEVELOPMENT.md](DEVELOPMENT.md) 为准。 完整配置口径以 [project.md](project.md) 和 [DEVELOPMENT.md](DEVELOPMENT.md) 为准。
@@ -378,14 +370,15 @@ uv run python app.py auth --account read --timeout 180
| 接口 | 当前用途 | 身份 Cookie | | 接口 | 当前用途 | 身份 Cookie |
|---|---|---| |---|---|---|
| `fav/resource/ids` / `infos` | 年度收藏夹同步与排序计划 | `read` 用于 P0;`favsort.py` 计划使用 `write`。 | | `view` / `relation/stat` / `nav` | 每日快照、入库详情、粉丝数、read 登录态 | `read`。 |
| `view` / `relation/stat` / `nav` | P1 快照与 read 登录态 | `read`。 | | `wbi/search/type` | 关键词搜索发现 | `read`(匿名可用)。 |
| `fav/folder/created/list-all` | 列出目标自建收藏夹 | `write`。 | | `fav/resource/ids` / `infos` | 收藏夹重排序计划(`favsort.py`) | `write`。 |
| `fav/resource/deal` / `sort` | 加入收藏夹、收藏夹排序 | `write`,需要完整 Cookie 和 CSRF。 | | `fav/folder/created/list-all` | 列出目标自建收藏夹(`favsort.py`) | `write`。 |
| `fav/resource/sort` | 收藏夹重排序 | `write`,需要完整 Cookie 和 CSRF。 |
只读客户端对 `ids` / `infos` 默认剥离 `read` 身份 Cookie,仅保留该账号设备指纹;如果失败才使用完整 `read` Cookie 重试。写操作一律使用完整 `write` Cookie 与 CSRF。 只读客户端默认剥离 `read` 身份 Cookie,仅保留该账号设备指纹;如果失败才使用完整 `read` Cookie 重试。写操作(`sort`)一律使用完整 `write` Cookie 与 CSRF,且仅由 `favsort.py` 触发。
B 站请求通过 `httpx` 连接池发送,并按 `data/raw-request.txt` 对齐 Chrome 155 的 UA、`sec-ch-ua`、`origin`、`referer` 和 `priority` 等请求头;收藏夹请求的 `referer` 会带上目标 `media_id`。 B 站请求通过 `httpx` 连接池发送,并按 `data/raw-request.txt` 对齐 Chrome 155 的 UA、`sec-ch-ua`、`origin`、`referer` 和 `priority` 等请求头;收藏夹请求的 `referer` 会带上目标 `media_id`。私聊手动入库的 `b23.tv` 短链通过同一传输层跟随重定向还原为视频 URL。
`x/v3/fav/resource/sort` 的实测语义为: `x/v3/fav/resource/sort` 的实测语义为:
@@ -407,16 +400,15 @@ uv run python tests/test_config.py # 配置加载、校验、ENV 覆盖
uv run python tests/test_migrations.py # 迁移、备份、回滚与保留期 uv run python tests/test_migrations.py # 迁移、备份、回滚与保留期
uv run python tests/test_database.py # 单写队列、事务与并发 uv run python tests/test_database.py # 单写队列、事务与并发
uv run python tests/test_repository.py # 收藏夹/视频状态机与黑名单 uv run python tests/test_repository.py # 收藏夹/视频状态机与黑名单
uv run python tests/test_bilibili_client.py # ids/infos/view/fans/nav 契约与重试 uv run python tests/test_bilibili_client.py # ids/infos/view/view-detail/fans/nav/短链契约与重试
uv run python tests/test_httpx_transport.py # httpx B 站/Telegram 传输层 uv run python tests/test_httpx_transport.py # httpx B 站/Telegram 传输层
uv run python tests/test_classify.py # 标题归一化、时长解析与打分 uv run python tests/test_classify.py # 标题归一化、时长解析与打分
uv run python tests/test_video_ref.py # BV/av/URL/b23.tv 引用解析(纯函数)
uv run python tests/test_search.py # WBI 签名、搜索解析与降级 uv run python tests/test_search.py # WBI 签名、搜索解析与降级
uv run python tests/test_candidates.py # 候选池、去重、通知与 fav_action uv run python tests/test_candidates.py # 候选池、去重、通知与 fav_action
uv run python tests/test_discovery.py # 发现过滤、候选池与通知批次 uv run python tests/test_discovery.py # 发现过滤、候选池与通知批次
uv run python tests/test_fav_position.py # 新视频插入位置 uv run python tests/test_library.py # 入库(候选/手动)、忽略、拉黑与幂等
uv run python tests/test_favorites.py # 收藏/忽略/拉黑与失败重试 uv run python tests/test_telegram.py # 消息、分片、长轮询、回调与文本入库
uv run python tests/test_telegram.py # 消息、分片、长轮询与回调授权
uv run python tests/test_folder_sync.py # 首次导入、差集、失效识别、容量预警
uv run python tests/test_snapshot.py # 七项快照、补采、断点续跑、粉丝、备份重试 uv run python tests/test_snapshot.py # 七项快照、补采、断点续跑、粉丝、备份重试
uv run python tests/test_maintenance.py # 日备份与保留期 uv run python tests/test_maintenance.py # 日备份与保留期
uv run python tests/test_timeutil.py # 上海时区与每日任务时间 uv run python tests/test_timeutil.py # 上海时区与每日任务时间
@@ -438,7 +430,7 @@ uv run python tests/test_cli.py # CLI 离线路径
config.json.bak 最近一次 auth 更新前的配置备份 config.json.bak 最近一次 auth 更新前的配置备份
data/plan-<media_id>.json 排序计划、顺序快照和元数据缓存 data/plan-<media_id>.json 排序计划、顺序快照和元数据缓存
data/journal-<media_id>.jsonl 排序执行日志 data/journal-<media_id>.jsonl 排序执行日志
data/app.db SQLite 数据库(收藏夹、视频、快照、粉丝、黑名单) data/app.db SQLite 数据库(视频、快照、粉丝、黑名单、历史收藏夹)
data/backup/app-YYYYMMDD.db 每日快照后的 SQLite 备份 data/backup/app-YYYYMMDD.db 每日快照后的 SQLite 备份
data/backup/app-pre-v*.db 数据库迁移前的自动备份 data/backup/app-pre-v*.db 数据库迁移前的自动备份
data/backup/app-pre-fans-*.db 粉丝历史回填前的手动备份 data/backup/app-pre-fans-*.db 粉丝历史回填前的手动备份
+1 -3
View File
@@ -8,7 +8,7 @@
"write": { "write": {
"mid": 0, "mid": 0,
"csrf": "从 write 账号 cookie 中的 bili_jct 取值", "csrf": "从 write 账号 cookie 中的 bili_jct 取值",
"cookie": "写入账号的完整 Cookie(用于加入收藏夹和排序)" "cookie": "写入账号的完整 Cookie(仅用于 favsort.py 收藏夹重排序)"
} }
}, },
"telegram": { "telegram": {
@@ -45,8 +45,6 @@
], ],
"schedule": { "schedule": {
"discover_minutes": 60, "discover_minutes": 60,
"folder_sync_minutes": 60,
"full_folder_refresh_hours": 6,
"snapshot_at": "00:01", "snapshot_at": "00:01",
"candidate_ttl_days": 7 "candidate_ttl_days": 7
}, },
+1 -3
View File
@@ -8,7 +8,7 @@
"write": { "write": {
"mid": 0, "mid": 0,
"csrf": "从 write 账号 cookie 中的 bili_jct 取值", "csrf": "从 write 账号 cookie 中的 bili_jct 取值",
"cookie": "写入账号的完整 Cookie(用于加入收藏夹和排序)" "cookie": "写入账号的完整 Cookie(仅用于 favsort.py 收藏夹重排序)"
} }
}, },
"overrides": {}, "overrides": {},
@@ -55,8 +55,6 @@
], ],
"schedule": { "schedule": {
"discover_minutes": 60, "discover_minutes": 60,
"folder_sync_minutes": 60,
"full_folder_refresh_hours": 6,
"snapshot_at": "00:01", "snapshot_at": "00:01",
"candidate_ttl_days": 7 "candidate_ttl_days": 7
}, },
+64 -60
View File
@@ -1,21 +1,23 @@
# 咻咻满 Reaction 视频数据看板 —— 可行性分析与技术路线 # 咻咻满 Reaction 视频数据看板 —— 可行性分析与技术路线
> 状态:**设计定稿;P0(数据底座与收藏夹同步)、P1(每日快照与粉丝追踪)、P2(只读本地看板)、P3(Telegram 与一键加入)已实施;第 9 轮加入时长效率榜、UP 粉丝快照与导航拆分** > 状态:**设计定稿;P0(数据底座)、P1(每日快照与粉丝追踪)、P2(只读本地看板)、P3(Telegram 发现与入库)已实施;第 9 轮加入时长效率榜、UP 粉丝快照与导航拆分;第 10 轮与 B 站收藏夹解耦并加入 Telegram 手动入库**
> 最后更新:2026-11-01(第 9 轮:时长效率榜、粉丝快照采集、视频/UP 导航拆分与数据说明页) > 最后更新:2026-11-01(第 10 轮:收藏夹解耦、手动入库、写鉴权仅用于重排序)
> B 站探测均为只读(GET),尚未调用 `fav/resource/deal`、`fav/resource/sort` > B 站写操作仅保留 `fav/resource/sort`(由 `favsort.py` 触发,频率极低);`fav/resource/deal` 已移除
--- ---
> 说明:本文档记录项目完整设计与历史验证。**第 10 轮起,收藏夹不再是库的真相源**:入库改由「搜索候选确认」与「Telegram 私聊手动入库」承担,项目不再轮询收藏夹;下方 §4、§7 等章节中关于「收藏夹同步 / 一键加入」的描述为历史设计,已被第 10 轮取代(见 §15)。
## 0. 结论摘要 ## 0. 结论摘要
**判定:可行,工程难度中等偏低。** 四个功能点都有稳定可用的官方 Web 接口,且匿名即可取数。 **判定:可行,工程难度中等偏低。** 核心功能点都有稳定可用的官方 Web 接口,且取数匿名即可完成。
| 功能点 | 主力接口 | 实测 | | 功能点 | 主力接口 | 实测 |
|---|---|---| |---|---|---|
| 视频发现 | `wbi/search/type` | ✅ `code=0`,匿名可用,单关键词上限 800~1000 条 | | 视频发现 | `wbi/search/type` | ✅ `code=0`,匿名可用,单关键词上限 800~1000 条 |
| 收藏夹同步(库的真相源) | `fav/resource/ids` + `fav/resource/infos` | ✅ infos **一次 400 条 / 0.2s**,664 条全量 2 次请求 | | 手动入库(链接/BV/av/短链) | `view?aid=` / `view?bvid=`、`b23.tv` 跳转 | ✅ 只读 |
| 每日统计 | `view?aid=` | ✅ 7 项指标齐全,0.07~0.14s/条,无风控 | | 每日统计 | `view?aid=` | ✅ 7 项指标齐全,0.07~0.14s/条,无风控 |
| 一键加入 | `fav/resource/deal` + `fav/resource/sort` | 排序语义前作已验证;加入待授权实测 | | 收藏夹重排序(唯一写操作) | `fav/resource/ids` + `infos` + `sort` | ✅ `favsort.py` 已实现 |
| 看板 | 本地 SQLite | ✅ 无外部依赖 | | 看板 | 本地 SQLite | ✅ 无外部依赖 |
**本轮确认的设计前提**(来自你的两轮答复): **本轮确认的设计前提**(来自你的两轮答复):
@@ -70,18 +72,18 @@
> **容量处理(不做自动改夹)**:目标夹满了就让它失败——`deal` 返回错误时记 `fav_action.state='failed'` 并通过通知渠道告警(含"请新建年度夹并更新配置")。可在接近上限(如 `media_count >= 950`)时提前发一次预警,但**系统绝不擅自把视频塞进另一个夹**。 > **容量处理(不做自动改夹)**:目标夹满了就让它失败——`deal` 返回错误时记 `fav_action.state='failed'` 并通过通知渠道告警(含"请新建年度夹并更新配置")。可在接近上限(如 `media_count >= 950`)时提前发一次预警,但**系统绝不擅自把视频塞进另一个夹**。
> **注意**:这只是"收藏夹归属",`snapshot` 与看板**不按年分表**,全部视频统一查询、统一出榜。 > **注意**:这只是"收藏夹归属",`snapshot` 与看板**不按年分表**,全部视频统一查询、统一出榜。
### 1.4 两个数据来源的关系 ### 1.4 数据来源的关系(第 10 轮更新)
``` ```
人工加收藏(兜底、可信) ──┐ 搜索候选 + Telegram 点确认 ──┐
├──► video 表(status=active)──► 每日统计 ──► 看板 ├──► video 表(status=active)──► 每日统计 ──► 看板
搜索候选 + 点击确认(补充) ──┘ Telegram 私聊发链接/BV 号 ───┘
``` ```
- **收藏夹是真相源**:用户手工加进去的,默认就是"符合条件",系统自动入库; - **本地库是真相源**:入库来自「搜索候选确认」与「Telegram 私聊手动入库」,只写本地 `video(status='active')`;
- **搜索是补漏**:只发通知,点确认才入库,同时顺手加进目标收藏夹; - **收藏夹解耦**:项目不再轮询收藏夹,也不会因取消收藏改变视频状态;收藏夹由人工维护,`favsort.py` 负责低频重排序;
- **日更对象** = 库内 `active` 视频(跨年度夹累计),与收藏夹当前内容解耦; - **日更对象** = 库内 `active` 视频;
- **黑名单只挡"发现"**:收藏夹里已有的黑名单 UP 视频一律当普通视频处理,不判断、不排除、也不反向更新黑名单。 - **黑名单只挡"发现"**:已入库的黑名单 UP 视频一律当普通视频处理,不判断、不排除、也不反向更新黑名单。
### 1.5 配置与凭据(统一放配置文件) ### 1.5 配置与凭据(统一放配置文件)
@@ -114,7 +116,7 @@
261052419, 1908537643, 1701121088, 542262, 376316958, 261052419, 1908537643, 1701121088, 542262, 376316958,
145178393, 66389997, 293976264, 3026109 145178393, 66389997, 293976264, 3026109
], ],
"schedule": { "discover_minutes": 60, "folder_sync_minutes": 60, "schedule": { "discover_minutes": 60,
"snapshot_at": "00:01", "candidate_ttl_days": 7 }, "snapshot_at": "00:01", "candidate_ttl_days": 7 },
"notify": { "min_score": 2, "max_items_per_message": 10 }, "notify": { "min_score": 2, "max_items_per_message": 10 },
"classify": { "classify": {
@@ -134,7 +136,7 @@
- 日志里**永不打印 token / cookie**,启动时只打印"已配置/未配置"; - 日志里**永不打印 token / cookie**,启动时只打印"已配置/未配置";
- 容器里把配置目录挂成**可写卷**(`-v ./config:/config`,权限 600),因为 `auth` 扫码续期后要回写 cookie; - 容器里把配置目录挂成**可写卷**(`-v ./config:/config`,权限 600),因为 `auth` 扫码续期后要回写 cookie;
- `python app.py auth --account read|write` 对指定账号扫码续期,只替换该账号身份字段、保留该账号设备指纹; - `python app.py auth --account read|write` 对指定账号扫码续期,只替换该账号身份字段、保留该账号设备指纹;
- `read` 只用于年度收藏夹同步、快照和查询;`write` 只用于目标收藏夹、`deal` 和 `sort` 写操作;不支持顶层扁平凭据; - `read` 用于快照、搜索发现、入库与所有查询;`write` 仅用于 `favsort.py` 收藏夹重排序(`sort`);不支持顶层扁平凭据;
- 可选的 `ENV` 覆盖(`TG_BOT_TOKEN` 等)留给 CI/临时调试,不作为主路径。 - 可选的 `ENV` 覆盖(`TG_BOT_TOKEN` 等)留给 CI/临时调试,不作为主路径。
### 1.6 初始黑名单(已核对昵称,实测 2026-10-31) ### 1.6 初始黑名单(已核对昵称,实测 2026-10-31)
@@ -166,12 +168,12 @@
| 旧版搜索(降级) | `GET x/web-interface/search/type` | 否 | ✅ `code=0`,结果略少 | 兜底 | | 旧版搜索(降级) | `GET x/web-interface/search/type` | 否 | ✅ `code=0`,结果略少 | 兜底 |
| UP 主搜索 | 同上 `search_type=bili_user` | 否 | ✅ 找到 咻咻满 mid=37754047 | 仅元数据 | | UP 主搜索 | 同上 `search_type=bili_user` | 否 | ✅ 找到 咻咻满 mid=37754047 | 仅元数据 |
| 空间投稿 | `GET x/space/wbi/arc/search` | 否(需 WBI) | ✅ `code=0` | 本方案不使用 | | 空间投稿 | `GET x/space/wbi/arc/search` | 否(需 WBI) | ✅ `code=0` | 本方案不使用 |
| 视频完整统计 | `GET x/web-interface/view?aid=/bvid=` | **否** | ✅ 7 项齐全,0.1s | **日更主力** | | 视频完整统计 | `GET x/web-interface/view?aid=/bvid=` | **否** | ✅ 7 项齐全,0.1s | **日更 + 手动入库主力** |
| 收藏夹 ID 列表 | `GET x/v3/fav/resource/ids` | 是 | ✅ 664 条 1 次拿全 | 收藏夹监控 | | 收藏夹 ID 列表 | `GET x/v3/fav/resource/ids` | 是 | ✅ 664 条 1 次拿全 | `favsort.py` 重排序 |
| 收藏夹批量详情 | `GET x/v3/fav/resource/infos` | 是 | ✅ **一次 400 条**,0.2s;664 条返回 648(缺 16) | 元数据同步/导入 | | 收藏夹批量详情 | `GET x/v3/fav/resource/infos` | 是 | ✅ **一次 400 条**,0.2s;664 条返回 648(缺 16) | `favsort.py` 重排序 |
| 加入收藏夹 | `POST x/v3/fav/resource/deal` | 是 + csrf | 文档明确,待实测 | 一键加入 | | ~~加入收藏夹~~ | `POST x/v3/fav/resource/deal` | — | — | **第 10 轮已移除** |
| 收藏夹内排序 | `POST x/v3/fav/resource/sort` | 是 + csrf | ✅ 已验证 `A:2:B:2` | 定位 | | 收藏夹内排序 | `POST x/v3/fav/resource/sort` | 是 + csrf | ✅ 已验证 `A:2:B:2` | 收藏夹重排序(唯一写操作) |
| 收藏夹列表 | `GET x/v3/fav/folder/created/list-all` | 是 | ✅ 返回 media_count | 容量监控 | | 收藏夹列表 | `GET x/v3/fav/folder/created/list-all` | 是 | ✅ 返回 media_count | 列出重排序目标夹 |
| 粉丝数 | `GET x/relation/stat?vmid=` | **否** | ✅ `code=0`,匿名可用 | 粉丝追踪 | | 粉丝数 | `GET x/relation/stat?vmid=` | **否** | ✅ `code=0`,匿名可用 | 粉丝追踪 |
| ~~视频统计(旧)~~ | `x/web-interface/archive/stat` | — | ❌ **HTTP 404,已下线** | 不可用 | | ~~视频统计(旧)~~ | `x/web-interface/archive/stat` | — | ❌ **HTTP 404,已下线** | 不可用 |
| ~~批量 view~~ | `view?aid=1,2` | — | ❌ `-400` | 不可用 | | ~~批量 view~~ | `view?aid=1,2` | — | ❌ `-400` | 不可用 |
@@ -528,17 +530,15 @@ CREATE TABLE kv (k TEXT PRIMARY KEY, v TEXT);
**要点** **要点**
- `status` 是核心状态机: - `status` 是核心状态机(第 10 轮更新):
- `pending` 搜索候选,等确认(只在 `candidate` 表,不进 `video`)
- `active` 在库,参与日更 - `active` 在库,参与日更
- `unfaved` 用户取消收藏,**仍继续日更**,看板可筛 - `unfaved` 历史遗留状态(迁移 v5 已归一为 `active`),不再由系统产生
- `removed` 视频失效(`-404` / `attr=9` / `infos` 缺失),停止请求但保留历史 - `removed` 视频失效(`view` 返回 `-404` / `62002` / `62012`),停止请求但保留历史
- `dropped` 用户手动"移出库",**停止发现与追踪**;若之后又出现在收藏夹,同步任务会自动改回 `active` - `dropped` 手动"移出库",**停止发现与追踪**;再次被候选确认或手动入库时会恢复 `active`
- `rejected` 用户点过"忽略",不再通知 - `candidate` 与 `video` 分表:候选可能永远不入库,避免污染主表;候选 `first_seen` 超过 **7 天**且 `decision IS NULL` → 删除 candidate 行,去重由 `seen_aid` 保证,不会重复通知;
- `candidate` 与 `video` 分表:候选可能永远不入库,避免污染主表;候选 `first_seen` 超过 **7 天**且 `decision IS NULL` → **直接删除 candidate 行**(按你的偏好),去重由 `seen_aid` 保证,不会重复通知;
- `seen_aid` 只存 `(aid, first_seen, source)`,每条约 30 字节,按 5 条/天增长,10 年也不到 100KB; - `seen_aid` 只存 `(aid, first_seen, source)`,每条约 30 字节,按 5 条/天增长,10 年也不到 100KB;
- `up_blacklist` 只影响发现:`folder_sync` **不查**这张表,收藏夹里黑名单 UP 的视频按普通视频入库; - `up_blacklist` 只影响发现,不影响已入库视频;
- `folder_id` 的取值遵循 §1.3:优先同 UP 最早视频所在夹,其次当前自然年夹; - `folder_id` 与 `folder_resource` 保留为历史数据,不再由同步任务写入(第 10 轮);
- 不预存"日增量",全部查询时算(历史数据修正后不会自相矛盾)。 - 不预存"日增量",全部查询时算(历史数据修正后不会自相矛盾)。
--- ---
@@ -755,8 +755,9 @@ config.docker.example.json 服务器配置样例(0.0.0.0 + /data 绝对路径
**进程结构** **进程结构**
``` ```
scheduler 线程:discovery(每 60min) / folder_sync(每 60min) / snapshot(每日 00:01) scheduler 线程:discovery(每 60min) / snapshot(每日 00:01) / candidate_cleanup(每日)
web 线程:ThreadingHTTPServer :8080 web 线程:ThreadingHTTPServer :8080
telegram:getUpdates long polling(回调 + 私聊文本入库)
storage:SQLite WAL,单写线程 storage:SQLite WAL,单写线程
``` ```
@@ -766,33 +767,33 @@ storage:SQLite WAL,单写线程
| 风险 | 影响 | 概率 | 缓解 | | 风险 | 影响 | 概率 | 缓解 |
|---|---|---|---| |---|---|---|---|
| 搜索排序抖动 + 只取 2 页 | 漏抓个别新视频 | 中 | 每小时扫一次 + 收藏夹监控兜底 + 候选池;业务上可接受 | | 搜索排序抖动 + 只取 2 页 | 漏抓个别新视频 | 中 | 每小时扫一次 + 候选池 + 私聊手动入库兜底;业务上可接受 |
| 搜索结果噪声 | 通知太吵 | 中 | 轻量规则降噪 + 人点确认;不达阈值进候选池 | | 搜索结果噪声 | 通知太吵 | 中 | 轻量规则降噪 + 人点确认;不达阈值进候选池 |
| 收藏夹 1000 条上限 | 夹满后无法加入 | 中 | 接近 950 时预警;夹满则 `deal` 失败 → **报错并通知**(不自动改夹),由人工新建年度夹并更新配置 | | 收藏夹与库不一致 | 库内视频不在收藏夹 | 低 | 收藏夹已解耦;人工维护收藏夹,`favsort.py` 只做重排序 |
| 同 UP 归属导致旧夹持续增长 | 2026 夹被 2027 视频撑爆 | **高** | 与上一条同一处置:预警 + 失败告警 + 人工建新夹;不加自动改夹逻辑 | | 收藏夹 1000 条上限 | 重排序目标夹过满 | 低 | 由人工新建收藏夹;`favsort.py` 仅排序,不自动加条 |
| 黑名单误伤 | 漏掉正常 reaction | 低 | 黑名单可随时移除;被排除的 UP 不写候选表,但可从日志审计 | | 黑名单误伤 | 漏掉正常 reaction | 低 | 黑名单可随时移除;被排除的 UP 不写候选表,但可从日志审计 |
| `infos` 静默丢条 | 误判同步成功 | **高** | 用 `请求 ids - 返回 ids` 判断缺失;缺失即 `removed` | | `infos` 静默丢条 | 重排序计划缺条 | 低 | `favsort.py` 用 `请求 ids - 返回 ids` 判断缺失 |
| `infos.reply` 恒 0 / 缺 like/coin/share | 统计不全 | **确定** | 日更以 `view` 为唯一事实来源,`infos` 只做同步/降级 | | `infos.reply` 恒 0 / 缺 like/coin/share | 统计不全 | **确定** | 日更以 `view` 为唯一事实来源,`infos` 仅用于重排序 |
| GET 链接被预览预取 | 误加入收藏夹 | 中 | GET 只渲染页面,写操作走 POST | | `b23.tv` 短链失效或改版 | 手动入库解析失败 | 低 | 解析失败会在 Telegram 回执提示;可直接改发 BV 号 |
| `deal` / `sort` 写失败 | 收藏夹状态不一致 | 中 | 先查 ids 幂等、失败重试、`fav_action` 记录、页面展示失败原因 | | GET 链接被预览预取 | 无(已无写入型 GET) | — | 写操作走 Telegram 消息/回调,Web 全只读 |
| 视频失效(当前 5.4%) | 日更报错 | **高** | `-404` / `attr=9` / 缺失 → `removed`,不再请求 | | `sort` 写失败 | 收藏夹顺序不一致 | 中 | `favsort.py` 回读校验、失败冷却重试、可 `run` 续跑 |
| 接口下线(`archive/stat` 已 404) | 功能中断 | 中 | 接口层抽象 + 降级(旧版 search / infos 三项)+ 监控告警 | | 视频失效 | 日更报错 | **高** | `view` 返回 `-404` / `62002` / `62012` → `removed`,不再请求 |
| 风控 `-352/-412` | 采集中断 | 低(实测未见) | 限速 1.2s + 抖动 + 指数退避 + 告警 | | 接口下线 | 功能中断 | 中 | 接口层抽象 + 降级(旧版 search);监控告警 |
| 跨年切换遗漏 | 新视频进错夹 | 低 | 按 pubdate 选夹 + 1 月 1 日提醒 | | 风控 `-352/-412` | 采集中断 | 低(实测未见) | 限速 + 抖动 + 指数退避 + 告警 |
| SQLite 并发 | 写冲突 | 低 | WAL + 单写线程 | | SQLite 并发 | 写冲突 | 低 | WAL + 单写线程 |
| cookie 过期 | 收藏夹相关接口失败 | 中 | 使用 `python app.py auth --account read|write` 扫码续期;`view`/搜索匿名可用作降级 | | cookie 过期 | 只读采集失败 | 中 | `python app.py auth --account read|write` 扫码续期;`view`/搜索匿名可用作降级 |
--- ---
## 12. 待确认 ## 12. 待确认
**无。** 三轮问答已覆盖:收录口径、发现策略、收藏夹归属与容量、通知渠道与动作、黑名单语义、候选池生命周期、粉丝追踪、部署形态。 **无。** 第 10 轮口径已确认:入库只写本地库;收藏夹解耦、不再追踪;手动入库支持视频链接 / BV 号 / av 号 / `b23.tv` 短链;`bilibili.write` 仅用于 `favsort.py` 重排序。
进入定稿/实施前只需要你提供**运行期凭据**(不建议写进本文档): 进入运行前只需要你提供**运行期凭据**(不建议写进本文档):
- Telegram `bot_token` + 私聊 `chat_id` - Telegram `bot_token` + 私聊 `chat_id`
- B 站 cookie(也可以先用 `auth` 扫码生成) - B 站 cookie(`read` 必需;`write` 仅重排序用;也可先用 `auth` 扫码生成)
- 年度收藏夹 `media_id`(已有 4451053851 / 3700023351) - 收藏夹 `media_id`(`favsort.py` 重排序目标;已有 4451053851 / 3700023351)
--- ---
@@ -845,16 +846,16 @@ storage:SQLite WAL,单写线程
| 41 | **全量离线测试** | 32 个 `tests/test_*.py` 全部通过(含新增 `test_up_profile.py` 与迁移/数据库断言更新) | | 41 | **全量离线测试** | 32 个 `tests/test_*.py` 全部通过(含新增 `test_up_profile.py` 与迁移/数据库断言更新) |
| 42 | **内容效率噪声修正** | 预览库实测:视频级 EB 收缩(κ=60 分钟)+ 播放 ≥ 2000 后,效率榜 Top10 全为 ≥110 分钟视频;「什么哦吼吼」4 条不再入榜;综合热度榜同样应用 2000 门槛(637 条) | | 42 | **内容效率噪声修正** | 预览库实测:视频级 EB 收缩(κ=60 分钟)+ 播放 ≥ 2000 后,效率榜 Top10 全为 ≥110 分钟视频;「什么哦吼吼」4 条不再入榜;综合热度榜同样应用 2000 门槛(637 条) |
| 43 | **榜单交互改版** | 排序/时间改为下拉;时间筛选实测 `year=2026` 640 条、`year=2025` 156 条(合计 796);`sort=published` 可用;`/api/overview` 返回 years `[2026,2025]`;`year=1800` 返回 400 | | 43 | **榜单交互改版** | 排序/时间改为下拉;时间筛选实测 `year=2026` 640 条、`year=2025` 156 条(合计 796);`sort=published` 可用;`/api/overview` 返回 years `[2026,2025]`;`year=1800` 返回 400 |
| 44 | **第 10 轮全量离线测试** | 32 个 `tests/test_*.py` 全部通过;新增 `test_video_ref.py`、`test_library.py`,重写 `test_telegram.py`,删除收藏夹同步相关测试 |
| 45 | **迁移 v5** | 历史 `video.status='unfaved'` 归一为 `active`;`app-pre-v4-*` 自动备份;`folder` / `folder_resource` 保留 |
**未验证(需授权或外部配合)**: **未验证(需授权或外部配合)**:
- `fav/resource/deal` 实际加入 + `sort` 定位(会写收藏夹;需用户点击 Telegram 按钮或授权测试视频); - 真实 Telegram 私聊文本入库与按钮回调(长轮询与解析逻辑已离线测试);
- Telegram 按钮真实回调(长轮询代码与授权逻辑已离线测试); - 真实 `b23.tv` 短链跳转(客户端跟随重定向逻辑已离线测试);
- 手机扫码 `auth` 完整链路; - 手机扫码 `auth` 完整链路;
- 各通知渠道的按钮/链接实际表现;
- 长期(>72h)每小时扫描的风控表现; - 长期(>72h)每小时扫描的风控表现;
- 收藏夹真实上限是否 1000; - `favsort.py` 对真实收藏夹的重排序(写操作,需用户确认计划后手动执行)。
- 本地真实数据副本删除后,未在生产库上执行 `sync --full`;预览库的 `video.pages` 仍为 NULL,需在真实库跑一次全量同步才会填充(不影响效率榜,效率榜只用 `duration`)。
**本轮有意不做**:基于粉丝数的"破圈"榜单(只采集与回填数据,见 §14)。 **本轮有意不做**:基于粉丝数的"破圈"榜单(只采集与回填数据,见 §14)。
@@ -862,19 +863,22 @@ storage:SQLite WAL,单写线程
## 14. 下一步 ## 14. 下一步
P0、P1、P2、P3 与第 9 轮已实施;P2 交付了只读本地看板与可重复生成的 7 天预览数据。 P0、P1、P2、P3、第 9 轮与第 10 轮已实施;P2 交付了只读本地看板与可重复生成的 7 天预览数据。
第 9 轮(2026-11-01)交付: 第 10 轮(2026-11-01)交付:
1. schema v4:`video.pages`、`video.up_follower*`、`up_profile`; 1. 收藏夹解耦:删除 `folder_sync`、`FavoriteActionService`、`app.py sync` 与调度同步任务;`folder` / `folder_resource` / `folder_id` 仅保留历史;
2. 入库时采集 UP 粉丝快照 + `python app.py backfill-followers` 历史回填(基于粉丝数的榜单暂不做); 2. 迁移 v5:历史 `unfaved` 归一为 `active`;
3. 时长效率榜(内容效率/互动效率)与 UP 内容效率(EB 收缩),过滤 < 600 秒与未满 2 天的视频; 3. 入库:候选确认与 Telegram 私聊手动入库统一走 `services/library.py`,只写本地库并补当天快照;
4. 主导航拆分:「榜单」→「视频」+「UP主」,两者独立页面;新增「数据说明」页(`/about` + `/api/about`);排序/时间改为下拉筛选,支持按发布时间排序与按年份过滤; 4. 手动入库解析:`video_ref.extract_video_refs`(BV/av/URL/`b23.tv`)+ `bilibili.client.view?bvid=` / `resolve_short_link`;
5. 列表精简:卡片只保留主指标 + 互动率,七项明细与分 P、粉丝快照移到详情页。 5. Telegram:`allowed_updates` 增加 `message`;按钮改为「入库 / 忽略 / 拉黑」(`add:`,兼容 `fav:`);
6. 写鉴权收口:删除 `write_client.deal`,`bilibili.write` 仅 `favsort.py` 重排序使用;
7. 配置移除 `schedule.folder_sync_minutes` / `full_folder_refresh_hours`;
8. 看板文案改为「历史收藏夹」,候选页改为「入库」。
仍待处理: 仍待处理:
1. 真实 `deal` / `sort` 与 Telegram 按钮回调的补充验证记录; 1. 真实 Telegram 文本入库 / 按钮回调 / `b23.tv` 跳转的补充验证记录;
2. 基于粉丝数的"破圈"维度(播放/入库时粉丝数、粉丝分段、UP 中位数)——数据已就绪,榜单未实现; 2. 基于粉丝数的"破圈"维度(播放/入库时粉丝数、粉丝分段、UP 中位数)——数据已就绪,榜单未实现;
3. P4(容器化 + 反代)继续保持可选。 3. P4(容器化 + 反代)继续保持可选。