Files
re-xxm/DEVELOPMENT.md
T
kairee e2cdf997af feat(p0): add favorite folder sync and sqlite data foundation
Implement P0 of the reaction dashboard: config loading with hot reload, SQLite migrations with backups, single-writer queue, read-only Bilibili fav client, folder import/diff sync, invalid-resource handling, blacklist seed, scheduler, and JSON redacted logging. Includes offline unit and transport-level contract tests; all existing and new tests pass.
2026-10-31 18:08:33 +08:00

22 KiB
Raw Blame History

咻咻满 Reaction 视频数据看板开发文档

状态: P0(数据底座与收藏夹同步)已实施并通过只读联调;P1 待开始

技术基线: 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 客户端 urllib.request 现有 B 站请求代码已使用;无需额外依赖。
数据库 SQLite(WAL) 单用户、单机、低并发写入;备份和迁移成本低。
调度 threading、queue.Queue、标准库时间工具 单进程内运行发现、同步、快照和清理任务。
前端 原生 HTML/CSS/JavaScript 无 Node.js、无构建链;前端仅渲染 /api/* JSON。
图表 仓库内置 ECharts 离线静态资源,Apache-2.0 许可。
通知 Telegram Bot long polling 具备按钮回调能力,开发和本机运行不需要公网 URL。
容器化 可选的后续部署方式 本地开发、调试和日常运行均不要求 Docker。

1.1 选型原则

  1. 复用现有能力。 当前仓库的 favsort.py 已实现 B 站设备指纹 Cookie、扫码登录、收藏夹读取和排序逻辑;新功能应拆分复用,而不是另起技术栈。
  2. 保持单体。 当前规模约数百到数千视频,每日采集量约千级请求;单进程、SQLite 和内置调度足够。
  3. 尽量标准库。 当前没有第三方 Python 依赖,第一期不因便利性引入 Web 框架、ORM、任务队列或 Redis。
  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

# 扫码登录并原子更新 config.json
uv run bili-fav auth

# 检查现有 B 站登录状态
uv run bili-fav auth --check

# 执行当前无网络依赖的测试
uv run python tests/test_auth.py
uv run python tests/test_moves.py

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

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

# 查看数据库状态(离线,P0)
uv run python app.py status

# P2 起启动本地看板服务
uv run python app.py
uv run python app.py --config .\config.json

不建议直接激活 .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;
  • 读操作使用短生命周期连接;
  • 网络请求期间不得持有数据库写事务;
  • 收藏操作通过 fav_action 状态锁保证幂等;
  • 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 热加载
│   ├── database.py                # SQLite 连接、迁移、单写队列
│   ├── migrations.py              # schema_version 与迁移
│   ├── scheduler.py               # 内置任务调度
│   ├── logging_json.py            # 脱敏 JSON 日志
│   ├── bilibili/
│   │   ├── client.py              # HTTP、WBI、重试与限速
│   │   ├── auth.py                # 从 favsort 抽取的扫码授权能力
│   │   └── favorite.py            # 加入收藏夹与排序定位
│   ├── services/
│   │   ├── discovery.py
│   │   ├── folder_sync.py
│   │   ├── snapshot.py
│   │   ├── fans.py
│   │   ├── candidates.py
│   │   ├── rankings.py
│   │   └── maintenance.py
│   ├── notify/telegram.py
│   └── web/
│       ├── server.py
│       ├── api.py
│       └── static/
│           ├── index.html
│           ├── app.js
│           ├── styles.css
│           └── vendor/echarts.min.js
│
├── tests/
└── data/                          # 本地运行期数据,默认不提交

favsort.py 继续保持可独立运行。P0 实施时优先抽取其通用的 Cookie、请求头、扫码授权和收藏夹 API 逻辑;不要直接复制出第二套不一致的身份与请求实现。


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,
    "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 中的年度夹由人工创建,系统只读;
  • 兼容现有 favsort.py 的扁平 mid / csrf / cookie 写法;存在 bilibili 嵌套对象时以嵌套值为准;
  • 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 改变时重建 B 站请求客户端;
  • 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;
  • video.folder_id 表示视频的主归属夹,用于新视频同 UP 归属和排序;
  • 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 分组

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:丢弃

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 时标记 removed;
  • 其他错误最多重试 3 次,仍失败则写入 ok=0 与错误信息,不写伪造的零值;
  • 每条视频请求之间随机等待 1.0~1.6 秒;
  • 对 track_fans 中的每个 mid 请求一次 relation/stat 并写入 up_stat;
  • 任务结束向 Telegram 发送成功数、失败数、移除数和总耗时摘要。

服务启动后若当天快照尚未成功完成,应补跑当日任务;已成功的 (stat_date, aid) 不重复请求。


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 页面或链接预览只能展示信息,绝不能触发任何写操作。


9. Web API 与页面

9.1 API 原则

  • /api/health 可匿名,仅返回健康状态;
  • 其他 /api/* 默认要求 Authorization: Bearer <token>;
  • 写操作额外要求 CSRF 防护与 Origin/Referer 校验;
  • API 返回 JSON;前端不复制热度、状态机或收藏夹归属判断;
  • CSV 导出由后端根据当前筛选条件生成。

核心接口:

方法 路径 用途
GET /api/overview 总览指标、容量、采集状态
GET /api/videos 视频分页、筛选和排序
GET /api/videos/{aid} 视频详情
GET /api/videos/{aid}/trend 七项趋势数据
GET /api/rankings 热度、增长、互动率与单项榜
GET /api/ups UP 聚合榜单
GET /api/fans 粉丝曲线与增量
GET /api/candidates 候选池
GET /api/system/status 最近任务、下次任务、错误日志
POST /api/candidates/{aid}/favorite 收藏候选
POST /api/candidates/{aid}/ignore 忽略候选
POST /api/candidates/{aid}/blacklist 拉黑候选所属 UP
DELETE /api/blacklist/{mid} 移除黑名单
POST /api/videos/{aid}/drop 手动停止追踪

9.2 页面

页面 内容
总览 库内数、UP 数、今日新增、增量、容量条、采集成功率、故障提示。
榜单 综合热度、七项单项、增长、增速、互动率、UP 聚合、筛选和 CSV。
视频详情 元数据、七项趋势、快照表、失败标记、同 UP 对比。
UP 页 UP 的全部视频、聚合趋势和平均互动率。
粉丝页 粉丝总数、日增、7 日净增、累计增长;不叠加视频数据。
候选池 待确认候选、规则命中、剩余保留时间和三个操作。
系统状态 最近扫描、同步、快照、下一次运行时间和错误。

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;
  • 样本不足两天的视频明确标注,不伪造增长数据。

11. 测试与质量要求

在引入第三方测试框架前,测试继续使用可直接运行的 Python 脚本:

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

新增模块至少覆盖:

  • 标题归一化、时长解析、分类打分;
  • 黑名单只影响发现;
  • infos 静默缺失资源的识别;
  • active、unfaved、removed、dropped 状态迁移;
  • 数据库迁移与幂等写入;
  • 每日快照缺失处理;
  • 热度、增长和互动率计算;
  • 收藏夹目标选择与排序锚点;
  • fav_action 的重复点击、失败重试和过期处理;
  • Telegram 用户与 chat ID 授权校验。

只有当标准库测试维护成本明显过高时,才考虑通过 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 备份。

P2:本地看板

  • 总览、榜单、详情、UP、粉丝、候选和系统状态页面;
  • 原生前端和 ECharts;
  • 本机默认绑定 127.0.0.1:8080。

P3:Telegram 与一键加入

  • long polling、候选通知、收藏/忽略/拉黑;
  • deal、sort、回读验证和失败重试;
  • 对真实写入操作使用独立测试视频完成授权验证后再启用。

P4:可选部署

  • 反向代理、TLS、远程访问鉴权;
  • 如有需要再提供 Dockerfile;
  • Docker 不作为本机开发、联调或日常运行的前置条件。

13. 明确非目标

第一期不实现:

  • 多用户、注册、角色权限;
  • 自动创建收藏夹或容量满后自动换夹;
  • AI 自动判定 reaction;
  • UP 白名单;
  • 视频历史统计回填;
  • Redis、消息队列、微服务、云数据库;
  • Node.js 前端工程;
  • Docker 作为本地开发依赖。