Files
re-xxm/DEVELOPMENT.md
T

45 KiB
Raw Blame History

咻咻满 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;
  • 网络联通以便 uv 首次下载 Python 3.14;
  • Windows PowerShell、Windows Terminal 或任意可运行 Python 的终端;
  • 不需要安装 Docker、Node.js、npm、SQLite 命令行工具或 PostgreSQL。

项目通过根目录的 .python-version 固定使用 Python 3.14。uv 会在需要时自动安装相应解释器,并在仓库根目录创建 .venv。

2.2 初始化与同步

在项目根目录执行:

uv sync --all-groups

该命令会:

  1. 读取 pyproject.toml;
  2. 确保 Python 3.14 可用;
  3. 创建或更新 .venv;
  4. 安装项目本身及后续声明的依赖;
  5. 根据 uv.lock 锁定可复现的环境。

目前项目功能仅依赖 Python 标准库,因此首次同步不会安装额外运行时包。这是预期行为,不表示 uv 没有生效。

2.3 常用命令

# 查看 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

未来看板服务实现后,统一使用:

# 建库、迁移与黑名单种子(离线,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:

# 新增运行依赖(仅在标准库无法合理满足需求时)
uv add <package>

# 新增仅用于开发的依赖
uv add --dev <package>

# 同步锁文件和环境
uv sync --all-groups

提交依赖变更时,必须同时提交:

  • pyproject.toml
  • uv.lock

不得提交:

  • .venv/
  • config.json
  • config.json.bak
  • data/ 中的运行期数据库、日志、计划和备份。

3. 总体架构

浏览器 / 手机                         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。
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000;
PRAGMA synchronous = NORMAL;

4. 推荐代码结构

.
├── 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                # 标题归一化、时长解析与打分
│   ├── client_ip.py               # 来源 IP 提取、路由归一化、机器人判定(纯函数)
│   ├── 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 与数据整形
│       ├── visits.py              # 访问统计内存缓冲与批量落库
│       └── static/                # 原生 JS/CSS、ECharts、预览页面
│
├── deploy/
│   └── nginx/                     # 反代示例:真实来源 IP(realip)与站点配置
│
├── 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 忽略,日志中不得输出其敏感值。

