Files
MediaCrawler/UPSTREAM.md
T
butubb e348de48d3
Deploy VitePress site to Pages / build (push) Canceled after 0s
Deploy VitePress site to Pages / Deploy (push) Canceled after 0s
feat(upstream): 上游更新检查——定期比上游、落后了推企业微信
本仓库在上游 MediaCrawler 之上加了一整层(见 UPSTREAM.md),可合并流程默认
「有人知道上游动了」。而部署是 git pull --ff-only,只从自己的 Gitea 拉——上游的
提交不主动 fetch 就永远看不见。拖着不合并的代价是复利的:越久越难合。

于是把「上游动了没有」变成一条会自己跑、会推企业微信的通知:

* api/monitor/upstream.py:git fetch <url> <branch> 到 FETCH_HEAD,用
  rev-list --count HEAD..FETCH_HEAD 算落后数、FETCH_HEAD..HEAD 算领先数。
  用 git 而非托管商 API,因为只有 git 知道共同祖先在哪——本仓库含有上游没有的
  提交,直接比 tip 会得出错误结论。增量 fetch 只传几个新提交,不会遇到
  UPSTREAM.md 里说的「大包必断」。
* 只 fetch 到 FETCH_HEAD:不配 remote、不写 refs/remotes、不碰索引与工作区,
  所以不打断正在跑的采集,也不和 deploy.sh 的 git pull 抢锁。
* 挂在调度器 tick 上(不是采集,所以不看 is_busy、不受活跃时段限制——定时检查
  放在半夜反而最合适),按 checked_at + 间隔 到期才跑;失败也写 checked_at,
  于是 GitHub 不通时是每间隔重试一次,而不是每个 tick 撞一次墙。
* 同一个 tip 只推一次(记 tip 而不是「推过没」),上游真又动了会再推。
* 两个接口:GET /monitor/upstream 只读缓存;POST /monitor/upstream/check 手动
  查一次且刻意不推通知——点按钮的人正看着结果。
* 默认关闭,间隔默认一天。

Dockerfile 显式装 git(python:slim 不带,而这是唯一的依赖);deploy.sh 顺带补上
一个真 bug 的提示:Dockerfile/requirements.txt 变了只 up -d 用的还是旧镜像。
2026-10-10 09:17:06 +08:00

