# OpenAPI 统一接口服务。线上入口:`https://openapi.rrrosmontis.icu` 聚合层把 **pixiv 图片服务**、**bilibili 视频服务**、**dufe 银杏签到码查询服务**与 **jm 漫画下载服务**收敛到单一对外入口。五个后端进程全部只监听 `127.0.0.1`,对外仅由 nginx 暴露 80/443——即"一个入口、零暴露端口"。 ## 架构 ``` 公网 443/80 nginx (server_name openapi.rrrosmontis.icu) ├── /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) ├── /jm/get_comic?id=xxx -> 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 跳转文章页) ``` ## 端口与进程 | 服务 | 监听 | 入口源码 | 说明 | |---|---|---|---| | openapi | `127.0.0.1:9008` | `source/main.py`(aiohttp) | 聚合层,纯转发,**唯一入口** | | pixiv | `127.0.0.1:9007` | `source/pixiv/pixiv.py`(Flask + waitress) | 图片代理 | | bilibili | `127.0.0.1:9006` | `source/bilibili/bilibili.py`(aiohttp,官方 API 直取 + yt-dlp 后备) | 视频代理 | | dufe 签到 | `127.0.0.1:9005` | `source/dufe/ginkgostu_sign_in/ginkgo_sign_in.py`(Flask + waitress 16 线程) | 银杏(DUFE)签到码查询,`GET\|POST /dufe/ginkgostu_sign_in`;`&mode=auto` 时拉取通知列表自动签到(与 bot ginkgo 同逻辑,连续重试,结果列表聚合返回) | | jm | `127.0.0.1:9004` | `source/jm/jm.py`(aiohttp + jmcomic) | 漫画下载打包(移植自 qqbot jm 模块),`GET /jm/get_comic?id=xxx[&range=起-止]` | | pixiv-token-refresh | — | `source/pixiv/refresh_token.py` | token 刷新守护(每 30–60 分钟随机) | | bilibili-refresh | — | `source/bilibili/refresh_cookie.py` | cookie 刷新守护(每 50–70 分钟随机) | > 历史端口 `9888 / 9088 / 19990` 已于 2026-09-12 废弃,全部改为上述 `900x`。 > pixiv 早期误绑 `0.0.0.0:9088` 对公网裸奔(绕过 nginx 的日志与限流),现已收口为 `127.0.0.1`。 ## 结构 ``` source/ ├── main.py # 聚合层 (127.0.0.1:9008),五条路由:/ /pixiv/get_image /bilibili/get_video /dufe/ginkgostu_sign_in /jm/get_comic ├── pixiv/ │ ├── pixiv.py # Pixiv 图片服务 (127.0.0.1:9007) │ ├── refresh_token.py # Pixiv OAuth token 刷新守护 │ └── tokens.template.json # token 模板(实际 tokens.json 不入库) ├── bilibili/ │ ├── bilibili.py # bilibili 视频服务 (127.0.0.1:9006) │ ├── refresh_cookie.py # cookie 定时刷新守护 │ ├── qr_login.py # 扫码登录获取 cookie │ └── qr_login_fast.py ├── 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 # 图片缓存 ├── bilibili/ # 视频缓存 └── jm/ # jm 临时 ZIP(TTL 10 分钟,返回后删除) nginx/ ├── openapi.rrrosmontis.icu.conf # 统一入口(/pixiv/ /bilibili/ /img-master/ /img-original/,其余路径由 location / 兜底至聚合层;含按 IP 限流) ├── 00-limits.conf # 限流 zone(limit_req_zone rate=10r/s、limit_conn_zone、429)——缺失会导致 nginx -t 失败 └── 00-websocket.conf # $connection_upgrade map 定义——缺失会导致 nginx -t 失败 systemd/ ├── openapi.service # 聚合层 ├── pixiv.service # Pixiv 图片服务(waitress --threads=8) ├── bilibili.service # bilibili 视频服务 ├── bilibili-refresh.service # cookie 刷新守护 ├── pixiv-token-refresh.service # Pixiv token 刷新守护 ├── ginkgo_sign_in.service # 银杏(DUFE)签到码查询服务 └── jm.service # JM 漫画下载打包服务 docs/ └── 部署与运维.md # 线上服务器的路径、缓存、迁移步骤与变更记录 ``` ## 缓存机制 **pixiv(图片)**:`check_cache()` 以 glob 命中即直接回文件;未命中按 `官方 App API → pixiv.re → pixiv.cat` 三级回退抓取,落盘用 `.tmp` + `os.replace` 原子写。 缓存目录不再硬编码,由模块位置推导: ```python IMAGE_DIR = (Path(__file__).resolve().parent / "../cache/pixiv/images").resolve() IMAGE_DIR.mkdir(parents=True, exist_ok=True) ``` 即 `source/cache/pixiv/images`;线上常用 `/home/images` 软链指向它,便于访问。 **bilibili(视频)**:以 **BV 号为键**缓存到 `source/cache/bilibili/{BV}.mp4` - 命中即直接 `FileResponse`,**不再下载** - 未命中时优先走 **官方 API 直取**(view → playurl → 媒体段;2026-10-22 起 hk 出口被页面 WAF 挑战,yt-dlp 页面提取失效),失败再回退 yt-dlp - 未命中才下载,且先落到缓存目录内的隐藏暂存目录 `.dl-XXXX`,成功后 `os.replace` 原子落盘 - **无论成功 / 失败 / 超时都会 `shutil.rmtree(tmpdir)`** —— 早期实现只清理 cookie 临时文件、从不清理视频暂存目录,导致每次取视频都把完整视频永久留在 `/tmp`(无上限磁盘泄漏),已修复 - 淘汰策略:单文件 `CACHE_TTL_SECONDS`(默认 7 天)未访问即过期;总量超过 `CACHE_MAX_BYTES`(默认 2 GiB)时按 mtime 从最旧开始删除 - 命中/落盘会在日志打印 `cache HIT / MISS / STORE`(已 `flush=True`,实时可见) 运维接口(只绑本机): ```bash curl -s 127.0.0.1:9006/bilibili/cache_info # {cache_dir, cache_max_bytes, cache_ttl_seconds, count, total_bytes, free_bytes, files[]} ``` **jm(漫画)**:**非持久缓存**,与 bilibili 不同——ZIP 仅作临时产物 - 请求 `id`(+ 可选 `range=起-止`,单次 ≤30 章)→ jmcomic 下载到 `cache/jm/` 内隐藏暂存目录 `.dl--XXXX` → webp 统一转 jpg 打包 ZIP(`{标题}_{id}_ch{起}-{止}.zip`)→ HTTP 返回 - 打包完成即 `rmtree` 图片暂存目录;ZIP 在返回后 `ZIP_TTL_SECONDS`(默认 600 秒)延时删除 - TTL 窗口内重复请求同一 ZIP 直接复用并重新计时;每次请求附带 `_cache_sweep()` 兜底清理过期 ZIP(防进程重启残留) - 同一本子串行处理(per-album asyncio.Lock),并发请求同本返回 409 - 运维接口:`curl -s 127.0.0.1:9004/jm/cache_info` ## 部署 1. **Python 环境**:各服务共用一个环境,需含 `flask`、`waitress`、`aiohttp`、`httpx`、`yt-dlp`、`jmcomic`、`pillow`;系统包另需 **ffmpeg**(yt-dlp 合流用,API 直取路径不需要) 2. **凭据**: - 复制 `source/pixiv/tokens.template.json` 为 `source/pixiv/tokens.json`(`chmod 600`)并填入实际 pixiv token - 放置 `source/bilibili/cookies.txt`(`name=value` 格式),或运行 `qr_login.py` 扫码生成 - jm 无需凭据,但要求部署机可直连源站(jmcomic 会自动更新 API 域名) 3. **服务**:将 `systemd/*.service` 安装到 `/etc/systemd/system/`,然后 `systemctl daemon-reload && systemctl enable --now openapi pixiv bilibili ginkgo_sign_in jm bilibili-refresh pixiv-token-refresh` 4. **nginx**:将 `nginx/openapi.rrrosmontis.icu.conf` 放入 `/etc/nginx/conf.d/`(需存在 `$connection_upgrade` 的 map 定义),`nginx -t && systemctl reload nginx` 5. **软链**(可选):`ln -s <部署根>/source/cache/pixiv/images /home/images` 6. **定时任务**:无 cron/timer——两个刷新守护均为常驻循环(内部随机间隔),systemd `Restart=always` 兜底 ## 验证 ```bash curl -k "https://openapi.rrrosmontis.icu/pixiv/get_image?id=147113746" curl -k "https://openapi.rrrosmontis.icu/bilibili/get_video?bv=BV16a41157Q4" curl -kI "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 curl -s 127.0.0.1:9006/bilibili/cache_info curl -s 127.0.0.1:9004/jm/cache_info ``` 端口暴露自检(五个后端都应"不可达",仅 443/80 通): ```bash for p in 9004 9005 9006 9007 9008; do timeout 3 bash -c "cat < /dev/null > /dev/tcp//$p" 2>/dev/null \ && echo "$p 可连通(异常)" || echo "$p 不可达(正常)"; done ``` ## 版本要点 - **2026-09-23** - dufe 签到服务新增 **`mode=auto`**:拉取通知列表(getMessageAll + busiMessage/list)→ 按 bot ginkgo 同款规则发现签到活动(互动消息 + 文本含"签到" + 可解析课程链接,按 signInId 去重)→ 逐个自动签到;**连续重试**(首次 + 最多 5 次,间隔 3s,对齐 bot `MAX_SIGNIN_ATTEMPTS=6 / SIGNIN_BACKOFF`;token 疑似失效时重登录一次立即重试,对齐 bot refresh 语义);**所有结果聚合为 `data.results` 列表**返回(每活动含 success/message/attempts/title/signInCode 等) - 非签到码签到逻辑与 bot ginkgo 比对后以 bot 为基准同步:移植限流防护体系(`check_response` / `is_rate_limited` / `safe_error` / `_LoginErrors`)、`parse_course_url` 的 courseId 放宽至 `[0-9A-Za-z]+`、`commit_sign_in` 的错误包装、`fetch_sign_in_info` 的 check_response 与外部登录态注入(`sess`/`token` 参数,auto 模式请求内复用同一登录态) - **2026-09-23**(早些):新增 **jm 漫画下载打包服务**(`source/jm/jm.py`,aiohttp + jmcomic,`127.0.0.1:9004`,端点 `GET /jm/get_comic?id=xxx[&range=起-止]`),移植自 qqbot 的 jm 模块(去掉 QQ/代理轮换/上传 cf 逻辑,改为 HTTP 直返 ZIP;hk 可直连源站无需代理)。ZIP 为临时产物:返回后 600 秒延时删除,TTL 窗口内同名请求直接复用。聚合层 `main.py` 注册路由(新增 `JM_BACKEND`,复用既有 `proxy()`),nginx 无需改动(`location /` 兜底) - **2026-09-13**:新增 **dufe 银杏签到码查询服务**(`source/dufe/ginkgostu_sign_in/ginkgo_sign_in.py`,Flask + waitress 16 线程,`127.0.0.1:9005`,端点 `GET|POST /dufe/ginkgostu_sign_in`),并在聚合层 `main.py` 注册该路由——为此新增 `proxy_forward()`(保留请求方法与请求体,原 `proxy()` 保持固定 GET 不变,零改动既有行为)。同期修复该服务两处缺陷:base64 参数补 `urllib.parse.unquote`(兼容 iOS 快捷指令的 `%3D` 编码),`commit` 判定改为 fail-closed(须 `commit.success is True` 才算签到成功) - **2026-09-12** - 端口重排 `9888/9088/19990 → 9008/9007/9006`,全部仅绑 `127.0.0.1`;pixiv 由 `0.0.0.0:9088` 收口 - 主入口 `main.py` 从 `source/openapi/` 上移到 `source/`,systemd `WorkingDirectory` 同步为 `source/` - pixiv 缓存路径由硬编码 `/home/pixiv/images` 改为相对 `../cache/pixiv/images` - pixiv 改由 **waitress 线程池**承载(`--threads=8`),替代 Flask 开发服务器(thread-per-request 无上限) - bilibili 新增 **BV 真缓存**(`../cache/bilibili`,2 GiB / 7 天淘汰),并修复视频暂存目录永不清理导致的 `/tmp` 泄漏 - 新增 `/bilibili/cache_info` 运维接口;缓存日志加 `flush=True` - **2026-09-02**:新增 pixiv token 刷新守护;补 `/img-original/` 反代(App API 原图路径) - **2026-07-31**:三服务迁移落位(Python 环境合并为单一 `general`)