{
  "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);
  • visit 访问统计表主键为 (stat_date, ip, path),hits 累加;数据先落内存缓冲、批量 upsert,PV = SUM(hits)、UV = COUNT(DISTINCT ip);
  • 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 发现任务

每小时扫描两个关键词、每个关键词前两页:

关键词:咻咻满 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:

+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=;
  • 成功写入播放、点赞、收藏、投币、转发、评论、弹幕七项数据,并把 view 的 owner.face 写入 video.up_face(UP 头像,供 UP 列表/详情展示);
  • -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 跟随跳转;
  • 手动入库不做「标题含咻咻满」等规则校验;单条消息最多处理 50 个引用,超出会在回执中提示分批发送;无效 BV/av 号(-400)逐条报错、不中断其余引用;
  • Telegram 只接受 chat_id 与 from.id 都等于配置私聊用户的回调与消息。

9. Web API 与页面

9.1 API 原则

  • P2 看板只读:业务接口只注册 GET,写方法统一返回 405;
  • 仅显式放行两个 POST 端点:POST /login(建立会话)与 POST /api/visit(访问统计埋点);二者都不修改业务数据,其余 POST/PUT/PATCH/DELETE 仍统一 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 聚合榜单(含总时长、播放·小时、内容效率、UP 头像 up_face、最新粉丝数 follower_latest;sort / within_days / year)
GET /api/ups/{mid} UP 详情、聚合趋势、头像和视频列表
GET /api/fans 粉丝曲线与增量
GET /api/candidates 候选池(只读)
GET /api/system 最近任务、下次任务、备份、黑名单
GET /api/about 数据说明页当前口径(配置阈值、权重、覆盖度;无凭据)
POST /api/visit 访问统计埋点(匿名):归一化路径后写内存缓冲,定期批量落库
GET /api/stats 访问统计(需登录):每日 PV/UV 序列、Top 页面,合并未落库缓冲

9.2 页面

页面 内容
首页 最新动态优先:顶部四个可点击数据卡片(今日入库→视频榜按入库排序、播放增量→视频榜按 1 日增长排序、粉丝日增→粉丝页、待确认候选→候选页);下方为最近发布、增长动态、热度速览、历史收藏夹与采集状态;三处视频区块统一使用竖排媒体卡(封面 + 两行截断标题 + 作者/数据行 + 高亮数据行),并统一为响应式网格:移动端每行 1 个、平板 2/3 个、PC 每行 5 个,每处各取前 5 条。
视频(/videos) 内容效率(默认)、综合热度、发布时间、入库时间、播放/投币/收藏/评论、互动率、播放增量、播放增速;工具条为「排序」下拉 + 时间下拉 + 右侧放大镜搜索框(一行展示),时间支持全部/7 天/30 天/90 天/1 年/年份。
UP主(/ups) 独立入口,不再与视频混排;卡片显示 UP 头像、视频数与总时长、最新粉丝数,以及当前排序的主/副指标(「总播放」排序时副指标为「平均播放」);排序下拉含内容效率、互动效率、内容量、总播放、视频数、平均播放、总互动、最近发布;不提供按视频数筛选。
数据说明(/about) 数据来源与更新、发布时间/入库时间/快照日期、指标公式、时长与成熟窗口筛选、粉丝快照覆盖与已知局限;数值由 /api/about 动态读取。
视频详情 元数据、分 P 数、七项指标、内容/互动效率、入库时粉丝数、单指标趋势、快照表、同 UP 对比。
UP 页 该 UP 的头像、首次观看与最近观看时间、全部视频(默认按发布时间排序)、聚合趋势、单视频对比、总时长与播放·小时/互动·小时、入库时与当前粉丝数。
粉丝页 粉丝总数、日增、7 日净增、累计增长(标注起始日期);支持预设和自选日期,范围超过一年时按每月峰值日期聚合;最近记录默认 14 天并显示“较上一成功日”增量;不叠加视频数据。
候选池 待确认候选、规则命中、剩余保留时间;操作指向 Telegram/CLI。
系统状态 最近扫描、同步、快照、下次运行、备份、鉴权、黑名单与访问统计(近 30 天 PV/UV 与 Top 页面)。

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)。

9.4 访问统计与真实来源 IP

  • 前端在整页加载与站内路由切换时通过 navigator.sendBeacon("/api/visit") 上报路径(sort/筛选等同路径重渲染不重复计数);
  • 服务端 reaction_board/web/visits.py 把上报按 (日期, IP, 路由) 聚合到内存缓冲,后台线程每 web.track.flush_seconds(默认 60 秒)批量 upsert 到 visit 表;server_close() 时做最后一次 flush;
  • /api/stats 合并未落库缓冲,因此系统页基本实时;keep_days 到期数据由后台按日清理;
  • 真实 IP 由 nginx 用 realip 模块按 CDN 网段归一化后写入 X-Real-IP;应用(reaction_board/client_ip.py)仅在 socket 对端属于 web.track.trusted_proxies 时采信请求头,否则回退对端,避免直连请求伪造 CF-Connecting-IP/EO-Connecting-IP/X-Forwarded-For;
  • 直连公网 EIP、Cloudflare、EdgeOne 三种来源的 nginx 配置与网段刷新脚本见 deploy/nginx/;配置项见 README 的 web.track 说明;
  • 进程被强杀时最多丢失一个刷新周期的上报(可接受)。

10. 热度与趋势口径

10.1 综合热度

对每项最新有效快照值计算:

x_i = log1p(metric_i)
z_i = (x_i - median(x)) / (IQR(x) / 1.349)
score = Σ weight_i * z_i

默认权重:

view 0.30, like 0.20, coin 0.20, favorite 0.15,
share 0.10, reply 0.03, danmaku 0.02

