mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-06-18 15:54:08 +08:00
feat(api): 支持挂载 Nuxt 静态 UI 并统一桌面端运行参数
- 若存在 WECHAT_TOOL_UI_DIR 或 frontend/.output/public,则在 / 挂载静态站点并支持 SPA 路由回退 - API 根端点调整为 /api,避免与静态 UI 冲突 - 新增 WECHAT_TOOL_DATA_DIR 输出目录约定,统一 databases/key store 等路径 - host/port 支持通过 WECHAT_TOOL_HOST / WECHAT_TOOL_PORT 配置,并打印可访问地址 - 新增 backend_entry.py 作为 PyInstaller 入口,减少动态导入识别问题
This commit is contained in:
@@ -1,7 +1,13 @@
|
||||
"""微信解密工具的FastAPI Web服务器"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
from starlette.responses import FileResponse
|
||||
from starlette.staticfiles import StaticFiles
|
||||
|
||||
from .logging_config import setup_logging, get_logger
|
||||
from .path_fix import PathFixRoute
|
||||
@@ -47,6 +53,70 @@ app.include_router(_chat_export_router)
|
||||
app.include_router(_chat_media_router)
|
||||
|
||||
|
||||
class _SPAStaticFiles(StaticFiles):
|
||||
"""StaticFiles with a SPA fallback (Nuxt generate output)."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._fallback_200 = Path(str(self.directory)) / "200.html"
|
||||
self._fallback_index = Path(str(self.directory)) / "index.html"
|
||||
|
||||
async def get_response(self, path: str, scope): # type: ignore[override]
|
||||
try:
|
||||
return await super().get_response(path, scope)
|
||||
except StarletteHTTPException as exc:
|
||||
if exc.status_code != 404:
|
||||
raise
|
||||
|
||||
# For client-side routes (no file extension), return Nuxt's SPA fallback.
|
||||
name = Path(path).name
|
||||
if "." in name:
|
||||
raise
|
||||
|
||||
if self._fallback_200.exists():
|
||||
return FileResponse(str(self._fallback_200))
|
||||
return FileResponse(str(self._fallback_index))
|
||||
|
||||
|
||||
def _maybe_mount_frontend() -> None:
|
||||
"""Serve the generated Nuxt static site at `/` if present.
|
||||
|
||||
This keeps web + desktop UI identical when the desktop shell (Electron) loads
|
||||
http://127.0.0.1:<port>/ from the same backend that serves `/api/*`.
|
||||
"""
|
||||
|
||||
ui_dir_env = os.environ.get("WECHAT_TOOL_UI_DIR", "").strip()
|
||||
|
||||
candidates: list[Path] = []
|
||||
if ui_dir_env:
|
||||
candidates.append(Path(ui_dir_env))
|
||||
|
||||
# Repo default: `frontend/.output/public` after `npm --prefix frontend run generate`.
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
candidates.append(repo_root / "frontend" / ".output" / "public")
|
||||
|
||||
ui_dir: Path | None = None
|
||||
for p in candidates:
|
||||
try:
|
||||
if (p / "index.html").is_file():
|
||||
ui_dir = p
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not ui_dir:
|
||||
return
|
||||
|
||||
try:
|
||||
app.mount("/", _SPAStaticFiles(directory=str(ui_dir), html=True), name="ui")
|
||||
logger.info("Serving frontend UI from: %s", ui_dir)
|
||||
except Exception:
|
||||
logger.exception("Failed to mount frontend UI from: %s", ui_dir)
|
||||
|
||||
|
||||
_maybe_mount_frontend()
|
||||
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def _shutdown_wcdb_realtime() -> None:
|
||||
try:
|
||||
@@ -62,4 +132,6 @@ async def _shutdown_wcdb_realtime() -> None:
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
host = os.environ.get("WECHAT_TOOL_HOST", "127.0.0.1")
|
||||
port = int(os.environ.get("WECHAT_TOOL_PORT", "8000"))
|
||||
uvicorn.run(app, host=host, port=port)
|
||||
|
||||
Reference in New Issue
Block a user