166 lines
8.0 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.
# 与上游的差异管理
本仓库在 [NanmiCoder/MediaCrawler](https://github.com/NanmiCoder/MediaCrawler) 之上加了一层
监控/鉴权/多平台面板。这份文档记录**改了上游哪些文件、为什么**,以及**上游更新时怎么合并**。
---
## 一、改动分三类
冲突风险从低到高:
### 1. 纯新增文件(零冲突)
上游怎么改都不会碰到它们:
```
api/auth.py WebUI 登录鉴权
api/monitor/* 监控层整体(含 platforms.py 能力矩阵)
api/monitor/db.py MySQL 连接层(可回退 SQLite 供测试用)
api/monitor/migrate_from_sqlite.py SQLite → MySQL 一次性迁移脚本
api/monitor/upstream.py 上游更新检查(定时 fetch 上游并比对)
api/routers/{auth,monitor,settings}.py
api/schemas/{auth,monitor,settings}.py
api/services/interpreter.py 解释器探测(uv / .venv / 当前解释器)
webui/src/components/{monitor,settings,auth}/ 新视图
webui/src/components/layout/{PlatformSwitcher,UnwiredPlatformNotice}.tsx
webui/src/{hooks/useMonitor.ts,hooks/usePlatform.ts,store/platformStore.ts,lib/monitorFormat.ts,types/monitor.ts}
docs/监控功能使用说明.md
tests/test_{auth,settings,platforms,qrlogin,monitor_*,upstream}.py
Dockerfile / .dockerignore / docker-compose.yml 服务器部署用
```
> `api/monitor/upstream.py` 要调 `git`,而 `python:3.11-slim` 不带它 —— Dockerfile 里为此
> **显式装了 git**。改了 Dockerfile 就必须重建镜像(`docker compose build`),`./deploy.sh`
> 只重建前端,不重建镜像。
其中 `api/monitor/qrlogin.py` + `webui/.../QrLoginPanel.tsx` 是**服务器专用**的扫码登录:
那台机器上 Chrome 跑在 Xvfb 里,`show_qrcode` 调的 PIL `Image.show()` 需要桌面看图程序,
服务器没有,二维码会无处可去。所以改成用 CDP 把二维码从页面里读出来交给前端 `<img>` 显示。
### 2. 加法改动(低冲突)
只在既有文件里**新增**内容,不改动原有行:
| 文件 | 加了什么 |
|---|---|
| `cmd_arg/arg.py` | typer 选项:`--enable_cdp_mode`、`--inject_all_cookies`、`--save_login_state`、`--cookies_file`、`--crawler_max_sleep_sec`,以及对应的 `config.*` 回写 |
| `api/schemas/crawler.py` | `CrawlerStartRequest` 的若干**可选**字段(默认 `None`,不传则不加对应 CLI 参数) |
| `config/base_config.py` | `INJECT_ALL_COOKIES = False` |
| `api/routers/__init__.py` | 导出新增的 router |
| `requirements.txt` | 补上 `websockets`(上游 `pyproject.toml` 里有、`requirements.txt` 里漏了) |
| `tests/conftest.py` | 新增 `_bypass_auth_for_non_auth_suites` fixture |
### 3. 接线改动(中冲突,需要人看)
| 文件 | 改了什么 | 上游若在此处变动 |
|---|---|---|
| `api/main.py` | 注册 4 个 router 并加 `Depends(require_auth)`;`lifespan` 里初始化监控库、启动调度器、跑设置键迁移;`load_dotenv`;CORS 可配;`docs/redoc/openapi` 关闭;监听地址改 env | **最需要人工合并的文件**。留意 router 注册块、lifespan、`__main__` |
| `api/routers/websocket.py` | 两个 WS 路由加 `dependencies=[Depends(require_ws_auth)]` | 上游若新增 WS 路由,**必须同样加上**,否则那条流是裸奔的 |
| `api/services/crawler_manager.py` | 解释器探测替换硬编码 `uv run`;`_build_command` 转发新增参数;新增 `is_busy()` / `run_and_wait()` 与完成事件 | 留意 `_build_command` 的参数拼装 |
| `media_platform/xhs/login.py` | `login_by_cookies` 在 `INJECT_ALL_COOKIES` 打开时注入**全部** cookie(默认关闭,行为不变) | 小改动,好合并 |
### 4. 上游 bug 修复(建议回馈上游)
| 文件 | 修的问题 |
|---|---|
| `media_platform/xhs/core.py` | 见下节 |
| `media_platform/xhs/login.py` | 同上(cookie 加固) |
---
## 二、应该给上游提 PR 的两个修复
这两处是**上游自身的缺陷**,提上去以后就不用自己背着:
### 1. 博主主页抓取失败会跳掉整个博主(`xhs/core.py`)
`get_creator_info()` 抓主页 HTML 解析 `window.__INITIAL_STATE__`,解析失败抛 `JSONDecodeError`——
它是 `ValueError` 的子类,被 `except ValueError` 误捕获,日志报成
"Failed to parse creator URL"(**误导**,URL 根本没解析错),然后 `continue` **跳过整个博主**。
而那份资料只喂给 `save_creator()`,它在教学版里是**空函数**。也就是说:
一个喂给空函数的抓取失败,让真正要抓的作品一条都没抓到,表现为"0 篇作品",
和"登录失效"长得一模一样。
修复:把资料抓取改成**尽力而为**,失败只警告、继续抓作品。
### 2. cookie 登录只注入 `web_session`(`xhs/login.py`)
`a1` / `webId` 等签名所需 cookie 只能靠持久化 profile 补,冷启动时签名会失败。
默认行为保持不变,用 `INJECT_ALL_COOKIES` 开关控制。
---
## 三、上游更新时怎么操作
### 先让机器替你盯着
「上游更新检查」(`api/monitor/upstream.py`,开关在 WebUI 的**系统设置 → 上游更新**)会按
间隔 `git fetch` 上游、算出落后几个提交,有更新就推企业微信。它是这份文档的自动化版:
没有它,「上游动了」这件事只取决于谁偶尔想起来去 fetch 一次。
两个细节决定了它为什么是安全的:它只 fetch 到 `FETCH_HEAD`,**不写工作区、不建 remote、不碰
`refs/remotes`**,所以和正在跑的采集、和下面的 `git pull` 都不冲突;默认**关闭**,因为要联网,
且需要镜像里有 git。
### 日常流程
```bash
git stash # 或先 commit 到自己的分支(推荐)
git fetch origin main
git rebase origin/main # 冲突只会出现在上表第 3、4 类文件里
./.venv/Scripts/python.exe -m pytest tests/ -q # 502 个测试就是回归网
```
### 直连 GitHub 不通时(本机常见)
本机到 `github.com` 时通时不通,**大包传输必断**(`Recv failure: Connection was reset`
或 `unexpected disconnect while reading sideband packet`),所以 `git clone` / `--unshallow`
这类一次性拉全量的操作基本必失败。可用的替代源:
```bash
# gitcode 的 GitHub 镜像,国内直连,比 GitHub 本身还新一天以内
git remote add gitcode https://gitcode.com/gh_mirrors/me/MediaCrawler.git
git fetch --no-tags --unshallow gitcode # 本仓库当初就是这样补全历史的,约 2 秒
```
注意 `git fetch` 只写 `refs/remotes/*`,**不会动本地 `main`**;
但拉镜像会把 `upstream/main` 指到镜像的 tip(可能比 GitHub 晚一天),
等 GitHub 通了再 `git fetch upstream` 正回来即可。
### 强烈建议:先把改动提交掉
当前状态是**未提交**的(25 个上游文件被改 + 31 个新文件)。在 `main` 分支上裸着工作区,
一次 `git checkout .` 就全没了,而且没法 rebase。
```bash
git checkout -b local/monitor-panel
git add -A && git commit -m "监控面板 / 鉴权 / 多平台"
```
### 如果改动持续增长:fork
把本仓库 fork 到自己名下,上游设为 remote:
```bash
git remote rename origin upstream
git remote add origin <你的 fork>
git push -u origin local/monitor-panel
```
之后同步上游用 `git fetch upstream && git rebase upstream/main`。
---
## 四、合并时最容易忘的三件事
1. **新增的 `/api` 路由必须带鉴权**。跑一下 `tests/test_auth.py`——
里面有个测试会遍历 `app.routes`,断言除豁免集外每个 `/api` 路由无凭据都返回 401。
上游新增接口忘了加鉴权,这个测试会直接失败。
2. **新增的 WebSocket 路由必须加 `require_ws_auth`**。
`BaseHTTPMiddleware` 对 WS 完全不生效(`scope["type"] != "http"` 直接放行),
只靠中间件会漏。同样有测试守着。
3. **上游若改动 `AsyncFileWriter` 的输出路径规则**,`api/monitor/ingest.py::find_run_files`
会跟着失效——它靠 glob `{out_dir}/{platform}/jsonl/*_contents_*.jsonl` 定位每轮的产物。