Files
openapi/docs/部署与运维.md
T
RRRosmontis c6908b5441 fix(bilibili): hk 出口 WAF 412 改用官方 API 直取 + 重建文档补齐
- bilibili.py: view -> playurl -> 媒体段 的 API 直取主路径,绕开页面
  WAF/JS 挑战;yt-dlp 保留为后备;cookie 仅后备路径需要
- docs: 迁移步骤补 /var/cache/nginx 与 ffmpeg 两个重建坑位
- 实测: 公网 200/1.3MB/6.5s
2026-10-22 17:28:12 +08:00

201 lines
16 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.
# 线上部署与运维(hk)
> 记录线上服务器 `hk`(`103.24.218.119:10526`)的实际路径、缓存策略与迁移步骤。
> 架构与代码结构见根目录 [README](../README.md)。
## 对外入口
统一入口:`https://openapi.rrrosmontis.icu`(nginx 监听 80/443)
| 路径 | 链路 |
|---|---|
| `/pixiv/get_image?id=xxx` | `127.0.0.1:9008`(main.py 聚合层)→ `127.0.0.1:9007`(pixiv) |
| `/bilibili/get_video?bv=xxx` | `127.0.0.1:9008`(main.py 聚合层)→ `127.0.0.1:9006`(bilibili) |
| `/dufe/ginkgostu_sign_in` | `127.0.0.1:9008`(main.py 聚合层)→ `127.0.0.1:9005`(dufe 签到码查询,支持 GET\|POST;`&mode=auto` 自动签到) |
| `/jm/get_comic?id=xxx[&range=起-止]` | `127.0.0.1:9008`(main.py 聚合层)→ `127.0.0.1:9004`(jm 漫画下载打包) |
| `/img-master/`、`/img-original/` | nginx 直接反代 `i.pximg.net`(无本地后端) |
| `/` | `127.0.0.1:9008`(302 跳转文章页) |
- 其余在服站点:`sqy.bu4.ren`、`xxx.aeolianchi.me`
- 已删除的 vhost:`kim1.work`、`zxx.aeolianchi.me`(2026-09-12)
- 历史域名 `api.bu4.ren` / `pixiv.bu4.ren` / `apixiv.bu4.ren` 已合并进 `openapi.rrrosmontis.icu`
## 进程与端口
五个服务**全部只绑 `127.0.0.1`**,对外仅 nginx 的 443/80。
| systemd 单元 | 源码 | 监听 | 说明 |
|---|---|---|---|
| `openapi.service` | `source/main.py` | `:9008` | 聚合层(aiohttp,纯转发,唯一入口) |
| `pixiv.service` | `source/pixiv/pixiv.py` | `:9007` | Flask,由 waitress(8 线程)承载 |
| `bilibili.service` | `source/bilibili/bilibili.py` | `:9006` | aiohttp,官方 API 直取 + yt-dlp 后备 |
| `ginkgo_sign_in.service` | `source/dufe/ginkgostu_sign_in/ginkgo_sign_in.py` | `:9005` | 银杏(DUFE)签到码查询,Flask + waitress(16 线程),`GET\|POST` |
| `jm.service` | `source/jm/jm.py` | `:9004` | JM 漫画下载打包,aiohttp + jmcomic |
| `pixiv-token-refresh.service` | `source/pixiv/refresh_token.py` | — | 常驻刷新 Pixiv OAuth token |
| `bilibili-refresh.service` | `source/bilibili/refresh_cookie.py` | — | 常驻刷新 bilibili cookie |
## 目录结构
```
/home/openapi/
├── source/
│ ├── main.py 聚合层源码(2026-09-12 从 source/openapi/ 上移至此)
│ ├── pixiv/
│ │ ├── pixiv.py 图像代理(Flask)
│ │ ├── refresh_token.py token 刷新守护
│ │ └── tokens.json Pixiv OAuth token(敏感,权限 600,不入库)
│ ├── bilibili/
│ │ ├── bilibili.py 视频代理(aiohttp,API 直取 + yt-dlp 后备)
│ │ ├── cookies.txt bilibili cookie(敏感,不入库)
│ │ ├── refresh_cookie.py cookie 刷新守护
│ │ └── qr_login*.py 扫码登录工具(cookie 失效时人工重登用)
│ ├── dufe/
│ │ └── ginkgostu_sign_in/
│ │ └── ginkgo_sign_in.py 银杏(DUFE)签到码查询服务(127.0.0.1:9005,GET|POST)
│ ├── jm/
│ │ └── jm.py JM 漫画下载打包服务(127.0.0.1:9004)
│ └── cache/ 运行时缓存(不入库)
│ ├── pixiv/images pixiv 图片缓存(代码中为 ../cache/pixiv/images)
│ ├── bilibili/ bilibili 视频缓存(上限 2 GiB / TTL 7 天)
│ └── jm/ jm 临时 ZIP(TTL 10 分钟,返回后删除)
├── systemd/ 7 个单元文件的归档副本(与 /etc/systemd/system/ 保持一致)
├── nginx/ 宝塔导出的历史存档,非生效路径
├── cert/ SSL 证书
├── env/ Python 环境(general + env_general.tar.gz 整包)
└── 备份说明.txt 本文档在服务器上的纯文本副本(随本文档同步维护)
/home/images -> /home/openapi/source/cache/pixiv/images 便捷软链
```
## 缓存机制
**pixiv(图片)**:`check_cache()` 以 glob 命中即直接回文件;未命中按 `官方 App API → pixiv.re → pixiv.cat` 三级回退抓取,落盘用 `.tmp` + `os.replace` 原子写。
**bilibili(视频)**:以 BV 号为键缓存到 `source/cache/bilibili/{BV}.mp4`
- 命中即复用、不再下载
- 未命中时优先走 **官方 API 直取**(view → playurl → 媒体段,不受页面 WAF 影响);失败回退 yt-dlp,均写入缓存目录内的隐藏暂存目录 `.dl-XXXX`,成功后原子 rename 落盘
- 无论成功 / 失败 / 超时都 `rmtree` 该暂存目录
(2026-09-12 修复:原实现从不清理 tmpdir,每次取视频都会把完整视频永久留在 `/tmp`,属无上限磁盘泄漏)
- 淘汰策略:单文件 7 天未访问过期 + 总量超过 2 GiB 时按 mtime 从最旧开始删
- 运维接口:`curl -s 127.0.0.1:9006/bilibili/cache_info`
**jm(漫画)**:**非持久临时产物**,移植自 qqbot 的 jm 模块(bj `C:/call/bot/utility/jm.py`)
- `GET /jm/get_comic?id=xxx[&range=起-止]`:jmcomic 下载 → `cache/jm/` 内隐藏暂存目录 `.dl-<id>-XXXX` → webp 统一转 jpg 打包 ZIP(`{标题}_{id}_ch{起}-{止}.zip`)→ HTTP 直返
- 单次上限 30 章(与 qqbot 一致,同时避免超过聚合层 300s 超时);总章节 ≤30 可省略 range 自动全量
- 打包完成即 `rmtree` 图片暂存目录;ZIP 在返回后 600 秒(`ZIP_TTL_SECONDS`)延时删除
- TTL 窗口内同名 ZIP 重复请求直接复用并 touch 重新计时;每次请求附带 `_cache_sweep()` 兜底清理过期 ZIP(防进程重启残留)
- 同一本子串行处理(per-album asyncio.Lock),并发请求同本返回 409
- hk 直连源站,无需代理(jmcomic 自动更新 API 域名);qqbot 侧的代理轮换 / 上传 cf / QQ 回复逻辑均未带入
- 运维接口:`curl -s 127.0.0.1:9004/jm/cache_info`
## nginx
| 文件 | 作用 |
|---|---|
| `/etc/nginx/conf.d/openapi.rrrosmontis.icu.conf` | 统一入口站点(`proxy_pass` → `127.0.0.1:9008`;含按 IP 限流,见下) |
| `/etc/nginx/conf.d/00-limits.conf` | 限流 zone 定义(`perip` 连接数 / `perip_req` 请求速率)+ 429 状态码 |
| `/etc/nginx/conf.d/00-websocket.conf` | `$connection_upgrade` map 定义(**必须存在**) |
| `/home/openapi/nginx/**` | 宝塔导出的历史存档,非生效路径 |
**按 IP 限流(2026-09-23 启用,轻防滥用档)**:域名 DNS 直连本机(无 CDN),`$binary_remote_addr` 即真实客户端 IP,后端全绑 `127.0.0.1` 使 nginx 成为唯一入口、限流无死角。参数:`limit_req_zone ... rate=10r/s` + `burst=20 nodelay`,`limit_conn perip 10`;超出返回 **429**。验证:30 并发请求中部分被拒(429),桶恢复后正常。调整档位改 `/etc/nginx/conf.d/00-limits.conf`(速率)与站点 conf(burst/并发)后 `nginx -t && systemctl reload nginx`。
## Python 环境
- `/home/openapi/env/general` —— 五服务共用(flask / waitress / aiohttp / httpx / yt-dlp / jmcomic / pillow)
- `/home/openapi/env/env_general.tar.gz` —— 环境整包(迁移用)
- 注意:pixiv 现由 **waitress** 承载(`pip install waitress`,纯 Python 线程池),不再使用 Flask 开发服务器
(开发服务器是 thread-per-request 无上限,受控线程池更适合长期运行)
## 迁移到新服务器
1. **环境**:解压 `env/env_general.tar.gz`,直接使用其 `bin/python`(依赖齐全,含 waitress);若整包过旧需补 `pip install jmcomic`;另装系统包 `ffmpeg`(yt-dlp 合流)
2. **源码**:恢复 `source/{main.py,pixiv/,bilibili/,dufe/,jm/}` 到 `/home/openapi/source/`
3. **systemd**:恢复 `systemd/*.service` 到 `/etc/systemd/system/`,`daemon-reload` + `enable` + `start`
4. **nginx**:先建缓存目录 `mkdir -p /var/cache/nginx`(站点 conf 的 `proxy_cache_path` 需要父目录存在,否则 `nginx -t` 报 mkdir 失败),再套用 `nginx/` 下三个文件到 `/etc/nginx/conf.d/`:`openapi.rrrosmontis.icu.conf` + `00-websocket.conf`(`$connection_upgrade` map,**必须**)+ `00-limits.conf`(限流 zone,**必须**,否则站点 conf 的 `limit_req zone=perip_req` 会使 `nginx -t` 失败),再 `nginx -t && systemctl reload nginx`
5. **SSL**:恢复 `cert/<域名>/` 到 `/etc/nginx/cert/<域名>/`
6. **凭据**:放回 `source/pixiv/tokens.json`(600) 与 `source/bilibili/cookies.txt`
7. **软链**:`ln -s /home/openapi/source/cache/pixiv/images /home/images`
8. **验证**:
```bash
curl -k "https://openapi.rrrosmontis.icu/pixiv/get_image?id=xxx"
curl -k "https://openapi.rrrosmontis.icu/bilibili/get_video?bv=BV16a41157Q4"
curl -k "https://openapi.rrrosmontis.icu/dufe/ginkgostu_sign_in" # 无参应返回 400 JSON
curl -k "https://openapi.rrrosmontis.icu/jm/get_comic?id=350234" -o jm.zip # 返回 ZIP
```
## 日志
```
/var/log/openapi.log /var/log/pixiv.log /var/log/bilibili.log
/var/log/ginkgo_sign_in.log /var/log/bilibili_refresh.log /var/log/pixiv_token_refresh.log
/var/log/jm.log
```
缓存命中/落盘会在 `bilibili.log` 打印 `cache HIT / MISS / STORE`(已加 `flush`,实时可见)。
## 变更记录
**2026-10-22(服务器重装重建 + bilibili 出口 WAF 修复)**
- hk 重装为 Ubuntu 22.04,按本文件从零重建:nginx + acme.sh(Let's Encrypt ECC)+ Python venv + 7 个 systemd 单元 + 凭据恢复;补充恢复时的两个坑位:
- `proxy_cache_path /var/cache/nginx/...` 要求父目录存在 → 迁移步骤补 `mkdir -p /var/cache/nginx`
- `yt-dlp` 合流需要系统包 `ffmpeg`
- **bilibili 修复**:hk 出口访问 `www.bilibili.com` 视频页被 WAF 下发 412/JS 挑战(浏览器 UA 触发,API 客户端 UA 可过),页面无 `__INITIAL_STATE__`,yt-dlp 页面提取必然失败;改为**官方 API 直取**(`x/web-interface/view` → `x/player/playurl` → 媒体段下载),yt-dlp 保留后备;实测公网 200/1.3MB/6.5s
- jm/dufe/pixiv/openapi 重建后实测正常;证书由 acme.sh 自动续期(cron 每 6h 检查)
**2026-09-23(仓库与线上一致性核对 + 完整性补齐)**
- 逐文件 md5 核对:源码/main.py、systemd 全部 7 个单元、ginkgo/jm/bilibili/refresh 各模块、站点 nginx conf 均与仓库一致
- **补录漂移**:`source/pixiv/pixiv.py` 线上版为 2026-09-17 的"图源错误码处理"增强(官方 API 404/401/403/429 分别返回明确错误、`fetch_illust_url` 返回 api_status、不再对确定性错误做无意义备用源重试),此前未入库;本次以线上版为准补录
- **仓库补齐恢复依赖**:新增 `nginx/00-limits.conf`(限流 zone,站点 conf 引用 `perip_req`/`perip`,缺失会使 `nginx -t` 失败)与 `nginx/00-websocket.conf`(`$connection_upgrade` map,同样必须);迁移步骤第 4 步已同步
- 凭据快照另存于本机备份目录(`tokens.json`、`cookies.txt`,0600),不入库
**2026-09-23(dufe mode=auto + bot 基准同步)**
- dufe 签到服务新增 **`&mode=auto`**(url 参数可省略):SSO 登录一次后复用登录态 → 拉取通知列表(`GET /student/message/getMessageAll` pageSize=20/readFlag=0 + `POST /student/busiMessage/list`)→ 按 bot ginkgo 同款规则发现活动(仅互动消息、文本含"签到"、jumpUrl 可解析为 `/course/detail/.../signin/<id>/<type>`,query 不入库,按 signInId 去重)→ 按时间序逐个自动签到
- **连续重试**:每活动首次 + 最多 5 次重试、间隔 3 秒(`MAX_SIGNIN_ATTEMPTS=6` / `SIGNIN_BACKOFF=(3,3,3,3,3)`,对齐 bot);上游错误/登录错时重登录一次立即重试(对齐 bot `_call_sign_in_local` 的 refresh 语义);**除限流外任何未确认成功都消耗重试次数**(对齐 bot `complete_signin`);限流即中断本轮,已完成结果照常返回(`data.rate_limited=true`)
- **结果聚合**:`data.results` 列表逐活动返回(activityKey/signInId/type/typeName/title/signInCode/status/success/message/attempts),另附 total/succeeded/failed/activities 计数;**`attempts` 为逐次完整记录列表**(每次尝试 `{"n", "success", "message"}`,relogin 后立即重试的那次带 `"relogin": true`;成功与重试用尽两种情况都携带全部尝试)
- **与 bot ginkgo 非签到码签到逻辑比对后以 bot 为基准同步**:
- 移植限流防护体系:`RateLimitedError` / `_RATE_MESSAGE` / `_RATE_CODES` / `is_rate_limited` / `_valid_retry_after` / `_LoginErrors` / `check_response` / `safe_error` / `_request`(原实现完全无限流检测)
- `parse_course_url`:courseId 由 `\d+` 放宽为 `[0-9A-Za-z]+`,报错文案对齐
- `commit_sign_in`:经 `_request` 包装(`check_response`),失败消息 "request to InTheSign failed: %s" % safe_error(e)(原为 "upstream request failed")
- `fetch_sign_in_info`:getSignInInfo 响应过 `check_response`;新增 `sess`/`token` 参数注入(auto 模式请求内复用登录态,省去每活动一次 SSO);补齐 "Malformed getSignInInfo response" 非 dict 校验
- 验证:免凭据单测(发现/过滤正反用例)+ 假凭据 401 线路测试 + bj 真账密**只读**联调(登录 OK,notices=20/busi=18,发现 12 个活动含 1 个普通签到,类型/标题解析正确);未做真实提交(避免与运行中的 bot 轮询交叉签到)
**2026-09-23(早些)**
- 新增 **jm 漫画下载打包服务**:`source/jm/jm.py` + `jm.service`(`127.0.0.1:9004`,aiohttp + jmcomic,端点 `GET /jm/get_comic?id=xxx[&range=起-止]`)
- 移植自 qqbot 的 jm 模块(bj `C:/call/bot/utility/jm.py`):保留章节范围解析 / 文件名清洗 / webp→jpg 打包逻辑;去掉 QQ 回复、代理轮换(hk 可直连源站)、上传 cf 文件服务器三部分,改为 HTTP 直返 ZIP
- ZIP 为临时产物:返回后 600 秒延时删除(`asyncio` 延时任务 + 每次请求 `_cache_sweep()` 兜底清理,防进程重启残留);TTL 窗口内同名请求直接复用
- 单次上限 30 章(`MAX_CHAPTERS`,与 qqbot 一致;也避免超过聚合层 `proxy()` 的 300s 超时)
- 在聚合层 `main.py` 注册该端点:新增 `JM_BACKEND`,复用既有 `proxy()`(固定 GET),零改动既有行为
- nginx **无需改动**:`location /` 兜底已把 `/jm/...` 转发给 `127.0.0.1:9008`(与 dufe 相同)
- `env/general` 新增依赖 `jmcomic`(2.7.7,`pip install jmcomic`)
**2026-09-13**
- 新增 **dufe 银杏签到码查询服务**:`source/dufe/ginkgostu_sign_in/ginkgo_sign_in.py` + `ginkgo_sign_in.service`(`127.0.0.1:9005`,Flask + waitress 16 线程,端点 `GET|POST /dufe/ginkgostu_sign_in`)
- 在聚合层 `main.py` 注册该端点:新增 `DUFE_BACKEND` 与 `proxy_forward()`(保留请求方法/请求体;原 `proxy()` 固定 GET 行为未改动)
- nginx **无需改动**:`location /` 兜底已把 `/dufe/...` 转发给 `127.0.0.1:9008`
- 修复 `ginkgo_sign_in.py` 两处缺陷:
- `_b64_decode` 先做 `urllib.parse.unquote`(iOS 快捷指令会把 base64 的 `=` 编码成 `%3D`,旧实现直接 `binascii.Error`);顺序为先 unquote、再 `" "→"+"`
- `commit` 判定由 fail-open 改为 fail-closed:须 `isinstance(dict)` 且 `success is True`,否则一律判失败并返回 `msg = "sign-in failed: <原因>"`
- `parse_course_url` 报错文案改为 `URL is not a sign-in page (missing /signin/<signInId>/<type>): <url>`
**2026-09-12**
- 端口重排 `9888 / 9088 / 19990 → 9008 / 9007 / 9006`(全部仅 `127.0.0.1`)
- pixiv 曾误绑 `0.0.0.0:9088` 对外裸奔(绕过 nginx 日志与限流),已收口为 `127.0.0.1`
- 主入口 `main.py` 由 `source/openapi/` 上移到 `source/`;systemd `WorkingDirectory` 同步
- pixiv 的 `IMAGE_DIR` 由硬编码 `/home/pixiv/images` 改为相对 `../cache/pixiv/images`,并建 `/home/images` 软链
- bilibili 新增真缓存(`../cache/bilibili`)并修复 `/tmp` 泄漏,新增 `cache_info` 接口
- pixiv 改用 waitress 8 线程
- 删除 `kim1.work` / `zxx.aeolianchi.me` 两个 vhost
- 清理历史 `.bak` / `__pycache__` / 空日志等冗余文件
**2026-09-02** 新增 `pixiv-token-refresh`;补 `/img-original/` 反代(App API 原图路径)
**2026-07-31** 三服务迁移落位(Python 环境合并为单一 `general`)