TraceStudio-dist/scripts/manage.py
2026-01-13 16:41:31 +08:00

260 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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"已创建 .envLOCAL_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()