权重从配置读取,改动后查询时重算,不存冗余结果。

10.2 增长与互动率

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 轮)

内容小时   = 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 脚本:

uv run python tests/test_auth.py
uv run python tests/test_moves.py

新增模块至少覆盖:

  • 标题归一化、时长解析、分类打分;
  • 视频引用解析(BV/av/URL/b23.tv 短链)与 view?bvid= 契约;
  • UP 头像与粉丝数列:view/view_detail 的 owner.face 解析、入库与快照写入 video.up_face、/api/ups 的 up_face/follower_latest 映射;
  • 黑名单只影响发现;
  • infos 静默缺失资源的识别;
  • active、unfaved、removed、dropped 状态迁移;
  • 数据库迁移与幂等写入;
  • 每日快照缺失处理;
  • 热度、增长和互动率计算;
  • 入库动作(候选确认 / 手动)与幂等;
  • fav_action 的重复点击、失败重试和过期处理;
  • Telegram 用户与 chat ID 授权校验;
  • 看板查询的筛选、分页、增长冷启动与状态过滤;
  • 只读路由、鉴权、静态资源路径穿越和预览标记校验;
  • 来源 IP 提取(直连 EIP/Cloudflare/EdgeOne、不可信对端伪造、IPv6、带端口、回退)与路由归一化、机器人判定;
  • 访问统计内存缓冲(聚合、跨日、单键上限、flush 幂等、写失败并回、保留期清理)与 POST /api/visit、GET /api/stats 契约;
  • 预览库生成的确定性、单调性、异常样本与真实库隔离;
  • 粉丝历史 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。

第 11 轮:UP 头像与 UP 列表信息完善 ✅(2026-11-01)

  • 迁移 v6:video.up_face(来自 view 的 owner.face);入库与每日快照都会刷新该字段;
  • ViewStat / VideoDetail 增加 UP mid/name/face 解析;
  • UP 列表卡片由视频封面改为 UP 头像(无头像时显示首字母占位)、常驻显示最新粉丝数;「总播放」排序时副指标显示「平均播放」,避免与主指标重复;
  • UP 详情页补充头像;
  • UP 详情页移除已解耦的收藏夹徽标,改为显示「首RE时间」(该 UP 最早视频发布时间)与最近发布时间;/api/ups/{mid} 不再返回 folders;
  • 视频详情页同步移除已解耦的收藏夹徽标;
  • /api/ups 返回 up_face 与 follower_latest,/api/ups/{mid} 返回 up_face;
  • 自动化测试更新:test_migrations.py、test_database.py、test_web_queries.py、test_bilibili_client.py、test_library.py、test_snapshot.py、web_fixtures.py。

第 12 轮:访问统计(PV/UV)✅(2026-11-01)

  • 迁移 v7:新增 visit(stat_date, ip, path, hits, first_at, last_at) 聚合表;
  • reaction_board/client_ip.py:来源 IP 提取、路由归一化与机器人判定纯函数(三种来源 + 伪造防护);
  • reaction_board/web/visits.py:访问上报先落内存缓冲,后台每分钟批量 upsert,支持单键上限与保留期清理,关闭时最后一次 flush;
  • 前端 navigator.sendBeacon("/api/visit") 埋点;Web 只读边界放宽为显式 POST 白名单(/api/visit + /login);
  • config.json 新增 web.track(enabled/header_order/trusted_proxies/count_bots/flush_seconds/keep_days/max_hits_per_ip_path_per_day);
  • 系统状态页展示近 30 天 PV/UV 与 Top 页面(GET /api/stats,需登录,合并未落库缓冲);
  • deploy/nginx/ 提供直连 EIP / Cloudflare / EdgeOne 的 realip 与站点配置、网段刷新脚本和说明;
  • 自动化测试新增 tests/test_client_ip.py、tests/test_visits.py,并扩展 test_config.py、test_web_queries.py、test_web_server.py、test_migrations.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 作为本地开发依赖。