TraceStudio-dist/scripts/manage.py

276 lines
8.6 KiB
Python
Raw Normal View History

2026-01-13 16:41:31 +08:00
#!/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():
# 遍历 DIST_DIR 下的所有文件和文件夹
for item in DIST_DIR.iterdir():
# 【关键】核心保护逻辑:如果名字是 .git直接跳过
if item.name == ".git":
continue
try:
if item.is_dir():
#如果是文件夹,递归删除
shutil.rmtree(item)
else:
# 如果是文件,直接删除
item.unlink()
except Exception as e:
print(f"!!! 删除失败: {item} - {e}")
# Windows 下有时候会因为文件被占用删不掉,视情况决定是否要 raise 阻断流程
else:
# 如果目录压根不存在,才需要 mkdir
DIST_DIR.mkdir(parents=True, exist_ok=True)
2026-01-13 16:41:31 +08:00
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)
2026-01-13 16:41:31 +08:00
# 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()