#!/usr/bin/env python3 """ TraceStudio 项目管家 统一管理开发、调试、发布的命令行工具。 """ import argparse import os import sys import shutil import subprocess import time from pathlib import Path # --- 路径配置 --- # 自动判断是在源码根目录,还是在 dist 目录下 CURRENT_SCRIPT_DIR = Path(__file__).resolve().parent if (CURRENT_SCRIPT_DIR.parent / "server" / "main.py").exists(): ROOT = CURRENT_SCRIPT_DIR.parent else: ROOT = Path.cwd() DIST_DIR = ROOT / "dist" ENV_FILE = ROOT / ".env" CLOUD_DIR = ROOT / "cloud" # --- 辅助函数 --- def print_step(msg): print(f"\n[TraceStudio] ➤ {msg}") def ensure_env(): """确保 .env 文件存在,用于 Docker Compose 读取变量""" if ENV_FILE.exists(): return print_step("初始化环境配置 (.env)...") if not CLOUD_DIR.exists(): CLOUD_DIR.mkdir(parents=True, exist_ok=True) # Windows 下路径处理 local_cloud = str(CLOUD_DIR) content = ( f"# 自动生成的本地开发配置\n" f"LOCAL_CLOUD_ROOT={local_cloud}\n" f"PYTHONUNBUFFERED=1\n" f"AUTO_START=1\n" ) with open(ENV_FILE, "w", encoding="utf-8") as f: f.write(content) print(f"已创建 .env,LOCAL_CLOUD_ROOT 指向: {local_cloud}") def run_concurrent(commands, cwd=ROOT): """ 并发运行多个进程,并处理退出信号 commands: list of {'cmd': list, 'name': str, 'cwd': Path, 'silent': bool} """ processes = [] try: for item in commands: cmd = item["cmd"] name = item["name"] work_dir = item.get("cwd", cwd) # 新增 silent 标记:如果为 True,则丢弃该进程的输出 is_silent = item.get("silent", False) print(f">>> 启动 {name} ... {'(日志已隐藏)' if is_silent else ''}") # Windows 平台特殊处理 is_shell = True if sys.platform == "win32" else False if isinstance(cmd, list) and len(cmd) > 0 and cmd[0] == "npm" and sys.platform == "win32": is_shell = True cmd = " ".join(cmd) # 配置输出流 # 如果 silent=True,指向 DEVNULL;否则为 None (继承父进程控制台输出) stdout_dest = subprocess.DEVNULL if is_silent else None stderr_dest = subprocess.DEVNULL if is_silent else None p = subprocess.Popen( cmd, cwd=str(work_dir), shell=is_shell, stdout=stdout_dest, stderr=stderr_dest ) processes.append((name, p)) time.sleep(1) print_step("所有服务已启动 (按 Ctrl+C 停止)") print("提示:Docker 运行在后台,请使用 Docker Desktop 或 'docker logs' 查看日志。") # 监控进程 while True: time.sleep(1) for name, p in processes: if p.poll() is not None: # 如果进程意外退出 print(f"\n!!! {name} 意外退出 (Code: {p.returncode})") # 如果是静默模式退出的,通常用户不知道发生了什么,给个提示 if item.get("silent", False): print(f"提示:{name} 是静默启动的,请手动运行命令检查报错原因。") raise KeyboardInterrupt except KeyboardInterrupt: print("\n\n正在停止所有服务...") for name, p in processes: if p.poll() is None: print(f"停止 {name}...") p.terminate() try: p.wait(timeout=3) except subprocess.TimeoutExpired: p.kill() print("服务已停止。") # --- 命令实现 --- def cmd_setup(args): ensure_env() print("环境配置检查完毕。") def cmd_dev(args): """混合开发模式: Docker (Server/Web) + Local Agent""" ensure_env() print_step("启动混合开发模式 (Docker Backend + Local Agent)") cmds = [ { "name": "Docker Compose", "cmd": ["docker", "compose", "-f", "docker-compose.dev.yml", "up", "--build"], "cwd": ROOT, "silent": True, # <--- 关键修改:隐藏 Docker 日志 }, { "name": "Local Agent", "cmd": [sys.executable, "agent/main.py"], "cwd": ROOT, "silent": False, # Agent 日志保留,方便调试 Exe 调用 }, ] run_concurrent(cmds) def cmd_local(args): """全本地模式: Local Server + Agent + Web""" ensure_env() print_step("启动全本地调试模式 (Local Host)") cmds = [ { "name": "Backend Server", "cmd": [sys.executable, "-m", "uvicorn", "server.main:app", "--reload", "--port", "8000"], "cwd": ROOT / "server", }, {"name": "Local Agent", "cmd": [sys.executable, "agent/main.py"], "cwd": ROOT}, {"name": "Web Frontend", "cmd": ["npm", "run", "dev"], "cwd": ROOT / "web"}, ] run_concurrent(cmds) def cmd_build(args): """构建发布包""" print_step(f"清理并构建发布包到: {DIST_DIR}") if DIST_DIR.exists(): shutil.rmtree(DIST_DIR) DIST_DIR.mkdir() ignore = shutil.ignore_patterns( ".git", ".gitignore", ".dockerignore", "__pycache__", "node_modules", ".venv", "dist", ".vscode", ".devcontainer", "*.pyc", "docker-compose.dev.yml", "tests" ) # 1. 复制核心代码 for item in ["server", "web", "agent", "scripts"]: src = ROOT / item dst = DIST_DIR / item if src.is_dir(): shutil.copytree(src, dst, ignore=ignore) shutil.copytree(ROOT / "cloud/custom_nodes", DIST_DIR / "cloud/custom_nodes", ignore=ignore) # 2. 复制生产配置 prod_compose = ROOT / "docker-compose.yml" if prod_compose.exists(): shutil.copy2(prod_compose, DIST_DIR / "docker-compose.yml") # 3. 复制管理脚本 if not (DIST_DIR / "scripts").exists(): (DIST_DIR / "scripts").mkdir() shutil.copy2(Path(__file__), DIST_DIR / "scripts" / "manage.py") print(f"构建完成!位置: {DIST_DIR}") def cmd_prod(args): """【发布模式】生产环境运行""" ensure_env() compose_file = ROOT / 'docker-compose.yml' if not compose_file.exists(): print(f"错误: 找不到 {compose_file},请确保这是发布包目录。") return print_step("启动生产环境 (Docker Prod + Agent)") cmds = [ { 'name': 'Docker Compose (Prod)', 'cmd': ['docker', 'compose', 'up', '--build'], 'cwd': ROOT, 'silent': False # 生产环境下建议保留日志以便排查 }, { 'name': 'Local Agent', 'cmd': [sys.executable, 'agent/main.py'], 'cwd': ROOT } ] run_concurrent(cmds) # --- 主入口 --- def main(): parser = argparse.ArgumentParser(description="TraceStudio 项目管理工具") subparsers = parser.add_subparsers(dest="command", help="可用命令") subparsers.add_parser("setup", help="生成 .env 配置文件") subparsers.add_parser("dev", help="启动 Docker开发环境(静默) + 本地Agent") subparsers.add_parser("local", help="启动全本地环境 (Server+Web+Agent)") subparsers.add_parser("build", help="构建发布包 (dist)") subparsers.add_parser("prod", help="启动生产发布包") args = parser.parse_args() mapping = { "setup": lambda x: ensure_env(), "dev": cmd_dev, "local": cmd_local, "build": cmd_build, "prod": cmd_prod } func = mapping.get(args.command) if func: func(args) else: parser.print_help() if __name__ == "__main__": main()