Files
MediaCrawler/docs/监控功能使用说明.md
T
butubb 06718a1351
Deploy VitePress site to Pages / build (push) Canceled after 0s
Deploy VitePress site to Pages / Deploy (push) Canceled after 0s
feat(monitor): 抖音接入博主监控
上游爬虫本身不缺抖音能力(三模式、四项指标、二级评论都与小红书对等、指标还是同名同列),
缺的全在监控层的适配。这次把「平台之间不一样」的管子集中到一个新模块,再把散落的
xhs 硬编码接上去。

* 新增 api/monitor/adapters.py:产物目录名、jsonl 字段别名、目标链接形态与正则、
  通知链接模板。不放进 platforms.py 是因为那个模块被 describe_all() 整个序列化进
  /api/config/platforms 交给前端,塞进正则和目录名会让爬虫内部细节漏进 API 载荷。
  代价是两个注册表可能漂移,用一条测试钉住「声明接通就必须有适配器」。
* 两个必须知道的坑,都在这版里处理掉了:
  1) 抖音的平台 id 是 dy,而 store 把产物写在 douyin/ 下(store/douyin/_store_impl.py:47)。
     不改就是 ingest 一个文件都读不到 —— 不报错,只是 0 条,然后被冒充成「疑似登录失效」。
  2) 抖音的作品没有 note_id(叫 aweme_id)、评论也用 aweme_id 指作品。ingest 第一步是
     `if not note_id: continue`,不映射就逐条全丢。
  另外抖音顶层评论的 parent_comment_id 是字符串 "0",归一成空串,免得前端多出悬空的父节点。
* 顺带把「东西抓到了、只是没落在期望目录里」单独识别出来。这类故障的现象和登录失效
  一模一样,按登录失效报会把人指去查完全错误的方向。
* 修两个既有 bug(今天只有小红书所以无害,加抖音就踩响):
  - service.py update_task 换目标时漏传 task.platform,回落到默认小红书
  - scheduler.py 取 cookie 没传 platform,抖音任务会读着小红书那份 cookie 不动
* 行为变更(已与用户确认):cookie 闸门改成「没 cookie 且没开 CDP」才跳过。
  CDP 模式下登录态来自被接管的浏览器,粘不粘 cookie 由不得它决定;不放行的话,
  选了「接管已有 Chrome」却没粘 cookie 的用户会看到任务永远不触发,而且不报错。
  副作用是开启了 CDP 的小红书任务也不再被该闸门拦住 —— 语义上是对的。
* 目标输入框的示例链接与措辞改由能力矩阵提供(notes_label 抖音说「作品」、小红书说
  「笔记」;「建议只填纯 ID」是小红书专属劝告,抖音链接不带令牌,不再显示)。

测试 +22 条(858 通过),其中最关键的是「抖音作品/评论不被静默丢弃」与「产物目录名
不等于平台 id」两条 —— 都是把最难查的失败模式钉死在回归网里。

注意:抖音这条路的**端到端尚未验证**,需要一份可用的抖音登录态(CDP 那台 Chrome 里
登录,或导出一份 cookie)。单测覆盖的是解析与入库,真实抓取还没跑过。
2026-10-10 14:55:28 +08:00

