mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-02-02 05:50:50 +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:
12
main.py
12
main.py
@@ -14,12 +14,16 @@ from pathlib import Path
|
||||
|
||||
def main():
|
||||
"""启动微信解密工具API服务"""
|
||||
host = os.environ.get("WECHAT_TOOL_HOST", "127.0.0.1")
|
||||
port = int(os.environ.get("WECHAT_TOOL_PORT", "8000"))
|
||||
access_host = "127.0.0.1" if host in {"0.0.0.0", "::"} else host
|
||||
|
||||
print("=" * 60)
|
||||
print("微信解密工具 API 服务")
|
||||
print("=" * 60)
|
||||
print("正在启动服务...")
|
||||
print("API文档: http://localhost:8000/docs")
|
||||
print("健康检查: http://localhost:8000/api/health")
|
||||
print(f"API文档: http://{access_host}:{port}/docs")
|
||||
print(f"健康检查: http://{access_host}:{port}/api/health")
|
||||
print("按 Ctrl+C 停止服务")
|
||||
print("=" * 60)
|
||||
|
||||
@@ -29,8 +33,8 @@ def main():
|
||||
# 启动API服务
|
||||
uvicorn.run(
|
||||
"wechat_decrypt_tool.api:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
host=host,
|
||||
port=port,
|
||||
reload=enable_reload,
|
||||
reload_dirs=[str(repo_root / "src")] if enable_reload else None,
|
||||
reload_excludes=[
|
||||
|
||||
@@ -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)
|
||||
|
||||
31
src/wechat_decrypt_tool/app_paths.py
Normal file
31
src/wechat_decrypt_tool/app_paths.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_data_dir() -> Path:
|
||||
"""Base writable directory for all runtime output (logs, databases, key store).
|
||||
|
||||
- Desktop (Electron) should set `WECHAT_TOOL_DATA_DIR` to a per-user directory
|
||||
(e.g. `%APPDATA%/WeChatDataAnalysis`).
|
||||
- Dev defaults to the current working directory (repo root).
|
||||
"""
|
||||
|
||||
v = os.environ.get("WECHAT_TOOL_DATA_DIR", "").strip()
|
||||
if v:
|
||||
return Path(v)
|
||||
return Path.cwd()
|
||||
|
||||
|
||||
def get_output_dir() -> Path:
|
||||
return get_data_dir() / "output"
|
||||
|
||||
|
||||
def get_output_databases_dir() -> Path:
|
||||
return get_output_dir() / "databases"
|
||||
|
||||
|
||||
def get_account_keys_path() -> Path:
|
||||
return get_output_dir() / "account_keys.json"
|
||||
|
||||
21
src/wechat_decrypt_tool/backend_entry.py
Normal file
21
src/wechat_decrypt_tool/backend_entry.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""Entry point for bundling the FastAPI backend into a standalone executable.
|
||||
|
||||
This avoids dynamic import strings like "pkg.module:app" which some bundlers
|
||||
cannot detect reliably.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import uvicorn
|
||||
|
||||
from wechat_decrypt_tool.api import app
|
||||
|
||||
|
||||
def main() -> None:
|
||||
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, log_level="info")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -12,6 +12,7 @@ from urllib.parse import quote
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from .app_paths import get_output_databases_dir
|
||||
from .logging_config import get_logger
|
||||
|
||||
try:
|
||||
@@ -21,8 +22,7 @@ except Exception:
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
_OUTPUT_DATABASES_DIR = _REPO_ROOT / "output" / "databases"
|
||||
_OUTPUT_DATABASES_DIR = get_output_databases_dir()
|
||||
_DEBUG_SESSIONS = os.environ.get("WECHAT_TOOL_DEBUG_SESSIONS", "0") == "1"
|
||||
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@ import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
_KEY_STORE_PATH = _REPO_ROOT / "output" / "account_keys.json"
|
||||
from .app_paths import get_account_keys_path
|
||||
|
||||
_KEY_STORE_PATH = get_account_keys_path()
|
||||
|
||||
|
||||
def _atomic_write_json(path: Path, payload: Any) -> None:
|
||||
@@ -66,4 +67,3 @@ def upsert_account_keys_in_store(
|
||||
pass
|
||||
|
||||
return item
|
||||
|
||||
|
||||
@@ -16,14 +16,14 @@ from urllib.parse import urlparse
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from .app_paths import get_output_databases_dir
|
||||
from .logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
# 仓库根目录(用于定位 output/databases)
|
||||
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
_OUTPUT_DATABASES_DIR = _REPO_ROOT / "output" / "databases"
|
||||
# 运行时输出目录(桌面端可通过 WECHAT_TOOL_DATA_DIR 指向可写目录)
|
||||
_OUTPUT_DATABASES_DIR = get_output_databases_dir()
|
||||
_PACKAGE_ROOT = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ logger = get_logger(__name__)
|
||||
router = APIRouter(route_class=PathFixRoute)
|
||||
|
||||
|
||||
@router.get("/", summary="根端点")
|
||||
async def root():
|
||||
"""根端点"""
|
||||
logger.info("访问根端点")
|
||||
@router.get("/api", summary="API 根端点")
|
||||
async def api_root():
|
||||
"""API 根端点"""
|
||||
logger.info("访问 API 根端点")
|
||||
return {"message": "微信数据库解密工具 API"}
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
|
||||
from .app_paths import get_output_databases_dir
|
||||
|
||||
# 注意:不再支持默认密钥,所有密钥必须通过参数传入
|
||||
|
||||
# SQLite文件头
|
||||
@@ -251,7 +253,7 @@ def decrypt_wechat_databases(db_storage_path: str = None, key: str = None) -> di
|
||||
logger.info("=" * 60)
|
||||
|
||||
# 创建基础输出目录
|
||||
base_output_dir = Path("output/databases")
|
||||
base_output_dir = get_output_databases_dir()
|
||||
base_output_dir.mkdir(parents=True, exist_ok=True)
|
||||
logger.info(f"基础输出目录: {base_output_dir.absolute()}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user