- bilibili.py: view -> playurl -> 媒体段 的 API 直取主路径,绕开页面 WAF/JS 挑战;yt-dlp 保留为后备;cookie 仅后备路径需要 - docs: 迁移步骤补 /var/cache/nginx 与 ffmpeg 两个重建坑位 - 实测: 公网 200/1.3MB/6.5s
201 lines
16 KiB
Markdown
201 lines
16 KiB
Markdown
# 线上部署与运维(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`)
|