528 lines
25 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.
# 小红书监控功能使用说明
> 定时重复采集一批博主或笔记,与上一轮快照对比,产出**新增作品 / 新增评论 / 点赞收藏评论数涨跌**。
本功能是在 MediaCrawler 之上新增的一层,代码集中在 `api/monitor/`,不侵入原有的
`media_platform/`、`store/` 等目录。
---
## 一、为什么需要单独一层
原项目是**一次性采集**:跑完即退出,没有调度、没有历史、没有差分。直接复用会遇到三个硬伤:
1. **指标会被覆盖**。`store/xhs/_store_impl.py::XhsDbStoreImplement.update_content()` 对已存在的笔记执行
`UPDATE ... SET liked_count = ...`,历史值直接丢失。跑第二遍根本看不出"点赞从 100 涨到了 500"。
2. **单进程串行**。`api/services/crawler_manager.py` 是全局单例,同一时刻只能跑一个 `main.py` 子进程。
3. **运行输出无法区分**。`AsyncFileWriter` 的文件名只带日期(`creator_contents_2026-10-07.jsonl`),
同一天多次运行会追加进同一个文件。
监控层为此做了对应处理:独立的快照库(保留历史)、调度器与手动采集互斥排队、以及**每轮采集写入独立目录**
(复用早已存在、但 API 层从未转发的 `--save_data_path` 参数)。
---
## 二、快速开始
### 1. 准备环境
```bash
# 依赖(若未安装 uv,本项目的解释器探测会自动回退到 .venv)
python -m venv .venv
.venv/Scripts/python -m pip install -r requirements.txt
.venv/Scripts/python -m playwright install chromium # 非 CDP 模式必需
# 前端
cd webui && npm install && npm run build
```
### 2. 启动
```bash
.venv/Scripts/python -m api.main # 或 uvicorn api.main:app --port 8080
```
打开 <http://localhost:8080>,右上角切换到「监控」。
> 解释器探测顺序:`uv`(若在 PATH)→ 项目 `.venv` → 当前解释器。
> `/api/env/check` 使用同一套逻辑,不会出现"检测失败但其实能跑"的情况。
### 3. 配置登录态(**无人值守的前提**)
在监控页左下角「小红书登录态」粘贴 Cookie。定时监控不能每次都扫码,必须持久化登录态。
> **强烈建议先手动扫码登录一次**,以播种 `browser_data/xhs_user_data_dir`,
> 之后再粘贴 Cookie 才可靠。原因见下方「限制」。
### 4. 新建监控任务
- **类型**
- `博主`:监控其作品,填博主主页链接或纯 ID
- `笔记`:批量监控指定内容,填笔记链接或纯 ID
- **目标**:每行一个。**建议只填纯 ID** —— 链接里的 `xsec_token` 会过期,纯 ID 永久有效。
- **间隔**:最小 30 分钟。每次运行都要拉起一次浏览器并多次请求平台,间隔过短容易触发风控。
- **每篇评论抓取条数**:默认 50。这个值直接决定能发现多少新评论,见下方限制。
任务创建后立即生效,也可随时点「立即运行」手动触发一轮。
---
## 二·五、报表
「报表」视图是**跨任务**的统计,用来回答"这批账号这段时间表现如何"。
**筛选**:勾选参与统计的任务(默认全选),选日期区间(或点「近 7/30/90 天」)。
**两类指标,含义不同,所以分列展示**:
| 列 | 含义 |
|---|---|
| 新增作品 / 新增评论 | 该日**首次发现**的作品数 / 评论条数 |
| 点赞 Δ / 评论 Δ / 收藏 Δ / 分享 Δ | 该日**互动增量**:Σ(当日末值 − 当日之前最后一次采到的值) |
增量的口径有两个要点:
- **作品首次出现的那天从 0 起算**,所以新作品的全部点赞都计入其首次发现日。这样做是为了让"新作品带来了多少赞"这件事可见,而不是把它的既有数据丢掉。
- **某天没采到某篇作品,那天的增量算 0**,不会把跨天的增长平摊到每一天。
底部会标明两件事:一是**哪些指标无法解析**(小红书可能返回 `"1.2万"` 这类值,解析失败的不会被当成 0 计入,否则会伪造出一个大的负增长),二是评论数受接口限制只覆盖前 N 条。
> 实现上聚合是在 Python 里做的,不是一条大 SQL。原因:按笔记、按天的"上一个基线值"查询是窗口操作,SQLite 表达起来很别扭,而这里的数据量很小,可读性比压榨查询计划更值钱。
---
## 二·六、企业微信通知
在「监控」视图左下角配置 Webhook 地址(企业微信群 → 添加群机器人 → 复制 Webhook 地址)。
**两个设计取舍**:
1. **一轮只发一条汇总**,不是每条事件发一条。一次跑出 20 篇新作品时,你收到的是"新增作品 20 篇"加前 10 条标题,而不是 20 条消息。
2. **推送失败绝不影响采集**。通知是在数据提交之后、用独立会话发送的,任何网络错误只记日志。爬虫跑成功了不会因为 webhook 挂了而被回滚。
**触发时机**(仅这两类):
- 任务失败 / 疑似登录态失效
- 发现新增作品
指标变化和新增评论**不会**推送(指标变化太频繁,评论量可能很大)。
**任务范围**:每个任务在编辑弹窗里有「推送企业微信通知」开关,**默认关闭**。这样一个 webhook 不会被一堆无关任务刷屏。
- 配置好地址后可以点「发测试」验证,也可以「保存前先测」。
- 地址里的 key 等同凭据,**服务端只回传打码形式**,要换只能重新粘贴(和 Cookie 一致)。
- 任务卡片上的 `last_notified_at`(列表接口会返回)可以回答"为什么这轮没收到推送"。
---
## 二·七、评论视图与导出
「评论」页默认**按作品分组**:每篇作品一个可折叠区块,**默认只展开最新的一组**,
避免打开就是一屏文字。切到「平铺」则是一条流,每条评论下方标注它属于哪篇作品
(封面缩略图 + 标题 + 跳原文链接)。
顶部可按作品筛选,选项里带每篇的评论数:
```
全部作品
烤面筋热量计算 (33)
孜卷热量计算 (3)
```
> 评论与作品的关联是后端 JOIN 出来的(`note_title` / `note_cover` / `note_url`),
> 因为评论表本身只存 `note_id`,光看 ID 没有任何可读性。
### 导出
评论页和报表页都有「导出」按钮,走浏览器下载:
| 端点 | 内容 |
|---|---|
| `?kind=notes` | 作品表(含互动增量列) |
| `?kind=comments` | 评论(含所属作品标题) |
| `?kind=report` | 报表按天汇总 |
- 支持 `csv` 与 `xlsx`
- **CSV 带 UTF-8 BOM**(`utf-8-sig`)—— 否则 Excel 打开中文是乱码,这是最常见的投诉
- 下载是**页面导航**(`window.open`),带不了自定义请求头,所以导出依赖 Cookie 鉴权 ——
这也是会话必须存在 Cookie 里的原因之一
---
## 二·八、登录与访问控制
面板默认要求登录 —— `/api` 下的所有接口都需要会话,只有 `/api/health`、
`/api/auth/login`、`/api/auth/logout` 例外。静态资源(页面本身、JS/CSS)不受限制,
否则登录页自己都加载不出来。
### 首次启动
自动生成一个随机密码并**打印在启动日志里**(只打印一次):
```
====================================================================
WebUI 首次启动,已生成登录密码:
94Shn1fMa7dV0jqF
请立即登录并修改。
====================================================================
```
> 刻意**不做**"打开页面让你设置密码"的流程。在局域网监听下,任何能访问到的人
> 都能抢先设置密码成为管理员;自动生成 + 打印避免了这种抢占,也避免了把自己锁在外面。
### 忘记密码
设置环境变量 `MC_PASSWORD` 后重启即可:
```bash
MC_PASSWORD=我的新密码 # Linux/macOS
set MC_PASSWORD=我的新密码 # Windows cmd
```
该变量**优先级始终高于**数据库里的密码,且**不会被写入磁盘**。登录后到设置页改成正式密码即可。
### 环境变量
配置写在项目根目录的 `.env`(已被 gitignore)。
| 变量 | 默认 | 说明 |
|---|---|---|
| `MC_HOST` | `127.0.0.1` | 监听地址。**要局域网访问须设为 `0.0.0.0`** |
| `MC_PORT` | `8080` | 端口 |
| `MC_PASSWORD` | 空 | 覆盖数据库密码,忘记密码时的恢复通道 |
| `MC_COOKIE_SECURE` | 关 | **面板走 HTTPS 时才开**。局域网明文下开启会导致浏览器丢弃 Cookie,表现为**登录页反复刷新且无任何报错** |
| `MC_SESSION_TTL_HOURS` | `336` | 登录有效期(14 天) |
| `MC_TRUST_PROXY` | 关 | 仅在受信任的反向代理之后开启,否则 `X-Forwarded-For` 可被伪造以绕过登录节流 |
| `MC_CORS_ORIGINS` | 空 | 附加的允许来源,逗号分隔 |
| `MC_CORS_ORIGIN_REGEX` | 空 | 允许来源的正则,用于局域网里的 Vite 开发服务器 |
> 本项目的 `.env` 此前**从未被加载过**(代码里没有任何 `load_dotenv` 调用,尽管
> `python-dotenv` 一直是依赖、`.env.example` 也一直在仓库里)。现已修复。
### 安全边界(请务必了解)
- **局域网是明文 HTTP**,所以 Cookie 没开 `Secure`,`SameSite=Lax`。
这意味着**同网段抓包能看到会话令牌**。安全边界是"内网 + 密码",不是传输加密。
- **超出可信网络之外请走 HTTPS 反向代理**,不要把本服务直接暴露到公网。
- **登录节流是进程内的**:重启即清零。单用户单 worker 场景足够;
若日后多 worker,节流会按 worker 各算各的。反向代理下 `request.client.host` 是代理地址,
需配合 `MC_TRUST_PROXY` 才能正确识别来源。
- `/docs`、`/redoc`、`/openapi.json` **已关闭** —— 它们默认不鉴权,等于免费公开整个 API 地图。
### 会话与登出
- 会话存在服务端(`auth_session` 表),库里只存令牌的 **SHA-256**,不存令牌本身
- 退出登录、**修改密码**都会立即失效(改密码会踢掉所有设备,并给当前设备补发一个新会话)
- 令牌可放在 Cookie(浏览器自动携带,WebSocket 与文件下载都依赖它)
或 `Authorization: Bearer`(方便脚本调用)
---
## 二·九、设置页
原先挤在监控页左下角的 Cookie 与 Webhook 面板已迁到这里,并补齐了采集策略、代理与账号安全。
### 分区与生效方式
| 分区 | 内容 | 生效时机 |
|---|---|---|
| 登录态 | 小红书 Cookie | 下一轮采集 |
| 通知 | 企业微信 Webhook | 下一条推送 |
| 采集策略 | 新任务默认间隔、默认单轮上限、默认评论条数 | **仅影响新建任务** |
| 采集策略 | 请求间隔、抓二级评论 | 下一轮采集 |
| 采集策略 | 活跃时段 | 定时任务的下一次触发 |
| 代理 | 开关、提供方、池大小、静态地址 | 下一轮采集 |
| 账号安全 | 修改密码 | 立即(其他设备全部掉线) |
**活跃时段**:只在此时段内触发定时采集,窗口外任务保持到期状态、不会丢失,
窗口一开照常执行。默认 `0–23` 即全天;也支持跨午夜(如 `22–6`)。
**"仅影响新建任务"** 的那几项是刻意的:改了默认间隔不应该把已有任务的间隔一起改掉。
### 设计要点
- **敏感值永不回传**:Cookie 和 Webhook 的 `GET` 只返回「是否已配置」与长度,不返回值。
表单不会把没动过的敏感项覆盖掉。
- **部分更新**:只有请求里出现的 key 会被写入。表单一角改动不会清空其他设置。
- **设置项由后端声明**:`api/monitor/app_settings.py` 里的注册表(类型、范围、选项、默认值)
是唯一事实来源,前端**按它生成表单**。加一个设置项不需要改前端字段清单。
- **越界即拒绝**:超出范围、未知的 key、非法的枚举值都返回 400 而不是静默接受。
> 「扫码登录」入口**尚未实现**。它需要跑起爬虫子进程、捕获二维码并实时推流,
> 属于一个独立功能而非设置项,这里不做一个半成品。
---
## 二·十、平台切换与能力矩阵
**右上角的下拉框统一切换平台**,「采集 / 监控 / 报表 / 设置」全部跟着变。选择会记住,
刷新后不会跳回小红书。采集页原来那个平台下拉已移除,避免出现两个事实来源。
### 已接通 vs 未接通
矩阵里有两个**不同**的概念,混淆会误导:
| 字段 | 含义 |
|---|---|
| `crawler_modes` / `metrics` / `comment_levels` / `media` | **上游爬虫模块**能做什么 |
| `monitor_wired` | **监控层**是否已接线 |
**7 个平台的爬虫模块都实现了 search / detail / creator**,真正的差异在指标上:
| 平台 | 指标 | 评论层级 | 媒体 | 监控接线 |
|---|---|---|---|---|
| 小红书 | 点赞 / 评论 / 收藏 / 分享 | 2 | ✅ | ✅ |
| 抖音 | 点赞 / 评论 / 收藏 / 分享(**无播放量**) | 2 | ✅ | ❌ |
| 快手 | 点赞 / 播放(无评论、分享、收藏) | 1 | ✅ | ❌ |
| B站 | 点赞 / **播放** / **弹幕** / 评论 / 收藏 / 投币 / 分享(最全) | 2 | ✅ | ❌ |
| 微博 | 点赞 / 评论 / 转发(无收藏) | 2 | ✅ | ❌ |
| 贴吧 | 仅回复数 | 2 | ❌ | ❌ |
| 知乎 | 赞同 / 评论 | 2 | ❌ | ❌ |
> **要更正一个常见误解**:这个代码库里**抖音不存播放量**(只映射点赞/收藏/评论/分享)。
> 有播放量的是 **B 站**,它还有弹幕。
未接通的平台**可以选,但各页会显示明确的说明面板**,并且**创建任务会被直接拒绝**:
```
400 B站的爬虫已支持,但监控层尚未接通,暂时无法创建监控任务。
```
而不是接受任务、然后让它永远跑不出数据 —— 那正是之前"博主主页解析失败被误报成登录失效"的同一种静默故障。
### 设置的两层
| 位置 | 范围 | 内容 |
|---|---|---|
| 左侧导航「设置」 | **按平台** | 登录 Cookie、采集策略、代理 |
| 右上角「系统设置」 | **全局** | 通知、活跃时段、上游更新、账号安全 |
**这不是随便分的**:企业微信只有一个群、调度器只有一套时段规则、密码只有一份 ——
把它们放进"小红书专属"的页面里,会让人以为它们是按平台存的。
存储上键名带作用域前缀:`platform.<平台>.<项>` 与 `system.<项>`。
**旧键会在启动时自动迁移**(`xhs_cookie` → `platform.xhs.cookie`),
且是幂等的:新键已存在时以新键为准,不会覆盖你后来改的值。
---
## 二·十一、上游更新检查
本仓库在 [NanmiCoder/MediaCrawler](https://github.com/NanmiCoder/MediaCrawler) 之上加了一整层
(监控 / 鉴权 / 多平台面板),差异管理与合并流程在根目录 `UPSTREAM.md` 里。但那份流程有个
隐含前提:**得有人知道上游动了**。部署脚本只从我们自己的 Gitea `git pull`,上游的提交不主动
去 fetch 就永远看不见 —— 拖着不合并的代价是复利的,越久越难合。
这一项就是替你定时去 fetch 的:按间隔(默认每天一次)拉一次上游,算出「当前部署落后几个
提交」,有更新就推一条企业微信,并把结果与提交列表显示在**右上角「系统设置」→「上游更新」**。
### 配置
| 项 | 默认 | 说明 |
|---|---|---|
| 检查上游仓库更新 | **关** | 总开关。默认关:它要联网 fetch,且需要容器里有 git(见下) |
| 上游检查间隔(分钟) | 1440 | 每天一次。最小 30 分钟 |
| 上游仓库地址 | GitHub 上游 | 国内直连 GitHub 不稳时改成 gitcode 镜像,见 `UPSTREAM.md` |
| 上游分支 | `main` | |
| 上游有更新时推送通知 | 开 | 只在出现**此前没推过**的提交时发一条,同一个更新不会反复推 |
### 几个刻意的行为
- **只读,不写工作区**:只 `git fetch <地址> <分支>` 到 `FETCH_HEAD` —— 不建 remote、不写
`refs/remotes`、不碰索引与工作区。所以它不会打断正在跑的采集,也不会和 `./deploy.sh`
的 `git pull` 抢锁。
- **不受活跃时段限制**:活跃时段是给采集定的(避免半夜去抓平台)。检查只是 fetch 一个公开
仓库,半夜跑反而更合适。
- **失败也是一种结果**:上游不通(尤其直连 GitHub)很常见。界面会显示失败原因与上次检查
时间,失败不推送,也**不会**因此改变下一次检查的时间 —— 每个间隔重试一次,而不是每个
调度 tick(20 秒)都去撞一次。
- **同一个更新只推一次**:推送状态记的是上游 tip。推过之后,上下游没动就不会再推;上游又
有新提交(tip 变了)时会再推一条。
- **「立即检查」不发通知**:点这个按钮的人正看着结果,没必要再给自己推一条群消息。那次
检查只写结果,没推的那批提交留给下一次定时检查推。
### 部署前提:镜像里要有 git
`python:3.11-slim` 不带 git,`Dockerfile` 里已显式安装。**因此这次更新需要重建镜像**:
```bash
docker compose build && ./deploy.sh
```
`./deploy.sh` 只重建前端,不会重建镜像。漏了这步的话,检查会报「未找到 git 命令」——
界面上看得见,不会静默。
---
## 三、必须知道的限制
### 1. 「新增评论」是近似值 —— 最重要的一条
小红书评论接口 `/api/sns/web/v2/comment/page` **没有排序参数**,只能拿到平台默认排序(热评优先)的
前 N 条。因此:
- 我们只能"每次抓前 N 条做差集",**新发布但沉底的评论不会被发现**
- N 调大能提高发现率,但请求量线性增长,风控风险上升
- 评论事件区分两种,UI 上也分别标注:
- `new_comment_posted`(新评论):`create_time` 晚于上一轮开始时间,是真·新发布
- `new_comment_seen`(新出现评论):只是本轮才进入可见窗口的历史评论
**这条限制无法通过调参绕过**,是该接口的固有限制。
### 2. Cookie 失效是「静默失败」
`login_by_cookies()` 只注入 `web_session`,而 API 签名还需要 `a1` / `webId` 等;
更麻烦的是**cookie 登录不做任何校验** —— 坏 Cookie 不会让进程报错退出,而是
**退出码 0、抓到 0 条**。
监控层因此把「退出码 0 且 0 条作品」判定为 `suspected_auth_failure` 并在 UI 上标红,
而不是当成"该博主没发新作品"。这是无人值守场景最容易误报的地方。
本实现额外做了两件事:
- 通过 `--inject_all_cookies` 注入**完整** Cookie(默认关闭,保持上游行为不变)
- 通过 `--cookies_file` 传 Cookie,避免明文出现在进程列表里
### 3. 作品窗口被截断
`每轮最多采集作品数`(默认 20)限定了"该博主的作品"到底指多少条。
UI 会把该上限显示在作品表旁,避免误以为看到了全部。
### 4. 昵称与用户 ID 已被上游脱敏
`store/xhs/__init__.py` 落库前调用 `mask_nickname()` 与 `anonymize_user_id()`,
存储的是**打码昵称**与哈希后的 `creator_hash`,没有真实昵称和 user_id。
这是上游的隐私保护设计,监控层未做改动。
### 5. 不发「笔记被删」事件
`creator` 模式只取前 N 条,笔记"消失"多半只是掉出窗口;`detail` 模式遇到
`xsec_token` 过期也会失败。二者与"真被删"无法区分,因此不产生删除事件,
改为在作品表里展示 `last_seen_at`。
### 6. 定时任务与手动采集互斥
二者共用同一个爬虫子进程。监控任务运行期间点「采集」会被拒绝(返回 400);
反之若有手动采集在跑,到期的监控任务会**保持到期状态排队**,不会丢失,空闲后自动补上。
---
## 四、数据存放
| 内容 | 位置 |
|---|---|
| 监控库(任务/快照/事件/评论/设置/会话) | **MySQL**,库名由 `MYSQL_DB_NAME` 指定 |
| 每轮原始 jsonl | `data/monitor_runs/{task_id}/{run_id}/{platform}/jsonl/` |
> 爬虫每轮的原始产出**仍然写独立 jsonl 目录**,不进 MySQL。
> 这是差分机制的基础:每轮写在单独目录里,才能算出"这轮新增了什么"。
> 多轮数据混在同一批表里的话,这个判断就做不到了。
### MySQL 配置与安全边界
连接信息写在 `.env`(已被 gitignore,不会进版本库):
```ini
MYSQL_DB_HOST=<数据库地址>
MYSQL_DB_PORT=3306
MYSQL_DB_USER=<账号>
MYSQL_DB_PWD=<密码>
MYSQL_DB_NAME=mediacrawler
```
> 真实凭据只写在 `.env` 里(已被 gitignore),**不要写进这个文档或任何会提交的文件**。
**"只操作这个库"由两层保证,缺一不可**:
1. **数据库授权(真正的保证)**。账号应只被授予目标库的权限:
```sql
REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'MediaCrawler'@'%';
GRANT ALL PRIVILEGES ON `mediacrawler`.* TO 'MediaCrawler'@'%';
FLUSH PRIVILEGES;
```
这样该账号 `SHOW DATABASES` 只能看到目标库,**代码就算写错也碰不到别的库**。
2. **启动自检(防配置写错)**。应用启动时会执行 `SELECT DATABASE()`,
与 `MYSQL_DB_NAME` 不符就**拒绝启动**,而不是往错误的库里写。
**字符集**:这台服务的服务端和库默认都是 `latin1`。代码在建表时**逐表强制
`utf8mb4`**,不依赖库默认值 —— 否则中文会被拒或变成问号。
**连接保活**:MySQL 默认 8 小时断开空闲连接,而监控服务是常驻的。
已配置 `pool_recycle=3600` + `pool_pre_ping`,避免"server has gone away"。
**表引擎**:全部 InnoDB(`monitor_run.exit_code` 用 `BIGINT` —— Windows 的退出码是
无符号 32 位,`0xC0000142` 会溢出有符号 `INT`)。
### 从 SQLite 迁移(如有旧数据)
```bash
python -m api.monitor.migrate_from_sqlite --dry-run # 先看要迁什么
python -m api.monitor.migrate_from_sqlite # 正式迁移
```
保留原主键(否则 `task_id` 关联会错位);目标库非空时会拒绝执行,除非加 `--force`。
监控库中的 Cookie 为明文存储,这是当前版本的已知取舍。
---
## 五、API
所有操作都有对应的 HTTP 接口,UI 只是其中一层封装:
```
GET /api/monitor/overview 看板汇总
GET /api/monitor/tasks 任务列表
POST /api/monitor/tasks 新建任务
PATCH /api/monitor/tasks/{id} 修改
DELETE /api/monitor/tasks/{id} 删除
POST /api/monitor/tasks/{id}/run 立即运行(后台执行,立即返回)
GET /api/monitor/tasks/{id}/runs 运行历史
GET /api/monitor/notes 作品表(含与上一轮的 Δ)
GET /api/monitor/notes/{id}/series 单篇指标时间序列
GET /api/monitor/comments 评论流(带所属作品;?note_id= 筛选,?group_by=note 按作品分组)
GET /api/monitor/comment-notes 有评论的作品及其条数(评论筛选下拉用)
GET /api/monitor/export 导出(?kind=notes|comments|report&format=csv|xlsx)
GET /api/monitor/events 变化事件流
POST /api/monitor/events/read 标记已读
GET /api/monitor/cookie 登录态健康度(**不返回 Cookie 值**)
POST /api/monitor/cookie 保存 Cookie
DELETE /api/monitor/cookie 清除 Cookie
GET /api/config/platforms 平台能力矩阵(含 monitor_wired,前端据此渲染切换器)
GET /api/settings 设置 + 表单描述(?platform=,敏感值只回状态)
PUT /api/settings 部分更新(只写请求里出现的 key)
GET /api/auth/me 身份探测(401 即未登录)
POST /api/auth/login 登录(发 HttpOnly Cookie)
POST /api/auth/logout 退出
POST /api/auth/password 修改密码(踢掉所有其他设备)
GET /api/monitor/report 报表(?task_id=1&task_id=2&start_date=&end_date=)
GET /api/monitor/webhook 通知配置状态(**只返回打码地址**)
POST /api/monitor/webhook 保存 Webhook 地址
DELETE /api/monitor/webhook 删除 Webhook
POST /api/monitor/webhook/test 发送测试消息
GET /api/monitor/upstream 最近一次上游检查的缓存结果(没查过返回 {})
POST /api/monitor/upstream/check 立刻检查一次(等 fetch 跑完才返回,**不发通知**)
```
> `task_id` 用**重复参数**而非逗号拼接(`?task_id=1&task_id=2`);不传表示统计全部任务。
---
## 六、故障排查
| 现象 | 原因 / 处理 |
|---|---|
| 任务一直不运行 | 未配置 Cookie(调度器会跳过并保持任务到期);或全局已有采集在跑 |
| 任务标红「疑似登录态失效」 | Cookie 过期。重新粘贴;若反复失败,先手动扫码登录一次播种浏览器 profile |
| 抓到的作品数长期为 0 | 同上;也可能是该博主确实没有作品 |
| 发现不了新评论 | 评论接口无时间排序所致,调大「每篇评论抓取条数」可缓解但无法根治 |
| 首轮没有任何"新增"事件 | 刻意设计:首轮建立基线,全部数据视为已有,不产生变化事件 |