Files
re-xxm/DEVELOPMENT.md
T

720 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 咻咻满 Reaction 视频数据看板开发文档
> **状态:** P0(数据底座与收藏夹同步)、P1(每日快照与粉丝追踪)、P2(只读本地看板)、P3(Telegram 与一键加入)已实施
>
> **技术基线:** Python 3.14、Python 标准库、SQLite、原生 HTML/CSS/JavaScript、`uv`
>
> **本地开发:** 不依赖 Docker;使用 `uv` 创建和管理隔离环境。
---
## 1. 技术选型
| 范围 | 选型 | 说明 |
|---|---|---|
| 开发语言 | Python 3.14 | 与现有 `favsort.py` 一致,复用 Cookie、扫码授权与收藏夹排序能力。 |
| 包与环境管理 | `uv` | 管理 Python 版本、`.venv`、依赖同步及统一命令。 |
| 后端服务 | Python 标准库 `ThreadingHTTPServer` | 个人单用户看板,接口与并发规模有限,不引入 Web 框架。 |
| HTTP 客户端 | `httpx` | 连接池、keepalive、明确的 connect/read 超时;B 站请求头与 UA 对齐 `data/raw-request.txt`。 |
| 数据库 | SQLite(WAL) | 单用户、单机、低并发写入;备份和迁移成本低。 |
| 调度 | `threading`、`queue.Queue`、标准库时间工具 | 单进程内运行发现、同步、快照和清理任务。 |
| 前端 | 原生 HTML/CSS/JavaScript | 无 Node.js、无构建链;前端仅渲染 `/api/*` JSON。 |
| 图表 | 仓库内置 ECharts | 离线静态资源,Apache-2.0 许可。 |
| 通知 | Telegram Bot long polling + `httpx` | 具备按钮回调能力,开发和本机运行不需要公网 URL。 |
| 容器化 | 可选的后续部署方式 | 本地开发、调试和日常运行均不要求 Docker。 |
### 1.1 选型原则
1. **复用现有能力。** 当前仓库的 `favsort.py` 已实现 B 站设备指纹 Cookie、扫码登录、收藏夹读取和排序逻辑;新功能应拆分复用,而不是另起技术栈。
2. **保持单体。** 当前规模约数百到数千视频,每日采集量约千级请求;单进程、SQLite 和内置调度足够。
3. **标准库优先,成熟依赖可用。** 当依赖能明确改善可靠性、性能或可维护性时允许引入;必须锁定在 `pyproject.toml` / `uv.lock`,隔离在适配层,并用契约测试覆盖。
4. **本地优先。** 可在 Windows 上通过 `uv` 直接运行,不要求 WSL、Docker、数据库服务或宿主机 cron。
5. **未来可部署。** 服务保持无状态代码与可挂载数据目录分离,未来可选用 Docker 与 Caddy/Nginx 部署,但不是开发前置条件。
---
## 2. 使用 uv 的本地开发环境
### 2.1 前置条件
- 安装 [uv](https://docs.astral.sh/uv/);
- 网络联通以便 `uv` 首次下载 Python 3.14;
- Windows PowerShell、Windows Terminal 或任意可运行 Python 的终端;
- 不需要安装 Docker、Node.js、npm、SQLite 命令行工具或 PostgreSQL。
项目通过根目录的 `.python-version` 固定使用 Python 3.14。`uv` 会在需要时自动安装相应解释器,并在仓库根目录创建 `.venv`。
### 2.2 初始化与同步
在项目根目录执行:
```powershell
uv sync --all-groups
```
该命令会:
1. 读取 `pyproject.toml`;
2. 确保 Python 3.14 可用;
3. 创建或更新 `.venv`;
4. 安装项目本身及后续声明的依赖;
5. 根据 `uv.lock` 锁定可复现的环境。
目前项目功能仅依赖 Python 标准库,因此首次同步不会安装额外运行时包。这是预期行为,不表示 `uv` 没有生效。
### 2.3 常用命令
```powershell
# 查看 Python 与 uv 管理的虚拟环境是否可用
uv run python --version
# 查看现有收藏夹工具帮助
uv run bili-fav help
# 扫码登录并原子更新指定账号
uv run python app.py auth --account read
uv run python app.py auth --account write
# 检查两套 B 站登录状态
uv run python app.py auth-check --account all
# 执行当前无网络依赖的测试
uv run python tests/test_auth.py
uv run python tests/test_moves.py
```
未来看板服务实现后,统一使用:
```powershell
# 建库、迁移与黑名单种子(离线,P0)
uv run python app.py init
# 收藏夹只读同步/首次导入(P0)
uv run python app.py sync
# 按配置周期常驻同步(P0);--once 只跑一次
uv run python app.py run
uv run python app.py run --once
# 每日七项快照与粉丝数(P1)
uv run python app.py snapshot
uv run python app.py snapshot --force
# 检查 Cookie 登录态(P1)
uv run python app.py auth-check
# 查看数据库状态(离线,P0)
uv run python app.py status
# P2 起启动本地看板服务
uv run python app.py serve
uv run python app.py serve --config .\config.json
# 无子命令启动完整服务:只读 Web + 调度 + Telegram
uv run python app.py
# 生成 7 天预览库并启动预览
uv run python tools/seed_preview_db.py --days 7
uv run python app.py serve --preview --config data/preview/config.json
# 从小满虫之家回填粉丝历史(先 dry-run,默认跳过已有成功记录)
uv run python tools/import_fans_history.py --days 90 --dry-run
uv run python tools/import_fans_history.py --days 90
```
不建议直接激活 `.venv`;优先使用 `uv run ...`,这样命令总是运行在项目锁定的解释器和环境中。
### 2.4 依赖变更规范
若后续确实需要新增依赖,应使用 `uv` 修改项目元数据,而不是直接使用 `pip install`:
```powershell
# 新增运行依赖(仅在标准库无法合理满足需求时)
uv add <package>
# 新增仅用于开发的依赖
uv add --dev <package>
# 同步锁文件和环境
uv sync --all-groups
```
提交依赖变更时,必须同时提交:
- `pyproject.toml`
- `uv.lock`
不得提交:
- `.venv/`
- `config.json`
- `config.json.bak`
- `data/` 中的运行期数据库、日志、计划和备份。
---
## 3. 总体架构
```text
浏览器 / 手机 Telegram 私聊
│ │
├──────────── HTTP / JSON ─────────────┤
│ │
┌─────▼──────────────────────────────────────▼─────┐
│ 单一 Python 进程 │
│ │
│ Web 线程:ThreadingHTTPServer :8080 │
│ 调度线程:发现 / 收藏夹同步 / 快照 / 清理 │
│ Telegram:getUpdates long polling │
│ 写入线程:SQLite 单写队列 │
│ │
│ BilibiliClient ── B 站 Web API │
│ SQLite (WAL) ── ./data/app.db 或配置的数据目录 │
└────────────────────────────────────────────────────┘
```
### 3.1 并发与事务原则
- Web 请求、调度任务和 Telegram 长轮询可以并发;
- 所有 SQLite 写操作经单写队列串行执行,避免 `database is locked`;
- 读操作使用短生命周期连接;
- 网络请求期间不得持有数据库写事务;
- 收藏操作通过 `fav_action` 状态锁保证幂等;
- SQLite 启用 WAL、外键和合理的 busy timeout。
```sql
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000;
PRAGMA synchronous = NORMAL;
```
---
## 4. 推荐代码结构
```text
.
├── app.py # 看板服务入口(P0 起新增)
├── favsort.py # 现有收藏夹排序与授权工具
├── pyproject.toml # uv / Python 项目元数据
├── uv.lock # uv 生成并提交的锁文件
├── .python-version # 固定 Python 3.14
├── config.json # 运行期私密配置,不提交
├── config.example.json # 脱敏配置样例
├── DEVELOPMENT.md
│
├── reaction_board/
│ ├── config.py # 配置、ENV 覆盖、mtime 热加载
│ ├── analytics.py # 热度、增长、互动率与封面工具
│ ├── database.py # SQLite 连接、迁移、单写队列
│ ├── migrations.py # schema_version 与迁移
│ ├── scheduler.py # 内置任务调度
│ ├── logging_json.py # 脱敏 JSON 日志
│ ├── classify.py # 标题归一化、时长解析与打分
│ ├── fav_position.py # 新视频插入位置计算
│ ├── timeutil.py # 上海时区与每日时间
│ ├── bilibili/
│ │ ├── client.py # 只读 HTTP、重试与限速
│ │ ├── write_client.py # deal / sort 写客户端
│ │ ├── search.py # WBI 搜索与旧接口降级
│ │ ├── provider.py # 凭据变化重建客户端
│ │ ├── auth.py # Cookie、设备指纹与请求头工具
│ │ ├── auth_flow.py # app.py auth 的扫码与原子写回流程
│ │ └── models.py # 接口响应模型
│ ├── services/
│ │ ├── discovery.py # 搜索发现与候选池
│ │ ├── favorites.py # 收藏 / 忽略 / 拉黑
│ │ ├── telegram_poller.py # getUpdates 与回调分发
│ │ ├── folder_sync.py
│ │ ├── snapshot.py
│ │ └── maintenance.py
│ ├── notify/telegram.py # Telegram 消息与按钮
│ └── web/ # P2 只读看板
│ ├── server.py # HTTP、路由、鉴权、静态资源
│ ├── queries.py # 看板 SQL 与数据整形
│ └── static/ # 原生 JS/CSS、ECharts、预览页面
│
├── tools/
│ ├── seed_preview_db.py # 复制真实库并回填 7 天预览数据
│ └── import_fans_history.py # 从小满虫之家回填 90 天粉丝历史
│
├── tests/
└── data/ # 本地运行期数据,默认不提交
```
`favsort.py` 继续保持可独立运行,但只操作 `bilibili.write`;扫码授权统一由 `app.py auth --account read|write` 提供。Cookie、设备指纹与请求头逻辑必须共用 `reaction_board.bilibili.auth`。
---
## 5. 配置与凭据
### 5.1 运行期配置
所有凭据和可变业务参数放在 `config.json` 中。文件必须被 Git 忽略,日志中不得输出其敏感值。
```jsonc
{
"bilibili": {
"mid": 28802351,
"csrf": "<bili_jct>",
"cookie": "SESSDATA=...; bili_jct=...; buvid3=..."
},
"telegram": {
"enabled": true,
"bot_token": "123456:ABC",
"chat_id": 123456789
},
"web": {
"host": "127.0.0.1",
"port": 8080,
"auth_token": "replace-with-a-long-random-token",
"public_base_url": ""
},
"storage": {
"database_path": "data/app.db",
"backup_dir": "data/backup",
"backup_keep_days": 14
},
"folders": [
{ "year": 2025, "media_id": 4451053851, "title": "咻咻满-reaction-2025" },
{ "year": 2026, "media_id": 3700023351, "title": "咻咻满 reaction 合集" }
],
"track_fans": [37754047],
"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
},
"notify": { "min_score": 2, "max_items_per_message": 10 }
}
```
- `folders` 中的年度夹由人工创建,系统只读;
- 配置必须为 `bilibili.read` / `bilibili.write` 双账号结构;不兼容顶层扁平 `mid` / `csrf` / `cookie`;
- P0 已实现的命令行:`python app.py init`、`python app.py sync [--full]`、`python app.py status`。
### 5.2 安全规则- Windows 本地开发时应限制 `config.json` 的文件访问权限;部署到 Linux 时权限必须为 `600`;
- 配置文件每 60 秒检查一次 mtime,凭据、收藏夹、分类规则和权重变更后无需重启;
- Cookie / CSRF 变化时由 `ClientProvider` 重建 B 站请求客户端,并保留该角色的限速与重试策略;
- `schedule.folder_sync_minutes` 与 `schedule.snapshot_at` 在启动时固化,修改后需重启 `run`;
- Telegram 操作必须同时验证 `from.id` 和 `chat.id` 是否为配置的私聊用户;
- 仅允许环境变量临时覆盖敏感配置,不能将真实凭据硬编码进源代码;
- `GET` 路由绝不进行收藏、忽略、拉黑等写操作。
---
## 6. 数据模型
### 6.1 核心状态
| 表 | 状态 | 含义 | 是否参与每日快照 |
|---|---|---|---|
| `video` | `active` | 在库追踪的视频 | 是 |
| `video` | `unfaved` | 已从收藏夹移除,但保留历史追踪 | 是 |
| `video` | `removed` | 视频失效、删除或不可访问 | 否 |
| `video` | `dropped` | 在看板中手动移出库 | 否 |
| `candidate` | `NULL` | 待人工决策 | 否 |
| `candidate` | `rejected` | 已忽略 | 否 |
| `candidate` | `blacklisted` | 所属 UP 已拉黑 | 否 |
### 6.2 建表原则
最终 DDL 由迁移模块维护,关键约束如下:
- `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` 只在发现任务中生效,收藏夹同步不得用它过滤既有视频;
- `seen_aid` 永久保留搜索去重标记,候选过期删除后也不重复通知;
- `fav_action` 保存 Telegram 或签名链接的动作状态,防止重复点击造成重复写入。
### 6.3 迁移规范
- 使用 `schema_version(version, applied_at)`;
- 每个版本迁移必须包在单一 SQLite 事务;
- 迁移只能向前追加,不能用“删除数据库重建”替代升级;
- 开发和正式运行产生的数据均保留;
- 迁移前自动创建 SQLite 备份,失败时回滚事务并保留原库。
---
## 7. B 站接口与任务设计
### 7.1 接口职责
| 能力 | 接口 | 用途 |
|---|---|---|
| 发现 | `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 分组 |
B 站请求统一使用 `httpx` 连接池,并按 `data/raw-request.txt` 对齐浏览器特征:
- `user-agent`:Chrome 155 / Windows;
- `sec-ch-ua`、`sec-ch-ua-mobile`、`sec-ch-ua-platform`;
- `origin: https://space.bilibili.com`;
- `referer`:收藏夹请求指向 `https://space.bilibili.com/<mid>/favlist?fid=<media_id>&ftype=create`;
- `priority: u=1, i`;
- 匿名请求仍剥离账号身份 Cookie,仅保留设备指纹。
请求头集中在 `bilibili/auth.py::make_headers`,不要在业务层散落自定义 Header。
### 7.2 发现任务
每小时扫描两个关键词、每个关键词前两页:
```text
关键词:咻咻满 reaction、咻咻满 充电
排序:pubdate
页面:1、2
```
处理规则:
1. 已在 `video` 或 `seen_aid` 的 aid 跳过;
2. 发布时间超过 14 天的结果跳过;
3. 标题不含“咻咻满”跳过;
4. 命中 `up_blacklist` 的 UP 跳过,不写候选池;
5. 通过分类规则计算分数;
6. 分数低于零丢弃;
7. 其余写入候选池;达到通知阈值的候选发送 Telegram 消息;
8. 请求之间随机等待 1.5~2.5 秒。
分类规则 v1:
```text
+2:标题含 reaction / re / 反应 / 看 / 听 / 初听 / 首次 / 点评 / 锐评 / 路人 / 震惊 / 双人
+2:标题含 充电 / 加急 / 专场
+2:时长大于 60 分钟
+1:时长大于 30 分钟且不超过 60 分钟
-5:标题含 歌切 / 切片 / 纯享 / 搬运 / 完整版
-3:UP 为咻咻满本人
score >= 2:发送通知
0 <= score < 2:仅候选池展示
score < 0:丢弃
```
P3 实现说明:
- WBI 密钥来自 `nav.data.wbi_img`,缓存 6 小时;WBI 失败自动降级旧版 `search/type`;
- 关键词、页数和时间窗由 `discover` 配置控制;
- 同一 aid 在多个关键词/页重复出现时,候选与通知都按 aid 去重;
- 已通知但未决策的候选不会重复通知;未通知的候选会在后续发现轮次补发;
- 人工把候选加入收藏夹后,收藏夹同步会入库并删除同 aid 候选。
### 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` 日更。
### 7.4 每日快照与粉丝采集
每天 `00:01`(`Asia/Shanghai`)执行:
- 对 `status IN ('active', 'unfaved')` 的视频逐条请求 `view?aid=`;
- 成功写入播放、点赞、收藏、投币、转发、评论、弹幕七项数据;
- `-404` / `62002` / `62012` 时标记 `removed`(62002/62012 为当前实测返回);
- 其他错误最多重试 3 次,仍失败则写入 `ok=0` 与错误信息,不写伪造的零值;
- 每条视频请求之间随机等待 1.0~1.6 秒;
- 对 `track_fans` 中的每个 mid 请求一次 `relation/stat` 并写入 `up_stat`;
- 可用 `tools/import_fans_history.py` 从小满虫之家公开 `data.js` 回填最近 90 天粉丝历史;只写 `up_stat`,默认跳过已有成功记录;
- 任务结束向 Telegram 发送成功数、失败数、移除数和总耗时摘要。
服务启动后若当天快照尚未成功完成,应补跑当日任务;已成功的 `(stat_date, aid)` 不重复请求。
P1 实现说明:
- 时区使用固定 UTC+8(中国无夏令时),避免 Windows 缺少 `tzdata`;
- 当日全部视频与粉丝采集成功后在 `kv` 写入 `snapshot:completed:<date>=1`;启动时未完成会补跑;
- 完成标记不是唯一门槛:每次检查 `pending_snapshot_aids(date)`,新入库或从 `dropped` 恢复的视频会立即补采当天快照;
- `ok=0` 的当日记录会在下次补跑时重试;`ok=1` 直接跳过;粉丝同样跳过已成功的 `(stat_date, mid)`;
- 开始采集前先写 `snapshot:completed:<date>=0`,强制重采中断后不会残留旧的完成标记;
- 每次采集后生成 `data/backup/app-YYYYMMDD.db` 并清理超过 `storage.backup_keep_days` 的日备份;
- 采集完成与备份完成分开记录;备份失败时 `snapshot` 返回 1,下次执行只重试备份、不重复请求 B 站;
- Cookie 登录态通过 `x/web-interface/nav` 检查,失效只告警,不阻塞匿名 `view`;
- Telegram 摘要推迟到 P3,P1 先写结构化 JSON 日志和 `kv` 状态。
---
## 8. Telegram 与收藏操作
### 8.1 通知动作
候选通知提供:
- **收藏**:加入目标收藏夹并入库;
- **忽略**:候选标记 `rejected`,不再通知;
- **拉黑**:写入 `up_blacklist`,同时拒绝该 UP 的其他待决策候选;
- **打开视频**:外链到 B 站视频页。
拉黑只影响未来搜索发现;不清理、不停止追踪已有视频,也不影响从收藏夹同步进来的视频。
### 8.2 幂等与失败处理
Telegram `callback_data` 只包含短操作 ID,例如 `fav:<id>`;真实 aid、目标夹、过期时间和状态保存在 `fav_action` 表。
收藏流程:
1. 将动作从 `pending` 原子切换为 `processing`;
2. 先读取目标夹 ids,若视频已存在则按成功处理;
3. 按“同 UP 最早视频所在夹优先,否则当年夹”的规则选择目标夹;
4. 调用 `deal` 加入收藏;
5. 调用 `sort` 将视频放进同 UP 连续分组;
6. 回读 ids 校验最终位置;
7. 成功:动作标记 `done`,视频标记 `active`,删除候选;
8. 失败:动作标记 `failed`,保留候选与原始错误,允许用户重试。
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` 都等于配置私聊用户的回调。
---
## 9. Web API 与页面
### 9.1 API 原则
- P2 看板**只读**:业务接口只注册 GET,写方法统一返回 `405`;
- 数据管理(收藏、忽略、拉黑、移出库、移除黑名单)继续通过 Telegram、CLI 或数据库完成;
- `/api/health` 可匿名;配置 `web.auth_token` 后其他 `/api/*` 要求 Cookie 或 `Authorization: Bearer <token>`;
- 只读接口不需要 CSRF;登录 POST 只建立会话,不修改业务数据;
- API 返回 JSON;前端不复制热度、状态机或收藏夹归属判断;
- 不提供导出接口,结果只通过页面浏览。
核心接口:
| 方法 | 路径 | 用途 |
|---|---|---|
| `GET` | `/api/overview` | 首页动态、总览指标、容量、采集状态 |
| `GET` | `/api/videos` | 视频分页、筛选和排序(支持 added/published/heat/七项/互动/增长) |
| `GET` | `/api/videos/{aid}` | 视频详情 |
| `GET` | `/api/videos/{aid}/trend` | 七项趋势数据 |
| `GET` | `/api/rankings` | 视频榜或 `scope=up` 的 UP 聚合榜 |
| `GET` | `/api/ups` | UP 聚合榜单 |
| `GET` | `/api/ups/{mid}` | UP 详情、聚合趋势和视频列表 |
| `GET` | `/api/fans` | 粉丝曲线与增量 |
| `GET` | `/api/candidates` | 候选池(只读) |
| `GET` | `/api/system` | 最近任务、下次任务、备份、黑名单 |
### 9.2 页面
| 页面 | 内容 |
|---|---|
| 首页 | 最新动态优先:今日增量、最新入库、最近发布、增长动态、热度速览、容量与采集状态。 |
| 榜单 | 综合热度、七项单项、播放增量、播放增速、互动率、UP 聚合和筛选。 |
| 视频详情 | 元数据、七项指标、单指标趋势、快照表、失败标记、同 UP 对比。 |
| UP 页 | UP 的全部视频、聚合趋势、单视频对比和平均互动率。 |
| 粉丝页 | 粉丝总数、日增、7 日净增、累计增长(标注起始日期);最近记录默认 14 天;不叠加视频数据。 |
| 候选池 | 待确认候选、规则命中、剩余保留时间;操作指向 Telegram/CLI。 |
| 系统状态 | 最近扫描、同步、快照、下次运行、备份、鉴权和黑名单。 |
### 9.3 预览模式
- `tools/seed_preview_db.py` 复制真实库并回填合成快照,真实库只读;
- 合成快照写 `src='preview'`,预览库写 `kv preview:seeded=1`;
- `python app.py serve --preview` 校验标记,缺失时拒绝启动;
- 预览配置不包含真实凭据,默认端口 8081,Telegram 关闭;
- 预览页面显示固定“预览数据 · 不影响真实数据库”横幅,右上角入口显示“预览”;
- 重新生成预览库前必须停止预览服务,避免 Windows 文件占用。
---
## 10. 热度与趋势口径
### 10.1 综合热度
对每项最新有效快照值计算:
```text
x_i = log1p(metric_i)
z_i = (x_i - median(x)) / (IQR(x) / 1.349)
score = Σ weight_i * z_i
```
默认权重:
```text
view 0.30, like 0.20, coin 0.20, favorite 0.15,
share 0.10, reply 0.03, danmaku 0.02
```
权重从配置读取,改动后查询时重算,不存冗余结果。
### 10.2 增长与互动率
```text
d7 = (view_today - view_previous) / actual_day_gap
r7 = (view_today - view_previous) / max(view_previous, 1)
interaction_rate = (like + coin + favorite) / max(view, 1)
```
- 查询前一有效快照而不是填补缺失日期;
- 缺失日期在图表中断开;
- 增速榜要求播放基数至少 200;
- 互动率榜要求播放至少 100;
- 样本不足两天的视频明确标注,不伪造增长数据。
---
## 11. 测试与质量要求
在引入第三方测试框架前,测试继续使用可直接运行的 Python 脚本:
```powershell
uv run python tests/test_auth.py
uv run python tests/test_moves.py
```
新增模块至少覆盖:
- 标题归一化、时长解析、分类打分;
- 黑名单只影响发现;
- `infos` 静默缺失资源的识别;
- `active`、`unfaved`、`removed`、`dropped` 状态迁移;
- 数据库迁移与幂等写入;
- 每日快照缺失处理;
- 热度、增长和互动率计算;
- 收藏夹目标选择与排序锚点;
- `fav_action` 的重复点击、失败重试和过期处理;
- Telegram 用户与 chat ID 授权校验;
- 看板查询的筛选、分页、增长冷启动与状态过滤;
- 只读路由、鉴权、静态资源路径穿越和预览标记校验;
- 预览库生成的确定性、单调性、异常样本与真实库隔离;
- 粉丝历史 data.js 解析、每日净增反推、缺口停止与幂等回填。
只有当标准库测试维护成本明显过高时,才考虑通过 `uv add --dev pytest` 引入 pytest。
---
## 12. 实施阶段
### P0:数据底座与收藏夹同步 ✅
- SQLite 迁移、配置加载、B 站只读客户端;
- 收藏夹首次导入与每小时差集同步;
- 全量 infos 元数据刷新、失效识别、黑名单种子;
- 在 Windows + `uv` 环境完成只读联调。
完成情况(2026-10-31):
- 已实现 `reaction_board/`:配置热加载、单写队列、`schema_version` 迁移与升级前备份;
- 已实现内置调度器,按 `schedule.folder_sync_minutes` 周期运行收藏夹同步;
- 已实现 `fav/resource/ids`、`fav/resource/infos`、`fav/folder/created/list-all` 只读客户端;
- 首次导入两个年度夹共 831 条资源:795 `active`、36 `removed`(20 条 `attr=9` + 16 条 `infos` 静默缺失),与探测结果一致;
- 增量同步在无变化时只发 2 次 `ids` 请求,不请求 `infos`;
- 黑名单 9 个 mid 已写入 `up_blacklist`,且不参与收藏夹同步过滤;
- 自动化测试见 `tests/test_config.py`、`test_migrations.py`、`test_database.py`、`test_repository.py`、`test_bilibili_client.py`、`test_folder_sync.py`、`test_scheduler.py`、`test_logging.py`、`test_cli.py`。
### P1:快照与粉丝追踪 ✅
- 每日七项视频快照;
- 粉丝快照;
- 失败占位、断点续跑、Cookie 失效告警、SQLite 备份。
完成情况(2026-10-31):
- 已实现 `services/snapshot.py`:`view` 七项指标、`relation/stat` 粉丝数、`ok=0` 失败占位、`ok=1` 断点跳过;
- 实测不可访问视频返回 `62012`,已和旧口径 `-404`、`62002` 一起按 `removed` 处理;
- `run` 调度器同时注册收藏夹同步与每日 `snapshot_at` 任务,启动时立即检查当日是否完成以支持补跑;
- 每次快照后生成日备份并清理超过 14 天的日备份;
- `auth-check` 与快照启动时检查 Cookie 登录态,失效写 `kv` 并告警;
- 真实全量联调:795 条视频全部 `ok=1`,0 失败、0 移除,耗时 1124 秒;粉丝 `follower=2767270`;日备份 `data/backup/app-20261031.db` 含 795 条快照;二次执行正确命中完成标记并跳过;
- 自动化测试见 `tests/test_snapshot.py`、`test_maintenance.py`、`test_timeutil.py`,并扩展了 `test_bilibili_client.py` 与 `test_scheduler.py`。
### P1 补丁:数据完整性与热更新修复 ✅
- 新入库/恢复视频当天补采:`pending_snapshot_aids()` + 同步后立即触发快照;
- 凭据热更新:`ClientProvider` 检测 `mid/cookie/csrf` 变化后重建客户端;
- 协议防御:`ids` / `infos` 缺失 `data` 或 `data=null` 时中止,不再当作空结果;
- 空远端保护:远端 0 条而本地非空时跳过差集,需 `--allow-empty-folder` 才执行清空;
- 粉丝续跑:非强制模式跳过已成功的 `(stat_date, mid)`;
- 强制重采:开始前先写未完成标记,中断后可续跑;
- 备份恢复:采集完成与备份完成分离,备份失败返回 1 并支持无请求重试;
- 跨夹主归属:迁移 v2 增加 `folder_resource.fav_time`,按有效关系重算 `video.folder_id`;
- `dropped` 视频失效时保留原始标题;
- `favsort.py`:`--verify-every` 必须为正数;`sort` 使用真实资源类型,不再硬编码 `2`;
- 配置每 60 秒检查 mtime;调度参数变更需重启。
### P3:Telegram 与一键加入 ✅
- long polling、候选通知、收藏/忽略/拉黑;
- `deal`、`sort`、回读验证和失败重试;
- 对真实写入操作使用独立测试视频完成授权验证后再启用。
完成情况(2026-10-31):
- 已实现 `bilibili/search.py`:WBI 签名、密钥缓存 6 小时、旧版搜索降级;
- 已实现 `classify.py`:标题归一化、时长解析与打分;
- 已实现 `services/discovery.py`:关键词/页数/时间窗过滤、黑名单、候选池、通知批次;
- 已实现 `notify/telegram.py` 与 `services/telegram_poller.py`:消息分片、Inline Keyboard、回调授权、offset 推进;
- 已实现 `bilibili/write_client.py`、`fav_position.py`、`services/favorites.py`:目标夹选择、`deal`、`sort`、回读校验、失败保留候选;
- `app.py` 新增 `discover` / `candidates`;`run` 注册发现与候选清理任务,并启动 Telegram poller;
- 真实只读联调:4 次 WBI 搜索、168 条结果、1 条候选(score=6),Telegram 通知发送成功;
- 真实 `deal` / `sort` 尚未在收藏夹执行,等待用户点击 Telegram 按钮或授权测试视频后验证;
- 自动化测试见 `tests/test_classify.py`、`test_search.py`、`test_candidates.py`、`test_discovery.py`、`test_fav_position.py`、`test_favorites.py`、`test_telegram.py`、`test_httpx_transport.py`、`test_favsort_http.py`。
### P2:只读本地看板 ✅
- 首页、榜单、视频/UP 详情、粉丝、候选和系统状态页面;
- 原生前端和本地 vendored ECharts;
- 本机默认绑定 `127.0.0.1:8080`;
- Web 业务接口只读,写操作继续由 Telegram/CLI/数据库承担;
- 首页默认“最新动态”,增长榜在不足 2 天时显示空态;
- 视频趋势按单指标切换,缺失日断开、失败快照按缺失处理;
- UP 榜默认平均播放,总播放/视频数排到末尾;单视频 UP 不显示单视频对比图;
- 实现 `tools/seed_preview_db.py`:复制真实库并回填 7 天合成快照,支持 `--profile live|first-day`;
- 实现 `tools/import_fans_history.py`:从小满虫之家 `data.js` 回填 90 天粉丝历史,支持 dry-run、缺口停止和幂等写入;
- `serve --preview` 校验 `preview:seeded=1` 标记,预览页面显示“预览数据”横幅;
- 自动化测试见 `tests/test_analytics.py`、`test_preview_seed.py`、`test_web_queries.py`、`test_web_server.py`。
### P4:可选部署
- 反向代理、TLS、远程访问鉴权;
- 仓库已提供 `Dockerfile` / `compose.example.yml` / `config.docker.example.json`;容器内执行 `python app.py --config /config/config.json`,挂载 `/config` 与 `/data`;
- Docker 不作为本机开发、联调或日常运行的前置条件。
---
## 13. 明确非目标
第一期不实现:
- 多用户、注册、角色权限;
- 自动创建收藏夹或容量满后自动换夹;
- AI 自动判定 reaction;
- UP 白名单;
- 视频历史统计回填;
- Redis、消息队列、微服务、云数据库;
- Node.js 前端工程;
- Docker 作为本地开发依赖。