优化docs显示效果和移动端适配 增加默认数据库支持并发数

This commit is contained in:
inliver233 committed 2026-02-24 01:50:23 +08:00
1 parent 810b2da60e
commit 638a2852f9
10 files changed
+203 -59

No files matched your search

+67 -43
View File
@@ -52,17 +52,18 @@ def _build_docs_html(*, base_url: str) -> str:
<title>Random Mage Docs</title>
<style>
:root {{
--bg: #0b1220;
--card: rgba(255, 255, 255, 0.06);
--card-2: rgba(255, 255, 255, 0.08);
--border: rgba(255, 255, 255, 0.12);
--text: rgba(255, 255, 255, 0.92);
--muted: rgba(255, 255, 255, 0.72);
--muted2: rgba(255, 255, 255, 0.60);
--link: #7dd3fc;
--accent: #a78bfa;
--ok: #34d399;
--warn: #fbbf24;
/* Clay / paper-like palette */
--bg: #f4efe6;
--card: rgba(255, 250, 243, 0.78);
--card-2: rgba(255, 250, 243, 0.92);
--border: rgba(58, 38, 26, 0.14);
--text: rgba(43, 29, 22, 0.94);
--muted: rgba(67, 51, 44, 0.78);
--muted2: rgba(67, 51, 44, 0.66);
--link: #a2522c;
--accent: #c07046;
--ok: #1f7a56;
--warn: #b45309;
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji",
"Segoe UI Emoji";
@@ -74,9 +75,9 @@ def _build_docs_html(*, base_url: str) -> str:
font-family: var(--sans);
color: var(--text);
background:
radial-gradient(1000px 600px at 15% 10%, rgba(167, 139, 250, 0.22), transparent 55%),
radial-gradient(900px 520px at 85% 0%, rgba(125, 211, 252, 0.18), transparent 60%),
radial-gradient(700px 700px at 60% 85%, rgba(52, 211, 153, 0.12), transparent 55%),
radial-gradient(1000px 620px at 12% 8%, rgba(192, 112, 70, 0.18), transparent 58%),
radial-gradient(900px 560px at 88% 0%, rgba(162, 82, 44, 0.14), transparent 60%),
radial-gradient(760px 760px at 60% 92%, rgba(31, 122, 86, 0.10), transparent 58%),
var(--bg);
}}
@@ -88,13 +89,16 @@ def _build_docs_html(*, base_url: str) -> str:
margin: 0 auto;
padding: 32px 20px 64px;
}}
@media (max-width: 520px) {{
.wrap {{ padding: 22px 14px 46px; }}
}}
.hero {{
padding: 18px 18px 16px;
border: 1px solid var(--border);
background: linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.04));
background: linear-gradient(180deg, rgba(255,250,243,0.92), rgba(255,250,243,0.70));
border-radius: 14px;
backdrop-filter: blur(10px);
box-shadow: 0 10px 30px rgba(40, 24, 16, 0.10);
}}
.hero h1 {{
@@ -102,6 +106,9 @@ def _build_docs_html(*, base_url: str) -> str:
font-size: 22px;
letter-spacing: 0.2px;
}}
@media (max-width: 520px) {{
.hero h1 {{ font-size: 20px; }}
}}
.hero p {{
margin: 10px 0 0;
color: var(--muted);
@@ -123,6 +130,10 @@ def _build_docs_html(*, base_url: str) -> str:
background: var(--card);
border-radius: 14px;
padding: 16px 16px 14px;
box-shadow: 0 10px 30px rgba(40, 24, 16, 0.06);
}}
@media (max-width: 520px) {{
.card {{ padding: 14px 14px 12px; }}
}}
.card h2 {{
font-size: 16px;
@@ -142,8 +153,8 @@ def _build_docs_html(*, base_url: str) -> str:
padding: 2px 6px;
border-radius: 8px;
border: 1px solid var(--border);
background: rgba(0, 0, 0, 0.18);
color: rgba(255,255,255,0.86);
background: rgba(43, 29, 22, 0.06);
color: rgba(43, 29, 22, 0.92);
}}
pre {{
@@ -151,12 +162,15 @@ def _build_docs_html(*, base_url: str) -> str:
padding: 12px 12px;
border-radius: 12px;
border: 1px solid var(--border);
background: rgba(0, 0, 0, 0.22);
background: rgba(43, 29, 22, 0.06);
overflow: auto;
font-family: var(--mono);
font-size: 12px;
line-height: 1.55;
color: rgba(255,255,255,0.88);
color: rgba(43, 29, 22, 0.92);
}}
@media (max-width: 520px) {{
pre {{ font-size: 11px; }}
}}
.chips {{
@@ -171,36 +185,43 @@ def _build_docs_html(*, base_url: str) -> str:
border: 1px solid var(--border);
background: var(--card-2);
font-size: 12px;
color: rgba(255,255,255,0.85);
color: rgba(43, 29, 22, 0.86);
white-space: nowrap;
}}
.chip strong {{ color: rgba(255,255,255,0.94); }}
.chip strong {{ color: rgba(43, 29, 22, 0.94); }}
.table-wrap {{
overflow-x: auto;
-webkit-overflow-scrolling: touch;
border-radius: 12px;
border: 1px solid var(--border);
margin-top: 10px;
background: var(--card-2);
}}
table {{
width: 100%;
border-collapse: collapse;
margin-top: 10px;
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--border);
min-width: 680px;
}}
th, td {{
padding: 10px 10px;
border-bottom: 1px solid rgba(255,255,255,0.10);
border-bottom: 1px solid rgba(58, 38, 26, 0.10);
vertical-align: top;
font-size: 13px;
line-height: 1.45;
word-break: break-word;
}}
th {{
text-align: left;
background: rgba(255,255,255,0.06);
color: rgba(255,255,255,0.92);
background: rgba(43, 29, 22, 0.04);
color: rgba(43, 29, 22, 0.92);
font-weight: 600;
}}
td code {{
font-family: var(--mono);
font-size: 12px;
color: rgba(255,255,255,0.9);
color: rgba(43, 29, 22, 0.92);
word-break: break-all;
}}
.muted {{ color: var(--muted2); }}
.note {{
@@ -208,10 +229,10 @@ def _build_docs_html(*, base_url: str) -> str:
padding: 12px 12px;
border-radius: 12px;
border: 1px solid var(--border);
background: rgba(251, 191, 36, 0.10);
color: rgba(255,255,255,0.90);
background: rgba(180, 83, 9, 0.10);
color: rgba(43, 29, 22, 0.92);
}}
.note strong {{ color: rgba(255,255,255,0.95); }}
.note strong {{ color: rgba(43, 29, 22, 0.95); }}
.footer {{
margin-top: 18px;
color: var(--muted2);
@@ -273,14 +294,15 @@ def _build_docs_html(*, base_url: str) -> str:
<section class="card" style="margin-top: 14px;">
<h2>筛选语法速记</h2>
<table>
<thead>
<tr>
<th style="width: 170px;">参数</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<div class="table-wrap" role="region" aria-label="filters-table" tabindex="0">
<table>
<thead>
<tr>
<th style="width: 170px;">参数</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>strategy</code></td>
<td>
@@ -293,6 +315,7 @@ def _build_docs_html(*, base_url: str) -> str:
<td>
<code>0</code>/<code>1</code>:强制使用第三方反向代理拉取图片上游(即使全局未开启)。<br/>
<span class="muted">上游可为 i.pixiv.cat / i.pixiv.re / i.pixiv.nl。</span><br/>
<span class="muted">未指定 pximg_mirror_host 时:大陆访问优先 i.pixiv.re,非大陆默认 i.pixiv.cat。</span><br/>
<span class="muted">仅影响服务端拉图的上游域名,客户端仍访问本站域名。</span>
</td>
</tr>
@@ -354,8 +377,9 @@ def _build_docs_html(*, base_url: str) -> str:
<td><code>attempts</code></td>
<td>上游失败时重试次数(1–10)。</td>
</tr>
</tbody>
</table>
</tbody>
</table>
</div>
<div class="footer">
其它接口:<a href="{examples["tags_api"]}">/tags</a>(标签列表)、<a href="{examples["authors_api"]}">/authors</a>(作者列表)。<br/>
OpenAPI:<a href="{examples["openapi"]}">/openapi.json</a>,Swagger:<a href="{examples["swagger"]}">/api/docs</a>。<br/>
+11 -2
View File
@@ -12,7 +12,11 @@ from app.core.http_stream import stream_url
import sqlalchemy as sa
from app.core.pixiv_urls import ALLOWED_IMAGE_EXTS
from app.core.pximg_reverse_proxy import normalize_pximg_mirror_host, rewrite_pximg_to_mirror
from app.core.pximg_reverse_proxy import (
normalize_pximg_mirror_host,
pick_pximg_mirror_host_for_request,
rewrite_pximg_to_mirror,
)
from app.core.request_id import get_or_create_request_id, set_request_id_header, set_request_id_on_state
from app.core.runtime_settings import load_runtime_config
from app.core.time import iso_utc_ms
@@ -304,7 +308,12 @@ async def proxy_image(
runtime = await load_runtime_config(engine)
use_pixiv_cat = bool(runtime.image_proxy_use_pixiv_cat) or int(pixiv_cat) == 1
mirror_host = mirror_host_override or str(getattr(runtime, "image_proxy_pximg_mirror_host", "") or "").strip() or "i.pixiv.cat"
runtime_mirror_host = str(getattr(runtime, "image_proxy_pximg_mirror_host", "") or "").strip() or "i.pixiv.cat"
mirror_host = mirror_host_override or (
pick_pximg_mirror_host_for_request(headers=request.headers, fallback_host=runtime_mirror_host)
if use_pixiv_cat
else runtime_mirror_host
)
async def _best_effort(fn, *args, timeout_s: float = 1.5, **kwargs) -> None: # type: ignore[no-untyped-def]
try:
+17 -3
View File
@@ -5,7 +5,11 @@ from fastapi import APIRouter, Request
from app.core.errors import ApiError, ErrorCode
from app.core.http_stream import stream_url
from app.core.pixiv_urls import ALLOWED_IMAGE_EXTS
from app.core.pximg_reverse_proxy import normalize_pximg_mirror_host, rewrite_pximg_to_mirror
from app.core.pximg_reverse_proxy import (
normalize_pximg_mirror_host,
pick_pximg_mirror_host_for_request,
rewrite_pximg_to_mirror,
)
from app.core.proxy_routing import select_proxy_uri_for_url
from app.core.runtime_settings import load_runtime_config
from app.db.images_get_by_illust import get_image_by_illust_page
@@ -52,7 +56,12 @@ async def legacy_multi(
runtime = await load_runtime_config(engine)
use_pixiv_cat = bool(runtime.image_proxy_use_pixiv_cat) or int(pixiv_cat) == 1
mirror_host = mirror_host_override or str(getattr(runtime, "image_proxy_pximg_mirror_host", "") or "").strip() or "i.pixiv.cat"
runtime_mirror_host = str(getattr(runtime, "image_proxy_pximg_mirror_host", "") or "").strip() or "i.pixiv.cat"
mirror_host = mirror_host_override or (
pick_pximg_mirror_host_for_request(headers=request.headers, fallback_host=runtime_mirror_host)
if use_pixiv_cat
else runtime_mirror_host
)
proxy_uri = None
source_url = rewrite_pximg_to_mirror(str(image.original_url), mirror_host=mirror_host) if use_pixiv_cat else str(image.original_url)
if not use_pixiv_cat:
@@ -110,7 +119,12 @@ async def legacy_single(
runtime = await load_runtime_config(engine)
use_pixiv_cat = bool(runtime.image_proxy_use_pixiv_cat) or int(pixiv_cat) == 1
mirror_host = mirror_host_override or str(getattr(runtime, "image_proxy_pximg_mirror_host", "") or "").strip() or "i.pixiv.cat"
runtime_mirror_host = str(getattr(runtime, "image_proxy_pximg_mirror_host", "") or "").strip() or "i.pixiv.cat"
mirror_host = mirror_host_override or (
pick_pximg_mirror_host_for_request(headers=request.headers, fallback_host=runtime_mirror_host)
if use_pixiv_cat
else runtime_mirror_host
)
proxy_uri = None
source_url = rewrite_pximg_to_mirror(str(image.original_url), mirror_host=mirror_host) if use_pixiv_cat else str(image.original_url)
if not use_pixiv_cat:
+11 -2
View File
@@ -13,7 +13,11 @@ from fastapi.responses import RedirectResponse
from app.core.errors import ApiError, ErrorCode
from app.core.http_stream import stream_url
from app.core.imgproxy import build_signed_processing_url, load_imgproxy_config_from_settings
from app.core.pximg_reverse_proxy import normalize_pximg_mirror_host, rewrite_pximg_to_mirror
from app.core.pximg_reverse_proxy import (
normalize_pximg_mirror_host,
pick_pximg_mirror_host_for_request,
rewrite_pximg_to_mirror,
)
from app.core.proxy_routing import select_proxy_uri_for_url
from app.core.runtime_settings import load_runtime_config
from app.core.time import iso_utc_ms
@@ -349,7 +353,12 @@ async def random_image(
Session = create_sessionmaker(engine)
runtime = await load_runtime_config(engine)
use_pixiv_cat = bool(runtime.image_proxy_use_pixiv_cat) or int(pixiv_cat) == 1
mirror_host = mirror_host_override or str(getattr(runtime, "image_proxy_pximg_mirror_host", "") or "").strip() or "i.pixiv.cat"
runtime_mirror_host = str(getattr(runtime, "image_proxy_pximg_mirror_host", "") or "").strip() or "i.pixiv.cat"
mirror_host = mirror_host_override or (
pick_pximg_mirror_host_for_request(headers=request.headers, fallback_host=runtime_mirror_host)
if use_pixiv_cat
else runtime_mirror_host
)
async def _best_effort(fn, *args, timeout_s: float = 1.5, **kwargs) -> None: # type: ignore[no-untyped-def]
try:
+66
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
from collections.abc import Mapping
from urllib.parse import urlparse, urlunparse
@@ -27,6 +28,71 @@ _PXIMG_MIRROR_ALIASES: dict[str, str] = {
}
_REQUEST_COUNTRY_HEADERS: tuple[str, ...] = (
# Cloudflare
"cf-ipcountry",
# Vercel
"x-vercel-ip-country",
# AWS CloudFront
"cloudfront-viewer-country",
# Google App Engine / GCP
"x-appengine-country",
# Common self-managed reverse proxies
"x-country-code",
"x-geo-country",
)
def _get_request_country_code(headers: Mapping[str, str] | None) -> str | None:
if not headers:
return None
lowered: dict[str, str] = {}
try:
for k, v in headers.items():
key = str(k or "").strip().lower()
if not key:
continue
val = str(v or "").strip()
if not val:
continue
lowered[key] = val
except Exception:
return None
for key in _REQUEST_COUNTRY_HEADERS:
val = lowered.get(key)
if not val:
continue
code = val.strip().upper()
if not code:
continue
# Some CDNs may return "CN,..." or other variants; we only need the first token.
code = code.split(",", 1)[0].strip()
if len(code) >= 2:
return code[:2]
return None
def pick_pximg_mirror_host_for_request(
*,
headers: Mapping[str, str] | None,
fallback_host: str | None,
) -> str:
"""
Pick the best pximg mirror host for a request.
- If request is detected as mainland China, prefer i.pixiv.re (commonly faster inside CN).
- Otherwise use the configured fallback host (defaults to i.pixiv.cat).
Detection is best-effort and relies on CDN/reverse-proxy injected country headers
(e.g. Cloudflare's CF-IPCountry). If missing, falls back to the configured host.
"""
fallback = normalize_pximg_mirror_host(fallback_host) or DEFAULT_PXIMG_MIRROR_HOST
if _get_request_country_code(headers) == "CN":
return "i.pixiv.re"
return fallback
def is_pximg_image_url(url: str) -> bool:
try:
parsed = urlparse(url)
+7 -4
View File
@@ -11,10 +11,13 @@ from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
# legitimately hold the SQLite writer lock for several seconds. A higher default
# busy timeout makes the system much more resilient under load (still overrideable
# via SQLITE_BUSY_TIMEOUT_MS env).
SQLITE_BUSY_TIMEOUT_MS = 30_000
SQLITE_POOL_SIZE = 10
SQLITE_MAX_OVERFLOW = 10
SQLITE_POOL_TIMEOUT_S = 5
#
# NOTE: These defaults favor availability on reasonably powerful servers. For
# smaller instances you may want to lower pool sizes and/or busy timeout.
SQLITE_BUSY_TIMEOUT_MS = 60_000
SQLITE_POOL_SIZE = 30
SQLITE_MAX_OVERFLOW = 30
SQLITE_POOL_TIMEOUT_S = 30
def apply_sqlite_pragmas(dbapi_connection: Any) -> None:
+3 -3
View File
@@ -33,7 +33,7 @@ def is_sqlite_busy_error(exc: BaseException) -> bool:
async def with_sqlite_busy_retry(
op: Callable[[], Awaitable[T]],
*,
retries: int = 8,
retries: int = 12,
base_delay_s: float = 0.05,
) -> T:
try:
@@ -49,9 +49,9 @@ async def with_sqlite_busy_retry(
base_delay = float(max(0.0, min(float(env_base), 5.0)))
try:
env_max_delay = float((os.environ.get("SQLITE_BUSY_MAX_DELAY_S") or "").strip() or 2.0)
env_max_delay = float((os.environ.get("SQLITE_BUSY_MAX_DELAY_S") or "").strip() or 5.0)
except Exception:
env_max_delay = 2.0
env_max_delay = 5.0
max_delay = float(max(0.0, min(float(env_max_delay), 30.0)))
attempt = 0
+1 -1
View File
@@ -350,7 +350,7 @@ async def main_async(*, max_iterations: int | None = None, poll_interval_s: floa
)
max_concurrency = _parse_int_env(
"WORKER_MAX_CONCURRENCY",
default=20,
default=50,
min_v=1,
max_v=200,
)
+19
View File
@@ -0,0 +1,19 @@
from __future__ import annotations
from app.core.pximg_reverse_proxy import pick_pximg_mirror_host_for_request
def test_pick_pximg_mirror_host_for_request_prefers_re_for_cn() -> None:
host = pick_pximg_mirror_host_for_request(headers={"CF-IPCountry": "CN"}, fallback_host="i.pixiv.cat")
assert host == "i.pixiv.re"
def test_pick_pximg_mirror_host_for_request_uses_fallback_for_non_cn() -> None:
host = pick_pximg_mirror_host_for_request(headers={"CF-IPCountry": "US"}, fallback_host="i.pixiv.nl")
assert host == "i.pixiv.nl"
def test_pick_pximg_mirror_host_for_request_falls_back_to_default_on_invalid_fallback() -> None:
host = pick_pximg_mirror_host_for_request(headers=None, fallback_host="not-a-host")
assert host == "i.pixiv.cat"
+1 -1
View File
@@ -12,7 +12,7 @@ WORKER_AUTO_CONCURRENCY=true
# 自动并发刷新频率(秒):token 数变化后多久生效
WORKER_AUTO_CONCURRENCY_REFRESH_SECONDS=15
# 并发上限(无论 token 有多少都不会超过这个值)
WORKER_MAX_CONCURRENCY=20
WORKER_MAX_CONCURRENCY=50
# Secrets (do NOT commit real values).
SECRET_KEY=dev-secret-key-change-me