# 线上部署与运维(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 + 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 + 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` - 命中即复用、不再下载 - 未命中才调 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--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` 2. **源码**:恢复 `source/{main.py,pixiv/,bilibili/,dufe/,jm/}` 到 `/home/openapi/source/` 3. **systemd**:恢复 `systemd/*.service` 到 `/etc/systemd/system/`,`daemon-reload` + `enable` + `start` 4. **nginx**:套用 `conf.d/openapi.rrrosmontis.icu.conf`(并确保 `00-websocket.conf` 的 map 存在) 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-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//`,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//): ` **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`)