1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
| import os import subprocess import asyncio import httpx import uvicorn from fastapi import FastAPI, Request, Response from fastapi.middleware.cors import CORSMiddleware
app = FastAPI(docs_url=None, redoc_url=None) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )
cwd = os.getcwd() ARTALK_BIN = f"{cwd}/artalk" ARTALK_ARGS = ["server"] TARGET = "http://127.0.0.1:23366" _check_lock = asyncio.Lock() _artalk_process: subprocess.Popen = None
async def _ensure_target_up(): """ 确保 TARGET 可访问。若不可访问,启动 artalk 服务并等待它可达。 """ global _artalk_process
if _check_lock.locked(): return
async with _check_lock: if await _target_is_up(): return
if _artalk_process is None or _artalk_process.poll() is not None: _artalk_process = subprocess.Popen( [ARTALK_BIN] + ARTALK_ARGS, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, )
for _ in range(30): if await _target_is_up(): return await asyncio.sleep(1.5)
raise RuntimeError("artalk 服务启动超时,TARGET 仍不可达")
async def _target_is_up() -> bool: """ 简单检测 TARGET 根路径是否返回 2xx/3xx 状态码。 """ try: async with httpx.AsyncClient(timeout=1.0) as client: resp = await client.get(TARGET) return 200 <= resp.status_code < 400 except Exception: return False
@app.api_route("/{full_path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]) async def proxy(full_path: str, request: Request): await _ensure_target_up()
url = f"{TARGET}/{full_path}" headers = dict(request.headers) headers.pop("host", None) body = await request.body()
async with httpx.AsyncClient(timeout=None) as client: resp = await client.request( request.method, url, headers=headers, content=body, params=request.query_params )
response_headers = dict(resp.headers)
if full_path.endswith(".js") or full_path.endswith(".css") \ or resp.headers.get("content-type", "").startswith(("text/javascript", "text/css")): response_headers["Cache-Control"] = "public, s-max-age=2592000" response_headers["Vercel-CDN-Cache-Control"] = "public, max-age=2592000" response_headers["CDN-Cache-Control"] = "public, max-age=2592000"
return Response( content=resp.content, status_code=resp.status_code, headers=response_headers )
if __name__ == "__main__": uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|