Files
happy-life-star/deploy.py
T
peanut a8b490eea3 fix: Windows 下子进程 python3 命令不存在的问题
Windows 上没有 python3 命令,只有 python。使用 sys.executable
作为当前 Python 可执行文件路径,兼容 Windows 和 Linux/Mac。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:16:56 +08:00

419 lines
14 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
# -*- coding: utf-8 -*-
"""
情绪博物馆 - 统一部署脚本
合并 deploy-all.sh / deploy-to-prod.sh / deploy-domain.sh
使用方法:
python deploy.py # 部署所有服务
python deploy.py backend # 仅部署后端
python deploy.py frontend # 仅部署前端
python deploy.py admin # 仅部署管理后台
python deploy.py life-script # 仅部署 Life-Script
python deploy.py ssl # 仅申请 SSL 证书
python deploy.py nginx # 仅部署 Nginx 配置
python deploy.py verify # 仅验证部署结果
python deploy.py all # 部署所有服务(同无参数)
Author: Peanut
Created: 2026-05-17
Purpose: 统一部署所有服务到生产服务器,替代原有的多个 shell 脚本
"""
import io
import os
import sys
import time
import subprocess
from pathlib import Path
# 强制 stdout/stderr 使用 UTF-8 编码,避免 Windows GBK 编码错误
if hasattr(sys.stdout, 'buffer'):
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace', line_buffering=True)
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace', line_buffering=True)
# 当前 Python 可执行文件(Windows 用 python.exeLinux/Mac 用 python3
PYTHON = sys.executable
# ============================================================================
# 配置
# ============================================================================
SERVER_IP = "101.200.208.45"
USERNAME = "root"
DOMAIN = "lifescript.happylifeos.com"
# 项目根目录(脚本所在目录)
PROJECT_DIR = Path(__file__).parent.absolute()
SSH_OPTS = f"-o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no"
# ============================================================================
# 日志
# ============================================================================
class Colors:
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
BLUE = '\033[0;34m'
NC = '\033[0m'
def log_info(msg):
print(f"{Colors.GREEN}[INFO]{Colors.NC} {msg}")
def log_warn(msg):
print(f"{Colors.YELLOW}[WARN]{Colors.NC} {msg}")
def log_error(msg):
print(f"{Colors.RED}[ERROR]{Colors.NC} {msg}")
def log_section(msg):
print(f"\n{Colors.BLUE}{'=' * 60}{Colors.NC}")
print(f"{Colors.BLUE}{msg}{Colors.NC}")
print(f"{Colors.BLUE}{'=' * 60}{Colors.NC}\n")
# ============================================================================
# 工具函数
# ============================================================================
def run_command(cmd, cwd=None, timeout=120, capture=True):
"""执行本地命令"""
# 设置 PYTHONUNBUFFERED=1 确保 Python 子进程不缓冲 stdout
env = os.environ.copy()
env['PYTHONUNBUFFERED'] = '1'
try:
if capture:
result = subprocess.run(
cmd, shell=True, cwd=cwd, env=env,
capture_output=True, text=True, encoding='utf-8', errors='replace',
timeout=timeout
)
return result.returncode == 0, result.stdout.strip(), result.stderr.strip()
else:
result = subprocess.run(
cmd, shell=True, cwd=cwd, env=env, timeout=timeout
)
return result.returncode == 0, "", ""
except subprocess.TimeoutExpired:
return False, "", f"命令执行超时 ({timeout}s): {cmd}"
except Exception as e:
return False, "", str(e)
def run_ssh_args(args, timeout=30, capture=True):
"""执行 ssh/scp 命令(使用列表参数,避免 Windows cmd.exe 找不到 ssh"""
try:
if capture:
result = subprocess.run(
args, shell=False, cwd=PROJECT_DIR,
capture_output=True, text=True, encoding='utf-8', errors='replace',
timeout=timeout
)
return result.returncode == 0, result.stdout.strip(), result.stderr.strip()
else:
result = subprocess.run(
args, shell=False, cwd=PROJECT_DIR, timeout=timeout
)
return result.returncode == 0, "", ""
except subprocess.TimeoutExpired:
return False, "", f"命令执行超时 ({timeout}s): {' '.join(args)}"
except Exception as e:
return False, "", str(e)
def ssh_command(cmd, timeout=30):
"""在远程服务器执行命令"""
return run_ssh_args([
"ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10",
"-o", "StrictHostKeyChecking=no",
f"{USERNAME}@{SERVER_IP}", cmd
], timeout=timeout)
def scp_file(local, remote, timeout=120):
"""上传文件到远程服务器"""
return run_ssh_args([
"scp", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10",
"-o", "StrictHostKeyChecking=no",
local, f"{USERNAME}@{SERVER_IP}:{remote}"
], timeout=timeout)
def check_ssh():
"""检查 SSH 连接"""
log_info("检查 SSH 连接...")
success, stdout, stderr = ssh_command("echo ok", timeout=15)
if not success:
log_error(f"SSH 连接失败,请先配置免密登录: ssh {USERNAME}@{SERVER_IP}")
if stderr:
log_error(f"错误详情: {stderr}")
sys.exit(1)
log_info("SSH 连接正常")
# ============================================================================
# Nginx 配置
# ============================================================================
def deploy_nginx():
"""部署 Nginx 配置"""
log_section("部署 Nginx 配置")
nginx_conf = PROJECT_DIR / "conf" / "emotion-museum.conf"
if not nginx_conf.exists():
log_error(f"Nginx 配置文件不存在: {nginx_conf}")
return False
log_info("上传 Nginx 配置文件...")
remote_conf = f"/etc/nginx/sites-available/{DOMAIN}.conf"
# 先创建远程目录
ssh_command("mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled", timeout=15)
ok, _, err = scp_file(str(nginx_conf), remote_conf, timeout=60)
if not ok:
log_error(f"上传 Nginx 配置失败: {err}")
return False
log_info("启用站点配置...")
ok, _, err = ssh_command(
f"ln -snf {remote_conf} /etc/nginx/sites-enabled/{DOMAIN}.conf "
f"&& rm -f /etc/nginx/sites-enabled/default"
)
if not ok:
log_error(f"启用站点失败: {err}")
return False
log_info("验证 Nginx 配置...")
ok, _, err = ssh_command("nginx -t", timeout=15)
if ok:
ssh_command("systemctl reload nginx", timeout=30)
log_info("Nginx 配置重载完成")
return True
else:
log_error(f"Nginx 配置验证失败: {err}")
return False
# ============================================================================
# 后端 - 调用 backend-single/deploy.sh remote
# ============================================================================
def deploy_backend():
"""部署后端服务"""
log_section("部署后端服务")
deploy_script = PROJECT_DIR / "backend-single" / "deploy.py"
if not deploy_script.exists():
log_error(f"后端部署脚本不存在: {deploy_script}")
return False
log_info("执行后端部署...")
ok, _, err = run_command(
f"{PYTHON} deploy.py remote",
cwd=str(PROJECT_DIR / "backend-single"),
timeout=600,
capture=False
)
if ok:
log_info("后端部署完成")
return True
else:
log_error(f"后端部署失败: {err}")
return False
# ============================================================================
# 前端 - 调用 web/deploy.sh
# ============================================================================
def deploy_frontend():
"""部署用户前端"""
log_section("部署用户前端")
deploy_script = PROJECT_DIR / "web" / "deploy.py"
if not deploy_script.exists():
log_error(f"前端部署脚本不存在: {deploy_script}")
return False
log_info("执行前端部署...")
ok, _, err = run_command(
f"{PYTHON} deploy.py",
cwd=str(PROJECT_DIR / "web"),
timeout=600,
capture=False
)
if ok:
log_info("前端部署完成")
return True
else:
log_error(f"前端部署失败: {err}")
return False
# ============================================================================
# 管理后台 - 调用 web-admin/deploy.py
# ============================================================================
def deploy_admin():
"""部署管理后台"""
log_section("部署管理后台")
deploy_script = PROJECT_DIR / "web-admin" / "deploy.py"
if not deploy_script.exists():
log_error(f"管理后台部署脚本不存在: {deploy_script}")
return False
log_info("执行管理后台部署...")
ok, _, err = run_command(
f"{PYTHON} deploy.py",
cwd=str(PROJECT_DIR / "web-admin"),
timeout=600,
capture=False
)
if ok:
log_info("管理后台部署完成")
return True
else:
log_error(f"管理后台部署失败: {err}")
return False
# ============================================================================
# Life-Script - 调用 life-script/deploy.sh
# ============================================================================
def deploy_life_script():
"""部署 Life-Script"""
log_section("部署 Life-Script")
deploy_script = PROJECT_DIR / "life-script" / "deploy.py"
if not deploy_script.exists():
log_error(f"Life-Script 部署脚本不存在: {deploy_script}")
return False
log_info("执行 Life-Script 部署...")
ok, _, err = run_command(
f"{PYTHON} deploy.py",
cwd=str(PROJECT_DIR / "life-script"),
timeout=600,
capture=False
)
if ok:
log_info("Life-Script 部署完成")
return True
else:
log_error(f"Life-Script 部署失败: {err}")
return False
# ============================================================================
# 验证部署
# ============================================================================
def verify_deploy():
"""验证部署结果"""
log_section("验证部署")
log_info("检查 HTTPS 访问...")
endpoints = [
(f"https://{DOMAIN}/", "前端页面"),
(f"https://{DOMAIN}/emotion-museum-admin/", "管理后台"),
(f"https://{DOMAIN}/api/", "API 代理"),
(f"http://{DOMAIN}/", "HTTP 跳转"),
]
for url, label in endpoints:
ok, stdout, _ = run_command(
f'curl -k -s -o /dev/null -w "HTTP %{{http_code}}" {url}',
timeout=30
)
if ok and stdout:
log_info(f" {label}: {stdout}")
else:
log_warn(f" {label}: 访问异常")
log_info("验证完成")
return True
# ============================================================================
# 主程序
# ============================================================================
def print_usage():
"""打印使用说明"""
print("情绪博物馆 - 统一部署脚本")
print()
print("使用方法:")
print(" python deploy.py # 部署所有服务")
print(" python deploy.py backend # 仅部署后端")
print(" python deploy.py frontend # 仅部署前端")
print(" python deploy.py admin # 仅部署管理后台")
print(" python deploy.py life-script # 仅部署 Life-Script")
print(" python deploy.py ssl # 仅申请 SSL 证书")
print(" python deploy.py nginx # 仅部署 Nginx 配置")
print(" python deploy.py verify # 仅验证部署结果")
print(" python deploy.py all # 部署所有服务(同无参数)")
def main():
deploy_type = sys.argv[1] if len(sys.argv) > 1 else "all"
if deploy_type in ("--help", "-h", "help"):
print_usage()
return
start_time = time.time()
log_section("情绪博物馆 - 统一部署")
log_info(f"部署类型: {deploy_type}")
log_info(f"部署时间: {time.strftime('%Y-%m-%d %H:%M:%S')}")
actions = {
"backend": lambda: check_ssh() or True and deploy_backend(),
"frontend": lambda: check_ssh() or True and deploy_frontend(),
"admin": lambda: check_ssh() or True and deploy_admin(),
"life-script": lambda: check_ssh() or True and deploy_life_script(),
"nginx": lambda: check_ssh() or True and deploy_nginx(),
"verify": lambda: verify_deploy(),
"all": None, # handled separately
}
if deploy_type == "all":
check_ssh()
deploy_nginx() or True
deploy_backend()
deploy_frontend()
deploy_admin()
deploy_life_script()
verify_deploy()
elif deploy_type in actions:
actions[deploy_type]()
else:
log_error(f"无效的部署类型: {deploy_type}")
print("使用方法: python deploy.py [backend|frontend|admin|life-script|nginx|verify|all]")
sys.exit(1)
duration = int(time.time() - start_time)
log_section(f"部署完成 (耗时 {duration}s)")
log_info(f"用户前端: https://{DOMAIN}/")
log_info(f"管理后台: https://{DOMAIN}/emotion-museum-admin/")
log_info(f"Life-Script: https://{DOMAIN}/life-script/")
log_info(f"API 地址: https://{DOMAIN}/api")
log_info(f"WebSocket: wss://{DOMAIN}/ws")
log_info(f"API 文档: https://{DOMAIN}/doc.html")
log_info(f"Swagger UI: https://{DOMAIN}/swagger-ui/index.html")
log_info(f"API Specs: https://{DOMAIN}/v3/api-docs")
if __name__ == "__main__":
main()