"""本地后端入口:FastAPI + PostgreSQL + Browser Use。 源码运行:python backend/main.py(项目根目录) 冻结运行:native/release/school-assistant-backend/school-assistant-backend.exe(PyInstaller onedir,由 Electron 拉起) 自检: … --selfcheck 只导入全部依赖与本项目模块并打印版本,不连数据库、不监听端口,供打包脚本验收 """ import json import sys from pathlib import Path # 支持从项目根目录运行: python backend/main.py ROOT = Path(__file__).resolve().parent.parent if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from dotenv import load_dotenv # noqa: E402 from backend.config import ENV_FILE, FROZEN, HOME_DIR, KNOWLEDGE_DIR, settings # noqa: E402 load_dotenv(ENV_FILE) def selfcheck() -> int: """打包验收:把运行时会碰到的模块都导入一遍,缺什么在这里就会暴露。""" import importlib.metadata as metadata import browser_use # noqa: F401 import bubus # noqa: F401 import cdp_use # noqa: F401 import cryptography # noqa: F401 import fastapi # noqa: F401 import httpx # noqa: F401 import psycopg # noqa: F401 import uvicorn # noqa: F401 import yaml # noqa: F401 import backend.prospecting.routes # noqa: F401 from backend.db.database import SCHEMA_PATH def version_of(dist: str, module) -> str: # PyInstaller 只为 browser-use 复制了 dist-info(它运行时要读),其余包冻结后没有 metadata,退回 __version__ try: return metadata.version(dist) except metadata.PackageNotFoundError: return str(getattr(module, "__version__", "n/a")) report = { "ok": True, "frozen": FROZEN, "python": sys.version.split()[0], "home": str(HOME_DIR), "env_file": str(ENV_FILE), "knowledge_dir": str(KNOWLEDGE_DIR), "knowledge_files": sorted(p.name for p in KNOWLEDGE_DIR.glob("*.yaml")) if KNOWLEDGE_DIR.exists() else [], "schema_sql": SCHEMA_PATH.exists(), "versions": { "browser-use": metadata.version("browser-use"), "bubus": version_of("bubus", bubus), "cdp-use": version_of("cdp-use", cdp_use), "fastapi": version_of("fastapi", fastapi), "uvicorn": version_of("uvicorn", uvicorn), "psycopg": version_of("psycopg", psycopg), "cryptography": version_of("cryptography", cryptography), }, } print(json.dumps(report, ensure_ascii=False)) return 0 if "--selfcheck" in sys.argv: sys.exit(selfcheck()) import uvicorn # noqa: E402 from fastapi import FastAPI # noqa: E402 from fastapi.middleware.cors import CORSMiddleware # noqa: E402 from backend.prospecting.routes import orchestrator, router # noqa: E402 from backend.db.database import init_db # noqa: E402 init_db() app = FastAPI(title="橘子拓客 API", version="0.4.0") app.add_middleware( CORSMiddleware, allow_origins=["app://ui", "http://127.0.0.1:8098", "http://localhost:8098"], allow_credentials=False, allow_methods=["*"], allow_headers=["*"], ) app.include_router(router, prefix="/api") @app.on_event("startup") async def _start_background_loops() -> None: """恢复被应用重启中断的拓客任务;不启用历史学校审批/中心同步循环。""" orchestrator.recover_interrupted() @app.get("/") async def root(): return {"name": "ai-prospecting-assistant", "docs": "/docs"} if __name__ == "__main__": # 传 app 对象而不是 "backend.main:app" 字符串:冻结后本文件是 __main__,按字符串再导入会把模块跑两遍 uvicorn.run( app, host="127.0.0.1", port=settings.api_port, reload=False, )