Files
re-xxm/DEVELOPMENT.md
T

770 lines
39 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 发现与入库)已实施;项目已与 B 站收藏夹解耦
>
> **技术基线:** 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
# 手动入库:视频链接、BV 号、av 号或 b23.tv 短链(只写本地库)
uv run python app.py ingest BV1x8x166EzY
uv run python app.py ingest https://www.bilibili.com/video/BV1x8x166EzY
# 按配置周期常驻运行发现与快照(P0/P3);--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
# 回填历史视频的 UP 粉丝数快照(第 9 轮;只补空值)
uv run python app.py backfill-followers
uv run python app.py backfill-followers --dry-run
# 检查 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
# 从 B 站粉丝历史 CSV 回填(先 dry-run;首次完整导入可用 --replace)
uv run python tools/import_fans_csv.py --csv E:\Downloads\history.csv --dry-run
uv run python tools/import_fans_csv.py --csv E:\Downloads\history.csv --replace
```
不建议直接激活 `.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`;
- 读操作使用短生命周期连接;
- 网络请求期间不得持有数据库写事务;
- 入库操作通过 `video` 存在性判断保证幂等;
- 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 # 热度、增长、互动率、时长效率、EB 收缩与封面工具
│ ├── database.py # SQLite 连接、迁移、单写队列
│ ├── migrations.py # schema_version 与迁移
│ ├── scheduler.py # 内置任务调度
│ ├── logging_json.py # 脱敏 JSON 日志
│ ├── classify.py # 标题归一化、时长解析与打分
│ ├── video_ref.py # BV/av/URL/b23.tv 引用解析(纯函数)
│ ├── timeutil.py # 上海时区与每日时间
│ ├── bilibili/
│ │ ├── client.py # 只读 HTTP、重试与限速(含 view 详情、短链解析)
│ │ ├── write_client.py # sort 写客户端(仅收藏夹重排序)
│ │ ├── search.py # WBI 搜索与旧接口降级
│ │ ├── provider.py # 凭据变化重建客户端
│ │ ├── auth.py # Cookie、设备指纹与请求头工具
│ │ ├── auth_flow.py # app.py auth 的扫码与原子写回流程
│ │ └── models.py # 接口响应模型
│ ├── services/
│ │ ├── discovery.py # 搜索发现与候选池
│ │ ├── library.py # 入库(候选/手动)/ 忽略 / 拉黑
│ │ ├── telegram_poller.py # getUpdates、回调分发与私聊手动入库
│ │ ├── snapshot.py
│ │ ├── up_profile.py # UP 粉丝快照采集与历史回填
│ │ └── 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_csv.py # 从 B 站粉丝历史 CSV 回填 up_stat
│
├── 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,
"snapshot_at": "00:01",
"candidate_ttl_days": 7
},
"notify": { "min_score": 2, "max_items_per_message": 10 }
}
```
- `folders` 仅作为历史来源与 `favsort.py` 重排序目标,系统不再自动同步或建夹;
- 配置必须为 `bilibili.read` / `bilibili.write` 双账号结构;不兼容顶层扁平 `mid` / `csrf` / `cookie`;
- 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,凭据、分类规则和权重变更后无需重启;
- Cookie / CSRF 变化时由 `ClientProvider` 重建 B 站请求客户端,并保留该角色的限速与重试策略;
- `schedule.discover_minutes` 与 `schedule.snapshot_at` 在启动时固化,修改后需重启 `run`;
- Telegram 操作必须同时验证 `from.id` 和 `chat.id` 是否为配置的私聊用户;
- 仅允许环境变量临时覆盖敏感配置,不能将真实凭据硬编码进源代码;
- `GET` 路由绝不进行入库、忽略、拉黑等写操作。
---
## 6. 数据模型
### 6.1 核心状态
| 表 | 状态 | 含义 | 是否参与每日快照 |
|---|---|---|---|
| `video` | `active` | 在库追踪的视频 | 是 |
| `video` | `unfaved` | 历史遗留状态(迁移 v5 已归一为 `active`) | 是 |
| `video` | `removed` | 视频失效、删除或不可访问 | 否 |
| `video` | `dropped` | 手动移出库;再次入库会恢复 `active` | 否 |
| `candidate` | `NULL` | 待人工决策 | 否 |
| `candidate` | `rejected` | 已忽略 / 所属 UP 已拉黑 | 否 |
### 6.2 建表原则
最终 DDL 由迁移模块维护,关键约束如下:
- `snapshot` 主键为 `(stat_date, aid)`,保证同日幂等;
- `up_stat` 主键为 `(stat_date, mid)`;
- `folder` / `folder_resource` 保留为历史数据与重排序参考,**不再由服务写入**;
- `up_blacklist` 只在发现任务中生效,不影响已入库视频;
- `seen_aid` 永久保留搜索去重标记,候选过期删除后也不重复通知;
- `fav_action` 保留为通用动作状态表(当前入库幂等由 `video` 存在性判断承担);
- 迁移 v5 把历史 `unfaved` 行归一为 `active`;表结构不做破坏式删除。
### 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 搜索失败时使用 |
| 视频详情 | `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` 对齐浏览器特征:
- `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 收藏夹解耦
项目**不再轮询收藏夹**,也不再根据收藏夹内容标记视频入库/出库:
- 不再调用 `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 每日快照与粉丝采集
每天 `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_csv.py` 从 B 站粉丝历史 CSV 回填 `up_stat`;只写 `up_stat`,默认跳过已有成功记录,`--force` 覆盖同日期,`--replace` 用完整历史覆盖重建(存在晚于 CSV 末尾的记录时默认拒绝,需显式 `--allow-lossy-replace`);
- 任务结束向 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 通知动作
候选通知提供:
- **入库**:写入本地 `video(status='active')` 并补当天快照,**不写 B 站收藏夹**;
- **忽略**:候选标记 `rejected`,不再通知;
- **拉黑**:写入 `up_blacklist`,同时拒绝该 UP 的其他待决策候选;
- **打开视频**:外链到 B 站视频页。
私聊文本消息还会被解析为视频引用(链接 / BV 号 / av 号 / `b23.tv` 短链)并逐条入库。拉黑只影响未来搜索发现;不清理、不停止追踪已有视频。
### 8.2 幂等与失败处理
Telegram `callback_data` 只包含短操作 ID,例如 `add:<aid>` / `ign:<aid>` / `blk:<aid>`;`fav:<aid>` 为旧按钮兼容。
入库流程:
1. 候选存在校验;若视频已在库则返回「已在库」;
2. 用只读客户端取一次 `view` 详情(失败时回退候选元数据);
3. 写 `video(status='active')`,删除同 aid 候选;
4. 用取到的 `view` 数据写一条当天快照(同日已有成功记录则跳过);
5. 采集该 UP 当前粉丝数并提升缓存版本。
私聊手动入库按消息中的每个引用重复上述步骤,错误逐条隔离,不中断其余引用。
GET 页面或链接预览只能展示信息,绝不能触发任何写操作。
P3 实现说明:
- `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` 都等于配置私聊用户的回调与消息。
---
## 9. Web API 与页面
### 9.1 API 原则
- P2 看板**只读**:业务接口只注册 GET,写方法统一返回 `405`;
- 数据管理(入库、忽略、拉黑、移出库、移除黑名单)继续通过 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;前端不复制热度或状态机判断;
- 不提供导出接口,结果只通过页面浏览;
- 首页、榜单和粉丝的聚合结果使用内存 TTL 缓存:首页/榜单 1 小时,粉丝 24 小时;
- 缓存版本存放在 `kv`(`cache:v:videos` / `cache:v:fans`);快照、发现、入库/忽略/拉黑、候选清理和粉丝 CSV 导入都会提升对应版本,使缓存立刻失效。
核心接口:
| 方法 | 路径 | 用途 |
|---|---|---|
| `GET` | `/api/overview` | 首页动态、总览指标、容量、采集状态 |
| `GET` | `/api/videos` | 视频分页、筛选和排序(`sort` / `within_days` / `year` / `q`;支持 added/published/heat/七项/互动/增长/内容效率) |
| `GET` | `/api/videos/{aid}` | 视频详情(含分 P 数、入库时粉丝数、内容/互动效率) |
| `GET` | `/api/videos/{aid}/trend` | 七项趋势数据 |
| `GET` | `/api/rankings` | 兼容旧链接;`scope=up` 时为 UP 聚合榜(`year` 可用) |
| `GET` | `/api/ups` | UP 聚合榜单(含总时长、播放·小时、内容效率;`sort` / `within_days` / `year`) |
| `GET` | `/api/ups/{mid}` | UP 详情、聚合趋势和视频列表 |
| `GET` | `/api/fans` | 粉丝曲线与增量 |
| `GET` | `/api/candidates` | 候选池(只读) |
| `GET` | `/api/system` | 最近任务、下次任务、备份、黑名单 |
| `GET` | `/api/about` | 数据说明页当前口径(配置阈值、权重、覆盖度;无凭据) |
### 9.2 页面
| 页面 | 内容 |
|---|---|
| 首页 | 最新动态优先:今日增量、最新入库、最近发布、增长动态、热度速览、容量与采集状态。 |
| 视频(`/videos`) | 内容效率(默认)、综合热度、发布时间、入库时间、播放/投币/收藏/评论、互动率、播放增量、播放增速;工具条为「排序」下拉 + 时间下拉 + 右侧放大镜搜索框(一行展示),时间支持全部/7 天/30 天/90 天/1 年/年份。 |
| UP主(`/ups`) | 独立入口,不再与视频混排;排序下拉含内容效率、互动效率、内容量、总播放、视频数、平均播放、总互动、最近发布;不提供按视频数筛选。 |
| 数据说明(`/about`) | 数据来源与更新、发布时间/入库时间/快照日期、指标公式、时长与成熟窗口筛选、粉丝快照覆盖与已知局限;数值由 `/api/about` 动态读取。 |
| 视频详情 | 元数据、分 P 数、七项指标、内容/互动效率、入库时粉丝数、单指标趋势、快照表、同 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 文件占用;
- 第 9 轮起本地不再保留 `data/app.db` 真实副本;重新生成预览时用 `--source` 指向 `data/backup/` 中的真实库备份(如 `app-local-before-reseed-20261101.db`)。
---
## 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;
- 样本不足两天的视频明确标注,不伪造增长数据。
### 10.3 时长效率与 UP 内容效率(第 9 轮)
```text
内容小时 = duration / 3600
内容效率 = (view + κ_v·μ₀) / (内容小时 + κ_v)
互动效率 = (like + coin + favorite + κ_v·μ₀ᵢ) / (内容小时 + κ_v)
UP 内容效率 = (Σview + κ·μ₀) / (Σ内容分钟 + κ) * 60
```
- 分母只用总时长,不用分 P 数(B 站播放记在 bvid 上,`view/pages` 无统计含义);
- 视频级 EB 收缩 `κ_v = analytics.video_shrink_minutes / 60`(默认 60 分钟),`μ₀` 为全库播放/小时;详情页展示原始播放/小时;
- 效率榜与综合热度榜要求播放 ≥ `analytics.min_view_for_ranking`(默认 2000);
- 效率榜要求 `duration >= analytics.min_duration_seconds`(默认 600 秒)且发布满 `analytics.maturity_days`(默认 2 天);
- 不引入 `log(age)` 等无界年龄项,避免系统性惩罚老视频;
- `duration` 缺失或非正数不参与效率排序;
- UP 收缩强度 `κ` 取全库 UP 总内容分钟中位数,`analytics.shrink_minutes` 可覆盖;
- 视频数与总时长并列展示、分别排序,不合成单一产出分。
---
## 11. 测试与质量要求
在引入第三方测试框架前,测试继续使用可直接运行的 Python 脚本:
```powershell
uv run python tests/test_auth.py
uv run python tests/test_moves.py
```
新增模块至少覆盖:
- 标题归一化、时长解析、分类打分;
- 视频引用解析(BV/av/URL/`b23.tv` 短链)与 `view?bvid=` 契约;
- 黑名单只影响发现;
- `infos` 静默缺失资源的识别;
- `active`、`unfaved`、`removed`、`dropped` 状态迁移;
- 数据库迁移与幂等写入;
- 每日快照缺失处理;
- 热度、增长和互动率计算;
- 入库动作(候选确认 / 手动)与幂等;
- `fav_action` 的重复点击、失败重试和过期处理;
- Telegram 用户与 chat ID 授权校验;
- 看板查询的筛选、分页、增长冷启动与状态过滤;
- 只读路由、鉴权、静态资源路径穿越和预览标记校验;
- 预览库生成的确定性、单调性、异常样本与真实库隔离;
- 粉丝历史 CSV 解析、同日去重、缺口计数与幂等/替换回填;
- UP 粉丝快照:首次写入不可覆盖、同轮去重、失败不中断、`live_since` 实时/回填标记、`backfill` 的 dry-run 与 limit;
- 时长效率:`duration` 缺失/非正数、短于门槛、未满成熟窗口的排除,EB 收缩边界(除零、负先验)。
只有当标准库测试维护成本明显过高时,才考虑通过 `uv add --dev pytest` 引入 pytest。
---
## 12. 实施阶段
### P0:数据底座与收藏夹同步 ✅(收藏夹同步已于第 10 轮移除)
- 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` 任务,启动时立即检查当日是否完成以支持补跑(早期同时注册收藏夹同步,已于第 10 轮移除);
- 每次快照后生成日备份并清理超过 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_csv.py`:从 B 站粉丝历史 CSV 回填 `up_stat`,支持 dry-run、`--force` 覆盖和 `--replace` 重建;
- `serve --preview` 校验 `preview:seeded=1` 标记,预览页面显示“预览数据”横幅;
- 自动化测试见 `tests/test_analytics.py`、`test_preview_seed.py`、`test_web_queries.py`、`test_web_server.py`。
### 第 9 轮:时长效率榜、粉丝快照与导航拆分 ✅(2026-11-01)
- 迁移 v4:`video.pages`、`video.up_follower` / `up_follower_at` / `up_follower_backfilled`、`up_profile`;
- 收藏夹同步(已于第 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 天的视频;
- `analytics` 配置段:`min_duration_seconds`、`maturity_days`、`min_view_for_ranking`、`video_shrink_minutes`、`shrink_minutes`;
- 主导航拆分:`/videos`(视频)与 `/ups`(UP主)独立;旧 `/rank` 保留兼容;
- 新增 `/about` 数据说明页与 `/api/about`;
- 卡片精简:列表只保留主指标与互动率,明细移到详情页;
- 本地只保留可再生的预览库 `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、远程访问鉴权;
- 仓库已提供 `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 作为本地开发依赖。