""" 爱莉希雅待办事项 - 项目启动入口 功能: 1. 编译前端项目 2. 启动 FastAPI 后端服务 """ import os import sys import shutil import subprocess import time # 路径配置 PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) WEBUI_DIR = os.path.join(PROJECT_ROOT, "WebUI") DIST_DIR = os.path.join(WEBUI_DIR, "dist") WEBUI_TARGET = os.path.join(PROJECT_ROOT, "api", "webui") API_MAIN = os.path.join(PROJECT_ROOT, "api", "app", "main.py") def log_info(msg): """打印信息""" print(f"[启动] {msg}") def log_error(msg): """打印错误""" print(f"[错误] {msg}", file=sys.stderr) def needs_rebuild() -> bool: """判断是否需要重新编译前端(基于文件修改时间)""" if not os.path.exists(DIST_DIR): return True src_dir = os.path.join(WEBUI_DIR, "src") if not os.path.exists(src_dir): return True # 获取 dist 目录最新修改时间 dist_mtime = max( os.path.getmtime(os.path.join(dp, f)) for dp, dn, filenames in os.walk(DIST_DIR) for f in filenames ) # 检查 src 目录中是否有比 dist 更新的文件 for dp, dn, filenames in os.walk(src_dir): for f in filenames: if f.endswith(('.vue', '.ts', '.js', '.scss', '.css')): filepath = os.path.join(dp, f) if os.path.getmtime(filepath) > dist_mtime: return True return False def build_frontend(): """编译前端项目""" # 智能判断是否需要重新编译 if not needs_rebuild(): log_info("前端产物已是最新,跳过编译") return True log_info("开始编译前端项目...") # 检查 WebUI 目录是否存在 if not os.path.exists(WEBUI_DIR): log_error(f"WebUI 目录不存在: {WEBUI_DIR}") return False # 检查 node_modules 是否存在 node_modules = os.path.join(WEBUI_DIR, "node_modules") if not os.path.exists(node_modules): log_info("安装前端依赖...") # Windows 上使用 npm.cmd npm_cmd = "npm.cmd" if os.name == "nt" else "npm" result = subprocess.run( [npm_cmd, "install"], cwd=WEBUI_DIR, encoding="utf-8", errors="ignore" ) if result.returncode != 0: log_error(f"安装依赖失败") return False # 执行编译 log_info("执行 npm run build...") npm_cmd = "npm.cmd" if os.name == "nt" else "npm" result = subprocess.run( [npm_cmd, "run", "build"], cwd=WEBUI_DIR, encoding="utf-8", errors="ignore" ) if result.returncode != 0: log_error(f"编译失败") return False log_info("前端编译完成!") return True def copy_dist_to_webui(): """复制编译产物到 webui 目录""" log_info("复制编译产物到 webui 目录...") # 检查 dist 目录是否存在 if not os.path.exists(DIST_DIR): log_error(f"编译产物目录不存在: {DIST_DIR}") return False # 删除旧的 webui 目录(如果存在) if os.path.exists(WEBUI_TARGET): shutil.rmtree(WEBUI_TARGET) # 复制 dist 到 webui shutil.copytree(DIST_DIR, WEBUI_TARGET) log_info(f"编译产物已复制到: {WEBUI_TARGET}") return True def find_pid_on_port(port: int) -> int | None: """查找占用指定端口的进程 PID""" if os.name == "nt": result = subprocess.run( ["netstat", "-ano", "-p", "TCP"], capture_output=True, text=True, encoding="gbk", errors="ignore" ) for line in result.stdout.splitlines(): if f":{port}" in line and "LISTENING" in line: parts = line.split() return int(parts[-1]) else: result = subprocess.run( ["lsof", "-t", "-i", f":{port}", "-sTCP:LISTEN"], capture_output=True, text=True, errors="ignore" ) output = result.stdout.strip() if output: return int(output.splitlines()[0]) return None def kill_process(pid: int) -> bool: """终止指定 PID 的进程""" log_info(f"正在终止占用端口的进程 (PID: {pid})...") try: if os.name == "nt": subprocess.run(["taskkill", "/PID", str(pid), "/F"], capture_output=True, timeout=10) else: os.kill(pid, 9) time.sleep(1) log_info(f"进程 {pid} 已终止") return True except Exception as e: log_error(f"终止进程 {pid} 失败: {e}") return False def check_and_free_port(port: int) -> bool: """检测端口是否被占用,如果被占用则尝试释放""" pid = find_pid_on_port(port) if pid is None: return True log_info(f"端口 {port} 已被进程 {pid} 占用") if kill_process(pid): # 验证端口是否已释放 if find_pid_on_port(port) is None: log_info(f"端口 {port} 已释放") return True log_error(f"端口 {port} 仍被占用,尝试再次终止...") time.sleep(2) pid2 = find_pid_on_port(port) if pid2 is not None: kill_process(pid2) if find_pid_on_port(port) is None: return True log_error(f"无法释放端口 {port},请手动处理") return False def start_backend(): """启动后端服务""" log_info("启动后端服务...") # 添加 api 目录到 Python 路径(不使用 os.chdir,避免全局副作用) api_dir = os.path.join(PROJECT_ROOT, "api") if api_dir not in sys.path: sys.path.insert(0, api_dir) from app.config import HOST, PORT # 检查端口是否被占用,自动释放 if not check_and_free_port(PORT): log_error(f"端口 {PORT} 无法释放,启动失败") sys.exit(1) import uvicorn log_info(f"服务启动成功: http://{HOST}:{PORT}") log_info(f"API 文档: http://{HOST}:{PORT}/docs") log_info(f"前端页面: http://{HOST}:{PORT}/") uvicorn.run("app.main:app", host=HOST, port=PORT, reload=False) def main(): """主函数""" print("=" * 50) print(" 爱莉希雅待办事项 - 项目启动") print("=" * 50) # 1. 编译前端 if not build_frontend(): sys.exit(1) # 2. 复制编译产物 if not copy_dist_to_webui(): sys.exit(1) # 3. 启动后端 start_backend() if __name__ == "__main__": main()